-
Notifications
You must be signed in to change notification settings - Fork 360
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MATH-1650: Clamped spline interpolation (WIP).
- Loading branch information
Gilles Sadowski
committed
Jan 8, 2025
1 parent
e016130
commit 73ce32d
Showing
4 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
161 changes: 161 additions & 0 deletions
161
...ava/org/apache/commons/math4/legacy/analysis/interpolation/ClampedSplineInterpolator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* | ||
* 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.math4.legacy.analysis.interpolation; | ||
|
||
import org.apache.commons.math4.legacy.analysis.polynomials.PolynomialFunction; | ||
import org.apache.commons.math4.legacy.analysis.polynomials.PolynomialSplineFunction; | ||
import org.apache.commons.math4.legacy.core.MathArrays; | ||
import org.apache.commons.math4.legacy.exception.DimensionMismatchException; | ||
import org.apache.commons.math4.legacy.exception.NonMonotonicSequenceException; | ||
import org.apache.commons.math4.legacy.exception.NumberIsTooSmallException; | ||
import org.apache.commons.math4.legacy.exception.util.LocalizedFormats; | ||
|
||
/** | ||
* Clamped cubic spline interpolator. | ||
* The interpolating function consists in cubic polynomial functions defined over the | ||
* subintervals determined by the "knot points". | ||
* | ||
* The interpolating polynomials satisfy: | ||
* <ol> | ||
* <li>The value of the interpolating function at each of the input {@code x} values | ||
* equals the corresponding input {@code y} value.</li> | ||
* <li>Adjacent polynomials are equal through two derivatives at the knot points | ||
* (i.e., adjacent polynomials "match up" at the knot points, as do their first and | ||
* second derivatives).</li> | ||
* <li>The clamped boundary condition, i.e. the interpolating function takes "a | ||
* specific direction" at both its start point and its end point by providing the | ||
* desired first derivative values (slopes) as function parameters to | ||
* {@link #interpolate(double[], double[], double, double)}.</li> | ||
* </ol> | ||
* | ||
* The algorithm is implemented as described in | ||
* <blockquote> | ||
* R.L. Burden, J.D. Faires, | ||
* <em>Numerical Analysis</em>, 9th Ed., 2010, Cengage Learning, ISBN 0-538-73351-9, pp 153-156. | ||
* </blockquote> | ||
* | ||
*/ | ||
public class ClampedSplineInterpolator implements UnivariateInterpolator { | ||
/** | ||
* | ||
* The first derivatives evaluated at the first and last knot points are | ||
* approximated from a natural/unclamped spline that passes through the same | ||
* set of points. | ||
* | ||
* @param x Arguments for the interpolation points. | ||
* @param y Values for the interpolation points. | ||
* @return the interpolating function. | ||
* @throws DimensionMismatchException if {@code x} and {@code y} have different sizes. | ||
* @throws NumberIsTooSmallException if the size of {@code x < 3}. | ||
* @throws NonMonotonicSequenceException if {@code x} is not sorted in strict increasing order. | ||
*/ | ||
@Override | ||
public PolynomialSplineFunction interpolate(final double[] x, | ||
final double[] y) { | ||
final SplineInterpolator spliner = new SplineInterpolator(); | ||
final PolynomialSplineFunction spline = spliner.interpolate(x, y); | ||
final PolynomialSplineFunction derivativeSpline = spline.polynomialSplineDerivative(); | ||
final double fpStart = derivativeSpline.value(x[0]); | ||
final double fpEnd = derivativeSpline.value(x[x.length - 1]); | ||
|
||
return this.interpolate(x, y, fpStart, fpEnd); | ||
} | ||
|
||
/** | ||
* Computes an interpolating function for the data set with defined | ||
* boundary conditions. | ||
* | ||
* @param x Arguments for the interpolation points. | ||
* @param y Values for the interpolation points. | ||
* @param fpStart First derivative at the starting point of the returned | ||
* spline function (starting slope). | ||
* @param fpEnd First derivative at the ending point of the returned | ||
* spline function (ending slope). | ||
* @return the interpolating function. | ||
* @throws DimensionMismatchException if {@code x} and {@code y} have different sizes. | ||
* @throws NumberIsTooSmallException if the size of {@code x < 3}. | ||
* @throws NonMonotonicSequenceException if {@code x} is not sorted in strict increasing order. | ||
*/ | ||
public PolynomialSplineFunction interpolate(final double[] x, | ||
final double[] y, | ||
final double fpStart, | ||
final double fpEnd) { | ||
if (x.length != y.length) { | ||
throw new DimensionMismatchException(x.length, y.length); | ||
} | ||
|
||
if (x.length < 3) { | ||
throw new NumberIsTooSmallException(LocalizedFormats.NUMBER_OF_POINTS, | ||
x.length, 3, true); | ||
} | ||
|
||
// Number of intervals. The number of data points is n + 1. | ||
final int n = x.length - 1; | ||
|
||
MathArrays.checkOrder(x); | ||
|
||
// Differences between knot points. | ||
final double[] h = new double[n]; | ||
for (int i = 0; i < n; i++) { | ||
h[i] = x[i + 1] - x[i]; | ||
} | ||
|
||
final double[] mu = new double[n]; | ||
final double[] z = new double[n + 1]; | ||
|
||
final double alpha0 = 3d * (y[1] - y[0]) / h[0] - 3d * fpStart; | ||
final double alphaN = 3d * fpEnd - 3d * (y[n] - y[n - 1]) / h[n - 1]; | ||
|
||
mu[0] = 0.5d; | ||
final double ell0 = 2d * h[0]; | ||
z[0] = alpha0 / ell0; | ||
|
||
for (int i = 1; i < n; i++) { | ||
final double alpha = (3d / h[i]) * (y[i + 1] - y[i]) - (3d / h[i - 1]) * (y[i] - y[i - 1]); | ||
final double ell = 2d * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1]; | ||
mu[i] = h[i] / ell; | ||
z[i] = (alpha - h[i - 1] * z[i - 1]) / ell; | ||
} | ||
|
||
// Cubic spline coefficients. | ||
final double[] b = new double[n]; // Linear. | ||
final double[] c = new double[n + 1]; // Quadratic. | ||
final double[] d = new double[n]; // Cubic. | ||
|
||
final double ellN = h[n - 1] * (2d - mu[n - 1]); | ||
z[n] = (alphaN - h[n - 1] * z[n - 1]) / ellN; | ||
c[n] = z[n]; | ||
|
||
for (int j = n - 1; j >= 0; j--) { | ||
c[j] = z[j] - mu[j] * c[j + 1]; | ||
b[j] = ((y[j + 1] - y[j]) / h[j]) - h[j] * (c[j + 1] + 2d * c[j]) / 3d; | ||
d[j] = (c[j + 1] - c[j]) / (3d * h[j]); | ||
} | ||
|
||
final PolynomialFunction[] polynomials = new PolynomialFunction[n]; | ||
final double[] coefficients = new double[4]; | ||
for (int i = 0; i < n; i++) { | ||
coefficients[0] = y[i]; | ||
coefficients[1] = b[i]; | ||
coefficients[2] = c[i]; | ||
coefficients[3] = d[i]; | ||
polynomials[i] = new PolynomialFunction(coefficients); | ||
} | ||
|
||
return new PolynomialSplineFunction(x, polynomials); | ||
} | ||
} |
176 changes: 176 additions & 0 deletions
176
...org/apache/commons/math4/legacy/analysis/interpolation/ClampedSplineInterpolatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/* | ||
* 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.math4.legacy.analysis.interpolation; | ||
|
||
import org.apache.commons.math4.legacy.TestUtils; | ||
import org.apache.commons.math4.legacy.analysis.UnivariateFunction; | ||
import org.apache.commons.math4.legacy.analysis.integration.SimpsonIntegrator; | ||
import org.apache.commons.math4.legacy.analysis.polynomials.PolynomialFunction; | ||
import org.apache.commons.math4.legacy.analysis.polynomials.PolynomialSplineFunction; | ||
import org.apache.commons.math4.legacy.exception.DimensionMismatchException; | ||
import org.apache.commons.math4.legacy.exception.NonMonotonicSequenceException; | ||
import org.apache.commons.math4.legacy.exception.NumberIsTooSmallException; | ||
import org.junit.Assert; | ||
import org.junit.Test; | ||
|
||
/** | ||
* Test the ClampedSplineInterpolator. | ||
*/ | ||
public class ClampedSplineInterpolatorTest { | ||
/** Error tolerance for spline interpolator value at knot points. */ | ||
private static final double KNOT_TOL = 1e-14; | ||
/** Error tolerance for interpolating polynomial coefficients. */ | ||
private static final double COEF_TOL = 1e-14; | ||
|
||
@Test | ||
public void testInterpolateLinearDegenerateTwoSegment() { | ||
final double[] x = {0, 0.5, 1}; | ||
final double[] y = {1, Math.exp(0.5), Math.exp(1)}; | ||
final double fpo = 1; | ||
final double fpn = Math.exp(1); | ||
final ClampedSplineInterpolator i = new ClampedSplineInterpolator(); | ||
final PolynomialSplineFunction f = i.interpolate(x, y, fpo, fpn); | ||
verifyInterpolation(f, x, y); | ||
verifyConsistency(f, x); | ||
|
||
// Verify coefficients using analytical values | ||
final PolynomialFunction[] polynomials = f.getPolynomials(); | ||
|
||
final double[] target0 = {1, 1, 0.4889506772539256, 0.21186881109317435}; | ||
final double[] target1 = {1.6487212707001282, 1.6478522855738063, 0.8067538938936871, 0.35156753198873575}; | ||
TestUtils.assertEquals(polynomials[0].getCoefficients(), target0, COEF_TOL); | ||
TestUtils.assertEquals(polynomials[1].getCoefficients(), target1, COEF_TOL); | ||
} | ||
|
||
@Test | ||
public void testInterpolateLinearDegenerateThreeSegment() { | ||
final double[] x = {0, 1, 2, 3}; | ||
final double[] y = {1, Math.exp(1), Math.exp(2), Math.exp(3)}; | ||
final double fpo = 1; | ||
final double fpn = Math.exp(3); | ||
final ClampedSplineInterpolator i = new ClampedSplineInterpolator(); | ||
final PolynomialSplineFunction f = i.interpolate(x, y, fpo, fpn); | ||
verifyInterpolation(f, x, y); | ||
verifyConsistency(f, x); | ||
|
||
// Verify coefficients using analytical values | ||
final PolynomialFunction[] polynomials = f.getPolynomials(); | ||
|
||
final double[] target0 = {1, 0.9999999999999999, 0.4446824969658283, 0.27359933149321697}; | ||
final double[] target1 = {2.718281828459045, 2.710162988411307, 1.2654804914454791, 0.6951307906148195}; | ||
final double[] target2 = {7.38905609893065, 7.326516343146723, 3.3508728632899376, 2.019091617820356}; | ||
TestUtils.assertEquals(polynomials[0].getCoefficients(), target0, COEF_TOL); | ||
TestUtils.assertEquals(polynomials[1].getCoefficients(), target1, COEF_TOL); | ||
TestUtils.assertEquals(polynomials[2].getCoefficients(), target2, COEF_TOL); | ||
} | ||
|
||
@Test(expected=DimensionMismatchException.class) | ||
public void testArrayLengthMismatch() { | ||
// Data set arrays of different size. | ||
new ClampedSplineInterpolator().interpolate(new double[] {1, 2, 3, 4}, | ||
new double[] {2, 3, 5}, | ||
2, 1); | ||
} | ||
@Test(expected=NonMonotonicSequenceException.class) | ||
public void testUnsortedArray() { | ||
// Knot values not sorted. | ||
new ClampedSplineInterpolator().interpolate(new double[] {1, 3, 2 }, | ||
new double[] {2, 3, 5}, | ||
2, 1); | ||
} | ||
@Test(expected=NumberIsTooSmallException.class) | ||
public void testInsufficientData() { | ||
// Not enough data to interpolate. | ||
new ClampedSplineInterpolator().interpolate(new double[] {1, 2 }, | ||
new double[] {2, 3}, | ||
2, 1); | ||
} | ||
|
||
/** | ||
* Verifies that a clamped spline <i>without</i> specified boundary conditions behaves similar to a natural | ||
* ("unclamped") spline. | ||
* | ||
* <p> | ||
* Using the exponential function <code>e<sup>x</sup></code> over the interval <code>[0, 3]</code>, the test | ||
* evaluates: | ||
* <ol> | ||
* <li>The integral of a clamped spline with specified boundary conditions (endpoint slopes/derivatives).</li> | ||
* <li>The integral of a clamped spline without specified boundary conditions.</li> | ||
* <li>The integral of a natural spline (without boundary conditions by default).</li> | ||
* </ol> | ||
* | ||
* These integrals are compared against the direct integral of the function <code>e<sup>x</sup></code> over the same | ||
* interval.</p> | ||
* | ||
* <p> | ||
* This test is based on "Example 4" in R.L. Burden, J.D. Faires, | ||
* <u>Numerical Analysis</u>, 9th Ed., 2010, Cengage Learning, ISBN 0-538-73351-9, pp 156-157. | ||
* </p> | ||
*/ | ||
@Test | ||
public void testIntegral() { | ||
final double[] x = {0, 1, 2, 3}; | ||
final double[] y = {1, Math.exp(1), Math.exp(2), Math.exp(3)}; | ||
final double fpo = 1; | ||
final double fpn = Math.exp(3); | ||
|
||
final ClampedSplineInterpolator clampedSplineInterpolator = new ClampedSplineInterpolator(); | ||
final PolynomialSplineFunction clampedSpline = clampedSplineInterpolator.interpolate(x, y, fpo, fpn); | ||
final PolynomialSplineFunction clampedSplineAsNaturalSpline = clampedSplineInterpolator.interpolate(x, y); | ||
|
||
final SplineInterpolator naturalSplineInterpolator = new SplineInterpolator(); | ||
final PolynomialSplineFunction naturalSpline = naturalSplineInterpolator.interpolate(x, y); | ||
|
||
final SimpsonIntegrator integrator = new SimpsonIntegrator(); | ||
|
||
final double clampedSplineIntegral = integrator.integrate(1000, clampedSpline, 0, 3); | ||
final double clampedSplineAsNaturalSplineIntegral = integrator.integrate(1000, clampedSplineAsNaturalSpline, 0, 3); | ||
final double naturalSplineIntegral = integrator.integrate(1000, naturalSpline, 0, 3); | ||
final double exponentialFunctionIntegral = integrator.integrate(1000, (arg) -> Math.exp(arg), 0, 3); | ||
|
||
Assert.assertEquals(Math.abs(clampedSplineAsNaturalSplineIntegral - naturalSplineIntegral), 0, 0); | ||
Assert.assertEquals(Math.abs(exponentialFunctionIntegral - clampedSplineIntegral), 0.02589, 0.1); | ||
Assert.assertEquals(Math.abs(exponentialFunctionIntegral - naturalSplineIntegral), 0.46675, 0.1); | ||
} | ||
|
||
/** | ||
* Verifies that f(x[i]) = y[i] for i = 0, ..., n-1 (where n is common length). | ||
*/ | ||
private void verifyInterpolation(PolynomialSplineFunction f, | ||
double[] x, double[] y) { | ||
for (int i = 0; i < x.length; i++) { | ||
Assert.assertEquals(f.value(x[i]), y[i], KNOT_TOL); | ||
} | ||
} | ||
|
||
/** | ||
* Verifies that interpolating polynomials satisfy consistency requirement: adjacent polynomials must agree through | ||
* two derivatives at knot points. | ||
*/ | ||
private void verifyConsistency(PolynomialSplineFunction f, | ||
double[] x) { | ||
PolynomialFunction polynomials[] = f.getPolynomials(); | ||
for (int i = 1; i < x.length - 2; i++) { | ||
// evaluate polynomials and derivatives at x[i + 1] | ||
Assert.assertEquals(polynomials[i].value(x[i + 1] - x[i]), polynomials[i + 1].value(0), 0.1); | ||
Assert.assertEquals(polynomials[i].polynomialDerivative().value(x[i + 1] - x[i]), | ||
polynomials[i + 1].polynomialDerivative().value(0), 0.5); | ||
Assert.assertEquals(polynomials[i].polynomialDerivative().polynomialDerivative().value(x[i + 1] - x[i]), | ||
polynomials[i + 1].polynomialDerivative().polynomialDerivative().value(0), 0.5); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters