use crate::error::{invalid_param, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BoundaryCondition {
Dirichlet(f64, f64),
NeumannZero,
Periodic,
}
#[derive(Debug, Clone)]
pub struct CrankNicolsonDiffusion {
pub dx: f64,
pub diffusivity: f64,
pub boundary: BoundaryCondition,
}
impl CrankNicolsonDiffusion {
pub fn new(dx: f64, diffusivity: f64, boundary: BoundaryCondition) -> Result<Self> {
if !dx.is_finite() || dx <= 0.0 {
return Err(invalid_param("dx", "must be finite and positive"));
}
if !diffusivity.is_finite() || diffusivity < 0.0 {
return Err(invalid_param(
"diffusivity",
"must be finite and non-negative",
));
}
Ok(Self {
dx,
diffusivity,
boundary,
})
}
pub fn step(&self, u: &[f64], source: &[f64], dt: f64) -> Result<Vec<f64>> {
if !dt.is_finite() || dt <= 0.0 {
return Err(invalid_param("dt", "must be finite and positive"));
}
if u.len() != source.len() {
return Err(invalid_param("source", "must have the same length as u"));
}
if u.len() < 3 {
return Err(invalid_param("u", "must have at least 3 nodes"));
}
let n = u.len();
let r = self.diffusivity * dt / (self.dx * self.dx);
let half_r = 0.5 * r;
match self.boundary {
BoundaryCondition::Dirichlet(left, right) => {
solve_dirichlet(u, source, dt, half_r, left, right)
},
BoundaryCondition::NeumannZero => solve_neumann(u, source, dt, r, half_r),
BoundaryCondition::Periodic => solve_periodic(u, source, dt, n, r, half_r),
}
}
pub fn evolve<F>(
&self,
u_init: &[f64],
source_fn: F,
t_end: f64,
dt: f64,
) -> Result<Vec<Vec<f64>>>
where
F: Fn(f64, &[f64]) -> Vec<f64>,
{
if !t_end.is_finite() || t_end <= 0.0 {
return Err(invalid_param("t_end", "must be finite and positive"));
}
if !dt.is_finite() || dt <= 0.0 {
return Err(invalid_param("dt", "must be finite and positive"));
}
let n_steps = (t_end / dt).round() as usize;
let mut history: Vec<Vec<f64>> = Vec::with_capacity(n_steps + 1);
history.push(u_init.to_vec());
let mut t = 0.0;
let mut current = u_init.to_vec();
for _ in 0..n_steps {
let source = source_fn(t + 0.5 * dt, ¤t);
current = self.step(¤t, &source, dt)?;
t += dt;
history.push(current.clone());
}
Ok(history)
}
pub fn steady_state(&self, source: &[f64]) -> Result<Vec<f64>> {
if source.len() < 3 {
return Err(invalid_param("source", "must have at least 3 nodes"));
}
let n = source.len();
let dx2 = self.dx * self.dx;
let d = self.diffusivity;
if d <= 0.0 {
return Err(invalid_param(
"diffusivity",
"must be positive for steady-state solve",
));
}
match self.boundary {
BoundaryCondition::Dirichlet(left, right) => {
let m = n - 2;
let mut a = vec![0.0_f64; m]; let mut b = vec![0.0_f64; m]; let mut c = vec![0.0_f64; m]; let mut rhs = vec![0.0_f64; m];
for i in 0..m {
a[i] = 1.0;
b[i] = -2.0;
c[i] = 1.0;
rhs[i] = -source[i + 1] * dx2 / d;
}
rhs[0] -= left;
rhs[m - 1] -= right;
let inner = thomas(&a, &b, &c, &rhs)?;
let mut out = vec![0.0_f64; n];
out[0] = left;
out[n - 1] = right;
out[1..(n - 1)].copy_from_slice(&inner);
Ok(out)
},
BoundaryCondition::NeumannZero => Err(invalid_param(
"boundary",
"NeumannZero steady-state is only defined up to an additive constant",
)),
BoundaryCondition::Periodic => Err(invalid_param(
"boundary",
"Periodic steady-state is only defined up to an additive constant",
)),
}
}
}
fn solve_dirichlet(
u: &[f64],
source: &[f64],
dt: f64,
half_r: f64,
left: f64,
right: f64,
) -> Result<Vec<f64>> {
let n = u.len();
let m = n - 2;
let mut a = vec![0.0_f64; m];
let mut b = vec![0.0_f64; m];
let mut c = vec![0.0_f64; m];
let mut rhs = vec![0.0_f64; m];
let one_minus = 1.0 - 2.0 * half_r; let one_plus = 1.0 + 2.0 * half_r;
for i in 0..m {
a[i] = -half_r;
b[i] = one_plus;
c[i] = -half_r;
let i_global = i + 1;
rhs[i] = half_r * u[i_global - 1]
+ one_minus * u[i_global]
+ half_r * u[i_global + 1]
+ dt * source[i_global];
}
rhs[0] += half_r * left;
rhs[m - 1] += half_r * right;
let inner = thomas(&a, &b, &c, &rhs)?;
let mut out = vec![0.0_f64; n];
out[0] = left;
out[n - 1] = right;
out[1..(n - 1)].copy_from_slice(&inner);
Ok(out)
}
fn solve_neumann(u: &[f64], source: &[f64], dt: f64, r: f64, half_r: f64) -> Result<Vec<f64>> {
let n = u.len();
let mut a = vec![0.0_f64; n];
let mut b = vec![0.0_f64; n];
let mut c = vec![0.0_f64; n];
let mut rhs = vec![0.0_f64; n];
let one_minus = 1.0 - r;
let one_plus = 1.0 + r;
a[0] = 0.0;
b[0] = one_plus;
c[0] = -r;
rhs[0] = one_minus * u[0] + r * u[1] + dt * source[0];
for i in 1..(n - 1) {
a[i] = -half_r;
b[i] = one_plus;
c[i] = -half_r;
rhs[i] = half_r * u[i - 1] + one_minus * u[i] + half_r * u[i + 1] + dt * source[i];
}
let last = n - 1;
a[last] = -r;
b[last] = one_plus;
c[last] = 0.0;
rhs[last] = r * u[last - 1] + one_minus * u[last] + dt * source[last];
thomas(&a, &b, &c, &rhs)
}
fn solve_periodic(
u: &[f64],
source: &[f64],
dt: f64,
n: usize,
r: f64,
half_r: f64,
) -> Result<Vec<f64>> {
let one_minus = 1.0 - r;
let one_plus = 1.0 + r;
let gamma = -one_plus;
let mut a = vec![0.0_f64; n];
let mut b = vec![0.0_f64; n];
let mut c = vec![0.0_f64; n];
let mut rhs = vec![0.0_f64; n];
for i in 0..n {
let im1 = if i == 0 { n - 1 } else { i - 1 };
let ip1 = if i == n - 1 { 0 } else { i + 1 };
a[i] = -half_r;
b[i] = one_plus;
c[i] = -half_r;
rhs[i] = half_r * u[im1] + one_minus * u[i] + half_r * u[ip1] + dt * source[i];
}
a[0] = 0.0;
c[n - 1] = 0.0;
b[0] -= gamma;
b[n - 1] -= -half_r * (-half_r / gamma);
let y = thomas(&a, &b, &c, &rhs)?;
let mut u_vec = vec![0.0_f64; n];
u_vec[0] = gamma;
u_vec[n - 1] = -half_r;
let z = thomas(&a, &b, &c, &u_vec)?;
let v_dot_y = y[0] + (-half_r / gamma) * y[n - 1];
let v_dot_z = z[0] + (-half_r / gamma) * z[n - 1];
let denom = 1.0 + v_dot_z;
if denom.abs() < 1e-30 {
return Err(invalid_param(
"periodic_solve",
"Sherman-Morrison denominator is singular",
));
}
let coeff = v_dot_y / denom;
let mut x = vec![0.0_f64; n];
for i in 0..n {
x[i] = y[i] - coeff * z[i];
}
Ok(x)
}
fn thomas(a: &[f64], b: &[f64], c: &[f64], d: &[f64]) -> Result<Vec<f64>> {
let n = b.len();
if a.len() != n || c.len() != n || d.len() != n {
return Err(invalid_param(
"thomas",
"tridiagonal coefficient slices must all have the same length",
));
}
if n == 0 {
return Ok(Vec::new());
}
let mut c_prime = vec![0.0_f64; n];
let mut d_prime = vec![0.0_f64; n];
if b[0].abs() < 1e-30 {
return Err(invalid_param("thomas", "zero pivot encountered"));
}
c_prime[0] = c[0] / b[0];
d_prime[0] = d[0] / b[0];
for i in 1..n {
let denom = b[i] - a[i] * c_prime[i - 1];
if denom.abs() < 1e-30 {
return Err(invalid_param("thomas", "zero pivot encountered"));
}
c_prime[i] = c[i] / denom;
d_prime[i] = (d[i] - a[i] * d_prime[i - 1]) / denom;
}
let mut x = vec![0.0_f64; n];
x[n - 1] = d_prime[n - 1];
for i in (0..(n - 1)).rev() {
x[i] = d_prime[i] - c_prime[i] * x[i + 1];
}
Ok(x)
}
#[derive(Debug, Clone)]
pub struct SpinDiffusionCrankNicolson {
pub cn: CrankNicolsonDiffusion,
pub spin_flip_time: f64,
}
impl SpinDiffusionCrankNicolson {
pub fn new(
dx: f64,
diffusivity: f64,
spin_flip_time: f64,
boundary: BoundaryCondition,
) -> Result<Self> {
let cn = CrankNicolsonDiffusion::new(dx, diffusivity, boundary)?;
if !spin_flip_time.is_finite() || spin_flip_time <= 0.0 {
return Err(invalid_param(
"spin_flip_time",
"must be finite and positive",
));
}
Ok(Self { cn, spin_flip_time })
}
pub fn step(&self, mu_s: &[f64], dt: f64) -> Result<Vec<f64>> {
if !dt.is_finite() || dt <= 0.0 {
return Err(invalid_param("dt", "must be finite and positive"));
}
if mu_s.len() < 3 {
return Err(invalid_param("mu_s", "must have at least 3 nodes"));
}
let n = mu_s.len();
let source = vec![0.0_f64; n];
let diffused = self.cn.step(mu_s, &source, dt)?;
let factor =
(1.0 - 0.5 * dt / self.spin_flip_time) / (1.0 + 0.5 * dt / self.spin_flip_time);
Ok(diffused.iter().map(|&v| v * factor).collect())
}
pub fn spin_diffusion_length(&self) -> f64 {
(self.cn.diffusivity * self.spin_flip_time).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constructor_valid() {
let cn = CrankNicolsonDiffusion::new(0.1, 0.5, BoundaryCondition::NeumannZero)
.expect("valid constructor");
assert!((cn.dx - 0.1).abs() < 1e-15);
assert!((cn.diffusivity - 0.5).abs() < 1e-15);
assert_eq!(cn.boundary, BoundaryCondition::NeumannZero);
}
#[test]
fn test_invalid_dx_rejected() {
let r = CrankNicolsonDiffusion::new(0.0, 1.0, BoundaryCondition::NeumannZero);
assert!(r.is_err());
let r = CrankNicolsonDiffusion::new(-0.1, 1.0, BoundaryCondition::NeumannZero);
assert!(r.is_err());
let r = CrankNicolsonDiffusion::new(f64::NAN, 1.0, BoundaryCondition::NeumannZero);
assert!(r.is_err());
}
#[test]
fn test_invalid_diffusivity_rejected() {
let r = CrankNicolsonDiffusion::new(0.1, -0.5, BoundaryCondition::NeumannZero);
assert!(r.is_err());
let r = CrankNicolsonDiffusion::new(0.1, f64::INFINITY, BoundaryCondition::NeumannZero);
assert!(r.is_err());
}
#[test]
fn test_dirichlet_steady_state_linear() {
let n = 21;
let l = 1.0;
let dx = l / (n as f64 - 1.0);
let cn =
CrankNicolsonDiffusion::new(dx, 1.0, BoundaryCondition::Dirichlet(0.0, 1.0)).unwrap();
let source = vec![0.0_f64; n];
let u = cn.steady_state(&source).expect("steady-state ok");
for (i, &val) in u.iter().enumerate() {
let x = i as f64 * dx;
let expected = x / l;
assert!(
(val - expected).abs() < 1e-12,
"node {}: got {}, expected {}",
i,
val,
expected
);
}
}
#[test]
fn test_neumann_mass_conservation() {
let n = 41;
let dx = 0.05;
let cn = CrankNicolsonDiffusion::new(dx, 1.0, BoundaryCondition::NeumannZero).unwrap();
let mut u = vec![0.0_f64; n];
u[n / 2] = 10.0;
let source = vec![0.0_f64; n];
let mass = |v: &[f64]| -> f64 {
let interior: f64 = v[1..(n - 1)].iter().sum();
dx * (0.5 * v[0] + interior + 0.5 * v[n - 1])
};
let m0 = mass(&u);
let dt = 0.01;
let mut current = u;
for _ in 0..100 {
current = cn.step(¤t, &source, dt).expect("step ok");
}
let m_final = mass(¤t);
let drift = ((m_final - m0) / m0).abs();
assert!(drift < 1e-10, "mass drift: {:.3e}", drift);
}
#[test]
fn test_periodic_mode_decay() {
let n = 64;
let l = 1.0;
let dx = l / n as f64;
let d = 1.0_f64;
let cn = CrankNicolsonDiffusion::new(dx, d, BoundaryCondition::Periodic).unwrap();
let k = 2.0 * std::f64::consts::PI / l;
let u0: Vec<f64> = (0..n).map(|i| (k * (i as f64) * dx).cos()).collect();
let source = vec![0.0_f64; n];
let dt = 0.001_f64;
let n_steps = 100;
let mut current = u0.clone();
for _ in 0..n_steps {
current = cn.step(¤t, &source, dt).expect("step ok");
}
let t_total = dt * n_steps as f64;
let expected_factor = (-d * k * k * t_total).exp();
let measured = current[0];
let expected = u0[0] * expected_factor;
let err = (measured - expected).abs();
assert!(
err < 5e-3,
"periodic decay error: got {}, expected {} (diff {:.3e})",
measured,
expected,
err
);
}
#[test]
fn test_spatial_second_order_accuracy() {
let mut errors = Vec::new();
let dxs = [0.05_f64, 0.025, 0.0125];
for &dx in &dxs {
let n = (1.0 / dx).round() as usize + 1;
let cn = CrankNicolsonDiffusion::new(dx, 1.0, BoundaryCondition::Dirichlet(0.0, 0.0))
.unwrap();
let pi = std::f64::consts::PI;
let source: Vec<f64> = (0..n)
.map(|i| pi * pi * (pi * i as f64 * dx).sin())
.collect();
let u = cn.steady_state(&source).expect("steady-state ok");
let mut max_err = 0.0_f64;
for (i, &val) in u.iter().enumerate() {
let x = i as f64 * dx;
let expected = (pi * x).sin();
let err = (val - expected).abs();
if err > max_err {
max_err = err;
}
}
errors.push(max_err);
}
let r1 = errors[0] / errors[1];
let r2 = errors[1] / errors[2];
assert!(
r1 > 3.0 && r1 < 5.0,
"expected O(dx^2) ratio ~ 4, got {:.2}",
r1
);
assert!(
r2 > 3.0 && r2 < 5.0,
"expected O(dx^2) ratio ~ 4, got {:.2}",
r2
);
}
#[test]
fn test_unconditional_stability_large_dt() {
let n = 21;
let dx = 0.05;
let d = 1.0;
let dt = 10.0 * dx * dx / d;
let cn = CrankNicolsonDiffusion::new(dx, d, BoundaryCondition::NeumannZero).unwrap();
let mut u = vec![0.0_f64; n];
u[n / 2] = 1.0;
let source = vec![0.0_f64; n];
for _ in 0..50 {
u = cn.step(&u, &source, dt).expect("step ok");
assert!(
u.iter().all(|x| x.is_finite()),
"CN must remain finite at large dt"
);
}
}
#[test]
fn test_spin_diffusion_length_matches_definition() {
let sd =
SpinDiffusionCrankNicolson::new(0.05, 1.0e-3, 5.0e-4, BoundaryCondition::NeumannZero)
.unwrap();
let expected = (1.0e-3_f64 * 5.0e-4_f64).sqrt();
assert!((sd.spin_diffusion_length() - expected).abs() < 1e-15);
}
#[test]
fn test_spin_flip_sink_decay() {
let dx = 0.05;
let d = 1.0;
let tau = 0.1;
let sd =
SpinDiffusionCrankNicolson::new(dx, d, tau, BoundaryCondition::NeumannZero).unwrap();
let n = 11;
let mut u = vec![1.0_f64; n];
let dt = 0.001;
let n_steps = 50;
for _ in 0..n_steps {
u = sd.step(&u, dt).expect("step ok");
}
let t_total = dt * n_steps as f64;
let expected = (-t_total / tau).exp();
let measured = u[n / 2];
assert!(
(measured - expected).abs() < 1e-3,
"spin-flip decay: got {}, expected {}",
measured,
expected
);
}
#[test]
fn test_evolve_snapshot_count() {
let dx = 0.1;
let cn = CrankNicolsonDiffusion::new(dx, 1.0, BoundaryCondition::NeumannZero).unwrap();
let n = 11;
let u0 = vec![0.0_f64; n];
let history = cn
.evolve(&u0, |_t, _u| vec![1.0_f64; n], 1.0, 0.1)
.expect("evolve ok");
assert_eq!(history.len(), 11);
assert_eq!(history[0], u0);
}
#[test]
fn test_step_rejects_bad_dt() {
let cn = CrankNicolsonDiffusion::new(0.1, 1.0, BoundaryCondition::NeumannZero).unwrap();
let n = 5;
let u = vec![0.0_f64; n];
let s = vec![0.0_f64; n];
assert!(cn.step(&u, &s, 0.0).is_err());
assert!(cn.step(&u, &s, -1.0).is_err());
assert!(cn.step(&u, &s, f64::NAN).is_err());
}
}