use crate::ray::paraxial::{ChromaticAnalysis, SystemMatrix};
use crate::ray::tracer::Surface;
#[derive(Debug, Clone, Copy)]
pub struct GlassMaterial {
pub n_d: f64,
pub abbe_number: f64,
pub name: &'static str,
}
impl GlassMaterial {
pub fn bk7() -> Self {
Self {
n_d: 1.5168,
abbe_number: 64.17,
name: "N-BK7",
}
}
pub fn f2() -> Self {
Self {
n_d: 1.6200,
abbe_number: 36.43,
name: "N-F2",
}
}
pub fn sf11() -> Self {
Self {
n_d: 1.7847,
abbe_number: 25.76,
name: "N-SF11",
}
}
pub fn fused_silica() -> Self {
Self {
n_d: 1.4585,
abbe_number: 67.82,
name: "SiO2",
}
}
pub fn lak22() -> Self {
Self {
n_d: 1.6516,
abbe_number: 58.90,
name: "N-LAK22",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Singlet {
pub glass: GlassMaterial,
pub focal_length: f64,
pub bending_factor: f64,
pub thickness: f64,
}
impl Singlet {
pub fn new(
glass: GlassMaterial,
focal_length: f64,
bending_factor: f64,
thickness: f64,
) -> Self {
Self {
glass,
focal_length,
bending_factor,
thickness,
}
}
pub fn aplanatic_bending(glass: GlassMaterial) -> f64 {
let n = glass.n_d;
2.0 * (n * n - 1.0) / (n + 2.0)
}
pub fn equiconvex(glass: GlassMaterial, focal_length: f64, thickness: f64) -> Self {
Self::new(glass, focal_length, 0.0, thickness)
}
pub fn plano_convex(glass: GlassMaterial, focal_length: f64, thickness: f64) -> Self {
Self::new(glass, focal_length, 1.0, thickness)
}
pub fn r1(&self) -> f64 {
let n = self.glass.n_d;
let q = self.bending_factor;
let power = (n - 1.0) / self.focal_length;
if (q - 1.0).abs() < 1e-10 {
(n - 1.0) * self.focal_length
} else {
2.0 * (n - 1.0) / (power * (1.0 + q))
}
}
pub fn r2(&self) -> f64 {
let n = self.glass.n_d;
let q = self.bending_factor;
let power = (n - 1.0) / self.focal_length;
if (q + 1.0).abs() < 1e-10 {
f64::INFINITY
} else {
2.0 * (n - 1.0) / (power * (q - 1.0))
}
}
pub fn system_matrix(&self) -> SystemMatrix {
let n = self.glass.n_d;
let r1 = self.r1();
let r2 = self.r2();
let surfaces = vec![
Surface::CurvedInterface {
r: r1,
n1: 1.0,
n2: n,
},
Surface::FreeSpace { d: self.thickness },
Surface::CurvedInterface {
r: r2,
n1: n,
n2: 1.0,
},
];
SystemMatrix::from_surfaces(&surfaces)
}
pub fn lca(&self) -> f64 {
ChromaticAnalysis::new(self.focal_length, self.glass.abbe_number).lca()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AchromaticDoublet {
pub crown: GlassMaterial,
pub flint: GlassMaterial,
pub focal_length: f64,
pub t1: f64,
pub t2: f64,
}
impl AchromaticDoublet {
pub fn new(
crown: GlassMaterial,
flint: GlassMaterial,
focal_length: f64,
t1: f64,
t2: f64,
) -> Self {
Self {
crown,
flint,
focal_length,
t1,
t2,
}
}
pub fn bk7_f2(focal_length: f64) -> Self {
let t = focal_length * 0.05; Self::new(
GlassMaterial::bk7(),
GlassMaterial::f2(),
focal_length,
t,
t * 0.5,
)
}
pub fn f_crown(&self) -> f64 {
let v1 = self.crown.abbe_number;
let v2 = self.flint.abbe_number;
self.focal_length * (v1 - v2) / v1
}
pub fn f_flint(&self) -> f64 {
let v1 = self.crown.abbe_number;
let v2 = self.flint.abbe_number;
-self.focal_length * (v1 - v2) / v2
}
pub fn cemented_radius(&self) -> f64 {
let n1 = self.crown.n_d;
let n2 = self.flint.n_d;
let fc = self.f_crown();
let phi_c = (n1 - 1.0) / fc;
if phi_c.abs() > 1e-30 {
(n2 - n1) / phi_c
} else {
f64::INFINITY
}
}
pub fn secondary_spectrum(&self) -> f64 {
self.focal_length / (self.crown.abbe_number + self.flint.abbe_number)
}
pub fn verify_power_sum(&self) -> f64 {
let phi1 = 1.0 / self.f_crown();
let phi2 = 1.0 / self.f_flint();
(phi1 + phi2 - 1.0 / self.focal_length).abs()
}
}
#[derive(Debug, Clone, Copy)]
pub struct CookeTriplet {
pub f1: f64,
pub f2: f64,
pub f3: f64,
pub d1: f64,
pub d2: f64,
pub crown: GlassMaterial,
pub flint: GlassMaterial,
}
impl CookeTriplet {
pub fn new(
f1: f64,
f2: f64,
f3: f64,
d1: f64,
d2: f64,
crown: GlassMaterial,
flint: GlassMaterial,
) -> Self {
Self {
f1,
f2,
f3,
d1,
d2,
crown,
flint,
}
}
pub fn standard_f100() -> Self {
Self::new(
0.120, -0.050, 0.120, 0.040, 0.040, GlassMaterial::bk7(),
GlassMaterial::f2(),
)
}
pub fn system_matrix(&self) -> SystemMatrix {
let surfaces = vec![
Surface::ThinLens { f: self.f1 },
Surface::FreeSpace { d: self.d1 },
Surface::ThinLens { f: self.f2 },
Surface::FreeSpace { d: self.d2 },
Surface::ThinLens { f: self.f3 },
];
SystemMatrix::from_surfaces(&surfaces)
}
pub fn effective_focal_length(&self) -> f64 {
let cp = self.system_matrix().cardinal_points();
cp.back_focal_length
}
pub fn petzval_sum(&self) -> f64 {
let phi1 = 1.0 / self.f1;
let phi2 = 1.0 / self.f2;
let phi3 = 1.0 / self.f3;
phi1 / self.crown.n_d + phi2 / self.flint.n_d + phi3 / self.crown.n_d
}
}
#[derive(Debug, Clone)]
pub struct LensMeritFunction {
pub target_focal_length: f64,
pub max_lca: f64,
pub w_focal: f64,
pub w_chromatic: f64,
}
impl LensMeritFunction {
pub fn new(target_focal_length: f64) -> Self {
Self {
target_focal_length,
max_lca: target_focal_length * 0.01,
w_focal: 1.0,
w_chromatic: 1.0,
}
}
pub fn evaluate_singlet(&self, singlet: &Singlet) -> f64 {
let m = singlet.system_matrix();
let cp = m.cardinal_points();
let focal_err = (cp.back_focal_length - self.target_focal_length).abs();
let lca = singlet.lca().abs();
self.w_focal * focal_err / self.target_focal_length + self.w_chromatic * lca / self.max_lca
}
pub fn evaluate_doublet(&self, doublet: &AchromaticDoublet) -> f64 {
let focal_err = (doublet.focal_length - self.target_focal_length).abs();
let secondary = doublet.secondary_spectrum().abs();
let power_err = doublet.verify_power_sum();
self.w_focal * focal_err / self.target_focal_length
+ self.w_chromatic * secondary / self.max_lca
+ 10.0 * power_err * self.target_focal_length
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn singlet_r1_positive_for_converging() {
let s = Singlet::equiconvex(GlassMaterial::bk7(), 0.1, 0.005);
assert!(s.r1() > 0.0, "R1={}", s.r1());
}
#[test]
fn singlet_r2_negative_for_equiconvex() {
let s = Singlet::equiconvex(GlassMaterial::bk7(), 0.1, 0.005);
assert!(s.r2() < 0.0, "R2={}", s.r2());
}
#[test]
fn singlet_plano_convex_r2_infinity() {
let s = Singlet::plano_convex(GlassMaterial::bk7(), 0.1, 0.005);
assert!(s.r2().is_infinite(), "R2={}", s.r2());
}
#[test]
fn aplanatic_bending_bk7() {
let q = Singlet::aplanatic_bending(GlassMaterial::bk7());
assert!(q > 0.5 && q < 1.5, "q_opt={q}");
}
#[test]
fn achromat_power_sum_correct() {
let d = AchromaticDoublet::bk7_f2(0.1);
let err = d.verify_power_sum();
assert!(err < 1e-10, "power sum error={err}");
}
#[test]
fn achromat_crown_flint_opposing_powers() {
let d = AchromaticDoublet::bk7_f2(0.1);
assert!(d.f_crown() > 0.0, "f_crown={}", d.f_crown());
assert!(d.f_flint() < 0.0, "f_flint={}", d.f_flint());
}
#[test]
fn triplet_petzval_sum_finite() {
let t = CookeTriplet::standard_f100();
let pz = t.petzval_sum();
assert!(pz.is_finite(), "petzval={pz}");
}
#[test]
fn triplet_efl_reasonable() {
let t = CookeTriplet::standard_f100();
let efl = t.effective_focal_length();
assert!(efl.is_finite() && efl > 0.0 && efl < 2.0, "EFL={efl}");
}
#[test]
fn merit_function_singlet() {
let mf = LensMeritFunction::new(0.1);
let s = Singlet::equiconvex(GlassMaterial::bk7(), 0.1, 0.005);
let merit = mf.evaluate_singlet(&s);
assert!(merit.is_finite() && merit >= 0.0, "merit={merit}");
}
#[test]
fn glass_materials_distinct() {
let bk7 = GlassMaterial::bk7();
let f2 = GlassMaterial::f2();
assert!((bk7.n_d - f2.n_d).abs() > 0.05);
assert!((bk7.abbe_number - f2.abbe_number).abs() > 20.0);
}
}