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 let shifted = (c_normalized + 90.0).rem_euclid(360.0);
132 if shifted <= 180.0 {
133 (shifted - 90.0).abs()
134 } else {
135 (270.0 - shifted).abs()
136 }
137 }
138 Symmetry::BothPlanes => {
139 let in_first_half = c_normalized <= 180.0;
140 let c_in_half = if in_first_half {
141 c_normalized
142 } else {
143 360.0 - c_normalized
144 };
145 if c_in_half <= 90.0 {
146 c_in_half
147 } else {
148 180.0 - c_in_half
149 }
150 }
151 }
152 }
153
154 fn find_interpolation_index(&self, angles: &[f64], target: f64) -> (usize, f64) {
156 if angles.is_empty() {
157 return (0, 0.0);
158 }
159
160 if target <= angles[0] {
161 return (0, 0.0);
162 }
163
164 if target >= angles[angles.len() - 1] {
165 return (angles.len() - 1, 0.0);
166 }
167
168 for i in 0..angles.len() - 1 {
169 if target >= angles[i] && target <= angles[i + 1] {
170 let fraction = (target - angles[i]) / (angles[i + 1] - angles[i]);
171 return (i, fraction);
172 }
173 }
174
175 (angles.len() - 1, 0.0)
176 }
177
178 fn bilinear_interpolate(&self, ci: usize, cf: f64, gi: usize, gf: f64) -> f64 {
180 let get = |c: usize, g: usize| -> f64 {
181 self.intensities
182 .get(c)
183 .and_then(|row| row.get(g))
184 .copied()
185 .unwrap_or(0.0)
186 };
187
188 let i00 = get(ci, gi);
189 let i01 = get(ci, gi + 1);
190 let i10 = get(ci + 1, gi);
191 let i11 = get(ci + 1, gi + 1);
192
193 let i0 = i00 * (1.0 - gf) + i01 * gf;
195 let i1 = i10 * (1.0 - gf) + i11 * gf;
196
197 i0 * (1.0 - cf) + i1 * cf
198 }
199}
200
201impl From<&Eulumdat> for PhotometricWeb {
202 fn from(ldt: &Eulumdat) -> Self {
203 Self::new(
204 ldt.c_angles.clone(),
205 ldt.g_angles.clone(),
206 ldt.intensities.clone(),
207 ldt.symmetry,
208 )
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn create_test_web() -> PhotometricWeb {
217 PhotometricWeb::new(
218 vec![0.0, 90.0, 180.0, 270.0],
219 vec![0.0, 45.0, 90.0, 135.0, 180.0],
220 vec![
221 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], ],
226 Symmetry::None,
227 )
228 }
229
230 #[test]
231 fn test_sample_exact_angles() {
232 let web = create_test_web();
233
234 let i = web.sample(0.0, 0.0);
236 assert!((i - 100.0).abs() < 0.001);
237
238 let i = web.sample(90.0, 45.0);
239 assert!((i - 70.0).abs() < 0.001);
240
241 let i = web.sample(180.0, 90.0);
242 assert!((i - 30.0).abs() < 0.001);
243 }
244
245 #[test]
246 fn test_sample_interpolated() {
247 let web = PhotometricWeb::new(
248 vec![0.0, 90.0],
249 vec![0.0, 90.0],
250 vec![
251 vec![100.0, 0.0], vec![100.0, 0.0], ],
254 Symmetry::None,
255 );
256
257 let i = web.sample(0.0, 45.0);
259 assert!((i - 50.0).abs() < 0.001);
260 }
261
262 #[test]
263 fn test_sample_normalized() {
264 let web = create_test_web();
265
266 let n = web.sample_normalized(0.0, 0.0);
268 assert!((n - 1.0).abs() < 0.001);
269
270 let n = web.sample_normalized(0.0, 90.0);
272 assert!((n - 0.5).abs() < 0.001);
273 }
274
275 #[test]
276 fn test_symmetry_both_planes() {
277 let web = PhotometricWeb::new(
278 vec![0.0, 45.0, 90.0],
279 vec![0.0, 45.0, 90.0],
280 vec![
281 vec![100.0, 80.0, 50.0], vec![90.0, 70.0, 40.0], vec![80.0, 60.0, 30.0], ],
285 Symmetry::BothPlanes,
286 );
287
288 let i_c0 = web.sample(0.0, 45.0);
290 let i_c180 = web.sample(180.0, 45.0);
291 assert!((i_c0 - i_c180).abs() < 0.001);
292
293 let i_c90 = web.sample(90.0, 45.0);
295 let i_c270 = web.sample(270.0, 45.0);
296 assert!((i_c90 - i_c270).abs() < 0.001);
297 }
298
299 #[test]
300 fn test_from_eulumdat() {
301 let mut ldt = Eulumdat::default();
302 ldt.c_angles = vec![0.0, 90.0];
303 ldt.g_angles = vec![0.0, 90.0];
304 ldt.intensities = vec![vec![100.0, 50.0], vec![80.0, 40.0]];
305 ldt.symmetry = Symmetry::None;
306
307 let web = PhotometricWeb::from(&ldt);
308 assert_eq!(web.max_intensity(), 100.0);
309 assert_eq!(web.c_angles().len(), 2);
310 }
311}