1use crate::caputo::{caputo_weights, l1_coefficient};
22use crate::system::{FdeOptions, FdeResult, FdeSolver, FdeStats, FdeSystem};
23use numra_core::Scalar;
24
25pub struct L1Solver;
29
30impl<S: Scalar> FdeSolver<S> for L1Solver {
31 fn solve<Sys: FdeSystem<S>>(
32 system: &Sys,
33 t0: S,
34 tf: S,
35 y0: &[S],
36 options: &FdeOptions<S>,
37 ) -> Result<FdeResult<S>, String> {
38 let dim = system.dim();
39 let alpha = system.alpha();
40
41 if !system.is_valid_order() {
42 return Err(format!(
43 "Invalid fractional order α = {}. Must be in (0, 1].",
44 alpha.to_f64()
45 ));
46 }
47
48 if y0.len() != dim {
49 return Err(format!(
50 "Initial state dimension {} doesn't match system dimension {}",
51 y0.len(),
52 dim
53 ));
54 }
55
56 let dt = options.dt;
57 let n_steps = ((tf - t0) / dt).to_f64().ceil() as usize;
58
59 if n_steps > options.max_steps {
60 return Err(format!(
61 "Required steps {} exceeds maximum {}",
62 n_steps, options.max_steps
63 ));
64 }
65
66 let c = l1_coefficient(dt, alpha);
68
69 let mut y_history: Vec<Vec<S>> = Vec::with_capacity(n_steps + 1);
71 y_history.push(y0.to_vec());
72
73 let mut t_out = vec![t0];
74 let mut y_out = y0.to_vec();
75 let mut stats = FdeStats::default();
76
77 let mut f_buf = vec![S::ZERO; dim];
78
79 for n in 1..=n_steps {
80 let t = t0 + S::from_usize(n) * dt;
81
82 let b = caputo_weights(n, alpha);
84
85 let mut history_sum = vec![S::ZERO; dim];
87 for j in 1..n {
88 let y_nmj = &y_history[n - j];
89 let y_nmjm1 = &y_history[n - j - 1];
90 for i in 0..dim {
91 history_sum[i] += b[j] * (y_nmj[i] - y_nmjm1[i]);
92 }
93 }
94
95 let y_prev = &y_history[n - 1];
97
98 let mut y_new = y_prev.clone();
105
106 let c_b0 = c * b[0];
107 let inv_c_b0 = S::ONE / c_b0;
108
109 for _iter in 0..options.max_iter {
110 system.rhs(t, &y_new, &mut f_buf);
112 stats.n_rhs += 1;
113
114 let mut y_next = vec![S::ZERO; dim];
116 let mut converged = true;
117
118 for i in 0..dim {
119 let rhs = f_buf[i] - c * history_sum[i];
120 y_next[i] = y_prev[i] + rhs * inv_c_b0;
121
122 let diff = (y_next[i] - y_new[i]).abs();
124 let scale = options.tol * (S::ONE + y_new[i].abs());
125 if diff > scale {
126 converged = false;
127 }
128 }
129
130 y_new = y_next;
131
132 if converged {
133 break;
134 }
135 }
136
137 y_history.push(y_new.clone());
139 t_out.push(t);
140 y_out.extend_from_slice(&y_new);
141 stats.n_steps += 1;
142 }
143
144 Ok(FdeResult::new(t_out, y_out, dim, stats))
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::caputo::mittag_leffler_1;
152
153 struct FractionalRelaxation {
156 lambda: f64,
157 alpha: f64,
158 }
159
160 impl FdeSystem<f64> for FractionalRelaxation {
161 fn dim(&self) -> usize {
162 1
163 }
164 fn alpha(&self) -> f64 {
165 self.alpha
166 }
167 fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
168 f[0] = -self.lambda * y[0];
169 }
170 }
171
172 #[test]
173 fn test_l1_fractional_relaxation_alpha_1() {
174 let system = FractionalRelaxation {
177 lambda: 1.0,
178 alpha: 1.0,
179 };
180 let options = FdeOptions::default().dt(0.01);
181
182 let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
183
184 assert!(result.success);
185 let y_final = result.y_final().unwrap()[0];
186 let y_exact = (-1.0_f64).exp();
187
188 assert!((y_final - y_exact).abs() < 0.01);
190 }
191
192 #[test]
193 fn test_l1_fractional_relaxation_alpha_half() {
194 let system = FractionalRelaxation {
197 lambda: 1.0,
198 alpha: 0.5,
199 };
200 let options = FdeOptions::default().dt(0.01);
201
202 let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
203
204 assert!(result.success);
205
206 for (i, &t) in result.t.iter().enumerate().step_by(10) {
208 if t > 0.0 {
209 let y_computed = result.y_at(i)[0];
210 let y_exact = mittag_leffler_1(-t.powf(0.5), 0.5);
211
212 let error = (y_computed - y_exact).abs();
215 assert!(
216 error < 0.05,
217 "At t={}: computed={}, exact={}, error={}",
218 t,
219 y_computed,
220 y_exact,
221 error
222 );
223 }
224 }
225 }
226
227 #[test]
228 fn test_l1_2d_system() {
229 struct TwoD;
230 impl FdeSystem<f64> for TwoD {
231 fn dim(&self) -> usize {
232 2
233 }
234 fn alpha(&self) -> f64 {
235 0.8
236 }
237 fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
238 f[0] = -y[0];
239 f[1] = -2.0 * y[1];
240 }
241 }
242
243 let options = FdeOptions::default().dt(0.01);
244 let result = L1Solver::solve(&TwoD, 0.0, 1.0, &[1.0, 1.0], &options).expect("Solve failed");
245
246 assert!(result.success);
247 let y_final = result.y_final().unwrap();
248
249 assert!(y_final[0] > y_final[1]);
251 }
252
253 #[test]
254 fn test_invalid_alpha() {
255 struct InvalidAlpha;
256 impl FdeSystem<f64> for InvalidAlpha {
257 fn dim(&self) -> usize {
258 1
259 }
260 fn alpha(&self) -> f64 {
261 1.5
262 } fn rhs(&self, _t: f64, _y: &[f64], f: &mut [f64]) {
264 f[0] = 0.0;
265 }
266 }
267
268 let options = FdeOptions::default();
269 let result = L1Solver::solve(&InvalidAlpha, 0.0, 1.0, &[1.0], &options);
270
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_l1_convergence_order() {
276 let system = FractionalRelaxation {
279 lambda: 1.0,
280 alpha: 0.5,
281 };
282 let tf = 1.0;
283
284 let opts_coarse = FdeOptions::default().dt(0.02);
286 let res_coarse =
287 L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_coarse).expect("Coarse solve failed");
288 let y_coarse = res_coarse.y_final().unwrap()[0];
289
290 let opts_fine = FdeOptions::default().dt(0.01);
292 let res_fine =
293 L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_fine).expect("Fine solve failed");
294 let y_fine = res_fine.y_final().unwrap()[0];
295
296 let opts_ref = FdeOptions::default().dt(0.001);
298 let res_ref =
299 L1Solver::solve(&system, 0.0, tf, &[1.0], &opts_ref).expect("Ref solve failed");
300 let y_ref = res_ref.y_final().unwrap()[0];
301
302 let err_coarse = (y_coarse - y_ref).abs();
303 let err_fine = (y_fine - y_ref).abs();
304
305 let ratio = err_coarse / err_fine;
306 assert!(
308 ratio > 2.0 && ratio < 4.0,
309 "Convergence ratio: got {}, expected ~2.83 (errors: coarse={}, fine={})",
310 ratio,
311 err_coarse,
312 err_fine
313 );
314 }
315
316 #[test]
317 fn test_l1_alpha_near_1() {
318 let system = FractionalRelaxation {
320 lambda: 1.0,
321 alpha: 0.99,
322 };
323 let options = FdeOptions::default().dt(0.01);
324
325 let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
326
327 let y_final = result.y_final().unwrap()[0];
328 let y_exact = (-1.0_f64).exp();
329
330 assert!(
331 (y_final - y_exact).abs() < 0.05,
332 "alpha=0.99: got {}, expected ~{}",
333 y_final,
334 y_exact
335 );
336 }
337
338 #[test]
339 fn test_l1_zero_rhs() {
340 struct ZeroRhs;
342 impl FdeSystem<f64> for ZeroRhs {
343 fn dim(&self) -> usize {
344 1
345 }
346 fn alpha(&self) -> f64 {
347 0.7
348 }
349 fn rhs(&self, _t: f64, _y: &[f64], f: &mut [f64]) {
350 f[0] = 0.0;
351 }
352 }
353
354 let options = FdeOptions::default().dt(0.01);
355 let result = L1Solver::solve(&ZeroRhs, 0.0, 1.0, &[5.0], &options).expect("Solve failed");
356
357 for (i, &t) in result.t.iter().enumerate() {
358 let y = result.y_at(i)[0];
359 assert!(
360 (y - 5.0).abs() < 1e-10,
361 "At t={}: y should be 5.0, got {}",
362 t,
363 y
364 );
365 }
366 }
367
368 #[test]
369 fn test_l1_dimension_mismatch() {
370 let system = FractionalRelaxation {
372 lambda: 1.0,
373 alpha: 0.5,
374 };
375 let options = FdeOptions::default();
376
377 let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0, 2.0], &options);
378 assert!(result.is_err(), "Should error on dimension mismatch");
379 }
380}