use super::{
context::{
BounceRay, BounceState, ExitEvent, ExitSplitCtx, RayMaterialContext, UniaxialBounceContext,
},
geometry::BounceRefractionGeometry,
tir::tir_phase_delta,
uniaxial_entry::entry_eigenmode_selection,
uniaxial_internal::{
HeroInternalSolve, apply_uniaxial_internal_reflect_channels,
apply_uniaxial_internal_transmit_channels,
},
};
use crate::optics::{
polarization::{MuellerMatrix, StokesVector},
raytracer::{
NUM_CHANNELS,
environment::EnvironmentSource,
sampling::{BIREFRINGENT_SPLIT_STREAM, hash_u32},
uniaxial_fresnel::{self, UniaxialFrame},
},
};
use glam::Vec3;
#[cfg(test)]
mod tir_phase_retardation_tests {
use super::*;
#[test]
fn tir_phase_delta_is_nonzero_past_critical_angle() {
let n1k = 2.42f32;
let cos_i = 60.0f32.to_radians().cos();
let sin_i = 60.0f32.to_radians().sin();
assert!(
n1k * sin_i > 1.0,
"test setup must be past the critical angle"
);
let delta = tir_phase_delta(n1k, cos_i, sin_i);
assert!(
delta.abs() > 1e-3,
"TIR phase retardation should be nonzero past the critical angle (got {delta})"
);
let tir_matrix = MuellerMatrix::tir_retardation(delta);
let linear_45 = StokesVector::new(1.0, 0.0, 1.0, 0.0);
let out = linear_45.apply_matrix(&tir_matrix);
assert!(
out.v.abs() > 1e-3,
"a nonzero TIR phase retardation must convert some linear polarization to circular (V) (got V={})",
out.v
);
}
#[test]
fn tir_phase_delta_is_finite_near_grazing_incidence() {
let n1k = 2.42f32;
let cos_i = 0.001f32;
let sin_i = cos_i.mul_add(-cos_i, 1.0).sqrt();
let delta = tir_phase_delta(n1k, cos_i, sin_i);
assert!(
delta.is_finite(),
"delta must remain finite near grazing incidence (got {delta})"
);
}
}
#[cfg(test)]
mod p2_fresnel_energy_conservation_tests {
use crate::optics::materials::GemMaterial;
#[test]
fn ordinary_and_extraordinary_indices_each_conserve_energy_at_several_angles() {
let material = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let n_o = material.dispersion.evaluate(589.3);
let n_e = n_o + material.birefringence_delta;
assert!(
(n_o - n_e).abs() > 0.01,
"test premise: Zircon must be strongly birefringent"
);
for n2 in [n_o, n_e] {
for angle_deg in [0.0f32, 15.0, 30.0, 45.0, 60.0, 75.0] {
let n1 = 1.0f32; let cos_i = angle_deg.to_radians().cos();
let sin_i = angle_deg.to_radians().sin();
let eta = n1 / n2;
let sin2_t = eta * eta * sin_i * sin_i;
assert!(
sin2_t <= 1.0,
"air->crystal entry (n1 == 1.0 < n2) should never TIR"
);
let cos_t = (1.0 - sin2_t).sqrt();
let r_s =
f32::mul_add(n2, -cos_t, n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let r_p =
f32::mul_add(n1, -cos_t, n2 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
let t_s = (2.0 * n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let t_p = (2.0 * n1 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
let r_unpol = 0.5 * r_p.mul_add(r_p, r_s * r_s);
let factor = (n2 * cos_t) / (n1 * cos_i).max(1e-6);
let t_unpol = 0.5 * f32::mul_add(t_p * t_p, factor, t_s * t_s * factor);
assert!(
(r_unpol + t_unpol - 1.0).abs() < 1e-6,
"R + T should equal 1 at n2={n2}, angle={angle_deg} deg \
(R={r_unpol}, T={t_unpol}, R+T={})",
r_unpol + t_unpol
);
}
}
}
}
#[cfg(test)]
mod p2_uniaxial_internal_wiring_energy_conservation_tests {
use super::*;
use crate::optics::materials::GemMaterial;
#[test]
#[expect(
clippy::too_many_lines,
reason = "the empty-arena/disabled `ExitSplitCtx` this test's \
`apply_uniaxial_internal_transmit_channels` call requires \
(splitting itself is irrelevant to this test's own energy-conservation \
sweep, which only reads the hero channel -- see that setup's own \
comment) adds to an already-long sweep body"
)]
fn uniaxial_internal_bounce_wiring_conserves_energy_at_exit_interface() {
let zircon = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let n_o = zircon.dispersion.evaluate(589.3);
let n_e = zircon.extraordinary_index_at(589.3, n_o);
let lambdas = [589.3f32; NUM_CHANNELS];
let axes = [Vec3::X, Vec3::Y, Vec3::new(0.4, 0.5, 0.767_2).normalize()];
let r_branch = 0.5f32;
let mut worst = 0.0f32;
for &c_axis in &axes {
let ctx = RayMaterialContext {
material: &zircon,
lambdas,
hero_idx: 0,
c_axis,
is_anisotropic: true,
enable_internal_mode_coupling: true,
};
for angle_deg in [5.0f32, 15.0, 25.0, 35.0, 45.0, 55.0, 65.0] {
let theta = angle_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);
if k_hat.cross(c_axis).length_squared() < 1e-4 {
continue;
}
let frame = UniaxialFrame::build(k_hat, normal, c_axis, cos_i, sin_i);
for is_extraordinary in [false, true] {
let n_inc = if is_extraordinary {
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()
} else {
n_o
};
let geo = BounceRefractionGeometry {
cos_i,
sin_i,
n1: n_inc,
n2: 1.0,
n1_ch: [n_inc; NUM_CHANNELS],
n2_ch: [1.0; NUM_CHANNELS],
n_o_ch: [n_o; NUM_CHANNELS],
..BounceRefractionGeometry::default()
};
let sol_hero = uniaxial_fresnel::internal_solve(
n_inc,
n_o,
n_e,
c_axis,
&frame,
!is_extraordinary,
);
let mut stokes_r = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut pdf_r = [1.0f32; NUM_CHANNELS];
let ubctx = UniaxialBounceContext {
ctx: &ctx,
geo: &geo,
frame: &frame,
};
let hero_solve = HeroInternalSolve {
hero: ctx.hero_idx,
is_extraordinary,
sol_hero,
r_branch,
};
let mut state_r = BounceState {
stokes: &mut stokes_r,
path_pdf: &mut pdf_r,
};
apply_uniaxial_internal_reflect_channels(&ubctx, &hero_solve, &mut state_r);
let mut stokes_t = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut pdf_t = [1.0f32; NUM_CHANNELS];
let empty_soa = crate::simd::PlanesSoA32::from_normals_d(std::iter::empty(), 0);
let mut split_radiance_t = [0.0f32; NUM_CHANNELS];
let mut exit_ctx = ExitSplitCtx {
plane_soa: &empty_soa,
environment: EnvironmentSource::Studio {
preset: crate::optics::LightingPreset::RingLights,
exposure: 1.0,
light_yaw: 0.0,
light_pitch: 0.85,
backdrop: 0.0,
},
studio_rig: None,
observer: Vec3::ZERO,
split_radiance: &mut split_radiance_t,
enabled: false,
compat: [u8::MAX; NUM_CHANNELS],
};
let mut state_t = BounceState {
stokes: &mut stokes_t,
path_pdf: &mut pdf_t,
};
let mut exit_event_t = ExitEvent {
exit: &mut exit_ctx,
hit_point: Vec3::ZERO,
};
apply_uniaxial_internal_transmit_channels(
&ubctx,
&hero_solve,
BounceRay { k_hat, normal },
&mut state_t,
&mut exit_event_t,
);
let reflected = stokes_r[0].i * r_branch;
let transmitted = stokes_t[0].i * (1.0 - r_branch);
let err = (reflected + transmitted - 1.0).abs();
worst = worst.max(err);
}
}
}
println!(
"uniaxial_internal_bounce_wiring_conserves_energy_at_exit_interface: worst_err={worst}"
);
assert!(
worst < 5e-6,
"reflected + transmitted power should equal incident power (1.0) through \
the actual CPU wiring, worst_err={worst}"
);
}
}
#[cfg(test)]
mod entry_eigenmode_selection_tests {
use super::*;
#[test]
fn unpolarized_light_returns_none() {
let c_axis = Vec3::Y;
let current_plane_normal = Vec3::X;
let k_hat = Vec3::NEG_Z;
let stokes = StokesVector::unpolarized(1.0);
assert!(
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes).is_none(),
"unpolarized light must not be weighted -- it must fall back to flat 50/50"
);
}
#[test]
fn degenerate_plane_of_incidence_returns_none() {
let c_axis = Vec3::Y;
let current_plane_normal = Vec3::ZERO;
let k_hat = Vec3::NEG_Z;
let stokes = StokesVector::new(1.0, 1.0, 0.0, 0.0);
assert!(entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes).is_none());
}
#[test]
fn fully_polarized_along_ordinary_axis_gives_p_o_one() {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::X;
let stokes = StokesVector::new(1.0, 1.0, 0.0, 0.0); let (p_o, cos_2psi_o, sin_2psi_o) =
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("polarized light with a well-defined frame must select Some");
assert!((p_o - 1.0).abs() < 1e-4, "p_o should be ~1.0, got {p_o}");
assert!((cos_2psi_o - 1.0).abs() < 1e-4, "got {cos_2psi_o}");
assert!(sin_2psi_o.abs() < 1e-4, "got {sin_2psi_o}");
}
#[test]
fn fully_polarized_along_extraordinary_axis_gives_p_o_zero() {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::X;
let stokes = StokesVector::new(1.0, -1.0, 0.0, 0.0); let (p_o, ..) = entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("polarized light with a well-defined frame must select Some");
assert!(p_o.abs() < 1e-4, "p_o should be ~0.0, got {p_o}");
}
}
#[cfg(test)]
mod entry_mode_projection_tests {
use super::*;
fn oblique_frame() -> (Vec3, Vec3, Vec3) {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::new(0.6, 0.8, 0.0);
(c_axis, current_plane_normal, k_hat)
}
#[test]
fn projected_stokes_is_fully_linear_at_several_dop_values() {
let (c_axis, current_plane_normal, k_hat) = oblique_frame();
let psi = 25.0f32.to_radians();
let (cos_2psi, sin_2psi) = ((2.0 * psi).cos(), (2.0 * psi).sin());
for dop in [0.05f32, 0.3, 0.6, 0.9, 1.0] {
let i = 1.0f32;
let q = i * dop * cos_2psi;
let u = i * dop * sin_2psi;
let stokes = StokesVector::new(i, q, u, 0.0);
let (_, cos_2psi_o, sin_2psi_o) =
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("nonzero DoP with a well-defined frame must select Some");
for (cos_x, sin_x) in [(cos_2psi_o, sin_2psi_o), (-cos_2psi_o, -sin_2psi_o)] {
let projected = StokesVector::new(i, i * cos_x, i * sin_x, 0.0);
assert!(
projected.v.abs() < 1e-6,
"projected V must be exactly zero at dop={dop}, got {}",
projected.v
);
let lin_energy = projected.q.mul_add(projected.q, projected.u * projected.u);
assert!(
projected.i.mul_add(-projected.i, lin_energy).abs() < 1e-4,
"projected Stokes vector must be fully linearly polarized (Q^2+U^2 \
== I^2) at dop={dop}: I={}, Q={}, U={}, Q^2+U^2={lin_energy}",
projected.i,
projected.q,
projected.u
);
}
}
}
#[test]
fn mode_selection_draw_is_an_unbiased_estimator_at_several_dop_values() {
const T_O: f32 = 0.8;
const T_E: f32 = 0.35;
const TRIALS: u32 = 20_000;
let (c_axis, current_plane_normal, k_hat) = oblique_frame();
let psi = 25.0f32.to_radians();
let (cos_2psi, sin_2psi) = ((2.0 * psi).cos(), (2.0 * psi).sin());
for dop in [0.05f32, 0.3, 0.6, 0.9, 1.0] {
let i = 1.0f32;
let q = i * dop * cos_2psi;
let u = i * dop * sin_2psi;
let stokes = StokesVector::new(i, q, u, 0.0);
let (p_o, ..) = entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("nonzero DoP with a well-defined frame must select Some");
let target = i * p_o.mul_add(T_O, (1.0 - p_o) * T_E);
let mut sum = 0.0f64;
for bounce in 0..TRIALS {
let mode_split_rand = (hash_u32(hash_u32(bounce ^ BIREFRINGENT_SPLIT_STREAM))
as f32)
/ 4_294_967_295.0;
let use_extraordinary = mode_split_rand < (1.0 - p_o);
let estimate = if use_extraordinary { T_E * i } else { T_O * i };
sum += f64::from(estimate);
}
let mean = (sum / f64::from(TRIALS)) as f32;
assert!(
(mean - target).abs() < 0.01,
"mode-selection draw should be an unbiased estimator of the \
polarization-weighted mixture at dop={dop}: p_o={p_o}, target={target}, \
Monte Carlo mean={mean} over {TRIALS} trials"
);
}
}
}