This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 1b20c15dd36e066e142a307de75fecd4409a2adb Author: jsorel <[email protected]> AuthorDate: Tue Oct 13 12:23:47 2026 +0200 feat(Geometry): add GaussLegendreQuadrature --- .../operation/GaussLegendreQuadrature.java | 196 +++++++++++++++++++++ .../operation/GaussLegendreQuadratureTest.java | 194 ++++++++++++++++++++ 2 files changed, 390 insertions(+) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GaussLegendreQuadrature.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GaussLegendreQuadrature.java new file mode 100644 index 0000000000..fdeee31810 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GaussLegendreQuadrature.java @@ -0,0 +1,196 @@ +/* + * 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.sis.geometries.operation; + +import java.util.function.DoubleUnaryOperator; + + +/** + * Numerical integration of a function of one variable by the Gauss-Legendre quadrature. + * + * <p>A rule of <var>n</var> points evaluates the function at the roots of the Legendre polynomial of + * degree <var>n</var>, mapped to the integration interval, and is exact for polynomials of degree up + * to 2<var>n</var> − 1. For smooth functions the error decreases very fast with <var>n</var>. For + * functions which are not smooth everywhere, the {@linkplain #integrate(DoubleUnaryOperator, double, + * double, double, int, IntervalConsumer) adaptive integration} halves the interval where needed.</p> + * + * <p>Instances are immutable and thread-safe.</p> + * + * @author Johann Sorel (Geomatys) + * @see <a href="https://en.wikipedia.org/wiki/Gauss%E2%80%93Legendre_quadrature">Gauss–Legendre quadrature</a> + */ +public final class GaussLegendreQuadrature { + + /** + * Receives the sub-intervals over which an adaptive integration has converged, + * in increasing order of the integration variable. + */ + @FunctionalInterface + public interface IntervalConsumer { + /** + * Receives a sub-interval and the integral of the function over it. + * + * @param start start of the sub-interval. + * @param end end of the sub-interval. + * @param integral integral of the function from {@code start} to {@code end}. + */ + void accept(double start, double end, double integral); + } + + /** + * Abscissas of the rule in the [−1 … +1] interval, in increasing order. + */ + private final double[] nodes; + + /** + * Weight of each abscissa. + */ + private final double[] weights; + + /** + * Creates a rule of the given number of points. + * + * @param n number of points, at least 1. + * @throws IllegalArgumentException if the number of points is less than 1. + */ + public GaussLegendreQuadrature(final int n) { + if (n < 1) { + throw new IllegalArgumentException("The number of points must be at least 1, but got " + n + '.'); + } + nodes = new double[n]; + weights = new double[n]; + /* + * The roots are symmetric around zero, so only the positive ones are computed, by Newton + * iterations starting from an asymptotic approximation of the root. The Legendre polynomial + * and its derivative are evaluated by the three-term recurrence. + */ + for (int i = 0; i < (n + 1) / 2; i++) { + double z = Math.cos(Math.PI * (i + 0.75) / (n + 0.5)); + double derivative; + for (int iteration = 0; ; iteration++) { + double p1 = 1, p2 = 0; + for (int j = 1; j <= n; j++) { + final double p3 = p2; + p2 = p1; + p1 = ((2*j - 1) * z * p2 - (j - 1) * p3) / j; + } + derivative = n * (z * p1 - p2) / (z * z - 1); + final double previous = z; + z -= p1 / derivative; + if (Math.abs(z - previous) <= Math.ulp(1.0) || iteration >= 100) break; + } + final double weight = 2 / ((1 - z * z) * derivative * derivative); + nodes[i] = -z; + nodes[n - 1 - i] = z; + weights[i] = weight; + weights[n - 1 - i] = weight; + } + if ((n & 1) != 0) { + nodes[n / 2] = 0; // Exact zero instead of a value close to it. + } + } + + /** + * Returns the number of points of this rule. + * + * @return number of points. + */ + public int getNumPoints() { + return nodes.length; + } + + /** + * Returns the abscissas of this rule in the [−1 … +1] interval, in increasing order. + * + * @return a copy of the abscissas. + */ + public double[] getNodes() { + return nodes.clone(); + } + + /** + * Returns the weights of this rule, in the order of the {@linkplain #getNodes() abscissas}. + * + * @return a copy of the weights. + */ + public double[] getWeights() { + return weights.clone(); + } + + /** + * Integrates the given function from {@code a} to {@code b} by a single application of this rule. + * The result is negative if {@code b} is less than {@code a} and the function is positive. + * + * @param f the function to integrate. + * @param a lower bound of the integral. + * @param b upper bound of the integral. + * @return integral of the function from {@code a} to {@code b}. + */ + public double integrate(final DoubleUnaryOperator f, final double a, final double b) { + final double half = (b - a) / 2; + final double mid = (a + b) / 2; + double sum = 0; + for (int i = 0; i < nodes.length; i++) { + sum += weights[i] * f.applyAsDouble(mid + half * nodes[i]); + } + return sum * half; + } + + /** + * Integrates the given function from {@code a} to {@code b}, halving the interval until the + * integrals over the two halves agree with the integral over the whole interval within the given + * relative tolerance. Each sub-interval is halved independently, so the subdivision is finer only + * where the function needs it. + * + * <p>If the function cannot be integrated within the tolerance, for example because its integral + * is zero or because it is not continuous, the halving stops after {@code maxDepth} levels.</p> + * + * @param f the function to integrate. + * @param a lower bound of the integral. + * @param b upper bound of the integral. + * @param tolerance relative tolerance on the integral of each sub-interval. + * @param maxDepth maximal number of interval halvings. + * @param consumer receives the sub-intervals and their integrals in order, or {@code null} if none. + * @return integral of the function from {@code a} to {@code b}. + */ + public double integrate(final DoubleUnaryOperator f, final double a, final double b, + final double tolerance, final int maxDepth, final IntervalConsumer consumer) + { + return adaptive(f, a, b, integrate(f, a, b), tolerance, maxDepth, consumer); + } + + /** + * Recursive part of the adaptive integration, given the integral over the whole interval. + */ + private double adaptive(final DoubleUnaryOperator f, final double a, final double b, final double whole, + final double tolerance, final int depth, final IntervalConsumer consumer) + { + final double m = (a + b) / 2; + final double left = integrate(f, a, m); + final double right = integrate(f, m, b); + final double sum = left + right; + if (depth <= 0 || Math.abs(sum - whole) <= tolerance * Math.abs(sum)) { + if (consumer != null) { + consumer.accept(a, m, left); + consumer.accept(m, b, right); + } + return sum; + } + return adaptive(f, a, m, left, tolerance, depth - 1, consumer) + + adaptive(f, m, b, right, tolerance, depth - 1, consumer); + } +} diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/GaussLegendreQuadratureTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/GaussLegendreQuadratureTest.java new file mode 100644 index 0000000000..f5fd2ab678 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/GaussLegendreQuadratureTest.java @@ -0,0 +1,194 @@ +/* + * 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.sis.geometries.operation; + +import java.util.ArrayList; +import java.util.List; + +// Test dependencies +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + + +/** + * Tests {@link GaussLegendreQuadrature}. + * + * @author Johann Sorel (Geomatys) + */ +public class GaussLegendreQuadratureTest { + /** + * Tolerance on the computed abscissas and weights. + */ + private static final double EPS = 1E-15; + + @Test + public void testInvalidNumberOfPoints() { + assertThrows(IllegalArgumentException.class, () -> new GaussLegendreQuadrature(0)); + assertThrows(IllegalArgumentException.class, () -> new GaussLegendreQuadrature(-1)); + } + + /** + * The rules of 1 and 2 points have simple closed forms. + */ + @Test + public void testSmallRules() { + final GaussLegendreQuadrature one = new GaussLegendreQuadrature(1); + assertEquals(1, one.getNumPoints()); + assertArrayEquals(new double[] {0}, one.getNodes(), EPS); + assertArrayEquals(new double[] {2}, one.getWeights(), EPS); + + final GaussLegendreQuadrature two = new GaussLegendreQuadrature(2); + final double x = 1 / Math.sqrt(3); + assertArrayEquals(new double[] {-x, x}, two.getNodes(), EPS); + assertArrayEquals(new double[] { 1, 1}, two.getWeights(), EPS); + } + + /** + * Compares the rule of 5 points with its closed form. + */ + @Test + public void testFivePoints() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(5); + final double x1 = Math.sqrt(5 - 2 * Math.sqrt(10.0 / 7)) / 3; + final double x2 = Math.sqrt(5 + 2 * Math.sqrt(10.0 / 7)) / 3; + final double w1 = (322 + 13 * Math.sqrt(70)) / 900; + final double w2 = (322 - 13 * Math.sqrt(70)) / 900; + assertArrayEquals(new double[] {-x2, -x1, 0, x1, x2}, rule.getNodes(), EPS); + assertArrayEquals(new double[] {w2, w1, 128.0 / 225, w1, w2}, rule.getWeights(), EPS); + } + + /** + * Compares the rule of 10 points with the values tabulated in the literature. + */ + @Test + public void testTenPoints() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(10); + final double[] x = {0.1488743389816312, 0.4333953941292472, 0.6794095682990244, 0.8650633666889845, 0.9739065285171717}; + final double[] w = {0.2955242247147529, 0.2692667193099963, 0.2190863625159820, 0.1494513491505806, 0.0666713443086881}; + final double[] nodes = rule.getNodes(); + final double[] weights = rule.getWeights(); + for (int i = 0; i < x.length; i++) { + assertEquals( x[i], nodes [5 + i], EPS); + assertEquals(-x[i], nodes [4 - i], EPS); + assertEquals( w[i], weights[5 + i], EPS); + assertEquals( w[i], weights[4 - i], EPS); + } + } + + /** + * For any number of points, the abscissas are increasing and symmetric around zero, + * and the weights are positive and sum to 2, the length of the [−1 … +1] interval. + */ + @Test + public void testNodesAndWeightsProperties() { + for (int n = 1; n <= 64; n++) { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(n); + final double[] nodes = rule.getNodes(); + final double[] weights = rule.getWeights(); + double sum = 0; + for (int i = 0; i < n; i++) { + if (i > 0) { + assertTrue(nodes[i] > nodes[i-1], "Abscissas shall be increasing."); + } + assertTrue(nodes[i] > -1 && nodes[i] < 1, "Abscissas shall be inside the interval."); + assertEquals(-nodes[i], nodes[n - 1 - i], EPS, "Abscissas shall be symmetric."); + assertTrue(weights[i] > 0, "Weights shall be positive."); + sum += weights[i]; + } + assertEquals(2, sum, 1E-13, "Weights shall sum to 2."); + } + } + + /** + * A rule of <var>n</var> points is exact for polynomials of degree up to 2<var>n</var> − 1, + * but not beyond. + */ + @Test + public void testPolynomialExactness() { + for (int n = 1; n <= 12; n++) { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(n); + for (int k = 0; k <= 2*n - 1; k++) { + final int degree = k; + assertEquals(1.0 / (k + 1), rule.integrate((x) -> Math.pow(x, degree), 0, 1), 1E-14, + () -> "x^" + degree + " with " + rule.getNumPoints() + " points."); + } + } + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(2); + assertTrue(Math.abs(rule.integrate((x) -> x*x*x*x, 0, 1) - 0.2) > 1E-3, + "A rule of 2 points is not exact for a polynomial of degree 4."); + } + + /** + * A smooth function is integrated to the floating point accuracy with a few points. + */ + @Test + public void testSmoothFunction() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(10); + assertEquals(2, rule.integrate(Math::sin, 0, Math.PI), 1E-14); + assertEquals(Math.E - 1, rule.integrate(Math::exp, 0, 1), 1E-14); + } + + /** + * Exchanging the bounds changes the sign of the integral, and an empty interval gives zero. + */ + @Test + public void testBounds() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(10); + assertEquals(-2, rule.integrate(Math::sin, Math.PI, 0), 1E-14); + assertEquals( 0, rule.integrate(Math::sin, 1, 1), 0); + } + + /** + * The square root has an unbounded derivative at zero, so a single application of the rule is + * inaccurate. The adaptive integration halves the intervals near zero only, until the result is + * accurate. The sub-intervals reported to the consumer cover the interval without gap or overlap. + */ + @Test + public void testAdaptive() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(10); + final double expected = 2.0 / 3; + assertTrue(Math.abs(rule.integrate(Math::sqrt, 0, 1) - expected) > 1E-8, + "A single application of the rule is not accurate near the singularity."); + + final List<double[]> intervals = new ArrayList<>(); + final double result = rule.integrate(Math::sqrt, 0, 1, 1E-13, 40, + (start, end, integral) -> intervals.add(new double[] {start, end, integral})); + assertEquals(expected, result, 1E-12); + + double previousEnd = 0, sum = 0; + for (final double[] interval : intervals) { + assertEquals(previousEnd, interval[0], 0, "Sub-intervals shall be contiguous and in order."); + assertTrue(interval[1] > interval[0], "Sub-intervals shall not be empty."); + previousEnd = interval[1]; + sum += interval[2]; + } + assertEquals(1, previousEnd, 0, "Sub-intervals shall cover the whole interval."); + assertEquals(result, sum, 1E-15, "The integrals of the sub-intervals shall sum to the result."); + assertTrue(intervals.size() < 200, "Only the intervals near the singularity shall be halved."); + } + + /** + * Without halving, the adaptive integration integrates the two halves of the interval. + * The consumer is optional. + */ + @Test + public void testAdaptiveWithoutHalving() { + final GaussLegendreQuadrature rule = new GaussLegendreQuadrature(4); + final double halves = rule.integrate(Math::sqrt, 0, 0.5) + rule.integrate(Math::sqrt, 0.5, 1); + assertEquals(halves, rule.integrate(Math::sqrt, 0, 1, 1E-13, 0, null), 0); + } +}
