1use std::fmt;
32
33use rayon::prelude::*;
34
35use crate::resolution::{ResolutionFunction, ResolutionParseError};
36
37type CompactResponseRow = (Vec<u32>, Vec<f64>, f64);
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct TwoArmCounts {
42 pub open_beam: Vec<f64>,
44 pub sample: Vec<f64>,
46 pub open_beam_window_loss: f64,
53 pub sample_window_loss: f64,
56}
57
58#[derive(Debug, Clone, PartialEq)]
65pub struct ArmCountPrediction {
66 pub neutron_signal: Vec<f64>,
68 pub background: Vec<f64>,
70 pub total: Vec<f64>,
72 pub window_loss: f64,
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct TwoArmCountPrediction {
87 pub open_beam: ArmCountPrediction,
89 pub sample: ArmCountPrediction,
91}
92
93#[derive(Debug, Clone, PartialEq)]
108pub struct DetectorBinResponseMatrix {
109 row_offsets: Vec<usize>,
110 detector_bins: Vec<u32>,
111 probabilities: Vec<f64>,
112 lost_fractions: Vec<f64>,
114 n_true_energies: usize,
115 n_detector_bins: usize,
116}
117
118impl DetectorBinResponseMatrix {
119 pub fn new(
121 true_energies_ev: &[f64],
122 detector_time_edges_us: &[f64],
123 timing_offset_us: f64,
124 response: &ResolutionFunction,
125 ) -> Result<Self, CountsResponseError> {
126 if true_energies_ev.is_empty() {
127 return Err(CountsResponseError::EmptyTrueEnergyGrid);
128 }
129 for (index, &energy) in true_energies_ev.iter().enumerate() {
130 if !energy.is_finite() || energy <= 0.0 {
131 return Err(CountsResponseError::InvalidTrueEnergy {
132 index,
133 value: energy,
134 });
135 }
136 }
137
138 let n_detector_bins = detector_time_edges_us.len().saturating_sub(1);
139 if n_detector_bins > u32::MAX as usize {
140 return Err(CountsResponseError::Resolution(
141 ResolutionParseError::InvalidFormat(format!(
142 "detector response has {n_detector_bins} bins, exceeding the u32 storage limit"
143 )),
144 ));
145 }
146 let row_results: Vec<Result<CompactResponseRow, ResolutionParseError>> = true_energies_ev
150 .par_iter()
151 .map(|&energy| {
152 let row = response.detector_bin_probabilities(
153 energy,
154 detector_time_edges_us,
155 timing_offset_us,
156 )?;
157 debug_assert_eq!(row.len(), n_detector_bins);
158 let mut bins = Vec::new();
159 let mut values = Vec::new();
160 let mut in_window = 0.0_f64;
161 for (detector_bin, probability) in row.into_iter().enumerate() {
162 if !probability.is_finite() || probability < 0.0 {
163 return Err(ResolutionParseError::InvalidFormat(format!(
164 "detector response probability at E = {energy} eV, bin {detector_bin} must be finite and >= 0, got {probability}"
165 )));
166 }
167 in_window += probability;
168 if probability > 0.0 {
169 bins.push(detector_bin as u32);
170 values.push(probability);
171 }
172 }
173 Ok::<_, ResolutionParseError>((bins, values, (1.0 - in_window).max(0.0)))
174 })
175 .collect();
176 let rows: Vec<CompactResponseRow> = row_results.into_iter().collect::<Result<_, _>>()?;
179 let nonzero_count = rows.iter().try_fold(0_usize, |total, (_, values, _)| {
180 total.checked_add(values.len()).ok_or_else(|| {
181 CountsResponseError::Resolution(ResolutionParseError::InvalidFormat(
182 "detector response nonzero count overflows usize".into(),
183 ))
184 })
185 })?;
186
187 let mut row_offsets = Vec::with_capacity(true_energies_ev.len() + 1);
188 let mut detector_bins = Vec::with_capacity(nonzero_count);
189 let mut probabilities = Vec::with_capacity(nonzero_count);
190 let mut lost_fractions = Vec::with_capacity(true_energies_ev.len());
191 row_offsets.push(0);
192 for (mut bins, mut values, lost) in rows {
193 detector_bins.append(&mut bins);
194 probabilities.append(&mut values);
195 row_offsets.push(probabilities.len());
196 lost_fractions.push(lost);
197 }
198
199 Ok(Self {
200 row_offsets,
201 detector_bins,
202 probabilities,
203 lost_fractions,
204 n_true_energies: true_energies_ev.len(),
205 n_detector_bins,
206 })
207 }
208
209 pub fn n_true_energies(&self) -> usize {
211 self.n_true_energies
212 }
213
214 pub fn n_detector_bins(&self) -> usize {
216 self.n_detector_bins
217 }
218
219 pub fn nnz(&self) -> usize {
221 self.probabilities.len()
222 }
223
224 pub fn storage_bytes(&self) -> usize {
229 self.row_offsets.capacity() * std::mem::size_of::<usize>()
230 + self.detector_bins.capacity() * std::mem::size_of::<u32>()
231 + self.probabilities.capacity() * std::mem::size_of::<f64>()
232 }
233
234 pub fn row_entries(&self, true_index: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
236 assert!(
237 true_index < self.n_true_energies,
238 "true-energy row out of range"
239 );
240 let start = self.row_offsets[true_index];
241 let end = self.row_offsets[true_index + 1];
242 self.detector_bins[start..end]
243 .iter()
244 .map(|&bin| bin as usize)
245 .zip(self.probabilities[start..end].iter().copied())
246 }
247
248 pub fn probability(&self, true_index: usize, detector_bin: usize) -> f64 {
251 assert!(
252 true_index < self.n_true_energies,
253 "true-energy row out of range"
254 );
255 assert!(
256 detector_bin < self.n_detector_bins,
257 "detector bin out of range"
258 );
259 let start = self.row_offsets[true_index];
260 let end = self.row_offsets[true_index + 1];
261 match self.detector_bins[start..end].binary_search(&(detector_bin as u32)) {
262 Ok(offset) => self.probabilities[start + offset],
263 Err(_) => 0.0,
264 }
265 }
266
267 pub fn apply(
272 &self,
273 incident_fluence_weights: &[f64],
274 transmission: &[f64],
275 ) -> Result<TwoArmCounts, CountsResponseError> {
276 if incident_fluence_weights.len() != self.n_true_energies
277 || transmission.len() != self.n_true_energies
278 {
279 return Err(CountsResponseError::LengthMismatch {
280 energies: self.n_true_energies,
281 incident_fluence: incident_fluence_weights.len(),
282 transmission: transmission.len(),
283 });
284 }
285 for (index, &fluence) in incident_fluence_weights.iter().enumerate() {
286 if !fluence.is_finite() || fluence < 0.0 {
287 return Err(CountsResponseError::InvalidIncidentFluence {
288 index,
289 value: fluence,
290 });
291 }
292 }
293 for (index, &value) in transmission.iter().enumerate() {
294 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
295 return Err(CountsResponseError::InvalidTransmission { index, value });
296 }
297 }
298
299 let mut open_beam = vec![0.0; self.n_detector_bins];
300 let mut sample = vec![0.0; self.n_detector_bins];
301 let mut open_compensation = vec![0.0; self.n_detector_bins];
302 let mut sample_compensation = vec![0.0; self.n_detector_bins];
303 let mut open_window_loss = 0.0;
304 let mut open_window_loss_compensation = 0.0;
305 let mut sample_window_loss = 0.0;
306 let mut sample_window_loss_compensation = 0.0;
307 for true_index in 0..self.n_true_energies {
308 let fluence = incident_fluence_weights[true_index];
309 let sample_weight = fluence * transmission[true_index];
310 let lost = self.lost_fractions[true_index];
311 compensated_add(
312 &mut open_window_loss,
313 &mut open_window_loss_compensation,
314 fluence * lost,
315 );
316 compensated_add(
317 &mut sample_window_loss,
318 &mut sample_window_loss_compensation,
319 sample_weight * lost,
320 );
321 for (detector_bin, probability) in self.row_entries(true_index) {
322 compensated_add(
323 &mut open_beam[detector_bin],
324 &mut open_compensation[detector_bin],
325 fluence * probability,
326 );
327 compensated_add(
328 &mut sample[detector_bin],
329 &mut sample_compensation[detector_bin],
330 sample_weight * probability,
331 );
332 }
333 }
334 for detector_bin in 0..self.n_detector_bins {
335 open_beam[detector_bin] += open_compensation[detector_bin];
336 sample[detector_bin] += sample_compensation[detector_bin];
337 }
338 Ok(TwoArmCounts {
339 open_beam,
340 sample,
341 open_beam_window_loss: open_window_loss + open_window_loss_compensation,
342 sample_window_loss: sample_window_loss + sample_window_loss_compensation,
343 })
344 }
345}
346
347#[derive(Debug)]
349pub enum CountsResponseError {
350 EmptyTrueEnergyGrid,
352 LengthMismatch {
354 energies: usize,
355 incident_fluence: usize,
356 transmission: usize,
357 },
358 InvalidTrueEnergy { index: usize, value: f64 },
360 InvalidIncidentFluence { index: usize, value: f64 },
362 InvalidTransmission { index: usize, value: f64 },
364 InvalidExpectedCount {
366 field: &'static str,
367 index: usize,
368 value: f64,
369 },
370 DetectorBinCountMismatch {
372 open_signal: usize,
373 sample_signal: usize,
374 open_background: usize,
375 sample_background: usize,
376 },
377 Resolution(ResolutionParseError),
379}
380
381impl fmt::Display for CountsResponseError {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 match self {
384 Self::EmptyTrueEnergyGrid => write!(f, "true_energies_ev must not be empty"),
385 Self::LengthMismatch {
386 energies,
387 incident_fluence,
388 transmission,
389 } => write!(
390 f,
391 "true_energies_ev ({energies}), incident_fluence_weights ({incident_fluence}), and transmission ({transmission}) must have equal lengths"
392 ),
393 Self::InvalidTrueEnergy { index, value } => write!(
394 f,
395 "true_energies_ev[{index}] must be positive and finite, got {value}"
396 ),
397 Self::InvalidIncidentFluence { index, value } => write!(
398 f,
399 "incident_fluence_weights[{index}] must be finite and >= 0, got {value}"
400 ),
401 Self::InvalidTransmission { index, value } => write!(
402 f,
403 "transmission[{index}] must be finite and in [0, 1], got {value}"
404 ),
405 Self::InvalidExpectedCount {
406 field,
407 index,
408 value,
409 } => write!(
410 f,
411 "{field}[{index}] must be finite and >= 0 expected counts, got {value}"
412 ),
413 Self::DetectorBinCountMismatch {
414 open_signal,
415 sample_signal,
416 open_background,
417 sample_background,
418 } => write!(
419 f,
420 "open neutron signal ({open_signal} bins), sample neutron signal ({sample_signal}), open_background_counts ({open_background}), and sample_background_counts ({sample_background}) must have equal lengths"
421 ),
422 Self::Resolution(error) => write!(f, "detector-time response failed: {error}"),
423 }
424 }
425}
426
427impl std::error::Error for CountsResponseError {
428 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
429 match self {
430 Self::Resolution(error) => Some(error),
431 _ => None,
432 }
433 }
434}
435
436impl From<ResolutionParseError> for CountsResponseError {
437 fn from(value: ResolutionParseError) -> Self {
438 Self::Resolution(value)
439 }
440}
441
442pub fn add_count_backgrounds(
460 signal: TwoArmCounts,
461 open_background_counts: &[f64],
462 sample_background_counts: &[f64],
463) -> Result<TwoArmCountPrediction, CountsResponseError> {
464 let n_bins = signal.open_beam.len();
465 if signal.sample.len() != n_bins
466 || open_background_counts.len() != n_bins
467 || sample_background_counts.len() != n_bins
468 {
469 return Err(CountsResponseError::DetectorBinCountMismatch {
470 open_signal: n_bins,
471 sample_signal: signal.sample.len(),
472 open_background: open_background_counts.len(),
473 sample_background: sample_background_counts.len(),
474 });
475 }
476
477 validate_expected_counts("open_neutron_signal", &signal.open_beam)?;
478 validate_expected_counts("sample_neutron_signal", &signal.sample)?;
479 validate_expected_counts("open_background_counts", open_background_counts)?;
480 validate_expected_counts("sample_background_counts", sample_background_counts)?;
481 for (field, value) in [
482 ("open_beam_window_loss", signal.open_beam_window_loss),
483 ("sample_window_loss", signal.sample_window_loss),
484 ] {
485 if !value.is_finite() || value < 0.0 {
486 return Err(CountsResponseError::InvalidExpectedCount {
487 field,
488 index: 0,
489 value,
490 });
491 }
492 }
493
494 let open_total = sum_expected_counts(
495 "open_total_expected_counts",
496 &signal.open_beam,
497 open_background_counts,
498 )?;
499 let sample_total = sum_expected_counts(
500 "sample_total_expected_counts",
501 &signal.sample,
502 sample_background_counts,
503 )?;
504
505 Ok(TwoArmCountPrediction {
506 open_beam: ArmCountPrediction {
507 neutron_signal: signal.open_beam,
508 background: open_background_counts.to_vec(),
509 total: open_total,
510 window_loss: signal.open_beam_window_loss,
511 },
512 sample: ArmCountPrediction {
513 neutron_signal: signal.sample,
514 background: sample_background_counts.to_vec(),
515 total: sample_total,
516 window_loss: signal.sample_window_loss,
517 },
518 })
519}
520
521fn validate_expected_counts(
522 field: &'static str,
523 values: &[f64],
524) -> Result<(), CountsResponseError> {
525 for (index, &value) in values.iter().enumerate() {
526 if !value.is_finite() || value < 0.0 {
527 return Err(CountsResponseError::InvalidExpectedCount {
528 field,
529 index,
530 value,
531 });
532 }
533 }
534 Ok(())
535}
536
537fn sum_expected_counts(
538 field: &'static str,
539 neutron_signal: &[f64],
540 background: &[f64],
541) -> Result<Vec<f64>, CountsResponseError> {
542 neutron_signal
543 .iter()
544 .zip(background)
545 .enumerate()
546 .map(|(index, (&neutron, &background))| {
547 let total = neutron + background;
548 if total.is_finite() {
549 Ok(total)
550 } else {
551 Err(CountsResponseError::InvalidExpectedCount {
552 field,
553 index,
554 value: total,
555 })
556 }
557 })
558 .collect()
559}
560
561pub fn two_arm_count_response(
577 true_energies_ev: &[f64],
578 incident_fluence_weights: &[f64],
579 transmission: &[f64],
580 detector_time_edges_us: &[f64],
581 timing_offset_us: f64,
582 response: &ResolutionFunction,
583) -> Result<TwoArmCounts, CountsResponseError> {
584 if true_energies_ev.is_empty() {
585 return Err(CountsResponseError::EmptyTrueEnergyGrid);
586 }
587 if incident_fluence_weights.len() != true_energies_ev.len()
588 || transmission.len() != true_energies_ev.len()
589 {
590 return Err(CountsResponseError::LengthMismatch {
591 energies: true_energies_ev.len(),
592 incident_fluence: incident_fluence_weights.len(),
593 transmission: transmission.len(),
594 });
595 }
596 for (index, &energy) in true_energies_ev.iter().enumerate() {
597 if !energy.is_finite() || energy <= 0.0 {
598 return Err(CountsResponseError::InvalidTrueEnergy {
599 index,
600 value: energy,
601 });
602 }
603 }
604 for (index, &fluence) in incident_fluence_weights.iter().enumerate() {
605 if !fluence.is_finite() || fluence < 0.0 {
606 return Err(CountsResponseError::InvalidIncidentFluence {
607 index,
608 value: fluence,
609 });
610 }
611 }
612 for (index, &value) in transmission.iter().enumerate() {
613 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
614 return Err(CountsResponseError::InvalidTransmission { index, value });
615 }
616 }
617
618 let n_bins = detector_time_edges_us.len().saturating_sub(1);
622 let mut open_beam = vec![0.0; n_bins];
623 let mut sample = vec![0.0; n_bins];
624 let mut open_compensation = vec![0.0; n_bins];
625 let mut sample_compensation = vec![0.0; n_bins];
626 let mut open_window_loss = 0.0;
627 let mut open_window_loss_compensation = 0.0;
628 let mut sample_window_loss = 0.0;
629 let mut sample_window_loss_compensation = 0.0;
630
631 for ((&energy, &fluence), &sample_transmission) in true_energies_ev
632 .iter()
633 .zip(incident_fluence_weights)
634 .zip(transmission)
635 {
636 let probabilities = response.detector_bin_probabilities(
637 energy,
638 detector_time_edges_us,
639 timing_offset_us,
640 )?;
641 debug_assert_eq!(probabilities.len(), n_bins);
642
643 for (detector_bin, &probability) in probabilities.iter().enumerate() {
649 if !probability.is_finite() || probability < 0.0 {
650 return Err(CountsResponseError::Resolution(
651 ResolutionParseError::InvalidFormat(format!(
652 "detector response probability at E = {energy} eV, bin \
653 {detector_bin} must be finite and >= 0, got {probability}"
654 )),
655 ));
656 }
657 }
658
659 let in_window: f64 = probabilities.iter().sum();
663 let lost = (1.0 - in_window).max(0.0);
664 compensated_add(
665 &mut open_window_loss,
666 &mut open_window_loss_compensation,
667 fluence * lost,
668 );
669 compensated_add(
670 &mut sample_window_loss,
671 &mut sample_window_loss_compensation,
672 fluence * sample_transmission * lost,
673 );
674
675 for (bin, probability) in probabilities.into_iter().enumerate() {
678 compensated_add(
679 &mut open_beam[bin],
680 &mut open_compensation[bin],
681 fluence * probability,
682 );
683 compensated_add(
684 &mut sample[bin],
685 &mut sample_compensation[bin],
686 fluence * sample_transmission * probability,
687 );
688 }
689 }
690 for bin in 0..n_bins {
691 open_beam[bin] += open_compensation[bin];
692 sample[bin] += sample_compensation[bin];
693 }
694
695 Ok(TwoArmCounts {
696 open_beam,
697 sample,
698 open_beam_window_loss: open_window_loss + open_window_loss_compensation,
699 sample_window_loss: sample_window_loss + sample_window_loss_compensation,
700 })
701}
702
703#[inline]
704fn compensated_add(sum: &mut f64, compensation: &mut f64, value: f64) {
705 let next = *sum + value;
706 if sum.abs() >= value.abs() {
707 *compensation += (*sum - next) + value;
708 } else {
709 *compensation += (value - next) + *sum;
710 }
711 *sum = next;
712}