Skip to main content

nereids_io/
rebin.rs

1//! Energy rebinning: coarsen the TOF/energy axis by integer factor.
2//!
3//! Two modes:
4//! - **Counts**: sum adjacent bins (conserves total counts).
5//! - **Transmission**: average adjacent bins (T = I/I₀ is a ratio,
6//!   so bin-averaging with uniform I₀ gives the correct result).
7
8use ndarray::{Array3, Axis, s};
9
10/// Rebin a 3D counts array along axis 0 by summing groups of `factor` slices.
11///
12/// Remainder slices at the end are summed into the last output bin.
13/// Returns the original array unchanged if `factor < 2`.
14///
15/// **NaN handling**: NaN values propagate through the sum — any NaN in a
16/// group produces NaN in that output bin. This matches ndarray semantics.
17pub fn rebin_counts(data: &Array3<f64>, factor: usize) -> Array3<f64> {
18    if factor < 2 {
19        return data.clone();
20    }
21    let n_old = data.shape()[0];
22    let height = data.shape()[1];
23    let width = data.shape()[2];
24    let n_new = n_old.div_ceil(factor);
25    let mut out = Array3::<f64>::zeros((n_new, height, width));
26    for i in 0..n_new {
27        let start = i * factor;
28        let end = (start + factor).min(n_old);
29        out.slice_mut(s![i, .., ..])
30            .assign(&data.slice(s![start..end, .., ..]).sum_axis(Axis(0)));
31    }
32    out
33}
34
35/// Rebin a 3D transmission array along axis 0 by averaging groups of
36/// `factor` slices.
37///
38/// Transmission T = I/I₀ is a ratio. With uniform I₀ across bins,
39/// the correct rebinned transmission is the arithmetic mean of the
40/// group. **This is an approximation** — real I₀ varies per pixel
41/// and energy bin. When I₀ varies significantly across the group
42/// (e.g., near absorption edges or chopper gaps), the arithmetic
43/// mean biases toward bins with higher I₀.
44///
45/// The correct approach is Σ(C_s) / Σ(C_ob) (sum counts then divide),
46/// but this requires raw counts which are not available at the
47/// transmission stage. Users working with raw event data should
48/// rebin counts before normalization via `rebin_counts`.
49///
50/// Returns the original array unchanged if `factor < 2`.
51///
52/// **NaN handling**: NaN values propagate through the average — any NaN
53/// in a group produces NaN in that output bin. This matches ndarray semantics.
54pub fn rebin_transmission(data: &Array3<f64>, factor: usize) -> Array3<f64> {
55    if factor < 2 {
56        return data.clone();
57    }
58    let n_old = data.shape()[0];
59    let height = data.shape()[1];
60    let width = data.shape()[2];
61    let n_new = n_old.div_ceil(factor);
62    let mut out = Array3::<f64>::zeros((n_new, height, width));
63    for i in 0..n_new {
64        let start = i * factor;
65        let end = (start + factor).min(n_old);
66        let group_len = (end - start) as f64;
67        let sum = data.slice(s![start..end, .., ..]).sum_axis(Axis(0));
68        out.slice_mut(s![i, .., ..]).assign(&(sum / group_len));
69    }
70    out
71}
72
73/// Rebin bin edges by keeping every `factor`-th edge, always including
74/// the final edge.
75///
76/// Input: N+1 edges for N bins.
77/// Output: ceil(N/factor)+1 edges for ceil(N/factor) bins.
78///
79/// Returns the original edges unchanged if `factor < 2`.
80pub fn rebin_edges(edges: &[f64], factor: usize) -> Vec<f64> {
81    if factor < 2 || edges.len() <= 1 {
82        return edges.to_vec();
83    }
84    let mut out: Vec<f64> = edges.iter().step_by(factor).copied().collect();
85    // Always include the final edge
86    if out.last() != edges.last() {
87        out.push(*edges.last().unwrap());
88    }
89    out
90}
91
92/// Rebin bin centers by averaging each group of `factor` centers.
93///
94/// Returns the original centers unchanged if `factor < 2`.
95pub fn rebin_centers(centers: &[f64], factor: usize) -> Vec<f64> {
96    if factor < 2 || centers.is_empty() {
97        return centers.to_vec();
98    }
99    centers
100        .chunks(factor)
101        .map(|chunk| chunk.iter().sum::<f64>() / chunk.len() as f64)
102        .collect()
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use ndarray::Array3;
109
110    #[test]
111    fn counts_conservation() {
112        let data = Array3::from_shape_fn((6, 2, 3), |(t, y, x)| (t + y + x) as f64);
113        let original_sum: f64 = data.sum();
114        let rebinned = rebin_counts(&data, 2);
115        assert_eq!(rebinned.shape(), &[3, 2, 3]);
116        assert!((rebinned.sum() - original_sum).abs() < 1e-10);
117    }
118
119    #[test]
120    fn counts_conservation_remainder() {
121        // 7 bins / 3 = 3 output bins (2+2+3 or ceil division)
122        let data = Array3::from_shape_fn((7, 2, 2), |(t, _y, _x)| t as f64 + 1.0);
123        let original_sum: f64 = data.sum();
124        let rebinned = rebin_counts(&data, 3);
125        assert_eq!(rebinned.shape(), &[3, 2, 2]);
126        assert!((rebinned.sum() - original_sum).abs() < 1e-10);
127    }
128
129    #[test]
130    fn counts_factor_one_is_identity() {
131        let data = Array3::from_shape_fn((5, 2, 2), |(t, y, x)| (t * 10 + y + x) as f64);
132        let rebinned = rebin_counts(&data, 1);
133        assert_eq!(rebinned, data);
134    }
135
136    #[test]
137    fn counts_factor_ge_nbins_single_bin() {
138        let data = Array3::from_shape_fn((4, 2, 2), |(t, _y, _x)| t as f64);
139        let rebinned = rebin_counts(&data, 100);
140        assert_eq!(rebinned.shape(), &[1, 2, 2]);
141        assert!((rebinned.sum() - data.sum()).abs() < 1e-10);
142    }
143
144    #[test]
145    fn transmission_averaging() {
146        // Uniform transmission of 0.5 should stay 0.5 after rebinning
147        let data = Array3::from_elem((6, 2, 2), 0.5);
148        let rebinned = rebin_transmission(&data, 3);
149        assert_eq!(rebinned.shape(), &[2, 2, 2]);
150        for &v in rebinned.iter() {
151            assert!((v - 0.5).abs() < 1e-10);
152        }
153    }
154
155    #[test]
156    fn transmission_averaging_remainder() {
157        // 7 bins / 3: groups of [0,1,2], [3,4,5], [6]
158        // Per-pixel values = bin index → means = 1.0, 4.0, 6.0
159        let data = Array3::from_shape_fn((7, 1, 1), |(t, _y, _x)| t as f64);
160        let rebinned = rebin_transmission(&data, 3);
161        assert_eq!(rebinned.shape(), &[3, 1, 1]);
162        assert!((rebinned[[0, 0, 0]] - 1.0).abs() < 1e-10); // mean(0,1,2)
163        assert!((rebinned[[1, 0, 0]] - 4.0).abs() < 1e-10); // mean(3,4,5)
164        assert!((rebinned[[2, 0, 0]] - 6.0).abs() < 1e-10); // mean(6)
165    }
166
167    #[test]
168    fn edges_even_division() {
169        let edges = vec![0.0, 1.0, 2.0, 3.0, 4.0]; // 4 bins
170        let rebinned = rebin_edges(&edges, 2);
171        assert_eq!(rebinned, vec![0.0, 2.0, 4.0]); // 2 bins
172    }
173
174    #[test]
175    fn edges_remainder() {
176        let edges = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; // 5 bins
177        let rebinned = rebin_edges(&edges, 2);
178        // step_by(2): [0, 2, 4] → last edge 5.0 appended
179        assert_eq!(rebinned, vec![0.0, 2.0, 4.0, 5.0]);
180    }
181
182    #[test]
183    fn edges_factor_one() {
184        let edges = vec![1.0, 2.0, 3.0];
185        assert_eq!(rebin_edges(&edges, 1), edges);
186    }
187
188    #[test]
189    fn centers_averaging() {
190        let centers = vec![1.0, 3.0, 5.0, 7.0]; // 4 bins
191        let rebinned = rebin_centers(&centers, 2);
192        assert_eq!(rebinned, vec![2.0, 6.0]);
193    }
194
195    #[test]
196    fn centers_remainder() {
197        let centers = vec![1.0, 3.0, 5.0]; // 3 bins
198        let rebinned = rebin_centers(&centers, 2);
199        assert_eq!(rebinned, vec![2.0, 5.0]); // mean(1,3)=2, mean(5)=5
200    }
201
202    #[test]
203    fn counts_empty_array() {
204        let data = Array3::<f64>::zeros((0, 2, 2));
205        let rebinned = rebin_counts(&data, 3);
206        assert_eq!(rebinned.shape(), &[0, 2, 2]);
207    }
208
209    #[test]
210    fn counts_factor_zero_returns_clone() {
211        let data = Array3::from_shape_fn((4, 1, 1), |(t, _, _)| t as f64);
212        let rebinned = rebin_counts(&data, 0);
213        assert_eq!(rebinned, data);
214    }
215
216    #[test]
217    fn transmission_factor_zero_returns_clone() {
218        let data = Array3::from_elem((4, 1, 1), 0.5);
219        let rebinned = rebin_transmission(&data, 0);
220        assert_eq!(rebinned, data);
221    }
222
223    #[test]
224    fn counts_nan_propagation() {
225        let mut data = Array3::from_shape_fn((4, 1, 1), |(t, _, _)| t as f64);
226        data[[1, 0, 0]] = f64::NAN; // bin 1 is NaN
227        let rebinned = rebin_counts(&data, 2);
228        // Group [0, NaN] should produce NaN
229        assert!(rebinned[[0, 0, 0]].is_nan());
230        // Group [2, 3] should produce 5.0
231        assert!((rebinned[[1, 0, 0]] - 5.0).abs() < 1e-10);
232    }
233
234    #[test]
235    fn transmission_nan_propagation() {
236        let mut data = Array3::from_shape_fn((4, 1, 1), |(t, _, _)| t as f64);
237        data[[1, 0, 0]] = f64::NAN; // bin 1 is NaN
238        let rebinned = rebin_transmission(&data, 2);
239        // Group [0, NaN] → mean is NaN
240        assert!(rebinned[[0, 0, 0]].is_nan());
241        // Group [2, 3] → mean = 2.5
242        assert!((rebinned[[1, 0, 0]] - 2.5).abs() < 1e-10);
243    }
244
245    #[test]
246    fn edges_factor_zero_returns_clone() {
247        let edges = vec![0.0, 1.0, 2.0, 3.0];
248        assert_eq!(rebin_edges(&edges, 0), edges);
249    }
250
251    #[test]
252    fn centers_factor_zero_returns_clone() {
253        let centers = vec![0.5, 1.5, 2.5];
254        assert_eq!(rebin_centers(&centers, 0), centers);
255    }
256}