use crate::error::QuadratureError;
use crate::golub_welsch::golub_welsch;
use crate::rule::QuadratureRule;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float as _;
#[derive(Debug, Clone)]
pub struct GaussJacobi {
rule: QuadratureRule<f64>,
alpha: f64,
beta: f64,
}
impl GaussJacobi {
pub fn new(n: usize, alpha: f64, beta: f64) -> Result<Self, QuadratureError> {
if n == 0 {
return Err(QuadratureError::ZeroOrder);
}
if !alpha.is_finite() || !beta.is_finite() || alpha <= -1.0 || beta <= -1.0 {
return Err(QuadratureError::InvalidInput(
"Jacobi parameters must be finite and satisfy alpha > -1 and beta > -1",
));
}
let (nodes, weights) = compute_jacobi(n, alpha, beta)?;
Ok(Self {
rule: QuadratureRule { nodes, weights },
alpha,
beta,
})
}
#[must_use]
pub fn alpha(&self) -> f64 {
self.alpha
}
#[must_use]
pub fn beta(&self) -> f64 {
self.beta
}
}
impl_rule_accessors!(GaussJacobi, nodes_doc: "Returns the nodes on \\[-1, 1\\].");
#[allow(clippy::cast_precision_loss)] fn compute_jacobi(
n: usize,
alpha: f64,
beta: f64,
) -> Result<(Vec<f64>, Vec<f64>), QuadratureError> {
let ab = alpha + beta;
let diag: Vec<f64> = (0..n)
.map(|k| {
let two_k_ab = 2.0 * k as f64 + ab;
let denom = two_k_ab * (two_k_ab + 2.0);
if denom.abs() < 1e-300 {
if k == 0 {
(beta - alpha) / (ab + 2.0)
} else {
0.0
}
} else {
(beta * beta - alpha * alpha) / denom
}
})
.collect();
let off_diag_sq: Vec<f64> = (1..n)
.map(|k| {
let k = k as f64;
let two_k_ab = 2.0 * k + ab;
let denom = two_k_ab * two_k_ab * (two_k_ab + 1.0) * (two_k_ab - 1.0);
if denom.abs() < 1e-300 {
2.0 * (1.0 + alpha) * (1.0 + beta)
} else {
let numer = 4.0 * k * (k + alpha) * (k + beta) * (k + ab);
numer / denom
}
})
.collect();
let mu0 = ((ab + 1.0) * core::f64::consts::LN_2 + ln_gamma(alpha + 1.0) + ln_gamma(beta + 1.0)
- ln_gamma(ab + 2.0))
.exp();
golub_welsch(&diag, &off_diag_sq, mu0)
}
#[allow(clippy::excessive_precision)]
#[allow(clippy::cast_precision_loss)] pub(crate) fn ln_gamma(x: f64) -> f64 {
const COEFF: [f64; 9] = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_13,
-176.615_029_162_140_59,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_571_6e-6,
1.505_632_735_149_311_6e-7,
];
if x < 0.5 {
let z = 1.0 - x;
core::f64::consts::PI.ln() - (core::f64::consts::PI * x).sin().abs().ln() - ln_gamma(z)
} else {
let z = x - 1.0;
let mut sum = COEFF[0];
for (i, &c) in COEFF.iter().enumerate().skip(1) {
sum += c / (z + i as f64);
}
let t = z + 7.5; 0.5 * (2.0 * core::f64::consts::PI).ln() + (t.ln() * (z + 0.5)) - t + sum.ln()
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
#[test]
fn zero_order() {
assert!(GaussJacobi::new(0, 0.0, 0.0).is_err());
}
#[test]
fn invalid_params() {
assert!(GaussJacobi::new(5, -1.0, 0.0).is_err());
assert!(GaussJacobi::new(5, 0.0, -1.5).is_err());
assert!(GaussJacobi::new(5, f64::NAN, 0.0).is_err());
assert!(GaussJacobi::new(5, f64::INFINITY, 0.0).is_err());
assert!(GaussJacobi::new(5, 0.0, f64::NEG_INFINITY).is_err());
}
#[test]
fn legendre_recovery() {
let gj = GaussJacobi::new(10, 0.0, 0.0).unwrap();
let sum: f64 = gj.weights().iter().sum();
assert!((sum - 2.0).abs() < 1e-12, "sum={sum}");
}
#[test]
fn weight_sum_various_params() {
let cases = [(0.5, 0.5), (1.0, 0.0), (0.0, 1.0), (2.0, 3.0), (0.5, 1.5)];
for (a, b) in cases {
let gj = GaussJacobi::new(20, a, b).unwrap();
let sum: f64 = gj.weights().iter().sum();
let expected = jacobi_integral(a, b);
assert!(
(sum - expected).abs() < 1e-10,
"a={a}, b={b}: sum={sum}, expected={expected}"
);
}
}
fn jacobi_integral(a: f64, b: f64) -> f64 {
let log_val =
(a + b + 1.0) * core::f64::consts::LN_2 + ln_gamma(a + 1.0) + ln_gamma(b + 1.0)
- ln_gamma(a + b + 2.0);
log_val.exp()
}
#[test]
fn nodes_sorted_and_bounded() {
let gj = GaussJacobi::new(20, 1.0, 0.5).unwrap();
for i in 0..gj.order() - 1 {
assert!(gj.nodes()[i] < gj.nodes()[i + 1]);
}
assert!(gj.nodes()[0] > -1.0);
assert!(*gj.nodes().last().unwrap() < 1.0);
}
#[test]
fn polynomial_exactness() {
let n = 10;
let alpha = 0.5;
let beta = 1.5;
let gj = GaussJacobi::new(n, alpha, beta).unwrap();
let numerical: f64 = gj
.nodes()
.iter()
.zip(gj.weights())
.map(|(&x, &w)| x * x * w)
.sum();
let gj_sym = GaussJacobi::new(n, 1.0, 1.0).unwrap();
let odd: f64 = gj_sym
.nodes()
.iter()
.zip(gj_sym.weights())
.map(|(&x, &w)| x * w)
.sum();
assert!(odd.abs() < 1e-12, "odd integral = {odd}");
let even: f64 = gj_sym
.nodes()
.iter()
.zip(gj_sym.weights())
.map(|(&x, &w)| x * x * w)
.sum();
assert!(even > 0.0);
assert!(numerical.is_finite());
assert!(numerical.abs() < 10.0);
}
#[test]
fn gegenbauer_symmetry() {
let gj = GaussJacobi::new(15, 1.5, 1.5).unwrap();
let n = gj.order();
for i in 0..n / 2 {
assert!(
(gj.nodes()[i] + gj.nodes()[n - 1 - i]).abs() < 1e-13,
"i={i}: {} vs {}",
gj.nodes()[i],
gj.nodes()[n - 1 - i]
);
assert!(
(gj.weights()[i] - gj.weights()[n - 1 - i]).abs() < 1e-13,
"i={i}: {} vs {}",
gj.weights()[i],
gj.weights()[n - 1 - i]
);
}
}
#[test]
fn chebyshev_type_i_recovery() {
let gj = GaussJacobi::new(20, -0.5, -0.5).unwrap();
let sum: f64 = gj.weights().iter().sum();
assert!((sum - PI).abs() < 1e-10, "sum={sum}, expected={PI}");
}
#[test]
fn jacobi_negative_alpha_beta() {
let alpha = -0.8;
let beta = -0.8;
let gj = GaussJacobi::new(20, alpha, beta).unwrap();
let sum: f64 = gj.weights().iter().sum();
let expected = jacobi_integral(alpha, beta);
assert!(
(sum - expected).abs() < 1e-8,
"alpha={alpha}, beta={beta}: sum={sum}, expected={expected}"
);
}
}