From c6ced5206aa31137a0beb483ef56c8383681bf36 Mon Sep 17 00:00:00 2001 From: JackieTien97 Date: Sat, 7 Nov 2026 09:51:51 +0800 Subject: [PATCH 1/2] Avoid intermediate overflow in Akima spline derivative weights --- .../AkimaSplineInterpolator.java | 8 +++- .../AkimaSplineInterpolatorTest.java | 48 +++++++++++++++++++ src/changes/changes.xml | 4 ++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java index 3be4dab3d6..2a558d11b6 100644 --- a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java +++ b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java @@ -144,7 +144,13 @@ public PolynomialSplineFunction interpolate(double[] xvals, final double xvM = xvals[i - 1]; firstDerivatives[i] = (((xvP - xv) * differences[i - 1]) + ((xv - xvM) * differences[i])) / (xvP - xvM); } else { - firstDerivatives[i] = ((wP * differences[i - 1]) + (wM * differences[i])) / (wP + wM); + // Normalize before multiplying to avoid overflow or underflow of weighted slopes. + final double weightScale = JdkMath.max(wP, wM); + final double scaledP = wP / weightScale; + final double scaledM = wM / weightScale; + final double scaledSum = scaledP + scaledM; + firstDerivatives[i] = (scaledP / scaledSum) * differences[i - 1] + + (scaledM / scaledSum) * differences[i]; } } diff --git a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java index aea67a9aa9..cd7a604a4d 100644 --- a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java +++ b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java @@ -36,6 +36,54 @@ import org.junit.Ignore; public class AkimaSplineInterpolatorTest { + @Test + public void testLargeFiniteValues() { + checkLargeFiniteValues(false); + } + + @Test + public void testLargeFiniteValuesModified() { + checkLargeFiniteValues(true); + } + + private void checkLargeFiniteValues(boolean useModified) { + final double[] x = {0, 2, 4, 6, 8}; + final double[] y = {0, 1e200, 0, 1e200, 0}; + final double[] expected = {0, 0.75, 1, 0.5, 0, 0.5, 1, 0.75, 0}; + final UnivariateFunction interpolation = new AkimaSplineInterpolator(useModified).interpolate(x, y); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], interpolation.value(i) / 1e200, 1e-14); + } + } + + @Test + public void testScaleInvariance() { + checkScaleInvariance(false); + } + + @Test + public void testScaleInvarianceModified() { + checkScaleInvariance(true); + } + + private void checkScaleInvariance(boolean useModified) { + final double[] x = {0, 2, 4, 6, 8}; + final double[] y = {0, 1, 3, 2, 5}; + final UnivariateFunction reference = new AkimaSplineInterpolator(useModified).interpolate(x, y); + for (double scale : new double[] {1e200, -1e200, 1e-200}) { + final double[] scaledY = new double[y.length]; + for (int i = 0; i < y.length; i++) { + scaledY[i] = y[i] * scale; + } + final UnivariateFunction interpolation = + new AkimaSplineInterpolator(useModified).interpolate(x, scaledY); + for (int i = 0; i <= 16; i++) { + final double point = i * 0.5; + assertEquals(reference.value(point), interpolation.value(point) / scale, 1e-14); + } + } + } + @Test public void testIllegalArguments() { // Data set arrays of different size. diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7f1a77b4b9..443abf996a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -96,6 +96,10 @@ Caveat: to support the whole codebase (it was one of the main reasons for creating more focused components). "> + + "AkimaSplineInterpolator": Normalize derivative weights to avoid + overflow and underflow in intermediate weighted products. + "MathArrays": Fix int overflow in the int[] L1 and Linf distance. From e2d3cf0f0ec918d4c31684cd1a8a1616999eb868 Mon Sep 17 00:00:00 2001 From: JackieTien97 Date: Sat, 7 Nov 2026 10:12:10 +0800 Subject: [PATCH 2/2] Preserve representable contributions from unbalanced Akima weights --- .../AkimaSplineInterpolator.java | 24 +++++++++++++------ .../AkimaSplineInterpolatorTest.java | 11 +++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java index 2a558d11b6..edc1b56128 100644 --- a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java +++ b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolator.java @@ -144,13 +144,23 @@ public PolynomialSplineFunction interpolate(double[] xvals, final double xvM = xvals[i - 1]; firstDerivatives[i] = (((xvP - xv) * differences[i - 1]) + ((xv - xvM) * differences[i])) / (xvP - xvM); } else { - // Normalize before multiplying to avoid overflow or underflow of weighted slopes. - final double weightScale = JdkMath.max(wP, wM); - final double scaledP = wP / weightScale; - final double scaledM = wM / weightScale; - final double scaledSum = scaledP + scaledM; - firstDerivatives[i] = (scaledP / scaledSum) * differences[i - 1] - + (scaledM / scaledSum) * differences[i]; + final double weightedP = wP * differences[i - 1]; + final double weightedM = wM * differences[i]; + final double weightedSum = weightedP + weightedM; + final double weightSum = wP + wM; + if (Double.isFinite(weightedSum) && Double.isFinite(weightSum) && + (weightedP != 0.0 || weightedM != 0.0)) { + // Preserve small contributions when normalizing would underflow a weight. + firstDerivatives[i] = weightedSum / weightSum; + } else { + // Avoid overflow or underflow of the intermediate weighted products. + final double weightScale = JdkMath.max(wP, wM); + final double scaledP = wP / weightScale; + final double scaledM = wM / weightScale; + final double scaledSum = scaledP + scaledM; + firstDerivatives[i] = (scaledP / scaledSum) * differences[i - 1] + + (scaledM / scaledSum) * differences[i]; + } } } diff --git a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java index cd7a604a4d..e472d70212 100644 --- a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java +++ b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/analysis/interpolation/AkimaSplineInterpolatorTest.java @@ -36,6 +36,17 @@ import org.junit.Ignore; public class AkimaSplineInterpolatorTest { + @Test + public void testUnbalancedWeights() { + final double[] x = {0, 2, 4, 6, 8}; + final double[] y = {1e-200, 0, 0, 1e200, 1e200}; + for (boolean useModified : new boolean[] {false, true}) { + final UnivariateFunction interpolation = + new AkimaSplineInterpolator(useModified).interpolate(x, y); + assertEquals(-1.875e-201, interpolation.value(3), 1e-215); + } + } + @Test public void testLargeFiniteValues() { checkLargeFiniteValues(false);