#[derive(Debug, Clone, PartialEq)]
pub enum CrystalClass {
Uniaxial { negative: bool },
Biaxial,
Isotropic,
}
#[derive(Debug, Clone)]
pub struct SellmeierCoeff {
pub a: f64,
pub b: f64,
pub c: f64,
pub d: f64,
}
impl SellmeierCoeff {
pub fn new(a: f64, b: f64, c: f64, d: f64) -> Self {
Self { a, b, c, d }
}
pub fn n_squared(&self, lambda_um: f64) -> f64 {
let l2 = lambda_um * lambda_um;
self.a + self.b * l2 / (l2 - self.c) - self.d * l2
}
pub fn n(&self, lambda_um: f64) -> f64 {
self.n_squared(lambda_um).max(1.0).sqrt()
}
pub fn dn_dlambda(&self, lambda_um: f64) -> f64 {
let l2 = lambda_um * lambda_um;
let denom = l2 - self.c;
let dn2_dl =
-2.0 * self.b * self.c * lambda_um / (denom * denom) - 2.0 * self.d * lambda_um;
let n_val = self.n(lambda_um);
if n_val < 1e-10 {
0.0
} else {
dn2_dl / (2.0 * n_val)
}
}
}
#[derive(Debug, Clone)]
pub struct NloCrystal {
pub name: String,
pub crystal_class: CrystalClass,
pub d_tensor: [[f64; 6]; 3],
pub sellmeier_o: SellmeierCoeff,
pub sellmeier_e: SellmeierCoeff,
pub transparency_range: (f64, f64),
pub damage_threshold_gw_per_cm2: f64,
pub thermo_optic_dn_dt: f64,
}
impl NloCrystal {
pub fn bbo() -> Self {
Self {
name: "BBO".to_string(),
crystal_class: CrystalClass::Uniaxial { negative: true },
d_tensor: [
[0.0, 0.0, 0.0, 0.0, 0.16, -2.3], [-2.3, 2.3, 0.0, 0.16, 0.0, 0.0], [0.16, 0.16, 0.0, 0.0, 0.0, 0.0], ],
sellmeier_o: SellmeierCoeff::new(2.7359, 0.01878, 0.01822, 0.01354),
sellmeier_e: SellmeierCoeff::new(2.3753, 0.01224, 0.01667, 0.01516),
transparency_range: (189.0, 3500.0),
damage_threshold_gw_per_cm2: 5.0,
thermo_optic_dn_dt: -16.6e-6,
}
}
pub fn ktp() -> Self {
Self {
name: "KTP".to_string(),
crystal_class: CrystalClass::Biaxial,
d_tensor: [
[0.0, 0.0, 0.0, 0.0, 6.1, 0.0], [0.0, 0.0, 0.0, 7.6, 0.0, 0.0], [5.0, 5.0, 13.7, 0.0, 0.0, 0.0], ],
sellmeier_o: SellmeierCoeff::new(3.0065, 0.03901, 0.04251, 0.01327),
sellmeier_e: SellmeierCoeff::new(3.3134, 0.05694, 0.05658, 0.01682),
transparency_range: (350.0, 4500.0),
damage_threshold_gw_per_cm2: 1.5,
thermo_optic_dn_dt: 13.0e-6,
}
}
pub fn linbo3() -> Self {
Self {
name: "LiNbO3".to_string(),
crystal_class: CrystalClass::Uniaxial { negative: true },
d_tensor: [
[0.0, 0.0, 0.0, 0.0, -3.4, -2.5], [-2.5, 2.5, 0.0, -3.4, 0.0, 0.0], [-4.3, -4.3, 27.0, 0.0, 0.0, 0.0], ],
sellmeier_o: SellmeierCoeff::new(4.9048, 0.11775, 0.04908, 0.027169),
sellmeier_e: SellmeierCoeff::new(4.5820, 0.099169, 0.044432, 0.021950),
transparency_range: (400.0, 5000.0),
damage_threshold_gw_per_cm2: 0.5,
thermo_optic_dn_dt: -40.0e-6,
}
}
pub fn kdp() -> Self {
Self {
name: "KDP".to_string(),
crystal_class: CrystalClass::Uniaxial { negative: true },
d_tensor: [
[0.0, 0.0, 0.0, 0.39, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.39, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.39], ],
sellmeier_o: SellmeierCoeff::new(2.259276, 0.01008956, 0.012942625, 0.013),
sellmeier_e: SellmeierCoeff::new(2.132668, 0.008637494, 0.012281043, 0.001),
transparency_range: (200.0, 1700.0),
damage_threshold_gw_per_cm2: 10.0,
thermo_optic_dn_dt: -2.4e-5,
}
}
pub fn ppln(_poling_period_um: f64) -> Self {
let mut crystal = Self::linbo3();
crystal.name = "PPLN".to_string();
crystal
}
pub fn n_ordinary(&self, lambda_nm: f64) -> f64 {
self.sellmeier_o.n(lambda_nm * 1e-3)
}
pub fn n_extraordinary(&self, lambda_nm: f64, theta_rad: f64) -> f64 {
let n_o = self.sellmeier_o.n(lambda_nm * 1e-3);
let n_e = self.sellmeier_e.n(lambda_nm * 1e-3);
let cos_t = theta_rad.cos();
let sin_t = theta_rad.sin();
let inv_n2 = cos_t * cos_t / (n_o * n_o) + sin_t * sin_t / (n_e * n_e);
if inv_n2 < 1e-30 {
n_o
} else {
(1.0 / inv_n2).sqrt()
}
}
pub fn gvd_ps2_per_mm(&self, lambda_nm: f64) -> f64 {
let c_mm_per_ps = 2.99792458e8 * 1e3 * 1e-12; let lambda_mm = lambda_nm * 1e-6;
let dl_mm = 0.001e-6; let lp = lambda_mm + dl_mm;
let lm = lambda_mm - dl_mm;
let n0 = self.sellmeier_o.n(lambda_mm * 1e3);
let np = self.sellmeier_o.n(lp * 1e3);
let nm = self.sellmeier_o.n(lm * 1e3);
let d2n_dl2 = (np - 2.0 * n0 + nm) / (dl_mm * dl_mm);
lambda_mm * lambda_mm * lambda_mm / (2.0 * std::f64::consts::PI * c_mm_per_ps * c_mm_per_ps)
* d2n_dl2
}
pub fn max_d_eff_pm_per_v(&self) -> f64 {
self.d_tensor
.iter()
.flat_map(|row| row.iter())
.copied()
.fold(0.0_f64, |acc, v| acc.max(v.abs()))
}
pub fn is_transparent(&self, lambda_nm: f64) -> bool {
lambda_nm >= self.transparency_range.0 && lambda_nm <= self.transparency_range.1
}
pub fn damage_threshold_w_per_cm2(&self, pulse_width_ns: f64) -> f64 {
let i_ref = self.damage_threshold_gw_per_cm2 * 1e9; i_ref * pulse_width_ns.max(0.0).sqrt()
}
pub fn temperature_tuning_rate_nm_per_c(&self) -> f64 {
let dn_dt = self.thermo_optic_dn_dt.abs();
dn_dt * 5e5 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bbo_n_ordinary() {
let bbo = NloCrystal::bbo();
let n_o = bbo.n_ordinary(532.0);
assert!(
(n_o - 1.67).abs() < 0.03,
"BBO n_o={:.4} at 532 nm, expected ≈ 1.67",
n_o
);
}
#[test]
fn test_bbo_n_extraordinary() {
let bbo = NloCrystal::bbo();
let n_o = bbo.n_ordinary(532.0);
let n_e_max = bbo.n_extraordinary(532.0, std::f64::consts::FRAC_PI_2);
assert!(
n_e_max < n_o,
"BBO should be negative uniaxial: n_e={:.4} < n_o={:.4}",
n_e_max,
n_o
);
}
#[test]
fn test_ktp_transparent_at_1064() {
let ktp = NloCrystal::ktp();
assert!(
ktp.is_transparent(1064.0),
"KTP should be transparent at 1064 nm"
);
}
#[test]
fn test_linbo3_transparent_at_1550() {
let lnb = NloCrystal::linbo3();
assert!(
lnb.is_transparent(1550.0),
"LiNbO3 should be transparent at 1550 nm"
);
}
#[test]
fn test_crystal_damage_threshold_scaling() {
let bbo = NloCrystal::bbo();
let i_1ns = bbo.damage_threshold_w_per_cm2(1.0);
let i_4ns = bbo.damage_threshold_w_per_cm2(4.0);
let ratio = i_4ns / i_1ns;
assert!(
(ratio - 2.0).abs() < 0.01,
"Damage threshold scaling: expected ratio 2.0, got {:.3}",
ratio
);
}
#[test]
fn test_n_extraordinary_at_90deg() {
let bbo = NloCrystal::bbo();
let lambda_nm = 1064.0;
let n_e_at_90 = bbo.n_extraordinary(lambda_nm, std::f64::consts::FRAC_PI_2);
let n_e_principal = bbo.sellmeier_e.n(lambda_nm * 1e-3);
assert!(
(n_e_at_90 - n_e_principal).abs() < 1e-6,
"n_e(90°)={:.6} should equal n_e={:.6}",
n_e_at_90,
n_e_principal
);
}
#[test]
fn test_n_extraordinary_at_0deg() {
let bbo = NloCrystal::bbo();
let lambda_nm = 1064.0;
let n_e_at_0 = bbo.n_extraordinary(lambda_nm, 0.0);
let n_o = bbo.n_ordinary(lambda_nm);
assert!(
(n_e_at_0 - n_o).abs() < 1e-6,
"n_e(0°)={:.6} should equal n_o={:.6}",
n_e_at_0,
n_o
);
}
}