use crate::kernels::PronyKernel;
use crate::system::{IdeOptions, IdeResult, IdeStats};
use numra_core::Scalar;
pub trait PronySystem<S: Scalar> {
fn dim(&self) -> usize;
fn rhs(&self, t: S, y: &[S], f: &mut [S]);
fn kernel(&self) -> &PronyKernel<S>;
fn coupling(&self) -> Option<Vec<Vec<S>>> {
None
}
}
pub struct PronySolver;
impl PronySolver {
pub fn solve<S: Scalar, Sys: PronySystem<S>>(
system: &Sys,
t0: S,
tf: S,
y0: &[S],
options: &IdeOptions<S>,
) -> Result<IdeResult<S>, String> {
let dim = system.dim();
let kernel = system.kernel();
let n_terms = kernel.num_terms();
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 mut y = y0.to_vec();
let mut integrals: Vec<Vec<S>> = vec![vec![S::ZERO; dim]; n_terms];
let mut t_out = vec![t0];
let mut y_out = y0.to_vec();
let mut stats = IdeStats::default();
let mut t = t0;
let mut f_buf = vec![S::ZERO; dim];
let coupling = system.coupling();
let half = S::from_f64(0.5);
let sixth = S::ONE / S::from_f64(6.0);
let two = S::from_f64(2.0);
for _n in 1..=n_steps {
let t_new = t + dt;
let (k1_y, k1_i) =
compute_derivatives(system, t, &y, &integrals, &coupling, &mut f_buf, &mut stats);
let y_mid1: Vec<S> = y
.iter()
.zip(k1_y.iter())
.map(|(&yi, &ki)| yi + half * dt * ki)
.collect();
let i_mid1: Vec<Vec<S>> = integrals
.iter()
.zip(k1_i.iter())
.map(|(ii, ki)| {
ii.iter()
.zip(ki.iter())
.map(|(&ij, &kij)| ij + half * dt * kij)
.collect()
})
.collect();
let (k2_y, k2_i) = compute_derivatives(
system,
t + half * dt,
&y_mid1,
&i_mid1,
&coupling,
&mut f_buf,
&mut stats,
);
let y_mid2: Vec<S> = y
.iter()
.zip(k2_y.iter())
.map(|(&yi, &ki)| yi + half * dt * ki)
.collect();
let i_mid2: Vec<Vec<S>> = integrals
.iter()
.zip(k2_i.iter())
.map(|(ii, ki)| {
ii.iter()
.zip(ki.iter())
.map(|(&ij, &kij)| ij + half * dt * kij)
.collect()
})
.collect();
let (k3_y, k3_i) = compute_derivatives(
system,
t + half * dt,
&y_mid2,
&i_mid2,
&coupling,
&mut f_buf,
&mut stats,
);
let y_end: Vec<S> = y
.iter()
.zip(k3_y.iter())
.map(|(&yi, &ki)| yi + dt * ki)
.collect();
let i_end: Vec<Vec<S>> = integrals
.iter()
.zip(k3_i.iter())
.map(|(ii, ki)| {
ii.iter()
.zip(ki.iter())
.map(|(&ij, &kij)| ij + dt * kij)
.collect()
})
.collect();
let (k4_y, k4_i) = compute_derivatives(
system,
t + dt,
&y_end,
&i_end,
&coupling,
&mut f_buf,
&mut stats,
);
for i in 0..dim {
y[i] += sixth * dt * (k1_y[i] + two * k2_y[i] + two * k3_y[i] + k4_y[i]);
}
for k in 0..n_terms {
for i in 0..dim {
integrals[k][i] += sixth
* dt
* (k1_i[k][i] + two * k2_i[k][i] + two * k3_i[k][i] + k4_i[k][i]);
}
}
t_out.push(t_new);
y_out.extend_from_slice(&y);
stats.n_steps += 1;
t = t_new;
}
Ok(IdeResult::new(t_out, y_out, dim, stats))
}
}
fn compute_derivatives<S: Scalar, Sys: PronySystem<S>>(
system: &Sys,
t: S,
y: &[S],
integrals: &[Vec<S>],
coupling: &Option<Vec<Vec<S>>>,
f_buf: &mut [S],
stats: &mut IdeStats,
) -> (Vec<S>, Vec<Vec<S>>) {
let dim = y.len();
let kernel = system.kernel();
let n_terms = kernel.num_terms();
system.rhs(t, y, f_buf);
stats.n_rhs += 1;
let mut dy = f_buf.to_vec();
if let Some(c) = coupling {
for i in 0..dim {
for k in 0..n_terms {
dy[i] += c[i][k] * integrals[k][i];
}
}
} else {
for i in 0..dim {
for integral in integrals.iter().take(n_terms) {
dy[i] += integral[i];
}
}
}
let mut di: Vec<Vec<S>> = Vec::with_capacity(n_terms);
for (k, integral) in integrals.iter().enumerate().take(n_terms) {
let a_k = kernel.amplitudes[k];
let b_k = kernel.rates[k];
let mut di_k = vec![S::ZERO; dim];
for i in 0..dim {
di_k[i] = a_k * y[i] - b_k * integral[i];
}
di.push(di_k);
stats.n_kernel += dim;
}
(dy, di)
}
#[cfg(test)]
mod tests {
use super::*;
struct Viscoelastic {
k: f64,
kernel: PronyKernel<f64>,
}
impl Viscoelastic {
fn new(k: f64, a: f64, b: f64) -> Self {
Self {
k,
kernel: PronyKernel::single(a, b),
}
}
}
impl PronySystem<f64> for Viscoelastic {
fn dim(&self) -> usize {
1
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -self.k * y[0];
}
fn kernel(&self) -> &PronyKernel<f64> {
&self.kernel
}
}
#[test]
fn test_prony_viscoelastic() {
let system = Viscoelastic::new(1.0, 0.5, 0.3);
let options = IdeOptions::default().dt(0.01);
let result = PronySolver::solve(&system, 0.0, 2.0, &[1.0], &options).expect("Solve failed");
assert!(result.success);
let y_final = result.y_final().unwrap()[0];
assert!(y_final > 0.0, "Solution should remain positive");
assert!(y_final < 1.0, "Solution should decay");
assert!(
y_final > 0.135,
"Memory should slow decay: y_final = {}",
y_final
);
}
#[test]
fn test_prony_two_term() {
struct TwoTermMaxwell {
kernel: PronyKernel<f64>,
}
impl PronySystem<f64> for TwoTermMaxwell {
fn dim(&self) -> usize {
1
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -2.0 * y[0]; }
fn kernel(&self) -> &PronyKernel<f64> {
&self.kernel
}
}
let system = TwoTermMaxwell {
kernel: PronyKernel::two_term(0.8, 0.5, 0.4, 2.0),
};
let options = IdeOptions::default().dt(0.01);
let result = PronySolver::solve(&system, 0.0, 3.0, &[1.0], &options).expect("Solve failed");
assert!(result.success);
for (i, &t) in result.t.iter().enumerate() {
let y = result.y_at(i)[0];
assert!(y.is_finite(), "Solution should be finite at t={}", t);
assert!(
y >= 0.0 || y.abs() < 0.1,
"Solution should be non-negative or small negative at t={}",
t
);
}
}
#[test]
fn test_prony_2d_system() {
struct TwoDProny {
kernel: PronyKernel<f64>,
}
impl PronySystem<f64> for TwoDProny {
fn dim(&self) -> usize {
2
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -y[0] + 0.1 * y[1];
f[1] = -0.5 * y[1];
}
fn kernel(&self) -> &PronyKernel<f64> {
&self.kernel
}
}
let system = TwoDProny {
kernel: PronyKernel::single(0.3, 0.5),
};
let options = IdeOptions::default().dt(0.01);
let result =
PronySolver::solve(&system, 0.0, 2.0, &[1.0, 1.0], &options).expect("Solve failed");
assert!(result.success);
let y_final = result.y_final().unwrap();
assert_eq!(y_final.len(), 2);
}
#[test]
fn test_prony_efficiency() {
let system = Viscoelastic::new(1.0, 0.5, 0.3);
let options_short = IdeOptions::default().dt(0.01);
let result_short = PronySolver::solve(&system, 0.0, 1.0, &[1.0], &options_short)
.expect("Short solve failed");
let options_long = IdeOptions::default().dt(0.01);
let result_long = PronySolver::solve(&system, 0.0, 10.0, &[1.0], &options_long)
.expect("Long solve failed");
assert!(result_short.success);
assert!(result_long.success);
let ratio = result_long.stats.n_kernel as f64 / result_short.stats.n_kernel as f64;
assert!(
ratio < 15.0,
"Kernel evals should scale linearly: ratio = {}",
ratio
);
}
#[test]
fn test_prony_dimension_mismatch() {
let system = Viscoelastic::new(1.0, 0.5, 0.3);
let options = IdeOptions::default().dt(0.01);
let result = PronySolver::solve(&system, 0.0, 1.0, &[1.0, 2.0], &options);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("dimension"), "Error message: {}", msg);
}
#[test]
fn test_prony_max_steps_exceeded() {
let system = Viscoelastic::new(1.0, 0.5, 0.3);
let options = IdeOptions::default().dt(0.001).max_steps(5);
let result = PronySolver::solve(&system, 0.0, 1.0, &[1.0], &options);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("exceeds maximum"), "Error message: {}", msg);
}
#[test]
fn test_prony_zero_kernel() {
struct PureDecay {
kernel: PronyKernel<f64>,
}
impl PronySystem<f64> for PureDecay {
fn dim(&self) -> usize {
1
}
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) {
f[0] = -y[0];
}
fn kernel(&self) -> &PronyKernel<f64> {
&self.kernel
}
}
let system = PureDecay {
kernel: PronyKernel::single(0.0, 1.0),
};
let options = IdeOptions::default().dt(0.001);
let result = PronySolver::solve(&system, 0.0, 1.0, &[1.0], &options).expect("Solve failed");
let y_final = result.y_final().unwrap()[0];
let expected = (-1.0_f64).exp(); assert!(
(y_final - expected).abs() < 1e-4,
"Zero kernel Prony should match pure ODE: got {}, expected {}",
y_final,
expected
);
}
}