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-2025.
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 java.util.logging.Logger;
41  import java.util.stream.IntStream;
42  
43  import static java.lang.String.format;
44  
45  /**
46   * This program integrates using four methods: rectangular integration, the trapezoidal method,
47   * Simpson's Three Point Integration, and Boole's Five Point Integration. Of these, Simpson's and
48   * Boole's integration methods generally perform the best. Parallelized methods exist and seem to
49   * function, but the performance of a parallelized method should not be necessary, and the
50   * parallelization introduces some complexity and inefficiency.
51   *
52   * <p>Primary methods: integrateData and integrateByBins, using some DataSet, which is often a
53   * DoublesDataSet.
54   *
55   * @author Claire O'Connell
56   * @author Jacob M. Litman
57   */
58  public class Integrate1DNumeric {
59  
60    private static final Logger logger = Logger.getLogger(Integrate1DNumeric.class.getName());
61    /**
62     * Constant for used for Simpson's rule.
63     */
64    private static final double ONE_THIRD = (1.0 / 3.0);
65    /**
66     * Constant for used for Boole's rule.
67     */
68    private static final double BOOLE_FACTOR = (2.0 / 45.0);
69  
70    private Integrate1DNumeric() {
71      // Prevent instantiation
72    }
73  
74    /**
75     * Numerically integrates a data set using Boole's rule.
76     *
77     * @param data Data set to integrate
78     * @param side Side to integrate from
79     * @return Area of integral
80     */
81    public static double booles(DataSet data, IntegrationSide side) {
82      double area = 0;
83      int lb = 0;
84      int ub = data.numPoints() - 1;
85      if (data.halfWidthEnds()) {
86        area = trapezoidalEnds(data, side);
87        lb++;
88        ub--;
89      }
90      area += booles(data, side, lb, ub);
91      return area;
92    }
93  
94    /**
95     * Numerically integrates a data set, in bounds lb-ub inclusive, using Boole's rule.
96     *
97     * @param data Data set to integrate
98     * @param side Side to integrate from
99     * @param lb   First index to integrate over
100    * @param ub   Last index to integrate over
101    * @return Area of integral
102    */
103   public static double booles(DataSet data, IntegrationSide side, int lb, int ub) {
104     double area = 0;
105     double width = data.binWidth();
106     double[] points = data.getAllFxPoints();
107     int increment = 4;
108 
109     int nBins = (ub - lb) / increment;
110     int lowerNeglected = 0;
111     int upperNeglected = 0;
112 
113     switch (side) {
114       case RIGHT -> {
115         for (int i = ub; i > (lb + increment - 1); i -= increment) {
116           area += (7 * points[i] + 32 * points[i - 1] + 12 * points[i - 2]
117               + 32 * points[i - 3] + 7 * points[i - 4]);
118         }
119         lowerNeglected = lb;
120         upperNeglected = ub - (increment * nBins);
121       }
122       case LEFT -> {
123         for (int i = lb; i < (ub - increment + 1); i += increment) {
124           area += (7 * points[i] + 32 * points[i + 1] + 12 * points[i + 2]
125               + 32 * points[i + 3] + 7 * points[i + 4]);
126         }
127         lowerNeglected = lb + (increment * nBins);
128         upperNeglected = ub;
129       }
130     }
131     area *= BOOLE_FACTOR;
132     area *= width;
133 
134     area += finishIntegration(data, side, lowerNeglected, upperNeglected, IntegrationType.BOOLE);
135     return area;
136   }
137 
138   /**
139    * Numerically integrates a data set using Boole's rule. The sequential version is preferred unless
140    * necessary.
141    *
142    * @param data Data set to integrate
143    * @param side Side to integrate from
144    * @return Area of integral
145    */
146   public static double boolesParallel(DataSet data, IntegrationSide side) {
147     double area = 0;
148     int lb = 0;
149     int ub = data.numPoints() - 1;
150     if (data.halfWidthEnds()) {
151       area = trapezoidalEnds(data, side);
152       lb++;
153       ub--;
154     }
155     area += boolesParallel(data, side, lb, ub);
156     return area;
157   }
158 
159   /**
160    * Numerically integrates a data set, in bounds lb-ub inclusive, using Boole's rule. The sequential
161    * version is preferred unless necessary.
162    *
163    * @param data Data set to integrate
164    * @param side Side to integrate from
165    * @param lb   First index to integrate over
166    * @param ub   Last index to integrate over
167    * @return Area of integral
168    */
169   public static double boolesParallel(DataSet data, IntegrationSide side, int lb, int ub) {
170     double area = 0.0;
171     double width = data.binWidth();
172     double[] points = data.getAllFxPoints();
173     int increment = 4;
174 
175     int nBins = (ub - lb) / increment;
176     int lowerNeglected = 0;
177     int upperNeglected = 0;
178 
179     switch (side) {
180       case RIGHT -> {
181         lowerNeglected = lb;
182         upperNeglected = ub - (increment * nBins);
183         area =
184             IntStream.range(0, nBins).parallel().mapToDouble(
185                 (int i) -> {
186                   int fromPoint = ub - (increment * i);
187                   return (7 * points[fromPoint - 4]
188                       + 32 * points[fromPoint - 3]
189                       + 12 * points[fromPoint - 2]
190                       + 32 * points[fromPoint - 1]
191                       + 7 * points[fromPoint]);
192                 }).sum();
193       }
194       case LEFT -> {
195         lowerNeglected = lb + (increment * nBins);
196         upperNeglected = ub;
197         area = IntStream.range(0, nBins).parallel().mapToDouble(
198             (int i) -> {
199               int fromPoint = lb + (increment * i);
200               return (7 * points[fromPoint]
201                   + 32 * points[fromPoint + 1]
202                   + 12 * points[fromPoint + 2]
203                   + 32 * points[fromPoint + 3]
204                   + 7 * points[fromPoint + 4]);
205             }).sum();
206       }
207     }
208     area *= BOOLE_FACTOR;
209     area *= width;
210 
211     area += finishIntegration(data, side, lowerNeglected, upperNeglected, IntegrationType.BOOLE);
212     return area;
213   }
214 
215   /**
216    * Generates a set of points along x.
217    *
218    * @param lb            Beginning value, inclusive
219    * @param ub            Ending value, inclusive
220    * @param nPoints       Total number of points
221    * @param halfWidthEnds If ends should have 1/2 regular separation
222    * @return an array of double values.
223    */
224   public static double[] generateXPoints(double lb, double ub, int nPoints, boolean halfWidthEnds) {
225     if (lb >= ub) {
226       throw new IllegalArgumentException("ub must be greater than lb");
227     }
228     double[] points = new double[nPoints];
229     int sepDivisor = halfWidthEnds ? nPoints - 2 : nPoints - 1;
230     double sep = (ub - lb) / ((double) sepDivisor);
231 
232     if (halfWidthEnds) {
233       points[0] = lb;
234       points[nPoints - 1] = ub;
235       for (int i = 1; i < nPoints - 1; i++) {
236         points[i] = lb + ((double) i) * sep - 0.5 * sep;
237       }
238     } else {
239       for (int i = 0; i < nPoints; i++) {
240         points[i] = lb + ((double) i) * sep;
241       }
242     }
243     return points;
244   }
245 
246   /**
247    * Returns the contribution of each bin to the overall integral as an array; will be most accurate
248    * at break-points for the integration type. Overall integral is the sum of the array of doubles,
249    * plus or minus minor rounding errors.
250    *
251    * <p>If N is a breakpoint for Boole's rule: N+1 is a trapezoid from N to N+1. N+2 is Simpson's
252    * from N to N+2, minus the prior trapezoid (N+1). N+3 is 4-point integration from N to N+3, minus
253    * the N to N+2 parabola. N+4 is the full Boole's Rule from N to N+4, minus the N to N+3 4-point
254    * integral.
255    *
256    * @param data    Data to integrate
257    * @param side    Side to integrate from
258    * @param maxType Maximum rule to be used
259    * @return Per-bin contributions to integral.
260    */
261   public static double[] integrateByBins(
262       DataSet data, IntegrationSide side, IntegrationType maxType) {
263     boolean halfWide = data.halfWidthEnds();
264     double[] fX = data.getAllFxPoints();
265     int numPoints = data.numPoints();
266     double width = data.binWidth();
267     double[] values = new double[numPoints];
268 
269     int lb = halfWide ? 1 : 0;
270     int ub = halfWide ? numPoints - 2 : numPoints - 1;
271 
272     int increment = maxType.pointsNeeded() - 1;
273     increment = Math.max(1, increment); // Deal w/ rectangle integration.
274 
275     switch (side) {
276       case RIGHT -> {
277         values[ub] = width * fX[ub]; // Begin with rectangle.
278         /*
279           For each bin, its contribution to this sub-window's value is the integral from (start to
280           bin) minus the integral from (start to prior bin).
281          */
282         for (int i = ub - 1; i >= lb; i--) {
283           int fromUB = ub - i - 1;
284           fromUB /= increment;
285           fromUB *= increment;
286           int lastBegin = ub - fromUB;
287           double val = finishIntegration(data, side, i, lastBegin, maxType);
288           val -= finishIntegration(data, side, i + 1, lastBegin, maxType);
289           values[i] = val;
290         }
291         values[ub - 1] -= values[ub]; // Remove double-counting at start.
292         if (halfWide) {
293           if (maxType == IntegrationType.RECTANGULAR) {
294             values[1] += 0.5 * width * fX[1];
295             values[numPoints - 1] = (0.5 * width * fX[numPoints - 1]);
296           } else {
297             values[0] = 0.25 * width * fX[0];
298             values[1] += 0.25 * width * fX[1];
299             values[numPoints - 2] += (0.25 * width * fX[numPoints - 2]);
300             values[numPoints - 1] = (0.25 * width * fX[numPoints - 1]);
301           }
302         }
303       } // LEFT
304       case LEFT -> {
305         int shift = halfWide ? 1 : 0;
306         values[lb] = width * fX[lb]; // Begin with rectangle.
307 
308         /*
309           For each bin, its contribution to this sub-window's value is the integral from (start to
310           bin) minus the integral from (start to prior bin).
311          */
312         for (int i = lb + 1; i <= ub; i++) {
313           // Remove remainder via division-and-multiplication.
314           int lastBegin = ((i - 1 - shift) / increment);
315           lastBegin *= increment;
316           lastBegin += shift;
317           double val = finishIntegration(data, side, lastBegin, i, maxType);
318           val -= finishIntegration(data, side, lastBegin, i - 1, maxType);
319           values[i] = val;
320         }
321         values[lb + 1] -= values[lb]; // Remove double-counting at start.
322         if (halfWide) {
323           if (maxType == IntegrationType.RECTANGULAR) {
324             values[0] = 0.5 * width * fX[0];
325             values[numPoints - 2] += (0.5 * width * fX[numPoints - 2]);
326           } else {
327             values[0] = 0.25 * width * fX[0];
328             values[1] += 0.25 * width * fX[1];
329             values[numPoints - 2] += (0.25 * width * fX[numPoints - 2]);
330             values[numPoints - 1] = (0.25 * width * fX[numPoints - 1]);
331           }
332         }
333       }
334     }
335 
336     return values;
337   }
338 
339   /**
340    * Generic caller for 1D integration schemes given an IntegrationType.
341    *
342    * @param data To integrate
343    * @param side Integrate from side
344    * @param type Scheme to use
345    * @return Numeric integral
346    */
347   public static double integrateData(DataSet data, IntegrationSide side, IntegrationType type) {
348     switch (type) {
349       case RECTANGULAR -> {
350         return rectangular(data, side);
351       }
352       case TRAPEZOIDAL -> {
353         return trapezoidal(data, side);
354       }
355       case SIMPSONS -> {
356         return simpsons(data, side);
357       }
358       case BOOLE -> {
359         return booles(data, side);
360       }
361     }
362     logger.warning(
363         format(" Integration type %s not recognized! Defaulting to Simpson's integration", type));
364     return simpsons(data, side);
365   }
366 
367   /**
368    * Numerically integrates a data set using rectangular integration. Not recommended; preferably use
369    * at least trapezoidal integration.
370    *
371    * @param data Data set to integrate
372    * @param side Side to integrate from
373    * @return Area of integral
374    */
375   public static double rectangular(DataSet data, IntegrationSide side) {
376     double area = 0;
377     int lb = 0;
378     int ub = data.numPoints() - 1;
379     if (data.halfWidthEnds()) {
380       area = rectangularEnds(data, side);
381       ++lb;
382       --ub;
383     }
384     area += rectangular(data, side, lb, ub);
385     return area;
386   }
387 
388   /**
389    * Numerically integrates a data set, in bounds lb-ub inclusive, using rectangular integration. Not
390    * recommended; preferably use at least trapezoidal integration.
391    *
392    * @param data Data set to integrate
393    * @param side Side to integrate from
394    * @param lb   First index to integrate over
395    * @param ub   Last index to integrate over
396    * @return Area of integral
397    */
398   public static double rectangular(DataSet data, IntegrationSide side, int lb, int ub) {
399     double area = 0;
400     double width = data.binWidth();
401     double[] points = data.getAllFxPoints();
402     assert ub > lb;
403     assert ub < points.length;
404 
405     switch (side) {
406       case RIGHT -> {
407         for (int i = ub; i > lb; i--) {
408           area += (width * points[i]);
409         }
410       }
411       case LEFT -> {
412         for (int i = lb; i < ub; i++) {
413           area += width * points[i];
414         }
415       }
416     }
417 
418     return area;
419   }
420 
421   /**
422    * Treats half-width bins at the ends of a DataSet using rectangular integration. Not recommended,
423    * preferably use trapezoidal integration.
424    *
425    * @param data Data set to integrate
426    * @param side Side to integrate from
427    * @return Estimate of area in half-width start/end bins.
428    */
429   public static double rectangularEnds(DataSet data, IntegrationSide side) {
430     double width = 0.5 * data.binWidth();
431     double area = 0;
432     int nPoints = data.numPoints();
433     switch (side) {
434       case LEFT -> {
435         area = data.getFxPoint(0) * width;
436         area += (data.getFxPoint(nPoints - 2) * width);
437       }
438       case RIGHT -> {
439         area = data.getFxPoint(1) * width;
440         area += (data.getFxPoint(nPoints - 1) * width);
441       }
442     }
443     return area;
444   }
445 
446   /**
447    * Numerically integrates a data set using parallelized rectangular integration. Not recommended;
448    * preferably use at least trapezoidal integration, and avoid parallelized versions unless
449    * necessary.
450    *
451    * @param data Data set to integrate
452    * @param side Side to integrate from
453    * @return Area of integral
454    */
455   public static double rectangularParallel(DataSet data, IntegrationSide side) {
456     double area = 0;
457     int lb = 0;
458     int ub = data.numPoints() - 1;
459     if (data.halfWidthEnds()) {
460       area = rectangularEnds(data, side);
461       ++lb;
462       --ub;
463     }
464     area += rectangularParallel(data, side, lb, ub);
465     return area;
466   }
467 
468   /**
469    * Numerically integrates a data set, in bounds lb-ub inclusive, using rectangular integration. Not
470    * recommended; preferably use at least trapezoidal integration. Also, prefer parallelized versions
471    * unless necessary.
472    *
473    * @param data Data set to integrate
474    * @param side Side to integrate from
475    * @param lb   First index to integrate over
476    * @param ub   Last index to integrate over
477    * @return Area of integral
478    */
479   public static double rectangularParallel(DataSet data, IntegrationSide side, int lb, int ub) {
480     double width = data.binWidth();
481     double[] points = data.getAllFxPoints();
482     assert ub > lb;
483     assert ub < points.length;
484 
485     if (side == IntegrationSide.RIGHT) {
486       ++ub;
487       ++lb;
488     }
489     return IntStream.range(lb, ub).parallel().mapToDouble(
490         (int i) -> points[i] * width).sum();
491   }
492 
493   /**
494    * Numerically integrates a data set using Simpson's rule.
495    *
496    * @param data Data set to integrate
497    * @param side Side to integrate from
498    * @return Area of integral
499    */
500   public static double simpsons(DataSet data, IntegrationSide side) {
501     double area = 0;
502     int lb = 0;
503     int ub = data.numPoints() - 1;
504     if (data.halfWidthEnds()) {
505       area = trapezoidalEnds(data, side);
506       lb++;
507       ub--;
508     }
509     area += simpsons(data, side, lb, ub);
510     return area;
511   }
512 
513   /**
514    * Numerically integrates a data set, in bounds lb-ub inclusive, using Simpson's rule.
515    *
516    * @param data Data set to integrate
517    * @param side Side to integrate from
518    * @param lb   First index to integrate over
519    * @param ub   Last index to integrate over
520    * @return Area of integral
521    */
522   public static double simpsons(DataSet data, IntegrationSide side, int lb, int ub) {
523     double area = 0;
524     double width = data.binWidth();
525     double[] points = data.getAllFxPoints();
526     int increment = 2;
527 
528     int nBins = (ub - lb) / increment;
529     int lowerNeglected = 0;
530     int upperNeglected = 0;
531 
532     switch (side) {
533       case RIGHT -> {
534         for (int i = ub; i > (lb + increment - 1); i -= increment) {
535           area += points[i] + (4 * points[i - 1]) + points[i - 2];
536         }
537         lowerNeglected = lb;
538         upperNeglected = ub - (increment * nBins);
539       }
540       case LEFT -> {
541         for (int i = lb; i < (ub - increment + 1); i += increment) {
542           area += points[i] + (4 * points[i + 1]) + points[i + 2];
543         }
544         lowerNeglected = lb + (increment * nBins);
545         upperNeglected = ub;
546       }
547     }
548 
549     area *= ONE_THIRD;
550     area *= width;
551 
552     area += finishIntegration(data, side, lowerNeglected, upperNeglected, IntegrationType.SIMPSONS);
553     return area;
554   }
555 
556   /**
557    * Numerically integrates a data set using Boole's rule. The sequential version is preferred unless
558    * necessary.
559    *
560    * @param data Data set to integrate
561    * @param side Side to integrate from
562    * @return Area of integral
563    */
564   public static double simpsonsParallel(DataSet data, IntegrationSide side) {
565     double area = 0;
566     int lb = 0;
567     int ub = data.numPoints() - 1;
568     if (data.halfWidthEnds()) {
569       area = trapezoidalEnds(data, side);
570       lb++;
571       ub--;
572     }
573     area += simpsonsParallel(data, side, lb, ub);
574     return area;
575   }
576 
577   /**
578    * Numerically integrates a data set, in bounds lb-ub inclusive, using Simpson's rule. The
579    * sequential version is preferred unless necessary.
580    *
581    * @param data Data set to integrate
582    * @param side Side to integrate from
583    * @param lb   First index to integrate over
584    * @param ub   Last index to integrate over
585    * @return Area of integral
586    */
587   public static double simpsonsParallel(DataSet data, IntegrationSide side, int lb, int ub) {
588     double area = 0;
589     double width = data.binWidth();
590     double[] points = data.getAllFxPoints();
591     int increment = 2;
592     int nBins = (ub - lb) / increment;
593     int lowerNeglected = 0;
594     int upperNeglected = 0;
595 
596     switch (side) {
597       case RIGHT -> {
598         lowerNeglected = lb;
599         upperNeglected = ub - (increment * nBins);
600         area = IntStream.range(0, nBins).parallel().mapToDouble(
601             (int i) -> {
602               int fromPoint = ub - (increment * i);
603               return (points[fromPoint - 2] + 4 * points[fromPoint - 1] + points[fromPoint]);
604             }).sum();
605       }
606       case LEFT -> {
607         area = IntStream.range(0, nBins).parallel().mapToDouble(
608             (int i) -> {
609               int fromPoint = lb + (increment * i);
610               return (points[fromPoint] + 4 * points[fromPoint + 1] + points[fromPoint + 2]);
611             }).sum();
612         lowerNeglected = lb + (increment * nBins);
613         upperNeglected = ub;
614       }
615     }
616 
617     area *= ONE_THIRD;
618     area *= width;
619     area += finishIntegration(data, side, lowerNeglected, upperNeglected, IntegrationType.SIMPSONS);
620     return area;
621   }
622 
623   /**
624    * Numerically integrates a data set using trapezoidal integration. For most data sets, Simpson's
625    * rule or Boole's rule will out-perform trapezoidal integration.
626    *
627    * @param data Data set to integrate
628    * @param side Side to integrate from
629    * @return Area of integral
630    */
631   public static double trapezoidal(DataSet data, IntegrationSide side) {
632     double area = 0;
633     int lb = 0;
634     int ub = data.numPoints() - 1;
635     if (data.halfWidthEnds()) {
636       area = trapezoidalEnds(data, side);
637       lb++;
638       ub--;
639     }
640     area += trapezoidal(data, side, lb, ub);
641     return area;
642   }
643 
644   /**
645    * Numerically integrates a data set, in bounds lb-ub inclusive, using trapezoidal integration. For
646    * most data sets, Simpson's rule or Boole's rule will out-perform trapezoidal integration.
647    *
648    * @param data Data set to integrate
649    * @param side Side to integrate from
650    * @param lb   First index to integrate over
651    * @param ub   Last index to integrate over
652    * @return Area of integral
653    */
654   public static double trapezoidal(DataSet data, IntegrationSide side, int lb, int ub) {
655     double width = data.binWidth();
656     double[] points = data.getAllFxPoints();
657 
658     double area = 0.5 * points[lb];
659     area += 0.5 * points[ub];
660     for (int i = lb + 1; i < ub; i++) {
661       area += points[i];
662     }
663     area *= width;
664     return area;
665   }
666 
667   /**
668    * Treats half-width bins at the ends of a DataSet using trapezoidal integration.
669    *
670    * @param data Data set to integrate
671    * @param side Side to integrate from
672    * @return Estimate of area in half-width start/end bins.
673    */
674   public static double trapezoidalEnds(DataSet data, IntegrationSide side) {
675     double width = 0.5 * data.binWidth();
676     int nPts = data.numPoints();
677     double area = data.getFxPoint(0) + data.getFxPoint(1)
678         + data.getFxPoint(nPts - 2) + data.getFxPoint(nPts - 1);
679     area *= (0.5 * width);
680     return area;
681   }
682 
683   /**
684    * Numerically integrates a data set using trapezoidal integration. For most data sets, Simpson's
685    * rule or Boole's rule will out-perform trapezoidal integration. Prefer use of the sequential
686    * version unless necessary.
687    *
688    * @param data Data set to integrate
689    * @param side Side to integrate from
690    * @return Area of integral
691    */
692   public static double trapezoidalParallel(DataSet data, IntegrationSide side) {
693     double area = 0;
694     int lb = 0;
695     int ub = data.numPoints() - 1;
696     if (data.halfWidthEnds()) {
697       area = trapezoidalEnds(data, side);
698       lb++;
699       ub--;
700     }
701     area += trapezoidalParallel(data, side, lb, ub);
702     return area;
703   }
704 
705   /**
706    * Numerically integrates a data set, in bounds lb-ub inclusive, using trapezoidal integration. For
707    * most data sets, Simpson's rule or Boole's rule will out-perform trapezoidal integration. Prefer
708    * use of the sequential version unless necessary.
709    *
710    * @param data Data set to integrate
711    * @param side Side to integrate from
712    * @param lb   First index to integrate over
713    * @param ub   Last index to integrate over
714    * @return Area of integral
715    */
716   public static double trapezoidalParallel(DataSet data, IntegrationSide side, int lb, int ub) {
717     double width = data.binWidth();
718     double[] points = data.getAllFxPoints();
719 
720     double area = 0.5 * points[lb];
721     area += 0.5 * points[ub];
722     area += IntStream.range(lb + 1, ub).parallel().mapToDouble(
723         (int i) -> points[i]).sum();
724     area *= width;
725     return area;
726   }
727 
728   /**
729    * Integrates the remaining points after higher-order integration rules cannot evenly fit over the
730    * remaining data. For example, Boole's rule requires 5 points; if a data set has 7 points, it will
731    * integrate the first 5, but not handle the remaining 2; this method will, apply the highest-order
732    * integration rule that the remaining points (including the last one used previously) permit. In
733    * that case, it would be Simpson's rule.
734    *
735    * @param data Finish numerical integration on
736    * @param side Integration side
737    * @param lb   Index of lower bound to complete integration on
738    * @param ub   Index of upper bound to complete integration on
739    * @param type Highest-order rule permitted to finish integration
740    * @return Area of un-integrated region
741    */
742   private static double finishIntegration(
743       DataSet data, IntegrationSide side, int lb, int ub, IntegrationType type) {
744     int totPoints = (ub - lb);
745 
746     int perBin = type.pointsNeeded();
747     int increment = perBin - 1;
748     increment = Math.max(1, increment); // Needed for rectangular integration
749 
750     int nBins = totPoints / increment;
751     int remainder = totPoints % increment;
752 
753     IntegrateWindow intMode = switch (type) {
754       case BOOLE -> Integrate1DNumeric::booles;
755       case SIMPSONS -> Integrate1DNumeric::simpsons;
756       case RECTANGULAR -> Integrate1DNumeric::rectangular;
757       case TRAPEZOIDAL -> Integrate1DNumeric::trapezoidal;
758     };
759 
760     double area = 0.0;
761 
762     switch (side) {
763       case RIGHT -> {
764         for (int i = ub; i > (lb - 1 + increment); i -= increment) {
765           area += intMode.toArea(data, side, (i - increment), i);
766         }
767         ub -= (nBins * increment);
768       }
769       case LEFT -> {
770         for (int i = lb; i < (ub + 1 - increment); i += increment) {
771           area += intMode.toArea(data, side, i, (i + increment));
772         }
773         lb += (nBins * increment);
774       }
775     }
776 
777     assert remainder == (ub - lb);
778     switch (remainder) {
779       case 0:
780         break;
781       case 1:
782         area += trapezoidal(data, side, lb, ub);
783         break;
784       case 2:
785       case 3:
786         area += simpsons(data, side, lb, ub);
787         break;
788       case 4:
789       default:
790         throw new IllegalArgumentException("This should not be currently possible.");
791     }
792     return area;
793   }
794 
795   /**
796    * Left vs right-hand integration; left-hand integration will start from the first available point,
797    * run right as far as possible, and then clean up any remaining points using finishIntegration,
798    * while right-hand integration will start from the last available point, run left as far as
799    * possible, and then clean up any remaining points using finishIntegration.
800    *
801    * <p>Values: LEFT, RIGHT.
802    *
803    * <p>Not meaningful for trapezoidal integration; there will never be any unused points, and the
804    * method is symmetrical.
805    */
806   public enum IntegrationSide {
807     /**
808      * Left-hand integration.
809      */
810     LEFT,
811     /**
812      * Right-hand integration.
813      */
814     RIGHT
815   }
816 
817   /**
818    * Enumeration of implemented integration methods, and the number of points required by them.
819    */
820   public enum IntegrationType {
821     /**
822      * Rectangular integration, requiring 1 point.
823      */
824     RECTANGULAR(1),
825     /**
826      * Trapezoidal integration, requiring 2 points.
827      */
828     TRAPEZOIDAL(2),
829     /**
830      * Simpson's Three Point Integration, requiring 3 points.
831      */
832     SIMPSONS(3),
833     /**
834      * Boole's Five Point Integration, requiring 5 points.
835      */
836     BOOLE(5);
837 
838     private final int pointsNeeded;
839 
840     /**
841      * Constructor for IntegrationType.
842      *
843      * @param points Number of points required for integration.
844      */
845     IntegrationType(int points) {
846       pointsNeeded = points;
847     }
848 
849     /**
850      * Returns the number of points required for integration.
851      *
852      * @return Number of points required for integration.
853      */
854     public final int pointsNeeded() {
855       return pointsNeeded;
856     }
857   }
858 
859   /**
860    * Functional interface used to select an integration method (primarily for finishIntegration).
861    */
862   @FunctionalInterface
863   private interface IntegrateWindow {
864 
865     /**
866      * Numerically integrates a range of x given f(x).
867      *
868      * @param data x and f(x) data to integrate
869      * @param side Side to integrate from
870      * @param lb   Lower bound of x[] to integrate
871      * @param ub   Upper bound of x[] to integrate
872      * @return Area of integral f(x) dx, from point lb to point ub
873      */
874     double toArea(DataSet data, IntegrationSide side, int lb, int ub);
875   }
876 }