#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::error::{invalid_param, Result};
use crate::vector3::Vector3;
pub const DEFAULT_TOLERANCE: f64 = 0.30_f64;
const _PT_THETA_SH: f64 = 0.07_f64;
const _: () = assert!(DEFAULT_TOLERANCE > 0.0);
const _: () = assert!(DEFAULT_TOLERANCE < 1.0);
const _: () = assert!(_PT_THETA_SH > 0.0);
const _: () = assert!(_PT_THETA_SH < 1.0);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpinHallMagnetoresistance {
pub theta_sh: f64,
pub resistivity_nm: f64,
pub lambda_sf: f64,
pub t_nm: f64,
pub sigma_nm: f64,
pub g_r: f64,
pub g_i: f64,
}
impl SpinHallMagnetoresistance {
pub fn new(
theta_sh: f64,
resistivity_nm: f64,
lambda_sf: f64,
t_nm: f64,
sigma_nm: f64,
g_r: f64,
g_i: f64,
) -> Result<Self> {
if theta_sh.abs() >= 1.0 {
return Err(invalid_param(
"theta_sh",
"spin Hall angle must satisfy |θ_SH| < 1",
));
}
if lambda_sf <= 0.0 {
return Err(invalid_param(
"lambda_sf",
"spin diffusion length must be positive",
));
}
if t_nm <= 0.0 {
return Err(invalid_param("t_nm", "NM layer thickness must be positive"));
}
if sigma_nm <= 0.0 {
return Err(invalid_param(
"sigma_nm",
"NM conductivity must be positive",
));
}
if g_r < 0.0 {
return Err(invalid_param(
"g_r",
"real part of spin mixing conductance must be non-negative",
));
}
Ok(Self {
theta_sh,
resistivity_nm,
lambda_sf,
t_nm,
sigma_nm,
g_r,
g_i,
})
}
pub fn platinum_yig() -> Self {
Self {
theta_sh: 0.07,
resistivity_nm: 1.06e-7, lambda_sf: 1.5e-9, t_nm: 7.0e-9, sigma_nm: 9.43e6, g_r: 3.0e18, g_i: 0.5e18, }
}
pub fn tungsten_yig() -> Self {
Self {
theta_sh: -0.3,
resistivity_nm: 5.0e-7,
lambda_sf: 1.2e-9,
t_nm: 5.0e-9,
sigma_nm: 2.0e6,
g_r: 1.5e18,
g_i: 0.3e18,
}
}
#[inline]
pub fn rho_0(&self) -> f64 {
self.resistivity_nm
}
pub fn rho_1(&self) -> f64 {
let ratio = self.t_nm / self.lambda_sf;
let half_ratio = self.t_nm / (2.0 * self.lambda_sf);
let tanh_half = half_ratio.tanh();
let coth_full = if ratio > 20.0 {
1.0
} else {
ratio.cosh() / ratio.sinh()
};
let numerator = 2.0 * self.g_r * self.lambda_sf * tanh_half;
let denominator = self.sigma_nm + 2.0 * self.g_r * self.lambda_sf * coth_full;
self.resistivity_nm * self.theta_sh * self.theta_sh * numerator / denominator
}
pub fn rho_2(&self) -> f64 {
if self.g_r.abs() > 0.0 {
self.rho_1() * (self.g_i / self.g_r)
} else {
0.0
}
}
pub fn g_sh(&self) -> f64 {
let ratio = self.t_nm / self.lambda_sf;
self.sigma_nm * ratio.tanh() / self.lambda_sf
}
pub fn smr_ratio(&self) -> f64 {
self.rho_1() / self.rho_0()
}
pub fn longitudinal_resistivity(&self, m: Vector3<f64>) -> f64 {
self.rho_0() + self.rho_1() * (1.0 - m.y * m.y)
}
pub fn hall_resistivity(&self, m: Vector3<f64>) -> f64 {
self.rho_1() * m.x * m.y - self.rho_2() * m.z
}
pub fn angular_scan(&self, n_angles: usize) -> Vec<(f64, f64, f64)> {
let n = n_angles.max(1);
let mut out = Vec::with_capacity(n);
for k in 0..n {
let alpha = 2.0 * std::f64::consts::PI * (k as f64) / (n as f64);
let m = Vector3::new(alpha.cos(), 0.0, alpha.sin());
out.push((
alpha,
self.longitudinal_resistivity(m),
self.hall_resistivity(m),
));
}
out
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UnidirectionalSmr {
pub smr: SpinHallMagnetoresistance,
pub usmr_coefficient: f64,
}
impl UnidirectionalSmr {
pub fn new(smr: SpinHallMagnetoresistance) -> Self {
let usmr_coefficient = smr.theta_sh * 2.5e-16;
Self {
smr,
usmr_coefficient,
}
}
pub fn platinum_cobalt() -> Result<Self> {
let smr = SpinHallMagnetoresistance::new(
0.07, 1.06e-7, 2.0e-9, 5.0e-9, 9.43e6, 4.5e18, 0.8e18, )?;
Ok(Self {
usmr_coefficient: 1.7e-16,
smr,
})
}
pub fn usmr_resistance(
&self,
m: Vector3<f64>,
j_density: f64,
j_direction: Vector3<f64>,
) -> f64 {
let z_hat = Vector3::new(0.0, 0.0, 1.0);
let j_cross_z = j_direction.cross(&z_hat);
let proj = j_cross_z.dot(&m);
self.smr.rho_0() * self.usmr_coefficient * j_density * proj
}
pub fn usmr_relative_change(&self, m: Vector3<f64>, j_density: f64) -> f64 {
self.usmr_coefficient * j_density * (-m.y)
}
pub fn current_scan(
&self,
m: Vector3<f64>,
j_min: f64,
j_max: f64,
n_points: usize,
) -> Vec<(f64, f64)> {
let n = n_points.max(1);
let mut out = Vec::with_capacity(n);
let j_direction = Vector3::new(1.0, 0.0, 0.0); if n == 1 {
let delta_r = self.usmr_resistance(m, j_min, j_direction);
out.push((j_min, delta_r));
} else {
for k in 0..n {
let t = (k as f64) / ((n - 1) as f64);
let j = j_min + t * (j_max - j_min);
let delta_r = self.usmr_resistance(m, j, j_direction);
out.push((j, delta_r));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_platinum_yig_builds() {
let smr = SpinHallMagnetoresistance::platinum_yig();
assert!(smr.theta_sh > 0.0);
assert!(smr.resistivity_nm > 0.0);
assert!(smr.lambda_sf > 0.0);
assert!(smr.t_nm > 0.0);
assert!(smr.sigma_nm > 0.0);
assert!(smr.g_r > 0.0);
}
#[test]
fn test_tungsten_yig_negative_theta() {
let smr = SpinHallMagnetoresistance::tungsten_yig();
assert!(smr.theta_sh < 0.0);
}
#[test]
fn test_new_validates_theta_sh_bounds() {
let result = SpinHallMagnetoresistance::new(1.5, 1e-7, 2e-9, 5e-9, 9e6, 3e18, 0.5e18);
assert!(result.is_err());
let result = SpinHallMagnetoresistance::new(-1.0, 1e-7, 2e-9, 5e-9, 9e6, 3e18, 0.5e18);
assert!(result.is_err());
let result = SpinHallMagnetoresistance::new(0.07, 1e-7, 2e-9, 5e-9, 9e6, 3e18, 0.5e18);
assert!(result.is_ok());
}
#[test]
fn test_new_validates_positive_lengths() {
assert!(SpinHallMagnetoresistance::new(0.07, 1e-7, 0.0, 5e-9, 9e6, 3e18, 0.5e18).is_err());
assert!(SpinHallMagnetoresistance::new(0.07, 1e-7, 2e-9, 0.0, 9e6, 3e18, 0.5e18).is_err());
assert!(SpinHallMagnetoresistance::new(0.07, 1e-7, 2e-9, 5e-9, 0.0, 3e18, 0.5e18).is_err());
}
#[test]
fn test_rho_0_equals_resistivity_nm() {
let smr = SpinHallMagnetoresistance::platinum_yig();
assert!((smr.rho_0() - smr.resistivity_nm).abs() < 1e-30);
}
#[test]
fn test_rho_1_positive_for_positive_theta() {
let smr = SpinHallMagnetoresistance::platinum_yig();
assert!(smr.rho_1() > 0.0, "ρ₁ must be positive for Pt/YIG");
}
#[test]
fn test_rho_1_positive_for_negative_theta() {
let smr = SpinHallMagnetoresistance::tungsten_yig();
assert!(smr.rho_1() > 0.0, "ρ₁ = ρ_NM θ_SH² … must be positive");
}
#[test]
fn test_smr_ratio_physical_magnitude() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let ratio = smr.smr_ratio();
assert!(ratio > 1e-5, "SMR ratio too small: {ratio:.3e}");
assert!(ratio < 1e-1, "SMR ratio too large: {ratio:.3e}");
println!("SMR ratio: {ratio:.4e}");
}
#[test]
fn test_rho_2_proportional_to_gi_over_gr() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let expected = smr.rho_1() * (smr.g_i / smr.g_r);
assert!((smr.rho_2() - expected).abs() < 1e-30 * smr.rho_0().abs());
}
#[test]
fn test_g_sh_positive() {
let smr = SpinHallMagnetoresistance::platinum_yig();
assert!(smr.g_sh() > 0.0);
}
#[test]
fn test_longitudinal_even_symmetry() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let test_vectors = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.6, 0.8, 0.0),
Vector3::new(0.5, 0.5, (0.5_f64).sqrt()),
];
for m in test_vectors {
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let diff =
(smr.longitudinal_resistivity(m) - smr.longitudinal_resistivity(m_neg)).abs();
assert!(
diff < 1e-30,
"ρ_L(m) ≠ ρ_L(−m) for m={m:?}: diff={diff:.3e}"
);
}
}
#[test]
fn test_hall_formula_direct() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let rho_1 = smr.rho_1();
let rho_2 = smr.rho_2();
let test_vectors = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.6, 0.8, 0.0),
Vector3::new(
1.0_f64 / 3.0_f64.sqrt(),
1.0 / 3.0_f64.sqrt(),
1.0 / 3.0_f64.sqrt(),
),
];
for m in test_vectors {
let rho_h_sim = smr.hall_resistivity(m);
let rho_h_ref = rho_1 * m.x * m.y - rho_2 * m.z;
let diff = (rho_h_sim - rho_h_ref).abs();
assert!(
diff < 1e-28,
"Hall formula mismatch for m={m:?}: sim={rho_h_sim:.3e} ref={rho_h_ref:.3e}"
);
}
}
#[test]
fn test_longitudinal_m_along_y_gives_rho0() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = Vector3::new(0.0, 1.0, 0.0);
let rho = smr.longitudinal_resistivity(m);
assert!((rho - smr.rho_0()).abs() < 1e-30);
}
#[test]
fn test_longitudinal_m_perp_y_gives_rho0_plus_rho1() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let m = Vector3::new(1.0, 0.0, 0.0);
let rho = smr.longitudinal_resistivity(m);
let expected = smr.rho_0() + smr.rho_1();
assert!((rho - expected).abs() < 1e-30);
}
#[test]
fn test_angular_scan_returns_correct_length() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let scan = smr.angular_scan(36);
assert_eq!(scan.len(), 36);
}
#[test]
fn test_angular_scan_rho_l_even() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let scan = smr.angular_scan(64);
let expected_rho_l = smr.rho_0() + smr.rho_1();
for (alpha, rho_l, _rho_h) in &scan {
assert!(
(rho_l - expected_rho_l).abs() < 1e-25,
"ρ_L deviation at α={alpha:.3}: Δ={:.3e}",
(rho_l - expected_rho_l).abs()
);
}
}
#[test]
fn test_angular_scan_hall_follows_sin_2alpha() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let scan = smr.angular_scan(128);
for (alpha, _rho_l, rho_h) in &scan {
let expected = -smr.rho_2() * alpha.sin();
let diff = (rho_h - expected).abs();
assert!(
diff < 1e-25,
"ρ_H vs −ρ₂ sin α at α={alpha:.3}: Δ={diff:.3e}"
);
}
}
#[test]
fn test_angular_scan_zero_angles_returns_one() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let scan = smr.angular_scan(0);
assert_eq!(scan.len(), 1);
}
#[test]
fn test_hall_symmetry_under_negation() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let rho_1 = smr.rho_1();
let angles: Vec<f64> = (0..8).map(|k| (k as f64) * PI / 4.0).collect();
for alpha in angles {
let m = Vector3::new(alpha.cos(), alpha.sin(), 0.3);
let norm = m.magnitude();
let m_n = Vector3::new(m.x / norm, m.y / norm, m.z / norm);
let m_neg = Vector3::new(-m_n.x, -m_n.y, -m_n.z);
let expected_sum = 2.0 * rho_1 * m_n.x * m_n.y;
let actual_sum = smr.hall_resistivity(m_n) + smr.hall_resistivity(m_neg);
let diff = (actual_sum - expected_sum).abs();
assert!(
diff < 1e-24,
"Hall sum ρ_H(m)+ρ_H(-m) mismatch at α={alpha:.3}: diff={diff:.3e}"
);
}
}
#[test]
fn test_usmr_new_sets_coefficient_from_theta_sh() {
let smr = SpinHallMagnetoresistance::platinum_yig();
let theta = smr.theta_sh;
let usmr = UnidirectionalSmr::new(smr);
let expected_coeff = theta * 2.5e-16;
assert!((usmr.usmr_coefficient - expected_coeff).abs() < 1e-28);
}
#[test]
fn test_platinum_cobalt_builds() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("platinum_cobalt must build");
assert!((usmr.usmr_coefficient - 1.7e-16).abs() < 1e-28);
assert!(usmr.smr.theta_sh > 0.0);
}
#[test]
fn test_usmr_linear_in_current() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let m = Vector3::new(0.0, 1.0, 0.0); let j_direction = Vector3::new(1.0, 0.0, 0.0);
let j1 = 1.0e11;
let j2 = 2.0e11;
let dr1 = usmr.usmr_resistance(m, j1, j_direction);
let dr2 = usmr.usmr_resistance(m, j2, j_direction);
let ratio = dr2 / dr1;
assert!(
(ratio - 2.0).abs() < 1e-10,
"USMR should be linear in J: ratio={ratio:.6}"
);
}
#[test]
fn test_usmr_odd_in_magnetisation() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let j_density = 1.0e11;
let j_direction = Vector3::new(1.0, 0.0, 0.0);
let test_vectors = [
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.5, 0.5_f64.sqrt(), 0.0),
Vector3::new(0.0, 0.8, 0.6),
Vector3::new(1.0, 0.0, 0.0),
];
for m in test_vectors {
let m_neg = Vector3::new(-m.x, -m.y, -m.z);
let dr_p = usmr.usmr_resistance(m, j_density, j_direction);
let dr_n = usmr.usmr_resistance(m_neg, j_density, j_direction);
let sum = (dr_p + dr_n).abs();
assert!(sum < 1e-30, "USMR not odd in m: {sum:.3e} for m={m:?}");
}
}
#[test]
fn test_usmr_relative_change_proportional_to_j() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let m = Vector3::new(0.0, 1.0, 0.0);
let dr1 = usmr.usmr_relative_change(m, 1.0e11);
let dr2 = usmr.usmr_relative_change(m, 3.0e11);
let ratio = dr2 / dr1;
assert!(
(ratio - 3.0).abs() < 1e-10,
"relative change ratio={ratio:.6}"
);
}
#[test]
fn test_current_scan_length_and_monotonicity() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let m = Vector3::new(0.0, 1.0, 0.0); let scan = usmr.current_scan(m, 1.0e10, 1.0e12, 20);
assert_eq!(scan.len(), 20);
for w in scan.windows(2) {
assert!(
w[1].1 < w[0].1,
"ΔR should decrease monotonically for ŷ magnetisation"
);
}
}
#[test]
fn test_current_scan_single_point() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let m = Vector3::new(0.0, 1.0, 0.0);
let scan = usmr.current_scan(m, 5.0e10, 1.0e11, 1);
assert_eq!(scan.len(), 1);
assert!((scan[0].0 - 5.0e10).abs() < 1.0);
}
#[test]
fn test_current_scan_zero_points_fallback() {
let usmr = UnidirectionalSmr::platinum_cobalt().expect("build");
let m = Vector3::new(1.0, 0.0, 0.0);
let scan = usmr.current_scan(m, 1.0e10, 1.0e11, 0);
assert_eq!(scan.len(), 1);
}
}