use std::f64::consts::PI;
use crate::constants::{GAMMA, HBAR, KB, MU_0};
use crate::error::{self, Result};
#[derive(Debug, Clone)]
pub struct FourMagnonScattering {
ms: f64,
exchange_a: f64,
alpha: f64,
h_ext: f64,
}
impl FourMagnonScattering {
pub fn new(ms: f64, exchange_a: f64, alpha: f64, h_ext: f64) -> Result<Self> {
if ms <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be positive",
));
}
if exchange_a <= 0.0 {
return Err(error::invalid_param(
"exchange_a",
"exchange stiffness must be positive",
));
}
if alpha <= 0.0 || alpha >= 1.0 {
return Err(error::invalid_param(
"alpha",
"Gilbert damping must be in the open interval (0, 1)",
));
}
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
Ok(Self {
ms,
exchange_a,
alpha,
h_ext,
})
}
pub fn yig(h_ext: f64) -> Self {
Self {
ms: 1.40e5,
exchange_a: 3.70e-12,
alpha: 1.0e-4,
h_ext: h_ext.max(0.0),
}
}
#[inline]
fn mu0_ms(&self) -> f64 {
MU_0 * self.ms
}
#[inline]
fn lambda_ex(&self) -> f64 {
2.0 * self.exchange_a / (MU_0 * self.ms * self.ms)
}
pub fn magnon_frequency(&self, k_magnitude: f64, theta_k: f64) -> f64 {
let mu0_ms = self.mu0_ms();
let lambda_ex = self.lambda_ex();
let b_eff = self.h_ext + lambda_ex * mu0_ms * k_magnitude * k_magnitude;
let sin_theta = theta_k.sin();
let dipolar_term = mu0_ms * sin_theta * sin_theta;
GAMMA * (b_eff * (b_eff + dipolar_term)).sqrt()
}
pub fn magnon_linewidth(&self, k_magnitude: f64) -> f64 {
self.alpha * self.magnon_frequency(k_magnitude, PI / 2.0)
}
pub fn four_magnon_coupling(&self, k_magnitude: f64, theta_k: f64) -> f64 {
let mu0_ms = self.mu0_ms();
let lambda_ex = self.lambda_ex();
let cos_theta = theta_k.cos();
let dipolar = (GAMMA * mu0_ms / 4.0) * (3.0 * cos_theta * cos_theta - 1.0).abs();
let exchange = GAMMA * lambda_ex * mu0_ms * k_magnitude * k_magnitude;
dipolar + exchange
}
pub fn suhl_threshold_field(&self, k_magnitude: f64, theta_k: f64) -> f64 {
let delta_omega = self.magnon_linewidth(k_magnitude);
let t_kk = self.four_magnon_coupling(k_magnitude, theta_k);
if t_kk < f64::EPSILON {
return f64::MAX;
}
MU_0 * delta_omega / t_kk
}
pub fn parametric_growth_rate(&self, pump_h: f64, k_magnitude: f64, theta_k: f64) -> f64 {
let delta_omega = self.magnon_linewidth(k_magnitude);
let t_kk = self.four_magnon_coupling(k_magnitude, theta_k);
let coupling_drive = t_kk * pump_h / MU_0;
let discriminant = coupling_drive * coupling_drive - delta_omega * delta_omega;
if discriminant <= 0.0 {
0.0
} else {
discriminant.sqrt()
}
}
pub fn optimal_k_for_suhl(&self, pump_freq: f64) -> f64 {
let target_omega = pump_freq / 2.0;
let theta = PI / 2.0;
let k_min = 1.0e1_f64;
let k_max = 1.0e8_f64;
let f_min = self.magnon_frequency(k_min, theta) - target_omega;
let f_max = self.magnon_frequency(k_max, theta) - target_omega;
if f_min * f_max >= 0.0 {
return if f_min.abs() < f_max.abs() {
k_min
} else {
k_max
};
}
let mut lo = k_min;
let mut hi = k_max;
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
let f_mid = self.magnon_frequency(mid, theta) - target_omega;
if f_mid == 0.0 {
return mid;
}
if (self.magnon_frequency(lo, theta) - target_omega) * f_mid < 0.0 {
hi = mid;
} else {
lo = mid;
}
}
0.5 * (lo + hi)
}
pub fn instability_is_present(&self, pump_h: f64, pump_freq: f64) -> bool {
let k_opt = self.optimal_k_for_suhl(pump_freq);
self.parametric_growth_rate(pump_h, k_opt, PI / 2.0) > 0.0
}
}
#[derive(Debug, Clone)]
pub struct ParametricAmplification {
coupling: f64,
omega_signal: f64,
pub omega_idler: f64,
pump_h: f64,
gamma_s: f64,
gamma_i: f64,
}
impl ParametricAmplification {
pub fn new(
coupling: f64,
omega_signal: f64,
omega_idler: f64,
pump_h: f64,
gamma_s: f64,
gamma_i: f64,
) -> Result<Self> {
if coupling <= 0.0 {
return Err(error::invalid_param("coupling", "must be positive"));
}
if omega_signal <= 0.0 {
return Err(error::invalid_param("omega_signal", "must be positive"));
}
if omega_idler <= 0.0 {
return Err(error::invalid_param("omega_idler", "must be positive"));
}
if pump_h <= 0.0 {
return Err(error::invalid_param("pump_h", "must be positive"));
}
if gamma_s <= 0.0 {
return Err(error::invalid_param("gamma_s", "must be positive"));
}
if gamma_i <= 0.0 {
return Err(error::invalid_param("gamma_i", "must be positive"));
}
Ok(Self {
coupling,
omega_signal,
omega_idler,
pump_h,
gamma_s,
gamma_i,
})
}
pub fn degenerate_from_yig(pump_freq: f64, alpha: f64, ms: f64) -> Self {
let omega_half = pump_freq / 2.0;
let coupling = GAMMA * ms / 2.0;
let gamma = alpha * omega_half;
let pump_h = (gamma * gamma).sqrt() / coupling;
Self {
coupling,
omega_signal: omega_half,
omega_idler: omega_half,
pump_h,
gamma_s: gamma,
gamma_i: gamma,
}
}
pub fn with_supercriticality(mut self, xi: f64) -> Result<Self> {
if xi <= 0.0 {
return Err(error::invalid_param(
"xi",
"supercriticality must be positive",
));
}
self.pump_h = xi * self.threshold_pump_field();
Ok(self)
}
pub fn idler_frequency(&self) -> f64 {
self.omega_idler
}
pub fn phase_mismatch(&self, pump_freq: f64) -> f64 {
self.omega_signal + self.omega_idler - pump_freq
}
pub fn threshold_pump_field(&self) -> f64 {
(self.gamma_s * self.gamma_i).sqrt() / self.coupling
}
pub fn gain_coefficient(&self) -> f64 {
let drive = self.coupling * self.pump_h;
let discriminant = drive * drive - self.gamma_s * self.gamma_i;
if discriminant <= 0.0 {
0.0
} else {
discriminant.sqrt()
}
}
pub fn signal_gain_db(&self, n_roundtrips: usize) -> f64 {
let g = self.gain_coefficient();
let t_roundtrip = 2.0 * PI / self.omega_signal;
let amplitude_ratio = (g * t_roundtrip * n_roundtrips as f64).exp();
20.0 * amplitude_ratio.log10()
}
pub fn is_above_threshold(&self) -> bool {
self.pump_h > self.threshold_pump_field()
}
pub fn pump_depletion_ratio(&self) -> f64 {
let drive_sq = (self.coupling * self.pump_h).powi(2);
let threshold_sq = self.gamma_s * self.gamma_i;
let ratio = 1.0 - threshold_sq / drive_sq;
ratio.clamp(0.0, 1.0)
}
pub fn noise_equivalent_power(&self) -> f64 {
const ROOM_TEMPERATURE: f64 = 300.0;
let energy = HBAR * self.omega_signal;
let exponent = energy / (KB * ROOM_TEMPERATURE);
let n_th = if exponent > 700.0 {
0.0
} else {
1.0 / (exponent.exp() - 1.0)
};
energy * (1.0 + 2.0 * n_th)
}
}
#[derive(Debug, Clone)]
pub struct NonlinearFmrLinewidth {
material: FourMagnonScattering,
}
impl NonlinearFmrLinewidth {
pub fn new(ms: f64, exchange_a: f64, alpha: f64, h_ext: f64) -> Result<Self> {
Ok(Self {
material: FourMagnonScattering::new(ms, exchange_a, alpha, h_ext)?,
})
}
pub fn from_yig(h_ext: f64) -> Self {
Self {
material: FourMagnonScattering::yig(h_ext),
}
}
#[inline]
fn omega_res(&self) -> f64 {
GAMMA * self.material.h_ext
}
pub fn linear_linewidth_field(&self) -> f64 {
self.material.alpha * self.omega_res() / GAMMA
}
pub fn nonlinear_linewidth_field(&self, pump_h_normalized: f64) -> f64 {
let dh0 = self.linear_linewidth_field();
let excess = (pump_h_normalized - 1.0).max(0.0);
dh0 * (1.0 + excess * excess)
}
pub fn foldover_field(&self) -> f64 {
let dh0 = self.linear_linewidth_field();
3.0_f64 / 3.0_f64.sqrt() * dh0
}
pub fn bistability_threshold_power(&self) -> f64 {
let dh0 = self.linear_linewidth_field();
let f_res = self.omega_res() / (2.0 * PI);
dh0 * dh0 * f_res / (GAMMA * GAMMA * self.material.ms)
}
pub fn power_saturation_factor(&self, input_power: f64) -> f64 {
let dh0 = self.linear_linewidth_field();
let p_crit = dh0 * dh0 * self.material.ms / (GAMMA * GAMMA);
1.0 / (1.0 + input_power / p_crit)
}
pub fn effective_damping(&self, spin_wave_density: f64) -> f64 {
let k_rep = 1.0e6_f64;
let t_kk = self.material.four_magnon_coupling(k_rep, PI / 2.0);
let n_sat = if t_kk < f64::EPSILON {
f64::MAX
} else {
1.0 / (t_kk * t_kk * self.material.alpha)
};
self.material.alpha * (1.0 + spin_wave_density / n_sat)
}
}
pub fn magnon_magnon_interaction_energy(n1: f64, n2: f64, t_coupling: f64) -> f64 {
t_coupling * n1 * n2
}
pub fn four_magnon_relaxation_rate(
n_k: f64,
t_coupling: f64,
omega: f64,
_temperature: f64,
) -> f64 {
const OMEGA_REF: f64 = 2.0 * PI * 1.0e9;
let dos = (omega / OMEGA_REF).sqrt().max(0.0);
PI * t_coupling * t_coupling * n_k * dos
}
pub fn suhl_spin_wave_instability_power(ms: f64, alpha: f64, h_ext: f64, gamma: f64) -> f64 {
let omega_res = gamma * h_ext;
alpha * alpha * omega_res * omega_res * ms / (MU_0 * gamma * gamma)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_magnon_frequency_kittel_limit() {
let h_ext = 0.1; let ms = 1.40e5_f64; let sys = FourMagnonScattering::yig(h_ext);
let mu0_ms = MU_0 * ms;
let omega_kittel = GAMMA * (h_ext * (h_ext + mu0_ms)).sqrt();
let omega = sys.magnon_frequency(0.0, PI / 2.0);
let rel_err = (omega - omega_kittel).abs() / omega_kittel;
assert!(omega > 0.0, "magnon frequency must be positive");
assert!(
rel_err < 1.0e-12,
"k=0 Kalinikos-Slavin must equal Kittel formula; \
omega={omega:.6e}, kittel={omega_kittel:.6e}, err={rel_err:.2e}"
);
}
#[test]
fn test_magnon_linewidth_proportional_to_alpha() {
let h_ext = 0.05;
let sys1 = FourMagnonScattering::new(1.4e5, 3.7e-12, 1.0e-4, h_ext).expect("valid params");
let sys2 = FourMagnonScattering::new(1.4e5, 3.7e-12, 2.0e-4, h_ext).expect("valid params");
let k = 1.0e6;
let dw1 = sys1.magnon_linewidth(k);
let dw2 = sys2.magnon_linewidth(k);
let ratio = dw2 / dw1;
assert!(
(ratio - 2.0).abs() < 1.0e-10,
"linewidth must be linear in alpha; got ratio = {ratio}"
);
}
#[test]
fn test_four_magnon_coupling_nonzero_away_from_magic_angle() {
let sys = FourMagnonScattering::yig(0.1);
let t_parallel = sys.four_magnon_coupling(1.0e6, 0.0);
assert!(
t_parallel > 0.0,
"coupling must be positive at θ = 0; got {t_parallel}"
);
let t_perp = sys.four_magnon_coupling(1.0e6, PI / 2.0);
assert!(
t_perp > 0.0,
"coupling must be positive at θ = π/2; got {t_perp}"
);
}
#[test]
fn test_suhl_threshold_field_positive_and_trend() {
let sys = FourMagnonScattering::yig(0.1);
let theta = PI / 2.0;
let h_th_low = sys.suhl_threshold_field(1.0e5, theta);
let h_th_mid = sys.suhl_threshold_field(1.0e6, theta);
let h_th_high = sys.suhl_threshold_field(5.0e6, theta);
assert!(h_th_low > 0.0, "threshold field must be positive");
assert!(h_th_mid > 0.0, "threshold field must be positive");
assert!(h_th_high > 0.0, "threshold field must be positive");
assert!(
h_th_high <= h_th_mid * 10.0,
"threshold should not blow up arbitrarily with k; \
got h_th_mid={h_th_mid:.3e}, h_th_high={h_th_high:.3e}"
);
}
#[test]
fn test_parametric_growth_rate_zero_below_threshold() {
let sys = FourMagnonScattering::yig(0.1);
let k = 1.0e6;
let theta = PI / 2.0;
let h_th = sys.suhl_threshold_field(k, theta);
let pump_h = 0.5 * h_th;
let sigma = sys.parametric_growth_rate(pump_h, k, theta);
assert_eq!(
sigma, 0.0,
"growth rate below threshold must be exactly 0; got {sigma}"
);
}
#[test]
fn test_parametric_growth_rate_positive_above_threshold() {
let sys = FourMagnonScattering::yig(0.1);
let k = 1.0e6;
let theta = PI / 2.0;
let h_th = sys.suhl_threshold_field(k, theta);
let pump_h = 10.0 * h_th;
let sigma = sys.parametric_growth_rate(pump_h, k, theta);
assert!(
sigma > 0.0,
"growth rate above threshold must be positive; got {sigma}"
);
}
#[test]
fn test_parametric_amplification_threshold_consistency() {
let coupling = GAMMA * 1.4e5 / 2.0; let gamma_s = 1.0e7_f64; let gamma_i = 1.0e7_f64;
let h_th = (gamma_s * gamma_i).sqrt() / coupling;
let below = ParametricAmplification::new(
coupling,
2.0 * PI * 5.0e9,
2.0 * PI * 5.0e9,
0.9 * h_th,
gamma_s,
gamma_i,
)
.expect("valid params");
let above = ParametricAmplification::new(
coupling,
2.0 * PI * 5.0e9,
2.0 * PI * 5.0e9,
1.1 * h_th,
gamma_s,
gamma_i,
)
.expect("valid params");
assert!(!below.is_above_threshold(), "should be below threshold");
assert!(above.is_above_threshold(), "should be above threshold");
}
#[test]
fn test_gain_coefficient_zero_at_threshold() {
let coupling = 1.0e9_f64; let gamma_s = 1.0e7_f64;
let gamma_i = 1.0e7_f64;
let h_th = (gamma_s * gamma_i).sqrt() / coupling;
let pa = ParametricAmplification::new(
coupling,
2.0 * PI * 5.0e9,
2.0 * PI * 5.0e9,
h_th,
gamma_s,
gamma_i,
)
.expect("valid params");
let g = pa.gain_coefficient();
assert!(
g.abs() < 1.0e-6,
"gain coefficient must be 0 at threshold; got {g}"
);
}
#[test]
fn test_noise_equivalent_power_positive() {
let pa = ParametricAmplification::new(
1.0e9,
2.0 * PI * 5.0e9,
2.0 * PI * 5.0e9,
1.0e-3,
1.0e7,
1.0e7,
)
.expect("valid params");
let nep = pa.noise_equivalent_power();
assert!(nep > 0.0, "NEP must be positive; got {nep}");
}
#[test]
fn test_linear_linewidth_field_formula() {
let h_ext = 0.1;
let alpha = 1.0e-4;
let nlw = NonlinearFmrLinewidth::new(1.4e5, 3.7e-12, alpha, h_ext).expect("valid params");
let dh0 = nlw.linear_linewidth_field();
let omega_res = GAMMA * h_ext;
let expected = alpha * omega_res / GAMMA;
let rel_error = (dh0 - expected).abs() / expected;
assert!(
rel_error < 0.01,
"linear linewidth formula error too large: {rel_error:.2e}"
);
}
#[test]
fn test_nonlinear_linewidth_monotone_above_threshold() {
let nlw = NonlinearFmrLinewidth::from_yig(0.1);
let mut prev = nlw.nonlinear_linewidth_field(1.0);
for i in 1..=10 {
let eta = 1.0 + 0.5 * i as f64;
let dh = nlw.nonlinear_linewidth_field(eta);
assert!(
dh >= prev,
"nonlinear linewidth must be non-decreasing above threshold; \
got {dh} < {prev} at η = {eta}"
);
prev = dh;
}
}
#[test]
fn test_bistability_threshold_power_scaling() {
let h_ext = 0.1;
let nlw1 = NonlinearFmrLinewidth::new(1.4e5, 3.7e-12, 1.0e-4, h_ext).expect("valid params");
let nlw2 = NonlinearFmrLinewidth::new(1.4e5, 3.7e-12, 2.0e-4, h_ext).expect("valid params");
let p1 = nlw1.bistability_threshold_power();
let p2 = nlw2.bistability_threshold_power();
assert!(p1 > 0.0, "bistability power must be positive");
let ratio = p2 / p1;
assert!(
(ratio - 4.0).abs() < 0.01,
"bistability power should scale as α² (4× for 2× α); got ratio = {ratio:.4}"
);
}
#[test]
fn test_optimal_k_for_suhl_finite_positive() {
let sys = FourMagnonScattering::yig(0.1);
let pump_freq = 2.0 * PI * 10.0e9;
let k_opt = sys.optimal_k_for_suhl(pump_freq);
assert!(k_opt.is_finite(), "optimal k must be finite; got {k_opt}");
assert!(k_opt > 0.0, "optimal k must be positive; got {k_opt}");
assert!(k_opt < 1.0e9, "optimal k should be in a physical range");
}
#[test]
fn test_magnon_magnon_interaction_energy_positive() {
let n1 = 100.0;
let n2 = 50.0;
let t = 1.0e8;
let e = magnon_magnon_interaction_energy(n1, n2, t);
assert!(
e > 0.0,
"interaction energy must be positive for positive n1, n2, T; got {e}"
);
let e2 = magnon_magnon_interaction_energy(2.0 * n1, n2, t);
assert!(
(e2 / e - 2.0).abs() < 1.0e-12,
"interaction energy must be linear in n1"
);
}
#[test]
fn test_degenerate_from_yig_pump_is_threshold_field() {
let pump_freq = 2.0 * PI * 7.0e9; let alpha = 3.0e-5;
let ms = 1.4e5; let pa = ParametricAmplification::degenerate_from_yig(pump_freq, alpha, ms);
let h_th = pa.threshold_pump_field();
assert!(
!pa.is_above_threshold(),
"degenerate preset must sit at (not above) threshold"
);
assert!(
pa.gain_coefficient() < 1.0e-3,
"gain must vanish at the threshold field; got {}",
pa.gain_coefficient()
);
let expected = alpha * pump_freq / (GAMMA * ms);
let rel_error = (h_th - expected).abs() / expected;
assert!(
rel_error < 1.0e-12,
"threshold field must equal α·ω_p/(γ·Ms); rel error {rel_error:.2e}"
);
assert!(
(h_th - 1.0e-4).abs() / 1.0e-4 > 0.1,
"pump field must be derived, not the legacy 0.1 mT placeholder"
);
}
#[test]
fn test_degenerate_from_yig_threshold_scaling() {
let f0 = 2.0 * PI * 6.0e9;
let a0 = 2.0e-5;
let m0 = 1.4e5;
let base = ParametricAmplification::degenerate_from_yig(f0, a0, m0).threshold_pump_field();
let da =
ParametricAmplification::degenerate_from_yig(f0, 2.0 * a0, m0).threshold_pump_field();
assert!((da / base - 2.0).abs() < 1.0e-12, "h_th must scale ∝ α");
let df =
ParametricAmplification::degenerate_from_yig(2.0 * f0, a0, m0).threshold_pump_field();
assert!((df / base - 2.0).abs() < 1.0e-12, "h_th must scale ∝ ω_p");
let dm =
ParametricAmplification::degenerate_from_yig(f0, a0, 2.0 * m0).threshold_pump_field();
assert!((dm / base - 0.5).abs() < 1.0e-12, "h_th must scale ∝ 1/Ms");
}
#[test]
fn test_with_supercriticality_operating_point() {
let pump_freq = 2.0 * PI * 8.0e9;
let alpha = 4.0e-5;
let ms = 1.4e5;
let base = ParametricAmplification::degenerate_from_yig(pump_freq, alpha, ms);
let gamma = alpha * pump_freq / 2.0;
let osc = base.clone().with_supercriticality(2.0).expect("xi=2 valid");
assert!(osc.is_above_threshold(), "ξ=2 must be above threshold");
let expected_g = gamma * 3.0_f64.sqrt();
let rel = (osc.gain_coefficient() - expected_g).abs() / expected_g;
assert!(
rel < 1.0e-9,
"G(ξ=2) must equal γ·√3; got {} expected {expected_g} (rel {rel:.2e})",
osc.gain_coefficient()
);
let amp = base
.clone()
.with_supercriticality(0.5)
.expect("xi=0.5 valid");
assert!(!amp.is_above_threshold(), "ξ=0.5 must be below threshold");
assert_eq!(
amp.gain_coefficient(),
0.0,
"sub-threshold growth must be 0"
);
assert!(
base.with_supercriticality(0.0).is_err(),
"ξ ≤ 0 must be rejected"
);
}
}