use numra_core::Scalar;
pub trait Kernel<S: Scalar>: Clone {
fn evaluate(&self, tau: S) -> S;
fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
None
}
}
#[derive(Clone, Debug)]
pub struct ExponentialKernel<S: Scalar> {
pub a: S,
pub b: S,
}
impl<S: Scalar> ExponentialKernel<S> {
pub fn new(a: S, b: S) -> Self {
Self { a, b }
}
}
impl<S: Scalar> Kernel<S> for ExponentialKernel<S> {
fn evaluate(&self, tau: S) -> S {
self.a * (-self.b * tau).exp()
}
fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
Some((vec![self.a], vec![self.b]))
}
}
#[derive(Clone, Debug)]
pub struct PowerLawKernel<S: Scalar> {
pub a: S,
pub alpha: S,
}
impl<S: Scalar> PowerLawKernel<S> {
pub fn new(a: S, alpha: S) -> Self {
Self { a, alpha }
}
}
impl<S: Scalar> Kernel<S> for PowerLawKernel<S> {
fn evaluate(&self, tau: S) -> S {
if tau <= S::ZERO {
S::ZERO } else {
self.a * tau.powf(-self.alpha)
}
}
}
#[derive(Clone, Debug)]
pub struct PronyKernel<S: Scalar> {
pub amplitudes: Vec<S>,
pub rates: Vec<S>,
}
impl<S: Scalar> PronyKernel<S> {
pub fn new(amplitudes: Vec<S>, rates: Vec<S>) -> Self {
assert_eq!(
amplitudes.len(),
rates.len(),
"Amplitudes and rates must have same length"
);
Self { amplitudes, rates }
}
pub fn single(a: S, b: S) -> Self {
Self {
amplitudes: vec![a],
rates: vec![b],
}
}
pub fn two_term(a1: S, b1: S, a2: S, b2: S) -> Self {
Self {
amplitudes: vec![a1, a2],
rates: vec![b1, b2],
}
}
pub fn num_terms(&self) -> usize {
self.amplitudes.len()
}
}
impl<S: Scalar> Kernel<S> for PronyKernel<S> {
fn evaluate(&self, tau: S) -> S {
let mut sum = S::ZERO;
for (a, b) in self.amplitudes.iter().zip(self.rates.iter()) {
sum += *a * (-*b * tau).exp();
}
sum
}
fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
Some((self.amplitudes.clone(), self.rates.clone()))
}
}
#[derive(Clone, Debug)]
pub struct MittagLefflerKernel<S: Scalar> {
pub lambda: S,
pub alpha: S,
pub beta: S,
}
impl<S: Scalar> MittagLefflerKernel<S> {
pub fn new(lambda: S, alpha: S, beta: S) -> Self {
Self {
lambda,
alpha,
beta,
}
}
pub fn relaxation(lambda: S, alpha: S) -> Self {
Self {
lambda,
alpha,
beta: alpha,
}
}
}
impl<S: Scalar> Kernel<S> for MittagLefflerKernel<S> {
fn evaluate(&self, tau: S) -> S {
if tau <= S::ZERO {
return S::ZERO;
}
let z = -self.lambda * tau.powf(self.alpha);
let ml = mittag_leffler_approx(z, self.alpha, self.beta, 50);
tau.powf(self.beta - S::ONE) * ml
}
}
fn mittag_leffler_approx<S: Scalar>(z: S, alpha: S, beta: S, max_terms: usize) -> S {
let tol = S::from_f64(1e-12);
let mut sum = S::ZERO;
let mut z_pow = S::ONE;
for k in 0..max_terms {
let gamma_arg = alpha * S::from_usize(k) + beta;
let gamma_val = gamma_approx(gamma_arg);
let term = z_pow / gamma_val;
sum += term;
if term.abs() < tol * sum.abs() && k > 0 {
break;
}
z_pow *= z;
}
sum
}
fn gamma_approx<S: Scalar>(z: S) -> S {
#[allow(clippy::excessive_precision)]
let p = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7,
];
let z_f = z.to_f64();
if z_f < 0.5 {
let pi = std::f64::consts::PI;
let sin_pz = (pi * z_f).sin();
let g1mz = gamma_approx(S::ONE - z);
return S::from_f64(pi) / (S::from_f64(sin_pz) * g1mz);
}
let z_f = z_f - 1.0;
let mut x = p[0];
for (i, coeff) in p.iter().enumerate().skip(1) {
x += coeff / (z_f + i as f64);
}
let t = z_f + 7.5;
let result = (2.0 * std::f64::consts::PI).sqrt() * t.powf(z_f + 0.5) * (-t).exp() * x;
S::from_f64(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exponential_kernel() {
let k = ExponentialKernel::new(2.0_f64, 0.5);
assert!((k.evaluate(0.0) - 2.0).abs() < 1e-10);
let expected = 2.0 * (-1.0_f64).exp();
assert!((k.evaluate(2.0) - expected).abs() < 1e-10);
let (a, b) = k.as_prony().unwrap();
assert_eq!(a.len(), 1);
assert!((a[0] - 2.0).abs() < 1e-10);
assert!((b[0] - 0.5).abs() < 1e-10);
}
#[test]
fn test_power_law_kernel() {
let k = PowerLawKernel::new(1.0_f64, 0.5);
assert!((k.evaluate(1.0) - 1.0).abs() < 1e-10);
assert!((k.evaluate(4.0) - 0.5).abs() < 1e-10);
assert!((k.evaluate(0.0) - 0.0).abs() < 1e-10);
assert!(k.as_prony().is_none());
}
#[test]
fn test_prony_kernel() {
let k = PronyKernel::two_term(1.0_f64, 0.5, 2.0, 1.0);
assert!((k.evaluate(0.0) - 3.0).abs() < 1e-10);
let expected = (-1.0_f64).exp() + 2.0 * (-2.0_f64).exp();
assert!((k.evaluate(2.0) - expected).abs() < 1e-10);
assert_eq!(k.num_terms(), 2);
}
#[test]
fn test_prony_single() {
let prony = PronyKernel::single(2.0_f64, 0.5);
let exp_k = ExponentialKernel::new(2.0_f64, 0.5);
for tau in [0.0, 0.5, 1.0, 2.0, 5.0] {
let p_val = prony.evaluate(tau);
let e_val = exp_k.evaluate(tau);
assert!(
(p_val - e_val).abs() < 1e-10,
"Prony single should match ExponentialKernel at tau={}: {} vs {}",
tau,
p_val,
e_val
);
}
}
#[test]
fn test_power_law_negative_tau() {
let k = PowerLawKernel::new(1.0_f64, 0.5);
assert!((k.evaluate(-1.0)).abs() < 1e-10);
assert!((k.evaluate(-0.001)).abs() < 1e-10);
}
#[test]
fn test_kernel_clone() {
let exp_k = ExponentialKernel::new(2.0_f64, 0.5);
let exp_clone = exp_k.clone();
assert!((exp_clone.evaluate(1.0) - exp_k.evaluate(1.0)).abs() < 1e-15);
let pow_k = PowerLawKernel::new(1.0_f64, 0.5);
let pow_clone = pow_k.clone();
assert!((pow_clone.evaluate(4.0) - pow_k.evaluate(4.0)).abs() < 1e-15);
let prony_k = PronyKernel::two_term(1.0_f64, 0.5, 2.0, 1.0);
let prony_clone = prony_k.clone();
assert!((prony_clone.evaluate(1.0) - prony_k.evaluate(1.0)).abs() < 1e-15);
let ml_k = MittagLefflerKernel::new(1.0_f64, 1.0, 1.0);
let ml_clone = ml_k.clone();
assert!((ml_clone.evaluate(1.0) - ml_k.evaluate(1.0)).abs() < 1e-15);
}
#[test]
fn test_mittag_leffler_reduces_to_exponential() {
let ml_kernel = MittagLefflerKernel::new(1.0_f64, 1.0, 1.0);
let exp_kernel = ExponentialKernel::new(1.0_f64, 1.0);
for tau in [0.1, 0.5, 1.0, 2.0] {
let ml = ml_kernel.evaluate(tau);
let exp = exp_kernel.evaluate(tau);
assert!(
(ml - exp).abs() < 0.01,
"ML({}) = {}, exp = {}",
tau,
ml,
exp
);
}
}
}