#![allow(clippy::needless_pass_by_value)]
#![cfg(not(target_arch = "wasm32"))]
use std::f64::consts::PI;
use proptest::prelude::*;
use spintronics::constants::{GAMMA, MU_0};
use spintronics::dynamics::llg::{calc_dm_dt, zeeman_energy, LlgSolver};
use spintronics::vector3::Vector3;
fn seed_to_unit_vector(seed_a: u64, seed_b: u64) -> Vector3<f64> {
let u = (seed_a as f64) / (u64::MAX as f64);
let v = (seed_b as f64) / (u64::MAX as f64);
let cos_theta = 2.0 * u - 1.0;
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let phi = 2.0 * PI * v;
Vector3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta)
}
fn unit_vector_strategy() -> impl Strategy<Value = Vector3<f64>> {
(any::<u64>(), any::<u64>()).prop_map(|(a, b)| seed_to_unit_vector(a, b))
}
fn nonzero_vector_strategy() -> impl Strategy<Value = Vector3<f64>> {
(any::<u64>(), any::<u64>(), any::<u64>()).prop_map(|(a, b, c)| {
let x = (a as f64) / (u64::MAX as f64) * 2.0 - 1.0;
let y = (b as f64) / (u64::MAX as f64) * 2.0 - 1.0;
let z = (c as f64) / (u64::MAX as f64) * 2.0 - 1.0;
let v = Vector3::new(x, y, z);
if v.magnitude() < 1.0e-3 {
Vector3::new(v.x, v.y, v.z + 1.0)
} else {
v
}
})
}
fn dt_strategy() -> impl Strategy<Value = f64> {
1.0e-15f64..1.0e-13
}
fn alpha_strategy() -> impl Strategy<Value = f64> {
0.001f64..0.5
}
fn h_field_strategy() -> impl Strategy<Value = f64> {
1.0e-3f64..1.0e1
}
fn ms_strategy() -> impl Strategy<Value = f64> {
1.0e4f64..2.0e6
}
fn n_steps_strategy() -> impl Strategy<Value = usize> {
5usize..30
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn norm_conserved_rk4(
m0 in unit_vector_strategy(),
alpha in alpha_strategy(),
h_z in h_field_strategy(),
dt in dt_strategy(),
n_steps in n_steps_strategy(),
) {
let mut m = m0;
let h = Vector3::new(0.0, 0.0, h_z);
let solver = LlgSolver::new(alpha, dt);
for _ in 0..n_steps {
m = solver.step_rk4(m, |_| h);
}
let mag = m.magnitude();
prop_assert!((mag - 1.0).abs() < 1.0e-10,
"|m| drifted to {} after {} RK4 steps", mag, n_steps);
}
#[test]
fn norm_conserved_heun(
m0 in unit_vector_strategy(),
alpha in alpha_strategy(),
h_x in h_field_strategy(),
h_z in h_field_strategy(),
dt in dt_strategy(),
n_steps in n_steps_strategy(),
) {
let mut m = m0;
let h = Vector3::new(h_x, 0.0, h_z);
let solver = LlgSolver::new(alpha, dt);
for _ in 0..n_steps {
m = solver.step_heun(m, |_| h);
}
prop_assert!((m.magnitude() - 1.0).abs() < 1.0e-10);
}
#[test]
fn norm_conserved_euler(
m0 in unit_vector_strategy(),
alpha in alpha_strategy(),
h_z in h_field_strategy(),
dt in dt_strategy(),
n_steps in n_steps_strategy(),
) {
let mut m = m0;
let h = Vector3::new(0.0, 0.0, h_z);
let solver = LlgSolver::new(alpha, dt);
for _ in 0..n_steps {
m = solver.step_euler(m, h);
}
prop_assert!((m.magnitude() - 1.0).abs() < 1.0e-10);
}
#[test]
fn zeeman_energy_conserved_zero_damping(
m0 in unit_vector_strategy(),
h_z in h_field_strategy(),
dt in dt_strategy(),
ms in ms_strategy(),
n_steps in n_steps_strategy(),
) {
let h = Vector3::new(0.0, 0.0, h_z);
let solver = LlgSolver::new(0.0, dt);
let mut m = m0;
let e0 = zeeman_energy(m, h, ms);
for _ in 0..n_steps {
m = solver.step_rk4(m, |_| h);
}
let e1 = zeeman_energy(m, h, ms);
let scale = (MU_0 * ms * h_z).abs().max(1.0e-20);
prop_assert!((e1 - e0).abs() / scale < 1.0e-2,
"Energy drift {} over {} steps (scale {})", e1 - e0, n_steps, scale);
}
#[test]
fn damping_aligns_m_with_field(
alpha in 0.05f64..0.3,
h_z in 1.0f64..5.0,
dt in 1.0e-13f64..5.0e-13,
) {
let h = Vector3::new(0.0, 0.0, h_z);
let m0 = Vector3::new(1.0, 0.0, 0.0);
let solver = LlgSolver::new(alpha, dt);
let mut m = m0;
for _ in 0..2000 {
m = solver.step_rk4(m, |_| h);
}
let proj_final = m.dot(&h) / h.magnitude();
prop_assert!(proj_final > 0.0,
"Damping did not align m with h: m.z/|h| = {}", proj_final);
}
#[test]
fn larmor_reverses_under_field_flip(
m in unit_vector_strategy(),
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let h = seed_to_unit_vector(seed_a, seed_b);
let dm_forward = calc_dm_dt(m, h, GAMMA, 0.0);
let dm_reversed = calc_dm_dt(m, h * -1.0, GAMMA, 0.0);
let sum = dm_forward + dm_reversed;
let scale = dm_forward.magnitude().max(1.0);
prop_assert!(sum.magnitude() / scale < 1.0e-10,
"Larmor not antisymmetric in H: |sum|/scale = {}",
sum.magnitude() / scale);
}
#[test]
fn dm_dt_perpendicular_to_m_zero_damping(
m in unit_vector_strategy(),
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let h = seed_to_unit_vector(seed_a, seed_b);
let dm = calc_dm_dt(m, h, GAMMA, 0.0);
let dm_mag = dm.magnitude().max(1.0);
prop_assert!(dm.dot(&m).abs() / dm_mag < 1.0e-10,
"dm/dt . m / |dm/dt| = {} not negligible at alpha=0",
dm.dot(&m) / dm_mag);
}
#[test]
fn dm_dt_perpendicular_to_h_zero_damping(
m in unit_vector_strategy(),
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let h = seed_to_unit_vector(seed_a, seed_b);
let dm = calc_dm_dt(m, h, GAMMA, 0.0);
let scale = (dm.magnitude() * h.magnitude()).max(1.0);
prop_assert!(dm.dot(&h).abs() / scale < 1.0e-10,
"dm/dt . h / scale = {} not negligible at alpha=0",
dm.dot(&h) / scale);
}
#[test]
fn cross_product_anticommutative(
a in unit_vector_strategy(),
b in unit_vector_strategy(),
) {
let lhs = a.cross(&b);
let rhs = b.cross(&a) * -1.0;
prop_assert!((lhs.x - rhs.x).abs() < 1.0e-12);
prop_assert!((lhs.y - rhs.y).abs() < 1.0e-12);
prop_assert!((lhs.z - rhs.z).abs() < 1.0e-12);
}
#[test]
fn triple_product_cyclic(
a in unit_vector_strategy(),
b in unit_vector_strategy(),
c in unit_vector_strategy(),
) {
let lhs = a.cross(&b).dot(&c);
let mid = b.cross(&c).dot(&a);
let rhs = c.cross(&a).dot(&b);
prop_assert!((lhs - mid).abs() < 1.0e-12);
prop_assert!((mid - rhs).abs() < 1.0e-12);
}
#[test]
fn lagrange_identity(
a in unit_vector_strategy(),
b in unit_vector_strategy(),
) {
let cross_sq = a.cross(&b).magnitude_squared();
let dot = a.dot(&b);
let lhs = cross_sq + dot * dot;
let rhs = a.magnitude_squared() * b.magnitude_squared();
prop_assert!((lhs - rhs).abs() < 1.0e-12);
}
#[test]
fn normalize_idempotent(v in nonzero_vector_strategy()) {
let n = v.normalize();
prop_assert!((n.magnitude() - 1.0).abs() < 1.0e-12);
let nn = n.normalize();
prop_assert!((nn.magnitude() - 1.0).abs() < 1.0e-12);
prop_assert!((nn - n).magnitude() < 1.0e-12);
}
}