Thread: Line intersection point is wrong

Line intersection point is wrong

From
Emre Hasegeli
Date:
While working on removing FP macros of the geometric type operators,
I noticed that line intersection is giving wrong results:

    regression=# select '{1,1,1}'::line # '{2,1,0}'::line;
     ?column?
    ----------
     (1,2)
    (1 row)

There are a few other operators relying on this function which are also
returning wrong results.  Segfaults should be possible at least on
assert enabled builds, though FP macros makes it hard to get them.

The bug seems to be introduced by 215bc83d on 1997.  I couldn't find
any relevant bug report on the archives except Tom Lane's doubtful
comment [1] from 1999.  Patch attached to fix the formula.

[1] https://www.postgresql.org/message-id/5537.941498236%40sss.pgh.pa.us

Attachment

Re: Line intersection point is wrong

From
Tom Lane
Date:
Emre Hasegeli <emre@hasegeli.com> writes:
> While working on removing FP macros of the geometric type operators,
> I noticed that line intersection is giving wrong results:

Hm.  I don't think I believe the vertical-line cases there either.
They seem to be assuming A = -1 in a vertical line, which would be
true if the line was computed by line_construct_pts, but otherwise
not necessarily.

regression=# select '((1,0),(1,1))'::line;
   line
----------
 {-1,0,1}
(1 row)

regression=# select '{-1,0,1}'::line # '((0,2),(2,2))'::line;
 ?column?
----------
 (1,2)                   -- this answer is correct
(1 row)

regression=# select '{-2,0,2}'::line # '((0,2),(2,2))'::line;
 ?column?
----------
 (2,2)
(1 row)

If I haven't lost it completely, {-1,0,1} and {-2,0,2} are
equivalent line values.  line_eq certainly thinks so.

Also: your formulation of the general case assumes that
(l1->A * l2->B - l2->A * l1->B) is not zero, which I'm
not entirely convinced of.  In principle the line_parallel test
would catch the case, but seeing that that is not exactly how
line_parallel computes its result, roundoff error could bite us
here.  I wonder if line_interpt_internal should skip the
line_parallel call and instead do its own tests for zero divide
to detect parallel lines.

            regards, tom lane

Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> Hm.  I don't think I believe the vertical-line cases there either.
> They seem to be assuming A = -1 in a vertical line, which would be
> true if the line was computed by line_construct_pts, but otherwise
> not necessarily.

I think we can just remove those cases.

> Also: your formulation of the general case assumes that
> (l1->A * l2->B - l2->A * l1->B) is not zero, which I'm
> not entirely convinced of.  In principle the line_parallel test
> would catch the case, but seeing that that is not exactly how
> line_parallel computes its result, roundoff error could bite us
> here.  I wonder if line_interpt_internal should skip the
> line_parallel call and instead do its own tests for zero divide
> to detect parallel lines.

If it would do its own test, the result would be inconsistent with ?#
and ?||.  It is not hard to get an overflow with the current logic
anyway:

hasegeli=# select '{1,1,-2}'::line # '{1,2,-3}'::line;
      ?column?
---------------------
 (Infinity,Infinity)
(1 row)

Evidently nobody is using these operators.  I am going to send a patch
reworking many of those to make them less vulnerable to roundoff
errors for the next release.

Re: Line intersection point is wrong

From
Tom Lane
Date:
Emre Hasegeli <emre@hasegeli.com> writes:
>> Hm.  I don't think I believe the vertical-line cases there either.
>> They seem to be assuming A = -1 in a vertical line, which would be
>> true if the line was computed by line_construct_pts, but otherwise
>> not necessarily.

> I think we can just remove those cases.

That would be nice.

>> Also: your formulation of the general case assumes that
>> (l1->A * l2->B - l2->A * l1->B) is not zero, which I'm
>> not entirely convinced of.  In principle the line_parallel test
>> would catch the case, but seeing that that is not exactly how
>> line_parallel computes its result, roundoff error could bite us
>> here.  I wonder if line_interpt_internal should skip the
>> line_parallel call and instead do its own tests for zero divide
>> to detect parallel lines.

> If it would do its own test, the result would be inconsistent with ?#
> and ?||.

Hmm, well, what if we change line_parallel() so that what it tests
is whether (l1->A * l2->B - l2->A * l1->B) is zero?  That seems
simpler and more symmetric than what it does now.

            regards, tom lane

Re: Line intersection point is wrong

From
Tom Lane
Date:
... also, a quick review of all the LINE-related code in geo_ops.c
says that it's just *full* of bugs.  Aside from the issues already
mentioned:

line_recv fails to reject A=B=0
line_perp can get division by zero
silly coding in line_eq (A and B can't both be zero, no need to look at C)
line_distance ignores l1->A, suspicious
close_pl busted in same way as line_interpt_internal
close_ps ignores tmp->B, suspicious

I suspect that a lot of this code was originally written on the assumption
that vertical and horizontal lines would have B or A exactly -1
respectively.  Somewhere along the line that restriction got dropped,
but the code wasn't improved to cope.

            regards, tom lane

Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> Hmm, well, what if we change line_parallel() so that what it tests
> is whether (l1->A * l2->B - l2->A * l1->B) is zero?  That seems
> simpler and more symmetric than what it does now.

I was thinking to do so.  I would also check for both sides not to
overflow, but that can wait.  Patch attached.  Unfortunately,
the change causes too much noise on the regression tests.

Attachment

Re: Line intersection point is wrong

From
Tom Lane
Date:
Emre Hasegeli <emre@hasegeli.com> writes:
>> Hmm, well, what if we change line_parallel() so that what it tests
>> is whether (l1->A * l2->B - l2->A * l1->B) is zero?  That seems
>> simpler and more symmetric than what it does now.

> I was thinking to do so.  I would also check for both sides not to
> overflow, but that can wait.  Patch attached.  Unfortunately,
> the change causes too much noise on the regression tests.

I don't think this is right:

+    x = (l1->B * l2->C - l2->B * l1->C) / (l1->A * l2->B - l2->A * l1->B);
+    y = (l1->A * x + l1->C) / l1->B;

You haven't done anything to exclude the possibility that l1->B is zero,
so you could be getting zero-divide in the y calculation.

FWIW, given that you're claiming the calculation is wrong as-is, there
is no particularly good reason to assume that the expected results
embedded in select_views.out are correct :-(.  I doubt anyone's ever
tried to verify those independently.

            regards, tom lane

Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> You haven't done anything to exclude the possibility that l1->B is zero,
> so you could be getting zero-divide in the y calculation.

Ah, yes.  We can use l2 when l1->B is zero.

I will send another patch on tomorrow addressing other bugs you found.

Re: Line intersection point is wrong

From
Tom Lane
Date:
Emre Hasegeli <emre@hasegeli.com> writes:
>> You haven't done anything to exclude the possibility that l1->B is zero,
>> so you could be getting zero-divide in the y calculation.

> Ah, yes.  We can use l2 when l1->B is zero.

After working out the algebra by hand, I think the attached is correct
(and it does pass the regression tests, yay).  I also made the
line_parallel and line_perp tests more symmetric and zero-divide-free.

            regards, tom lane

diff --git a/src/backend/utils/adt/geo_ops.c b/src/backend/utils/adt/geo_ops.c
index 657bcee..ce796c7 100644
*** a/src/backend/utils/adt/geo_ops.c
--- b/src/backend/utils/adt/geo_ops.c
*************** line_parallel(PG_FUNCTION_ARGS)
*** 1108,1117 ****
      LINE       *l1 = PG_GETARG_LINE_P(0);
      LINE       *l2 = PG_GETARG_LINE_P(1);

!     if (FPzero(l1->B))
          PG_RETURN_BOOL(FPzero(l2->B));

!     PG_RETURN_BOOL(FPeq(l2->A, l1->A * (l2->B / l1->B)));
  }

  Datum
--- 1108,1119 ----
      LINE       *l1 = PG_GETARG_LINE_P(0);
      LINE       *l2 = PG_GETARG_LINE_P(1);

!     if (FPzero(l1->A))            /* horizontal? */
!         PG_RETURN_BOOL(FPzero(l2->A));
!     if (FPzero(l1->B))            /* vertical? */
          PG_RETURN_BOOL(FPzero(l2->B));

!     PG_RETURN_BOOL(FPeq(l2->A * l1->B, l1->A * l2->B));
  }

  Datum
*************** line_perp(PG_FUNCTION_ARGS)
*** 1120,1131 ****
      LINE       *l1 = PG_GETARG_LINE_P(0);
      LINE       *l2 = PG_GETARG_LINE_P(1);

!     if (FPzero(l1->A))
          PG_RETURN_BOOL(FPzero(l2->B));
!     else if (FPzero(l1->B))
          PG_RETURN_BOOL(FPzero(l2->A));

!     PG_RETURN_BOOL(FPeq(((l1->A * l2->B) / (l1->B * l2->A)), -1.0));
  }

  Datum
--- 1122,1133 ----
      LINE       *l1 = PG_GETARG_LINE_P(0);
      LINE       *l2 = PG_GETARG_LINE_P(1);

!     if (FPzero(l1->A))            /* horizontal? */
          PG_RETURN_BOOL(FPzero(l2->B));
!     if (FPzero(l1->B))            /* vertical? */
          PG_RETURN_BOOL(FPzero(l2->A));

!     PG_RETURN_BOOL(FPeq(l2->A * l1->B, -(l1->A * l2->B)));
  }

  Datum
*************** line_interpt_internal(LINE *l1, LINE *l2
*** 1233,1250 ****

      if (FPzero(l1->B))            /* l1 vertical? */
      {
!         x = l1->C;
!         y = (l2->A * x + l2->C);
      }
      else if (FPzero(l2->B))        /* l2 vertical? */
      {
!         x = l2->C;
!         y = (l1->A * x + l1->C);
      }
      else
      {
!         x = (l1->C - l2->C) / (l2->A - l1->A);
!         y = (l1->A * x + l1->C);
      }
      result = point_construct(x, y);

--- 1235,1252 ----

      if (FPzero(l1->B))            /* l1 vertical? */
      {
!         x = -(l1->C / l1->A);
!         y = -(l2->A * x + l2->C) / l2->B;
      }
      else if (FPzero(l2->B))        /* l2 vertical? */
      {
!         x = -(l2->C / l2->A);
!         y = -(l1->A * x + l1->C) / l1->B;
      }
      else
      {
!         x = (l1->B * l2->C - l2->B * l1->C) / (l1->A * l2->B - l2->A * l1->B);
!         y = -(l1->A * x + l1->C) / l1->B;
      }
      result = point_construct(x, y);


Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> ... also, a quick review of all the LINE-related code in geo_ops.c
> says that it's just *full* of bugs.  Aside from the issues already
> mentioned:
>
> line_recv fails to reject A=B=0
> line_perp can get division by zero
> silly coding in line_eq (A and B can't both be zero, no need to look at C)
> line_distance ignores l1->A, suspicious
> close_pl busted in same way as line_interpt_internal

I have tried to fix them.  Patch attached.

> close_ps ignores tmp->B, suspicious

I think we better throw this one away, and implement another one using
close_pl().

Attachment

Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> After working out the algebra by hand, I think the attached is correct
> (and it does pass the regression tests, yay).  I also made the
> line_parallel and line_perp tests more symmetric and zero-divide-free.

It looks better to me, though not completely symmetric:

hasegeli=# select '{0.000001,1000,0}'::line ?|| '{0.00009,90000,0}'::line;
 ?column?
----------
 f
(1 row)

hasegeli=# select '{0.00009,90000,0}'::line ?|| '{0.000001,1000,0}'::line;
 ?column?
----------
 t
(1 row)

Re: Line intersection point is wrong

From
Tom Lane
Date:
Emre Hasegeli <emre@hasegeli.com> writes:
>> After working out the algebra by hand, I think the attached is correct
>> (and it does pass the regression tests, yay).  I also made the
>> line_parallel and line_perp tests more symmetric and zero-divide-free.

> It looks better to me, though not completely symmetric:

> hasegeli=# select '{0.000001,1000,0}'::line ?|| '{0.00009,90000,0}'::line;
>  ?column?
> ----------
>  f
> (1 row)

> hasegeli=# select '{0.00009,90000,0}'::line ?|| '{0.000001,1000,0}'::line;
>  ?column?
> ----------
>  t
> (1 row)

Hmm, that's annoying, although of course the existing code has problems
of the same ilk:

regression=# select '{1000,0.000001,0}'::line ?|| '{90000,0.00009,0}'::line;
 ?column?
----------
 f
(1 row)

regression=# select '{90000,0.00009,0}'::line ?|| '{1000,0.000001,0}'::line;
 ?column?
----------
 t
(1 row)

Basically this comes down to the fuzziness of the FPzero/FPeq tests.

It might be possible to fix this particular problem by changing the FPzero
tests in line_parallel() to exact "== 0" tests; though we would likely
be well advised to still keep the main test as FPeq.

            regards, tom lane

Re: Line intersection point is wrong

From
Emre Hasegeli
Date:
> Basically this comes down to the fuzziness of the FPzero/FPeq tests.
>
> It might be possible to fix this particular problem by changing the FPzero
> tests in line_parallel() to exact "== 0" tests; though we would likely
> be well advised to still keep the main test as FPeq.

That can create whole bunch of different problems, because then the
fuzziness wouldn't apply to the values around the zero.  I don't think
there is any good solution without removing those macros all together.
We should just fix what is obviously broken for now.