This is an automated email from the ASF dual-hosted git repository.
aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-math.git
The following commit(s) were added to refs/heads/master by this push:
new 73bf53b42 MATH-1678: Avoid Canberra distance overflow (#336)
73bf53b42 is described below
commit 73bf53b42efe71d62be5dd71a1a552be91d1f08c
Author: hutiefang76 <[email protected]>
AuthorDate: Sat Oct 24 05:57:42 2026 -0700
MATH-1678: Avoid Canberra distance overflow (#336)
Large finite coordinates can overflow the denominator and produce
NaN or zero instead of the distance contribution. Scale both
coordinates by one half only when their finite magnitudes overflow,
while preserving ordinary and non-finite input behavior.
Add extreme-value, independent BigDecimal reference, and special-value
regressions.
Generated-by: DeepSeek (deepseek-v4-flash), Codex
---
.../math4/legacy/ml/distance/CanberraDistance.java | 11 ++-
.../legacy/ml/distance/CanberraDistanceTest.java | 80 ++++++++++++++++++++++
src/changes/changes.xml | 3 +
3 files changed, 92 insertions(+), 2 deletions(-)
diff --git
a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistance.java
b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistance.java
index 8f9295bef..d1f82a97c 100644
---
a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistance.java
+++
b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistance.java
@@ -31,9 +31,16 @@ public class CanberraDistance implements DistanceMeasure {
MathArrays.checkEqualLength(a, b);
double sum = 0;
for (int i = 0; i < a.length; i++) {
+ final double absA = JdkMath.abs(a[i]);
+ final double absB = JdkMath.abs(b[i]);
final double num = JdkMath.abs(a[i] - b[i]);
- final double denom = JdkMath.abs(a[i]) + JdkMath.abs(b[i]);
- sum += num == 0.0 && denom == 0.0 ? 0.0 : num / denom;
+ final double denom = absA + absB;
+ if (Double.isInfinite(denom) && Double.isFinite(a[i]) &&
Double.isFinite(b[i])) {
+ // Rescale large finite coordinates to avoid intermediate
overflow.
+ sum += JdkMath.abs(a[i] * 0.5 - b[i] * 0.5) / (absA * 0.5 +
absB * 0.5);
+ } else {
+ sum += denom == 0.0 ? 0.0 : num / denom;
+ }
}
return sum;
}
diff --git
a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistanceTest.java
b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistanceTest.java
index 85384ba11..8b38df937 100644
---
a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistanceTest.java
+++
b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/ml/distance/CanberraDistanceTest.java
@@ -16,6 +16,9 @@
*/
package org.apache.commons.math4.legacy.ml.distance;
+import java.math.BigDecimal;
+import java.math.MathContext;
+
import org.junit.Assert;
import org.junit.Test;
@@ -45,4 +48,81 @@ public class CanberraDistanceTest {
Assert.assertEquals(expected, distance.compute(a, b), 0d);
Assert.assertEquals(expected, distance.compute(b, a), 0d);
}
+
+ @Test
+ public void testExtremeOppositeSigns() {
+ // Both |MAX - (-MAX)| and |MAX| + |-MAX| overflow to infinity: the
true ratio is 1.
+ final double max = Double.MAX_VALUE;
+ Assert.assertEquals(1, distance.compute(new double[] {max}, new
double[] {-max}), 0d);
+ Assert.assertEquals(1, distance.compute(new double[] {-max}, new
double[] {max}), 0d);
+ Assert.assertEquals(1, distance.compute(new double[] {max}, new
double[] {-max / 2}), 0d);
+ Assert.assertEquals(1, distance.compute(new double[] {-max / 2}, new
double[] {max}), 0d);
+ }
+
+ @Test
+ public void testExtremeSameSign() {
+ // |MAX - MAX/2| / (|MAX| + |MAX/2|) == 1/3; only the denominator
overflows.
+ final double max = Double.MAX_VALUE;
+ // 1/3 is not representable, so allow one ulp instead of an exact
match.
+ final double expected = 1.0 / 3.0;
+ final double tol = Math.ulp(expected);
+ Assert.assertEquals(expected, distance.compute(new double[] {max}, new
double[] {max / 2}), tol);
+ Assert.assertEquals(expected, distance.compute(new double[] {max / 2},
new double[] {max}), tol);
+ Assert.assertEquals(expected, distance.compute(new double[] {-max},
new double[] {-max / 2}), tol);
+ Assert.assertEquals(expected, distance.compute(new double[] {-max /
2}, new double[] {-max}), tol);
+ // Identical extreme values have distance 0.
+ Assert.assertEquals(0, distance.compute(new double[] {max}, new
double[] {max}), 0d);
+ Assert.assertEquals(0, distance.compute(new double[] {-max}, new
double[] {-max}), 0d);
+ }
+
+ @Test
+ public void testExtremeVector() {
+ final double max = Double.MAX_VALUE;
+ final double[] a = { max, max / 2, max };
+ final double[] b = { -max, max, max };
+ // 1 + 1/3 + 0
+ Assert.assertEquals(4.0 / 3.0, distance.compute(a, b), 1e-15);
+ Assert.assertEquals(4.0 / 3.0, distance.compute(b, a), 1e-15);
+ }
+
+ @Test
+ public void testExtremeFiniteValuesAgainstBigDecimalReference() {
+ final double max = Double.MAX_VALUE;
+ final double[] a = {max, max, 0.75 * max, max, -max, max};
+ final double[] b = {max / 2, Math.nextDown(max), Math.nextDown(0.75 *
max), -max, -max / 2, max / 16};
+ for (int i = 0; i < a.length; i++) {
+ final double expected = referenceCanberra(a[i], b[i]);
+ assertWithinOneUlp(expected, distance.compute(new double[] {a[i]},
new double[] {b[i]}));
+ assertWithinOneUlp(expected, distance.compute(new double[] {b[i]},
new double[] {a[i]}));
+ }
+ }
+
+ /**
+ * Computes a high-precision reference from the exact decimal
representations of the input doubles.
+ */
+ private static double referenceCanberra(double a, double b) {
+ final BigDecimal bdA = new BigDecimal(a);
+ final BigDecimal bdB = new BigDecimal(b);
+ final BigDecimal num = bdA.subtract(bdB).abs();
+ final BigDecimal den = bdA.abs().add(bdB.abs());
+ return num.divide(den, MathContext.DECIMAL128).doubleValue();
+ }
+
+ private static void assertWithinOneUlp(double expected, double actual) {
+ Assert.assertEquals("expected " + expected + " but was " + actual,
+ expected, actual, Math.ulp(expected));
+ }
+
+ @Test
+ public void testSpecialValuesPreserved() {
+ final double min = Double.MIN_VALUE;
+ // Subnormal ratios are unaffected by the extreme-value handling.
+ Assert.assertEquals(1.0 / 3.0, distance.compute(new double[] {min},
new double[] {2 * min}), 0d);
+ Assert.assertEquals(1, distance.compute(new double[] {min}, new
double[] {-min}), 0d);
+ // Signed zero: the zero/zero convention contributes 0.
+ Assert.assertEquals(0, distance.compute(new double[] {0.0}, new
double[] {-0.0}), 0d);
+ // Infinite and NaN inputs keep their historical NaN result.
+ Assert.assertTrue(Double.isNaN(distance.compute(new double[]
{Double.POSITIVE_INFINITY}, new double[] {1})));
+ Assert.assertTrue(Double.isNaN(distance.compute(new double[]
{Double.NaN}, new double[] {0})));
+ }
}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 077104d34..b111a20cc 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -101,6 +101,9 @@ Caveat:
Instances not cannot be constructed that would silently overwrite
entries
due to key duplication from interger overflow.
</action>
+ <action type="fix" issue="MATH-1678">
+ "CanberraDistance": Avoid overflow for large finite coordinates.
+ </action>
<action dev="aherbert" type="fix" due-to="Lyin-OTTF">
"SynchronizedDescriptiveStatistics": Synchronize all mutators
of the internal state. Updates previously unsynchronized methods