Skip to main content

rint/
lib.rs

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