use crate::error::{OxiPhotonError, Result};
use std::f64::consts::PI;
const ME_C2_GEV: f64 = 0.000_510_998_950;
const E_CHARGE: f64 = 1.602_176_634e-19;
const C0: f64 = 2.997_924_58e8;
const I_ALFVEN: f64 = 17_045.0;
const HBAR_C_KEV_M: f64 = 1.973_269_804e-10;
#[inline]
fn b_rho_from_energy_gev(energy_gev: f64) -> f64 {
energy_gev / 0.299_792_458
}
#[derive(Debug, Clone)]
pub struct SynchrotronSource {
pub energy_gev: f64,
pub current_ma: f64,
pub bending_radius: f64,
}
impl SynchrotronSource {
pub fn new(energy_gev: f64, current_ma: f64, radius: f64) -> Result<Self> {
if !energy_gev.is_finite() || energy_gev <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"energy_gev must be positive and finite".into(),
));
}
if !current_ma.is_finite() || current_ma <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"current_ma must be positive and finite".into(),
));
}
if !radius.is_finite() || radius <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"bending_radius must be positive and finite".into(),
));
}
Ok(Self {
energy_gev,
current_ma,
bending_radius: radius,
})
}
pub fn lorentz_factor(&self) -> f64 {
self.energy_gev / ME_C2_GEV
}
pub fn critical_energy_kev(&self) -> f64 {
let gamma = self.lorentz_factor();
1.5 * HBAR_C_KEV_M * gamma.powi(3) / self.bending_radius
}
pub fn critical_wavelength_m(&self) -> f64 {
let gamma = self.lorentz_factor();
4.0 * PI * self.bending_radius / (3.0 * gamma.powi(3))
}
pub fn total_power_kw(&self) -> f64 {
let c_gamma = 8.846_3e-5; c_gamma * self.energy_gev.powi(4) * self.current_ma / self.bending_radius
}
pub fn spectral_flux(&self, energy_kev: f64) -> f64 {
let e_c = self.critical_energy_kev();
if e_c <= 0.0 {
return 0.0;
}
let y = energy_kev / e_c;
let h2 = universal_synchrotron_function(y);
1.744e13 * self.energy_gev * self.current_ma * h2
}
pub fn natural_opening_angle_rad(&self) -> f64 {
1.0 / self.lorentz_factor()
}
pub fn bending_field_t(&self) -> f64 {
b_rho_from_energy_gev(self.energy_gev) / self.bending_radius
}
}
fn universal_synchrotron_function(y: f64) -> f64 {
if y <= 0.0 {
return 0.0;
}
if y < 0.01 {
1.333 * y.powf(1.0 / 3.0)
} else if y > 20.0 {
((PI / (2.0 * y)).sqrt()) * (-y).exp()
} else {
let y13 = y.powf(1.0 / 3.0);
let gauss = (-0.97 * y).exp();
1.333 * y13 * gauss + 0.5 * (PI / (2.0 * y)).sqrt() * (-y).exp() * (1.0 - gauss)
}
}
#[derive(Debug, Clone)]
pub struct Undulator {
pub period_m: f64,
pub n_periods: usize,
pub k_parameter: f64,
pub beam_energy_gev: f64,
pub beam_current_ma: f64,
}
impl Undulator {
pub fn new(
period_mm: f64,
n_periods: usize,
b_field_t: f64,
energy_gev: f64,
current_ma: f64,
) -> Result<Self> {
if period_mm <= 0.0 || !period_mm.is_finite() {
return Err(OxiPhotonError::NumericalError(
"period_mm must be positive and finite".into(),
));
}
if n_periods == 0 {
return Err(OxiPhotonError::NumericalError(
"n_periods must be at least 1".into(),
));
}
if !b_field_t.is_finite() || b_field_t <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"b_field_t must be positive and finite".into(),
));
}
if !energy_gev.is_finite() || energy_gev <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"energy_gev must be positive and finite".into(),
));
}
if !current_ma.is_finite() || current_ma <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"current_ma must be positive and finite".into(),
));
}
let period_m = period_mm * 1e-3;
let period_cm = period_mm * 0.1;
let k = 0.9341 * b_field_t * period_cm;
Ok(Self {
period_m,
n_periods,
k_parameter: k,
beam_energy_gev: energy_gev,
beam_current_ma: current_ma,
})
}
pub fn lorentz_factor(&self) -> f64 {
self.beam_energy_gev / ME_C2_GEV
}
pub fn fundamental_wavelength_m(&self) -> f64 {
let gamma = self.lorentz_factor();
self.period_m / (2.0 * gamma * gamma) * (1.0 + self.k_parameter * self.k_parameter / 2.0)
}
pub fn harmonic_wavelength_m(&self, n: usize) -> f64 {
if n == 0 {
return f64::INFINITY;
}
self.fundamental_wavelength_m() / n as f64
}
pub fn bandwidth_fraction(&self, harmonic: usize) -> f64 {
if harmonic == 0 || self.n_periods == 0 {
return 0.0;
}
1.0 / (harmonic as f64 * self.n_periods as f64)
}
pub fn peak_brilliance_factor(&self) -> f64 {
let k2 = self.k_parameter * self.k_parameter;
let n2 = (self.n_periods as f64).powi(2);
n2 * k2 / (1.0 + k2 / 2.0)
}
pub fn tune_wavelength(&mut self, target_wavelength_m: f64) -> Result<f64> {
let gamma = self.lorentz_factor();
let lambda_min = self.period_m / (2.0 * gamma * gamma); if target_wavelength_m < lambda_min {
return Err(OxiPhotonError::NumericalError(format!(
"target wavelength {:.3e} m is below the K=0 limit {:.3e} m",
target_wavelength_m, lambda_min
)));
}
let k2 = 2.0 * (target_wavelength_m * 2.0 * gamma * gamma / self.period_m - 1.0);
if k2 < 0.0 {
return Err(OxiPhotonError::NumericalError(
"numerical underflow when computing K".into(),
));
}
self.k_parameter = k2.sqrt();
Ok(self.k_parameter)
}
pub fn angular_divergence_rad(&self, harmonic: usize) -> f64 {
if harmonic == 0 || self.n_periods == 0 {
return 0.0;
}
let lambda_n = self.harmonic_wavelength_m(harmonic);
let l_u = self.n_periods as f64 * self.period_m;
(lambda_n / (harmonic as f64 * l_u)).sqrt()
}
pub fn total_length_m(&self) -> f64 {
self.n_periods as f64 * self.period_m
}
}
#[derive(Debug, Clone)]
pub struct FreeElectronLaser {
pub undulator: Undulator,
pub peak_current_ka: f64,
pub emittance_m_rad: f64,
pub energy_spread: f64,
}
impl FreeElectronLaser {
pub fn new(undulator: Undulator, current_ka: f64, emittance_nm: f64) -> Self {
Self {
undulator,
peak_current_ka: current_ka,
emittance_m_rad: emittance_nm * 1e-9,
energy_spread: 1e-4, }
}
pub fn pierce_parameter(&self) -> f64 {
let gamma = self.undulator.lorentz_factor();
let k = self.undulator.k_parameter;
let lambda_u = self.undulator.period_m;
let i_peak_a = self.peak_current_ka * 1e3;
let beta_func = lambda_u; let sigma_x = (self.emittance_m_rad * beta_func / gamma).sqrt().max(1e-9);
let beam_area = PI * sigma_x * sigma_x;
let j_e = i_peak_a / beam_area;
let k_jj = k * k / (2.0 + k * k); let rho = (1.0 / gamma) * (k_jj * lambda_u * j_e / (I_ALFVEN * gamma)).powf(1.0 / 3.0)
/ (4.0 * PI);
rho.max(1e-5) }
pub fn gain_length_m(&self) -> f64 {
let rho = self.pierce_parameter();
self.undulator.period_m / (4.0 * PI * 3.0_f64.sqrt() * rho)
}
pub fn saturation_length_m(&self) -> f64 {
20.0 * self.gain_length_m()
}
pub fn saturation_power_gw(&self) -> f64 {
let gamma = self.undulator.lorentz_factor();
let me_c2_j = 9.109_383_7e-31 * C0 * C0; let i_a = self.peak_current_ka * 1e3; let p_beam_w = gamma * me_c2_j * i_a / E_CHARGE;
let rho = self.pierce_parameter();
rho * p_beam_w * 1e-9 }
pub fn coherence_time_fs(&self) -> f64 {
let lambda1 = self.undulator.fundamental_wavelength_m();
let rho = self.pierce_parameter();
if rho <= 0.0 || C0 <= 0.0 {
return 0.0;
}
(lambda1 / (C0 * rho)) * 1e15 }
pub fn pulse_duration_fs(&self, bunch_length_fs: f64) -> f64 {
bunch_length_fs
}
pub fn n_temporal_modes(&self, bunch_length_fs: f64) -> f64 {
let tau_c = self.coherence_time_fs();
if tau_c <= 0.0 {
return 1.0;
}
(bunch_length_fs / tau_c).max(1.0)
}
pub fn saturation_pulse_energy_uj(&self, bunch_length_fs: f64) -> f64 {
let p_gw = self.saturation_power_gw();
let tau_s = bunch_length_fs * 1e-15; p_gw * 1e9 * tau_s * 1e6 }
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn esrf() -> SynchrotronSource {
SynchrotronSource::new(6.0, 200.0, 23.58).expect("valid ESRF parameters")
}
fn lcls_undulator() -> Undulator {
Undulator::new(26.0, 130, 1.0, 4.0, 100.0).expect("valid undulator")
}
#[test]
fn synchrotron_lorentz_factor() {
let src = esrf();
let gamma = src.lorentz_factor();
let expected = 6.0 / ME_C2_GEV;
assert_abs_diff_eq!(gamma, expected, epsilon = 1.0);
}
#[test]
fn synchrotron_critical_energy_esrf() {
let src = esrf();
let ec = src.critical_energy_kev();
assert!(
(ec - 20.5).abs() < 2.0,
"E_c should be ~20.5 keV, got {ec:.2} keV"
);
}
#[test]
fn synchrotron_total_power_positive() {
let src = esrf();
let p = src.total_power_kw();
assert!(p > 0.0 && p.is_finite());
}
#[test]
fn synchrotron_opening_angle_small() {
let src = esrf();
let angle = src.natural_opening_angle_rad();
assert!(
angle < 1e-3,
"opening angle should be small (µrad range), got {angle:.3e}"
);
}
#[test]
fn synchrotron_bad_energy() {
assert!(SynchrotronSource::new(-1.0, 200.0, 23.58).is_err());
assert!(SynchrotronSource::new(6.0, 0.0, 23.58).is_err());
}
#[test]
fn undulator_fundamental_wavelength() {
let und = lcls_undulator();
let lambda1 = und.fundamental_wavelength_m();
assert!(
lambda1 < 10e-9 && lambda1 > 1e-12,
"λ₁ = {lambda1:.3e} m out of expected X-ray range"
);
}
#[test]
fn undulator_harmonic_halves_wavelength() {
let und = lcls_undulator();
let l1 = und.harmonic_wavelength_m(1);
let l2 = und.harmonic_wavelength_m(2);
assert_abs_diff_eq!(l2, l1 / 2.0, epsilon = 1e-20);
}
#[test]
fn undulator_bandwidth_decreases_with_harmonic() {
let und = lcls_undulator();
let bw1 = und.bandwidth_fraction(1);
let bw3 = und.bandwidth_fraction(3);
assert!(bw3 < bw1, "higher harmonic should have narrower bandwidth");
}
#[test]
fn undulator_tune_wavelength_roundtrip() {
let mut und = lcls_undulator();
let original_lambda = und.fundamental_wavelength_m();
let target = original_lambda * 1.5;
und.tune_wavelength(target).expect("tuning should succeed");
let new_lambda = und.fundamental_wavelength_m();
assert_abs_diff_eq!(new_lambda, target, epsilon = target * 1e-10);
}
#[test]
fn undulator_tune_wavelength_too_short() {
let mut und = lcls_undulator();
let gamma = und.lorentz_factor();
let lambda_min = und.period_m / (2.0 * gamma * gamma);
let result = und.tune_wavelength(lambda_min * 0.5);
assert!(result.is_err());
}
#[test]
fn fel_pierce_parameter_reasonable() {
let und = lcls_undulator();
let fel = FreeElectronLaser::new(und, 3.0, 0.5);
let rho = fel.pierce_parameter();
assert!(
rho > 1e-5 && rho < 1e-1,
"Pierce parameter out of range: {rho:.3e}"
);
}
#[test]
fn fel_saturation_power_positive() {
let und = lcls_undulator();
let fel = FreeElectronLaser::new(und, 3.0, 0.5);
let p = fel.saturation_power_gw();
assert!(p > 0.0 && p.is_finite());
}
#[test]
fn fel_coherence_time_positive() {
let und = lcls_undulator();
let fel = FreeElectronLaser::new(und, 3.0, 0.5);
let tau = fel.coherence_time_fs();
assert!(tau > 0.0 && tau.is_finite());
}
#[test]
fn fel_saturation_length_longer_than_gain_length() {
let und = lcls_undulator();
let fel = FreeElectronLaser::new(und, 3.0, 0.5);
assert!(fel.saturation_length_m() > fel.gain_length_m());
}
}