#![allow(clippy::needless_pass_by_value)]
use std::f64::consts::PI;
use proptest::prelude::*;
use spintronics::effect::smr::{SpinHallMagnetoresistance, UnidirectionalSmr};
use spintronics::effect::stno::SpinTorqueOscillator;
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 j_density_strategy() -> impl Strategy<Value = f64> {
1.0e9f64..1.0e12
}
fn temperature_strategy() -> impl Strategy<Value = f64> {
1.0f64..1000.0
}
fn n_angles_strategy() -> impl Strategy<Value = usize> {
4usize..16
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn smr_longitudinal_symmetric_in_m(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = seed_to_unit_vector(seed_a, seed_b);
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let rho_pos = smr.longitudinal_resistivity(m);
let rho_neg = smr.longitudinal_resistivity(m_neg);
prop_assert!(
(rho_pos - rho_neg).abs() < 1.0e-12,
"ρ_L(m) ≠ ρ_L(−m): Δ = {:.3e} for m = ({:.4}, {:.4}, {:.4})",
(rho_pos - rho_neg).abs(), m.x, m.y, m.z
);
}
#[test]
fn smr_hall_sum_identity(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = seed_to_unit_vector(seed_a, seed_b);
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let rho_h_pos = smr.hall_resistivity(m);
let rho_h_neg = smr.hall_resistivity(m_neg);
let actual_sum = rho_h_pos + rho_h_neg;
let expected_sum = 2.0 * smr.rho_1() * m.x * m.y;
let scale = smr.rho_1().abs().max(1.0e-30);
prop_assert!(
(actual_sum - expected_sum).abs() / scale < 1.0e-10,
"ρ_H(m)+ρ_H(−m) − 2ρ₁ mₓmᵧ = {:.3e} (scale {:.3e})",
(actual_sum - expected_sum).abs(), scale
);
}
#[test]
fn smr_longitudinal_lower_bound(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = seed_to_unit_vector(seed_a, seed_b);
let rho_l = smr.longitudinal_resistivity(m);
prop_assert!(
rho_l >= smr.rho_0() - 1.0e-15,
"ρ_L = {:.6e} < ρ₀ = {:.6e} for m = ({:.4}, {:.4}, {:.4})",
rho_l, smr.rho_0(), m.x, m.y, m.z
);
}
#[test]
fn smr_longitudinal_upper_bound(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = seed_to_unit_vector(seed_a, seed_b);
let rho_l = smr.longitudinal_resistivity(m);
let rho_max = smr.rho_0() + smr.rho_1();
prop_assert!(
rho_l <= rho_max + 1.0e-15,
"ρ_L = {:.6e} > ρ₀+ρ₁ = {:.6e} for m = ({:.4}, {:.4}, {:.4})",
rho_l, rho_max, m.x, m.y, m.z
);
}
#[test]
fn smr_rho_1_non_negative(
_seed in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
prop_assert!(
smr.rho_1() >= 0.0,
"ρ₁ = {:.3e} must be non-negative", smr.rho_1()
);
let smr_w = SpinHallMagnetoresistance::tungsten_yig();
prop_assert!(
smr_w.rho_1() >= 0.0,
"ρ₁ = {:.3e} must be non-negative for W/YIG", smr_w.rho_1()
);
}
#[test]
fn smr_rho_2_formula_consistent(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = seed_to_unit_vector(seed_a, seed_b);
let rho_h_computed = smr.hall_resistivity(m);
let rho_1 = smr.rho_1();
let rho_2 = smr.rho_2();
let rho_h_expected = rho_1 * m.x * m.y - rho_2 * m.z;
let scale = (rho_1 + rho_2).abs().max(1.0e-30);
prop_assert!(
(rho_h_computed - rho_h_expected).abs() / scale < 1.0e-10,
"Hall formula mismatch: computed {:.6e} ≠ expected {:.6e}",
rho_h_computed, rho_h_expected
);
let rho_2_ref = rho_1 * (smr.g_i / smr.g_r);
prop_assert!(
(rho_2 - rho_2_ref).abs() / scale < 1.0e-12,
"ρ₂ = {:.6e} ≠ ρ₁·g_i/g_r = {:.6e}", rho_2, rho_2_ref
);
}
#[test]
fn smr_g_sh_positive(
_seed in any::<u64>(),
) {
let smr_pt = SpinHallMagnetoresistance::platinum_yig();
prop_assert!(
smr_pt.g_sh() > 0.0,
"G_sh = {:.3e} must be positive for Pt/YIG", smr_pt.g_sh()
);
let smr_w = SpinHallMagnetoresistance::tungsten_yig();
prop_assert!(
smr_w.g_sh() > 0.0,
"G_sh = {:.3e} must be positive for W/YIG", smr_w.g_sh()
);
}
#[test]
fn smr_angular_scan_length(
n_angles in n_angles_strategy(),
) {
let smr = SpinHallMagnetoresistance::platinum_yig();
let scan = smr.angular_scan(n_angles);
prop_assert_eq!(
scan.len(),
n_angles,
"angular_scan({}) returned {} entries", n_angles, scan.len()
);
if !scan.is_empty() {
prop_assert!(
scan[0].0.abs() < 1.0e-15,
"first angle = {:.3e} ≠ 0", scan[0].0
);
}
let two_pi = 2.0 * PI;
for (alpha, _rho_l, _rho_h) in &scan {
prop_assert!(
*alpha >= 0.0 && *alpha < two_pi + 1.0e-12,
"angle {:.6} outside [0, 2π)", alpha
);
}
for w in scan.windows(2) {
prop_assert!(
w[1].0 > w[0].0,
"angles not monotonically increasing: {} >= {}", w[0].0, w[1].0
);
}
}
#[test]
fn usmr_odd_in_magnetization(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
j_density in j_density_strategy(),
) {
let usmr = UnidirectionalSmr::platinum_cobalt()
.expect("platinum_cobalt preset must be valid");
let m = seed_to_unit_vector(seed_a, seed_b);
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let delta_r_pos = usmr.usmr_relative_change(m, j_density);
let delta_r_neg = usmr.usmr_relative_change(m_neg, j_density);
prop_assert!(
(delta_r_pos + delta_r_neg).abs() < 1.0e-12,
"USMR not odd in m: ΔR(m)+ΔR(−m) = {:.3e} at J = {:.3e}",
(delta_r_pos + delta_r_neg).abs(), j_density
);
}
#[test]
fn usmr_linear_in_current(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
j_base in j_density_strategy(),
) {
let usmr = UnidirectionalSmr::platinum_cobalt()
.expect("platinum_cobalt preset must be valid");
let m = seed_to_unit_vector(seed_a, seed_b);
prop_assume!(m.y.abs() > 0.01);
let delta_r_j = usmr.usmr_relative_change(m, j_base);
let delta_r_2j = usmr.usmr_relative_change(m, 2.0 * j_base);
let scale = delta_r_2j.abs().max(1.0e-40);
prop_assert!(
(2.0 * delta_r_j - delta_r_2j).abs() < 1.0e-10 * scale,
"USMR not linear in J: 2·ΔR(J) − ΔR(2J) = {:.3e}, scale = {:.3e}",
(2.0 * delta_r_j - delta_r_2j).abs(), scale
);
}
#[test]
fn usmr_resistance_antisymmetric_in_m(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
j_density in j_density_strategy(),
) {
let usmr = UnidirectionalSmr::platinum_cobalt()
.expect("platinum_cobalt preset must be valid");
let m = seed_to_unit_vector(seed_a, seed_b);
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let j_direction = Vector3::new(1.0, 0.0, 0.0);
let dr_pos = usmr.usmr_resistance(m, j_density, j_direction);
let dr_neg = usmr.usmr_resistance(m_neg, j_density, j_direction);
prop_assert!(
(dr_pos + dr_neg).abs() < 1.0e-20,
"USMR resistance not antisymmetric: ΔR(m)+ΔR(−m) = {:.3e}",
(dr_pos + dr_neg).abs()
);
}
#[test]
fn stno_magnetization_norm_conserved_no_stt(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
) {
let stno = SpinTorqueOscillator::permalloy_nanopillar()
.expect("permalloy_nanopillar must build");
let ms = stno.solver.material.ms;
let m_hat = seed_to_unit_vector(seed_a, seed_b);
let m0 = m_hat * ms;
let dt = 5.0e-13;
let traj = stno.simulate(m0, dt, 100, 0.0);
for (i, m_i) in traj.iter().enumerate() {
let norm_err = (m_i.magnitude() - ms).abs();
prop_assert!(
norm_err < 1.0e-3,
"step {i}: |m| = {:.6e} ≠ Mₛ = {ms:.6e} (error = {norm_err:.3e})",
m_i.magnitude()
);
}
}
#[test]
fn stno_stt_perpendicular_to_m(
seed_a in any::<u64>(),
seed_b in any::<u64>(),
j_density in j_density_strategy(),
) {
let stno = SpinTorqueOscillator::permalloy_nanopillar()
.expect("permalloy_nanopillar must build");
let ms = stno.solver.material.ms;
let m_hat = seed_to_unit_vector(seed_a, seed_b);
let m = m_hat * ms;
let tau = stno.slonczewski_torque(m, j_density);
let dot = tau.dot(&m_hat).abs();
let tau_norm = tau.magnitude().max(1.0e-20);
prop_assert!(
dot / tau_norm < 1.0e-9,
"τ_STT · m̂ = {:.3e} / |τ| = {:.3e} ≠ 0 for m̂ = ({:.4}, {:.4}, {:.4})",
dot, tau_norm, m_hat.x, m_hat.y, m_hat.z
);
}
#[test]
fn stno_threshold_positive(
_seed in any::<u64>(),
) {
let stno = SpinTorqueOscillator::permalloy_nanopillar()
.expect("permalloy_nanopillar must build");
let j_th = stno.threshold_current_density();
prop_assert!(
j_th > 0.0,
"J_th = {:.3e} must be strictly positive", j_th
);
prop_assert!(
j_th.is_finite(),
"J_th = {j_th} must be finite"
);
prop_assert!(
j_th < 1.0e14,
"J_th = {j_th:.3e} is unrealistically large for Permalloy"
);
}
#[test]
fn stno_thermal_linewidth_positive(
j_density in j_density_strategy(),
temperature in temperature_strategy(),
) {
let stno = SpinTorqueOscillator::permalloy_nanopillar()
.expect("permalloy_nanopillar must build");
let lw = stno.thermal_linewidth(j_density, temperature);
prop_assert!(
lw >= 0.0,
"thermal linewidth = {:.3e} must be ≥ 0 for T = {:.1} K, J = {:.3e} A/m²",
lw, temperature, j_density
);
prop_assert!(
lw.is_finite(),
"thermal linewidth = {lw} must be finite for T = {temperature:.1} K"
);
}
}