1use nalgebra::Vector3;
2use pyo3::prelude::*;
3#[cfg(feature = "stub-gen")]
4use pyo3_stub_gen::derive::*;
5use serde::{Deserialize, Deserializer, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
9pub struct SolidAngleBin {
10 pub theta: AngleBin,
11 pub phi: AngleBin,
12}
13
14impl SolidAngleBin {
15 pub fn new(theta_bin: AngleBin, phi_bin: AngleBin) -> Self {
17 SolidAngleBin {
18 theta: theta_bin,
19 phi: phi_bin,
20 }
21 }
22
23 pub fn solid_angle(&self) -> f32 {
24 2.0 * (self.theta.center).to_radians().sin().abs()
25 * (0.5 * self.theta.width()).to_radians().sin()
26 * self.phi.width().to_radians()
27 }
28
29 pub fn unit_vector(&self) -> Vector3<f32> {
34 let (sin_theta, cos_theta) = self.theta.center.to_radians().sin_cos();
35 let (sin_phi, cos_phi) = self.phi.center.to_radians().sin_cos();
36 Vector3::new(sin_theta * cos_phi, sin_theta * sin_phi, -cos_theta)
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
42pub struct AngleBin {
43 pub min: f32, pub max: f32, pub center: f32, }
47
48impl AngleBin {
49 pub fn new(min: f32, max: f32) -> Self {
51 AngleBin {
52 min,
53 max,
54 center: (min + max) / 2.0,
55 }
56 }
57
58 pub fn from_center_width(center: f32, width: f32) -> Self {
60 AngleBin {
61 min: center - width / 2.0,
62 max: center + width / 2.0,
63 center,
64 }
65 }
66
67 pub fn width(&self) -> f32 {
69 self.max - self.min
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_interval_bins() {
79 let values = vec![0.0, 1.0, 2.0];
80 let spacings = vec![0.5, 0.5];
81 let result = interval_spacings(&values, &spacings);
82 let expected = vec![0.0, 0.5, 1.0, 1.5, 2.0];
83 assert_eq!(result, expected);
84 }
85
86 #[test]
87 #[should_panic]
88 fn test_interval_bins_bad_angle() {
89 let values = vec![0.0, 1.0, 2.0];
90 let spacings = vec![0.3, 0.5];
91 interval_spacings(&values, &spacings);
92 }
93
94 #[test]
95 fn test_simple_bins() {
96 let num_theta = 3;
97 let num_phi = 3;
98 let result = simple_bins(num_theta, num_phi);
99 assert_eq!(result.len(), 9);
101 assert_eq!(result[0].phi.center, 60.0);
103 assert_eq!(result[0].phi.center, 60.0);
104 assert_eq!(result[0].theta.min, 0.0);
106 assert_eq!(result[0].theta.max, 60.0);
107 }
108}
109
110#[derive(Debug, Clone, Serialize, PartialEq)]
111pub enum Scheme {
112 Simple {
113 num_theta: usize,
114 num_phi: usize,
115 delta_theta: f32,
116 delta_phi: f32,
117 },
118 Interval {
119 thetas: Vec<f32>,
120 theta_spacings: Vec<f32>,
121 phis: Vec<f32>,
122 phi_spacings: Vec<f32>,
123 },
124 Custom {
125 bins: Vec<[[f32; 2]; 2]>, file: Option<String>,
127 },
128}
129
130impl<'de> Deserialize<'de> for Scheme {
132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133 where
134 D: Deserializer<'de>,
135 {
136 #[derive(Deserialize)]
137 struct SimpleHelper {
138 num_theta: usize,
139 num_phi: usize,
140 #[serde(default)]
141 delta_theta: Option<f32>,
142 #[serde(default)]
143 delta_phi: Option<f32>,
144 }
145
146 #[derive(Deserialize)]
147 struct IntervalHelper {
148 thetas: Vec<f32>,
149 theta_spacings: Vec<f32>,
150 phis: Vec<f32>,
151 phi_spacings: Vec<f32>,
152 }
153
154 #[derive(Deserialize)]
155 struct CustomHelper {
156 #[serde(default)]
157 bins: Vec<[[f32; 2]; 2]>,
158 file: Option<String>,
159 }
160
161 #[derive(Deserialize)]
162 enum SchemeHelper {
163 Simple(SimpleHelper),
164 Interval(IntervalHelper),
165 Custom(CustomHelper),
166 }
167
168 let helper = SchemeHelper::deserialize(deserializer)?;
169 match helper {
170 SchemeHelper::Simple(SimpleHelper {
171 num_theta,
172 num_phi,
173 delta_theta,
174 delta_phi,
175 }) => {
176 let delta_theta = delta_theta.unwrap_or(180.0 / num_theta as f32);
178 let delta_phi = delta_phi.unwrap_or(360.0 / num_phi as f32);
179 Ok(Scheme::Simple {
180 num_theta,
181 num_phi,
182 delta_theta,
183 delta_phi,
184 })
185 }
186 SchemeHelper::Interval(IntervalHelper {
187 thetas,
188 theta_spacings,
189 phis,
190 phi_spacings,
191 }) => Ok(Scheme::Interval {
192 thetas,
193 theta_spacings,
194 phis,
195 phi_spacings,
196 }),
197 SchemeHelper::Custom(CustomHelper { mut bins, file }) => {
198 if let Some(ref filepath) = file {
200 #[derive(Deserialize)]
201 struct CustomBinsFile {
202 bins: Vec<[[f32; 2]; 2]>,
203 }
204
205 let content = std::fs::read_to_string(filepath).map_err(|e| {
206 serde::de::Error::custom(format!(
207 "Failed to read custom bins file '{}': {}",
208 filepath, e
209 ))
210 })?;
211
212 let file_data: CustomBinsFile = toml::from_str(&content).map_err(|e| {
213 serde::de::Error::custom(format!(
214 "Failed to parse custom bins file '{}': {}",
215 filepath, e
216 ))
217 })?;
218
219 bins = file_data.bins;
220 }
221
222 Ok(Scheme::Custom { bins, file })
223 }
224 }
225 }
226}
227
228impl Scheme {
229 pub fn new_simple(num_theta: usize, num_phi: usize) -> Self {
230 let delta_theta = 180.0 / num_theta as f32;
231 let delta_phi = 360.0 / num_phi as f32;
232 Scheme::Simple {
233 num_theta,
234 num_phi,
235 delta_theta,
236 delta_phi,
237 }
238 }
239
240 pub fn theta_range(&self) -> (f32, f32) {
242 match self {
243 Scheme::Simple { .. } => (0.0, 180.0),
244 Scheme::Interval { thetas, .. } => {
245 let min = thetas.first().copied().unwrap_or(0.0);
246 let max = thetas.last().copied().unwrap_or(180.0);
247 (min, max)
248 }
249 Scheme::Custom { bins, .. } => {
250 if bins.is_empty() {
251 return (0.0, 0.0);
252 }
253 let min = bins.iter().map(|b| b[0][0]).fold(f32::INFINITY, f32::min);
254 let max = bins
255 .iter()
256 .map(|b| b[0][1])
257 .fold(f32::NEG_INFINITY, f32::max);
258 (min, max)
259 }
260 }
261 }
262
263 pub fn generate(&self) -> Vec<SolidAngleBin> {
265 match self {
266 Scheme::Simple {
267 num_theta, num_phi, ..
268 } => simple_bins(*num_theta, *num_phi),
269 Scheme::Interval {
270 thetas,
271 theta_spacings,
272 phis,
273 phi_spacings,
274 } => interval_bins(theta_spacings, thetas, phi_spacings, phis),
275 Scheme::Custom { bins, .. } => custom_bins(bins),
276 }
277 }
278}
279
280#[cfg_attr(feature = "stub-gen", gen_stub_pyclass)]
286#[pyclass(module = "goad._goad")]
287#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
288pub struct BinningScheme {
289 pub scheme: Scheme,
290}
291
292#[cfg_attr(feature = "stub-gen", gen_stub_pymethods)]
293#[pymethods]
294impl BinningScheme {
295 #[new]
296 fn py_new(bins: Vec<[[f32; 2]; 2]>) -> Self {
297 BinningScheme {
298 scheme: Scheme::Custom { bins, file: None },
299 }
300 }
301
302 #[staticmethod]
304 fn simple(num_theta: usize, num_phi: usize) -> PyResult<Self> {
305 if num_theta <= 0 {
306 return Err(pyo3::exceptions::PyValueError::new_err(
307 "num_theta must be greater than 0",
308 ));
309 }
310 if num_phi <= 0 {
311 return Err(pyo3::exceptions::PyValueError::new_err(
312 "num_phi must be greater than 0",
313 ));
314 }
315
316 Ok(BinningScheme {
317 scheme: Scheme::new_simple(num_theta, num_phi),
318 })
319 }
320
321 #[staticmethod]
323 fn interval(
324 thetas: Vec<f32>,
325 theta_spacings: Vec<f32>,
326 phis: Vec<f32>,
327 phi_spacings: Vec<f32>,
328 ) -> Self {
329 BinningScheme {
330 scheme: Scheme::Interval {
331 thetas,
332 theta_spacings,
333 phis,
334 phi_spacings,
335 },
336 }
337 }
338
339 #[staticmethod]
342 fn custom(bins: Vec<[[f32; 2]; 2]>) -> Self {
343 BinningScheme {
344 scheme: Scheme::Custom { bins, file: None },
345 }
346 }
347
348 fn thetas(&self) -> Vec<f32> {
350 let bins = match &self.scheme {
351 Scheme::Simple { num_theta, .. } => simple_spacings(*num_theta, 180.0)
352 .iter()
353 .map(|&bin| bin.center)
354 .collect(),
355 Scheme::Interval {
356 thetas,
357 theta_spacings,
358 ..
359 } => edges_to_bins(interval_spacings(&thetas, &theta_spacings))
360 .iter()
361 .map(|bin| bin.center)
362 .collect(),
363 Scheme::Custom { bins, .. } => custom_bins(&bins)
364 .iter()
365 .map(|bin| bin.theta.center)
366 .collect(),
367 };
368 bins
369 }
370
371 fn phis(&self) -> Vec<f32> {
373 let bins = match &self.scheme {
374 Scheme::Simple { num_phi, .. } => simple_spacings(*num_phi, 360.0)
375 .iter()
376 .map(|&bin| bin.center)
377 .collect(),
378 Scheme::Interval {
379 phis, phi_spacings, ..
380 } => edges_to_bins(interval_spacings(&phis, &phi_spacings))
381 .iter()
382 .map(|bin| bin.center)
383 .collect(),
384 Scheme::Custom { bins, .. } => custom_bins(&bins)
385 .iter()
386 .map(|bin| bin.phi.center)
387 .collect(),
388 };
389 bins
390 }
391
392 fn bins<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2<f32>> {
394 use numpy::IntoPyArray;
395 let solid_bins = self.scheme.generate();
396 let flat: Vec<f32> = solid_bins
397 .iter()
398 .flat_map(|bin| vec![bin.theta.center, bin.phi.center])
399 .collect();
400 ndarray::Array2::from_shape_vec((solid_bins.len(), 2), flat)
401 .unwrap()
402 .into_pyarray(py)
403 }
404
405 fn bins_1d<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1<f32>> {
407 use numpy::IntoPyArray;
408 let thetas = self.thetas();
409 let mut unique_thetas: Vec<f32> = thetas.clone();
411 unique_thetas.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
412 ndarray::Array1::from_vec(unique_thetas).into_pyarray(py)
413 }
414
415 fn num_bins(&self) -> usize {
417 self.scheme.generate().len()
418 }
419}
420
421pub fn interval_spacings(splits: &[f32], spacings: &[f32]) -> Vec<f32> {
422 let num_values = splits.len();
423 let mut values = Vec::new();
424
425 for i in 0..num_values - 1 {
426 let jmax = ((splits[i + 1] - splits[i]) / spacings[i]).round() as usize;
430
431 let remainder = (splits[i + 1] - splits[i]) % spacings[i];
433 if remainder.abs() > 1e-3 && (spacings[i] - remainder).abs() > 1e-3 {
434 panic!(
435 "Invalid spacing: split at index {} (value: {}) to index {} (value: {}) is not an integer multiple of spacing {}. Computed remainder: {}",
436 i,
437 splits[i],
438 i + 1,
439 splits[i + 1],
440 spacings[i],
441 remainder
442 );
443 }
444
445 for j in 0..=jmax {
446 let val = splits[i] + j as f32 * spacings[i];
447
448 if i != num_values - 2 && j == jmax {
450 continue;
452 }
453
454 values.push(val);
455 }
456 }
457
458 values
459}
460
461pub fn interval_bins(
462 theta_spacing: &Vec<f32>,
463 theta_splits: &Vec<f32>,
464 phi_spacing: &Vec<f32>,
465 phi_splits: &Vec<f32>,
466) -> Vec<SolidAngleBin> {
467 let theta_edges = interval_spacings(theta_splits, theta_spacing);
469 let phi_edges = interval_spacings(phi_splits, phi_spacing);
470
471 let theta_bins = edges_to_bins(theta_edges);
473 let phi_bins = edges_to_bins(phi_edges);
474
475 let mut bins = Vec::new();
476 for theta_bin in theta_bins.iter() {
477 for phi_bin in phi_bins.iter() {
478 bins.push(SolidAngleBin::new(*theta_bin, *phi_bin));
479 }
480 }
481
482 bins
483}
484
485fn edges_to_bins(edges: Vec<f32>) -> Vec<AngleBin> {
486 let bins: Vec<AngleBin> = edges
487 .windows(2)
488 .map(|edges| AngleBin::new(edges[0], edges[1]))
489 .collect();
490 bins
491}
492
493fn simple_spacings(num_bins: usize, limit: f32) -> Vec<AngleBin> {
495 let dangle = limit / (num_bins as f32);
496 (0..num_bins)
497 .map(|i| {
498 let min = i as f32 * dangle;
499 let max = (i + 1) as f32 * dangle;
500 AngleBin::new(min, max)
501 })
502 .collect()
503}
504
505pub fn simple_bins(num_theta: usize, num_phi: usize) -> Vec<SolidAngleBin> {
507 let theta_bins = simple_spacings(num_theta, 180.0);
508 let phi_bins = simple_spacings(num_phi, 360.0);
509
510 let mut bins = Vec::new();
512 for theta_bin in theta_bins.iter() {
513 for phi_bin in phi_bins.iter() {
514 bins.push(SolidAngleBin::new(*theta_bin, *phi_bin));
515 }
516 }
517
518 bins
519}
520
521pub fn custom_bins(bin_specs: &[[[f32; 2]; 2]]) -> Vec<SolidAngleBin> {
524 bin_specs
525 .iter()
526 .map(|&[[theta_min, theta_max], [phi_min, phi_max]]| {
527 let theta_bin = AngleBin::new(theta_min, theta_max);
528 let phi_bin = AngleBin::new(phi_min, phi_max);
529 SolidAngleBin::new(theta_bin, phi_bin)
530 })
531 .collect()
532}
533
534pub fn get_n_simple(
551 num_theta: usize,
552 num_phi: usize,
553 delta_theta: f32,
554 delta_phi: f32,
555 theta: f32,
556 phi: f32,
557) -> Option<usize> {
558 let n_theta = ((theta / delta_theta).floor() as usize).min(num_theta - 1);
559 let n_phi = ((phi / delta_phi).floor() as usize).min(num_phi - 1);
560 Some(n_theta * num_phi + n_phi)
561}
562
563pub fn get_n_linear_search(bins: &[SolidAngleBin], theta: f32, phi: f32) -> Option<usize> {
565 let mut bin_idx = None;
567 for (i, bin) in bins.iter().enumerate() {
568 if theta >= bin.theta.min
569 && theta < bin.theta.max
570 && phi >= bin.phi.min
571 && phi < bin.phi.max
572 {
573 bin_idx = Some(i);
574 break;
575 }
576 }
577 bin_idx
578}