This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 2d7d48ccc6 feat(Geometry): update Maths.inCircle and Maths.lineSide to
use ShewchukPredicates when more precision is needed
2d7d48ccc6 is described below
commit 2d7d48ccc6efc5b181090577393e842485ef5fc8
Author: jsorel <[email protected]>
AuthorDate: Fri Aug 28 09:31:23 2026 +0200
feat(Geometry): update Maths.inCircle and Maths.lineSide to use
ShewchukPredicates when more precision is needed
---
.../main/org/apache/sis/geometries/math/Maths.java | 170 ++++++++++++++++++---
.../org/apache/sis/geometries/math/MathsTest.java | 63 ++++++--
.../operation/ShewchukPredicatesTest.java | 8 +-
3 files changed, 199 insertions(+), 42 deletions(-)
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/math/Maths.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/math/Maths.java
index 0c498ad323..7ce5508c19 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/math/Maths.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/math/Maths.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.List;
import static org.apache.sis.geometries.math.Vectors.*;
import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometries.operation.ShewchukPredicates;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.util.internal.shared.Numerics;
@@ -35,6 +36,29 @@ public final class Maths {
private Maths(){}
+ /**
+ * Unit roundoff for {@code double} : the largest relative rounding error
a single
+ * IEEE 754 double-precision operation can introduce. Used as the base
unit for the
+ * static error-bound filters of {@link #lineSide} and {@link #inCircle}.
+ */
+ private static final double DOUBLE_EPSILON = 0x1.0p-53;
+
+ /**
+ * Safety factor applied to {@link #DOUBLE_EPSILON} in {@link #lineSide}'s
error bound.
+ * Deliberately conservative (larger than strictly required) : a bound
that is too small
+ * would silently trust a wrong sign, a bound that is too large only costs
a few extra
+ * calls to {@link ShewchukPredicates#orient2d}.
+ */
+ private static final double ORIENTATION_ERROR_FACTOR = 8.0;
+
+ /**
+ * Safety factor applied to {@link #DOUBLE_EPSILON} in {@link #inCircle}'s
error bound.
+ * Unlike {@link #ORIENTATION_ERROR_FACTOR}, this multiplies a "permanent"
(see
+ * {@link #det33Abs}) rather than the raw intermediate terms, since those
terms can
+ * themselves be the result of internal cancellation.
+ */
+ private static final double INCIRCLE_ERROR_FACTOR = 8.0;
+
/**
* Calculate normal of triangle made of given 3 points.
*
@@ -183,30 +207,48 @@ public final class Maths {
}
/**
- * Test the side of a point compare to a line.
- * Only X,Y ordinates are used.
+ * @see #lineSideFast(double, double, double, double, double, double)
+ */
+ public static double lineSideFast(ReadOnly.Tuple<?> a, ReadOnly.Tuple<?>
b, ReadOnly.Tuple<?> c) {
+ return lineSideFast(a.get(0), a.get(1), b.get(0), b.get(1), c.get(0),
c.get(1));
+ }
+
+ /**
+ * @see #lineSideFast(double, double, double, double, double, double)
+ */
+ public static double lineSideFast(float[] a, float[] b, float[] c) {
+ return lineSideFast(a[0], a[1], b[0], b[1], c[0], c[1]);
+ }
+
+ /**
+ * Test the side of a point compare to a line.Only X,Y ordinates are used.
+ * Fast, naive {@code double} arithmetic : can give a wrong (or
wrong-sign) result on
+ * nearly-collinear inputs. See {@link #lineSide(double, double, double,
double, double, double)}
+ * for a robust equivalent.
*
- * @param a line start
- * @param b line end
- * @param c to test
+ * @param x1 line start X
+ * @param y1 line start Y
+ * @param x2 line end X
+ * @param y2 line end Y
+ * @param x point X
+ * @param y point Y
* @return greater than 0 if point is on the left side
* equal 0 if point is on the line
* inferior than 0 if point is on the right side
*/
+ public static double lineSideFast(double x1, double y1, double x2, double
y2, double x, double y) {
+ return (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
+ }
+
+ /**
+ * @see #lineSide(double, double, double, double, double, double)
+ */
public static double lineSide(ReadOnly.Tuple<?> a, ReadOnly.Tuple<?> b,
ReadOnly.Tuple<?> c) {
return lineSide(a.get(0), a.get(1), b.get(0), b.get(1), c.get(0),
c.get(1));
}
/**
- * Test the side of a point compare to a line.
- * Only X,Y ordinates are used.
- *
- * @param a line start
- * @param b line end
- * @param c to test
- * @return greater than 0 if point is on the left side
- * equal 0 if point is on the line
- * inferior than 0 if point is on the right side
+ * @see #lineSide(double, double, double, double, double, double)
*/
public static double lineSide(float[] a, float[] b, float[] c) {
return lineSide(a[0], a[1], b[0], b[1], c[0], c[1]);
@@ -214,6 +256,8 @@ public final class Maths {
/**
* Test the side of a point compare to a line.Only X,Y ordinates are used.
+ * Robust variant : uses fast {@code double} arithmetic when it is
provably safe to do so,
+ * and falls back on {@link ShewchukPredicates#orient2d} otherwise.
*
* @param x1 line start X
* @param y1 line start Y
@@ -226,7 +270,13 @@ public final class Maths {
* inferior than 0 if point is on the right side
*/
public static double lineSide(double x1, double y1, double x2, double y2,
double x, double y) {
- return (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
+ final double left = (x2 - x1) * (y - y1);
+ final double right = (x - x1) * (y2 - y1);
+ final double result = left - right;
+ if (Math.abs(result) > ORIENTATION_ERROR_FACTOR * DOUBLE_EPSILON *
(Math.abs(left) + Math.abs(right))) {
+ return result;
+ }
+ return ShewchukPredicates.orient2d(x1, y1, x2, y2, x, y);
}
/**
@@ -448,11 +498,17 @@ public final class Maths {
return new double[]{u, v, w};
}
- public static boolean inCircle(ReadOnly.Tuple<?> a, ReadOnly.Tuple<?> b,
ReadOnly.Tuple<?> c, ReadOnly.Tuple<?> d) {
- return inCircle(a.get(0), a.get(1), b.get(0), b.get(1), c.get(0),
c.get(1), d.get(0), d.get(1));
+ /**
+ * @see #inCircleFast(double, double, double, double, double, double,
double, double)
+ */
+ public static boolean inCircleFast(ReadOnly.Tuple<?> a, ReadOnly.Tuple<?>
b, ReadOnly.Tuple<?> c, ReadOnly.Tuple<?> d) {
+ return inCircleFast(a.get(0), a.get(1), b.get(0), b.get(1), c.get(0),
c.get(1), d.get(0), d.get(1));
}
- public static boolean inCircle(float[] a, float[] b, float[] c, float[] d)
{
+ /**
+ * @see #inCircleFast(double, double, double, double, double, double,
double, double)
+ */
+ public static boolean inCircleFast(float[] a, float[] b, float[] c,
float[] d) {
double t;
double a2 = (t = a[0]) * t + (t = a[1]) * t;
double b2 = (t = b[0]) * t + (t = b[1]) * t;
@@ -471,17 +527,72 @@ public final class Maths {
return false;
}
+ /**
+ * Fast, naive {@code double} arithmetic : can give a wrong result on
nearly-cocircular
+ * inputs. See {@link #inCircle(double, double, double, double, double,
double, double, double)}
+ * for a robust equivalent.
+ */
+ public static boolean inCircleFast(double ax, double ay, double bx, double
by, double cx, double cy, double dx, double dy) {
+ return det44(ax, ay, bx, by, cx, cy, dx, dy) < 0;
+ }
+
+ /**
+ * @see #inCircle(double, double, double, double, double, double, double,
double)
+ */
+ public static boolean inCircle(ReadOnly.Tuple<?> a, ReadOnly.Tuple<?> b,
ReadOnly.Tuple<?> c, ReadOnly.Tuple<?> d) {
+ return inCircle(a.get(0), a.get(1), b.get(0), b.get(1), c.get(0),
c.get(1), d.get(0), d.get(1));
+ }
+
+ /**
+ * @see #inCircle(double, double, double, double, double, double, double,
double)
+ */
+ public static boolean inCircle(float[] a, float[] b, float[] c, float[] d)
{
+ return inCircle(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]);
+ }
+
+ /**
+ * Robust variant : uses fast {@code double} arithmetic when it is
provably safe to do so,
+ * and falls back on {@link ShewchukPredicates#inCircle} otherwise.
+ */
public static boolean inCircle(double ax, double ay, double bx, double by,
double cx, double cy, double dx, double dy) {
final double a2 = ax*ax + ay*ay;
final double b2 = bx*bx + by*by;
final double c2 = cx*cx + cy*cy;
final double d2 = dx*dx + dy*dy;
- double det44 = (d2 * det33(ax, ay, 1, bx, by, 1, cx, cy, 1))
- - (dx * det33(a2, ay, 1, b2, by, 1, c2, cy, 1))
- + (dy * det33(a2, ax, 1, b2, bx, 1, c2, cx, 1))
- - ( 1 * det33(a2, ax, ay, b2, bx, by, c2, cx, cy));
- return det44 < 0;
+ final double term1 = d2 * det33(ax, ay, 1, bx, by, 1, cx, cy, 1);
+ final double term2 = dx * det33(a2, ay, 1, b2, by, 1, c2, cy, 1);
+ final double term3 = dy * det33(a2, ax, 1, b2, bx, 1, c2, cx, 1);
+ final double term4 = det33(a2, ax, ay, b2, bx, by, c2, cx, cy);
+ final double det44 = term1 - term2 + term3 - term4;
+
+ //Each termN is itself a determinant that can suffer internal
cancellation, so the
+ //error bound cannot simply be based on |term1|+..+|term4| (those can
be deceptively
+ //small already). Instead use the "permanent" of the whole formula :
the same formula,
+ //but with every subtraction turned into an addition of absolute
values, which bounds
+ //the magnitude of every partial sum that could arise anywhere in the
computation.
+ final double permanent = d2 * det33Abs(ax, ay, 1, bx, by, 1, cx, cy, 1)
+ + Math.abs(dx) * det33Abs(a2, ay, 1, b2, by, 1, c2, cy, 1)
+ + Math.abs(dy) * det33Abs(a2, ax, 1, b2, bx, 1, c2, cx, 1)
+ + det33Abs(a2, ax, ay, b2, bx, by, c2, cx, cy);
+ final double bound = INCIRCLE_ERROR_FACTOR * DOUBLE_EPSILON *
permanent;
+ if (Math.abs(det44) > bound) {
+ return det44 < 0;
+ }
+ //Maths' det44 sign convention is the negation of
ShewchukPredicates.inCircle's.
+ return ShewchukPredicates.inCircle(ax, ay, bx, by, cx, cy, dx, dy) > 0;
+ }
+
+ private static double det44(double ax, double ay, double bx, double by,
double cx, double cy, double dx, double dy) {
+ final double a2 = ax*ax + ay*ay;
+ final double b2 = bx*bx + by*by;
+ final double c2 = cx*cx + cy*cy;
+ final double d2 = dx*dx + dy*dy;
+
+ return (d2 * det33(ax, ay, 1, bx, by, 1, cx, cy, 1))
+ - (dx * det33(a2, ay, 1, b2, by, 1, c2, cy, 1))
+ + (dy * det33(a2, ax, 1, b2, bx, 1, c2, cx, 1))
+ - ( 1 * det33(a2, ax, ay, b2, bx, by, c2, cx, cy));
}
private static double det33(double... m) {
@@ -490,6 +601,19 @@ public final class Maths {
+ (m[2] * (m[3] * m[7] - m[4] * m[6]));
}
+ /**
+ * "Permanent" of {@link #det33} : the same cofactor expansion, but every
subtraction
+ * replaced by an addition of absolute values. Bounds the magnitude of
every partial sum
+ * that {@link #det33} could compute internally, which is what a static
error-bound filter
+ * built on top of it must be based on (not on {@code det33}'s own,
possibly-cancelled,
+ * result).
+ */
+ private static double det33Abs(double... m) {
+ return (Math.abs(m[0]) * (Math.abs(m[4]) * Math.abs(m[8]) +
Math.abs(m[5]) * Math.abs(m[7])))
+ + (Math.abs(m[1]) * (Math.abs(m[3]) * Math.abs(m[8]) +
Math.abs(m[5]) * Math.abs(m[6])))
+ + (Math.abs(m[2]) * (Math.abs(m[3]) * Math.abs(m[7]) +
Math.abs(m[4]) * Math.abs(m[6])));
+ }
+
public static double dot2D(final float[] vector, final float[] other){
return (double) vector[0] * (double) other[0]
+ (double) vector[1] * (double) other[1];
diff --git
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/math/MathsTest.java
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/math/MathsTest.java
index 2aa0fbb0c2..551cbef9a4 100644
---
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/math/MathsTest.java
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/math/MathsTest.java
@@ -236,11 +236,9 @@ public class MathsTest {
}
/**
- * A known mathematical problem.
- * TODO : find a more robust way to compute this operation.
+ * A known double precisions mathematical problem.
*/
@Test
- @Disabled
public void testInCircle() {
{
@@ -253,12 +251,21 @@ public class MathsTest {
final double dx = 8.64685;
final double dy = 41.7668;
- System.out.println(Maths.inCircle(ax, ay, bx, by, dx, dy, cx, cy));
- System.out.println(Maths.inCircle(bx, by, dx, dy, ax, ay, cx, cy));
- System.out.println(Maths.inCircle(dx, dy, ax, ay, bx, by, cx, cy));
- System.out.println(Maths.inCircle(ax, ay, dx, dy, cx, cy, bx, by));
- System.out.println(Maths.inCircle(dx, dy, cx, cy, ax, ay, bx, by));
- System.out.println(Maths.inCircle(cx, cy, ax, ay, dx, dy, bx, by));
+ //double precision arithmetic fails on this
+ assertTrue(Maths.inCircleFast(ax, ay, bx, by, dx, dy, cx, cy));
+ assertFalse(Maths.inCircleFast(bx, by, dx, dy, ax, ay, cx, cy));
+ assertFalse(Maths.inCircleFast(dx, dy, ax, ay, bx, by, cx, cy));
+ assertTrue(Maths.inCircleFast(ax, ay, dx, dy, cx, cy, bx, by));
+ assertTrue(Maths.inCircleFast(dx, dy, cx, cy, ax, ay, bx, by));
+ assertFalse(Maths.inCircleFast(cx, cy, ax, ay, dx, dy, bx, by));
+
+ //check the new method find the right answer
+ assertTrue(Maths.inCircle(ax, ay, bx, by, dx, dy, cx, cy));
+ assertTrue(Maths.inCircle(bx, by, dx, dy, ax, ay, cx, cy));
+ assertTrue(Maths.inCircle(dx, dy, ax, ay, bx, by, cx, cy));
+ assertTrue(Maths.inCircle(ax, ay, dx, dy, cx, cy, bx, by));
+ assertTrue(Maths.inCircle(dx, dy, cx, cy, ax, ay, bx, by));
+ assertTrue(Maths.inCircle(cx, cy, ax, ay, dx, dy, bx, by));
}
{
@@ -271,15 +278,41 @@ public class MathsTest {
final double dx = 8.68772;
final double dy = 41.90366;
- System.out.println(Maths.inCircle(ax, ay, bx, by, dx, dy, cx, cy));
- System.out.println(Maths.inCircle(bx, by, dx, dy, ax, ay, cx, cy));
- System.out.println(Maths.inCircle(dx, dy, ax, ay, bx, by, cx, cy));
- System.out.println(Maths.inCircle(ax, ay, dx, dy, cx, cy, bx, by));
- System.out.println(Maths.inCircle(dx, dy, cx, cy, ax, ay, bx, by));
- System.out.println(Maths.inCircle(cx, cy, ax, ay, dx, dy, bx, by));
+ //double precision arithmetic fails on this
+ assertTrue(Maths.inCircleFast(ax, ay, bx, by, dx, dy, cx, cy));
+ assertTrue(Maths.inCircleFast(bx, by, dx, dy, ax, ay, cx, cy));
+ assertTrue(Maths.inCircleFast(dx, dy, ax, ay, bx, by, cx, cy));
+ assertTrue(Maths.inCircleFast(ax, ay, dx, dy, cx, cy, bx, by));
+ assertFalse(Maths.inCircleFast(dx, dy, cx, cy, ax, ay, bx, by));
+ assertTrue(Maths.inCircleFast(cx, cy, ax, ay, dx, dy, bx, by));
+
+ //check the new method find the right answer
+ assertTrue(Maths.inCircle(ax, ay, bx, by, dx, dy, cx, cy));
+ assertTrue(Maths.inCircle(bx, by, dx, dy, ax, ay, cx, cy));
+ assertTrue(Maths.inCircle(dx, dy, ax, ay, bx, by, cx, cy));
+ assertTrue(Maths.inCircle(ax, ay, dx, dy, cx, cy, bx, by));
+ assertTrue(Maths.inCircle(dx, dy, cx, cy, ax, ay, bx, by));
+ assertTrue(Maths.inCircle(cx, cy, ax, ay, dx, dy, bx, by));
}
}
+ /**
+ * Same ill-conditioned (nearly collinear) coordinates as
+ * {@code ShewchukPredicatesTest#testOrient2dRobustness()} : the naive
+ * {@link Maths#lineSideFast} collapses to exactly 0 (falsely reporting
the points as
+ * collinear), while the robust {@link Maths#lineSide} must fall back and
find the true,
+ * small but non-zero, signed area.
+ */
+ @Test
+ public void testLineSideRobust() {
+ final double ax = -2552049145.4853754, ay = 954889314.1911564;
+ final double bx = -8744220500.533537, by = -8807976600.675346;
+ final double cx = -3827380787.3278, cy = -1055857983.0624254;
+
+ assertEquals(0.0, Maths.lineSideFast(ax,ay, bx,by, cx,cy), 0.0);
+ assertEquals(126.54970229522473, Maths.lineSide(ax,ay, bx,by, cx,cy),
0.1);
+ }
+
/**
* A known mathematical problem.
* TODO : find a more robust way to compute this operation.
diff --git
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ShewchukPredicatesTest.java
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ShewchukPredicatesTest.java
index 417322f778..33572fa95d 100644
---
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ShewchukPredicatesTest.java
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ShewchukPredicatesTest.java
@@ -77,7 +77,7 @@ public class ShewchukPredicatesTest {
/**
* Ill-conditioned (nearly collinear) case where the naive double-precision
- * {@link Maths#lineSide} formula collapses to exactly 0 (falsely reporting
+ * {@link Maths#lineSideFast} formula collapses to exactly 0 (falsely
reporting
* the points as collinear), while the true signed area is not zero.
* {@link ShewchukPredicates#orient2d} must still find the correct sign.
*/
@@ -88,7 +88,7 @@ public class ShewchukPredicatesTest {
final double cx = -3827380787.3278, cy = -1055857983.0624254;
//naive double arithmetic wrongly concludes the points are collinear
- assertEquals(0.0, Maths.lineSide(ax,ay, bx,by, cx,cy), 0.0);
+ assertEquals(0.0, Maths.lineSideFast(ax,ay, bx,by, cx,cy), 0.0);
//the robust predicate finds the true, small but non-zero, signed area
assertEquals(126.54970229522473, ShewchukPredicates.orient2d(ax,ay,
bx,by, cx,cy), 0.1);
@@ -97,7 +97,7 @@ public class ShewchukPredicatesTest {
/**
* Ill-conditioned (nearly cocircular) case where the naive
double-precision
- * {@link Maths#inCircle} formula flips sign due to cancellation and
wrongly
+ * {@link Maths#inCircleFast} formula flips sign due to cancellation and
wrongly
* reports the point as inside the circle, while it is actually outside.
* {@link ShewchukPredicates#inCircle} must still find the correct sign.
*/
@@ -109,7 +109,7 @@ public class ShewchukPredicatesTest {
final double dx = -24494980.530095316, dy = -92015557.8766704;
//naive double arithmetic wrongly concludes the point is inside the
circle
- assertTrue(Maths.inCircle(ax,ay, bx,by, cx,cy, dx,dy));
+ assertTrue(Maths.inCircleFast(ax,ay, bx,by, cx,cy, dx,dy));
//the robust predicate finds the true, negative (outside) value
assertEquals(-2243068334.102574, ShewchukPredicates.inCircle(ax,ay,
bx,by, cx,cy, dx,dy), 100.0);