use std::f64::consts::PI;
pub const C_LIGHT: f64 = 2.99792458e8;
const C_CM_S: f64 = 2.99792458e10;
#[derive(Debug, Clone)]
pub struct CarsSetup {
pub pump_wavelength_m: f64,
pub stokes_wavelength_m: f64,
pub probe_wavelength_m: f64,
pub n_medium: f64,
}
impl CarsSetup {
pub fn new(
pump_wavelength_m: f64,
stokes_wavelength_m: f64,
probe_wavelength_m: f64,
n_medium: f64,
) -> Self {
Self {
pump_wavelength_m,
stokes_wavelength_m,
probe_wavelength_m,
n_medium,
}
}
pub fn raman_shift_cm1(&self) -> f64 {
(1.0 / self.pump_wavelength_m - 1.0 / self.stokes_wavelength_m) * 1e-2
}
pub fn cars_wavelength_m(&self) -> f64 {
let inv = 1.0 / self.pump_wavelength_m - 1.0 / self.stokes_wavelength_m
+ 1.0 / self.probe_wavelength_m;
1.0 / inv.max(f64::EPSILON)
}
pub fn phase_mismatch_collinear(&self) -> f64 {
let lambda_cars = self.cars_wavelength_m();
let k_cars = 2.0 * PI * self.n_medium / lambda_cars;
let k_pump = 2.0 * PI * self.n_medium / self.pump_wavelength_m;
let k_probe = 2.0 * PI * self.n_medium / self.probe_wavelength_m;
let k_stokes = 2.0 * PI * self.n_medium / self.stokes_wavelength_m;
k_cars - k_pump - k_probe + k_stokes
}
pub fn coherence_length(&self) -> f64 {
let dk = self.phase_mismatch_collinear().abs();
if dk < 1e-30 {
f64::INFINITY
} else {
PI / dk
}
}
}
#[derive(Debug, Clone)]
pub struct RamanSusceptibility {
pub resonance_freq_cm1: f64,
pub linewidth_cm1: f64,
pub amplitude: f64,
}
impl RamanSusceptibility {
pub fn new(resonance_freq_cm1: f64, linewidth_cm1: f64, amplitude: f64) -> Self {
Self {
resonance_freq_cm1,
linewidth_cm1,
amplitude,
}
}
pub fn susceptibility(&self, omega_cm1: f64) -> (f64, f64) {
let delta = self.resonance_freq_cm1 - omega_cm1;
let gamma_half = self.linewidth_cm1 * 0.5;
let denom = delta * delta + gamma_half * gamma_half;
let re = self.amplitude * delta / denom;
let im = self.amplitude * gamma_half / denom;
(re, im)
}
pub fn peak_frequency(&self) -> f64 {
self.resonance_freq_cm1
}
pub fn dephasing_time(&self) -> f64 {
1.0 / (PI * C_CM_S * self.linewidth_cm1)
}
}
#[derive(Debug, Clone)]
pub struct CarsSignal {
pub chi_nr: f64,
pub resonances: Vec<RamanSusceptibility>,
}
impl CarsSignal {
pub fn new(chi_nr: f64, resonances: Vec<RamanSusceptibility>) -> Self {
Self { chi_nr, resonances }
}
pub fn total_chi3(&self, omega_cm1: f64) -> (f64, f64) {
let mut re = self.chi_nr;
let mut im = 0.0_f64;
for res in &self.resonances {
let (r, i) = res.susceptibility(omega_cm1);
re += r;
im += i;
}
(re, im)
}
pub fn cars_intensity(
&self,
omega_cm1: f64,
pump_intensity: f64,
stokes_intensity: f64,
) -> f64 {
let (re, im) = self.total_chi3(omega_cm1);
let chi_sq = re * re + im * im;
chi_sq * pump_intensity * pump_intensity * stokes_intensity
}
pub fn srs_signal(&self, omega_cm1: f64, pump_intensity: f64, stokes_intensity: f64) -> f64 {
let mut im_r = 0.0_f64;
for res in &self.resonances {
let (_, i) = res.susceptibility(omega_cm1);
im_r += i;
}
im_r * pump_intensity * stokes_intensity
}
pub fn spectrum(
&self,
omega_min_cm1: f64,
omega_max_cm1: f64,
n_points: usize,
pump_power: f64,
stokes_power: f64,
) -> Vec<(f64, f64)> {
if n_points == 0 {
return Vec::new();
}
let step = (omega_max_cm1 - omega_min_cm1) / (n_points.saturating_sub(1).max(1)) as f64;
(0..n_points)
.map(|i| {
let omega = omega_min_cm1 + i as f64 * step;
let intensity = self.cars_intensity(omega, pump_power, stokes_power);
(omega, intensity)
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SrsDetector {
pub detection_sensitivity: f64,
}
impl SrsDetector {
pub fn new(detection_sensitivity: f64) -> Self {
Self {
detection_sensitivity,
}
}
pub fn srl_signal(&self, chi_r_im: f64, pump_intensity: f64, stokes_intensity: f64) -> f64 {
chi_r_im * pump_intensity * stokes_intensity
}
pub fn srg_signal(&self, chi_r_im: f64, pump_intensity: f64, stokes_intensity: f64) -> f64 {
chi_r_im * pump_intensity * stokes_intensity
}
pub fn minimum_detectable_concentration(
&self,
raman_cross_section: f64,
pump_intensity: f64,
) -> f64 {
let n_a = 6.02214076e23_f64;
let denominator = raman_cross_section * pump_intensity * n_a;
if denominator < f64::EPSILON {
f64::INFINITY
} else {
self.detection_sensitivity / denominator
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ch2_setup() -> CarsSetup {
CarsSetup::new(817e-9, 1064e-9, 817e-9, 1.33)
}
fn ch2_resonance() -> RamanSusceptibility {
RamanSusceptibility::new(2850.0, 10.0, 1.0)
}
#[test]
fn test_raman_shift_ch2() {
let setup = ch2_setup();
let shift = setup.raman_shift_cm1();
assert!(
(shift - 2845.0).abs() < 30.0,
"CH₂ Raman shift {} far from 2850 cm⁻¹",
shift
);
}
#[test]
fn test_cars_wavelength_shorter_than_pump() {
let setup = ch2_setup();
let lambda_cars = setup.cars_wavelength_m();
assert!(
lambda_cars < setup.pump_wavelength_m,
"CARS wavelength {} should be < pump {}",
lambda_cars,
setup.pump_wavelength_m
);
}
#[test]
fn test_raman_susceptibility_peak_on_resonance() {
let res = ch2_resonance();
let (re_on, im_on) = res.susceptibility(2850.0);
let (re_off, _) = res.susceptibility(2900.0);
assert!(re_on.abs() < 1e-6, "Re[χ_R] should be zero on resonance");
assert!(im_on > 0.0, "Im[χ_R] should be positive (our convention)");
assert!(
re_off.abs() > 0.0,
"Re[χ_R] should be non-zero off resonance"
);
}
#[test]
fn test_dephasing_time_physical() {
let res = ch2_resonance(); let t2 = res.dephasing_time();
assert!(t2 > 0.5e-12 && t2 < 5e-12, "T₂ = {} s out of range", t2);
}
#[test]
fn test_cars_background_beats_resonance_far_off_peak() {
let chi_nr = 1.0;
let signal = CarsSignal::new(chi_nr, vec![ch2_resonance()]);
let i_off = signal.cars_intensity(3500.0, 1.0, 1.0);
let i_on = signal.cars_intensity(2850.0, 1.0, 1.0);
assert!(i_on > i_off, "On-resonance CARS should exceed background");
}
#[test]
fn test_srs_background_free() {
let signal = CarsSignal::new(10.0, vec![ch2_resonance()]);
let srs_on = signal.srs_signal(2850.0, 1.0, 1.0);
let srs_off = signal.srs_signal(3500.0, 1.0, 1.0);
assert!(srs_on > srs_off, "SRS should be maximal on resonance");
}
#[test]
fn test_spectrum_length_and_ordering() {
let signal = CarsSignal::new(0.5, vec![ch2_resonance()]);
let spec = signal.spectrum(2700.0, 3000.0, 50, 1.0, 1.0);
assert_eq!(spec.len(), 50);
for w in spec.windows(2) {
assert!(w[1].0 > w[0].0, "Spectrum frequencies not increasing");
}
}
#[test]
fn test_srs_detector_minimum_concentration() {
let det = SrsDetector::new(1e-7);
let c_min = det.minimum_detectable_concentration(1e-30, 1e12);
assert!(
c_min > 0.0 && c_min.is_finite(),
"C_min must be positive finite"
);
}
}