use crate::error::{invalid_param, Result};
use crate::stochastic::thermal::ThermalField;
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
pub struct HeunAdaptive {
pub current_dt: f64,
pub atol: f64,
pub rtol: f64,
pub safety: f64,
pub dt_min: f64,
pub dt_max: f64,
pub max_factor: f64,
pub min_factor: f64,
pub gamma: f64,
pub alpha: f64,
pub max_retries: usize,
}
impl HeunAdaptive {
pub fn new(initial_dt: f64, alpha: f64) -> Result<Self> {
if !initial_dt.is_finite() || initial_dt <= 0.0 {
return Err(invalid_param(
"initial_dt",
"must be a positive, finite number",
));
}
if !alpha.is_finite() || alpha < 0.0 {
return Err(invalid_param(
"alpha",
"Gilbert damping must be non-negative and finite",
));
}
Ok(Self {
current_dt: initial_dt,
atol: 1.0e-6,
rtol: 1.0e-4,
safety: 0.9,
dt_min: 1.0e-20,
dt_max: 1.0e-6,
max_factor: 5.0,
min_factor: 0.2,
gamma: crate::constants::GAMMA,
alpha,
max_retries: 20,
})
}
pub fn with_tolerances(mut self, atol: f64, rtol: f64) -> Self {
self.atol = atol;
self.rtol = rtol;
self
}
pub fn with_dt_bounds(mut self, dt_min: f64, dt_max: f64) -> Self {
self.dt_min = dt_min;
self.dt_max = dt_max;
self
}
pub fn with_safety(mut self, safety: f64) -> Self {
self.safety = safety;
self
}
pub fn with_max_retries(mut self, n: usize) -> Self {
self.max_retries = n;
self
}
pub fn with_gamma(mut self, gamma: f64) -> Self {
self.gamma = gamma;
self
}
pub fn step<F>(
&mut self,
m: Vector3<f64>,
h_eff_fn: F,
thermal: &mut ThermalField,
) -> Result<(Vector3<f64>, f64, f64)>
where
F: Fn(Vector3<f64>) -> Vector3<f64>,
{
let dt_for_noise = self.current_dt;
let h_thermal_unit_scaled = if dt_for_noise > 0.0 {
let h_raw = thermal.generate(dt_for_noise);
h_raw * dt_for_noise.sqrt()
} else {
Vector3::zero()
};
let mut current_dt = self
.current_dt
.clamp(self.dt_min.max(0.0), self.dt_max.max(self.dt_min));
for _ in 0..self.max_retries.max(1) {
let h_thermal_step = if current_dt > 0.0 {
h_thermal_unit_scaled * (1.0 / current_dt.sqrt())
} else {
Vector3::zero()
};
let (m_euler, m_heun) = Self::evolve_one_dt(
m,
&h_eff_fn,
h_thermal_step,
current_dt,
self.gamma,
self.alpha,
);
let err_vec = m_heun - m_euler;
let err = err_vec.magnitude();
let scale = m.magnitude().max(m_heun.magnitude()).max(1.0);
let tol = self.atol + self.rtol * scale;
let factor = if err > 0.0 {
self.safety * (tol / err).sqrt()
} else {
self.max_factor
};
let factor = factor.clamp(self.min_factor, self.max_factor);
if err <= tol {
let dt_next = (current_dt * factor).clamp(self.dt_min, self.dt_max);
self.current_dt = dt_next;
return Ok((m_heun, current_dt, err));
}
let new_dt = (current_dt * factor).clamp(self.dt_min, self.dt_max);
if new_dt >= current_dt {
self.current_dt = self.dt_min;
return Ok((m_heun, current_dt, err));
}
current_dt = new_dt;
}
Err(crate::error::numerical_error(
"HeunAdaptive: exceeded max_retries without meeting tolerance",
))
}
pub fn evolve_one_dt<F>(
m: Vector3<f64>,
h_eff_fn: &F,
h_thermal: Vector3<f64>,
dt: f64,
gamma: f64,
alpha: f64,
) -> (Vector3<f64>, Vector3<f64>)
where
F: Fn(Vector3<f64>) -> Vector3<f64>,
{
let h0 = h_eff_fn(m) + h_thermal;
let k1 = sllg_rhs(m, h0, gamma, alpha);
let m_euler = (m + k1 * dt).normalize();
let h1 = h_eff_fn(m_euler) + h_thermal;
let k2 = sllg_rhs(m_euler, h1, gamma, alpha);
let m_heun = (m + (k1 + k2) * (0.5 * dt)).normalize();
(m_euler, m_heun)
}
}
#[inline]
fn sllg_rhs(m: Vector3<f64>, h: Vector3<f64>, gamma: f64, alpha: f64) -> Vector3<f64> {
let m_cross_h = m.cross(&h);
let damping = m.cross(&m_cross_h) * alpha;
(m_cross_h + damping) * (-gamma / (1.0 + alpha * alpha))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::{GAMMA, KB};
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_construct_and_defaults() {
let h = HeunAdaptive::new(1.0e-13, 0.01).expect("construct ok");
assert!(close(h.current_dt, 1.0e-13, 1e-25));
assert!(close(h.atol, 1.0e-6, 1e-20));
assert!(close(h.rtol, 1.0e-4, 1e-20));
assert!(close(h.safety, 0.9, 1e-15));
assert!(close(h.alpha, 0.01, 1e-15));
assert!(close(h.gamma, GAMMA, 1.0));
assert_eq!(h.max_retries, 20);
}
#[test]
fn test_construct_rejects_bad_args() {
assert!(HeunAdaptive::new(-1.0, 0.01).is_err());
assert!(HeunAdaptive::new(f64::NAN, 0.01).is_err());
assert!(HeunAdaptive::new(0.0, 0.01).is_err());
assert!(HeunAdaptive::new(1.0e-13, -0.01).is_err());
assert!(HeunAdaptive::new(1.0e-13, f64::INFINITY).is_err());
}
#[test]
fn test_tolerance_setters() {
let h = HeunAdaptive::new(1.0e-13, 0.01)
.expect("ok")
.with_tolerances(1.0e-8, 1.0e-6)
.with_dt_bounds(1.0e-18, 1.0e-9)
.with_safety(0.85)
.with_max_retries(50)
.with_gamma(0.5 * GAMMA);
assert!(close(h.atol, 1.0e-8, 1e-20));
assert!(close(h.rtol, 1.0e-6, 1e-20));
assert!(close(h.dt_min, 1.0e-18, 1e-25));
assert!(close(h.dt_max, 1.0e-9, 1e-25));
assert!(close(h.safety, 0.85, 1e-15));
assert_eq!(h.max_retries, 50);
assert!(close(h.gamma, 0.5 * GAMMA, 1.0));
}
#[test]
fn test_zero_temperature_matches_deterministic_heun() {
use crate::dynamics::llg::LlgSolver;
let alpha = 0.05_f64;
let dt = 1.0e-13_f64;
let m0 = Vector3::new(1.0, 0.1, 0.05).normalize();
let h_field = Vector3::new(0.0, 0.0, 1.0e5);
let solver = LlgSolver::new(alpha, dt);
let m_ref = solver.step_heun(m0, |_m| h_field);
let mut integ = HeunAdaptive::new(dt, alpha)
.expect("ok")
.with_tolerances(1.0e-12, 1.0e-12)
.with_dt_bounds(dt, dt); let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let (m_new, dt_used, _err) = integ.step(m0, |_m| h_field, &mut thermal).expect("step ok");
assert!(close(dt_used, dt, 1e-25));
assert!(
(m_new - m_ref).magnitude() < 1e-12,
"adaptive Heun at T=0 must reproduce fixed-step Heun: \
|m_new - m_ref| = {:.3e}",
(m_new - m_ref).magnitude()
);
}
#[test]
fn test_error_scales_with_step_squared() {
let alpha = 0.02_f64;
let m0 = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0);
let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let mut errs = Vec::new();
for &dt in &[5.0e-13_f64, 1.0e-12_f64] {
let mut integ = HeunAdaptive::new(dt, alpha)
.expect("ok")
.with_tolerances(1.0e-30, 1.0e-30) .with_dt_bounds(dt, dt)
.with_max_retries(1);
let (_, _, err) = integ
.step(m0, |_m| h_field, &mut thermal)
.expect("step still returns best-effort");
errs.push(err);
}
let ratio = errs[1] / errs[0].max(1.0e-300);
assert!(
ratio > 2.5 && ratio < 8.0,
"error-vs-dt ratio = {:.3} (expected ~4)",
ratio
);
}
#[test]
fn test_norm_preserved_after_step() {
let alpha = 0.01_f64;
let mut integ = HeunAdaptive::new(1.0e-13, alpha).expect("ok");
let mut thermal = ThermalField::new(300.0, 1.0e-24, 1.0e6, alpha);
let mut m = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e5);
for _ in 0..50 {
let (m_new, _, _) = integ.step(m, |_m| h_field, &mut thermal).expect("step ok");
m = m_new;
assert!(
(m.magnitude() - 1.0).abs() < 1e-10,
"|m| drifted from unit sphere: {}",
m.magnitude()
);
}
}
#[test]
fn test_thermal_norm_stability_at_300k() {
let alpha = 0.1_f64;
let mut integ = HeunAdaptive::new(1.0e-14, alpha)
.expect("ok")
.with_tolerances(1.0e-4, 1.0e-3);
let mut thermal = ThermalField::new(300.0, 1.0e-26, 5.0e5, alpha);
let mut m = Vector3::new(0.0, 0.0, 1.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e4);
let mut mean_norm_sq = 0.0_f64;
let n_steps = 500;
for _ in 0..n_steps {
let (m_new, _, _) = integ.step(m, |_m| h_field, &mut thermal).expect("step ok");
m = m_new;
mean_norm_sq += m.magnitude_squared();
assert!(m.magnitude().is_finite());
}
mean_norm_sq /= n_steps as f64;
assert!(
(mean_norm_sq - 1.0).abs() < 1e-8,
"⟨|m|²⟩ = {} should be ≈ 1 (we re-normalize)",
mean_norm_sq
);
}
#[test]
fn test_dt_grows_when_error_small() {
let alpha = 0.01_f64;
let dt0 = 1.0e-16_f64;
let mut integ = HeunAdaptive::new(dt0, alpha)
.expect("ok")
.with_tolerances(1.0e-3, 1.0e-3) .with_dt_bounds(1.0e-20, 1.0e-9);
let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let m = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e4);
let (_, dt_used, _) = integ.step(m, |_m| h_field, &mut thermal).expect("step ok");
assert!(close(dt_used, dt0, 1e-25));
assert!(
integ.current_dt > dt0,
"dt did not grow (was {}, now {})",
dt0,
integ.current_dt
);
}
#[test]
fn test_dt_shrinks_when_error_large() {
let alpha = 0.5_f64; let dt0 = 1.0e-9_f64; let mut integ = HeunAdaptive::new(dt0, alpha)
.expect("ok")
.with_tolerances(1.0e-12, 1.0e-12)
.with_dt_bounds(1.0e-20, 1.0e-9)
.with_max_retries(30);
let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let m = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e6);
let result = integ.step(m, |_m| h_field, &mut thermal);
match result {
Ok((_, dt_used, _)) => {
assert!(
dt_used < dt0,
"dt should have shrunk from {}, got {}",
dt0,
dt_used
);
},
Err(_) => {
assert!(integ.current_dt <= dt0);
},
}
}
#[test]
fn test_small_alpha_precession() {
let alpha = 1.0e-5_f64;
let dt = 1.0e-13_f64;
let m0 = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e4);
let mut integ = HeunAdaptive::new(dt, alpha)
.expect("ok")
.with_dt_bounds(dt, dt);
let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let mut m = m0;
for _ in 0..100 {
let (m_new, _, _) = integ.step(m, |_m| h_field, &mut thermal).expect("step ok");
m = m_new;
}
assert!(m.magnitude_squared().is_finite());
assert!((m.magnitude() - 1.0).abs() < 1e-8);
}
#[test]
fn test_reject_retry_terminates() {
let alpha = 0.05_f64;
let dt0 = 1.0e-10_f64; let mut integ = HeunAdaptive::new(dt0, alpha)
.expect("ok")
.with_tolerances(1.0e-10, 1.0e-10)
.with_dt_bounds(1.0e-18, 1.0e-9)
.with_max_retries(10);
let mut thermal = ThermalField::new(0.0, 1.0e-24, 1.0e6, alpha);
let m = Vector3::new(1.0, 0.0, 0.0);
let h_field = Vector3::new(0.0, 0.0, 1.0e5);
let _ = integ.step(m, |_m| h_field, &mut thermal);
assert!(integ.current_dt.is_finite());
assert!(integ.current_dt > 0.0);
}
#[test]
fn test_sllg_rhs_orthogonality() {
let m = Vector3::new(0.7, 0.1, -0.5).normalize();
let h = Vector3::new(1.0e4, -2.0e3, 5.0e2);
let dm = sllg_rhs(m, h, GAMMA, 0.05);
let rel = m.dot(&dm).abs() / dm.magnitude().max(1.0);
assert!(
rel < 1.0e-12,
"dm/dt not perpendicular to m (relative): {:.3e}",
rel
);
}
#[test]
fn test_uses_kb_for_thermal() {
assert!(close(KB, 1.380_649e-23, 1.0e-30));
}
}