use std::f64::consts::PI;
use crate::error::{self, Result};
#[derive(Debug, Clone)]
pub struct BrillouinScattering {
pub magnon_frequency: f64,
pub optical_frequency: f64,
pub coupling: f64,
}
impl BrillouinScattering {
pub fn new(magnon_frequency: f64, optical_frequency: f64, coupling: f64) -> Result<Self> {
if magnon_frequency < 0.0 {
return Err(error::invalid_param(
"magnon_frequency",
"magnon frequency must be non-negative",
));
}
if optical_frequency < 0.0 {
return Err(error::invalid_param(
"optical_frequency",
"optical photon frequency must be non-negative",
));
}
if coupling < 0.0 {
return Err(error::invalid_param(
"coupling",
"optomagnonic coupling must be non-negative",
));
}
Ok(Self {
magnon_frequency,
optical_frequency,
coupling,
})
}
pub fn yig_optical() -> Self {
Self {
magnon_frequency: 2.0 * PI * 10.0e9,
optical_frequency: 2.0 * PI * 193.0e12,
coupling: 2.0 * PI * 1.0e6,
}
}
pub fn scattering_rate(&self, detuning: f64, magnon_density: f64) -> f64 {
let g = self.coupling;
let denom = g.powi(2) + detuning.powi(2);
g.powi(2) * magnon_density / denom
}
pub fn frequency_shift(&self, stokes: bool) -> f64 {
if stokes {
self.optical_frequency - self.magnon_frequency
} else {
self.optical_frequency + self.magnon_frequency
}
}
pub fn cross_section(&self, scattering_angle_rad: f64) -> f64 {
let x = 2.0 * scattering_angle_rad / PI - 1.0;
1.0 / (1.0 + x.powi(2))
}
}
#[derive(Debug, Clone)]
pub struct OptomagnonicCoupling {
pub g_om: f64,
pub kerr_strength: f64,
}
impl OptomagnonicCoupling {
pub fn new(g_om: f64, kerr_strength: f64) -> Result<Self> {
if g_om < 0.0 {
return Err(error::invalid_param(
"g_om",
"optomagnonic coupling must be non-negative",
));
}
if kerr_strength < 0.0 {
return Err(error::invalid_param(
"kerr_strength",
"Kerr coefficient must be non-negative",
));
}
Ok(Self {
g_om,
kerr_strength,
})
}
pub fn yig() -> Self {
Self {
g_om: 2.0 * PI * 1.0e6,
kerr_strength: 2.0 * PI * 100.0,
}
}
pub fn effective_coupling(&self, n_optical_photons: f64) -> f64 {
self.g_om * n_optical_photons.max(0.0).sqrt()
}
pub fn kerr_shift(&self, n_photons: f64) -> f64 {
self.kerr_strength * n_photons
}
}
#[derive(Debug, Clone)]
pub struct MicrowaveToOptical {
pub microwave_freq: f64,
pub magnon_freq: f64,
pub optical_freq: f64,
pub g_me: f64,
pub g_mo: f64,
pub kappa_m: f64,
pub kappa_o: f64,
pub gamma_m: f64,
}
impl MicrowaveToOptical {
pub fn new(
microwave_freq: f64,
magnon_freq: f64,
optical_freq: f64,
g_me: f64,
g_mo: f64,
kappa_m: f64,
kappa_o: f64,
gamma_m: f64,
) -> Result<Self> {
let params = [
("microwave_freq", microwave_freq),
("magnon_freq", magnon_freq),
("optical_freq", optical_freq),
("g_me", g_me),
("g_mo", g_mo),
("kappa_m", kappa_m),
("kappa_o", kappa_o),
("gamma_m", gamma_m),
];
for (name, val) in ¶ms {
if *val < 0.0 {
return Err(error::invalid_param(
name,
"transducer parameter must be non-negative",
));
}
}
Ok(Self {
microwave_freq,
magnon_freq,
optical_freq,
g_me,
g_mo,
kappa_m,
kappa_o,
gamma_m,
})
}
pub fn yig_telecom() -> Self {
Self {
microwave_freq: 2.0 * PI * 10.0e9,
magnon_freq: 2.0 * PI * 10.0e9,
optical_freq: 2.0 * PI * 193.0e12,
g_me: 2.0 * PI * 100.0e6,
g_mo: 2.0 * PI * 1.0e6,
kappa_m: 2.0 * PI * 1.0e6,
kappa_o: 2.0 * PI * 10.0e6,
gamma_m: 2.0 * PI * 1.0e6,
}
}
fn cooperativity_me(&self) -> f64 {
self.g_me.powi(2) / (self.kappa_m * self.gamma_m)
}
fn cooperativity_mo(&self) -> f64 {
self.g_mo.powi(2) / (self.kappa_o * self.gamma_m)
}
pub fn conversion_efficiency(&self) -> f64 {
let c_me = self.cooperativity_me();
let c_mo = self.cooperativity_mo();
let denom = (1.0 + c_me + c_mo).powi(2);
if denom == 0.0 {
return 0.0;
}
(4.0 * c_me * c_mo / denom).min(1.0)
}
pub fn bandwidth(&self) -> f64 {
let c_me = self.cooperativity_me();
let c_mo = self.cooperativity_mo();
self.gamma_m * (1.0 + c_me + c_mo)
}
pub fn is_impedance_matched(&self) -> bool {
let c_me = self.cooperativity_me();
let c_mo = self.cooperativity_mo();
if c_me + c_mo == 0.0 {
return true;
}
((c_me - c_mo) / (c_me + c_mo)).abs() < 0.1
}
}
#[derive(Debug, Clone)]
pub struct MagnonicFrequencyComb {
pub f_repetition: f64,
pub n_lines: usize,
pub decoherence: f64,
pub carrier: f64,
}
impl MagnonicFrequencyComb {
pub fn new(f_repetition: f64, n_lines: usize, decoherence: f64, carrier: f64) -> Result<Self> {
if f_repetition < 0.0 {
return Err(error::invalid_param(
"f_repetition",
"repetition rate must be non-negative",
));
}
if n_lines == 0 {
return Err(error::invalid_param(
"n_lines",
"comb must have at least one line",
));
}
if decoherence < 0.0 {
return Err(error::invalid_param(
"decoherence",
"decoherence must be non-negative",
));
}
if carrier < 0.0 {
return Err(error::invalid_param(
"carrier",
"carrier frequency must be non-negative",
));
}
Ok(Self {
f_repetition,
n_lines,
decoherence,
carrier,
})
}
pub fn yig_microwave(f_rep_ghz: f64) -> Self {
Self {
f_repetition: f_rep_ghz * 1.0e9,
n_lines: 21,
decoherence: 1.0e6,
carrier: 10.0e9,
}
}
pub fn comb_spectrum(&self) -> Vec<f64> {
let half = (self.n_lines / 2) as i64;
let start = -half;
(start..start + self.n_lines as i64)
.map(|n| self.carrier + n as f64 * self.f_repetition)
.collect()
}
pub fn phase_noise(&self, offset_freq_hz: f64) -> f64 {
if offset_freq_hz <= 0.0 {
return f64::INFINITY;
}
let omega_offset = 2.0 * PI * offset_freq_hz;
self.decoherence.powi(2) / omega_offset.powi(2)
}
pub fn coherence_time(&self) -> f64 {
if self.decoherence == 0.0 {
return f64::INFINITY;
}
1.0 / (PI * self.decoherence)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_scattering_rate_positive() {
let bls = BrillouinScattering::yig_optical();
let rate = bls.scattering_rate(0.0, 5.0);
assert!(rate > 0.0, "scattering rate must be positive, got {rate}");
}
#[test]
fn test_stokes_lower_frequency() {
let bls = BrillouinScattering::yig_optical();
let f_stokes = bls.frequency_shift(true);
assert!(
f_stokes < bls.optical_frequency,
"Stokes frequency {f_stokes} should be below optical {freq}",
freq = bls.optical_frequency
);
}
#[test]
fn test_antistokes_higher_frequency() {
let bls = BrillouinScattering::yig_optical();
let f_as = bls.frequency_shift(false);
assert!(
f_as > bls.optical_frequency,
"anti-Stokes frequency {f_as} should be above optical {freq}",
freq = bls.optical_frequency
);
}
#[test]
fn test_cross_section_at_pi_over_2() {
let bls = BrillouinScattering::yig_optical();
let at_90 = bls.cross_section(PI / 2.0);
let at_30 = bls.cross_section(PI / 6.0);
let at_150 = bls.cross_section(5.0 * PI / 6.0);
assert!(
approx(at_90, 1.0, 1e-12),
"cross section at 90° should be 1.0, got {at_90}"
);
assert!(at_90 > at_30, "σ(90°) should exceed σ(30°)");
assert!(at_90 > at_150, "σ(90°) should exceed σ(150°)");
}
#[test]
fn test_conversion_efficiency_in_0_1() {
let t = MicrowaveToOptical::yig_telecom();
let eta = t.conversion_efficiency();
assert!(
(0.0..=1.0).contains(&eta),
"efficiency {eta} must be in [0, 1]"
);
}
#[test]
fn test_conversion_efficiency_unit_cooperativity() {
let gamma_m = 1.0e6_f64;
let kappa_m = 1.0e6_f64;
let kappa_o = 1.0e6_f64;
let g_me = (kappa_m * gamma_m).sqrt();
let g_mo = (kappa_o * gamma_m).sqrt();
let t = MicrowaveToOptical::new(
1.0e10,
1.0e10,
2.0 * PI * 193.0e12,
g_me,
g_mo,
kappa_m,
kappa_o,
gamma_m,
)
.unwrap();
let eta = t.conversion_efficiency();
let expected = 4.0 / 9.0;
assert!(
approx(eta, expected, 1e-10),
"η at unit cooperativity should be 4/9={expected:.6}, got {eta:.6}"
);
}
#[test]
fn test_bandwidth_increases_with_c() {
let t_low = MicrowaveToOptical::new(
1.0e10,
1.0e10,
2.0 * PI * 193.0e12,
1.0e5,
1.0e5,
1.0e6,
1.0e6,
1.0e6,
)
.unwrap();
let t_high = MicrowaveToOptical::new(
1.0e10,
1.0e10,
2.0 * PI * 193.0e12,
1.0e7,
1.0e7,
1.0e6,
1.0e6,
1.0e6,
)
.unwrap();
assert!(
t_high.bandwidth() > t_low.bandwidth(),
"higher cooperativity should yield larger bandwidth"
);
}
#[test]
fn test_comb_spectrum_length() {
let comb = MagnonicFrequencyComb::new(100.0e6, 15, 1.0e6, 10.0e9).unwrap();
let spec = comb.comb_spectrum();
assert_eq!(
spec.len(),
15,
"comb spectrum should have 15 lines, got {}",
spec.len()
);
}
#[test]
fn test_comb_spacing_equals_f_rep() {
let f_rep = 500.0e6_f64;
let comb = MagnonicFrequencyComb::new(f_rep, 11, 1.0e6, 10.0e9).unwrap();
let spec = comb.comb_spectrum();
for i in 1..spec.len() {
let spacing = spec[i] - spec[i - 1];
assert!(
approx(spacing, f_rep, 1.0),
"spacing at index {i} is {spacing}, expected {f_rep}"
);
}
}
#[test]
fn test_phase_noise_decreases_with_offset() {
let comb = MagnonicFrequencyComb::yig_microwave(0.1);
let l_low = comb.phase_noise(1.0e3);
let l_mid = comb.phase_noise(1.0e6);
let l_high = comb.phase_noise(1.0e9);
assert!(
l_low > l_mid && l_mid > l_high,
"phase noise should decrease with offset: L(1kHz)={l_low:.3e}, L(1MHz)={l_mid:.3e}, L(1GHz)={l_high:.3e}"
);
}
}