#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn uniform(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn normal(&mut self) -> f64 {
let u1 = self.uniform().max(1e-300);
let u2 = self.uniform();
(-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
}
}
pub fn gamma(x: f64) -> f64 {
if x <= 0.0 {
let sinpix = (PI * x).sin();
if sinpix.abs() < 1e-300 {
return f64::INFINITY;
}
return PI / (sinpix * gamma(1.0 - x));
}
if x < 0.5 {
return PI / ((PI * x).sin() * gamma(1.0 - x));
}
let g = 7.0_f64;
let c = [
0.999_999_999_999_809_9_f64,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
let z = x - 1.0;
let mut sum = c[0];
for (i, &ci) in c[1..].iter().enumerate() {
sum += ci / (z + (i as f64) + 1.0);
}
let t = z + g + 0.5;
(2.0 * PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * sum
}
pub fn log_gamma(x: f64) -> f64 {
if x <= 0.0 {
return f64::INFINITY;
}
gamma(x).abs().ln()
}
pub fn mittag_leffler_1(alpha: f64, z: f64, max_terms: usize) -> f64 {
assert!(alpha > 0.0, "alpha must be positive");
let mut sum = 0.0_f64;
let mut z_pow = 1.0_f64; for k in 0..max_terms {
let g = gamma(alpha * k as f64 + 1.0);
if g.is_infinite() || g.is_nan() {
break;
}
let term = z_pow / g;
sum += term;
if term.abs() < 1e-15 * sum.abs().max(1e-300) {
break;
}
z_pow *= z;
}
sum
}
pub fn mittag_leffler_2(alpha: f64, beta: f64, z: f64, max_terms: usize) -> f64 {
assert!(alpha > 0.0, "alpha must be positive");
assert!(beta > 0.0, "beta must be positive");
let mut sum = 0.0_f64;
let mut z_pow = 1.0_f64;
for k in 0..max_terms {
let g = gamma(alpha * k as f64 + beta);
if g.is_infinite() || g.is_nan() {
break;
}
let term = z_pow / g;
sum += term;
if term.abs() < 1e-15 * sum.abs().max(1e-300) {
break;
}
z_pow *= z;
}
sum
}
pub fn mittag_leffler_derivative(alpha: f64, z: f64, max_terms: usize) -> f64 {
mittag_leffler_2(alpha, alpha, z, max_terms)
}
pub fn rl_integral(f_values: &[f64], h: f64, alpha: f64) -> f64 {
assert!(alpha > 0.0, "alpha must be positive");
let n = f_values.len();
if n == 0 {
return 0.0;
}
let g_alpha1 = gamma(alpha + 1.0);
let mut sum = 0.0_f64;
let n_idx = n - 1;
for j in 0..n {
let k = n_idx - j; let b = (k as f64 + 1.0).powf(alpha) - (k as f64).powf(alpha);
sum += b * f_values[j];
}
h.powf(alpha) * sum / g_alpha1
}
pub fn rl_integral_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
(1..=f_values.len())
.map(|n| rl_integral(&f_values[..n], h, alpha))
.collect()
}
fn gl_coeff(alpha: f64, j: usize) -> f64 {
let mut g = 1.0_f64;
for k in 1..=j {
g *= (k as f64 - 1.0 - alpha) / k as f64;
}
g
}
pub fn rl_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
assert!(alpha > 0.0 && alpha < 2.0, "alpha must be in (0,2)");
let n = f_values.len();
if n == 0 {
return 0.0;
}
let mut sum = 0.0_f64;
for j in 0..n {
let g = gl_coeff(alpha, j);
sum += g * f_values[n - 1 - j];
}
h.powf(-alpha) * sum
}
pub fn rl_derivative_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
(1..=f_values.len())
.map(|n| rl_derivative(&f_values[..n], h, alpha))
.collect()
}
pub fn caputo_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
assert!(alpha > 0.0 && alpha < 1.0, "alpha must be in (0,1)");
let n = f_values.len();
if n < 2 {
return 0.0;
}
let mu = 1.0 - alpha;
let g = gamma(mu + 1.0); let mut sum = 0.0_f64;
let m = n - 1;
for j in 0..m {
let df = f_values[j + 1] - f_values[j];
let b = ((m - j) as f64).powf(mu) - ((m - j - 1) as f64).powf(mu);
sum += b * df;
}
sum / (h.powf(alpha) * g)
}
pub fn caputo_derivative_series(f_values: &[f64], h: f64, alpha: f64) -> Vec<f64> {
(2..=f_values.len())
.map(|n| caputo_derivative(&f_values[..n], h, alpha))
.collect()
}
pub fn gl_derivative(f_values: &[f64], h: f64, alpha: f64) -> f64 {
rl_derivative(f_values, h, alpha)
}
pub fn gl_coefficients(alpha: f64, n: usize) -> Vec<f64> {
let mut coeffs = Vec::with_capacity(n + 1);
let mut g = 1.0_f64;
coeffs.push(g);
for k in 1..=n {
g *= (k as f64 - 1.0 - alpha) / k as f64;
coeffs.push(g);
}
coeffs
}
pub fn riesz_derivative(f: &[f64], h: f64, alpha: f64) -> Vec<f64> {
assert!(
alpha > 0.0 && alpha < 2.0 && (alpha - 1.0).abs() > 1e-12,
"alpha must be in (0,2) and not equal to 1"
);
let n = f.len();
let c = -1.0 / (2.0 * (PI * alpha / 2.0).cos());
let coeffs = gl_coefficients(alpha, n + 1);
let mut result = vec![0.0_f64; n];
for j in 0..n {
let mut left = 0.0_f64;
let mut right = 0.0_f64;
for k in 0..=j + 1 {
let fk_left = if k <= j { f[j - k] } else { 0.0 };
let fk_right = if j + k < n { f[j + k] } else { 0.0 };
left += coeffs[k] * fk_left;
right += coeffs[k] * fk_right;
}
result[j] = c * (left + right) / h.powf(alpha);
}
result
}
pub fn fractional_laplacian_1d(f: &[f64], h: f64, s: f64) -> Vec<f64> {
assert!(s > 0.0 && s <= 1.0, "s must be in (0,1]");
let alpha = 2.0 * s;
if (alpha - 1.0).abs() < 1e-12 {
return vec![0.0; f.len()];
}
riesz_derivative(f, h, alpha).iter().map(|&v| -v).collect()
}
pub fn fractional_laplacian_2d(f: &[f64], nx: usize, ny: usize, h: f64, s: f64) -> Vec<f64> {
assert_eq!(f.len(), nx * ny);
let mut result = vec![0.0_f64; nx * ny];
for iy in 0..ny {
let row: Vec<f64> = (0..nx).map(|ix| f[iy * nx + ix]).collect();
let d = fractional_laplacian_1d(&row, h, s);
for ix in 0..nx {
result[iy * nx + ix] += d[ix];
}
}
for ix in 0..nx {
let col: Vec<f64> = (0..ny).map(|iy| f[iy * nx + ix]).collect();
let d = fractional_laplacian_1d(&col, h, s);
for iy in 0..ny {
result[iy * nx + ix] += d[iy];
}
}
result
}
pub fn memory_kernel_power_law(t: f64, alpha: f64) -> f64 {
assert!(alpha > 0.0);
if t <= 0.0 {
return 0.0;
}
t.powf(alpha - 1.0) / gamma(alpha)
}
pub fn memory_kernel_exponential(t: f64, lambda: f64) -> f64 {
if t < 0.0 {
return 0.0;
}
lambda * (-lambda * t).exp()
}
pub fn memory_kernel_mittag_leffler(t: f64, alpha: f64, lambda: f64) -> f64 {
assert!(alpha > 0.0 && alpha <= 1.0);
if t <= 0.0 {
return 0.0;
}
t.powf(alpha - 1.0) * mittag_leffler_2(alpha, alpha, -lambda * t.powf(alpha), 100)
}
pub fn memory_convolution(kernel_vals: &[f64], f_vals: &[f64], h: f64) -> f64 {
assert_eq!(kernel_vals.len(), f_vals.len());
let n = kernel_vals.len();
if n == 0 {
return 0.0;
}
let mut sum = 0.5 * (kernel_vals[0] * f_vals[n - 1] + kernel_vals[n - 1] * f_vals[0]);
for i in 1..n - 1 {
sum += kernel_vals[i] * f_vals[n - 1 - i];
}
h * sum
}
#[derive(Debug, Clone, Copy)]
pub struct AnomalousDiffusionParams {
pub alpha: f64,
pub diffusivity: f64,
}
impl AnomalousDiffusionParams {
pub fn new(alpha: f64, diffusivity: f64) -> Self {
assert!(alpha > 0.0 && alpha < 2.0, "alpha must be in (0,2)");
assert!(diffusivity > 0.0, "diffusivity must be positive");
Self { alpha, diffusivity }
}
pub fn msd(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
2.0 * self.diffusivity * t.powf(self.alpha) / gamma(1.0 + self.alpha)
}
pub fn anomalous_exponent(&self) -> f64 {
self.alpha
}
pub fn is_subdiffusion(&self) -> bool {
self.alpha < 1.0
}
pub fn is_superdiffusion(&self) -> bool {
self.alpha > 1.0
}
}
pub fn subdiffusion_pdf(x: f64, t: f64, params: &AnomalousDiffusionParams) -> f64 {
if t <= 0.0 {
return 0.0;
}
let sigma2 = params.msd(t);
let _sigma = sigma2.sqrt().max(1e-300);
(-x * x / (2.0 * sigma2)).exp() / ((2.0 * PI * sigma2).sqrt())
}
pub fn fde_predictor_corrector<F>(
f: F,
y0: f64,
t_end: f64,
n_steps: usize,
alpha: f64,
) -> (Vec<f64>, Vec<f64>)
where
F: Fn(f64, f64) -> f64,
{
assert!(alpha > 0.0 && alpha <= 1.0);
assert!(n_steps >= 1);
let h = t_end / n_steps as f64;
let mut t = vec![0.0_f64; n_steps + 1];
let mut y = vec![0.0_f64; n_steps + 1];
y[0] = y0;
for k in 0..n_steps {
t[k + 1] = (k + 1) as f64 * h;
}
let gl = gl_coefficients(alpha, n_steps + 1);
let g2 = gamma(alpha + 2.0);
for n_idx in 0..n_steps {
let mut gl_sum = 0.0_f64;
for j in 0..=n_idx {
gl_sum += gl[n_idx - j + 1] * y[j];
}
let pred = y0 - gl_sum + h.powf(alpha) * f(t[n_idx], y[n_idx]) / gamma(alpha + 1.0);
let mut corr_sum = 0.0_f64;
for j in 0..=n_idx {
let b = (n_idx as f64 - j as f64 + 1.0).powf(alpha + 1.0)
- 2.0 * (n_idx as f64 - j as f64).powf(alpha + 1.0).max(0.0)
+ (n_idx as f64 - j as f64 - 1.0).abs().powf(alpha + 1.0)
* if n_idx > j { 1.0 } else { 0.0 };
corr_sum += b * f(t[j], y[j]);
}
let b_pred = 1.0_f64; y[n_idx + 1] =
y0 - gl_sum + h.powf(alpha) / g2 * (b_pred * f(t[n_idx + 1], pred) + corr_sum);
}
(t, y)
}
#[derive(Debug, Clone, Copy)]
pub struct FractionalOscillator {
pub mass: f64,
pub damping: f64,
pub stiffness: f64,
pub alpha: f64,
}
impl FractionalOscillator {
pub fn new(mass: f64, damping: f64, stiffness: f64, alpha: f64) -> Self {
assert!(alpha > 0.0 && alpha <= 1.0);
Self {
mass,
damping,
stiffness,
alpha,
}
}
pub fn natural_frequency(&self) -> f64 {
(self.stiffness / self.mass).sqrt()
}
pub fn damping_ratio(&self) -> f64 {
self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
}
pub fn relaxation_time(&self) -> f64 {
(self.mass / self.stiffness).powf(1.0 / (2.0 * self.alpha))
}
pub fn free_response(&self, x0: f64, t: f64) -> f64 {
if t <= 0.0 {
return x0;
}
let omega2 = self.stiffness / self.mass;
let two_alpha = 2.0 * self.alpha;
x0 * mittag_leffler_1(two_alpha, -omega2 * t.powf(two_alpha), 80)
}
}
pub fn bagley_torvik<F>(
mass: f64,
a_coeff: f64,
stiffness: f64,
force: F,
x0: f64,
v0: f64,
t_end: f64,
n_steps: usize,
) -> (Vec<f64>, Vec<f64>)
where
F: Fn(f64) -> f64,
{
let h = t_end / n_steps as f64;
let mut t = vec![0.0; n_steps + 1];
let mut x = vec![0.0; n_steps + 1];
let mut v = vec![0.0; n_steps + 1];
x[0] = x0;
v[0] = v0;
for k in 0..n_steps {
t[k + 1] = (k + 1) as f64 * h;
}
let alpha_bt = 1.5_f64;
let gl = gl_coefficients(alpha_bt, n_steps + 1);
for n_idx in 0..n_steps {
let mut frac_sum = 0.0_f64;
for j in 0..=n_idx {
frac_sum += gl[j] * x[n_idx - j];
}
let frac_deriv = h.powf(-alpha_bt) * frac_sum;
let acc = (force(t[n_idx]) - a_coeff * frac_deriv - stiffness * x[n_idx]) / mass;
v[n_idx + 1] = v[n_idx] + h * acc;
x[n_idx + 1] = x[n_idx] + h * v[n_idx];
}
(t, x)
}
#[derive(Debug, Clone, Copy)]
pub struct LevyStableParams {
pub alpha: f64,
pub beta: f64,
pub scale: f64,
pub location: f64,
}
impl LevyStableParams {
pub fn new(alpha: f64, beta: f64, scale: f64, location: f64) -> Self {
assert!(alpha > 0.0 && alpha <= 2.0);
assert!((-1.0..=1.0).contains(&beta));
assert!(scale > 0.0);
Self {
alpha,
beta,
scale,
location,
}
}
pub fn symmetric(alpha: f64, scale: f64) -> Self {
Self::new(alpha, 0.0, scale, 0.0)
}
pub fn cauchy(scale: f64, location: f64) -> Self {
Self::new(1.0, 0.0, scale, location)
}
pub fn gaussian(sigma: f64) -> Self {
Self::new(2.0, 0.0, sigma / 2.0_f64.sqrt(), 0.0)
}
}
pub fn levy_stable_sample(params: &LevyStableParams, seed: u64) -> f64 {
let mut rng = Lcg::new(seed);
let alpha = params.alpha;
let beta = params.beta;
if (alpha - 2.0).abs() < 1e-12 {
return params.location + params.scale * 2.0_f64.sqrt() * rng.normal();
}
if (alpha - 1.0).abs() < 1e-12 && beta.abs() < 1e-12 {
let u = (PI * (rng.uniform() - 0.5)).tan();
return params.location + params.scale * u;
}
let u = PI * (rng.uniform() - 0.5); let w = -rng.uniform().max(1e-300).ln();
let sample = if (alpha - 1.0).abs() > 1e-12 {
let xi = (PI / 2.0) * beta * (PI * alpha / 2.0).tan().atan() / (PI / 2.0);
let b = (PI / 2.0 * alpha).tan();
let s = (1.0 + beta * beta * b * b).powf(1.0 / (2.0 * alpha));
let phi0 = (beta * b).atan() / alpha;
s * ((alpha * (u + phi0)).sin() / u.cos().powf(1.0 / alpha))
* ((u - alpha * (u + phi0)).cos() / w).powf((1.0 - alpha) / alpha)
- xi
} else {
(PI / 2.0 + beta * u).tan() - beta * ((PI / 2.0 + beta * u) * w / (PI / 2.0 * u.cos())).ln()
};
params.location + params.scale * sample
}
pub fn levy_log_char_fn_symmetric(theta: f64, alpha: f64, scale: f64) -> f64 {
-(scale * theta.abs()).powf(alpha)
}
pub fn levy_stable_pdf(x: f64, alpha: f64, scale: f64, n_quad: usize, theta_max: f64) -> f64 {
let dtheta = 2.0 * theta_max / n_quad as f64;
let mut sum = 0.0_f64;
for k in 0..=n_quad {
let theta = -theta_max + k as f64 * dtheta;
let log_cf = levy_log_char_fn_symmetric(theta, alpha, scale);
let weight = if k == 0 || k == n_quad { 0.5 } else { 1.0 };
sum += weight * (log_cf.exp()) * (theta * x).cos();
}
sum * dtheta / (2.0 * PI)
}
pub fn fractional_relaxation(y0: f64, t: f64, alpha: f64, tau: f64) -> f64 {
if t < 0.0 {
return y0;
}
y0 * mittag_leffler_1(alpha, -(t / tau).powf(alpha), 150)
}
pub fn relaxation_time_threshold(
alpha: f64,
tau: f64,
threshold: f64,
t_max: f64,
n_pts: usize,
) -> Option<f64> {
for k in 0..=n_pts {
let t = k as f64 * t_max / n_pts as f64;
let y = fractional_relaxation(1.0, t, alpha, tau);
if y.abs() < threshold {
return Some(t);
}
}
None
}
pub fn subdiffusion_trajectory(
n_steps: usize,
alpha: f64,
diffusivity: f64,
seed: u64,
) -> (Vec<f64>, Vec<f64>) {
assert!(alpha > 0.0 && alpha < 1.0);
let mut rng = Lcg::new(seed);
let mut x = vec![0.0_f64; n_steps + 1];
let mut y = vec![0.0_f64; n_steps + 1];
for k in 0..n_steps {
let u = rng.uniform().max(1e-300);
let dt_op = (-u.ln()).powf(1.0 / alpha);
let sigma = (2.0 * diffusivity * dt_op).sqrt();
x[k + 1] = x[k] + sigma * rng.normal();
y[k + 1] = y[k] + sigma * rng.normal();
}
(x, y)
}
pub fn superdiffusion_trajectory(
n_steps: usize,
alpha: f64,
scale: f64,
seed: u64,
) -> (Vec<f64>, Vec<f64>) {
assert!(alpha > 1.0 && alpha < 2.0);
let params = LevyStableParams::symmetric(alpha, scale);
let mut x = vec![0.0_f64; n_steps + 1];
let mut y = vec![0.0_f64; n_steps + 1];
for k in 0..n_steps {
let dx = levy_stable_sample(¶ms, seed.wrapping_add(k as u64 * 2));
let dy = levy_stable_sample(¶ms, seed.wrapping_add(k as u64 * 2 + 1));
x[k + 1] = x[k] + dx;
y[k + 1] = y[k] + dy;
}
(x, y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gamma_integer_values() {
assert!((gamma(1.0) - 1.0).abs() < 1e-10);
assert!((gamma(2.0) - 1.0).abs() < 1e-10);
assert!((gamma(3.0) - 2.0).abs() < 1e-10);
assert!((gamma(4.0) - 6.0).abs() < 1e-10);
assert!((gamma(5.0) - 24.0).abs() < 1e-8);
}
#[test]
fn test_gamma_half() {
let g = gamma(0.5);
assert!((g - PI.sqrt()).abs() < 1e-10, "Γ(1/2) = {:.6}", g);
}
#[test]
fn test_mittag_leffler_1_at_zero() {
for alpha in [0.3, 0.5, 0.7, 1.0, 1.5] {
let ml = mittag_leffler_1(alpha, 0.0, 50);
assert!(
(ml - 1.0).abs() < 1e-12,
"E_{alpha}(0) = {:.6} for alpha={}",
ml,
alpha
);
}
}
#[test]
fn test_mittag_leffler_1_reduces_to_exp() {
for z in [-1.0, 0.0, 0.5, 1.0] {
let ml = mittag_leffler_1(1.0, z, 100);
let exp = z.exp();
assert!(
(ml - exp).abs() < 1e-8,
"E_1({}) = {:.6}, exp({}) = {:.6}",
z,
ml,
z,
exp
);
}
}
#[test]
fn test_mittag_leffler_2_reduces_to_exp() {
for z in [-0.5, 0.0, 0.5] {
let ml = mittag_leffler_2(1.0, 1.0, z, 100);
let exp = z.exp();
assert!(
(ml - exp).abs() < 1e-8,
"E_{{1,1}}({}) = {:.6}, e^z={:.6}",
z,
ml,
exp
);
}
}
#[test]
fn test_gl_coefficients_first_few() {
let coeffs = gl_coefficients(1.0, 3);
assert!((coeffs[0] - 1.0).abs() < 1e-12);
assert!((coeffs[1] - (-1.0)).abs() < 1e-12);
assert!(coeffs[2].abs() < 1e-12, "g_2 for alpha=1: {:.6}", coeffs[2]);
}
#[test]
fn test_rl_integral_constant_function() {
let n = 100;
let t_end = 1.0;
let h = t_end / n as f64;
let f_vals: Vec<f64> = vec![1.0; n + 1];
let alpha = 0.5;
let result = rl_integral(&f_vals, h, alpha);
let exact = t_end.powf(alpha) / gamma(alpha + 1.0);
assert!(
(result - exact).abs() < 0.05,
"RL integral: {:.6} vs {:.6}",
result,
exact
);
}
#[test]
fn test_caputo_derivative_power_function() {
let alpha = 0.5;
let beta = 1.5;
let n = 200;
let t_end = 2.0;
let h = t_end / n as f64;
let f_vals: Vec<f64> = (0..=n).map(|k| (k as f64 * h).powf(beta)).collect();
let result = caputo_derivative(&f_vals, h, alpha);
let t_n = t_end;
let exact = gamma(beta + 1.0) / gamma(beta - alpha + 1.0) * t_n.powf(beta - alpha);
let rel_err = (result - exact).abs() / exact.abs();
assert!(
rel_err < 0.05,
"Caputo D^0.5[t^1.5]: {:.6} vs {:.6}, err={:.4}",
result,
exact,
rel_err
);
}
#[test]
fn test_riesz_derivative_shape() {
let n = 64;
let h = 0.1;
let center = n as f64 * h / 2.0;
let f: Vec<f64> = (0..n)
.map(|i| {
let x = i as f64 * h - center;
(-x * x / 0.5).exp()
})
.collect();
let d = riesz_derivative(&f, h, 0.5);
assert_eq!(d.len(), n);
for &v in &d {
assert!(v.is_finite(), "Riesz derivative contains non-finite value");
}
}
#[test]
fn test_fractional_laplacian_1d() {
let n = 32;
let h = 0.2;
let f: Vec<f64> = (0..n)
.map(|i| {
let x = (i as f64 - n as f64 / 2.0) * h;
(-x * x).exp()
})
.collect();
let lap = fractional_laplacian_1d(&f, h, 0.5);
assert_eq!(lap.len(), n);
for &v in &lap {
assert!(v.is_finite());
}
}
#[test]
fn test_memory_kernel_power_law() {
let alpha = 0.5;
let k = memory_kernel_power_law(1.0, alpha);
let expected = 1.0 / gamma(alpha);
assert!((k - expected).abs() < 1e-10);
}
#[test]
fn test_memory_kernel_exponential() {
let lambda = 2.0;
let t = 1.0;
let k = memory_kernel_exponential(t, lambda);
let expected = lambda * (-lambda * t).exp();
assert!((k - expected).abs() < 1e-12);
assert_eq!(memory_kernel_exponential(-1.0, lambda), 0.0);
}
#[test]
fn test_anomalous_diffusion_msd() {
let params = AnomalousDiffusionParams::new(0.5, 1.0);
assert_eq!(params.msd(0.0), 0.0);
assert!(params.msd(1.0) > params.msd(0.5));
let t = 2.0_f64;
let expected = 2.0 * 1.0 * t.powf(0.5) / gamma(1.5);
assert!((params.msd(t) - expected).abs() < 1e-10);
}
#[test]
fn test_anomalous_diffusion_classification() {
let sub = AnomalousDiffusionParams::new(0.5, 1.0);
let normal = AnomalousDiffusionParams::new(1.0, 1.0);
let sup = AnomalousDiffusionParams::new(1.5, 1.0);
assert!(sub.is_subdiffusion());
assert!(!sub.is_superdiffusion());
assert!(!normal.is_subdiffusion());
assert!(!normal.is_superdiffusion());
assert!(sup.is_superdiffusion());
}
#[test]
fn test_fractional_relaxation_initial_value() {
let y = fractional_relaxation(3.0, 0.0, 0.5, 1.0);
assert!((y - 3.0).abs() < 1e-10);
}
#[test]
fn test_fractional_relaxation_decay() {
let y0 = 1.0;
let alpha = 0.7;
let tau = 1.0;
let y_large = fractional_relaxation(y0, 10.0, alpha, tau);
let y_small = fractional_relaxation(y0, 1.0, alpha, tau);
assert!(
y_large < y_small,
"y(10)={:.6} should be less than y(1)={:.6}",
y_large,
y_small
);
}
#[test]
fn test_fractional_oscillator_free_response() {
let osc = FractionalOscillator::new(1.0, 0.0, 1.0, 0.8);
let x0 = 2.0;
let x = osc.free_response(x0, 0.0);
assert!((x - x0).abs() < 1e-10);
let x1 = osc.free_response(x0, 1.0);
assert!(x1.abs() <= x0.abs() + 1e-6);
}
#[test]
fn test_fractional_oscillator_natural_freq() {
let osc = FractionalOscillator::new(4.0, 0.0, 16.0, 1.0);
assert!((osc.natural_frequency() - 2.0).abs() < 1e-10);
}
#[test]
fn test_levy_stable_gaussian_limit() {
let params = LevyStableParams::gaussian(1.0);
let samples: Vec<f64> = (0..1000)
.map(|i| levy_stable_sample(¶ms, i as u64 * 7 + 13))
.collect();
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
assert!(mean.abs() < 0.2, "mean = {:.6}", mean);
assert!((var - 1.0).abs() < 0.5, "var = {:.6}", var);
}
#[test]
fn test_levy_stable_pdf_normalizes() {
let alpha = 1.5;
let scale = 1.0;
let n = 2000;
let x_max = 30.0;
let dx = 2.0 * x_max / n as f64;
let mut integral = 0.0_f64;
for k in 0..=n {
let x = -x_max + k as f64 * dx;
let p = levy_stable_pdf(x, alpha, scale, 500, 20.0);
let w = if k == 0 || k == n { 0.5 } else { 1.0 };
integral += w * p;
}
integral *= dx;
assert!((integral - 1.0).abs() < 0.1, "integral = {:.6}", integral);
}
#[test]
fn test_subdiffusion_trajectory_length() {
let (x, y) = subdiffusion_trajectory(50, 0.5, 1.0, 42);
assert_eq!(x.len(), 51);
assert_eq!(y.len(), 51);
assert_eq!(x[0], 0.0);
assert_eq!(y[0], 0.0);
}
#[test]
fn test_superdiffusion_trajectory_start() {
let (x, y) = superdiffusion_trajectory(30, 1.5, 1.0, 99);
assert_eq!(x.len(), 31);
assert_eq!(y.len(), 31);
assert_eq!(x[0], 0.0);
assert_eq!(y[0], 0.0);
}
#[test]
fn test_gl_derivative_integer_order() {
let h = 0.1;
let n = 20;
let f_vals: Vec<f64> = (0..=n).map(|k| k as f64 * h).collect();
let d = rl_derivative(&f_vals, h, 1.0);
assert!((d - 1.0).abs() < 0.1, "GL d/dt[t] ≈ {:.6}", d);
}
#[test]
fn test_fde_predictor_corrector_linear() {
let alpha = 0.5;
let t_end = 1.0;
let n_steps = 100;
let (_t, y) = fde_predictor_corrector(|_t, _y| 1.0, 0.0, t_end, n_steps, alpha);
let expected = t_end.powf(alpha) / gamma(alpha + 1.0);
assert!(y.last().unwrap().is_finite());
assert!(
*y.last().unwrap() > 0.0,
"y(t_end) should be positive, got {:.6}",
y.last().unwrap()
);
let _ = expected; }
#[test]
fn test_bagley_torvik_no_force() {
let (_, x) = bagley_torvik(1.0, 0.1, 1.0, |_| 0.0, 0.0, 0.0, 1.0, 50);
for &xi in &x {
assert!(xi.abs() < 1e-12, "x={:.6e}", xi);
}
}
#[test]
fn test_fractional_laplacian_2d_shape() {
let nx = 8;
let ny = 8;
let h = 0.1;
let f: Vec<f64> = (0..nx * ny)
.map(|i| {
let ix = i % nx;
let iy = i / nx;
let x = (ix as f64 - nx as f64 / 2.0) * h;
let y2 = (iy as f64 - ny as f64 / 2.0) * h;
(-x * x - y2 * y2).exp()
})
.collect();
let lap = fractional_laplacian_2d(&f, nx, ny, h, 0.5);
assert_eq!(lap.len(), nx * ny);
for &v in &lap {
assert!(v.is_finite());
}
}
#[test]
fn test_memory_kernel_mittag_leffler_at_zero() {
assert_eq!(memory_kernel_mittag_leffler(0.0, 0.5, 1.0), 0.0);
}
#[test]
fn test_relaxation_time_threshold() {
let t1 = relaxation_time_threshold(1.0, 1.0, 0.01, 20.0, 1000);
let t2 = relaxation_time_threshold(0.3, 1.0, 0.01, 200.0, 10000);
assert!(t1.is_some());
assert!(t2.is_some());
assert!(
t1.unwrap() < t2.unwrap(),
"α=1 reaches threshold at t={:.4}, α=0.3 at t={:.4}",
t1.unwrap(),
t2.unwrap()
);
}
#[test]
fn test_log_gamma_positive() {
let lg = log_gamma(5.0);
assert!((lg - 24.0_f64.ln()).abs() < 1e-10);
}
#[test]
fn test_mittag_leffler_derivative() {
let alpha = 0.7;
let z = -0.5;
let deriv = mittag_leffler_derivative(alpha, z, 100);
let direct = mittag_leffler_2(alpha, alpha, z, 100);
assert!((deriv - direct).abs() < 1e-12);
}
#[test]
fn test_rl_integral_series_length() {
let f = vec![1.0; 10];
let series = rl_integral_series(&f, 0.1, 0.5);
assert_eq!(series.len(), 10);
}
#[test]
fn test_rl_derivative_series_length() {
let f = vec![1.0; 10];
let series = rl_derivative_series(&f, 0.1, 0.5);
assert_eq!(series.len(), 10);
}
#[test]
fn test_caputo_series_length() {
let f: Vec<f64> = (0..20).map(|k| k as f64 * 0.1).collect();
let series = caputo_derivative_series(&f, 0.1, 0.5);
assert_eq!(series.len(), 19);
}
#[test]
fn test_subdiffusion_pdf_normalizes_approx() {
let params = AnomalousDiffusionParams::new(0.5, 0.1);
let t = 1.0;
let n = 1000;
let x_max = 5.0;
let dx = 2.0 * x_max / n as f64;
let integral: f64 = (0..=n)
.map(|k| {
let x = -x_max + k as f64 * dx;
let p = subdiffusion_pdf(x, t, ¶ms);
let w = if k == 0 || k == n { 0.5 } else { 1.0 };
w * p
})
.sum::<f64>()
* dx;
assert!((integral - 1.0).abs() < 0.05, "integral = {:.6}", integral);
}
}