use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::prelude::{Normal, Rng, StdRng};
use scirs2_core::Distribution;
#[derive(Debug, Clone)]
pub struct AndersonMattinglyConfig {
pub diffusion: f64,
pub sigma: f64,
pub correlation_length: f64,
pub dt: f64,
pub n_nodes: usize,
pub domain_length: f64,
}
impl Default for AndersonMattinglyConfig {
fn default() -> Self {
Self {
diffusion: 1.0,
sigma: 0.1,
correlation_length: 0.2,
dt: 1e-4,
n_nodes: 32,
domain_length: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct AndersonMattinglySolution {
pub times: Vec<f64>,
pub snapshots: Vec<Array1<f64>>,
pub grid: Array1<f64>,
}
impl AndersonMattinglySolution {
pub fn len(&self) -> usize {
self.times.len()
}
pub fn is_empty(&self) -> bool {
self.times.is_empty()
}
}
pub struct AndersonMattinglyScheme {
cfg: AndersonMattinglyConfig,
x_coords: Array1<f64>,
dx: f64,
cov_chol: Vec<f64>, heat_weights: Vec<f64>,
save_every: usize,
}
impl AndersonMattinglyScheme {
pub fn new(cfg: AndersonMattinglyConfig, save_every: usize) -> IntegrateResult<Self> {
let n = cfg.n_nodes;
if n == 0 {
return Err(IntegrateError::InvalidInput(
"n_nodes must be positive".to_string(),
));
}
let dx = cfg.domain_length / (n + 1) as f64;
let x_coords =
Array1::linspace(dx, cfg.domain_length - dx, n);
let mut cov = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let r = (x_coords[i] - x_coords[j]).abs();
cov[i * n + j] = (-r / cfg.correlation_length).exp();
}
}
let cov_chol = cholesky_lower(&cov, n)?;
let heat_weights: Vec<f64> = (1..=n)
.map(|k| {
let lam = cfg.diffusion * (k as f64 * std::f64::consts::PI / cfg.domain_length).powi(2);
(-lam * cfg.dt).exp()
})
.collect();
Ok(Self {
cfg,
x_coords,
dx,
cov_chol,
heat_weights,
save_every: save_every.max(1),
})
}
pub fn solve(
&self,
u0: &Array1<f64>,
t0: f64,
t_end: f64,
rng: &mut StdRng,
) -> IntegrateResult<AndersonMattinglySolution> {
let n = self.cfg.n_nodes;
if u0.len() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"u0 length {} ≠ n_nodes {}",
u0.len(),
n
)));
}
if t_end <= t0 {
return Err(IntegrateError::InvalidInput(
"t_end must be greater than t0".to_string(),
));
}
let normal = Normal::new(0.0_f64, 1.0).map_err(|e| {
IntegrateError::ComputationError(format!("Normal distribution: {e}"))
})?;
let n_steps = ((t_end - t0) / self.cfg.dt).ceil() as usize;
let capacity = (n_steps / self.save_every + 2).max(2);
let mut times = Vec::with_capacity(capacity);
let mut snapshots = Vec::with_capacity(capacity);
let mut u = u0.clone();
times.push(t0);
snapshots.push(u.clone());
let mut t = t0;
for step in 0..n_steps {
let actual_dt = self.cfg.dt.min(t_end - t);
if actual_dt <= 0.0 {
break;
}
u = self.apply_heat_semigroup(&u, actual_dt);
let xi: Vec<f64> = (0..n).map(|_| rng.sample(&normal)).collect();
let correlated_noise = self.apply_cholesky(&xi);
let noise_scale = self.cfg.sigma * actual_dt.sqrt();
for i in 0..n {
u[i] += noise_scale * correlated_noise[i];
}
t += actual_dt;
if (step + 1) % self.save_every == 0 || t >= t_end - 1e-14 {
times.push(t);
snapshots.push(u.clone());
}
}
Ok(AndersonMattinglySolution {
times,
snapshots,
grid: self.x_coords.clone(),
})
}
fn apply_heat_semigroup(&self, u: &Array1<f64>, dt: f64) -> Array1<f64> {
let n = self.cfg.n_nodes;
let l = self.cfg.domain_length;
let dx = self.dx;
let mut a = vec![0.0_f64; n];
for k in 0..n {
let mode = k + 1;
let mut sum = 0.0_f64;
for i in 0..n {
let xi = self.x_coords[i];
sum += u[i] * (mode as f64 * std::f64::consts::PI * xi / l).sin();
}
a[k] = sum * dx * 2.0 / l;
}
let weights: Vec<f64> = (0..n)
.map(|k| {
let lam = self.cfg.diffusion
* ((k + 1) as f64 * std::f64::consts::PI / l).powi(2);
(-lam * dt).exp()
})
.collect();
let mut u_new = Array1::zeros(n);
for i in 0..n {
let xi = self.x_coords[i];
let mut val = 0.0_f64;
for k in 0..n {
let mode = k + 1;
val += weights[k]
* a[k]
* (mode as f64 * std::f64::consts::PI * xi / l).sin();
}
u_new[i] = val;
}
u_new
}
fn apply_cholesky(&self, xi: &[f64]) -> Vec<f64> {
let n = self.cfg.n_nodes;
let mut result = vec![0.0_f64; n];
for i in 0..n {
let mut s = 0.0_f64;
for j in 0..=i {
s += self.cov_chol[i * n + j] * xi[j];
}
result[i] = s;
}
result
}
}
pub struct ImexParabolicSolver {
diffusion: f64,
sigma: f64,
dt: f64,
n_nodes: usize,
dx: f64,
x_coords: Array1<f64>,
thomas_diag: Vec<f64>,
thomas_super: Vec<f64>,
save_every: usize,
}
#[derive(Debug, Clone)]
pub struct ImexSolution {
pub times: Vec<f64>,
pub snapshots: Vec<Array1<f64>>,
pub grid: Array1<f64>,
}
impl ImexSolution {
pub fn len(&self) -> usize {
self.times.len()
}
pub fn is_empty(&self) -> bool {
self.times.is_empty()
}
}
impl ImexParabolicSolver {
pub fn new(
diffusion: f64,
sigma: f64,
dt: f64,
domain_length: f64,
n_nodes: usize,
save_every: usize,
) -> IntegrateResult<Self> {
if n_nodes == 0 {
return Err(IntegrateError::InvalidInput(
"n_nodes must be positive".to_string(),
));
}
if dt <= 0.0 || diffusion <= 0.0 {
return Err(IntegrateError::InvalidInput(
"dt and diffusion must be positive".to_string(),
));
}
let dx = domain_length / (n_nodes + 1) as f64;
let x_coords = Array1::linspace(dx, domain_length - dx, n_nodes);
let r = diffusion * dt / (dx * dx);
let main_diag = 1.0 + 2.0 * r;
let off_diag = -r;
let mut thomas_diag = vec![main_diag; n_nodes];
let thomas_super = vec![off_diag; n_nodes];
for i in 1..n_nodes {
let factor = off_diag / thomas_diag[i - 1];
thomas_diag[i] -= factor * off_diag;
}
Ok(Self {
diffusion,
sigma,
dt,
n_nodes,
dx,
x_coords,
thomas_diag,
thomas_super,
save_every: save_every.max(1),
})
}
pub fn solve<F>(
&self,
u0: &Array1<f64>,
nonlinear: F,
t0: f64,
t_end: f64,
rng: &mut StdRng,
) -> IntegrateResult<ImexSolution>
where
F: Fn(f64, &Array1<f64>) -> Array1<f64>,
{
if u0.len() != self.n_nodes {
return Err(IntegrateError::DimensionMismatch(format!(
"u0 length {} ≠ n_nodes {}",
u0.len(),
self.n_nodes
)));
}
if t_end <= t0 {
return Err(IntegrateError::InvalidInput(
"t_end must be greater than t0".to_string(),
));
}
let normal = Normal::new(0.0_f64, 1.0).map_err(|e| {
IntegrateError::ComputationError(format!("Normal distribution: {e}"))
})?;
let n_steps = ((t_end - t0) / self.dt).ceil() as usize;
let capacity = (n_steps / self.save_every + 2).max(2);
let mut times = Vec::with_capacity(capacity);
let mut snapshots = Vec::with_capacity(capacity);
let mut u = u0.clone();
times.push(t0);
snapshots.push(u.clone());
let noise_scale = self.sigma * (self.dt / self.dx).sqrt();
let mut t = t0;
for step in 0..n_steps {
let actual_dt = self.dt.min(t_end - t);
if actual_dt <= 0.0 {
break;
}
let ns = self.sigma * (actual_dt / self.dx).sqrt();
let f_u = nonlinear(t, &u);
let mut rhs = Array1::zeros(self.n_nodes);
for i in 0..self.n_nodes {
let xi = rng.sample(&normal);
rhs[i] = u[i] + actual_dt * f_u[i] + ns * xi;
}
u = self.thomas_solve(&rhs, actual_dt)?;
t += actual_dt;
if (step + 1) % self.save_every == 0 || t >= t_end - 1e-14 {
times.push(t);
snapshots.push(u.clone());
}
}
Ok(ImexSolution {
times,
snapshots,
grid: self.x_coords.clone(),
})
}
fn thomas_solve(&self, rhs: &Array1<f64>, dt: f64) -> IntegrateResult<Array1<f64>> {
let n = self.n_nodes;
let r = self.diffusion * dt / (self.dx * self.dx);
let main_diag_val = 1.0 + 2.0 * r;
let off_diag_val = -r;
let mut diag = vec![main_diag_val; n];
for i in 1..n {
let factor = off_diag_val / diag[i - 1];
diag[i] -= factor * off_diag_val;
}
let mut d = rhs.to_vec();
for i in 1..n {
let factor = off_diag_val / diag[i - 1];
d[i] -= factor * d[i - 1];
}
let mut x = vec![0.0_f64; n];
x[n - 1] = d[n - 1] / diag[n - 1];
for i in (0..n - 1).rev() {
x[i] = (d[i] - off_diag_val * x[i + 1]) / diag[i];
}
Ok(Array1::from_vec(x))
}
}
#[derive(Debug, Clone)]
pub struct SpectralGalerkinConfig {
pub diffusion: f64,
pub sigma: f64,
pub mode_sigmas: Option<Vec<f64>>,
pub n_modes: usize,
pub domain_length: f64,
}
impl Default for SpectralGalerkinConfig {
fn default() -> Self {
Self {
diffusion: 1.0,
sigma: 0.1,
mode_sigmas: None,
n_modes: 32,
domain_length: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SpectralGalerkinSolution {
pub times: Vec<f64>,
pub modal_snapshots: Vec<Array1<f64>>,
pub physical_snapshots: Vec<Array1<f64>>,
pub grid: Array1<f64>,
}
impl SpectralGalerkinSolution {
pub fn len(&self) -> usize {
self.times.len()
}
pub fn is_empty(&self) -> bool {
self.times.is_empty()
}
pub fn modal_energy_series(&self) -> Vec<f64> {
self.modal_snapshots
.iter()
.map(|a| 0.5 * a.iter().map(|&v| v * v).sum::<f64>())
.collect()
}
}
pub struct SpectralGalerkinSolver {
cfg: SpectralGalerkinConfig,
lambda: Vec<f64>,
sigma_k: Vec<f64>,
save_every: usize,
}
impl SpectralGalerkinSolver {
pub fn new(cfg: SpectralGalerkinConfig, save_every: usize) -> IntegrateResult<Self> {
if cfg.n_modes == 0 {
return Err(IntegrateError::InvalidInput(
"n_modes must be positive".to_string(),
));
}
let n = cfg.n_modes;
let lambda: Vec<f64> = (1..=n)
.map(|k| {
cfg.diffusion * (k as f64 * std::f64::consts::PI / cfg.domain_length).powi(2)
})
.collect();
let sigma_k: Vec<f64> = if let Some(ref ms) = cfg.mode_sigmas {
if ms.len() < n {
return Err(IntegrateError::InvalidInput(format!(
"mode_sigmas has {} entries but n_modes = {}",
ms.len(),
n
)));
}
ms[..n].to_vec()
} else {
vec![cfg.sigma; n]
};
Ok(Self {
cfg,
lambda,
sigma_k,
save_every: save_every.max(1),
})
}
pub fn solve(
&self,
a0: &Array1<f64>,
dt: f64,
eval_grid: &Array1<f64>,
t0: f64,
t_end: f64,
rng: &mut StdRng,
) -> IntegrateResult<SpectralGalerkinSolution> {
let n = self.cfg.n_modes;
if a0.len() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"a0 length {} ≠ n_modes {}",
a0.len(),
n
)));
}
if t_end <= t0 {
return Err(IntegrateError::InvalidInput(
"t_end must be greater than t0".to_string(),
));
}
if dt <= 0.0 {
return Err(IntegrateError::InvalidInput("dt must be positive".to_string()));
}
let normal = Normal::new(0.0_f64, 1.0).map_err(|e| {
IntegrateError::ComputationError(format!("Normal distribution: {e}"))
})?;
let n_steps = ((t_end - t0) / dt).ceil() as usize;
let capacity = (n_steps / self.save_every + 2).max(2);
let mut times = Vec::with_capacity(capacity);
let mut modal_snapshots = Vec::with_capacity(capacity);
let mut physical_snapshots = Vec::with_capacity(capacity);
let mut a = a0.clone();
let ng = eval_grid.len();
let l = self.cfg.domain_length;
let mut basis = vec![0.0_f64; n * ng]; for k in 0..n {
for g in 0..ng {
basis[k * ng + g] = ((k + 1) as f64 * std::f64::consts::PI * eval_grid[g] / l).sin();
}
}
let reconstruct = |a: &Array1<f64>| -> Array1<f64> {
let mut u = Array1::zeros(ng);
for g in 0..ng {
let mut s = 0.0_f64;
for k in 0..n {
s += a[k] * basis[k * ng + g];
}
u[g] = s;
}
u
};
times.push(t0);
modal_snapshots.push(a.clone());
physical_snapshots.push(reconstruct(&a));
let mut t = t0;
for step in 0..n_steps {
let actual_dt = dt.min(t_end - t);
if actual_dt <= 0.0 {
break;
}
for k in 0..n {
let lam = self.lambda[k];
let e_lam = (-lam * actual_dt).exp();
let var_k = if lam > 1e-15 {
self.sigma_k[k] * self.sigma_k[k] * (1.0 - e_lam * e_lam) / (2.0 * lam)
} else {
self.sigma_k[k] * self.sigma_k[k] * actual_dt
};
let std_k = var_k.max(0.0).sqrt();
let xi = rng.sample(&normal);
a[k] = e_lam * a[k] + std_k * xi;
}
t += actual_dt;
if (step + 1) % self.save_every == 0 || t >= t_end - 1e-14 {
times.push(t);
modal_snapshots.push(a.clone());
physical_snapshots.push(reconstruct(&a));
}
}
Ok(SpectralGalerkinSolution {
times,
modal_snapshots,
physical_snapshots,
grid: eval_grid.clone(),
})
}
}
fn cholesky_lower(a: &[f64], n: usize) -> IntegrateResult<Vec<f64>> {
let mut l = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..=i {
let mut s = 0.0_f64;
for k in 0..j {
s += l[i * n + k] * l[j * n + k];
}
if i == j {
let diag = a[i * n + i] - s;
if diag < 0.0 {
return Err(IntegrateError::ComputationError(format!(
"Cholesky failed: non-positive diagonal {diag} at row {i}. \
Increase correlation_length.",
)));
}
l[i * n + j] = diag.sqrt();
} else {
let lii = l[j * n + j];
l[i * n + j] = if lii.abs() > 1e-15 {
(a[i * n + j] - s) / lii
} else {
0.0
};
}
}
}
Ok(l)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
use scirs2_core::random::prelude::*;
fn make_rng() -> StdRng {
seeded_rng(77777)
}
#[test]
fn test_am_scheme_runs() {
let cfg = AndersonMattinglyConfig {
diffusion: 0.5,
sigma: 0.05,
correlation_length: 0.2,
dt: 1e-3,
n_nodes: 16,
domain_length: 1.0,
};
let solver = AndersonMattinglyScheme::new(cfg, 5).expect("AndersonMattinglyScheme::new should succeed");
let u0 = Array1::zeros(16);
let mut rng = make_rng();
let sol = solver.solve(&u0, 0.0, 0.05, &mut rng).expect("solver.solve should succeed");
assert!(!sol.is_empty());
for snap in &sol.snapshots {
assert!(snap.iter().all(|v| v.is_finite()));
}
}
#[test]
fn test_am_decay_without_noise() {
let cfg = AndersonMattinglyConfig {
diffusion: 1.0,
sigma: 0.0,
correlation_length: 0.2,
dt: 1e-3,
n_nodes: 10,
domain_length: 1.0,
};
let solver = AndersonMattinglyScheme::new(cfg, 1).expect("AndersonMattinglyScheme::new should succeed");
let u0 = Array1::from_vec(vec![1.0_f64; 10]);
let mut rng = make_rng();
let sol = solver.solve(&u0, 0.0, 0.1, &mut rng).expect("solver.solve should succeed");
let u_final = sol.snapshots.last().expect("solution has snapshots");
let norm_final: f64 = u_final.iter().map(|v| v * v).sum::<f64>().sqrt();
let norm_init: f64 = 10.0_f64.sqrt();
assert!(
norm_final < norm_init,
"Solution should decay without noise: norm_init={norm_init:.4}, norm_final={norm_final:.4}"
);
}
#[test]
fn test_imex_zero_nonlinear_runs() {
let solver = ImexParabolicSolver::new(0.1, 0.05, 1e-3, 1.0, 16, 5).expect("ImexParabolicSolver::new should succeed");
let u0 = Array1::zeros(16);
let mut rng = make_rng();
let sol = solver
.solve(&u0, |_t, _u| Array1::zeros(16), 0.0, 0.05, &mut rng)
.expect("solver.solve should succeed");
assert!(!sol.is_empty());
for snap in &sol.snapshots {
assert!(snap.iter().all(|v| v.is_finite()));
}
}
#[test]
fn test_imex_with_nonlinear_term() {
let solver = ImexParabolicSolver::new(0.1, 0.05, 1e-3, 1.0, 10, 10).expect("ImexParabolicSolver::new should succeed");
let u0 = Array1::from_vec(vec![0.5_f64; 10]);
let mut rng = make_rng();
let sol = solver
.solve(
&u0,
|_t, u| u.mapv(|ui| ui * (1.0 - ui)),
0.0,
0.05,
&mut rng,
)
.expect("solver.solve should succeed");
assert!(!sol.is_empty());
}
#[test]
fn test_spectral_galerkin_zero_ic() {
let cfg = SpectralGalerkinConfig {
diffusion: 1.0,
sigma: 0.1,
mode_sigmas: None,
n_modes: 8,
domain_length: 1.0,
};
let solver = SpectralGalerkinSolver::new(cfg, 10).expect("SpectralGalerkinSolver::new should succeed");
let a0 = Array1::zeros(8);
let grid = Array1::linspace(0.05, 0.95, 10);
let mut rng = make_rng();
let sol = solver.solve(&a0, 1e-3, &grid, 0.0, 0.1, &mut rng).expect("solver.solve should succeed");
assert!(!sol.is_empty());
for snap in &sol.physical_snapshots {
assert!(snap.iter().all(|v| v.is_finite()));
}
}
#[test]
fn test_spectral_galerkin_energy_series() {
let cfg = SpectralGalerkinConfig {
diffusion: 1.0,
sigma: 0.5,
mode_sigmas: None,
n_modes: 4,
domain_length: 1.0,
};
let solver = SpectralGalerkinSolver::new(cfg, 1).expect("SpectralGalerkinSolver::new should succeed");
let a0 = Array1::zeros(4);
let grid = Array1::linspace(0.1, 0.9, 5);
let mut rng = make_rng();
let sol = solver.solve(&a0, 1e-3, &grid, 0.0, 0.05, &mut rng).expect("solver.solve should succeed");
let energies = sol.modal_energy_series();
assert_eq!(energies.len(), sol.len());
for &e in &energies {
assert!(e >= 0.0 && e.is_finite());
}
}
#[test]
fn test_spectral_galerkin_no_noise_decays() {
let cfg = SpectralGalerkinConfig {
diffusion: 1.0,
sigma: 0.0,
mode_sigmas: None,
n_modes: 4,
domain_length: 1.0,
};
let solver = SpectralGalerkinSolver::new(cfg, 1).expect("SpectralGalerkinSolver::new should succeed");
let a0 = Array1::from_vec(vec![1.0, 0.5, 0.25, 0.1]);
let grid = Array1::linspace(0.1, 0.9, 5);
let mut rng = make_rng();
let sol = solver.solve(&a0, 1e-3, &grid, 0.0, 0.1, &mut rng).expect("solver.solve should succeed");
let energies = sol.modal_energy_series();
let e0 = energies[0];
let ef = *energies.last().expect("energies series is non-empty");
assert!(ef < e0, "Energy should decay without noise: e0={e0:.6}, ef={ef:.6}");
}
#[test]
fn test_per_mode_sigmas() {
let n = 8;
let mut mode_sigmas = vec![0.0_f64; n];
mode_sigmas[0] = 1.0;
let cfg = SpectralGalerkinConfig {
diffusion: 0.1,
sigma: 0.0,
mode_sigmas: Some(mode_sigmas),
n_modes: n,
domain_length: 1.0,
};
let solver = SpectralGalerkinSolver::new(cfg, 10).expect("SpectralGalerkinSolver::new should succeed");
let a0 = Array1::zeros(n);
let grid = Array1::linspace(0.1, 0.9, 10);
let mut rng = make_rng();
let sol = solver.solve(&a0, 1e-3, &grid, 0.0, 0.5, &mut rng).expect("solver.solve should succeed");
let a_final = sol.modal_snapshots.last().expect("modal snapshots is non-empty");
let a1_abs = a_final[0].abs();
let rest_max = a_final.iter().skip(1).map(|v| v.abs()).fold(0.0_f64, f64::max);
assert!(a_final.iter().all(|v| v.is_finite()));
}
}