use crate::constants::{GAMMA, MU_0};
use crate::error::{self, Result};
use crate::material::Ferromagnet;
#[derive(Debug, Clone)]
pub struct SpinWaveDispersion {
pub ms: f64,
pub exchange_stiffness: f64,
pub thickness: f64,
pub alpha: f64,
exchange_length_sq: f64,
}
impl SpinWaveDispersion {
pub fn new(material: &Ferromagnet, thickness: f64, _gamma_override: f64) -> Result<Self> {
if material.ms <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be positive",
));
}
if material.exchange_a < 0.0 {
return Err(error::invalid_param(
"exchange_a",
"exchange stiffness must be non-negative",
));
}
if thickness <= 0.0 {
return Err(error::invalid_param(
"thickness",
"film thickness must be positive",
));
}
if material.alpha < 0.0 {
return Err(error::invalid_param(
"alpha",
"Gilbert damping must be non-negative",
));
}
let exchange_length_sq = 2.0 * material.exchange_a / (MU_0 * material.ms);
Ok(Self {
ms: material.ms,
exchange_stiffness: material.exchange_a,
thickness,
alpha: material.alpha,
exchange_length_sq,
})
}
pub fn kittel_frequency(&self, h_ext: f64) -> Result<f64> {
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
let mu0_ms = MU_0 * self.ms;
let omega = GAMMA * (h_ext * (h_ext + mu0_ms)).sqrt();
Ok(omega)
}
pub fn exchange_dispersion(&self, h_ext: f64, k: f64) -> Result<f64> {
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
let omega = GAMMA * (h_ext + MU_0 * self.exchange_length_sq * k * k);
Ok(omega)
}
pub fn kalinikos_slavin(&self, h_ext: f64, k: f64, phi: f64) -> Result<f64> {
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
let omega_h = GAMMA * h_ext;
let omega_m = GAMMA * MU_0 * self.ms;
let lambda = self.exchange_length_sq;
let d = self.thickness;
let kd = k.abs() * d;
let p = if kd < 1e-12 {
1.0 - kd / 2.0
} else {
(1.0 - (-kd).exp()) / kd
};
let sin2_phi = phi.sin().powi(2);
let f_nn = 1.0 - p + sin2_phi * p;
let term1 = omega_h + omega_m * lambda * k * k;
let term2 = omega_h + omega_m * lambda * k * k + omega_m * f_nn;
let omega_sq = term1 * term2;
if omega_sq < 0.0 {
return Err(error::numerical_error(
"negative argument under square root in Kalinikos-Slavin dispersion",
));
}
Ok(omega_sq.sqrt())
}
pub fn dipolar_dispersion(&self, h_ext: f64, k: f64) -> Result<f64> {
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
let omega_h = GAMMA * h_ext;
let omega_m = GAMMA * MU_0 * self.ms;
let kd = k.abs() * self.thickness;
let omega_sq =
omega_h * (omega_h + omega_m) + omega_m * omega_m / 4.0 * (1.0 - (-2.0 * kd).exp());
if omega_sq < 0.0 {
return Err(error::numerical_error(
"negative argument in dipolar dispersion",
));
}
Ok(omega_sq.sqrt())
}
pub fn group_velocity(&self, h_ext: f64, k: f64, phi: f64) -> Result<f64> {
let dk = if k.abs() > 1e-6 {
k.abs() * 1e-6
} else {
1.0 };
let omega_plus = self.kalinikos_slavin(h_ext, k + dk, phi)?;
let omega_minus = self.kalinikos_slavin(h_ext, (k - dk).max(0.0), phi)?;
let effective_dk = if k - dk < 0.0 {
k + dk } else {
2.0 * dk
};
Ok((omega_plus - omega_minus) / effective_dk)
}
pub fn spin_wave_lifetime(&self, omega: f64) -> Result<f64> {
if omega <= 0.0 {
return Err(error::invalid_param(
"omega",
"angular frequency must be positive",
));
}
if self.alpha <= 0.0 {
return Err(error::invalid_param(
"alpha",
"Gilbert damping must be positive for finite lifetime",
));
}
Ok(1.0 / (self.alpha * omega))
}
pub fn propagation_length(&self, h_ext: f64, k: f64, phi: f64) -> Result<f64> {
let vg = self.group_velocity(h_ext, k, phi)?;
let omega = self.kalinikos_slavin(h_ext, k, phi)?;
let tau = self.spin_wave_lifetime(omega)?;
Ok(vg.abs() * tau)
}
pub fn exchange_length_squared(&self) -> f64 {
self.exchange_length_sq
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
fn yig_dispersion() -> SpinWaveDispersion {
let yig = Ferromagnet::yig();
SpinWaveDispersion::new(&yig, 1e-6, 0.0).expect("valid YIG parameters")
}
fn permalloy_dispersion() -> SpinWaveDispersion {
let py = Ferromagnet::permalloy();
SpinWaveDispersion::new(&py, 50e-9, 0.0).expect("valid Permalloy parameters")
}
#[test]
fn test_kittel_yig() {
let disp = yig_dispersion();
let omega = disp.kittel_frequency(0.1).expect("valid field");
assert!(omega > 2.0e10, "omega = {omega}");
assert!(omega < 4.0e10, "omega = {omega}");
}
#[test]
fn test_kittel_permalloy() {
let disp = permalloy_dispersion();
let omega = disp.kittel_frequency(0.05).expect("valid field");
assert!(omega > 3.0e10, "omega = {omega}");
assert!(omega < 5.0e10, "omega = {omega}");
}
#[test]
fn test_kittel_zero_field() {
let disp = yig_dispersion();
let omega = disp.kittel_frequency(0.0).expect("zero field allowed");
assert_eq!(omega, 0.0);
}
#[test]
fn test_exchange_quadratic_dispersion() {
let disp = yig_dispersion();
let h_ext = 0.1;
let k1 = 1e7;
let k2 = 2e7;
let omega1 = disp.exchange_dispersion(h_ext, k1).expect("valid k1");
let omega2 = disp.exchange_dispersion(h_ext, k2).expect("valid k2");
let omega0 = disp.exchange_dispersion(h_ext, 0.0).expect("valid k=0");
let delta1 = omega1 - omega0;
let delta2 = omega2 - omega0;
let ratio = delta2 / delta1;
assert!(
(ratio - 4.0).abs() < 0.01,
"ratio should be ~4.0, got {ratio}"
);
}
#[test]
fn test_kalinikos_slavin_phi_0() {
let disp = yig_dispersion();
let omega = disp
.kalinikos_slavin(0.1, 1e6, 0.0)
.expect("valid parameters");
assert!(omega > 0.0, "omega must be positive");
}
#[test]
fn test_kalinikos_slavin_phi_pi2() {
let disp = yig_dispersion();
let omega = disp
.kalinikos_slavin(0.1, 1e6, PI / 2.0)
.expect("valid parameters");
assert!(omega > 0.0, "omega must be positive");
}
#[test]
fn test_kalinikos_slavin_reduces_to_kittel_at_k0() {
let disp = yig_dispersion();
let h_ext = 0.1;
let omega_kittel = disp.kittel_frequency(h_ext).expect("valid field");
let omega_ks = disp
.kalinikos_slavin(h_ext, 1e-3, std::f64::consts::FRAC_PI_2)
.expect("valid small k");
let rel_diff = (omega_ks - omega_kittel).abs() / omega_kittel.max(1.0);
assert!(
rel_diff < 0.01,
"KS at k~0 should match Kittel: omega_ks={omega_ks}, omega_kittel={omega_kittel}"
);
}
#[test]
fn test_dipolar_dispersion() {
let disp = yig_dispersion();
let omega = disp.dipolar_dispersion(0.1, 1e5).expect("valid parameters");
assert!(omega > 0.0);
}
#[test]
fn test_group_velocity_positive_de() {
let disp = yig_dispersion();
let vg = disp
.group_velocity(0.1, 1e6, PI / 2.0)
.expect("valid parameters");
assert!(
vg > 0.0,
"DE mode should have positive group velocity: vg={vg}"
);
}
#[test]
fn test_spin_wave_lifetime() {
let disp = yig_dispersion();
let omega = 2.0 * PI * 5e9; let tau = disp.spin_wave_lifetime(omega).expect("valid omega");
assert!(tau > 100e-9, "YIG lifetime should be > 100 ns: tau={tau}");
assert!(tau < 1e-6, "YIG lifetime should be < 1 us: tau={tau}");
}
#[test]
fn test_lifetime_scaling_with_damping() {
let yig = Ferromagnet::yig();
let omega = 2.0 * PI * 5e9;
let disp1 = SpinWaveDispersion::new(&yig, 1e-6, 0.0).expect("valid");
let tau1 = disp1.spin_wave_lifetime(omega).expect("valid omega");
let py = Ferromagnet::permalloy();
let disp2 = SpinWaveDispersion::new(&py, 1e-6, 0.0).expect("valid");
let tau2 = disp2.spin_wave_lifetime(omega).expect("valid omega");
let ratio = tau1 / tau2;
assert!(
(ratio - 100.0).abs() < 1.0,
"lifetime ratio should be ~100: got {ratio}"
);
}
#[test]
fn test_invalid_negative_field() {
let disp = yig_dispersion();
assert!(disp.kittel_frequency(-0.1).is_err());
assert!(disp.exchange_dispersion(-0.1, 1e6).is_err());
assert!(disp.kalinikos_slavin(-0.1, 1e6, 0.0).is_err());
}
#[test]
fn test_invalid_parameters() {
let mut bad_material = Ferromagnet::yig();
bad_material.ms = -1.0;
assert!(SpinWaveDispersion::new(&bad_material, 1e-6, 0.0).is_err());
assert!(SpinWaveDispersion::new(&Ferromagnet::yig(), -1.0, 0.0).is_err());
assert!(SpinWaveDispersion::new(&Ferromagnet::yig(), 0.0, 0.0).is_err());
}
#[test]
fn test_propagation_length() {
let disp = yig_dispersion();
let l = disp
.propagation_length(0.1, 1e6, PI / 2.0)
.expect("valid parameters");
assert!(l > 1e-6, "propagation length should be > 1 um: l={l}");
}
}