You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ma...@apache.org on 2021/04/09 01:10:20 UTC
[commons-geometry] branch master updated: GEOMETRY-119: adding
Vectors.normalizeOrNull() method;
scaling vectors if needed during normalization
This is an automated email from the ASF dual-hosted git repository.
mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
The following commit(s) were added to refs/heads/master by this push:
new ea68dfb GEOMETRY-119: adding Vectors.normalizeOrNull() method; scaling vectors if needed during normalization
ea68dfb is described below
commit ea68dfb0093aace23d856970b32e2b7f0941e425
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Wed Apr 7 08:03:35 2021 -0400
GEOMETRY-119: adding Vectors.normalizeOrNull() method; scaling vectors if needed during normalization
---
.../org/apache/commons/geometry/core/Vector.java | 12 +-
.../geometry/euclidean/internal/Vectors.java | 18 +--
.../commons/geometry/euclidean/oned/Vector1D.java | 77 ++++++++----
.../geometry/euclidean/threed/Vector3D.java | 139 ++++++++++++++++-----
.../threed/rotation/QuaternionRotation.java | 7 +-
.../commons/geometry/euclidean/twod/Vector2D.java | 125 ++++++++++++++----
.../geometry/euclidean/internal/VectorsTest.java | 15 ---
.../geometry/euclidean/oned/Vector1DTest.java | 52 +++++++-
.../geometry/euclidean/threed/Vector3DTest.java | 75 ++++++++++-
.../geometry/euclidean/twod/Vector2DTest.java | 68 +++++++++-
.../examples/jmh/euclidean/VectorPerformance.java | 43 +++++--
.../io/euclidean/threed/obj/PolygonObjParser.java | 5 +-
.../geometry/io/euclidean/threed/stl/StlUtils.java | 5 +-
13 files changed, 504 insertions(+), 137 deletions(-)
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
index 65a009b..dbd3e49 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -93,11 +93,21 @@ public interface Vector<V extends Vector<V>> extends Spatial {
/** Get a normalized vector aligned with the instance. The returned
* vector has a magnitude of 1.
- * @return a new normalized vector
+ * @return normalized vector
* @throws IllegalArgumentException if the norm is zero, NaN, or infinite
+ * @see #normalizeOrNull()
*/
V normalize();
+ /** Attempt to compute a normalized vector aligned with the instance, returning null if
+ * such a vector cannot be computed. This method is equivalent to {@link #normalize()}
+ * but returns null instead of throwing an exception on failure.
+ * @return normalized vector or null if such a vector cannot be computed, i.e. if the
+ * norm is zero, NaN, or infinite
+ * @see #normalize()
+ */
+ V normalizeOrNull();
+
/** Multiply the instance by a scalar.
* @param a scalar
* @return a new vector
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
index 4f33535..aac199c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
@@ -46,7 +46,7 @@ public final class Vectors {
*/
public static double checkedNorm(final double norm) {
if (!isRealNonZero(norm)) {
- throw new IllegalArgumentException("Illegal norm: " + norm);
+ throw illegalNorm(norm);
}
return norm;
@@ -63,18 +63,12 @@ public final class Vectors {
return checkedNorm(vec.norm());
}
- /** Attempt to normalize the given vector, returning null if the vector cannot be normalized
- * due to the norm being NaN, infinite, or null.
- * @param <V> Vector implementation type
- * @param vec the vector to attempt to normalize
- * @return the normalized vector if successful, otherwise null
+ /** Return an exception indicating an illegal norm value.
+ * @param norm illegal norm value
+ * @return exception indicating an illegal norm value
*/
- public static <V extends Vector<V>> V tryNormalize(final V vec) {
- final double norm = vec.norm();
- if (isRealNonZero(norm)) {
- return vec.normalize();
- }
- return null;
+ public static IllegalArgumentException illegalNorm(final double norm) {
+ return new IllegalArgumentException("Illegal norm: " + norm);
}
/** Get the L<sub>2</sub> norm (commonly known as the Euclidean norm) for the vector
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
index ed968fe..009762c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
@@ -188,6 +188,12 @@ public class Vector1D extends EuclideanVector<Vector1D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return Unit.tryCreateNormalized(x, false);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector1D multiply(final double a) {
return new Vector1D(a * x);
}
@@ -413,31 +419,6 @@ public class Vector1D extends EuclideanVector<Vector1D> {
super(x);
}
- /**
- * Creates a normalized vector.
- *
- * @param x Vector coordinate.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
- */
- public static Unit from(final double x) {
- Vectors.checkedNorm(Vectors.norm(x));
- return x > 0 ? PLUS : MINUS;
- }
-
- /**
- * Creates a normalized vector.
- *
- * @param v Vector.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
- */
- public static Unit from(final Vector1D v) {
- return v instanceof Unit ?
- (Unit) v :
- from(v.getX());
- }
-
/** {@inheritDoc} */
@Override
public double norm() {
@@ -458,6 +439,12 @@ public class Vector1D extends EuclideanVector<Vector1D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector1D withNorm(final double mag) {
return multiply(mag);
}
@@ -467,5 +454,45 @@ public class Vector1D extends EuclideanVector<Vector1D> {
public Vector1D negate() {
return this == PLUS ? MINUS : PLUS;
}
+
+ /** Create a normalized vector.
+ * @param x Vector coordinate.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
+ */
+ public static Unit from(final double x) {
+ return tryCreateNormalized(x, true);
+ }
+
+ /** Create a normalized vector.
+ * @param v Vector.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
+ */
+ public static Unit from(final Vector1D v) {
+ return v instanceof Unit ?
+ (Unit) v :
+ from(v.getX());
+ }
+
+ /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+ * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+ * is returned.
+ * @param x x coordinate
+ * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+ * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+ * is false
+ * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+ */
+ private static Unit tryCreateNormalized(final double x, final boolean throwOnFailure) {
+ final double norm = Vectors.norm(x);
+
+ if (Vectors.isRealNonZero(norm)) {
+ return x > 0 ? PLUS : MINUS;
+ } else if (throwOnFailure) {
+ throw Vectors.illegalNorm(norm);
+ }
+ return null;
+ }
}
}
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 0585789..0cb86c0 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
@@ -248,6 +248,12 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return Unit.tryCreateNormalized(x, y, z, false);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector3D multiply(final double a) {
return new Vector3D(a * x, a * y, a * z);
}
@@ -760,6 +766,21 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
/** Negation of unit vector (coordinates: 0, 0, -1). */
public static final Unit MINUS_Z = new Unit(0d, 0d, -1d);
+ /** Maximum coordinate value for computing normalized vectors
+ * with raw, unscaled values.
+ */
+ private static final double UNSCALED_MAX = 0x1.0p+500;
+
+ /** Factor used to scale up coordinate values in order to produce
+ * normalized coordinates without overflow or underflow.
+ */
+ private static final double SCALE_UP_FACTOR = 0x1.0p+600;
+
+ /** Factor used to scale down coordinate values in order to produce
+ * normalized coordinates without overflow or underflow.
+ */
+ private static final double SCALE_DOWN_FACTOR = 0x1.0p-600;
+
/** Simple constructor. Callers are responsible for ensuring that the given
* values represent a normalized vector.
* @param x x coordinate value
@@ -770,35 +791,6 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
super(x, y, z);
}
- /**
- * Creates a normalized vector.
- *
- * @param x Vector coordinate.
- * @param y Vector coordinate.
- * @param z Vector coordinate.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given value
- * is zero, NaN, or infinite
- */
- public static Unit from(final double x, final double y, final double z) {
- final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y, z));
- return new Unit(x * invNorm, y * invNorm, z * invNorm);
- }
-
- /**
- * Creates a normalized vector.
- *
- * @param v Vector.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given
- * value is zero, NaN, or infinite
- */
- public static Unit from(final Vector3D v) {
- return v instanceof Unit ?
- (Unit) v :
- from(v.getX(), v.getY(), v.getZ());
- }
-
/** {@inheritDoc} */
@Override
public double norm() {
@@ -819,6 +811,12 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector3D withNorm(final double mag) {
return multiply(mag);
}
@@ -828,5 +826,88 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
public Unit negate() {
return new Unit(-getX(), -getY(), -getZ());
}
+
+ /** Create a normalized vector.
+ * @param x Vector coordinate.
+ * @param y Vector coordinate.
+ * @param z Vector coordinate.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+ * or infinite
+ */
+ public static Unit from(final double x, final double y, final double z) {
+ return tryCreateNormalized(x, y, z, true);
+ }
+
+ /** Create a normalized vector.
+ * @param v Vector.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+ * or infinite
+ */
+ public static Unit from(final Vector3D v) {
+ return v instanceof Unit ?
+ (Unit) v :
+ from(v.getX(), v.getY(), v.getZ());
+ }
+
+ /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+ * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+ * is returned.
+ * @param x x coordinate
+ * @param y y coordinate
+ * @param z z coordinate
+ * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+ * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+ * is false
+ * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+ */
+ private static Unit tryCreateNormalized(final double x, final double y, final double z,
+ final boolean throwOnFailure) {
+
+ // Compute the inverse norm directly. If the result is a non-zero real number,
+ // then we can go ahead and construct the unit vector immediately. If not,
+ // we'll do some extra work for edge cases.
+ final double norm = Math.sqrt((x * x) + (y * y) + (z * z));
+ final double normInv = 1.0 / norm;
+ if (Vectors.isRealNonZero(normInv)) {
+ return new Unit(
+ x * normInv,
+ y * normInv,
+ z * normInv);
+ }
+
+ // Direct computation did not work. Try scaled versions of the coordinates
+ // to handle overflow and underflow.
+ final double scaledX;
+ final double scaledY;
+ final double scaledZ;
+
+ final double maxCoord = Math.max(Math.max(Math.abs(x), Math.abs(y)), Math.abs(z));
+ if (maxCoord > UNSCALED_MAX) {
+ scaledX = x * SCALE_DOWN_FACTOR;
+ scaledY = y * SCALE_DOWN_FACTOR;
+ scaledZ = z * SCALE_DOWN_FACTOR;
+ } else {
+ scaledX = x * SCALE_UP_FACTOR;
+ scaledY = y * SCALE_UP_FACTOR;
+ scaledZ = z * SCALE_UP_FACTOR;
+ }
+
+ final double scaledNormInv = 1.0 / Math.sqrt(
+ (scaledX * scaledX) +
+ (scaledY * scaledY) +
+ (scaledZ * scaledZ));
+
+ if (Vectors.isRealNonZero(scaledNormInv)) {
+ return new Unit(
+ scaledX * scaledNormInv,
+ scaledY * scaledNormInv,
+ scaledZ * scaledNormInv);
+ } else if (throwOnFailure) {
+ throw Vectors.illegalNorm(norm);
+ }
+ return null;
+ }
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
index 62e950a..76f1a2f 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
@@ -83,10 +83,11 @@ public final class QuaternionRotation implements Rotation3D {
*/
@Override
public Vector3D getAxis() {
- final Vector3D axis = Vectors.tryNormalize(Vector3D.of(quat.getX(), quat.getY(), quat.getZ()));
+ final Vector3D axis = Vector3D.of(quat.getX(), quat.getY(), quat.getZ())
+ .normalizeOrNull();
return axis != null ?
- axis :
- Vector3D.Unit.PLUS_X;
+ axis :
+ Vector3D.Unit.PLUS_X;
}
/**
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 84b1807..3a0cf9e 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
@@ -215,6 +215,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return Unit.tryCreateNormalized(x, y, false);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector2D multiply(final double a) {
return new Vector2D(a * x, a * y);
}
@@ -686,6 +692,21 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
/** Negation of unit vector (coordinates: 0, -1). */
public static final Unit MINUS_Y = new Unit(0d, -1d);
+ /** Maximum coordinate value for computing normalized vectors
+ * with raw, unscaled values.
+ */
+ private static final double UNSCALED_MAX = 0x1.0p+500;
+
+ /** Factor used to scale up coordinate values in order to produce
+ * normalized coordinates without overflow or underflow.
+ */
+ private static final double SCALE_UP_FACTOR = 0x1.0p+600;
+
+ /** Factor used to scale down coordinate values in order to produce
+ * normalized coordinates without overflow or underflow.
+ */
+ private static final double SCALE_DOWN_FACTOR = 0x1.0p-600;
+
/** Simple constructor. Callers are responsible for ensuring that the given
* values represent a normalized vector.
* @param x abscissa (first coordinate value)
@@ -695,32 +716,6 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
super(x, y);
}
- /**
- * Creates a normalized vector.
- *
- * @param x Vector coordinate.
- * @param y Vector coordinate.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
- */
- public static Unit from(final double x, final double y) {
- final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y));
- return new Unit(x * invNorm, y * invNorm);
- }
-
- /**
- * Creates a normalized vector.
- *
- * @param v Vector.
- * @return a vector whose norm is 1.
- * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
- */
- public static Unit from(final Vector2D v) {
- return v instanceof Unit ?
- (Unit) v :
- from(v.getX(), v.getY());
- }
-
/** {@inheritDoc} */
@Override
public double norm() {
@@ -741,6 +736,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
/** {@inheritDoc} */
@Override
+ public Unit normalizeOrNull() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector2D.Unit orthogonal() {
return new Unit(-getY(), getX());
}
@@ -756,5 +757,77 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
public Unit negate() {
return new Unit(-getX(), -getY());
}
+
+ /** Create a normalized vector.
+ * @param x Vector coordinate.
+ * @param y Vector coordinate.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+ * or infinite
+ */
+ public static Unit from(final double x, final double y) {
+ return tryCreateNormalized(x, y, true);
+ }
+
+ /** Create a normalized vector.
+ * @param v Vector.
+ * @return a vector whose norm is 1.
+ * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+ * or infinite
+ */
+ public static Unit from(final Vector2D v) {
+ return v instanceof Unit ?
+ (Unit) v :
+ from(v.getX(), v.getY());
+ }
+
+ /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+ * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+ * is returned.
+ * @param x x coordinate
+ * @param y y coordinate
+ * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+ * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+ * is false
+ * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+ */
+ private static Unit tryCreateNormalized(final double x, final double y, final boolean throwOnFailure) {
+
+ // Compute the inverse norm directly. If the result is a non-zero real number,
+ // then we can go ahead and construct the unit vector immediately. If not,
+ // we'll do some extra work for edge cases.
+ final double norm = Vectors.norm(x, y);
+ final double normInv = 1.0 / norm;
+ if (Vectors.isRealNonZero(normInv)) {
+ return new Unit(
+ x * normInv,
+ y * normInv);
+ }
+
+ // Direct computation did not work. Try scaled versions of the coordinates
+ // to handle overflow and underflow.
+ final double scaledX;
+ final double scaledY;
+
+ final double maxCoord = Math.max(Math.abs(x), Math.abs(y));
+ if (maxCoord > UNSCALED_MAX) {
+ scaledX = x * SCALE_DOWN_FACTOR;
+ scaledY = y * SCALE_DOWN_FACTOR;
+ } else {
+ scaledX = x * SCALE_UP_FACTOR;
+ scaledY = y * SCALE_UP_FACTOR;
+ }
+
+ final double scaledNormInv = 1.0 / Vectors.norm(scaledX, scaledY);
+
+ if (Vectors.isRealNonZero(scaledNormInv)) {
+ return new Unit(
+ scaledX * scaledNormInv,
+ scaledY * scaledNormInv);
+ } else if (throwOnFailure) {
+ throw Vectors.illegalNorm(norm);
+ }
+ return null;
+ }
}
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
index 0109a7d..1191d2d 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
@@ -17,7 +17,6 @@
package org.apache.commons.geometry.euclidean.internal;
import org.apache.commons.geometry.core.GeometryTestUtils;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.junit.jupiter.api.Assertions;
@@ -85,20 +84,6 @@ public class VectorsTest {
}
@Test
- public void testTryNormalize() {
- // act/assert
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X,
- Vectors.tryNormalize(Vector3D.of(2, 0, 0)), EPS);
-
- Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(0, 0, 0)));
- Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(-0, 0, 0)));
-
- Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(Double.NaN, 1, 1)));
- Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(1, Double.POSITIVE_INFINITY, 1)));
- Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY)));
- }
-
- @Test
public void testNorm_oneD() {
// act/assert
Assertions.assertEquals(0.0, Vectors.norm(0.0), EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
index b44edfe..bd6f56d 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
@@ -269,15 +269,28 @@ public class Vector1DTest {
checkVector(Vector1D.of(-1).normalize(), -1);
checkVector(Vector1D.of(5).normalize(), 1);
checkVector(Vector1D.of(-5).normalize(), -1);
+
+ checkVector(Vector1D.of(Double.MIN_VALUE).normalize(), 1);
+ checkVector(Vector1D.of(-Double.MIN_VALUE).normalize(), -1);
+
+ checkVector(Vector1D.of(Double.MAX_VALUE).normalize(), 1);
+ checkVector(Vector1D.of(-Double.MAX_VALUE).normalize(), -1);
}
@Test
public void testNormalize_illegalNorm() {
+ // arrange
+ final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
// act/assert
- Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(0.0).normalize());
- Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.NaN).normalize());
- Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.POSITIVE_INFINITY).normalize());
- Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.NEGATIVE_INFINITY).normalize());
+ GeometryTestUtils.assertThrowsWithMessage(Vector1D.ZERO::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector1D.NaN::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector1D.POSITIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector1D.NEGATIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
}
@Test
@@ -291,6 +304,37 @@ public class Vector1DTest {
}
@Test
+ public void testNormalizeOrNull() {
+ // act/assert
+ checkVector(Vector1D.of(100).normalizeOrNull(), 1);
+ checkVector(Vector1D.of(-100).normalizeOrNull(), -1);
+
+ checkVector(Vector1D.of(2).normalizeOrNull(), 1);
+ checkVector(Vector1D.of(-2).normalizeOrNull(), -1);
+
+ checkVector(Vector1D.of(Double.MIN_VALUE).normalizeOrNull(), 1);
+ checkVector(Vector1D.of(-Double.MIN_VALUE).normalizeOrNull(), -1);
+
+ checkVector(Vector1D.of(Double.MAX_VALUE).normalizeOrNull(), 1);
+ checkVector(Vector1D.of(-Double.MAX_VALUE).normalizeOrNull(), -1);
+
+ Assertions.assertNull(Vector1D.ZERO.normalizeOrNull());
+ Assertions.assertNull(Vector1D.NaN.normalizeOrNull());
+ Assertions.assertNull(Vector1D.POSITIVE_INFINITY.normalizeOrNull());
+ Assertions.assertNull(Vector1D.NEGATIVE_INFINITY.normalizeOrNull());
+ }
+
+ @Test
+ public void testNormalizeOrNull_isIdempotent() {
+ // arrange
+ final Vector1D v = Vector1D.of(2).normalizeOrNull();
+
+ // act/assert
+ Assertions.assertSame(v, v.normalizeOrNull());
+ checkVector(v.normalizeOrNull(), 1.0);
+ }
+
+ @Test
public void testNegate() {
// act/assert
checkVector(Vector1D.of(0.1).negate(), -0.1);
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 daec297..e02460a 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
@@ -34,7 +34,6 @@ import org.apache.commons.rng.simple.RandomSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
-
public class Vector3DTest {
private static final double EPS = 1e-15;
@@ -396,16 +395,40 @@ public class Vector3DTest {
checkVector(Vector3D.of(2, 2, 2).normalize(), invSqrt3, invSqrt3, invSqrt3);
checkVector(Vector3D.of(-2, -2, -2).normalize(), -invSqrt3, -invSqrt3, -invSqrt3);
+ checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalize(), 1, 0, 0);
+ checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalize(), 0, 1, 0);
+ checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalize(), 0, 0, 1);
+
+ checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalize(),
+ -invSqrt3, invSqrt3, invSqrt3);
+
+ checkVector(Vector3D.of(Double.MIN_NORMAL, 0, 0).normalize(), 1, 0, 0);
+ checkVector(Vector3D.of(0, Double.MIN_NORMAL, 0).normalize(), 0, 1, 0);
+ checkVector(Vector3D.of(0, 0, Double.MIN_NORMAL).normalize(), 0, 0, 1);
+
+ checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(),
+ invSqrt3, invSqrt3, -invSqrt3);
+
+ checkVector(Vector3D.of(Double.MAX_VALUE, -Double.MAX_VALUE, Double.MAX_VALUE).normalize(),
+ invSqrt3, -invSqrt3, invSqrt3);
+
Assertions.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().norm(), EPS);
}
@Test
public void testNormalize_illegalNorm() {
+ // arrange
+ final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
// act/assert
- Assertions.assertThrows(IllegalArgumentException.class, Vector3D.ZERO::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NaN::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector3D.POSITIVE_INFINITY::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NEGATIVE_INFINITY::normalize);
+ GeometryTestUtils.assertThrowsWithMessage(Vector3D.ZERO::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector3D.NaN::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector3D.POSITIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector3D.NEGATIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
}
@Test
@@ -420,6 +443,48 @@ public class Vector3DTest {
}
@Test
+ public void testNormalizeOrNull() {
+ // arrange
+ final double invSqrt3 = 1 / Math.sqrt(3);
+
+ // act/assert
+ checkVector(Vector3D.of(100, 0, 0).normalizeOrNull(), 1, 0, 0);
+ checkVector(Vector3D.of(-100, 0, 0).normalizeOrNull(), -1, 0, 0);
+
+ checkVector(Vector3D.of(2, 2, 2).normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
+ checkVector(Vector3D.of(-2, -2, -2).normalizeOrNull(), -invSqrt3, -invSqrt3, -invSqrt3);
+
+ checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalizeOrNull(), 1, 0, 0);
+ checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalizeOrNull(), 0, 1, 0);
+ checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalizeOrNull(), 0, 0, 1);
+
+ checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalizeOrNull(),
+ -invSqrt3, invSqrt3, invSqrt3);
+
+ checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(),
+ invSqrt3, invSqrt3, -invSqrt3);
+
+ checkVector(Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(),
+ -invSqrt3, -invSqrt3, -invSqrt3);
+
+ Assertions.assertNull(Vector3D.ZERO.normalizeOrNull());
+ Assertions.assertNull(Vector3D.NaN.normalizeOrNull());
+ Assertions.assertNull(Vector3D.POSITIVE_INFINITY.normalizeOrNull());
+ Assertions.assertNull(Vector3D.NEGATIVE_INFINITY.normalizeOrNull());
+ }
+
+ @Test
+ public void testNormalizeOrNull_isIdempotent() {
+ // arrange
+ final double invSqrt3 = 1 / Math.sqrt(3);
+ final Vector3D v = Vector3D.of(2, 2, 2).normalizeOrNull();
+
+ // act/assert
+ Assertions.assertSame(v, v.normalizeOrNull());
+ checkVector(v.normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
+ }
+
+ @Test
public void testOrthogonal() {
// arrange
final Vector3D v1 = Vector3D.of(0.1, 2.5, 1.3);
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 07eba28..581b11b 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
@@ -317,21 +317,43 @@ public class Vector2DTest {
@Test
public void testNormalize() {
+ // arrange
+ final double invSqrt2 = 1.0 / Math.sqrt(2);
+
// act/assert
checkVector(Vector2D.of(100, 0).normalize(), 1, 0);
checkVector(Vector2D.of(-100, 0).normalize(), -1, 0);
checkVector(Vector2D.of(0, 100).normalize(), 0, 1);
checkVector(Vector2D.of(0, -100).normalize(), 0, -1);
checkVector(Vector2D.of(-1, 2).normalize(), -1.0 / Math.sqrt(5), 2.0 / Math.sqrt(5));
+
+ checkVector(Vector2D.of(Double.MIN_VALUE, 0).normalize(), 1, 0);
+ checkVector(Vector2D.of(0, Double.MIN_VALUE).normalize(), 0, 1);
+
+ checkVector(Vector2D.of(-Double.MIN_VALUE, Double.MIN_VALUE).normalize(), -invSqrt2, invSqrt2);
+
+ checkVector(Vector2D.of(Double.MIN_NORMAL, 0).normalize(), 1, 0, 0);
+ checkVector(Vector2D.of(0, Double.MIN_NORMAL).normalize(), 0, 1, 0);
+
+ checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(), invSqrt2, -invSqrt2);
+
+ checkVector(Vector2D.of(-Double.MAX_VALUE, -Double.MAX_VALUE).normalize(), -invSqrt2, -invSqrt2);
}
@Test
public void testNormalize_illegalNorm() {
+ // arrange
+ final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
// act/assert
- Assertions.assertThrows(IllegalArgumentException.class, Vector2D.ZERO::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NaN::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector2D.POSITIVE_INFINITY::normalize);
- Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NEGATIVE_INFINITY::normalize);
+ GeometryTestUtils.assertThrowsWithMessage(Vector2D.ZERO::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector2D.NaN::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector2D.POSITIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
+ GeometryTestUtils.assertThrowsWithMessage(Vector2D.NEGATIVE_INFINITY::normalize,
+ IllegalArgumentException.class, illegalNorm);
}
@Test
@@ -346,6 +368,44 @@ public class Vector2DTest {
}
@Test
+ public void testNormalizeOrNull() {
+ // arrange
+ final double invSqrt2 = 1 / Math.sqrt(2);
+
+ // act/assert
+ checkVector(Vector2D.of(100, 0).normalizeOrNull(), 1, 0);
+ checkVector(Vector2D.of(-100, 0).normalizeOrNull(), -1, 0);
+
+ checkVector(Vector2D.of(2, 2).normalizeOrNull(), invSqrt2, invSqrt2);
+ checkVector(Vector2D.of(-2, -2).normalizeOrNull(), -invSqrt2, -invSqrt2);
+
+ checkVector(Vector2D.of(Double.MIN_VALUE, 0).normalizeOrNull(), 1, 0);
+ checkVector(Vector2D.of(0, Double.MIN_VALUE).normalizeOrNull(), 0, 1);
+
+ checkVector(Vector2D.of(-Double.MIN_VALUE, -Double.MIN_VALUE).normalizeOrNull(), -invSqrt2, -invSqrt2);
+
+ checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(), invSqrt2, -invSqrt2);
+
+ checkVector(Vector2D.of(Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(), invSqrt2, -invSqrt2);
+
+ Assertions.assertNull(Vector2D.ZERO.normalizeOrNull());
+ Assertions.assertNull(Vector2D.NaN.normalizeOrNull());
+ Assertions.assertNull(Vector2D.POSITIVE_INFINITY.normalizeOrNull());
+ Assertions.assertNull(Vector2D.NEGATIVE_INFINITY.normalizeOrNull());
+ }
+
+ @Test
+ public void testNormalizeOrNull_isIdempotent() {
+ // arrange
+ final double invSqrt2 = 1 / Math.sqrt(2);
+ final Vector2D v = Vector2D.of(2, 2).normalizeOrNull();
+
+ // act/assert
+ Assertions.assertSame(v, v.normalizeOrNull());
+ checkVector(v.normalizeOrNull(), invSqrt2, invSqrt2);
+ }
+
+ @Test
public void testNegate() {
// act/assert
checkVector(Vector2D.of(1, 2).negate(), -1, -2);
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
index 85df71e..65f0a28 100644
--- a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
@@ -305,14 +305,14 @@ public class VectorPerformance {
}
}
- /** Run a benchmark test on a function that produces a vector.
+ /** Run a benchmark test on a function that accepts a vector.
* @param <V> Vector implementation type
* @param input vector input
* @param bh jmh blackhole for consuming output
* @param fn function to call
*/
- private static <V extends Vector<V>> void testUnary(final VectorInputBase<V> input, final Blackhole bh,
- final UnaryOperator<V> fn) {
+ private static <V extends Vector<V>> void testFunction(final VectorInputBase<V> input, final Blackhole bh,
+ final Function<V, ?> fn) {
for (final V vec : input.getVectors()) {
bh.consume(fn.apply(vec));
}
@@ -324,7 +324,7 @@ public class VectorPerformance {
*/
@Benchmark
public void baseline(final VectorInput1D input, final Blackhole bh) {
- testUnary(input, bh, UnaryOperator.identity());
+ testFunction(input, bh, UnaryOperator.identity());
}
/** Benchmark testing the performance of the {@link Vector1D#norm()} method.
@@ -360,7 +360,16 @@ public class VectorPerformance {
*/
@Benchmark
public void normalize1D(final NormalizableVectorInput1D input, final Blackhole bh) {
- testUnary(input, bh, Vector1D::normalize);
+ testFunction(input, bh, Vector1D::normalize);
+ }
+
+ /** Benchmark testing the performance of the {@link Vector1D#normalizeOrNull()} method.
+ * @param input benchmark state input
+ * @param bh jmh blackhole for consuming output
+ */
+ @Benchmark
+ public void normalizeOrNull1D(final VectorInput1D input, final Blackhole bh) {
+ testFunction(input, bh, v -> v.normalizeOrNull());
}
/** Benchmark testing the performance of the {@link Vector2D#normalize()}
@@ -370,7 +379,17 @@ public class VectorPerformance {
*/
@Benchmark
public void normalize2D(final NormalizableVectorInput2D input, final Blackhole bh) {
- testUnary(input, bh, Vector2D::normalize);
+ testFunction(input, bh, Vector2D::normalize);
+ }
+
+ /** Benchmark testing the performance of the {@link Vector2D#normalizeOrNull()}
+ * method.
+ * @param input benchmark state input
+ * @param bh jmh blackhole for consuming output
+ */
+ @Benchmark
+ public void normalizeOrNull2D(final VectorInput2D input, final Blackhole bh) {
+ testFunction(input, bh, v -> v.normalizeOrNull());
}
/** Benchmark testing the performance of the {@link Vector3D#normalize()}
@@ -380,6 +399,16 @@ public class VectorPerformance {
*/
@Benchmark
public void normalize3D(final NormalizableVectorInput3D input, final Blackhole bh) {
- testUnary(input, bh, Vector3D::normalize);
+ testFunction(input, bh, Vector3D::normalize);
+ }
+
+ /** Benchmark testing the performance of the {@link Vector3D#normalizeOrNull()}
+ * method.
+ * @param input benchmark state input
+ * @param bh jmh blackhole for consuming output
+ */
+ @Benchmark
+ public void normalizeOrNull3D(final VectorInput3D input, final Blackhole bh) {
+ testFunction(input, bh, v -> v.normalizeOrNull());
}
}
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
index c2d7031..5b7b452 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
@@ -28,7 +28,6 @@ import java.util.function.IntFunction;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
-import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
@@ -323,7 +322,7 @@ public class PolygonObjParser extends AbstractObjParser {
}
}
- return Vectors.tryNormalize(sum);
+ return sum.normalizeOrNull();
}
/** Compute a normal for the face using its first three vertices. The vertices will wind in a
@@ -340,7 +339,7 @@ public class PolygonObjParser extends AbstractObjParser {
final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex());
final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex());
- return Vectors.tryNormalize(p0.vectorTo(p1).cross(p0.vectorTo(p2)));
+ return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull();
}
/** Get the vertex attributes for the face listed in the order that produces a counter-clockwise
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
index e47505f..2c81b5b 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
@@ -18,7 +18,6 @@ package org.apache.commons.geometry.io.euclidean.threed.stl;
import java.nio.ByteBuffer;
-import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Utility methods for the STL format.
@@ -51,7 +50,7 @@ final class StlUtils {
final Vector3D normal) {
if (normal != null) {
// try to normalize it
- final Vector3D normalized = Vectors.tryNormalize(normal);
+ final Vector3D normalized = normal.normalizeOrNull();
if (normalized != null) {
return normalized;
}
@@ -93,7 +92,7 @@ final class StlUtils {
* @return the normal for the given triangle vertices or null if one could not be computed
*/
private static Vector3D computeTriangleNormal(final Vector3D p1, final Vector3D p2, final Vector3D p3) {
- final Vector3D normal = Vectors.tryNormalize(p1.vectorTo(p2).cross(p1.vectorTo(p3)));
+ final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalizeOrNull();
return normal != null ?
normal :
null;