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 a78ced7f202486d2bf57eeb070e9529c2e77269d Author: Matt Juntunen <[email protected]> AuthorDate: Sun Sep 2 22:05:35 2018 -0400 GEOMETRY-9: adding project and reject methods to Vector2D and Vector3D; adding MultiDimensionalVector interface to contain new methods --- .../geometry/core/MultiDimensionalVector.java | 60 ++++++++++++ .../geometry/euclidean/threed/Vector3D.java | 53 +++++++++- .../commons/geometry/euclidean/twod/Vector2D.java | 56 +++++++++-- .../geometry/euclidean/threed/Vector3DTest.java | 107 +++++++++++++++++++++ .../geometry/euclidean/twod/Vector2DTest.java | 101 +++++++++++++++++++ 5 files changed, 364 insertions(+), 13 deletions(-) diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/MultiDimensionalVector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/MultiDimensionalVector.java new file mode 100644 index 0000000..a8eb74e --- /dev/null +++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/MultiDimensionalVector.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.geometry.core; + +/** Interface representing a vector in a vector space with two or more + * dimensions. + * + * @param <V> Vector implementation type + */ +public interface MultiDimensionalVector<V extends MultiDimensionalVector<V>> extends Vector<V> { + + /** Get the projection of the instance onto the given base vector. The returned + * vector is parallel to {@code base}. Vector projection and rejection onto + * a given base are related by the equation + * <code> + * <strong>v</strong> = <strong>v<sub>projection</sub></strong> + <strong>v<sub>rejection</sub></strong> + * </code> + * @param base + * @return the vector projection of the instance onto {@code base} + * @exception IllegalStateException if the norm of the base vector is zero + * @see #reject(MultiDimensionalVector) + */ + V project(V base) throws IllegalStateException; + + /** Get the rejection of the instance from the given base vector. The returned + * vector is orthogonal to {@code base}. This operation can be interpreted as + * returning the orthogonal projection of the instance onto the hyperplane + * orthogonal to {@code base}. Vector projection and rejection onto + * a given base are related by the equation + * <code> + * <strong>v</strong> = <strong>v<sub>projection</sub></strong> + <strong>v<sub>rejection</sub></strong> + * </code> + * @param base + * @return the vector rejection of the instance from {@code base} + * @exception IllegalStateException if the norm of the base vector is zero + * @see #project(MultiDimensionalVector) + */ + V reject(V base) throws IllegalStateException; + + /** Compute the angular separation in radians between two vectors. + * @param v other vector + * @return angular separation between this instance and v in radians + * @exception IllegalStateException if either vector has a zero norm + */ + double angle(V v) throws IllegalStateException; +} 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 cac3b23..62856a3 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.MultiDimensionalVector; import org.apache.commons.geometry.core.internal.DoubleFunction3N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; import org.apache.commons.geometry.core.util.Vectors; @@ -25,7 +26,7 @@ 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 final class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Vector3D>, MultiDimensionalVector<Vector3D> { /** Zero (null) vector (coordinates: 0, 0, 0). */ public static final Vector3D ZERO = new Vector3D(0, 0, 0); @@ -229,16 +230,14 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point return new Vector3D(inverse * y, -inverse * x, 0); } - /** Compute the angular separation between two vectors. + /** {@inheritDoc} * <p>This method computes the angular separation between two * vectors using the dot product for well separated vectors and the * cross product for almost aligned vectors. This allows to have a * good accuracy in all cases, even for vectors very close to each * other.</p> - * @param v other vector - * @return angular separation between this instance and v - * @exception IllegalStateException if either vector has a zero norm */ + @Override public double angle(Vector3D v) throws IllegalStateException { double normProduct = getNonZeroNorm() * v.getNonZeroNorm(); @@ -326,6 +325,50 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point return LinearCombination.value(getX(), v.getX(), getY(), v.getY(), getZ(), v.getZ()); } + /** {@inheritDoc} */ + @Override + public Vector3D project(Vector3D base) throws IllegalStateException { + return getComponent(base, false); + } + + /** {@inheritDoc} */ + @Override + public Vector3D reject(Vector3D base) throws IllegalStateException { + return getComponent(base, true); + } + + /** Returns a component of the current instance relative to the given base + * vector. If {@code reject} is true, the vector rejection is returned; otherwise, + * the projection is returned. + * @param base The base vector + * @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. + * @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 { + final double aDotB = dotProduct(base); + + final double baseMagSq = base.getNormSq(); + if (baseMagSq == 0.0) { + throw new IllegalStateException("Invalid base vector: norm is zero"); + } + + final double scale = aDotB / baseMagSq; + + final double projX = scale * base.getX(); + final double projY = scale * base.getY(); + final double projZ = scale * base.getZ(); + + if (reject) { + return new Vector3D(getX() - projX, getY() - projY, getZ() - projZ); + } + + return new Vector3D(projX, projY, projZ); + } + /** * Get a hashCode for the vector. * <p>All NaN values have the same hash code.</p> diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java index 52a16f3..004b6c3 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java @@ -16,6 +16,7 @@ */ package org.apache.commons.geometry.euclidean.twod; +import org.apache.commons.geometry.core.MultiDimensionalVector; import org.apache.commons.geometry.core.internal.DoubleFunction2N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; import org.apache.commons.geometry.core.util.Vectors; @@ -25,7 +26,7 @@ import org.apache.commons.numbers.arrays.LinearCombination; /** This class represents a vector in two-dimensional Euclidean space. * Instances of this class are guaranteed to be immutable. */ -public final class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Vector2D> { +public final class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Vector2D>, MultiDimensionalVector<Vector2D> { /** Zero vector (coordinates: 0, 0). */ public static final Vector2D ZERO = new Vector2D(0, 0); @@ -215,19 +216,58 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point return LinearCombination.value(getX(), v.getX(), getY(), v.getY()); } - /** Compute the angular separation in radians between this vector - * and the given vector. + /** {@inheritDoc} */ + @Override + public Vector2D project(Vector2D base) throws IllegalStateException { + return getComponent(base, false); + } + + /** {@inheritDoc} */ + @Override + public Vector2D reject(Vector2D base) throws IllegalStateException { + return getComponent(base, true); + } + + /** Returns a component of the current instance relative to the given base + * vector. If {@code reject} is true, the vector rejection is returned; otherwise, + * the projection is returned. + * @param base The base vector + * @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. + * @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 Vector2D getComponent(Vector2D base, boolean reject) throws IllegalStateException { + final double aDotB = dotProduct(base); + + final double baseMagSq = base.getNormSq(); + if (baseMagSq == 0.0) { + throw new IllegalStateException("Invalid base vector: norm is zero"); + } + + final double scale = aDotB / baseMagSq; + + final double projX = scale * base.getX(); + final double projY = scale * base.getY(); + + if (reject) { + return new Vector2D(getX() - projX, getY() - projY); + } + + return new Vector2D(projX, projY); + } + + /** {@inheritDoc} * <p>This method computes the angular separation between the two * vectors using the dot product for well separated vectors and the * cross product for almost aligned vectors. This allows to have a * good accuracy in all cases, even for vectors very close to each * other.</p> - * - * @param v vector to compute the angle with - * @return angular separation between this vector and v in radians - * @exception IllegalStateException if either vector has a zero norm */ - public double angle(Vector2D v) throws IllegalArgumentException { + @Override + public double angle(Vector2D v) throws IllegalStateException { double normProduct = getNonZeroNorm() * v.getNonZeroNorm(); double dot = dotProduct(v); 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 a0dfd4b..834b472 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 @@ -20,6 +20,7 @@ package org.apache.commons.geometry.euclidean.threed; import java.util.regex.Pattern; import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.euclidean.EuclideanTestUtils; import org.apache.commons.numbers.core.Precision; import org.apache.commons.rng.UniformRandomProvider; import org.apache.commons.rng.simple.RandomSource; @@ -616,6 +617,112 @@ public class Vector3DTest { } @Test + public void testProject() { + // arrange + Vector3D v1 = Vector3D.of(2.0, 3.0, 4.0); + Vector3D v2 = Vector3D.of(-5.0, -6.0, -7.0); + + // act/assert + checkVector(v1.project(Vector3D.PLUS_X), 2.0, 0.0, 0.0); + checkVector(v1.project(Vector3D.MINUS_X), 2.0, 0.0, 0.0); + checkVector(v1.project(Vector3D.PLUS_Y), 0.0, 3.0, 0.0); + checkVector(v1.project(Vector3D.MINUS_Y), 0.0, 3.0, 0.0); + checkVector(v1.project(Vector3D.PLUS_Z), 0.0, 0.0, 4.0); + checkVector(v1.project(Vector3D.MINUS_Z), 0.0, 0.0, 4.0); + + checkVector(v2.project(Vector3D.PLUS_X), -5.0, 0.0, 0.0); + checkVector(v2.project(Vector3D.MINUS_X), -5.0, 0.0, 0.0); + checkVector(v2.project(Vector3D.PLUS_Y), 0.0, -6.0, 0.0); + checkVector(v2.project(Vector3D.MINUS_Y), 0.0, -6.0, 0.0); + checkVector(v2.project(Vector3D.PLUS_Z), 0.0, 0.0, -7.0); + checkVector(v2.project(Vector3D.MINUS_Z), 0.0, 0.0, -7.0); + + checkVector(v1.project(Vector3D.of(1.0, 1.0, 1.0)), 3.0, 3.0, 3.0); + checkVector(v1.project(Vector3D.of(-1.0, -1.0, -1.0)), 3.0, 3.0, 3.0); + + checkVector(v2.project(Vector3D.of(1.0, 1.0, 1.0)), -6.0, -6.0, -6.0); + checkVector(v2.project(Vector3D.of(-1.0, -1.0, -1.0)), -6.0, -6.0, -6.0); + } + + @Test + public void testReject() { + // arrange + Vector3D v1 = Vector3D.of(2.0, 3.0, 4.0); + Vector3D v2 = Vector3D.of(-5.0, -6.0, -7.0); + + // act/assert + checkVector(v1.reject(Vector3D.PLUS_X), 0.0, 3.0, 4.0); + checkVector(v1.reject(Vector3D.MINUS_X), 0.0, 3.0, 4.0); + checkVector(v1.reject(Vector3D.PLUS_Y), 2.0, 0.0, 4.0); + checkVector(v1.reject(Vector3D.MINUS_Y), 2.0, 0.0, 4.0); + checkVector(v1.reject(Vector3D.PLUS_Z), 2.0, 3.0, 0.0); + checkVector(v1.reject(Vector3D.MINUS_Z), 2.0, 3.0, 0.0); + + checkVector(v2.reject(Vector3D.PLUS_X), 0.0, -6.0, -7.0); + checkVector(v2.reject(Vector3D.MINUS_X), 0.0, -6.0, -7.0); + checkVector(v2.reject(Vector3D.PLUS_Y), -5.0, 0.0, -7.0); + checkVector(v2.reject(Vector3D.MINUS_Y), -5.0, 0.0, -7.0); + checkVector(v2.reject(Vector3D.PLUS_Z), -5.0, -6.0, 0.0); + checkVector(v2.reject(Vector3D.MINUS_Z), -5.0, -6.0, 0.0); + + checkVector(v1.reject(Vector3D.of(1.0, 1.0, 1.0)), -1.0, 0.0, 1.0); + checkVector(v1.reject(Vector3D.of(-1.0, -1.0, -1.0)), -1.0, 0.0, 1.0); + + checkVector(v2.reject(Vector3D.of(1.0, 1.0, 1.0)), 1.0, 0.0, -1.0); + checkVector(v2.reject(Vector3D.of(-1.0, -1.0, -1.0)), 1.0, 0.0, -1.0); + } + + @Test + public void testProjectAndReject_areComplementary() { + // arrange + double eps = 1e-12; + + // act/assert + checkProjectAndRejectFullSphere(Vector3D.of(1.0, 0.0, 0.0), 1.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(0.0, 1.0, 0.0), 2.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(0.0, 0.0, 1.0), 2.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(1.0, 1.0, 1.0), 3.0, eps); + + checkProjectAndRejectFullSphere(Vector3D.of(-2.0, 0.0, 0.0), 1.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(0.0, -2.0, 0.0), 2.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(0.0, 0.0, -2.0), 2.0, eps); + checkProjectAndRejectFullSphere(Vector3D.of(-2.0, -2.0, -2.0), 3.0, eps); + } + + private void checkProjectAndRejectFullSphere(Vector3D vec, double baseMag, double eps) { + for (double polar = 0.0; polar <= Geometry.PI; polar += 0.5) { + for (double azimuth = 0.0; azimuth <= Geometry.TWO_PI; azimuth += 0.5) { + Vector3D base = Vector3D.ofSpherical(baseMag, azimuth, polar); + + Vector3D proj = vec.project(base); + Vector3D rej = vec.reject(base); + + // ensure that the projection and rejection sum to the original vector + EuclideanTestUtils.assertCoordinatesEqual(vec, proj.add(rej), eps); + + double angle = base.angle(vec); + + // check the angle between the projection and the base; this will + // be undefined when the angle between the original vector and the + // base is pi/2 (which means that the projection is the zero vector) + if (angle < Geometry.HALF_PI) { + Assert.assertEquals(0.0, proj.angle(base), eps); + } + else if (angle > Geometry.HALF_PI) { + Assert.assertEquals(Geometry.PI, proj.angle(base), eps); + } + + // check the angle between the rejection and the base; this should + // always be pi/2 except for when the angle between the original vector + // and the base is 0 or pi, in which case the rejection is the zero vector. + if (angle > 0.0 && angle < Geometry.PI) { + Assert.assertEquals(Geometry.HALF_PI, rej.angle(base), eps); + } + } + } + } + + @Test public void testHashCode() { // arrange double delta = 10 * Precision.EPSILON; diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java index 3bf0687..7c31092 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java @@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod; import java.util.regex.Pattern; import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.euclidean.EuclideanTestUtils; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; import org.junit.Test; @@ -436,6 +437,106 @@ public class Vector2DTest { } @Test + public void testProject() { + // arrange + Vector2D v1 = Vector2D.of(3.0, 4.0); + Vector2D v2 = Vector2D.of(1.0, 4.0); + + // act/assert + checkVector(Vector2D.ZERO.project(v1), 0.0, 0.0); + + checkVector(v1.project(v1), 3.0, 4.0); + checkVector(v1.project(v1.negate()), 3.0, 4.0); + + checkVector(v1.project(Vector2D.PLUS_X), 3.0, 0.0); + checkVector(v1.project(Vector2D.MINUS_X), 3.0, 0.0); + + checkVector(v1.project(Vector2D.PLUS_Y), 0.0, 4.0); + checkVector(v1.project(Vector2D.MINUS_Y), 0.0, 4.0); + + checkVector(v2.project(v1), (19.0 / 25.0) * 3.0, (19.0 / 25.0) * 4.0); + } + + @Test(expected = IllegalStateException.class) + public void testProject_baseHasZeroNorm() { + // act/assert + Vector2D.of(1.0, 1.0).project(Vector2D.ZERO); + } + + @Test + public void testReject() { + // arrange + Vector2D v1 = Vector2D.of(3.0, 4.0); + Vector2D v2 = Vector2D.of(1.0, 4.0); + + // act/assert + checkVector(Vector2D.ZERO.reject(v1), 0.0, 0.0); + + checkVector(v1.reject(v1), 0.0, 0.0); + checkVector(v1.reject(v1.negate()), 0.0, 0.0); + + checkVector(v1.reject(Vector2D.PLUS_X), 0.0, 4.0); + checkVector(v1.reject(Vector2D.MINUS_X), 0.0, 4.0); + + checkVector(v1.reject(Vector2D.PLUS_Y), 3.0, 0.0); + checkVector(v1.reject(Vector2D.MINUS_Y), 3.0, 0.0); + + checkVector(v2.reject(v1), (-32.0 / 25.0), (6.0 / 25.0) * 4.0); + } + + @Test(expected = IllegalStateException.class) + public void testReject_baseHasZeroNorm() { + // act/assert + Vector2D.of(1.0, 1.0).reject(Vector2D.ZERO); + } + + @Test + public void testProjectAndReject_areComplementary() { + // arrange + double eps = 1e-12; + + // act/assert + checkProjectAndRejectFullCircle(Vector2D.of(1.0, 0.0), 1.0, eps); + checkProjectAndRejectFullCircle(Vector2D.of(0.0, 1.0), 2.0, eps); + checkProjectAndRejectFullCircle(Vector2D.of(1.0, 1.0), 3.0, eps); + + checkProjectAndRejectFullCircle(Vector2D.of(-2.0, 0.0), 4.0, eps); + checkProjectAndRejectFullCircle(Vector2D.of(0.0, -2.0), 5.0, eps); + checkProjectAndRejectFullCircle(Vector2D.of(-2.0, -2.0), 6.0, eps); + } + + private void checkProjectAndRejectFullCircle(Vector2D vec, double baseMag, double eps) { + for (double theta = 0.0; theta <= Geometry.TWO_PI; theta += 0.5) { + Vector2D base = Vector2D.ofPolar(baseMag, theta); + + Vector2D proj = vec.project(base); + Vector2D rej = vec.reject(base); + + // ensure that the projection and rejection sum to the original vector + EuclideanTestUtils.assertCoordinatesEqual(vec, proj.add(rej), eps); + + double angle = base.angle(vec); + + // check the angle between the projection and the base; this will + // be undefined when the angle between the original vector and the + // base is pi/2 (which means that the projection is the zero vector) + if (angle < Geometry.HALF_PI) { + Assert.assertEquals(0.0, proj.angle(base), eps); + } + else if (angle > Geometry.HALF_PI) { + Assert.assertEquals(Geometry.PI, proj.angle(base), eps); + } + + // check the angle between the rejection and the base; this should + // always be pi/2 except for when the angle between the original vector + // and the base is 0 or pi, in which case the rejection is the zero vector. + if (angle > 0.0 && angle < Geometry.PI) { + Assert.assertEquals(Geometry.HALF_PI, rej.angle(base), eps); + } + } + } + + @Test public void testHashCode() { // arrange Vector2D u = Vector2D.of(1, 1);
