use num_complex::Complex64;
use std::f64::consts::PI;
use crate::error::{OxiPhotonError, Result};
const C0: f64 = 2.99792458e8;
#[derive(Debug, Clone)]
pub struct FiberBraggGrating {
pub center_wavelength_nm: f64,
pub grating_length_mm: f64,
pub index_modulation: f64,
pub average_index: f64,
pub grating_period_nm: f64,
pub strain_sensitivity_pm_per_microstrain: f64,
pub temperature_sensitivity_pm_per_c: f64,
}
impl FiberBraggGrating {
pub fn new(center_lambda_nm: f64, length_mm: f64, delta_n: f64, n_eff: f64) -> Result<Self> {
if center_lambda_nm <= 0.0 || !center_lambda_nm.is_finite() {
return Err(OxiPhotonError::InvalidWavelength(center_lambda_nm * 1e-9));
}
if length_mm <= 0.0 || !length_mm.is_finite() {
return Err(OxiPhotonError::NumericalError(format!(
"grating_length_mm must be positive, got {length_mm}"
)));
}
if delta_n <= 0.0 || !delta_n.is_finite() {
return Err(OxiPhotonError::NumericalError(format!(
"index_modulation must be positive, got {delta_n}"
)));
}
if n_eff <= 0.0 || !n_eff.is_finite() {
return Err(OxiPhotonError::InvalidRefractiveIndex { n: n_eff, k: 0.0 });
}
let period = center_lambda_nm / (2.0 * n_eff);
let strain_sens = center_lambda_nm * 1e3 * (1.0 - 0.212) * 1e-6; let temp_sens = center_lambda_nm * 1e3 * (0.55e-6 + 8.6e-6); Ok(Self {
center_wavelength_nm: center_lambda_nm,
grating_length_mm: length_mm,
index_modulation: delta_n,
average_index: n_eff,
grating_period_nm: period,
strain_sensitivity_pm_per_microstrain: strain_sens,
temperature_sensitivity_pm_per_c: temp_sens,
})
}
pub fn smf28_1550() -> Result<Self> {
Self::new(1550.0, 10.0, 3e-4, 1.4682)
}
pub fn grating_period_nm(&self) -> f64 {
self.center_wavelength_nm / (2.0 * self.average_index)
}
pub fn coupling_coefficient(&self) -> f64 {
let lambda_m = self.center_wavelength_nm * 1e-9;
PI * self.index_modulation / lambda_m
}
pub fn peak_reflectivity(&self) -> f64 {
let kappa = self.coupling_coefficient();
let l_m = self.grating_length_mm * 1e-3;
let x = kappa * l_m;
let th = x.tanh();
th * th
}
pub fn bandwidth_nm(&self) -> f64 {
let lambda_b = self.center_wavelength_nm;
let n_eff = self.average_index;
let l_nm = self.grating_length_mm * 1e6; let big_n = self.n_periods() as f64;
if big_n < 1.0 {
return 0.0;
}
let term1 = self.index_modulation / (2.0 * n_eff);
let term2 = 1.0 / big_n;
lambda_b * lambda_b / (n_eff * l_nm) * (term1 * term1 + term2 * term2).sqrt()
}
pub fn n_periods(&self) -> usize {
let l_nm = self.grating_length_mm * 1e6; (l_nm / self.grating_period_nm()).round() as usize
}
pub fn reflection_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
if n_pts == 0 || lambda_min_nm >= lambda_max_nm {
return Vec::new();
}
let kappa = self.coupling_coefficient();
let l_m = self.grating_length_mm * 1e-3;
let lambda_b_m = self.center_wavelength_nm * 1e-9;
let n_eff = self.average_index;
let kappa_sq = kappa * kappa;
(0..n_pts)
.map(|i| {
let lambda_nm = lambda_min_nm
+ (lambda_max_nm - lambda_min_nm) * i as f64 / (n_pts - 1).max(1) as f64;
let lambda_m = lambda_nm * 1e-9;
let delta = PI * n_eff * (1.0 / lambda_m - 1.0 / lambda_b_m);
let s_sq = kappa_sq - delta * delta;
let r = if s_sq > 0.0 {
let s = s_sq.sqrt();
let sl = s * l_m;
let sinh_sl = sl.sinh();
let cosh_sl = sl.cosh();
let denom = cosh_sl * cosh_sl - delta * delta / kappa_sq;
if denom.abs() < 1e-30 {
1.0
} else {
(sinh_sl * sinh_sl / denom).clamp(0.0, 1.0)
}
} else if s_sq < 0.0 {
let s = (-s_sq).sqrt();
let sl = s * l_m;
let sin_sl = sl.sin();
let cos_sl = sl.cos();
let denom = cos_sl * cos_sl + delta * delta / kappa_sq;
if denom.abs() < 1e-30 {
0.0
} else {
(sin_sl * sin_sl / denom).clamp(0.0, 1.0)
}
} else {
let x = kappa * l_m;
let th = x.tanh();
th * th
};
(lambda_nm, r)
})
.collect()
}
pub fn transmission_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
self.reflection_spectrum(lambda_min_nm, lambda_max_nm, n_pts)
.into_iter()
.map(|(lam, r)| (lam, 1.0 - r))
.collect()
}
pub fn strain_shift_nm(&self, strain_microstrain: f64) -> f64 {
let pe = 0.212_f64;
self.center_wavelength_nm * (1.0 - pe) * strain_microstrain * 1e-6
}
pub fn temperature_shift_nm(&self, delta_t_celsius: f64) -> f64 {
let alpha_l = 0.55e-6_f64; let xi = 8.6e-6_f64; self.center_wavelength_nm * (alpha_l + xi) * delta_t_celsius
}
pub fn combined_shift_nm(&self, strain_microstrain: f64, delta_t_celsius: f64) -> f64 {
self.strain_shift_nm(strain_microstrain) + self.temperature_shift_nm(delta_t_celsius)
}
pub fn strain_from_shift_microstrain(&self, delta_lambda_nm: f64) -> f64 {
let pe = 0.212_f64;
let sensitivity = self.center_wavelength_nm * (1.0 - pe) * 1e-6; if sensitivity.abs() < 1e-60 {
return 0.0;
}
delta_lambda_nm / sensitivity
}
pub fn temperature_from_shift_c(&self, delta_lambda_nm: f64) -> f64 {
let alpha_l = 0.55e-6_f64;
let xi = 8.6e-6_f64;
let sensitivity = self.center_wavelength_nm * (alpha_l + xi); if sensitivity.abs() < 1e-60 {
return 0.0;
}
delta_lambda_nm / sensitivity
}
fn complex_reflection(&self, lambda_nm: f64) -> Complex64 {
let kappa = self.coupling_coefficient();
let l_m = self.grating_length_mm * 1e-3;
let lambda_b_m = self.center_wavelength_nm * 1e-9;
let lambda_m = lambda_nm * 1e-9;
let n_eff = self.average_index;
let delta = PI * n_eff * (1.0 / lambda_m - 1.0 / lambda_b_m);
let s_sq = kappa * kappa - delta * delta;
if s_sq > 1e-30 {
let s = s_sq.sqrt();
let sl = s * l_m;
let sinh_sl = Complex64::new(sl.sinh(), 0.0);
let cosh_sl = Complex64::new(sl.cosh(), 0.0);
let numerator = Complex64::new(0.0, -kappa) * sinh_sl;
let denominator = s * cosh_sl + Complex64::new(0.0, delta) * sinh_sl;
if denominator.norm() < 1e-60 {
Complex64::new(0.0, 0.0)
} else {
numerator / denominator
}
} else if s_sq < -1e-30 {
let s_abs = (-s_sq).sqrt();
let sl = s_abs * l_m;
let sinh_isl = Complex64::new(0.0, sl.sin());
let cosh_isl = Complex64::new(sl.cos(), 0.0);
let numerator = Complex64::new(0.0, -kappa) * sinh_isl;
let denominator = s_abs * cosh_isl + Complex64::new(0.0, delta) * sinh_isl;
if denominator.norm() < 1e-60 {
Complex64::new(0.0, 0.0)
} else {
numerator / denominator
}
} else {
let x = kappa * l_m;
Complex64::new(0.0, -x.tanh())
}
}
pub fn phase_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
if n_pts == 0 || lambda_min_nm >= lambda_max_nm {
return Vec::new();
}
(0..n_pts)
.map(|i| {
let lambda_nm = lambda_min_nm
+ (lambda_max_nm - lambda_min_nm) * i as f64 / (n_pts - 1).max(1) as f64;
let r = self.complex_reflection(lambda_nm);
(lambda_nm, r.im.atan2(r.re))
})
.collect()
}
pub fn group_delay_ps(&self, lambda_nm: f64) -> f64 {
let dlambda = 0.001; let lam1 = lambda_nm - dlambda;
let lam2 = lambda_nm + dlambda;
let phi1 = {
let r = self.complex_reflection(lam1);
r.im.atan2(r.re)
};
let phi2 = {
let r = self.complex_reflection(lam2);
r.im.atan2(r.re)
};
let dphi_dlambda = (phi2 - phi1) / (2.0 * dlambda * 1e-9); let lambda_m = lambda_nm * 1e-9;
let domega_dlambda = -2.0 * PI * C0 / (lambda_m * lambda_m); let tau_s = -dphi_dlambda / domega_dlambda; tau_s * 1e12 }
pub fn with_gaussian_apodization(self, sigma_fraction: f64) -> ApodizedFbg {
let sigma = sigma_fraction.clamp(0.01, 0.5);
ApodizedFbg {
fbg: self,
apodization_profile: ApodizationProfile::Gaussian { sigma },
}
}
}
#[derive(Debug, Clone)]
pub enum ApodizationProfile {
Gaussian { sigma: f64 },
RaisedCosine,
Hamming,
Uniform,
}
impl ApodizationProfile {
fn weight(&self, u: f64) -> f64 {
match self {
ApodizationProfile::Gaussian { sigma } => {
let z = u - 0.5; (-z * z / (2.0 * sigma * sigma)).exp()
}
ApodizationProfile::RaisedCosine => 0.5 * (1.0 - (2.0 * PI * u).cos()),
ApodizationProfile::Hamming => 0.54 - 0.46 * (2.0 * PI * u).cos(),
ApodizationProfile::Uniform => 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct ApodizedFbg {
pub fbg: FiberBraggGrating,
pub apodization_profile: ApodizationProfile,
}
impl ApodizedFbg {
const N_SECTIONS: usize = 200;
fn complex_reflection_tmm(&self, lambda_nm: f64) -> Complex64 {
let n = Self::N_SECTIONS;
let kappa_0 = self.fbg.coupling_coefficient();
let l_m = self.fbg.grating_length_mm * 1e-3;
let dz = l_m / n as f64;
let lambda_b_m = self.fbg.center_wavelength_nm * 1e-9;
let lambda_m = lambda_nm * 1e-9;
let n_eff = self.fbg.average_index;
let delta = PI * n_eff * (1.0 / lambda_m - 1.0 / lambda_b_m);
let mut m11 = Complex64::new(1.0, 0.0);
let mut m12 = Complex64::new(0.0, 0.0);
let mut m21 = Complex64::new(0.0, 0.0);
let mut m22 = Complex64::new(1.0, 0.0);
for j in 0..n {
let u = (j as f64 + 0.5) / n as f64;
let kappa_j = kappa_0 * self.apodization_profile.weight(u);
let s_sq = kappa_j * kappa_j - delta * delta;
let (t11, t12, t21, t22) = if s_sq > 1e-30 {
let s = s_sq.sqrt();
let sdz = s * dz;
let cosh_s = sdz.cosh();
let sinh_s = sdz.sinh();
let t11 = Complex64::new(cosh_s, -delta * sinh_s / s);
let t22 = Complex64::new(cosh_s, delta * sinh_s / s);
let t12 = Complex64::new(0.0, -kappa_j * sinh_s / s);
let t21 = Complex64::new(0.0, kappa_j * sinh_s / s);
(t11, t12, t21, t22)
} else if s_sq < -1e-30 {
let s_abs = (-s_sq).sqrt();
let sdz = s_abs * dz;
let cos_s = sdz.cos();
let sin_s = sdz.sin();
let t11 = Complex64::new(cos_s, -delta * sin_s / s_abs);
let t22 = Complex64::new(cos_s, delta * sin_s / s_abs);
let t12 = Complex64::new(0.0, -kappa_j * sin_s / s_abs);
let t21 = Complex64::new(0.0, kappa_j * sin_s / s_abs);
(t11, t12, t21, t22)
} else {
let t11 = Complex64::new(1.0, -delta * dz);
let t22 = Complex64::new(1.0, delta * dz);
let t12 = Complex64::new(0.0, 0.0);
let t21 = Complex64::new(0.0, 0.0);
(t11, t12, t21, t22)
};
let new11 = t11 * m11 + t12 * m21;
let new12 = t11 * m12 + t12 * m22;
let new21 = t21 * m11 + t22 * m21;
let new22 = t21 * m12 + t22 * m22;
m11 = new11;
m12 = new12;
m21 = new21;
m22 = new22;
}
if m22.norm() < 1e-60 {
Complex64::new(0.0, 0.0)
} else {
-m21 / m22
}
}
pub fn peak_reflectivity(&self) -> f64 {
let r = self.complex_reflection_tmm(self.fbg.center_wavelength_nm);
r.norm_sqr().min(1.0)
}
pub fn sidelobe_level_db(&self) -> f64 {
let bw = self.fbg.bandwidth_nm().max(0.01);
let lambda_b = self.fbg.center_wavelength_nm;
let scan_width = 5.0 * bw;
let spectrum = self.reflection_spectrum(lambda_b - scan_width, lambda_b + scan_width, 4000);
let r_peak = spectrum.iter().map(|&(_, r)| r).fold(0.0_f64, f64::max);
let sidelobe_max = spectrum
.iter()
.filter(|&&(lam, _)| (lam - lambda_b).abs() > 1.5 * bw)
.map(|&(_, r)| r)
.fold(0.0_f64, f64::max);
if r_peak < 1e-30 || sidelobe_max < 1e-30 {
return -60.0; }
10.0 * (sidelobe_max / r_peak).log10()
}
pub fn bandwidth_nm(&self) -> f64 {
let bw_est = self.fbg.bandwidth_nm().max(0.01);
let lambda_b = self.fbg.center_wavelength_nm;
let spectrum =
self.reflection_spectrum(lambda_b - 3.0 * bw_est, lambda_b + 3.0 * bw_est, 2000);
let r_peak = spectrum.iter().map(|&(_, r)| r).fold(0.0_f64, f64::max);
let half_max = r_peak / 2.0;
let above_half: Vec<f64> = spectrum
.iter()
.filter(|&&(_, r)| r >= half_max)
.map(|&(lam, _)| lam)
.collect();
if above_half.is_empty() {
return 0.0;
}
let lam_min = above_half.iter().cloned().fold(f64::INFINITY, f64::min);
let lam_max = above_half.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
lam_max - lam_min
}
pub fn reflection_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
if n_pts == 0 || lambda_min_nm >= lambda_max_nm {
return Vec::new();
}
(0..n_pts)
.map(|i| {
let lambda_nm = lambda_min_nm
+ (lambda_max_nm - lambda_min_nm) * i as f64 / (n_pts - 1).max(1) as f64;
let r = self.complex_reflection_tmm(lambda_nm);
(lambda_nm, r.norm_sqr().clamp(0.0, 1.0))
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct FbgInterrogator {
pub fbg_array: Vec<FiberBraggGrating>,
pub light_source_bandwidth_nm: f64,
pub wavelength_resolution_pm: f64,
pub scan_rate_hz: f64,
}
impl FbgInterrogator {
pub fn new(
fbg_array: Vec<FiberBraggGrating>,
source_bw_nm: f64,
resolution_pm: f64,
scan_rate_hz: f64,
) -> Result<Self> {
if source_bw_nm <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"source bandwidth must be positive, got {source_bw_nm}"
)));
}
if resolution_pm <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"wavelength resolution must be positive, got {resolution_pm}"
)));
}
if scan_rate_hz <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"scan rate must be positive, got {scan_rate_hz}"
)));
}
Ok(Self {
fbg_array,
light_source_bandwidth_nm: source_bw_nm,
wavelength_resolution_pm: resolution_pm,
scan_rate_hz,
})
}
pub fn measure_shifts_nm(&self, strains: &[f64], delta_temps: &[f64]) -> Vec<f64> {
self.fbg_array
.iter()
.enumerate()
.map(|(i, fbg)| {
let eps = strains.get(i).copied().unwrap_or(0.0);
let dt = delta_temps.get(i).copied().unwrap_or(0.0);
fbg.combined_shift_nm(eps, dt)
})
.collect()
}
pub fn strain_resolution_microstrain(&self) -> f64 {
match self.fbg_array.first() {
None => f64::INFINITY,
Some(fbg) => {
let sens = fbg.strain_sensitivity_pm_per_microstrain;
if sens.abs() < 1e-60 {
f64::INFINITY
} else {
self.wavelength_resolution_pm / sens
}
}
}
}
pub fn temperature_resolution_c(&self) -> f64 {
match self.fbg_array.first() {
None => f64::INFINITY,
Some(fbg) => {
let sens = fbg.temperature_sensitivity_pm_per_c;
if sens.abs() < 1e-60 {
f64::INFINITY
} else {
self.wavelength_resolution_pm / sens
}
}
}
}
pub fn multiplexing_capacity(&self) -> usize {
if self.fbg_array.is_empty() {
return 0;
}
let mean_bw: f64 = self.fbg_array.iter().map(|f| f.bandwidth_nm()).sum::<f64>()
/ self.fbg_array.len() as f64;
let slot_nm = (10.0 * mean_bw).max(0.5); (self.light_source_bandwidth_nm / slot_nm).floor() as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn smf28() -> FiberBraggGrating {
FiberBraggGrating::smf28_1550().expect("SMF-28 FBG construction should succeed")
}
#[test]
fn test_fbg_grating_period() {
let fbg = smf28();
let expected = 1550.0 / (2.0 * 1.4682);
assert_relative_eq!(fbg.grating_period_nm(), expected, max_relative = 1e-9);
}
#[test]
fn test_fbg_coupling_coefficient() {
let fbg = smf28();
let expected = PI * 3e-4 / (1550e-9);
assert_relative_eq!(fbg.coupling_coefficient(), expected, max_relative = 1e-9);
}
#[test]
fn test_fbg_peak_reflectivity_high() {
let fbg = smf28();
let r = fbg.peak_reflectivity();
assert!(r > 0.99, "Expected high reflectivity, got {r:.6}");
assert!(r <= 1.0, "Reflectivity must be ≤ 1, got {r:.6}");
}
#[test]
fn test_fbg_strain_shift_positive() {
let fbg = smf28();
let shift = fbg.strain_shift_nm(1000.0); assert!(
shift > 0.0,
"Tensile strain should produce a red shift, got {shift:.6} nm"
);
}
#[test]
fn test_fbg_temperature_shift_positive() {
let fbg = smf28();
let shift = fbg.temperature_shift_nm(10.0); assert!(
shift > 0.0,
"Heating should produce a red shift, got {shift:.6} nm"
);
}
#[test]
fn test_fbg_combined_shift() {
let fbg = smf28();
let eps = 500.0; let dt = 5.0; let combined = fbg.combined_shift_nm(eps, dt);
let expected = fbg.strain_shift_nm(eps) + fbg.temperature_shift_nm(dt);
assert_relative_eq!(combined, expected, max_relative = 1e-12);
}
#[test]
fn test_fbg_bandwidth_positive() {
let fbg = smf28();
let bw = fbg.bandwidth_nm();
assert!(bw > 0.0, "Bandwidth should be positive, got {bw:.6}");
}
#[test]
fn test_reflection_spectrum_peak_at_center() {
let fbg = smf28();
let lambda_b = fbg.center_wavelength_nm;
let spectrum = fbg.reflection_spectrum(lambda_b - 1.0, lambda_b + 1.0, 1001);
let (lam_peak, r_peak) = spectrum
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|&(l, r)| (l, r))
.expect("spectrum should be non-empty");
assert!(
r_peak > 0.99,
"Peak reflectivity should be > 0.99, got {r_peak}"
);
assert!(
(lam_peak - lambda_b).abs() < 0.01,
"Peak should be at λ_B ≈ {lambda_b} nm, found {lam_peak} nm"
);
}
#[test]
fn test_interrogator_resolution() {
let fbg = smf28();
let resolution_pm = 1.0; let interrogator = FbgInterrogator::new(
vec![fbg.clone()],
40.0, resolution_pm,
1000.0,
)
.expect("Interrogator construction should succeed");
let strain_res = interrogator.strain_resolution_microstrain();
assert!(
strain_res > 0.0 && strain_res < 5.0,
"Strain resolution should be < 5 με, got {strain_res:.4} με"
);
let temp_res = interrogator.temperature_resolution_c();
assert!(
temp_res > 0.0 && temp_res < 1.0,
"Temp resolution should be < 1 °C, got {temp_res:.4} °C"
);
}
#[test]
fn test_n_periods() {
let fbg = smf28();
let period = fbg.grating_period_nm();
let expected = (10e6_f64 / period).round() as usize;
let got = fbg.n_periods();
assert!(
(got as i64 - expected as i64).abs() <= 1,
"Expected ~{expected} periods, got {got}"
);
}
#[test]
fn test_strain_roundtrip() {
let fbg = smf28();
let eps_original = 200.0_f64; let shift = fbg.strain_shift_nm(eps_original);
let eps_recovered = fbg.strain_from_shift_microstrain(shift);
assert_relative_eq!(eps_recovered, eps_original, max_relative = 1e-9);
}
#[test]
fn test_temperature_roundtrip() {
let fbg = smf28();
let dt_original = 25.0_f64; let shift = fbg.temperature_shift_nm(dt_original);
let dt_recovered = fbg.temperature_from_shift_c(shift);
assert_relative_eq!(dt_recovered, dt_original, max_relative = 1e-9);
}
#[test]
fn test_apodized_fbg_peak_below_uniform() {
let fbg = smf28();
let uniform_peak = fbg.peak_reflectivity();
let apodized = fbg.clone().with_gaussian_apodization(0.3);
let apodized_peak = apodized.peak_reflectivity();
assert!(
apodized_peak > 0.0 && apodized_peak <= 1.0,
"Apodized peak {apodized_peak} out of [0,1]"
);
let _ = uniform_peak;
}
#[test]
fn test_apodized_fbg_spectrum_nonempty() {
let fbg = smf28();
let apodized = fbg.with_gaussian_apodization(0.3);
let spec = apodized.reflection_spectrum(1548.0, 1552.0, 200);
assert_eq!(spec.len(), 200);
assert!(spec.iter().all(|&(_, r)| (0.0..=1.0).contains(&r)));
}
#[test]
fn test_group_delay_at_bragg_finite() {
let fbg = smf28();
let tau = fbg.group_delay_ps(fbg.center_wavelength_nm);
assert!(tau.is_finite(), "Group delay should be finite, got {tau}");
assert!(tau.abs() < 1e6, "Group delay suspiciously large: {tau} ps");
}
#[test]
fn test_transmission_plus_reflection_equals_unity() {
let fbg = smf28();
let lambda_b = fbg.center_wavelength_nm;
let r_spec = fbg.reflection_spectrum(lambda_b - 0.5, lambda_b + 0.5, 51);
let t_spec = fbg.transmission_spectrum(lambda_b - 0.5, lambda_b + 0.5, 51);
for (&(_, r), &(_, t)) in r_spec.iter().zip(t_spec.iter()) {
assert_relative_eq!(r + t, 1.0, max_relative = 1e-10);
}
}
}