1use ndarray::Array1;
11use nereids_core::constants;
12
13use crate::error::IoError;
14
15#[derive(Debug, Clone, Copy)]
17pub struct BeamlineParams {
18 pub flight_path_m: f64,
20 pub delay_us: f64,
22}
23
24impl Default for BeamlineParams {
25 fn default() -> Self {
26 Self {
27 flight_path_m: 25.0, delay_us: 0.0,
29 }
30 }
31}
32
33pub fn tof_edges_to_energy(
45 tof_edges: &[f64],
46 params: &BeamlineParams,
47) -> Result<Array1<f64>, IoError> {
48 if tof_edges.len() < 2 {
49 return Err(IoError::InvalidParameter(
50 "Need at least 2 TOF bin edges".into(),
51 ));
52 }
53
54 if !params.flight_path_m.is_finite() || params.flight_path_m <= 0.0 {
55 return Err(IoError::InvalidParameter(
56 "Flight path must be finite and positive".into(),
57 ));
58 }
59
60 if !params.delay_us.is_finite() {
61 return Err(IoError::InvalidParameter(
62 "Delay time must be finite".into(),
63 ));
64 }
65
66 let mut energies: Vec<f64> = Vec::with_capacity(tof_edges.len());
67 for (i, &tof) in tof_edges.iter().enumerate() {
68 if !tof.is_finite() {
69 return Err(IoError::InvalidParameter(format!(
70 "TOF edge is non-finite ({}) at index {}",
71 tof, i
72 )));
73 }
74 let corrected_tof = tof - params.delay_us;
75 if !corrected_tof.is_finite() || corrected_tof <= 0.0 {
76 return Err(IoError::InvalidParameter(format!(
77 "TOF edge {:.6} us minus delay {:.6} us is non-positive or non-finite at index {}",
78 tof, params.delay_us, i
79 )));
80 }
81 energies.push(constants::tof_to_energy(
82 corrected_tof,
83 params.flight_path_m,
84 ));
85 }
86
87 energies.reverse();
89 Ok(Array1::from_vec(energies))
90}
91
92pub fn tof_edges_to_energy_centers(
104 tof_edges: &[f64],
105 params: &BeamlineParams,
106) -> Result<Array1<f64>, IoError> {
107 let edges = tof_edges_to_energy(tof_edges, params)?;
108 let n = edges.len() - 1;
109 let centers: Vec<f64> = (0..n).map(|i| (edges[i] * edges[i + 1]).sqrt()).collect();
110 Ok(Array1::from_vec(centers))
111}
112
113pub fn linspace_tof_edges(tof_min: f64, tof_max: f64, n_bins: usize) -> Result<Vec<f64>, IoError> {
127 if n_bins == 0 {
128 return Err(IoError::InvalidParameter("n_bins must be positive".into()));
129 }
130 if !tof_min.is_finite() || !tof_max.is_finite() {
131 return Err(IoError::InvalidParameter(
132 "TOF bounds must be finite".into(),
133 ));
134 }
135 if tof_min < 0.0 {
136 return Err(IoError::InvalidParameter(format!(
137 "tof_min ({}) must be non-negative",
138 tof_min
139 )));
140 }
141 if tof_max <= tof_min {
142 return Err(IoError::InvalidParameter(format!(
143 "tof_max ({}) must be greater than tof_min ({})",
144 tof_max, tof_min
145 )));
146 }
147 let dt = (tof_max - tof_min) / n_bins as f64;
148 Ok((0..=n_bins).map(|i| tof_min + i as f64 * dt).collect())
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_tof_to_energy_roundtrip() {
157 let params = BeamlineParams {
158 flight_path_m: 25.0,
159 delay_us: 0.0,
160 };
161
162 let e_low = 1.0;
164 let e_high = 100.0;
165 let tof_high = constants::energy_to_tof(e_low, params.flight_path_m);
166 let tof_low = constants::energy_to_tof(e_high, params.flight_path_m);
167
168 let tof_edges = linspace_tof_edges(tof_low, tof_high, 100).unwrap();
169 let energy_edges = tof_edges_to_energy(&tof_edges, ¶ms).unwrap();
170
171 assert!(
173 (energy_edges[0] - e_low).abs() / e_low < 0.01,
174 "First energy {} != expected {}",
175 energy_edges[0],
176 e_low,
177 );
178
179 let last = energy_edges[energy_edges.len() - 1];
180 assert!(
181 (last - e_high).abs() / e_high < 0.01,
182 "Last energy {} != expected {}",
183 last,
184 e_high,
185 );
186
187 for i in 1..energy_edges.len() {
192 assert!(
193 energy_edges[i] >= energy_edges[i - 1],
194 "Energies not ascending at index {}: {} < {}",
195 i,
196 energy_edges[i],
197 energy_edges[i - 1],
198 );
199 }
200 }
201
202 #[test]
203 fn test_tof_to_energy_with_delay() {
204 let params_no_delay = BeamlineParams {
205 flight_path_m: 25.0,
206 delay_us: 0.0,
207 };
208 let params_with_delay = BeamlineParams {
209 flight_path_m: 25.0,
210 delay_us: 10.0,
211 };
212
213 let tof_edges = vec![100.0, 200.0, 300.0];
214
215 let e_no_delay = tof_edges_to_energy(&tof_edges, ¶ms_no_delay).unwrap();
216 let e_with_delay = tof_edges_to_energy(&tof_edges, ¶ms_with_delay).unwrap();
217
218 for i in 0..e_no_delay.len() {
220 assert!(
221 e_with_delay[i] >= e_no_delay[i],
222 "Delayed energy should be >= no-delay at index {}",
223 i,
224 );
225 }
226 }
227
228 #[test]
229 fn test_energy_centers_geometric_mean() {
230 let params = BeamlineParams {
231 flight_path_m: 25.0,
232 delay_us: 0.0,
233 };
234 let tof_edges = vec![100.0, 200.0, 300.0, 400.0];
235 let edges = tof_edges_to_energy(&tof_edges, ¶ms).unwrap();
236 let centers = tof_edges_to_energy_centers(&tof_edges, ¶ms).unwrap();
237
238 assert_eq!(centers.len(), edges.len() - 1);
239 for i in 0..centers.len() {
240 let expected = (edges[i] * edges[i + 1]).sqrt();
241 assert!(
242 (centers[i] - expected).abs() < 1e-10,
243 "Center[{}] = {}, expected {}",
244 i,
245 centers[i],
246 expected,
247 );
248 }
249 }
250
251 #[test]
252 fn test_linspace_tof_edges() {
253 let edges = linspace_tof_edges(100.0, 200.0, 5).unwrap();
254 assert_eq!(edges.len(), 6);
255 assert!((edges[0] - 100.0).abs() < 1e-10);
256 assert!((edges[5] - 200.0).abs() < 1e-10);
257 assert!((edges[1] - 120.0).abs() < 1e-10);
258 }
259
260 #[test]
261 fn test_insufficient_edges() {
262 let params = BeamlineParams::default();
263 let result = tof_edges_to_energy(&[100.0], ¶ms);
264 assert!(result.is_err());
265 }
266
267 #[test]
268 fn test_linspace_tof_edges_rejects_inf() {
269 let result = linspace_tof_edges(f64::INFINITY, 200.0, 10);
270 assert!(result.is_err(), "should reject Inf tof_min");
271
272 let result = linspace_tof_edges(100.0, f64::INFINITY, 10);
273 assert!(result.is_err(), "should reject Inf tof_max");
274
275 let result = linspace_tof_edges(f64::NEG_INFINITY, 200.0, 10);
276 assert!(result.is_err(), "should reject -Inf tof_min");
277 }
278
279 #[test]
280 fn test_linspace_tof_edges_rejects_nan() {
281 let result = linspace_tof_edges(f64::NAN, 200.0, 10);
282 assert!(result.is_err(), "should reject NaN tof_min");
283
284 let result = linspace_tof_edges(100.0, f64::NAN, 10);
285 assert!(result.is_err(), "should reject NaN tof_max");
286 }
287
288 #[test]
289 fn test_tof_edges_to_energy_rejects_nan_flight_path() {
290 let tof_edges = vec![100.0, 200.0, 300.0];
291
292 let params_nan = BeamlineParams {
293 flight_path_m: f64::NAN,
294 delay_us: 0.0,
295 };
296 assert!(
297 tof_edges_to_energy(&tof_edges, ¶ms_nan).is_err(),
298 "should reject NaN flight path"
299 );
300
301 let params_inf = BeamlineParams {
302 flight_path_m: f64::INFINITY,
303 delay_us: 0.0,
304 };
305 assert!(
306 tof_edges_to_energy(&tof_edges, ¶ms_inf).is_err(),
307 "should reject Inf flight path"
308 );
309
310 let params_nan_delay = BeamlineParams {
311 flight_path_m: 25.0,
312 delay_us: f64::NAN,
313 };
314 assert!(
315 tof_edges_to_energy(&tof_edges, ¶ms_nan_delay).is_err(),
316 "should reject NaN delay"
317 );
318 }
319
320 #[test]
321 fn test_tof_edges_to_energy_rejects_nan_inf_edges() {
322 let params = BeamlineParams {
323 flight_path_m: 25.0,
324 delay_us: 0.0,
325 };
326
327 let nan_edges = vec![100.0, f64::NAN, 300.0];
329 assert!(
330 tof_edges_to_energy(&nan_edges, ¶ms).is_err(),
331 "should reject NaN in TOF edges"
332 );
333
334 let inf_edges = vec![100.0, f64::INFINITY, 300.0];
336 assert!(
337 tof_edges_to_energy(&inf_edges, ¶ms).is_err(),
338 "should reject Inf in TOF edges"
339 );
340
341 let neg_inf_edges = vec![f64::NEG_INFINITY, 200.0, 300.0];
343 assert!(
344 tof_edges_to_energy(&neg_inf_edges, ¶ms).is_err(),
345 "should reject -Inf in TOF edges"
346 );
347 }
348
349 #[test]
350 fn test_linspace_tof_edges_rejects_zero_bins() {
351 let result = linspace_tof_edges(100.0, 200.0, 0);
352 assert!(result.is_err(), "should reject n_bins == 0");
353 }
354
355 #[test]
356 fn test_linspace_tof_edges_rejects_reversed_range() {
357 let result = linspace_tof_edges(200.0, 100.0, 10);
358 assert!(result.is_err(), "should reject tof_max <= tof_min");
359
360 let result = linspace_tof_edges(100.0, 100.0, 10);
361 assert!(result.is_err(), "should reject tof_max == tof_min");
362 }
363
364 #[test]
365 fn test_linspace_tof_edges_rejects_negative() {
366 let result = linspace_tof_edges(-10.0, 200.0, 10);
367 assert!(result.is_err(), "should reject negative tof_min");
368 }
369}