use crate::errors::QlResult;
use crate::math::gammafunction::log_gamma;
use crate::require;
use crate::types::{Real, Size};
use super::momentbasedgaussianpolynomial::MomentBasedPolynomial;
pub struct GaussNonCentralChiSquaredPolynomial {
nu: Real,
lambda: Real,
}
impl GaussNonCentralChiSquaredPolynomial {
pub fn new(nu: Real, lambda: Real) -> QlResult<Self> {
require!(nu.is_finite() && nu > 0.0, "nu must be positive");
require!(
lambda.is_finite() && lambda >= 0.0,
"lambda must be non-negative"
);
Ok(GaussNonCentralChiSquaredPolynomial { nu, lambda })
}
}
impl MomentBasedPolynomial for GaussNonCentralChiSquaredPolynomial {
fn moment(&self, i: Size) -> Real {
let mut sum = 0.0;
let mut binom = 1.0;
for j in 0..=i {
let mut term = binom * self.lambda.powi(j as i32) * 2f64.powi((i - j) as i32);
for m in 0..(i - j) {
term *= 0.5 * self.nu + (j + m) as Real;
}
sum += term;
binom *= (i - j) as Real / (j + 1) as Real;
}
sum
}
fn w(&self, x: Real) -> Real {
non_central_chi_squared_pdf(self.nu, self.lambda, x)
}
}
fn non_central_chi_squared_pdf(nu: Real, lambda: Real, x: Real) -> Real {
assert!(
x >= 0.0,
"non-central chi-squared density is undefined for x < 0"
);
let half_lambda = 0.5 * lambda;
if x == 0.0 {
return match nu.partial_cmp(&2.0).expect("nu is finite") {
std::cmp::Ordering::Less => Real::INFINITY,
std::cmp::Ordering::Equal => 0.5 * (-half_lambda).exp(),
std::cmp::Ordering::Greater => 0.0,
};
}
let ln_x = x.ln();
let ln_2 = std::f64::consts::LN_2;
let ln_chi2 = |half_df: Real, ln_gamma_half_df: Real| {
(half_df - 1.0) * ln_x - 0.5 * x - half_df * ln_2 - ln_gamma_half_df
};
let ln_gamma_half_nu = log_gamma(0.5 * nu).expect("nu/2 > 0");
if half_lambda == 0.0 {
return ln_chi2(0.5 * nu, ln_gamma_half_nu).exp();
}
let ln_half_lambda = half_lambda.ln();
let k_max = 1000.max((half_lambda + 10.0 * half_lambda.sqrt()) as Size + 10);
let mut ln_k_fact = 0.0;
let mut ln_gamma_half_df = ln_gamma_half_nu;
let mut sum = 0.0;
for k in 0..=k_max {
let k_ = k as Real;
let half_df = 0.5 * nu + k_;
let ln_poisson = -half_lambda + k_ * ln_half_lambda - ln_k_fact;
let term = (ln_poisson + ln_chi2(half_df, ln_gamma_half_df)).exp();
sum += term;
if k_ > half_lambda && term < sum * Real::EPSILON {
break;
}
ln_k_fact += (k_ + 1.0).ln();
ln_gamma_half_df += half_df.ln();
}
sum
}
#[cfg(test)]
#[allow(clippy::excessive_precision)]
mod tests {
use super::*;
use crate::math::integrals::gaussianorthogonalpolynomial::GaussianOrthogonalPolynomial;
use crate::math::integrals::gaussianquadratures::GaussianQuadrature;
use crate::math::integrals::momentbasedgaussianpolynomial::MomentBasedGaussianPolynomial;
#[test]
fn non_central_chi_squared_quadrature() {
let poly41 = MomentBasedGaussianPolynomial::new(
GaussNonCentralChiSquaredPolynomial::new(4.0, 1.0).expect("valid parameters"),
);
let quad = GaussianQuadrature::new(2, &poly41).expect("2 > 0");
let calculated = quad.integrate(|x| x * x * poly41.w(x));
assert!(
(calculated - 37.0).abs() <= 1.0e-4,
"integrating f(x) = x^2 * nonCentralChiSquared(4, 1)(x): \
calculated {calculated}, expected 37.0"
);
let poly11 = MomentBasedGaussianPolynomial::new(
GaussNonCentralChiSquaredPolynomial::new(1.0, 1.0).expect("valid parameters"),
);
let quad = GaussianQuadrature::new(14, &poly11).expect("14 > 0");
let calculated = quad.integrate(|x| x * (0.1 * x).sin() * (0.3 * x).exp() * poly11.w(x));
assert!(
(calculated - 17.408092).abs() <= 1.0e-4,
"integrating f(x) = x * sin(0.1*x)*exp(0.3*x)*nonCentralChiSquared(1, 1)(x): \
calculated {calculated}, expected 17.408092"
);
}
#[test]
fn density_converges_for_large_non_centrality() {
let (nu, lambda) = (4.0, 10000.0);
let calculated = non_central_chi_squared_pdf(nu, lambda, nu + lambda);
let sigma2 = 2.0 * (nu + 2.0 * lambda);
let approx = 1.0 / (std::f64::consts::TAU * sigma2).sqrt();
assert!(
(calculated / approx - 1.0).abs() <= 0.01,
"density at the mean for lambda = {lambda}: \
calculated {calculated}, normal approximation {approx}"
);
}
#[test]
fn central_case_matches_chi_squared_density() {
let poly = GaussNonCentralChiSquaredPolynomial::new(4.0, 0.0).expect("lambda = 0 is valid");
for x in [0.5f64, 1.0, 3.0, 10.0] {
let expected = x * (-0.5 * x).exp() / 4.0;
let calculated = poly.w(x);
assert!(
(calculated - expected).abs() <= 1e-14,
"central chi-squared density at {x}: \
calculated {calculated}, expected {expected}"
);
}
assert_eq!(poly.moment(1), 4.0);
}
#[test]
fn density_at_the_origin_takes_the_exact_limit() {
assert_eq!(non_central_chi_squared_pdf(4.0, 1.0, 0.0), 0.0);
let expected = 0.5 * (-0.5f64).exp();
assert!((non_central_chi_squared_pdf(2.0, 1.0, 0.0) - expected).abs() <= 1e-16);
assert_eq!(non_central_chi_squared_pdf(1.0, 1.0, 0.0), Real::INFINITY);
}
#[test]
#[should_panic(expected = "undefined for x < 0")]
fn density_panics_for_negative_argument() {
non_central_chi_squared_pdf(4.0, 1.0, -1.0);
}
#[test]
fn non_central_chi_squared_sum_of_nodes() {
let expected = [
47.53491786730293,
70.6103295419633383,
98.0593406849441607,
129.853401537905341,
165.96963582663912,
206.389183233992043,
];
let orth_poly = MomentBasedGaussianPolynomial::new(
GaussNonCentralChiSquaredPolynomial::new(4.0, 1.0).expect("valid parameters"),
);
let tol = 1e-5;
for n in 4..10 {
let quad = GaussianQuadrature::new(n, &orth_poly).expect("n > 0");
let calculated: Real = quad.abscissas().iter().sum();
assert!(
(calculated - expected[n - 4]).abs() <= tol,
"failed to reproduce rule of sum for n = {n}: \
calculated {calculated}, expected {}",
expected[n - 4]
);
}
}
}