use ogeom_core::{OgeomResult, ogeom_bail};
const NODES: [f64; 5] = [
0.148_874_338_981_631_21,
0.433_395_394_129_247_2,
0.679_409_568_299_024_4,
0.865_063_366_688_984_5,
0.973_906_528_517_171_7,
];
const WEIGHTS: [f64; 5] = [
0.295_524_224_714_752_87,
0.269_266_719_309_996_35,
0.219_086_362_515_982_04,
0.149_451_349_150_580_6,
0.066_671_344_308_688_14,
];
const MAX_DEPTH: u32 = 24;
pub fn gauss_legendre<F: FnMut(f64) -> f64>(mut f: F, a: f64, b: f64) -> f64 {
let half = (b - a) * 0.5;
let middle = f64::midpoint(a, b);
let mut total = 0.0;
for (node, weight) in NODES.iter().zip(&WEIGHTS) {
let offset = half * node;
total += weight * (f(middle - offset) + f(middle + offset));
}
total * half
}
pub fn integrate<F: FnMut(f64) -> f64>(
mut f: F,
a: f64,
b: f64,
tolerance: f64,
) -> OgeomResult<f64> {
if !a.is_finite() || !b.is_finite() {
ogeom_bail!(Domain, "cannot integrate over [{a}, {b}]");
}
if !tolerance.is_finite() || tolerance <= 0.0 {
ogeom_bail!(Domain, "integration tolerance {tolerance} must be positive");
}
if a == b {
return Ok(0.0);
}
let whole = gauss_legendre(&mut f, a, b);
refine(&mut f, a, b, tolerance, whole, 0)
}
fn refine<F: FnMut(f64) -> f64>(
f: &mut F,
a: f64,
b: f64,
tolerance: f64,
whole: f64,
depth: u32,
) -> OgeomResult<f64> {
let middle = f64::midpoint(a, b);
let left = gauss_legendre(&mut *f, a, middle);
let right = gauss_legendre(&mut *f, middle, b);
let split = left + right;
if (split - whole).abs() <= tolerance {
return Ok(split + (split - whole) / 1023.0);
}
if left.abs() + right.abs() <= tolerance {
return Ok(split);
}
if depth >= MAX_DEPTH {
ogeom_bail!(
NotDone,
"the integral over [{a}, {b}] did not converge to {tolerance} \
within {MAX_DEPTH} subdivisions; the integrand has a singularity \
there rather than a resolution problem"
);
}
let half = tolerance * 0.5;
Ok(
refine(f, a, middle, half, left, depth + 1)?
+ refine(f, middle, b, half, right, depth + 1)?,
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use core::f64::consts::PI;
#[test]
fn a_polynomial_within_the_rules_degree_is_exact_in_one_go() {
let f = |x: f64| x.powi(19) + 3.0 * x.powi(4) - 7.0 * x + 2.0;
let exact = 1.0 / 20.0 + 3.0 / 5.0 - 7.0 / 2.0 + 2.0;
assert_relative_eq!(gauss_legendre(f, 0.0, 1.0), exact, epsilon = 1e-14);
}
#[test]
fn transcendental_integrands_converge() {
assert_relative_eq!(
integrate(f64::sin, 0.0, PI, 1e-12).unwrap(),
2.0,
epsilon = 1e-12
);
assert_relative_eq!(
integrate(|x| 1.0 / x, 1.0, core::f64::consts::E, 1e-12).unwrap(),
1.0,
epsilon = 1e-12
);
}
#[test]
fn an_infinite_derivative_at_an_endpoint_is_handled_to_a_stated_limit() {
let quarter = |x: f64| (1.0 - x * x).max(0.0).sqrt();
let found = integrate(quarter, 0.0, 1.0, 1e-7).unwrap();
assert_relative_eq!(found, PI / 4.0, epsilon = 1e-12);
assert!(
integrate(quarter, 0.0, 1.0, 1e-8).is_err(),
"asked for more than the method can give, it should say so"
);
}
#[test]
fn a_reversed_interval_integrates_to_the_negative() {
let forward = integrate(f64::sin, 0.0, PI, 1e-12).unwrap();
let backward = integrate(f64::sin, PI, 0.0, 1e-12).unwrap();
assert_relative_eq!(forward, -backward, epsilon = 1e-12);
}
#[test]
fn an_empty_interval_integrates_to_nothing() {
assert_eq!(integrate(f64::sin, 1.0, 1.0, 1e-12).unwrap(), 0.0);
}
#[test]
fn an_integrand_that_will_not_converge_says_so() {
assert!(integrate(|x| 1.0 / x, 0.0, 1.0, 1e-12).is_err());
}
#[test]
fn non_finite_bounds_and_tolerances_are_refused() {
assert!(integrate(f64::sin, 0.0, f64::NAN, 1e-9).is_err());
assert!(integrate(f64::sin, f64::NEG_INFINITY, 0.0, 1e-9).is_err());
assert!(integrate(f64::sin, 0.0, 1.0, 0.0).is_err());
assert!(integrate(f64::sin, 0.0, 1.0, -1.0).is_err());
}
}