You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2018/09/17 13:41:53 UTC

[commons-geometry] 01/07: GEOMETRY-10: adding Vector3D private class for normalization optimizations; adding Vector3D#orthogonal(Vector3D) method

This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit fb028f007055c035c3ac66f557eac871b940e17f
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Tue Sep 4 23:49:44 2018 -0400

    GEOMETRY-10: adding Vector3D private class for normalization optimizations; adding Vector3D#orthogonal(Vector3D) method
---
 .../commons/geometry/euclidean/threed/Point3D.java |  7 ++
 .../geometry/euclidean/threed/Vector3D.java        | 93 +++++++++++++++++----
 .../geometry/euclidean/threed/Vector3DTest.java    | 96 +++++++++++++++++++++-
 3 files changed, 178 insertions(+), 18 deletions(-)

diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
index 52c574b..cfe0af9 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
@@ -87,6 +87,13 @@ public final class Point3D extends Cartesian3D implements EuclideanPoint<Point3D
         return p.subtract(this);
     }
 
+    public Vector3D directionTo(Point3D p) {
+        return Vector3D.normalize(
+                p.getX() - getX(),
+                p.getY() - getY(),
+                p.getZ() - getZ());
+    }
+
     /** {@inheritDoc} */
     @Override
     public Point3D lerp(Point3D p, double t) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
index 2a1a5f7..61c73d6 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import org.apache.commons.geometry.core.internal.DoubleFunction3N;
 import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
 import org.apache.commons.geometry.core.util.Vectors;
 import org.apache.commons.geometry.euclidean.EuclideanVector;
@@ -25,28 +26,28 @@ import org.apache.commons.numbers.arrays.LinearCombination;
 /** This class represents a vector in three-dimensional Euclidean space.
  * Instances of this class are guaranteed to be immutable.
  */
-public final class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Vector3D> {
+public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Vector3D> {
 
     /** Zero (null) vector (coordinates: 0, 0, 0). */
     public static final Vector3D ZERO   = new Vector3D(0, 0, 0);
 
     /** First canonical vector (coordinates: 1, 0, 0). */
-    public static final Vector3D PLUS_X = new Vector3D(1, 0, 0);
+    public static final Vector3D PLUS_X = new UnitVector(1, 0, 0);
 
     /** Opposite of the first canonical vector (coordinates: -1, 0, 0). */
-    public static final Vector3D MINUS_X = new Vector3D(-1, 0, 0);
+    public static final Vector3D MINUS_X = new UnitVector(-1, 0, 0);
 
     /** Second canonical vector (coordinates: 0, 1, 0). */
-    public static final Vector3D PLUS_Y = new Vector3D(0, 1, 0);
+    public static final Vector3D PLUS_Y = new UnitVector(0, 1, 0);
 
     /** Opposite of the second canonical vector (coordinates: 0, -1, 0). */
-    public static final Vector3D MINUS_Y = new Vector3D(0, -1, 0);
+    public static final Vector3D MINUS_Y = new UnitVector(0, -1, 0);
 
     /** Third canonical vector (coordinates: 0, 0, 1). */
-    public static final Vector3D PLUS_Z = new Vector3D(0, 0, 1);
+    public static final Vector3D PLUS_Z = new UnitVector(0, 0, 1);
 
     /** Opposite of the third canonical vector (coordinates: 0, 0, -1).  */
-    public static final Vector3D MINUS_Z = new Vector3D(0, 0, -1);
+    public static final Vector3D MINUS_Z = new UnitVector(0, 0, -1);
 
     // CHECKSTYLE: stop ConstantName
     /** A vector with all coordinates set to NaN. */
@@ -61,8 +62,8 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
     public static final Vector3D NEGATIVE_INFINITY =
         new Vector3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
 
-    /** Serializable UID */
-    private static final long serialVersionUID = 20180710L;
+    /** Serializable version identifier */
+    private static final long serialVersionUID = 20180903L;
 
     /** Simple constructor.
      * Build a vector from its coordinates
@@ -189,7 +190,7 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
     /** {@inheritDoc} */
     @Override
     public Vector3D normalize() throws IllegalStateException {
-        return scalarMultiply(1.0 / getNonZeroNorm());
+        return normalize(getX(), getY(), getZ());
     }
 
     /** Get a vector orthogonal to the instance.
@@ -225,6 +226,19 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
         return new Vector3D(inverse * y, -inverse * x, 0);
     }
 
+    /** Returns a unit vector orthogonal to the current vector and pointing in the direction
+     * of {@code dir}. This method is equivalent to calling {@code dir.reject(vec).normalize()}
+     * except that no intermediate vector object is produced.
+     * @param dir the direction to use for generating the orthogonal vector
+     * @return unit vector orthogonal to the current vector and pointing in the direction of
+     *      {@code dir} that does not lie along the current vector
+     * @throws IllegalStateException if the norm of the current vector is zero or the given
+     *      vector is collinear with this vector.
+     */
+    public Vector3D orthogonal(Vector3D dir) throws IllegalStateException {
+        return dir.getComponent(this, true, Vector3D::normalize);
+    }
+
     /** {@inheritDoc}
      * <p>This method computes the angular separation between two
      * vectors using the dot product for well separated vectors and the
@@ -323,13 +337,13 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
     /** {@inheritDoc} */
     @Override
     public Vector3D project(Vector3D base) throws IllegalStateException {
-        return getComponent(base, false);
+        return getComponent(base, false, Vector3D::new);
     }
 
     /** {@inheritDoc} */
     @Override
     public Vector3D reject(Vector3D base) throws IllegalStateException {
-        return getComponent(base, true);
+        return getComponent(base, true, Vector3D::new);
     }
 
     /**
@@ -402,11 +416,12 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
      * @param reject If true, the rejection of this instance from {@code base} is
      *      returned. If false, the projection of this instance onto {@code base}
      *      is returned.
+     * @param factory factory function used to build the final vector
      * @return The projection or rejection of this instance relative to {@code base},
      *      depending on the value of {@code reject}.
      * @throws IllegalStateException if {@code base} has a zero norm
      */
-    private Vector3D getComponent(Vector3D base, boolean reject) throws IllegalStateException {
+    private Vector3D getComponent(Vector3D base, boolean reject, DoubleFunction3N<Vector3D> factory) throws IllegalStateException {
         final double aDotB = dotProduct(base);
 
         final double baseMagSq = base.getNormSq();
@@ -421,10 +436,10 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
         final double projZ = scale * base.getZ();
 
         if (reject) {
-            return new Vector3D(getX() - projX, getY() - projY, getZ() - projZ);
+            return factory.apply(getX() - projX, getY() - projY, getZ() - projZ);
         }
 
-        return new Vector3D(projX, projY, projZ);
+        return factory.apply(projX, projY, projZ);
     }
 
     /** Computes the dot product between to vectors. This method simply
@@ -515,6 +530,23 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
         return SphericalCoordinates.toCartesian(radius, azimuth, polar, Vector3D::new);
     }
 
+    /** Returns a normalized vector derived from the given values.
+     * @param x abscissa (first coordinate value)
+     * @param y abscissa (second coordinate value)
+     * @param z height (third coordinate value)
+     * @return normalized vector instance
+     * @throws IllegalStateException if the norm of the given values is zero
+     */
+    public static Vector3D normalize(final double x, final double y, final double z) throws IllegalStateException {
+        final double norm = Vectors.norm(x, y, z);
+        if (norm == 0.0) {
+            throw new ZeroNormException();
+        }
+        final double invNorm = 1.0 / norm;
+
+        return new UnitVector(x * invNorm, y * invNorm, z * invNorm);
+    }
+
     /** Parses the given string and returns a new vector instance. The expected string
      * format is the same as that returned by {@link #toString()}.
      * @param str the string to parse
@@ -623,4 +655,35 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point
                 LinearCombination.value(a1, c1.getY(), a2, c2.getY(), a3, c3.getY(), a4, c4.getY()),
                 LinearCombination.value(a1, c1.getZ(), a2, c2.getZ(), a3, c3.getZ(), a4, c4.getZ()));
     }
+
+    /** Private class used to represent unit vectors. This allows optimizations to be performed for certain
+     * operations.
+     */
+    private static final class UnitVector extends Vector3D {
+
+        /** Serializable version identifier */
+        private static final long serialVersionUID = 20180903L;
+
+        /** Simple constructor. Callers are responsible for ensuring that the given
+         * values represent a normalized vector.
+         * @param x abscissa (first coordinate value)
+         * @param y abscissa (second coordinate value)
+         * @param z height (third coordinate value)
+         */
+        private UnitVector(final double x, final double y, final double z) {
+            super(x, y, z);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector3D normalize() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector3D withMagnitude(final double mag) {
+            return scalarMultiply(mag);
+        }
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
index 4880438..e0de1c5 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
@@ -51,6 +51,19 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testNonZeroConstants_areUnitVectorInstances() {
+        // act/assert
+        Assert.assertSame(Vector3D.PLUS_X.normalize(), Vector3D.PLUS_X);
+        Assert.assertSame(Vector3D.MINUS_X.normalize(), Vector3D.MINUS_X);
+
+        Assert.assertSame(Vector3D.PLUS_Y.normalize(), Vector3D.PLUS_Y);
+        Assert.assertSame(Vector3D.MINUS_Y.normalize(), Vector3D.MINUS_Y);
+
+        Assert.assertSame(Vector3D.PLUS_Z.normalize(), Vector3D.PLUS_Z);
+        Assert.assertSame(Vector3D.MINUS_Z.normalize(), Vector3D.MINUS_Z);
+    }
+
+    @Test
     public void testZero() {
         // act
         Vector3D zero = Vector3D.of(1, 2, 3).getZero();
@@ -131,6 +144,8 @@ public class Vector3DTest {
         double normZ = z / len;
 
         // act/assert
+        checkVector(Vector3D.of(x, y, z).withMagnitude(0.0), 0.0, 0.0, 0.0);
+
         checkVector(Vector3D.of(x, y, z).withMagnitude(1.0), normX, normY, normZ);
         checkVector(Vector3D.of(x, y, -z).withMagnitude(1.0), normX, normY, -normZ);
         checkVector(Vector3D.of(x, -y, z).withMagnitude(1.0), normX, -normY, normZ);
@@ -142,6 +157,14 @@ public class Vector3DTest {
 
         checkVector(Vector3D.of(x, y, z).withMagnitude(0.5), 0.5 * normX, 0.5 * normY, 0.5 * normZ);
         checkVector(Vector3D.of(x, y, z).withMagnitude(3), 3 * normX, 3 * normY, 3 * normZ);
+
+        checkVector(Vector3D.of(x, y, z).withMagnitude(-0.5), -0.5 * normX, -0.5 * normY, -0.5 * normZ);
+        checkVector(Vector3D.of(x, y, z).withMagnitude(-3), -3 * normX, -3 * normY, -3 * normZ);
+
+        for (double mag = -10.0; mag <= 10.0; ++mag)
+        {
+            Assert.assertEquals(Math.abs(mag), Vector3D.of(x, y, z).withMagnitude(mag).getMagnitude(), EPS);
+        }
     }
 
     @Test(expected = IllegalStateException.class)
@@ -151,6 +174,22 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testWithMagnitude_unitVectors() {
+        // arrange
+        Vector3D v = Vector3D.of(2.0, -3.0, 4.0).normalize();
+
+        // act/assert
+        checkVector(Vector3D.PLUS_X.withMagnitude(2.5), 2.5, 0.0, 0.0);
+        checkVector(Vector3D.MINUS_Y.withMagnitude(3.14), 0.0, -3.14, 0.0);
+        checkVector(Vector3D.PLUS_Z.withMagnitude(-1.1), 0.0, 0.0, -1.1);
+
+        for (double mag = -10.0; mag <= 10.0; ++mag)
+        {
+            Assert.assertEquals(Math.abs(mag), v.withMagnitude(mag).getMagnitude(), EPS);
+        }
+    }
+
+    @Test
     public void testAdd() {
         // arrange
         Vector3D v1 = Vector3D.of(1, 2, 3);
@@ -247,7 +286,7 @@ public class Vector3DTest {
         checkVector(Vector3D.of(2, 2, 2).normalize(), invSqrt3, invSqrt3, invSqrt3);
         checkVector(Vector3D.of(-2, -2, -2).normalize(), -invSqrt3, -invSqrt3, -invSqrt3);
 
-        Assert.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().getNorm(), 1.0e-12);
+        Assert.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().getNorm(), EPS);
     }
 
     @Test(expected = IllegalStateException.class)
@@ -257,6 +296,17 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testNormalize_isIdempotent() {
+        // arrange
+        double invSqrt3 = 1 / Math.sqrt(3);
+        Vector3D v = Vector3D.of(2, 2, 2).normalize();
+
+        // act/assert
+        Assert.assertSame(v, v.normalize());
+        checkVector(v.normalize(), invSqrt3, invSqrt3, invSqrt3);
+    }
+
+    @Test
     public void testOrthogonal() {
         // arrange
         Vector3D v1 = Vector3D.of(0.1, 2.5, 1.3);
@@ -278,6 +328,31 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testOrthogonal_givenDirection() {
+        // arrange
+        double invSqrt2 = 1.0 / Math.sqrt(2.0);
+
+        // act/assert
+        checkVector(Vector3D.PLUS_X.orthogonal(Vector3D.of(-1.0, 0.1, 0.0)), 0.0, 1.0, 0.0);
+        checkVector(Vector3D.PLUS_Y.orthogonal(Vector3D.of(2.0, 2.0, 2.0)), invSqrt2, 0.0, invSqrt2);
+        checkVector(Vector3D.PLUS_Z.orthogonal(Vector3D.of(3.0, 3.0, -3.0)), invSqrt2, invSqrt2, 0.0);
+
+        checkVector(Vector3D.of(invSqrt2, invSqrt2, 0.0).orthogonal(Vector3D.of(1.0, 1.0, 0.2)), 0.0, 0.0, 1.0);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOrthogonal_givenDirection_zeroNorm() {
+        // act/assert
+        Vector3D.ZERO.orthogonal(Vector3D.PLUS_X);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOrthogonal_givenDirection_directionIsCollinear() {
+        // act/assert
+        Vector3D.PLUS_X.orthogonal(Vector3D.of(-2.0, 0.0, 0.0));
+    }
+
+    @Test
     public void testAngle() {
         // arrange
         double tolerance = 1e-10;
@@ -956,7 +1031,7 @@ public class Vector3DTest {
     }
 
     @Test
-    public void testOf_arrayArg() {
+    public void testOfArray() {
         // act/assert
         checkVector(Vector3D.ofArray(new double[] { 1, 2, 3 }), 1, 2, 3);
         checkVector(Vector3D.ofArray(new double[] { -1, -2, -3 }), -1, -2, -3);
@@ -967,7 +1042,7 @@ public class Vector3DTest {
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void testOf_arrayArg_invalidDimensions() {
+    public void testOfArray_invalidDimensions() {
         // act/assert
         Vector3D.ofArray(new double[] { 0.0, 0.0 });
     }
@@ -994,6 +1069,21 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testNormalize_static() {
+        // arrange
+        double invSqrt3 = 1.0 / Math.sqrt(3.0);
+
+        // act/assert
+        checkVector(Vector3D.normalize(2.0, -2.0, 2.0), invSqrt3, -invSqrt3, invSqrt3);
+        checkVector(Vector3D.normalize(-4.0, 4.0, -4.0), -invSqrt3, invSqrt3, -invSqrt3);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testNormalize_static_zeroNorm() {
+        Vector3D.normalize(0.0, 0.0, 0.0);
+    }
+
+    @Test
     public void testLinearCombination1() {
         // arrange
         Vector3D p1 = Vector3D.of(1, 2, 3);