use std::f64::consts::PI;
use crate::constants::{GAMMA, MU_0};
use crate::error::{self, Result};
use crate::spinwave::quantization::QuantizedModes;
#[derive(Debug, Clone)]
pub struct NanodiskSpinWaves {
pub radius: f64,
pub thickness: f64,
pub h_ext: f64,
pub ms: f64,
pub a_ex: f64,
pub alpha: f64,
}
impl NanodiskSpinWaves {
pub fn new(
radius: f64,
thickness: f64,
h_ext: f64,
ms: f64,
a_ex: f64,
alpha: f64,
) -> Result<Self> {
if radius <= 0.0 {
return Err(error::invalid_param(
"radius",
"disk radius must be positive",
));
}
if thickness <= 0.0 {
return Err(error::invalid_param(
"thickness",
"disk thickness must be positive",
));
}
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 magnetisation 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 {
radius,
thickness,
h_ext,
ms,
a_ex,
alpha,
})
}
pub fn yig_nanodisk(radius_nm: f64) -> Result<Self> {
Self::new(radius_nm * 1e-9, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-4)
}
pub fn permalloy_nanodisk(radius_nm: f64) -> Result<Self> {
Self::new(radius_nm * 1e-9, 20e-9, 1_000_000.0, 8e5, 1.3e-11, 8e-3)
}
pub fn cofeb_nanodisk(radius_nm: f64) -> Result<Self> {
Self::new(radius_nm * 1e-9, 10e-9, 1_200_000.0, 1.05e6, 1.5e-11, 5e-3)
}
#[inline]
pub fn omega_h(&self) -> f64 {
GAMMA.abs() * MU_0 * self.h_ext
}
#[inline]
pub fn omega_m(&self) -> f64 {
GAMMA.abs() * MU_0 * self.ms
}
#[inline]
fn exchange_length_sq(&self) -> f64 {
2.0 * self.a_ex / (MU_0 * self.ms * self.ms)
}
pub fn radial_wavevector(&self, n_rad: usize, m_az: usize) -> Result<f64> {
if n_rad == 0 {
return Err(error::invalid_param(
"n_rad",
"radial quantum number must be >= 1",
));
}
let m_u32 = u32::try_from(m_az).map_err(|_| {
error::invalid_param(
"m_az",
"azimuthal quantum number too large to convert to u32",
)
})?;
let zeros = QuantizedModes::bessel_zeros(m_u32, n_rad);
let alpha = zeros
.get(n_rad - 1)
.copied()
.ok_or_else(|| error::numerical_error("Bessel zero retrieval failed"))?;
Ok(alpha / self.radius)
}
pub fn mode_frequency(&self, n_rad: usize, m_az: usize) -> Result<f64> {
let k = self.radial_wavevector(n_rad, m_az)?;
let lambda_ex_sq = self.exchange_length_sq();
Ok(self.omega_h() + self.omega_m() * lambda_ex_sq * k * k)
}
pub fn mode_profile(&self, n_rad: usize, m_az: usize, r: f64, theta: f64) -> Result<f64> {
if r < 0.0 {
return Err(error::invalid_param(
"r",
"radial coordinate must be non-negative",
));
}
if r > self.radius {
return Err(error::invalid_param(
"r",
"radial coordinate must not exceed the disk radius",
));
}
let k = self.radial_wavevector(n_rad, m_az)?;
let radial = bessel_j(m_az, k * r);
let azimuthal = ((m_az as f64) * theta).cos();
Ok(radial * azimuthal)
}
pub fn mode_spectrum(&self, n_max: usize, m_max: usize) -> Result<Vec<(usize, usize, f64)>> {
if n_max == 0 {
return Err(error::invalid_param(
"n_max",
"n_max must be at least 1 to enumerate modes",
));
}
let mut modes: Vec<(usize, usize, f64)> = Vec::with_capacity(n_max * (m_max + 1));
for n_rad in 1..=n_max {
for m_az in 0..=m_max {
let omega = self.mode_frequency(n_rad, m_az)?;
modes.push((n_rad, m_az, omega));
}
}
modes.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
Ok(modes)
}
pub fn density_of_states(
&self,
omega: f64,
broadening: f64,
n_max: usize,
m_max: usize,
) -> Result<f64> {
if broadening <= 0.0 {
return Err(error::invalid_param(
"broadening",
"broadening must be positive",
));
}
let modes = self.mode_spectrum(n_max, m_max)?;
let gamma = broadening;
let mut rho = 0.0_f64;
for (_, _, omega_mn) in &modes {
let delta = omega - *omega_mn;
rho += (gamma / PI) / (delta * delta + gamma * gamma);
}
Ok(rho)
}
pub fn group_velocity(&self, n_rad: usize, m_az: usize) -> Result<f64> {
let k = self.radial_wavevector(n_rad, m_az)?;
Ok(2.0 * self.omega_m() * self.exchange_length_sq() * k)
}
pub fn propagation_length(&self, n_rad: usize, m_az: usize) -> Result<f64> {
if self.alpha <= 0.0 {
return Err(error::invalid_param(
"alpha",
"propagation length is undefined for zero damping",
));
}
let omega = self.mode_frequency(n_rad, m_az)?;
if omega <= 0.0 {
return Err(error::numerical_error(
"mode frequency is non-positive; propagation length undefined",
));
}
let vg = self.group_velocity(n_rad, m_az)?;
Ok(vg / (self.alpha * omega))
}
}
pub(crate) fn bessel_j(n: usize, x: f64) -> f64 {
if x == 0.0 {
return if n == 0 { 1.0 } else { 0.0 };
}
let ax = x.abs();
if ax > 25.0 + n as f64 {
let phase = ax - (n as f64) * std::f64::consts::FRAC_PI_2 - std::f64::consts::FRAC_PI_4;
let envelope = (2.0 / (PI * ax)).sqrt();
let value = envelope * phase.cos();
if x < 0.0 && (n % 2 == 1) {
-value
} else {
value
}
} else {
let half_x = x / 2.0;
let mut term = 1.0_f64;
for k in 1..=n {
term *= half_x / (k as f64);
}
let mut sum = term;
let z2 = -half_x * half_x;
let mut k = 0_usize;
loop {
let denom = ((k + 1) as f64) * ((n + k + 1) as f64);
let ratio = z2 / denom;
term *= ratio;
sum += term;
k += 1;
if term.abs() < 1e-18 * sum.abs().max(1e-300) {
break;
}
if k > 200 {
break;
}
}
sum
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL_REL: f64 = 1e-3;
#[test]
fn test_construct_and_parameters() {
let nd = NanodiskSpinWaves::new(500e-9, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-4)
.expect("valid parameters");
assert!(nd.radius > 0.0);
assert!(nd.thickness > 0.0);
assert!(nd.h_ext >= 0.0);
assert!(nd.ms > 0.0);
assert!(nd.a_ex > 0.0);
assert!(nd.alpha >= 0.0);
assert!(nd.omega_m() > 0.0);
}
#[test]
fn test_yig_preset_realistic() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid YIG nanodisk");
assert!((nd.ms - 1.4e5).abs() < 1.0);
assert!((nd.a_ex - 3.5e-12).abs() < 1e-20);
assert!((nd.thickness - 20e-9).abs() < 1e-15);
let omega = nd.mode_frequency(1, 0).expect("valid mode");
let f_ghz = omega / (2.0 * PI * 1e9);
assert!(
(0.5..200.0).contains(&f_ghz),
"YIG (1,0) frequency should be in 0.5-200 GHz range, got {f_ghz:.2} GHz"
);
}
#[test]
fn test_radial_wavevector_first_j0_zero() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let k = nd.radial_wavevector(1, 0).expect("valid");
let expected = 2.4048 / nd.radius;
let rel = (k - expected).abs() / expected;
assert!(rel < TOL_REL, "k(1,0)={k}, expected {expected}");
}
#[test]
fn test_mode_frequency_increases_with_n_rad() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let f1 = nd.mode_frequency(1, 0).expect("valid");
let f2 = nd.mode_frequency(2, 0).expect("valid");
let f3 = nd.mode_frequency(3, 0).expect("valid");
assert!(f2 > f1, "f(2,0)={f2} should exceed f(1,0)={f1}");
assert!(f3 > f2, "f(3,0)={f3} should exceed f(2,0)={f2}");
}
#[test]
fn test_mode_frequency_increases_with_m_az() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let f00 = nd.mode_frequency(1, 0).expect("valid");
let f01 = nd.mode_frequency(1, 1).expect("valid");
let f02 = nd.mode_frequency(1, 2).expect("valid");
assert!(f01 > f00, "f(1,1)={f01} should exceed f(1,0)={f00}");
assert!(f02 > f01, "f(1,2)={f02} should exceed f(1,1)={f01}");
}
#[test]
fn test_mode_spectrum_sorted() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let spectrum = nd.mode_spectrum(4, 3).expect("valid");
assert_eq!(spectrum.len(), 4 * 4);
for w in spectrum.windows(2) {
assert!(
w[1].2 >= w[0].2,
"spectrum unsorted: {} > {}",
w[0].2,
w[1].2
);
}
}
#[test]
fn test_mode_profile_boundary_condition_m0() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let r = nd.radius;
let amp = nd.mode_profile(1, 0, r, 0.0).expect("valid");
assert!(amp.abs() < 1e-3, "boundary amplitude should vanish: {amp}");
}
#[test]
fn test_mode_profile_known_value_at_origin() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let amp = nd.mode_profile(1, 0, 0.0, 0.0).expect("valid");
assert!(
(amp - 1.0).abs() < 1e-12,
"profile at r=0 should be 1: {amp}"
);
let amp1 = nd.mode_profile(1, 1, 0.0, 0.7).expect("valid");
assert!(amp1.abs() < 1e-12, "J_1(0)=0 so profile = 0: {amp1}");
}
#[test]
fn test_density_of_states_positive() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let omega0 = nd.mode_frequency(1, 0).expect("valid");
let rho = nd.density_of_states(omega0, 1e7, 3, 2).expect("valid");
assert!(rho > 0.0, "DOS must be positive: {rho}");
}
#[test]
fn test_group_velocity_positive() {
let nd = NanodiskSpinWaves::yig_nanodisk(500.0).expect("valid");
let vg = nd.group_velocity(2, 1).expect("valid");
assert!(vg > 0.0, "group velocity must be positive: {vg}");
}
#[test]
fn test_propagation_length_inverse_alpha() {
let nd_low =
NanodiskSpinWaves::new(500e-9, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-4).expect("valid");
let nd_high =
NanodiskSpinWaves::new(500e-9, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-3).expect("valid");
let l_low = nd_low.propagation_length(1, 0).expect("valid");
let l_high = nd_high.propagation_length(1, 0).expect("valid");
assert!(l_low > 0.0 && l_high > 0.0);
let ratio = l_low / l_high;
assert!(
(ratio - 10.0).abs() < 0.5,
"L should scale as 1/α: ratio={ratio}"
);
}
#[test]
fn test_preset_constructions() {
let _yig = NanodiskSpinWaves::yig_nanodisk(200.0).expect("YIG preset");
let py = NanodiskSpinWaves::permalloy_nanodisk(150.0).expect("Py preset");
let cofeb = NanodiskSpinWaves::cofeb_nanodisk(100.0).expect("CoFeB preset");
assert!((py.ms - 8e5).abs() < 1.0);
assert!((cofeb.ms - 1.05e6).abs() < 1.0);
}
#[test]
fn test_error_on_invalid_radius() {
let r = NanodiskSpinWaves::new(0.0, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-4);
assert!(r.is_err());
let r = NanodiskSpinWaves::new(-1e-9, 20e-9, 40_000.0, 1.4e5, 3.5e-12, 1e-4);
assert!(r.is_err());
}
#[test]
fn test_bessel_j_basic_values() {
assert!((bessel_j(0, 0.0) - 1.0).abs() < 1e-15);
assert!(bessel_j(1, 0.0).abs() < 1e-15);
assert!(bessel_j(0, 2.4048).abs() < 1e-4);
assert!(bessel_j(1, 3.8317).abs() < 1e-4);
assert!((bessel_j(0, 1.0) - 0.7651976865).abs() < 1e-6);
assert!(bessel_j(0, 30.0).abs() < 1.0);
assert!(bessel_j(1, 30.0).abs() < 1.0);
}
}