1use std::fmt;
15
16use rayon::prelude::*;
17
18use crate::ikeda_carpenter::{DetectorPulse, EnergyLaw, IkedaCarpenterParams};
19use crate::resolution::{ResolutionParseError, TOF_FACTOR};
20
21pub const MAX_POINTS: usize = 100_000;
23
24#[derive(Debug)]
26pub enum FlightTimeGridError {
27 InvalidTimeEdges,
29 InvalidTimingOffset(f64),
31 InvalidFlightPath(f64),
33 LengthensWithEnergy { parameter: &'static str },
38 Pulse(ResolutionParseError),
40 OutsideCalibration { low_ev: f64, high_ev: f64 },
43 TooManyPoints { step_us: f64 },
45 ValuesLength { expected: usize, found: usize },
47}
48
49impl fmt::Display for FlightTimeGridError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::InvalidTimeEdges => {
53 write!(
54 f,
55 "time edges must be at least two finite, strictly ascending values"
56 )
57 }
58 Self::InvalidTimingOffset(t0) => write!(f, "t0 = {t0} µs must be finite"),
59 Self::InvalidFlightPath(l) => {
60 write!(f, "the flight path, {l} m, must be finite and positive")
61 }
62 Self::LengthensWithEnergy { parameter } => write!(
63 f,
64 "unsupported pulse: its {parameter} law lengthens the pulse as energy rises; \
65 the flight-time grid needs α and β that do not fall with energy"
66 ),
67 Self::Pulse(e) => write!(f, "pulse: {e}"),
68 Self::OutsideCalibration { low_ev, high_ev } => write!(
69 f,
70 "neutrons from outside the pulse's energy span, {low_ev} to \
71 {high_ev} eV, can reach the window"
72 ),
73 Self::TooManyPoints { step_us } => write!(
74 f,
75 "a step of {step_us} µs needs more than {MAX_POINTS} grid points"
76 ),
77 Self::ValuesLength { expected, found } => write!(
78 f,
79 "{found} values were given for a grid of {expected} points"
80 ),
81 }
82 }
83}
84
85impl std::error::Error for FlightTimeGridError {}
86
87impl From<ResolutionParseError> for FlightTimeGridError {
88 fn from(e: ResolutionParseError) -> Self {
89 Self::Pulse(e)
90 }
91}
92
93#[derive(Debug, Clone)]
96pub struct Rows {
97 step_us: f64,
98 bins: usize,
99 row_offsets: Vec<usize>,
100 columns: Vec<usize>,
101 values: Vec<f64>,
102}
103
104impl Rows {
105 fn new<'a>(step_us: f64, bins: usize, rows: impl Iterator<Item = &'a [(usize, f64)]>) -> Self {
106 let mut row_offsets = vec![0];
107 let mut columns = Vec::new();
108 let mut values = Vec::new();
109 for row in rows {
110 for &(k, v) in row {
111 columns.push(k);
112 values.push(v);
113 }
114 row_offsets.push(values.len());
115 }
116 Self {
117 step_us,
118 bins,
119 row_offsets,
120 columns,
121 values,
122 }
123 }
124
125 pub fn predict(&self, values: &[f64]) -> Result<Vec<f64>, FlightTimeGridError> {
132 let points = self.row_offsets.len() - 1;
133 if values.len() != points {
134 return Err(FlightTimeGridError::ValuesLength {
135 expected: points,
136 found: values.len(),
137 });
138 }
139 let mut counts = vec![0.0; self.bins];
140 for (&v, row) in values.iter().zip(self.row_offsets.windows(2)) {
141 for (&k, &p) in self.columns[row[0]..row[1]]
142 .iter()
143 .zip(&self.values[row[0]..row[1]])
144 {
145 counts[k] += self.step_us * v * p;
146 }
147 }
148 Ok(counts)
149 }
150}
151
152#[derive(Debug, Clone)]
155pub struct FlightTimeGrid {
156 time_edges_us: Vec<f64>,
157 t0_us: f64,
158 flight_path_m: f64,
159 pulse: DetectorPulse,
160 range_us: (f64, f64),
161 step_us: f64,
162 flight_times_us: Vec<f64>,
163 rows: Rows,
164}
165
166impl FlightTimeGrid {
167 pub fn new(
179 time_edges_us: &[f64],
180 t0_us: f64,
181 flight_path_m: f64,
182 pulse: &DetectorPulse,
183 ) -> Result<Self, FlightTimeGridError> {
184 Self::spanning(time_edges_us, (t0_us, t0_us), flight_path_m, pulse)
185 }
186
187 pub fn spanning(
194 time_edges_us: &[f64],
195 (t0_us, last_t0_us): (f64, f64),
196 flight_path_m: f64,
197 pulse: &DetectorPulse,
198 ) -> Result<Self, FlightTimeGridError> {
199 if time_edges_us.len() < 2
200 || !time_edges_us.iter().all(|t| t.is_finite())
201 || time_edges_us.windows(2).any(|w| w[0] >= w[1])
202 {
203 return Err(FlightTimeGridError::InvalidTimeEdges);
204 }
205 let range = |t0_us: f64| {
206 range_us(
207 time_edges_us,
208 (t0_us, flight_path_m),
209 pulse.params(),
210 pulse.energy_span_ev(),
211 )
212 };
213 let ((first_lo, first_hi), (last_lo, last_hi)) = (range(t0_us)?, range(last_t0_us)?);
214 let (u_lo, u_hi) = (first_lo.min(last_lo), first_hi.max(last_hi));
215 let energy = |u: f64| (TOF_FACTOR * flight_path_m / u).powi(2);
216 let rise = pulse
217 .rise_us(energy(u_lo))?
218 .min(pulse.rise_us(energy(u_hi))?);
219 let intervals = ((u_hi - u_lo) / (0.5 * rise)).ceil();
220 let step_us = (u_hi - u_lo) / intervals;
221 Self::build(
222 time_edges_us,
223 (t0_us, flight_path_m),
224 pulse,
225 (u_lo - step_us, u_hi + step_us),
226 intervals as usize + 2,
227 )
228 }
229
230 pub fn covers(
238 &self,
239 t0_us: f64,
240 flight_path_m: f64,
241 params: &IkedaCarpenterParams,
242 ) -> Result<bool, FlightTimeGridError> {
243 let (u_lo, u_hi) = range_us(
244 &self.time_edges_us,
245 (t0_us, flight_path_m),
246 params,
247 self.pulse.energy_span_ev(),
248 )?;
249 let scale = self.flight_path_m / flight_path_m;
250 Ok(u_lo * scale >= self.range_us.0 && u_hi * scale <= self.range_us.1)
251 }
252}
253
254fn range_us(
255 time_edges_us: &[f64],
256 (t0_us, flight_path_m): (f64, f64),
257 params: &IkedaCarpenterParams,
258 (e_min, e_max): (f64, f64),
259) -> Result<(f64, f64), FlightTimeGridError> {
260 if !t0_us.is_finite() {
261 return Err(FlightTimeGridError::InvalidTimingOffset(t0_us));
262 }
263 if !(flight_path_m.is_finite() && flight_path_m > 0.0) {
264 return Err(FlightTimeGridError::InvalidFlightPath(flight_path_m));
265 }
266 let change = |law: &EnergyLaw| law.eval(e_max) - law.eval(e_min);
267 let storage = params.r.eval(e_min).max(params.r.eval(e_max));
268 for (parameter, lengthens) in [
269 ("α", change(¶ms.alpha) < 0.0),
270 ("β", change(¶ms.beta) < 0.0 && storage > 0.0),
271 ] {
272 if lengthens {
273 return Err(FlightTimeGridError::LengthensWithEnergy { parameter });
274 }
275 }
276 let longest = IkedaCarpenterParams {
277 r: if change(¶ms.r) > 0.0 {
278 EnergyLaw::Const(storage)
279 } else {
280 params.r.clone()
281 },
282 ..params.clone()
283 };
284 let clock = TOF_FACTOR * flight_path_m;
285 let energy = |u: f64| (clock / u).powi(2);
286 let first_edge = time_edges_us[0] - t0_us;
287 let last_edge = time_edges_us[time_edges_us.len() - 1] - t0_us;
288 let latest = |u: f64| {
289 longest
290 .delays_us(energy(u))
291 .map(|(_, last)| u + last - first_edge)
292 };
293 let earliest = |u: f64| {
294 params
295 .delays_us(energy(u))
296 .map(|(first, _)| u + first - last_edge)
297 };
298 let (fastest, slowest) = (clock / e_max.sqrt(), clock / e_min.sqrt());
299 let (Some(u_lo), Some(u_hi)) = (
300 crossing(&latest, fastest, slowest)?,
301 crossing(&earliest, fastest, slowest)?,
302 ) else {
303 return Err(FlightTimeGridError::OutsideCalibration {
304 low_ev: e_min,
305 high_ev: e_max,
306 });
307 };
308 Ok((u_lo, u_hi))
309}
310
311impl FlightTimeGrid {
312 fn build(
313 time_edges_us: &[f64],
314 (t0_us, flight_path_m): (f64, f64),
315 pulse: &DetectorPulse,
316 range_us: (f64, f64),
317 intervals: usize,
318 ) -> Result<Self, FlightTimeGridError> {
319 let (u_lo, u_hi) = range_us;
320 let step_us = (u_hi - u_lo) / intervals as f64;
321 if intervals >= MAX_POINTS {
322 return Err(FlightTimeGridError::TooManyPoints { step_us });
323 }
324 let flight_times_us: Vec<f64> =
325 (0..=intervals).map(|j| u_lo + step_us * j as f64).collect();
326 let mut grid = Self {
327 time_edges_us: time_edges_us.to_vec(),
328 t0_us,
329 flight_path_m,
330 pulse: pulse.clone(),
331 range_us,
332 step_us,
333 flight_times_us,
334 rows: Rows::new(step_us, 0, std::iter::empty()),
335 };
336 grid.rows = grid.rows_at(t0_us, flight_path_m, pulse.params())?;
337 Ok(grid)
338 }
339
340 pub fn halved(&self) -> Result<Self, FlightTimeGridError> {
345 Self::build(
346 &self.time_edges_us,
347 (self.t0_us, self.flight_path_m),
348 &self.pulse,
349 self.range_us,
350 2 * (self.flight_times_us.len() - 1),
351 )
352 }
353
354 pub fn rows_at(
366 &self,
367 t0_us: f64,
368 flight_path_m: f64,
369 params: &IkedaCarpenterParams,
370 ) -> Result<Rows, FlightTimeGridError> {
371 let [rows] = self.rows_with(t0_us, flight_path_m, |energy, arrival| {
372 Ok([params.bin_probabilities_at(energy, arrival, &self.time_edges_us)?])
373 })?;
374 Ok(rows)
375 }
376
377 pub fn arrival_slopes_at(
383 &self,
384 t0_us: f64,
385 flight_path_m: f64,
386 params: &IkedaCarpenterParams,
387 ) -> Result<Rows, FlightTimeGridError> {
388 let [rows] = self.rows_with(t0_us, flight_path_m, |energy, arrival| {
389 Ok([params.bin_arrival_slopes_at(energy, arrival, &self.time_edges_us)?])
390 })?;
391 Ok(rows)
392 }
393
394 pub fn pulse_slopes_at(
403 &self,
404 t0_us: f64,
405 flight_path_m: f64,
406 params: &IkedaCarpenterParams,
407 ) -> Result<[Rows; 4], FlightTimeGridError> {
408 self.rows_with(t0_us, flight_path_m, |energy, arrival| {
409 params.bin_pulse_slopes_at(energy, arrival, &self.time_edges_us)
410 })
411 }
412
413 pub fn tail_slopes_at(
421 &self,
422 t0_us: f64,
423 flight_path_m: f64,
424 params: &IkedaCarpenterParams,
425 ) -> Result<[Rows; 2], FlightTimeGridError> {
426 self.rows_with(t0_us, flight_path_m, |energy, arrival| {
427 params.bin_tail_slopes_at(energy, arrival, &self.time_edges_us)
428 })
429 }
430
431 fn rows_with<const N: usize>(
432 &self,
433 t0_us: f64,
434 flight_path_m: f64,
435 row: impl Fn(f64, f64) -> Result<[Vec<f64>; N], ResolutionParseError> + Sync,
436 ) -> Result<[Rows; N], FlightTimeGridError> {
437 if !t0_us.is_finite() {
438 return Err(FlightTimeGridError::InvalidTimingOffset(t0_us));
439 }
440 if !(flight_path_m.is_finite() && flight_path_m > 0.0) {
441 return Err(FlightTimeGridError::InvalidFlightPath(flight_path_m));
442 }
443 let scale = flight_path_m / self.flight_path_m;
444 let clock = TOF_FACTOR * self.flight_path_m;
445 let points: Vec<[Vec<(usize, f64)>; N]> = self
446 .energies_ev()
447 .par_iter()
448 .map(|&energy| {
449 Ok(
450 row(energy, t0_us + scale * (clock / energy.sqrt()))?.map(|values| {
451 values
452 .into_iter()
453 .enumerate()
454 .filter(|&(_, v)| v != 0.0)
455 .collect()
456 }),
457 )
458 })
459 .collect::<Result<_, ResolutionParseError>>()?;
460 let bins = self.time_edges_us.len() - 1;
461 Ok(std::array::from_fn(|n| {
462 Rows::new(
463 self.step_us * scale,
464 bins,
465 points.iter().map(|point| point[n].as_slice()),
466 )
467 }))
468 }
469
470 #[must_use]
473 pub fn rows(&self) -> &Rows {
474 &self.rows
475 }
476
477 #[must_use]
479 pub fn pulse(&self) -> &DetectorPulse {
480 &self.pulse
481 }
482
483 #[must_use]
485 pub fn t0_us(&self) -> f64 {
486 self.t0_us
487 }
488
489 #[must_use]
491 pub fn flight_path_m(&self) -> f64 {
492 self.flight_path_m
493 }
494
495 #[must_use]
498 pub fn range_us(&self) -> (f64, f64) {
499 self.range_us
500 }
501
502 #[must_use]
504 pub fn step_us(&self) -> f64 {
505 self.step_us
506 }
507
508 #[must_use]
510 pub fn flight_times_us(&self) -> &[f64] {
511 &self.flight_times_us
512 }
513
514 #[must_use]
516 pub fn time_edges_us(&self) -> &[f64] {
517 &self.time_edges_us
518 }
519
520 #[must_use]
522 pub fn energies_ev(&self) -> Vec<f64> {
523 let clock = TOF_FACTOR * self.flight_path_m;
524 self.flight_times_us
525 .iter()
526 .map(|u| (clock / u).powi(2))
527 .collect()
528 }
529
530 pub fn predict(&self, values: &[f64]) -> Result<Vec<f64>, FlightTimeGridError> {
542 self.rows.predict(values)
543 }
544}
545
546fn crossing(
547 signed: &dyn Fn(f64) -> Result<f64, ResolutionParseError>,
548 fast: f64,
549 slow: f64,
550) -> Result<Option<f64>, ResolutionParseError> {
551 if signed(fast)? >= 0.0 || signed(slow)? < 0.0 {
552 return Ok(None);
553 }
554 let (mut low, mut high) = (fast, slow);
555 while high - low > f64::EPSILON * high {
556 let middle = 0.5 * (low + high);
557 if signed(middle)? < 0.0 {
558 low = middle;
559 } else {
560 high = middle;
561 }
562 }
563 Ok(Some(high))
564}