eulumdat_goniosim/
detector.rs1use nalgebra::Vector3;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone)]
12pub struct Detector {
13 bins: Vec<Vec<f64>>,
15 counts: Vec<Vec<u64>>,
17 c_resolution_deg: f64,
19 g_resolution_deg: f64,
21 num_c: usize,
23 num_g: usize,
25 total_energy: f64,
27}
28
29impl Detector {
30 pub fn new(c_resolution_deg: f64, g_resolution_deg: f64) -> Self {
32 let num_c = (360.0 / c_resolution_deg).round() as usize;
33 let num_g = (180.0 / g_resolution_deg).round() as usize + 1;
34 Self {
35 bins: vec![vec![0.0; num_g]; num_c],
36 counts: vec![vec![0; num_g]; num_c],
37 c_resolution_deg,
38 g_resolution_deg,
39 num_c,
40 num_g,
41 total_energy: 0.0,
42 }
43 }
44
45 pub fn record(&mut self, direction: &Vector3<f64>, energy: f64) {
47 let (c, g) = direction_to_cg(direction);
48 let ci = ((c / self.c_resolution_deg).floor() as usize).min(self.num_c - 1);
49 let gi = ((g / self.g_resolution_deg).round() as usize).min(self.num_g - 1);
50 self.bins[ci][gi] += energy;
51 self.counts[ci][gi] += 1;
52 self.total_energy += energy;
53 }
54
55 #[allow(clippy::needless_range_loop)]
59 pub fn to_candela(&self, source_flux_lm: f64) -> Vec<Vec<f64>> {
60 if self.total_energy <= 0.0 {
61 return self.bins.clone();
62 }
63
64 let dc_rad = self.c_resolution_deg.to_radians();
65 let dg_rad = self.g_resolution_deg.to_radians();
66 let flux_per_energy = source_flux_lm / self.total_energy;
67
68 let mut candela = vec![vec![0.0; self.num_g]; self.num_c];
69
70 for ci in 0..self.num_c {
71 for gi in 0..self.num_g {
72 let g_center_rad = (gi as f64 * self.g_resolution_deg).to_radians();
73 let solid_angle = solid_angle_for_bin(g_center_rad, dg_rad, dc_rad);
74
75 if solid_angle > 0.0 {
76 let flux_in_bin = self.bins[ci][gi] * flux_per_energy;
80 candela[ci][gi] = flux_in_bin / solid_angle;
81 }
82 }
83 }
84
85 candela
86 }
87
88 pub fn total_flux(&self, source_flux_lm: f64) -> f64 {
90 if self.total_energy <= 0.0 {
91 return 0.0;
92 }
93 self.total_energy * (source_flux_lm / self.total_energy)
94 }
95
96 pub fn total_energy(&self) -> f64 {
98 self.total_energy
99 }
100
101 pub fn num_c(&self) -> usize {
103 self.num_c
104 }
105
106 pub fn num_g(&self) -> usize {
108 self.num_g
109 }
110
111 pub fn c_resolution_deg(&self) -> f64 {
113 self.c_resolution_deg
114 }
115
116 pub fn g_resolution_deg(&self) -> f64 {
118 self.g_resolution_deg
119 }
120
121 pub fn bins(&self) -> &Vec<Vec<f64>> {
123 &self.bins
124 }
125
126 pub fn counts(&self) -> &Vec<Vec<u64>> {
128 &self.counts
129 }
130
131 pub fn merge(&mut self, other: &Detector) {
133 assert_eq!(self.num_c, other.num_c);
134 assert_eq!(self.num_g, other.num_g);
135 for ci in 0..self.num_c {
136 for gi in 0..self.num_g {
137 self.bins[ci][gi] += other.bins[ci][gi];
138 self.counts[ci][gi] += other.counts[ci][gi];
139 }
140 }
141 self.total_energy += other.total_energy;
142 }
143
144 pub fn candela_at(&self, c_deg: f64, g_deg: f64, source_flux_lm: f64) -> f64 {
150 if self.total_energy <= 0.0 {
151 return 0.0;
152 }
153
154 let flux_per_energy = source_flux_lm / self.total_energy;
155 let dc_rad = self.c_resolution_deg.to_radians();
156 let dg_rad = self.g_resolution_deg.to_radians();
157
158 let c_norm = c_deg.rem_euclid(360.0);
160 let ci_f = c_norm / self.c_resolution_deg;
161 let gi_f = g_deg / self.g_resolution_deg;
162
163 let ci0 = (ci_f.floor() as usize).min(self.num_c - 1);
164 let ci1 = (ci0 + 1) % self.num_c;
165 let gi0 = (gi_f.floor() as usize).min(self.num_g - 1);
166 let gi1 = (gi0 + 1).min(self.num_g - 1);
167
168 let cf = ci_f - ci_f.floor(); let gf = gi_f - gi_f.floor();
170
171 let cd = |ci: usize, gi: usize| -> f64 {
173 let g_center_rad = (gi as f64 * self.g_resolution_deg).to_radians();
174 let sa = solid_angle_for_bin(g_center_rad, dg_rad, dc_rad);
175 if sa > 0.0 {
176 self.bins[ci][gi] * flux_per_energy / sa
177 } else {
178 0.0
179 }
180 };
181
182 let v00 = cd(ci0, gi0);
184 let v10 = cd(ci1, gi0);
185 let v01 = cd(ci0, gi1);
186 let v11 = cd(ci1, gi1);
187
188 let v0 = v00 * (1.0 - cf) + v10 * cf;
189 let v1 = v01 * (1.0 - cf) + v11 * cf;
190 v0 * (1.0 - gf) + v1 * gf
191 }
192
193 pub fn resample(&self, c_step_deg: f64, g_step_deg: f64) -> Detector {
195 let mut resampled = Detector::new(c_step_deg, g_step_deg);
196
197 for ci in 0..self.num_c {
199 let c_angle = ci as f64 * self.c_resolution_deg;
200 let new_ci = ((c_angle / c_step_deg).floor() as usize).min(resampled.num_c - 1);
201
202 for gi in 0..self.num_g {
203 let g_angle = gi as f64 * self.g_resolution_deg;
204 let new_gi = ((g_angle / g_step_deg).round() as usize).min(resampled.num_g - 1);
205
206 resampled.bins[new_ci][new_gi] += self.bins[ci][gi];
207 resampled.counts[new_ci][new_gi] += self.counts[ci][gi];
208 }
209 }
210 resampled.total_energy = self.total_energy;
211
212 resampled
213 }
214}
215
216fn direction_to_cg(dir: &Vector3<f64>) -> (f64, f64) {
228 let gamma = (-dir.z).acos().to_degrees();
230
231 let c = dir.y.atan2(dir.x).to_degrees();
233 let c = if c < 0.0 { c + 360.0 } else { c };
234
235 (c, gamma)
236}
237
238fn solid_angle_for_bin(g_center_rad: f64, dg_rad: f64, dc_rad: f64) -> f64 {
243 let g_lo = (g_center_rad - dg_rad / 2.0).max(0.0);
244 let g_hi = (g_center_rad + dg_rad / 2.0).min(PI);
245 let d_cos = (g_lo.cos() - g_hi.cos()).abs();
246 dc_rad * d_cos
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn direction_to_cg_nadir() {
255 let (c, g) = direction_to_cg(&Vector3::new(0.0, 0.0, -1.0));
256 assert!(g.abs() < 1.0, "Nadir should be gamma ~0, got {g}");
257 let _ = c; }
259
260 #[test]
261 fn direction_to_cg_zenith() {
262 let (_, g) = direction_to_cg(&Vector3::new(0.0, 0.0, 1.0));
263 assert!(
264 (g - 180.0).abs() < 1.0,
265 "Zenith should be gamma ~180, got {g}"
266 );
267 }
268
269 #[test]
270 fn direction_to_cg_horizontal_front() {
271 let (c, g) = direction_to_cg(&Vector3::new(1.0, 0.0, 0.0));
272 assert!(
273 (g - 90.0).abs() < 1.0,
274 "Horizontal should be gamma ~90, got {g}"
275 );
276 assert!(c.abs() < 1.0 || (c - 360.0).abs() < 1.0, "C0 = +X, got {c}");
277 }
278
279 #[test]
280 fn direction_to_cg_horizontal_right() {
281 let (c, g) = direction_to_cg(&Vector3::new(0.0, 1.0, 0.0));
282 assert!((g - 90.0).abs() < 1.0);
283 assert!((c - 90.0).abs() < 1.0, "C90 = +Y, got {c}");
284 }
285
286 #[test]
287 fn detector_records_and_merges() {
288 let mut d1 = Detector::new(10.0, 5.0);
289 let mut d2 = Detector::new(10.0, 5.0);
290
291 d1.record(&Vector3::new(0.0, 0.0, -1.0), 1.0);
292 d2.record(&Vector3::new(0.0, 0.0, -1.0), 2.0);
293
294 d1.merge(&d2);
295 assert!((d1.total_energy - 3.0).abs() < 1e-10);
296 }
297
298 #[test]
299 fn solid_angle_equator_larger_than_pole() {
300 let dg = 1.0f64.to_radians();
301 let dc = 1.0f64.to_radians();
302 let sa_equator = solid_angle_for_bin(90.0f64.to_radians(), dg, dc);
303 let sa_pole = solid_angle_for_bin(0.0, dg, dc);
304 assert!(sa_equator > sa_pole);
305 }
306}