use serde::{Deserialize, Serialize};
use tracing::trace;
use super::snell_3d;
use crate::error::{PrakashError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SurfaceShape {
Sphere {
radius: f64,
},
Plane,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct OpticalSurface {
pub shape: SurfaceShape,
pub z_position: f64,
pub n_after: f64,
pub aperture_radius: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TraceRay {
pub position: [f64; 3],
pub direction: [f64; 3],
pub n: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TraceHit {
pub hit_point: [f64; 3],
pub normal: [f64; 3],
pub ray_after: TraceRay,
pub reflectance: f64,
}
#[must_use = "returns the trace hit result"]
#[inline]
pub fn trace_surface(ray: &TraceRay, surface: &OpticalSurface) -> Result<TraceHit> {
trace!(
z = surface.z_position,
n_after = surface.n_after,
"trace_surface"
);
let (hit_point, normal) = match surface.shape {
SurfaceShape::Plane => {
let dz = ray.direction[2];
if dz.abs() < 1e-15 {
return Err(PrakashError::InvalidParameter {
reason: "ray parallel to plane surface".into(),
});
}
let t = (surface.z_position - ray.position[2]) / dz;
if t < 0.0 {
return Err(PrakashError::InvalidParameter {
reason: "surface is behind the ray".into(),
});
}
let hit = [
ray.position[0] + t * ray.direction[0],
ray.position[1] + t * ray.direction[1],
ray.position[2] + t * ray.direction[2],
];
let n = if dz > 0.0 {
[0.0, 0.0, -1.0]
} else {
[0.0, 0.0, 1.0]
};
(hit, n)
}
SurfaceShape::Sphere { radius } => {
let center = [0.0, 0.0, surface.z_position + radius];
let oc = [
ray.position[0] - center[0],
ray.position[1] - center[1],
ray.position[2] - center[2],
];
let half_b =
oc[0] * ray.direction[0] + oc[1] * ray.direction[1] + oc[2] * ray.direction[2];
let c = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - radius * radius;
let discriminant = half_b * half_b - c;
if discriminant < 0.0 {
return Err(PrakashError::InvalidParameter {
reason: "ray misses spherical surface".into(),
});
}
let sqrt_d = discriminant.sqrt();
let t1 = -half_b - sqrt_d;
let t2 = -half_b + sqrt_d;
let t = if t1 > 1e-10 {
t1
} else if t2 > 1e-10 {
t2
} else {
return Err(PrakashError::InvalidParameter {
reason: "surface is behind the ray".into(),
});
};
let hit = [
ray.position[0] + t * ray.direction[0],
ray.position[1] + t * ray.direction[1],
ray.position[2] + t * ray.direction[2],
];
let mut n = [hit[0] - center[0], hit[1] - center[1], hit[2] - center[2]];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
n[0] /= len;
n[1] /= len;
n[2] /= len;
let dot_dn =
ray.direction[0] * n[0] + ray.direction[1] * n[1] + ray.direction[2] * n[2];
if dot_dn > 0.0 {
n[0] = -n[0];
n[1] = -n[1];
n[2] = -n[2];
}
(hit, n)
}
};
let r2 = hit_point[0] * hit_point[0] + hit_point[1] * hit_point[1];
if r2 > surface.aperture_radius * surface.aperture_radius {
return Err(PrakashError::InvalidParameter {
reason: "ray outside aperture".into(),
});
}
let (refracted_dir, reflectance) = snell_3d(ray.direction, normal, ray.n, surface.n_after)?;
Ok(TraceHit {
hit_point,
normal,
ray_after: TraceRay {
position: hit_point,
direction: refracted_dir,
n: surface.n_after,
},
reflectance,
})
}
#[must_use = "returns the trace hits"]
#[inline]
pub fn trace_sequential(
initial_ray: &TraceRay,
surfaces: &[OpticalSurface],
) -> Result<Vec<TraceHit>> {
trace!(num_surfaces = surfaces.len(), "trace_sequential");
let mut hits = Vec::with_capacity(surfaces.len());
let mut current_ray = *initial_ray;
for surface in surfaces {
let hit = trace_surface(¤t_ray, surface)?;
current_ray = hit.ray_after;
hits.push(hit);
}
Ok(hits)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PolarizedTraceHit {
pub hit: TraceHit,
pub rp_over_rs: f64,
pub transmittance_s: f64,
pub transmittance_p: f64,
}
#[must_use = "returns the polarized trace hits"]
pub fn trace_sequential_polarized(
initial_ray: &TraceRay,
surfaces: &[OpticalSurface],
) -> Result<Vec<PolarizedTraceHit>> {
trace!(num_surfaces = surfaces.len(), "trace_sequential_polarized");
let mut results = Vec::with_capacity(surfaces.len());
let mut current_ray = *initial_ray;
let mut cum_ts = 1.0;
let mut cum_tp = 1.0;
for surface in surfaces {
let hit = trace_surface(¤t_ray, surface)?;
let cos_i = {
let d = ¤t_ray.direction;
let n = &hit.normal;
-(d[0] * n[0] + d[1] * n[1] + d[2] * n[2]).abs()
};
let cos_t = {
let d = &hit.ray_after.direction;
let n = &hit.normal;
-(d[0] * n[0] + d[1] * n[1] + d[2] * n[2]).abs()
};
let n1 = current_ray.n;
let n2 = surface.n_after;
let rs = super::fresnel_s(n1, n2, cos_i, cos_t);
let rp = super::fresnel_p(n1, n2, cos_i, cos_t);
let ts = 1.0 - rs;
let tp = 1.0 - rp;
cum_ts *= ts;
cum_tp *= tp;
let rp_over_rs = if rs > 1e-15 { rp / rs } else { 0.0 };
results.push(PolarizedTraceHit {
hit,
rp_over_rs,
transmittance_s: cum_ts,
transmittance_p: cum_tp,
});
current_ray = hit.ray_after;
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
#[test]
fn test_trace_plane_surface() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 10.0,
n_after: 1.5,
aperture_radius: 5.0,
};
let hit = trace_surface(&ray, &surface).unwrap();
assert!((hit.hit_point[2] - 10.0).abs() < EPS);
assert!((hit.ray_after.direction[2] - 1.0).abs() < 0.01); }
#[test]
fn test_trace_sphere_surface() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Sphere { radius: 50.0 },
z_position: 10.0,
n_after: 1.5,
aperture_radius: 25.0,
};
let hit = trace_surface(&ray, &surface).unwrap();
assert!((hit.hit_point[0]).abs() < EPS);
assert!((hit.hit_point[1]).abs() < EPS);
assert!(hit.hit_point[2] > 9.0 && hit.hit_point[2] < 11.0);
}
#[test]
fn test_trace_aperture_blocks_ray() {
let ray = TraceRay {
position: [10.0, 0.0, 0.0], direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 10.0,
n_after: 1.5,
aperture_radius: 5.0,
};
assert!(trace_surface(&ray, &surface).is_err());
}
#[test]
fn test_trace_sequential_biconvex_lens() {
let surfaces = [
OpticalSurface {
shape: SurfaceShape::Sphere { radius: 100.0 },
z_position: 0.0,
n_after: 1.5,
aperture_radius: 25.0,
},
OpticalSurface {
shape: SurfaceShape::Sphere { radius: -100.0 },
z_position: 5.0, n_after: 1.0,
aperture_radius: 25.0,
},
];
let ray = TraceRay {
position: [0.0, 0.0, -50.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let hits = trace_sequential(&ray, &surfaces).unwrap();
assert_eq!(hits.len(), 2);
assert!((hits[1].ray_after.direction[0]).abs() < 0.01);
}
#[test]
fn test_trace_off_axis_converges() {
let surfaces = [
OpticalSurface {
shape: SurfaceShape::Sphere { radius: 50.0 },
z_position: 0.0,
n_after: 1.5,
aperture_radius: 25.0,
},
OpticalSurface {
shape: SurfaceShape::Sphere { radius: -50.0 },
z_position: 5.0,
n_after: 1.0,
aperture_radius: 25.0,
},
];
let ray = TraceRay {
position: [5.0, 0.0, -50.0],
direction: [0.0, 0.0, 1.0], n: 1.0,
};
let hits = trace_sequential(&ray, &surfaces).unwrap();
assert!(
hits[1].ray_after.direction[0] < 0.0,
"Off-axis ray through converging lens should bend toward axis"
);
}
#[test]
fn test_trace_sequential_empty() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let hits = trace_sequential(&ray, &[]).unwrap();
assert!(hits.is_empty());
}
#[test]
fn test_polarized_trace_normal_incidence() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 10.0,
n_after: 1.5,
aperture_radius: 5.0,
};
let results = trace_sequential_polarized(&ray, &[surface]).unwrap();
assert_eq!(results.len(), 1);
assert!(
(results[0].transmittance_s - results[0].transmittance_p).abs() < 0.01,
"s and p should match at normal incidence"
);
assert!(results[0].transmittance_s > 0.9);
}
#[test]
fn test_polarized_trace_oblique() {
let s2 = 1.0 / 2.0f64.sqrt();
let ray = TraceRay {
position: [-5.0, 0.0, 0.0],
direction: [s2, 0.0, s2], n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 5.0,
n_after: 1.5,
aperture_radius: 10.0,
};
let results = trace_sequential_polarized(&ray, &[surface]).unwrap();
assert!(
results[0].transmittance_s < results[0].transmittance_p,
"s should transmit less than p at 45°: Ts={}, Tp={}",
results[0].transmittance_s,
results[0].transmittance_p
);
}
#[test]
fn test_polarized_trace_multi_surface() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surfaces = [
OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 10.0,
n_after: 1.5,
aperture_radius: 5.0,
},
OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 20.0,
n_after: 1.0,
aperture_radius: 5.0,
},
];
let results = trace_sequential_polarized(&ray, &surfaces).unwrap();
assert_eq!(results.len(), 2);
assert!(results[1].transmittance_s < results[0].transmittance_s);
let t1 = results[0].transmittance_s;
let t2_single = 1.0 - crate::ray::fresnel_normal(1.5, 1.0);
assert!(
(results[1].transmittance_s - t1 * t2_single).abs() < 0.01,
"Cumulative transmittance should multiply"
);
}
#[test]
fn test_polarized_trace_transmittance_range() {
let ray = TraceRay {
position: [0.0, 0.0, 0.0],
direction: [0.0, 0.0, 1.0],
n: 1.0,
};
let surface = OpticalSurface {
shape: SurfaceShape::Plane,
z_position: 10.0,
n_after: 1.5,
aperture_radius: 5.0,
};
let results = trace_sequential_polarized(&ray, &[surface]).unwrap();
assert!(
(0.0..=1.0).contains(&results[0].transmittance_s),
"Ts out of range"
);
assert!(
(0.0..=1.0).contains(&results[0].transmittance_p),
"Tp out of range"
);
}
}