use crate::constants::{GAMMA, MU_0};
use crate::error::{self, Result};
#[derive(Debug, Clone)]
pub struct SurfaceSpinWave {
pub h_ext: f64,
pub ms: f64,
pub a_ex: f64,
pub alpha: f64,
pub surface_anisotropy: f64,
}
impl SurfaceSpinWave {
pub fn new(
h_ext: f64,
ms: f64,
a_ex: f64,
alpha: f64,
surface_anisotropy: f64,
) -> Result<Self> {
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
if ms <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be positive",
));
}
if a_ex <= 0.0 {
return Err(error::invalid_param(
"a_ex",
"exchange stiffness must be positive",
));
}
if alpha < 0.0 {
return Err(error::invalid_param(
"alpha",
"Gilbert damping must be non-negative",
));
}
Ok(Self {
h_ext,
ms,
a_ex,
alpha,
surface_anisotropy,
})
}
pub fn bulk_yig() -> Self {
Self {
h_ext: 62_460.0, ms: 1.4e5,
a_ex: 3.5e-12,
alpha: 3e-5,
surface_anisotropy: 0.0,
}
}
pub fn bulk_iron() -> Self {
Self {
h_ext: 0.0,
ms: 1.71e6,
a_ex: 2.1e-11,
alpha: 1e-3,
surface_anisotropy: 0.0, }
}
#[inline]
pub fn omega_h(&self) -> f64 {
GAMMA * MU_0 * self.h_ext
}
#[inline]
pub fn omega_m(&self) -> f64 {
GAMMA * MU_0 * self.ms
}
#[inline]
fn d_exchange(&self) -> f64 {
2.0 * self.a_ex * GAMMA / self.ms
}
#[inline]
fn k_exchange(&self) -> f64 {
(MU_0 * self.ms * self.ms / (2.0 * self.a_ex)).sqrt()
}
pub fn dispersion_omega(&self, k: f64) -> f64 {
let omega_h = self.omega_h();
let omega_m = self.omega_m();
let d = self.d_exchange();
let d_k2 = d * k * k;
let term1 = omega_h + d_k2;
let term2 = omega_h + omega_m + d_k2;
let omega_sq = term1 * term2;
if omega_sq < 0.0 {
0.0
} else {
omega_sq.sqrt()
}
}
pub fn penetration_depth(&self, k: f64) -> f64 {
let k_ex = self.k_exchange();
1.0 / (k * k + k_ex * k_ex).sqrt()
}
pub fn surface_anisotropy_shift(&self, k: f64) -> f64 {
if self.surface_anisotropy == 0.0 {
return 0.0;
}
let k_ex = self.k_exchange();
let k2 = k * k;
let k_ex2 = k_ex * k_ex;
-2.0 * GAMMA * self.surface_anisotropy / self.ms * k2 / (k2 + k_ex2)
}
pub fn field_amplitude(&self, k: f64, z: f64) -> f64 {
let z_pos = z.max(0.0);
(-k.abs() * z_pos).exp()
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
fn yig_surface() -> SurfaceSpinWave {
SurfaceSpinWave::bulk_yig()
}
fn iron_surface() -> SurfaceSpinWave {
SurfaceSpinWave::bulk_iron()
}
#[test]
fn test_yig_preset_valid() {
let s = yig_surface();
assert!(s.ms > 0.0);
assert!(s.a_ex > 0.0);
assert!(s.alpha > 0.0);
assert!(s.surface_anisotropy == 0.0);
}
#[test]
fn test_iron_preset_valid() {
let s = iron_surface();
assert!((s.ms - 1.71e6).abs() < 1e3, "Fe Ms should be 1.71e6 A/m");
assert!(s.a_ex > 0.0);
}
#[test]
fn test_new_invalid_ms() {
let result = SurfaceSpinWave::new(0.0, 0.0, 1e-12, 1e-4, 0.0);
assert!(result.is_err());
let result = SurfaceSpinWave::new(0.0, -1.0, 1e-12, 1e-4, 0.0);
assert!(result.is_err());
}
#[test]
fn test_new_invalid_a_ex() {
let result = SurfaceSpinWave::new(0.0, 1e5, 0.0, 1e-4, 0.0);
assert!(result.is_err());
let result = SurfaceSpinWave::new(0.0, 1e5, -1e-12, 1e-4, 0.0);
assert!(result.is_err());
}
#[test]
fn test_new_invalid_h_ext() {
let result = SurfaceSpinWave::new(-100.0, 1e5, 1e-12, 1e-4, 0.0);
assert!(result.is_err());
}
#[test]
fn test_new_valid_negative_surface_anisotropy() {
let result = SurfaceSpinWave::new(0.0, 1e5, 1e-12, 1e-4, -0.5e-3);
assert!(result.is_ok());
}
#[test]
fn test_dispersion_positive_frequency() {
let s = yig_surface();
for &k in &[1e4_f64, 1e5, 1e6, 1e7, 1e8] {
let omega = s.dispersion_omega(k);
assert!(
omega > 0.0,
"surface spin wave frequency must be positive at k={k:.2e}: {omega}"
);
}
}
#[test]
fn test_dispersion_k_zero_limit() {
let s = yig_surface();
let omega_k0 = s.dispersion_omega(0.0);
let omega_h = s.omega_h();
let omega_m = s.omega_m();
let omega_expected = (omega_h * (omega_h + omega_m)).sqrt();
let rel_err = (omega_k0 - omega_expected).abs() / omega_expected.max(1.0);
assert!(
rel_err < 0.01,
"At k=0, ω should be Kittel frequency: got {omega_k0:.4e}, expected {omega_expected:.4e}"
);
}
#[test]
fn test_dispersion_increases_with_k() {
let s = yig_surface();
let omega1 = s.dispersion_omega(1e6);
let omega2 = s.dispersion_omega(1e7);
assert!(
omega2 > omega1,
"Frequency should increase with k: ω(1e6)={omega1:.4e}, ω(1e7)={omega2:.4e}"
);
}
#[test]
fn test_penetration_depth_decreases_with_k() {
let s = yig_surface();
let xi1 = s.penetration_depth(1e6);
let xi2 = s.penetration_depth(1e7);
assert!(
xi1 > xi2,
"Penetration depth should decrease with k: ξ(1e6)={xi1:.4e}, ξ(1e7)={xi2:.4e}"
);
}
#[test]
fn test_penetration_depth_k_ex_limit() {
let s = iron_surface();
let k_ex = s.k_exchange();
let xi_k0 = s.penetration_depth(0.0);
let expected = 1.0 / k_ex;
let rel_err = (xi_k0 - expected).abs() / expected;
assert!(
rel_err < 0.01,
"At k=0: ξ = 1/k_ex = {expected:.4e}, got {xi_k0:.4e}"
);
}
#[test]
fn test_surface_anisotropy_shift_zero_when_ks_zero() {
let s = yig_surface(); let shift = s.surface_anisotropy_shift(1e6);
assert_eq!(shift, 0.0, "Zero surface anisotropy → zero frequency shift");
}
#[test]
fn test_surface_anisotropy_shift_sign() {
let s = SurfaceSpinWave::new(0.0, 1.4e5, 3.5e-12, 3e-5, 0.5e-3).expect("valid parameters");
let shift = s.surface_anisotropy_shift(1e7);
assert!(
shift < 0.0,
"Positive K_s should give negative frequency shift: Δω = {shift:.4e}"
);
}
#[test]
fn test_field_amplitude_at_surface() {
let s = yig_surface();
let amp = s.field_amplitude(1e6, 0.0);
assert!(
(amp - 1.0).abs() < 1e-14,
"Surface amplitude should be 1.0: {amp}"
);
}
#[test]
fn test_field_amplitude_decays_into_bulk() {
let s = yig_surface();
let k = 1e6;
let z1 = 100e-9; let z2 = 500e-9;
let amp1 = s.field_amplitude(k, z1);
let amp2 = s.field_amplitude(k, z2);
assert!(
amp1 > amp2,
"Amplitude should decay with depth: A(100nm)={amp1:.4e}, A(500nm)={amp2:.4e}"
);
assert!(amp1 > 0.0 && amp1 <= 1.0, "Amplitude must be in (0,1]");
assert!(amp2 > 0.0 && amp2 <= 1.0, "Amplitude must be in (0,1]");
}
#[test]
fn test_omega_m_yig_value() {
let s = yig_surface();
let omega_m = s.omega_m();
assert!(
omega_m > 1e10 && omega_m < 1e11,
"YIG ω_M should be ~3e10 rad/s: {omega_m:.4e}"
);
}
#[test]
fn test_fmr_frequency_near_5ghz_yig() {
let s = yig_surface();
let omega = s.dispersion_omega(1.0); let f_ghz = omega / (2.0 * PI * 1e9);
assert!(
f_ghz > 3.0 && f_ghz < 8.0,
"YIG surface mode near k=0 should be ~5 GHz: {f_ghz:.2} GHz"
);
}
}