diff --git a/src/main/java/org/apache/commons/math/geometry/Vector3D.java b/src/main/java/org/apache/commons/math/geometry/Vector3D.java index 0a4adb840..2d915e570 100644 --- a/src/main/java/org/apache/commons/math/geometry/Vector3D.java +++ b/src/main/java/org/apache/commons/math/geometry/Vector3D.java @@ -454,10 +454,41 @@ public class Vector3D implements Serializable { * @param v2 second vector * @return the cross product v1 ^ v2 as a new Vector */ - public static Vector3D crossProduct(Vector3D v1, Vector3D v2) { - return new Vector3D(v1.y * v2.z - v1.z * v2.y, - v1.z * v2.x - v1.x * v2.z, - v1.x * v2.y - v1.y * v2.x); + public static Vector3D crossProduct(final Vector3D v1, final Vector3D v2) { + + final double n1 = v1.getNormSq(); + final double n2 = v2.getNormSq(); + if ((n1 * n2) < MathUtils.SAFE_MIN) { + return ZERO; + } + + // rescale both vectors without losing precision, + // to ensure their norm are the same order of magnitude + final int deltaExp = (FastMath.getExponent(n1) - FastMath.getExponent(n2)) / 4; + final double x1 = FastMath.scalb(v1.x, -deltaExp); + final double y1 = FastMath.scalb(v1.y, -deltaExp); + final double z1 = FastMath.scalb(v1.z, -deltaExp); + final double x2 = FastMath.scalb(v2.x, deltaExp); + final double y2 = FastMath.scalb(v2.y, deltaExp); + final double z2 = FastMath.scalb(v2.z, deltaExp); + + // we reduce cancellation errors by preconditioning, + // we replace v1 by v3 = v1 - rho v2 with rho chosen in order to compute + // v3 without loss of precision. See Kahan lecture + // "Computing Cross-Products and Rotations in 2- and 3-Dimensional Euclidean Spaces" + // available at http://www.cs.berkeley.edu/~wkahan/MathH110/Cross.pdf + + // compute rho as an 8 bits approximation of v1.v2 / v2.v2 + final double ratio = (x1 * x2 + y1 * y2 + z1 * z2) / FastMath.scalb(n2, 2 * deltaExp); + final double rho = FastMath.rint(256 * ratio) / 256; + + final double x3 = x1 - rho * x2; + final double y3 = y1 - rho * y2; + final double z3 = z1 - rho * z2; + + // compute cross product from v3 and v2 instead of v1 and v2 + return new Vector3D(y3 * z2 - z3 * y2, z3 * x2 - x3 * z2, x3 * y2 - y3 * x2); + } /** Compute the distance between two vectors according to the L1 norm. diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 72035c1a9..45a699128 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -52,6 +52,9 @@ The type attribute can be add,update,fix,remove. If the output is not quite correct, check for invisible trailing spaces! --> + + Reduced cancellation errors in Vector3D.crossProduct + Fixed bug in "MultidimensionalCounter". diff --git a/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java b/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java index 1a80543ef..4dc474c62 100644 --- a/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java +++ b/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java @@ -152,6 +152,19 @@ public class Vector3DTest { Assert.assertTrue(FastMath.abs(Vector3D.dotProduct(v2, v3)) < 1.0e-12); } + @Test + public void testCrossProductCancellation() { + Vector3D v1 = new Vector3D(9070467121.0, 4535233560.0, 1); + Vector3D v2 = new Vector3D(9070467123.0, 4535233561.0, 1); + checkVector(Vector3D.crossProduct(v1, v2), -1, 2, 1); + + double scale = FastMath.scalb(1.0, 100); + Vector3D big1 = new Vector3D(scale, v1); + Vector3D small2 = new Vector3D(1 / scale, v2); + checkVector(Vector3D.crossProduct(big1, small2), -1, 2, 1); + + } + @Test public void testAngular() { Assert.assertEquals(0, Vector3D.PLUS_I.getAlpha(), 1.0e-10);