use crate::caputo::{caputo_weights, l1_coefficient};
use crate::system::{FdeOptions, FdeResult, FdeSolver, FdeStats, FdeSystem};
use numra_core::Scalar;
pub struct L1Solver;
impl<S: Scalar> FdeSolver<S> for L1Solver {
fn solve<Sys: FdeSystem<S>>(
system: &Sys,
t0: S,
tf: S,
y0: &[S],
options: &FdeOptions<S>,
) -> Result<FdeResult<S>, String> {
let dim = system.dim();
let alpha = system.alpha();
if !system.is_valid_order() {
return Err(format!(
"Invalid fractional order α = {}. Must be in (0, 1].",
alpha.to_f64()
));
}
if y0.len() != dim {
return Err(format!(
"Initial state dimension {} doesn't match system dimension {}",
y0.len(),
dim
));
}
let dt = options.dt;
let n_steps = ((tf - t0) / dt).to_f64().ceil() as usize;
if n_steps > options.max_steps {
return Err(format!(
"Required steps {} exceeds maximum {}",
n_steps, options.max_steps
));
}
let c = l1_coefficient(dt, alpha);
let mut y_history: Vec<Vec<S>> = Vec::with_capacity(n_steps + 1);
y_history.push(y0.to_vec());
let mut t_out = vec![t0];
let mut y_out = y0.to_vec();
let mut stats = FdeStats::default();
let mut f_buf = vec![S::ZERO; dim];
for n in 1..=n_steps {
let t = t0 + S::from_usize(n) * dt;
let b = caputo_weights(n, alpha);
let mut history_sum = vec![S::ZERO; dim];
for j in 1..n {
let y_nmj = &y_history[n - j];
let y_nmjm1 = &y_history[n - j - 1];
for i in 0..dim {
history_sum[i] += b[j] * (y_nmj[i] - y_nmjm1[i]);
}
}
let y_prev = &y_history[n - 1];
let mut y_new = y_prev.clone();
let c_b0 = c * b[0];
let inv_c_b0 = S::ONE / c_b0;
for _iter in 0..options.max_iter {
system.rhs(t, &y_new, &mut f_buf);
stats.n_rhs += 1;
let mut y_next = vec![S::ZERO; dim];
let mut converged = true;
for i in 0..dim {
let rhs = f_buf[i] - c * history_sum[i];
y_next[i] = y_prev[i] + rhs * inv_c_b0;
let diff = (y_next[i] - y_new[i]).abs();
let scale = options.tol * (S::ONE + y_new[i].abs());
if diff > scale {
converged = false;
}
}
y_new = y_next;
if converged {
break;
}
}
y_history.push(y_new.clone());
t_out.push(t);
y_out.extend_from_slice(&y_new);
stats.n_steps += 1;
}
Ok(FdeResult::new(t_out, y_out, dim, stats))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::caputo::mittag_leffler_1;
struct FractionalRelaxation {
lambda: f64,
alpha: f64,
}
impl FdeSystem<f64> for FractionalRelaxation {
fn dim(&self) -> usize {
1
}
fn alpha(&self) -> f64 {
self.alpha
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -self.lambda * y[0];
}
}
#[test]
fn test_l1_fractional_relaxation_alpha_1() {
let system = FractionalRelaxation {
lambda: 1.0,
alpha: 1.0,
};
let options = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
assert!(result.success);
let y_final = result.y_final().unwrap()[0];
let y_exact = (-1.0_f64).exp();
assert!((y_final - y_exact).abs() < 0.01);
}
#[test]
fn test_l1_fractional_relaxation_alpha_half() {
let system = FractionalRelaxation {
lambda: 1.0,
alpha: 0.5,
};
let options = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
assert!(result.success);
for (i, &t) in result.t.iter().enumerate().step_by(10) {
if t > 0.0 {
let y_computed = result.y_at(i)[0];
let y_exact = mittag_leffler_1(-t.powf(0.5), 0.5);
let error = (y_computed - y_exact).abs();
assert!(
error < 0.05,
"At t={}: computed={}, exact={}, error={}",
t,
y_computed,
y_exact,
error
);
}
}
}
#[test]
fn test_l1_2d_system() {
struct TwoD;
impl FdeSystem<f64> for TwoD {
fn dim(&self) -> usize {
2
}
fn alpha(&self) -> f64 {
0.8
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -y[0];
f[1] = -2.0 * y[1];
}
}
let options = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&TwoD, 0.0, 1.0, &[1.0, 1.0], &options).expect("Solve failed");
assert!(result.success);
let y_final = result.y_final().unwrap();
assert!(y_final[0] > y_final[1]);
}
#[test]
fn test_invalid_alpha() {
struct InvalidAlpha;
impl FdeSystem<f64> for InvalidAlpha {
fn dim(&self) -> usize {
1
}
fn alpha(&self) -> f64 {
1.5
} fn rhs(&self, _t: f64, _y: &[f64], f: &mut [f64]) {
f[0] = 0.0;
}
}
let options = FdeOptions::default();
let result = L1Solver::solve(&InvalidAlpha, 0.0, 1.0, &[1.0], &options);
assert!(result.is_err());
}
#[test]
fn test_l1_convergence_order() {
let system = FractionalRelaxation {
lambda: 1.0,
alpha: 0.5,
};
let tf = 1.0;
let opts_coarse = FdeOptions::default().dt(0.02);
let res_coarse =
L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_coarse).expect("Coarse solve failed");
let y_coarse = res_coarse.y_final().unwrap()[0];
let opts_fine = FdeOptions::default().dt(0.01);
let res_fine =
L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_fine).expect("Fine solve failed");
let y_fine = res_fine.y_final().unwrap()[0];
let opts_ref = FdeOptions::default().dt(0.001);
let res_ref =
L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_ref).expect("Ref solve failed");
let y_ref = res_ref.y_final().unwrap()[0];
let err_coarse = (y_coarse - y_ref).abs();
let err_fine = (y_fine - y_ref).abs();
let ratio = err_coarse / err_fine;
assert!(
ratio > 2.0 && ratio < 4.0,
"Convergence ratio: got {}, expected ~2.83 (errors: coarse={}, fine={})",
ratio,
err_coarse,
err_fine
);
}
#[test]
fn test_l1_alpha_near_1() {
let system = FractionalRelaxation {
lambda: 1.0,
alpha: 0.99,
};
let options = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
let y_final = result.y_final().unwrap()[0];
let y_exact = (-1.0_f64).exp();
assert!(
(y_final - y_exact).abs() < 0.05,
"alpha=0.99: got {}, expected ~{}",
y_final,
y_exact
);
}
#[test]
fn test_l1_zero_rhs() {
struct ZeroRhs;
impl FdeSystem<f64> for ZeroRhs {
fn dim(&self) -> usize {
1
}
fn alpha(&self) -> f64 {
0.7
}
fn rhs(&self, _t: f64, _y: &[f64], f: &mut [f64]) {
f[0] = 0.0;
}
}
let options = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&ZeroRhs, 0.0, 1.0, &[5.0], &options).expect("Solve failed");
for (i, &t) in result.t.iter().enumerate() {
let y = result.y_at(i)[0];
assert!(
(y - 5.0).abs() < 1e-10,
"At t={}: y should be 5.0, got {}",
t,
y
);
}
}
#[test]
fn test_l1_dimension_mismatch() {
let system = FractionalRelaxation {
lambda: 1.0,
alpha: 0.5,
};
let options = FdeOptions::default();
let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0, 2.0], &options);
assert!(result.is_err(), "Should error on dimension mismatch");
}
}