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 2021/06/03 16:03:30 UTC
[commons-numbers] branch master updated: NUMBERS-161: Refactoring
of angle normalization.
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-numbers.git
The following commit(s) were added to refs/heads/master by this push:
new 8e2334a NUMBERS-161: Refactoring of angle normalization.
8e2334a is described below
commit 8e2334ac190892d3aede6edc3a44035d161d5659
Author: Gilles Sadowski <gi...@gmail.com>
AuthorDate: Thu Jun 3 18:01:44 2021 +0200
NUMBERS-161: Refactoring of angle normalization.
---
.../org/apache/commons/numbers/angle/Angle.java | 59 +++++++++++--------
.../apache/commons/numbers/angle/AngleTest.java | 66 ++++++++++++++++------
2 files changed, 84 insertions(+), 41 deletions(-)
diff --git a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
index a582f57..64ea55e 100644
--- a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
+++ b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
@@ -87,6 +87,8 @@ public abstract class Angle implements DoubleSupplier {
public static final class Turn extends Angle {
/** Zero. */
public static final Turn ZERO = Turn.of(0d);
+ /** Normalizing operator (result will be within the {@code [0, 1[} interval). */
+ public static final UnaryOperator<Turn> WITHIN_0_AND_1 = normalizer(of(0d));
/**
* @param angle (in turns).
@@ -140,13 +142,13 @@ public abstract class Angle implements DoubleSupplier {
/**
* Creates an operator for normalizing/reducing an angle.
- * The output will be within the {@code [c - 0.5, c + 0.5[} interval.
+ * The output will be within the {@code [lo, lo + 1[} interval.
*
- * @param c Center.
+ * @param lo Lower bound of the normalized interval.
* @return the normalization operator.
*/
- public static UnaryOperator<Turn> normalizer(Turn c) {
- final Normalizer n = new Normalizer(c.value, 1d);
+ public static UnaryOperator<Turn> normalizer(Turn lo) {
+ final Normalizer n = new Normalizer(lo.value, 1d);
return (Turn a) -> Turn.of(n.applyAsDouble(a.value));
}
}
@@ -161,6 +163,10 @@ public abstract class Angle implements DoubleSupplier {
public static final Rad PI = Rad.of(Math.PI);
/** 2π. */
public static final Rad TWO_PI = Rad.of(2 * Math.PI);
+ /** Normalizing operator (result will be within the <code>[0, 2π[</code> interval). */
+ public static final UnaryOperator<Rad> WITHIN_0_AND_2PI = normalizer(of(0d));
+ /** Normalizing operator (result will be within the <code>[-π, π[</code> interval). */
+ public static final UnaryOperator<Rad> WITHIN_MINUS_PI_AND_PI = normalizer(of(-Math.PI));
/**
* @param angle (in radians).
@@ -214,13 +220,13 @@ public abstract class Angle implements DoubleSupplier {
/**
* Creates an operator for normalizing/reducing an angle.
- * The output will be within the <code> [c - π, c + π[</code> interval.
+ * The output will be within the <code> [lo, lo + 2π[</code> interval.
*
- * @param c Center.
+ * @param lo Lower bound of the normalized interval.
* @return the normalization operator.
*/
- public static UnaryOperator<Rad> normalizer(Rad c) {
- final Normalizer n = new Normalizer(c.value, TURN_TO_RAD);
+ public static UnaryOperator<Rad> normalizer(Rad lo) {
+ final Normalizer n = new Normalizer(lo.value, TURN_TO_RAD);
return (Rad a) -> Rad.of(n.applyAsDouble(a.value));
}
}
@@ -231,6 +237,8 @@ public abstract class Angle implements DoubleSupplier {
public static final class Deg extends Angle {
/** Zero. */
public static final Deg ZERO = Deg.of(0d);
+ /** Normalizing operator (result will be within the {@code [0, 360[} interval). */
+ public static final UnaryOperator<Deg> WITHIN_0_AND_360 = normalizer(of(0d));
/**
* @param angle (in degrees).
@@ -284,13 +292,13 @@ public abstract class Angle implements DoubleSupplier {
/**
* Creates an operator for normalizing/reducing an angle.
- * The output will be within the {@code [c - 180, c + 180[} interval.
+ * The output will be within the {@code [c, c + 360[} interval.
*
- * @param c Center.
+ * @param lo Lower bound of the normalized interval.
* @return the normalization operator.
*/
- public static UnaryOperator<Deg> normalizer(Deg c) {
- final Normalizer n = new Normalizer(c.value, TURN_TO_DEG);
+ public static UnaryOperator<Deg> normalizer(Deg lo) {
+ final Normalizer n = new Normalizer(lo.value, TURN_TO_DEG);
return (Deg a) -> Deg.of(n.applyAsDouble(a.value));
}
}
@@ -300,9 +308,9 @@ public abstract class Angle implements DoubleSupplier {
*/
private static final class Normalizer implements DoubleUnaryOperator {
/** Lower bound. */
- private final double lowerBound;
+ private final double lo;
/** Upper bound. */
- private final double upperBound;
+ private final double hi;
/** Period. */
private final double period;
/** Normalizer. */
@@ -311,27 +319,32 @@ public abstract class Angle implements DoubleSupplier {
/**
* Note: It is assumed that both arguments have the same unit.
*
- * @param center Center of the desired interval.
+ * @param lo Lower bound of the desired interval.
* @param period Circonference of the circle.
*/
- Normalizer(double center,
+ Normalizer(double lo,
double period) {
- final double halfPeriod = 0.5 * period;
this.period = period;
- lowerBound = center - halfPeriod;
- upperBound = center + halfPeriod;
- reduce = new Reduce(lowerBound, period);
+ this.lo = lo;
+ this.hi = lo + period;
+ reduce = new Reduce(lo, period);
}
/**
* @param a Angle.
* @return {@code = a - k} where {@code k} is an integer that satisfies
- * {@code center - 0.5 <= a - k < center + 0.5} (in turns).
+ * {@code lo <= a - k < lo + period}.
*/
@Override
public double applyAsDouble(double a) {
- final double normalized = reduce.applyAsDouble(a) + lowerBound;
- return normalized < upperBound ?
+ if (lo <= a &&
+ a < hi) {
+ // Already within the main interval.
+ return a;
+ }
+
+ final double normalized = reduce.applyAsDouble(a) + lo;
+ return normalized < hi ?
normalized :
// If value is too small to be representable compared to the
// floor expression above (ie, if value + x = x), then we may
diff --git a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
index ae4c9bc..54ac3e3 100644
--- a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
+++ b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
@@ -59,13 +59,14 @@ class AngleTest {
@Test
void testNormalizeRadians() {
+ final double twopi = 2 * Math.PI;
for (double a = -15.0; a <= 15.0; a += 0.1) {
for (double b = -15.0; b <= 15.0; b += 0.2) {
final Angle.Rad aA = Angle.Rad.of(a);
final Angle.Rad aB = Angle.Rad.of(b);
final double c = Angle.Rad.normalizer(aB).apply(aA).getAsDouble();
- Assertions.assertTrue((b - Math.PI) <= c);
- Assertions.assertTrue(c <= (b + Math.PI));
+ Assertions.assertTrue(b <= c);
+ Assertions.assertTrue(c <= b + twopi);
double twoK = Math.rint((a - c) / Math.PI);
Assertions.assertEquals(c, a - twoK * Math.PI, 1e-14);
}
@@ -73,42 +74,42 @@ class AngleTest {
}
@Test
- void testNormalizeAroundZero1() {
+ void testNormalizeAboveZero1() {
final double value = 1.25;
final double expected = 0.25;
- final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+ final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
final double tol = Math.ulp(expected);
Assertions.assertEquals(expected, actual, tol);
}
@Test
- void testNormalizeAroundZero2() {
- final double value = 0.75;
- final double expected = -0.25;
- final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+ void testNormalizeAboveZero2() {
+ final double value = -0.75;
+ final double expected = 0.25;
+ final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
final double tol = Math.ulp(expected);
Assertions.assertEquals(expected, actual, tol);
}
@Test
- void testNormalizeAroundZero3() {
- final double value = 0.5 + 1e-10;
- final double expected = -0.5 + 1e-10;
- final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+ void testNormalizeAboveZero3() {
+ final double value = -0.5 + 1e-10;
+ final double expected = 0.5 + 1e-10;
+ final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
final double tol = Math.ulp(expected);
Assertions.assertEquals(expected, actual, tol);
}
@Test
- void testNormalizeAroundZero4() {
+ void testNormalizeAroundZero() {
final double value = 5 * Math.PI / 4;
final double expected = Math.PI * (1d / 4 - 1);
- final double actual = Angle.Rad.normalizer(Angle.Rad.ZERO).apply(Angle.Rad.of(value)).getAsDouble();
+ final double actual = Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(value)).getAsDouble();
final double tol = Math.ulp(expected);
Assertions.assertEquals(expected, actual, tol);
}
@Test
void testNormalizeUpperAndLowerBounds() {
- final UnaryOperator<Angle.Rad> nZero = Angle.Rad.normalizer(Angle.Rad.ZERO);
- final UnaryOperator<Angle.Rad> nPi = Angle.Rad.normalizer(Angle.Rad.PI);
+ final UnaryOperator<Angle.Rad> nZero = Angle.Rad.WITHIN_MINUS_PI_AND_PI;
+ final UnaryOperator<Angle.Rad> nPi = Angle.Rad.WITHIN_0_AND_2PI;
// act/assert
Assertions.assertEquals(-0.5, nZero.apply(Angle.Turn.of(-0.5).toRad()).toTurn().getAsDouble(), 0d);
@@ -126,8 +127,8 @@ class AngleTest {
@Test
void testNormalizeVeryCloseToBounds() {
- final UnaryOperator<Angle.Rad> nZero = Angle.Rad.normalizer(Angle.Rad.ZERO);
- final UnaryOperator<Angle.Rad> nPi = Angle.Rad.normalizer(Angle.Rad.PI);
+ final UnaryOperator<Angle.Rad> nZero = Angle.Rad.WITHIN_MINUS_PI_AND_PI;
+ final UnaryOperator<Angle.Rad> nPi = Angle.Rad.WITHIN_0_AND_2PI;
// arrange
final double pi = Math.PI;
@@ -177,4 +178,33 @@ class AngleTest {
void testPi() {
Assertions.assertEquals(Math.PI, Angle.Rad.PI.getAsDouble());
}
+
+ @Test
+ void testNormalizeRetainsInputPrecision() {
+ final double aboveZero = Math.nextUp(0);
+ final double belowZero = Math.nextDown(0);
+
+ Assertions.assertEquals(aboveZero,
+ Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(aboveZero)).getAsDouble());
+ Assertions.assertEquals(aboveZero,
+ Angle.Rad.WITHIN_0_AND_2PI.apply(Angle.Rad.of(aboveZero)).getAsDouble());
+
+ Assertions.assertEquals(belowZero,
+ Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(belowZero)).getAsDouble());
+ Assertions.assertEquals(0,
+ Angle.Rad.WITHIN_0_AND_2PI.apply(Angle.Rad.of(belowZero)).getAsDouble());
+ }
+
+ @Test
+ void testNormalizePreciseLowerBound() {
+ final double x = Math.PI / 3;
+ final double above = Math.nextUp(x);
+ final double below = Math.nextDown(x);
+
+ final UnaryOperator<Angle.Rad> normalizer = Angle.Rad.normalizer(Angle.Rad.of(x));
+
+ Assertions.assertEquals(x, normalizer.apply(Angle.Rad.of(x)).getAsDouble());
+ Assertions.assertEquals(above, normalizer.apply(Angle.Rad.of(above)).getAsDouble());
+ // Assertions.assertEquals(below + 2 * Math.PI, normalizer.apply(Angle.Rad.of(below)).getAsDouble());
+ }
}