use super::comb::{FrequencyComb, C0};
#[derive(Debug, Clone)]
pub struct DualCombSpectroscopy {
pub comb1: FrequencyComb,
pub comb2: FrequencyComb,
pub delta_frep: f64,
}
impl DualCombSpectroscopy {
pub fn new(comb1: FrequencyComb, comb2: FrequencyComb) -> Self {
let delta_frep = (comb1.f_rep - comb2.f_rep).abs();
Self {
comb1,
comb2,
delta_frep,
}
}
pub fn rf_comb_spacing_hz(&self) -> f64 {
self.delta_frep
}
pub fn acquisition_time_s(&self) -> f64 {
if self.delta_frep <= 0.0 {
return f64::INFINITY;
}
1.0 / self.delta_frep
}
pub fn n_resolved_teeth(&self) -> usize {
if self.delta_frep <= 0.0 {
return 0;
}
let n = (self.comb1.f_rep / (2.0 * self.delta_frep)).floor() as usize;
n.max(1)
}
pub fn spectral_resolution_ghz(&self) -> f64 {
self.comb1.f_rep * 1e-9 }
pub fn min_detectable_absorbance(&self, integration_time_s: f64, snr_per_comb: f64) -> f64 {
let n_teeth = self.n_resolved_teeth() as f64;
let t_acq = self.acquisition_time_s();
let n_avg = (integration_time_s / t_acq.max(1e-12)).max(1.0);
1.0 / (snr_per_comb * (n_teeth * n_avg).sqrt())
}
pub fn figure_of_merit(&self, snr_per_comb: f64) -> f64 {
let n = self.n_resolved_teeth() as f64;
let t = self.acquisition_time_s();
n * t * snr_per_comb * snr_per_comb
}
pub fn coherence_requirement_hz(&self) -> f64 {
let f_rep = self.comb1.f_rep;
if f_rep <= 0.0 {
return 0.0;
}
self.delta_frep * self.delta_frep / f_rep
}
pub fn spectral_coverage_nm(&self) -> f64 {
self.comb1.bandwidth_nm
}
pub fn is_alias_free(&self) -> bool {
let n = self.n_resolved_teeth() as f64;
self.delta_frep * n < self.comb1.f_rep / 2.0
}
}
#[derive(Debug, Clone)]
pub struct DirectCombSpectroscopy {
pub comb: FrequencyComb,
pub cavity_finesse: f64,
pub interaction_length_m: f64,
}
impl DirectCombSpectroscopy {
pub fn new(comb: FrequencyComb, finesse: f64) -> Self {
let interaction_length_m = C0 / (2.0 * comb.f_rep); Self {
comb,
cavity_finesse: finesse,
interaction_length_m,
}
}
pub fn enhancement_factor(&self) -> f64 {
self.cavity_finesse / std::f64::consts::PI
}
pub fn effective_path_length_m(&self) -> f64 {
2.0 * self.cavity_finesse * self.interaction_length_m / std::f64::consts::PI
}
pub fn min_detectable_concentration(&self, cross_section_cm2: f64) -> f64 {
let a_min = 1e-6; let l_eff_cm = self.effective_path_length_m() * 100.0; if cross_section_cm2 <= 0.0 || l_eff_cm <= 0.0 {
return f64::INFINITY;
}
a_min / (cross_section_cm2 * l_eff_cm) }
pub fn cavity_fsr_hz(&self) -> f64 {
C0 / (2.0 * self.interaction_length_m)
}
pub fn rep_rate_mismatch(&self) -> f64 {
let fsr = self.cavity_fsr_hz();
if fsr <= 0.0 {
return f64::INFINITY;
}
(self.comb.f_rep - fsr).abs() / fsr
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HhgGas {
Helium,
Neon,
Argon,
Krypton,
Xenon,
}
impl HhgGas {
pub fn ionization_potential_ev(&self) -> f64 {
match self {
HhgGas::Helium => 24.59,
HhgGas::Neon => 21.56,
HhgGas::Argon => 15.76,
HhgGas::Krypton => 14.00,
HhgGas::Xenon => 12.13,
}
}
pub fn cutoff_harmonic_estimate(&self, intensity_wcm2: f64, wavelength_nm: f64) -> usize {
let ip_ev = self.ionization_potential_ev();
let lambda_um = wavelength_nm * 1e-3; let up_ev = 9.33e-14 * intensity_wcm2 * lambda_um * lambda_um;
let e_cut_ev = ip_ev + 3.17 * up_ev;
let h_omega_ev = 1240.0 / wavelength_nm;
if h_omega_ev <= 0.0 {
return 1;
}
let harmonic = (e_cut_ev / h_omega_ev).floor() as usize;
harmonic.max(1)
}
}
#[derive(Debug, Clone)]
pub struct HhgSource {
pub drive_wavelength_nm: f64,
pub drive_intensity_wcm2: f64,
pub target_gas: HhgGas,
}
impl HhgSource {
pub fn new(wavelength_nm: f64, intensity_wcm2: f64, gas: HhgGas) -> Self {
Self {
drive_wavelength_nm: wavelength_nm,
drive_intensity_wcm2: intensity_wcm2,
target_gas: gas,
}
}
pub fn new_ti_sa_argon() -> Self {
Self::new(800.0, 2e14, HhgGas::Argon)
}
pub fn ponderomotive_energy_ev(&self) -> f64 {
let lambda_um = self.drive_wavelength_nm * 1e-3;
9.33e-14 * self.drive_intensity_wcm2 * lambda_um * lambda_um
}
pub fn cutoff_energy_ev(&self) -> f64 {
let ip = self.target_gas.ionization_potential_ev();
let up = self.ponderomotive_energy_ev();
ip + 3.17 * up
}
pub fn max_harmonic_order(&self) -> usize {
self.target_gas
.cutoff_harmonic_estimate(self.drive_intensity_wcm2, self.drive_wavelength_nm)
}
pub fn attosecond_pulse_duration_as(&self) -> f64 {
let n_cut = self.max_harmonic_order().max(1) as f64;
let t_laser_as = self.drive_wavelength_nm / (C0 * 1e9 / 1e18); t_laser_as / (3.0 * n_cut)
}
pub fn phase_matching_pressure_mbar(&self) -> f64 {
let i_ref = 2e14_f64; let base_pressure = match self.target_gas {
HhgGas::Helium => 200.0,
HhgGas::Neon => 80.0,
HhgGas::Argon => 20.0,
HhgGas::Krypton => 10.0,
HhgGas::Xenon => 5.0,
};
base_pressure * (self.drive_intensity_wcm2 / i_ref).sqrt()
}
pub fn is_single_attosecond_pulse(&self, drive_duration_fs: f64) -> bool {
let t_laser_fs = self.drive_wavelength_nm * 1e-9 / C0 * 1e15; drive_duration_fs < 2.0 * t_laser_fs
}
pub fn xuv_photon_flux(&self, photon_energy_ev: f64) -> f64 {
let e_cut = self.cutoff_energy_ev();
let ip = self.target_gas.ionization_potential_ev();
if photon_energy_ev > e_cut || photon_energy_ev < ip {
return 0.0;
}
let base_flux = 1e10_f64; let i_ref = 2e14_f64;
base_flux * (self.drive_intensity_wcm2 / i_ref).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_dcs_rf_spacing_is_delta_frep() {
let c1 = FrequencyComb::new_ti_sapphire(100_000_000.0, 10e6);
let c2 = FrequencyComb::new_ti_sapphire(100_001_000.0, 11e6); let dcs = DualCombSpectroscopy::new(c1, c2);
assert_abs_diff_eq!(dcs.rf_comb_spacing_hz(), 1000.0, epsilon = 0.01);
}
#[test]
fn test_dcs_acquisition_time() {
let c1 = FrequencyComb::new_ti_sapphire(100e6, 10e6);
let c2 = FrequencyComb::new_ti_sapphire(100e6 + 100.0, 11e6); let dcs = DualCombSpectroscopy::new(c1, c2);
assert_abs_diff_eq!(dcs.acquisition_time_s(), 0.01, epsilon = 1e-6);
}
#[test]
fn test_dcs_coherence_requirement_positive() {
let c1 = FrequencyComb::new_erbium_fiber(250e6, 0.0);
let c2 = FrequencyComb::new_erbium_fiber(250e6 + 500.0, 0.0);
let dcs = DualCombSpectroscopy::new(c1, c2);
let req = dcs.coherence_requirement_hz();
assert!(req > 0.0, "coherence requirement must be positive: {req}");
let expected = 500.0 * 500.0 / 250e6;
assert_abs_diff_eq!(req, expected, epsilon = 1e-6);
}
#[test]
fn test_dfcs_enhancement_factor() {
let comb = FrequencyComb::new_ti_sapphire(100e6, 0.0);
let dfcs = DirectCombSpectroscopy::new(comb, 10_000.0); let enh = dfcs.enhancement_factor();
assert_abs_diff_eq!(enh, 10_000.0 / std::f64::consts::PI, epsilon = 0.001);
}
#[test]
fn test_dfcs_effective_path_length_greater_than_physical() {
let comb = FrequencyComb::new_ti_sapphire(100e6, 0.0);
let dfcs = DirectCombSpectroscopy::new(comb, 1000.0);
assert!(
dfcs.effective_path_length_m() > dfcs.interaction_length_m,
"effective path {:.3} m must exceed physical length {:.3} m",
dfcs.effective_path_length_m(),
dfcs.interaction_length_m
);
}
#[test]
fn test_hhg_ponderomotive_energy_argon_800nm() {
let src = HhgSource::new_ti_sa_argon();
let up = src.ponderomotive_energy_ev();
let expected = 9.33e-14 * 2e14 * 0.8_f64.powi(2); assert_abs_diff_eq!(up, expected, epsilon = 0.01);
}
#[test]
fn test_hhg_cutoff_energy_exceeds_ip() {
let src = HhgSource::new_ti_sa_argon();
let e_cut = src.cutoff_energy_ev();
let ip = src.target_gas.ionization_potential_ev();
assert!(e_cut > ip, "cutoff {e_cut} eV must exceed Ip={ip} eV");
}
#[test]
fn test_hhg_gas_ionization_potentials_ordering() {
let gases = [
HhgGas::Helium,
HhgGas::Neon,
HhgGas::Argon,
HhgGas::Krypton,
HhgGas::Xenon,
];
for pair in gases.windows(2) {
assert!(
pair[0].ionization_potential_ev() > pair[1].ionization_potential_ev(),
"{:?} Ip should exceed {:?} Ip",
pair[0],
pair[1]
);
}
}
#[test]
fn test_hhg_attosecond_duration_positive() {
let src = HhgSource::new_ti_sa_argon();
let tau = src.attosecond_pulse_duration_as();
assert!(tau > 0.0, "attosecond duration must be positive: {tau} as");
assert!(tau < 1000.0, "duration {tau} as unreasonably large");
}
}