cherry_rs/core/
sequential_model.rs

1/// Data types for modeling sequential ray tracing systems.
2use std::collections::HashMap;
3use std::fmt::{Display, Formatter};
4use std::ops::Range;
5
6use anyhow::{anyhow, Result};
7use serde::{Deserialize, Serialize};
8
9use crate::core::{
10    math::{mat3::Mat3, vec3::Vec3},
11    Cursor, Float, RefractiveIndex,
12};
13use crate::specs::{
14    gaps::GapSpec,
15    surfaces::{SurfaceSpec, SurfaceType},
16};
17
18/// The transverse direction along which system properties will be computed with
19/// respect to the reference frame of the system.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum Axis {
22    X,
23    Y,
24}
25
26/// A gap between two surfaces in a sequential system.
27#[derive(Debug)]
28pub struct Gap {
29    pub thickness: Float,
30    pub refractive_index: RefractiveIndex,
31}
32
33/// A collection of submodels for sequential ray tracing.
34///
35/// A sequential model is a collection of surfaces and gaps that define the
36/// optical system. The model is divided into submodels, each of which is
37/// computed along a specific axis and for a specific wavelength.
38///
39/// See the documentation for
40/// [SequentialSubModel](trait@SequentialSubModel) for more information.
41#[derive(Debug)]
42pub struct SequentialModel {
43    surfaces: Vec<Surface>,
44    submodels: HashMap<SubModelID, SequentialSubModelBase>,
45}
46
47/// A submodel of a sequential optical system.
48///
49/// A sequential submodel is the primary unit of computation in a sequential
50/// optical system. It is a collection of N + 1 surfaces and N gaps from which
51/// an iterator can be created to trace rays through the system.
52///
53/// Each submodel represents a sequence of surfaces and gaps for a given
54/// set of system parameters, such as wavelength and transverse axis. The set of
55/// all submodels spans the entire set of parameters of interest.
56///
57/// The iterator over a submodel yields a series of steps, each of which is a
58/// tuple of the form (Gap, Surface, Option\<Gap\>). The first element of a step
59/// is the gap before the surface, the second element is the surface itself, and
60/// the third element is the gap after the surface. The last Gap is optional
61/// because no gap exists after the image plane surface.
62///
63/// Given a system of N + 1 surfaces and N gaps, the first surface S0 is always
64/// an object plane and the last surface S(N) is always an image plane. The
65/// length of the iterator is N.
66///
67/// A forward iterator for such a system looks like the following:
68///
69/// ```text
70/// S0   S1    S2    S3        S(N-1)    S(N)
71///  \  /  \  /  \  /  \   ... /    \    /  \
72///   G0    G1    G2    G3          G(N-1)   None
73///   --------    --------          -------------
74///    Step 0      Step 2             Step(N-1)
75///         --------
76///          Step 1
77/// ```
78///
79/// Step 0 is the tuple (G0, S1, G1), Step 1 is (G1, S2, G2), and so on.
80///
81/// A reverse iterator for the same system looks like the following:
82///
83/// ```text
84///    S(N)   S(N-1)  S(N-2)  S(N-3)            S1    S0
85///   /    \  /    \  /    \  /    \      ...  /  \  /
86/// None  G(N-1)  G(N-2)  G(N-3)    G(N-4)    G1    G0
87///       --------------  ----------------    ---------
88///           Step 0           Step 2         Step(N-2)
89///               --------------
90///                   Step 1
91/// ```
92///
93/// In the reverse iteration, we  never iterate from the image plane surface.
94/// For this reason, the number of steps in the reverse iterator is N - 1.
95///
96/// If `i` is the index of a surface in the forward iterator and `j` the index
97/// of the surface in the reverse iterator, then the two indexes are related by
98/// the equation j = N - i as shown above.
99///
100/// Strictly speaking, the last gap need not be None. Additionally, the first
101/// and last surfaces need not be object and image planes, repectively. These
102/// constraints are guaranteed at the level of the SequentialModel. However,
103/// since a SequentialSubModel is always created by a SequentialModel, we can
104/// assume that these constraints are always met. This would not be the case for
105/// user-supplied implementations of this trait, where care should be taken to
106/// ensure that the implementation conforms to these constraints.
107///
108/// With all of these constraints, the problem of sequential optical modeling is
109/// reduced to the problem of iterating over the surfaces and gaps in a submodel
110/// and determining what happens at each step. The same iterator can be used for
111/// different modeling approaches, e.g. paraxial ray tracing, 3D ray tracing,
112/// paraxial Gaussian beam propagation, etc. without changing the representation
113/// of the underlying system.
114pub trait SequentialSubModel {
115    fn gaps(&self) -> &[Gap];
116    fn is_obj_at_inf(&self) -> bool;
117
118    fn is_empty(&self) -> bool {
119        self.gaps().is_empty()
120    }
121    fn len(&self) -> usize {
122        self.gaps().len()
123    }
124    fn try_iter<'a>(&'a self, surfaces: &'a [Surface]) -> Result<SequentialSubModelIter<'a>>;
125
126    fn slice(&self, idx: Range<usize>) -> SequentialSubModelSlice<'_> {
127        SequentialSubModelSlice {
128            gaps: &self.gaps()[idx],
129        }
130    }
131}
132
133#[derive(Debug)]
134pub struct SequentialSubModelBase {
135    gaps: Vec<Gap>,
136}
137
138/// A view of a single submodel in a sequential system.
139///
140/// This is used to slice the system into smaller parts.
141#[derive(Debug)]
142pub struct SequentialSubModelSlice<'a> {
143    gaps: &'a [Gap],
144}
145
146/// A unique identifier for a submodel.
147///
148/// The first element is the index of the wavelength in the system's list of
149/// wavelengths. The second element is the transverse axis along which the model
150/// is computed.
151///
152/// The wavelength index is None if no wavelengths are provided. This is the
153/// case when refractive indices are constant and provided directly by the user.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
155pub struct SubModelID(pub Option<usize>, pub Axis);
156
157/// An iterator over the surfaces and gaps in a submodel.
158///
159/// Most operations in sequential modeling involve use of this iterator.
160pub struct SequentialSubModelIter<'a> {
161    surfaces: &'a [Surface],
162    gaps: &'a [Gap],
163    index: usize,
164}
165
166/// A reverse iterator over the surfaces and gaps in a submodel.
167pub struct SequentialSubModelReverseIter<'a> {
168    surfaces: &'a [Surface],
169    gaps: &'a [Gap],
170    index: usize,
171}
172
173/// A single ray tracing step in a sequential system.
174///
175/// See the documentation for
176/// [SequentialSubModel](trait@SequentialSubModel) for more information.
177pub type Step<'a> = (&'a Gap, &'a Surface, Option<&'a Gap>);
178
179#[derive(Debug)]
180pub enum Surface {
181    Conic(Conic),
182    Image(Image),
183    Object(Object),
184    Probe(Probe),
185    Stop(Stop),
186    //Toric(Toric),
187}
188
189#[derive(Debug)]
190pub struct Conic {
191    pos: Vec3,
192    rotation_matrix: Mat3,
193    semi_diameter: Float,
194    radius_of_curvature: Float,
195    conic_constant: Float,
196    surface_type: SurfaceType,
197}
198
199#[derive(Debug)]
200pub struct Image {
201    pos: Vec3,
202    rotation_matrix: Mat3,
203}
204
205#[derive(Debug)]
206pub struct Object {
207    pos: Vec3,
208    rotation_matrix: Mat3,
209}
210
211/// A surface without any effect on rays that is used to measure intersections.
212#[derive(Debug)]
213pub struct Probe {
214    pos: Vec3,
215    rotation_matrix: Mat3,
216}
217
218#[derive(Debug)]
219pub struct Stop {
220    pos: Vec3,
221    rotation_matrix: Mat3,
222    semi_diameter: Float,
223}
224
225// TODO: Implement Toric surfaces
226//#[derive(Debug)]
227//pub struct Toric {
228//    pos: Vec3,
229//    rotation_matrix: Mat3,
230//    semi_diameter: Float,
231//    radius_of_curvature_y: Float,
232//    radius_of_curvature_x: Float,
233//    conic_constant: Float,
234//    surface_type: SurfaceType,
235//}
236
237/// Returns the index of the first physical surface in the system.
238/// This is the first surface that is not an object, image, or probe surface.
239/// If no such surface exists, then the function returns None.
240pub(crate) fn first_physical_surface(surfaces: &[Surface]) -> Option<usize> {
241    surfaces
242        .iter()
243        .position(|surf| matches!(surf, Surface::Conic(_) | Surface::Stop(_)))
244}
245
246/// Returns the index of the last physical surface in the system.
247/// This is the last surface that is not an object, image, or probe surface.
248/// If no such surface exists, then the function returns None.
249pub fn last_physical_surface(surfaces: &[Surface]) -> Option<usize> {
250    surfaces
251        .iter()
252        .rposition(|surf| matches!(surf, Surface::Conic(_) | Surface::Stop(_)))
253}
254
255/// Returns the id of a surface in a reversed system.
256pub fn reversed_surface_id(surfaces: &[Surface], surf_id: usize) -> usize {
257    // Reversed IDs are ray starts, then image plane, then surfaces
258    surfaces.len() - surf_id - 1
259}
260
261impl Conic {
262    pub fn sag_norm(&self, pos: Vec3) -> (Float, Vec3) {
263        if self.radius_of_curvature.is_infinite() {
264            return (0.0, Vec3::new(0.0, 0.0, 1.0));
265        }
266
267        // Convert to polar coordinates in x, y plane
268        let r = (pos.x().powi(2) + pos.y().powi(2)).sqrt();
269        let theta = pos.y().atan2(pos.x());
270
271        // Compute surface sag
272        let a = r.powi(2) / self.radius_of_curvature;
273        let sag =
274            a / (1.0 + (1.0 - (1.0 + self.conic_constant) * a / self.radius_of_curvature).sqrt());
275
276        // Compute surface normal
277        let denom = (self.radius_of_curvature.powi(4)
278            - (1.0 + self.conic_constant) * (r * self.radius_of_curvature).powi(2))
279        .sqrt();
280        let dfdx = -r * self.radius_of_curvature * theta.cos() / denom;
281        let dfdy = -r * self.radius_of_curvature * theta.sin() / denom;
282        let dfdz = 1.0 as Float;
283        let norm = Vec3::new(dfdx, dfdy, dfdz).normalize();
284
285        (sag, norm)
286    }
287}
288
289impl Gap {
290    pub(crate) fn try_from_spec(spec: &GapSpec, wavelength: Option<Float>) -> Result<Self> {
291        let thickness = spec.thickness;
292        let refractive_index = RefractiveIndex::try_from_spec(&spec.refractive_index, wavelength)?;
293        Ok(Self {
294            thickness,
295            refractive_index,
296        })
297    }
298}
299
300impl SequentialModel {
301    /// Creates a new sequential model of an optical system.
302    ///
303    /// # Arguments
304    /// * `gap_specs` - The specifications for the gaps between the surfaces.
305    /// * `surface_specs` - The specifications for the surfaces in the system.
306    /// * `wavelengths` - The wavelengths at which to model the system.
307    ///
308    /// # Returns
309    /// A new sequential model.
310    pub fn new(
311        gap_specs: &[GapSpec],
312        surface_specs: &[SurfaceSpec],
313        wavelengths: &[Float],
314    ) -> Result<Self> {
315        Self::validate_specs(gap_specs, wavelengths)?;
316
317        let surfaces = Self::surf_specs_to_surfs(surface_specs, gap_specs);
318
319        let model_ids: Vec<SubModelID> = Self::calc_model_ids(&surfaces, wavelengths);
320        let mut models: HashMap<SubModelID, SequentialSubModelBase> = HashMap::new();
321        for model_id in model_ids.iter() {
322            let wavelength = model_id.0.map(|idx| wavelengths[idx]);
323            let gaps = Self::gap_specs_to_gaps(gap_specs, wavelength)?;
324            let model = SequentialSubModelBase::new(gaps);
325            models.insert(*model_id, model);
326        }
327
328        Ok(Self {
329            surfaces,
330            submodels: models,
331        })
332    }
333
334    /// Returns the surfaces in the system.
335    pub fn surfaces(&self) -> &[Surface] {
336        &self.surfaces
337    }
338
339    /// Returns the submodels in the system.
340    pub fn submodels(&self) -> &HashMap<SubModelID, impl SequentialSubModel> {
341        &self.submodels
342    }
343
344    /// Returns the largest semi-diameter of any surface in the system.
345    ///
346    /// This ignores surfaces without any size, such as object, probe, and image
347    /// surfaces.
348    pub fn largest_semi_diameter(&self) -> Float {
349        self.surfaces
350            .iter()
351            .filter_map(|surf| match surf {
352                Surface::Conic(conic) => Some(conic.semi_diameter),
353                //Surface::Toric(toric) => Some(toric.semi_diameter),
354                Surface::Stop(stop) => Some(stop.semi_diameter),
355                _ => None,
356            })
357            .fold(0.0, |acc, x| acc.max(x))
358    }
359
360    /// Computes the unique IDs for each paraxial model.
361    fn calc_model_ids(surfaces: &[Surface], wavelengths: &[Float]) -> Vec<SubModelID> {
362        let mut ids = Vec::new();
363        if wavelengths.is_empty() && Self::is_rotationally_symmetric(surfaces) {
364            ids.push(SubModelID(None, Axis::Y));
365            return ids;
366        } else if wavelengths.is_empty() {
367            ids.push(SubModelID(None, Axis::X));
368            ids.push(SubModelID(None, Axis::Y));
369            return ids;
370        }
371
372        let axes: Vec<Axis> = if Self::is_rotationally_symmetric(surfaces) {
373            vec![Axis::Y]
374        } else {
375            vec![Axis::X, Axis::Y]
376        };
377
378        for (idx, _wavelength) in wavelengths.iter().enumerate() {
379            for axis in axes.iter() {
380                let id = SubModelID(Some(idx), *axis);
381                ids.push(id);
382            }
383        }
384        ids
385    }
386
387    fn gap_specs_to_gaps(gap_specs: &[GapSpec], wavelength: Option<Float>) -> Result<Vec<Gap>> {
388        let mut gaps = Vec::new();
389        for gap_spec in gap_specs.iter() {
390            let gap = Gap::try_from_spec(gap_spec, wavelength)?;
391            gaps.push(gap);
392        }
393        Ok(gaps)
394    }
395
396    /// Returns true if the system is rotationally symmetric about the optical
397    /// axis.
398    fn is_rotationally_symmetric(_surfaces: &[Surface]) -> bool {
399        // Return false if any toric surface is present in the system.
400        //!surfaces
401        //    .iter()
402        //    .any(|surf| matches!(surf, Surface::Toric(_)))
403        true
404    }
405
406    fn surf_specs_to_surfs(surf_specs: &[SurfaceSpec], gap_specs: &[GapSpec]) -> Vec<Surface> {
407        let mut surfaces = Vec::new();
408
409        // The first surface is an object surface.
410        // The second surface is at z=0 by convention.
411        let mut cursor = Cursor::new(-gap_specs[0].thickness);
412
413        // Create surfaces 0 to n-1
414        for (surf_spec, gap_spec) in surf_specs.iter().zip(gap_specs.iter()) {
415            let surf = Surface::from_spec(surf_spec, cursor.pos());
416
417            // Flip the cursor upon reflection
418            if let SurfaceType::Reflecting = surf.surface_type() {
419                cursor.invert();
420            }
421
422            // Add the surface to the list and advance the cursor
423            surfaces.push(surf);
424            cursor.advance(gap_spec.thickness);
425        }
426
427        // Add the last surface
428        surfaces.push(Surface::from_spec(
429            surf_specs
430                .last()
431                .expect("There should always be one last surface."),
432            cursor.pos(),
433        ));
434        surfaces
435    }
436
437    fn validate_gaps(gaps: &[GapSpec], wavelengths: &[Float]) -> Result<()> {
438        if gaps.is_empty() {
439            return Err(anyhow!("The system must have at least one gap."));
440        }
441
442        // If no wavelengths are specified, then the gaps must explicitly specify the
443        // refractive index.
444        if wavelengths.is_empty() {
445            for gap in gaps.iter() {
446                if gap.refractive_index.depends_on_wavelength() {
447                    return Err(anyhow!(
448                        "The refractive index of the gap must be a constant when no wavelengths are provided."
449                    ));
450                }
451            }
452        }
453        Ok(())
454    }
455
456    fn validate_specs(gaps: &[GapSpec], wavelengths: &[Float]) -> Result<()> {
457        // TODO: Validate surface specs as well!
458        Self::validate_gaps(gaps, wavelengths)?;
459        Ok(())
460    }
461}
462
463impl SequentialSubModelBase {
464    pub(crate) fn new(gaps: Vec<Gap>) -> Self {
465        Self { gaps }
466    }
467}
468
469impl SequentialSubModel for SequentialSubModelBase {
470    fn gaps(&self) -> &[Gap] {
471        &self.gaps
472    }
473
474    fn is_obj_at_inf(&self) -> bool {
475        self.gaps
476            .first()
477            .expect("There must be at least one gap in a sequential submodel.")
478            .thickness
479            .is_infinite()
480    }
481
482    fn try_iter<'a>(&'a self, surfaces: &'a [Surface]) -> Result<SequentialSubModelIter<'a>> {
483        SequentialSubModelIter::new(surfaces, &self.gaps)
484    }
485}
486
487impl<'a> SequentialSubModel for SequentialSubModelSlice<'a> {
488    fn gaps(&self) -> &[Gap] {
489        self.gaps
490    }
491
492    fn is_obj_at_inf(&self) -> bool {
493        self.gaps
494            .first()
495            .expect("There must be at least one gap in a sequential submodel.")
496            .thickness
497            .is_infinite()
498    }
499
500    fn try_iter<'b>(&'b self, surfaces: &'b [Surface]) -> Result<SequentialSubModelIter<'b>> {
501        SequentialSubModelIter::new(surfaces, self.gaps)
502    }
503}
504
505impl<'a> SequentialSubModelIter<'a> {
506    fn new(surfaces: &'a [Surface], gaps: &'a [Gap]) -> Result<Self> {
507        if surfaces.len() != gaps.len() + 1 {
508            return Err(anyhow!(
509                "The number of surfaces must be one more than the number of gaps in a forward sequential submodel."
510            ));
511        }
512
513        Ok(Self {
514            surfaces,
515            gaps,
516            index: 0,
517        })
518    }
519
520    pub fn try_reverse(self) -> Result<SequentialSubModelReverseIter<'a>> {
521        SequentialSubModelReverseIter::new(self.surfaces, self.gaps)
522    }
523}
524
525impl<'a> Iterator for SequentialSubModelIter<'a> {
526    type Item = Step<'a>;
527
528    fn next(&mut self) -> Option<Self::Item> {
529        if self.index == self.gaps.len() - 1 {
530            // We are at the image space gap
531            let result = Some((&self.gaps[self.index], &self.surfaces[self.index + 1], None));
532            self.index += 1;
533            result
534        } else if self.index < self.gaps.len() {
535            let result = Some((
536                &self.gaps[self.index],
537                &self.surfaces[self.index + 1],
538                Some(&self.gaps[self.index + 1]),
539            ));
540            self.index += 1;
541            result
542        } else {
543            None
544        }
545    }
546}
547
548impl<'a> ExactSizeIterator for SequentialSubModelIter<'a> {
549    fn len(&self) -> usize {
550        self.gaps.len()
551    }
552}
553
554impl<'a> SequentialSubModelReverseIter<'a> {
555    fn new(surfaces: &'a [Surface], gaps: &'a [Gap]) -> Result<Self> {
556        // Note that this requirement is different than the forward iterator.
557        if surfaces.len() != gaps.len() + 1 {
558            return Err(anyhow!(
559                "The number of surfaces must be one more than the number of gaps in a reversed sequential submodel."
560            ));
561        }
562
563        Ok(Self {
564            surfaces,
565            gaps,
566            // We will never iterate from the image space surface in reverse.
567            index: 1,
568        })
569    }
570}
571
572impl<'a> Iterator for SequentialSubModelReverseIter<'a> {
573    type Item = Step<'a>;
574
575    fn next(&mut self) -> Option<Self::Item> {
576        // Verify index's starting value; it's not necessarily 0.
577        let n = self.gaps.len();
578        let forward_index = n - self.index;
579        if self.index < n {
580            // We are somewhere in the middle of the system or at the object space gap.
581            let result = Some((
582                &self.gaps[forward_index],
583                &self.surfaces[forward_index],
584                Some(&self.gaps[forward_index - 1]),
585            ));
586            self.index += 1;
587            result
588        } else {
589            None
590        }
591    }
592}
593
594impl Surface {
595    /// Returns the z-coordinate of the surface's vertex.
596    pub fn z(&self) -> Float {
597        self.pos().z()
598    }
599
600    pub(crate) fn from_spec(spec: &SurfaceSpec, pos: Vec3) -> Self {
601        // No rotation for the moment
602        let euler_angles = Vec3::new(0.0, 0.0, 0.0);
603        let rotation_matrix =
604            Mat3::from_euler_angles(euler_angles.x(), euler_angles.y(), euler_angles.z());
605
606        match spec {
607            SurfaceSpec::Conic {
608                semi_diameter,
609                radius_of_curvature,
610                conic_constant,
611                surf_type,
612            } => Self::Conic(Conic {
613                pos,
614                rotation_matrix,
615                semi_diameter: *semi_diameter,
616                radius_of_curvature: *radius_of_curvature,
617                conic_constant: *conic_constant,
618                surface_type: *surf_type,
619            }),
620            SurfaceSpec::Image => Self::Image(Image {
621                pos,
622                rotation_matrix,
623            }),
624            SurfaceSpec::Object => Self::Object(Object {
625                pos,
626                rotation_matrix,
627            }),
628            SurfaceSpec::Probe => Self::Probe(Probe {
629                pos,
630                rotation_matrix,
631            }),
632            SurfaceSpec::Stop { semi_diameter } => Self::Stop(Stop {
633                pos,
634                rotation_matrix,
635                semi_diameter: *semi_diameter,
636            }),
637            // SurfaceSpec::Toric {
638            //     semi_diameter,
639            //     radius_of_curvature_vert,
640            //     radius_of_curvature_horz,
641            //     conic_constant,
642            //     surf_type,
643            // } => Self::Toric(Toric {
644            //     pos,
645            //     rotation_matrix,
646            //     semi_diameter: *semi_diameter,
647            //     radius_of_curvature_y: *radius_of_curvature_vert,
648            //     radius_of_curvature_x: *radius_of_curvature_horz,
649            //     conic_constant: *conic_constant,
650            //     surface_type: *surf_type,
651            // }),
652        }
653    }
654
655    /// Determines whether a transverse point is outside the clear aperture of
656    /// the surface.
657    ///
658    /// The axial z-position is ignored.
659    pub(crate) fn outside_clear_aperture(&self, pos: Vec3) -> bool {
660        let r_transv = pos.x() * pos.x() + pos.y() * pos.y();
661        let r_max = self.semi_diameter();
662
663        r_transv > r_max * r_max
664    }
665
666    pub(crate) fn roc(&self, axis: &Axis) -> Float {
667        match axis {
668            Axis::X => self.rocx(),
669            Axis::Y => self.rocy(),
670        }
671    }
672
673    /// The radius of curvature in the horizontal direction.
674    fn rocx(&self) -> Float {
675        match self {
676            Self::Conic(conic) => conic.radius_of_curvature,
677            //Self::Toric(toric) => toric.radius_of_curvature_x,
678            _ => Float::INFINITY,
679        }
680    }
681
682    /// The radius of curvature in the vertical direction.
683    fn rocy(&self) -> Float {
684        match self {
685            Self::Conic(conic) => conic.radius_of_curvature,
686            //Self::Toric(toric) => toric.radius_of_curvature_y,
687            _ => Float::INFINITY,
688        }
689    }
690
691    /// Returns the rotation matrix of the surface into the local coordinate
692    /// system.
693    pub(crate) fn rot_mat(&self) -> Mat3 {
694        match self {
695            Self::Conic(conic) => conic.rotation_matrix,
696            Self::Image(image) => image.rotation_matrix,
697            Self::Object(object) => object.rotation_matrix,
698            Self::Probe(probe) => probe.rotation_matrix,
699            Self::Stop(stop) => stop.rotation_matrix,
700            //Self::Toric(toric) => toric.rotation_matrix,
701        }
702    }
703
704    /// Returns the position of the surface in the global coordinate system.
705    pub(crate) fn pos(&self) -> Vec3 {
706        match self {
707            Self::Conic(conic) => conic.pos,
708            Self::Image(image) => image.pos,
709            Self::Object(object) => object.pos,
710            Self::Probe(probe) => probe.pos,
711            Self::Stop(stop) => stop.pos,
712            //Self::Toric(toric) => toric.pos,
713        }
714    }
715
716    /// Returns the surface sag and normal vector on the surface at a given
717    /// position.
718    ///
719    /// The position is given in the local coordinate system of the surface.
720    pub(crate) fn sag_norm(&self, pos: Vec3) -> (Float, Vec3) {
721        match self {
722            Self::Conic(conic) => conic.sag_norm(pos),
723            // Flat surfaces
724            Self::Image(_) | Self::Object(_) | Self::Probe(_) | Self::Stop(_) => {
725                (0.0, Vec3::new(0.0, 0.0, 1.0))
726            } //Self::Toric(_) => unimplemented!(),
727        }
728    }
729
730    pub(crate) fn semi_diameter(&self) -> Float {
731        match self {
732            Self::Conic(conic) => conic.semi_diameter,
733            //Self::Toric(toric) => toric.semi_diameter,
734            Self::Stop(stop) => stop.semi_diameter,
735            _ => Float::INFINITY,
736        }
737    }
738
739    pub(crate) fn surface_type(&self) -> SurfaceType {
740        match self {
741            Self::Conic(conic) => conic.surface_type,
742            //Self::Toric(toric) => toric.surface_type,
743            _ => SurfaceType::NoOp,
744        }
745    }
746}
747
748impl Display for Surface {
749    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
750        match self {
751            Self::Conic(_) => write!(f, "Conic"),
752            Self::Image(_) => write!(f, "Image"),
753            Self::Object(_) => write!(f, "Object"),
754            Self::Probe(_) => write!(f, "Probe"),
755            Self::Stop(_) => write!(f, "Stop"),
756            //Self::Toric(toric) => write!(f, "Toric surface at z = {}", toric.pos.z()),
757        }
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764    use crate::{
765        core::math::mat3::Mat3,
766        examples::convexplano_lens::sequential_model,
767        specs::gaps::{RealSpec, RefractiveIndexSpec},
768    };
769
770    #[test]
771    fn gaps_must_specify_ri_when_no_wavelengths_provided() {
772        let gaps = vec![
773            GapSpec {
774                thickness: 1.0,
775                refractive_index: RefractiveIndexSpec {
776                    real: RealSpec::Constant(1.0),
777                    imag: None,
778                },
779            },
780            GapSpec {
781                thickness: 1.0,
782                refractive_index: RefractiveIndexSpec {
783                    real: RealSpec::Formula2 {
784                        wavelength_range: [0.3, 0.8],
785                        coefficients: vec![1.0, 2.0, 3.0, 4.0],
786                    },
787                    imag: None,
788                },
789            },
790        ];
791        let wavelengths = Vec::new();
792
793        let result = SequentialModel::validate_gaps(&gaps, &wavelengths);
794        assert!(result.is_err());
795    }
796
797    #[test]
798    fn is_rotationally_symmetric() {
799        let surfaces = vec![
800            Surface::Conic(Conic {
801                pos: Vec3::new(0.0, 0.0, 0.0),
802                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
803                semi_diameter: 1.0,
804                radius_of_curvature: 1.0,
805                conic_constant: 0.0,
806                surface_type: SurfaceType::Refracting,
807            }),
808            Surface::Conic(Conic {
809                pos: Vec3::new(0.0, 0.0, 0.0),
810                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
811                semi_diameter: 1.0,
812                radius_of_curvature: 1.0,
813                conic_constant: 0.0,
814                surface_type: SurfaceType::Refracting,
815            }),
816        ];
817        assert!(SequentialModel::is_rotationally_symmetric(&surfaces));
818
819        // let surfaces = vec![
820        //     Surface::Conic(Conic {
821        //         pos: Vec3::new(0.0, 0.0, 0.0),
822        //         rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
823        // 0.0, 1.0),         semi_diameter: 1.0,
824        //         radius_of_curvature: 1.0,
825        //         conic_constant: 0.0,
826        //         surface_type: SurfaceType::Refracting,
827        //     }),
828        //     Surface::Toric(Toric {
829        //         pos: Vec3::new(0.0, 0.0, 0.0),
830        //         rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
831        // 0.0, 1.0),         semi_diameter: 1.0,
832        //         radius_of_curvature_y: 1.0,
833        //         radius_of_curvature_x: 1.0,
834        //         conic_constant: 0.0,
835        //         surface_type: SurfaceType::Refracting,
836        //     }),
837        // ];
838        // assert!(!SequentialModel::is_rotationally_symmetric(&surfaces));
839    }
840
841    #[test]
842    fn test_calc_model_ids() {
843        let sequential_model = sequential_model();
844        let surfaces = sequential_model.surfaces();
845        let wavelengths = vec![0.4, 0.6];
846
847        let model_ids = SequentialModel::calc_model_ids(surfaces, &wavelengths);
848
849        assert_eq!(model_ids.len(), 2); // Two wavelengths, rotationally
850                                        // symmetric
851    }
852
853    #[test]
854    fn test_calc_model_ids_no_wavelength() {
855        let sequential_model = sequential_model();
856        let surfaces = sequential_model.surfaces();
857        let wavelengths = Vec::new();
858
859        let model_ids = SequentialModel::calc_model_ids(surfaces, &wavelengths);
860
861        assert_eq!(model_ids.len(), 1); // Circularly symmetric, no wavelengths
862    }
863
864    #[test]
865    fn test_first_physical_surface() {
866        let surfaces = vec![
867            Surface::Object(Object {
868                pos: Vec3::new(0.0, 0.0, 0.0),
869                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
870            }),
871            Surface::Probe(Probe {
872                pos: Vec3::new(0.0, 0.0, 0.0),
873                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
874            }),
875            Surface::Conic(Conic {
876                pos: Vec3::new(0.0, 0.0, 0.0),
877                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
878                semi_diameter: 1.0,
879                radius_of_curvature: 1.0,
880                conic_constant: 0.0,
881                surface_type: SurfaceType::Refracting,
882            }),
883            Surface::Conic(Conic {
884                pos: Vec3::new(0.0, 0.0, 0.0),
885                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
886                semi_diameter: 1.0,
887                radius_of_curvature: 1.0,
888                conic_constant: 0.0,
889                surface_type: SurfaceType::Refracting,
890            }),
891            Surface::Image(Image {
892                pos: Vec3::new(0.0, 0.0, 0.0),
893                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
894            }),
895        ];
896
897        let result = first_physical_surface(&surfaces);
898        assert_eq!(result, Some(2));
899    }
900
901    #[test]
902    fn test_last_physical_surface() {
903        let surfaces = vec![
904            Surface::Object(Object {
905                pos: Vec3::new(0.0, 0.0, 0.0),
906                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
907            }),
908            Surface::Conic(Conic {
909                pos: Vec3::new(0.0, 0.0, 0.0),
910                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
911                semi_diameter: 1.0,
912                radius_of_curvature: 1.0,
913                conic_constant: 0.0,
914                surface_type: SurfaceType::Refracting,
915            }),
916            Surface::Conic(Conic {
917                pos: Vec3::new(0.0, 0.0, 0.0),
918                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
919                semi_diameter: 1.0,
920                radius_of_curvature: 1.0,
921                conic_constant: 0.0,
922                surface_type: SurfaceType::Refracting,
923            }),
924            Surface::Probe(Probe {
925                pos: Vec3::new(0.0, 0.0, 0.0),
926                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
927            }),
928            Surface::Image(Image {
929                pos: Vec3::new(0.0, 0.0, 0.0),
930                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
931            }),
932        ];
933
934        let result = last_physical_surface(&surfaces);
935        assert_eq!(result, Some(2));
936    }
937
938    #[test]
939    fn test_reversed_surface_id() {
940        let surfaces = vec![
941            Surface::Object(Object {
942                pos: Vec3::new(0.0, 0.0, 0.0),
943                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
944            }),
945            Surface::Conic(Conic {
946                pos: Vec3::new(0.0, 0.0, 0.0),
947                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
948                semi_diameter: 1.0,
949                radius_of_curvature: 1.0,
950                conic_constant: 0.0,
951                surface_type: SurfaceType::Refracting,
952            }),
953            Surface::Conic(Conic {
954                pos: Vec3::new(0.0, 0.0, 0.0),
955                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
956                semi_diameter: 1.0,
957                radius_of_curvature: 1.0,
958                conic_constant: 0.0,
959                surface_type: SurfaceType::Refracting,
960            }),
961            Surface::Probe(Probe {
962                pos: Vec3::new(0.0, 0.0, 0.0),
963                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
964            }),
965            Surface::Image(Image {
966                pos: Vec3::new(0.0, 0.0, 0.0),
967                rotation_matrix: Mat3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
968            }),
969        ];
970
971        let result = reversed_surface_id(&surfaces, 2);
972        assert_eq!(result, 2);
973
974        let result = reversed_surface_id(&surfaces, 1);
975        assert_eq!(result, 3);
976    }
977}