cherry_rs/views/
paraxial.rs

1/// A paraxial view into an optical system.
2///
3/// Paraxial optics is a simplified model of optical systems that assumes that
4/// rays are close to the optic axis and that angles are small. Rays are traced
5/// through the system using ray transfer matrices, which are 2x2 matrices that
6/// describe how rays propagate through and interact with optical surfaces. The
7/// paraxial view is used to compute the paraxial parameters of an optical
8/// system, such as the entrance and exit pupils, the back and front focal
9/// distances, and the effective focal length.
10use std::{borrow::Borrow, collections::HashMap};
11
12use anyhow::{anyhow, Result};
13use ndarray::{arr2, s, Array, Array1, Array2, Array3, ArrayView2};
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    core::{
18        argmin,
19        math::vec3::Vec3,
20        sequential_model::{
21            first_physical_surface, last_physical_surface, reversed_surface_id, Axis,
22            SequentialModel, SequentialSubModel, Step, SubModelID, Surface,
23        },
24        Float,
25    },
26    specs::surfaces::SurfaceType,
27    FieldSpec,
28};
29
30const DEFAULT_THICKNESS: Float = 0.0;
31
32/// A 2 x Nr array of paraxial rays.
33///
34/// Nr is the number of rays. The first column is the height of the ray at the
35/// surface, and the second column is the paraxial angle of the ray at the
36/// surface.
37type ParaxialRays = Array2<Float>;
38
39/// A view into an array of Paraxial rays.
40type ParaxialRaysView<'a> = ArrayView2<'a, Float>;
41
42/// A Ns x 2 x Nr array of paraxial ray trace results.
43///
44/// Ns is the number of surfaces, and Nr is the number of rays. The first
45/// element of the 2nd dimension is the height of the ray at the surface, and
46/// the second element is the angle of the ray at the surface.
47type ParaxialRayTraceResults = Array3<Float>;
48
49/// A 2 x 2 array representing a ray transfer matrix for paraxial rays.
50type RayTransferMatrix = Array2<Float>;
51
52/// A paraxial view into an optical system.
53///
54/// A paraxial view is a set of paraxial subviews that describe the first order
55/// properties of an optical system, such as the entrance and exit pupils, the
56/// back and front focal distances, and the effective focal length.
57///
58/// Subviews are indexed by a pair of submodel IDs.
59#[derive(Debug)]
60pub struct ParaxialView {
61    subviews: HashMap<SubModelID, ParaxialSubView>,
62}
63
64/// A description of a paraxial optical system.
65///
66/// This is used primarily for serialization of data for export.
67#[derive(Debug, Serialize)]
68pub struct ParaxialViewDescription {
69    subviews: HashMap<SubModelID, ParaxialSubViewDescription>,
70}
71
72/// A paraxial subview of an optical system.
73///
74/// A paraxial subview is identified by a single submodel ID that corresponds to
75/// a submodel of a sequential model. It is not created by the user, but rather
76/// by instantiating a new ParaxialView struct.
77#[derive(Debug)]
78pub struct ParaxialSubView {
79    is_obj_space_telecentric: bool,
80
81    aperture_stop: usize,
82    back_focal_distance: Float,
83    back_principal_plane: Float,
84    chief_ray: ParaxialRayTraceResults,
85    effective_focal_length: Float,
86    entrance_pupil: Pupil,
87    exit_pupil: Pupil,
88    front_focal_distance: Float,
89    front_principal_plane: Float,
90    marginal_ray: ParaxialRayTraceResults,
91    paraxial_image_plane: ImagePlane,
92}
93
94/// A paraxial description of a submodel of an optical system.
95///
96/// This is used primarily for serialization of data for export.
97#[derive(Debug, Serialize)]
98pub struct ParaxialSubViewDescription {
99    aperture_stop: usize,
100    back_focal_distance: Float,
101    back_principal_plane: Float,
102    chief_ray: ParaxialRayTraceResults,
103    effective_focal_length: Float,
104    entrance_pupil: Pupil,
105    exit_pupil: Pupil,
106    front_focal_distance: Float,
107    front_principal_plane: Float,
108    marginal_ray: ParaxialRayTraceResults,
109    paraxial_image_plane: ImagePlane,
110}
111
112/// A paraxial entrance or exit pupil.
113///
114/// # Attributes
115/// * `location` - The location of the pupil relative to the first non-object
116///   surface.
117/// * `semi_diameter` - The semi-diameter of the pupil.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct Pupil {
120    pub location: Float,
121    pub semi_diameter: Float,
122}
123
124/// A paraxial image plane.
125///
126/// # Attributes
127/// * `location` - The location of the image plane relative to the first
128///   physical surface
129/// * `semi_diameter` - The semi-diameter of the image plane
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct ImagePlane {
132    pub location: Float,
133    pub semi_diameter: Float,
134}
135
136/// Propagate paraxial rays a distance along the optic axis.
137fn propagate(rays: ParaxialRaysView, distance: Float) -> ParaxialRays {
138    let mut propagated = rays.to_owned();
139    let mut ray_heights = propagated.row_mut(0);
140
141    ray_heights += &(distance * &rays.row(1));
142
143    propagated
144}
145
146/// Compute the z-intercepts of a set of paraxial rays.
147///
148/// This will return an error if any of the z-intercepts are NaNs.
149fn z_intercepts(rays: ParaxialRaysView) -> Result<Array1<Float>> {
150    let results = (-&rays.row(0) / rays.row(1)).to_owned();
151
152    if results.iter().any(|&x| x.is_nan()) {
153        return Err(anyhow!("Some z_intercepts are NaNs"));
154    }
155
156    Ok(results)
157}
158
159/// Compute the maximum field angle given a set of field specs.
160///
161/// The maximum field angle is the maximum absolute value of the paraxial angle.
162///
163/// # Arguments
164/// * `obj_pupil_sepration` - The separation between the object and the entrance
165///   pupil.
166/// * `field_specs` - The field specs.
167///
168/// # Returns
169/// A tuple containing the maximum field angle and the height of the field.
170fn max_field(obj_pupil_sepration: Float, field_specs: &[FieldSpec]) -> (Float, Float) {
171    let mut max_angle = 0.0;
172    let mut max_height = 0.0;
173
174    for field_spec in field_specs {
175        let (height, paraxial_angle) = match field_spec {
176            FieldSpec::Angle {
177                angle,
178                pupil_sampling: _,
179            } => {
180                let paraxial_angle = angle.to_radians().tan();
181                let height = -obj_pupil_sepration * paraxial_angle;
182                (height, paraxial_angle)
183            }
184            FieldSpec::ObjectHeight {
185                height,
186                pupil_sampling: _,
187            } => {
188                let paraxial_angle = -height / obj_pupil_sepration;
189                (*height, paraxial_angle)
190            }
191        };
192
193        if paraxial_angle.abs() > max_angle {
194            max_angle = paraxial_angle.abs();
195            max_height = height;
196        }
197    }
198
199    (max_angle, max_height)
200}
201
202impl ParaxialView {
203    /// Creates a new ParaxialView of a SequentialModel.
204    ///
205    /// # Arguments
206    /// * `sequential_model` - The sequential model to create a paraxial view
207    ///   of.
208    /// * `field_specs` - The field specs of the optical system. These are
209    ///   necessary to compute parameters such as the chief ray.
210    /// * `is_obj_space_telecentric` - Whether the object space is telecentric.
211    ///   This forces the chief ray to be parallel to the optic axis.
212    ///
213    /// # Returns
214    /// A new ParaxialView.
215    pub fn new(
216        sequential_model: &SequentialModel,
217        field_specs: &[FieldSpec],
218        is_obj_space_telecentric: bool,
219    ) -> Result<Self> {
220        let subviews: Result<HashMap<SubModelID, ParaxialSubView>> = sequential_model
221            .submodels()
222            .iter()
223            .map(|(id, submodel)| {
224                let surfaces = sequential_model.surfaces();
225                let axis = id.1;
226                Ok((
227                    *id,
228                    ParaxialSubView::new(
229                        submodel,
230                        surfaces,
231                        axis,
232                        field_specs,
233                        is_obj_space_telecentric,
234                    )?,
235                ))
236            })
237            .collect();
238
239        Ok(Self {
240            subviews: subviews?,
241        })
242    }
243
244    /// Returns a description of the paraxial view.
245    ///
246    /// This is used primarily for serialization of data for export.
247    ///
248    /// # Returns
249    /// A description of the paraxial view.
250    pub fn describe(&self) -> ParaxialViewDescription {
251        ParaxialViewDescription {
252            subviews: self
253                .subviews
254                .iter()
255                .map(|(id, subview)| (*id, subview.describe()))
256                .collect(),
257        }
258    }
259
260    /// Returns the subviews of the paraxial view.
261    ///
262    /// Each subview corresponds to a submodel of the sequential model.
263    ///
264    /// # Returns
265    /// The subviews of the paraxial view.
266    pub fn subviews(&self) -> &HashMap<SubModelID, ParaxialSubView> {
267        &self.subviews
268    }
269}
270
271impl ParaxialSubView {
272    /// Create a new paraxial view of an optical system.
273    fn new(
274        sequential_sub_model: &impl SequentialSubModel,
275        surfaces: &[Surface],
276        axis: Axis,
277        field_specs: &[FieldSpec],
278        is_obj_space_telecentric: bool,
279    ) -> Result<Self> {
280        let pseudo_marginal_ray =
281            Self::calc_pseudo_marginal_ray(sequential_sub_model, surfaces, axis)?;
282        let parallel_ray = Self::calc_parallel_ray(sequential_sub_model, surfaces, axis)?;
283        let reverse_parallel_ray =
284            Self::calc_reverse_parallel_ray(sequential_sub_model, surfaces, axis)?;
285
286        let aperture_stop = Self::calc_aperture_stop(surfaces, &pseudo_marginal_ray);
287        let back_focal_distance = Self::calc_back_focal_distance(surfaces, &parallel_ray)?;
288        let front_focal_distance =
289            Self::calc_front_focal_distance(surfaces, &reverse_parallel_ray)?;
290        let marginal_ray = Self::calc_marginal_ray(surfaces, &pseudo_marginal_ray, &aperture_stop);
291        let entrance_pupil = Self::calc_entrance_pupil(
292            sequential_sub_model,
293            surfaces,
294            is_obj_space_telecentric,
295            &aperture_stop,
296            &axis,
297            &marginal_ray,
298        )?;
299        let exit_pupil = Self::calc_exit_pupil(
300            sequential_sub_model,
301            surfaces,
302            &aperture_stop,
303            &marginal_ray,
304        )?;
305        let effective_focal_length = Self::calc_effective_focal_length(&parallel_ray);
306
307        let back_principal_plane =
308            Self::calc_back_prinicpal_plane(surfaces, back_focal_distance, effective_focal_length)?;
309        let front_principal_plane =
310            Self::calc_front_principal_plane(front_focal_distance, effective_focal_length);
311
312        let chief_ray: ParaxialRayTraceResults = Self::calc_chief_ray(
313            surfaces,
314            sequential_sub_model,
315            &axis,
316            field_specs,
317            &entrance_pupil,
318        )?;
319        let paraxial_image_plane =
320            Self::calc_paraxial_image_plane(surfaces, &marginal_ray, &chief_ray)?;
321
322        Ok(Self {
323            is_obj_space_telecentric,
324
325            aperture_stop,
326            back_focal_distance,
327            back_principal_plane,
328            chief_ray,
329            effective_focal_length,
330            entrance_pupil,
331            exit_pupil,
332            front_focal_distance,
333            front_principal_plane,
334            marginal_ray,
335            paraxial_image_plane,
336        })
337    }
338
339    fn describe(&self) -> ParaxialSubViewDescription {
340        ParaxialSubViewDescription {
341            aperture_stop: self.aperture_stop,
342            back_focal_distance: self.back_focal_distance,
343            back_principal_plane: self.back_principal_plane,
344            chief_ray: self.chief_ray.clone(),
345            effective_focal_length: self.effective_focal_length,
346            entrance_pupil: self.entrance_pupil.clone(),
347            exit_pupil: self.exit_pupil.clone(),
348            front_focal_distance: self.front_focal_distance,
349            front_principal_plane: self.front_principal_plane,
350            marginal_ray: self.marginal_ray.clone(),
351            paraxial_image_plane: self.paraxial_image_plane.clone(),
352        }
353    }
354
355    pub fn aperture_stop(&self) -> &usize {
356        &self.aperture_stop
357    }
358
359    pub fn back_focal_distance(&self) -> &Float {
360        &self.back_focal_distance
361    }
362
363    pub fn back_principal_plane(&self) -> &Float {
364        &self.back_principal_plane
365    }
366
367    pub fn chief_ray(&self) -> &ParaxialRayTraceResults {
368        &self.chief_ray
369    }
370
371    pub fn effective_focal_length(&self) -> &Float {
372        &self.effective_focal_length
373    }
374
375    pub fn entrance_pupil(&self) -> &Pupil {
376        &self.entrance_pupil
377    }
378
379    pub fn exit_pupil(&self) -> &Pupil {
380        &self.exit_pupil
381    }
382
383    pub fn front_focal_distance(&self) -> &Float {
384        &self.front_focal_distance
385    }
386
387    pub fn front_principal_plane(&self) -> &Float {
388        &self.front_principal_plane
389    }
390
391    pub fn is_obj_space_telecentric(&self) -> &bool {
392        &self.is_obj_space_telecentric
393    }
394
395    pub fn marginal_ray(&self) -> &ParaxialRayTraceResults {
396        &self.marginal_ray
397    }
398
399    pub fn paraxial_image_plane(&self) -> &ImagePlane {
400        &self.paraxial_image_plane
401    }
402
403    fn calc_aperture_stop(
404        surfaces: &[Surface],
405        pseudo_marginal_ray: &ParaxialRayTraceResults,
406    ) -> usize {
407        // Get all the semi-diameters of the surfaces and put them in an ndarray.
408        let semi_diameters = Array::from_vec(
409            surfaces
410                .iter()
411                .map(|surface| surface.semi_diameter())
412                .collect::<Vec<Float>>(),
413        );
414
415        // Absolute value is necessary because the pseudo-marginal ray trace
416        // can result in surface intersections that are negative.
417        let ratios = (semi_diameters
418            / pseudo_marginal_ray[[pseudo_marginal_ray.shape()[0] - 1, 0, 0]])
419        .mapv(|x| x.abs());
420
421        // Do not include the object or image surfaces when computing the aperture stop.
422        argmin(&ratios.slice(s![1..(ratios.len() - 1)])) + 1
423    }
424
425    fn calc_back_focal_distance(
426        surfaces: &[Surface],
427        parallel_ray: &ParaxialRayTraceResults,
428    ) -> Result<Float> {
429        let last_physical_surface_index =
430            last_physical_surface(surfaces).ok_or(anyhow!("There are no physical surfaces"))?;
431        let z_intercepts =
432            z_intercepts(parallel_ray.slice(s![last_physical_surface_index, .., ..]))?;
433
434        let bfd = z_intercepts[0];
435
436        // Handle edge case for infinite BFD
437        if bfd.is_infinite() {
438            return Ok(Float::INFINITY);
439        }
440
441        Ok(bfd)
442    }
443
444    fn calc_back_prinicpal_plane(
445        surfaces: &[Surface],
446        back_focal_distance: Float,
447        effective_focal_length: Float,
448    ) -> Result<Float> {
449        let delta = back_focal_distance - effective_focal_length;
450
451        // Principal planes make no sense for lenses without power
452        if delta.is_infinite() {
453            return Ok(Float::NAN);
454        }
455
456        // Find the z position of the last real surface
457        let last_physical_surface_index =
458            last_physical_surface(surfaces).ok_or(anyhow!("There are no physical surfaces"))?;
459        let last_surface_z = surfaces[last_physical_surface_index].z();
460
461        Ok(last_surface_z + delta)
462    }
463
464    /// Computes the paraxial chief ray for a given field.
465    fn calc_chief_ray(
466        surfaces: &[Surface],
467        sequential_sub_model: &impl SequentialSubModel,
468        axis: &Axis,
469        field_specs: &[FieldSpec],
470        entrance_pupil: &Pupil,
471    ) -> Result<ParaxialRayTraceResults> {
472        let enp_loc = entrance_pupil.location;
473        let obj_loc = surfaces.first().ok_or(anyhow!("No surfaces provided"))?.z();
474        let sep = if obj_loc.is_infinite() {
475            0.0
476        } else {
477            enp_loc - obj_loc
478        };
479
480        let (paraxial_angle, height) = max_field(sep, field_specs);
481
482        if paraxial_angle.is_infinite() {
483            return Err(anyhow!(
484                "Cannot compute chief ray from an infinite field angle"
485            ));
486        }
487
488        let initial_ray: ParaxialRays = arr2(&[[height], [paraxial_angle]]);
489        Self::trace(initial_ray, sequential_sub_model, surfaces, axis, false)
490    }
491
492    fn calc_effective_focal_length(parallel_ray: &ParaxialRayTraceResults) -> Float {
493        let y_1 = parallel_ray.slice(s![1, 0, 0]);
494        let u_final = parallel_ray.slice(s![-2, 1, 0]);
495        let efl = -y_1.into_scalar() / u_final.into_scalar();
496
497        // Handle edge case for negatively infinite EFL
498        if efl.is_infinite() {
499            return Float::INFINITY;
500        }
501
502        efl
503    }
504
505    fn calc_entrance_pupil(
506        sequential_sub_model: &impl SequentialSubModel,
507        surfaces: &[Surface],
508        is_obj_space_telecentric: bool,
509        aperture_stop: &usize,
510        axis: &Axis,
511        marginal_ray: &ParaxialRayTraceResults,
512    ) -> Result<Pupil> {
513        // In case the object space is telecentric, the entrance pupil is at infinity.
514        if is_obj_space_telecentric {
515            return Ok(Pupil {
516                location: Float::INFINITY,
517                semi_diameter: Float::NAN,
518            });
519        }
520
521        // In case the aperture stop is the first surface.
522        if *aperture_stop == 1usize {
523            return Ok(Pupil {
524                location: 0.0,
525                semi_diameter: surfaces[1].semi_diameter(),
526            });
527        }
528
529        // Trace a ray from the aperture stop to the object space to determine the
530        // entrance pupil location.
531        let ray = arr2(&[[0.0], [1.0]]);
532        let results = Self::trace(
533            ray,
534            &sequential_sub_model.slice(0..*aperture_stop),
535            &surfaces[0..aperture_stop + 1],
536            axis,
537            true,
538        )?;
539        let location = z_intercepts(results.slice(s![-1, .., ..]))?[0];
540
541        // Propagate the marginal ray to the entrance pupil location to determine its
542        // semi-diameter. I'm not sure, but I think the [0, .., ..1] slice on
543        // the marginal ray is required by the compiler because otherwise the
544        // dimensionality of the slice becomes incompatible with the argument of the
545        // propagate function, i.e. the slice [0, .., 0] has the wrong dimensions.
546        let distance = if sequential_sub_model.is_obj_at_inf() {
547            location
548        } else {
549            sequential_sub_model
550                .gaps()
551                .first()
552                .expect("A submodel should always have at least one gap.")
553                .thickness
554                + location
555        };
556        let init_marginal_ray = marginal_ray.slice(s![0, .., ..1]);
557        let semi_diameter = propagate(init_marginal_ray, distance)[[0, 0]];
558
559        Ok(Pupil {
560            location,
561            semi_diameter,
562        })
563    }
564
565    fn calc_exit_pupil(
566        sequential_sub_model: &impl SequentialSubModel,
567        surfaces: &[Surface],
568        aperture_stop: &usize,
569        marginal_ray: &ParaxialRayTraceResults,
570    ) -> Result<Pupil> {
571        let last_physical_surface_id =
572            last_physical_surface(surfaces).ok_or(anyhow!("There are no physical surfaces"))?;
573        if last_physical_surface_id == *aperture_stop {
574            return Ok(Pupil {
575                location: surfaces[last_physical_surface_id].z(),
576                semi_diameter: surfaces[last_physical_surface_id].semi_diameter(),
577            });
578        }
579
580        // Trace a ray through the aperture stop forwards through the system
581        let ray = arr2(&[[0.0], [1.0]]);
582
583        let results = Self::trace(
584            ray,
585            &sequential_sub_model.slice(*aperture_stop..sequential_sub_model.len()),
586            &surfaces[*aperture_stop..],
587            &Axis::Y,
588            false,
589        )?;
590
591        // Distance is relative to the last physical surface
592        let sliced_last_physical_surface_id = last_physical_surface_id - aperture_stop;
593        let distance = z_intercepts(results.slice(s![sliced_last_physical_surface_id, .., ..]))?[0];
594        let last_physical_surface = surfaces[last_physical_surface_id].borrow();
595        let location = last_physical_surface.z() + distance;
596
597        // Propagate the marginal ray to the exit pupil location and find its height
598        let semi_diameter = propagate(
599            marginal_ray.slice(s![last_physical_surface_id, .., ..]),
600            distance,
601        )[[0, 0]];
602
603        Ok(Pupil {
604            location,
605            semi_diameter,
606        })
607    }
608
609    fn calc_front_focal_distance(
610        surfaces: &[Surface],
611        reverse_parallel_ray: &ParaxialRayTraceResults,
612    ) -> Result<Float> {
613        let first_physical_surface_index =
614            first_physical_surface(surfaces).ok_or(anyhow!("There are no physical surfaces"))?;
615        let index = reversed_surface_id(surfaces, first_physical_surface_index);
616        let z_intercepts = z_intercepts(reverse_parallel_ray.slice(s![index, .., ..]))?;
617
618        let ffd = z_intercepts[0];
619
620        // Handle edge case for infinite FFD
621        if ffd.is_infinite() {
622            return Ok(Float::INFINITY);
623        }
624
625        Ok(ffd)
626    }
627
628    fn calc_front_principal_plane(
629        front_focal_distance: Float,
630        effective_focal_length: Float,
631    ) -> Float {
632        // Principal planes make no sense for lenses without power
633        if front_focal_distance.is_infinite() {
634            return Float::NAN;
635        }
636
637        front_focal_distance + effective_focal_length
638    }
639
640    fn calc_marginal_ray(
641        surfaces: &[Surface],
642        pseudo_marginal_ray: &ParaxialRayTraceResults,
643        aperture_stop: &usize,
644    ) -> ParaxialRayTraceResults {
645        let semi_diameters = Array::from_vec(
646            surfaces
647                .iter()
648                .map(|surface| surface.semi_diameter())
649                .collect::<Vec<Float>>(),
650        );
651        let ratios = semi_diameters / pseudo_marginal_ray.slice(s![.., 0, 0]);
652
653        let scale_factor = ratios[*aperture_stop];
654
655        pseudo_marginal_ray * scale_factor
656    }
657
658    /// Compute the parallel ray.
659    fn calc_parallel_ray(
660        sequential_sub_model: &impl SequentialSubModel,
661        surfaces: &[Surface],
662        axis: Axis,
663    ) -> Result<ParaxialRayTraceResults> {
664        let ray = arr2(&[[1.0], [0.0]]);
665
666        Self::trace(ray, sequential_sub_model, surfaces, &axis, false)
667    }
668
669    /// Compute the paraxial image plane.
670    fn calc_paraxial_image_plane(
671        surfaces: &[Surface],
672        marginal_ray: &ParaxialRayTraceResults,
673        chief_ray: &ParaxialRayTraceResults,
674    ) -> Result<ImagePlane> {
675        let last_physical_surface_id =
676            last_physical_surface(surfaces).ok_or(anyhow!("There are no physical surfaces"))?;
677        let last_surface = surfaces[last_physical_surface_id].borrow();
678
679        let dz = z_intercepts(marginal_ray.slice(s![last_physical_surface_id, .., ..]))?[0];
680        let location = if dz.is_infinite() {
681            // Ensure positive infinity is returned for infinite image planes
682            Float::INFINITY
683        } else {
684            last_surface.z() + dz
685        };
686
687        // Propagate the chief ray from the last physical surface to the image plane to
688        // determine its semi-diameter.
689        let ray = chief_ray.slice(s![last_physical_surface_id, .., ..]);
690        let propagated = propagate(ray, dz);
691        let semi_diameter = propagated[[0, 0]].abs();
692
693        Ok(ImagePlane {
694            location,
695            semi_diameter,
696        })
697    }
698
699    /// Compute the pseudo-marginal ray.
700    fn calc_pseudo_marginal_ray(
701        sequential_sub_model: &impl SequentialSubModel,
702        surfaces: &[Surface],
703        axis: Axis,
704    ) -> Result<ParaxialRayTraceResults> {
705        let ray = if sequential_sub_model.is_obj_at_inf() {
706            // Ray parallel to axis at a height of 1
707            arr2(&[[1.0], [0.0]])
708        } else {
709            // Ray starting from the axis at an angle of 1
710            arr2(&[[0.0], [1.0]])
711        };
712
713        Self::trace(ray, sequential_sub_model, surfaces, &axis, false)
714    }
715
716    /// Compute the reverse parallel ray.
717    fn calc_reverse_parallel_ray(
718        sequential_sub_model: &impl SequentialSubModel,
719        surfaces: &[Surface],
720        axis: Axis,
721    ) -> Result<ParaxialRayTraceResults> {
722        let ray = arr2(&[[1.0], [0.0]]);
723
724        Self::trace(ray, sequential_sub_model, surfaces, &axis, true)
725    }
726
727    /// Compute the ray transfer matrix for each gap/surface pair.
728    fn rtms(
729        sequential_sub_model: &impl SequentialSubModel,
730        surfaces: &[Surface],
731        axis: &Axis,
732        reverse: bool,
733    ) -> Result<Vec<RayTransferMatrix>> {
734        let mut txs: Vec<RayTransferMatrix> = Vec::new();
735        let mut forward_iter;
736        let mut reverse_iter;
737        let steps: &mut dyn Iterator<Item = Step> = if reverse {
738            reverse_iter = sequential_sub_model.try_iter(surfaces)?.try_reverse()?;
739            &mut reverse_iter
740        } else {
741            forward_iter = sequential_sub_model.try_iter(surfaces)?;
742            &mut forward_iter
743        };
744
745        for (gap_0, surface, gap_1) in steps {
746            let t = if gap_0.thickness.is_infinite() {
747                DEFAULT_THICKNESS
748            } else if reverse {
749                // Reverse ray tracing is implemented as negative distances to avoid hassles
750                // with inverses of ray transfer matrices.
751                -gap_0.thickness
752            } else {
753                gap_0.thickness
754            };
755
756            let roc = surface.roc(axis);
757
758            let n_0 = gap_0.refractive_index.n();
759
760            let n_1 = if let Some(gap_1) = gap_1 {
761                gap_1.refractive_index.n()
762            } else {
763                gap_0.refractive_index.n()
764            };
765
766            let rtm = surface_to_rtm(surface, t, roc, n_0, n_1);
767            txs.push(rtm);
768        }
769
770        Ok(txs)
771    }
772
773    fn trace(
774        rays: ParaxialRays,
775        sequential_sub_model: &impl SequentialSubModel,
776        surfaces: &[Surface],
777        axis: &Axis,
778        reverse: bool,
779    ) -> Result<ParaxialRayTraceResults> {
780        let txs = Self::rtms(sequential_sub_model, surfaces, axis, reverse)?;
781
782        // Initialize the results array by assigning the input rays to the first
783        // surface.
784        let mut results = Array3::zeros((txs.len() + 1, 2, rays.shape()[1]));
785        results.slice_mut(s![0, .., ..]).assign(&rays);
786
787        // Iterate over the surfaces and compute the ray trace results.
788        for (i, tx) in txs.iter().enumerate() {
789            let rays = results.slice(s![i, .., ..]);
790            let rays = tx.dot(&rays);
791            results.slice_mut(s![i + 1, .., ..]).assign(&rays);
792        }
793
794        Ok(results)
795    }
796}
797
798impl Pupil {
799    pub fn pos(&self) -> Vec3 {
800        Vec3::new(0.0, 0.0, self.location)
801    }
802}
803
804/// Compute the ray transfer matrix for propagation to and interaction with a
805/// surface.
806fn surface_to_rtm(
807    surface: &Surface,
808    t: Float,
809    roc: Float,
810    n_0: Float,
811    n_1: Float,
812) -> RayTransferMatrix {
813    let surface_type = surface.surface_type();
814
815    match surface {
816        // Conics and torics behave the same in paraxial subviews.
817        //Surface::Conic(_) | Surface::Toric(_) => match surface_type {
818        Surface::Conic(_) => match surface_type {
819            SurfaceType::Refracting => arr2(&[
820                [1.0, t],
821                [
822                    (n_0 - n_1) / n_1 / roc,
823                    t * (n_0 - n_1) / n_1 / roc + n_0 / n_1,
824                ],
825            ]),
826            SurfaceType::Reflecting => arr2(&[[1.0, t], [-2.0 / roc, 1.0 - 2.0 * t / roc]]),
827            SurfaceType::NoOp => panic!("Conics and torics cannot be NoOp surfaces."),
828        },
829        Surface::Image(_) | Surface::Probe(_) | Surface::Stop(_) => arr2(&[[1.0, t], [0.0, 1.0]]),
830        Surface::Object(_) => arr2(&[[1.0, 0.0], [0.0, 1.0]]),
831    }
832}
833
834// Consider moving these to integration tests once the paraxial view and
835// sequential models are combined into a system.
836#[cfg(test)]
837mod test {
838    use approx::assert_abs_diff_eq;
839    use ndarray::{arr1, arr3};
840
841    use crate::core::sequential_model::SubModelID;
842    use crate::examples::convexplano_lens;
843
844    use super::*;
845
846    #[test]
847    fn test_propagate() {
848        let rays = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
849        let propagated = propagate(rays.view(), 2.0);
850
851        let expected = arr2(&[[9.0, 12.0, 15.0], [4.0, 5.0, 6.0]]);
852
853        assert_abs_diff_eq!(propagated, expected, epsilon = 1e-4);
854    }
855
856    #[test]
857    fn test_z_intercepts() {
858        let rays = arr2(&[[1.0, 2.0, 3.0, 0.0], [4.0, 5.0, 6.0, 7.0]]);
859        let z_intercepts = z_intercepts(rays.view()).unwrap();
860
861        let expected = arr1(&[-0.25, -0.4, -0.5, 0.0]);
862
863        assert_abs_diff_eq!(z_intercepts, expected, epsilon = 1e-4);
864    }
865
866    #[test]
867    fn test_z_intercepts_divide_by_zero() {
868        let rays = arr2(&[[1.0], [0.0]]);
869        let z_intercepts = z_intercepts(rays.view()).unwrap();
870
871        assert!(z_intercepts.shape() == [1]);
872        assert!(z_intercepts[0].is_infinite());
873    }
874
875    #[test]
876    fn test_z_intercepts_zero_height_divide_by_zero() {
877        let rays = arr2(&[[0.0], [0.0]]);
878        let z_intercepts = z_intercepts(rays.view());
879
880        assert!(z_intercepts.is_err());
881    }
882
883    fn setup() -> (ParaxialSubView, SequentialModel) {
884        let sequential_model = convexplano_lens::sequential_model();
885        let seq_sub_model = sequential_model
886            .submodels()
887            .get(&SubModelID(Some(0usize), Axis::Y))
888            .expect("Submodel not found.");
889        let field_specs = vec![
890            FieldSpec::Angle {
891                angle: 0.0,
892                pupil_sampling: crate::PupilSampling::SquareGrid { spacing: 0.1 },
893            },
894            FieldSpec::Angle {
895                angle: 5.0,
896                pupil_sampling: crate::PupilSampling::SquareGrid { spacing: 0.1 },
897            },
898        ];
899
900        (
901            ParaxialSubView::new(
902                seq_sub_model,
903                sequential_model.surfaces(),
904                Axis::Y,
905                &field_specs,
906                false,
907            )
908            .unwrap(),
909            sequential_model,
910        )
911    }
912
913    #[test]
914    fn test_aperture_stop() {
915        let (view, _) = setup();
916
917        let aperture_stop = view.aperture_stop();
918        let expected = 1;
919
920        assert_eq!(*aperture_stop, expected);
921    }
922
923    #[test]
924    fn test_entrance_pupil() {
925        let (view, _) = setup();
926
927        let entrance_pupil = view.entrance_pupil();
928        let expected = Pupil {
929            location: 0.0,
930            semi_diameter: 12.5,
931        };
932
933        assert_abs_diff_eq!(entrance_pupil.location, expected.location, epsilon = 1e-4);
934        assert_abs_diff_eq!(
935            entrance_pupil.semi_diameter,
936            expected.semi_diameter,
937            epsilon = 1e-4
938        );
939    }
940
941    #[test]
942    fn test_marginal_ray() {
943        let (view, _) = setup();
944
945        let marginal_ray = view.marginal_ray();
946        let expected = arr3(&[
947            [[12.5000], [0.0]],
948            [[12.5000], [-0.1647]],
949            [[11.6271], [-0.2495]],
950            [[-0.0003], [-0.2495]],
951        ]);
952
953        assert_abs_diff_eq!(*marginal_ray, expected, epsilon = 1e-4);
954    }
955
956    #[test]
957    fn test_pseudo_marginal_ray() {
958        let sequential_model = convexplano_lens::sequential_model();
959        let seq_sub_model = sequential_model
960            .submodels()
961            .get(&SubModelID(Some(0usize), Axis::Y))
962            .expect("Submodel not found.");
963        let pseudo_marginal_ray = ParaxialSubView::calc_pseudo_marginal_ray(
964            seq_sub_model,
965            sequential_model.surfaces(),
966            Axis::Y,
967        )
968        .unwrap();
969
970        let expected = arr3(&[
971            [[1.0000], [0.0]],
972            [[1.0000], [-0.0132]],
973            [[0.9302], [-0.0200]],
974            [[0.0], [-0.0200]],
975        ]);
976
977        assert_abs_diff_eq!(pseudo_marginal_ray, expected, epsilon = 1e-4);
978    }
979
980    #[test]
981    fn test_reverse_parallel_ray() {
982        let sequential_model = convexplano_lens::sequential_model();
983        let seq_sub_model = sequential_model
984            .submodels()
985            .get(&SubModelID(Some(0usize), Axis::Y))
986            .expect("Submodel not found.");
987        let reverse_parallel_ray = ParaxialSubView::calc_reverse_parallel_ray(
988            seq_sub_model,
989            sequential_model.surfaces(),
990            Axis::Y,
991        )
992        .unwrap();
993
994        let expected = arr3(&[[[1.0000], [0.0]], [[1.0000], [0.0]], [[1.0000], [0.0200]]]);
995
996        assert_abs_diff_eq!(reverse_parallel_ray, expected, epsilon = 1e-4);
997    }
998}