1use 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
32type ParaxialRays = Array2<Float>;
38
39type ParaxialRaysView<'a> = ArrayView2<'a, Float>;
41
42type ParaxialRayTraceResults = Array3<Float>;
48
49type RayTransferMatrix = Array2<Float>;
51
52#[derive(Debug)]
60pub struct ParaxialView {
61 subviews: HashMap<SubModelID, ParaxialSubView>,
62}
63
64#[derive(Debug, Serialize)]
68pub struct ParaxialViewDescription {
69 subviews: HashMap<SubModelID, ParaxialSubViewDescription>,
70}
71
72#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct Pupil {
120 pub location: Float,
121 pub semi_diameter: Float,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct ImagePlane {
132 pub location: Float,
133 pub semi_diameter: Float,
134}
135
136fn 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
146fn 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
159fn 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 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 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 pub fn subviews(&self) -> &HashMap<SubModelID, ParaxialSubView> {
267 &self.subviews
268 }
269}
270
271impl ParaxialSubView {
272 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, ¶llel_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(¶llel_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 let semi_diameters = Array::from_vec(
409 surfaces
410 .iter()
411 .map(|surface| surface.semi_diameter())
412 .collect::<Vec<Float>>(),
413 );
414
415 let ratios = (semi_diameters
418 / pseudo_marginal_ray[[pseudo_marginal_ray.shape()[0] - 1, 0, 0]])
419 .mapv(|x| x.abs());
420
421 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 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 if delta.is_infinite() {
453 return Ok(Float::NAN);
454 }
455
456 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 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 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 if is_obj_space_telecentric {
515 return Ok(Pupil {
516 location: Float::INFINITY,
517 semi_diameter: Float::NAN,
518 });
519 }
520
521 if *aperture_stop == 1usize {
523 return Ok(Pupil {
524 location: 0.0,
525 semi_diameter: surfaces[1].semi_diameter(),
526 });
527 }
528
529 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 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 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 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 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 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 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 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 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 Float::INFINITY
683 } else {
684 last_surface.z() + dz
685 };
686
687 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 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 arr2(&[[1.0], [0.0]])
708 } else {
709 arr2(&[[0.0], [1.0]])
711 };
712
713 Self::trace(ray, sequential_sub_model, surfaces, &axis, false)
714 }
715
716 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 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 -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 let mut results = Array3::zeros((txs.len() + 1, 2, rays.shape()[1]));
785 results.slice_mut(s![0, .., ..]).assign(&rays);
786
787 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
804fn 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 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#[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}