use num_complex::Complex64;
use crate::error::{OxiPhotonError, Result};
#[allow(dead_code)]
const HBAR: f64 = 1.054_571_817e-34;
#[derive(Debug, Clone)]
pub struct JaynesCummings {
pub omega_cavity: f64,
pub omega_atom: f64,
pub coupling_g: f64,
pub kappa: f64,
pub gamma: f64,
pub n_photons_max: usize,
}
impl JaynesCummings {
pub fn new(
omega_cavity: f64,
omega_atom: f64,
coupling_g: f64,
kappa: f64,
gamma: f64,
n_max: usize,
) -> Result<Self> {
if omega_cavity <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"omega_cavity must be positive, got {omega_cavity}"
)));
}
if omega_atom <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"omega_atom must be positive, got {omega_atom}"
)));
}
if coupling_g <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"coupling_g must be positive, got {coupling_g}"
)));
}
if kappa <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"kappa must be positive, got {kappa}"
)));
}
if gamma <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"gamma must be positive, got {gamma}"
)));
}
if n_max == 0 {
return Err(OxiPhotonError::NumericalError(
"n_photons_max must be at least 1".to_string(),
));
}
Ok(Self {
omega_cavity,
omega_atom,
coupling_g,
kappa,
gamma,
n_photons_max: n_max,
})
}
#[inline]
pub fn detuning(&self) -> f64 {
self.omega_atom - self.omega_cavity
}
#[inline]
pub fn vacuum_rabi_splitting(&self) -> f64 {
2.0 * self.coupling_g
}
#[inline]
pub fn rabi_frequency(&self, n_photons: usize) -> f64 {
2.0 * self.coupling_g * ((n_photons + 1) as f64).sqrt()
}
pub fn dressed_state_energies(&self, n: usize) -> (f64, f64) {
let delta = self.detuning();
let generalized =
(delta * delta + 4.0 * self.coupling_g * self.coupling_g * (n + 1) as f64).sqrt();
let center = (n as f64 + 0.5) * self.omega_cavity;
(center - 0.5 * generalized, center + 0.5 * generalized)
}
pub fn hamiltonian_matrix(&self) -> Vec<Vec<Complex64>> {
let n = self.n_photons_max;
let dim = 2 * (n + 1);
let mut h = vec![vec![Complex64::new(0.0, 0.0); dim]; dim];
for fock in 0..=n {
let ig = 2 * fock;
let ie = 2 * fock + 1;
let e_cav = fock as f64 * self.omega_cavity;
h[ig][ig] = Complex64::new(e_cav - self.omega_atom * 0.5, 0.0);
h[ie][ie] = Complex64::new(e_cav + self.omega_atom * 0.5, 0.0);
if fock < n {
let ig_next = 2 * (fock + 1); let coupling = self.coupling_g * ((fock + 1) as f64).sqrt();
h[ie][ig_next] = Complex64::new(coupling, 0.0);
h[ig_next][ie] = Complex64::new(coupling, 0.0);
}
}
h
}
pub fn rabi_oscillation(
&self,
n_photons: usize,
t_max_ns: f64,
n_steps: usize,
) -> Result<Vec<(f64, f64)>> {
if n_steps < 2 {
return Err(OxiPhotonError::NumericalError(
"n_steps must be ≥ 2".to_string(),
));
}
let delta = self.detuning();
let omega_tilde_sq =
delta * delta + 4.0 * self.coupling_g * self.coupling_g * (n_photons + 1) as f64;
let omega_tilde = omega_tilde_sq.sqrt();
let t_max_s = t_max_ns * 1e-9;
let mut result = Vec::with_capacity(n_steps);
for i in 0..n_steps {
let t = t_max_s * (i as f64) / ((n_steps - 1) as f64);
let w = if omega_tilde_sq < f64::EPSILON {
-1.0
} else {
-(delta * delta
+ 4.0
* self.coupling_g
* self.coupling_g
* (n_photons + 1) as f64
* (omega_tilde * t).cos())
/ omega_tilde_sq
};
result.push((t * 1e9, w));
}
Ok(result)
}
pub fn collapse_time(&self, mean_photon_number: f64) -> f64 {
mean_photon_number.sqrt() / self.coupling_g
}
pub fn revival_time(&self, mean_photon_number: f64) -> f64 {
2.0 * std::f64::consts::PI * mean_photon_number.sqrt() / self.coupling_g
}
pub fn is_strong_coupling(&self) -> bool {
let max_rate = self.kappa.max(self.gamma);
self.coupling_g > max_rate / 2.0
}
pub fn purcell_factor(&self) -> f64 {
4.0 * self.coupling_g * self.coupling_g / (self.kappa * self.gamma)
}
pub fn cooperativity(&self) -> f64 {
self.coupling_g * self.coupling_g / (self.kappa * self.gamma)
}
pub fn transmission_spectrum(
&self,
omega_range: (f64, f64),
n_pts: usize,
) -> Result<Vec<(f64, f64)>> {
if n_pts < 2 {
return Err(OxiPhotonError::NumericalError(
"n_pts must be ≥ 2".to_string(),
));
}
let (e_minus, e_plus) = self.dressed_state_energies(0);
let half_kappa = self.kappa / 2.0;
let half_gamma_4 = self.gamma / 4.0;
let denom_extra = half_kappa + half_gamma_4;
let mut result: Vec<(f64, f64)> = Vec::with_capacity(n_pts);
let mut t_max = 0.0_f64;
for i in 0..n_pts {
let omega =
omega_range.0 + (omega_range.1 - omega_range.0) * (i as f64) / ((n_pts - 1) as f64);
let lorentz = |omega_res: f64| -> f64 {
let denum = (omega - omega_res).powi(2) + denom_extra.powi(2);
(half_kappa * half_kappa) / denum
};
let t = lorentz(e_minus) + lorentz(e_plus);
result.push((omega, t));
if t > t_max {
t_max = t;
}
}
if t_max > 0.0 {
for entry in &mut result {
entry.1 /= t_max;
}
}
Ok(result)
}
pub fn dispersive_shift(&self) -> Result<f64> {
let delta = self.detuning();
if delta.abs() < f64::EPSILON {
return Err(OxiPhotonError::NumericalError(
"Dispersive shift is undefined at resonance (Δ = 0)".to_string(),
));
}
Ok(self.coupling_g * self.coupling_g / delta)
}
pub fn qnd_readout_snr(&self, kappa_ex: f64, measurement_time_s: f64) -> Result<f64> {
let chi = self.dispersive_shift()?;
let phase_per_photon = (2.0 * chi.abs()) / (self.kappa / 2.0);
let snr = phase_per_photon * (kappa_ex * measurement_time_s).sqrt();
Ok(snr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn test_model() -> JaynesCummings {
let two_pi = 2.0 * std::f64::consts::PI;
JaynesCummings::new(
2.0 * two_pi * 1e9, 2.0 * two_pi * 1e9, two_pi * 10e6, two_pi * 1e6, two_pi * 0.1e6, 20,
)
.expect("valid model")
}
#[test]
fn test_vacuum_rabi_splitting() {
let jc = test_model();
let two_pi = 2.0 * std::f64::consts::PI;
assert_relative_eq!(
jc.vacuum_rabi_splitting(),
2.0 * two_pi * 10e6,
epsilon = 1e-6
);
}
#[test]
fn test_rabi_frequency_n_photons() {
let jc = test_model();
let two_pi = 2.0 * std::f64::consts::PI;
assert_relative_eq!(jc.rabi_frequency(0), 2.0 * two_pi * 10e6, epsilon = 1e-6);
assert_relative_eq!(jc.rabi_frequency(3), 4.0 * two_pi * 10e6, epsilon = 1e-6);
}
#[test]
fn test_strong_coupling_condition() {
let jc = test_model();
assert!(jc.is_strong_coupling());
let two_pi = 2.0 * std::f64::consts::PI;
let weak = JaynesCummings::new(
two_pi * 1e9,
two_pi * 1e9,
two_pi * 0.1e6, two_pi * 10e6, two_pi * 0.1e6,
5,
)
.expect("valid model");
assert!(!weak.is_strong_coupling());
}
#[test]
fn test_purcell_factor() {
let jc = test_model();
let two_pi = 2.0 * std::f64::consts::PI;
let g = two_pi * 10e6;
let kappa = two_pi * 1e6;
let gamma = two_pi * 0.1e6;
let expected = 4.0 * g * g / (kappa * gamma);
assert_relative_eq!(jc.purcell_factor(), expected, epsilon = 1e-6);
}
#[test]
fn test_rabi_oscillation_cosine() {
let jc = test_model();
let omega0 = jc.rabi_frequency(0);
let t_pi_ns = (std::f64::consts::PI / omega0) * 1e9;
let steps = 1000;
let oscillation = jc.rabi_oscillation(0, t_pi_ns, steps).expect("ok");
let w_at_0 = oscillation[0].1;
let w_at_pi = oscillation[steps - 1].1;
assert_relative_eq!(w_at_0, -1.0, epsilon = 1e-10);
assert_relative_eq!(w_at_pi, 1.0, epsilon = 1e-6);
}
#[test]
fn test_dispersive_shift() {
let two_pi = 2.0 * std::f64::consts::PI;
let g = two_pi * 10e6;
let delta = two_pi * 100e6;
let jc = JaynesCummings::new(
two_pi * 1e9,
two_pi * 1e9 + delta,
g,
two_pi * 1e6,
two_pi * 0.1e6,
10,
)
.expect("valid model");
let chi = jc.dispersive_shift().expect("off-resonant");
let expected = g * g / delta;
assert_relative_eq!(chi, expected, max_relative = 1e-10);
}
#[test]
fn test_dressed_state_splitting() {
let jc = test_model(); let (e_minus, e_plus) = jc.dressed_state_energies(0);
assert_relative_eq!(e_plus - e_minus, 2.0 * jc.coupling_g, max_relative = 1e-10);
}
#[test]
fn test_hamiltonian_hermitian() {
let jc = test_model();
let h = jc.hamiltonian_matrix();
let dim = h.len();
for (i, _) in h.iter().enumerate().take(dim) {
for (j, _) in h.iter().enumerate().take(dim) {
assert_relative_eq!(h[i][j].re, h[j][i].re, epsilon = 1e-10);
assert_relative_eq!(h[i][j].im, -h[j][i].im, epsilon = 1e-10);
}
}
}
}