use std::num::NonZeroUsize;
use gauss_quad::laguerre::GaussLaguerre;
use ndarray::Array1;
use stochastic_rs_distributions::special::gamma;
use stochastic_rs_distributions::special::ln_gamma;
use crate::traits::FloatExt;
#[derive(Debug, Clone)]
pub struct RlKernel<T: FloatExt> {
pub hurst: T,
pub nodes: Array1<T>,
pub weights: Array1<T>,
pub gamma_h_half: T,
}
impl<T: FloatExt> RlKernel<T> {
#[must_use]
pub fn default_degree(n: usize) -> usize {
((n.max(2) as f64).ln() as usize) + 20
}
#[must_use]
pub fn new(hurst: T, degree: usize) -> Self {
let h_f64 = hurst.to_f64().expect("Hurst must be convertible to f64");
assert!(
h_f64 > 0.0 && h_f64 < 0.5,
"RL kernel requires Hurst in (0, 1/2), got {h_f64}"
);
assert!(degree > 0, "quadrature degree must be positive");
let alpha = -(h_f64 + 0.5);
let (nodes_f64, weights_f64) = gen_laguerre_nodes_weights(degree, alpha);
let inv_gamma_half_minus_h = 1.0 / gamma(0.5 - h_f64);
let mut nodes = Array1::<T>::zeros(degree);
let mut weights = Array1::<T>::zeros(degree);
for i in 0..degree {
nodes[i] = T::from_f64_fast(nodes_f64[i]);
weights[i] = T::from_f64_fast(weights_f64[i] * nodes_f64[i].exp() * inv_gamma_half_minus_h);
}
Self {
hurst,
nodes,
weights,
gamma_h_half: T::from_f64_fast(gamma(h_f64 + 0.5)),
}
}
#[must_use]
pub fn degree(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn evaluate(&self, t: T) -> T {
let mut acc = T::zero();
for (x, w) in self.nodes.iter().zip(self.weights.iter()) {
acc += *w * (-*x * t).exp();
}
acc
}
}
fn laguerre_l_and_dl(n: usize, alpha: f64, x: f64) -> (f64, f64) {
if n == 0 {
return (1.0, 0.0);
}
let mut lnm1 = 1.0;
let mut ln = 1.0 + alpha - x;
for k in 1..n {
let kf = k as f64;
let lnp1 = ((2.0 * kf + 1.0 + alpha - x) * ln - (kf + alpha) * lnm1) / (kf + 1.0);
lnm1 = ln;
ln = lnp1;
}
let nf = n as f64;
let dln = (nf * ln - (nf + alpha) * lnm1) / x;
(ln, dln)
}
fn gen_laguerre_nodes_weights(n: usize, alpha: f64) -> (Vec<f64>, Vec<f64>) {
assert!(alpha > -1.0, "alpha must be > -1");
let quad = GaussLaguerre::new(
NonZeroUsize::new(n).expect("n must be positive"),
alpha.try_into().expect("alpha > -1 checked above"),
);
let log_norm = ln_gamma(n as f64 + alpha + 1.0) - ln_gamma(n as f64 + 1.0);
let norm = log_norm.exp();
let nodes: Vec<f64> = quad.nodes().copied().collect();
let weights: Vec<f64> = nodes
.iter()
.map(|&x| {
let (_l, dl) = laguerre_l_and_dl(n, alpha, x);
norm / (x * dl * dl)
})
.collect();
(nodes, weights)
}
#[cfg(test)]
mod tests {
use super::RlKernel;
use super::gen_laguerre_nodes_weights;
#[test]
fn exp_sum_approximates_power_law() {
let hurst = 0.1_f64;
let k = RlKernel::<f64>::new(hurst, 40);
let exponent = hurst - 0.5;
for t in [0.2_f64, 1.0, 5.0] {
let approx = k.evaluate(t);
let truth = t.powf(exponent);
let rel = (approx - truth).abs() / truth;
assert!(rel < 5e-3, "t={t} approx={approx} truth={truth} rel={rel}");
}
}
#[test]
fn laguerre_matches_scipy_reference_first_nodes() {
let (nodes, weights) = gen_laguerre_nodes_weights(20, -0.6);
let scipy_first = [
(0.023547480568583978_f64, 1.0134437918563453_f64),
(0.25573619389320856, 0.622864701359439),
(0.7340211023623413, 0.34792337656527667),
(1.4612387213185818, 0.1575950982469561),
(2.44197358108164, 0.05648501697792918),
];
for (i, (xs, ws)) in scipy_first.iter().enumerate() {
let dx = (nodes[i] - xs).abs();
let dw = (weights[i] - ws).abs() / ws;
assert!(
dx < 1e-10,
"node {i}: got {} vs scipy {xs} (diff {dx})",
nodes[i]
);
assert!(
dw < 1e-8,
"weight {i}: got {} vs scipy {ws} (rel {dw})",
weights[i]
);
}
}
#[test]
fn weights_exp_x_stay_bounded_at_high_degree() {
let (nodes, weights) = gen_laguerre_nodes_weights(40, -0.6);
let eff_max = nodes
.iter()
.zip(weights.iter())
.map(|(x, w)| w * x.exp())
.fold(f64::NEG_INFINITY, f64::max);
assert!(
eff_max < 10.0,
"w*exp(x) must be bounded; max={eff_max} indicates Golub-Welsch-style blowup"
);
}
#[test]
#[should_panic(expected = "Hurst in (0, 1/2)")]
fn rejects_h_at_half() {
let _ = RlKernel::<f64>::new(0.5, 20);
}
#[test]
#[should_panic(expected = "Hurst in (0, 1/2)")]
fn rejects_h_above_half() {
let _ = RlKernel::<f64>::new(0.7, 20);
}
#[test]
fn degree_default_scales_with_log_n() {
assert_eq!(RlKernel::<f64>::default_degree(1000), 26);
assert_eq!(RlKernel::<f64>::default_degree(10_000), 29);
}
}