use crate::constants::{GAMMA, MU_0};
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
fn jacobi_real_symmetric(mat: &[Vec<f64>]) -> Result<(Vec<f64>, Vec<Vec<f64>>)> {
let n = mat.len();
let mut a: Vec<Vec<f64>> = mat.to_vec();
let mut q: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = vec![0.0_f64; n];
row[i] = 1.0;
row
})
.collect();
let max_sweeps = 200;
let tol_rel = 1.0e-12_f64;
for _sweep in 0..max_sweeps {
let frob_sq: f64 = (0..n)
.flat_map(|i| (0..n).map(move |j| (i, j)))
.map(|(i, j)| a[i][j].powi(2))
.sum();
let off_sq: f64 = (0..n)
.flat_map(|i| (0..n).filter(move |&j| j != i).map(move |j| (i, j)))
.map(|(i, j)| a[i][j].powi(2))
.sum();
if off_sq < tol_rel * tol_rel * frob_sq || frob_sq == 0.0 {
break;
}
for p in 0..n {
for qp in (p + 1)..n {
let a_pq = a[p][qp];
if a_pq.abs() < tol_rel * 1e-3 * frob_sq.sqrt() {
continue;
}
let a_pp = a[p][p];
let a_qq = a[qp][qp];
let beta = (a_qq - a_pp) / (2.0 * a_pq);
let t = beta.signum() / (beta.abs() + (1.0 + beta * beta).sqrt());
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
for row in a.iter_mut() {
let x = row[p];
let y = row[qp];
row[p] = c * x - s * y;
row[qp] = s * x + c * y;
}
let (p_row, qp_row) = if p < qp {
let (lo, hi) = a.split_at_mut(qp);
(&mut lo[p], &mut hi[0])
} else {
let (lo, hi) = a.split_at_mut(p);
(&mut hi[0], &mut lo[qp])
};
for (x_ref, y_ref) in p_row.iter_mut().zip(qp_row.iter_mut()) {
let x = *x_ref;
let y = *y_ref;
*x_ref = c * x - s * y;
*y_ref = s * x + c * y;
}
for q_row in q.iter_mut() {
let x = q_row[p];
let y = q_row[qp];
q_row[p] = c * x - s * y;
q_row[qp] = s * x + c * y;
}
}
}
}
let mut pairs: Vec<(f64, usize)> = (0..n).map(|i| (a[i][i], i)).collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let eigenvalues: Vec<f64> = pairs.iter().map(|(v, _)| *v).collect();
let eigenvectors: Vec<Vec<f64>> = pairs
.iter()
.map(|(_, col)| (0..n).map(|row| q[row][*col]).collect())
.collect();
Ok((eigenvalues, eigenvectors))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Branch {
Lower,
Upper,
}
#[derive(Debug, Clone)]
pub struct MagnonPolariton {
pub omega_cavity: f64,
pub omega_magnon: f64,
pub coupling_g: f64,
}
impl MagnonPolariton {
pub fn new(omega_cavity: f64, omega_magnon: f64, coupling_g: f64) -> Self {
Self {
omega_cavity,
omega_magnon,
coupling_g,
}
}
pub fn from_yig_cavity() -> Self {
let omega_cavity = 2.0 * std::f64::consts::PI * 10.0e9;
let h_bias = 8.0e4_f64; let omega_magnon = GAMMA * MU_0 * h_bias;
let coupling_g = 2.0 * std::f64::consts::PI * 100.0e6;
Self {
omega_cavity,
omega_magnon,
coupling_g,
}
}
pub fn eigenfrequencies(&self) -> (f64, f64) {
let center = (self.omega_cavity + self.omega_magnon) / 2.0;
let delta = self.omega_cavity - self.omega_magnon;
let disc = ((delta / 2.0).powi(2) + self.coupling_g.powi(2)).sqrt();
(center - disc, center + disc)
}
pub fn hopfield_coefficients(&self) -> ((f64, f64), (f64, f64)) {
let delta = self.omega_cavity - self.omega_magnon;
let g = self.coupling_g;
let disc = ((delta / 2.0).powi(2) + g.powi(2)).sqrt();
let denom_lo_sq = g.powi(2) + (disc + delta / 2.0).powi(2);
let x_lo = if denom_lo_sq > 0.0 {
g / denom_lo_sq.sqrt()
} else {
std::f64::consts::FRAC_1_SQRT_2
};
let y_lo = if denom_lo_sq > 0.0 {
(disc + delta / 2.0) / denom_lo_sq.sqrt()
} else {
std::f64::consts::FRAC_1_SQRT_2
};
let x_up = y_lo;
let y_up = -x_lo;
((x_lo, y_lo), (x_up, y_up))
}
pub fn photon_fraction(&self, branch: Branch) -> f64 {
let ((xl, _), (xu, _)) = self.hopfield_coefficients();
match branch {
Branch::Lower => xl.powi(2),
Branch::Upper => xu.powi(2),
}
}
pub fn magnon_fraction(&self, branch: Branch) -> f64 {
let ((_, yl), (_, yu)) = self.hopfield_coefficients();
match branch {
Branch::Lower => yl.powi(2),
Branch::Upper => yu.powi(2),
}
}
pub fn vacuum_rabi_splitting(&self) -> f64 {
2.0 * self.coupling_g
}
pub fn is_strong_coupling(&self, kappa: f64, gamma: f64) -> bool {
self.coupling_g > (kappa + gamma) / 4.0
}
}
#[derive(Debug, Clone)]
pub struct MultiModePolariton {
pub cavity_freq: f64,
pub magnon_freqs: Vec<f64>,
pub couplings: Vec<f64>,
}
impl MultiModePolariton {
pub fn new(cavity_freq: f64, magnon_freqs: Vec<f64>, couplings: Vec<f64>) -> Result<Self> {
if cavity_freq < 0.0 {
return Err(error::invalid_param(
"cavity_freq",
"cavity frequency must be non-negative",
));
}
if magnon_freqs.len() != couplings.len() {
return Err(error::invalid_param(
"couplings",
"couplings must have the same length as magnon_freqs",
));
}
for (i, &g) in couplings.iter().enumerate() {
if g < 0.0 {
return Err(error::invalid_param(
"couplings",
&format!("coupling[{i}] = {g} is negative; all couplings must be non-negative"),
));
}
}
Ok(Self {
cavity_freq,
magnon_freqs,
couplings,
})
}
fn dim(&self) -> usize {
1 + self.magnon_freqs.len()
}
fn build_matrix(&self) -> Result<CMatrix> {
let n = self.dim();
let mut rows = vec![vec![Complex::ZERO; n]; n];
rows[0][0] = Complex::from_real(self.cavity_freq);
for (i, (&omega_m, &g)) in self
.magnon_freqs
.iter()
.zip(self.couplings.iter())
.enumerate()
{
rows[i + 1][i + 1] = Complex::from_real(omega_m);
rows[0][i + 1] = Complex::from_real(g);
rows[i + 1][0] = Complex::from_real(g);
}
CMatrix::from_rows(rows)
}
fn diagonalise(&self) -> Result<(Vec<f64>, CMatrix)> {
let cmat = self.build_matrix()?;
let n = self.dim();
let real_mat: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|j| cmat.get(i, j).re).collect())
.collect();
let (vals, vecs_cols) = jacobi_real_symmetric(&real_mat)?;
let mut q = CMatrix::zeros(n);
for (col, col_vec) in vecs_cols.iter().enumerate() {
for (row, &v) in col_vec.iter().enumerate() {
q.set(row, col, Complex::from_real(v));
}
}
Ok((vals, q))
}
pub fn eigenfrequencies(&self) -> Result<Vec<f64>> {
let (vals, _) = self.diagonalise()?;
Ok(vals)
}
pub fn mode_compositions(&self) -> Result<Vec<Vec<f64>>> {
let (_, vecs) = self.diagonalise()?;
let n = self.dim();
let mut compositions = Vec::with_capacity(n);
for col in 0..n {
let row_comp: Vec<f64> = (0..n).map(|row| vecs.get(row, col).norm_sq()).collect();
compositions.push(row_comp);
}
Ok(compositions)
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_anticrossing_at_resonance() {
let omega = 2.0 * PI * 10.0e9;
let g = 2.0 * PI * 100.0e6;
let mp = MagnonPolariton::new(omega, omega, g);
let (lo, hi) = mp.eigenfrequencies();
assert!(
approx(hi - lo, 2.0 * g, 1.0e3),
"splitting = {}, 2g = {}",
hi - lo,
2.0 * g
);
}
#[test]
fn test_hopfield_fractions_sum_to_one() {
let omega_c = 2.0 * PI * 10.0e9;
let omega_m = 2.0 * PI * 9.0e9;
let g = 2.0 * PI * 80.0e6;
let mp = MagnonPolariton::new(omega_c, omega_m, g);
let sum_lo = mp.photon_fraction(Branch::Lower) + mp.magnon_fraction(Branch::Lower);
let sum_up = mp.photon_fraction(Branch::Upper) + mp.magnon_fraction(Branch::Upper);
assert!(
approx(sum_lo, 1.0, 1e-12),
"lower branch fractions sum = {sum_lo}"
);
assert!(
approx(sum_up, 1.0, 1e-12),
"upper branch fractions sum = {sum_up}"
);
}
#[test]
fn test_large_positive_detuning() {
let omega_c = 2.0 * PI * 12.0e9;
let omega_m = 2.0 * PI * 9.0e9; let g = 2.0 * PI * 100.0e6;
let mp = MagnonPolariton::new(omega_c, omega_m, g);
let mf_lo = mp.magnon_fraction(Branch::Lower);
assert!(
mf_lo > 0.9,
"lower branch should be mostly magnon when Δ>>g, got magnon_fraction={mf_lo}"
);
}
#[test]
fn test_large_negative_detuning() {
let omega_c = 2.0 * PI * 9.0e9;
let omega_m = 2.0 * PI * 12.0e9; let g = 2.0 * PI * 100.0e6;
let mp = MagnonPolariton::new(omega_c, omega_m, g);
let pf_lo = mp.photon_fraction(Branch::Lower);
assert!(
pf_lo > 0.9,
"lower branch should be mostly photon when ω_m>>ω_c, got photon_fraction={pf_lo}"
);
}
#[test]
fn test_vacuum_rabi_splitting_equals_2g() {
let g = 2.0 * PI * 123.0e6;
let mp = MagnonPolariton::new(1.0e10, 1.0e10, g);
assert!(
approx(mp.vacuum_rabi_splitting(), 2.0 * g, 1.0),
"splitting = {}, 2g = {}",
mp.vacuum_rabi_splitting(),
2.0 * g
);
}
#[test]
fn test_multi_mode_n1_matches_single_mode() {
let omega_c = 2.0 * PI * 10.0e9;
let omega_m = 2.0 * PI * 9.5e9;
let g = 2.0 * PI * 100.0e6;
let single = MagnonPolariton::new(omega_c, omega_m, g);
let (lo_s, hi_s) = single.eigenfrequencies();
let multi = MultiModePolariton::new(omega_c, vec![omega_m], vec![g]).unwrap();
let freqs = multi.eigenfrequencies().unwrap();
assert_eq!(freqs.len(), 2);
assert!(
approx(freqs[0], lo_s, 1.0e3),
"multi lo={} single lo={}",
freqs[0],
lo_s
);
assert!(
approx(freqs[1], hi_s, 1.0e3),
"multi hi={} single hi={}",
freqs[1],
hi_s
);
}
#[test]
fn test_multi_mode_eigenvalues_sorted() {
let omega_c = 2.0 * PI * 10.0e9;
let magnon_freqs = vec![2.0 * PI * 8.0e9, 2.0 * PI * 9.0e9, 2.0 * PI * 11.0e9];
let couplings = vec![2.0 * PI * 80.0e6, 2.0 * PI * 60.0e6, 2.0 * PI * 70.0e6];
let multi = MultiModePolariton::new(omega_c, magnon_freqs, couplings).unwrap();
let freqs = multi.eigenfrequencies().unwrap();
for i in 1..freqs.len() {
assert!(
freqs[i] >= freqs[i - 1],
"eigenvalues not sorted: freqs[{i}]={} < freqs[{}]={}",
freqs[i],
i - 1,
freqs[i - 1]
);
}
}
#[test]
fn test_mode_compositions_sum_to_one() {
let omega_c = 2.0 * PI * 10.0e9;
let magnon_freqs = vec![2.0 * PI * 9.0e9, 2.0 * PI * 10.5e9];
let couplings = vec![2.0 * PI * 100.0e6, 2.0 * PI * 80.0e6];
let multi = MultiModePolariton::new(omega_c, magnon_freqs, couplings).unwrap();
let comps = multi.mode_compositions().unwrap();
for (i, row) in comps.iter().enumerate() {
let s: f64 = row.iter().sum();
assert!(
approx(s, 1.0, 1e-10),
"composition row {i} sums to {s}, expected 1"
);
}
}
}