1use eulumdat::{Eulumdat, Symmetry};
4
5#[derive(Debug, Clone)]
10pub struct PhotometricWeb {
11 c_angles: Vec<f64>,
13 g_angles: Vec<f64>,
15 intensities: Vec<Vec<f64>>,
17 symmetry: Symmetry,
19 max_intensity: f64,
21 min_intensity: f64,
23}
24
25impl PhotometricWeb {
26 pub fn new(
28 c_angles: Vec<f64>,
29 g_angles: Vec<f64>,
30 intensities: Vec<Vec<f64>>,
31 symmetry: Symmetry,
32 ) -> Self {
33 let max_intensity = intensities
34 .iter()
35 .flat_map(|row| row.iter())
36 .copied()
37 .fold(0.0, f64::max);
38 let min_intensity = intensities
39 .iter()
40 .flat_map(|row| row.iter())
41 .copied()
42 .fold(f64::MAX, f64::min);
43
44 Self {
45 c_angles,
46 g_angles,
47 intensities,
48 symmetry,
49 max_intensity,
50 min_intensity,
51 }
52 }
53
54 pub fn sample(&self, c_angle: f64, g_angle: f64) -> f64 {
66 let c_normalized = c_angle.rem_euclid(360.0);
68 let g_clamped = g_angle.clamp(0.0, 180.0);
70
71 let effective_c = self.apply_symmetry(c_normalized);
73
74 let (ci, cf) = self.find_interpolation_index(&self.c_angles, effective_c);
76 let (gi, gf) = self.find_interpolation_index(&self.g_angles, g_clamped);
77
78 self.bilinear_interpolate(ci, cf, gi, gf)
80 }
81
82 pub fn sample_normalized(&self, c_angle: f64, g_angle: f64) -> f64 {
87 if self.max_intensity <= 0.0 {
88 return 0.0;
89 }
90 self.sample(c_angle, g_angle) / self.max_intensity
91 }
92
93 pub fn max_intensity(&self) -> f64 {
95 self.max_intensity
96 }
97
98 pub fn min_intensity(&self) -> f64 {
100 self.min_intensity
101 }
102
103 pub fn symmetry(&self) -> Symmetry {
105 self.symmetry
106 }
107
108 pub fn c_angles(&self) -> &[f64] {
110 &self.c_angles
111 }
112
113 pub fn g_angles(&self) -> &[f64] {
115 &self.g_angles
116 }
117
118 fn apply_symmetry(&self, c_normalized: f64) -> f64 {
120 match self.symmetry {
121 Symmetry::None => c_normalized,
122 Symmetry::VerticalAxis => 0.0, Symmetry::PlaneC0C180 => {
124 if c_normalized <= 180.0 {
125 c_normalized
126 } else {
127 360.0 - c_normalized
128 }
129 }
130 Symmetry::PlaneC90C270 => {
131 if c_normalized > 180.0 {
135 360.0 - c_normalized } else {
137 c_normalized }
139 }
140 Symmetry::BothPlanes => {
141 let in_first_half = c_normalized <= 180.0;
142 let c_in_half = if in_first_half {
143 c_normalized
144 } else {
145 360.0 - c_normalized
146 };
147 if c_in_half <= 90.0 {
148 c_in_half
149 } else {
150 180.0 - c_in_half
151 }
152 }
153 }
154 }
155
156 fn find_interpolation_index(&self, angles: &[f64], target: f64) -> (usize, f64) {
158 if angles.is_empty() {
159 return (0, 0.0);
160 }
161
162 if target <= angles[0] {
163 return (0, 0.0);
164 }
165
166 if target >= angles[angles.len() - 1] {
167 return (angles.len() - 1, 0.0);
168 }
169
170 for i in 0..angles.len() - 1 {
171 if target >= angles[i] && target <= angles[i + 1] {
172 let fraction = (target - angles[i]) / (angles[i + 1] - angles[i]);
173 return (i, fraction);
174 }
175 }
176
177 (angles.len() - 1, 0.0)
178 }
179
180 fn bilinear_interpolate(&self, ci: usize, cf: f64, gi: usize, gf: f64) -> f64 {
182 let get = |c: usize, g: usize| -> f64 {
183 self.intensities
184 .get(c)
185 .and_then(|row| row.get(g))
186 .copied()
187 .unwrap_or(0.0)
188 };
189
190 let i00 = get(ci, gi);
191 let i01 = get(ci, gi + 1);
192 let i10 = get(ci + 1, gi);
193 let i11 = get(ci + 1, gi + 1);
194
195 let i0 = i00 * (1.0 - gf) + i01 * gf;
197 let i1 = i10 * (1.0 - gf) + i11 * gf;
198
199 i0 * (1.0 - cf) + i1 * cf
200 }
201}
202
203impl From<&Eulumdat> for PhotometricWeb {
204 fn from(ldt: &Eulumdat) -> Self {
205 Self::new(
206 ldt.c_angles.clone(),
207 ldt.g_angles.clone(),
208 ldt.intensities.clone(),
209 ldt.symmetry,
210 )
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 fn create_test_web() -> PhotometricWeb {
219 PhotometricWeb::new(
220 vec![0.0, 90.0, 180.0, 270.0],
221 vec![0.0, 45.0, 90.0, 135.0, 180.0],
222 vec![
223 vec![100.0, 80.0, 50.0, 30.0, 10.0], vec![90.0, 70.0, 40.0, 25.0, 8.0], vec![80.0, 60.0, 30.0, 20.0, 5.0], vec![85.0, 65.0, 35.0, 22.0, 6.0], ],
228 Symmetry::None,
229 )
230 }
231
232 #[test]
233 fn test_sample_exact_angles() {
234 let web = create_test_web();
235
236 let i = web.sample(0.0, 0.0);
238 assert!((i - 100.0).abs() < 0.001);
239
240 let i = web.sample(90.0, 45.0);
241 assert!((i - 70.0).abs() < 0.001);
242
243 let i = web.sample(180.0, 90.0);
244 assert!((i - 30.0).abs() < 0.001);
245 }
246
247 #[test]
248 fn test_sample_interpolated() {
249 let web = PhotometricWeb::new(
250 vec![0.0, 90.0],
251 vec![0.0, 90.0],
252 vec![
253 vec![100.0, 0.0], vec![100.0, 0.0], ],
256 Symmetry::None,
257 );
258
259 let i = web.sample(0.0, 45.0);
261 assert!((i - 50.0).abs() < 0.001);
262 }
263
264 #[test]
265 fn test_sample_normalized() {
266 let web = create_test_web();
267
268 let n = web.sample_normalized(0.0, 0.0);
270 assert!((n - 1.0).abs() < 0.001);
271
272 let n = web.sample_normalized(0.0, 90.0);
274 assert!((n - 0.5).abs() < 0.001);
275 }
276
277 #[test]
278 fn test_symmetry_both_planes() {
279 let web = PhotometricWeb::new(
280 vec![0.0, 45.0, 90.0],
281 vec![0.0, 45.0, 90.0],
282 vec![
283 vec![100.0, 80.0, 50.0], vec![90.0, 70.0, 40.0], vec![80.0, 60.0, 30.0], ],
287 Symmetry::BothPlanes,
288 );
289
290 let i_c0 = web.sample(0.0, 45.0);
292 let i_c180 = web.sample(180.0, 45.0);
293 assert!((i_c0 - i_c180).abs() < 0.001);
294
295 let i_c90 = web.sample(90.0, 45.0);
297 let i_c270 = web.sample(270.0, 45.0);
298 assert!((i_c90 - i_c270).abs() < 0.001);
299 }
300
301 #[test]
302 fn test_from_eulumdat() {
303 let ldt = Eulumdat {
304 c_angles: vec![0.0, 90.0],
305 g_angles: vec![0.0, 90.0],
306 intensities: vec![vec![100.0, 50.0], vec![80.0, 40.0]],
307 symmetry: Symmetry::None,
308 ..Default::default()
309 };
310
311 let web = PhotometricWeb::from(&ldt);
312 assert_eq!(web.max_intensity(), 100.0);
313 assert_eq!(web.c_angles().len(), 2);
314 }
315}