use crate::error::{invalid_param, Result};
use crate::vector3::Vector3;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[inline]
fn f_newell(x: f64, y: f64, z: f64) -> f64 {
let x2 = x * x;
let y2 = y * y;
let z2 = z * z;
let r = (x2 + y2 + z2).sqrt();
if r < 1e-30 {
return 0.0;
}
let t1 = (2.0 * x2 - y2 - z2) / 6.0 * r;
let t2 = if y + r > 1e-30 {
y * (z2 - x2) / 6.0 * (y + r).ln()
} else {
0.0
};
let t3 = if z + r > 1e-30 {
z * (y2 - x2) / 6.0 * (z + r).ln()
} else {
0.0
};
let t4 = -x * y * z / 2.0 * f64::atan2(y * z, x * r);
t1 + t2 + t3 + t4
}
fn self_n_xx(dx: f64, dy: f64, dz: f64) -> f64 {
let mut acc = 0.0_f64;
for i in 0..=1_usize {
for j in 0..=1_usize {
for k in 0..=1_usize {
let sign = if (i + j + k) % 2 == 0 { 1.0 } else { -1.0 };
let xi = i as f64 * dx;
let yj = j as f64 * dy;
let zk = k as f64 * dz;
acc += sign * f_newell(xi, yj, zk);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * dx * dy * dz)
}
fn self_n_yy(dx: f64, dy: f64, dz: f64) -> f64 {
let mut acc = 0.0_f64;
for i in 0..=1_usize {
for j in 0..=1_usize {
for k in 0..=1_usize {
let sign = if (i + j + k) % 2 == 0 { 1.0 } else { -1.0 };
let xi = i as f64 * dx;
let yj = j as f64 * dy;
let zk = k as f64 * dz;
acc += sign * f_newell(yj, xi, zk);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * dx * dy * dz)
}
fn self_n_zz(dx: f64, dy: f64, dz: f64) -> f64 {
let mut acc = 0.0_f64;
for i in 0..=1_usize {
for j in 0..=1_usize {
for k in 0..=1_usize {
let sign = if (i + j + k) % 2 == 0 { 1.0 } else { -1.0 };
let xi = i as f64 * dx;
let yj = j as f64 * dy;
let zk = k as f64 * dz;
acc += sign * f_newell(zk, yj, xi);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * dx * dy * dz)
}
#[inline]
fn newell_n_xx(p: i64, q: i64, r: i64, dx: f64, dy: f64, dz: f64) -> f64 {
let ap = p.unsigned_abs() as i64;
let aq = q.unsigned_abs() as i64;
let ar = r.unsigned_abs() as i64;
let volume = dx * dy * dz;
let mut acc = 0.0_f64;
for i in 0..=1_i64 {
for j in 0..=1_i64 {
for k in 0..=1_i64 {
let sign = if (i + j + k) % 2 == 0 {
1.0_f64
} else {
-1.0_f64
};
let xi = (ap + i) as f64 * dx;
let yj = (aq + j) as f64 * dy;
let zk = (ar + k) as f64 * dz;
acc += sign * f_newell(xi, yj, zk);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * volume)
}
#[inline]
fn newell_n_yy(p: i64, q: i64, r: i64, dx: f64, dy: f64, dz: f64) -> f64 {
let ap = p.unsigned_abs() as i64;
let aq = q.unsigned_abs() as i64;
let ar = r.unsigned_abs() as i64;
let volume = dx * dy * dz;
let mut acc = 0.0_f64;
for i in 0..=1_i64 {
for j in 0..=1_i64 {
for k in 0..=1_i64 {
let sign = if (i + j + k) % 2 == 0 {
1.0_f64
} else {
-1.0_f64
};
let xi = (ap + i) as f64 * dx;
let yj = (aq + j) as f64 * dy;
let zk = (ar + k) as f64 * dz;
acc += sign * f_newell(yj, xi, zk);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * volume)
}
#[inline]
fn newell_n_zz(p: i64, q: i64, r: i64, dx: f64, dy: f64, dz: f64) -> f64 {
let ap = p.unsigned_abs() as i64;
let aq = q.unsigned_abs() as i64;
let ar = r.unsigned_abs() as i64;
let volume = dx * dy * dz;
let mut acc = 0.0_f64;
for i in 0..=1_i64 {
for j in 0..=1_i64 {
for k in 0..=1_i64 {
let sign = if (i + j + k) % 2 == 0 {
1.0_f64
} else {
-1.0_f64
};
let xi = (ap + i) as f64 * dx;
let yj = (aq + j) as f64 * dy;
let zk = (ar + k) as f64 * dz;
acc += sign * f_newell(zk, xi, yj);
}
}
}
16.0 * acc / (4.0 * std::f64::consts::PI * volume)
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct NewellTensor {
pub dx: f64,
pub dy: f64,
pub dz: f64,
pub nx: usize,
pub ny: usize,
pub nz: usize,
n_xx: Vec<f64>,
n_yy: Vec<f64>,
n_zz: Vec<f64>,
}
impl NewellTensor {
pub fn new(dx: f64, dy: f64, dz: f64, nx: usize, ny: usize, nz: usize) -> Result<Self> {
if dx <= 0.0 {
return Err(invalid_param("dx", "cell width must be positive"));
}
if dy <= 0.0 {
return Err(invalid_param("dy", "cell height must be positive"));
}
if dz <= 0.0 {
return Err(invalid_param("dz", "cell depth must be positive"));
}
if nx == 0 {
return Err(invalid_param("nx", "grid dimension must be >= 1"));
}
if ny == 0 {
return Err(invalid_param("ny", "grid dimension must be >= 1"));
}
if nz == 0 {
return Err(invalid_param("nz", "grid dimension must be >= 1"));
}
let lu = 2 * nx - 1;
let lv = 2 * ny - 1;
let lw = 2 * nz - 1;
let total = lu * lv * lw;
let mut n_xx = vec![0.0_f64; total];
let mut n_yy = vec![0.0_f64; total];
let mut n_zz = vec![0.0_f64; total];
for iw in 0..lw {
let w = iw as i64 - (nz as i64 - 1);
for iv in 0..lv {
let v = iv as i64 - (ny as i64 - 1);
for iu in 0..lu {
let u = iu as i64 - (nx as i64 - 1);
let flat = iw * lv * lu + iv * lu + iu;
if u == 0 && v == 0 && w == 0 {
n_xx[flat] = self_n_xx(dx, dy, dz);
n_yy[flat] = self_n_yy(dx, dy, dz);
n_zz[flat] = self_n_zz(dx, dy, dz);
} else {
n_xx[flat] = newell_n_xx(u, v, w, dx, dy, dz);
n_yy[flat] = newell_n_yy(u, v, w, dx, dy, dz);
n_zz[flat] = newell_n_zz(u, v, w, dx, dy, dz);
}
}
}
}
Ok(Self {
dx,
dy,
dz,
nx,
ny,
nz,
n_xx,
n_yy,
n_zz,
})
}
#[inline]
fn flat_index(&self, iu: i64, iv: i64, iw: i64) -> Option<usize> {
let nx = self.nx as i64;
let ny = self.ny as i64;
let nz = self.nz as i64;
if iu < -(nx - 1) || iu > (nx - 1) {
return None;
}
if iv < -(ny - 1) || iv > (ny - 1) {
return None;
}
if iw < -(nz - 1) || iw > (nz - 1) {
return None;
}
let lu = (2 * self.nx - 1) as i64;
let lv = (2 * self.ny - 1) as i64;
let u_idx = iu + (nx - 1);
let v_idx = iv + (ny - 1);
let w_idx = iw + (nz - 1);
Some((w_idx * lv * lu + v_idx * lu + u_idx) as usize)
}
pub fn get_n_xx(&self, iu: i64, iv: i64, iw: i64) -> f64 {
self.flat_index(iu, iv, iw)
.map(|idx| self.n_xx[idx])
.unwrap_or(0.0)
}
pub fn get_n_yy(&self, iu: i64, iv: i64, iw: i64) -> f64 {
self.flat_index(iu, iv, iw)
.map(|idx| self.n_yy[idx])
.unwrap_or(0.0)
}
pub fn get_n_zz(&self, iu: i64, iv: i64, iw: i64) -> f64 {
self.flat_index(iu, iv, iw)
.map(|idx| self.n_zz[idx])
.unwrap_or(0.0)
}
pub fn self_demag_factors(&self) -> (f64, f64, f64) {
(
self.get_n_xx(0, 0, 0),
self.get_n_yy(0, 0, 0),
self.get_n_zz(0, 0, 0),
)
}
pub fn trace_check(&self) -> f64 {
let (nxx, nyy, nzz) = self.self_demag_factors();
(nxx + nyy + nzz - 1.0).abs()
}
#[inline]
pub fn n_cells(&self) -> usize {
self.nx * self.ny * self.nz
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DemagField {
pub tensor: NewellTensor,
}
impl DemagField {
pub fn new(tensor: NewellTensor) -> Self {
Self { tensor }
}
pub fn compute(&self, magnetization: &[Vector3<f64>]) -> Vec<Vector3<f64>> {
let nx = self.tensor.nx;
let ny = self.tensor.ny;
let nz = self.tensor.nz;
let n_cells = nx * ny * nz;
if magnetization.len() != n_cells {
return vec![Vector3::zero(); n_cells];
}
let lu = 2 * nx - 1;
let lv = 2 * ny - 1;
let compute_target = |tgt: usize| -> Vector3<f64> {
let iz = tgt / (ny * nx);
let rem = tgt % (ny * nx);
let iy = rem / nx;
let ix = rem % nx;
let u_base = ix + (nx - 1);
let mut hx = 0.0_f64;
let mut hy = 0.0_f64;
let mut hz = 0.0_f64;
for jz in 0..nz {
let w_idx = iz + (nz - 1) - jz;
for jy in 0..ny {
let v_idx = iy + (ny - 1) - jy;
let row_base = (w_idx * lv + v_idx) * lu;
let src_row = (jz * ny + jy) * nx;
for jx in 0..nx {
let kidx = row_base + (u_base - jx);
let m_src = magnetization[src_row + jx];
hx -= self.tensor.n_xx[kidx] * m_src.x;
hy -= self.tensor.n_yy[kidx] * m_src.y;
hz -= self.tensor.n_zz[kidx] * m_src.z;
}
}
}
Vector3::new(hx, hy, hz)
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
(0..n_cells).into_par_iter().map(compute_target).collect()
}
#[cfg(not(feature = "parallel"))]
{
(0..n_cells).map(compute_target).collect()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_newell_trace_cubic_cell() {
let d = 5e-9;
let tensor =
NewellTensor::new(d, d, d, 4, 4, 4).expect("tensor construction should succeed");
let trace_err = tensor.trace_check();
assert!(
trace_err < 1e-6,
"Trace error {trace_err:.2e} exceeds 1e-6 (demagnetization sum rule violated)"
);
let (nxx, nyy, nzz) = tensor.self_demag_factors();
let expected = 1.0 / 3.0;
assert!(
(nxx - expected).abs() < 1e-6,
"N_xx(0,0,0) = {nxx:.8} should be ≈ 1/3 = {expected:.8}"
);
assert!(
(nyy - expected).abs() < 1e-6,
"N_yy(0,0,0) = {nyy:.8} should be ≈ 1/3"
);
assert!(
(nzz - expected).abs() < 1e-6,
"N_zz(0,0,0) = {nzz:.8} should be ≈ 1/3"
);
}
#[test]
fn test_newell_thin_plate_demag() {
let tensor = NewellTensor::new(500e-9, 500e-9, 1e-9, 2, 2, 1).expect("tensor construction");
let (nxx, _nyy, nzz) = tensor.self_demag_factors();
assert!(
nzz > nxx * 5.0,
"Thin plate: expected N_zz ({nzz:.4}) >> N_xx ({nxx:.4})"
);
}
#[test]
fn test_newell_invalid_params() {
assert!(NewellTensor::new(-1.0, 5e-9, 5e-9, 2, 2, 2).is_err());
assert!(NewellTensor::new(5e-9, 0.0, 5e-9, 2, 2, 2).is_err());
assert!(NewellTensor::new(5e-9, 5e-9, 5e-9, 0, 2, 2).is_err());
}
#[test]
fn test_demag_uniform_x_symmetry() {
let d = 5e-9;
let tensor = NewellTensor::new(d, d, d, 3, 3, 1).expect("tensor");
let ms = 800e3_f64;
let n = tensor.n_cells();
let mag: Vec<Vector3<f64>> = vec![Vector3::new(ms, 0.0, 0.0); n];
let demag = DemagField::new(tensor);
let h = demag.compute(&mag);
for (i, hi) in h.iter().enumerate() {
assert!(
hi.y.abs() < ms * 1e-10,
"Cell {i}: H_y = {:.3e} should be ≈ 0",
hi.y
);
assert!(
hi.z.abs() < ms * 1e-10,
"Cell {i}: H_z = {:.3e} should be ≈ 0",
hi.z
);
}
}
#[test]
fn test_demag_zero_magnetization() {
let d = 5e-9;
let tensor = NewellTensor::new(d, d, d, 2, 2, 2).expect("tensor");
let n = tensor.n_cells();
let mag = vec![Vector3::zero(); n];
let demag = DemagField::new(tensor);
let h = demag.compute(&mag);
for hi in &h {
assert!(hi.x.abs() < 1e-30 && hi.y.abs() < 1e-30 && hi.z.abs() < 1e-30);
}
}
#[test]
fn test_get_n_xx_out_of_range() {
let d = 5e-9;
let tensor = NewellTensor::new(d, d, d, 2, 2, 2).expect("tensor");
let val = tensor.get_n_xx(10, 0, 0);
assert_eq!(val, 0.0, "Out-of-range offset should return 0.0");
}
#[test]
fn test_f_newell_zero_and_finite() {
assert_eq!(f_newell(0.0, 0.0, 0.0), 0.0);
assert!(f_newell(1e-25, 0.0, 0.0).is_finite());
assert!(f_newell(5e-9, 2.5e-9, 1e-9).is_finite());
}
#[test]
fn test_newell_trace_asymmetric_cell() {
let tensor = NewellTensor::new(10e-9, 5e-9, 2e-9, 2, 2, 2).expect("tensor");
let trace_err = tensor.trace_check();
assert!(
trace_err < 1e-6,
"Asymmetric cell trace error {trace_err:.2e} exceeds 1e-6"
);
}
#[test]
fn test_compute_matches_reference_bruteforce() {
let dx = 4e-9;
let dy = 5e-9;
let dz = 6e-9;
let nx = 3;
let ny = 4;
let nz = 2;
let tensor =
NewellTensor::new(dx, dy, dz, nx, ny, nz).expect("tensor construction should succeed");
let n = tensor.n_cells();
let mut mag: Vec<Vector3<f64>> = Vec::with_capacity(n);
for idx in 0..n {
let t = idx as f64;
mag.push(Vector3::new(
(0.7 * t + 0.3).sin() * 8.0e5,
(0.4 * t - 1.1).cos() * 5.0e5,
((t + 1.0) * 0.13).sin() * 6.0e5,
));
}
let h_fast = DemagField::new(tensor.clone()).compute(&mag);
let mut h_ref = vec![Vector3::zero(); n];
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let tgt = iz * ny * nx + iy * nx + ix;
let mut hx = 0.0_f64;
let mut hy = 0.0_f64;
let mut hz = 0.0_f64;
for jz in 0..nz {
for jy in 0..ny {
for jx in 0..nx {
let src = jz * ny * nx + jy * nx + jx;
let m_src = mag[src];
let du = ix as i64 - jx as i64;
let dv = iy as i64 - jy as i64;
let dw = iz as i64 - jz as i64;
hx -= tensor.get_n_xx(du, dv, dw) * m_src.x;
hy -= tensor.get_n_yy(du, dv, dw) * m_src.y;
hz -= tensor.get_n_zz(du, dv, dw) * m_src.z;
}
}
}
h_ref[tgt] = Vector3::new(hx, hy, hz);
}
}
}
assert_eq!(h_fast.len(), h_ref.len(), "output length mismatch");
let close = |a: f64, b: f64| (a - b).abs() <= 1e-9 + 1e-9 * b.abs();
for (i, (fast, refv)) in h_fast.iter().zip(h_ref.iter()).enumerate() {
assert!(
close(fast.x, refv.x) && close(fast.y, refv.y) && close(fast.z, refv.z),
"Cell {i}: optimized H = ({:.6e}, {:.6e}, {:.6e}) \
differs from brute-force reference ({:.6e}, {:.6e}, {:.6e})",
fast.x,
fast.y,
fast.z,
refv.x,
refv.y,
refv.z
);
}
}
}