use crate::error::{self, Result};
use crate::material::disorder::Xorshift64;
use crate::vector3::Vector3;
const MU_0: f64 = 1.256_637_062_12e-6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RandomAnisotropyDistribution {
Gaussian,
Uniform,
FixedAxes,
}
#[derive(Debug, Clone)]
pub struct RandomAnisotropy {
pub k_mean: f64,
pub k_std: f64,
pub correlation_length: f64,
pub distribution: RandomAnisotropyDistribution,
pub seed: u64,
rng: Xorshift64,
}
impl RandomAnisotropy {
pub fn new(
k_mean: f64,
k_std: f64,
correlation_length: f64,
distribution: RandomAnisotropyDistribution,
seed: u64,
) -> Result<Self> {
if k_std < 0.0 {
return Err(error::invalid_param(
"k_std",
"anisotropy strength standard deviation must be non-negative",
));
}
if correlation_length <= 0.0 {
return Err(error::invalid_param(
"correlation_length",
"grain correlation length must be strictly positive",
));
}
Ok(Self {
k_mean,
k_std,
correlation_length,
distribution,
seed,
rng: Xorshift64::new(seed),
})
}
pub fn nanocrystalline() -> Self {
Self {
k_mean: 1e3,
k_std: 1e2,
correlation_length: 5e-9,
distribution: RandomAnisotropyDistribution::Gaussian,
seed: 42,
rng: Xorshift64::new(42),
}
}
pub fn generate_axes(&mut self, n: usize) -> Vec<Vector3<f64>> {
(0..n).map(|_| self.rng.next_unit_vector()).collect()
}
pub fn generate_strengths(&mut self, n: usize) -> Vec<f64> {
let sqrt3 = 3.0_f64.sqrt();
match self.distribution {
RandomAnisotropyDistribution::Gaussian | RandomAnisotropyDistribution::FixedAxes => {
let mut out = Vec::with_capacity(n);
let mut remaining = n;
while remaining > 0 {
let (z0, z1) = self.rng.next_normal_pair();
out.push(self.k_mean + self.k_std * z0);
remaining -= 1;
if remaining > 0 {
out.push(self.k_mean + self.k_std * z1);
remaining -= 1;
}
}
out
},
RandomAnisotropyDistribution::Uniform => (0..n)
.map(|_| {
let u = self.rng.next_f64();
self.k_mean + self.k_std * sqrt3 * (2.0 * u - 1.0)
})
.collect(),
}
}
pub fn correlation_function(&self, r: f64) -> f64 {
(-r / self.correlation_length).exp()
}
pub fn imry_ma_correlation_length(&self, exchange_a: f64) -> f64 {
let k_rms = self.k_rms();
if k_rms < f64::EPSILON {
f64::INFINITY
} else {
let ratio = exchange_a / k_rms;
self.correlation_length * ratio * ratio
}
}
pub fn harris_relevant(&self, sample_size: f64) -> bool {
sample_size > self.correlation_length
}
pub fn anisotropy_energy(
&self,
magnetization: &[Vector3<f64>],
axes: &[Vector3<f64>],
strengths: &[f64],
) -> f64 {
let n = magnetization.len().min(axes.len()).min(strengths.len());
let mut energy = 0.0_f64;
for i in 0..n {
let cos_theta = axes[i].dot(&magnetization[i]);
energy -= strengths[i] * cos_theta * cos_theta;
}
energy
}
pub fn effective_field(
&self,
magnetization: &[Vector3<f64>],
axes: &[Vector3<f64>],
strengths: &[f64],
ms: f64,
) -> Result<Vec<Vector3<f64>>> {
if ms <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetisation must be strictly positive",
));
}
let prefactor_denom = MU_0 * ms;
let n = magnetization.len().min(axes.len()).min(strengths.len());
let mut fields = Vec::with_capacity(n);
for i in 0..n {
let cos_theta = axes[i].dot(&magnetization[i]);
let scalar = 2.0 * strengths[i] * cos_theta / prefactor_denom;
fields.push(Vector3::new(
scalar * axes[i].x,
scalar * axes[i].y,
scalar * axes[i].z,
));
}
Ok(fields)
}
pub fn k_rms(&self) -> f64 {
(self.k_mean * self.k_mean + self.k_std * self.k_std).sqrt()
}
pub fn reset_rng(&mut self) {
self.rng = Xorshift64::new(self.seed);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_gaussian(seed: u64) -> RandomAnisotropy {
RandomAnisotropy::new(
1e3, 1e2, 5e-9, RandomAnisotropyDistribution::Gaussian,
seed,
)
.expect("parameters are valid")
}
#[test]
fn test_isotropic_axis_average() {
let mut model = make_gaussian(1);
let n = 10_000_usize;
let axes = model.generate_axes(n);
let sum_x: f64 = axes.iter().map(|v| v.x).sum();
let sum_y: f64 = axes.iter().map(|v| v.y).sum();
let sum_z: f64 = axes.iter().map(|v| v.z).sum();
let mean_x = sum_x / n as f64;
let mean_y = sum_y / n as f64;
let mean_z = sum_z / n as f64;
let net_mag = (mean_x * mean_x + mean_y * mean_y + mean_z * mean_z).sqrt();
assert!(
net_mag < 0.05,
"axis distribution is not isotropic: |mean| = {net_mag}"
);
}
#[test]
fn test_gaussian_strengths_mean() {
let mut model = make_gaussian(2);
let n = 10_000_usize;
let k_std = model.k_std;
let k_mean = model.k_mean;
let strengths = model.generate_strengths(n);
let sample_mean: f64 = strengths.iter().sum::<f64>() / n as f64;
let tolerance = 3.0 * k_std / (n as f64).sqrt();
let deviation = (sample_mean - k_mean).abs();
assert!(
deviation < tolerance,
"sample mean {sample_mean} deviates from k_mean {k_mean} by {deviation} > 3σ = {tolerance}"
);
}
#[test]
fn test_correlation_decay_monotone() {
let model = make_gaussian(3);
let radii: [f64; 6] = [1e-10, 1e-9, 5e-9, 1e-8, 5e-8, 1e-7];
for &r in &radii {
let c_r = model.correlation_function(r);
let c_2r = model.correlation_function(2.0 * r);
assert!(
c_r > c_2r,
"C({r}) = {c_r} should be > C({d}) = {c_2r}",
d = 2.0 * r
);
}
}
#[test]
fn test_correlation_at_zero_is_one() {
let model = make_gaussian(4);
let c0 = model.correlation_function(0.0);
assert!((c0 - 1.0).abs() < 1e-15, "C(0) should be 1.0, got {c0}");
}
#[test]
fn test_imry_ma_length_positive() {
let model = make_gaussian(5);
let a_values: [f64; 4] = [1e-13, 1e-12, 1e-11, 1e-10];
for &exchange_a in &a_values {
let xi_m = model.imry_ma_correlation_length(exchange_a);
assert!(
xi_m > 0.0,
"Imry-Ma length should be positive for A = {exchange_a}, got {xi_m}"
);
}
}
#[test]
fn test_harris_relevance_boundary() {
let xi_a = 5e-9_f64;
let model =
RandomAnisotropy::new(1e3, 1e2, xi_a, RandomAnisotropyDistribution::Gaussian, 6)
.expect("valid params");
let above = xi_a + 1e-12;
assert!(
model.harris_relevant(above),
"disorder should be Harris-relevant for L = {above} > xi_a = {xi_a}"
);
let below = xi_a - 1e-12;
assert!(
!model.harris_relevant(below),
"disorder should not be Harris-relevant for L = {below} < xi_a = {xi_a}"
);
}
#[test]
fn test_anisotropy_energy_aligned() {
let mut model = make_gaussian(7);
let n = 50_usize;
let axes = model.generate_axes(n);
let strengths = model.generate_strengths(n);
let e_aligned = model.anisotropy_energy(&axes, &axes, &strengths);
let e_expected: f64 = -strengths.iter().sum::<f64>();
let tol = 1e-10 * e_expected.abs().max(1.0);
assert!(
(e_aligned - e_expected).abs() < tol,
"aligned energy {e_aligned} should equal −ΣK_i = {e_expected}"
);
let perp_mag: Vec<Vector3<f64>> = axes
.iter()
.map(|n_hat| {
let candidate = if n_hat.x.abs() < 0.9 {
Vector3::unit_x()
} else {
Vector3::unit_z()
};
let proj_scale = n_hat.dot(&candidate);
let perp = Vector3::new(
candidate.x - proj_scale * n_hat.x,
candidate.y - proj_scale * n_hat.y,
candidate.z - proj_scale * n_hat.z,
);
perp.normalize()
})
.collect();
let e_perp = model.anisotropy_energy(&perp_mag, &axes, &strengths);
let tol_perp = 1e-10 * (strengths.iter().map(|k| k.abs()).sum::<f64>() + 1.0);
assert!(
e_perp.abs() < tol_perp,
"perpendicular energy should be ~0, got {e_perp}"
);
assert!(
e_aligned <= e_perp,
"aligned energy {e_aligned} should be ≤ perpendicular energy {e_perp}"
);
}
#[test]
fn test_effective_field_nonzero() {
let mut model = make_gaussian(8);
let n = 10_usize;
let axes = model.generate_axes(n);
let strengths = model.generate_strengths(n);
let ms = 8.0e5_f64; let fields = model
.effective_field(&axes, &axes, &strengths, ms)
.expect("ms is positive");
assert_eq!(fields.len(), n);
for (i, h) in fields.iter().enumerate() {
let mag = (h.x * h.x + h.y * h.y + h.z * h.z).sqrt();
assert!(
mag > 0.0,
"effective field at site {i} should be non-zero, got magnitude {mag}"
);
}
let err = model.effective_field(&axes, &axes, &strengths, 0.0);
assert!(err.is_err(), "effective_field with ms=0 should return Err");
let err_neg = model.effective_field(&axes, &axes, &strengths, -1.0);
assert!(
err_neg.is_err(),
"effective_field with ms<0 should return Err"
);
}
}