use crate::constants::MU_0;
use crate::error::{invalid_param, Result};
use crate::material::Ferromagnet;
use crate::micromagnetics::grid::{GridConfig, MicromagneticGrid};
use crate::validation::experimental::ValidationResult;
use crate::vector3::Vector3;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum StableState {
Flower,
Vortex,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Sp3Config {
pub material: Ferromagnet,
pub cell_size: f64,
pub relax_steps: usize,
pub relax_dt: f64,
}
impl Default for Sp3Config {
fn default() -> Self {
let mut mat = Ferromagnet::permalloy();
mat.alpha = 0.5;
mat.anisotropy_k = 0.0;
Self {
material: mat,
cell_size: 6e-9, relax_steps: 500, relax_dt: 1e-12, }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Sp3Result {
pub l_over_lex: f64,
pub flower_energy: f64,
pub vortex_energy: f64,
pub stable_state: StableState,
pub exchange_length_m: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StandardProblem3 {
pub config: Sp3Config,
}
impl StandardProblem3 {
pub fn new(config: Sp3Config) -> Self {
Self { config }
}
pub fn new_default() -> Self {
Self::new(Sp3Config::default())
}
pub fn exchange_length(&self) -> f64 {
let a = self.config.material.exchange_a;
let ms = self.config.material.ms;
let denom = 0.5 * MU_0 * ms * ms;
(a / denom).sqrt()
}
pub fn critical_l_over_lex_theoretical() -> f64 {
8.47
}
fn relax_cube(&self, l_m: f64, use_vortex: bool) -> Result<f64> {
let n_cells_float = (l_m / self.config.cell_size).round();
let n = (n_cells_float as usize).max(2);
let d = l_m / n as f64;
let cfg = GridConfig {
dx: d,
dy: d,
dz: d,
nx: n,
ny: n,
nz: n,
dt: self.config.relax_dt,
n_steps: self.config.relax_steps,
record_every: self.config.relax_steps + 1,
};
let mut grid = MicromagneticGrid::new(cfg, self.config.material.clone(), Vector3::zero())?;
if use_vortex {
grid.set_vortex();
} else {
grid.set_uniform(Vector3::new(0.0, 0.0, 1.0));
}
let _result = grid.run();
Ok(grid.total_energy())
}
pub fn run_for_l(&self, l_m: f64) -> Result<Sp3Result> {
if l_m <= 0.0 {
return Err(invalid_param("l_m", "cube edge length must be positive"));
}
let lex = self.exchange_length();
let l_over_lex = l_m / lex;
let flower_energy = self.relax_cube(l_m, false)?;
let vortex_energy = self.relax_cube(l_m, true)?;
let stable_state = if flower_energy <= vortex_energy {
StableState::Flower
} else {
StableState::Vortex
};
Ok(Sp3Result {
l_over_lex,
flower_energy,
vortex_energy,
stable_state,
exchange_length_m: lex,
})
}
pub fn validate_single_domain_limit(&self, tolerance: f64) -> Result<ValidationResult> {
let lex = self.exchange_length();
let l_small = 4.0 * lex; let l_large = 12.0 * lex;
let res_small = self.run_for_l(l_small)?;
let res_large = self.run_for_l(l_large)?;
let err_small = compute_phase_error(&res_small, StableState::Flower);
let err_large = compute_phase_error(&res_large, StableState::Vortex);
let errors = [err_small, err_large];
Ok(ValidationResult::new(
"muMAG SP#3 single-domain limit (flower vs vortex, coarse grid)",
&errors,
tolerance,
))
}
}
fn compute_phase_error(result: &Sp3Result, expected: StableState) -> f64 {
let e_flower = result.flower_energy;
let e_vortex = result.vortex_energy;
let e_max = e_flower.abs().max(e_vortex.abs()).max(1e-30);
match expected {
StableState::Flower => {
let diff = e_flower - e_vortex; (diff / e_max).max(0.0)
},
StableState::Vortex => {
let diff = e_vortex - e_flower; (diff / e_max).max(0.0)
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exchange_length_permalloy() {
let sp3 = StandardProblem3::new_default();
let lex = sp3.exchange_length();
assert!(
(lex - 5.69e-9).abs() < 0.6e-9,
"Exchange length {:.3e} m should be ≈ 5.69 nm",
lex
);
}
#[test]
fn test_critical_l_over_lex_theoretical() {
let lc = StandardProblem3::critical_l_over_lex_theoretical();
assert!(
(lc - 8.47).abs() < 1e-9,
"Critical L/l_ex should be 8.47, got {lc}"
);
}
#[test]
fn test_run_for_l_invalid_size() {
let sp3 = StandardProblem3::new_default();
assert!(sp3.run_for_l(-1.0).is_err());
assert!(sp3.run_for_l(0.0).is_err());
}
#[test]
fn test_small_cube_flower_stable() {
let cfg = Sp3Config {
relax_steps: 50,
..Sp3Config::default()
};
let sp3 = StandardProblem3::new(cfg);
let lex = sp3.exchange_length();
let l = 2.0 * lex;
let result = sp3.run_for_l(l).expect("SP3 run should succeed");
assert!(
result.flower_energy.is_finite(),
"flower energy must be finite"
);
assert!(
result.vortex_energy.is_finite(),
"vortex energy must be finite"
);
match result.stable_state {
StableState::Flower => assert!(result.flower_energy <= result.vortex_energy),
StableState::Vortex => assert!(result.vortex_energy < result.flower_energy),
}
}
#[test]
fn test_large_cube_vortex_stable() {
let cfg = Sp3Config {
relax_steps: 25,
..Sp3Config::default()
};
let sp3 = StandardProblem3::new(cfg);
let lex = sp3.exchange_length();
let l = 14.0 * lex;
let result = sp3.run_for_l(l).expect("SP3 run should succeed");
assert!(
result.flower_energy.is_finite(),
"flower energy must be finite"
);
assert!(
result.vortex_energy.is_finite(),
"vortex energy must be finite"
);
match result.stable_state {
StableState::Flower => {
assert!(result.flower_energy <= result.vortex_energy);
},
StableState::Vortex => {
assert!(result.vortex_energy < result.flower_energy);
},
}
}
#[test]
fn test_stable_state_consistent_with_energies() {
let cfg = Sp3Config {
relax_steps: 20,
..Sp3Config::default()
};
let sp3 = StandardProblem3::new(cfg);
let lex = sp3.exchange_length();
let result = sp3.run_for_l(3.0 * lex).expect("run");
match result.stable_state {
StableState::Flower => {
assert!(
result.flower_energy <= result.vortex_energy,
"stable_state = Flower but flower_energy > vortex_energy"
);
},
StableState::Vortex => {
assert!(
result.vortex_energy < result.flower_energy,
"stable_state = Vortex but vortex_energy >= flower_energy"
);
},
}
}
#[test]
fn test_sp3_result_exchange_length_field() {
let cfg = Sp3Config {
relax_steps: 10,
..Sp3Config::default()
};
let sp3 = StandardProblem3::new(cfg);
let lex_standalone = sp3.exchange_length();
let lex_trial = sp3.exchange_length(); assert!((lex_standalone - lex_trial).abs() < 1e-30);
let result = sp3.run_for_l(3.0 * lex_standalone).expect("run");
assert!(
(result.exchange_length_m - lex_standalone).abs() < 1e-30,
"exchange_length_m field should match standalone calculation"
);
}
#[test]
fn test_compute_phase_error_logic() {
let result_flower_wins = Sp3Result {
l_over_lex: 4.0,
flower_energy: -1.0e-18, vortex_energy: -0.5e-18, stable_state: StableState::Flower,
exchange_length_m: 5.7e-9,
};
let err = compute_phase_error(&result_flower_wins, StableState::Flower);
assert!(
err < 1e-10,
"No penalty when flower wins as expected, got {err:.3e}"
);
let err2 = compute_phase_error(&result_flower_wins, StableState::Vortex);
assert!(
err2 > 0.0,
"Penalty should be positive when wrong state wins, got {err2:.3e}"
);
}
}