use glam::{Mat4, Vec3};
use crate::optics::polarization::StokesVector;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Cplx {
pub re: f32,
pub im: f32,
}
impl Cplx {
pub(crate) const ZERO: Self = Self { re: 0.0, im: 0.0 };
#[inline]
pub(crate) const fn re(re: f32) -> Self {
Self { re, im: 0.0 }
}
#[inline]
pub(crate) const fn add(self, o: Self) -> Self {
Self {
re: self.re + o.re,
im: self.im + o.im,
}
}
#[inline]
pub(crate) const fn sub(self, o: Self) -> Self {
Self {
re: self.re - o.re,
im: self.im - o.im,
}
}
#[inline]
pub(crate) const fn mul(self, o: Self) -> Self {
Self {
re: self.re * o.re - self.im * o.im,
im: self.re * o.im + self.im * o.re,
}
}
#[inline]
pub(crate) const fn scale(self, s: f32) -> Self {
Self {
re: self.re * s,
im: self.im * s,
}
}
#[inline]
pub(crate) const fn conj(self) -> Self {
Self {
re: self.re,
im: -self.im,
}
}
#[inline]
pub(crate) fn norm_sqr(self) -> f32 {
self.im.mul_add(self.im, self.re * self.re)
}
#[inline]
pub(crate) fn div(self, o: Self) -> Self {
let denom = o.im.mul_add(o.im, o.re * o.re).max(1e-20);
Self {
re: self.im.mul_add(o.im, self.re * o.re) / denom,
im: self.re.mul_add(-o.im, self.im * o.re) / denom,
}
}
#[inline]
pub(crate) fn sqrt_forward_branch(self) -> Self {
if self.im == 0.0 && self.re < 0.0 {
return Self {
re: 0.0,
im: (-self.re).sqrt(),
};
}
let r = self.norm_sqr().sqrt().sqrt();
let theta = self.im.atan2(self.re) * 0.5;
let mut out = Self {
re: r * theta.cos(),
im: r * theta.sin(),
};
if out.re < 0.0 {
out = Self {
re: -out.re,
im: -out.im,
};
}
if out.re.abs() < 1e-9 && out.im < 0.0 {
out = Self {
re: -out.re,
im: -out.im,
};
}
out
}
}
impl std::ops::Add for Cplx {
type Output = Self;
#[inline]
fn add(self, o: Self) -> Self {
Self::add(self, o)
}
}
impl std::ops::Sub for Cplx {
type Output = Self;
#[inline]
fn sub(self, o: Self) -> Self {
Self::sub(self, o)
}
}
impl std::ops::Mul for Cplx {
type Output = Self;
#[inline]
fn mul(self, o: Self) -> Self {
Self::mul(self, o)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct CVec3 {
pub re: Vec3,
pub im: Vec3,
}
impl CVec3 {
#[inline]
pub(crate) const fn from_real(v: Vec3) -> Self {
Self {
re: v,
im: Vec3::ZERO,
}
}
#[inline]
pub(crate) fn wavevector(that: Vec3, zhat: Vec3, tangential: f32, q: Cplx) -> Self {
Self {
re: that * tangential + zhat * q.re,
im: zhat * q.im,
}
}
#[inline]
pub(crate) fn add(self, o: Self) -> Self {
Self {
re: self.re + o.re,
im: self.im + o.im,
}
}
#[inline]
pub(crate) fn scale_real(self, s: f32) -> Self {
Self {
re: self.re * s,
im: self.im * s,
}
}
#[inline]
pub(crate) fn scale_complex(self, s: Cplx) -> Self {
Self {
re: self.re * s.re - self.im * s.im,
im: self.re * s.im + self.im * s.re,
}
}
#[inline]
pub(crate) fn cross_real(self, r: Vec3) -> Self {
Self {
re: self.re.cross(r),
im: self.im.cross(r),
}
}
#[inline]
pub(crate) fn cross(self, o: Self) -> Self {
Self {
re: self.re.cross(o.re) - self.im.cross(o.im),
im: self.re.cross(o.im) + self.im.cross(o.re),
}
}
#[inline]
pub(crate) fn dot_real(self, r: Vec3) -> Cplx {
Cplx {
re: self.re.dot(r),
im: self.im.dot(r),
}
}
#[inline]
pub(crate) fn conj(self) -> Self {
Self {
re: self.re,
im: -self.im,
}
}
}
impl std::ops::Add for CVec3 {
type Output = Self;
#[inline]
fn add(self, o: Self) -> Self {
Self::add(self, o)
}
}
#[derive(Clone, Copy)]
pub(crate) struct UniaxialFrame {
pub that: Vec3,
pub zhat: Vec3,
pub s_axis: Vec3,
#[expect(
dead_code,
reason = "kept for API completeness -- see doc comment above"
)]
pub p_axis: Vec3,
pub cos_i: f32,
pub sin_i: f32,
pub alpha: f32,
pub beta: f32,
pub gamma: f32,
}
impl UniaxialFrame {
#[must_use]
pub(crate) fn build(k_hat: Vec3, normal: Vec3, c_axis: Vec3, cos_i: f32, sin_i: f32) -> Self {
let zhat = -normal;
let that = if sin_i > 1e-5 {
((k_hat + normal * cos_i) / sin_i).normalize_or_zero()
} else {
let fallback = if zhat.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
(fallback - zhat * zhat.dot(fallback)).normalize_or_zero()
};
let s_axis = k_hat.cross(normal).normalize_or_zero();
let s_axis = if s_axis.length_squared() > 1e-8 {
s_axis
} else {
zhat.cross(that).normalize_or_zero()
};
let p_axis = k_hat.cross(s_axis);
Self {
that,
zhat,
s_axis,
p_axis,
cos_i,
sin_i,
alpha: c_axis.dot(that),
beta: c_axis.dot(s_axis),
gamma: c_axis.dot(zhat),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct QRoots {
pub qo_plus: Cplx,
pub qo_minus: Cplx,
pub qe_plus: Cplx,
pub qe_minus: Cplx,
}
#[must_use]
pub(crate) fn uniaxial_q_roots(
n_o: f32,
n_e: f32,
frame: &UniaxialFrame,
tangential: f32,
) -> QRoots {
let n_o2 = n_o * n_o;
let k2 = tangential * tangential;
let qo2 = Cplx::re(n_o2 - k2);
let qo_plus = qo2.sqrt_forward_branch();
let qo_minus = Cplx::ZERO.sub(qo_plus);
if n_o == n_e {
return QRoots {
qo_plus,
qo_minus,
qe_plus: qo_plus,
qe_minus: qo_minus,
};
}
let n_e2 = n_e * n_e;
let deleps = n_e2 - n_o2;
let denom = (frame.gamma * frame.gamma).mul_add(deleps, n_o2);
let d_val = n_o2
* (frame.beta * frame.beta).mul_add(-deleps, n_e2).mul_add(
-k2,
n_e2 * (frame.gamma * frame.gamma).mul_add(deleps, n_o2),
);
let sqrt_d = Cplx::re(d_val).sqrt_forward_branch();
let shift = Cplx::re(frame.alpha * frame.gamma * tangential * deleps);
let denom_c = Cplx::re(denom.max(1e-12));
let qe_plus = sqrt_d.sub(shift).div(denom_c);
let qe_minus = Cplx::ZERO.sub(sqrt_d).sub(shift).div(denom_c);
QRoots {
qo_plus,
qo_minus,
qe_plus,
qe_minus,
}
}
pub(crate) struct ModeFields {
#[expect(
dead_code,
reason = "public field kept for API completeness -- see doc comment above"
)]
pub k: CVec3,
pub e: CVec3,
pub h: CVec3,
}
#[must_use]
pub(crate) fn ordinary_mode_fields(
n_o: f32,
c_axis: Vec3,
that: Vec3,
zhat: Vec3,
tangential: f32,
q: Cplx,
) -> ModeFields {
let k = CVec3::wavevector(that, zhat, tangential, q);
let d_o = k.cross_real(c_axis);
let e = d_o.scale_real(1.0 / (n_o * n_o));
let h = k.cross(e);
ModeFields { k, e, h }
}
#[must_use]
pub(crate) fn extraordinary_mode_fields(
n_o: f32,
n_e: f32,
c_axis: Vec3,
that: Vec3,
zhat: Vec3,
tangential: f32,
q: Cplx,
) -> ModeFields {
let n_o2 = n_o * n_o;
let n_e2 = n_e * n_e;
let deleps = n_e2 - n_o2;
let k = CVec3::wavevector(that, zhat, tangential, q);
let d_o = k.cross_real(c_axis);
let d_e = k.cross(d_o);
let c_dot_de = d_e.dot_real(c_axis);
let e = d_e
.scale_real(1.0 / n_o2)
.add(CVec3::from_real(c_axis).scale_complex(c_dot_de.scale(-(deleps / (n_o2 * n_e2)))));
let h = k.cross(e);
ModeFields { k, e, h }
}
#[must_use]
pub(crate) fn poynting_z(fields: &ModeFields, amp: Cplx, zhat: Vec3) -> f32 {
let e = fields.e.scale_complex(amp);
let h = fields.h.scale_complex(amp);
let hc = h.conj();
let s = e.cross(hc);
0.5 * s.dot_real(zhat).re
}
fn solve4(mut a: [[Cplx; 4]; 4], mut b: [Cplx; 4]) -> [Cplx; 4] {
for col in 0..4 {
let mut piv = col;
let mut piv_mag = a[col][col].norm_sqr();
for (row, row_data) in a.iter().enumerate().skip(col + 1) {
let mag = row_data[col].norm_sqr();
if mag > piv_mag {
piv = row;
piv_mag = mag;
}
}
a.swap(col, piv);
b.swap(col, piv);
let pivot = a[col][col];
for x in &mut a[col][col..] {
*x = x.div(pivot);
}
b[col] = b[col].div(pivot);
for row in 0..4 {
if row == col {
continue;
}
let factor = a[row][col];
if factor.norm_sqr() == 0.0 {
continue;
}
for j in col..4 {
a[row][j] = a[row][j].sub(factor.mul(a[col][j]));
}
b[row] = b[row].sub(factor.mul(b[col]));
}
}
b
}
fn solve4_two_rhs(
mut a: [[Cplx; 4]; 4],
mut b1: [Cplx; 4],
mut b2: [Cplx; 4],
) -> ([Cplx; 4], [Cplx; 4]) {
for col in 0..4 {
let mut piv = col;
let mut piv_mag = a[col][col].norm_sqr();
for (row, row_data) in a.iter().enumerate().skip(col + 1) {
let mag = row_data[col].norm_sqr();
if mag > piv_mag {
piv = row;
piv_mag = mag;
}
}
a.swap(col, piv);
b1.swap(col, piv);
b2.swap(col, piv);
let pivot = a[col][col];
for x in &mut a[col][col..] {
*x = x.div(pivot);
}
b1[col] = b1[col].div(pivot);
b2[col] = b2[col].div(pivot);
for row in 0..4 {
if row == col {
continue;
}
let factor = a[row][col];
if factor.norm_sqr() == 0.0 {
continue;
}
for j in col..4 {
a[row][j] = a[row][j].sub(factor.mul(a[col][j]));
}
b1[row] = b1[row].sub(factor.mul(b1[col]));
b2[row] = b2[row].sub(factor.mul(b2[col]));
}
}
(b1, b2)
}
#[inline]
fn tangential_components(fields: &ModeFields, that: Vec3, s_axis: Vec3) -> [Cplx; 4] {
[
fields.e.dot_real(that),
fields.e.dot_real(s_axis),
fields.h.dot_real(that),
fields.h.dot_real(s_axis),
]
}
pub(crate) struct EntryPolarizationSolution {
pub r_s: Cplx,
pub r_p: Cplx,
pub t_o: Cplx,
pub t_e: Cplx,
pub flux_o: f32,
pub flux_e: f32,
pub o_hat: Vec3,
pub e_hat: Vec3,
}
#[must_use]
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "kept for tests/possible future single-polarization callers -- see doc comment above"
)
)]
pub(crate) fn entry_solve(
n1: f32,
n_o: f32,
n_e: f32,
c_axis: Vec3,
frame: &UniaxialFrame,
incident_s: bool,
) -> EntryPolarizationSolution {
let (s_sol, p_sol) = entry_solve_pair(n1, n_o, n_e, c_axis, frame);
if incident_s { s_sol } else { p_sol }
}
#[derive(Clone, Copy)]
pub(crate) struct EntryIncidenceFrame {
incident_s: [Cplx; 4],
incident_p: [Cplx; 4],
reflected_s: [Cplx; 4],
reflected_p: [Cplx; 4],
}
#[must_use]
pub(crate) fn entry_incidence_frame(n1: f32, frame: &UniaxialFrame) -> EntryIncidenceFrame {
let k_tan = n1 * frame.sin_i;
let k_fwd = CVec3::wavevector(frame.that, frame.zhat, k_tan, Cplx::re(n1 * frame.cos_i));
let k_bwd = CVec3::wavevector(frame.that, frame.zhat, k_tan, Cplx::re(-n1 * frame.cos_i));
let s_axis_c = CVec3::from_real(frame.s_axis);
let es_fwd = s_axis_c;
let hs_fwd = k_fwd.cross(es_fwd);
let ep_bwd = k_bwd.cross(s_axis_c).scale_real(-1.0 / (n1 * n1));
let ep_fwd = k_fwd.cross(s_axis_c).scale_real(-1.0 / (n1 * n1));
let inc_s = ModeFields {
k: k_fwd,
e: es_fwd,
h: hs_fwd,
};
let inc_p = ModeFields {
k: k_fwd,
e: ep_fwd,
h: k_fwd.cross(ep_fwd),
};
let refl_s = ModeFields {
k: k_bwd,
e: s_axis_c,
h: k_bwd.cross(s_axis_c),
};
let refl_p = ModeFields {
k: k_bwd,
e: ep_bwd,
h: k_bwd.cross(ep_bwd),
};
EntryIncidenceFrame {
incident_s: tangential_components(&inc_s, frame.that, frame.s_axis),
incident_p: tangential_components(&inc_p, frame.that, frame.s_axis),
reflected_s: tangential_components(&refl_s, frame.that, frame.s_axis),
reflected_p: tangential_components(&refl_p, frame.that, frame.s_axis),
}
}
#[must_use]
pub(crate) fn entry_solve_pair(
n1: f32,
n_o: f32,
n_e: f32,
c_axis: Vec3,
frame: &UniaxialFrame,
) -> (EntryPolarizationSolution, EntryPolarizationSolution) {
let inc = entry_incidence_frame(n1, frame);
entry_solve_pair_with_incidence(&inc, n1, n_o, n_e, c_axis, frame)
}
#[must_use]
pub(crate) fn entry_solve_pair_with_incidence(
inc: &EntryIncidenceFrame,
n1: f32,
n_o: f32,
n_e: f32,
c_axis: Vec3,
frame: &UniaxialFrame,
) -> (EntryPolarizationSolution, EntryPolarizationSolution) {
let k_tan = n1 * frame.sin_i;
let roots = uniaxial_q_roots(n_o, n_e, frame, k_tan);
let fo = ordinary_mode_fields(n_o, c_axis, frame.that, frame.zhat, k_tan, roots.qo_plus);
let fe = extraordinary_mode_fields(
n_o,
n_e,
c_axis,
frame.that,
frame.zhat,
k_tan,
roots.qe_plus,
);
let eo_c = tangential_components(&fo, frame.that, frame.s_axis);
let ee_c = tangential_components(&fe, frame.that, frame.s_axis);
let mut a = [[Cplx::ZERO; 4]; 4];
let mut b_s = [Cplx::ZERO; 4];
let mut b_p = [Cplx::ZERO; 4];
for i in 0..4 {
a[i] = [
inc.reflected_s[i],
inc.reflected_p[i],
Cplx::ZERO.sub(eo_c[i]),
Cplx::ZERO.sub(ee_c[i]),
];
b_s[i] = Cplx::ZERO.sub(inc.incident_s[i]);
b_p[i] = Cplx::ZERO.sub(inc.incident_p[i]);
}
let (sol_s, sol_p) = solve4_two_rhs(a, b_s, b_p);
let flux_o = poynting_z(&fo, Cplx::re(1.0), frame.zhat).abs();
let flux_e = poynting_z(&fe, Cplx::re(1.0), frame.zhat).abs();
let o_hat = fo.e.re.normalize_or_zero();
let e_hat = fe.e.re.normalize_or_zero();
let s_sol = EntryPolarizationSolution {
r_s: sol_s[0],
r_p: sol_s[1],
t_o: sol_s[2],
t_e: sol_s[3],
flux_o,
flux_e,
o_hat,
e_hat,
};
let p_sol = EntryPolarizationSolution {
r_s: sol_p[0],
r_p: sol_p[1],
t_o: sol_p[2],
t_e: sol_p[3],
flux_o,
flux_e,
o_hat,
e_hat,
};
(s_sol, p_sol)
}
#[derive(Clone, Copy)]
pub(crate) struct InternalPolarizationSolution {
pub r_o: Cplx,
pub r_e: Cplx,
pub t_s: Cplx,
pub t_p: Cplx,
pub flux_ro: f32,
pub flux_re: f32,
pub flux_ts: f32,
pub flux_tp: f32,
pub flux_inc: f32,
#[cfg_attr(
not(feature = "gpu"),
expect(
dead_code,
reason = "validated reflected-mode-axis data, not wired into any bounce \
dispatch -- see doc comment above; read by the gpu-feature-gated \
Tier 2 parity check"
)
)]
pub o_hat: Vec3,
#[cfg_attr(
not(feature = "gpu"),
expect(
dead_code,
reason = "validated reflected-mode-axis data, not wired into any bounce \
dispatch -- see o_hat's doc comment"
)
)]
pub e_hat: Vec3,
}
#[must_use]
pub(crate) fn internal_solve(
n_mode_inc: f32,
n_o: f32,
n_e: f32,
c_axis: Vec3,
frame: &UniaxialFrame,
incident_is_ordinary: bool,
) -> InternalPolarizationSolution {
let k_tan = n_mode_inc * frame.sin_i;
let roots = uniaxial_q_roots(n_o, n_e, frame, k_tan);
let f_inc = if incident_is_ordinary {
ordinary_mode_fields(n_o, c_axis, frame.that, frame.zhat, k_tan, roots.qo_plus)
} else {
extraordinary_mode_fields(
n_o,
n_e,
c_axis,
frame.that,
frame.zhat,
k_tan,
roots.qe_plus,
)
};
let f_o_bwd = ordinary_mode_fields(n_o, c_axis, frame.that, frame.zhat, k_tan, roots.qo_minus);
let f_e_bwd = extraordinary_mode_fields(
n_o,
n_e,
c_axis,
frame.that,
frame.zhat,
k_tan,
roots.qe_minus,
);
let n2 = 1.0f32;
let zeta = k_tan / n2;
let cos_t = if zeta.abs() <= 1.0 {
Cplx::re(zeta.mul_add(-zeta, 1.0).max(0.0).sqrt())
} else {
Cplx {
re: 0.0,
im: zeta.mul_add(zeta, -1.0).sqrt(),
}
};
let k_iso = CVec3::wavevector(frame.that, frame.zhat, k_tan, cos_t.scale(n2));
let s_axis_c = CVec3::from_real(frame.s_axis);
let es_t = s_axis_c;
let hs_t = k_iso.cross(es_t);
let ep_t = k_iso.cross(s_axis_c).scale_real(-1.0 / (n2 * n2));
let hp_t = k_iso.cross(ep_t);
let f_s_t = ModeFields {
k: k_iso,
e: es_t,
h: hs_t,
};
let f_p_t = ModeFields {
k: k_iso,
e: ep_t,
h: hp_t,
};
let inc_c = tangential_components(&f_inc, frame.that, frame.s_axis);
let eo_c = tangential_components(&f_o_bwd, frame.that, frame.s_axis);
let ee_c = tangential_components(&f_e_bwd, frame.that, frame.s_axis);
let es_c = tangential_components(&f_s_t, frame.that, frame.s_axis);
let ep_c = tangential_components(&f_p_t, frame.that, frame.s_axis);
let mut a = [[Cplx::ZERO; 4]; 4];
let mut b = [Cplx::ZERO; 4];
for i in 0..4 {
a[i] = [
eo_c[i],
ee_c[i],
Cplx::ZERO.sub(es_c[i]),
Cplx::ZERO.sub(ep_c[i]),
];
b[i] = Cplx::ZERO.sub(inc_c[i]);
}
let sol = solve4(a, b);
let [r_o, r_e, t_s, t_p] = sol;
let flux_ro = poynting_z(&f_o_bwd, Cplx::re(1.0), frame.zhat).abs();
let flux_re = poynting_z(&f_e_bwd, Cplx::re(1.0), frame.zhat).abs();
let flux_ts = poynting_z(&f_s_t, Cplx::re(1.0), frame.zhat).abs();
let flux_tp = poynting_z(&f_p_t, Cplx::re(1.0), frame.zhat).abs();
let flux_inc = poynting_z(&f_inc, Cplx::re(1.0), frame.zhat).abs();
let o_hat = f_o_bwd.e.re.normalize_or_zero();
let e_hat = f_e_bwd.e.re.normalize_or_zero();
InternalPolarizationSolution {
r_o,
r_e,
t_s,
t_p,
flux_ro,
flux_re,
flux_ts,
flux_tp,
flux_inc,
o_hat,
e_hat,
}
}
#[must_use]
pub(crate) fn azimuth2_in_frame(dir: Vec3, s_axis: Vec3, p_axis: Vec3) -> (f32, f32) {
let s_comp = dir.dot(s_axis);
let p_comp = dir.dot(p_axis);
let norm = p_comp.mul_add(p_comp, s_comp * s_comp).max(1e-12);
let cos_2psi = p_comp.mul_add(-p_comp, s_comp * s_comp) / norm;
let sin_2psi = 2.0 * s_comp * p_comp / norm;
(cos_2psi, sin_2psi)
}
#[must_use]
pub(crate) fn jones_to_mueller(j_ss: Cplx, j_sp: Cplx, j_ps: Cplx, j_pp: Cplx) -> Mat4 {
let m_ss = j_ss.norm_sqr();
let m_sp = j_sp.norm_sqr();
let m_ps = j_ps.norm_sqr();
let m_pp = j_pp.norm_sqr();
let a_ssp = j_ss.mul(j_sp.conj());
let a_pep = j_ps.mul(j_pp.conj());
let a_ssp_plus = a_ssp.add(a_pep);
let a_ssp_minus = a_ssp.sub(a_pep);
let b_sp = j_ss.mul(j_ps.conj());
let b_pp = j_sp.mul(j_pp.conj());
let c_sspp = j_ss.mul(j_pp.conj());
let c_spps = j_sp.mul(j_ps.conj());
let i_i = 0.5 * (m_ss + m_ps + m_sp + m_pp);
let i_q = 0.5 * (m_ss + m_ps - m_sp - m_pp);
let i_u = a_ssp_plus.re;
let i_v = a_ssp_plus.im;
let q_i = 0.5 * (m_ss - m_ps + m_sp - m_pp);
let q_q = f32::midpoint(m_ss - m_ps - m_sp, m_pp);
let q_u = a_ssp_minus.re;
let q_v = a_ssp_minus.im;
let u_i = b_sp.re + b_pp.re;
let u_q = b_sp.re - b_pp.re;
let u_u = c_sspp.re + c_spps.re;
let u_v = c_sspp.im - c_spps.im;
let v_i = -(b_sp.im + b_pp.im);
let v_q = -(b_sp.im) + b_pp.im;
let v_u = -(c_sspp.im + c_spps.im);
let v_v = c_sspp.re - c_spps.re;
Mat4::from_cols_array(&[
i_i, q_i, u_i, v_i, i_q, q_q, u_q, v_q, i_u, q_u, u_u, v_u, i_v, q_v, u_v, v_v, ])
}
#[must_use]
pub(crate) fn mode_power(
t_s: Cplx,
t_p: Cplx,
mode_flux: f32,
inc_flux: f32,
stokes: StokesVector,
) -> f32 {
let m_s = t_s.norm_sqr();
let m_p = t_p.norm_sqr();
let cross = t_s.mul(t_p.conj());
let raw = cross.im.mul_add(
stokes.v,
cross.re.mul_add(
stokes.u,
0.5 * m_s.mul_add(stokes.i + stokes.q, m_p * (stokes.i - stokes.q)),
),
);
(mode_flux / inc_flux.max(1e-12)) * raw
}
#[cfg(test)]
mod tests {
use super::*;
fn frame_for(ang_deg: f32, c_axis: Vec3) -> UniaxialFrame {
let theta = ang_deg.to_radians();
let cos_i = theta.cos();
let sin_i = theta.sin();
let k_hat = Vec3::new(sin_i, 0.0, cos_i);
let normal = Vec3::new(0.0, 0.0, -1.0); UniaxialFrame::build(k_hat, normal, c_axis, cos_i, sin_i)
}
#[test]
fn entry_solve_matches_existing_scalar_fresnel_at_zero_birefringence() {
let n = 1.7f32;
for ang in [0.0f32, 15.0, 30.0, 45.0, 60.0, 75.0] {
let frame = frame_for(ang, Vec3::Y);
for incident_s in [true, false] {
let sol = entry_solve(1.0, n, n, Vec3::X, &frame, incident_s);
let cos_t = (1.0 - frame.sin_i * frame.sin_i / (n * n)).max(0.0).sqrt();
let r_s_expected = n.mul_add(-cos_t, frame.cos_i) / n.mul_add(cos_t, frame.cos_i);
let r_p_expected = n.mul_add(frame.cos_i, -cos_t) / n.mul_add(frame.cos_i, cos_t);
let (got, expected) = if incident_s {
(sol.r_s.re, r_s_expected)
} else {
(sol.r_p.re, r_p_expected)
};
assert!(
(got - expected).abs() < 1e-4,
"ang={ang} incident_s={incident_s}: got {got}, expected {expected}"
);
let (cross, cross_im) = if incident_s {
(sol.r_p, sol.r_s.im)
} else {
(sol.r_s, sol.r_p.im)
};
assert!(
cross.norm_sqr().sqrt() < 1e-4 && cross_im.abs() < 1e-4,
"cross-polarization reflection must vanish at zero birefringence: {cross:?}"
);
}
}
}
#[test]
fn entry_energy_conservation_holds_across_materials_axes_and_angles() {
let materials = [(1.925f32, 1.984f32), (2.65, 2.67), (2.616, 2.903)];
let axes = [
Vec3::X,
Vec3::Y,
Vec3::Z,
Vec3::new(0.4, 0.5, 0.767_2).normalize(),
Vec3::new(-0.3, 0.8, 0.514_8).normalize(),
];
let mut worst = 0.0f32;
for (n_o, n_e) in materials {
for &c_axis in &axes {
for ang in [0.0f32, 15.0, 30.0, 45.0, 60.0, 75.0] {
let frame = frame_for(ang, c_axis);
let k_hat = Vec3::new(frame.sin_i, 0.0, frame.cos_i);
if k_hat.dot(c_axis).abs() > 0.999_999 {
continue; }
for incident_s in [true, false] {
let sol = entry_solve(1.0, n_o, n_e, c_axis, &frame, incident_s);
let roots = uniaxial_q_roots(n_o, n_e, &frame, frame.sin_i);
let fo = ordinary_mode_fields(
n_o,
c_axis,
frame.that,
frame.zhat,
frame.sin_i,
roots.qo_plus,
);
let fe = extraordinary_mode_fields(
n_o,
n_e,
c_axis,
frame.that,
frame.zhat,
frame.sin_i,
roots.qe_plus,
);
let inc_flux = 0.5 * frame.cos_i;
let sz_o = poynting_z(&fo, sol.t_o, frame.zhat);
let sz_e = poynting_z(&fe, sol.t_e, frame.zhat);
let r_total = sol.r_s.norm_sqr() + sol.r_p.norm_sqr();
let t_total = (sz_o + sz_e) / inc_flux;
let err = (r_total + t_total - 1.0).abs();
worst = worst.max(err);
}
}
}
}
assert!(
worst < 1e-4,
"entry R+T should equal 1 (Poynting-projected) to numerical precision, worst_err={worst}"
);
}
#[test]
fn internal_exit_energy_conservation_holds_including_tir() {
let materials = [(1.925f32, 1.984f32), (2.65, 2.67), (2.616, 2.903)];
let axes = [
Vec3::X,
Vec3::Y,
Vec3::Z,
Vec3::new(0.4, 0.5, 0.767_2).normalize(),
];
let mut worst = 0.0f32;
let mut tir_cases = 0u32;
for (n_o, n_e) in materials {
for &c_axis in &axes {
for ang in [0.0f32, 20.0, 35.0, 50.0, 65.0, 80.0] {
let frame = frame_for(ang, c_axis);
let k_hat = Vec3::new(frame.sin_i, 0.0, frame.cos_i);
if k_hat.dot(c_axis).abs() > 0.999_999 {
continue;
}
for incident_is_ordinary in [true, false] {
let n_inc = if incident_is_ordinary {
n_o
} else {
let cos_kc =
frame.gamma.mul_add(frame.cos_i, frame.alpha * frame.sin_i);
let sin2 = cos_kc.mul_add(-cos_kc, 1.0).max(0.0);
1.0 / (cos_kc * cos_kc / (n_o * n_o) + sin2 / (n_e * n_e)).sqrt()
};
let sol =
internal_solve(n_inc, n_o, n_e, c_axis, &frame, incident_is_ordinary);
let big_k = n_inc * frame.sin_i;
if big_k > 1.0 {
tir_cases += 1;
}
let inc_flux = sol.flux_inc;
let sz_ro = sol.r_o.norm_sqr() * sol.flux_ro;
let sz_re = sol.r_e.norm_sqr() * sol.flux_re;
let r_total = (sz_ro + sz_re) / inc_flux;
let t_total = sol
.t_p
.norm_sqr()
.mul_add(sol.flux_tp, sol.t_s.norm_sqr() * sol.flux_ts)
/ inc_flux;
let err = (r_total + t_total - 1.0).abs();
worst = worst.max(err);
}
}
}
}
assert!(
tir_cases > 20,
"test setup should exercise real TIR cases, got {tir_cases}"
);
assert!(
worst < 1e-3,
"internal R+T should equal 1 (Poynting-projected) including TIR, worst_err={worst}"
);
}
#[test]
fn normal_incidence_inplane_axis_gives_pure_ordinary_extraordinary_fresnel() {
let n_o = 1.925f32;
let n_e = 1.984f32;
let frame = frame_for(0.0, Vec3::X); let sol_s = entry_solve(1.0, n_o, n_e, Vec3::X, &frame, true);
let sol_p = entry_solve(1.0, n_o, n_e, Vec3::X, &frame, false);
let r_o_expected = (1.0 - n_o) / (1.0 + n_o);
let r_e_expected = (n_e - 1.0) / (n_e + 1.0);
assert!((sol_s.r_s.re - r_o_expected).abs() < 1e-4);
assert!(sol_s.r_p.norm_sqr().sqrt() < 1e-4);
assert!((sol_p.r_p.re - r_e_expected).abs() < 1e-4);
assert!(sol_p.r_s.norm_sqr().sqrt() < 1e-4);
}
#[test]
fn rutile_fresnel_diverges_from_isotropic_effective_index_approximation_by_about_15_percent() {
use crate::optics::{birefringence::BirefringenceParams, materials::GemMaterial};
let rutile = GemMaterial::by_name("Rutile").expect("Rutile must be a built-in material");
let n_o = rutile.dispersion.evaluate(589.3);
let n_e = rutile.extraordinary_index_at(589.3, n_o);
assert!(
(n_e - n_o - 0.287).abs() < 1e-3,
"test premise: Rutile's birefringence should be +0.287, got n_o={n_o} n_e={n_e}"
);
let c_axis = Vec3::new(0.05, 0.95, 0.3).normalize();
let frame = frame_for(20.0, c_axis);
let k_hat = Vec3::new(frame.sin_i, 0.0, frame.cos_i);
let theta_c = k_hat.dot(c_axis).clamp(-1.0, 1.0).abs().acos();
let n_eff = BirefringenceParams::effective_extraordinary_index(n_o, n_e, theta_c);
let cos_t_old = (frame.sin_i / n_eff)
.mul_add(-(frame.sin_i / n_eff), 1.0)
.max(0.0)
.sqrt();
let r_p_old =
n_eff.mul_add(frame.cos_i, -cos_t_old) / n_eff.mul_add(frame.cos_i, cos_t_old);
let r_pp_old_sq = r_p_old * r_p_old;
let sol_p = entry_solve(1.0, n_o, n_e, c_axis, &frame, false);
let r_pp_new_sq = sol_p.r_p.norm_sqr();
let relative_deviation = (r_pp_new_sq - r_pp_old_sq).abs() / r_pp_old_sq;
assert!(
(0.08..=0.25).contains(&relative_deviation),
"Rutile's new closed-form |r_pp|^2 (={r_pp_new_sq}) should diverge from the \
old effective-index approximation r_p^2 (={r_pp_old_sq}) by roughly the \
~15% the review predicted -- got {:.2}%",
relative_deviation * 100.0
);
}
#[test]
fn jones_to_mueller_matches_existing_diagonal_fresnel_reflection() {
use crate::optics::polarization::MuellerMatrix;
let r_s = 0.42f32;
let r_p = -0.17f32;
let got = jones_to_mueller(Cplx::re(r_s), Cplx::ZERO, Cplx::ZERO, Cplx::re(r_p));
let expected = MuellerMatrix::fresnel_reflection(r_s, r_p);
for i in 0..4 {
for j in 0..4 {
let g = got.col(j)[i];
let e = expected.col(j)[i];
assert!((g - e).abs() < 1e-5, "[{i}][{j}]: got {g}, expected {e}");
}
}
}
fn lcg(state: &mut u64) -> f32 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(((*state >> 33) as f32) / u32::MAX as f32).mul_add(2.0, -1.0)
}
#[test]
fn batched_entry_solve_pair_matches_scalar_bitwise() {
let mut state = 7u64;
for _ in 0..500 {
let ang_deg = 80.0 * lcg(&mut state).abs();
let c_axis = Vec3::new(lcg(&mut state), lcg(&mut state), lcg(&mut state))
.try_normalize()
.unwrap_or(Vec3::Y);
let n_o = 0.6f32.mul_add(lcg(&mut state).abs(), 1.4);
let n_e = 0.3f32.mul_add(lcg(&mut state), n_o);
let frame = frame_for(ang_deg, c_axis);
let (scalar_s, scalar_p) = entry_solve_pair(1.0, n_o, n_e, c_axis, &frame);
let inc = entry_incidence_frame(1.0, &frame);
let (batched_s, batched_p) =
entry_solve_pair_with_incidence(&inc, 1.0, n_o, n_e, c_axis, &frame);
let cplx_bits_eq = |a: Cplx, b: Cplx| {
a.re.to_bits() == b.re.to_bits() && a.im.to_bits() == b.im.to_bits()
};
for (label, s, b) in [
("s_incident", scalar_s, batched_s),
("p_incident", scalar_p, batched_p),
] {
assert!(
cplx_bits_eq(s.r_s, b.r_s)
&& cplx_bits_eq(s.r_p, b.r_p)
&& cplx_bits_eq(s.t_o, b.t_o)
&& cplx_bits_eq(s.t_e, b.t_e)
&& s.flux_o.to_bits() == b.flux_o.to_bits()
&& s.flux_e.to_bits() == b.flux_e.to_bits()
&& s.o_hat == b.o_hat
&& s.e_hat == b.e_hat,
"{label}: batched result must be bit-identical to scalar at \
ang_deg={ang_deg} c_axis={c_axis:?} n_o={n_o} n_e={n_e} \
(scalar r_s={:?}, batched r_s={:?})",
s.r_s,
b.r_s
);
}
}
}
}