1use std::fmt;
11use std::sync::Arc;
12
13use crate::counts_response::{CountsResponseError, DetectorBinResponseMatrix};
14use crate::ikeda_carpenter::{EnergyLaw, IkedaCarpenter};
15use crate::resolution::{ResolutionFunction, ResolutionParseError, TOF_FACTOR};
16
17pub const MAX_POINTS: usize = 100_000;
19
20#[derive(Debug)]
22pub enum FlightTimeGridError {
23 InvalidTimeEdges,
25 InvalidTimingOffset(f64),
27 LengthensWithEnergy { parameter: &'static str },
33 Pulse(ResolutionParseError),
35 OutsideCalibration { low_ev: f64, high_ev: f64 },
38 TooManyPoints { step_us: f64 },
40 Response(CountsResponseError),
42 ValuesLength { expected: usize, found: usize },
44}
45
46impl fmt::Display for FlightTimeGridError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::InvalidTimeEdges => {
50 write!(
51 f,
52 "time edges must be at least two finite, strictly ascending values"
53 )
54 }
55 Self::InvalidTimingOffset(t0) => write!(f, "t0 = {t0} µs must be finite"),
56 Self::LengthensWithEnergy { parameter } => write!(
57 f,
58 "unsupported pulse: its {parameter} law lengthens the pulse as energy rises; \
59 the flight-time grid needs α and β that do not fall and R that does not \
60 rise with energy"
61 ),
62 Self::Pulse(e) => write!(f, "pulse: {e}"),
63 Self::OutsideCalibration { low_ev, high_ev } => write!(
64 f,
65 "neutrons from outside the pulse's synthesis grid, {low_ev} to \
66 {high_ev} eV, can reach the window"
67 ),
68 Self::TooManyPoints { step_us } => write!(
69 f,
70 "a step of {step_us} µs needs more than {MAX_POINTS} grid points"
71 ),
72 Self::Response(e) => write!(f, "bin probabilities: {e}"),
73 Self::ValuesLength { expected, found } => write!(
74 f,
75 "{found} values were given for a grid of {expected} points"
76 ),
77 }
78 }
79}
80
81impl std::error::Error for FlightTimeGridError {}
82
83impl From<ResolutionParseError> for FlightTimeGridError {
84 fn from(e: ResolutionParseError) -> Self {
85 Self::Pulse(e)
86 }
87}
88
89#[derive(Debug, Clone)]
92pub struct FlightTimeGrid {
93 time_edges_us: Vec<f64>,
94 t0_us: f64,
95 pulse: Arc<IkedaCarpenter>,
96 range_us: (f64, f64),
97 step_us: f64,
98 flight_times_us: Vec<f64>,
99 response: DetectorBinResponseMatrix,
100}
101
102impl FlightTimeGrid {
103 pub fn new(
111 time_edges_us: &[f64],
112 t0_us: f64,
113 pulse: &Arc<IkedaCarpenter>,
114 ) -> Result<Self, FlightTimeGridError> {
115 if time_edges_us.len() < 2
116 || !time_edges_us.iter().all(|t| t.is_finite())
117 || time_edges_us.windows(2).any(|w| w[0] >= w[1])
118 {
119 return Err(FlightTimeGridError::InvalidTimeEdges);
120 }
121 if !t0_us.is_finite() {
122 return Err(FlightTimeGridError::InvalidTimingOffset(t0_us));
123 }
124 let references = pulse.ref_energies();
125 let (e_min, e_max) = (references[0], references[references.len() - 1]);
126 let params = pulse.params();
127 let change = |law: &EnergyLaw| law.eval(e_max) - law.eval(e_min);
128 for (parameter, lengthens) in [
129 ("α", change(¶ms.alpha) < 0.0),
130 (
131 "β",
132 change(¶ms.beta) < 0.0 && params.r.eval(e_min) > 0.0,
133 ),
134 ("R", change(¶ms.r) > 0.0),
135 ] {
136 if lengthens {
137 return Err(FlightTimeGridError::LengthensWithEnergy { parameter });
138 }
139 }
140 let clock = TOF_FACTOR * pulse.flight_path_m();
141 let energy = |u: f64| (clock / u).powi(2);
142 let first_edge = time_edges_us[0] - t0_us;
143 let last_edge = time_edges_us[time_edges_us.len() - 1] - t0_us;
144 let latest = |u: f64| {
145 pulse
146 .delays_us(energy(u))
147 .map(|(_, last)| u + last - first_edge)
148 };
149 let earliest = |u: f64| {
150 pulse
151 .delays_us(energy(u))
152 .map(|(first, _)| u + first - last_edge)
153 };
154 let (fastest, slowest) = (clock / e_max.sqrt(), clock / e_min.sqrt());
155 let (Some(u_lo), Some(u_hi)) = (
156 crossing(&latest, fastest, slowest)?,
157 crossing(&earliest, fastest, slowest)?,
158 ) else {
159 return Err(FlightTimeGridError::OutsideCalibration {
160 low_ev: e_min,
161 high_ev: e_max,
162 });
163 };
164
165 let rise = pulse
166 .rise_us(energy(u_lo))?
167 .min(pulse.rise_us(energy(u_hi))?);
168 let intervals = ((u_hi - u_lo) / (0.5 * rise)).ceil();
169 Self::build(
170 time_edges_us,
171 t0_us,
172 pulse,
173 (u_lo, u_hi),
174 intervals as usize,
175 )
176 }
177
178 fn build(
179 time_edges_us: &[f64],
180 t0_us: f64,
181 pulse: &Arc<IkedaCarpenter>,
182 range_us: (f64, f64),
183 intervals: usize,
184 ) -> Result<Self, FlightTimeGridError> {
185 let (u_lo, u_hi) = range_us;
186 let step_us = (u_hi - u_lo) / intervals as f64;
187 if intervals >= MAX_POINTS {
188 return Err(FlightTimeGridError::TooManyPoints { step_us });
189 }
190 let flight_times_us: Vec<f64> =
191 (0..=intervals).map(|j| u_lo + step_us * j as f64).collect();
192 let clock = TOF_FACTOR * pulse.flight_path_m();
193 let energies: Vec<f64> = flight_times_us
194 .iter()
195 .map(|u| (clock / u).powi(2))
196 .collect();
197 let response = DetectorBinResponseMatrix::new(
198 &energies,
199 time_edges_us,
200 t0_us,
201 &ResolutionFunction::IkedaCarpenter(Arc::clone(pulse)),
202 )
203 .map_err(FlightTimeGridError::Response)?;
204 Ok(Self {
205 time_edges_us: time_edges_us.to_vec(),
206 t0_us,
207 pulse: Arc::clone(pulse),
208 range_us,
209 step_us,
210 flight_times_us,
211 response,
212 })
213 }
214
215 pub fn halved(&self) -> Result<Self, FlightTimeGridError> {
220 Self::build(
221 &self.time_edges_us,
222 self.t0_us,
223 &self.pulse,
224 self.range_us,
225 2 * (self.flight_times_us.len() - 1),
226 )
227 }
228
229 #[must_use]
231 pub fn range_us(&self) -> (f64, f64) {
232 self.range_us
233 }
234
235 #[must_use]
237 pub fn step_us(&self) -> f64 {
238 self.step_us
239 }
240
241 #[must_use]
243 pub fn flight_times_us(&self) -> &[f64] {
244 &self.flight_times_us
245 }
246
247 #[must_use]
249 pub fn energies_ev(&self) -> Vec<f64> {
250 let clock = TOF_FACTOR * self.pulse.flight_path_m();
251 self.flight_times_us
252 .iter()
253 .map(|u| (clock / u).powi(2))
254 .collect()
255 }
256
257 pub fn predict(&self, values: &[f64]) -> Result<Vec<f64>, FlightTimeGridError> {
269 if values.len() != self.flight_times_us.len() {
270 return Err(FlightTimeGridError::ValuesLength {
271 expected: self.flight_times_us.len(),
272 found: values.len(),
273 });
274 }
275 let mut counts = vec![0.0; self.response.n_detector_bins()];
276 for (j, &v) in values.iter().enumerate() {
277 for (k, p) in self.response.row_entries(j) {
278 counts[k] += self.step_us * v * p;
279 }
280 }
281 Ok(counts)
282 }
283}
284
285fn crossing(
286 signed: &dyn Fn(f64) -> Result<f64, ResolutionParseError>,
287 fast: f64,
288 slow: f64,
289) -> Result<Option<f64>, ResolutionParseError> {
290 if signed(fast)? >= 0.0 || signed(slow)? < 0.0 {
291 return Ok(None);
292 }
293 let (mut low, mut high) = (fast, slow);
294 while high - low > f64::EPSILON * high {
295 let middle = 0.5 * (low + high);
296 if signed(middle)? < 0.0 {
297 low = middle;
298 } else {
299 high = middle;
300 }
301 }
302 Ok(Some(high))
303}