View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.math4.legacy.analysis.integration;
18  
19  import org.apache.commons.math4.legacy.analysis.QuinticFunction;
20  import org.apache.commons.math4.legacy.analysis.UnivariateFunction;
21  import org.apache.commons.math4.legacy.analysis.function.Identity;
22  import org.apache.commons.math4.legacy.analysis.function.Inverse;
23  import org.apache.commons.math4.legacy.analysis.function.Sin;
24  import org.apache.commons.math4.legacy.exception.NumberIsTooLargeException;
25  import org.apache.commons.math4.legacy.exception.NumberIsTooSmallException;
26  import org.apache.commons.math4.core.jdkmath.JdkMath;
27  import org.junit.Assert;
28  import org.junit.Test;
29  
30  
31  /**
32   * Test case for Simpson integrator.
33   * <p>
34   * Test runs show that for a default relative accuracy of 1E-6, it
35   * generally takes 5 to 10 iterations for the integral to converge.
36   *
37   */
38  public final class SimpsonIntegratorTest {
39      private static final int SIMPSON_MAX_ITERATIONS_COUNT = 30;
40  
41      /**
42       * Test of integrator for the sine function.
43       */
44      @Test
45      public void testSinFunction() {
46          UnivariateFunction f = new Sin();
47          UnivariateIntegrator integrator = new SimpsonIntegrator();
48          double min;
49          double max;
50          double expected;
51          double result;
52          double tolerance;
53  
54          min = 0; max = JdkMath.PI; expected = 2;
55          tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
56          result = integrator.integrate(1000, f, min, max);
57          Assert.assertTrue(integrator.getEvaluations() < 100);
58          Assert.assertTrue(integrator.getIterations()  < 10);
59          Assert.assertEquals(expected, result, tolerance);
60  
61          min = -JdkMath.PI/3; max = 0; expected = -0.5;
62          tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
63          result = integrator.integrate(1000, f, min, max);
64          Assert.assertTrue(integrator.getEvaluations() < 50);
65          Assert.assertTrue(integrator.getIterations()  < 10);
66          Assert.assertEquals(expected, result, tolerance);
67      }
68  
69      /**
70       * Test of integrator for the quintic function.
71       */
72      @Test
73      public void testQuinticFunction() {
74          UnivariateFunction f = new QuinticFunction();
75          UnivariateIntegrator integrator = new SimpsonIntegrator();
76          double min;
77          double max;
78          double expected;
79          double result;
80          double tolerance;
81  
82          min = 0; max = 1; expected = -1.0/48;
83          tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
84          result = integrator.integrate(1000, f, min, max);
85          Assert.assertTrue(integrator.getEvaluations() < 150);
86          Assert.assertTrue(integrator.getIterations()  < 10);
87          Assert.assertEquals(expected, result, tolerance);
88  
89          min = 0; max = 0.5; expected = 11.0/768;
90          tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
91          result = integrator.integrate(1000, f, min, max);
92          Assert.assertTrue(integrator.getEvaluations() < 100);
93          Assert.assertTrue(integrator.getIterations()  < 10);
94          Assert.assertEquals(expected, result, tolerance);
95  
96          min = -1; max = 4; expected = 2048/3.0 - 78 + 1.0/48;
97          tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
98          result = integrator.integrate(1000, f, min, max);
99          Assert.assertTrue(integrator.getEvaluations() < 150);
100         Assert.assertTrue(integrator.getIterations()  < 10);
101         Assert.assertEquals(expected, result, tolerance);
102     }
103 
104     /**
105      * Test of parameters for the integrator.
106      */
107     @Test
108     public void testParameters() {
109         UnivariateFunction f = new Sin();
110         try {
111             // bad interval
112             new SimpsonIntegrator().integrate(1000, f, 1, -1);
113             Assert.fail("Expecting NumberIsTooLargeException - bad interval");
114         } catch (NumberIsTooLargeException ex) {
115             // expected
116         }
117         try {
118             // bad iteration limits
119             new SimpsonIntegrator(5, 4);
120             Assert.fail("Expecting NumberIsTooSmallException - bad iteration limits");
121         } catch (NumberIsTooSmallException ex) {
122             // expected
123         }
124         try {
125             // bad iteration limits
126             new SimpsonIntegrator(10, SIMPSON_MAX_ITERATIONS_COUNT + 1);
127             Assert.fail("Expecting NumberIsTooLargeException - bad iteration limits");
128         } catch (NumberIsTooLargeException ex) {
129             // expected
130         }
131     }
132 
133     // Tests for MATH-1458:
134     // The SimpsonIntegrator had the following bugs:
135     // - minimalIterationCount==1 results in no possible iteration
136     // - minimalIterationCount==1 computes incorrect Simpson sum (following no iteration)
137     // - minimalIterationCount>1 computes the first iteration sum as the Trapezoid sum
138     // - minimalIterationCount>1 computes the second iteration sum as the first Simpson sum
139 
140     /**
141      * Test iteration is possible when minimalIterationCount==1.
142      * <br/>
143      * MATH-1458: No iterations were performed when minimalIterationCount==1.
144      */
145     @Test
146     public void testIterationIsPossibleWhenMinimalIterationCountIs1() {
147         UnivariateFunction f = new Sin();
148         UnivariateIntegrator integrator = new SimpsonIntegrator(1, SIMPSON_MAX_ITERATIONS_COUNT);
149         // The range or result is not relevant.
150         // This sum should not converge at 1 iteration.
151         // This tests iteration occurred.
152         integrator.integrate(1000, f, 0, 1);
153         // MATH-1458: No iterations were performed when minimalIterationCount==1
154         Assert.assertTrue("Iteration is not above 1",
155                 integrator.getIterations() > 1);
156     }
157 
158     /**
159      * Test convergence at iteration 1 when minimalIterationCount==1.
160      * <br/>
161      * MATH-1458: No iterations were performed when minimalIterationCount==1.
162      */
163     @Test
164     public void testConvergenceIsPossibleAtIteration1() {
165         // A linear function y=x should converge immediately
166         UnivariateFunction f = new Identity();
167         UnivariateIntegrator integrator = new SimpsonIntegrator(1, SIMPSON_MAX_ITERATIONS_COUNT);
168 
169         double min;
170         double max;
171         double expected;
172         double result;
173         double tolerance;
174 
175         min = 0; max = 1; expected = 0.5;
176         tolerance = JdkMath.abs(expected * integrator.getRelativeAccuracy());
177         result = integrator.integrate(1000, f, min, max);
178         // MATH-1458: No iterations were performed when minimalIterationCount==1
179         Assert.assertTrue("Iteration is not above 0",
180                 integrator.getIterations()  > 0);
181         // This should converge immediately
182         Assert.assertEquals("Iteration", integrator.getIterations(), 1);
183         Assert.assertEquals("Result", expected, result, tolerance);
184     }
185 
186     /**
187      * Compute the integral using the composite Simpson's rule.
188      *
189      * @param f the function
190      * @param a the lower limit
191      * @param b the upper limit
192      * @param n the number of intervals (must be even)
193      * @return the integral between a and b
194      * @see <a href="https://en.wikipedia.org/wiki/Simpson%27s_rule#Composite_Simpson's_rule">
195      *       Composite_Simpson's_rule</a>
196      */
197     private static double compositeSimpsonsRule(UnivariateFunction f, double a,
198             double b, int n) {
199         // Sum interval [a,b] split into n subintervals, with n an even number:
200         // sum ~ h/3 * [ f(x0) + 4f(x1) + 2f(x2) + 4f(x3) + 2f(x4) ... + 4f(xn-1) + f(xn) ]
201         // h = (b-a)/n
202         // f(xi) = f(a + i*h)
203         assert n > 0 && (n & 1) == 0 : "n must be strictly positive and even";
204         final double h = (b - a) / n;
205         double sum4 = 0;
206         double sum2 = 0;
207         for (int i = 1; i < n; i++) {
208             // Alternate sums that are multiplied by 4 and 2
209             final double fxi = f.value(a + i * h);
210             if ((i & 1) == 0) {
211                 sum2 += fxi;
212             } else {
213                 sum4 += fxi;
214             }
215         }
216         return (h / 3) * (f.value(a) + 4 * sum4 + 2 * sum2 + f.value(b));
217     }
218 
219     /**
220      * Compute the iteration of Simpson's rule.
221      *
222      * @param f the function
223      * @param a the lower limit
224      * @param b the upper limit
225      * @param iteration the refinement iteration
226      * @return the integral between a and b
227      */
228     private static double computeSimpsonIteration(UnivariateFunction f, double a,
229             double b, int iteration) {
230         // The first possible Simpson's sum uses n=2.
231         // The next uses n=4. This is the 1st refinement expected when the
232         // integrator has performed 1 iteration.
233         final int n = 2 << iteration;
234         return compositeSimpsonsRule(f, a, b, n);
235     }
236 
237     /**
238      * Test the reference Simpson integration is doing what is expected
239      */
240     @Test
241     public void testReferenceSimpsonItegrationIsCorrect() {
242         UnivariateFunction f = new Sin();
243 
244         double a;
245         double b;
246         double h;
247         double expected;
248         double result;
249         double tolerance;
250 
251         a = 0.5;
252         b = 1;
253 
254         double b_a = b - a;
255 
256         // First Simpson sum. 1 midpoint evaluation:
257         h = b_a / 2;
258         double f00 = f.value(a);
259         double f01 = f.value(a + 1 * h);
260         double f0n = f.value(b);
261         expected = (b_a / 6) * (f00 + 4 * f01 + f0n);
262         tolerance = JdkMath.abs(expected * SimpsonIntegrator.DEFAULT_RELATIVE_ACCURACY);
263         result = computeSimpsonIteration(f, a, b, 0);
264         Assert.assertEquals("Result", expected, result, tolerance);
265 
266         // Second Simpson sum: 2 more evaluations:
267         h = b_a / 4;
268         double f11 = f.value(a + 1 * h);
269         double f13 = f.value(a + 3 * h);
270         expected = (h / 3) * (f00 + 4 * f11 + 2 * f01 + 4 * f13 + f0n);
271         tolerance = JdkMath.abs(expected * SimpsonIntegrator.DEFAULT_RELATIVE_ACCURACY);
272         result = computeSimpsonIteration(f, a, b, 1);
273         Assert.assertEquals("Result", expected, result, tolerance);
274 
275         // Third Simpson sum: 4 more evaluations:
276         h = b_a / 8;
277         double f21 = f.value(a + 1 * h);
278         double f23 = f.value(a + 3 * h);
279         double f25 = f.value(a + 5 * h);
280         double f27 = f.value(a + 7 * h);
281         expected = (h / 3) * (f00 + 4 * f21 + 2 * f11 + 4 * f23 + 2 * f01 + 4 * f25 +
282                 2 * f13 + 4 * f27 + f0n);
283         tolerance = JdkMath.abs(expected * SimpsonIntegrator.DEFAULT_RELATIVE_ACCURACY);
284         result = computeSimpsonIteration(f, a, b, 2);
285         Assert.assertEquals("Result", expected, result, tolerance);
286     }
287 
288     /**
289      * Test iteration 1 returns the expected sum when minimalIterationCount==1.
290      * <br/>
291      * MATH-1458: minimalIterationCount==1 computes incorrect Simpson sum
292      * (following no iteration).
293      */
294     @Test
295     public void testIteration1ComputesTheExpectedSimpsonSum() {
296         UnivariateFunction f = new Sin();
297         // Set convergence criteria to force immediate convergence
298         UnivariateIntegrator integrator = new SimpsonIntegrator(
299                 0, Double.POSITIVE_INFINITY,
300                 1, SIMPSON_MAX_ITERATIONS_COUNT);
301         double min;
302         double max;
303         double expected;
304         double result;
305         double tolerance;
306 
307         // MATH-1458: minimalIterationCount==1 computes incorrect
308         // Simpson sum (following no iteration)
309         min = 0;
310         max = 1;
311         result = integrator.integrate(1000, f, min, max);
312         // Immediate convergence
313         Assert.assertEquals("Iteration", 1, integrator.getIterations());
314 
315         // Check the sum is as expected
316         expected = computeSimpsonIteration(f, min, max, 1);
317         tolerance = JdkMath.abs(expected * SimpsonIntegrator.DEFAULT_RELATIVE_ACCURACY);
318         Assert.assertEquals("Result", expected, result, tolerance);
319     }
320 
321     /**
322      * Test iteration N returns the expected sum when minimalIterationCount==1.
323      * <br/>
324      * MATH-1458: minimalIterationCount>1 computes the second iteration sum as
325      * the first Simpson sum.
326      */
327     @Test
328     public void testIterationNComputesTheExpectedSimpsonSum() {
329         // Use 1/x as the function as the sum will asymptote in a monotonic
330         // series. The convergence can then be controlled.
331         UnivariateFunction f = new Inverse();
332 
333         double min;
334         double max;
335         double expected;
336         double result;
337         double tolerance;
338         int minIteration;
339         int maxIteration;
340 
341         // Range for integration
342         min = 1;
343         max = 2;
344 
345         // This is the expected sum.
346         // Each iteration will monotonically converge to this.
347         expected = JdkMath.log(max) - JdkMath.log(min);
348 
349         // Test convergence at the given iteration
350         minIteration = 2;
351         maxIteration = 4;
352 
353         // Compute the sums expected for different iterations.
354         // Add an additional sum so that the test can compare to the next value.
355         double[] sums = new double[maxIteration + 2];
356         for (int i = 0; i < sums.length; i++) {
357             sums[i] = computeSimpsonIteration(f, min, max, i);
358             // Check monotonic
359             if (i > 0) {
360                 Assert.assertTrue("Expected series not monotonic descending",
361                         sums[i] < sums[i - 1]);
362                 // Check monotonic difference
363                 if (i > 1) {
364                     Assert.assertTrue("Expected convergence not monotonic descending",
365                            sums[i - 1] - sums[i] < sums[i - 2] - sums[i - 1]);
366                 }
367             }
368         }
369 
370         // Check the test function is correct.
371         tolerance = JdkMath.abs(expected * SimpsonIntegrator.DEFAULT_RELATIVE_ACCURACY);
372         Assert.assertEquals("Expected result", expected, sums[maxIteration], tolerance);
373 
374         // Set-up to test convergence at a specific iteration.
375         // Allow enough function evaluations.
376         // Iteration 0 = 3 evaluations
377         // Iteration 1 = 5 evaluations
378         // Iteration n = 2^(n+1)+1 evaluations
379         int evaluations = 2 << (maxIteration + 1) + 1;
380 
381         for (int i = minIteration; i <= maxIteration; i++) {
382             // Create convergence criteria.
383             // (sum - previous) is monotonic descending.
384             // So use a point half-way between them:
385             // ((sums[i-1] - sums[i]) + (sums[i-2] - sums[i-1])) / 2
386             final double absoluteAccuracy = (sums[i - 2] - sums[i]) / 2;
387 
388             // Use minimalIterationCount>1
389             UnivariateIntegrator integrator = new SimpsonIntegrator(
390                     0, absoluteAccuracy,
391                     2, SIMPSON_MAX_ITERATIONS_COUNT);
392 
393             result = integrator.integrate(evaluations, f, min, max);
394 
395             // Check the iteration is as expected
396             Assert.assertEquals("Test failed to control iteration", i, integrator.getIterations());
397 
398             // MATH-1458: minimalIterationCount>1 computes incorrect Simpson sum
399             // for the iteration. Check it is the correct sum.
400             // It should be closer to this one than the previous or next.
401             final double dp = JdkMath.abs(sums[i-1] - result);
402             final double d  = JdkMath.abs(sums[i]   - result);
403             final double dn = JdkMath.abs(sums[i+1] - result);
404 
405             Assert.assertTrue("Result closer to sum expected from previous iteration: " + i, d < dp);
406             Assert.assertTrue("Result closer to sum expected from next iteration: " + i, d < dn);
407         }
408     }
409 }