1use numra_core::Scalar;
11
12pub trait Kernel<S: Scalar>: Clone {
16 fn evaluate(&self, tau: S) -> S;
18
19 fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
23 None
24 }
25}
26
27#[derive(Clone, Debug)]
31pub struct ExponentialKernel<S: Scalar> {
32 pub a: S,
34 pub b: S,
36}
37
38impl<S: Scalar> ExponentialKernel<S> {
39 pub fn new(a: S, b: S) -> Self {
40 Self { a, b }
41 }
42}
43
44impl<S: Scalar> Kernel<S> for ExponentialKernel<S> {
45 fn evaluate(&self, tau: S) -> S {
46 self.a * (-self.b * tau).exp()
47 }
48
49 fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
50 Some((vec![self.a], vec![self.b]))
51 }
52}
53
54#[derive(Clone, Debug)]
58pub struct PowerLawKernel<S: Scalar> {
59 pub a: S,
61 pub alpha: S,
63}
64
65impl<S: Scalar> PowerLawKernel<S> {
66 pub fn new(a: S, alpha: S) -> Self {
67 Self { a, alpha }
68 }
69}
70
71impl<S: Scalar> Kernel<S> for PowerLawKernel<S> {
72 fn evaluate(&self, tau: S) -> S {
73 if tau <= S::ZERO {
74 S::ZERO } else {
76 self.a * tau.powf(-self.alpha)
77 }
78 }
79}
80
81#[derive(Clone, Debug)]
88pub struct PronyKernel<S: Scalar> {
89 pub amplitudes: Vec<S>,
91 pub rates: Vec<S>,
93}
94
95impl<S: Scalar> PronyKernel<S> {
96 pub fn new(amplitudes: Vec<S>, rates: Vec<S>) -> Self {
102 assert_eq!(
103 amplitudes.len(),
104 rates.len(),
105 "Amplitudes and rates must have same length"
106 );
107 Self { amplitudes, rates }
108 }
109
110 pub fn single(a: S, b: S) -> Self {
112 Self {
113 amplitudes: vec![a],
114 rates: vec![b],
115 }
116 }
117
118 pub fn two_term(a1: S, b1: S, a2: S, b2: S) -> Self {
120 Self {
121 amplitudes: vec![a1, a2],
122 rates: vec![b1, b2],
123 }
124 }
125
126 pub fn num_terms(&self) -> usize {
128 self.amplitudes.len()
129 }
130}
131
132impl<S: Scalar> Kernel<S> for PronyKernel<S> {
133 fn evaluate(&self, tau: S) -> S {
134 let mut sum = S::ZERO;
135 for (a, b) in self.amplitudes.iter().zip(self.rates.iter()) {
136 sum += *a * (-*b * tau).exp();
137 }
138 sum
139 }
140
141 fn as_prony(&self) -> Option<(Vec<S>, Vec<S>)> {
142 Some((self.amplitudes.clone(), self.rates.clone()))
143 }
144}
145
146#[derive(Clone, Debug)]
152pub struct MittagLefflerKernel<S: Scalar> {
153 pub lambda: S,
155 pub alpha: S,
157 pub beta: S,
159}
160
161impl<S: Scalar> MittagLefflerKernel<S> {
162 pub fn new(lambda: S, alpha: S, beta: S) -> Self {
163 Self {
164 lambda,
165 alpha,
166 beta,
167 }
168 }
169
170 pub fn relaxation(lambda: S, alpha: S) -> Self {
172 Self {
173 lambda,
174 alpha,
175 beta: alpha,
176 }
177 }
178}
179
180impl<S: Scalar> Kernel<S> for MittagLefflerKernel<S> {
181 fn evaluate(&self, tau: S) -> S {
182 if tau <= S::ZERO {
183 return S::ZERO;
184 }
185
186 let z = -self.lambda * tau.powf(self.alpha);
188 let ml = mittag_leffler_approx(z, self.alpha, self.beta, 50);
189 tau.powf(self.beta - S::ONE) * ml
190 }
191}
192
193fn mittag_leffler_approx<S: Scalar>(z: S, alpha: S, beta: S, max_terms: usize) -> S {
195 let tol = S::from_f64(1e-12);
196 let mut sum = S::ZERO;
197 let mut z_pow = S::ONE;
198
199 for k in 0..max_terms {
200 let gamma_arg = alpha * S::from_usize(k) + beta;
201 let gamma_val = gamma_approx(gamma_arg);
202 let term = z_pow / gamma_val;
203 sum += term;
204
205 if term.abs() < tol * sum.abs() && k > 0 {
206 break;
207 }
208 z_pow *= z;
209 }
210 sum
211}
212
213fn gamma_approx<S: Scalar>(z: S) -> S {
215 #[allow(clippy::excessive_precision)]
216 let p = [
217 0.99999999999980993,
218 676.5203681218851,
219 -1259.1392167224028,
220 771.32342877765313,
221 -176.61502916214059,
222 12.507343278686905,
223 -0.13857109526572012,
224 9.9843695780195716e-6,
225 1.5056327351493116e-7,
226 ];
227
228 let z_f = z.to_f64();
229 if z_f < 0.5 {
230 let pi = std::f64::consts::PI;
231 let sin_pz = (pi * z_f).sin();
232 let g1mz = gamma_approx(S::ONE - z);
233 return S::from_f64(pi) / (S::from_f64(sin_pz) * g1mz);
234 }
235
236 let z_f = z_f - 1.0;
237 let mut x = p[0];
238 for (i, coeff) in p.iter().enumerate().skip(1) {
239 x += coeff / (z_f + i as f64);
240 }
241
242 let t = z_f + 7.5;
243 let result = (2.0 * std::f64::consts::PI).sqrt() * t.powf(z_f + 0.5) * (-t).exp() * x;
244 S::from_f64(result)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_exponential_kernel() {
253 let k = ExponentialKernel::new(2.0_f64, 0.5);
254
255 assert!((k.evaluate(0.0) - 2.0).abs() < 1e-10);
257
258 let expected = 2.0 * (-1.0_f64).exp();
260 assert!((k.evaluate(2.0) - expected).abs() < 1e-10);
261
262 let (a, b) = k.as_prony().unwrap();
264 assert_eq!(a.len(), 1);
265 assert!((a[0] - 2.0).abs() < 1e-10);
266 assert!((b[0] - 0.5).abs() < 1e-10);
267 }
268
269 #[test]
270 fn test_power_law_kernel() {
271 let k = PowerLawKernel::new(1.0_f64, 0.5);
272
273 assert!((k.evaluate(1.0) - 1.0).abs() < 1e-10);
275
276 assert!((k.evaluate(4.0) - 0.5).abs() < 1e-10);
278
279 assert!((k.evaluate(0.0) - 0.0).abs() < 1e-10);
281
282 assert!(k.as_prony().is_none());
284 }
285
286 #[test]
287 fn test_prony_kernel() {
288 let k = PronyKernel::two_term(1.0_f64, 0.5, 2.0, 1.0);
290
291 assert!((k.evaluate(0.0) - 3.0).abs() < 1e-10);
293
294 let expected = (-1.0_f64).exp() + 2.0 * (-2.0_f64).exp();
296 assert!((k.evaluate(2.0) - expected).abs() < 1e-10);
297
298 assert_eq!(k.num_terms(), 2);
299 }
300
301 #[test]
302 fn test_prony_single() {
303 let prony = PronyKernel::single(2.0_f64, 0.5);
305 let exp_k = ExponentialKernel::new(2.0_f64, 0.5);
306
307 for tau in [0.0, 0.5, 1.0, 2.0, 5.0] {
308 let p_val = prony.evaluate(tau);
309 let e_val = exp_k.evaluate(tau);
310 assert!(
311 (p_val - e_val).abs() < 1e-10,
312 "Prony single should match ExponentialKernel at tau={}: {} vs {}",
313 tau,
314 p_val,
315 e_val
316 );
317 }
318 }
319
320 #[test]
321 fn test_power_law_negative_tau() {
322 let k = PowerLawKernel::new(1.0_f64, 0.5);
323
324 assert!((k.evaluate(-1.0)).abs() < 1e-10);
326 assert!((k.evaluate(-0.001)).abs() < 1e-10);
327 }
328
329 #[test]
330 fn test_kernel_clone() {
331 let exp_k = ExponentialKernel::new(2.0_f64, 0.5);
332 let exp_clone = exp_k.clone();
333 assert!((exp_clone.evaluate(1.0) - exp_k.evaluate(1.0)).abs() < 1e-15);
334
335 let pow_k = PowerLawKernel::new(1.0_f64, 0.5);
336 let pow_clone = pow_k.clone();
337 assert!((pow_clone.evaluate(4.0) - pow_k.evaluate(4.0)).abs() < 1e-15);
338
339 let prony_k = PronyKernel::two_term(1.0_f64, 0.5, 2.0, 1.0);
340 let prony_clone = prony_k.clone();
341 assert!((prony_clone.evaluate(1.0) - prony_k.evaluate(1.0)).abs() < 1e-15);
342
343 let ml_k = MittagLefflerKernel::new(1.0_f64, 1.0, 1.0);
344 let ml_clone = ml_k.clone();
345 assert!((ml_clone.evaluate(1.0) - ml_k.evaluate(1.0)).abs() < 1e-15);
346 }
347
348 #[test]
349 fn test_mittag_leffler_reduces_to_exponential() {
350 let ml_kernel = MittagLefflerKernel::new(1.0_f64, 1.0, 1.0);
352 let exp_kernel = ExponentialKernel::new(1.0_f64, 1.0);
353
354 for tau in [0.1, 0.5, 1.0, 2.0] {
355 let ml = ml_kernel.evaluate(tau);
356 let exp = exp_kernel.evaluate(tau);
357 assert!(
358 (ml - exp).abs() < 0.01,
359 "ML({}) = {}, exp = {}",
360 tau,
361 ml,
362 exp
363 );
364 }
365 }
366}