1#[derive(Clone, Copy, Debug, Default, PartialEq)]
8#[repr(C)]
9pub struct Neutron {
10 pub x: f64,
12 pub y: f64,
14 pub tof: u32,
16 pub tot: u16,
18 pub n_hits: u16,
20 pub chip_id: u8,
22 #[doc(hidden)]
24 pub reserved: [u8; 3],
25}
26
27impl Neutron {
28 #[must_use]
30 pub fn new(x: f64, y: f64, tof: u32, tot: u16, n_hits: u16, chip_id: u8) -> Self {
31 Self {
32 x,
33 y,
34 tof,
35 tot,
36 n_hits,
37 chip_id,
38 reserved: [0; 3],
39 }
40 }
41
42 #[inline]
44 #[must_use]
45 pub fn tof_ns(&self) -> f64 {
46 f64::from(self.tof) * 25.0
47 }
48
49 #[inline]
51 #[must_use]
52 pub fn tof_ms(&self) -> f64 {
53 self.tof_ns() / 1_000_000.0
54 }
55
56 #[inline]
58 #[must_use]
59 pub fn pixel_coords(&self, super_res: f64) -> (f64, f64) {
60 (self.x / super_res, self.y / super_res)
61 }
62
63 #[must_use]
65 pub fn cluster_size_category(&self) -> ClusterSize {
66 match self.n_hits {
67 1 => ClusterSize::Single,
68 2..=4 => ClusterSize::Small,
69 5..=10 => ClusterSize::Medium,
70 _ => ClusterSize::Large,
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum ClusterSize {
78 Single,
80 Small,
82 Medium,
84 Large,
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct NeutronStatistics {
91 pub count: usize,
93 pub mean_tof: f64,
95 pub std_tof: f64,
97 pub mean_tot: f64,
99 pub mean_cluster_size: f64,
101 pub single_hit_fraction: f64,
103 pub x_range: (f64, f64),
105 pub y_range: (f64, f64),
107 pub tof_range: (u32, u32),
109}
110
111#[derive(Clone, Debug, Default)]
113pub struct NeutronBatch {
114 pub x: Vec<f64>,
116 pub y: Vec<f64>,
118 pub tof: Vec<u32>,
120 pub tot: Vec<u16>,
122 pub n_hits: Vec<u16>,
124 pub chip_id: Vec<u8>,
126}
127
128impl NeutronBatch {
129 #[must_use]
131 pub fn with_capacity(capacity: usize) -> Self {
132 Self {
133 x: Vec::with_capacity(capacity),
134 y: Vec::with_capacity(capacity),
135 tof: Vec::with_capacity(capacity),
136 tot: Vec::with_capacity(capacity),
137 n_hits: Vec::with_capacity(capacity),
138 chip_id: Vec::with_capacity(capacity),
139 }
140 }
141
142 #[must_use]
144 pub fn len(&self) -> usize {
145 self.x.len()
146 }
147
148 #[must_use]
150 pub fn is_empty(&self) -> bool {
151 self.x.is_empty()
152 }
153
154 pub fn push(&mut self, neutron: Neutron) {
156 self.x.push(neutron.x);
157 self.y.push(neutron.y);
158 self.tof.push(neutron.tof);
159 self.tot.push(neutron.tot);
160 self.n_hits.push(neutron.n_hits);
161 self.chip_id.push(neutron.chip_id);
162 }
163
164 pub fn append(&mut self, other: &NeutronBatch) {
166 self.x.extend_from_slice(&other.x);
167 self.y.extend_from_slice(&other.y);
168 self.tof.extend_from_slice(&other.tof);
169 self.tot.extend_from_slice(&other.tot);
170 self.n_hits.extend_from_slice(&other.n_hits);
171 self.chip_id.extend_from_slice(&other.chip_id);
172 }
173
174 pub fn clear(&mut self) {
176 self.x.clear();
177 self.y.clear();
178 self.tof.clear();
179 self.tot.clear();
180 self.n_hits.clear();
181 self.chip_id.clear();
182 }
183}
184
185impl NeutronStatistics {
186 pub fn from_neutrons(neutrons: &[Neutron]) -> Self {
188 if neutrons.is_empty() {
189 return Self::default();
190 }
191
192 let count = neutrons.len();
193 let count_u32_value = u32::try_from(count).unwrap_or(u32::MAX);
194 let count_as_f64 = f64::from(count_u32_value);
195 let sum_tof: f64 = neutrons.iter().map(|n| f64::from(n.tof)).sum();
196 let mean_tof = sum_tof / count_as_f64;
197
198 let variance: f64 = neutrons
199 .iter()
200 .map(|n| (f64::from(n.tof) - mean_tof).powi(2))
201 .sum::<f64>()
202 / count_as_f64;
203 let std_tof = variance.sqrt();
204
205 let mean_total_tot = neutrons.iter().map(|n| f64::from(n.tot)).sum::<f64>() / count_as_f64;
206 let mean_cluster_size =
207 neutrons.iter().map(|n| f64::from(n.n_hits)).sum::<f64>() / count_as_f64;
208 let single_hit_count =
209 u32::try_from(neutrons.iter().filter(|n| n.n_hits == 1).count()).unwrap_or(u32::MAX);
210 let single_hit_fraction = f64::from(single_hit_count) / count_as_f64;
211
212 let x_min = neutrons.iter().map(|n| n.x).fold(f64::INFINITY, f64::min);
213 let x_max = neutrons
214 .iter()
215 .map(|n| n.x)
216 .fold(f64::NEG_INFINITY, f64::max);
217 let y_min = neutrons.iter().map(|n| n.y).fold(f64::INFINITY, f64::min);
218 let y_max = neutrons
219 .iter()
220 .map(|n| n.y)
221 .fold(f64::NEG_INFINITY, f64::max);
222 let tof_min = neutrons.iter().map(|n| n.tof).min().unwrap_or(0);
223 let tof_max = neutrons.iter().map(|n| n.tof).max().unwrap_or(0);
224
225 Self {
226 count,
227 mean_tof,
228 std_tof,
229 mean_tot: mean_total_tot,
230 mean_cluster_size,
231 single_hit_fraction,
232 x_range: (x_min, x_max),
233 y_range: (y_min, y_max),
234 tof_range: (tof_min, tof_max),
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_neutron_creation() {
245 let neutron = Neutron::new(1024.0, 2048.0, 1000, 150, 5, 0);
246 assert!((neutron.x - 1024.0).abs() < f64::EPSILON);
247 assert!((neutron.y - 2048.0).abs() < f64::EPSILON);
248 assert_eq!(neutron.tof, 1000);
249 assert_eq!(neutron.tot, 150);
250 assert_eq!(neutron.n_hits, 5);
251 }
252
253 #[test]
254 fn test_tof_conversions() {
255 let neutron = Neutron::new(0.0, 0.0, 1000, 0, 1, 0);
256 assert!((neutron.tof_ns() - 25_000.0).abs() < f64::EPSILON);
257 assert!((neutron.tof_ms() - 0.025).abs() < f64::EPSILON);
258 }
259
260 #[test]
261 fn test_pixel_coords() {
262 let neutron = Neutron::new(800.0, 1600.0, 0, 0, 1, 0);
263 let (px, py) = neutron.pixel_coords(8.0);
264 assert!((px - 100.0).abs() < f64::EPSILON);
265 assert!((py - 200.0).abs() < f64::EPSILON);
266 }
267
268 #[test]
269 fn test_cluster_size_category() {
270 assert_eq!(
271 Neutron::new(0.0, 0.0, 0, 0, 1, 0).cluster_size_category(),
272 ClusterSize::Single
273 );
274 assert_eq!(
275 Neutron::new(0.0, 0.0, 0, 0, 3, 0).cluster_size_category(),
276 ClusterSize::Small
277 );
278 assert_eq!(
279 Neutron::new(0.0, 0.0, 0, 0, 7, 0).cluster_size_category(),
280 ClusterSize::Medium
281 );
282 assert_eq!(
283 Neutron::new(0.0, 0.0, 0, 0, 15, 0).cluster_size_category(),
284 ClusterSize::Large
285 );
286 }
287
288 #[test]
289 fn test_statistics() {
290 let neutrons = vec![
291 Neutron::new(100.0, 200.0, 1000, 50, 1, 0),
292 Neutron::new(110.0, 210.0, 1010, 60, 3, 0),
293 Neutron::new(105.0, 205.0, 1005, 55, 2, 0),
294 ];
295 let stats = NeutronStatistics::from_neutrons(&neutrons);
296 assert_eq!(stats.count, 3);
297 assert!((stats.mean_tof - 1005.0).abs() < 0.01);
298 assert!((stats.single_hit_fraction - 1.0 / 3.0).abs() < 0.01);
299 }
300}