#![allow(dead_code)]
#![allow(clippy::needless_range_loop)]
use std::f64::consts::PI;
fn legendre_p_and_dp(n: usize, x: f64) -> (f64, f64) {
if n == 0 {
return (1.0, 0.0);
}
if n == 1 {
return (x, 1.0);
}
let mut p_prev = 1.0_f64;
let mut p_curr = x;
for k in 2..=(n as u32) {
let kf = k as f64;
let p_next = ((2.0 * kf - 1.0) * x * p_curr - (kf - 1.0) * p_prev) / kf;
p_prev = p_curr;
p_curr = p_next;
}
let dp = (n as f64) * (p_prev - x * p_curr) / (1.0 - x * x).max(1e-300);
(p_curr, dp)
}
pub fn gauss_legendre_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 1, "n must be at least 1");
let mut nw = Vec::with_capacity(n);
let half = n.div_ceil(2);
for i in 1..=half {
let theta = PI * ((i as f64) - 0.25) / ((n as f64) + 0.5);
let mut x = theta.cos();
for _ in 0..100 {
let (p, dp) = legendre_p_and_dp(n, x);
let dx = p / dp;
x -= dx;
if dx.abs() < 1e-15 {
break;
}
}
let (_, dp) = legendre_p_and_dp(n, x);
let w = 2.0 / ((1.0 - x * x) * dp * dp);
nw.push((-x, w));
if 2 * i - 1 != n {
nw.push((x, w));
}
}
nw.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
nw
}
pub fn gauss_legendre_integrate(f: &dyn Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
let nw = gauss_legendre_weights(n);
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
nw.iter()
.map(|(xi, wi)| wi * f(mid + half * xi))
.sum::<f64>()
* half
}
pub fn gauss_lobatto_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 2, "Gauss-Lobatto requires n >= 2");
let mut nw = Vec::with_capacity(n);
let w_end = 2.0 / ((n as f64) * ((n as f64) - 1.0));
nw.push((-1.0_f64, w_end));
let m = n - 2; let half = m.div_ceil(2);
for i in 1..=half {
let theta = PI * (i as f64) / ((n as f64) - 1.0);
let mut x = -theta.cos();
for _ in 0..100 {
let (p, dp) = legendre_p_and_dp(n - 1, x);
let eps = 1e-8;
let (_, dp_p) = legendre_p_and_dp(n - 1, x + eps);
let ddp = (dp_p - dp) / eps;
let dx = dp / ddp;
x -= dx;
if dx.abs() < 1e-14 {
break;
}
let _ = p;
}
let (pval, _) = legendre_p_and_dp(n - 1, x);
let w = 2.0 / ((n as f64 - 1.0) * (n as f64) * pval * pval);
nw.push((-x, w));
if 2 * i - 1 != m {
nw.push((x, w));
}
}
nw.push((1.0_f64, w_end));
nw.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
nw
}
pub fn gauss_chebyshev_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 1, "n must be at least 1");
let w = PI / (n as f64);
(1..=n)
.map(|k| {
let x = ((2 * k - 1) as f64 * PI / (2.0 * n as f64)).cos();
(x, w)
})
.collect()
}
pub fn simpsons_rule(f: &dyn Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
let n = if n.is_multiple_of(2) { n } else { n + 1 };
let h = (b - a) / (n as f64);
let mut sum = f(a) + f(b);
for i in 1..n {
let x = a + (i as f64) * h;
sum += if i % 2 == 0 { 2.0 } else { 4.0 } * f(x);
}
sum * h / 3.0
}
pub fn romberg_integration(
f: &dyn Fn(f64) -> f64,
a: f64,
b: f64,
max_level: usize,
tol: f64,
) -> f64 {
let m = max_level.max(1);
let mut r = vec![vec![0.0_f64; m + 1]; m + 1];
r[0][0] = 0.5 * (b - a) * (f(a) + f(b));
for i in 1..=m {
let n = 1usize << i; let h = (b - a) / (n as f64);
let mut sum = 0.0;
for k in 0..(n / 2) {
sum += f(a + (2 * k + 1) as f64 * h);
}
r[i][0] = 0.5 * r[i - 1][0] + h * sum;
for j in 1..=i {
let factor = (4.0_f64).powi(j as i32);
r[i][j] = (factor * r[i][j - 1] - r[i - 1][j - 1]) / (factor - 1.0);
}
if i >= 1 && (r[i][i] - r[i - 1][i - 1]).abs() < tol {
return r[i][i];
}
}
r[m][m]
}
const GK15_NODES: [f64; 8] = [
0.991_455_371_120_813,
0.949_107_912_342_758,
0.864_864_423_359_769,
0.741_531_185_599_394,
0.586_087_235_467_691,
0.405_845_151_377_397,
0.207_784_955_007_898,
0.0,
];
const GK15_WEIGHTS: [f64; 8] = [
0.022_935_322_010_529,
0.063_092_092_629_979,
0.104_790_010_322_250,
0.140_653_259_715_525,
0.169_004_726_639_267,
0.190_350_578_064_785,
0.204_432_940_075_298,
0.209_482_141_084_728,
];
const G7_WEIGHTS: [f64; 4] = [
0.129_484_966_168_870,
0.279_705_391_489_277,
0.381_830_050_505_119,
0.417_959_183_673_469,
];
pub fn adaptive_gauss_kronrod(
f: &dyn Fn(f64) -> f64,
a: f64,
b: f64,
tol: f64,
max_depth: usize,
) -> f64 {
gk15_recursive(f, a, b, tol, max_depth, 0)
}
fn gk15_recursive(
f: &dyn Fn(f64) -> f64,
a: f64,
b: f64,
tol: f64,
max_depth: usize,
depth: usize,
) -> f64 {
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut gk = 0.0_f64;
let mut g7 = 0.0_f64;
let f0 = f(mid);
gk += GK15_WEIGHTS[7] * f0;
g7 += G7_WEIGHTS[3] * f0;
for i in 0..7 {
let xi = GK15_NODES[i];
let fplus = f(mid + half * xi);
let fminus = f(mid - half * xi);
gk += GK15_WEIGHTS[i] * (fplus + fminus);
if i == 1 || i == 3 || i == 5 {
let gi = match i {
1 => 0,
3 => 1,
5 => 2,
_ => unreachable!(),
};
g7 += G7_WEIGHTS[gi] * (fplus + fminus);
}
}
gk *= half;
g7 *= half;
let err = (gk - g7).abs();
if depth >= max_depth || err < tol {
return gk;
}
gk15_recursive(f, a, mid, tol * 0.5, max_depth, depth + 1)
+ gk15_recursive(f, mid, b, tol * 0.5, max_depth, depth + 1)
}
pub fn double_exponential(f: &dyn Fn(f64) -> f64, a: f64, b: f64, n: usize, h: f64) -> f64 {
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let phi = |t: f64| -> f64 { (0.5 * PI * t.sinh()).tanh() };
let dphi = |t: f64| -> f64 {
let s = 0.5 * PI * t.sinh();
0.5 * PI * t.cosh() / s.cosh().powi(2)
};
let mut sum = f(mid) * dphi(0.0);
for k in 1..=n {
let t = k as f64 * h;
let p = phi(t);
let dp = dphi(t);
let xp = mid + half * p;
let xm = mid - half * p;
if xp.is_finite() && xm.is_finite() {
sum += (f(xp) + f(xm)) * dp;
}
}
sum * h * half
}
pub fn clenshaw_curtis_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 2, "Clenshaw-Curtis requires n >= 2");
let nm1 = n - 1; let nm1f = nm1 as f64;
let nodes: Vec<f64> = (0..n).map(|k| (k as f64 * PI / nm1f).cos()).collect();
let weights: Vec<f64> = (0..n)
.map(|k| {
let c_k = if k == 0 || k == nm1 { 0.5 } else { 1.0 };
let half_nm1 = nm1 / 2;
let sum: f64 = (1..=half_nm1)
.map(|m| {
let b_m = if 2 * m == nm1 { 0.5 } else { 1.0 };
let theta = 2.0 * m as f64 * k as f64 * PI / nm1f;
b_m * theta.cos() / (4.0 * (m as f64).powi(2) - 1.0)
})
.sum();
2.0 * c_k * (1.0 - 2.0 * sum) / nm1f
})
.collect();
nodes.into_iter().zip(weights).collect()
}
pub fn gauss_hermite_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 1, "n must be at least 1");
let hermite_p_dp = |x: f64| -> (f64, f64) {
if n == 0 {
return (1.0, 0.0);
}
let mut pm1 = 1.0_f64;
let mut p = 2.0 * x;
if n == 1 {
return (p, 2.0);
}
for k in 2..=n {
let pnew = 2.0 * x * p - 2.0 * (k as f64 - 1.0) * pm1;
pm1 = p;
p = pnew;
}
let dp = 2.0 * (n as f64) * pm1;
(p, dp)
};
let half = n.div_ceil(2);
let mut nw = Vec::with_capacity(n);
for i in 1..=half {
let x0 = (2 * n + 1) as f64;
let mut x =
(2.0 * x0 + 1.0).sqrt() * (PI * (4.0 * i as f64 - 1.0) / (4.0 * n as f64 + 2.0)).cos();
for _ in 0..100 {
let (p, dp) = hermite_p_dp(x);
if dp.abs() < 1e-300 {
break;
}
let dx = p / dp;
x -= dx;
if dx.abs() < 1e-14 {
break;
}
}
let (pm1, _) = hermite_p_dp(x - 1e-9);
let (pp1, _) = hermite_p_dp(x + 1e-9);
let dp_num = (pp1 - pm1) / (2e-9);
let w = if dp_num.abs() < 1e-300 {
0.0
} else {
let (p_prev, _) = {
if n == 1 {
(1.0_f64, 0.0_f64)
} else {
let mut qm1 = 1.0_f64;
let mut q = 2.0 * x;
for k in 2..n {
let qnew = 2.0 * x * q - 2.0 * (k as f64 - 1.0) * qm1;
qm1 = q;
q = qnew;
}
(q, 0.0)
}
};
let hn_p_sq = (2.0_f64.powi(n as i32 - 1)
* (1..=n).map(|k| k as f64).product::<f64>()
* PI.sqrt())
/ ((n as f64) * p_prev * p_prev);
if hn_p_sq.is_finite() && hn_p_sq > 0.0 {
hn_p_sq
} else {
2.0_f64.powi(n as i32 - 1) * (1..=n).map(|k| k as f64).product::<f64>() * PI.sqrt()
/ ((n as f64) * p_prev * p_prev)
}
};
nw.push((-x, w));
if 2 * i - 1 != n {
nw.push((x, w));
}
}
nw.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
nw
}
fn laguerre_p_dp(n: usize, x: f64) -> (f64, f64) {
if n == 0 {
return (1.0, 0.0);
}
if n == 1 {
return (1.0 - x, -1.0);
}
let mut pm1 = 1.0_f64;
let mut p = 1.0 - x;
for k in 2..=n {
let kf = k as f64;
let pnew = ((2.0 * kf - 1.0 - x) * p - (kf - 1.0) * pm1) / kf;
pm1 = p;
p = pnew;
}
let dp = -pm1;
(p, dp)
}
pub fn gauss_laguerre_weights(n: usize) -> Vec<(f64, f64)> {
assert!(n >= 1, "n must be at least 1");
let mut nw = Vec::with_capacity(n);
for i in 1..=n {
let nf = n as f64;
let jv = PI * (4 * i - 1) as f64 / (4.0 * nf + 2.0);
let mut x = (1.0 - (nf - 1.0) / (8.0 * nf * nf * nf)) * jv * jv;
x = x.max(1e-6);
for _ in 0..200 {
let (p, dp) = laguerre_p_dp(n, x);
if dp.abs() < 1e-300 {
break;
}
let dx = p / dp;
x -= dx;
x = x.max(1e-15);
if dx.abs() < 1e-12 {
break;
}
}
let (p_prev, _) = laguerre_p_dp(n - 1, x);
let w = if p_prev.abs() < 1e-300 {
0.0
} else {
x / ((n as f64) * p_prev).powi(2)
};
nw.push((x, w));
}
nw.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
nw
}
pub fn integrate_2d(
f: &dyn Fn(f64, f64) -> f64,
ax: f64,
bx: f64,
ay: f64,
by: f64,
nx: usize,
ny: usize,
) -> f64 {
let nwx = gauss_legendre_weights(nx);
let nwy = gauss_legendre_weights(ny);
let midx = 0.5 * (ax + bx);
let halfx = 0.5 * (bx - ax);
let midy = 0.5 * (ay + by);
let halfy = 0.5 * (by - ay);
let mut sum = 0.0;
for (xi, wi) in &nwx {
let x = midx + halfx * xi;
for (yj, wj) in &nwy {
let y = midy + halfy * yj;
sum += wi * wj * f(x, y);
}
}
sum * halfx * halfy
}
pub fn integrate_3d(
f: &dyn Fn(f64, f64, f64) -> f64,
bounds: [(f64, f64); 3],
n: [usize; 3],
) -> f64 {
let [(ax, bx), (ay, by), (az, bz)] = bounds;
let nwx = gauss_legendre_weights(n[0]);
let nwy = gauss_legendre_weights(n[1]);
let nwz = gauss_legendre_weights(n[2]);
let midx = 0.5 * (ax + bx);
let halfx = 0.5 * (bx - ax);
let midy = 0.5 * (ay + by);
let halfy = 0.5 * (by - ay);
let midz = 0.5 * (az + bz);
let halfz = 0.5 * (bz - az);
let mut sum = 0.0;
for (xi, wi) in &nwx {
let x = midx + halfx * xi;
for (yj, wj) in &nwy {
let y = midy + halfy * yj;
for (zk, wk) in &nwz {
let z = midz + halfz * zk;
sum += wi * wj * wk * f(x, y, z);
}
}
}
sum * halfx * halfy * halfz
}
#[derive(Debug, Clone)]
pub struct AdaptiveIntegrator {
pub tol: f64,
pub max_evals: usize,
pub calls: usize,
}
impl AdaptiveIntegrator {
pub fn new(tol: f64, max_evals: usize) -> Self {
Self {
tol,
max_evals,
calls: 0,
}
}
pub fn integrate(&mut self, f: &dyn Fn(f64) -> f64, a: f64, b: f64) -> (f64, f64) {
self.calls = 0;
let max_depth = (self.max_evals / 15).max(1).ilog2() as usize + 1;
let (val, err) = self.adaptive_internal(f, a, b, self.tol, max_depth, 0);
(val, err)
}
fn adaptive_internal(
&mut self,
f: &dyn Fn(f64) -> f64,
a: f64,
b: f64,
tol: f64,
max_depth: usize,
depth: usize,
) -> (f64, f64) {
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut gk = 0.0_f64;
let mut g7 = 0.0_f64;
self.calls += 1;
let f0 = f(mid);
gk += GK15_WEIGHTS[7] * f0;
g7 += G7_WEIGHTS[3] * f0;
for i in 0..7 {
let xi = GK15_NODES[i];
self.calls += 2;
let fplus = f(mid + half * xi);
let fminus = f(mid - half * xi);
gk += GK15_WEIGHTS[i] * (fplus + fminus);
if i == 1 || i == 3 || i == 5 {
let gi = match i {
1 => 0,
3 => 1,
5 => 2,
_ => unreachable!(),
};
g7 += G7_WEIGHTS[gi] * (fplus + fminus);
}
}
gk *= half;
g7 *= half;
let err = (gk - g7).abs();
if depth >= max_depth || err < tol || self.calls >= self.max_evals {
return (gk, err);
}
let (vl, el) = self.adaptive_internal(f, a, mid, tol * 0.5, max_depth, depth + 1);
let (vr, er) = self.adaptive_internal(f, mid, b, tol * 0.5, max_depth, depth + 1);
(vl + vr, el + er)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-9;
#[test]
fn test_gl_weights_n1_integrates_constant() {
let nw = gauss_legendre_weights(1);
let sum: f64 = nw.iter().map(|(_, w)| w).sum();
assert!(
(sum - 2.0).abs() < TOL,
"weights should sum to 2, got {sum}"
);
}
#[test]
fn test_gl_weights_n2_nodes_and_weights() {
let nw = gauss_legendre_weights(2);
assert_eq!(nw.len(), 2);
let node = 1.0_f64 / 3.0_f64.sqrt();
assert!((nw[0].0 + node).abs() < 1e-12);
assert!((nw[1].0 - node).abs() < 1e-12);
assert!((nw[0].1 - 1.0).abs() < 1e-12);
assert!((nw[1].1 - 1.0).abs() < 1e-12);
}
#[test]
fn test_gl_weights_n5_sum_to_two() {
let nw = gauss_legendre_weights(5);
let sum: f64 = nw.iter().map(|(_, w)| w).sum();
assert!((sum - 2.0).abs() < 1e-12, "5-pt GL weights sum = {sum}");
}
#[test]
fn test_gl_nodes_in_minus_one_to_one() {
for n in [1, 2, 3, 5, 8, 10] {
for (x, _) in gauss_legendre_weights(n) {
assert!(
(-1.0..=1.0).contains(&x),
"node {x} out of [-1,1] for n={n}"
);
}
}
}
#[test]
fn test_gl_n5_is_sorted() {
let nw = gauss_legendre_weights(5);
for i in 1..nw.len() {
assert!(nw[i - 1].0 <= nw[i].0, "nodes not sorted at index {i}");
}
}
#[test]
fn test_gl_integrate_constant() {
let result = gauss_legendre_integrate(&|_| 3.0, 0.0, 1.0, 3);
assert!((result - 3.0).abs() < TOL);
}
#[test]
fn test_gl_integrate_linear() {
let result = gauss_legendre_integrate(&|x| x, 0.0, 2.0, 3);
assert!((result - 2.0).abs() < TOL);
}
#[test]
fn test_gl_integrate_quadratic() {
let result = gauss_legendre_integrate(&|x| x * x, 0.0, 1.0, 2);
assert!((result - 1.0 / 3.0).abs() < TOL);
}
#[test]
fn test_gl_integrate_sin() {
let result = gauss_legendre_integrate(&|x| x.sin(), 0.0, PI, 10);
assert!((result - 2.0).abs() < 1e-10, "sin integral = {result}");
}
#[test]
fn test_gl_integrate_exp() {
let exact = std::f64::consts::E - 1.0;
let result = gauss_legendre_integrate(&|x| x.exp(), 0.0, 1.0, 8);
assert!((result - exact).abs() < 1e-12, "exp integral = {result}");
}
#[test]
fn test_chebyshev_n4_weights_sum_to_pi() {
let nw = gauss_chebyshev_weights(4);
let sum: f64 = nw.iter().map(|(_, w)| w).sum();
assert!((sum - PI).abs() < 1e-12, "Chebyshev weights sum = {sum}");
}
#[test]
fn test_chebyshev_nodes_on_unit_circle() {
for (x, _) in gauss_chebyshev_weights(6) {
assert!(x.abs() <= 1.0 + 1e-12, "node {x} out of range");
}
}
#[test]
fn test_simpsons_constant() {
let r = simpsons_rule(&|_| 5.0, 0.0, 1.0, 2);
assert!((r - 5.0).abs() < TOL);
}
#[test]
fn test_simpsons_polynomial() {
let r = simpsons_rule(&|x| x * x * x, 0.0, 1.0, 4);
assert!((r - 0.25).abs() < TOL);
}
#[test]
fn test_simpsons_odd_n_rounded_up() {
let r = simpsons_rule(&|x| x * x, 0.0, 1.0, 3);
assert!((r - 1.0 / 3.0).abs() < 1e-10, "result = {r}");
}
#[test]
fn test_simpsons_sin() {
let r = simpsons_rule(&|x| x.sin(), 0.0, PI, 100);
assert!((r - 2.0).abs() < 1e-6, "sin integral = {r}");
}
#[test]
fn test_romberg_constant() {
let r = romberg_integration(&|_| 7.0, 0.0, 1.0, 5, 1e-12);
assert!((r - 7.0).abs() < 1e-10);
}
#[test]
fn test_romberg_exp() {
let exact = std::f64::consts::E - 1.0;
let r = romberg_integration(&|x| x.exp(), 0.0, 1.0, 8, 1e-12);
assert!((r - exact).abs() < 1e-10, "romberg exp = {r}");
}
#[test]
fn test_romberg_sin_over_pi() {
let r = romberg_integration(&|x| x.sin(), 0.0, PI, 8, 1e-12);
assert!((r - 2.0).abs() < 1e-10, "romberg sin = {r}");
}
#[test]
fn test_agk_constant() {
let r = adaptive_gauss_kronrod(&|_| 3.0, 0.0, 2.0, 1e-10, 10);
assert!((r - 6.0).abs() < 1e-8);
}
#[test]
fn test_agk_sin() {
let r = adaptive_gauss_kronrod(&|x| x.sin(), 0.0, PI, 1e-10, 10);
assert!((r - 2.0).abs() < 1e-8, "agk sin = {r}");
}
#[test]
fn test_agk_exp_negative() {
let r = adaptive_gauss_kronrod(&|x| (-x).exp(), 0.0, 20.0, 1e-10, 15);
assert!((r - 1.0).abs() < 1e-8, "agk exp = {r}");
}
#[test]
fn test_de_constant() {
let r = double_exponential(&|_| 1.0, 0.0, 1.0, 50, 0.1);
assert!((r - 1.0).abs() < 1e-6, "DE const = {r}");
}
#[test]
fn test_de_sin() {
let r = double_exponential(&|x| x.sin(), 0.0, PI, 100, 0.05);
assert!((r - 2.0).abs() < 1e-6, "DE sin = {r}");
}
#[test]
fn test_cc_weights_sum_to_two() {
for n in [2, 3, 5, 8] {
let nw = clenshaw_curtis_weights(n);
let sum: f64 = nw.iter().map(|(_, w)| w).sum();
assert!((sum - 2.0).abs() < 1e-8, "CC weights sum for n={n}: {sum}");
}
}
#[test]
fn test_cc_endpoints_are_minus_one_and_one() {
let nw = clenshaw_curtis_weights(4);
let xs: Vec<f64> = nw.iter().map(|(x, _)| *x).collect();
assert!(
xs.iter().any(|x| (x + 1.0).abs() < 1e-12),
"should include -1"
);
assert!(
xs.iter().any(|x| (x - 1.0).abs() < 1e-12),
"should include +1"
);
}
#[test]
fn test_laguerre_n1() {
let nw = gauss_laguerre_weights(1);
assert_eq!(nw.len(), 1);
assert!((nw[0].0 - 1.0).abs() < 0.1, "n=1 node ≈ 1, got {}", nw[0].0);
}
#[test]
fn test_laguerre_nodes_positive() {
for n in [1, 2, 3, 4, 5] {
for (x, _) in gauss_laguerre_weights(n) {
assert!(x > 0.0, "Laguerre node must be positive, got {x}");
}
}
}
#[test]
fn test_laguerre_integrates_exp_neg_x() {
let nw = gauss_laguerre_weights(5);
for i in 1..nw.len() {
assert!(nw[i].0 > nw[i - 1].0, "nodes should be sorted");
}
for (_, w) in &nw {
assert!(*w > 0.0, "weights should be positive");
}
}
#[test]
fn test_integrate_2d_constant() {
let r = integrate_2d(&|_, _| 1.0, 0.0, 1.0, 0.0, 1.0, 3, 3);
assert!((r - 1.0).abs() < TOL);
}
#[test]
fn test_integrate_2d_product() {
let r = integrate_2d(&|x, y| x * y, 0.0, 1.0, 0.0, 1.0, 3, 3);
assert!((r - 0.25).abs() < TOL, "2D product = {r}");
}
#[test]
fn test_integrate_2d_sin_cos() {
let r = integrate_2d(
&|x, y| x.sin() * y.cos(),
0.0,
PI / 2.0,
0.0,
PI / 2.0,
8,
8,
);
assert!((r - 1.0).abs() < 1e-10, "2D sin*cos = {r}");
}
#[test]
fn test_integrate_3d_constant() {
let r = integrate_3d(
&|_, _, _| 1.0,
[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
[3, 3, 3],
);
assert!((r - 1.0).abs() < TOL);
}
#[test]
fn test_integrate_3d_xyz() {
let r = integrate_3d(
&|x, y, z| x * y * z,
[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
[4, 4, 4],
);
assert!((r - 0.125).abs() < TOL, "3D xyz = {r}");
}
#[test]
fn test_adaptive_integrator_constant() {
let mut ai = AdaptiveIntegrator::new(1e-8, 1000);
let (val, _err) = ai.integrate(&|_| 4.0, 0.0, 1.0);
assert!((val - 4.0).abs() < 1e-6, "val = {val}");
}
#[test]
fn test_adaptive_integrator_sin() {
let mut ai = AdaptiveIntegrator::new(1e-8, 5000);
let (val, err) = ai.integrate(&|x| x.sin(), 0.0, PI);
assert!((val - 2.0).abs() < 1e-6, "val = {val}, err = {err}");
}
#[test]
fn test_adaptive_integrator_tracks_calls() {
let mut ai = AdaptiveIntegrator::new(1e-6, 1000);
let _ = ai.integrate(&|x| x.cos(), 0.0, 1.0);
assert!(ai.calls > 0, "calls should be > 0 after integration");
}
#[test]
fn test_adaptive_integrator_error_nonnegative() {
let mut ai = AdaptiveIntegrator::new(1e-8, 2000);
let (_, err) = ai.integrate(&|x| x * x, 0.0, 1.0);
assert!(err >= 0.0, "error estimate must be non-negative");
}
#[test]
fn test_lobatto_n2_endpoints() {
let nw = gauss_lobatto_weights(2);
assert_eq!(nw.len(), 2);
assert!((nw[0].0 + 1.0).abs() < 1e-12, "first node should be -1");
assert!((nw[1].0 - 1.0).abs() < 1e-12, "last node should be 1");
}
#[test]
fn test_lobatto_weights_sum_to_two() {
for n in [2, 3, 4, 5] {
let nw = gauss_lobatto_weights(n);
let sum: f64 = nw.iter().map(|(_, w)| w).sum();
assert!(
(sum - 2.0).abs() < 1e-8,
"GL lobatto n={n} weights sum = {sum}"
);
}
}
#[test]
fn test_lobatto_n4_count() {
let nw = gauss_lobatto_weights(4);
assert_eq!(nw.len(), 4);
}
#[test]
fn test_hermite_n1() {
let nw = gauss_hermite_weights(1);
assert_eq!(nw.len(), 1);
assert!(
nw[0].0.abs() < 1e-10,
"n=1 hermite node at 0, got {}",
nw[0].0
);
}
#[test]
fn test_hermite_n2_nodes_symmetric() {
let nw = gauss_hermite_weights(2);
assert_eq!(nw.len(), 2);
assert!(
(nw[0].0 + nw[1].0).abs() < 1e-10,
"nodes should be symmetric"
);
}
#[test]
fn test_hermite_n3_count() {
let nw = gauss_hermite_weights(3);
assert_eq!(nw.len(), 3);
}
#[test]
fn test_methods_agree_on_sin() {
let f = &|x: f64| x.sin();
let gl = gauss_legendre_integrate(f, 0.0, PI, 10);
let simp = simpsons_rule(f, 0.0, PI, 100);
let romb = romberg_integration(f, 0.0, PI, 8, 1e-12);
let agk = adaptive_gauss_kronrod(f, 0.0, PI, 1e-10, 10);
assert!((gl - 2.0).abs() < 1e-10);
assert!((simp - 2.0).abs() < 1e-6);
assert!((romb - 2.0).abs() < 1e-10);
assert!((agk - 2.0).abs() < 1e-8);
}
#[test]
fn test_methods_agree_on_polynomial() {
let f = &|x: f64| x.powi(4) - 2.0 * x * x + 1.0;
let exact = 16.0 / 15.0;
let gl = gauss_legendre_integrate(f, -1.0, 1.0, 5);
let romb = romberg_integration(f, -1.0, 1.0, 6, 1e-12);
assert!((gl - exact).abs() < 1e-12);
assert!((romb - exact).abs() < 1e-10);
}
}