1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
23#![allow(clippy::too_many_arguments)]
24
25use std::f64::consts::PI;
26
27struct Lcg {
31 state: u64,
32}
33
34impl Lcg {
35 fn new(seed: u64) -> Self {
36 Self {
37 state: seed.wrapping_add(1),
38 }
39 }
40 fn next_u64(&mut self) -> u64 {
41 self.state = self
42 .state
43 .wrapping_mul(6_364_136_223_846_793_005)
44 .wrapping_add(1_442_695_040_888_963_407);
45 self.state
46 }
47 fn uniform(&mut self) -> f64 {
48 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
49 }
50 fn normal(&mut self) -> f64 {
51 let u1 = self.uniform().max(1e-300);
52 let u2 = self.uniform();
53 (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
54 }
55}
56
57pub fn gamma(x: f64) -> f64 {
65 if x <= 0.0 {
66 let sinpix = (PI * x).sin();
68 if sinpix.abs() < 1e-300 {
69 return f64::INFINITY;
70 }
71 return PI / (sinpix * gamma(1.0 - x));
72 }
73 if x < 0.5 {
74 return PI / ((PI * x).sin() * gamma(1.0 - x));
75 }
76 let g = 7.0_f64;
78 let c = [
79 0.999_999_999_999_809_9_f64,
80 676.520_368_121_885_1,
81 -1_259.139_216_722_402_8,
82 771.323_428_777_653_1,
83 -176.615_029_162_140_6,
84 12.507_343_278_686_905,
85 -0.138_571_095_265_720_12,
86 9.984_369_578_019_572e-6,
87 1.505_632_735_149_311_6e-7,
88 ];
89 let z = x - 1.0;
90 let mut sum = c[0];
91 for (i, &ci) in c[1..].iter().enumerate() {
92 sum += ci / (z + (i as f64) + 1.0);
93 }
94 let t = z + g + 0.5;
95 (2.0 * PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * sum
96}
97
98pub fn log_gamma(x: f64) -> f64 {
102 if x <= 0.0 {
103 return f64::INFINITY;
104 }
105 gamma(x).abs().ln()
106}
107
108pub fn mittag_leffler_1(alpha: f64, z: f64, max_terms: usize) -> f64 {
117 assert!(alpha > 0.0, "alpha must be positive");
118 let mut sum = 0.0_f64;
119 let mut z_pow = 1.0_f64; for k in 0..max_terms {
121 let g = gamma(alpha * k as f64 + 1.0);
122 if g.is_infinite() || g.is_nan() {
123 break;
124 }
125 let term = z_pow / g;
126 sum += term;
127 if term.abs() < 1e-15 * sum.abs().max(1e-300) {
128 break;
129 }
130 z_pow *= z;
131 }
132 sum
133}
134
135pub fn mittag_leffler_2(alpha: f64, beta: f64, z: f64, max_terms: usize) -> f64 {
140 assert!(alpha > 0.0, "alpha must be positive");
141 assert!(beta > 0.0, "beta must be positive");
142 let mut sum = 0.0_f64;
143 let mut z_pow = 1.0_f64;
144 for k in 0..max_terms {
145 let g = gamma(alpha * k as f64 + beta);
146 if g.is_infinite() || g.is_nan() {
147 break;
148 }
149 let term = z_pow / g;
150 sum += term;
151 if term.abs() < 1e-15 * sum.abs().max(1e-300) {
152 break;
153 }
154 z_pow *= z;
155 }
156 sum
157}
158
159pub fn mittag_leffler_derivative(alpha: f64, z: f64, max_terms: usize) -> f64 {
164 mittag_leffler_2(alpha, alpha, z, max_terms)
165}
166
167pub fn rl_integral(f_values: &[f64], h: f64, alpha: f64) -> f64 {
183 assert!(alpha > 0.0, "alpha must be positive");
184 let n = f_values.len();
185 if n == 0 {
186 return 0.0;
187 }
188 let g_alpha1 = gamma(alpha + 1.0);
191 let mut sum = 0.0_f64;
192 let n_idx = n - 1;
193 for j in 0..n {
194 let k = n_idx - j; let b = (k as f64 + 1.0).powf(alpha) - (k as f64).powf(alpha);
196 sum += b * f_values[j];
197 }
198 h.powf(alpha) * sum / g_alpha1
199}
200
201pub fn rl_integral_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
205 (1..=f_values.len())
206 .map(|n| rl_integral(&f_values[..n], h, alpha))
207 .collect()
208}
209
210fn gl_coeff(alpha: f64, j: usize) -> f64 {
218 let mut g = 1.0_f64;
220 for k in 1..=j {
221 g *= (k as f64 - 1.0 - alpha) / k as f64;
222 }
223 g
224}
225
226pub fn rl_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
236 assert!(alpha > 0.0 && alpha < 2.0, "alpha must be in (0,2)");
237 let n = f_values.len();
238 if n == 0 {
239 return 0.0;
240 }
241 let mut sum = 0.0_f64;
242 for j in 0..n {
243 let g = gl_coeff(alpha, j);
244 sum += g * f_values[n - 1 - j];
245 }
246 h.powf(-alpha) * sum
247}
248
249pub fn rl_derivative_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
251 (1..=f_values.len())
252 .map(|n| rl_derivative(&f_values[..n], h, alpha))
253 .collect()
254}
255
256pub fn caputo_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
274 assert!(alpha > 0.0 && alpha < 1.0, "alpha must be in (0,1)");
275 let n = f_values.len();
276 if n < 2 {
277 return 0.0;
278 }
279 let mu = 1.0 - alpha;
280 let g = gamma(mu + 1.0); let mut sum = 0.0_f64;
282 let m = n - 1;
283 for j in 0..m {
284 let df = f_values[j + 1] - f_values[j];
285 let b = ((m - j) as f64).powf(mu) - ((m - j - 1) as f64).powf(mu);
286 sum += b * df;
287 }
288 sum / (h.powf(alpha) * g)
289}
290
291pub fn caputo_derivative_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
293 (2..=f_values.len())
294 .map(|n| caputo_derivative(&f_values[..n], h, alpha))
295 .collect()
296}
297
298pub fn gl_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
309 rl_derivative(f_values, h, alpha)
310}
311
312pub fn gl_coefficients(alpha: f64, n: usize) -> Vec<f64> {
316 let mut coeffs = Vec::with_capacity(n + 1);
317 let mut g = 1.0_f64;
318 coeffs.push(g);
319 for k in 1..=n {
320 g *= (k as f64 - 1.0 - alpha) / k as f64;
321 coeffs.push(g);
322 }
323 coeffs
324}
325
326pub fn riesz_derivative(f: &[f64], h: f64, alpha: f64) -> Vec<f64> {
345 assert!(
346 alpha > 0.0 && alpha < 2.0 && (alpha - 1.0).abs() > 1e-12,
347 "alpha must be in (0,2) and not equal to 1"
348 );
349 let n = f.len();
350 let c = -1.0 / (2.0 * (PI * alpha / 2.0).cos());
351 let coeffs = gl_coefficients(alpha, n + 1);
352 let mut result = vec![0.0_f64; n];
353 for j in 0..n {
354 let mut left = 0.0_f64;
355 let mut right = 0.0_f64;
356 for k in 0..=j + 1 {
357 let fk_left = if k <= j { f[j - k] } else { 0.0 };
358 let fk_right = if j + k < n { f[j + k] } else { 0.0 };
359 left += coeffs[k] * fk_left;
360 right += coeffs[k] * fk_right;
361 }
362 result[j] = c * (left + right) / h.powf(alpha);
363 }
364 result
365}
366
367pub fn fractional_laplacian_1d(f: &[f64], h: f64, s: f64) -> Vec<f64> {
384 assert!(s > 0.0 && s <= 1.0, "s must be in (0,1]");
385 let alpha = 2.0 * s;
387 if (alpha - 1.0).abs() < 1e-12 {
388 return vec![0.0; f.len()];
390 }
391 riesz_derivative(f, h, alpha).iter().map(|&v| -v).collect()
393}
394
395pub fn fractional_laplacian_2d(f: &[f64], nx: usize, ny: usize, h: f64, s: f64) -> Vec<f64> {
406 assert_eq!(f.len(), nx * ny);
407 let mut result = vec![0.0_f64; nx * ny];
408 for iy in 0..ny {
410 let row: Vec<f64> = (0..nx).map(|ix| f[iy * nx + ix]).collect();
411 let d = fractional_laplacian_1d(&row, h, s);
412 for ix in 0..nx {
413 result[iy * nx + ix] += d[ix];
414 }
415 }
416 for ix in 0..nx {
418 let col: Vec<f64> = (0..ny).map(|iy| f[iy * nx + ix]).collect();
419 let d = fractional_laplacian_1d(&col, h, s);
420 for iy in 0..ny {
421 result[iy * nx + ix] += d[iy];
422 }
423 }
424 result
425}
426
427pub fn memory_kernel_power_law(t: f64, alpha: f64) -> f64 {
436 assert!(alpha > 0.0);
437 if t <= 0.0 {
438 return 0.0;
439 }
440 t.powf(alpha - 1.0) / gamma(alpha)
441}
442
443pub fn memory_kernel_exponential(t: f64, lambda: f64) -> f64 {
447 if t < 0.0 {
448 return 0.0;
449 }
450 lambda * (-lambda * t).exp()
451}
452
453pub fn memory_kernel_mittag_leffler(t: f64, alpha: f64, lambda: f64) -> f64 {
458 assert!(alpha > 0.0 && alpha <= 1.0);
459 if t <= 0.0 {
460 return 0.0;
461 }
462 t.powf(alpha - 1.0) * mittag_leffler_2(alpha, alpha, -lambda * t.powf(alpha), 100)
463}
464
465pub fn memory_convolution(kernel_vals: &[f64], f_vals: &[f64], h: f64) -> f64 {
472 assert_eq!(kernel_vals.len(), f_vals.len());
473 let n = kernel_vals.len();
474 if n == 0 {
475 return 0.0;
476 }
477 let mut sum = 0.5 * (kernel_vals[0] * f_vals[n - 1] + kernel_vals[n - 1] * f_vals[0]);
479 for i in 1..n - 1 {
480 sum += kernel_vals[i] * f_vals[n - 1 - i];
481 }
482 h * sum
483}
484
485#[derive(Debug, Clone, Copy)]
495pub struct AnomalousDiffusionParams {
496 pub alpha: f64,
498 pub diffusivity: f64,
500}
501
502impl AnomalousDiffusionParams {
503 pub fn new(alpha: f64, diffusivity: f64) -> Self {
505 assert!(alpha > 0.0 && alpha < 2.0, "alpha must be in (0,2)");
506 assert!(diffusivity > 0.0, "diffusivity must be positive");
507 Self { alpha, diffusivity }
508 }
509
510 pub fn msd(&self, t: f64) -> f64 {
512 if t <= 0.0 {
513 return 0.0;
514 }
515 2.0 * self.diffusivity * t.powf(self.alpha) / gamma(1.0 + self.alpha)
516 }
517
518 pub fn anomalous_exponent(&self) -> f64 {
520 self.alpha
521 }
522
523 pub fn is_subdiffusion(&self) -> bool {
525 self.alpha < 1.0
526 }
527
528 pub fn is_superdiffusion(&self) -> bool {
530 self.alpha > 1.0
531 }
532}
533
534pub fn subdiffusion_pdf(x: f64, t: f64, params: &AnomalousDiffusionParams) -> f64 {
548 if t <= 0.0 {
549 return 0.0;
550 }
551 let sigma2 = params.msd(t);
552 let _sigma = sigma2.sqrt().max(1e-300);
553 (-x * x / (2.0 * sigma2)).exp() / ((2.0 * PI * sigma2).sqrt())
555}
556
557pub fn fde_predictor_corrector<F>(
575 f: F,
576 y0: f64,
577 t_end: f64,
578 n_steps: usize,
579 alpha: f64,
580) -> (Vec<f64>, Vec<f64>)
581where
582 F: Fn(f64, f64) -> f64,
583{
584 assert!(alpha > 0.0 && alpha <= 1.0);
585 assert!(n_steps >= 1);
586 let h = t_end / n_steps as f64;
587 let mut t = vec![0.0_f64; n_steps + 1];
588 let mut y = vec![0.0_f64; n_steps + 1];
589 y[0] = y0;
590 for k in 0..n_steps {
591 t[k + 1] = (k + 1) as f64 * h;
592 }
593 let gl = gl_coefficients(alpha, n_steps + 1);
595 let g2 = gamma(alpha + 2.0);
597 for n_idx in 0..n_steps {
598 let mut gl_sum = 0.0_f64;
601 for j in 0..=n_idx {
602 gl_sum += gl[n_idx - j + 1] * y[j];
603 }
604 let pred = y0 - gl_sum + h.powf(alpha) * f(t[n_idx], y[n_idx]) / gamma(alpha + 1.0);
606
607 let mut corr_sum = 0.0_f64;
609 for j in 0..=n_idx {
610 let b = (n_idx as f64 - j as f64 + 1.0).powf(alpha + 1.0)
611 - 2.0 * (n_idx as f64 - j as f64).powf(alpha + 1.0).max(0.0)
612 + (n_idx as f64 - j as f64 - 1.0).abs().powf(alpha + 1.0)
613 * if n_idx > j { 1.0 } else { 0.0 };
614 corr_sum += b * f(t[j], y[j]);
615 }
616 let b_pred = 1.0_f64; y[n_idx + 1] =
619 y0 - gl_sum + h.powf(alpha) / g2 * (b_pred * f(t[n_idx + 1], pred) + corr_sum);
620 }
621 (t, y)
622}
623
624#[derive(Debug, Clone, Copy)]
633pub struct FractionalOscillator {
634 pub mass: f64,
636 pub damping: f64,
638 pub stiffness: f64,
640 pub alpha: f64,
642}
643
644impl FractionalOscillator {
645 pub fn new(mass: f64, damping: f64, stiffness: f64, alpha: f64) -> Self {
647 assert!(alpha > 0.0 && alpha <= 1.0);
648 Self {
649 mass,
650 damping,
651 stiffness,
652 alpha,
653 }
654 }
655
656 pub fn natural_frequency(&self) -> f64 {
658 (self.stiffness / self.mass).sqrt()
659 }
660
661 pub fn damping_ratio(&self) -> f64 {
663 self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
664 }
665
666 pub fn relaxation_time(&self) -> f64 {
668 (self.mass / self.stiffness).powf(1.0 / (2.0 * self.alpha))
669 }
670
671 pub fn free_response(&self, x0: f64, t: f64) -> f64 {
675 if t <= 0.0 {
676 return x0;
677 }
678 let omega2 = self.stiffness / self.mass;
679 let two_alpha = 2.0 * self.alpha;
680 x0 * mittag_leffler_1(two_alpha, -omega2 * t.powf(two_alpha), 80)
681 }
682}
683
684pub fn bagley_torvik<F>(
699 mass: f64,
700 a_coeff: f64,
701 stiffness: f64,
702 force: F,
703 x0: f64,
704 v0: f64,
705 t_end: f64,
706 n_steps: usize,
707) -> (Vec<f64>, Vec<f64>)
708where
709 F: Fn(f64) -> f64,
710{
711 let h = t_end / n_steps as f64;
712 let mut t = vec![0.0; n_steps + 1];
713 let mut x = vec![0.0; n_steps + 1];
714 let mut v = vec![0.0; n_steps + 1];
715 x[0] = x0;
716 v[0] = v0;
717 for k in 0..n_steps {
718 t[k + 1] = (k + 1) as f64 * h;
719 }
720 let alpha_bt = 1.5_f64;
722 let gl = gl_coefficients(alpha_bt, n_steps + 1);
723 for n_idx in 0..n_steps {
724 let mut frac_sum = 0.0_f64;
726 for j in 0..=n_idx {
727 frac_sum += gl[j] * x[n_idx - j];
728 }
729 let frac_deriv = h.powf(-alpha_bt) * frac_sum;
730 let acc = (force(t[n_idx]) - a_coeff * frac_deriv - stiffness * x[n_idx]) / mass;
732 v[n_idx + 1] = v[n_idx] + h * acc;
733 x[n_idx + 1] = x[n_idx] + h * v[n_idx];
734 }
735 (t, x)
736}
737
738#[derive(Debug, Clone, Copy)]
747pub struct LevyStableParams {
748 pub alpha: f64,
750 pub beta: f64,
752 pub scale: f64,
754 pub location: f64,
756}
757
758impl LevyStableParams {
759 pub fn new(alpha: f64, beta: f64, scale: f64, location: f64) -> Self {
761 assert!(alpha > 0.0 && alpha <= 2.0);
762 assert!((-1.0..=1.0).contains(&beta));
763 assert!(scale > 0.0);
764 Self {
765 alpha,
766 beta,
767 scale,
768 location,
769 }
770 }
771
772 pub fn symmetric(alpha: f64, scale: f64) -> Self {
774 Self::new(alpha, 0.0, scale, 0.0)
775 }
776
777 pub fn cauchy(scale: f64, location: f64) -> Self {
779 Self::new(1.0, 0.0, scale, location)
780 }
781
782 pub fn gaussian(sigma: f64) -> Self {
784 Self::new(2.0, 0.0, sigma / 2.0_f64.sqrt(), 0.0)
786 }
787}
788
789pub fn levy_stable_sample(params: &LevyStableParams, seed: u64) -> f64 {
797 let mut rng = Lcg::new(seed);
798 let alpha = params.alpha;
799 let beta = params.beta;
800
801 if (alpha - 2.0).abs() < 1e-12 {
803 return params.location + params.scale * 2.0_f64.sqrt() * rng.normal();
804 }
805 if (alpha - 1.0).abs() < 1e-12 && beta.abs() < 1e-12 {
807 let u = (PI * (rng.uniform() - 0.5)).tan();
808 return params.location + params.scale * u;
809 }
810
811 let u = PI * (rng.uniform() - 0.5); let w = -rng.uniform().max(1e-300).ln(); let sample = if (alpha - 1.0).abs() > 1e-12 {
816 let xi = (PI / 2.0) * beta * (PI * alpha / 2.0).tan().atan() / (PI / 2.0);
817 let b = (PI / 2.0 * alpha).tan();
819 let s = (1.0 + beta * beta * b * b).powf(1.0 / (2.0 * alpha));
820 let phi0 = (beta * b).atan() / alpha;
821 s * ((alpha * (u + phi0)).sin() / u.cos().powf(1.0 / alpha))
822 * ((u - alpha * (u + phi0)).cos() / w).powf((1.0 - alpha) / alpha)
823 - xi
824 } else {
825 (PI / 2.0 + beta * u).tan() - beta * ((PI / 2.0 + beta * u) * w / (PI / 2.0 * u.cos())).ln()
827 };
828
829 params.location + params.scale * sample
830}
831
832pub fn levy_log_char_fn_symmetric(theta: f64, alpha: f64, scale: f64) -> f64 {
836 -(scale * theta.abs()).powf(alpha)
837}
838
839pub fn levy_stable_pdf(x: f64, alpha: f64, scale: f64, n_quad: usize, theta_max: f64) -> f64 {
850 let dtheta = 2.0 * theta_max / n_quad as f64;
853 let mut sum = 0.0_f64;
854 for k in 0..=n_quad {
855 let theta = -theta_max + k as f64 * dtheta;
856 let log_cf = levy_log_char_fn_symmetric(theta, alpha, scale);
857 let weight = if k == 0 || k == n_quad { 0.5 } else { 1.0 };
858 sum += weight * (log_cf.exp()) * (theta * x).cos();
859 }
860 sum * dtheta / (2.0 * PI)
861}
862
863pub fn fractional_relaxation(y0: f64, t: f64, alpha: f64, tau: f64) -> f64 {
879 if t < 0.0 {
880 return y0;
881 }
882 y0 * mittag_leffler_1(alpha, -(t / tau).powf(alpha), 150)
883}
884
885pub fn relaxation_time_threshold(
890 alpha: f64,
891 tau: f64,
892 threshold: f64,
893 t_max: f64,
894 n_pts: usize,
895) -> Option<f64> {
896 for k in 0..=n_pts {
897 let t = k as f64 * t_max / n_pts as f64;
898 let y = fractional_relaxation(1.0, t, alpha, tau);
899 if y.abs() < threshold {
900 return Some(t);
901 }
902 }
903 None
904}
905
906pub fn subdiffusion_trajectory(
924 n_steps: usize,
925 alpha: f64,
926 diffusivity: f64,
927 seed: u64,
928) -> (Vec<f64>, Vec<f64>) {
929 assert!(alpha > 0.0 && alpha < 1.0);
930 let mut rng = Lcg::new(seed);
931 let mut x = vec![0.0_f64; n_steps + 1];
932 let mut y = vec![0.0_f64; n_steps + 1];
933 for k in 0..n_steps {
934 let u = rng.uniform().max(1e-300);
937 let dt_op = (-u.ln()).powf(1.0 / alpha);
938 let sigma = (2.0 * diffusivity * dt_op).sqrt();
939 x[k + 1] = x[k] + sigma * rng.normal();
940 y[k + 1] = y[k] + sigma * rng.normal();
941 }
942 (x, y)
943}
944
945pub fn superdiffusion_trajectory(
956 n_steps: usize,
957 alpha: f64,
958 scale: f64,
959 seed: u64,
960) -> (Vec<f64>, Vec<f64>) {
961 assert!(alpha > 1.0 && alpha < 2.0);
962 let params = LevyStableParams::symmetric(alpha, scale);
963 let mut x = vec![0.0_f64; n_steps + 1];
964 let mut y = vec![0.0_f64; n_steps + 1];
965 for k in 0..n_steps {
966 let dx = levy_stable_sample(¶ms, seed.wrapping_add(k as u64 * 2));
967 let dy = levy_stable_sample(¶ms, seed.wrapping_add(k as u64 * 2 + 1));
968 x[k + 1] = x[k] + dx;
969 y[k + 1] = y[k] + dy;
970 }
971 (x, y)
972}
973
974#[cfg(test)]
979mod tests {
980 use super::*;
981
982 #[test]
983 fn test_gamma_integer_values() {
984 assert!((gamma(1.0) - 1.0).abs() < 1e-10);
986 assert!((gamma(2.0) - 1.0).abs() < 1e-10);
987 assert!((gamma(3.0) - 2.0).abs() < 1e-10);
988 assert!((gamma(4.0) - 6.0).abs() < 1e-10);
989 assert!((gamma(5.0) - 24.0).abs() < 1e-8);
990 }
991
992 #[test]
993 fn test_gamma_half() {
994 let g = gamma(0.5);
996 assert!((g - PI.sqrt()).abs() < 1e-10, "Γ(1/2) = {:.6}", g);
997 }
998
999 #[test]
1000 fn test_mittag_leffler_1_at_zero() {
1001 for alpha in [0.3, 0.5, 0.7, 1.0, 1.5] {
1003 let ml = mittag_leffler_1(alpha, 0.0, 50);
1004 assert!(
1005 (ml - 1.0).abs() < 1e-12,
1006 "E_{alpha}(0) = {:.6} for alpha={}",
1007 ml,
1008 alpha
1009 );
1010 }
1011 }
1012
1013 #[test]
1014 fn test_mittag_leffler_1_reduces_to_exp() {
1015 for z in [-1.0, 0.0, 0.5, 1.0] {
1017 let ml = mittag_leffler_1(1.0, z, 100);
1018 let exp = z.exp();
1019 assert!(
1020 (ml - exp).abs() < 1e-8,
1021 "E_1({}) = {:.6}, exp({}) = {:.6}",
1022 z,
1023 ml,
1024 z,
1025 exp
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn test_mittag_leffler_2_reduces_to_exp() {
1032 for z in [-0.5, 0.0, 0.5] {
1034 let ml = mittag_leffler_2(1.0, 1.0, z, 100);
1035 let exp = z.exp();
1036 assert!(
1037 (ml - exp).abs() < 1e-8,
1038 "E_{{1,1}}({}) = {:.6}, e^z={:.6}",
1039 z,
1040 ml,
1041 exp
1042 );
1043 }
1044 }
1045
1046 #[test]
1047 fn test_gl_coefficients_first_few() {
1048 let coeffs = gl_coefficients(1.0, 3);
1050 assert!((coeffs[0] - 1.0).abs() < 1e-12);
1051 assert!((coeffs[1] - (-1.0)).abs() < 1e-12);
1052 assert!(coeffs[2].abs() < 1e-12, "g_2 for alpha=1: {:.6}", coeffs[2]);
1053 }
1054
1055 #[test]
1056 fn test_rl_integral_constant_function() {
1057 let n = 100;
1059 let t_end = 1.0;
1060 let h = t_end / n as f64;
1061 let f_vals: Vec<f64> = vec![1.0; n + 1];
1062 let alpha = 0.5;
1063 let result = rl_integral(&f_vals, h, alpha);
1064 let exact = t_end.powf(alpha) / gamma(alpha + 1.0);
1065 assert!(
1066 (result - exact).abs() < 0.05,
1067 "RL integral: {:.6} vs {:.6}",
1068 result,
1069 exact
1070 );
1071 }
1072
1073 #[test]
1074 fn test_caputo_derivative_power_function() {
1075 let alpha = 0.5;
1077 let beta = 1.5;
1078 let n = 200;
1079 let t_end = 2.0;
1080 let h = t_end / n as f64;
1081 let f_vals: Vec<f64> = (0..=n).map(|k| (k as f64 * h).powf(beta)).collect();
1082 let result = caputo_derivative(&f_vals, h, alpha);
1083 let t_n = t_end;
1084 let exact = gamma(beta + 1.0) / gamma(beta - alpha + 1.0) * t_n.powf(beta - alpha);
1085 let rel_err = (result - exact).abs() / exact.abs();
1086 assert!(
1087 rel_err < 0.05,
1088 "Caputo D^0.5[t^1.5]: {:.6} vs {:.6}, err={:.4}",
1089 result,
1090 exact,
1091 rel_err
1092 );
1093 }
1094
1095 #[test]
1096 fn test_riesz_derivative_shape() {
1097 let n = 64;
1099 let h = 0.1;
1100 let center = n as f64 * h / 2.0;
1101 let f: Vec<f64> = (0..n)
1102 .map(|i| {
1103 let x = i as f64 * h - center;
1104 (-x * x / 0.5).exp()
1105 })
1106 .collect();
1107 let d = riesz_derivative(&f, h, 0.5);
1108 assert_eq!(d.len(), n);
1109 for &v in &d {
1111 assert!(v.is_finite(), "Riesz derivative contains non-finite value");
1112 }
1113 }
1114
1115 #[test]
1116 fn test_fractional_laplacian_1d() {
1117 let n = 32;
1118 let h = 0.2;
1119 let f: Vec<f64> = (0..n)
1120 .map(|i| {
1121 let x = (i as f64 - n as f64 / 2.0) * h;
1122 (-x * x).exp()
1123 })
1124 .collect();
1125 let lap = fractional_laplacian_1d(&f, h, 0.5);
1126 assert_eq!(lap.len(), n);
1127 for &v in &lap {
1128 assert!(v.is_finite());
1129 }
1130 }
1131
1132 #[test]
1133 fn test_memory_kernel_power_law() {
1134 let alpha = 0.5;
1135 let k = memory_kernel_power_law(1.0, alpha);
1137 let expected = 1.0 / gamma(alpha);
1138 assert!((k - expected).abs() < 1e-10);
1139 }
1140
1141 #[test]
1142 fn test_memory_kernel_exponential() {
1143 let lambda = 2.0;
1144 let t = 1.0;
1145 let k = memory_kernel_exponential(t, lambda);
1146 let expected = lambda * (-lambda * t).exp();
1147 assert!((k - expected).abs() < 1e-12);
1148 assert_eq!(memory_kernel_exponential(-1.0, lambda), 0.0);
1150 }
1151
1152 #[test]
1153 fn test_anomalous_diffusion_msd() {
1154 let params = AnomalousDiffusionParams::new(0.5, 1.0);
1155 assert_eq!(params.msd(0.0), 0.0);
1157 assert!(params.msd(1.0) > params.msd(0.5));
1159 let t = 2.0_f64;
1161 let expected = 2.0 * 1.0 * t.powf(0.5) / gamma(1.5);
1162 assert!((params.msd(t) - expected).abs() < 1e-10);
1163 }
1164
1165 #[test]
1166 fn test_anomalous_diffusion_classification() {
1167 let sub = AnomalousDiffusionParams::new(0.5, 1.0);
1168 let normal = AnomalousDiffusionParams::new(1.0, 1.0);
1169 let sup = AnomalousDiffusionParams::new(1.5, 1.0);
1170 assert!(sub.is_subdiffusion());
1171 assert!(!sub.is_superdiffusion());
1172 assert!(!normal.is_subdiffusion());
1173 assert!(!normal.is_superdiffusion());
1174 assert!(sup.is_superdiffusion());
1175 }
1176
1177 #[test]
1178 fn test_fractional_relaxation_initial_value() {
1179 let y = fractional_relaxation(3.0, 0.0, 0.5, 1.0);
1181 assert!((y - 3.0).abs() < 1e-10);
1182 }
1183
1184 #[test]
1185 fn test_fractional_relaxation_decay() {
1186 let y0 = 1.0;
1188 let alpha = 0.7;
1189 let tau = 1.0;
1190 let y_large = fractional_relaxation(y0, 10.0, alpha, tau);
1191 let y_small = fractional_relaxation(y0, 1.0, alpha, tau);
1192 assert!(
1193 y_large < y_small,
1194 "y(10)={:.6} should be less than y(1)={:.6}",
1195 y_large,
1196 y_small
1197 );
1198 }
1199
1200 #[test]
1201 fn test_fractional_oscillator_free_response() {
1202 let osc = FractionalOscillator::new(1.0, 0.0, 1.0, 0.8);
1203 let x0 = 2.0;
1205 let x = osc.free_response(x0, 0.0);
1206 assert!((x - x0).abs() < 1e-10);
1207 let x1 = osc.free_response(x0, 1.0);
1209 assert!(x1.abs() <= x0.abs() + 1e-6);
1210 }
1211
1212 #[test]
1213 fn test_fractional_oscillator_natural_freq() {
1214 let osc = FractionalOscillator::new(4.0, 0.0, 16.0, 1.0);
1215 assert!((osc.natural_frequency() - 2.0).abs() < 1e-10);
1217 }
1218
1219 #[test]
1220 fn test_levy_stable_gaussian_limit() {
1221 let params = LevyStableParams::gaussian(1.0);
1223 let samples: Vec<f64> = (0..1000)
1224 .map(|i| levy_stable_sample(¶ms, i as u64 * 7 + 13))
1225 .collect();
1226 let mean = samples.iter().sum::<f64>() / samples.len() as f64;
1227 let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
1228 assert!(mean.abs() < 0.2, "mean = {:.6}", mean);
1229 assert!((var - 1.0).abs() < 0.5, "var = {:.6}", var);
1230 }
1231
1232 #[test]
1233 fn test_levy_stable_pdf_normalizes() {
1234 let alpha = 1.5;
1236 let scale = 1.0;
1237 let n = 2000;
1238 let x_max = 30.0;
1239 let dx = 2.0 * x_max / n as f64;
1240 let mut integral = 0.0_f64;
1241 for k in 0..=n {
1242 let x = -x_max + k as f64 * dx;
1243 let p = levy_stable_pdf(x, alpha, scale, 500, 20.0);
1244 let w = if k == 0 || k == n { 0.5 } else { 1.0 };
1245 integral += w * p;
1246 }
1247 integral *= dx;
1248 assert!((integral - 1.0).abs() < 0.1, "integral = {:.6}", integral);
1249 }
1250
1251 #[test]
1252 fn test_subdiffusion_trajectory_length() {
1253 let (x, y) = subdiffusion_trajectory(50, 0.5, 1.0, 42);
1254 assert_eq!(x.len(), 51);
1255 assert_eq!(y.len(), 51);
1256 assert_eq!(x[0], 0.0);
1257 assert_eq!(y[0], 0.0);
1258 }
1259
1260 #[test]
1261 fn test_superdiffusion_trajectory_start() {
1262 let (x, y) = superdiffusion_trajectory(30, 1.5, 1.0, 99);
1263 assert_eq!(x.len(), 31);
1264 assert_eq!(y.len(), 31);
1265 assert_eq!(x[0], 0.0);
1266 assert_eq!(y[0], 0.0);
1267 }
1268
1269 #[test]
1270 fn test_gl_derivative_integer_order() {
1271 let h = 0.1;
1273 let n = 20;
1274 let f_vals: Vec<f64> = (0..=n).map(|k| k as f64 * h).collect();
1276 let d = rl_derivative(&f_vals, h, 1.0);
1277 assert!((d - 1.0).abs() < 0.1, "GL d/dt[t] ≈ {:.6}", d);
1279 }
1280
1281 #[test]
1282 fn test_fde_predictor_corrector_linear() {
1283 let alpha = 0.5;
1285 let t_end = 1.0;
1286 let n_steps = 100;
1287 let (_t, y) = fde_predictor_corrector(|_t, _y| 1.0, 0.0, t_end, n_steps, alpha);
1288 let expected = t_end.powf(alpha) / gamma(alpha + 1.0);
1289 assert!(y.last().unwrap().is_finite());
1290 assert!(
1292 *y.last().unwrap() > 0.0,
1293 "y(t_end) should be positive, got {:.6}",
1294 y.last().unwrap()
1295 );
1296 let _ = expected; }
1298
1299 #[test]
1300 fn test_bagley_torvik_no_force() {
1301 let (_, x) = bagley_torvik(1.0, 0.1, 1.0, |_| 0.0, 0.0, 0.0, 1.0, 50);
1303 for &xi in &x {
1304 assert!(xi.abs() < 1e-12, "x={:.6e}", xi);
1305 }
1306 }
1307
1308 #[test]
1309 fn test_fractional_laplacian_2d_shape() {
1310 let nx = 8;
1311 let ny = 8;
1312 let h = 0.1;
1313 let f: Vec<f64> = (0..nx * ny)
1314 .map(|i| {
1315 let ix = i % nx;
1316 let iy = i / nx;
1317 let x = (ix as f64 - nx as f64 / 2.0) * h;
1318 let y2 = (iy as f64 - ny as f64 / 2.0) * h;
1319 (-x * x - y2 * y2).exp()
1320 })
1321 .collect();
1322 let lap = fractional_laplacian_2d(&f, nx, ny, h, 0.5);
1323 assert_eq!(lap.len(), nx * ny);
1324 for &v in &lap {
1325 assert!(v.is_finite());
1326 }
1327 }
1328
1329 #[test]
1330 fn test_memory_kernel_mittag_leffler_at_zero() {
1331 assert_eq!(memory_kernel_mittag_leffler(0.0, 0.5, 1.0), 0.0);
1333 }
1334
1335 #[test]
1336 fn test_relaxation_time_threshold() {
1337 let t1 = relaxation_time_threshold(1.0, 1.0, 0.01, 20.0, 1000);
1339 let t2 = relaxation_time_threshold(0.3, 1.0, 0.01, 200.0, 10000);
1340 assert!(t1.is_some());
1341 assert!(t2.is_some());
1342 assert!(
1343 t1.unwrap() < t2.unwrap(),
1344 "α=1 reaches threshold at t={:.4}, α=0.3 at t={:.4}",
1345 t1.unwrap(),
1346 t2.unwrap()
1347 );
1348 }
1349
1350 #[test]
1351 fn test_log_gamma_positive() {
1352 let lg = log_gamma(5.0);
1354 assert!((lg - 24.0_f64.ln()).abs() < 1e-10);
1355 }
1356
1357 #[test]
1358 fn test_mittag_leffler_derivative() {
1359 let alpha = 0.7;
1361 let z = -0.5;
1362 let deriv = mittag_leffler_derivative(alpha, z, 100);
1363 let direct = mittag_leffler_2(alpha, alpha, z, 100);
1364 assert!((deriv - direct).abs() < 1e-12);
1365 }
1366
1367 #[test]
1368 fn test_rl_integral_series_length() {
1369 let f = vec![1.0; 10];
1370 let series = rl_integral_series(&f, 0.1, 0.5);
1371 assert_eq!(series.len(), 10);
1372 }
1373
1374 #[test]
1375 fn test_rl_derivative_series_length() {
1376 let f = vec![1.0; 10];
1377 let series = rl_derivative_series(&f, 0.1, 0.5);
1378 assert_eq!(series.len(), 10);
1379 }
1380
1381 #[test]
1382 fn test_caputo_series_length() {
1383 let f: Vec<f64> = (0..20).map(|k| k as f64 * 0.1).collect();
1384 let series = caputo_derivative_series(&f, 0.1, 0.5);
1385 assert_eq!(series.len(), 19);
1386 }
1387
1388 #[test]
1389 fn test_subdiffusion_pdf_normalizes_approx() {
1390 let params = AnomalousDiffusionParams::new(0.5, 0.1);
1392 let t = 1.0;
1393 let n = 1000;
1394 let x_max = 5.0;
1395 let dx = 2.0 * x_max / n as f64;
1396 let integral: f64 = (0..=n)
1397 .map(|k| {
1398 let x = -x_max + k as f64 * dx;
1399 let p = subdiffusion_pdf(x, t, ¶ms);
1400 let w = if k == 0 || k == n { 0.5 } else { 1.0 };
1401 w * p
1402 })
1403 .sum::<f64>()
1404 * dx;
1405 assert!((integral - 1.0).abs() < 0.05, "integral = {:.6}", integral);
1406 }
1407}