View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.integrate;
39  
40  import static ffx.numerics.integrate.Integrate1DNumeric.IntegrationSide.LEFT;
41  import static ffx.numerics.integrate.Integrate1DNumeric.IntegrationSide.RIGHT;
42  import static java.lang.String.format;
43  import static org.junit.Assert.assertEquals;
44  import static org.junit.Assert.assertTrue;
45  
46  import ffx.numerics.integrate.Integrate1DNumeric.IntegrationSide;
47  import ffx.numerics.integrate.Integrate1DNumeric.IntegrationType;
48  import java.util.Arrays;
49  import java.util.List;
50  import java.util.logging.Logger;
51  import java.util.stream.Collectors;
52  
53  import ffx.utilities.FFXTest;
54  import org.junit.Test;
55  
56  /**
57   * The IntegrationTest is a JUnit test for the Integration program that ensures that the integrals
58   * are calculated correctly. This test is run using known integrals calculated with the equation
59   * y=10sin(6x)-7cos(5x)+11sin(8x).
60   *
61   * @author Claire O'Connell
62   */
63  public class Integrate1DTest extends FFXTest {
64  
65    private static final int NUM_INTEGRATION_TYPES = 8; // Left/right, rect/trap/simp/boole.
66    private static final Logger logger = Logger.getLogger(Integrate1DTest.class.getName());
67  
68    /**
69     * Assert that doubles are equal to within a multiplier of ulp (machine precision).
70     *
71     * @param trueVal True answer
72     * @param ulpMult Multiple of ulp to use
73     * @param values Values to check
74     */
75    private static void assertToUlp(double trueVal, double ulpMult, double... values) {
76      double ulp = Math.ulp(trueVal) * ulpMult;
77      for (double val : values) {
78        assertEquals(trueVal, val, ulp);
79      }
80    }
81  
82    /**
83     * Assert that doubles are equal to within a multiplier of ulp (machine precision).
84     *
85     * @param message To print if assertion fails
86     * @param trueVal True answer
87     * @param ulpMult Multiple of ulp to use
88     * @param values Values to check
89     */
90    private static void assertToUlp(
91        String message, double trueVal, double ulpMult, double... values) {
92      double ulp = Math.ulp(trueVal) * ulpMult;
93      for (double val : values) {
94        assertEquals(message, trueVal, val, ulp);
95      }
96    }
97  
98    /**
99     * Ensures that integration performs the same on a DoublesDataSet constructed from a function as
100    * directly from the function; uses both DoublesDataSet constructors.
101    */
102   @Test
103   public void castToDoubleSetTest() {
104     logger.info(" Testing casting of a function to a DoublesDataSet");
105 
106     double[] points = Integrate1DNumeric.generateXPoints(0.0, 1.0, 11, false);
107 
108     double[] zeroOrder = {1.0}; // f(x) = 1
109     double[] firstOrder = {2.0, 1.0}; // f(x) = x+2
110     double[] secondOrder = {1.5, -4.0, 1.0}; // f(x) = x^2 - 4x + 1.5
111     double[] thirdOrder = {2.0, -1.0, -3.0, 1.0}; // f(x) = x^3 - 3x^2 - x + 2
112     double[] fourthOrder = {1, -4.0, -6.0, 4.0, 1.0}; // f(x) = x^4 + 4x^3 - 6x^2 - 4x + 1
113     double[] fifthOrder = {
114       2.0, 10.0, -18.0, 8.0, -5.0, 1.0
115     }; // f(x) = x^5 - 5x^4 + 8x^3 - 18x^2 + 10x + 2
116 
117     // Accumulate the coefficients into a 2-D array.
118     double[][] coeffs = {zeroOrder, firstOrder, secondOrder, thirdOrder, fourthOrder, fifthOrder};
119     List<FunctionDataCurve> polynomials =
120         Arrays.stream(coeffs)
121             .map(
122                 (double[] coeff) -> {
123                   return new PolynomialCurve(points, false, coeff);
124                 })
125             .collect(Collectors.toList());
126 
127     for (FunctionDataCurve pn : polynomials) {
128       pn.assertXIntegrity();
129       DataSet dpn = new DoublesDataSet(pn);
130       double[] ypts = pn.getAllFxPoints();
131       DataSet abInitio = new DoublesDataSet(points, ypts, false);
132 
133       for (int j = 0; j < NUM_INTEGRATION_TYPES; j++) {
134         IntegrationResult rpn = new IntegrationResult(pn, j);
135         IntegrationResult rdpn = new IntegrationResult(dpn, j);
136         IntegrationResult raipn = new IntegrationResult(abInitio, j);
137 
138         double pnVal = rpn.getValue();
139         double dpnVal = rdpn.getValue();
140         double aipnVal = raipn.getValue();
141 
142         String message =
143             format(
144                 " Casted function:\n%s\nfunction "
145                     + "result %9.3g, casted result %9.3g, from points %9.3g, "
146                     + "difference to casted %9.3g, difference to from-points %9.3g"
147                     + "\nintegration method %s",
148                 pn.toString(),
149                 pnVal,
150                 dpnVal,
151                 aipnVal,
152                 (pnVal - dpnVal),
153                 (pnVal - aipnVal),
154                 rpn.toString());
155         assertToUlp(message, pnVal, 10.0, dpnVal, aipnVal);
156       }
157     }
158     logger.info(" Polynomial casting tests without halved sides complete.");
159 
160     double[] points2 = Integrate1DNumeric.generateXPoints(0.0, 1.0, 12, true);
161     polynomials =
162         Arrays.stream(coeffs)
163             .map(
164                 (double[] coeff) -> {
165                   return new PolynomialCurve(points2, true, coeff);
166                 })
167             .collect(Collectors.toList());
168 
169     for (FunctionDataCurve pn : polynomials) {
170       pn.assertXIntegrity();
171       DataSet dpn = new DoublesDataSet(pn);
172       double[] ypts = pn.getAllFxPoints();
173       DataSet abInitio = new DoublesDataSet(points2, ypts, true);
174 
175       for (int j = 0; j < NUM_INTEGRATION_TYPES; j++) {
176         IntegrationResult rpn = new IntegrationResult(pn, j);
177         IntegrationResult rdpn = new IntegrationResult(dpn, j);
178         IntegrationResult raipn = new IntegrationResult(abInitio, j);
179 
180         double pnVal = rpn.getValue();
181         double dpnVal = rdpn.getValue();
182         double aipnVal = raipn.getValue();
183 
184         String message =
185             format(
186                 " Casted function:\n%s\nfunction "
187                     + "result %9.3g, casted result %9.3g, from points %9.3g, "
188                     + "difference to casted %9.3g, difference to from-points %9.3g"
189                     + "\nintegration method %s",
190                 pn.toString(),
191                 pnVal,
192                 dpnVal,
193                 aipnVal,
194                 (pnVal - dpnVal),
195                 (pnVal - aipnVal),
196                 rpn.toString());
197         assertToUlp(message, pnVal, 10.0, dpnVal, aipnVal);
198       }
199     }
200     logger.info(" Polynomial casting tests with halved sides complete.");
201 
202     double[] sinePoints = Integrate1DNumeric.generateXPoints(0, 4.0, 101, false);
203     FunctionDataCurve sinWave = new SinWave(sinePoints, false, 20, 30);
204     sinWave.assertXIntegrity();
205     DataSet dsw = new DoublesDataSet(sinWave);
206     double[] ypts = sinWave.getAllFxPoints();
207     DataSet abInitio = new DoublesDataSet(sinePoints, ypts, false);
208 
209     for (int j = 0; j < NUM_INTEGRATION_TYPES; j++) {
210       IntegrationResult rsw = new IntegrationResult(sinWave, j);
211       IntegrationResult rdsw = new IntegrationResult(dsw, j);
212       IntegrationResult raisw = new IntegrationResult(abInitio, j);
213 
214       double swVal = rsw.getValue();
215       double dswVal = rdsw.getValue();
216       double aiswVal = raisw.getValue();
217 
218       String message =
219           format(
220               " Casted function:\n%s\nfunction "
221                   + "result %9.3g, casted result %9.3g, from points %9.3g, "
222                   + "difference to casted %9.3g, difference to from-points %9.3g"
223                   + "\nintegration method %s",
224               sinWave.toString(),
225               swVal,
226               dswVal,
227               aiswVal,
228               (swVal - dswVal),
229               (swVal - aiswVal),
230               rsw.toString());
231       assertToUlp(message, swVal, 10.0, dswVal, aiswVal);
232     }
233     logger.info(" Sine-wave casting tests with halved sides complete.");
234 
235     double[] sinePoints2 = Integrate1DNumeric.generateXPoints(0, 4.0, 102, true);
236     sinWave = new SinWave(sinePoints2, true, 20, 30);
237     sinWave.assertXIntegrity();
238     dsw = new DoublesDataSet(sinWave);
239     ypts = sinWave.getAllFxPoints();
240     abInitio = new DoublesDataSet(sinePoints2, ypts, true);
241 
242     for (int j = 0; j < NUM_INTEGRATION_TYPES; j++) {
243       IntegrationResult rsw = new IntegrationResult(sinWave, j);
244       IntegrationResult rdsw = new IntegrationResult(dsw, j);
245       IntegrationResult raisw = new IntegrationResult(abInitio, j);
246 
247       double swVal = rsw.getValue();
248       double dswVal = rdsw.getValue();
249       double aiswVal = raisw.getValue();
250 
251       String message =
252           format(
253               " Casted function:\n%s\nfunction "
254                   + "result %9.3g, casted result %9.3g, from points %9.3g, "
255                   + "difference to casted %9.3g, difference to from-points %9.3g"
256                   + "\nintegration method %s",
257               sinWave.toString(),
258               swVal,
259               dswVal,
260               aiswVal,
261               (swVal - dswVal),
262               (swVal - aiswVal),
263               rsw.toString());
264       assertToUlp(message, swVal, 10.0, dswVal, aiswVal);
265     }
266     logger.info(" Sine-wave casting tests with halved sides complete.");
267     logger.info(" Casting tests complete.");
268   }
269 
270   /**
271    * Tests the parallelized versions of the integration methods. Overall, these are not recommended
272    * for production use; it would require an enormous number of points for numerical integration of
273    * a 1-D function to be at all significant for timing, and the parallelized versions are probably
274    * not quite as CPU-efficient.
275    */
276   @Test
277   public void parallelTest() {
278     double[] pts = Integrate1DNumeric.generateXPoints(0, 2.0, 92, false);
279     FunctionDataCurve tcurve = new SinWave(pts, false, 60, 60);
280     tcurve.assertXIntegrity();
281     for (int i = 0; i < NUM_INTEGRATION_TYPES; i++) {
282       IntegrationResult seqResult = new IntegrationResult(tcurve, i, false);
283       IntegrationResult parResult = new IntegrationResult(tcurve, i, true);
284       double seqVal = seqResult.getValue();
285       double parVal = parResult.getValue();
286       String message =
287           format(
288               "Parallel value %9.3g did not match sequential value %9.3g, error %9.3g",
289               parVal, seqVal, (parVal - seqVal));
290       assertToUlp(message, seqVal, 80.0, parVal);
291     }
292   }
293 
294   /**
295    * A more difficult test on polynomials, with just five points and larger coefficients; intended
296    * to test stability in extreme cases.
297    */
298   @Test
299   public void polynomialGrinderTest() {
300     logger.info(" Testing integration methods on polynomials with large coefficients.");
301     double[] points = Integrate1DNumeric.generateXPoints(0.0, 2.0, 5, false);
302 
303     double[] zeroOrder = {-2.0};
304     double[] firstOrder = {6.0, -14.0};
305     double[] secondOrder = {4.5, -5.0, 3.0};
306     double[] thirdOrder = {-3.0, 5.0, -2.0, 12.0};
307     double[] fourthOrder = {12, -4.5, 8.0, -5.0, 4.0};
308     double[] fifthOrder = {-4.0, 10.0, -18.0, 8.0, -5.0, 8.0};
309 
310     double[][] coeffs = {zeroOrder, firstOrder, secondOrder, thirdOrder, fourthOrder, fifthOrder};
311 
312     List<FunctionDataCurve> polynomials =
313         Arrays.stream(coeffs)
314             .map(
315                 (double[] coeff) -> {
316                   return new PolynomialCurve(points, false, coeff);
317                 })
318             .collect(Collectors.toList());
319 
320     for (int i = 0; i < polynomials.size(); i++) {
321       FunctionDataCurve pn = polynomials.get(i);
322       pn.assertXIntegrity();
323 
324       // Get the analyticalIntegral over the range.
325       double analytical = pn.analyticalIntegral();
326 
327       double rLeft = Integrate1DNumeric.rectangular(pn, LEFT);
328       double rRight = Integrate1DNumeric.rectangular(pn, RIGHT);
329 
330       double tLeft = Integrate1DNumeric.trapezoidal(pn, LEFT);
331       double tRight = Integrate1DNumeric.trapezoidal(pn, RIGHT);
332 
333       double sLeft = Integrate1DNumeric.simpsons(pn, LEFT);
334       double sRight = Integrate1DNumeric.simpsons(pn, RIGHT);
335 
336       double bLeft = Integrate1DNumeric.booles(pn, LEFT);
337       double bRight = Integrate1DNumeric.booles(pn, RIGHT);
338 
339       StringBuilder sb = new StringBuilder();
340 
341       sb.append(format(" Integrals for polynomial of degree %d\n", i));
342       sb.append(format(" %-18s %9.3g\n", "Analytical", analytical));
343       sb.append(" Numerical integration errors:\n");
344       sb.append(
345           format(
346               " %-18s %9.3g  %-18s %9.3g\n",
347               "Left rectangular", rLeft - analytical, "Right rectangular", rRight - analytical));
348       sb.append(
349           format(
350               " %-18s %9.3g  %-18s %9.3g\n",
351               "Left trapezoidal", tLeft - analytical, "Right trapezoidal", tRight - analytical));
352       sb.append(
353           format(
354               " %-18s %9.3g  %-18s %9.3g\n",
355               "Left Simpson's", sLeft - analytical, "Right Simpson's", sRight - analytical));
356       sb.append(
357           format(
358               " %-18s %9.3g  %-18s %9.3g\n",
359               "Left Boole's", bLeft - analytical, "Right Boole's", bRight - analytical));
360       sb.append("\n");
361 
362       logger.info(sb.toString());
363 
364       if (i == 0) {
365         assertToUlp(analytical, 500.0, rLeft, rRight);
366       }
367       if (i <= 1) {
368         assertToUlp(analytical, 500.0, tLeft, tRight);
369       }
370       if (i <= 2) {
371         assertToUlp(analytical, 500.0, sLeft, sRight);
372       }
373       if (i <= 4) {
374         assertToUlp(analytical, 500.0, bLeft, bRight);
375       }
376     }
377   }
378 
379   /**
380    * Basic test for polynomial functions; checks to ensure that each integration method is behaving
381    * as expected, and returns the exact value (to within machine precision) when the method should
382    * be exact. For example, Simpson's method fits a quadratic curve to 3 points, and should return
383    * the exact integral for polynomials of degree 2 or less.
384    */
385   @Test
386   public void polynomialTest() {
387     logger.info(" Testing integration methods on polynomials with known integrals.");
388 
389     /*
390      * This sets up 9 evenly spaced points from 0-1, which should be 0, 0.125,
391      * 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, and 1.0. Having very few points
392      * of integration should stress the integration methods to the limit...
393      * not that it appears to be causing any problems for Simpson's method
394      * on the third-order integral.
395      *
396      * 9 points also helps because neither Simpson's nor Boole's rule should
397      * be truncated and require lower-order integration at an end. If we
398      * add Simpson's 3/8 rule, 13 points may be preferable.
399      */
400     double[] points = Integrate1DNumeric.generateXPoints(0.0, 1.0, 9, false);
401 
402     /*
403      * These are the coefficients for polynomials of order 0 to 5, in reverse
404      * order; thus the second-order polynomial is x^2 - 4x + 1.5.
405      */
406     double[] zeroOrder = {1.0}; // f(x) = 1
407     double[] firstOrder = {2.0, 1.0}; // f(x) = x+2
408     double[] secondOrder = {1.5, -4.0, 1.0}; // f(x) = x^2 - 4x + 1.5
409     double[] thirdOrder = {2.0, -1.0, -3.0, 1.0}; // f(x) = x^3 - 3x^2 - x + 2
410     double[] fourthOrder = {1, -4.0, -6.0, 4.0, 1.0}; // f(x) = x^4 + 4x^3 - 6x^2 - 4x + 1
411     double[] fifthOrder = {
412       2.0, 10.0, -18.0, 8.0, -5.0, 1.0
413     }; // f(x) = x^5 - 5x^4 + 8x^3 - 18x^2 + 10x + 2
414 
415     /*
416      * Hand-calculated integrals from 0-1, to test that the analyticalIntegral
417      * function is indeed working for PolynomialCurve.
418      */
419     double[] trueVals = {1.0, 2.5, (-1.0 / 6.0), 0.75, -1.8, (13.0 / 6.0)};
420 
421     // Accumulate the coefficients into a 2-D array.
422     double[][] coeffs = {zeroOrder, firstOrder, secondOrder, thirdOrder, fourthOrder, fifthOrder};
423 
424     /*
425      * A bit of streaming/mapping logic here; coeffs[][] is streamed to a
426      * Stream of double[]. For each of these double[], a PolynomialCurve
427      * is constructed using the points array (points along x), explicitly
428      * setting halfWidthBins to false, and then the streamed double[] of
429      * coefficients. That transforms (maps) a Stream of double[] to a Stream
430      * of PolynomialCurve, which is then collected into a list.
431      *
432      * Each PolynomialCurve is:
433      * A DataSet, describing a set of points f(x), starting at lb (lower bound),
434      * ending at ub (upper bound), with even spacing with the optional exception
435      * of half-width bins. The original set of points x[] is not guaranteed
436      * to be stored, but can be reconstructed by knowing the number of points,
437      * the lower bound, the upper bound, and if it used half-width start/end
438      * bins.
439      *
440      * A FunctionDataCurve, describing a DataSet generated from some function
441      * f(x) which can be analytically integrated; this adds methods for
442      * analytical integration and evaluating f(x) at any arbitrary x.
443      *
444      * A PolynomialCurve, a concrete class extending FunctionDataCurve, where
445      * f(x) is an arbitrary-order polynomial function.
446      *
447      * This is very standard inheritance; an interface (DataSet) to describe
448      * what you want something to do, an abstract class (FunctionDataCurve)
449      * to provide some basic implementations, and, in this case, some
450      * additional methods for that particular subset of its interface,
451      * and finally a concrete class (PolynomialCurve) which defines as little
452      * as possible.
453      *
454      * The whole point of this, then, is that the integration methods can
455      * use any arbitrary DataSet, such as a DoublesDataSet taken straight
456      * from OST, and can be thoroughly tested using any arbitrary
457      * FunctionDataCurve, so that we're confident they are working as intended
458      * when applied to DataSets where we don't know the true answer.
459      */
460 
461     /*
462      * Create the polynomial curves, using points[] as x, disabling half-width
463      * bins, and using each double[] of coefficients in turn. The values of
464      * f(x) are automatically generated by the PolynomialCurve constructor.
465      */
466     List<FunctionDataCurve> polynomials =
467         Arrays.stream(coeffs)
468             .map(
469                 (double[] coeff) -> {
470                   return new PolynomialCurve(points, false, coeff);
471                 })
472             .collect(Collectors.toList());
473 
474     for (int i = 0; i < polynomials.size(); i++) {
475       FunctionDataCurve pn = polynomials.get(i);
476       pn.assertXIntegrity();
477 
478       // Get the true value, and analyticalIntegral over the range.
479       double trueVal = trueVals[i];
480       double analytical = pn.analyticalIntegral();
481 
482       double rLeft = Integrate1DNumeric.rectangular(pn, LEFT);
483       double rRight = Integrate1DNumeric.rectangular(pn, RIGHT);
484 
485       double tLeft = Integrate1DNumeric.trapezoidal(pn, LEFT);
486       double tRight = Integrate1DNumeric.trapezoidal(pn, RIGHT);
487 
488       double sLeft = Integrate1DNumeric.simpsons(pn, LEFT);
489       double sRight = Integrate1DNumeric.simpsons(pn, RIGHT);
490 
491       double bLeft = Integrate1DNumeric.booles(pn, LEFT);
492       double bRight = Integrate1DNumeric.booles(pn, RIGHT);
493 
494       StringBuilder sb = new StringBuilder();
495 
496       sb.append(format(" Integrals for polynomial of degree %d\n", i));
497       sb.append(format(" %-18s %9.3g  %-18s %9.3g\n", "Exact", trueVal, "Analytical", analytical));
498       sb.append(" Numerical integration errors:\n");
499       sb.append(
500           format(
501               " %-18s %9.3g  %-18s %9.3g\n",
502               "Left rectangular", rLeft - trueVal, "Right rectangular", rRight - trueVal));
503       sb.append(
504           format(
505               " %-18s %9.3g  %-18s %9.3g\n",
506               "Left trapezoidal", tLeft - trueVal, "Right trapezoidal", tRight - trueVal));
507       sb.append(
508           format(
509               " %-18s %9.3g  %-18s %9.3g\n",
510               "Left Simpson's", sLeft - trueVal, "Right Simpson's", sRight - trueVal));
511       sb.append(
512           format(
513               " %-18s %9.3g  %-18s %9.3g\n",
514               "Left Boole's", bLeft - trueVal, "Right Boole's", bRight - trueVal));
515       sb.append("\n");
516       logger.info(sb.toString());
517 
518       assertToUlp(trueVal, 10.0, analytical);
519       if (i == 0) {
520         assertToUlp(trueVal, 500.0, rLeft, rRight);
521       }
522       if (i <= 1) {
523         assertToUlp(trueVal, 500.0, tLeft, tRight);
524       }
525       if (i <= 2) {
526         assertToUlp(trueVal, 500.0, sLeft, sRight);
527       }
528       if (i <= 4) {
529         assertToUlp(trueVal, 500.0, bLeft, bRight);
530       }
531     }
532   }
533 
534   /** Ensures that the by-bin integration machinery matches an all-at-once integral. */
535   @Test
536   public void testByBinIntegral() {
537     double[] pts = Integrate1DNumeric.generateXPoints(0, 1.0, 18, false);
538     FunctionDataCurve curve = new SinWave(pts, 1, 20.0);
539     curve.assertXIntegrity();
540 
541     logger.info(" Testing by-bin integration with a sin wave (sum vs. overall result).");
542     for (int i = 0; i < NUM_INTEGRATION_TYPES; i++) {
543       IntegrationResult knownResult = new IntegrationResult(curve, i, false);
544       double[] sequentialPts =
545           Integrate1DNumeric.integrateByBins(curve, knownResult.getSide(), knownResult.getType());
546       double seqResult = Arrays.stream(sequentialPts).sum();
547 
548       double knownVal = knownResult.getValue();
549       assertToUlp(
550           format(" Mismatch for result %s\n ", knownResult, knownVal, seqResult),
551           knownVal,
552           200.0,
553           seqResult);
554     }
555     logger.info(" Testing match to analytical integrals.\n");
556     double[] leftPts = Integrate1DNumeric.integrateByBins(curve, LEFT, IntegrationType.BOOLE);
557     double[] rightPts = Integrate1DNumeric.integrateByBins(curve, RIGHT, IntegrationType.BOOLE);
558     double maxDelta = 0.075;
559     for (int i = 0; i < pts.length; i++) {
560       double leftAnalytic = curve.analyticalIntegral(pts[0], pts[i]);
561       if (i > 0) {
562         leftAnalytic -= curve.analyticalIntegral(pts[0], pts[i - 1]);
563       }
564       double rightAnalytic = curve.analyticalIntegral(pts[i], pts[pts.length - 1]);
565       if (i < (pts.length - 1)) {
566         rightAnalytic -= curve.analyticalIntegral(pts[i + 1], pts[pts.length - 1]);
567       }
568 
569       assertEquals(
570           format(
571               " Contribution of bin %d to left-hand integral of %s does not match analytic solution",
572               i, curve),
573           leftAnalytic,
574           leftPts[i],
575           maxDelta);
576       assertEquals(
577           format(
578               " Contribution of bin %d to right-hand integral of %s does not match analytic solution",
579               i, curve),
580           rightAnalytic,
581           rightPts[i],
582           maxDelta);
583     }
584 
585     logger.info(" Testing by-bin integration with a higher-frequency wave.");
586     curve = new CosineWave(pts, 1, 120.0);
587     curve.assertXIntegrity();
588     for (int i = 0; i < NUM_INTEGRATION_TYPES; i++) {
589       IntegrationResult knownResult = new IntegrationResult(curve, i, false);
590       double[] sequentialPts =
591           Integrate1DNumeric.integrateByBins(curve, knownResult.getSide(), knownResult.getType());
592       double seqResult = Arrays.stream(sequentialPts).sum();
593 
594       double knownVal = knownResult.getValue();
595       assertToUlp(
596           format(" Mismatch for result %s\n ", knownResult, knownVal, seqResult),
597           knownVal,
598           200.0,
599           seqResult);
600     }
601     logger.info(" Testing match to analytical integrals.\n");
602     leftPts = Integrate1DNumeric.integrateByBins(curve, LEFT, IntegrationType.BOOLE);
603     rightPts = Integrate1DNumeric.integrateByBins(curve, RIGHT, IntegrationType.BOOLE);
604     maxDelta = 0.075;
605     for (int i = 0; i < pts.length; i++) {
606       double leftAnalytic = curve.analyticalIntegral(pts[0], pts[i]);
607       if (i > 0) {
608         leftAnalytic -= curve.analyticalIntegral(pts[0], pts[i - 1]);
609       }
610       double rightAnalytic = curve.analyticalIntegral(pts[i], pts[pts.length - 1]);
611       if (i < (pts.length - 1)) {
612         rightAnalytic -= curve.analyticalIntegral(pts[i + 1], pts[pts.length - 1]);
613       }
614 
615       assertEquals(
616           format(
617               " Contribution of bin %d to left-hand integral of %s does not match analytic solution",
618               i, curve),
619           leftAnalytic,
620           leftPts[i],
621           maxDelta);
622       assertEquals(
623           format(
624               " Contribution of bin %d to right-hand integral of %s does not match analytic solution",
625               i, curve),
626           rightAnalytic,
627           rightPts[i],
628           maxDelta);
629     }
630 
631     logger.info(
632         " Testing by-bin integration with a higher-frequency wave with half-width ending bins.");
633     pts = Integrate1DNumeric.generateXPoints(0, 1.0, 19, true);
634     curve = new CosineWave(pts, true, 1, 120.0);
635     for (int i = 0; i < NUM_INTEGRATION_TYPES; i++) {
636       IntegrationResult knownResult = new IntegrationResult(curve, i, false);
637       double[] sequentialPts =
638           Integrate1DNumeric.integrateByBins(curve, knownResult.getSide(), knownResult.getType());
639       double seqResult = Arrays.stream(sequentialPts).sum();
640 
641       double knownVal = knownResult.getValue();
642       assertToUlp(
643           format(
644               " Mismatch for result %s\n Points %s\n ",
645               knownResult, Arrays.toString(sequentialPts), knownVal, seqResult),
646           knownVal,
647           200.0,
648           seqResult);
649     }
650     logger.info(" Testing match to analytical integrals.\n");
651     leftPts = Integrate1DNumeric.integrateByBins(curve, LEFT, IntegrationType.BOOLE);
652     rightPts = Integrate1DNumeric.integrateByBins(curve, RIGHT, IntegrationType.BOOLE);
653     maxDelta = 0.1;
654     for (int i = 0; i < pts.length; i++) {
655       double leftAnalytic = curve.analyticalIntegral(pts[0], pts[i]);
656       if (i > 0) {
657         leftAnalytic -= curve.analyticalIntegral(pts[0], pts[i - 1]);
658       }
659       double rightAnalytic = curve.analyticalIntegral(pts[i], pts[pts.length - 1]);
660       if (i < (pts.length - 1)) {
661         rightAnalytic -= curve.analyticalIntegral(pts[i + 1], pts[pts.length - 1]);
662       }
663 
664       assertEquals(
665           format(
666               " Contribution of bin %d to left-hand integral of %s does not match analytic solution",
667               i, curve),
668           leftAnalytic,
669           leftPts[i],
670           maxDelta);
671       assertEquals(
672           format(
673               " Contribution of bin %d to right-hand integral of %s does not match analytic solution",
674               i, curve),
675           rightAnalytic,
676           rightPts[i],
677           maxDelta);
678     }
679 
680     logger.info(" Testing by-bin integration with a polynomial function.");
681     pts = Integrate1DNumeric.generateXPoints(0.0, 2.0, 5, false);
682     double[] fifthOrder = {-4.0, 10.0, -18.0, 8.0, -5.0, 8.0};
683     curve = new PolynomialCurve(pts, false, fifthOrder);
684     curve.assertXIntegrity();
685 
686     for (int i = 0; i < NUM_INTEGRATION_TYPES; i++) {
687       IntegrationResult knownResult = new IntegrationResult(curve, i, false);
688       double[] sequentialPts =
689           Integrate1DNumeric.integrateByBins(curve, knownResult.getSide(), knownResult.getType());
690       double seqResult = Arrays.stream(sequentialPts).sum();
691 
692       double knownVal = knownResult.getValue();
693       assertToUlp(
694           format(" Mismatch for result %s\n ", knownResult, knownVal, seqResult),
695           knownVal,
696           200.0,
697           seqResult);
698     }
699     // Skip testing to analytical integrals: the points done by rectangular & trapezoidal are simply
700     // too inaccurate.
701 
702     logger.info(" Completed testing of by-bin integration\n");
703   }
704 
705   @Test
706   public void testSinCosine() {
707     double[] points = Integrate1DNumeric.generateXPoints(0, 1, 201, false);
708     boolean verb = false;
709     // Test sine with full-width ends.
710     logger.info("\n Sin wave test without half-width bins");
711     sinTest(points, false, false, verb);
712     logger.info("\n Cosine wave test without half-width bins");
713     sinTest(points, false, true, verb);
714 
715     points = Integrate1DNumeric.generateXPoints(0, 1, 202, true);
716     logger.info("\n Sin wave test with half-width bins");
717     sinTest(points, true, false, verb);
718     logger.info("\n Cosine wave test with half-width bins");
719     sinTest(points, true, true, verb);
720     logger.info("\n");
721   }
722 
723   /**
724    * Common code for testing sin/cosine waves; effectively a sub-method for the testSinCosine test.
725    *
726    * @param points
727    * @param halvedEnds
728    * @param cosine If cosine, else sine wave
729    * @param verbose
730    */
731   private void sinTest(double[] points, boolean halvedEnds, boolean cosine, boolean verbose) {
732     int failRleft = 0;
733     int failRright = 0;
734     int failTleft = 0;
735     int failTright = 0;
736     int failSleft = 0;
737     int failSright = 0;
738     int failBleft = 0;
739     int failBright = 0;
740     double maxDelta = 0.05;
741 
742     for (int j = 1; j <= 500; j++) {
743       FunctionDataCurve wave =
744           cosine ? new CosineWave(points, halvedEnds, j, j) : new SinWave(points, halvedEnds, j, j);
745       wave.assertXIntegrity();
746 
747       // Get the analyticalIntegral over the range.
748       double analytical = wave.analyticalIntegral();
749 
750       double rLeft = Integrate1DNumeric.rectangular(wave, LEFT);
751       double erLeft = analytical - rLeft;
752       double rRight = Integrate1DNumeric.rectangular(wave, RIGHT);
753       double erRight = analytical - rRight;
754 
755       double tLeft = Integrate1DNumeric.trapezoidal(wave, LEFT);
756       double etLeft = analytical - tLeft;
757       double tRight = Integrate1DNumeric.trapezoidal(wave, RIGHT);
758       double etRight = analytical - tRight;
759 
760       double sLeft = Integrate1DNumeric.simpsons(wave, LEFT);
761       double esLeft = analytical - sLeft;
762       double sRight = Integrate1DNumeric.simpsons(wave, RIGHT);
763       double esRight = analytical - sRight;
764 
765       double bLeft = Integrate1DNumeric.booles(wave, LEFT);
766       double ebLeft = analytical - bLeft;
767       double bRight = Integrate1DNumeric.booles(wave, RIGHT);
768       double ebRight = analytical - bRight;
769       if (verbose) {
770         StringBuilder sb = new StringBuilder();
771 
772         sb.append(format(" Integrals for sine wave j*sin(j) of j %d\n", j));
773         sb.append(format(" %-18s %9.3g\n", "Analytical", analytical));
774         sb.append(" Numerical integration errors:\n");
775         sb.append(
776             format(
777                 " %-18s %9.3g  %-18s %9.3g\n",
778                 "Left rectangular", erLeft, "Right rectangular", erRight));
779         sb.append(
780             format(
781                 " %-18s %9.3g  %-18s %9.3g\n",
782                 "Left trapezoidal", etLeft, "Right trapezoidal", etRight));
783         sb.append(
784             format(
785                 " %-18s %9.3g  %-18s %9.3g\n",
786                 "Left Simpson's", esLeft, "Right Simpson's", esRight));
787         sb.append(
788             format(
789                 " %-18s %9.3g  %-18s %9.3g\n", "Left Boole's", ebLeft, "Right Boole's", ebRight));
790         sb.append("\n");
791 
792         logger.info(sb.toString());
793       }
794 
795       if (failRleft == 0 && Math.abs(erLeft) > maxDelta) {
796         failRleft = j;
797       }
798       if (failRright == 0 && Math.abs(erRight) > maxDelta) {
799         failRright = j;
800       }
801 
802       if (failTleft == 0 && Math.abs(etLeft) > maxDelta) {
803         failTleft = j;
804       }
805       if (failTright == 0 && Math.abs(etRight) > maxDelta) {
806         failTright = j;
807       }
808 
809       if (failSleft == 0 && Math.abs(esLeft) > maxDelta) {
810         failSleft = j;
811       }
812       if (failSright == 0 && Math.abs(esRight) > maxDelta) {
813         failSright = j;
814       }
815 
816       if (failBleft == 0 && Math.abs(ebLeft) > maxDelta) {
817         failBleft = j;
818       }
819       if (failBright == 0 && Math.abs(ebRight) > maxDelta) {
820         failBright = j;
821       }
822     }
823 
824     StringBuilder sb1 =
825         new StringBuilder(format(" Error exceeded delta %8.3f at iterations:\n", maxDelta));
826     sb1.append(
827         format(
828             " %-18s %9d  %-18s %9d\n",
829             "Left rectangular", failRleft, "Right rectangular", failRright));
830     sb1.append(
831         format(
832             " %-18s %9d  %-18s %9d\n",
833             "Left trapezoidal", failTleft, "Right trapezoidal", failTright));
834     sb1.append(
835         format(
836             " %-18s %9d  %-18s %9d\n", "Left Simpson's", failSleft, "Right Simpson's", failSright));
837     sb1.append(
838         format(" %-18s %9d  %-18s %9d\n", "Left Boole's", failBleft, "Right Boole's", failBright));
839     logger.info(sb1.toString());
840 
841     StringBuilder sb = new StringBuilder(" integration failed for ");
842     if (halvedEnds) {
843       if (cosine) {
844         sb.append("cosine wave, with half-width end bins, of frequency ");
845       } else {
846         sb.append("sine wave, with half-width end bins, of frequency ");
847       }
848       String midMessage = sb.toString();
849       assertTrue(format("Rectangular%s%d", midMessage, 12), failRleft > 12 && failRright > 12);
850       assertTrue(format("Trapezoidal%s%d", midMessage, 100), failTleft > 100 && failTright > 100);
851       assertTrue(format("Simpson's%s%d", midMessage, 150), failSleft > 150 && failSright > 150);
852       assertTrue(format("Boole's%s%d", midMessage, 150), failBleft > 150 && failBright > 150);
853 
854     } else {
855       if (cosine) {
856         sb.append("cosine wave of frequency ");
857       } else {
858         sb.append("sine wave of frequency ");
859       }
860       String midMessage = sb.toString();
861       assertTrue(format("Rectangular%s%d", midMessage, 12), failRleft > 12 && failRright > 12);
862       assertTrue(format("Trapezoidal%s%d", midMessage, 100), failTleft > 100 && failTright > 100);
863       assertTrue(format("Simpson's%s%d", midMessage, 250), failSleft > 250 && failSright > 250);
864       assertTrue(format("Boole's%s%d", midMessage, 240), failBleft > 240 && failBright > 240);
865     }
866   }
867 
868   /**
869    * Intended to be a shorthand way of performing all the standard integrations, instead of
870    * tediously coding in separate tests for all four integration types in both directions;
871    * auto-assigns several fields based on the provided index.
872    */
873   private class IntegrationResult {
874     private final IntegrationType type;
875     private final IntegrationSide side;
876     private final boolean halvedSides;
877     private final DataSet data;
878     private final double integratedValue;
879     private final boolean parallel;
880 
881     /**
882      * Basic constructor, automatically setting direction and integration method based on index with
883      * a useful toString.
884      *
885      * <p>Even index values produce a left-hand integral, odd a right-hand integral.
886      *
887      * <p>Integration type is as follows, 0-7, after modulo(8) 0-1: Rectangular 2-3: Trapezoidal
888      * 4-5: Simpson's rule 6-7: Boole's rule
889      *
890      * @param data Data to numerically integrate
891      * @param index Automatically set method and direction
892      */
893     public IntegrationResult(DataSet data, int index) {
894       this(data, index, false);
895     }
896 
897     public IntegrationResult(DataSet data, int index, boolean parallel) {
898       switch ((index / 2) % 4) {
899         case 0:
900           type = IntegrationType.RECTANGULAR;
901           break;
902         case 1:
903           type = IntegrationType.TRAPEZOIDAL;
904           break;
905         case 2:
906           type = IntegrationType.SIMPSONS;
907           break;
908         case 3:
909           type = IntegrationType.BOOLE;
910           break;
911         default:
912           throw new IllegalArgumentException(
913               " How did ((index / 2) % 4) ever come out to not be 0-3?");
914       }
915       side = (index % 2 == 0) ? LEFT : RIGHT;
916       halvedSides = data.halfWidthEnds();
917       this.data = data;
918       this.parallel = parallel;
919 
920       if (parallel) {
921         switch (type) {
922           case RECTANGULAR:
923             integratedValue = Integrate1DNumeric.rectangularParallel(data, side);
924             break;
925           case TRAPEZOIDAL:
926             integratedValue = Integrate1DNumeric.trapezoidalParallel(data, side);
927             break;
928           case SIMPSONS:
929             integratedValue = Integrate1DNumeric.simpsonsParallel(data, side);
930             break;
931           case BOOLE:
932             integratedValue = Integrate1DNumeric.boolesParallel(data, side);
933             break;
934           default:
935             throw new IllegalArgumentException(
936                 " How did this end up with an integration type not rectangular, trapezoidal, Simpson's, or Boole's?");
937         }
938 
939       } else {
940         switch (type) {
941           case RECTANGULAR:
942             integratedValue = Integrate1DNumeric.rectangular(data, side);
943             break;
944           case TRAPEZOIDAL:
945             integratedValue = Integrate1DNumeric.trapezoidal(data, side);
946             break;
947           case SIMPSONS:
948             integratedValue = Integrate1DNumeric.simpsons(data, side);
949             break;
950           case BOOLE:
951             integratedValue = Integrate1DNumeric.booles(data, side);
952             break;
953           default:
954             throw new IllegalArgumentException(
955                 " How did this end up with an integration type not rectangular, trapezoidal, Simpson's, or Boole's?");
956         }
957       }
958     }
959 
960     public DataSet getDataSet() {
961       return data;
962     }
963 
964     public boolean getHalvedSides() {
965       return halvedSides;
966     }
967 
968     public IntegrationSide getSide() {
969       return side;
970     }
971 
972     public IntegrationType getType() {
973       return type;
974     }
975 
976     public double getValue() {
977       return integratedValue;
978     }
979 
980     public boolean isParallel() {
981       return parallel;
982     }
983 
984     @Override
985     public String toString() {
986       StringBuilder sb = new StringBuilder(type.toString().toLowerCase());
987       sb.append(", ").append(side.toString().toLowerCase());
988       sb.append("-side integral");
989       if (halvedSides) {
990         sb.append(" with half-width ending bins");
991       }
992       sb.append(".");
993       return sb.toString();
994     }
995   }
996 }