1use std::sync::RwLock;
5
6use faer::linalg::solvers::DenseSolveCore;
7use faer::{Mat, Side};
8use ndarray::{Array2, ArrayView2, ArrayView3, s};
9use nereids_fitting::error::FittingError;
10use nereids_fitting::lm::FlatMatrix;
11use nereids_fitting::poisson::{
12 InformationInverse, NEWTON_DECREMENT_TOL, Prior, half_deviance, information_inverse,
13};
14use nereids_fitting::statistics::{Consistency, consistency};
15use rayon::prelude::*;
16
17use crate::counts_fit::TEMPERATURE_BOUNDS_K;
18use crate::counts_fit::{
19 BACKGROUND_TERMS, BeamStart, BeamStarts, Bypass, MOST_PASSES, Material, Measurement, Region,
20 RegionFit, RegionTerms, Role, RoomBackground, SHARED, Starts, Value, checked, excluded_bins,
21 fit_counts_answer, labelled, open_frames, shared_parameters, starting_bypass,
22 starts_and_widths_us,
23};
24use crate::error::PipelineError;
25use crate::open_beam::{
26 BOUND, Calibration, PULSE_NUMBERS, Pulse, open_beam_with_intervals, validate_live, whole,
27};
28
29const MOST_STEPS: usize = 20;
30
31const MOST_HALVINGS: i32 = 10;
32
33#[derive(Debug, Clone)]
37pub struct MapMeasurement<'a> {
38 pub time_edges_us: Vec<f64>,
40 pub charge_ratio: f64,
42 pub normalization: Value,
44 pub time_background: [Value; 3],
49 pub room_background: Option<RoomBackground>,
52 pub open_counts: ArrayView3<'a, f64>,
54 pub sample_counts: ArrayView3<'a, f64>,
56 pub open_live: Option<Vec<f64>>,
59 pub sample_live: Option<Vec<f64>>,
62 pub excluded: ArrayView2<'a, bool>,
64 pub sample: ArrayView2<'a, bool>,
66 pub empty: ArrayView2<'a, bool>,
69 pub binning: usize,
71 pub material: Material,
74 pub background: [Value; 5],
77 pub gamma_transmission: Value,
81 pub capture_gammas: Value,
85 pub in_scatter: Value,
89 pub empty_background: [Value; 5],
91}
92
93impl MapMeasurement<'_> {
94 pub fn patches(&self) -> Result<Array2<Patch>, PipelineError> {
103 let bins = self.time_edges_us.len().saturating_sub(1);
104 let (time_bins, height, width) = self.open_counts.dim();
105 if self.sample_counts.dim() != self.open_counts.dim() || time_bins != bins {
106 return Err(PipelineError::ShapeMismatch(format!(
107 "open-beam counts of shape {:?} and sample counts of shape {:?} for {bins} \
108 time bins",
109 self.open_counts.dim(),
110 self.sample_counts.dim()
111 )));
112 }
113 for (name, mask) in [
114 ("excluded", self.excluded),
115 ("sample", self.sample),
116 ("empty", self.empty),
117 ] {
118 if mask.dim() != (height, width) {
119 return Err(PipelineError::ShapeMismatch(format!(
120 "the {name} mask has shape {:?} for {height}×{width} pixels",
121 mask.dim()
122 )));
123 }
124 }
125 let b = self.binning;
126 if b == 0 {
127 return Err(PipelineError::InvalidParameter(
128 "a patch is at least one pixel wide".into(),
129 ));
130 }
131 Ok(Array2::from_shape_fn(
132 (height.div_ceil(b), width.div_ceil(b)),
133 |patch| {
134 let pixels = self.pixels(patch);
135 let behind = pixels.iter().filter(|&&pixel| self.sample[pixel]).count();
136 match (behind, pixels.len() - behind) {
137 (0, _) => Patch::Outside,
138 (_, 0) => Patch::Sample,
139 _ => Patch::Mixed,
140 }
141 },
142 ))
143 }
144
145 fn pixels(&self, (i, j): (usize, usize)) -> Vec<(usize, usize)> {
146 let (height, width) = self.excluded.dim();
147 let b = self.binning;
148 (i * b..height.min((i + 1) * b))
149 .flat_map(|y| (j * b..width.min((j + 1) * b)).map(move |x| (y, x)))
150 .filter(|&pixel| !self.excluded[pixel])
151 .collect()
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum Patch {
158 Sample,
160 Mixed,
162 Outside,
164}
165
166#[derive(Debug, Clone)]
168pub struct CountsMap<'a> {
169 measurement: MapMeasurement<'a>,
170 pub patches: Array2<Patch>,
172 pub fits: Array2<Option<RegionFit>>,
175 pub failed: Array2<Option<String>>,
181 pub empty: Option<RegionFit>,
185 pub densities: Vec<Array2<f64>>,
188 pub density_sd: Vec<Array2<f64>>,
193 pub temperature_k: Array2<f64>,
196 pub temperature_sd_k: Array2<f64>,
198 pub overdispersion: [Array2<f64>; 2],
201 pub identified: Array2<bool>,
207 pub normalization: f64,
209 pub time_background: [f64; 3],
211 pub room_background: f64,
214 pub t0_us: f64,
216 pub flight_path_m: f64,
218 pub alpha: [f64; 2],
220 pub beta: [f64; 2],
222 pub r: f64,
224 pub fwhm_squared_us2: f64,
227 pub r_high: Option<f64>,
229 pub detector_tail: Option<[f64; 2]>,
232 pub shared: Vec<&'static str>,
237 pub shared_covariance: Option<FlatMatrix>,
245 pub measured_pulls: Option<Vec<f64>>,
252 pub pulse_consistency: Option<Consistency>,
258 pub quantities: Vec<String>,
263 pub covariances: PatchCovariances,
266 pub deviance: f64,
270 pub converged: bool,
274 pub unconverged: Option<String>,
278 pub steps: usize,
280}
281
282impl CountsMap<'_> {
283 pub fn residual_rows(&self) -> impl Iterator<Item = [Array2<f64>; 2]> + '_ {
292 let map = &self.measurement;
293 let bins = map.open_counts.dim().0;
294 let width = self.patches.ncols();
295 (0..self.patches.nrows()).map(move |i| {
296 let mut row = [
297 Array2::from_elem((bins, width), f64::NAN),
298 Array2::from_elem((bins, width), f64::NAN),
299 ];
300 for j in 0..width {
301 let Some(fit) = &self.fits[[i, j]] else {
302 continue;
303 };
304 let counts =
305 summed(map, &map.pixels((i, j))).expect("the fit summed this patch's counts");
306 for (run, residuals) in row.iter_mut().enumerate() {
307 for (k, (&y, &mu)) in counts[run].iter().zip(&fit.predicted[run]).enumerate() {
308 residuals[[k, j]] = (2.0 * half_deviance(y, mu)).sqrt().copysign(y - mu);
309 }
310 }
311 }
312 row
313 })
314 }
315
316 fn unsettled(mut self, reason: String) -> Self {
317 self.converged = false;
318 self.unconverged = Some(reason);
319 self.shared_covariance = None;
320 self.covariances.carried = None;
321 self.measured_pulls = None;
322 self.pulse_consistency = None;
323 self.covariances.covariance.fill(None);
324 for sd in self
325 .density_sd
326 .iter_mut()
327 .chain([&mut self.temperature_sd_k])
328 {
329 sd.fill(f64::NAN);
330 }
331 self.identified.fill(false);
332 self
333 }
334}
335
336#[derive(Debug, Clone)]
340pub struct PatchCovariances {
341 pub covariance: Array2<Option<FlatMatrix>>,
347 pub sensitivity: Array2<Option<FlatMatrix>>,
352 carried: Option<FlatMatrix>,
353}
354
355impl PatchCovariances {
356 pub fn cross_covariance(&self, a: (usize, usize), b: (usize, usize)) -> Option<FlatMatrix> {
363 let left = self.sensitivity.get(a)?.as_ref()?;
364 let right = self.sensitivity.get(b)?.as_ref()?;
365 let shared = self.carried.as_ref()?;
366 let k = shared.nrows;
367 let undetermined = |patch: (usize, usize), i: usize| {
368 self.covariance[patch]
369 .as_ref()
370 .is_none_or(|covariance| covariance.get(i, i).is_nan())
371 };
372 let mut cross = FlatMatrix::zeros(left.nrows, right.nrows);
373 for i in 0..left.nrows {
374 for j in 0..right.nrows {
375 *cross.get_mut(i, j) = if undetermined(a, i) || undetermined(b, j) {
376 f64::NAN
377 } else {
378 (0..k)
379 .flat_map(|s| (0..k).map(move |t| (s, t)))
380 .map(|(s, t)| left.get(i, s) * shared.get(s, t) * right.get(j, t))
381 .sum()
382 };
383 }
384 }
385 Some(cross)
386 }
387}
388
389pub fn fit_map<'a>(
478 map: &MapMeasurement<'a>,
479 calibration: &Calibration,
480) -> Result<CountsMap<'a>, PipelineError> {
481 map.patches()?;
482 let mut open = starting_bypass(&Measurement {
483 time_edges_us: Vec::new(),
484 charge_ratio: map.charge_ratio,
485 normalization: map.normalization,
486 time_background: map.time_background,
487 room_background: map.room_background,
488 spread: None,
489 regions: Vec::new(),
490 })?;
491 let bins = starts_and_widths_us(&map.time_edges_us);
492 let frames = open_frames(map.room_background.as_ref());
493 let empty_pixels = map
494 .empty
495 .indexed_iter()
496 .filter(|&(pixel, &e)| e && !map.excluded[pixel])
497 .count();
498 let mut intervals: Option<Vec<Option<usize>>> = None;
499 for passes in 1..=MOST_PASSES {
500 let result = fit_map_from(map, calibration, open, intervals.as_deref())?;
501 let fitted = Bypass {
502 time: result.time_background,
503 room: result.room_background,
504 };
505 let used = open.open_counts_per_pixel(&bins, frames);
506 let now = fitted.open_counts_per_pixel(&bins, frames);
507 let regions = || {
508 result
509 .fits
510 .indexed_iter()
511 .filter_map(|(patch, fit)| Some((fit.as_ref()?, map.pixels(patch).len())))
512 .chain(result.empty.as_ref().map(|fit| (fit, empty_pixels)))
513 };
514 let mut spread = 0.0;
515 for (fit, pixels) in regions() {
516 for (k, &mu) in fit.predicted[0]
517 .iter()
518 .enumerate()
519 .filter(|(_, mu)| **mu > 0.0)
520 {
521 let live = map.open_live.as_ref().map_or(1.0, |live| live[k]);
522 spread += (live * pixels as f64 * (now[k] - used[k])).powi(2) / mu;
523 }
524 }
525 if !result.converged || spread <= BOUND {
526 return Ok(result);
527 }
528 if passes == MOST_PASSES {
529 return Ok(result.unsettled(format!(
530 "the open-beam fits' time and room backgrounds did not settle in {MOST_PASSES} \
531 fits"
532 )));
533 }
534 if passes >= 2 {
535 intervals.get_or_insert_with(|| {
536 result
537 .patches
538 .indexed_iter()
539 .filter(|&(_, &kind)| kind == Patch::Sample)
540 .map(|(patch, _)| result.fits[patch].as_ref().map(|f| f.beam.intervals()))
541 .chain(
542 (empty_pixels > 0)
543 .then(|| result.empty.as_ref().map(|f| f.beam.intervals())),
544 )
545 .collect()
546 });
547 }
548 open = fitted;
549 }
550 unreachable!("the passes end at MOST_PASSES")
551}
552
553fn fit_map_from<'a>(
554 map: &MapMeasurement<'a>,
555 calibration: &Calibration,
556 open: Bypass,
557 intervals: Option<&[Option<usize>]>,
558) -> Result<CountsMap<'a>, PipelineError> {
559 let invalid = |message: String| Err(PipelineError::InvalidParameter(message));
560 let patches = map.patches()?;
561 if map.sample.iter().zip(&map.empty).any(|(&s, &e)| s && e) {
562 return invalid("a pixel is both behind the sample and empty".into());
563 }
564 let material = &map.material;
565 if material
566 .isotopes
567 .iter()
568 .map(|(_, density)| density)
569 .chain([&material.temperature_k, &material.thickness_variance])
570 .chain(&map.background)
571 .chain([
572 &map.gamma_transmission,
573 &map.capture_gammas,
574 &map.in_scatter,
575 ])
576 .any(|value| matches!(value, Value::Measured { .. }))
577 {
578 return invalid(
579 "a measured density, temperature, thickness variance, background, gamma \
580 transmission, capture gammas or in-scatter of the sample's patches would count its \
581 measurement once per patch; give it known, fitted or within bounds"
582 .into(),
583 );
584 }
585 if !material.covariances.is_empty() {
586 return invalid("a map does not yet carry the resonance parameters' covariance".into());
587 }
588 if map
589 .empty_background
590 .iter()
591 .any(|value| matches!(value, Value::Measured { .. }))
592 {
593 return invalid(
594 "a measured background of the empty pixels is not supported by the map's region by \
595 region fit; give it known, fitted or within bounds"
596 .into(),
597 );
598 }
599 let fitted_patches: Vec<(usize, usize)> = patches
600 .indexed_iter()
601 .filter(|&(_, &kind)| kind == Patch::Sample)
602 .map(|(patch, _)| patch)
603 .collect();
604 if fitted_patches.is_empty() {
605 let b = map.binning;
606 return invalid(format!(
607 "no patch of {b}×{b} pixels has all its pixels not excluded behind the sample"
608 ));
609 }
610 let empty: Vec<(usize, usize)> = map
611 .empty
612 .indexed_iter()
613 .filter(|&(pixel, &e)| e && !map.excluded[pixel])
614 .map(|(pixel, _)| pixel)
615 .collect();
616 let region =
617 |pixels: &[(usize, usize)], background, gamma_transmission, material: Option<Material>| {
618 let [open_counts, sample_counts] = summed(map, pixels)?;
619 Ok::<_, PipelineError>(Region {
620 open_counts,
621 sample_counts,
622 open_live: map.open_live.clone(),
623 sample_live: map.sample_live.clone(),
624 background,
625 pixels: pixels.len() as f64,
626 gamma_transmission,
627 capture_gammas: if material.is_some() {
628 map.capture_gammas
629 } else {
630 Value::Known(0.0)
631 },
632 in_scatter: if material.is_some() {
633 map.in_scatter
634 } else {
635 Value::Known(0.0)
636 },
637 timing_offset_us: Value::Known(0.0),
638 material,
639 })
640 };
641 let mut regions = fitted_patches
642 .iter()
643 .map(|&patch| {
644 region(
645 &map.pixels(patch),
646 map.background,
647 map.gamma_transmission,
648 Some(material.clone()),
649 )
650 })
651 .collect::<Result<Vec<Region>, PipelineError>>()?;
652 if !empty.is_empty() {
653 regions.push(region(
654 &empty,
655 map.empty_background,
656 Value::Known(1.0),
657 None,
658 )?);
659 }
660 let measurement = |normalization, time_background, room_background, regions| Measurement {
661 time_edges_us: map.time_edges_us.clone(),
662 charge_ratio: map.charge_ratio,
663 normalization,
664 time_background,
665 room_background,
666 spread: None,
667 regions,
668 };
669
670 let template = |background, gamma_transmission, material: Option<Material>| Region {
671 open_counts: Vec::new(),
672 sample_counts: Vec::new(),
673 open_live: None,
674 sample_live: None,
675 background,
676 pixels: 1.0,
677 gamma_transmission,
678 capture_gammas: if material.is_some() {
679 map.capture_gammas
680 } else {
681 Value::Known(0.0)
682 },
683 in_scatter: if material.is_some() {
684 map.in_scatter
685 } else {
686 Value::Known(0.0)
687 },
688 timing_offset_us: Value::Known(0.0),
689 material,
690 };
691 checked(
692 &measurement(
693 map.normalization,
694 map.time_background,
695 map.room_background,
696 std::iter::once(template(
697 map.background,
698 map.gamma_transmission,
699 Some(material.clone()),
700 ))
701 .chain(
702 (!empty.is_empty())
703 .then(|| template(map.empty_background, Value::Known(1.0), None)),
704 )
705 .collect(),
706 ),
707 calibration,
708 )?;
709 let parameters = shared_parameters(
710 &measurement(
711 map.normalization,
712 map.time_background,
713 map.room_background,
714 vec![],
715 ),
716 calibration,
717 )?;
718 let at = |role: Role| {
719 SHARED
720 .iter()
721 .position(|&shared| shared == role)
722 .expect("a shared role")
723 };
724 let (time_background, room, t0) = (
725 at(Role::TimeBackground(0)),
726 at(Role::RoomBackground),
727 at(Role::T0),
728 );
729 let instrument = t0..SHARED.len();
730 let fitted: Vec<usize> = (0..SHARED.len())
731 .filter(|&i| !parameters[i].1.fixed)
732 .collect();
733 let roles: Vec<Role> = fitted.iter().map(|&i| SHARED[i]).collect();
734 let k = fitted.len();
735 let mut prior = Mat::<f64>::zeros(k, k);
736 let mut center = vec![0.0; k];
737 for (a, &i) in fitted.iter().enumerate() {
738 if let Value::Measured { value, sd } = parameters[i].0 {
739 prior[(a, a)] = 1.0 / (sd * sd);
740 center[a] = value;
741 }
742 }
743 let mut calibrated = None;
744 if let Some(pulse) = &calibration.pulse.prior {
745 let at: Vec<usize> = pulse
746 .numbers
747 .iter()
748 .map(|&n| fitted.iter().position(|&i| SHARED[i] == Role::Pulse(n)))
749 .collect::<Option<_>>()
750 .expect("a calibrated number is fitted");
751 let numbers = Prior::correlated(&at, &pulse.mean, &pulse.covariance)?;
752 let inverse = Mat::from_fn(at.len(), at.len(), |a, b| pulse.covariance.get(a, b))
753 .llt(Side::Lower)
754 .map_err(|e| FittingError::EvaluationFailed(format!("{e:?}")))?
755 .inverse();
756 for (a, &i) in at.iter().enumerate() {
757 center[i] = pulse.mean[a];
758 for (b, &j) in at.iter().enumerate() {
759 prior[(i, j)] = inverse[(a, b)];
760 }
761 }
762 calibrated = Some((numbers, at));
763 }
764 let penalty = |values: &[f64]| -> (f64, Vec<f64>) {
765 let offset: Vec<f64> = (0..k).map(|a| values[fitted[a]] - center[a]).collect();
766 let slope: Vec<f64> = (0..k)
767 .map(|a| (0..k).map(|b| prior[(a, b)] * offset[b]).sum())
768 .collect();
769 (
770 0.5 * offset.iter().zip(&slope).map(|(o, s)| o * s).sum::<f64>(),
771 slope,
772 )
773 };
774
775 let mut values: Vec<f64> = parameters.iter().map(|(_, p)| p.value).collect();
776 let origin_us = values[t0];
777 let open_per_pixel = open.open_counts_per_pixel(
778 &starts_and_widths_us(&map.time_edges_us),
779 open_frames(map.room_background.as_ref()),
780 );
781 let bins = map.open_counts.dim().0;
782 validate_live("open-beam", map.open_live.as_deref(), bins)?;
783 validate_live("sample", map.sample_live.as_deref(), bins)?;
784 let (t_low, t_high) = TEMPERATURE_BOUNDS_K;
785 let fitted_temperature = !matches!(material.temperature_k, Value::Known(_));
786 let labels: Vec<String> = fitted_patches
787 .iter()
788 .map(|patch| format!("patch {patch:?}"))
789 .chain((!empty.is_empty()).then(|| "the empty pixels".to_string()))
790 .collect();
791 let patch_count = fitted_patches.len();
792 let held_at = |values: &[f64]| {
793 let known = |i: usize| Value::Known(values[t0 + i]);
794 Calibration {
795 t0_us: known(0),
796 flight_path_m: known(1),
797 pulse: Pulse {
798 alpha: [known(2), known(3)],
799 beta: [known(4), known(5)],
800 r: known(6),
801 fwhm_squared_us2: known(7),
802 r_high: calibration.pulse.r_high.map(|_| known(8)),
803 detector_tail: calibration
804 .pulse
805 .detector_tail
806 .map(|_| [known(9), known(10)]),
807 prior: None,
808 ..calibration.pulse.clone()
809 },
810 }
811 };
812 let starting = held_at(&values);
813 let excluded_at = |values: &[f64]| -> Vec<Vec<usize>> {
814 regions
815 .iter()
816 .map(|region| {
817 excluded_bins(
818 &map.time_edges_us,
819 region.material.as_ref(),
820 &values[instrument.clone()],
821 calibration.pulse.storage_span(),
822 )
823 })
824 .collect()
825 };
826 let excluded = RwLock::new(excluded_at(&values));
827 let counted_in =
828 |region: &Region| -> f64 { region.open_counts.iter().chain(®ion.sample_counts).sum() };
829 let all_counts: f64 = regions.iter().map(counted_in).sum();
830 let budgets: Vec<f64> = regions
831 .iter()
832 .map(|region| BOUND * counted_in(region) / all_counts)
833 .collect();
834 let fit_region = |r: usize,
835 values: &[f64],
836 previous: Option<&RegionFit>,
837 shared_leverage: Option<f64>|
838 -> Swept {
839 let held = held_at(values);
840 let label = &labels[r];
841 let one = measurement(
842 Value::Known(values[at(Role::Normalization)]),
843 std::array::from_fn(|i| Value::Known(values[time_background + i])),
844 map.room_background.map(|room_background| RoomBackground {
845 rate: Value::Known(values[room]),
846 ..room_background
847 }),
848 vec![restarted(®ions[r], previous)],
849 );
850 let beam = match previous {
851 Some(fit) => BeamStart {
852 beam: fit.beam.clone(),
853 overdispersion: fit.overdispersion[0],
854 at_limit: fit.beam_at_limit,
855 },
856 None => {
857 let known: Vec<f64> = open_per_pixel
858 .iter()
859 .map(|g| regions[r].pixels * g)
860 .collect();
861 open_beam_with_intervals(
862 &map.time_edges_us,
863 ®ions[r].open_counts,
864 &starting,
865 regions[r].open_live.as_deref(),
866 Some(&known),
867 intervals.and_then(|chosen| chosen[r]),
868 )
869 .map_err(|e| named(label, e))?
870 .into()
871 }
872 };
873 let starts = Starts {
874 beams: vec![beam],
875 origin_us,
876 shared_leverage: shared_leverage.map(|extra| vec![extra]),
877 bound: budgets[r],
878 excluded: vec![excluded.read().expect("the bin lists")[r].clone()],
879 };
880 let (fit, answer) = fit_counts_answer(&one, &held, BeamStarts::Given(&starts), None)
881 .map_err(|e| named(label, e))?;
882 let region_fit = fit.regions.into_iter().next().expect("one region");
883 let edge = fitted_temperature
884 && region_fit
885 .temperature_k
886 .is_some_and(|t| t == t_low || t == t_high);
887 if edge {
888 return Err(PipelineError::InvalidParameter(format!(
889 "{label}: its temperature ended at {t_low} K or {t_high} K"
890 )));
891 }
892 if !fit.converged {
893 return Err(PipelineError::InvalidParameter(format!(
894 "{label}: its fit did not converge"
895 )));
896 }
897 let mut terms = answer
898 .region_terms(0, &roles)
899 .map_err(|e| named(label, e))?;
900 if fit.unbounded.is_none() {
901 terms.unbounded = None;
902 }
903 Ok(Inner {
904 fit: region_fit,
905 deviance: fit.deviance,
906 terms,
907 })
908 };
909 let sweep = |values: &[f64],
910 previous: &[Option<RegionFit>],
911 leverage: &[Option<f64>],
912 failures: &[Option<String>],
913 failed: bool|
914 -> Vec<Option<Swept>> {
915 (0..regions.len())
916 .into_par_iter()
917 .map(|r| {
918 (failures[r].is_some() == failed)
919 .then(|| fit_region(r, values, previous[r].as_ref(), leverage[r]))
920 })
921 .collect()
922 };
923 let objective = |inner: &[Option<Inner>], weights: &[Option<Inner>]| -> f64 {
924 inner
925 .iter()
926 .zip(weights)
927 .zip(®ions)
928 .filter_map(|((inner, weights), region)| {
929 Some((inner.as_ref()?, weights.as_ref()?, region))
930 })
931 .map(|(inner, weights, region)| {
932 [®ion.open_counts, ®ion.sample_counts]
933 .into_iter()
934 .zip(&inner.fit.predicted)
935 .enumerate()
936 .map(|(run, (counts, predicted))| {
937 counts
938 .iter()
939 .zip(predicted)
940 .enumerate()
941 .filter(|(k, _)| run == 0 || !inner.fit.excluded_bins.contains(k))
942 .map(|(_, (&y, &mu))| half_deviance(y, mu))
943 .sum::<f64>()
944 / weight(&weights.fit, run)
945 })
946 .sum::<f64>()
947 })
948 .sum()
949 };
950
951 let mut failures: Vec<Option<String>> = vec![None; regions.len()];
952 let mut leverage: Vec<Option<f64>> = vec![None; regions.len()];
953 let mut previous: Vec<Option<RegionFit>> = vec![None; regions.len()];
954 let mut inner: Vec<Option<Inner>> = Vec::with_capacity(regions.len());
955 let mut first_failure = None;
956 for (r, swept) in sweep(&values, &previous, &leverage, &failures, false)
957 .into_iter()
958 .enumerate()
959 {
960 match swept.expect("every region is fitted at the start") {
961 Ok(one) => inner.push(Some(one)),
962 Err(error) if r >= patch_count => return Err(error),
963 Err(error) => {
964 failures[r] = Some(error.to_string());
965 first_failure.get_or_insert(error);
966 inner.push(None);
967 }
968 }
969 }
970 if inner[..patch_count].iter().all(Option::is_none) {
971 return Err(first_failure.expect("a patch failed"));
972 }
973 let absorb = |swept: Vec<Option<Swept>>,
974 inner: &mut [Option<Inner>],
975 failures: &mut [Option<String>]| {
976 let mut joined = false;
977 for (r, one) in swept.into_iter().enumerate() {
978 match one {
979 Some(Ok(one)) => {
980 joined |= failures[r].is_some();
981 failures[r] = None;
982 inner[r] = Some(one);
983 }
984 Some(Err(error)) => {
985 failures[r] = Some(error.to_string());
986 inner[r] = None;
987 }
988 None => {}
989 }
990 }
991 joined
992 };
993 let none_left = |failures: &[Option<String>]| {
994 let reason = failures[..patch_count].iter().flatten().last();
995 invalid(format!(
996 "every patch of the map failed; the last: {}",
997 reason.map_or("", String::as_str)
998 ))
999 };
1000 let counted = 2 * bins;
1001 let inverse_over = |total: &Mat<f64>, diagonal: &[f64], set: &[usize]| {
1002 let mut block = FlatMatrix::zeros(set.len(), set.len());
1003 for (a, &i) in set.iter().enumerate() {
1004 for (b, &j) in set.iter().enumerate() {
1005 *block.get_mut(a, b) = total[(i, j)];
1006 }
1007 }
1008 let diagonal: Vec<f64> = set.iter().map(|&i| diagonal[i]).collect();
1009 information_inverse(&block, &diagonal, counted)
1010 };
1011 let mut steps = 0;
1012 let mut converged = false;
1013 let mut unconverged = None;
1014 let mut weighted = false;
1015 let information = loop {
1016 for (kept, inner) in previous.iter_mut().zip(&inner) {
1017 if let Some(inner) = inner {
1018 *kept = Some(inner.fit.clone());
1019 }
1020 }
1021 let (_, prior_slope) = penalty(&values);
1022 let mut gradient = prior_slope;
1023 let mut total = prior.clone();
1024 let mut diagonal: Vec<f64> = (0..k).map(|a| prior[(a, a)]).collect();
1025 for terms in inner.iter().flatten().map(|inner| &inner.terms) {
1026 for a in 0..k {
1027 gradient[a] += terms.gradient[a];
1028 diagonal[a] += terms.diagonal[a];
1029 for b in 0..k {
1030 total[(a, b)] += terms.information.get(a, b);
1031 }
1032 }
1033 }
1034 let free: Vec<usize> = (0..k)
1035 .filter(|&a| {
1036 let (value, parameter) = (values[fitted[a]], ¶meters[fitted[a]].1);
1037 !(value == parameter.lower && gradient[a] > 0.0
1038 || value == parameter.upper && gradient[a] < 0.0)
1039 })
1040 .collect();
1041 let off_bounds: Vec<usize> = (0..k)
1042 .filter(|&a| {
1043 let (value, parameter) = (values[fitted[a]], ¶meters[fitted[a]].1);
1044 value != parameter.lower && value != parameter.upper
1045 })
1046 .collect();
1047 let spread = inverse_over(&total, &diagonal, &off_bounds)?.spanned;
1048 for (shared_leverage, inner) in leverage.iter_mut().zip(&inner) {
1049 if let Some(Inner { terms, .. }) = inner {
1050 *shared_leverage = Some(
1051 (0..off_bounds.len())
1052 .flat_map(|a| (0..off_bounds.len()).map(move |b| (a, b)))
1053 .map(|(a, b)| {
1054 terms.counted_information.get(off_bounds[a], off_bounds[b])
1055 * spread.get(a, b)
1056 })
1057 .sum(),
1058 );
1059 }
1060 }
1061 let covariance = inverse_over(&total, &diagonal, &free)?.spanned;
1062 let step: Vec<f64> = (0..free.len())
1063 .map(|a| {
1064 -(0..free.len())
1065 .map(|b| covariance.get(a, b) * gradient[free[b]])
1066 .sum::<f64>()
1067 })
1068 .collect();
1069 let decrement: f64 = -0.5
1070 * (0..free.len())
1071 .map(|a| gradient[free[a]] * step[a])
1072 .sum::<f64>();
1073 if decrement < NEWTON_DECREMENT_TOL {
1074 let at_answer = excluded_at(&values);
1075 if weighted && at_answer != *excluded.read().expect("the bin lists") {
1076 if steps == MOST_STEPS {
1077 unconverged = Some(format!("{MOST_STEPS} Newton steps did not converge"));
1078 break None;
1079 }
1080 steps += 1;
1081 *excluded.write().expect("the bin lists") = at_answer;
1082 let retried = sweep(&values, &previous, &leverage, &failures, true);
1083 absorb(retried, &mut inner, &mut failures);
1084 } else if weighted {
1085 converged = true;
1086 let mut unbounded = Some((prior.clone(), penalty(&values).1));
1087 for terms in inner.iter().flatten().map(|inner| &inner.terms) {
1088 match (&mut unbounded, &terms.unbounded) {
1089 (Some((total, slope)), Some((information, region_slope))) => {
1090 for a in 0..k {
1091 slope[a] += region_slope[a];
1092 for b in 0..k {
1093 total[(a, b)] += information.get(a, b);
1094 }
1095 }
1096 }
1097 _ => unbounded = None,
1098 }
1099 }
1100 break Some((total, diagonal, unbounded));
1101 }
1102 weighted = true;
1103 let swept = sweep(&values, &previous, &leverage, &failures, false);
1104 absorb(swept, &mut inner, &mut failures);
1105 if let Some(reason) = failures[patch_count..].iter().flatten().next() {
1106 unconverged = Some(reason.clone());
1107 break None;
1108 }
1109 if inner[..patch_count].iter().all(Option::is_none) {
1110 return none_left(&failures);
1111 }
1112 continue;
1113 }
1114 if steps == MOST_STEPS {
1115 unconverged = Some(format!("{MOST_STEPS} Newton steps did not converge"));
1116 break None;
1117 }
1118 steps += 1;
1119 let before = objective(&inner, &inner) + penalty(&values).0;
1120 let mut lost: Vec<(usize, String)> = Vec::new();
1121 let accepted = (0..=MOST_HALVINGS).find_map(|halving| {
1122 let mut trial = values.clone();
1123 for (a, &f) in free.iter().enumerate() {
1124 let parameter = ¶meters[fitted[f]].1;
1125 trial[fitted[f]] = (values[fitted[f]] + step[a] / f64::powi(2.0, halving))
1126 .clamp(parameter.lower, parameter.upper);
1127 }
1128 let swept = sweep(&trial, &previous, &leverage, &failures, false);
1129 lost = swept
1130 .iter()
1131 .enumerate()
1132 .filter_map(|(r, one)| match one {
1133 Some(Err(error)) => Some((r, error.to_string())),
1134 _ => None,
1135 })
1136 .collect();
1137 if !lost.is_empty() {
1138 return None;
1139 }
1140 let candidate: Vec<Option<Inner>> = swept
1141 .into_iter()
1142 .map(|one| one.and_then(Result::ok))
1143 .collect();
1144 (objective(&candidate, &inner) + penalty(&trial).0 <= before)
1145 .then_some((trial, candidate))
1146 });
1147 match accepted {
1148 Some((trial, candidate)) => {
1149 values = trial;
1150 inner = candidate;
1151 let retried = sweep(&values, &previous, &leverage, &failures, true);
1152 weighted = !absorb(retried, &mut inner, &mut failures);
1153 }
1154 None if !lost.is_empty() && lost.iter().all(|&(r, _)| r < patch_count) => {
1155 for (r, reason) in lost {
1156 failures[r] = Some(format!("{reason} (at a halved step not taken)"));
1157 inner[r] = None;
1158 }
1159 weighted = false;
1160 if inner[..patch_count].iter().all(Option::is_none) {
1161 return none_left(&failures);
1162 }
1163 }
1164 None => {
1165 let empty_first = lost
1166 .iter()
1167 .position(|&(r, _)| r >= patch_count)
1168 .unwrap_or(0);
1169 unconverged = Some(lost.into_iter().nth(empty_first).map_or_else(
1170 || "no halving of a Newton step lowered the objective".to_string(),
1171 |(_, reason)| reason,
1172 ));
1173 break None;
1174 }
1175 }
1176 };
1177
1178 let interior: Vec<usize> = (0..k)
1179 .filter(|&a| {
1180 let (value, parameter) = (values[fitted[a]], ¶meters[fitted[a]].1);
1181 value != parameter.lower && value != parameter.upper
1182 })
1183 .collect();
1184 let embedded = |set: &[usize], inverse: &InformationInverse| {
1185 let mut covariance = FlatMatrix::zeros(k, k);
1186 covariance.data.fill(f64::NAN);
1187 for (a, &i) in set.iter().enumerate() {
1188 for (b, &j) in set.iter().enumerate() {
1189 if inverse.resolved[a] && inverse.resolved[b] {
1190 *covariance.get_mut(i, j) = inverse.determined.get(a, b);
1191 }
1192 }
1193 }
1194 covariance
1195 };
1196 let interior_inverse = match &information {
1197 Some((total, diagonal, _)) => Some(inverse_over(total, diagonal, &interior)?),
1198 None => None,
1199 };
1200 let shared_covariance = interior_inverse
1201 .as_ref()
1202 .map(|inverse| embedded(&interior, inverse));
1203 let propagated = interior_inverse
1204 .as_ref()
1205 .map(|inverse| propagated(k, &interior, inverse));
1206 let null: Vec<Vec<f64>> = interior_inverse
1207 .iter()
1208 .flat_map(|inverse| &inverse.undetermined)
1209 .map(|direction| {
1210 let mut along = vec![0.0; k];
1211 for (a, &i) in interior.iter().enumerate() {
1212 along[i] = direction[a];
1213 }
1214 along
1215 })
1216 .collect();
1217 let every: Vec<usize> = (0..k).collect();
1218 let unbounded = match &information {
1219 Some((_, diagonal, Some((total, gradient)))) => {
1220 let inverse = inverse_over(total, diagonal, &every)?;
1221 let mean: Vec<f64> = (0..k)
1222 .map(|a| {
1223 if inverse.resolved[a] {
1224 values[fitted[a]]
1225 - (0..k)
1226 .map(|b| inverse.determined.get(a, b) * gradient[b])
1227 .sum::<f64>()
1228 } else {
1229 f64::NAN
1230 }
1231 })
1232 .collect();
1233 Some((mean, embedded(&every, &inverse)))
1234 }
1235 _ => None,
1236 };
1237 let pulse_consistency = match (&calibrated, &unbounded) {
1238 (Some((numbers, at)), Some((mean, inverse))) => {
1239 let mut posterior = FlatMatrix::zeros(at.len(), at.len());
1240 for (a, &i) in at.iter().enumerate() {
1241 for (b, &j) in at.iter().enumerate() {
1242 *posterior.get_mut(a, b) = inverse.get(i, j);
1243 }
1244 }
1245 let estimate: Vec<f64> = at.iter().map(|&i| mean[i]).collect();
1246 consistency(numbers, &estimate, &posterior)?
1247 }
1248 _ => None,
1249 };
1250 let measured_pulls = unbounded.as_ref().map(|(mean, inverse)| {
1251 (0..k)
1252 .filter_map(|a| match parameters[fitted[a]].0 {
1253 Value::Measured { value, sd } => {
1254 Some((mean[a] - value) / (sd * sd - inverse.get(a, a)).sqrt())
1255 }
1256 _ => None,
1257 })
1258 .collect()
1259 });
1260 let template: Vec<(Role, String)> = material
1261 .isotopes
1262 .iter()
1263 .enumerate()
1264 .filter(|(_, (_, density))| !matches!(density, Value::Known(_)))
1265 .map(|(isotope, (data, _))| {
1266 (
1267 Role::Density { region: 0, isotope },
1268 format!("density of {}", data.isotope),
1269 )
1270 })
1271 .chain(
1272 (!matches!(material.temperature_k, Value::Known(_)))
1273 .then(|| (Role::Temperature { region: 0 }, "temperature".to_string())),
1274 )
1275 .chain(
1276 (!matches!(material.thickness_variance, Value::Known(_))).then(|| {
1277 (
1278 Role::ThicknessVariance { region: 0 },
1279 "thickness variance".to_string(),
1280 )
1281 }),
1282 )
1283 .chain(
1284 map.background
1285 .iter()
1286 .enumerate()
1287 .filter(|(_, value)| !matches!(value, Value::Known(_)))
1288 .map(|(term, _)| {
1289 (
1290 Role::Background { region: 0, term },
1291 BACKGROUND_TERMS[term].to_string(),
1292 )
1293 }),
1294 )
1295 .chain(
1296 (!matches!(map.gamma_transmission, Value::Known(_))).then(|| {
1297 (
1298 Role::GammaTransmission { region: 0 },
1299 "gamma transmission".to_string(),
1300 )
1301 }),
1302 )
1303 .chain((!matches!(map.capture_gammas, Value::Known(_))).then(|| {
1304 (
1305 Role::CaptureGammas { region: 0 },
1306 "capture gammas".to_string(),
1307 )
1308 }))
1309 .chain(
1310 (!matches!(map.in_scatter, Value::Known(_)))
1311 .then(|| (Role::InScatter { region: 0 }, "in-scatter".to_string())),
1312 )
1313 .collect();
1314 let q = template.len();
1315
1316 let (rows, cols) = patches.dim();
1317 let blank = Array2::from_elem((rows, cols), f64::NAN);
1318 let isotopes = material.isotopes.len();
1319 let mut densities = vec![blank.clone(); isotopes];
1320 let mut density_sd = vec![blank.clone(); isotopes];
1321 let mut temperature_k = blank.clone();
1322 let mut temperature_sd_k = blank.clone();
1323 let mut overdispersion = [blank.clone(), blank];
1324 let mut identified = Array2::from_elem((rows, cols), false);
1325 let mut failed = Array2::from_elem((rows, cols), None);
1326 let mut fits = Array2::from_elem((rows, cols), None);
1327 let mut covariance = Array2::from_elem((rows, cols), None);
1328 let mut sensitivity = Array2::from_elem((rows, cols), None);
1329 for (r, &patch) in fitted_patches.iter().enumerate() {
1330 let Some(Inner { fit, terms, .. }) = &inner[r] else {
1331 failed[patch] = failures[r].clone();
1332 continue;
1333 };
1334 let rows_of: Vec<Option<usize>> = template
1335 .iter()
1336 .map(|(role, _)| terms.quantities.iter().position(|q| q == role))
1337 .collect();
1338 let mut slopes = FlatMatrix::zeros(q, k);
1339 for (a, row) in rows_of.iter().enumerate() {
1340 for s in 0..k {
1341 *slopes.get_mut(a, s) = row.map_or(f64::NAN, |i| terms.sensitivity.get(i, s));
1342 }
1343 }
1344 let marginal = propagated.as_ref().map(|shared| {
1345 let mut marginal = FlatMatrix::zeros(q, q);
1346 for (a, row_a) in rows_of.iter().enumerate() {
1347 for (b, row_b) in rows_of.iter().enumerate() {
1348 *marginal.get_mut(a, b) = match (row_a, row_b) {
1349 (Some(i), Some(j)) => marginal_entry(
1350 &terms.covariance,
1351 &terms.sensitivity,
1352 shared,
1353 &null,
1354 (*i, *j),
1355 ),
1356 _ => f64::NAN,
1357 };
1358 }
1359 }
1360 marginal
1361 });
1362 let sd = |role: Role| {
1363 template
1364 .iter()
1365 .position(|(r, _)| *r == role)
1366 .zip(marginal.as_ref())
1367 .map_or(f64::NAN, |(a, marginal)| marginal.get(a, a).sqrt())
1368 };
1369 for (m, &density) in fit.densities.iter().enumerate() {
1370 densities[m][patch] = density;
1371 density_sd[m][patch] = sd(Role::Density {
1372 region: 0,
1373 isotope: m,
1374 });
1375 }
1376 temperature_k[patch] = fit.temperature_k.unwrap_or(f64::NAN);
1377 temperature_sd_k[patch] = sd(Role::Temperature { region: 0 });
1378 for (run, phi) in overdispersion.iter_mut().zip(fit.overdispersion) {
1379 run[patch] = phi.unwrap_or(f64::NAN);
1380 }
1381 identified[patch] = converged
1382 && template.iter().all(|&(role, _)| match role {
1383 Role::Density { isotope, .. } => {
1384 sd(role).is_finite() || fit.densities[isotope] == 0.0
1385 }
1386 Role::Temperature { .. } => sd(role).is_finite(),
1387 _ => true,
1388 });
1389 fits[patch] = Some(fit.clone());
1390 covariance[patch] = marginal;
1391 sensitivity[patch] = Some(slopes);
1392 }
1393
1394 Ok(CountsMap {
1395 measurement: map.clone(),
1396 patches,
1397 fits,
1398 failed,
1399 empty: (!empty.is_empty())
1400 .then(|| inner.last().and_then(|i| i.as_ref().map(|i| i.fit.clone())))
1401 .flatten(),
1402 densities,
1403 density_sd,
1404 temperature_k,
1405 temperature_sd_k,
1406 overdispersion,
1407 identified,
1408 normalization: values[at(Role::Normalization)],
1409 time_background: std::array::from_fn(|i| values[time_background + i]),
1410 room_background: values[room],
1411 t0_us: values[t0],
1412 flight_path_m: values[t0 + 1],
1413 alpha: [values[t0 + 2], values[t0 + 3]],
1414 beta: [values[t0 + 4], values[t0 + 5]],
1415 r: values[t0 + 6],
1416 fwhm_squared_us2: values[t0 + 7],
1417 r_high: calibration.pulse.r_high.map(|_| values[t0 + 8]),
1418 detector_tail: calibration
1419 .pulse
1420 .detector_tail
1421 .map(|_| [values[t0 + 9], values[t0 + 10]]),
1422 shared: fitted
1423 .iter()
1424 .map(|&i| {
1425 [
1426 "normalization",
1427 "time background C",
1428 "time background A",
1429 "time background τ",
1430 "room background ρ",
1431 "t0",
1432 "flight path",
1433 ]
1434 .into_iter()
1435 .chain(PULSE_NUMBERS)
1436 .nth(i)
1437 .expect("a shared quantity")
1438 })
1439 .collect(),
1440 shared_covariance,
1441 measured_pulls,
1442 pulse_consistency,
1443 quantities: template.into_iter().map(|(_, name)| name).collect(),
1444 covariances: PatchCovariances {
1445 covariance,
1446 sensitivity,
1447 carried: propagated,
1448 },
1449 deviance: inner.iter().flatten().map(|i| i.deviance).sum(),
1450 converged,
1451 unconverged,
1452 steps,
1453 })
1454}
1455
1456type Swept = Result<Inner, PipelineError>;
1457
1458struct Inner {
1459 fit: RegionFit,
1460 deviance: f64,
1461 terms: RegionTerms,
1462}
1463
1464fn propagated(k: usize, interior: &[usize], inverse: &InformationInverse) -> FlatMatrix {
1465 let mut propagated = FlatMatrix::zeros(k, k);
1466 for (a, &i) in interior.iter().enumerate() {
1467 for (b, &j) in interior.iter().enumerate() {
1468 *propagated.get_mut(i, j) = inverse.determined.get(a, b);
1469 }
1470 }
1471 propagated
1472}
1473
1474fn marginal_entry(
1475 covariance: &FlatMatrix,
1476 sensitivity: &FlatMatrix,
1477 shared: &FlatMatrix,
1478 null: &[Vec<f64>],
1479 (i, j): (usize, usize),
1480) -> f64 {
1481 let coupled = |i: usize| moves_along(|s| sensitivity.get(i, s), null);
1482 if coupled(i) || coupled(j) {
1483 return f64::NAN;
1484 }
1485 let k = shared.nrows;
1486 covariance.get(i, j)
1487 + (0..k)
1488 .flat_map(|s| (0..k).map(move |t| (s, t)))
1489 .map(|(s, t)| sensitivity.get(i, s) * shared.get(s, t) * sensitivity.get(j, t))
1490 .sum::<f64>()
1491}
1492
1493fn moves_along(sensitivity: impl Fn(usize) -> f64, null: &[Vec<f64>]) -> bool {
1494 null.iter().any(|direction| {
1495 let terms: Vec<f64> = direction
1496 .iter()
1497 .enumerate()
1498 .map(|(s, &v)| sensitivity(s) * v)
1499 .collect();
1500 let magnitude: f64 = terms.iter().map(|t| t.abs()).sum();
1501 terms.iter().sum::<f64>().abs() > f64::EPSILON.sqrt() * magnitude
1502 })
1503}
1504
1505fn named(label: &str, error: PipelineError) -> PipelineError {
1506 match error {
1507 PipelineError::InvalidParameter(_) | PipelineError::ShapeMismatch(_) => {
1508 labelled(label, error)
1509 }
1510 other => PipelineError::InvalidParameter(format!("{label}: {other}")),
1511 }
1512}
1513
1514fn weight(fit: &RegionFit, run: usize) -> f64 {
1515 fit.overdispersion[run]
1516 .or(fit.overdispersion[0])
1517 .unwrap_or(1.0)
1518}
1519
1520fn restarted(region: &Region, previous: Option<&RegionFit>) -> Region {
1521 let mut region = region.clone();
1522 let Some(fit) = previous else {
1523 return region;
1524 };
1525 if let Some(material) = &mut region.material {
1526 for ((_, density), &at) in material.isotopes.iter_mut().zip(&fit.densities) {
1527 *density = restart(*density, at);
1528 }
1529 if let Some(t) = fit.temperature_k {
1530 material.temperature_k = restart(material.temperature_k, t);
1531 }
1532 if let Some(v) = fit.thickness_variance {
1533 material.thickness_variance = restart(material.thickness_variance, v);
1534 }
1535 }
1536 for (term, &at) in region.background.iter_mut().zip(&fit.background) {
1537 *term = restart(*term, at);
1538 }
1539 region.gamma_transmission = restart(region.gamma_transmission, fit.gamma_transmission);
1540 region.capture_gammas = restart(region.capture_gammas, fit.capture_gammas);
1541 region.in_scatter = restart(region.in_scatter, fit.in_scatter);
1542 region
1543}
1544
1545fn restart(value: Value, at: f64) -> Value {
1546 match value {
1547 Value::Fitted(_) => Value::Fitted(at),
1548 Value::Within { lower, upper, .. } => Value::Within {
1549 start: at,
1550 lower,
1551 upper,
1552 },
1553 Value::Known(_) | Value::Measured { .. } => value,
1554 }
1555}
1556
1557fn summed(
1558 map: &MapMeasurement<'_>,
1559 pixels: &[(usize, usize)],
1560) -> Result<[Vec<f64>; 2], PipelineError> {
1561 let bins = map.open_counts.dim().0;
1562 let mut sums = [vec![0.0; bins], vec![0.0; bins]];
1563 for ((run, counts), sum) in [
1564 ("open-beam", map.open_counts),
1565 ("sample", map.sample_counts),
1566 ]
1567 .into_iter()
1568 .zip(&mut sums)
1569 {
1570 for &(y, x) in pixels {
1571 for (k, (&count, total)) in counts
1572 .slice(s![.., y, x])
1573 .iter()
1574 .zip(sum.iter_mut())
1575 .enumerate()
1576 {
1577 if !whole(count) {
1578 return Err(PipelineError::InvalidParameter(format!(
1579 "{run} counts must be whole non-negative numbers, got {count} in bin \
1580 {k} of pixel ({y}, {x})"
1581 )));
1582 }
1583 *total += count;
1584 }
1585 }
1586 }
1587 Ok(sums)
1588}
1589
1590#[cfg(test)]
1591mod tests {
1592 use super::*;
1593
1594 #[test]
1595 fn a_patch_quantity_has_the_joint_fits_variance_under_an_oblique_shared_degeneracy() {
1596 let own = [1.0, 2.0, 0.0, 1.0, 3.0];
1597 let first = [1.0, 0.0, 1.0, 2.0, 1.0];
1598 for (column, resolved) in [(own, true), (first, false)] {
1599 let jacobian: Vec<[f64; 3]> = (0..5)
1600 .map(|row| [column[row], first[row], 2.0 * first[row]])
1601 .collect();
1602 let information =
1603 |a: usize, b: usize| -> f64 { jacobian.iter().map(|row| row[a] * row[b]).sum() };
1604 let joint = FlatMatrix {
1605 data: (0..9).map(|e| information(e / 3, e % 3)).collect(),
1606 nrows: 3,
1607 ncols: 3,
1608 };
1609 let diagonal: Vec<f64> = (0..3).map(|a| information(a, a)).collect();
1610 let oracle = information_inverse(&joint, &diagonal, 5).unwrap();
1611 let sensitivity = FlatMatrix {
1612 data: (1..3)
1613 .map(|s| -information(0, s) / information(0, 0))
1614 .collect(),
1615 nrows: 1,
1616 ncols: 2,
1617 };
1618 let profiled = FlatMatrix {
1619 data: (0..4)
1620 .map(|e| {
1621 let (s, t) = (1 + e / 2, 1 + e % 2);
1622 information(s, t)
1623 - information(s, 0) * information(0, t) / information(0, 0)
1624 })
1625 .collect(),
1626 nrows: 2,
1627 ncols: 2,
1628 };
1629 let shared = information_inverse(&profiled, &diagonal[1..], 5).unwrap();
1630 let own_covariance = FlatMatrix {
1631 data: vec![1.0 / information(0, 0)],
1632 nrows: 1,
1633 ncols: 1,
1634 };
1635 let variance = marginal_entry(
1636 &own_covariance,
1637 &sensitivity,
1638 &propagated(2, &[0, 1], &shared),
1639 &shared.undetermined,
1640 (0, 0),
1641 );
1642 assert_eq!(oracle.resolved[0], resolved);
1643 if resolved {
1644 let expected = oracle.determined.get(0, 0);
1645 assert!(
1646 (variance / expected - 1.0).abs() <= 1e-9,
1647 "{variance} vs {expected}"
1648 );
1649 } else {
1650 assert!(variance.is_nan(), "{variance}");
1651 }
1652 }
1653 }
1654
1655 #[test]
1656 fn a_quantity_moves_along_a_null_direction_unless_its_slopes_cancel_on_it() {
1657 let null = vec![vec![1.0, -2.0]];
1658 assert!(moves_along(|s| [1.0, 0.0][s], &null));
1659 assert!(!moves_along(|s| [2.0, 1.0][s], &null));
1660 assert!(!moves_along(|_| 0.0, &null));
1661 }
1662}