use crate::error::OxiPhotonError;
use crate::nonlinear_crystal::crystals::NloCrystal;
const C0: f64 = 2.99792458e8;
const EPS0: f64 = 8.854187817e-12;
#[derive(Debug, Clone)]
pub enum PhaseMatchingType {
TypeI,
TypeII,
TypeIII,
TypeZero,
QuasiPm { period_um: f64 },
}
pub struct SHGPhaseMatching {
pub crystal: NloCrystal,
pub pm_type: PhaseMatchingType,
pub fundamental_wavelength_nm: f64,
}
impl SHGPhaseMatching {
pub fn new(crystal: NloCrystal, pm_type: PhaseMatchingType, lambda_nm: f64) -> Self {
Self {
crystal,
pm_type,
fundamental_wavelength_nm: lambda_nm,
}
}
pub fn shg_wavelength_nm(&self) -> f64 {
self.fundamental_wavelength_nm / 2.0
}
fn k_ordinary(&self, lambda_nm: f64) -> f64 {
let n = self.crystal.n_ordinary(lambda_nm);
2.0 * std::f64::consts::PI * n / (lambda_nm * 1e-9)
}
fn k_extraordinary(&self, lambda_nm: f64, theta_rad: f64) -> f64 {
let n = self.crystal.n_extraordinary(lambda_nm, theta_rad);
2.0 * std::f64::consts::PI * n / (lambda_nm * 1e-9)
}
pub fn phase_mismatch(&self, theta_rad: f64) -> f64 {
let lambda1 = self.fundamental_wavelength_nm;
let lambda2 = self.shg_wavelength_nm();
match &self.pm_type {
PhaseMatchingType::TypeI => {
let k1 = self.k_ordinary(lambda1);
let k2 = self.k_extraordinary(lambda2, theta_rad);
k2 - 2.0 * k1
}
PhaseMatchingType::TypeII => {
let k1_o = self.k_ordinary(lambda1);
let k1_e = self.k_extraordinary(lambda1, theta_rad);
let k2_e = self.k_extraordinary(lambda2, theta_rad);
k2_e - k1_o - k1_e
}
PhaseMatchingType::TypeIII => {
let k1_e = self.k_extraordinary(lambda1, theta_rad);
let k2_o = self.k_ordinary(lambda2);
k2_o - 2.0 * k1_e
}
PhaseMatchingType::TypeZero => {
let k1 = self.k_ordinary(lambda1);
let k2 = self.k_ordinary(lambda2);
k2 - 2.0 * k1
}
PhaseMatchingType::QuasiPm { period_um } => {
let k1 = self.k_ordinary(lambda1);
let k2 = self.k_ordinary(lambda2); let delta_k_free = k2 - 2.0 * k1;
let k_qpm = 2.0 * std::f64::consts::PI / (period_um * 1e-6);
delta_k_free - k_qpm
}
}
}
pub fn phase_matching_angle_rad(&self) -> Result<f64, OxiPhotonError> {
let lo = 1.0_f64.to_radians();
let hi = 89.0_f64.to_radians();
let f_lo = self.phase_mismatch(lo);
let f_hi = self.phase_mismatch(hi);
if f_lo * f_hi > 0.0 {
return Err(OxiPhotonError::NumericalError(
"No phase matching angle found in [1°, 89°] — check crystal and wavelength"
.to_string(),
));
}
let mut a = lo;
let mut b = hi;
for _ in 0..64 {
let mid = 0.5 * (a + b);
if self.phase_mismatch(mid) * f_lo <= 0.0 {
b = mid;
} else {
a = mid;
}
if (b - a).abs() < 1e-10 {
break;
}
}
Ok(0.5 * (a + b))
}
pub fn walkoff_angle_rad(&self, theta_rad: f64) -> f64 {
let lambda2 = self.shg_wavelength_nm();
let dtheta = 1e-7_f64;
let ne_p = self.crystal.n_extraordinary(lambda2, theta_rad + dtheta);
let ne_m = self.crystal.n_extraordinary(lambda2, theta_rad - dtheta);
let dne_dtheta = (ne_p - ne_m) / (2.0 * dtheta);
let ne = self.crystal.n_extraordinary(lambda2, theta_rad);
if ne.abs() < 1e-10 {
return 0.0;
}
(-dne_dtheta / ne).atan()
}
pub fn walkoff_length_mm(&self, beam_waist_um: f64, theta_rad: f64) -> f64 {
let rho = self.walkoff_angle_rad(theta_rad).abs();
if rho < 1e-12 {
return f64::INFINITY;
}
let w0_mm = beam_waist_um * 1e-3;
w0_mm / rho.tan()
}
pub fn coherence_length_mm(&self, theta_rad: f64) -> f64 {
let dk = self.phase_mismatch(theta_rad).abs();
if dk < 1e-6 {
return f64::INFINITY;
}
std::f64::consts::PI / dk * 1e3 }
pub fn angular_acceptance_mrad(&self, crystal_length_mm: f64, theta_pm: f64) -> f64 {
let l_m = crystal_length_mm * 1e-3;
let dtheta = 1e-7_f64;
let dk_p = self.phase_mismatch(theta_pm + dtheta);
let dk_m = self.phase_mismatch(theta_pm - dtheta);
let ddk_dtheta = (dk_p - dk_m) / (2.0 * dtheta);
if ddk_dtheta.abs() < 1e-20 {
return f64::INFINITY;
}
let delta_theta_rad = 2.0 * std::f64::consts::PI / (l_m * ddk_dtheta.abs());
delta_theta_rad * 1e3 }
pub fn spectral_acceptance_nm(&self, crystal_length_mm: f64) -> f64 {
let theta_pm = self
.phase_matching_angle_rad()
.unwrap_or(std::f64::consts::FRAC_PI_4);
let l_m = crystal_length_mm * 1e-3;
let dlambda_nm = 0.01_f64;
let dk_at_lambda = |lam_nm: f64| -> f64 {
let crystal_tmp = self.crystal.clone();
let pm_tmp = self.pm_type.clone();
let shg_tmp = SHGPhaseMatching::new(crystal_tmp, pm_tmp, lam_nm);
shg_tmp.phase_mismatch(theta_pm)
};
let lam0 = self.fundamental_wavelength_nm;
let ddk_dl = (dk_at_lambda(lam0 + dlambda_nm) - dk_at_lambda(lam0 - dlambda_nm))
/ (2.0 * dlambda_nm * 1e-9);
if ddk_dl.abs() < 1e-10 {
return f64::INFINITY;
}
let delta_lambda_m = 2.0 * std::f64::consts::PI / (l_m * ddk_dl.abs());
delta_lambda_m * 1e9 }
pub fn temperature_acceptance_degc(&self, crystal_length_mm: f64, _theta_pm: f64) -> f64 {
let l_m = crystal_length_mm * 1e-3;
let lambda_m = self.fundamental_wavelength_nm * 1e-9;
let lambda2_m = lambda_m / 2.0;
let dn_dt = self.crystal.thermo_optic_dn_dt;
let ddk_dt = (2.0 * std::f64::consts::PI / lambda2_m
- 2.0 * 2.0 * std::f64::consts::PI / lambda_m)
* dn_dt;
if ddk_dt.abs() < 1e-30 {
return f64::INFINITY;
}
2.0 * std::f64::consts::PI / (l_m * ddk_dt.abs())
}
pub fn sinc_efficiency(&self, crystal_length_mm: f64, theta_rad: f64) -> f64 {
let dk = self.phase_mismatch(theta_rad);
let l_m = crystal_length_mm * 1e-3;
let x = dk * l_m / 2.0;
if x.abs() < 1e-12 {
1.0
} else {
(x.sin() / x).powi(2)
}
}
pub fn optimal_focus_parameter() -> f64 {
2.84
}
pub fn plane_wave_efficiency(
&self,
crystal_length_mm: f64,
peak_power_w: f64,
beam_area_um2: f64,
d_eff_pm_per_v: f64,
theta_rad: f64,
) -> f64 {
let lambda1_m = self.fundamental_wavelength_nm * 1e-9;
let lambda2_m = self.shg_wavelength_nm() * 1e-9;
let l_m = crystal_length_mm * 1e-3;
let area_m2 = beam_area_um2 * 1e-12;
let d_eff = d_eff_pm_per_v * 1e-12; let n1 = self.crystal.n_ordinary(self.fundamental_wavelength_nm);
let n2 = self
.crystal
.n_extraordinary(self.shg_wavelength_nm(), theta_rad);
let numerator =
8.0 * std::f64::consts::PI.powi(2) * d_eff * d_eff * l_m * l_m * peak_power_w;
let denominator = n1 * n1 * n2 * EPS0 * C0 * lambda2_m * lambda1_m * lambda1_m * area_m2;
if denominator < 1e-100 {
return 0.0;
}
let eta_pw = numerator / denominator;
let sinc2 = self.sinc_efficiency(crystal_length_mm, theta_rad);
(eta_pw * sinc2).min(1.0)
}
pub fn effective_d_coefficient(&self, theta_rad: f64, phi_rad: f64) -> f64 {
let d22 = self.crystal.d_tensor[1][1].abs();
let d31 = self.crystal.d_tensor[2][0].abs();
match &self.pm_type {
PhaseMatchingType::TypeI => {
d22 * theta_rad.cos() * (3.0 * phi_rad).cos() - d31 * theta_rad.sin()
}
PhaseMatchingType::TypeII => {
d22 * (2.0 * phi_rad).cos() * theta_rad.cos() * theta_rad.cos()
- d31 * theta_rad.sin()
}
_ => d22 * theta_rad.cos(),
}
}
pub fn qpm_period_um(&self, temperature_c: f64) -> f64 {
let dn_dt = self.crystal.thermo_optic_dn_dt;
let dt = temperature_c - 25.0; let lambda1_nm = self.fundamental_wavelength_nm;
let lambda2_nm = self.shg_wavelength_nm();
let lambda1_m = lambda1_nm * 1e-9;
let lambda2_m = lambda2_nm * 1e-9;
let n_o1 = self.crystal.n_ordinary(lambda1_nm) + dn_dt * dt;
let n_o2 = self.crystal.n_ordinary(lambda2_nm) + dn_dt * dt;
let k1 = 2.0 * std::f64::consts::PI * n_o1 / lambda1_m;
let k2 = 2.0 * std::f64::consts::PI * n_o2 / lambda2_m;
let dk_free = (k2 - 2.0 * k1).abs();
if dk_free < 1e-6 {
return f64::INFINITY;
}
2.0 * std::f64::consts::PI / dk_free * 1e6
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConversionProcess {
SHG,
SFG,
DFG,
OPA,
OPO,
THG,
}
pub struct FrequencyConversion {
pub lambda1_nm: f64,
pub lambda2_nm: f64,
pub lambda3_nm: f64,
pub process: ConversionProcess,
}
impl FrequencyConversion {
pub fn new(lambda1_nm: f64, lambda2_nm: f64, process: ConversionProcess) -> Self {
let lambda3_nm = match &process {
ConversionProcess::SHG => lambda1_nm / 2.0,
ConversionProcess::THG => lambda1_nm / 3.0,
ConversionProcess::SFG => {
1.0 / (1.0 / lambda1_nm + 1.0 / lambda2_nm)
}
ConversionProcess::DFG | ConversionProcess::OPA | ConversionProcess::OPO => {
let inv = 1.0 / lambda1_nm - 1.0 / lambda2_nm;
if inv > 0.0 {
1.0 / inv
} else {
f64::INFINITY
}
}
};
Self {
lambda1_nm,
lambda2_nm,
lambda3_nm,
process,
}
}
pub fn shg(lambda_nm: f64) -> Self {
Self::new(lambda_nm, lambda_nm, ConversionProcess::SHG)
}
pub fn sfg(lambda1_nm: f64, lambda2_nm: f64) -> Self {
Self::new(lambda1_nm, lambda2_nm, ConversionProcess::SFG)
}
pub fn dfg(pump_nm: f64, signal_nm: f64) -> Self {
Self::new(pump_nm, signal_nm, ConversionProcess::DFG)
}
pub fn idler_wavelength_nm(&self) -> f64 {
self.lambda3_nm
}
pub fn energy_conservation_check(&self) -> bool {
match &self.process {
ConversionProcess::SHG => {
let err = (self.lambda1_nm / 2.0 - self.lambda3_nm).abs() / self.lambda3_nm;
err < 1e-6
}
ConversionProcess::THG => {
let err = (self.lambda1_nm / 3.0 - self.lambda3_nm).abs() / self.lambda3_nm;
err < 1e-6
}
ConversionProcess::SFG => {
let lhs = 1.0 / self.lambda1_nm + 1.0 / self.lambda2_nm;
let rhs = 1.0 / self.lambda3_nm;
(lhs - rhs).abs() / rhs < 1e-6
}
ConversionProcess::DFG | ConversionProcess::OPA | ConversionProcess::OPO => {
let lhs = 1.0 / self.lambda1_nm;
let rhs = 1.0 / self.lambda2_nm + 1.0 / self.lambda3_nm;
(lhs - rhs).abs() / lhs < 1e-6
}
}
}
pub fn manley_rowe_ratio(&self) -> f64 {
match &self.process {
ConversionProcess::SHG => 0.5,
ConversionProcess::THG => 1.0 / 3.0,
ConversionProcess::SFG => {
self.lambda1_nm / self.lambda3_nm
}
ConversionProcess::DFG | ConversionProcess::OPA | ConversionProcess::OPO => {
1.0
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nonlinear_crystal::crystals::NloCrystal;
fn bbo_shg_type1() -> SHGPhaseMatching {
SHGPhaseMatching::new(NloCrystal::bbo(), PhaseMatchingType::TypeI, 1064.0)
}
#[test]
fn test_shg_wavelength_is_half() {
let shg = bbo_shg_type1();
let lambda_shg = shg.shg_wavelength_nm();
assert!(
(lambda_shg - 532.0).abs() < 1e-10,
"SHG wavelength {:.2} should be 532 nm",
lambda_shg
);
}
#[test]
fn test_dfg_idler_wavelength() {
let pump_nm = 532.0;
let signal_nm = 800.0;
let fc = FrequencyConversion::dfg(pump_nm, signal_nm);
let idler = fc.idler_wavelength_nm();
let expected = 1.0 / (1.0 / pump_nm - 1.0 / signal_nm);
assert!(
(idler - expected).abs() < 0.01,
"DFG idler {:.2} nm, expected {:.2} nm",
idler,
expected
);
}
#[test]
fn test_energy_conservation() {
let fc_shg = FrequencyConversion::shg(1064.0);
assert!(
fc_shg.energy_conservation_check(),
"SHG energy conservation failed"
);
let fc_sfg = FrequencyConversion::sfg(1064.0, 532.0);
assert!(
fc_sfg.energy_conservation_check(),
"SFG energy conservation failed"
);
let fc_dfg = FrequencyConversion::dfg(532.0, 800.0);
assert!(
fc_dfg.energy_conservation_check(),
"DFG energy conservation failed"
);
}
#[test]
fn test_coherence_length_at_phase_match() {
let shg = bbo_shg_type1();
if let Ok(theta_pm) = shg.phase_matching_angle_rad() {
let lc = shg.coherence_length_mm(theta_pm);
assert!(
lc > 1e3,
"Coherence length at PM angle should be >> 1 m, got {:.2} mm",
lc
);
}
}
#[test]
fn test_sinc_efficiency_at_zero_mismatch() {
let shg = bbo_shg_type1();
if let Ok(theta_pm) = shg.phase_matching_angle_rad() {
let eta = shg.sinc_efficiency(10.0, theta_pm);
assert!(
(eta - 1.0).abs() < 0.01,
"sinc² at PM angle should be ≈ 1, got {:.4}",
eta
);
}
}
#[test]
fn test_sinc_efficiency_decreases_with_mismatch() {
let shg = bbo_shg_type1();
let eta_on = shg.sinc_efficiency(10.0, 22.8_f64.to_radians());
let eta_off = shg.sinc_efficiency(10.0, 30.0_f64.to_radians());
assert!(
eta_off <= eta_on + 1e-10,
"Off-angle sinc²={:.4} should not exceed on-angle {:.4}",
eta_off,
eta_on
);
}
#[test]
fn test_qpm_period_physical() {
let shg = SHGPhaseMatching::new(
NloCrystal::linbo3(),
PhaseMatchingType::QuasiPm { period_um: 19.0 },
1064.0,
);
let period = shg.qpm_period_um(25.0);
assert!(
period > 1.0 && period < 100.0,
"QPM period {:.2} μm should be in [1, 100] μm",
period
);
}
}