pub mod states;
use std::marker::PhantomData;
use states::{NoField, NoMaterial, NoSolver, WithField, WithMaterial, WithSolver};
use crate::dynamics::{
anisotropy_energy, calc_dm_dt, zeeman_energy, AdaptiveIntegrator, DormandPrince45,
DormandPrince87, ForestRuth, Integrator, LlgSolver, SemiImplicit, Yoshida4,
};
use crate::error::{Error, Result};
use crate::material::Ferromagnet;
use crate::vector3::Vector3;
type RhsFn<'a> = &'a dyn Fn(&[Vector3<f64>], f64) -> Vec<Vector3<f64>>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SolverKind {
Rk4,
Euler,
Heun,
Dp45 {
tolerance: f64,
},
Dp87 {
tolerance: f64,
},
Yoshida4,
ForestRuth,
SemiImplicit {
tolerance: f64,
max_iter: usize,
},
}
pub struct SimulationBuilder<M, F, S> {
material: Option<Ferromagnet>,
external_field: Option<Vector3<f64>>,
initial_magnetization: Option<Vector3<f64>>,
damping: Option<f64>,
temperature: Option<f64>,
dt: f64,
num_steps: usize,
solver_kind: Option<SolverKind>,
_material: PhantomData<M>,
_field: PhantomData<F>,
_solver: PhantomData<S>,
}
impl SimulationBuilder<NoMaterial, NoField, NoSolver> {
pub fn new() -> Self {
Self {
material: None,
external_field: None,
initial_magnetization: None,
damping: None,
temperature: None,
dt: 1.0e-13,
num_steps: 1000,
solver_kind: None,
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
}
impl Default for SimulationBuilder<NoMaterial, NoField, NoSolver> {
fn default() -> Self {
Self::new()
}
}
impl<M, F, S> SimulationBuilder<M, F, S> {
pub fn material(self, mat: Ferromagnet) -> SimulationBuilder<WithMaterial, F, S> {
SimulationBuilder {
material: Some(mat),
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: self.solver_kind,
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn external_field(self, field: Vector3<f64>) -> SimulationBuilder<M, WithField, S> {
SimulationBuilder {
material: self.material,
external_field: Some(field),
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: self.solver_kind,
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_rk4(self) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Rk4),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_euler(self) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Euler),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_heun(self) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Heun),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_dp45(self, tolerance: f64) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Dp45 { tolerance }),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_dp87(self, tolerance: f64) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Dp87 { tolerance }),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_yoshida4(self) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::Yoshida4),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_forest_ruth(self) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::ForestRuth),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
pub fn solver_semi_implicit(
self,
tolerance: f64,
max_iter: usize,
) -> SimulationBuilder<M, F, WithSolver> {
SimulationBuilder {
material: self.material,
external_field: self.external_field,
initial_magnetization: self.initial_magnetization,
damping: self.damping,
temperature: self.temperature,
dt: self.dt,
num_steps: self.num_steps,
solver_kind: Some(SolverKind::SemiImplicit {
tolerance,
max_iter,
}),
_material: PhantomData,
_field: PhantomData,
_solver: PhantomData,
}
}
}
impl<M, F, S> SimulationBuilder<M, F, S> {
pub fn initial_magnetization(mut self, m0: Vector3<f64>) -> Self {
self.initial_magnetization = Some(m0);
self
}
pub fn damping(mut self, alpha: f64) -> Self {
self.damping = Some(alpha);
self
}
pub fn temperature(mut self, temp: f64) -> Self {
self.temperature = Some(temp);
self
}
pub fn time_step(mut self, dt: f64) -> Self {
self.dt = dt;
self
}
pub fn num_steps(mut self, n: usize) -> Self {
self.num_steps = n;
self
}
}
impl SimulationBuilder<WithMaterial, WithField, WithSolver> {
pub fn build(self) -> Result<Simulation> {
if self.dt <= 0.0 {
return Err(Error::InvalidParameter {
param: "dt".to_string(),
reason: "time step must be positive".to_string(),
});
}
if self.num_steps == 0 {
return Err(Error::InvalidParameter {
param: "num_steps".to_string(),
reason: "must have at least one integration step".to_string(),
});
}
let material = self.material.ok_or_else(|| Error::ConfigurationError {
description: "material is required (type-state violated)".to_string(),
})?;
let external_field = self
.external_field
.ok_or_else(|| Error::ConfigurationError {
description: "external field is required (type-state violated)".to_string(),
})?;
let solver_kind = self.solver_kind.ok_or_else(|| Error::ConfigurationError {
description: "solver is required (type-state violated)".to_string(),
})?;
let alpha = self.damping.unwrap_or(material.alpha);
let m0_raw = self.initial_magnetization.unwrap_or(material.easy_axis);
let m0_mag = m0_raw.magnitude();
if m0_mag < 1.0e-30 {
return Err(Error::InvalidParameter {
param: "initial_magnetization".to_string(),
reason: "magnetization vector must be non-zero".to_string(),
});
}
let m0 = m0_raw * (1.0 / m0_mag);
let solver = LlgSolver::new(alpha, self.dt);
let temperature = self.temperature.unwrap_or(0.0);
Ok(Simulation {
material,
external_field,
magnetization: m0,
solver,
solver_kind,
temperature,
dt: self.dt,
num_steps: self.num_steps,
})
}
}
impl SimulationBuilder<NoMaterial, NoField, NoSolver> {
pub fn yig_pt_pumping() -> SimulationBuilder<WithMaterial, WithField, WithSolver> {
SimulationBuilder::<NoMaterial, NoField, NoSolver>::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.initial_magnetization(Vector3::new(1.0, 0.0, 0.0))
.time_step(1.0e-13)
.num_steps(10_000)
}
pub fn skyrmion_dynamics() -> SimulationBuilder<WithMaterial, WithField, WithSolver> {
let skyrmion_material = Ferromagnet {
alpha: 0.01,
ms: 1.0e6,
anisotropy_k: 8.0e5,
easy_axis: Vector3::new(0.0, 0.0, 1.0),
exchange_a: 1.5e-11,
};
SimulationBuilder::<NoMaterial, NoField, NoSolver>::new()
.material(skyrmion_material)
.external_field(Vector3::new(0.0, 0.0, 0.3))
.solver_rk4()
.initial_magnetization(Vector3::new(0.1, 0.1, 1.0))
.time_step(5.0e-14)
.num_steps(20_000)
}
pub fn ferromagnetic_resonance(
material: Ferromagnet,
h_dc: f64,
h_rf: f64,
) -> SimulationBuilder<WithMaterial, WithField, WithSolver> {
let field = Vector3::new(h_rf, 0.0, h_dc);
SimulationBuilder::<NoMaterial, NoField, NoSolver>::new()
.material(material)
.external_field(field)
.solver_rk4()
.initial_magnetization(Vector3::new(1.0, 0.0, 0.0))
.time_step(1.0e-13)
.num_steps(50_000)
}
}
pub struct Simulation {
material: Ferromagnet,
external_field: Vector3<f64>,
magnetization: Vector3<f64>,
solver: LlgSolver,
solver_kind: SolverKind,
#[allow(dead_code)]
temperature: f64,
#[allow(dead_code)]
dt: f64,
num_steps: usize,
}
impl Simulation {
pub fn run(&mut self) -> Result<SimulationResult> {
let mut trajectory = Vec::with_capacity(self.num_steps + 1);
let mut energies = Vec::with_capacity(self.num_steps + 1);
trajectory.push(self.magnetization);
energies.push(self.compute_energy(self.magnetization));
let alpha = self.solver.alpha;
let gamma = self.solver.gamma;
let dt = self.solver.dt;
let h_ext = self.external_field;
let mat_ms = self.material.ms;
let mat_k = self.material.anisotropy_k;
let mat_easy = self.material.easy_axis;
let num_steps = self.num_steps;
let solver_kind = self.solver_kind;
let eff_field = |m_eval: Vector3<f64>| -> Vector3<f64> {
use crate::constants::MU_0;
let projection = m_eval.dot(&mat_easy);
let h_anis = if mat_ms.abs() > 1.0e-30 {
mat_easy * (2.0 * mat_k * projection / (MU_0 * mat_ms))
} else {
Vector3::zero()
};
h_ext + h_anis
};
let llg_rhs = |m_eval: Vector3<f64>| -> Vector3<f64> {
calc_dm_dt(m_eval, eff_field(m_eval), gamma, alpha)
};
match solver_kind {
SolverKind::Dp45 { tolerance } => {
let inner = DormandPrince45::new();
let mut adaptive = AdaptiveIntegrator::new(inner, tolerance, 4.0)
.with_dt_min(1e-20)
.with_dt_max(dt * 10.0);
let mut cur_dt = dt;
for step_idx in 0..num_steps {
let m = self.magnetization;
let state = vec![m];
let rhs: RhsFn<'_> = &|s: &[Vector3<f64>], _t: f64| vec![llg_rhs(s[0])];
let (output, used_dt) = adaptive.adaptive_step(&state, 0.0, cur_dt, rhs)?;
cur_dt = output.suggested_dt.unwrap_or(used_dt);
let new_m = output.new_state.into_iter().next().ok_or_else(|| {
Error::NumericalError {
description: format!(
"DP45: empty output state at step {}",
step_idx + 1
),
}
})?;
Self::check_finite(new_m, trajectory.len())?;
let new_m = Self::renormalize(new_m);
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
SolverKind::Dp87 { tolerance } => {
let inner = DormandPrince87::new();
let mut adaptive = AdaptiveIntegrator::new(inner, tolerance, 7.0)
.with_dt_min(1e-20)
.with_dt_max(dt * 10.0);
let mut cur_dt = dt;
for step_idx in 0..num_steps {
let m = self.magnetization;
let state = vec![m];
let rhs: RhsFn<'_> = &|s: &[Vector3<f64>], _t: f64| vec![llg_rhs(s[0])];
let (output, used_dt) = adaptive.adaptive_step(&state, 0.0, cur_dt, rhs)?;
cur_dt = output.suggested_dt.unwrap_or(used_dt);
let new_m = output.new_state.into_iter().next().ok_or_else(|| {
Error::NumericalError {
description: format!(
"DP87: empty output state at step {}",
step_idx + 1
),
}
})?;
Self::check_finite(new_m, trajectory.len())?;
let new_m = Self::renormalize(new_m);
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
SolverKind::Yoshida4 => {
let mut integrator = Yoshida4::new();
let mut p = llg_rhs(self.magnetization);
for step_idx in 0..num_steps {
let q = self.magnetization;
let state = vec![q, p];
let rhs: RhsFn<'_> = &|s: &[Vector3<f64>], _t: f64| vec![s[1], llg_rhs(s[0])];
let output = integrator.step(&state, 0.0, dt, rhs)?;
let mut it = output.new_state.into_iter();
let new_m = it.next().ok_or_else(|| Error::NumericalError {
description: format!("Yoshida4: missing q at step {}", step_idx + 1),
})?;
p = it.next().unwrap_or_else(|| llg_rhs(new_m));
Self::check_finite(new_m, trajectory.len())?;
let new_m = Self::renormalize(new_m);
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
SolverKind::ForestRuth => {
let mut integrator = ForestRuth::new();
let mut p = llg_rhs(self.magnetization);
for step_idx in 0..num_steps {
let q = self.magnetization;
let state = vec![q, p];
let rhs: RhsFn<'_> = &|s: &[Vector3<f64>], _t: f64| vec![s[1], llg_rhs(s[0])];
let output = integrator.step(&state, 0.0, dt, rhs)?;
let mut it = output.new_state.into_iter();
let new_m = it.next().ok_or_else(|| Error::NumericalError {
description: format!("ForestRuth: missing q at step {}", step_idx + 1),
})?;
p = it.next().unwrap_or_else(|| llg_rhs(new_m));
Self::check_finite(new_m, trajectory.len())?;
let new_m = Self::renormalize(new_m);
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
SolverKind::SemiImplicit {
tolerance,
max_iter,
} => {
let mut integrator = SemiImplicit::new(max_iter, tolerance);
for step_idx in 0..num_steps {
let m = self.magnetization;
let state = vec![m];
let rhs: RhsFn<'_> = &|s: &[Vector3<f64>], _t: f64| vec![llg_rhs(s[0])];
let output = integrator.step(&state, 0.0, dt, rhs)?;
let new_m = output.new_state.into_iter().next().ok_or_else(|| {
Error::NumericalError {
description: format!(
"SemiImplicit: empty output state at step {}",
step_idx + 1
),
}
})?;
Self::check_finite(new_m, trajectory.len())?;
let new_m = Self::renormalize(new_m);
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
_ => {
for _ in 0..num_steps {
let m = self.magnetization;
let new_m = match solver_kind {
SolverKind::Rk4 => self.solver.step_rk4(m, eff_field),
SolverKind::Euler => self.solver.step_euler(m, eff_field(m)),
SolverKind::Heun => self.solver.step_heun(m, eff_field),
_ => {
return Err(Error::ConfigurationError {
description: "solver dispatch error: unexpected variant in \
legacy arm"
.to_string(),
});
},
};
Self::check_finite(new_m, trajectory.len())?;
self.magnetization = new_m;
trajectory.push(new_m);
energies.push(self.compute_energy(new_m));
}
},
}
Ok(SimulationResult {
trajectory,
final_magnetization: self.magnetization,
energies,
})
}
fn check_finite(m: Vector3<f64>, step: usize) -> Result<()> {
if !m.x.is_finite() || !m.y.is_finite() || !m.z.is_finite() {
return Err(Error::NumericalError {
description: format!("magnetization became non-finite at step {} : {:?}", step, m),
});
}
Ok(())
}
fn renormalize(m: Vector3<f64>) -> Vector3<f64> {
let mag = m.magnitude();
if mag > 1.0e-30 {
m * (1.0 / mag)
} else {
m
}
}
fn compute_energy(&self, m: Vector3<f64>) -> f64 {
let e_zeeman = zeeman_energy(m, self.external_field, self.material.ms);
let e_anis = anisotropy_energy(m, self.material.easy_axis, self.material.anisotropy_k);
e_zeeman + e_anis
}
}
#[derive(Debug, Clone)]
pub struct SimulationResult {
pub trajectory: Vec<Vector3<f64>>,
pub final_magnetization: Vector3<f64>,
pub energies: Vec<f64>,
}
impl SimulationResult {
pub fn len(&self) -> usize {
self.trajectory.len()
}
pub fn is_empty(&self) -> bool {
self.trajectory.is_empty()
}
pub fn max_relative_energy_change(&self) -> Option<f64> {
let e0 = *self.energies.first()?;
if e0.abs() < 1.0e-30 {
return self
.energies
.iter()
.map(|e| (e - e0).abs())
.fold(None, |acc, v| {
Some(match acc {
Some(prev) => {
if v > prev {
v
} else {
prev
}
},
None => v,
})
});
}
self.energies
.iter()
.map(|e| ((e - e0) / e0).abs())
.fold(None, |acc, v| {
Some(match acc {
Some(prev) => {
if v > prev {
v
} else {
prev
}
},
None => v,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_with_all_parameters() {
let sim = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.initial_magnetization(Vector3::new(1.0, 0.0, 0.0))
.temperature(300.0)
.time_step(1.0e-13)
.num_steps(500)
.build();
assert!(sim.is_ok(), "build with all parameters should succeed");
}
#[test]
fn test_preset_yig_pt_pumping_builds() {
let sim = SimulationBuilder::yig_pt_pumping().build();
assert!(
sim.is_ok(),
"yig_pt_pumping preset should build successfully"
);
}
#[test]
fn test_preset_skyrmion_dynamics_builds() {
let sim = SimulationBuilder::skyrmion_dynamics().build();
assert!(
sim.is_ok(),
"skyrmion_dynamics preset should build successfully"
);
}
#[test]
fn test_preset_fmr_builds() {
let sim = SimulationBuilder::ferromagnetic_resonance(Ferromagnet::permalloy(), 0.5, 0.001)
.build();
assert!(
sim.is_ok(),
"ferromagnetic_resonance preset should build successfully"
);
}
#[test]
fn test_run_produces_trajectory() {
let mut sim = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.initial_magnetization(Vector3::new(1.0, 0.0, 0.0))
.num_steps(200)
.build()
.expect("builder should succeed");
let result = sim.run().expect("simulation should run without error");
assert_eq!(result.len(), 201);
assert!(!result.is_empty());
let mag = result.final_magnetization.magnitude();
assert!(
(mag - 1.0).abs() < 1.0e-6,
"final magnetization should be normalised, got magnitude {}",
mag
);
}
#[test]
fn test_custom_parameters_override_defaults() {
let custom_dt = 5.0e-14;
let custom_steps = 42;
let custom_alpha = 0.05;
let mut sim = SimulationBuilder::new()
.material(Ferromagnet::yig()) .external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.damping(custom_alpha)
.time_step(custom_dt)
.num_steps(custom_steps)
.build()
.expect("builder with overrides should succeed");
let result = sim.run().expect("simulation should run");
assert_eq!(result.len(), custom_steps + 1);
}
#[test]
fn test_energy_conservation_zero_damping() {
let zero_damping_material = Ferromagnet {
alpha: 0.0,
ms: 8.0e5,
anisotropy_k: 0.0,
easy_axis: Vector3::new(0.0, 0.0, 1.0),
exchange_a: 1.3e-11,
};
let mut sim = SimulationBuilder::new()
.material(zero_damping_material)
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.damping(0.0) .initial_magnetization(Vector3::new(1.0, 0.0, 1.0)) .time_step(1.0e-14) .num_steps(5000)
.build()
.expect("zero-damping builder should succeed");
let result = sim.run().expect("zero-damping simulation should run");
let max_change = result
.max_relative_energy_change()
.expect("should have energy data");
assert!(
max_change < 1.0e-6,
"energy should be conserved for zero damping, max relative change = {:.2e}",
max_change
);
}
#[test]
fn test_invalid_dt_rejected() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.time_step(-1.0)
.build();
assert!(result.is_err(), "negative dt should be rejected");
}
#[test]
fn test_zero_steps_rejected() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.num_steps(0)
.build();
assert!(result.is_err(), "zero num_steps should be rejected");
}
#[test]
fn test_type_state_requires_all_three() {
let _sim = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.build()
.expect("fully configured builder must succeed");
}
#[test]
fn test_builder_dp45_builds_without_panic() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_dp45(1.0e-6)
.num_steps(50)
.time_step(1.0e-13)
.build();
assert!(
result.is_ok(),
"DP45 builder should succeed: {:?}",
result.err()
);
}
#[test]
fn test_builder_dp87_builds_without_panic() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_dp87(1.0e-8)
.num_steps(50)
.time_step(1.0e-13)
.build();
assert!(
result.is_ok(),
"DP87 builder should succeed: {:?}",
result.err()
);
}
#[test]
fn test_builder_yoshida4_builds_without_panic() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_yoshida4()
.num_steps(50)
.time_step(1.0e-13)
.build();
assert!(
result.is_ok(),
"Yoshida4 builder should succeed: {:?}",
result.err()
);
}
#[test]
fn test_builder_forest_ruth_builds_without_panic() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_forest_ruth()
.num_steps(50)
.time_step(1.0e-13)
.build();
assert!(
result.is_ok(),
"ForestRuth builder should succeed: {:?}",
result.err()
);
}
#[test]
fn test_builder_semi_implicit_builds_without_panic() {
let result = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_semi_implicit(1.0e-12, 50)
.num_steps(50)
.time_step(1.0e-13)
.build();
assert!(
result.is_ok(),
"SemiImplicit builder should succeed: {:?}",
result.err()
);
}
#[test]
fn test_dp45_simulation_runs_produces_trajectory() {
let mut sim = SimulationBuilder::new()
.material(Ferromagnet::yig())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_dp45(1.0e-6)
.initial_magnetization(Vector3::new(1.0, 0.0, 0.0))
.num_steps(100)
.time_step(1.0e-13)
.build()
.expect("DP45 builder should succeed");
let result = sim.run().expect("DP45 simulation should run");
assert_eq!(result.len(), 101);
assert!(!result.is_empty());
let mag = result.final_magnetization.magnitude();
assert!(
(mag - 1.0).abs() < 1.0e-6,
"DP45 final |m| should be ~1, got {}",
mag
);
}
#[test]
fn test_dp45_energy_conservation_similar_to_rk4() {
let zero_damping = Ferromagnet {
alpha: 0.0,
ms: 8.0e5,
anisotropy_k: 0.0,
easy_axis: Vector3::new(0.0, 0.0, 1.0),
exchange_a: 1.3e-11,
};
let mut sim_rk4 = SimulationBuilder::new()
.material(zero_damping.clone())
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_rk4()
.damping(0.0)
.initial_magnetization(Vector3::new(1.0, 0.0, 1.0))
.time_step(1.0e-14)
.num_steps(500)
.build()
.expect("RK4 zero-damping builder should succeed");
let rk4_result = sim_rk4.run().expect("RK4 zero-damping sim should run");
let rk4_max_change = rk4_result
.max_relative_energy_change()
.expect("RK4 should have energies");
let mut sim_dp45 = SimulationBuilder::new()
.material(zero_damping)
.external_field(Vector3::new(0.0, 0.0, 0.1))
.solver_dp45(1.0e-8)
.damping(0.0)
.initial_magnetization(Vector3::new(1.0, 0.0, 1.0))
.time_step(1.0e-14)
.num_steps(500)
.build()
.expect("DP45 zero-damping builder should succeed");
let dp45_result = sim_dp45.run().expect("DP45 zero-damping sim should run");
let dp45_max_change = dp45_result
.max_relative_energy_change()
.expect("DP45 should have energies");
assert!(
dp45_max_change < 1.0e-3,
"DP45 energy conservation should be within 0.1%, got {:.2e}",
dp45_max_change
);
assert!(
dp45_max_change <= rk4_max_change * 100.0 + 1.0e-10,
"DP45 energy drift {:.2e} unexpectedly much larger than RK4 {:.2e}",
dp45_max_change,
rk4_max_change
);
}
#[test]
fn test_adaptive_runs_fixed_steps() {
let num_steps = 75;
let mut sim = SimulationBuilder::new()
.material(Ferromagnet::permalloy())
.external_field(Vector3::new(0.0, 0.0, 0.05))
.solver_dp87(1.0e-8)
.initial_magnetization(Vector3::new(0.0, 1.0, 0.0))
.time_step(1.0e-13)
.num_steps(num_steps)
.build()
.expect("DP87 builder should succeed");
let result = sim.run().expect("DP87 simulation should run");
assert_eq!(
result.len(),
num_steps + 1,
"DP87 should produce num_steps + 1 = {} trajectory points, got {}",
num_steps + 1,
result.len()
);
for (i, &m) in result.trajectory.iter().enumerate() {
let mag = m.magnitude();
assert!(
(mag - 1.0).abs() < 1.0e-5,
"trajectory point {} has |m| = {:.6}, expected ~1.0",
i,
mag
);
}
}
}