use crate::constants::MU_0;
use crate::error::{invalid_param, Result};
use crate::material::Ferromagnet;
use crate::micromagnetics::demag::{DemagField, NewellTensor};
use crate::vector3::Vector3;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GridConfig {
pub dx: f64,
pub dy: f64,
pub dz: f64,
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub dt: f64,
pub n_steps: usize,
pub record_every: usize,
}
impl Default for GridConfig {
fn default() -> Self {
Self {
dx: 5e-9,
dy: 5e-9,
dz: 3e-9,
nx: 100,
ny: 25,
nz: 1,
dt: 1e-13,
n_steps: 10_000,
record_every: 1_000,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LlgResult {
pub m_avg: Vec<Vector3<f64>>,
pub times: Vec<f64>,
pub final_state: Vec<Vector3<f64>>,
}
#[derive(Debug, Clone)]
pub struct MicromagneticGrid {
pub config: GridConfig,
pub material: Ferromagnet,
pub magnetization: Vec<Vector3<f64>>,
pub h_ext: Vector3<f64>,
demag: DemagField,
}
impl MicromagneticGrid {
pub fn new(config: GridConfig, material: Ferromagnet, h_ext: Vector3<f64>) -> Result<Self> {
if config.nx == 0 {
return Err(invalid_param("nx", "must be >= 1"));
}
if config.ny == 0 {
return Err(invalid_param("ny", "must be >= 1"));
}
if config.nz == 0 {
return Err(invalid_param("nz", "must be >= 1"));
}
if config.dx <= 0.0 {
return Err(invalid_param("dx", "must be positive"));
}
if config.dy <= 0.0 {
return Err(invalid_param("dy", "must be positive"));
}
if config.dz <= 0.0 {
return Err(invalid_param("dz", "must be positive"));
}
if config.dt <= 0.0 {
return Err(invalid_param("dt", "must be positive"));
}
let n = config.nx * config.ny * config.nz;
let magnetization = vec![Vector3::new(1.0, 0.0, 0.0); n];
let tensor = NewellTensor::new(
config.dx, config.dy, config.dz, config.nx, config.ny, config.nz,
)?;
let demag = DemagField::new(tensor);
Ok(Self {
config,
material,
magnetization,
h_ext,
demag,
})
}
#[inline]
pub fn n_cells(&self) -> usize {
self.config.nx * self.config.ny * self.config.nz
}
#[inline]
fn cell_index(&self, ix: usize, iy: usize, iz: usize) -> usize {
iz * self.config.ny * self.config.nx + iy * self.config.nx + ix
}
pub fn set_uniform(&mut self, direction: Vector3<f64>) {
let mag = direction.magnitude();
if mag < 1e-30 {
return;
}
let d_norm = direction.normalize();
for m in self.magnetization.iter_mut() {
*m = d_norm;
}
}
pub fn set_vortex(&mut self) {
let nx = self.config.nx;
let ny = self.config.ny;
let nz = self.config.nz;
let half_nx = nx as f64 / 2.0;
let half_ny = ny as f64 / 2.0;
let eps = 0.01_f64;
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let x_rel = (ix as f64 + 0.5 - half_nx) / half_nx;
let y_rel = (iy as f64 + 0.5 - half_ny) / half_ny;
let m = Vector3::new(-y_rel, x_rel, eps).normalize();
let idx = self.cell_index(ix, iy, iz);
self.magnetization[idx] = m;
}
}
}
}
pub fn mean_magnetization(&self) -> Vector3<f64> {
let n = self.n_cells();
if n == 0 {
return Vector3::zero();
}
let sum = self.magnetization.iter().fold(Vector3::zero(), |acc, &m| {
Vector3::new(acc.x + m.x, acc.y + m.y, acc.z + m.z)
});
let inv_n = 1.0 / n as f64;
Vector3::new(sum.x * inv_n, sum.y * inv_n, sum.z * inv_n)
}
pub fn effective_field(&self, m: &[Vector3<f64>]) -> Vec<Vector3<f64>> {
let nx = self.config.nx;
let ny = self.config.ny;
let nz = self.config.nz;
let dx = self.config.dx;
let dy = self.config.dy;
let dz = self.config.dz;
let ms = self.material.ms;
let a_ex = self.material.exchange_a;
let k_ani = self.material.anisotropy_k;
let easy = self.material.easy_axis;
let ex_pref = 2.0 * a_ex / (MU_0 * ms);
let ani_pref = 2.0 * k_ani / (MU_0 * ms);
let m_phys: Vec<Vector3<f64>> = m.iter().map(|mi| *mi * ms).collect();
let h_demag = self.demag.compute(&m_phys);
let n_cells = nx * ny * nz;
let mut h_eff = vec![Vector3::zero(); n_cells];
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let idx = self.cell_index(ix, iy, iz);
let mi = m[idx];
let mut h_ex = Vector3::zero();
if ix > 0 {
let j = self.cell_index(ix - 1, iy, iz);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dx * dx),
h_ex.y + diff.y * ex_pref / (dx * dx),
h_ex.z + diff.z * ex_pref / (dx * dx),
);
}
if ix + 1 < nx {
let j = self.cell_index(ix + 1, iy, iz);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dx * dx),
h_ex.y + diff.y * ex_pref / (dx * dx),
h_ex.z + diff.z * ex_pref / (dx * dx),
);
}
if iy > 0 {
let j = self.cell_index(ix, iy - 1, iz);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dy * dy),
h_ex.y + diff.y * ex_pref / (dy * dy),
h_ex.z + diff.z * ex_pref / (dy * dy),
);
}
if iy + 1 < ny {
let j = self.cell_index(ix, iy + 1, iz);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dy * dy),
h_ex.y + diff.y * ex_pref / (dy * dy),
h_ex.z + diff.z * ex_pref / (dy * dy),
);
}
if iz > 0 {
let j = self.cell_index(ix, iy, iz - 1);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dz * dz),
h_ex.y + diff.y * ex_pref / (dz * dz),
h_ex.z + diff.z * ex_pref / (dz * dz),
);
}
if iz + 1 < nz {
let j = self.cell_index(ix, iy, iz + 1);
let diff = m[j] - mi;
h_ex = Vector3::new(
h_ex.x + diff.x * ex_pref / (dz * dz),
h_ex.y + diff.y * ex_pref / (dz * dz),
h_ex.z + diff.z * ex_pref / (dz * dz),
);
}
let m_dot_e = mi.dot(&easy);
let h_ani = easy * (ani_pref * m_dot_e);
let h_total = Vector3::new(
h_ex.x + h_demag[idx].x + self.h_ext.x + h_ani.x,
h_ex.y + h_demag[idx].y + self.h_ext.y + h_ani.y,
h_ex.z + h_demag[idx].z + self.h_ext.z + h_ani.z,
);
h_eff[idx] = h_total;
}
}
}
h_eff
}
#[inline]
pub fn dmdt_cell(&self, m_i: Vector3<f64>, h_eff_i: Vector3<f64>) -> Vector3<f64> {
let alpha = self.material.alpha;
let gamma = crate::constants::GAMMA;
let factor = gamma / (1.0 + alpha * alpha);
let m_cross_h = m_i.cross(&h_eff_i);
let m_cross_m_cross_h = m_i.cross(&m_cross_h);
let dmdt_x = -factor * m_cross_h.x - factor * alpha * m_cross_m_cross_h.x;
let dmdt_y = -factor * m_cross_h.y - factor * alpha * m_cross_m_cross_h.y;
let dmdt_z = -factor * m_cross_h.z - factor * alpha * m_cross_m_cross_h.z;
Vector3::new(dmdt_x, dmdt_y, dmdt_z)
}
pub fn rk4_step(&mut self) {
let dt = self.config.dt;
let n = self.n_cells();
let h1 = self.effective_field(&self.magnetization);
let k1: Vec<Vector3<f64>> = (0..n)
.map(|i| self.dmdt_cell(self.magnetization[i], h1[i]))
.collect();
let m2: Vec<Vector3<f64>> = (0..n)
.map(|i| {
let trial = Vector3::new(
self.magnetization[i].x + 0.5 * dt * k1[i].x,
self.magnetization[i].y + 0.5 * dt * k1[i].y,
self.magnetization[i].z + 0.5 * dt * k1[i].z,
);
trial.normalize()
})
.collect();
let h2 = self.effective_field(&m2);
let k2: Vec<Vector3<f64>> = (0..n).map(|i| self.dmdt_cell(m2[i], h2[i])).collect();
let m3: Vec<Vector3<f64>> = (0..n)
.map(|i| {
let trial = Vector3::new(
self.magnetization[i].x + 0.5 * dt * k2[i].x,
self.magnetization[i].y + 0.5 * dt * k2[i].y,
self.magnetization[i].z + 0.5 * dt * k2[i].z,
);
trial.normalize()
})
.collect();
let h3 = self.effective_field(&m3);
let k3: Vec<Vector3<f64>> = (0..n).map(|i| self.dmdt_cell(m3[i], h3[i])).collect();
let m4: Vec<Vector3<f64>> = (0..n)
.map(|i| {
let trial = Vector3::new(
self.magnetization[i].x + dt * k3[i].x,
self.magnetization[i].y + dt * k3[i].y,
self.magnetization[i].z + dt * k3[i].z,
);
trial.normalize()
})
.collect();
let h4 = self.effective_field(&m4);
let k4: Vec<Vector3<f64>> = (0..n).map(|i| self.dmdt_cell(m4[i], h4[i])).collect();
for i in 0..n {
let new_m = Vector3::new(
self.magnetization[i].x
+ dt / 6.0 * (k1[i].x + 2.0 * k2[i].x + 2.0 * k3[i].x + k4[i].x),
self.magnetization[i].y
+ dt / 6.0 * (k1[i].y + 2.0 * k2[i].y + 2.0 * k3[i].y + k4[i].y),
self.magnetization[i].z
+ dt / 6.0 * (k1[i].z + 2.0 * k2[i].z + 2.0 * k3[i].z + k4[i].z),
);
self.magnetization[i] = new_m.normalize();
}
}
pub fn run(&mut self) -> LlgResult {
let n_steps = self.config.n_steps;
let record_every = self.config.record_every.max(1);
let dt = self.config.dt;
let capacity = n_steps / record_every + 2;
let mut m_avg = Vec::with_capacity(capacity);
let mut times = Vec::with_capacity(capacity);
m_avg.push(self.mean_magnetization());
times.push(0.0);
for step in 0..n_steps {
self.rk4_step();
if (step + 1) % record_every == 0 {
m_avg.push(self.mean_magnetization());
times.push((step + 1) as f64 * dt);
}
}
LlgResult {
m_avg,
times,
final_state: self.magnetization.clone(),
}
}
pub fn total_exchange_energy(&self) -> f64 {
let nx = self.config.nx;
let ny = self.config.ny;
let nz = self.config.nz;
let dx = self.config.dx;
let dy = self.config.dy;
let dz = self.config.dz;
let a_ex = self.material.exchange_a;
let v_cell = dx * dy * dz;
let mut energy = 0.0_f64;
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..(nx.saturating_sub(1)) {
let i = self.cell_index(ix, iy, iz);
let j = self.cell_index(ix + 1, iy, iz);
let diff = self.magnetization[j] - self.magnetization[i];
let diff_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
energy += a_ex * diff_sq / (dx * dx) * v_cell;
}
}
}
for iz in 0..nz {
for iy in 0..(ny.saturating_sub(1)) {
for ix in 0..nx {
let i = self.cell_index(ix, iy, iz);
let j = self.cell_index(ix, iy + 1, iz);
let diff = self.magnetization[j] - self.magnetization[i];
let diff_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
energy += a_ex * diff_sq / (dy * dy) * v_cell;
}
}
}
for iz in 0..(nz.saturating_sub(1)) {
for iy in 0..ny {
for ix in 0..nx {
let i = self.cell_index(ix, iy, iz);
let j = self.cell_index(ix, iy, iz + 1);
let diff = self.magnetization[j] - self.magnetization[i];
let diff_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
energy += a_ex * diff_sq / (dz * dz) * v_cell;
}
}
}
energy
}
pub fn total_demag_energy(&self) -> f64 {
let ms = self.material.ms;
let v_cell = self.config.dx * self.config.dy * self.config.dz;
let m_phys: Vec<Vector3<f64>> = self.magnetization.iter().map(|mi| *mi * ms).collect();
let h_demag = self.demag.compute(&m_phys);
let mut energy = 0.0_f64;
for (i, m_i) in m_phys.iter().enumerate() {
let h_dot_m = h_demag[i].x * m_i.x + h_demag[i].y * m_i.y + h_demag[i].z * m_i.z;
energy -= MU_0 / 2.0 * h_dot_m * v_cell;
}
energy
}
pub fn total_zeeman_energy(&self) -> f64 {
let ms = self.material.ms;
let v_cell = self.config.dx * self.config.dy * self.config.dz;
let h = self.h_ext;
let mut energy = 0.0_f64;
for mi in &self.magnetization {
let m_dot_h = mi.x * h.x * ms + mi.y * h.y * ms + mi.z * h.z * ms;
energy -= MU_0 * m_dot_h * v_cell;
}
energy
}
pub fn total_energy(&self) -> f64 {
self.total_exchange_energy() + self.total_demag_energy() + self.total_zeeman_energy()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::material::Ferromagnet;
fn tiny_grid() -> MicromagneticGrid {
let cfg = GridConfig {
dx: 5e-9,
dy: 5e-9,
dz: 5e-9,
nx: 2,
ny: 2,
nz: 2,
dt: 1e-12,
n_steps: 10,
record_every: 5,
};
let mat = Ferromagnet::permalloy();
MicromagneticGrid::new(cfg, mat, Vector3::zero()).expect("tiny grid construction")
}
#[test]
fn test_grid_construction_and_uniform_init() {
let grid = tiny_grid();
assert_eq!(grid.n_cells(), 8);
for mi in &grid.magnetization {
assert!((mi.x - 1.0).abs() < 1e-12);
assert!(mi.y.abs() < 1e-12);
assert!(mi.z.abs() < 1e-12);
}
}
#[test]
fn test_set_uniform() {
let mut grid = tiny_grid();
grid.set_uniform(Vector3::new(0.0, 1.0, 0.0));
let m = grid.mean_magnetization();
assert!((m.y - 1.0).abs() < 1e-10);
}
#[test]
fn test_set_vortex_normalized() {
let mut grid = tiny_grid();
grid.set_vortex();
for (i, mi) in grid.magnetization.iter().enumerate() {
let mag = mi.magnitude();
assert!((mag - 1.0).abs() < 1e-10, "Cell {i} magnitude = {mag:.6}");
}
}
#[test]
fn test_mean_magnetization_uniform() {
let grid = tiny_grid();
let m = grid.mean_magnetization();
assert!((m.x - 1.0).abs() < 1e-12);
}
#[test]
fn test_effective_field_uniform_no_exchange() {
let grid = tiny_grid();
let h = grid.effective_field(&grid.magnetization);
let h0 = h[0];
for (i, hi) in h.iter().enumerate() {
assert!(
(hi.x - h0.x).abs() < 1e-3 * h0.x.abs() + 1.0,
"Cell {i}: H_eff differs unexpectedly"
);
}
}
#[test]
fn test_rk4_step_preserves_normalization() {
let mut grid = tiny_grid();
grid.set_vortex();
for _ in 0..5 {
grid.rk4_step();
}
for (i, mi) in grid.magnetization.iter().enumerate() {
let mag = mi.magnitude();
assert!(
(mag - 1.0).abs() < 1e-10,
"After RK4: cell {i} magnitude = {mag:.8}"
);
}
}
#[test]
fn test_run_returns_correct_snapshot_count() {
let cfg = GridConfig {
dx: 5e-9,
dy: 5e-9,
dz: 5e-9,
nx: 2,
ny: 2,
nz: 1,
dt: 1e-12,
n_steps: 10,
record_every: 5,
};
let mat = Ferromagnet::permalloy();
let mut grid = MicromagneticGrid::new(cfg, mat, Vector3::zero()).expect("grid");
let result = grid.run();
assert_eq!(result.m_avg.len(), 3, "Expected 3 snapshots");
assert_eq!(result.times.len(), 3);
}
#[test]
fn test_exchange_energy_uniform_is_zero() {
let grid = tiny_grid();
let e_ex = grid.total_exchange_energy();
assert!(
e_ex.abs() < 1e-50,
"Exchange energy for uniform state should be 0, got {e_ex:.3e}"
);
}
#[test]
fn test_exchange_energy_vortex_is_positive() {
let mut grid = tiny_grid();
grid.set_vortex();
let e_ex = grid.total_exchange_energy();
assert!(
e_ex > 0.0,
"Exchange energy for vortex should be positive, got {e_ex:.3e}"
);
}
#[test]
fn test_total_energy_finite() {
let grid = tiny_grid();
let e = grid.total_energy();
assert!(e.is_finite(), "Total energy must be finite");
}
#[test]
fn test_invalid_grid_config() {
let cfg = GridConfig {
dx: -1.0,
..GridConfig::default()
};
let result = MicromagneticGrid::new(cfg, Ferromagnet::permalloy(), Vector3::zero());
assert!(result.is_err());
}
#[test]
fn test_demag_energy_sign() {
let mut grid = tiny_grid();
let e1 = grid.total_demag_energy();
grid.set_uniform(Vector3::new(0.0, 0.0, 1.0));
let e2 = grid.total_demag_energy();
assert!(e1.is_finite() && e2.is_finite());
}
}