use crate::error::OxiPhotonError;
use std::f64::consts::PI;
const C0: f64 = 2.99792458e8;
#[derive(Debug, Clone, Copy)]
pub struct PhotonicCrystalFiber {
pub n_silica: f64,
pub n_hole: f64,
pub pitch: f64,
pub hole_diameter: f64,
pub core_radius: f64,
}
impl PhotonicCrystalFiber {
pub fn new(n_silica: f64, n_hole: f64, pitch: f64, hole_diameter: f64) -> Self {
Self {
n_silica,
n_hole,
pitch,
hole_diameter,
core_radius: pitch, }
}
pub fn esm_1550() -> Self {
Self::new(1.444, 1.0, 5e-6, 1.5e-6)
}
pub fn highly_nonlinear() -> Self {
Self::new(1.444, 1.0, 1.5e-6, 1.2e-6)
}
pub fn large_mode_area() -> Self {
Self::new(1.444, 1.0, 15e-6, 3.0e-6)
}
pub fn air_fill_fraction(&self) -> f64 {
let ratio = self.hole_diameter / self.pitch;
PI / (2.0 * 3.0_f64.sqrt()) * ratio * ratio
}
pub fn effective_cladding_index(&self) -> f64 {
let f = self.air_fill_fraction();
(f * self.n_hole * self.n_hole + (1.0 - f) * self.n_silica * self.n_silica).sqrt()
}
pub fn numerical_aperture(&self) -> f64 {
let n_clad = self.effective_cladding_index();
if self.n_silica <= n_clad {
return 0.0;
}
(self.n_silica * self.n_silica - n_clad * n_clad).sqrt()
}
pub fn v_number(&self, wavelength: f64) -> f64 {
2.0 * PI * self.core_radius * self.numerical_aperture() / wavelength
}
pub fn d_over_lambda(&self) -> f64 {
self.hole_diameter / self.pitch
}
pub fn is_endlessly_single_mode(&self) -> bool {
self.d_over_lambda() < 0.45
}
pub fn mode_field_diameter(&self, wavelength: f64) -> f64 {
let v = self.v_number(wavelength).max(0.5);
2.0 * self.pitch * (0.65 + 1.619 / v.powf(1.5) + 2.879 / v.powi(6))
}
pub fn effective_mode_area(&self, wavelength: f64) -> f64 {
let mfd = self.mode_field_diameter(wavelength);
PI * (mfd / 2.0).powi(2)
}
pub fn nonlinear_coefficient(&self, wavelength: f64) -> f64 {
use crate::units::conversion::SPEED_OF_LIGHT;
let n2_silica = 2.6e-20; let omega = 2.0 * PI * SPEED_OF_LIGHT / wavelength;
let a_eff = self.effective_mode_area(wavelength);
n2_silica * omega / (SPEED_OF_LIGHT * a_eff)
}
pub fn zero_dispersion_wavelength_nm(&self) -> f64 {
let core_um = self.core_radius * 1e6;
(1270.0 - 90.0 * (2.5 - core_um)).clamp(600.0, 1550.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoreDefect {
SolidCore,
HollowCore,
LargeMode,
}
#[derive(Debug, Clone)]
pub struct PcfGeometry {
pub pitch_um: f64,
pub hole_diameter_um: f64,
pub n_rings: u32,
pub core_defect: CoreDefect,
pub background_index: f64,
}
impl PcfGeometry {
pub fn new(
pitch_um: f64,
hole_diameter_um: f64,
n_rings: u32,
core_defect: CoreDefect,
background_index: f64,
) -> Result<Self, OxiPhotonError> {
if pitch_um <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"pitch_um must be positive, got {pitch_um}"
)));
}
if hole_diameter_um <= 0.0 || hole_diameter_um >= pitch_um {
return Err(OxiPhotonError::NumericalError(format!(
"hole_diameter_um must be in (0, pitch_um), got {hole_diameter_um} vs pitch {pitch_um}"
)));
}
if n_rings == 0 {
return Err(OxiPhotonError::NumericalError(
"n_rings must be at least 1".to_string(),
));
}
if !(1.0..=4.0).contains(&background_index) {
return Err(OxiPhotonError::NumericalError(format!(
"background_index must be in [1, 4], got {background_index}"
)));
}
Ok(Self {
pitch_um,
hole_diameter_um,
n_rings,
core_defect,
background_index,
})
}
pub fn fill_fraction(&self) -> f64 {
let ratio = self.hole_diameter_um / self.pitch_um;
PI * ratio * ratio / (2.0 * 3.0_f64.sqrt())
}
pub fn d_over_lambda(&self) -> f64 {
self.hole_diameter_um / self.pitch_um
}
pub fn cladding_area_um2(&self) -> f64 {
let n = self.n_rings as f64;
let r_outer = (n + 0.5) * self.pitch_um;
let r_inner = self.pitch_um;
PI * (r_outer * r_outer - r_inner * r_inner)
}
pub fn core_area_um2(&self) -> f64 {
let unit_cell = 3.0_f64.sqrt() / 2.0 * self.pitch_um * self.pitch_um;
match self.core_defect {
CoreDefect::SolidCore => unit_cell,
CoreDefect::HollowCore => unit_cell,
CoreDefect::LargeMode => 7.0 * unit_cell,
}
}
pub fn approximate_mfd_um(&self) -> f64 {
let d_lam = self.d_over_lambda();
let scale = 1.0 - 0.6 * d_lam + 0.2 * d_lam * d_lam;
2.0 * self.pitch_um * scale.max(0.3)
}
pub fn n_holes(&self) -> usize {
let n = self.n_rings as usize;
3 * n * (n + 1)
}
pub fn hole_positions(&self) -> Vec<(f64, f64)> {
let lambda = self.pitch_um;
let mut positions = Vec::with_capacity(self.n_holes());
let n = self.n_rings as i32;
for q in -n..=n {
for r in (-n).max(-q - n)..=(n).min(-q + n) {
if q == 0 && r == 0 {
continue;
}
let x = lambda * (q as f64 + 0.5 * r as f64);
let y = lambda * (3.0_f64.sqrt() / 2.0 * r as f64);
positions.push((x, y));
}
}
positions
}
}
#[derive(Debug, Clone)]
pub struct PcfMode {
pub geometry: PcfGeometry,
pub wavelength_nm: f64,
}
impl PcfMode {
pub fn new(geometry: PcfGeometry, wavelength_nm: f64) -> Self {
Self {
geometry,
wavelength_nm,
}
}
fn wavelength_m(&self) -> f64 {
self.wavelength_nm * 1e-9
}
fn n_core(&self) -> f64 {
self.geometry.background_index
}
pub fn effective_cladding_index(&self) -> f64 {
let d_lam = self.geometry.d_over_lambda();
let n_bg = self.n_core();
let v_pcf = self.normalized_frequency();
let a = 0.29_f64;
let b = -0.97_f64;
let f_v = if v_pcf > 1e-6 {
(a * v_pcf.powf(b)).tanh().clamp(0.0, 1.0)
} else {
0.0
};
let correction = (1.5 * d_lam) * (1.5 * d_lam) * f_v;
let n_clad_sq = n_bg * n_bg * (1.0 - correction);
n_clad_sq.max(1.0).sqrt()
}
pub fn v_number(&self) -> f64 {
let lambda_m = self.wavelength_m();
let pitch_m = self.geometry.pitch_um * 1e-6;
let n_c = self.n_core();
let n_cl = self.effective_cladding_index();
if n_c <= n_cl {
return 0.0;
}
2.0 * PI / lambda_m * pitch_m * (n_c * n_c - n_cl * n_cl).sqrt()
}
pub fn normalized_frequency(&self) -> f64 {
let lambda_m = self.wavelength_m();
let pitch_m = self.geometry.pitch_um * 1e-6;
let n_bg = self.n_core();
let d_lam = self.geometry.d_over_lambda();
let dn = (n_bg * n_bg - 1.0_f64).max(0.0).sqrt();
2.0 * PI * pitch_m / lambda_m * dn * d_lam
}
pub fn effective_index(&self) -> f64 {
let n_c = self.n_core();
let n_cl = self.effective_cladding_index();
let v = self.v_number();
let w = 1.0 - (-v * v / 2.0).exp();
n_cl + (n_c - n_cl) * w
}
pub fn is_endlessly_single_mode(&self) -> bool {
self.geometry.d_over_lambda() < 0.45
}
pub fn single_mode_cutoff_d_over_lambda() -> f64 {
0.45
}
pub fn group_index(&self) -> f64 {
let delta_nm = 0.1; let lambda = self.wavelength_nm;
let n_eff_c = self.effective_index();
let mode_p = PcfMode::new(self.geometry.clone(), lambda + delta_nm);
let mode_m = PcfMode::new(self.geometry.clone(), lambda - delta_nm);
let dn_dlam = (mode_p.effective_index() - mode_m.effective_index()) / (2.0 * delta_nm);
n_eff_c - lambda * dn_dlam
}
pub fn chromatic_dispersion_ps_per_nm_km(&self) -> f64 {
let delta_nm = 1.0; let lambda = self.wavelength_nm;
let mode_p = PcfMode::new(self.geometry.clone(), lambda + delta_nm);
let mode_m = PcfMode::new(self.geometry.clone(), lambda - delta_nm);
let n_eff_c = self.effective_index();
let n_eff_p = mode_p.effective_index();
let n_eff_m = mode_m.effective_index();
let d2n_dlam2 = (n_eff_p - 2.0 * n_eff_c + n_eff_m) / (delta_nm * delta_nm);
let c_nm_per_s = C0 * 1e9; -(lambda / c_nm_per_s) * d2n_dlam2 * 1e15
}
pub fn effective_area_um2(&self) -> f64 {
let pitch_um = self.geometry.pitch_um;
let v = self.v_number().max(0.5);
let w_um = pitch_um * (0.65 + 1.619 / v.powf(1.5) + 2.879 / v.powi(6));
PI * w_um * w_um
}
pub fn nonlinear_coefficient_per_w_km(&self, n2_m2_per_w: f64) -> f64 {
let lambda_m = self.wavelength_m();
let a_eff_m2 = self.effective_area_um2() * 1e-12; let gamma_per_w_m = 2.0 * PI * n2_m2_per_w / (lambda_m * a_eff_m2);
gamma_per_w_m * 1e3 }
pub fn numerical_aperture(&self) -> f64 {
let n_c = self.n_core();
let n_cl = self.effective_cladding_index();
if n_c <= n_cl {
return 0.0;
}
(n_c * n_c - n_cl * n_cl).sqrt()
}
pub fn acceptance_angle_rad(&self) -> f64 {
self.numerical_aperture().min(1.0).asin()
}
pub fn confinement_loss_db_per_m(&self) -> f64 {
let n_rings = self.geometry.n_rings as f64;
let d_lam = self.geometry.d_over_lambda();
let v = self.v_number().max(0.5);
let alpha0 = 10.0_f64; let beta = 3.0_f64;
let v_factor = (-0.3 * (v - 1.0).max(0.0)).exp(); alpha0 * (-beta * n_rings * d_lam).exp() * v_factor
}
pub fn bend_loss_db_per_m(&self, bend_radius_mm: f64) -> f64 {
if bend_radius_mm <= 0.0 {
return f64::INFINITY;
}
let na = self.numerical_aperture();
let lambda_um = self.wavelength_nm * 1e-3; let pitch_um = self.geometry.pitch_um;
let d_coeff = PI * na * na * pitch_um / lambda_um;
4.343 / bend_radius_mm * (-d_coeff * bend_radius_mm).exp()
}
}
#[derive(Debug, Clone)]
pub struct BirefringentPcf {
pub base_geometry: PcfGeometry,
pub hole_aspect_ratio: f64,
pub stress_rod_dn: f64,
}
impl BirefringentPcf {
pub fn new(
base_geometry: PcfGeometry,
hole_aspect_ratio: f64,
stress_rod_dn: f64,
) -> Result<Self, OxiPhotonError> {
if hole_aspect_ratio <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"hole_aspect_ratio must be positive, got {hole_aspect_ratio}"
)));
}
if stress_rod_dn < 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"stress_rod_dn must be non-negative, got {stress_rod_dn}"
)));
}
Ok(Self {
base_geometry,
hole_aspect_ratio,
stress_rod_dn,
})
}
pub fn birefringence(&self, lambda_nm: f64) -> f64 {
let d_lam = self.base_geometry.d_over_lambda();
let n_bg = self.base_geometry.background_index;
let mode = PcfMode::new(self.base_geometry.clone(), lambda_nm);
let na = mode.numerical_aperture();
let c_form = 0.15_f64;
let b_form = c_form
* (self.hole_aspect_ratio - 1.0).abs()
* d_lam
* d_lam
* n_bg
* (na / n_bg).powi(2);
let c_stress = 0.5_f64;
let b_stress = c_stress * self.stress_rod_dn;
b_form + b_stress
}
pub fn beat_length_mm(&self, lambda_nm: f64) -> f64 {
let b = self.birefringence(lambda_nm);
if b < 1e-15 {
return f64::INFINITY;
}
let lambda_mm = lambda_nm * 1e-6; lambda_mm / b
}
pub fn is_polarization_maintaining(&self) -> bool {
self.birefringence(1550.0) > 5e-4
}
pub fn polarization_crosstalk_db(&self, length_m: f64, lambda_nm: f64) -> f64 {
let h = self.h_parameter(lambda_nm);
if h <= 0.0 || length_m <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * (h * length_m).log10()
}
pub fn h_parameter(&self, lambda_nm: f64) -> f64 {
let l_b_m = self.beat_length_mm(lambda_nm) * 1e-3; if l_b_m.is_infinite() || l_b_m <= 0.0 {
return 0.0;
}
let sigma = 1e-4_f64; let lambda_m = lambda_nm * 1e-9;
sigma * (lambda_m / l_b_m).powi(2)
}
}
#[derive(Debug, Clone)]
pub struct HollowCorePcf {
pub pitch_um: f64,
pub core_radius_um: f64,
pub cladding_hole_diameter_um: f64,
pub silica_index: f64,
pub n_rings: u32,
}
impl HollowCorePcf {
pub fn new(
pitch_um: f64,
core_radius_um: f64,
cladding_hole_diameter_um: f64,
silica_index: f64,
n_rings: u32,
) -> Result<Self, OxiPhotonError> {
if pitch_um <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"pitch_um must be positive, got {pitch_um}"
)));
}
if core_radius_um <= 0.0 || core_radius_um > 5.0 * pitch_um {
return Err(OxiPhotonError::NumericalError(format!(
"core_radius_um must be in (0, 5*pitch), got {core_radius_um}"
)));
}
if cladding_hole_diameter_um <= 0.0 || cladding_hole_diameter_um >= pitch_um {
return Err(OxiPhotonError::NumericalError(format!(
"cladding_hole_diameter_um must be in (0, pitch), got {cladding_hole_diameter_um}"
)));
}
if silica_index < 1.0 {
return Err(OxiPhotonError::NumericalError(format!(
"silica_index must be ≥ 1.0, got {silica_index}"
)));
}
if n_rings == 0 {
return Err(OxiPhotonError::NumericalError(
"n_rings must be ≥ 1".to_string(),
));
}
Ok(Self {
pitch_um,
core_radius_um,
cladding_hole_diameter_um,
silica_index,
n_rings,
})
}
fn web_thickness_um(&self) -> f64 {
self.pitch_um - self.cladding_hole_diameter_um
}
pub fn bandgap_center_wavelength_nm(&self) -> f64 {
let t_um = self.web_thickness_um();
let dn = (self.silica_index * self.silica_index - 1.0_f64).sqrt();
2.0 * t_um * dn * 1e3
}
pub fn bandgap_width_nm(&self) -> f64 {
let d_lam = self.cladding_hole_diameter_um / self.pitch_um;
let width_fraction = (0.12 + 0.08 * (d_lam - 0.8)).clamp(0.10, 0.25);
self.bandgap_center_wavelength_nm() * width_fraction
}
pub fn transmission_window(&self) -> (f64, f64) {
let center = self.bandgap_center_wavelength_nm();
let half_width = self.bandgap_width_nm() / 2.0;
(center - half_width, center + half_width)
}
pub fn air_fill_fraction(&self) -> f64 {
let r_c = self.core_radius_um;
let pitch = self.pitch_um;
let core_area = PI * r_c * r_c;
let cell_area = 7.0 * (3.0_f64.sqrt() / 2.0 * pitch * pitch);
(core_area / cell_area).min(0.9999)
}
pub fn group_velocity_fraction(&self) -> f64 {
let eta_silica = 1.0 - self.air_fill_fraction(); let n_sil = self.silica_index;
let delta = eta_silica * (n_sil * n_sil - 1.0) / 2.0;
(1.0 - delta).max(0.99) }
pub fn latency_advantage_ns_per_km(&self) -> f64 {
let n_g_smf = 1.4677_f64; let v_g_frac = self.group_velocity_fraction(); let delay_smf_ns_per_km = n_g_smf / C0 * 1e3 * 1e9;
let delay_hcpcf_ns_per_km = 1.0 / (v_g_frac * C0) * 1e3 * 1e9;
(delay_smf_ns_per_km - delay_hcpcf_ns_per_km).max(0.0)
}
pub fn nonlinear_coefficient_per_w_km(&self) -> f64 {
let n2_silica = 2.6e-20_f64; let n2_air = 3e-23_f64; let eta_sil = 1.0 - self.air_fill_fraction();
let eta_air = self.air_fill_fraction();
let n2_eff = eta_sil * n2_silica + eta_air * n2_air;
let a_eff_m2 = PI * (self.core_radius_um * 1e-6).powi(2);
let lambda_m = self.bandgap_center_wavelength_nm() * 1e-9;
let omega = 2.0 * PI * C0 / lambda_m;
let gamma_per_w_m = n2_eff * omega / (C0 * a_eff_m2);
gamma_per_w_m * 1e3 }
}
#[derive(Debug, Clone)]
pub struct PcfOptimizer {
pub target_mfd_um: Option<f64>,
pub target_dispersion_ps_per_nm_km: Option<f64>,
pub target_wavelength_nm: f64,
pub max_d_over_lambda: f64,
pub background_index: f64,
}
impl PcfOptimizer {
pub fn new(target_wavelength_nm: f64, background_index: f64) -> Self {
Self {
target_mfd_um: None,
target_dispersion_ps_per_nm_km: None,
target_wavelength_nm,
max_d_over_lambda: 1.0,
background_index,
}
}
pub fn with_target_mfd(mut self, mfd_um: f64) -> Self {
self.target_mfd_um = Some(mfd_um);
self
}
pub fn with_target_dispersion(mut self, d_ps_per_nm_km: f64) -> Self {
self.target_dispersion_ps_per_nm_km = Some(d_ps_per_nm_km);
self
}
pub fn with_single_mode_constraint(mut self) -> Self {
self.max_d_over_lambda = 0.45;
self
}
pub fn figure_of_merit(&self, geom: &PcfGeometry) -> f64 {
let mode = PcfMode::new(geom.clone(), self.target_wavelength_nm);
let mut fom = 0.0_f64;
let mut n_terms = 0_u32;
if let Some(target_mfd) = self.target_mfd_um {
let mfd = mode.effective_area_um2().sqrt() * (PI / 4.0).sqrt() * 2.0;
let rel_err = (mfd - target_mfd) / target_mfd;
fom += rel_err * rel_err;
n_terms += 1;
}
if let Some(target_disp) = self.target_dispersion_ps_per_nm_km {
let d = mode.chromatic_dispersion_ps_per_nm_km();
let ref_disp = 20.0_f64;
let abs_err = (d - target_disp) / ref_disp;
fom += abs_err * abs_err;
n_terms += 1;
}
if n_terms == 0 {
return 0.0;
}
fom / n_terms as f64
}
pub fn optimize(&self) -> Result<PcfGeometry, OxiPhotonError> {
if self.target_mfd_um.is_none() && self.target_dispersion_ps_per_nm_km.is_none() {
return Err(OxiPhotonError::NumericalError(
"At least one optimization target must be set".to_string(),
));
}
let n_pitch = 40_usize;
let n_d = 40_usize;
let pitch_min = 1.0_f64;
let pitch_max = 20.0_f64;
let d_min = 0.05_f64;
let d_max = self.max_d_over_lambda;
let mut best_fom = f64::INFINITY;
let mut best_geom: Option<PcfGeometry> = None;
for i_pitch in 0..n_pitch {
let pitch = pitch_min + (pitch_max - pitch_min) * i_pitch as f64 / (n_pitch - 1) as f64;
for i_d in 0..n_d {
let d_lam = d_min + (d_max - d_min) * i_d as f64 / (n_d - 1) as f64;
let hole_d = pitch * d_lam;
let geom = match PcfGeometry::new(
pitch,
hole_d,
3,
CoreDefect::SolidCore,
self.background_index,
) {
Ok(g) => g,
Err(_) => continue,
};
let fom = self.figure_of_merit(&geom);
if fom < best_fom {
best_fom = fom;
best_geom = Some(geom);
}
}
}
best_geom.ok_or_else(|| {
OxiPhotonError::NumericalError("Optimizer found no valid geometry".to_string())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pcf_esm_is_single_mode() {
let pcf = PhotonicCrystalFiber::esm_1550();
assert!(
pcf.is_endlessly_single_mode(),
"d/Λ={:.2}",
pcf.d_over_lambda()
);
}
#[test]
fn pcf_hn_not_single_mode() {
let pcf = PhotonicCrystalFiber::highly_nonlinear();
assert!(!pcf.is_endlessly_single_mode());
}
#[test]
fn pcf_air_fill_fraction_range() {
let pcf = PhotonicCrystalFiber::esm_1550();
let f = pcf.air_fill_fraction();
assert!(f > 0.0 && f < 1.0, "f={f:.3}");
}
#[test]
fn pcf_effective_cladding_below_silica() {
let pcf = PhotonicCrystalFiber::esm_1550();
assert!(pcf.effective_cladding_index() < pcf.n_silica);
}
#[test]
fn pcf_na_positive() {
let pcf = PhotonicCrystalFiber::esm_1550();
assert!(pcf.numerical_aperture() > 0.0);
}
#[test]
fn pcf_mfd_reasonable() {
let pcf = PhotonicCrystalFiber::esm_1550();
let mfd = pcf.mode_field_diameter(1550e-9) * 1e6;
assert!(mfd > 1.0 && mfd < 30.0, "MFD={mfd:.1}µm");
}
#[test]
fn pcf_hn_large_gamma() {
let pcf_hn = PhotonicCrystalFiber::highly_nonlinear();
let pcf_lma = PhotonicCrystalFiber::large_mode_area();
let g_hn = pcf_hn.nonlinear_coefficient(1550e-9);
let g_lma = pcf_lma.nonlinear_coefficient(1550e-9);
assert!(g_hn > g_lma, "HN-PCF should have larger γ");
}
#[test]
fn pcf_a_eff_positive() {
let pcf = PhotonicCrystalFiber::esm_1550();
let a_eff = pcf.effective_mode_area(1550e-9);
assert!(a_eff > 0.0);
}
#[test]
fn test_pcf_fill_fraction() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let f_expected = PI * 1.5 * 1.5 / (2.0 * 3.0_f64.sqrt() * 25.0);
let f_actual = geom.fill_fraction();
assert!(
(f_actual - f_expected).abs() < 1e-10,
"f={f_actual:.6} expected {f_expected:.6}"
);
assert!(
f_actual > 0.0 && f_actual < 1.0,
"fill fraction out of range: {f_actual}"
);
}
#[test]
fn test_pcf_d_over_lambda_constraint() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let d_lam = geom.d_over_lambda();
assert!(d_lam > 0.0 && d_lam < 1.0, "d/Λ={d_lam:.3}");
}
#[test]
fn test_endlessly_single_mode() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
assert!((geom.d_over_lambda() - 0.3).abs() < 1e-10);
let mode = PcfMode::new(geom, 1550.0);
assert!(mode.is_endlessly_single_mode());
}
#[test]
fn test_not_single_mode() {
let geom = PcfGeometry::new(5.0, 3.0, 3, CoreDefect::SolidCore, 1.444).unwrap();
assert!((geom.d_over_lambda() - 0.6).abs() < 1e-10);
let mode = PcfMode::new(geom, 1550.0);
assert!(!mode.is_endlessly_single_mode());
}
#[test]
fn test_v_number_positive() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let mode = PcfMode::new(geom, 1550.0);
let v = mode.v_number();
assert!(v > 0.0, "V-number should be positive, got {v}");
}
#[test]
fn test_effective_index_between_air_and_silica() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let mode = PcfMode::new(geom, 1550.0);
let n_eff = mode.effective_index();
assert!(n_eff > 1.0, "n_eff={n_eff:.5} should be above air index");
assert!(
n_eff < 1.444,
"n_eff={n_eff:.5} should be below silica index"
);
}
#[test]
fn test_nonlinear_coeff() {
let geom = PcfGeometry::new(15.0, 3.0, 3, CoreDefect::LargeMode, 1.444).unwrap();
let mode = PcfMode::new(geom, 1550.0);
let n2_silica = 2.6e-20_f64; let gamma = mode.nonlinear_coefficient_per_w_km(n2_silica);
assert!(gamma > 0.0, "γ must be positive");
assert!(gamma < 10.0, "LMA-PCF γ={gamma:.3} W⁻¹km⁻¹ should be small");
}
#[test]
fn test_confinement_loss_decreases_with_rings() {
let geom3 = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let geom6 = PcfGeometry::new(5.0, 1.5, 6, CoreDefect::SolidCore, 1.444).unwrap();
let mode3 = PcfMode::new(geom3, 1550.0);
let mode6 = PcfMode::new(geom6, 1550.0);
let cl3 = mode3.confinement_loss_db_per_m();
let cl6 = mode6.confinement_loss_db_per_m();
assert!(
cl6 < cl3,
"More rings → lower CL: CL(3)={cl3:.4} CL(6)={cl6:.4}"
);
}
#[test]
fn test_birefringent_beat_length() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let bpcf = BirefringentPcf::new(geom, 2.0, 0.0).unwrap();
let lambda_nm = 1550.0;
let b = bpcf.birefringence(lambda_nm);
let l_b = bpcf.beat_length_mm(lambda_nm);
if b > 1e-15 {
let expected_l_b_mm = lambda_nm * 1e-6 / b;
assert!(
(l_b - expected_l_b_mm).abs() / expected_l_b_mm < 1e-6,
"L_B mismatch: got {l_b:.4} mm, expected {expected_l_b_mm:.4} mm"
);
}
}
#[test]
fn test_birefringent_pm_condition() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let bpcf = BirefringentPcf::new(geom.clone(), 5.0, 1e-3).unwrap();
assert!(
bpcf.is_polarization_maintaining(),
"B={:.2e} should be > 5e-4",
bpcf.birefringence(1550.0)
);
let bpcf_low = BirefringentPcf::new(geom, 1.0001, 0.0).unwrap();
assert!(!bpcf_low.is_polarization_maintaining());
}
#[test]
fn test_hollow_core_bandgap_center() {
let hc = HollowCorePcf::new(3.8, 7.5, 3.3, 1.444, 5).unwrap();
let lc = hc.bandgap_center_wavelength_nm();
assert!(
lc > 200.0 && lc < 5000.0,
"bandgap center λ={lc:.1} nm out of range"
);
}
#[test]
fn test_hollow_core_low_nonlinearity() {
let hc = HollowCorePcf::new(3.8, 7.5, 3.3, 1.444, 5).unwrap();
let gamma = hc.nonlinear_coefficient_per_w_km();
assert!(gamma < 0.1, "HC-PCF γ={gamma:.4} W⁻¹km⁻¹ should be << 1");
assert!(gamma > 0.0, "γ must be positive");
}
#[test]
fn test_hole_positions_count() {
for n_rings in 1..=5_u32 {
let geom = PcfGeometry::new(5.0, 1.5, n_rings, CoreDefect::SolidCore, 1.444).unwrap();
let positions = geom.hole_positions();
let expected = 3 * n_rings as usize * (n_rings as usize + 1);
assert_eq!(
positions.len(),
expected,
"n_rings={n_rings}: got {} holes, expected {}",
positions.len(),
expected
);
}
}
#[test]
fn test_pcf_numerical_aperture_positive() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let mode = PcfMode::new(geom, 1550.0);
let na = mode.numerical_aperture();
assert!(na > 0.0, "NA={na:.4} should be positive");
}
#[test]
fn test_pcf_chromatic_dispersion() {
let geom = PcfGeometry::new(5.0, 1.5, 3, CoreDefect::SolidCore, 1.444).unwrap();
let mode = PcfMode::new(geom, 1550.0);
let d = mode.chromatic_dispersion_ps_per_nm_km();
assert!(
d.is_finite(),
"chromatic dispersion should be finite, got {d}"
);
}
#[test]
fn test_pcf_optimizer_mfd_target() {
let opt = PcfOptimizer::new(1550.0, 1.444)
.with_target_mfd(10.0)
.with_single_mode_constraint();
let result = opt.optimize();
assert!(result.is_ok(), "Optimizer failed: {:?}", result.err());
let geom = result.unwrap();
assert!(
geom.d_over_lambda() <= 0.45 + 1e-9,
"SM constraint violated"
);
}
}