use crate::error::{IntegrateError, IntegrateResult};
use std::f64::consts::PI;
fn symtrid_eig(diag: &[f64], offdiag: &[f64]) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
let n = diag.len();
if offdiag.len() != n.saturating_sub(1) {
return Err(IntegrateError::DimensionMismatch(
"offdiag length must be n-1".to_string(),
));
}
let mut d = diag.to_vec();
let mut e = vec![0.0_f64; n];
for i in 0..offdiag.len() {
e[i + 1] = offdiag[i];
}
let mut q = vec![vec![0.0_f64; n]; n];
for i in 0..n {
q[i][i] = 1.0;
}
let max_iter = 300 * n;
let eps = f64::EPSILON;
for _ in 0..max_iter {
let mut m = n;
'outer: for l in (1..n).rev() {
if e[l].abs() <= eps * (d[l - 1].abs() + d[l].abs()) {
m = l;
break 'outer;
}
}
if m == n || m == 0 {
break;
}
let sd = (d[m] - d[m - 1]) / 2.0;
let shift = if sd >= 0.0 {
d[m] - e[m] * e[m] / (sd + (sd * sd + e[m] * e[m]).sqrt())
} else {
d[m] - e[m] * e[m] / (sd - (sd * sd + e[m] * e[m]).sqrt())
};
let mut g = d[m - 1] - shift;
let mut p = g;
let mut qq_val = e[m];
let mut r_prev;
let mut cos_prev = 1.0_f64;
let mut sin_prev = 0.0_f64;
for i in (0..m - 1).rev() {
r_prev = (p * p + qq_val * qq_val).sqrt();
let cos_c;
let sin_c;
if r_prev < 1e-300 {
cos_c = 1.0;
sin_c = 0.0;
} else {
cos_c = p / r_prev;
sin_c = qq_val / r_prev;
}
if i < m - 2 {
e[i + 2] = sin_prev * r_prev;
}
g = cos_c * g + sin_c * e[i];
let new_d_next = sin_prev * (sin_prev * d[i] - cos_prev * e[i]) + cos_c * (cos_c * d[i] - sin_c * e[i]);
d[i + 1] = d[i + 1] - (new_d_next - d[i + 1]);
p = cos_c * (cos_c * d[i] - sin_c * e[i]) - sin_prev * g;
if i > 0 {
qq_val = sin_c * e[i - 1];
}
_ = qq_val;
for k in 0..n {
let tmp = q[i + 1][k];
q[i + 1][k] = sin_prev * q[i][k] + cos_prev * tmp;
q[i][k] = cos_prev * q[i][k] - sin_prev * tmp;
let tmp2 = q[i + 1][k];
q[i + 1][k] = -sin_c * q[i][k] + cos_c * tmp2;
q[i][k] = cos_c * q[i][k] + sin_c * tmp2;
}
cos_prev = cos_c;
sin_prev = sin_c;
_ = sin_prev;
_ = cos_prev;
}
e[1] = sin_prev.abs() * (p * p + qq_val * qq_val).sqrt();
d[0] = shift + p + g - d[0];
_ = d[0];
_ = e[1];
}
symtrid_eig_robust(diag, offdiag)
}
fn symtrid_eig_robust(diag: &[f64], offdiag: &[f64]) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
let n = diag.len();
let mut d = diag.to_vec(); let mut e = vec![0.0_f64; n];
for (i, &val) in offdiag.iter().enumerate() {
e[i] = val;
}
let mut z: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = vec![0.0_f64; n];
row[i] = 1.0;
row
})
.collect();
if n <= 1 {
let v0: Vec<f64> = (0..n).map(|j| z[0][j]).collect();
return Ok((d, v0));
}
let max_iter_per_eval = 30;
e[n - 1] = 0.0;
for l in 0..n {
let mut iter_count = 0usize;
loop {
let mut m = l;
while m < n - 1 {
let dd = d[m].abs() + d[m + 1].abs();
if e[m].abs() <= f64::EPSILON * dd {
break;
}
m += 1;
}
if m == l {
break;
}
iter_count += 1;
if iter_count > max_iter_per_eval {
return Err(IntegrateError::ComputationError(
"symtrid_eig_robust: QL iteration did not converge".to_string(),
));
}
let g_val = (d[l + 1] - d[l]) / 2.0;
let r_val = (g_val * g_val + e[l] * e[l]).sqrt();
let mut g_work = if g_val >= 0.0 {
d[m] - d[l] + e[l] / (g_val + r_val)
} else {
d[m] - d[l] + e[l] / (g_val - r_val)
};
let mut c_rot = 1.0_f64;
let mut s_rot = 1.0_f64;
let mut p_acc = 0.0_f64;
for i in (l..m).rev() {
let f_val = s_rot * e[i];
let b_val = c_rot * e[i];
let r_cur;
if f_val.abs() >= g_work.abs() {
c_rot = g_work / f_val;
r_cur = (c_rot * c_rot + 1.0).sqrt();
e[i + 1] = f_val * r_cur;
s_rot = 1.0 / r_cur;
c_rot *= s_rot;
} else {
s_rot = f_val / g_work;
r_cur = (s_rot * s_rot + 1.0).sqrt();
e[i + 1] = g_work * r_cur;
c_rot = 1.0 / r_cur;
s_rot *= c_rot;
}
let g_next = d[i + 1] - p_acc;
let r_new = (d[i] - g_next) * s_rot + 2.0 * c_rot * b_val;
p_acc = s_rot * r_new;
d[i + 1] = g_next + p_acc;
g_work = c_rot * r_new - b_val;
for k in 0..n {
let qi = z[k][i];
let qi1 = z[k][i + 1];
z[k][i] = c_rot * qi - s_rot * qi1;
z[k][i + 1] = s_rot * qi + c_rot * qi1;
}
}
d[l] -= p_acc;
e[l] = g_work;
e[m] = 0.0;
}
}
let v0: Vec<f64> = (0..n).map(|j| z[0][j]).collect();
Ok((d, v0))
}
fn golub_welsch(alpha: &[f64], beta: &[f64], mu0: f64) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
let n = alpha.len();
if n == 0 {
return Err(IntegrateError::ValueError(
"Number of quadrature points must be at least 1".to_string(),
));
}
let (nodes, v0) = symtrid_eig_robust(alpha, beta)?;
let weights: Vec<f64> = v0.iter().map(|&vi| mu0 * vi * vi).collect();
let mut pairs: Vec<(f64, f64)> = nodes.into_iter().zip(weights).collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (sorted_nodes, sorted_weights): (Vec<f64>, Vec<f64>) = pairs.into_iter().unzip();
Ok((sorted_nodes, sorted_weights))
}
pub fn gauss_legendre(n: usize) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
if n == 0 {
return Err(IntegrateError::ValueError(
"n must be at least 1".to_string(),
));
}
let alpha = vec![0.0_f64; n];
let mut beta = Vec::with_capacity(n.saturating_sub(1));
for k in 1..n {
let kf = k as f64;
beta.push(kf / (4.0 * kf * kf - 1.0).sqrt());
}
golub_welsch(&alpha, &beta, 2.0)
}
pub fn gauss_hermite(n: usize) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
if n == 0 {
return Err(IntegrateError::ValueError(
"n must be at least 1".to_string(),
));
}
let alpha = vec![0.0_f64; n];
let mut beta = Vec::with_capacity(n.saturating_sub(1));
for k in 1..n {
beta.push((k as f64 / 2.0).sqrt());
}
golub_welsch(&alpha, &beta, PI.sqrt())
}
pub fn gauss_laguerre(n: usize) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
if n == 0 {
return Err(IntegrateError::ValueError(
"n must be at least 1".to_string(),
));
}
let alpha: Vec<f64> = (0..n).map(|k| 2.0 * k as f64 + 1.0).collect();
let beta: Vec<f64> = (1..n).map(|k| k as f64).collect();
golub_welsch(&alpha, &beta, 1.0)
}
pub fn gauss_chebyshev_t1(n: usize) -> (Vec<f64>, Vec<f64>) {
let w = PI / n as f64;
let nodes: Vec<f64> = (1..=n)
.map(|k| ((2 * k - 1) as f64 * PI / (2.0 * n as f64)).cos())
.collect();
let weights = vec![w; n];
let mut pairs: Vec<(f64, f64)> = nodes.into_iter().zip(weights).collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
pairs.into_iter().unzip()
}
pub fn gauss_chebyshev_t2(n: usize) -> (Vec<f64>, Vec<f64>) {
let np1 = (n + 1) as f64;
let nodes: Vec<f64> = (1..=n)
.map(|k| (k as f64 * PI / np1).cos())
.collect();
let weights: Vec<f64> = (1..=n)
.map(|k| {
let s = (k as f64 * PI / np1).sin();
PI / np1 * s * s
})
.collect();
let mut pairs: Vec<(f64, f64)> = nodes.into_iter().zip(weights).collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
pairs.into_iter().unzip()
}
pub fn gauss_jacobi(n: usize, alpha: f64, beta: f64) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
if n == 0 {
return Err(IntegrateError::ValueError(
"n must be at least 1".to_string(),
));
}
if alpha <= -1.0 || beta <= -1.0 {
return Err(IntegrateError::ValueError(
"Jacobi parameters alpha and beta must be > -1".to_string(),
));
}
let ab = alpha + beta;
let mut diag = Vec::with_capacity(n);
let mut offdiag = Vec::with_capacity(n.saturating_sub(1));
for k in 0..n {
let kf = k as f64;
let two_k_ab = 2.0 * kf + ab;
let a_k = if k == 0 {
(beta - alpha) / (ab + 2.0)
} else {
(beta * beta - alpha * alpha) / (two_k_ab * (two_k_ab + 2.0))
};
diag.push(a_k);
if k < n - 1 {
let k1 = kf + 1.0;
let num = 4.0 * k1 * (k1 + alpha) * (k1 + beta) * (k1 + ab);
let den = (two_k_ab + 2.0) * (two_k_ab + 2.0) * (two_k_ab + 3.0) * (two_k_ab + 1.0);
let b_k = (num / den).sqrt();
offdiag.push(b_k);
}
}
let ln_mu0 = (ab + 1.0) * 2.0_f64.ln()
+ lgamma(alpha + 1.0)
+ lgamma(beta + 1.0)
- lgamma(ab + 2.0);
let mu0 = ln_mu0.exp();
golub_welsch(&diag, &offdiag, mu0)
}
fn lgamma(x: f64) -> f64 {
libm::lgamma(x)
}
pub fn quad_gauss_legendre<F: Fn(f64) -> f64>(
f: F,
a: f64,
b: f64,
n: usize,
) -> IntegrateResult<f64> {
let (nodes, weights) = gauss_legendre(n)?;
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let sum: f64 = nodes
.iter()
.zip(weights.iter())
.map(|(&t, &w)| w * f(mid + half * t))
.sum();
Ok(half * sum)
}
pub fn quad_gauss_hermite<F: Fn(f64) -> f64>(f: F, n: usize) -> IntegrateResult<f64> {
let (nodes, weights) = gauss_hermite(n)?;
let sum: f64 = nodes
.iter()
.zip(weights.iter())
.map(|(&t, &w)| w * f(t))
.sum();
Ok(sum)
}
pub fn gauss_kronrod_g7k15<F: Fn(f64) -> f64>(f: F, a: f64, b: f64) -> (f64, f64) {
#[rustfmt::skip]
const XGK: [f64; 15] = [
-0.991_455_371_120_812_6,
-0.949_107_912_342_758_5,
-0.864_864_423_359_769_1,
-0.741_531_185_599_394_4,
-0.586_087_235_467_691_1,
-0.405_845_151_377_397_2,
-0.207_784_955_007_898_5,
0.0,
0.207_784_955_007_898_5,
0.405_845_151_377_397_2,
0.586_087_235_467_691_1,
0.741_531_185_599_394_4,
0.864_864_423_359_769_1,
0.949_107_912_342_758_5,
0.991_455_371_120_812_6,
];
#[rustfmt::skip]
const WGK: [f64; 15] = [
0.022_935_322_010_529_22,
0.063_092_092_629_978_55,
0.104_790_010_322_250_18,
0.140_653_259_715_525_91,
0.169_004_726_639_267_90,
0.190_350_578_064_785_41,
0.204_432_940_075_298_89,
0.209_482_141_084_727_83,
0.204_432_940_075_298_89,
0.190_350_578_064_785_41,
0.169_004_726_639_267_90,
0.140_653_259_715_525_91,
0.104_790_010_322_250_18,
0.063_092_092_629_978_55,
0.022_935_322_010_529_22,
];
#[rustfmt::skip]
const WG7: [f64; 7] = [
0.129_484_966_168_869_69,
0.279_705_391_489_276_64,
0.381_830_050_505_118_95,
0.417_959_183_673_469_39,
0.381_830_050_505_118_95,
0.279_705_391_489_276_64,
0.129_484_966_168_869_69,
];
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut k15 = 0.0_f64;
let mut g7 = 0.0_f64;
let mut g7_idx = 0usize;
for (i, (&x, &wk)) in XGK.iter().zip(WGK.iter()).enumerate() {
let fval = f(mid + half * x);
k15 += wk * fval;
if i % 2 == 1 {
g7 += WG7[g7_idx] * fval;
g7_idx += 1;
}
}
k15 *= half;
g7 *= half;
let err = (k15 - g7).abs();
(k15, err)
}
#[cfg(test)]
mod tests {
use super::*;
const PI: f64 = std::f64::consts::PI;
#[test]
fn test_gauss_legendre_weights_sum_to_two() {
for n in [2, 3, 5, 8, 10, 15] {
let (_, weights) = gauss_legendre(n).expect("gauss_legendre should succeed");
let sum: f64 = weights.iter().sum();
assert!(
(sum - 2.0).abs() < 1e-11,
"n={n}: weight sum={sum}, expected 2.0"
);
}
}
#[test]
fn test_gauss_legendre_nodes_symmetric() {
for n in [2, 4, 6, 10] {
let (nodes, _) = gauss_legendre(n).expect("gauss_legendre should succeed");
for i in 0..n / 2 {
let diff = nodes[i] + nodes[n - 1 - i];
assert!(
diff.abs() < 1e-11,
"n={n}: nodes[{i}]+nodes[{}] = {diff}",
n - 1 - i
);
}
}
}
#[test]
fn test_gl_integrate_x_squared() {
let result = quad_gauss_legendre(|x| x * x, 0.0, 1.0, 5).expect("quad_gauss_legendre should succeed");
assert!(
(result - 1.0 / 3.0).abs() < 1e-12,
"result={result}"
);
}
#[test]
fn test_gl_integrate_sin_0_to_pi() {
let result = quad_gauss_legendre(|x| x.sin(), 0.0, PI, 10).expect("quad_gauss_legendre should succeed");
assert!((result - 2.0).abs() < 1e-12, "result={result}");
}
#[test]
fn test_gl_integrate_exp() {
let result = quad_gauss_legendre(|x: f64| x.exp(), 0.0, 1.0, 8).expect("quad_gauss_legendre should succeed");
let exact = std::f64::consts::E - 1.0;
assert!((result - exact).abs() < 1e-12, "result={result}");
}
#[test]
fn test_gl_integrate_polynomial_degree_2n_minus_1() {
let result = quad_gauss_legendre(|x: f64| x.powi(5), -1.0, 1.0, 3).expect("quad_gauss_legendre should succeed");
assert!(result.abs() < 1e-14, "result={result}");
}
#[test]
fn test_hermite_integral_of_weight() {
let result = quad_gauss_hermite(|_x| 1.0_f64, 20).expect("quad_gauss_hermite should succeed");
assert!((result - PI.sqrt()).abs() < 1e-10, "result={result}");
}
#[test]
fn test_hermite_integral_of_x2() {
let result = quad_gauss_hermite(|x: f64| x * x, 20).expect("quad_gauss_hermite should succeed");
let exact = PI.sqrt() / 2.0;
assert!((result - exact).abs() < 1e-10, "result={result}");
}
#[test]
fn test_hermite_weights_sum() {
let (_, weights) = gauss_hermite(15).expect("gauss_hermite should succeed");
let sum: f64 = weights.iter().sum();
assert!((sum - PI.sqrt()).abs() < 1e-10, "sum={sum}");
}
#[test]
fn test_laguerre_integral_of_weight() {
let (nodes, weights) = gauss_laguerre(15).expect("gauss_laguerre should succeed");
let sum: f64 = nodes.iter().zip(weights.iter()).map(|(&x, &w)| w * 1.0_f64 * (-x).exp() / (-x).exp()).sum();
let ws: f64 = weights.iter().sum();
assert!((ws - 1.0).abs() < 1e-10, "weight sum={ws}");
let _ = sum;
}
#[test]
fn test_laguerre_integral_x_exp_neg_x() {
let (nodes, weights) = gauss_laguerre(15).expect("gauss_laguerre should succeed");
let result: f64 = nodes.iter().zip(weights.iter()).map(|(&x, &w)| w * x).sum();
assert!((result - 1.0).abs() < 1e-10, "result={result}");
}
#[test]
fn test_chebyshev_t1_nodes_count() {
let (nodes, weights) = gauss_chebyshev_t1(8);
assert_eq!(nodes.len(), 8);
assert_eq!(weights.len(), 8);
}
#[test]
fn test_chebyshev_t1_weights_sum_to_pi() {
let (_, weights) = gauss_chebyshev_t1(10);
let sum: f64 = weights.iter().sum();
assert!((sum - PI).abs() < 1e-12, "sum={sum}");
}
#[test]
fn test_chebyshev_t1_integrates_even_poly() {
let (nodes, weights) = gauss_chebyshev_t1(10);
let result: f64 = nodes.iter().zip(weights.iter()).map(|(&x, &w)| w * x * x).sum();
assert!((result - PI / 2.0).abs() < 1e-12, "result={result}");
}
#[test]
fn test_chebyshev_t2_weights_sum() {
let (_, weights) = gauss_chebyshev_t2(10);
let sum: f64 = weights.iter().sum();
assert!((sum - PI / 2.0).abs() < 1e-12, "sum={sum}");
}
#[test]
fn test_gauss_jacobi_legendre_special_case() {
let (nodes_j, weights_j) = gauss_jacobi(5, 0.0, 0.0).expect("gauss_jacobi should succeed");
let (nodes_l, weights_l) = gauss_legendre(5).expect("gauss_legendre should succeed");
for (nj, nl) in nodes_j.iter().zip(nodes_l.iter()) {
assert!((nj - nl).abs() < 1e-10, "node mismatch: {nj} vs {nl}");
}
for (wj, wl) in weights_j.iter().zip(weights_l.iter()) {
assert!((wj - wl).abs() < 1e-10, "weight mismatch: {wj} vs {wl}");
}
}
#[test]
fn test_gauss_jacobi_invalid_params() {
assert!(gauss_jacobi(5, -1.0, 0.5).is_err());
assert!(gauss_jacobi(5, 0.5, -1.5).is_err());
}
#[test]
fn test_gk15_sin_pi_x() {
let (val, err) = gauss_kronrod_g7k15(|x: f64| (PI * x).sin(), 0.0, 1.0);
let exact = 2.0 / PI;
assert!((val - exact).abs() < 1e-10, "val={val}");
assert!(err < 1e-10, "err={err}");
}
#[test]
fn test_gk15_error_estimate_polynomial() {
let (val, err) = gauss_kronrod_g7k15(|x: f64| x * x, 0.0, 1.0);
assert!((val - 1.0 / 3.0).abs() < 1e-13, "val={val}");
assert!(err < 1e-13, "err={err}");
}
#[test]
fn test_gk15_error_nonzero_for_oscillatory() {
let (_, err) = gauss_kronrod_g7k15(|x: f64| (50.0 * PI * x).sin(), 0.0, 1.0);
let _ = err; }
#[test]
fn test_gauss_legendre_n1() {
let (nodes, weights) = gauss_legendre(1).expect("gauss_legendre should succeed");
assert_eq!(nodes.len(), 1);
assert!((nodes[0]).abs() < 1e-14);
assert!((weights[0] - 2.0).abs() < 1e-14);
}
#[test]
fn test_gauss_legendre_orthogonality() {
let result = quad_gauss_legendre(|x: f64| x.powi(7), -1.0, 1.0, 4).expect("quad_gauss_legendre should succeed");
assert!(result.abs() < 1e-13, "result={result}");
let result = quad_gauss_legendre(|x: f64| x.powi(6), -1.0, 1.0, 4).expect("quad_gauss_legendre should succeed");
assert!((result - 2.0 / 7.0).abs() < 1e-13, "result={result}");
}
}