1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
//! Numerical integration routines written in Rust.
//!
//!
//!
//! # Overview
//!
//! This library contains numerical integration routines for both functions of one dimension (see
//! [`quadrature`]) and functions with multiple dimensions up to $N = 15$ (see [`multi`]). The
//! basic principle of the library is to expose all of the routines through the trait system. Each
//! of the one- and multi-dimensional integrators take as a parameter a type implementing the
//! trait [`Integrand`].
//!
//! The integration routines attempt to make approximations to integrals such as the
//! one-dimensional integral,
//! $$
//! I = \int_{b}^{a} f(x) dx
//! $$
//! or the $N$-dimensional
//! $$
//! I = \int_{\Sigma_{N}} f(\mathbf{x}) d\mathbf{x}
//! $$
//! where $\mathbf{x} = (x_{1}, x_{2}, \dots, x_{N})$ and $\Sigma_{N}$ is an $N$-dimensional
//! hypercube. The functions $f(x)$ and $f(\mathbf{x})$ can be real valued, with return type
//! [`f64`] _or_ complex valued with return type [`Complex<f64>`]. The numerical integration
//! routines approximate the integral of a function by performing a weighted sum of the function
//! evaluated at defined points/abscissae. For example, in the one-dimensional case,
//! $$
//! I = \int_{b}^{a} f(x) dx \approx \sum_{i = 1}^{n} W_{i} f(X_{i}) = I_{n}
//! $$
//! where the $X_{i}$ and $W_{i}$ are the rescaled abscissae and weights,
//! $$
//! X_{i} = \frac{b + a + (a - b) x_{i}}{2} ~~~~~~~~ W_{i} = \frac{(a - b) w_{i}}{2}
//! $$
//! Integration rules have a polynomial order. Rules of higher polynomial order use more
//! points/abscissae and weights for evaluating the sum.
//!
//! The library contains both non-adaptive and adaptive integration routines. In the case of an
//! adaptive routine, the user supplies an error [`Tolerance`] which acts as a goal for the
//! numerical error estimate. The numerical integration is considered successful when the numerical
//! error is less than the user supplied tolerance. On success, the output of the numerical
//! integration is an [`IntegralEstimate`].
//!
//!
//! # [`Integrand`] trait
//!
//! The primary entry point for the library is the [`Integrand`] trait. A type implementing the
//! [`Integrand`] trait represents a real or complex valued function which is to be integrated. The
//! trait requires the definition of two associated types, [`Integrand::Point`] and
//! [`Integrand::Scalar`], and an implementation of the method [`Integrand::evaluate`].
//!
//! - [`Integrand::Point`]: This associated type defines the point at which the function is to be
//! evaluated, and determines the types of numerical integrators which are available to the user to
//! integrate the function. Integrators are provided for univariate functions $f(x)$ through the
//! associated type `Point=f64`, while integrators for multivariate functions $f(\mathbf{x})$ are
//! provided through the associated type `Point=[f64;N]` where $N$ is the dimensionality of the
//! point $\mathbf{x}=(x_{1},\dots,x_{N})$ which is limited to $2 \le N \le 15$.
//!
//! - [`Integrand::Scalar`]: This is the output type of the function to be integrated. A _real_
//! valued function should have the output type `Scalar=`[`f64`], while a _complex_ valued function
//! should have output type `Scalar=`[`Complex<f64>`].
//!
//! - [`Integrand::evaluate`]: The trait requires an implementation of an `evaluate` method, which
//! defines how the function takes the input [`Integrand::Point`] and turns this into the output
//! type [`Integrand::Scalar`]. In other words, this method tells the integrators how to evaluate
//! the function at a point, allowing the integration to be done.
//!
//! As an example, consider probability density function of a normal distribution,
//! $$
//! f(x) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} e^{- \frac{(x - \mu)^{2}}{2 \sigma^{2}}}
//! $$
//! which has mean $\mu$ and standard deviation $\sigma$. To integrate this function, one first
//! implements the [`Integrand`] trait,
//!```rust
//! use rint::Integrand;
//!
//! struct NormalDist {
//! mean: f64,
//! standard_dev: f64,
//! }
//!
//! impl Integrand for NormalDist {
//! type Point = f64;
//! type Scalar = f64;
//! fn evaluate(&self, x: &Self::Point) -> Self::Scalar {
//! let mu = self.mean;
//! let sigma = self.standard_dev;
//!
//! let prefactor = 1.0 / (2.0 * std::f64::consts::PI * sigma.powi(2));
//! let exponent = - (x - mu).powi(2) / (2.0 * sigma.powi(2));
//!
//! prefactor * exponent.exp()
//! }
//! }
//!```
//! The type `NormalDist` can then be passed as a parameter to a one-dimensional integration
//! routine in the module [`quadrature`] and integrated over a given (possibly infinite) region.
//! Since [`Integrand::evaluate`] does not return a [`Result`] it is the users responsibility to
//! ensure that the evaluation implementation is correct.
//!
//!
//! # Modules
//!
//! ## [`quadrature`]
//!
//! The [`quadrature`] module provides a number of numerical quadrature integration routines of a
//! function in one dimension. The routines are based primarily on the algorithms presented in the
//! Fortran library [QUADPACK] (Piessens, de Doncker-Kapenga, Ueberhuber and Kahaner) and
//! reimplemented in the [GNU scientific library] (GSL) (Gough, Alken, Gonnet, Holoborodko,
//! Griessinger). The integrators use Gauss-Kronrod integration rules, combining two rules of
//! different order for efficient estimation of the numerical error. The rules for an $n$-point
//! Gauss-Kronrod rule contain $m = (n - 1) / 2$ abscissae _shared_ by the Gaussian and Kronrod
//! rules and an extended set of $n - m$ Kronrod abscissae. Thus, only $n$ total function
//! evaluations are required for both the integral and error estimates.
//!
//! The `rint` library departs from the naming conventions of the [QUADPACK] and [GSL], but
//! provides a selection of comparable implementations:
//!
//! - [`quadrature::Basic`]: A non-adaptive routine which applies a provided Gauss-Kronrod
//! integration [`quadrature::Rule`] to a function exactly once.
//!
//! - [`quadrature::Adaptive`]: A one-dimensional adaptive routine based on the `qag.f` [QUADPACK]
//! and `qag.c` [GSL] implementations. The integration is region is bisected into subregions and an
//! initial estimate is performed. Upon each further iteration of the routine the subregion with
//! the highest numerical error estimate is bisected again and new estimates are calculated for
//! these newly bisected regions. This concentrates the integration refinement to the regions with
//! highest error, rapidly reducing the numerical error of the routine. Gauss-Kronrod integration
//! [`quadrature::Rule`]s are provided of various order to use with the adaptive algorithm.
//!
//! - [`quadrature::AdaptiveSingularity`]: A one-dimensional adaptive routine based on the `qags.f`
//! [QUADPACK] and `qags.c` [GSL] implementations. Adaptive routines concentrate new subintervals
//! around the region of highest error. If this region contains an integrable singularity, then the
//! adaptive routine of [`quadrature::Adaptive`] may fail to obtain a suitable estimate. However,
//! this can be combined with an extrapolation proceedure such as the Wynn epsilon-algorithm to
//! extrapolate the value at these integrable singularities and provide a suitable estimate. As
//! well as handling integrable singularities, [`quadrature::AdaptiveSingularity`] can be used to
//! calculate integrals with infinite or semi-infinite bounds by using the appropriate constructors.
//!
//! [QUADPACK]: <https://www.netlib.org/quadpack/>
//! [GNU Scientific Library]: <https://www.gnu.org/software/gsl/doc/html/integration.html>
//! [GSL]: <https://www.gnu.org/software/gsl/doc/html/integration.html>
//!
//! [`quadrature::Rule`]: crate::quadrature::Rule
//!
//! ## [`multi`]
//!
//! The [`multi`] module provides numerical integration routines for integrating functions with
//! dimensionality between $2 \le N \le 15$. The functions can be either real-valued or complex,
//! and are integrated over an $N$-dimensional hypercube. The routines are based primarily on the
//! [DCUHRE] FORTRAN library (Bernsten, Espelid, Genz), which use sets of fully-symmetric
//! integration rules to obtain integral and error estimates. Unlike the original algorithm the
//! routines presented in [`multi`] currently only operate on a single function _not_ a vector of
//! functions. The module provides two classes of routine:
//!
//! - [`multi::Basic`]: A non-adaptive routine which applies a fully-symmetric integration rule
//! [`multi::Rule`] to a multi-dimensional function exactly once.
//!
//! - [`multi::Adaptive`]: A $2 \le N \le 15$ dimensional adaptive routine with a similar approach
//! to the one-dimensional adaptive routines found in [`quadrature`]. On each iteration of the
//! algorithm the axis along which the largest contribution to the error estimate was obtained is
//! used as the bisection axis to bisect the integration region and then calculate new estimates
//! for these newly bisected volumes. This concentrates the integration refinement to the regions
//! with highest error, rapidly reducing the numerical error of the routine. The algorithm uses
//! fully-symmetric integration rules, [`crate::multi::Rule`], of varying order and generality.
//! These are generated through the `Rule*::generate` constructors.
//!
//! [`multi::Rule`]: crate::multi::Rule
//! [DCUHRE]: <https://dl.acm.org/doi/10.1145/210232.210234>
//!
//! # One-dimensional example
//!
//! The following example integrates the function
//! $$
//! f(x) = \frac{\log(x)}{(1 + 100 x^{2})}
//! $$
//! over the
//! semi-infinite interval $0 < x < \infty$ using an adaptive integration routine with singularity
//! detection (see [`quadrature::AdaptiveSingularity`]). Adapted from the [QUADPACK] & [GSL] numerical
//! integration test suites.
//!
//! [`quadrature::AdaptiveSingularity`]: crate::quadrature::AdaptiveSingularity
//!
//!```rust
//! use rint::{Limits, Integrand, Tolerance};
//! use rint::quadrature::AdaptiveSingularity;
//!
//! struct F;
//!
//! impl Integrand for F {
//! type Point = f64;
//! type Scalar = f64;
//! fn evaluate(&self, x: &Self::Point) -> Self::Scalar {
//! x.ln() / (1.0 + 100.0 * x.powi(2))
//! }
//! }
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let function = F;
//! let lower = 0.0;
//! let tolerance = Tolerance::Relative(1.0e-3);
//!
//! let integrator = AdaptiveSingularity::semi_infinite_upper(&function, lower, tolerance, 1000)?;
//!
//! let exp_result = -3.616_892_186_127_022_568E-01;
//! let exp_error = 3.016_716_913_328_831_851E-06;
//!
//! let integral = integrator.integrate()?;
//! let result = integral.result();
//! let error = integral.error();
//!
//! let tol = 1.0e-9;
//! assert!((exp_result - result).abs() / exp_result.abs() < tol);
//! assert!((exp_error - error).abs() / exp_error.abs() < tol);
//! # Ok(())
//! # }
//!```
//!
//! # Multi-dimensional example
//!
//! The following example integtates a 4-dimensional function $f(\mathbf{x})$,
//! $$
//! f(\mathbf{x}) = \frac{x_{3}^{2} x_{4} e^{x_{3} x_{4}}}{(1 + x_{1} + x_{2})^{2}}
//! $$
//! where $\mathbf{x} = (x_{1}, x_{2}, x_{3}, x_{4})$ over an $N=4$ dimensional hypercube
//! $((0,1),(0,1),(0,2),(0,1))$ using a fully-symmetric 7-point adaptive algorithm.
//! Adapted from P. van Dooren & L. de Ridder, "An adaptive algorithm for numerical integration over
//! an n-dimensional cube", J. Comp. App. Math., Vol. 2, (1976) 207-217
//!
//!```rust
//! use rint::{Limits, Integrand, Tolerance};
//! use rint::multi::{Adaptive, Rule07};
//!
//! const N: usize = 4;
//!
//! struct F;
//!
//! impl Integrand for F {
//! type Point = [f64; N];
//! type Scalar = f64;
//! fn evaluate(&self, coordinates: &[f64; N]) -> Self::Scalar {
//! let [x1, x2, x3, x4] = coordinates;
//! x3.powi(2) * x4 * (x3 * x4).exp() / (x1 + x2 + 1.0).powi(2)
//! }
//! }
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! const TARGET: f64 = 5.753_641_449_035_616e-1;
//! const TOL: f64 = 1e-2;
//!
//! let function = F;
//! let limits = [
//! Limits::new(0.0, 1.0)?,
//! Limits::new(0.0, 1.0)?,
//! Limits::new(0.0, 1.0)?,
//! Limits::new(0.0, 2.0)?
//! ];
//! let rule = Rule07::<N>::generate()?;
//! let tolerance = Tolerance::Relative(TOL);
//!
//! let integrator = Adaptive::new(&function, &rule, limits, tolerance, 10000)?;
//!
//! let integral = integrator.integrate()?;
//! let result = integral.result();
//! let error = integral.error();
//!
//! let actual_error = (result - TARGET).abs();
//! let requested_error = TOL * result.abs();
//!
//! assert!(actual_error < error);
//! assert!(error < requested_error);
//! # Ok(())
//! # }
//!```
use ;
use Zero;
use ;
pub use IntegralEstimate;
pub use Limits;
/// A real or complex-valued function to be integrated.
///
/// The primary entry point for the library is the [`Integrand`] trait. A type implementing the
/// [`Integrand`] trait represents a real or complex valued function which is to be integrated. The
/// trait requires the definition of two associated types, [`Integrand::Point`] and
/// [`Integrand::Scalar`], and an implementation of the method [`Integrand::evaluate`].
///
/// - [`Integrand::Point`]: This associated type defines the point at which the function is to be
/// evaluated, and determines the types of numerical integrators which are available to the user to
/// integrate the function. Integrators are provided for univariate functions $f(x)$ through the
/// associated type `Point=f64`, while integrators for multivariate functions $f(\mathbf{x})$ are
/// provided through the associated type `Point=[f64;N]` where $N$ is the dimensionality of the
/// point $\mathbf{x}=(x_{1},\dots,x_{N})$ which is limited to $2 \le N \le 15$.
///
/// - [`Integrand::Scalar`]: This is the output type of the function to be integrated. A _real_
/// valued function should have the output type `Scalar=`[`f64`], while a _complex_ valued function
/// should have output type `Scalar=`[`Complex<f64>`].
///
/// - [`Integrand::evaluate`]: The trait requires an implementation of an `evaluate` method, which
/// defines how the function takes the input [`Integrand::Point`] and turns this into the output
/// type [`Integrand::Scalar`]. In other words, this method tells the integrators how to evaluate
/// the function at a point, allowing the integration to be done.
///
/// # Examples
///
/// ## One-dimensional example
///
/// For example, consider probability density function of a normal distribution,
/// $$
/// f(x) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} e^{- \frac{(x - \mu)^{2}}{2 \sigma^{2}}}
/// $$
/// which has mean $\mu$ and standard deviation $\sigma$. To integrate this function, one first
/// implements the [`Integrand`] trait,
///```rust
/// use rint::Integrand;
///
/// struct NormalDist {
/// mean: f64,
/// standard_dev: f64,
/// }
///
/// impl Integrand for NormalDist {
/// type Point = f64;
/// type Scalar = f64;
/// fn evaluate(&self, x: &Self::Point) -> Self::Scalar {
/// let mu = self.mean;
/// let sigma = self.standard_dev;
///
/// let prefactor = 1.0 / (2.0 * std::f64::consts::PI * sigma.powi(2));
/// let exponent = - (x - mu).powi(2) / (2.0 * sigma.powi(2));
///
/// prefactor * exponent.exp()
/// }
/// }
///```
/// The type `NormalDist` can then be used as the `function` parameter in one of the numerical
/// integration routines provided by the [`quadrature`] module.
///
/// ## Multi-dimensional example
///
/// For example, consider a nonlinear 4-dimensional function $f(\mathbf{x})$,
/// $$
/// f(\mathbf{x}) = a \frac{x_{3}^{2} x_{4} e^{x_{3} x_{4}}}{(1 + x_{1} + x_{2})^{2}}
/// $$
/// where $\mathbf{x} = (x_{1}, x_{2}, x_{3}, x_{4})$, which has some amplitude $a$. To integrate
/// this function, one first implements the [`Integrand`] trait,
///```rust
/// use rint::Integrand;
///
/// const N: usize = 4;
///
/// struct F {
/// amplitude: f64,
/// }
///
/// impl Integrand for F {
/// type Point = [f64; N];
/// type Scalar = f64;
/// fn evaluate(&self, coordinates: &[f64; N]) -> Self::Scalar {
/// let [x1, x2, x3, x4] = coordinates;
/// let a = self.amplitude;
/// a * x3.powi(2) * x4 * (x3 * x4).exp() / (x1 + x2 + 1.0).powi(2)
/// }
/// }
///```
/// The type `F` can then be used as the `function` parameter in one of the numerical integration
/// routines provided by the [`multi`] module.
/// A numerical scalar.
///
/// The [`Integrand`] trait is implemented for both real- and complex-valued functions of a _real_
/// variable. The *sealed* trait [`ScalarF64`] is implemented for both [`f64`] and [`Complex<f64>`].
pub
/// User selected tolerance type.
///
/// The adaptive routines will return the first approximation, `result`, to the integral which has an
/// absolute `error` smaller than the tolerance set by the choice of [`Tolerance`], where
/// * [`Tolerance::Absolute(abserr)`] specifies an absolute error and returns final
/// [`IntegralEstimate`] when `error <= abserr`,
/// * [`Tolerance::Relative(relerr)`] specifies a relative error and returns final
/// [`IntegralEstimate`] when `error <= relerr * abs(result)`,
/// * [`Tolerance::Either{ abserr, relerr }`] to return a result as soon as _either_ the relative or
/// absolute error bound has been satisfied.
///
/// [`Tolerance::Absolute(abserr)`]: crate::Tolerance#variant.Absolute
/// [`Tolerance::Relative(relerr)`]: crate::Tolerance#variant.Relative
/// [`Tolerance::Either{ abserr, relerr }`]: crate::Tolerance#variant.Either
/// General error enum for the `rint` crate.
///
/// Errors can occur in two ways in the `rint` crate, either on initialisation/setup of the
/// integrators _before_ numerical integration has been attempted *or* as a result of
/// encountering an error _during_ the running of the numerical integration routine. As a result,
/// the [`Error`] enum provides two variants [`Error::Initialisation`] to notify of errors on
/// initialisation and [`Error::Integration`] to notify of errors during integration. Each variant
/// holds a struct giving more information about the error that occurred---see
/// [`InitialisationError`] and [`IntegrationError`] for more details.
/// Error type for errors that occur during initialisation/setup of an integrator.
///
/// Errors can occur in two ways in the `rint` crate, either on initialisation/setup of the
/// integrators _before_ numerical integration has been attempted *or* as a result of
/// encountering an error _during_ the running of the numerical integration routine.
/// [`InitialisationError`] provides further details about the error that occurred during the
/// initialisation/setup of the integrators. The only reason that an initialisation error can occur
/// is when there is bad user input when generating either the [`Tolerance`], such as a negative
/// absolute tolerance value or a relative tolerance value which is too close to [`f64::EPSILON`],
/// or when an invalid dimensionality $N$ is used to generate a multi-dimensional integrator and/or
/// rule. The kind of error [`InitialisationErrorKind`] which occurred during initialisation is
/// obtained through the [`InitialisationError::kind`] method.
///
/// See also [`Error`] and [`IntegrationError`].
/// The kind of [`InitialisationError`] which occurred.
///
/// The only reason that an initialisation error can occur is when there is bad user input when
/// generating either the [`Tolerance`], such as a negative absolute tolerance value or a relative
/// tolerance value which is too close to [`f64::EPSILON`], or when an invalid dimensionality $N$
/// is used to generate a multi-dimensional integrator and/or rule.
/// Error type for errors that occur during integration of the user supplied function.
///
/// Errors can occur in two ways in the `rint` crate, either on initialisation/setup of the
/// integrators _before_ numerical integration has been attempted *or* as a result of
/// encountering an error _during_ the running of the numerical integration routine.
/// [`IntegrationError`] provides further details about the error that occurred during the
/// integration of the user supplied function, specifically the reason for the error
/// [`IntegrationErrorKind`] (accessed through the [`IntegrationError::kind`] method) and the
/// [`IntegralEstimate`] which was calculated up to the point that the error occurred (accessed
/// through the [`IntegrationError::estimate`] method). Errors typically occur during integration
/// due to, for example, difficult integration regions, non-integrable singularities, the maximum
/// number of iterations being reached, etc. See [`IntegrationErrorKind`] for more details.
///
/// See also [`Error`] and [`InitialisationError`].
/// The kind of [`IntegrationError`] which occurred.
///
/// Errors typically occur during integration due to, for example, difficult integration regions,
/// non-integrable singularities, the maximum number of iterations being reached, etc.