use num_complex::Complex64;
#[derive(Debug, Clone, Copy)]
pub struct DielectricTensor {
pub eps_x: Complex64,
pub eps_y: Complex64,
pub eps_z: Complex64,
}
impl DielectricTensor {
pub fn new(eps_x: Complex64, eps_y: Complex64, eps_z: Complex64) -> Self {
Self {
eps_x,
eps_y,
eps_z,
}
}
pub fn isotropic(eps: Complex64) -> Self {
Self {
eps_x: eps,
eps_y: eps,
eps_z: eps,
}
}
pub fn uniaxial(eps_o: Complex64, eps_e: Complex64) -> Self {
Self {
eps_x: eps_o,
eps_y: eps_o,
eps_z: eps_e,
}
}
pub fn refractive_indices(&self) -> (f64, f64, f64) {
(
self.eps_x.re.sqrt(),
self.eps_y.re.sqrt(),
self.eps_z.re.sqrt(),
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct AnisotropicMaterial {
pub n_o: f64,
pub n_e: f64,
pub delta_n: f64,
}
impl AnisotropicMaterial {
pub fn new(n_o: f64, n_e: f64) -> Self {
Self {
n_o,
n_e,
delta_n: n_e - n_o,
}
}
pub fn lithium_niobate() -> Self {
Self::new(2.138, 2.211) }
pub fn calcite() -> Self {
Self::new(1.6584, 1.4864) }
pub fn bbo() -> Self {
Self::new(1.6551, 1.5425)
}
pub fn ktp() -> Self {
Self::new(1.7377, 1.8297)
}
pub fn quartz() -> Self {
Self::new(1.5442, 1.5533)
}
pub fn tensor(&self) -> DielectricTensor {
DielectricTensor::uniaxial(
Complex64::new(self.n_o * self.n_o, 0.0),
Complex64::new(self.n_e * self.n_e, 0.0),
)
}
pub fn walkoff_angle(&self, theta: f64) -> f64 {
let no2 = self.n_o * self.n_o;
let ne2 = self.n_e * self.n_e;
let num = -(ne2 - no2) * theta.sin() * theta.cos();
let den = ne2 * theta.sin() * theta.sin() + no2 * theta.cos() * theta.cos();
(num / den).atan()
}
pub fn n_extraordinary(&self, theta: f64) -> f64 {
let no = self.n_o;
let ne = self.n_e;
let cos2 = theta.cos() * theta.cos();
let sin2 = theta.sin() * theta.sin();
1.0 / (cos2 / (no * no) + sin2 / (ne * ne)).sqrt()
}
pub fn phase_match_angle_shg_type1(
&self,
n_o_fundamental: f64,
n_e_shg: &AnisotropicMaterial,
) -> Option<f64> {
let n_target = n_o_fundamental;
let n_o2 = n_e_shg.n_o;
let n_e2 = n_e_shg.n_e;
let inv_nt2 = 1.0 / (n_target * n_target);
let inv_no2 = 1.0 / (n_o2 * n_o2);
let inv_ne2 = 1.0 / (n_e2 * n_e2);
let sin2_theta = (inv_nt2 - inv_no2) / (inv_ne2 - inv_no2);
if !(0.0..=1.0).contains(&sin2_theta) {
None
} else {
Some(sin2_theta.sqrt().asin())
}
}
pub fn is_positive_uniaxial(&self) -> bool {
self.n_e > self.n_o
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lnbo3_positive_uniaxial() {
let m = AnisotropicMaterial::lithium_niobate();
assert!(m.is_positive_uniaxial());
assert!(m.delta_n > 0.0);
}
#[test]
fn calcite_negative_uniaxial() {
let m = AnisotropicMaterial::calcite();
assert!(!m.is_positive_uniaxial());
assert!(m.delta_n < 0.0);
}
#[test]
fn extraordinary_index_at_zero_angle_is_ordinary() {
let m = AnisotropicMaterial::calcite();
let ne_at_0 = m.n_extraordinary(0.0);
assert!((ne_at_0 - m.n_o).abs() < 1e-8);
}
#[test]
fn extraordinary_index_at_90deg_is_ne() {
let m = AnisotropicMaterial::calcite();
let ne_at_90 = m.n_extraordinary(std::f64::consts::FRAC_PI_2);
assert!((ne_at_90 - m.n_e).abs() < 1e-8);
}
#[test]
fn walkoff_zero_on_axis() {
let m = AnisotropicMaterial::bbo();
let rho = m.walkoff_angle(0.0);
assert!(rho.abs() < 1e-12);
}
#[test]
fn tensor_uniaxial_xy_equal() {
let m = AnisotropicMaterial::calcite();
let t = m.tensor();
assert!((t.eps_x - t.eps_y).norm() < 1e-12);
assert!((t.eps_x - t.eps_z).norm() > 0.01);
}
#[test]
fn phase_match_angle_bbo_shg() {
let bbo_fund = AnisotropicMaterial::new(1.6551, 1.5425); let bbo_shg = AnisotropicMaterial::new(1.6749, 1.5555); let angle = bbo_fund.phase_match_angle_shg_type1(bbo_fund.n_o, &bbo_shg);
assert!(angle.is_some());
let theta = angle.unwrap();
assert!(theta > 0.1 && theta < 1.0, "theta={theta:.3} rad");
}
}