use std::f64::consts::PI;
use num_complex::Complex64;
use crate::error::OxiPhotonError;
use crate::fiber::pulse::fft_radix2;
const C0: f64 = 2.997_924_58e8; #[allow(dead_code)]
const HBAR: f64 = 1.054_571_8e-34;
#[derive(Debug, Clone)]
pub struct HausMasterEquation {
pub gain_per_roundtrip: f64,
pub loss_per_roundtrip: f64,
pub gain_bandwidth_thz: f64,
pub gdd_fs2: f64,
pub spe_phase_shift_rad: f64,
pub saturable_absorber_mod: f64,
pub saturation_power_w: f64,
}
impl HausMasterEquation {
pub fn new(
gain: f64,
loss: f64,
bw_thz: f64,
gdd_fs2: f64,
spe: f64,
sa_mod: f64,
p_sat: f64,
) -> Self {
Self {
gain_per_roundtrip: gain,
loss_per_roundtrip: loss,
gain_bandwidth_thz: bw_thz,
gdd_fs2,
spe_phase_shift_rad: spe,
saturable_absorber_mod: sa_mod,
saturation_power_w: p_sat,
}
}
pub fn gain_filtering_fs2(&self) -> f64 {
let delta_omega_g_rad_per_fs = 2.0 * PI * self.gain_bandwidth_thz * 1.0e-3;
self.gain_per_roundtrip / (delta_omega_g_rad_per_fs * delta_omega_g_rad_per_fs)
}
pub fn soliton_pulse_width_fs(&self, pulse_energy_pj: f64) -> f64 {
let e_j = pulse_energy_pj * 1.0e-12;
let gamma = self.spe_phase_shift_rad.abs();
let d_abs = self.gdd_fs2.abs() * 1.0e-30; if gamma < 1.0e-30 || e_j < 1.0e-30 {
return 1.0e15; }
let tau_s = 2.0 * d_abs / (gamma * e_j);
tau_s * 1.0e15 }
pub fn steady_state_pulse_width_fs(&self) -> f64 {
let g = self.gain_per_roundtrip;
let l = self.loss_per_roundtrip;
let dg = self.gain_filtering_fs2(); let net = g - l - self.saturable_absorber_mod / 2.0;
if net <= 0.0 || dg <= 0.0 {
return f64::INFINITY;
}
let tau_gf = (dg / net).sqrt();
let d_abs = self.gdd_fs2.abs(); let gamma = self.spe_phase_shift_rad;
if gamma > 0.0 && d_abs > 0.0 {
let tau_sol = (d_abs / (gamma * (g - l))).sqrt();
tau_gf.min(tau_sol)
} else {
tau_gf
}
}
pub fn steady_state_peak_power_w(&self) -> f64 {
let tau_fs = self.steady_state_pulse_width_fs();
if !tau_fs.is_finite() || tau_fs < 1.0e-6 {
return 0.0;
}
let tau_s = tau_fs * 1.0e-15;
let d_abs = self.gdd_fs2.abs() * 1.0e-30; let gamma = self.spe_phase_shift_rad;
if gamma < 1.0e-40 {
return 0.0;
}
d_abs / (gamma * tau_s * tau_s)
}
pub fn pulse_energy_pj(&self) -> f64 {
let p0 = self.steady_state_peak_power_w();
let tau_s = self.steady_state_pulse_width_fs() * 1.0e-15;
2.0 * p0 * tau_s * 1.0e12 }
pub fn rep_rate_mhz(&self, cavity_length_m: f64) -> f64 {
if cavity_length_m <= 0.0 {
return 0.0;
}
C0 / (2.0 * cavity_length_m) / 1.0e6
}
pub fn average_power_mw(&self, cavity_length_m: f64) -> f64 {
let e_j = self.pulse_energy_pj() * 1.0e-12;
let f_rep = self.rep_rate_mhz(cavity_length_m) * 1.0e6;
e_j * f_rep * 1.0e3 }
pub fn kelly_sideband_wavelength_nm(
&self,
m: i32,
center_lambda_nm: f64,
cavity_length_m: f64,
) -> f64 {
let f_rep_hz = self.rep_rate_mhz(cavity_length_m) * 1.0e6;
let lambda_m = center_lambda_nm * 1.0e-9;
let d_s2 = self.gdd_fs2.abs() * 1.0e-30;
if d_s2 < 1.0e-50 || f_rep_hz < 1.0 {
return center_lambda_nm;
}
let delta_omega_sq = 2.0 * 2.0 * PI * (m.abs() as f64) * f_rep_hz / d_s2;
if delta_omega_sq < 0.0 {
return center_lambda_nm;
}
let delta_omega = delta_omega_sq.sqrt();
let delta_lambda_m = lambda_m * lambda_m / (2.0 * PI * C0) * delta_omega;
let sign = if m > 0 { 1.0 } else { -1.0 };
center_lambda_nm + sign * delta_lambda_m * 1.0e9
}
pub fn time_bandwidth_product(&self) -> f64 {
let beta = self.chirp_parameter();
0.314_8 * (1.0 + beta * beta).sqrt()
}
pub fn chirp_parameter(&self) -> f64 {
let dg = self.gain_filtering_fs2(); let d_abs = self.gdd_fs2.abs(); let gamma = self.spe_phase_shift_rad;
if d_abs < 1.0e-20 {
return 0.0;
}
dg * gamma / d_abs
}
pub fn is_self_consistent(&self) -> bool {
let tau_fs = self.steady_state_pulse_width_fs();
if !tau_fs.is_finite() {
return false;
}
let dg = self.gain_filtering_fs2();
let gain_filtering_loss = dg / (tau_fs * tau_fs);
let lhs = self.gain_per_roundtrip;
let rhs = self.loss_per_roundtrip + gain_filtering_loss;
(lhs - rhs).abs() / (lhs + rhs + 1.0e-10) < 0.20
}
pub fn q_switch_free(&self, sat_energy_pj: f64) -> bool {
let e_pulse = self.pulse_energy_pj();
if sat_energy_pj < 1.0e-20 {
return false;
}
let g = self.gain_per_roundtrip;
let l = self.loss_per_roundtrip;
if l < 1.0e-20 {
return false;
}
(e_pulse / sat_energy_pj) * (g / l) < 1.0
}
pub fn propagate_one_roundtrip(&self, amplitude: &[Complex64], dt_fs: f64) -> Vec<Complex64> {
let n = amplitude.len();
if n == 0 {
return Vec::new();
}
let spectrum = fft_radix2(amplitude, false);
let df = 1.0 / (n as f64 * dt_fs); let omega: Vec<f64> = (0..n)
.map(|i| {
let fi = if i < n / 2 {
i as f64 * df
} else {
(i as f64 - n as f64) * df
};
2.0 * PI * fi })
.collect();
let g = self.gain_per_roundtrip;
let l = self.loss_per_roundtrip;
let dg = self.gain_filtering_fs2(); let d = self.gdd_fs2;
let spectrum_out: Vec<Complex64> = spectrum
.iter()
.zip(omega.iter())
.map(|(&s, &om)| {
let om2 = om * om;
let net_gain = (g - l - dg * om2).exp();
let gdd_phase = Complex64::new(0.0, -d * om2 / 2.0).exp();
s * net_gain * gdd_phase
})
.collect();
let mut field = fft_radix2(&spectrum_out, true);
let gamma = self.spe_phase_shift_rad;
let q0 = self.saturable_absorber_mod;
let p_sat = self.saturation_power_w;
let loss_factor = 1.0 - l;
for sample in field.iter_mut() {
let power = sample.norm_sqr();
*sample *= loss_factor;
let spm_phase = Complex64::new(0.0, gamma * power).exp();
*sample *= spm_phase;
let sa_transmission = if p_sat > 0.0 {
1.0 - q0 / (1.0 + power / p_sat)
} else {
1.0 - q0
};
*sample *= sa_transmission;
}
field
}
pub fn find_steady_state(
&self,
n_pts: usize,
t_window_ps: f64,
n_iterations: usize,
) -> Result<Vec<Complex64>, OxiPhotonError> {
if n_pts == 0 {
return Err(OxiPhotonError::NumericalError("n_pts must be > 0".into()));
}
if t_window_ps <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"t_window_ps must be positive".into(),
));
}
let n_pts_pow2 = n_pts.next_power_of_two();
let dt_ps = t_window_ps / n_pts_pow2 as f64;
let dt_fs = dt_ps * 1.0e3;
let tau_fs = self.steady_state_pulse_width_fs();
let tau_ps = if tau_fs.is_finite() {
tau_fs * 1.0e-3
} else {
0.1 };
let p0 = self.steady_state_peak_power_w().max(1.0);
let a0 = p0.sqrt();
let half_ps = t_window_ps / 2.0;
let mut amplitude: Vec<Complex64> = (0..n_pts_pow2)
.map(|i| {
let t = i as f64 * dt_ps - half_ps;
let env = if tau_ps > 0.0 {
1.0 / (t / tau_ps).cosh()
} else {
0.0
};
Complex64::new(a0 * env, 0.0)
})
.collect();
for _ in 0..n_iterations {
amplitude = self.propagate_one_roundtrip(&litude, dt_fs);
let peak = amplitude
.iter()
.map(|a| a.norm_sqr())
.fold(0.0_f64, f64::max)
.sqrt()
.max(1.0e-30);
let target = a0;
let scale = target / peak;
for s in amplitude.iter_mut() {
*s *= scale;
}
}
Ok(amplitude)
}
}
#[derive(Debug, Clone)]
pub struct Sesam {
pub modulation_depth: f64,
pub saturation_fluence_uj_cm2: f64,
pub recovery_time_ps: f64,
pub non_sat_loss: f64,
}
impl Sesam {
pub fn new(
mod_depth: f64,
fsat: f64,
recovery_ps: f64,
ns_loss: f64,
) -> Result<Self, OxiPhotonError> {
if !(0.0..=1.0).contains(&mod_depth) {
return Err(OxiPhotonError::NumericalError(format!(
"modulation_depth {mod_depth} must be in [0, 1]"
)));
}
if !(0.0..=1.0).contains(&ns_loss) {
return Err(OxiPhotonError::NumericalError(format!(
"non_sat_loss {ns_loss} must be in [0, 1]"
)));
}
if fsat <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"saturation_fluence_uj_cm2 must be positive".into(),
));
}
Ok(Self {
modulation_depth: mod_depth,
saturation_fluence_uj_cm2: fsat,
recovery_time_ps: recovery_ps,
non_sat_loss: ns_loss,
})
}
pub fn reflectivity(&self, fluence_uj_cm2: f64) -> f64 {
let base = 1.0 - self.non_sat_loss;
let saturable =
self.modulation_depth * (-fluence_uj_cm2 / self.saturation_fluence_uj_cm2).exp();
(base - saturable).clamp(0.0, 1.0)
}
pub fn modulation_for_pulse(&self, peak_power_w: f64, pulse_width_ps: f64) -> f64 {
let fluence = peak_power_w * pulse_width_ps * 1.0e-12; let fluence_uj = fluence * 1.0e6; let r_with = self.reflectivity(fluence_uj);
let r_unsaturated = 1.0 - self.non_sat_loss - self.modulation_depth;
r_with - r_unsaturated.max(0.0)
}
pub fn is_mode_locking_stable(&self, gain: f64, loss: f64) -> bool {
self.modulation_depth > (gain - loss).abs() && self.non_sat_loss < gain - loss
}
}
#[derive(Debug, Clone)]
pub struct KerrLensModelocking {
pub kerr_medium_length_mm: f64,
pub n2_cm2_per_w: f64,
pub beam_waist_in_kerr_um: f64,
pub aperture_position: f64,
}
impl KerrLensModelocking {
pub fn new(length_mm: f64, n2: f64, waist_um: f64, aperture_pos: f64) -> Self {
Self {
kerr_medium_length_mm: length_mm,
n2_cm2_per_w: n2,
beam_waist_in_kerr_um: waist_um,
aperture_position: aperture_pos,
}
}
pub fn nonlinear_phase_rad(&self, peak_power_w: f64) -> f64 {
let lambda_m = 800.0e-9; let w0_m = self.beam_waist_in_kerr_um * 1.0e-6;
let n2_m2_per_w = self.n2_cm2_per_w * 1.0e-4; let intensity_w_per_m2 = peak_power_w / (PI * w0_m * w0_m);
let length_m = self.kerr_medium_length_mm * 1.0e-3;
(2.0 * PI / lambda_m) * n2_m2_per_w * intensity_w_per_m2 * length_m
}
pub fn effective_modulation_depth(&self, peak_power_w: f64) -> f64 {
let b = self.nonlinear_phase_rad(peak_power_w);
let ap = self.aperture_position;
let alpha = ap * ap / (1.0 + ap * ap + 1.0e-10);
alpha * b * b / (1.0 + b * b)
}
pub fn minimum_pulse_width_fs(&self, bandwidth_nm: f64, lambda_nm: f64) -> f64 {
if bandwidth_nm <= 0.0 || lambda_nm <= 0.0 {
return f64::INFINITY;
}
let lambda_m = lambda_nm * 1.0e-9;
let delta_lambda_m = bandwidth_nm * 1.0e-9;
let tau_s = 0.314_8 * lambda_m * lambda_m / (C0 * delta_lambda_m);
tau_s * 1.0e15 }
}
#[derive(Debug, Clone)]
pub struct ModeLockLaser {
pub equation: HausMasterEquation,
pub cavity_length_m: f64,
pub center_wavelength_nm: f64,
pub output_coupler_transmission: f64,
}
impl ModeLockLaser {
pub fn new(eq: HausMasterEquation, length_m: f64, lambda_nm: f64, t_oc: f64) -> Self {
Self {
equation: eq,
cavity_length_m: length_m,
center_wavelength_nm: lambda_nm,
output_coupler_transmission: t_oc.clamp(0.0, 1.0),
}
}
pub fn rep_rate_mhz(&self) -> f64 {
self.equation.rep_rate_mhz(self.cavity_length_m)
}
pub fn pulse_width_fs(&self) -> f64 {
self.equation.steady_state_pulse_width_fs()
}
pub fn peak_power_kw(&self) -> f64 {
self.equation.steady_state_peak_power_w() / 1.0e3
}
pub fn average_power_mw(&self) -> f64 {
self.equation.average_power_mw(self.cavity_length_m) * self.output_coupler_transmission
}
pub fn output_pulse_energy_pj(&self) -> f64 {
self.equation.pulse_energy_pj() * self.output_coupler_transmission
}
pub fn spectral_bandwidth_nm(&self) -> f64 {
let tau_s = self.pulse_width_fs() * 1.0e-15;
if tau_s <= 0.0 || !tau_s.is_finite() {
return 0.0;
}
let tbp = self.equation.time_bandwidth_product();
let lambda_m = self.center_wavelength_nm * 1.0e-9;
tbp * lambda_m * lambda_m / (C0 * tau_s) * 1.0e9
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_hme() -> HausMasterEquation {
HausMasterEquation::new(
0.10, 0.05, 5.0, -1000.0, 1.0e-3, 0.02, 50.0, )
}
#[test]
fn test_rep_rate_from_cavity_length() {
let hme = default_hme();
let l_m = 1.5; let f_rep = hme.rep_rate_mhz(l_m);
let expected = C0 / (2.0 * l_m) / 1.0e6;
assert!(
(f_rep - expected).abs() / expected < 1.0e-10,
"rep rate: expected {expected:.3} MHz, got {f_rep:.3} MHz"
);
}
#[test]
fn test_soliton_pulse_energy_consistency() {
let hme = default_hme();
let p0 = hme.steady_state_peak_power_w();
let tau_fs = hme.steady_state_pulse_width_fs();
let tau_s = tau_fs * 1.0e-15;
let e_analytical = 2.0 * p0 * tau_s * 1.0e12; let e_method = hme.pulse_energy_pj();
assert!(
(e_analytical - e_method).abs() / (e_analytical + 1.0e-20) < 1.0e-10,
"Energy consistency: analytical={e_analytical:.4e} pJ, method={e_method:.4e} pJ"
);
}
#[test]
fn test_time_bandwidth_product() {
let hme = HausMasterEquation::new(
0.10, 0.05, 50.0, -1000.0, 1.0e-3, 0.02, 50.0, );
let tbp = hme.time_bandwidth_product();
assert!(tbp >= 0.314_8 - 1.0e-6, "TBP {tbp:.5} should be ≥ 0.3148");
assert!(
tbp < 0.40,
"TBP {tbp:.5} should be < 0.40 for small chirp case"
);
}
#[test]
fn test_self_consistency_check() {
let hme = default_hme();
let _ = hme.is_self_consistent();
let tau = hme.steady_state_pulse_width_fs();
assert!(tau.is_finite(), "steady-state pulse width should be finite");
assert!(tau > 0.0, "pulse width should be positive");
}
#[test]
fn test_sesam_reflectivity_at_saturation() {
let sesam = Sesam::new(0.02, 10.0, 1.0, 0.005).expect("valid SESAM");
let r_high = sesam.reflectivity(1.0e6); let expected = 1.0 - sesam.non_sat_loss;
assert!(
(r_high - expected).abs() < 1.0e-4,
"R at saturation: expected {expected:.5}, got {r_high:.5}"
);
}
#[test]
fn test_sesam_reflectivity_at_low_fluence() {
let sesam = Sesam::new(0.02, 10.0, 1.0, 0.005).expect("valid SESAM");
let r_low = sesam.reflectivity(0.0);
let expected = 1.0 - sesam.modulation_depth - sesam.non_sat_loss;
assert!(
(r_low - expected).abs() < 1.0e-10,
"R at low fluence: expected {expected:.5}, got {r_low:.5}"
);
}
#[test]
fn test_klm_phase_increases_with_power() {
let klm = KerrLensModelocking::new(3.0, 3.0e-16, 50.0, 0.5);
let phi_low = klm.nonlinear_phase_rad(1.0e3);
let phi_high = klm.nonlinear_phase_rad(1.0e6);
assert!(
phi_high > phi_low,
"Nonlinear phase should increase with power: low={phi_low:.4e}, high={phi_high:.4e}"
);
}
#[test]
fn test_steady_state_convergence() {
let hme = HausMasterEquation::new(
0.10, 0.05, 5.0, -1000.0, 1.0e-3, 0.02, 50.0, );
let result = hme.find_steady_state(256, 10.0, 50);
assert!(result.is_ok(), "find_steady_state should succeed");
let field = result.expect("steady state result");
let peak = field.iter().map(|a| a.norm_sqr()).fold(0.0_f64, f64::max);
assert!(peak > 0.0, "steady-state peak power should be positive");
assert!(peak.is_finite(), "steady-state peak power should be finite");
}
}