#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ray {
pub y: f64,
pub u: f64,
}
impl Ray {
pub fn new(y: f64, u: f64) -> Self {
Self { y, u }
}
pub fn axial() -> Self {
Self { y: 0.0, u: 1.0 }
}
pub fn chief() -> Self {
Self { y: 1.0, u: 0.0 }
}
}
#[derive(Debug, Clone)]
pub enum Surface {
FreeSpace { d: f64 },
ThinLens { f: f64 },
CurvedInterface { r: f64, n1: f64, n2: f64 },
FlatInterface { n1: f64, n2: f64 },
Mirror { r: f64 },
ApertureStop { radius: f64 },
DiffractionGrating {
groove_density: f64,
m: i32,
n1: f64,
n2: f64,
wavelength: f64,
},
}
impl Surface {
pub fn apply(&self, ray: Ray) -> Ray {
match *self {
Surface::FreeSpace { d } => Ray {
y: ray.y + d * ray.u,
u: ray.u,
},
Surface::ThinLens { f } => Ray {
y: ray.y,
u: ray.u - ray.y / f,
},
Surface::CurvedInterface { r, n1, n2 } => {
let u_new = (n1 * ray.u - (n2 - n1) / r * ray.y) / n2;
Ray { y: ray.y, u: u_new }
}
Surface::FlatInterface { n1, n2 } => Ray {
y: ray.y,
u: ray.u * n1 / n2,
},
Surface::Mirror { r } => {
if r.is_infinite() {
Ray {
y: ray.y,
u: -ray.u,
}
} else {
Ray {
y: ray.y,
u: ray.u - 2.0 * ray.y / r,
}
}
}
Surface::ApertureStop { radius } => {
if ray.y.abs() > radius {
Ray {
y: ray.y,
u: f64::NAN,
}
} else {
ray
}
}
Surface::DiffractionGrating {
groove_density,
m,
n1,
n2,
wavelength,
} => {
let u_out = (n1 * ray.u + m as f64 * wavelength * groove_density) / n2;
Ray { y: ray.y, u: u_out }
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OpticalSystem {
surfaces: Vec<Surface>,
}
impl OpticalSystem {
pub fn new() -> Self {
Self {
surfaces: Vec::new(),
}
}
pub fn push(mut self, surface: Surface) -> Self {
self.surfaces.push(surface);
self
}
pub fn trace(&self, ray: Ray) -> Ray {
self.surfaces.iter().fold(ray, |r, s| s.apply(r))
}
pub fn trace_full(&self, ray: Ray) -> Vec<Ray> {
let mut snapshots = vec![ray];
let mut current = ray;
for s in &self.surfaces {
current = s.apply(current);
snapshots.push(current);
}
snapshots
}
pub fn effective_focal_length(&self) -> Option<f64> {
let m = self.abcd_matrix();
if m.c.abs() < 1e-30 {
None
} else {
Some(-1.0 / m.c)
}
}
pub fn abcd_matrix(&self) -> crate::ray::gaussian_beam::AbcdMatrix {
use crate::ray::gaussian_beam::AbcdMatrix;
self.surfaces
.iter()
.map(|s| match s {
Surface::FreeSpace { d } => AbcdMatrix::free_space(*d),
Surface::ThinLens { f } => AbcdMatrix::thin_lens(*f),
Surface::CurvedInterface { r, n1, n2 } => {
AbcdMatrix::curved_interface(*r, *n1, *n2)
}
Surface::FlatInterface { n1, n2 } => AbcdMatrix::flat_interface(*n1, *n2),
Surface::Mirror { r } => AbcdMatrix {
a: 1.0,
b: 0.0,
c: -2.0 / r,
d: 1.0,
},
Surface::ApertureStop { .. } => AbcdMatrix::identity(),
Surface::DiffractionGrating { n1, n2, .. } => AbcdMatrix::flat_interface(*n1, *n2),
})
.fold(AbcdMatrix::identity(), |acc, m| m.then(&acc))
}
}
#[derive(Debug, Clone)]
pub struct AsphericSurface {
pub r: f64,
pub k: f64,
pub a4: f64,
pub a6: f64,
pub n1: f64,
pub n2: f64,
}
impl AsphericSurface {
pub fn spherical(r: f64, n1: f64, n2: f64) -> Self {
Self {
r,
k: 0.0,
a4: 0.0,
a6: 0.0,
n1,
n2,
}
}
pub fn parabolic(r: f64, n1: f64, n2: f64) -> Self {
Self {
r,
k: -1.0,
a4: 0.0,
a6: 0.0,
n1,
n2,
}
}
pub fn sag_paraxial(&self, h: f64) -> f64 {
h * h / (2.0 * self.r)
}
pub fn sag(&self, h: f64) -> f64 {
let c = 1.0 / self.r;
let disc = 1.0 - (1.0 + self.k) * c * c * h * h;
if disc <= 0.0 {
return f64::NAN;
}
c * h * h / (1.0 + disc.sqrt()) + self.a4 * h.powi(4) + self.a6 * h.powi(6)
}
pub fn power(&self) -> f64 {
(self.n2 - self.n1) / self.r
}
pub fn apply_paraxial(&self, ray: Ray) -> Ray {
let u_new = (self.n1 * ray.u - self.power() * ray.y) / self.n2;
Ray { y: ray.y, u: u_new }
}
pub fn conic_aberration_coeff(&self) -> f64 {
-self.k / (8.0 * self.r.powi(3))
}
}
pub fn prism_deviation(n_glass: f64, apex_angle_deg: f64, incident_angle_deg: f64) -> f64 {
let apex = apex_angle_deg.to_radians();
let theta1 = incident_angle_deg.to_radians();
let sin_theta2 = theta1.sin() / n_glass;
if sin_theta2.abs() > 1.0 {
return f64::NAN; }
let theta2 = sin_theta2.asin();
let theta3 = apex - theta2;
let sin_theta4 = n_glass * theta3.sin();
if sin_theta4.abs() > 1.0 {
return f64::NAN;
}
let theta4 = sin_theta4.asin();
let deviation = theta1 + theta4 - apex;
deviation.to_degrees()
}
pub fn prism_minimum_deviation(n_glass: f64, apex_angle_deg: f64) -> f64 {
let apex = apex_angle_deg.to_radians();
let sin_val = n_glass * (apex / 2.0).sin();
if sin_val.abs() > 1.0 {
return f64::NAN;
}
let d_min = 2.0 * sin_val.asin() - apex;
d_min.to_degrees()
}
pub fn is_vignetted(ray: &Ray) -> bool {
ray.u.is_nan()
}
pub fn filter_vignetted(rays: &[Ray]) -> Vec<Ray> {
rays.iter().cloned().filter(|r| !is_vignetted(r)).collect()
}
pub fn numerical_aperture(half_angle_rad: f64, n: f64) -> f64 {
n * half_angle_rad.sin()
}
pub fn abbe_resolution(wavelength: f64, na: f64) -> f64 {
wavelength / (2.0 * na)
}
pub fn f_number(focal_length: f64, aperture_diameter: f64) -> f64 {
focal_length / aperture_diameter
}
pub fn depth_of_field(wavelength: f64, f_num: f64) -> f64 {
2.0 * wavelength * f_num * f_num
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ray_through_thin_lens() {
let system = OpticalSystem::new()
.push(Surface::ThinLens { f: 100e-3 })
.push(Surface::FreeSpace { d: 100e-3 });
let ray = Ray::new(1.0, 0.0);
let out = system.trace(ray);
assert!(
out.y.abs() < 1e-10,
"Ray should cross axis at focal plane, y={:.2e}",
out.y
);
}
#[test]
fn ray_through_two_lenses_telescope() {
let f1 = -50e-3_f64;
let f2 = 200e-3_f64;
let d = f2 + f1; let system = OpticalSystem::new()
.push(Surface::ThinLens { f: f1 })
.push(Surface::FreeSpace { d })
.push(Surface::ThinLens { f: f2 });
let ray = Ray::new(1.0, 0.0);
let out = system.trace(ray);
let magnification = -f2 / f1; assert!(
(out.u).abs() < 1e-10,
"Output should be parallel: u={:.2e}",
out.u
);
assert!(
(out.y - magnification).abs() < 1e-8,
"Magnification should be {magnification:.1}, got {:.4}",
out.y
);
}
#[test]
fn focal_length_from_matrix() {
let f = 50e-3_f64;
let system = OpticalSystem::new().push(Surface::ThinLens { f });
let efl = system.effective_focal_length().unwrap();
assert!((efl - f).abs() < 1e-12);
}
#[test]
fn flat_interface_snell_paraxial() {
let n1 = 1.0;
let n2 = 1.5;
let s = Surface::FlatInterface { n1, n2 };
let ray = Ray::new(0.0, 0.5);
let out = s.apply(ray);
let rel_err = (n1 * ray.u - n2 * out.u).abs();
assert!(
rel_err < 1e-12,
"Snell's law violated: n1*u1={:.4} n2*u2={:.4}",
n1 * ray.u,
n2 * out.u
);
}
#[test]
fn numerical_aperture_values() {
let na = numerical_aperture(30.0_f64.to_radians(), 1.5);
assert!((na - 0.75).abs() < 0.01);
}
#[test]
fn abbe_resolution_values() {
let res = abbe_resolution(500e-9, 1.0);
assert!((res - 250e-9).abs() < 1e-12);
}
#[test]
fn trace_full_returns_snapshots() {
let system = OpticalSystem::new()
.push(Surface::FreeSpace { d: 10e-3 })
.push(Surface::ThinLens { f: 50e-3 })
.push(Surface::FreeSpace { d: 50e-3 });
let rays = system.trace_full(Ray::new(1.0, 0.0));
assert_eq!(rays.len(), 4); }
#[test]
fn abcd_system_det_is_unity() {
let system = OpticalSystem::new()
.push(Surface::FreeSpace { d: 10e-3 })
.push(Surface::ThinLens { f: 50e-3 })
.push(Surface::FreeSpace { d: 50e-3 });
let m = system.abcd_matrix();
assert!((m.det() - 1.0).abs() < 1e-10);
}
#[test]
fn aperture_stop_clips_large_ray() {
let stop = Surface::ApertureStop { radius: 1e-3 };
let large_ray = Ray::new(2e-3, 0.0);
let out = stop.apply(large_ray);
assert!(is_vignetted(&out));
}
#[test]
fn aperture_stop_passes_small_ray() {
let stop = Surface::ApertureStop { radius: 1e-3 };
let small_ray = Ray::new(0.5e-3, 0.1);
let out = stop.apply(small_ray);
assert!(!is_vignetted(&out));
assert!((out.u - 0.1).abs() < 1e-12);
}
#[test]
fn diffraction_grating_deflects_ray() {
let groove_density = 600e3; let grating = Surface::DiffractionGrating {
groove_density,
m: 1,
n1: 1.0,
n2: 1.0,
wavelength: 633e-9,
};
let ray = Ray::new(0.0, 0.0); let out = grating.apply(ray);
assert!((out.u - 633e-9 * groove_density).abs() < 1e-6);
}
#[test]
fn aspheric_sphere_sag_paraxial() {
let surf = AsphericSurface::spherical(100e-3, 1.0, 1.5);
let h = 5e-3;
let sag_p = surf.sag_paraxial(h);
let sag_full = surf.sag(h);
assert!((sag_p - sag_full).abs() / sag_p < 0.01);
}
#[test]
fn aspheric_power_equals_formula() {
let surf = AsphericSurface::spherical(50e-3, 1.0, 1.5);
let expected = (1.5 - 1.0) / 50e-3; assert!((surf.power() - expected).abs() < 1e-10);
}
#[test]
fn prism_deviation_positive() {
let dev = prism_deviation(1.5, 60.0, 50.0);
assert!(dev > 0.0, "Deviation should be positive, got {dev:.2}");
assert!(dev < 90.0, "Deviation should be < 90deg");
}
#[test]
fn prism_minimum_deviation_less_than_apex() {
let d_min = prism_minimum_deviation(1.5, 60.0);
assert!(d_min.is_finite(), "Minimum deviation should be finite");
assert!(d_min > 0.0);
}
#[test]
fn filter_vignetted_removes_nan_rays() {
let rays = vec![
Ray::new(0.5e-3, 0.1),
Ray::new(2e-3, f64::NAN), Ray::new(0.3e-3, -0.05),
];
let filtered = filter_vignetted(&rays);
assert_eq!(filtered.len(), 2);
}
}