use std::f64::consts::PI;
use crate::constants::{GAMMA, HBAR, KB, MU_0};
use crate::error::{invalid_param, Result};
use crate::vector3::Vector3;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[allow(dead_code)]
const _KB_USED: f64 = KB;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PiezoSubstrate {
#[cfg_attr(feature = "serde", serde(skip))]
pub name: &'static str,
pub saw_velocity: f64,
pub electromechanical_k2: f64,
pub poisson_ratio: f64,
pub density: f64,
}
impl PiezoSubstrate {
pub fn linbo3() -> Self {
Self {
name: "LiNbO3",
saw_velocity: 3985.0,
electromechanical_k2: 0.055,
poisson_ratio: 0.25,
density: 4640.0,
}
}
pub fn gaas() -> Self {
Self {
name: "GaAs",
saw_velocity: 2900.0,
electromechanical_k2: 0.007,
poisson_ratio: 0.31,
density: 5320.0,
}
}
pub fn zno() -> Self {
Self {
name: "ZnO",
saw_velocity: 2700.0,
electromechanical_k2: 0.012,
poisson_ratio: 0.36,
density: 5600.0,
}
}
#[inline]
pub fn wavelength_m(&self, frequency_hz: f64) -> f64 {
self.saw_velocity / frequency_hz
}
#[inline]
pub fn wavenumber(&self, frequency_hz: f64) -> f64 {
2.0 * PI * frequency_hz / self.saw_velocity
}
#[inline]
pub fn penetration_depth_m(&self, frequency_hz: f64) -> f64 {
self.saw_velocity / (2.0 * PI * frequency_hz)
}
#[inline]
pub fn acoustic_impedance(&self) -> f64 {
self.density * self.saw_velocity
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MagnetoelasticMaterial {
#[cfg_attr(feature = "serde", serde(skip))]
pub name: &'static str,
pub ms: f64,
pub alpha: f64,
pub anisotropy_k: f64,
pub b1: f64,
pub b2: f64,
}
impl MagnetoelasticMaterial {
pub fn nickel() -> Self {
Self {
name: "Nickel",
ms: 490.0e3,
alpha: 0.045,
anisotropy_k: 5.0e3,
b1: -8.5e6,
b2: -9.2e6,
}
}
pub fn permalloy() -> Self {
Self {
name: "Permalloy",
ms: 800.0e3,
alpha: 0.01,
anisotropy_k: 200.0,
b1: 6.7e6,
b2: 9.0e6,
}
}
pub fn cobalt() -> Self {
Self {
name: "Cobalt",
ms: 1.4e6,
alpha: 0.01,
anisotropy_k: 410.0e3,
b1: -8.0e6,
b2: -9.0e6,
}
}
pub fn anisotropy_field(&self) -> f64 {
2.0 * self.anisotropy_k / (MU_0 * self.ms)
}
pub fn fmr_frequency_hz(&self, h_ext: f64) -> f64 {
GAMMA / (2.0 * PI) * MU_0 * (h_ext + self.anisotropy_field())
}
pub fn quality_factor(&self, _h_ext: f64) -> f64 {
1.0 / self.alpha
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SawSource {
pub substrate: PiezoSubstrate,
pub frequency_hz: f64,
pub strain_amplitude: f64,
pub propagation_dir: Vector3<f64>,
}
impl SawSource {
pub fn new(
substrate: PiezoSubstrate,
frequency_hz: f64,
strain_amplitude: f64,
) -> Result<Self> {
if frequency_hz <= 0.0 {
return Err(invalid_param(
"frequency_hz",
"SAW frequency must be positive",
));
}
if strain_amplitude <= 0.0 {
return Err(invalid_param(
"strain_amplitude",
"strain amplitude must be positive",
));
}
if strain_amplitude >= 0.1 {
return Err(invalid_param(
"strain_amplitude",
"strain amplitude ≥ 0.1 is unphysical (nonlinear / fracture regime)",
));
}
Ok(Self {
substrate,
frequency_hz,
strain_amplitude,
propagation_dir: Vector3::unit_x(),
})
}
pub fn linbo3_1ghz() -> Self {
Self {
substrate: PiezoSubstrate::linbo3(),
frequency_hz: 1.0e9,
strain_amplitude: 1.0e-4,
propagation_dir: Vector3::unit_x(),
}
}
pub fn linbo3_3ghz() -> Self {
Self {
substrate: PiezoSubstrate::linbo3(),
frequency_hz: 3.0e9,
strain_amplitude: 1.0e-4,
propagation_dir: Vector3::unit_x(),
}
}
#[inline]
pub fn wavelength_m(&self) -> f64 {
self.substrate.wavelength_m(self.frequency_hz)
}
#[inline]
pub fn angular_frequency(&self) -> f64 {
2.0 * PI * self.frequency_hz
}
pub fn strain_at_point(&self, x: f64, z: f64, t: f64) -> f64 {
let k = self.substrate.wavenumber(self.frequency_hz);
let omega = self.angular_frequency();
self.strain_amplitude * (k * x - omega * t).sin() * (-k * z).exp()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SawMagnetoacoustics {
pub saw: SawSource,
pub material: MagnetoelasticMaterial,
pub film_thickness_m: f64,
pub h_ext: f64,
}
impl SawMagnetoacoustics {
pub fn new(
saw: SawSource,
material: MagnetoelasticMaterial,
film_thickness_m: f64,
h_ext: f64,
) -> Result<Self> {
if film_thickness_m <= 0.0 {
return Err(invalid_param(
"film_thickness_m",
"ferromagnetic film thickness must be positive",
));
}
if h_ext < 0.0 {
return Err(invalid_param(
"h_ext",
"external bias field must be non-negative",
));
}
Ok(Self {
saw,
material,
film_thickness_m,
h_ext,
})
}
pub fn magnetoelastic_field_amplitude(&self) -> f64 {
(2.0 * self.material.b1.abs() * self.saw.strain_amplitude) / (MU_0 * self.material.ms)
}
pub fn fmr_frequency(&self) -> f64 {
self.material.fmr_frequency_hz(self.h_ext)
}
pub fn resonance_detuning(&self) -> f64 {
self.saw.frequency_hz - self.fmr_frequency()
}
pub fn lorentzian_response(&self) -> f64 {
let omega_fmr = 2.0 * PI * self.fmr_frequency();
let omega_saw = self.saw.angular_frequency();
let alpha = self.material.alpha;
let detuning_sq = (omega_saw / omega_fmr - 1.0).powi(2) + alpha.powi(2);
1.0 / (detuning_sq.sqrt() * omega_fmr)
}
pub fn precession_cone_angle_rad(&self) -> f64 {
let h_me = self.magnetoelastic_field_amplitude();
let theta_raw = h_me * self.lorentzian_response();
theta_raw.clamp(0.0, PI / 4.0)
}
pub fn spin_current_density(&self, g_r: f64) -> f64 {
let theta = self.precession_cone_angle_rad();
(HBAR / (4.0 * PI)) * g_r * self.saw.angular_frequency() * theta.sin().powi(2) / 2.0
}
pub fn resonant_absorption(&self) -> f64 {
let theta = self.precession_cone_angle_rad();
let omega_fmr = 2.0 * PI * self.fmr_frequency();
0.5 * MU_0
* self.material.ms
* self.material.alpha
* omega_fmr
* theta.powi(2)
* self.film_thickness_m
}
pub fn acoustic_fmr_condition_h_ext(&self) -> f64 {
let h_ext_raw =
self.saw.angular_frequency() / (GAMMA * MU_0) - self.material.anisotropy_field();
h_ext_raw.max(0.0)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SawSpinWaveExcitation {
pub saw: SawSource,
pub material: MagnetoelasticMaterial,
}
impl SawSpinWaveExcitation {
pub fn new(saw: SawSource, material: MagnetoelasticMaterial) -> Self {
Self { saw, material }
}
pub fn excited_spinwave_k(&self) -> f64 {
self.saw.substrate.wavenumber(self.saw.frequency_hz)
}
pub fn excited_spinwave_frequency(&self, h_ext: f64) -> f64 {
let omega_sw = GAMMA * MU_0 * (h_ext + self.material.anisotropy_field());
omega_sw / (2.0 * PI)
}
pub fn phase_velocity_mismatch(&self, h_ext: f64) -> f64 {
let f_saw = self.saw.frequency_hz;
let f_sw = self.excited_spinwave_frequency(h_ext);
(f_saw - f_sw).abs() * self.saw.wavelength_m()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_saw_wavelength_linbo3_1ghz() {
let sub = PiezoSubstrate::linbo3();
let lambda = sub.wavelength_m(1.0e9);
let expected = 3.985e-6;
let rel_err = (lambda - expected).abs() / expected;
assert!(
rel_err < 0.01,
"LiNbO₃ wavelength at 1 GHz should be ~3.985 μm, got {:.4e} m (rel_err={:.4})",
lambda,
rel_err
);
}
#[test]
fn test_saw_wavenumber_linbo3() {
let sub = PiezoSubstrate::linbo3();
let k = sub.wavenumber(1.0e9);
let expected = 2.0 * PI / 3.985e-6;
let rel_err = (k - expected).abs() / expected;
assert!(
rel_err < 1.0e-6,
"Wavenumber mismatch: got {:.6e} expected {:.6e}",
k,
expected
);
}
#[test]
fn test_penetration_depth_is_lambda_over_2pi() {
let sub = PiezoSubstrate::linbo3();
let f = 2.0e9;
let depth = sub.penetration_depth_m(f);
let lambda = sub.wavelength_m(f);
let expected = lambda / (2.0 * PI);
let rel_err = (depth - expected).abs() / expected;
assert!(
rel_err < 1.0e-10,
"Penetration depth should equal λ/(2π), rel_err = {:.2e}",
rel_err
);
}
#[test]
fn test_gaas_slower_than_linbo3() {
let linbo3 = PiezoSubstrate::linbo3();
let gaas = PiezoSubstrate::gaas();
assert!(
gaas.saw_velocity < linbo3.saw_velocity,
"GaAs SAW velocity ({}) should be less than LiNbO₃ ({})",
gaas.saw_velocity,
linbo3.saw_velocity
);
}
#[test]
fn test_magnetoelastic_field_nickel() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::nickel();
let dev = SawMagnetoacoustics::new(saw, mat, 20.0e-9, 0.0).expect("valid parameters");
let h_me = dev.magnetoelastic_field_amplitude();
assert!(h_me > 0.0, "H_me must be positive, got {}", h_me);
assert!(
h_me < 1.0e6,
"H_me should be < 1 MA/m for ε₀=1e-4, got {:.3e} A/m",
h_me
);
}
#[test]
fn test_fmr_frequency_permalloy_zero_field() {
let mat = MagnetoelasticMaterial::permalloy();
let f = mat.fmr_frequency_hz(0.0);
assert!(
f > 0.0,
"FMR frequency must be positive at zero field, got {}",
f
);
assert!(
f < 100.0e9,
"FMR frequency should be < 100 GHz, got {:.3e} Hz",
f
);
}
#[test]
fn test_quality_factor_equals_one_over_alpha() {
let mat = MagnetoelasticMaterial::nickel();
let q = mat.quality_factor(0.0);
let expected = 1.0 / mat.alpha;
assert!(
(q - expected).abs() < 1.0e-10,
"Q factor should be 1/α = {}, got {}",
expected,
q
);
}
#[test]
fn test_saw_source_new_rejects_negative_frequency() {
let result = SawSource::new(PiezoSubstrate::linbo3(), -1.0e9, 1.0e-4);
assert!(result.is_err(), "Negative frequency should be rejected");
}
#[test]
fn test_saw_source_new_rejects_large_strain() {
let result = SawSource::new(PiezoSubstrate::linbo3(), 1.0e9, 0.5);
assert!(result.is_err(), "Strain ≥ 0.1 should be rejected");
}
#[test]
fn test_strain_at_surface_matches_amplitude() {
let saw = SawSource::linbo3_1ghz();
let lambda = saw.wavelength_m();
let x_quarter = lambda / 4.0;
let strain = saw.strain_at_point(x_quarter, 0.0, 0.0);
let expected = saw.strain_amplitude;
let rel_err = (strain - expected).abs() / expected;
assert!(
rel_err < 1.0e-10,
"Surface strain at λ/4 should equal ε₀ = {:.4e}, got {:.4e} (rel_err={:.2e})",
expected,
strain,
rel_err
);
}
#[test]
fn test_strain_decays_with_depth() {
let saw = SawSource::linbo3_1ghz();
let lambda = saw.wavelength_m();
let x_quarter = lambda / 4.0;
let s_surface = saw.strain_at_point(x_quarter, 0.0, 0.0);
let depth = saw.substrate.penetration_depth_m(saw.frequency_hz);
let s_depth = saw.strain_at_point(x_quarter, depth, 0.0);
let ratio = s_depth / s_surface;
let expected_ratio = (-1.0_f64).exp();
assert!(
(ratio - expected_ratio).abs() < 1.0e-6,
"Strain ratio at depth δ should be e⁻¹ = {:.4}, got {:.4}",
expected_ratio,
ratio
);
}
#[test]
fn test_resonance_at_zero_detuning() {
let saw = SawSource::linbo3_3ghz();
let mat = MagnetoelasticMaterial::permalloy();
let helper = SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, 0.0)
.expect("valid parameters");
let h_res = helper.acoustic_fmr_condition_h_ext();
let on_res = SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, h_res)
.expect("valid parameters");
let h_off = h_res + 100.0 * mat.anisotropy_field() + 1.0e5;
let off_res = SawMagnetoacoustics::new(saw, mat, 20.0e-9, h_off).expect("valid parameters");
let theta_on = on_res.precession_cone_angle_rad();
let theta_off = off_res.precession_cone_angle_rad();
assert!(
theta_on > theta_off,
"On-resonance cone angle ({:.4e} rad) should exceed off-resonance ({:.4e} rad)",
theta_on,
theta_off
);
}
#[test]
fn test_precession_angle_grows_with_strain() {
let sub = PiezoSubstrate::linbo3();
let mat = MagnetoelasticMaterial::permalloy();
let saw_small = SawSource::new(sub.clone(), 1.0e9, 1.0e-5).expect("valid");
let saw_large = SawSource::new(sub, 1.0e9, 1.0e-3).expect("valid");
let dev_small = SawMagnetoacoustics::new(saw_small, mat.clone(), 20.0e-9, 0.0)
.expect("valid parameters");
let dev_large =
SawMagnetoacoustics::new(saw_large, mat, 20.0e-9, 0.0).expect("valid parameters");
let theta_small = dev_small.precession_cone_angle_rad();
let theta_large = dev_large.precession_cone_angle_rad();
assert!(
theta_large > theta_small,
"Larger strain (1e-3) should give larger cone angle ({:.4e}) than small strain (1e-5) ({:.4e})",
theta_large,
theta_small
);
}
#[test]
fn test_spin_current_positive_at_resonance() {
let saw = SawSource::linbo3_3ghz();
let mat = MagnetoelasticMaterial::permalloy();
let helper =
SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, 0.0).expect("valid");
let h_res = helper.acoustic_fmr_condition_h_ext();
let device = SawMagnetoacoustics::new(saw, mat, 20.0e-9, h_res).expect("valid parameters");
let g_r = 4.18e18_f64;
let j_s = device.spin_current_density(g_r);
assert!(
j_s > 0.0,
"Spin current density must be positive at resonance, got {:.3e} A/m²",
j_s
);
}
#[test]
fn test_spin_current_zero_for_zero_g_r() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let device = SawMagnetoacoustics::new(saw, mat, 20.0e-9, 0.0).expect("valid");
let j_s = device.spin_current_density(0.0);
assert!(
j_s.abs() < 1.0e-60,
"Spin current with g_r=0 should be ~0, got {:.3e}",
j_s
);
}
#[test]
fn test_acoustic_fmr_field_positive_for_linbo3_1ghz() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let device = SawMagnetoacoustics::new(saw, mat, 20.0e-9, 0.0).expect("valid");
let h_cond = device.acoustic_fmr_condition_h_ext();
assert!(
h_cond >= 0.0,
"acoustic_fmr_condition_h_ext must be ≥ 0, got {}",
h_cond
);
}
#[test]
fn test_resonant_absorption_positive() {
let saw = SawSource::linbo3_3ghz();
let mat = MagnetoelasticMaterial::permalloy();
let helper =
SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, 0.0).expect("valid");
let h_res = helper.acoustic_fmr_condition_h_ext();
let device = SawMagnetoacoustics::new(saw, mat, 20.0e-9, h_res).expect("valid");
let p_abs = device.resonant_absorption();
assert!(
p_abs > 0.0,
"Resonant absorption must be positive, got {:.3e} W/m²",
p_abs
);
}
#[test]
fn test_film_thickness_validation() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let result = SawMagnetoacoustics::new(saw, mat, -5.0e-9, 0.0);
assert!(
result.is_err(),
"Negative film thickness should be rejected"
);
}
#[test]
fn test_h_ext_validation() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let result = SawMagnetoacoustics::new(saw, mat, 20.0e-9, -1.0);
assert!(result.is_err(), "Negative H_ext should be rejected");
}
#[test]
fn test_excited_spinwave_k_equals_saw_k() {
let saw = SawSource::linbo3_1ghz();
let k_saw = saw.substrate.wavenumber(saw.frequency_hz);
let mat = MagnetoelasticMaterial::permalloy();
let exc = SawSpinWaveExcitation::new(saw, mat);
let k_sw = exc.excited_spinwave_k();
assert!(
(k_sw - k_saw).abs() < 1.0e-6,
"Spin-wave k ({:.4e}) must equal SAW k ({:.4e})",
k_sw,
k_saw
);
}
#[test]
fn test_spinwave_frequency_increases_with_field() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let exc = SawSpinWaveExcitation::new(saw, mat);
let f_low = exc.excited_spinwave_frequency(0.0);
let f_high = exc.excited_spinwave_frequency(1.0e5);
assert!(
f_high > f_low,
"Spin-wave frequency should increase with bias field: f(0)={:.3e} f(1e5)={:.3e}",
f_low,
f_high
);
}
#[test]
fn test_phase_velocity_mismatch_zero_at_resonance() {
let saw = SawSource::linbo3_1ghz();
let mat = MagnetoelasticMaterial::permalloy();
let exc = SawSpinWaveExcitation::new(saw.clone(), mat.clone());
let h_k = mat.anisotropy_field();
let h_res = saw.frequency_hz * 2.0 * PI / (GAMMA * MU_0) - h_k;
let h_res_clamped = h_res.max(0.0);
let mismatch = exc.phase_velocity_mismatch(h_res_clamped);
assert!(
mismatch < 1.0e3,
"Phase velocity mismatch at resonance should be near zero, got {:.3e} m/s",
mismatch
);
}
#[test]
fn test_lorentzian_response_peaks_at_resonance() {
let saw = SawSource::linbo3_3ghz();
let mat = MagnetoelasticMaterial::permalloy();
let helper =
SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, 0.0).expect("valid");
let h_res = helper.acoustic_fmr_condition_h_ext();
let on_res =
SawMagnetoacoustics::new(saw.clone(), mat.clone(), 20.0e-9, h_res).expect("valid");
let h_far = h_res + 10.0 * mat.ms;
let off_res = SawMagnetoacoustics::new(saw, mat, 20.0e-9, h_far).expect("valid");
let l_on = on_res.lorentzian_response();
let l_off = off_res.lorentzian_response();
assert!(
l_on > l_off,
"Lorentzian response on-resonance ({:.3e}) should exceed off-resonance ({:.3e})",
l_on,
l_off
);
}
#[test]
fn test_cobalt_higher_fmr_than_permalloy() {
let py = MagnetoelasticMaterial::permalloy();
let co = MagnetoelasticMaterial::cobalt();
let f_py = py.fmr_frequency_hz(0.0);
let f_co = co.fmr_frequency_hz(0.0);
assert!(
f_co > f_py,
"Co FMR ({:.3e} Hz) should exceed Py FMR ({:.3e} Hz) at zero field",
f_co,
f_py
);
}
}