#![allow(clippy::excessive_precision)]
use crate::errors::QlResult;
use crate::fail;
use crate::math::integrals::{Integrator, require_accuracy};
use crate::types::{Real, Size};
const ALPHA: Real = 0.816496580927726;
const BETA: Real = 0.4472135954999579;
const X1: Real = 0.94288241569547971906;
const X2: Real = 0.64185334234578130578;
const X3: Real = 0.23638319966214988028;
#[derive(Clone, Copy)]
struct Panel {
a: Real,
b: Real,
fa: Real,
fb: Real,
}
pub struct GaussLobattoIntegral {
max_evaluations: Size,
abs_accuracy: Real,
rel_accuracy: Option<Real>,
use_convergence_estimate: bool,
}
impl GaussLobattoIntegral {
pub fn new(max_evaluations: Size, abs_accuracy: Real) -> QlResult<Self> {
require_accuracy(abs_accuracy)?;
if max_evaluations < 15 {
fail!("required max evaluations ({max_evaluations}) must be >= 15");
}
Ok(GaussLobattoIntegral {
max_evaluations,
abs_accuracy,
rel_accuracy: None,
use_convergence_estimate: true,
})
}
pub fn with_relative_accuracy(mut self, rel_accuracy: Real) -> QlResult<Self> {
if !rel_accuracy.is_finite() || rel_accuracy < 0.0 {
fail!("relative accuracy must be finite and non-negative, got {rel_accuracy}");
}
self.rel_accuracy = Some(rel_accuracy);
Ok(self)
}
pub fn with_convergence_estimate(mut self, enabled: bool) -> Self {
self.use_convergence_estimate = enabled;
self
}
fn calculate_abs_tolerance<F>(
&self,
f: &mut F,
a: Real,
b: Real,
evaluations: &mut Size,
) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
let m = (a + b) / 2.0;
let h = (b - a) / 2.0;
let y1 = f(a);
let y3 = f(m - ALPHA * h);
let y5 = f(m - BETA * h);
let y7 = f(m);
let y9 = f(m + BETA * h);
let y11 = f(m + ALPHA * h);
let y13 = f(b);
let f1 = f(m - X1 * h);
let f2 = f(m + X1 * h);
let f3 = f(m - X2 * h);
let f4 = f(m + X2 * h);
let f5 = f(m - X3 * h);
let f6 = f(m + X3 * h);
let acc = h
* (0.0158271919734801831 * (y1 + y13)
+ 0.0942738402188500455 * (f1 + f2)
+ 0.1550719873365853963 * (y3 + y11)
+ 0.1888215739601824544 * (f3 + f4)
+ 0.1997734052268585268 * (y5 + y9)
+ 0.2249264653333395270 * (f5 + f6)
+ 0.2426110719014077338 * y7);
*evaluations += 13;
if acc == 0.0
&& (f1 != 0.0 || f2 != 0.0 || f3 != 0.0 || f4 != 0.0 || f5 != 0.0 || f6 != 0.0)
{
fail!("cannot calculate absolute accuracy from relative accuracy");
}
let mut r = 1.0;
if self.use_convergence_estimate {
let integral2 = (h / 6.0) * (y1 + y13 + 5.0 * (y5 + y9));
let integral1 = (h / 1470.0)
* (77.0 * (y1 + y13) + 432.0 * (y3 + y11) + 625.0 * (y5 + y9) + 672.0 * y7);
if (integral2 - acc).abs() != 0.0 {
r = (integral1 - acc).abs() / (integral2 - acc).abs();
}
if r == 0.0 || r > 1.0 {
r = 1.0;
}
}
Ok(match self.rel_accuracy {
Some(rel) => {
let rel_tol = rel.max(Real::EPSILON);
self.abs_accuracy.min(acc * rel_tol) / (r * Real::EPSILON)
}
None => self.abs_accuracy / (r * Real::EPSILON),
})
}
#[allow(clippy::float_cmp)]
fn adaptive_step<F>(
&self,
f: &mut F,
panel: Panel,
acc: Real,
evaluations: &mut Size,
) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
if *evaluations + 5 > self.max_evaluations {
fail!("maximum number of function evaluations reached");
}
let Panel { a, b, fa, fb } = panel;
let h = (b - a) / 2.0;
let m = (a + b) / 2.0;
let mll = m - ALPHA * h;
let ml = m - BETA * h;
let mr = m + BETA * h;
let mrr = m + ALPHA * h;
let fmll = f(mll);
let fml = f(ml);
let fm = f(m);
let fmr = f(mr);
let fmrr = f(mrr);
*evaluations += 5;
let integral2 = (h / 6.0) * (fa + fb + 5.0 * (fml + fmr));
let integral1 = (h / 1470.0)
* (77.0 * (fa + fb) + 432.0 * (fmll + fmrr) + 625.0 * (fml + fmr) + 672.0 * fm);
let dist = acc + (integral1 - integral2);
if dist == acc || mll <= a || b <= mrr {
if !(m > a && b > m) {
fail!("interval contains no more machine numbers");
}
Ok(integral1)
} else {
let panels = [
Panel {
a,
b: mll,
fa,
fb: fmll,
},
Panel {
a: mll,
b: ml,
fa: fmll,
fb: fml,
},
Panel {
a: ml,
b: m,
fa: fml,
fb: fm,
},
Panel {
a: m,
b: mr,
fa: fm,
fb: fmr,
},
Panel {
a: mr,
b: mrr,
fa: fmr,
fb: fmrr,
},
Panel {
a: mrr,
b,
fa: fmrr,
fb,
},
];
let mut total = 0.0;
for p in panels {
total += self.adaptive_step(f, p, acc, evaluations)?;
}
Ok(total)
}
}
}
impl Integrator for GaussLobattoIntegral {
fn integrate_impl<F>(&self, f: &mut F, a: Real, b: Real) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
let mut evaluations: Size = 0;
let acc = self.calculate_abs_tolerance(f, a, b, &mut evaluations)?;
evaluations += 2;
let (fa, fb) = (f(a), f(b));
self.adaptive_step(f, Panel { a, b, fa, fb }, acc, &mut evaluations)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::distributions::normal::NormalDistribution;
const TOL: Real = 1e-6;
#[test]
fn matches_known_integrals() {
let gl = GaussLobattoIntegral::new(1000, TOL).unwrap();
assert!((gl.integrate(|_| 1.0, 0.0, 1.0).unwrap() - 1.0).abs() < TOL);
assert!((gl.integrate(|x| x, 0.0, 1.0).unwrap() - 0.5).abs() < TOL);
assert!((gl.integrate(|x| x * x, 0.0, 1.0).unwrap() - 1.0 / 3.0).abs() < TOL);
assert!(
(gl.integrate(|x| x.sin(), 0.0, std::f64::consts::PI)
.unwrap()
- 2.0)
.abs()
< TOL
);
assert!(
(gl.integrate(|x| x.cos(), 0.0, std::f64::consts::PI)
.unwrap()
- 0.0)
.abs()
< TOL
);
let g = NormalDistribution::standard();
assert!((gl.integrate(|x| g.value(x), -10.0, 10.0).unwrap() - 1.0).abs() < TOL);
}
#[test]
fn reversed_limits_negate() {
let gl = GaussLobattoIntegral::new(1000, TOL).unwrap();
assert!((gl.integrate(|x| x, 1.0, 0.0).unwrap() - (-0.5)).abs() < TOL);
}
#[test]
fn relative_accuracy_and_convergence_options() {
let gl = GaussLobattoIntegral::new(1000, TOL)
.unwrap()
.with_relative_accuracy(TOL)
.unwrap()
.with_convergence_estimate(false);
assert!((gl.integrate(|x| x * x, 0.0, 1.0).unwrap() - 1.0 / 3.0).abs() < TOL);
}
#[test]
fn evaluation_budget_is_never_exceeded() {
for max in [0usize, 1, 14] {
assert!(GaussLobattoIntegral::new(max, TOL).is_err(), "budget {max}");
}
for max in 15..=19 {
let gl = GaussLobattoIntegral::new(max, TOL).unwrap();
let mut count = 0usize;
let _ = gl.integrate(
|x| {
count += 1;
x.sin()
},
0.0,
std::f64::consts::PI,
);
assert!(count <= max, "used {count} evaluations, budget {max}");
}
}
#[test]
fn invalid_configuration_rejected() {
for acc in [0.0, -1.0, Real::EPSILON, Real::NAN, Real::INFINITY] {
assert!(GaussLobattoIntegral::new(1000, acc).is_err(), "abs={acc}");
}
let gl = GaussLobattoIntegral::new(1000, TOL).unwrap();
assert!(gl.with_relative_accuracy(-1.0).is_err());
}
}