use num_complex::Complex64;
use std::f64::consts::PI;
pub use crate::nonlinear_crystal::PhaseMatchingType;
const C: f64 = 2.997_924_58e8;
const EPSILON_0: f64 = 8.854_187_817e-12;
#[derive(Debug, Clone)]
pub enum SpdcCrystal {
Ppktp,
Ppln,
Bbo,
Ktp,
Periodically {
period_um: f64,
material: String,
d_eff: f64,
},
}
impl SpdcCrystal {
pub fn d_eff_pm_per_v(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => 9.5, SpdcCrystal::Ppln => 16.5, SpdcCrystal::Bbo => 2.0, SpdcCrystal::Ktp => 3.7, SpdcCrystal::Periodically { d_eff, .. } => *d_eff,
}
}
pub fn group_velocity_signal(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => C / 1.745, SpdcCrystal::Ppln => C / 2.211, SpdcCrystal::Bbo => C / 1.668, SpdcCrystal::Ktp => C / 1.745,
SpdcCrystal::Periodically { .. } => C / 2.0,
}
}
pub fn group_velocity_idler(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => C / 1.830, SpdcCrystal::Ppln => C / 2.211, SpdcCrystal::Bbo => C / 1.730, SpdcCrystal::Ktp => C / 1.830,
SpdcCrystal::Periodically { .. } => C / 2.0,
}
}
pub fn gvd_signal(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => -1.8e-26, SpdcCrystal::Ppln => 1.0e-25, SpdcCrystal::Bbo => -6.0e-26, SpdcCrystal::Ktp => -1.8e-26,
SpdcCrystal::Periodically { .. } => -1.0e-26,
}
}
pub fn gvd_idler(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => -1.5e-26,
SpdcCrystal::Ppln => 1.0e-25,
SpdcCrystal::Bbo => -4.0e-26,
SpdcCrystal::Ktp => -1.5e-26,
SpdcCrystal::Periodically { .. } => -1.0e-26,
}
}
fn refractive_index_pump(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => 1.738, SpdcCrystal::Ppln => 2.156, SpdcCrystal::Bbo => 1.672, SpdcCrystal::Ktp => 1.738,
SpdcCrystal::Periodically { .. } => 2.0,
}
}
fn refractive_index_signal(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => 1.736,
SpdcCrystal::Ppln => 2.211,
SpdcCrystal::Bbo => 1.655,
SpdcCrystal::Ktp => 1.736,
SpdcCrystal::Periodically { .. } => 2.0,
}
}
fn refractive_index_idler(&self) -> f64 {
match self {
SpdcCrystal::Ppktp => 1.818,
SpdcCrystal::Ppln => 2.211,
SpdcCrystal::Bbo => 1.720,
SpdcCrystal::Ktp => 1.818,
SpdcCrystal::Periodically { .. } => 2.0,
}
}
pub fn group_velocity_mismatch(&self) -> f64 {
1.0 / self.group_velocity_signal() - 1.0 / self.group_velocity_idler()
}
}
#[derive(Debug, Clone)]
pub struct JointSpectralAmplitude {
pub signal_freqs: Vec<f64>,
pub idler_freqs: Vec<f64>,
pub amplitude: Vec<Vec<Complex64>>,
}
#[derive(Debug, Clone)]
pub struct SchmidtDecomposition {
pub eigenvalues: Vec<f64>,
pub schmidt_number: f64,
}
#[derive(Debug, Clone)]
pub struct SpdcSource {
pub crystal: SpdcCrystal,
pub pump_wavelength: f64,
pub pump_power_mw: f64,
pub crystal_length_mm: f64,
pub phase_matching: PhaseMatchingType,
}
impl SpdcSource {
pub fn new_ppktp_1550(pump_power_mw: f64, length_mm: f64) -> Self {
Self {
crystal: SpdcCrystal::Ppktp,
pump_wavelength: 775e-9,
pump_power_mw,
crystal_length_mm: length_mm,
phase_matching: PhaseMatchingType::TypeII,
}
}
pub fn new_ppln_telecom(pump_power_mw: f64, length_mm: f64) -> Self {
Self {
crystal: SpdcCrystal::Ppln,
pump_wavelength: 780e-9,
pump_power_mw,
crystal_length_mm: length_mm,
phase_matching: PhaseMatchingType::QuasiPm { period_um: 19.3 },
}
}
pub fn signal_wavelength(&self) -> f64 {
2.0 * self.pump_wavelength
}
pub fn idler_wavelength(&self) -> f64 {
let lambda_p = self.pump_wavelength;
let lambda_s = self.signal_wavelength();
let inv_lambda_i = 1.0 / lambda_p - 1.0 / lambda_s;
if inv_lambda_i > 0.0 {
1.0 / inv_lambda_i
} else {
lambda_s
}
}
pub fn crystal_length_m(&self) -> f64 {
self.crystal_length_mm * 1e-3
}
pub fn pair_rate_per_second(&self) -> f64 {
let d_eff_si = self.crystal.d_eff_pm_per_v() * 1e-12; let l = self.crystal_length_m();
let p = self.pump_power_mw * 1e-3; let lambda_s = self.signal_wavelength();
let lambda_i = self.idler_wavelength();
let n_s = self.crystal.refractive_index_signal();
let n_i = self.crystal.refractive_index_idler();
let n_p = self.crystal.refractive_index_pump();
let omega_s = 2.0 * PI * C / lambda_s;
let omega_i = 2.0 * PI * C / lambda_i;
let a_eff = PI * (10e-6_f64).powi(2);
let numerator = omega_s * omega_i * d_eff_si.powi(2) * l.powi(2) * p;
let denominator = 2.0 * n_s * n_i * n_p * EPSILON_0 * C.powi(3) * a_eff;
numerator / denominator
}
pub fn spectral_brightness(&self) -> f64 {
let r = self.pair_rate_per_second();
let p_mw = self.pump_power_mw;
let bandwidth_nm = self.phase_matching_bandwidth_nm();
if p_mw > 0.0 && bandwidth_nm > 0.0 {
r / (p_mw * bandwidth_nm)
} else {
0.0
}
}
pub fn heralding_efficiency(&self) -> f64 {
match &self.crystal {
SpdcCrystal::Ppktp => 0.80,
SpdcCrystal::Ppln => 0.75,
SpdcCrystal::Bbo => 0.55,
SpdcCrystal::Ktp => 0.72,
SpdcCrystal::Periodically { .. } => 0.70,
}
}
pub fn phase_matching_bandwidth_nm(&self) -> f64 {
let gvm = self.crystal.group_velocity_mismatch().abs();
let l = self.crystal_length_m();
let lambda_s = self.signal_wavelength();
if gvm < 1e-20 || l < 1e-10 {
return 10.0; }
let delta_omega = 0.886 * PI / (l * gvm);
let delta_lambda = (lambda_s.powi(2) / (2.0 * PI * C)) * delta_omega;
delta_lambda * 1e9 }
pub fn g2_zero(&self) -> f64 {
let k = self.schmidt_number(32);
1.0 + 1.0 / k.max(1.0)
}
pub fn joint_spectral_amplitude(
&self,
n_points: usize,
) -> (Vec<f64>, Vec<f64>, Vec<Vec<Complex64>>) {
let n = n_points.max(4);
let omega_s0 = 2.0 * PI * C / self.signal_wavelength();
let omega_i0 = 2.0 * PI * C / self.idler_wavelength();
let omega_p0 = 2.0 * PI * C / self.pump_wavelength;
let sigma_pump_nm = 0.5_f64;
let sigma_pump_m = sigma_pump_nm * 1e-9;
let sigma_omega_pump = (2.0 * PI * C / self.pump_wavelength.powi(2)) * sigma_pump_m;
let bw_nm = self.phase_matching_bandwidth_nm();
let bw_m = bw_nm * 1e-9;
let sigma_omega_pm = (2.0 * PI * C / self.signal_wavelength().powi(2)) * bw_m;
let half_span = 3.0 * sigma_omega_pump.max(sigma_omega_pm);
let d_omega = 2.0 * half_span / (n as f64 - 1.0);
let signal_freqs: Vec<f64> = (0..n)
.map(|i| omega_s0 - half_span + i as f64 * d_omega)
.collect();
let idler_freqs: Vec<f64> = (0..n)
.map(|i| omega_i0 - half_span + i as f64 * d_omega)
.collect();
let gvm = self.crystal.group_velocity_mismatch();
let l = self.crystal_length_m();
let mut jsa = vec![vec![Complex64::new(0.0, 0.0); n]; n];
let mut norm_sq = 0.0_f64;
for (si, &omega_s) in signal_freqs.iter().enumerate() {
for (ii, &omega_i) in idler_freqs.iter().enumerate() {
let delta_sum = omega_s + omega_i - omega_p0;
let pump_env = (-delta_sum.powi(2) / (2.0 * sigma_omega_pump.powi(2))).exp();
let delta_omega_s = omega_s - omega_s0;
let delta_k_l_half = gvm * delta_omega_s * l / 2.0;
let pm = sinc(delta_k_l_half);
let val = pump_env * pm;
jsa[si][ii] = Complex64::new(val, 0.0);
norm_sq += val * val;
}
}
if norm_sq > 0.0 {
let norm = norm_sq.sqrt();
for row in &mut jsa {
for v in row.iter_mut() {
*v /= norm;
}
}
}
(signal_freqs, idler_freqs, jsa)
}
pub fn schmidt_modes(&self, n_points: usize) -> Vec<f64> {
let (_, _, jsa) = self.joint_spectral_amplitude(n_points);
let n = jsa.len();
if n == 0 {
return vec![1.0];
}
let a: Vec<Vec<f64>> = jsa
.iter()
.map(|row| row.iter().map(|v| v.re).collect())
.collect();
let s = mat_transpose_times_mat(&a, n);
let eigenvalues = symmetric_eigenvalues_power_deflation(&s, n, 32);
let mut lambdas: Vec<f64> = eigenvalues.into_iter().filter(|&v| v > 1e-12).collect();
lambdas.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let total: f64 = lambdas.iter().sum();
if total > 0.0 {
for v in lambdas.iter_mut() {
*v /= total;
}
}
lambdas
}
pub fn schmidt_number(&self, n_points: usize) -> f64 {
let lambdas = self.schmidt_modes(n_points);
let sum_sq: f64 = lambdas.iter().map(|&l| l * l).sum();
if sum_sq > 0.0 {
1.0 / sum_sq
} else {
1.0
}
}
pub fn spectral_purity(&self, n_points: usize) -> f64 {
1.0 / self.schmidt_number(n_points).max(1.0)
}
}
#[derive(Debug, Clone)]
pub struct IntegratedSpdcSource {
pub crystal_length: f64,
pub effective_area: f64,
pub phase_matching: PhaseMatchingType,
pub pump_wavelength: f64,
pub pump_bandwidth_fwhm: f64,
pub d_eff: f64,
pub n_p: f64,
pub n_s: f64,
pub n_i: f64,
pub n_g_p: f64,
pub n_g_s: f64,
pub n_g_i: f64,
}
impl IntegratedSpdcSource {
fn omega_p0(&self) -> f64 {
2.0 * PI * C / self.pump_wavelength
}
fn omega_s0(&self) -> f64 {
self.omega_p0() / 2.0
}
fn omega_i0(&self) -> f64 {
self.omega_p0() / 2.0
}
fn sigma_omega_pump(&self) -> f64 {
let sigma_freq = self.pump_bandwidth_fwhm / (2.0 * (2.0_f64 * 2.0_f64.ln()).sqrt());
2.0 * PI * sigma_freq
}
fn k_pump(&self, omega: f64) -> f64 {
let omega_0 = self.omega_p0();
self.n_p * omega_0 / C + (self.n_g_p / C) * (omega - omega_0)
}
fn k_signal(&self, omega: f64) -> f64 {
let omega_0 = self.omega_s0();
self.n_s * omega_0 / C + (self.n_g_s / C) * (omega - omega_0)
}
fn k_idler(&self, omega: f64) -> f64 {
let omega_0 = self.omega_i0();
self.n_i * omega_0 / C + (self.n_g_i / C) * (omega - omega_0)
}
fn k_qpm(&self) -> f64 {
match &self.phase_matching {
PhaseMatchingType::QuasiPm { period_um } => 2.0 * PI / (period_um * 1e-6),
_ => 0.0,
}
}
fn delta_k(&self, omega_s: f64, omega_i: f64) -> f64 {
let omega_p = omega_s + omega_i;
self.k_pump(omega_p) - self.k_signal(omega_s) - self.k_idler(omega_i) - self.k_qpm()
}
fn pump_envelope(&self, omega_p: f64) -> f64 {
let sigma = self.sigma_omega_pump();
let delta = omega_p - self.omega_p0();
(-delta * delta / (2.0 * sigma * sigma)).exp()
}
pub fn jsa(&self, ds: usize, di: usize) -> JointSpectralAmplitude {
let ns = ds.max(4);
let ni = di.max(4);
let omega_s0 = self.omega_s0();
let omega_i0 = self.omega_i0();
let sigma = self.sigma_omega_pump();
let half_span = 3.0 * sigma;
let l = self.crystal_length;
let signal_freqs: Vec<f64> = (0..ns)
.map(|i| omega_s0 - half_span + (2.0 * half_span) * (i as f64) / ((ns - 1) as f64))
.collect();
let idler_freqs: Vec<f64> = (0..ni)
.map(|i| omega_i0 - half_span + (2.0 * half_span) * (i as f64) / ((ni - 1) as f64))
.collect();
let mut amplitude = vec![vec![Complex64::new(0.0, 0.0); ni]; ns];
let mut norm_sq = 0.0_f64;
for (si, &omega_s) in signal_freqs.iter().enumerate() {
for (ii, &omega_i) in idler_freqs.iter().enumerate() {
let alpha = self.pump_envelope(omega_s + omega_i);
let dk_l_half = self.delta_k(omega_s, omega_i) * l / 2.0;
let pm = sinc(dk_l_half);
let val = alpha * pm;
amplitude[si][ii] = Complex64::new(val, 0.0);
norm_sq += val * val;
}
}
if norm_sq > 0.0 {
let norm = norm_sq.sqrt();
for row in &mut amplitude {
for v in row.iter_mut() {
*v /= norm;
}
}
}
JointSpectralAmplitude {
signal_freqs,
idler_freqs,
amplitude,
}
}
pub fn schmidt_decomposition(&self, n: usize) -> SchmidtDecomposition {
let jsa = self.jsa(n, n);
let dim = jsa.amplitude.len();
if dim == 0 {
return SchmidtDecomposition {
eigenvalues: vec![1.0],
schmidt_number: 1.0,
};
}
let a: Vec<Vec<f64>> = jsa
.amplitude
.iter()
.map(|row| row.iter().map(|v| v.re).collect())
.collect();
let s = mat_transpose_times_mat(&a, dim);
let raw_eigs = symmetric_eigenvalues_power_deflation(&s, dim, dim.min(64));
let mut lambdas: Vec<f64> = raw_eigs.into_iter().filter(|&v| v > 1e-14).collect();
lambdas.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let total: f64 = lambdas.iter().sum();
if total > 0.0 {
for v in lambdas.iter_mut() {
*v /= total;
}
} else {
lambdas = vec![1.0];
}
let sum_sq: f64 = lambdas.iter().map(|&l| l * l).sum();
let schmidt_number = if sum_sq > 0.0 { 1.0 / sum_sq } else { 1.0 };
SchmidtDecomposition {
eigenvalues: lambdas,
schmidt_number,
}
}
pub fn hom_visibility(&self) -> f64 {
let decomp = self.schmidt_decomposition(20);
let sum_sq: f64 = decomp.eigenvalues.iter().map(|&l| l * l).sum();
if sum_sq > 0.0 {
(1.0 / sum_sq).min(1.0)
} else {
1.0
}
}
pub fn pair_generation_rate(&self, pump_power_w: f64) -> f64 {
let lambda_p = self.pump_wavelength;
let numerator =
8.0 * PI * PI * self.d_eff * self.d_eff * self.crystal_length * pump_power_w;
let denominator =
EPSILON_0 * C * self.n_p * self.n_s * self.n_i * lambda_p.powi(3) * self.effective_area;
numerator / denominator
}
pub fn brightness_per_mw_per_nm(&self) -> f64 {
let lambda_s = 2.0 * self.pump_wavelength;
let gvm = (self.n_g_s - self.n_g_i).abs() / C; let bandwidth_nm = if gvm < 1e-20 {
10.0 } else {
let delta_omega = 0.886 * PI / (self.crystal_length * gvm);
let delta_lambda = (lambda_s * lambda_s / (2.0 * PI * C)) * delta_omega;
delta_lambda * 1e9 };
let r = self.pair_generation_rate(1e-3);
r / (1.0 * bandwidth_nm)
}
}
fn sinc(x: f64) -> f64 {
if x.abs() < 1e-10 {
1.0
} else {
x.sin() / x
}
}
fn mat_transpose_times_mat(a: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
let mut b = vec![vec![0.0_f64; n]; n];
for i in 0..n {
for j in 0..n {
let mut acc = 0.0;
for row in a.iter().take(n) {
acc += row[i] * row[j];
}
b[i][j] = acc;
}
}
b
}
fn symmetric_eigenvalues_power_deflation(s: &[Vec<f64>], n: usize, n_eigs: usize) -> Vec<f64> {
let max_eigs = n_eigs.min(n);
let mut eigenvalues = Vec::with_capacity(max_eigs);
let mut m: Vec<Vec<f64>> = s.to_vec();
for _ in 0..max_eigs {
let mut v = vec![1.0_f64 / (n as f64).sqrt(); n];
let mut eigenvalue = 0.0_f64;
for _iter in 0..200 {
let mv = mat_vec_mul(&m, &v, n);
let norm: f64 = mv.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-30 {
break;
}
let v_new: Vec<f64> = mv.iter().map(|x| x / norm).collect();
let mv2 = mat_vec_mul(&m, &v_new, n);
eigenvalue = v_new.iter().zip(mv2.iter()).map(|(a, b)| a * b).sum();
let diff: f64 = v_new
.iter()
.zip(v.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
v = v_new;
if diff < 1e-12 {
break;
}
}
if eigenvalue.abs() < 1e-14 {
break;
}
eigenvalues.push(eigenvalue);
for i in 0..n {
for j in 0..n {
m[i][j] -= eigenvalue * v[i] * v[j];
}
}
}
eigenvalues
}
fn mat_vec_mul(m: &[Vec<f64>], v: &[f64], n: usize) -> Vec<f64> {
let mut result = vec![0.0_f64; n];
for i in 0..n {
for j in 0..n {
result[i] += m[i][j] * v[j];
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ppktp_wavelengths() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let sig = src.signal_wavelength();
let idl = src.idler_wavelength();
assert!((sig - 1550e-9).abs() < 1e-12, "signal should be ~1550 nm");
assert!((idl - sig).abs() < 1e-12, "degenerate: signal = idler");
}
#[test]
fn test_pair_rate_positive() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let rate = src.pair_rate_per_second();
assert!(rate > 0.0, "pair rate must be positive");
assert!(rate.is_finite(), "pair rate must be finite");
}
#[test]
fn test_pair_rate_scales_with_power() {
let src1 = SpdcSource::new_ppktp_1550(1.0, 10.0);
let src2 = SpdcSource::new_ppktp_1550(2.0, 10.0);
let r1 = src1.pair_rate_per_second();
let r2 = src2.pair_rate_per_second();
assert!(
(r2 / r1 - 2.0).abs() < 1e-9,
"Rate should double when power doubles"
);
}
#[test]
fn test_spectral_brightness_positive() {
let src = SpdcSource::new_ppln_telecom(1.0, 20.0);
let b = src.spectral_brightness();
assert!(b > 0.0, "spectral brightness must be positive");
}
#[test]
fn test_phase_matching_bandwidth() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let bw = src.phase_matching_bandwidth_nm();
assert!(bw > 0.0, "bandwidth must be positive");
assert!(bw < 100.0, "bandwidth sanity check (< 100 nm)");
}
#[test]
fn test_schmidt_number_ge_one() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let k = src.schmidt_number(16);
assert!(k >= 1.0 - 1e-9, "Schmidt number K ≥ 1");
}
#[test]
fn test_spectral_purity_range() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let p = src.spectral_purity(16);
assert!(p > 0.0 && p <= 1.0 + 1e-9, "spectral purity ∈ (0, 1]");
}
#[test]
fn test_g2_zero_thermal() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let g2 = src.g2_zero();
assert!((1.0..=2.0 + 1e-9).contains(&g2), "g²(0) ∈ [1, 2]");
}
#[test]
fn test_jsa_normalised() {
let src = SpdcSource::new_ppktp_1550(1.0, 10.0);
let (_, _, jsa) = src.joint_spectral_amplitude(16);
let norm_sq: f64 = jsa
.iter()
.flat_map(|row| row.iter())
.map(|v| v.norm_sqr())
.sum();
assert!(
(norm_sq - 1.0).abs() < 0.01,
"JSA should be normalised to ~1"
);
}
#[test]
fn test_sinc_zero() {
assert!((sinc(0.0) - 1.0).abs() < 1e-15, "sinc(0) = 1");
assert!((sinc(1e-12) - 1.0).abs() < 1e-10, "sinc(near-0) ≈ 1");
}
#[test]
fn test_sinc_at_pi() {
let v = sinc(PI);
assert!(v.abs() < 1e-14, "sinc(π) ≈ 0, got {v}");
}
}