use super::{
complex::Cplx,
entry::entry_solve,
entry_incidence_frame, entry_solve_pair, entry_solve_pair_with_incidence,
frame::{
UniaxialFrame, extraordinary_mode_fields, ordinary_mode_fields, poynting_z,
uniaxial_q_roots,
},
internal::internal_solve,
mueller::jones_to_mueller,
};
use glam::Vec3;
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 expected_r_p = 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, expected_r_p)
};
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_ext = sol.r_e.norm_sqr() * sol.flux_re;
let r_total = (sz_ro + sz_ext) / 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 expected_r_e = (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 - expected_r_e).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 - rutile.birefringence_delta).abs() < 1e-3
&& (rutile.birefringence_delta - 0.2957).abs() < 1e-4,
"test premise: Rutile's birefringence should be +0.2957, 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% expected -- 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
);
}
}
}