use std::f64::consts::PI;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HopfionDynamicsConfig {
pub exchange_a: f64,
pub dmi_d: f64,
pub alpha: f64,
pub h_ext: Vector3<f64>,
pub ms: f64,
pub dt: f64,
pub dx: f64,
}
impl HopfionDynamicsConfig {
pub fn new(
exchange_a: f64,
dmi_d: f64,
alpha: f64,
h_ext: Vector3<f64>,
ms: f64,
dt: f64,
dx: f64,
) -> Self {
Self {
exchange_a,
dmi_d,
alpha,
h_ext,
ms,
dt,
dx,
}
}
pub fn validate(&self) -> Result<(), Error> {
if self.ms <= 0.0 || !self.ms.is_finite() {
return Err(Error::InvalidParameter {
param: "ms".to_string(),
reason: "Saturation magnetization must be positive and finite".to_string(),
});
}
if self.dt <= 0.0 || !self.dt.is_finite() {
return Err(Error::InvalidParameter {
param: "dt".to_string(),
reason: "Time step must be positive and finite".to_string(),
});
}
if self.dx <= 0.0 || !self.dx.is_finite() {
return Err(Error::InvalidParameter {
param: "dx".to_string(),
reason: "Grid spacing must be positive and finite".to_string(),
});
}
if self.alpha < 0.0 || !self.alpha.is_finite() {
return Err(Error::InvalidParameter {
param: "alpha".to_string(),
reason: "Gilbert damping must be non-negative and finite".to_string(),
});
}
Ok(())
}
}
#[derive(Debug)]
pub struct HopfionDynamicsSolver {
config: HopfionDynamicsConfig,
gamma: f64,
}
impl HopfionDynamicsSolver {
pub const GAMMA_DEFAULT: f64 = 1.760_859_644e11;
const MU0: f64 = 4.0 * PI * 1.0e-7;
pub fn new(config: HopfionDynamicsConfig) -> Result<Self, Error> {
config.validate()?;
Ok(Self {
config,
gamma: Self::GAMMA_DEFAULT,
})
}
pub fn run(
&self,
mut grid: Vec<Vec<Vec<Vector3<f64>>>>,
num_steps: usize,
record_every: usize,
) -> Result<HopfionDynamicsResult, Error> {
let nx = grid.len();
if nx == 0 {
return Err(Error::InvalidParameter {
param: "grid".to_string(),
reason: "Grid must be non-empty (nx > 0)".to_string(),
});
}
let ny = grid[0].len();
if ny == 0 {
return Err(Error::InvalidParameter {
param: "grid".to_string(),
reason: "Grid must be non-empty (ny > 0)".to_string(),
});
}
let nz = grid[0][0].len();
if nz == 0 {
return Err(Error::InvalidParameter {
param: "grid".to_string(),
reason: "Grid must be non-empty (nz > 0)".to_string(),
});
}
let safe_record_every = if record_every == 0 { 1 } else { record_every };
let capacity = num_steps / safe_record_every + 1;
let mut hopf_history = Vec::with_capacity(capacity);
let mut energy_history = Vec::with_capacity(capacity);
let mut time_vec = Vec::with_capacity(capacity);
let q0 = self.compute_hopf_invariant(&grid, nx, ny, nz);
let e0 = self.total_energy(&grid, nx, ny, nz);
hopf_history.push(q0);
energy_history.push(e0);
time_vec.push(0.0);
for step in 1..=num_steps {
let h_eff = self.compute_heff(&grid, nx, ny, nz);
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
let m_new = self.llg_rk4_step(&grid[ix][iy][iz], &h_eff[ix][iy][iz])?;
grid[ix][iy][iz] = m_new.normalize();
}
}
}
if step % safe_record_every == 0 {
let q = self.compute_hopf_invariant(&grid, nx, ny, nz);
let e = self.total_energy(&grid, nx, ny, nz);
let t = step as f64 * self.config.dt;
hopf_history.push(q);
energy_history.push(e);
time_vec.push(t);
}
}
Ok(HopfionDynamicsResult {
hopf_invariant_history: hopf_history,
energy_history,
time: time_vec,
})
}
fn compute_heff(
&self,
grid: &[Vec<Vec<Vector3<f64>>>],
nx: usize,
ny: usize,
nz: usize,
) -> Vec<Vec<Vec<Vector3<f64>>>> {
let mut h_eff = vec![vec![vec![Vector3::zero(); nz]; ny]; nx];
#[allow(clippy::needless_range_loop)]
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
let h_ex = self.exchange_field(grid, ix, iy, iz, nx, ny, nz);
let h_dmi = self.dmi_field(grid, ix, iy, iz, nx, ny, nz);
h_eff[ix][iy][iz] = h_ex + h_dmi + self.config.h_ext;
}
}
}
h_eff
}
fn exchange_field(
&self,
grid: &[Vec<Vec<Vector3<f64>>>],
ix: usize,
iy: usize,
iz: usize,
nx: usize,
ny: usize,
nz: usize,
) -> Vector3<f64> {
let dx2 = self.config.dx * self.config.dx;
let prefactor = 2.0 * self.config.exchange_a / (Self::MU0 * self.config.ms * dx2);
let m_center = grid[ix][iy][iz];
let ixp = (ix + 1) % nx;
let ixm = (ix + nx - 1) % nx;
let iyp = (iy + 1) % ny;
let iym = (iy + ny - 1) % ny;
let izp = (iz + 1) % nz;
let izm = (iz + nz - 1) % nz;
let laplacian = grid[ixp][iy][iz]
+ grid[ixm][iy][iz]
+ grid[ix][iyp][iz]
+ grid[ix][iym][iz]
+ grid[ix][iy][izp]
+ grid[ix][iy][izm]
+ m_center * (-6.0);
laplacian * prefactor
}
fn dmi_field(
&self,
grid: &[Vec<Vec<Vector3<f64>>>],
ix: usize,
iy: usize,
iz: usize,
nx: usize,
ny: usize,
nz: usize,
) -> Vector3<f64> {
let two_dx = 2.0 * self.config.dx;
let prefactor = 2.0 * self.config.dmi_d / (Self::MU0 * self.config.ms * two_dx);
let ixp = (ix + 1) % nx;
let ixm = (ix + nx - 1) % nx;
let iyp = (iy + 1) % ny;
let iym = (iy + ny - 1) % ny;
let izp = (iz + 1) % nz;
let izm = (iz + nz - 1) % nz;
let mxp = grid[ixp][iy][iz];
let mxm = grid[ixm][iy][iz];
let myp = grid[ix][iyp][iz];
let mym = grid[ix][iym][iz];
let mzp = grid[ix][iy][izp];
let mzm = grid[ix][iy][izm];
let dmz_dy = myp.z - mym.z;
let dmy_dz = mzp.y - mzm.y;
let dmx_dz = mzp.x - mzm.x;
let dmz_dx = mxp.z - mxm.z;
let dmy_dx = mxp.y - mxm.y;
let dmx_dy = myp.x - mym.x;
Vector3::new(
(dmz_dy - dmy_dz) * prefactor,
(dmx_dz - dmz_dx) * prefactor,
(dmy_dx - dmx_dy) * prefactor,
)
}
fn llg_rk4_step(&self, m: &Vector3<f64>, h_eff: &Vector3<f64>) -> Result<Vector3<f64>, Error> {
let dt = self.config.dt;
let k1 = self.llg_dm_dt(m, h_eff);
let m1 = (*m + k1 * (dt * 0.5)).normalize();
let k2 = self.llg_dm_dt(&m1, h_eff);
let m2 = (*m + k2 * (dt * 0.5)).normalize();
let k3 = self.llg_dm_dt(&m2, h_eff);
let m3 = (*m + k3 * dt).normalize();
let k4 = self.llg_dm_dt(&m3, h_eff);
let m_new = *m + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
if !m_new.x.is_finite() || !m_new.y.is_finite() || !m_new.z.is_finite() {
return Err(Error::NumericalError {
description: "LLG RK4 step produced non-finite magnetization".to_string(),
});
}
Ok(m_new)
}
#[inline]
fn llg_dm_dt(&self, m: &Vector3<f64>, h_eff: &Vector3<f64>) -> Vector3<f64> {
let alpha = self.config.alpha;
let coeff = -self.gamma / (1.0 + alpha * alpha);
let m_cross_h = m.cross(h_eff);
let m_cross_m_cross_h = m.cross(&m_cross_h);
(m_cross_h + m_cross_m_cross_h * alpha) * coeff
}
pub fn total_energy(
&self,
grid: &[Vec<Vec<Vector3<f64>>>],
nx: usize,
ny: usize,
nz: usize,
) -> f64 {
let dv = self.config.dx.powi(3);
let mu0_ms = Self::MU0 * self.config.ms;
let mut e_exchange = 0.0;
let mut e_zeeman = 0.0;
let mut e_dmi = 0.0;
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
let m = grid[ix][iy][iz];
let h_ex = self.exchange_field(grid, ix, iy, iz, nx, ny, nz);
e_exchange += m.dot(&h_ex);
e_zeeman += m.dot(&self.config.h_ext);
let h_dmi = self.dmi_field(grid, ix, iy, iz, nx, ny, nz);
e_dmi += m.dot(&h_dmi);
}
}
}
let exchange_energy = -0.5 * mu0_ms * e_exchange * dv;
let zeeman_energy = -mu0_ms * e_zeeman * dv;
let dmi_energy = 0.5 * mu0_ms * e_dmi * dv;
exchange_energy + zeeman_energy + dmi_energy
}
pub fn compute_hopf_invariant(
&self,
grid: &[Vec<Vec<Vector3<f64>>>],
nx: usize,
ny: usize,
nz: usize,
) -> f64 {
if nx < 4 || ny < 4 || nz < 4 {
return 0.0;
}
let dx = self.config.dx;
let dv = dx * dx * dx;
let mut total = 0.0;
for ix in 1..nx - 1 {
for iy in 1..ny - 1 {
for iz in 1..nz - 1 {
let berry_a = Self::berry_connection_at_grid(grid, ix, iy, iz, nx, ny, nz, dx);
let a_xp = Self::berry_connection_at_grid(grid, ix + 1, iy, iz, nx, ny, nz, dx);
let a_xm = Self::berry_connection_at_grid(grid, ix - 1, iy, iz, nx, ny, nz, dx);
let a_yp = Self::berry_connection_at_grid(grid, ix, iy + 1, iz, nx, ny, nz, dx);
let a_ym = Self::berry_connection_at_grid(grid, ix, iy - 1, iz, nx, ny, nz, dx);
let a_zp = Self::berry_connection_at_grid(grid, ix, iy, iz + 1, nx, ny, nz, dx);
let a_zm = Self::berry_connection_at_grid(grid, ix, iy, iz - 1, nx, ny, nz, dx);
let inv_2dx = 0.5 / dx;
let curl_a = Vector3::new(
(a_yp.z - a_ym.z - a_zp.y + a_zm.y) * inv_2dx,
(a_zp.x - a_zm.x - a_xp.z + a_xm.z) * inv_2dx,
(a_xp.y - a_xm.y - a_yp.x + a_ym.x) * inv_2dx,
);
total += berry_a.dot(&curl_a) * dv;
}
}
}
total / (4.0 * PI * PI)
}
fn berry_connection_at_grid(
grid: &[Vec<Vec<Vector3<f64>>>],
ix: usize,
iy: usize,
iz: usize,
nx: usize,
ny: usize,
nz: usize,
dx: f64,
) -> Vector3<f64> {
let ix = ix.min(nx - 1);
let iy = iy.min(ny - 1);
let iz = iz.min(nz - 1);
let ix_lo = if ix > 0 { ix - 1 } else { ix };
let ix_hi = if ix + 1 < nx { ix + 1 } else { ix };
let iy_lo = if iy > 0 { iy - 1 } else { iy };
let iy_hi = if iy + 1 < ny { iy + 1 } else { iy };
let iz_lo = if iz > 0 { iz - 1 } else { iz };
let iz_hi = if iz + 1 < nz { iz + 1 } else { iz };
let m = grid[ix][iy][iz];
let dx_eff = (ix_hi - ix_lo) as f64 * dx;
let dy_eff = (iy_hi - iy_lo) as f64 * dx;
let dz_eff = (iz_hi - iz_lo) as f64 * dx;
let dm_dx = (grid[ix_hi][iy][iz] - grid[ix_lo][iy][iz]) * (1.0 / dx_eff.max(dx));
let dm_dy = (grid[ix][iy_hi][iz] - grid[ix][iy_lo][iz]) * (1.0 / dy_eff.max(dx));
let dm_dz = (grid[ix][iy][iz_hi] - grid[ix][iy][iz_lo]) * (1.0 / dz_eff.max(dx));
Vector3::new(
m.dot(&dm_dy.cross(&dm_dz)),
m.dot(&dm_dz.cross(&dm_dx)),
m.dot(&dm_dx.cross(&dm_dy)),
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HopfionDynamicsResult {
pub hopf_invariant_history: Vec<f64>,
pub energy_history: Vec<f64>,
pub time: Vec<f64>,
}
pub fn uniform_grid(nx: usize, ny: usize, nz: usize) -> Vec<Vec<Vec<Vector3<f64>>>> {
vec![vec![vec![Vector3::unit_z(); nz]; ny]; nx]
}
#[cfg(test)]
mod tests {
use super::*;
fn default_config() -> HopfionDynamicsConfig {
HopfionDynamicsConfig::new(
1.0e-11, 1.0e-3, 0.1, Vector3::new(0.0, 0.0, 0.1), 8.0e5, 1.0e-14, 5.0e-10, )
}
fn small_solver() -> HopfionDynamicsSolver {
HopfionDynamicsSolver::new(default_config()).expect("default config must be valid")
}
#[test]
fn test_config_validation_valid() {
let cfg = default_config();
assert!(
cfg.validate().is_ok(),
"Valid config should pass validation"
);
}
#[test]
fn test_config_validation_invalid_ms() {
let mut cfg = default_config();
cfg.ms = -1.0;
assert!(
cfg.validate().is_err(),
"Negative ms should fail validation"
);
cfg.ms = 0.0;
assert!(cfg.validate().is_err(), "Zero ms should fail validation");
cfg.ms = f64::INFINITY;
assert!(
cfg.validate().is_err(),
"Infinite ms should fail validation"
);
}
#[test]
fn test_uniform_exchange_field_is_zero() {
let solver = small_solver();
let nx = 6;
let ny = 6;
let nz = 6;
let grid = uniform_grid(nx, ny, nz);
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
let h_ex = solver.exchange_field(&grid, ix, iy, iz, nx, ny, nz);
assert!(
h_ex.magnitude() < 1e-30,
"Exchange field must vanish on uniform grid, got {:?}",
h_ex
);
}
}
}
}
#[test]
fn test_known_gradient_exchange_field() {
let mut cfg = default_config();
cfg.exchange_a = 1.0e-11;
let solver = HopfionDynamicsSolver::new(cfg.clone()).expect("valid config");
let nx = 5;
let ny = 5;
let nz = 5;
let mut grid = uniform_grid(nx, ny, nz);
grid[2][2][2] = Vector3::unit_x();
let h_ex = solver.exchange_field(&grid, 2, 2, 2, nx, ny, nz);
let expected_mx =
-6.0 / (cfg.dx * cfg.dx) * 2.0 * cfg.exchange_a / (HopfionDynamicsSolver::MU0 * cfg.ms);
assert!(
(h_ex.x - expected_mx).abs() < expected_mx.abs() * 1e-10,
"H_ex.x = {} expected = {}",
h_ex.x,
expected_mx
);
let expected_mz =
6.0 / (cfg.dx * cfg.dx) * 2.0 * cfg.exchange_a / (HopfionDynamicsSolver::MU0 * cfg.ms);
assert!(
(h_ex.z - expected_mz).abs() < expected_mz.abs() * 1e-10,
"H_ex.z = {} expected = {}",
h_ex.z,
expected_mz
);
}
#[test]
fn test_zeeman_only_heff() {
let h_target = Vector3::new(0.3, -0.1, 0.5);
let cfg = HopfionDynamicsConfig::new(0.0, 0.0, 0.1, h_target, 8.0e5, 1.0e-14, 5.0e-10);
let solver = HopfionDynamicsSolver::new(cfg).expect("valid config");
let nx = 4;
let ny = 4;
let nz = 4;
let grid = uniform_grid(nx, ny, nz);
let h_eff = solver.compute_heff(&grid, nx, ny, nz);
for row in &h_eff {
for col in row {
for h in col {
let diff = (*h - h_target).magnitude();
assert!(
diff < 1e-20,
"H_eff should equal h_ext when A=D=0, diff={}",
diff
);
}
}
}
}
#[test]
fn test_llg_rk4_magnitude_preserved() {
let solver = small_solver();
let m = Vector3::new(
1.0_f64 / 3.0_f64.sqrt(),
1.0_f64 / 3.0_f64.sqrt(),
1.0_f64 / 3.0_f64.sqrt(),
);
let h = Vector3::new(0.0, 0.0, 0.05);
let m_new = solver.llg_rk4_step(&m, &h).expect("RK4 step must succeed");
let m_norm = m_new.normalize();
assert!(
(m_norm.magnitude() - 1.0).abs() < 1e-10,
"Normalised spin magnitude = {}",
m_norm.magnitude()
);
}
#[test]
fn test_zero_damping_energy_conservation() {
let cfg = HopfionDynamicsConfig::new(
1.0e-11,
0.0,
0.0, Vector3::new(0.0, 0.0, 0.05),
8.0e5,
5.0e-15, 5.0e-10,
);
let solver = HopfionDynamicsSolver::new(cfg).expect("valid config");
let nx = 4;
let ny = 4;
let nz = 4;
let grid = uniform_grid(nx, ny, nz);
let result = solver.run(grid, 20, 10).expect("run must succeed");
let e0 = result.energy_history[0];
for &e in &result.energy_history {
let rel = (e - e0).abs() / (e0.abs() + 1e-30);
assert!(
rel < 0.01,
"Energy conservation violated: e0={:.6e}, e={:.6e}, rel={:.4e}",
e0,
e,
rel
);
}
}
#[test]
fn test_uniform_state_hopf_invariant_zero() {
let solver = small_solver();
let nx = 6;
let ny = 6;
let nz = 6;
let grid = uniform_grid(nx, ny, nz);
let q = solver.compute_hopf_invariant(&grid, nx, ny, nz);
assert!(
q.abs() < 1e-10,
"Uniform state should have Q_H = 0, got {}",
q
);
}
#[test]
fn test_run_history_length_correct() {
let solver = small_solver();
let nx = 4;
let ny = 4;
let nz = 4;
let grid = uniform_grid(nx, ny, nz);
let num_steps = 10;
let record_every = 5;
let result = solver
.run(grid, num_steps, record_every)
.expect("run must succeed");
let expected = 1 + num_steps / record_every;
assert_eq!(
result.hopf_invariant_history.len(),
expected,
"Hopf history length mismatch"
);
assert_eq!(
result.energy_history.len(),
expected,
"Energy history length mismatch"
);
assert_eq!(result.time.len(), expected, "Time history length mismatch");
}
#[test]
fn test_energy_is_finite() {
let solver = small_solver();
let nx = 4;
let ny = 4;
let nz = 4;
let grid = uniform_grid(nx, ny, nz);
let e = solver.total_energy(&grid, nx, ny, nz);
assert!(e.is_finite(), "Total energy must be finite, got {}", e);
}
#[test]
fn test_periodic_bc_index_wrapping() {
let solver = small_solver();
let nx = 4;
let ny = 4;
let nz = 4;
let grid = uniform_grid(nx, ny, nz);
for &ix in &[0, nx - 1] {
for &iy in &[0, ny - 1] {
for &iz in &[0, nz - 1] {
let h = solver.exchange_field(&grid, ix, iy, iz, nx, ny, nz);
assert!(
h.magnitude() < 1e-30,
"Exchange field at corner ({},{},{}) must be 0 on uniform grid",
ix,
iy,
iz
);
}
}
}
}
#[test]
fn test_history_lengths_consistent() {
let solver = small_solver();
let grid = uniform_grid(4, 4, 4);
let result = solver.run(grid, 8, 4).expect("run must succeed");
let n = result.hopf_invariant_history.len();
assert_eq!(result.energy_history.len(), n);
assert_eq!(result.time.len(), n);
}
#[test]
#[allow(clippy::needless_range_loop)]
fn test_external_field_alignment() {
let h_z = Vector3::new(0.0, 0.0, 1.0e3); let cfg = HopfionDynamicsConfig::new(
0.0, 0.0, 0.99, h_z, 8.0e5, 1.0e-13, 5.0e-10,
);
let solver = HopfionDynamicsSolver::new(cfg).expect("valid config");
let nx = 4;
let ny = 4;
let nz = 4;
let mut grid = vec![vec![vec![Vector3::zero(); nz]; ny]; nx];
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
grid[ix][iy][iz] = Vector3::new(0.5, 0.5, 0.5_f64.sqrt()).normalize();
}
}
}
let result = solver.run(grid, 500, 500).expect("run must succeed");
let e_init = result.energy_history[0];
let e_final = *result.energy_history.last().expect("non-empty");
assert!(
e_final <= e_init + 1e-30,
"Energy should decrease (or stay same) during damped relaxation: \
e_init={:.6e}, e_final={:.6e}",
e_init,
e_final
);
}
#[test]
fn test_time_step_spacing() {
let cfg = default_config();
let dt = cfg.dt;
let solver = HopfionDynamicsSolver::new(cfg).expect("valid config");
let grid = uniform_grid(4, 4, 4);
let num_steps = 9;
let record_every = 3;
let result = solver
.run(grid, num_steps, record_every)
.expect("run must succeed");
for (i, &t) in result.time.iter().enumerate() {
let expected = i as f64 * record_every as f64 * dt;
assert!(
(t - expected).abs() < expected.abs() * 1e-10 + 1e-30,
"Time[{}] = {:.6e}, expected {:.6e}",
i,
t,
expected
);
}
}
#[test]
fn test_small_grid_no_panic() {
let solver = small_solver();
let grid = uniform_grid(2, 2, 2);
let result = solver.run(grid, 2, 1).expect("2×2×2 run must succeed");
for &q in &result.hopf_invariant_history {
assert_eq!(q, 0.0, "Q_H on 2×2×2 must be 0 (small-grid bypass)");
}
for &e in &result.energy_history {
assert!(e.is_finite(), "Energy must be finite");
}
}
}