cherry_rs/specs/
fields.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4use crate::core::Float;
5
6/// Specifies a pupil sampling method.
7#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
8pub enum PupilSampling {
9    /// A square grid of rays in the the entrance pupil.
10    ///
11    /// Spacing is the spacing between rays in the grid in normalized pupil
12    /// distances, i.e. [0, 1]. A spacing of 1.0 means that one ray will lie
13    /// at the pupil center (the chief ray), and the others will lie at the
14    /// pupil edge (marginal rays).
15    SquareGrid { spacing: Float },
16
17    /// The chief and marginal rays.
18    ChiefAndMarginalRays,
19}
20
21/// Specifies an object field.
22#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
23pub enum FieldSpec {
24    /// The angle the field makes with the optical axis, in degrees.
25    Angle {
26        angle: Float,
27        pupil_sampling: PupilSampling,
28    },
29
30    /// The height of the field above the optical axis.
31    ObjectHeight {
32        height: Float,
33        pupil_sampling: PupilSampling,
34    },
35}
36
37impl PupilSampling {
38    /// Validate the pupil sampling method.
39    pub fn validate(&self) -> Result<()> {
40        match self {
41            PupilSampling::SquareGrid { spacing } => {
42                if spacing.is_nan() {
43                    anyhow::bail!("Pupil grid spacing must be a number");
44                }
45                if *spacing < 0.0 || *spacing > 1.0 {
46                    anyhow::bail!("Pupil grid spacing must be in the range [0, 1]");
47                }
48            }
49            PupilSampling::ChiefAndMarginalRays => {}
50        }
51        Ok(())
52    }
53}
54
55impl Default for PupilSampling {
56    fn default() -> Self {
57        Self::SquareGrid { spacing: 0.1 }
58    }
59}
60
61impl FieldSpec {
62    /// Validate the field specification.
63    pub fn validate(&self) -> Result<()> {
64        match self {
65            FieldSpec::Angle {
66                angle,
67                pupil_sampling,
68            } => {
69                if angle.is_nan() {
70                    anyhow::bail!("Field angle must be a number");
71                }
72                if *angle < -90.0 || *angle > 90.0 {
73                    anyhow::bail!("Field angle must be in the range [-90.0, 90]");
74                }
75                pupil_sampling.validate()?;
76            }
77            FieldSpec::ObjectHeight {
78                height,
79                pupil_sampling,
80            } => {
81                if height.is_nan() {
82                    anyhow::bail!("Field height must be a number");
83                }
84                pupil_sampling.validate()?;
85            }
86        }
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod test {
93    use super::*;
94
95    #[test]
96    fn test_pupil_sampling_validate() {
97        let square_grid = PupilSampling::SquareGrid { spacing: 0.1 };
98        assert!(square_grid.validate().is_ok());
99
100        let square_grid = PupilSampling::SquareGrid { spacing: 1.1 };
101        assert!(square_grid.validate().is_err());
102
103        let square_grid = PupilSampling::SquareGrid { spacing: -0.1 };
104        assert!(square_grid.validate().is_err());
105
106        let square_grid = PupilSampling::SquareGrid {
107            spacing: Float::NAN,
108        };
109        assert!(square_grid.validate().is_err());
110    }
111
112    #[test]
113    fn test_field_spec_validate() {
114        let angle = FieldSpec::Angle {
115            angle: 45.0,
116            pupil_sampling: PupilSampling::SquareGrid { spacing: 0.1 },
117        };
118        assert!(angle.validate().is_ok());
119
120        let angle = FieldSpec::Angle {
121            angle: 95.0,
122            pupil_sampling: PupilSampling::SquareGrid { spacing: 0.1 },
123        };
124        assert!(angle.validate().is_err());
125
126        let angle = FieldSpec::Angle {
127            angle: -5.0,
128            pupil_sampling: PupilSampling::SquareGrid { spacing: 0.1 },
129        };
130        assert!(angle.validate().is_ok());
131
132        let angle = FieldSpec::Angle {
133            angle: 45.0,
134            pupil_sampling: PupilSampling::SquareGrid { spacing: 1.1 },
135        };
136        assert!(angle.validate().is_err());
137
138        let angle = FieldSpec::Angle {
139            angle: 45.0,
140            pupil_sampling: PupilSampling::SquareGrid { spacing: -0.1 },
141        };
142        assert!(angle.validate().is_err());
143
144        let angle = FieldSpec::Angle {
145            angle: 45.0,
146            pupil_sampling: PupilSampling::SquareGrid {
147                spacing: Float::NAN,
148            },
149        };
150        assert!(angle.validate().is_err());
151
152        let height = FieldSpec::ObjectHeight {
153            height: 0.1,
154            pupil_sampling: PupilSampling::SquareGrid { spacing: 0.1 },
155        };
156        assert!(height.validate().is_ok());
157
158        let height = FieldSpec::ObjectHeight {
159            height: 0.1,
160            pupil_sampling: PupilSampling::SquareGrid { spacing: 1.1 },
161        };
162        assert!(height.validate().is_err());
163    }
164}