Skip to main content

oxiphysics_core/
fractional_calculus.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Fractional calculus module for the OxiPhysics engine.
6//!
7//! Implements key fractional calculus operators and models used in anomalous
8//! diffusion, viscoelasticity, and non-local dynamics:
9//!
10//! - **Riemann-Liouville** fractional integral and derivative
11//! - **Caputo** fractional derivative (initial-value-friendly form)
12//! - **Grünwald-Letnikov** discrete approximation
13//! - **Mittag-Leffler** function E_{α,β}(z) (one- and two-parameter)
14//! - **Fractional differential equations** (FDE) via predictor-corrector
15//! - **Anomalous diffusion**: subdiffusion (α<1) and superdiffusion (α>1)
16//! - **Riesz fractional derivative** (symmetric, space-fractional)
17//! - **Fractional Laplacian** approximation on uniform grids
18//! - **Memory kernels**: power-law, exponential, Mittag-Leffler
19//! - **Fractional oscillator** (Bagley-Torvik, fractional spring-dashpot)
20//! - **Lévy stable distributions**: characteristic function and sampling
21
22#![allow(dead_code)]
23#![allow(clippy::too_many_arguments)]
24
25use std::f64::consts::PI;
26
27// ─── Internal LCG RNG ────────────────────────────────────────────────────────
28
29/// Minimal linear congruential generator for reproducible Monte Carlo.
30struct 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
57// ============================================================================
58// 1. Gamma Function
59// ============================================================================
60
61/// Lanczos approximation of the Gamma function Γ(x) for x > 0.
62///
63/// Accurate to ~15 significant digits for real positive arguments.
64pub fn gamma(x: f64) -> f64 {
65    if x <= 0.0 {
66        // Reflection formula: Γ(x)Γ(1-x) = π/sin(πx)
67        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    // Lanczos coefficients g=7
77    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
98/// Log-Gamma: ln Γ(x) for x > 0.
99///
100/// Numerically stable version using the Lanczos result.
101pub fn log_gamma(x: f64) -> f64 {
102    if x <= 0.0 {
103        return f64::INFINITY;
104    }
105    gamma(x).abs().ln()
106}
107
108// ============================================================================
109// 2. Mittag-Leffler Function
110// ============================================================================
111
112/// One-parameter Mittag-Leffler function E_α(z) = Σ z^k / Γ(αk+1).
113///
114/// Converges for |z| small; use at most `max_terms` terms.
115/// For z < 0 and 0 < α ≤ 1 this is the standard anomalous relaxation kernel.
116pub 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; // z^k
120    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
135/// Two-parameter Mittag-Leffler function E_{α,β}(z) = Σ z^k / Γ(αk+β).
136///
137/// Generalises the one-parameter version: E_{α,1}(z) = E_α(z).
138/// Used in the solution of fractional differential equations.
139pub 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
159/// Derivative of E_{α,1}(z) with respect to z, computed by differentiating
160/// the series term by term: d/dz E_α(z) = E_{α,α}(z).
161///
162/// Useful for FDE Jacobians and sensitivity analysis.
163pub fn mittag_leffler_derivative(alpha: f64, z: f64, max_terms: usize) -> f64 {
164    mittag_leffler_2(alpha, alpha, z, max_terms)
165}
166
167// ============================================================================
168// 3. Riemann-Liouville Fractional Integral
169// ============================================================================
170
171/// Riemann-Liouville fractional integral I^α\[f\] at point `t_n` using the
172/// product-integration rule on a uniform grid with step `h`.
173///
174/// I^α\[f\](t_n) = (1/Γ(α)) Σ_{j=0}^{n} w_j * f(t_j)
175///
176/// where the weights are the standard RL quadrature coefficients.
177///
178/// # Arguments
179/// * `f_values` – sampled function values f(t_0), …, f(t_n)
180/// * `h`        – uniform step size
181/// * `alpha`    – fractional order (0 < α < 1 for subdiffusion)
182pub 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    // Piecewise constant quadrature: I^α[f](t_n) ≈ (h^α/Γ(α+1)) Σ b_j f_j
189    // where b_j = (n-j)^α - (n-j-1)^α  (rectangular rule, Γ(α+1) denominator)
190    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; // k = n-1-j goes from n-1 down to 0
195        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
201/// Riemann-Liouville fractional integral I^α\[f\] evaluated at all grid points.
202///
203/// Returns a `Vec`f64` of length equal to `f_values`.
204pub 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
210// ============================================================================
211// 4. Riemann-Liouville Fractional Derivative
212// ============================================================================
213
214/// Grünwald-Letnikov coefficients g_j^(α) = (-1)^j C(α,j).
215///
216/// Used in both GL and RL discrete approximations.
217fn gl_coeff(alpha: f64, j: usize) -> f64 {
218    // Recurrence: g_0=1, g_j = g_{j-1} * (j - 1 - α) / j
219    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
226/// Riemann-Liouville fractional derivative D^α\[f\] at t_n via the
227/// Grünwald-Letnikov shifted approximation (first-order accurate).
228///
229/// D^α\[f\](t_n) ≈ h^{-α} Σ_{j=0}^{n} g_j * f(t_{n-j})
230///
231/// # Arguments
232/// * `f_values` – f(t_0), …, f(t_n)
233/// * `h`        – uniform time step
234/// * `alpha`    – fractional order 0 < α < 2
235pub 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
249/// Riemann-Liouville fractional derivative at all grid points.
250pub 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
256// ============================================================================
257// 5. Caputo Fractional Derivative
258// ============================================================================
259
260/// Caputo fractional derivative ^C D^α\[f\](t_n) for 0 < α < 1.
261///
262/// The Caputo definition is preferable for initial-value problems because it
263/// uses classical integer-order initial conditions.
264///
265/// ^C D^α\[f\](t_n) = (1/Γ(1-α)) Σ_{j=0}^{n-1} (f(t_{j+1})-f(t_j))/h * b_{n-1-j}
266///
267/// where b_k = ((k+1)^{1-α} - k^{1-α}) / (1-α) ... simplified via the L1 scheme.
268///
269/// # Arguments
270/// * `f_values` – f(t_0), …, f(t_n)
271/// * `h`        – uniform time step
272/// * `alpha`    – fractional order 0 < α < 1
273pub 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); // = Γ(2-α)
281    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
291/// Caputo fractional derivative computed at every grid point.
292pub 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
298// ============================================================================
299// 6. Grünwald-Letnikov Definition
300// ============================================================================
301
302/// Grünwald-Letnikov fractional derivative of order α at t_n.
303///
304/// Direct definition: D^α\[f\](t_n) = lim_{h→0} h^{-α} Σ_{j=0}^{n} g_j f_{n-j}
305///
306/// Functionally identical to `rl_derivative` on a uniform grid; provided as a
307/// named implementation reflecting the GL definition explicitly.
308pub fn gl_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
309    rl_derivative(f_values, h, alpha)
310}
311
312/// All GL coefficients g_0 … g_n for order α.
313///
314/// Efficient computation via the recurrence relation.
315pub 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
326// ============================================================================
327// 7. Riesz Fractional Derivative
328// ============================================================================
329
330/// Riesz fractional derivative ∂^α/∂|x|^α on a uniform 1-D grid.
331///
332/// The Riesz derivative is symmetric and non-local:
333/// (∂^α f / ∂|x|^α)_j ≈ -c_α/h^α Σ_k g_k (f_{j-k} + f_{j+k})
334///
335/// where c_α = -1/(2 cos(πα/2)) and g_k are GL coefficients.
336/// Requires 0 < α < 2, α ≠ 1.
337///
338/// Boundary values outside the domain are treated as zero (zero-padding).
339///
340/// # Arguments
341/// * `f`     – spatial function values on uniform grid
342/// * `h`     – spatial step
343/// * `alpha` – fractional order 0 < α < 2, α ≠ 1
344pub 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
367// ============================================================================
368// 8. Fractional Laplacian
369// ============================================================================
370
371/// Fractional Laplacian (-Δ)^{s} on a 1-D uniform grid via spectral method.
372///
373/// Uses discrete cosine / DFT approach: multiply eigenvalues by |k|^{2s}.
374/// Here we use a simple finite-difference approximation based on Riesz derivative
375/// in each spatial direction for 1-D.
376///
377/// For s = 1 this reduces to the classical second derivative.
378///
379/// # Arguments
380/// * `f`     – values on uniform grid of length n
381/// * `h`     – grid spacing
382/// * `s`     – fractional power, 0 < s < 1
383pub 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    // Use Riesz with alpha = 2s
386    let alpha = 2.0 * s;
387    if (alpha - 1.0).abs() < 1e-12 {
388        // Degenerate case: return zeros
389        return vec![0.0; f.len()];
390    }
391    // (-Δ)^s corresponds to -Riesz(2s) up to sign convention
392    riesz_derivative(f, h, alpha).iter().map(|&v| -v).collect()
393}
394
395/// Fractional Laplacian (-Δ)^{s} on a 2-D uniform grid (n_x × n_y).
396///
397/// Row-major layout. Applies 1-D fractional Laplacian along each axis and sums.
398///
399/// # Arguments
400/// * `f`    – row-major 2-D data, length n_x * n_y
401/// * `nx`   – number of x grid points
402/// * `ny`   – number of y grid points
403/// * `h`    – uniform grid spacing (same in x and y)
404/// * `s`    – fractional power 0 < s ≤ 1
405pub 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    // x-direction
409    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    // y-direction
417    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
427// ============================================================================
428// 9. Memory Kernels
429// ============================================================================
430
431/// Power-law memory kernel K(t) = t^{α-1} / Γ(α).
432///
433/// This is the kernel of the Riemann-Liouville fractional integral.
434/// For 0 < α < 1 it is weakly singular at t = 0.
435pub 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
443/// Exponential memory kernel K(t) = λ e^{-λt}.
444///
445/// Corresponds to standard Markovian (α → 1) limit of fractional dynamics.
446pub 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
453/// Mittag-Leffler memory kernel K(t) = t^{α-1} E_{α,α}(-λ t^α).
454///
455/// Appears in the solution of fractional relaxation equations.
456/// Interpolates between exponential (α=1) and power-law decay.
457pub 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
465/// Compute the convolution integral of a memory kernel K with function f on
466/// a uniform grid [0, (n-1)*h] using the trapezoidal rule.
467///
468/// Returns the convolution value at the last grid point t_n.
469///
470/// (K * f)(t_n) = ∫_0^{t_n} K(t_n - s) f(s) ds
471pub 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    // Trapezoidal rule
478    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// ============================================================================
486// 10. Anomalous Diffusion
487// ============================================================================
488
489/// Parameters for an anomalous diffusion model.
490///
491/// Subdiffusion: 0 < α < 1, mean-squared displacement ⟨x²⟩ ~ t^α.
492/// Normal diffusion: α = 1, ⟨x²⟩ ~ t.
493/// Superdiffusion: 1 < α < 2, ⟨x²⟩ ~ t^α.
494#[derive(Debug, Clone, Copy)]
495pub struct AnomalousDiffusionParams {
496    /// Fractional order α ∈ (0, 2).
497    pub alpha: f64,
498    /// Generalized diffusion coefficient K_α [m² s^{-α}].
499    pub diffusivity: f64,
500}
501
502impl AnomalousDiffusionParams {
503    /// Create new anomalous diffusion parameters.
504    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    /// Mean squared displacement at time t: ⟨x²⟩(t) = 2 K_α t^α / Γ(1+α).
511    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    /// Diffusion exponent (anomalous scaling exponent).
519    pub fn anomalous_exponent(&self) -> f64 {
520        self.alpha
521    }
522
523    /// Check if the process is subdiffusive.
524    pub fn is_subdiffusion(&self) -> bool {
525        self.alpha < 1.0
526    }
527
528    /// Check if the process is superdiffusive.
529    pub fn is_superdiffusion(&self) -> bool {
530        self.alpha > 1.0
531    }
532}
533
534/// Subdiffusion probability density (Green's function) via Mittag-Leffler.
535///
536/// For the fractional diffusion equation with Caputo time derivative:
537/// ∂^α C / ∂t^α = K_α ∂² C / ∂x²
538///
539/// The fundamental solution in an infinite domain is expressed through
540/// the Fox H-function; this function returns the Gaussian approximation
541/// valid for large |x| or small α deviations from 1.
542///
543/// # Arguments
544/// * `x`      – spatial coordinate
545/// * `t`      – time
546/// * `params` – anomalous diffusion parameters
547pub 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    // Gaussian approximation (exact for α=1, approximate otherwise)
554    (-x * x / (2.0 * sigma2)).exp() / ((2.0 * PI * sigma2).sqrt())
555}
556
557// ============================================================================
558// 11. Fractional Differential Equations (Predictor-Corrector)
559// ============================================================================
560
561/// Solve a scalar fractional initial-value problem using the Adams-Bashforth-
562/// Moulton predictor-corrector method (Diethelm et al. 2002).
563///
564/// Equation: ^C D^α y(t) = f(t, y(t)),  y(0) = y0,  0 < α ≤ 1.
565///
566/// Returns `(t_values, y_values)` with `n_steps + 1` points.
567///
568/// # Arguments
569/// * `f`       – right-hand side f(t, y)
570/// * `y0`      – initial condition
571/// * `t_end`   – final time
572/// * `n_steps` – number of time steps
573/// * `alpha`   – fractional order 0 < α ≤ 1
574pub 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    // Precompute GL coefficients up to n_steps
594    let gl = gl_coefficients(alpha, n_steps + 1);
595    // Adams weights for corrector: b_{n+1,j} = h^α/Γ(α+2) * [(n-j+1)^{α+1} - (n-j-1+α)(n-j)^α]
596    let g2 = gamma(alpha + 2.0);
597    for n_idx in 0..n_steps {
598        // --- Predictor (Adams-Bashforth) ---
599        // RL-style sum using GL coefficients
600        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        // Predictor: y^P_{n+1} = y0 - sum + h^α/Γ(α+1) * f(t_n, y_n)
605        let pred = y0 - gl_sum + h.powf(alpha) * f(t[n_idx], y[n_idx]) / gamma(alpha + 1.0);
606
607        // --- Corrector (Adams-Moulton) ---
608        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        // Corrector contribution from predicted value
617        let b_pred = 1.0_f64; // (1)^{α+1}
618        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// ============================================================================
625// 12. Fractional Oscillator
626// ============================================================================
627
628/// Fractional oscillator model: m D^{2α} x + b D^α x + k x = F(t).
629///
630/// For α = 1 this reduces to the classical damped harmonic oscillator.
631/// For 0 < α < 1 the oscillator exhibits anomalous (sub-diffusive) behavior.
632#[derive(Debug, Clone, Copy)]
633pub struct FractionalOscillator {
634    /// Mass m.
635    pub mass: f64,
636    /// Fractional damping coefficient b.
637    pub damping: f64,
638    /// Spring stiffness k.
639    pub stiffness: f64,
640    /// Fractional order α ∈ (0, 1].
641    pub alpha: f64,
642}
643
644impl FractionalOscillator {
645    /// Create a new fractional oscillator.
646    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    /// Natural frequency ω_n for the integer-order limit (α=1).
657    pub fn natural_frequency(&self) -> f64 {
658        (self.stiffness / self.mass).sqrt()
659    }
660
661    /// Damping ratio ζ for the integer-order limit.
662    pub fn damping_ratio(&self) -> f64 {
663        self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
664    }
665
666    /// Relaxation time τ = (m/k)^{1/(2α)}.
667    pub fn relaxation_time(&self) -> f64 {
668        (self.mass / self.stiffness).powf(1.0 / (2.0 * self.alpha))
669    }
670
671    /// Free response via Mittag-Leffler: x(t) = x0 E_{2α,1}(-ω² t^{2α}).
672    ///
673    /// Valid for underdamped (ζ < 1) or undamped (b=0) oscillators.
674    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
684/// Bagley-Torvik equation solver: m x'' + A D^{3/2} x + k x = F(t).
685///
686/// Returns time and displacement arrays for the Bagley-Torvik fractional model.
687/// Uses a simplified Euler-fractional step as a first-order approximation.
688///
689/// # Arguments
690/// * `mass`      – mass m
691/// * `a_coeff`   – fractional damping coefficient A
692/// * `stiffness` – spring stiffness k
693/// * `force`     – external force function F(t)
694/// * `x0`        – initial displacement
695/// * `v0`        – initial velocity
696/// * `t_end`     – final time
697/// * `n_steps`   – number of steps
698pub 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    // Fractional GL coefficients for order 3/2
721    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        // Fractional term: D^{3/2} x ≈ h^{-3/2} Σ g_j x_{n-j}
725        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        // Acceleration from equation of motion
731        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// ============================================================================
739// 13. Lévy Stable Distributions
740// ============================================================================
741
742/// Parameters of a Lévy stable distribution S(α, β, c, μ).
743///
744/// The characteristic function is:
745/// φ(θ) = exp(i μ θ - |cθ|^α (1 - iβ sign(θ) tan(πα/2))) for α ≠ 1
746#[derive(Debug, Clone, Copy)]
747pub struct LevyStableParams {
748    /// Stability index α ∈ (0, 2].
749    pub alpha: f64,
750    /// Skewness β ∈ [-1, 1].
751    pub beta: f64,
752    /// Scale c > 0.
753    pub scale: f64,
754    /// Location μ.
755    pub location: f64,
756}
757
758impl LevyStableParams {
759    /// Create Lévy stable parameters.
760    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    /// Symmetric Lévy stable (β=0, μ=0).
773    pub fn symmetric(alpha: f64, scale: f64) -> Self {
774        Self::new(alpha, 0.0, scale, 0.0)
775    }
776
777    /// Standard Cauchy distribution (α=1, β=0).
778    pub fn cauchy(scale: f64, location: f64) -> Self {
779        Self::new(1.0, 0.0, scale, location)
780    }
781
782    /// Standard Gaussian distribution (α=2, β=0).
783    pub fn gaussian(sigma: f64) -> Self {
784        // For S(2,0,c,0): variance = 2c², so c = sigma/sqrt(2)
785        Self::new(2.0, 0.0, sigma / 2.0_f64.sqrt(), 0.0)
786    }
787}
788
789/// Sample from a Lévy stable distribution using the Chambers-Mallows-Stuck algorithm.
790///
791/// Returns a single sample from S(α, β, c, μ).
792///
793/// # Arguments
794/// * `params` – Lévy stable parameters
795/// * `seed`   – random seed for reproducibility
796pub 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    // Special case: Gaussian α=2
802    if (alpha - 2.0).abs() < 1e-12 {
803        return params.location + params.scale * 2.0_f64.sqrt() * rng.normal();
804    }
805    // Special case: Cauchy α=1, β=0
806    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    // Chambers-Mallows-Stuck (1976) algorithm
812    let u = PI * (rng.uniform() - 0.5); // uniform on (-π/2, π/2)
813    let w = -rng.uniform().max(1e-300).ln(); // exponential(1)
814
815    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        // Simplified CMS without the exact ξ shift for non-unit α
818        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        // α = 1
826        (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
832/// Log characteristic function of a symmetric Lévy stable distribution.
833///
834/// ln φ(θ) = -|cθ|^α for the symmetric case (β=0, μ=0).
835pub fn levy_log_char_fn_symmetric(theta: f64, alpha: f64, scale: f64) -> f64 {
836    -(scale * theta.abs()).powf(alpha)
837}
838
839/// Compute the PDF of a symmetric Lévy stable distribution by numerical Fourier inversion.
840///
841/// Uses trapezoidal integration over frequency domain [-θ_max, θ_max].
842///
843/// # Arguments
844/// * `x`       – evaluation point
845/// * `alpha`   – stability index
846/// * `scale`   – scale parameter
847/// * `n_quad`  – number of quadrature points (must be even)
848/// * `theta_max` – truncation of frequency domain
849pub fn levy_stable_pdf(x: f64, alpha: f64, scale: f64, n_quad: usize, theta_max: f64) -> f64 {
850    // Inverse Fourier transform: p(x) = (1/2π) ∫ φ(θ) e^{-iθx} dθ
851    // For symmetric φ: integrate over [-θ_max, θ_max] and divide by 2π.
852    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
863// ============================================================================
864// 14. Fractional Relaxation
865// ============================================================================
866
867/// Fractional relaxation equation: D^α y + y/τ = 0, y(0) = y0.
868///
869/// Exact solution: y(t) = y0 E_α(-(t/τ)^α).
870///
871/// Models anomalous (non-exponential) relaxation in complex systems.
872///
873/// # Arguments
874/// * `y0`    – initial value
875/// * `t`     – time
876/// * `alpha` – fractional order 0 < α ≤ 1
877/// * `tau`   – relaxation time constant
878pub 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
885/// Time array for fractional relaxation to fall below threshold.
886///
887/// Scans a time grid [0, t_max] and returns the first time where
888/// |y(t)/y0| < threshold.  Returns None if threshold is never reached.
889pub 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
906// ============================================================================
907// 15. Subdiffusion / Superdiffusion Simulation
908// ============================================================================
909
910/// Single-particle subdiffusion trajectory via subordination.
911///
912/// Generates a discrete-time trajectory of a subdiffusing particle
913/// using the random-walk subordination method: operational time is drawn
914/// from a stable subordinator with index α.
915///
916/// # Arguments
917/// * `n_steps`   – number of steps
918/// * `alpha`     – subdiffusion exponent 0 < α < 1
919/// * `diffusivity` – generalized diffusion coefficient
920/// * `seed`      – random seed
921///
922/// Returns `(x, y)` position arrays.
923pub 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        // Operational time increment from a one-sided stable distribution
935        // Approximated by a Weibull-like draw: dt_op ~ Gamma(alpha+1)^{1/alpha}
936        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
945/// Superdiffusion trajectory via Lévy flights.
946///
947/// Steps are drawn from a symmetric Lévy stable distribution with index α ∈ (1, 2).
948/// The long-tailed step lengths produce superdiffusive dynamics.
949///
950/// # Arguments
951/// * `n_steps` – number of Lévy flight steps
952/// * `alpha`   – stability index 1 < α < 2
953/// * `scale`   – scale parameter c
954/// * `seed`    – random seed
955pub 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(&params, seed.wrapping_add(k as u64 * 2));
967        let dy = levy_stable_sample(&params, 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// ============================================================================
975// Tests
976// ============================================================================
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981
982    #[test]
983    fn test_gamma_integer_values() {
984        // Γ(n) = (n-1)!
985        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        // Γ(1/2) = sqrt(π)
995        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        // E_α(0) = 1 for any α
1002        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        // E_1(z) = e^z
1016        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        // E_{1,1}(z) = e^z
1033        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        // For α=1: g_0=1, g_1=-1, g_2=0, ...
1049        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        // I^α[1](t) = t^α / Γ(α+1)
1058        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        // ^C D^α [t^β] = Γ(β+1)/Γ(β-α+1) * t^{β-α}
1076        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        // Riesz derivative of a Gaussian bump should be smooth with zero mean
1098        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        // Check the result is finite
1110        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        // K(1.0) = 1 / Γ(0.5) = 1 / sqrt(π)
1136        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        // At t<0 should be zero
1149        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        // MSD at t=0 should be 0
1156        assert_eq!(params.msd(0.0), 0.0);
1157        // MSD should grow with time
1158        assert!(params.msd(1.0) > params.msd(0.5));
1159        // Check formula: 2 K t^α / Γ(1+α)
1160        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        // At t=0, y = y0
1180        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        // y(t) should decay towards 0 for large t
1187        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        // At t=0, x = x0
1204        let x0 = 2.0;
1205        let x = osc.free_response(x0, 0.0);
1206        assert!((x - x0).abs() < 1e-10);
1207        // Response should be bounded and start decaying
1208        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        // ω_n = sqrt(k/m) = sqrt(16/4) = 2
1216        assert!((osc.natural_frequency() - 2.0).abs() < 1e-10);
1217    }
1218
1219    #[test]
1220    fn test_levy_stable_gaussian_limit() {
1221        // α=2 should produce Gaussian samples
1222        let params = LevyStableParams::gaussian(1.0);
1223        let samples: Vec<f64> = (0..1000)
1224            .map(|i| levy_stable_sample(&params, 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        // Integral of symmetric Lévy PDF ≈ 1 over a wide range
1235        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        // For α=1, GL derivative ~ first difference / h
1272        let h = 0.1;
1273        let n = 20;
1274        // f(t) = t  => f' = 1
1275        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        // Should be approximately 1.0
1278        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        // ^C D^0.5 y = 1, y(0) = 0  => y(t) = t^0.5 / Γ(1.5)
1284        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        // Loose check: solution grows from 0
1291        assert!(
1292            *y.last().unwrap() > 0.0,
1293            "y(t_end) should be positive, got {:.6}",
1294            y.last().unwrap()
1295        );
1296        let _ = expected; // acknowledge
1297    }
1298
1299    #[test]
1300    fn test_bagley_torvik_no_force() {
1301        // With zero force and zero initial conditions, solution stays at zero
1302        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        // K(0, alpha, lambda) = 0 by definition (t <= 0)
1332        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        // Exponential relaxation α=1 reaches threshold faster than α=0.3
1338        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        // ln Γ(5) = ln 24
1353        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        // d/dz E_α(z) = E_{α,α}(z)
1360        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        // PDF should integrate to ≈ 1 over a wide range
1391        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, &params);
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}