1use faer::dyn_stack::{MemBuffer, MemStack};
17use faer::linalg::svd::{ComputeSvdVectors, svd, svd_scratch};
18use faer::{Mat, Par, Side};
19
20use crate::error::FittingError;
21use crate::lm::{FitModel, FlatMatrix};
22use crate::parameters::{FitParameter, ParameterSet};
23
24#[derive(Debug, Clone)]
26pub struct PoissonConfig {
27 pub max_iter: usize,
29 pub compute_covariance: bool,
31}
32
33impl Default for PoissonConfig {
34 fn default() -> Self {
35 Self {
36 max_iter: 200,
37 compute_covariance: true,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq)]
45pub struct Prior {
46 pub(crate) parameters: Vec<usize>,
47 pub(crate) mean: Vec<f64>,
48 pub(crate) factor: Vec<f64>,
49}
50
51impl Prior {
52 pub fn measured(parameter: usize, mean: f64, sd: f64) -> Self {
56 Self {
57 parameters: vec![parameter],
58 mean: vec![mean],
59 factor: vec![sd],
60 }
61 }
62
63 pub fn correlated(
75 parameters: &[usize],
76 mean: &[f64],
77 covariance: &FlatMatrix,
78 ) -> Result<Self, FittingError> {
79 let k = parameters.len();
80 if k == 0 {
81 return Err(FittingError::InvalidConfig(
82 "a prior needs at least one parameter".into(),
83 ));
84 }
85 for (expected, actual, field) in [
86 (k, mean.len(), "prior mean"),
87 (k, covariance.nrows, "prior covariance rows"),
88 (k, covariance.ncols, "prior covariance columns"),
89 (k * k, covariance.data.len(), "prior covariance entries"),
90 ] {
91 if actual != expected {
92 return Err(FittingError::LengthMismatch {
93 expected,
94 actual,
95 field,
96 });
97 }
98 }
99 if !mean.iter().all(|m| m.is_finite())
100 || (0..k).any(|i| parameters[..i].contains(¶meters[i]))
101 {
102 return Err(FittingError::InvalidConfig(format!(
103 "a prior needs distinct parameters and a finite mean; got {parameters:?} and \
104 {mean:?}"
105 )));
106 }
107 let sd: Vec<f64> = (0..k).map(|i| covariance.get(i, i).sqrt()).collect();
108 if !(covariance.data.iter().all(|c| c.is_finite())
109 && sd.iter().all(|&s| s > 0.0)
110 && symmetric(k, |i, j| covariance.get(i, j)))
111 {
112 return Err(FittingError::InvalidConfig(format!(
113 "a prior covariance must be finite and symmetric with a positive diagonal; \
114 got {covariance:?}"
115 )));
116 }
117 let correlation = Mat::from_fn(k, k, |i, j| {
118 if i == j {
119 1.0
120 } else {
121 covariance.get(i, j) / sd[i] / sd[j]
122 }
123 });
124 let eigenvalues = correlation
125 .self_adjoint_eigenvalues(Side::Lower)
126 .map_err(|e| FittingError::EvaluationFailed(format!("{e:?}")))?;
127 let (smallest, largest) = (eigenvalues[0], eigenvalues[k - 1]);
128 if !(smallest > 0.0 && largest <= MAX_PRIOR_CONDITION * smallest) {
129 return Err(FittingError::InvalidConfig(format!(
130 "a prior covariance must be positive definite with a correlation condition \
131 number within {MAX_PRIOR_CONDITION:e}; its correlation eigenvalues are \
132 {eigenvalues:?}"
133 )));
134 }
135 Ok(Self::factored(parameters, mean, covariance))
136 }
137
138 pub(crate) fn factored(parameters: &[usize], mean: &[f64], covariance: &FlatMatrix) -> Self {
139 let k = parameters.len();
140 let sd: Vec<f64> = (0..k).map(|i| covariance.get(i, i).sqrt()).collect();
141 let correlation = |i: usize, j: usize| {
142 if i == j {
143 1.0
144 } else {
145 covariance.get(i, j) / sd[i] / sd[j]
146 }
147 };
148 let mut factor = vec![0.0; k * k];
149 for i in 0..k {
150 for j in 0..=i {
151 let dot: f64 = (0..j).map(|l| factor[i * k + l] * factor[j * k + l]).sum();
152 factor[i * k + j] = if i == j {
153 (correlation(i, i) - dot).sqrt()
154 } else {
155 (correlation(i, j) - dot) / factor[j * k + j]
156 };
157 }
158 }
159 for i in 0..k {
160 for j in 0..=i {
161 factor[i * k + j] *= sd[i];
162 }
163 }
164 Self {
165 parameters: parameters.to_vec(),
166 mean: mean.to_vec(),
167 factor,
168 }
169 }
170
171 pub(crate) fn covariance(&self, i: usize, j: usize) -> f64 {
172 let k = self.parameters.len();
173 (0..=i.min(j))
174 .map(|l| self.factor[i * k + l] * self.factor[j * k + l])
175 .sum()
176 }
177
178 pub(crate) fn is_valid(&self) -> bool {
179 let k = self.parameters.len();
180 self.mean.iter().all(|m| m.is_finite())
181 && self.factor.iter().all(|f| f.is_finite())
182 && (0..k).all(|j| self.factor[j * k + j] > 0.0)
183 }
184
185 pub(crate) fn whiten(&self, v: &mut [f64]) {
186 let k = self.parameters.len();
187 for i in 0..k {
188 let dot: f64 = (0..i).map(|j| self.factor[i * k + j] * v[j]).sum();
189 v[i] = (v[i] - dot) / self.factor[i * k + i];
190 }
191 }
192
193 fn residual(&self, values: &[f64]) -> Vec<f64> {
194 let mut difference: Vec<f64> = self
195 .parameters
196 .iter()
197 .zip(&self.mean)
198 .map(|(&p, m)| values[p] - m)
199 .collect();
200 self.whiten(&mut difference);
201 difference
202 }
203
204 fn inverse_factor(&self) -> Vec<f64> {
205 let k = self.parameters.len();
206 let mut inverse = vec![0.0; k * k];
207 for j in 0..k {
208 let mut column = vec![0.0; k];
209 column[j] = 1.0;
210 self.whiten(&mut column);
211 for (i, c) in column.iter().enumerate() {
212 inverse[i * k + j] = *c;
213 }
214 }
215 inverse
216 }
217}
218
219pub(crate) fn symmetric(k: usize, entry: impl Fn(usize, usize) -> f64) -> bool {
220 (0..k).all(|i| {
221 (0..i).all(|j| {
222 let scale = (entry(i, i) * entry(j, j)).abs().sqrt();
223 (entry(j, i) - entry(i, j)).abs() <= 1e-12 * scale
224 })
225 })
226}
227
228#[derive(Debug, Clone)]
230pub struct PoissonResult {
231 pub deviance: f64,
234 pub iterations: usize,
236 pub converged: bool,
240 pub params: Vec<f64>,
242 pub covariance: Option<FlatMatrix>,
246 pub uncertainties: Option<Vec<Option<f64>>>,
251 pub on_bound: Vec<bool>,
253 pub leverage: Option<Vec<f64>>,
259 pub unbounded: Option<Unbounded>,
264}
265
266#[derive(Debug, Clone)]
275pub struct Unbounded {
276 pub mean: Vec<f64>,
277 pub covariance: FlatMatrix,
278}
279
280pub const NEWTON_DECREMENT_TOL: f64 = 1e-6;
283
284const DEGENERATE_EIGENVALUE: f64 = 1e-12;
285
286const MAX_PRIOR_CONDITION: f64 = 1e-2 / DEGENERATE_EIGENVALUE;
287
288const MAX_REJECTIONS: usize = 60;
289
290const INITIAL_DAMPING: f64 = 1e-3;
291
292const DAMPING_FACTOR: f64 = 10.0;
293
294pub fn half_deviance(obs: f64, mean: f64) -> f64 {
300 let half_sum = obs / 2.0 + mean / 2.0;
301 let difference = obs - mean;
302 if difference.abs() < 0.2 * half_sum {
303 let v = difference / 2.0 / half_sum;
304 let mut sum = difference * v;
305 let mut term = obs * (2.0 * v);
306 let mut j = 1.0;
307 loop {
308 term *= v * v;
309 let next = sum + term / (2.0 * j + 1.0);
310 if next == sum {
311 return sum;
312 }
313 sum = next;
314 j += 1.0;
315 }
316 } else if obs == 0.0 {
317 mean
318 } else {
319 obs * (obs.ln() - mean.ln()) + mean - obs
320 }
321}
322
323fn deviance(y_obs: &[f64], y_model: &[f64]) -> f64 {
324 y_obs
325 .iter()
326 .zip(y_model)
327 .map(|(&obs, &mean)| {
328 if mean > 0.0 || (mean == 0.0 && obs == 0.0) {
329 half_deviance(obs, mean)
330 } else {
331 f64::INFINITY
332 }
333 })
334 .sum()
335}
336
337fn objective(y_obs: &[f64], y_model: &[f64], priors: &[Prior], params: &ParameterSet) -> f64 {
338 let values = params.all_values();
339 deviance(y_obs, y_model)
340 + priors
341 .iter()
342 .map(|p| 0.5 * p.residual(&values).iter().map(|w| w.powi(2)).sum::<f64>())
343 .sum::<f64>()
344}
345
346struct Linearization {
347 weighted: FlatMatrix,
348 residual: Vec<f64>,
349 zero_slope: Vec<f64>,
350 gradient: Vec<f64>,
351}
352
353fn linearize(
354 model: &dyn FitModel,
355 params: &ParameterSet,
356 free: &[usize],
357 priors: &[Prior],
358 y_obs: &[f64],
359 y_model: &[f64],
360) -> Result<Linearization, FittingError> {
361 let mut weighted = model
362 .analytical_jacobian(¶ms.all_values(), free, y_model)
363 .ok_or_else(|| {
364 FittingError::InvalidConfig(
365 "poisson_fit needs a model with an analytical Jacobian".into(),
366 )
367 })?;
368 for (expected, actual, field) in [
369 (y_obs.len(), weighted.nrows, "analytical Jacobian rows"),
370 (free.len(), weighted.ncols, "analytical Jacobian columns"),
371 ] {
372 if expected != actual {
373 return Err(FittingError::LengthMismatch {
374 expected,
375 actual,
376 field,
377 });
378 }
379 }
380 let root: Vec<f64> = y_model.iter().map(|mean| mean.sqrt()).collect();
381 let mut zero_slope = vec![0.0; weighted.ncols];
382 for (i, &r) in root.iter().enumerate() {
383 for (j, z) in zero_slope.iter_mut().enumerate() {
384 let slope = weighted.get(i, j);
385 if r > 0.0 {
386 *weighted.get_mut(i, j) = slope / r;
387 } else {
388 *z += slope;
389 *weighted.get_mut(i, j) = 0.0;
390 }
391 }
392 }
393 let mut residual: Vec<f64> = y_obs
394 .iter()
395 .zip(y_model)
396 .zip(&root)
397 .map(|((&obs, &mean), &r)| if r > 0.0 { (mean - obs) / r } else { 0.0 })
398 .collect();
399 let mut row = weighted.nrows;
400 weighted.nrows += priors.iter().map(|p| p.parameters.len()).sum::<usize>();
401 weighted.data.resize(weighted.nrows * weighted.ncols, 0.0);
402 let values = params.all_values();
403 for prior in priors {
404 let k = prior.parameters.len();
405 let inverse = prior.inverse_factor();
406 for (i, w) in prior.residual(&values).into_iter().enumerate() {
407 for (j, parameter) in prior.parameters.iter().enumerate().take(i + 1) {
408 let col = free
409 .iter()
410 .position(|index| index == parameter)
411 .expect("priors are on free parameters");
412 *weighted.get_mut(row, col) = inverse[i * k + j];
413 }
414 residual.push(w);
415 row += 1;
416 }
417 }
418 let gradient = zero_slope
419 .iter()
420 .enumerate()
421 .map(|(j, z)| {
422 z + (0..weighted.nrows)
423 .map(|i| weighted.get(i, j) * residual[i])
424 .sum::<f64>()
425 })
426 .collect();
427 Ok(Linearization {
428 weighted,
429 residual,
430 zero_slope,
431 gradient,
432 })
433}
434
435fn column_scale(weighted: &FlatMatrix, col: usize) -> (f64, f64) {
436 let peak = (0..weighted.nrows).fold(0.0_f64, |m, i| m.max(weighted.get(i, col).abs()));
437 let length = (0..weighted.nrows)
438 .map(|i| (weighted.get(i, col) / peak).powi(2))
439 .sum::<f64>()
440 .sqrt();
441 (peak, length)
442}
443
444struct Decomposition {
445 columns: Vec<usize>,
446 largest: Vec<f64>,
447 length: Vec<f64>,
448 rows: usize,
449 singular: Vec<f64>,
450 left: Mat<f64>,
451 right: Vec<Vec<f64>>,
452}
453
454impl Decomposition {
455 fn new(weighted: &FlatMatrix, columns: &[usize]) -> Option<Self> {
456 let (mut kept, mut largest, mut length) = (vec![], vec![], vec![]);
457 for &col in columns {
458 let (peak, norm) = column_scale(weighted, col);
459 if peak > 0.0 {
460 kept.push(col);
461 largest.push(peak);
462 length.push(norm);
463 }
464 }
465 let (rows, k) = (weighted.nrows, kept.len());
466 let scaled = Mat::from_fn(rows, k, |row, i| {
467 weighted.get(row, kept[i]) / largest[i] / length[i]
468 });
469 let mut values = Mat::<f64>::zeros(rows.min(k), rows.min(k));
470 let mut left = Mat::<f64>::zeros(rows, rows.min(k));
471 let mut v = Mat::<f64>::zeros(k, k);
472 let params = Default::default();
473 svd(
474 scaled.as_ref(),
475 values.diagonal_mut(),
476 Some(left.as_mut()),
477 Some(v.as_mut()),
478 Par::Seq,
479 MemStack::new(&mut MemBuffer::new(svd_scratch::<f64>(
480 rows,
481 k,
482 ComputeSvdVectors::Thin,
483 ComputeSvdVectors::Full,
484 Par::Seq,
485 params,
486 ))),
487 params,
488 )
489 .ok()?;
490 Some(Self {
491 columns: kept,
492 largest,
493 length,
494 rows,
495 singular: (0..k)
496 .map(|j| if j < rows { values[(j, j)] } else { 0.0 })
497 .collect(),
498 left,
499 right: (0..k)
500 .map(|j| (0..k).map(|i| v[(i, j)]).collect())
501 .collect(),
502 })
503 }
504
505 fn unscale(&self, i: usize, value: f64) -> f64 {
506 value / self.length[i] / self.largest[i]
507 }
508
509 fn spanned(&self) -> impl Iterator<Item = usize> + '_ {
510 let largest = self.singular.iter().fold(0.0_f64, |m, &s| m.max(s));
511 let rank_floor = f64::EPSILON * self.rows.max(self.singular.len()) as f64 * largest;
512 (0..self.singular.len()).filter(move |&k| self.singular[k] > rank_floor)
513 }
514
515 fn along(&self, k: usize, linear: &Linearization) -> f64 {
516 let zero_part: f64 = self
517 .columns
518 .iter()
519 .enumerate()
520 .map(|(i, &col)| self.right[k][i] * self.unscale(i, linear.zero_slope[col]))
521 .sum();
522 let residual_part: f64 = (0..self.rows)
523 .map(|row| self.left[(row, k)] * linear.residual[row])
524 .sum();
525 residual_part + zero_part / self.singular[k]
526 }
527
528 fn newton_decrement(&self, linear: &Linearization) -> f64 {
529 0.5 * self
530 .spanned()
531 .map(|k| self.along(k, linear).powi(2))
532 .sum::<f64>()
533 }
534
535 fn is_determined(&self, k: usize) -> bool {
536 self.singular[k].powi(2) >= DEGENERATE_EIGENVALUE
537 }
538
539 fn determined(&self) -> impl Iterator<Item = usize> + '_ {
540 (0..self.singular.len()).filter(|&k| self.is_determined(k))
541 }
542
543 fn step(
544 &self,
545 directions: impl Iterator<Item = usize>,
546 linear: &Linearization,
547 n_free: usize,
548 damping: f64,
549 ) -> Vec<f64> {
550 let mut direction = vec![0.0; n_free];
551 for k in directions {
552 let s = self.singular[k];
553 let coefficient = self.along(k, linear) * s / (s * s + damping);
554 for (i, &col) in self.columns.iter().enumerate() {
555 direction[col] += self.unscale(i, self.right[k][i] * coefficient);
556 }
557 }
558 direction
559 }
560
561 fn leverage(&self, counts: usize) -> Vec<f64> {
562 (0..counts)
563 .map(|row| self.spanned().map(|k| self.left[(row, k)].powi(2)).sum())
564 .collect()
565 }
566
567 fn error_bars(&self, n_free: usize) -> (FlatMatrix, Vec<Option<f64>>) {
568 let n = self.columns.len();
569 let determined: Vec<bool> = (0..n).map(|k| self.is_determined(k)).collect();
570 let largest = self.singular.iter().fold(0.0_f64, |m, &s| m.max(s));
571 let resolved: Vec<bool> = (0..n)
572 .map(|i| {
573 let sensitivity: f64 = (0..n)
574 .filter(|&k| determined[k])
575 .map(|k| self.right[k][i].abs() / self.singular[k])
576 .sum();
577 let rounding = f64::EPSILON * self.rows.max(n) as f64 * largest * sensitivity;
578 (0..n)
579 .filter(|&k| !determined[k])
580 .map(|k| self.right[k][i].powi(2))
581 .sum::<f64>()
582 <= rounding.powi(2)
583 })
584 .collect();
585 let variance = |i: usize, j: usize| -> f64 {
586 (0..n)
587 .filter(|&k| determined[k])
588 .map(|k| self.right[k][i] * self.right[k][j] / self.singular[k].powi(2))
589 .sum()
590 };
591 let (mut covariance, mut errors) = withheld(n_free);
592 for i in (0..n).filter(|&i| resolved[i]) {
593 for j in (0..n).filter(|&j| resolved[j]) {
594 *covariance.get_mut(self.columns[i], self.columns[j]) =
595 self.unscale(j, self.unscale(i, variance(i, j)));
596 }
597 errors[self.columns[i]] = Some(self.unscale(i, variance(i, i).sqrt()));
598 }
599 (covariance, errors)
600 }
601}
602
603#[derive(Debug, Clone)]
616pub struct InformationInverse {
617 pub spanned: FlatMatrix,
618 pub determined: FlatMatrix,
619 pub resolved: Vec<bool>,
620 pub undetermined: Vec<Vec<f64>>,
621}
622
623pub fn information_inverse(
633 information: &FlatMatrix,
634 diagonal: &[f64],
635 rows: usize,
636) -> Result<InformationInverse, FittingError> {
637 let n = information.nrows;
638 if information.ncols != n
639 || information.data.len() != n * n
640 || !information.data.iter().all(|x| x.is_finite())
641 || diagonal.len() != n
642 || !diagonal.iter().all(|d| d.is_finite() && *d >= 0.0)
643 || (0..n).any(|i| diagonal[i] == 0.0 && (0..n).any(|j| information.get(i, j) != 0.0))
644 || !(0..n).all(|i| {
645 (0..i).all(|j| {
646 (information.get(i, j) - information.get(j, i)).abs()
647 <= 1e-12 * (diagonal[i] * diagonal[j]).sqrt()
648 })
649 })
650 {
651 return Err(FittingError::InvalidConfig(format!(
652 "an expected information is a finite symmetric square matrix with a finite \
653 non-negative diagonal to scale it by; got {} by {} with {} entries and {} \
654 diagonal entries",
655 information.nrows,
656 information.ncols,
657 information.data.len(),
658 diagonal.len()
659 )));
660 }
661 let kept: Vec<usize> = (0..n).filter(|&i| diagonal[i] > 0.0).collect();
662 let k = kept.len();
663 if k == 0 {
664 return Ok(InformationInverse {
665 spanned: FlatMatrix::zeros(n, n),
666 determined: FlatMatrix::zeros(n, n),
667 resolved: vec![false; n],
668 undetermined: Vec::new(),
669 });
670 }
671 let scale: Vec<f64> = kept.iter().map(|&i| diagonal[i].sqrt()).collect();
672 let eigen = Mat::from_fn(k, k, |a, b| {
673 information.get(kept[a], kept[b]) / (scale[a] * scale[b])
674 })
675 .self_adjoint_eigen(Side::Lower)
676 .map_err(|e| FittingError::EvaluationFailed(format!("{e:?}")))?;
677 let (values, vectors) = (eigen.S().column_vector(), eigen.U());
678 let singular: Vec<f64> = (0..k).map(|d| values[d].max(0.0).sqrt()).collect();
679 let largest = singular.iter().fold(0.0_f64, |m, &s| m.max(s));
680 let rank_floor = f64::EPSILON * rows.max(k) as f64 * largest.powi(2);
681 let inverse = |keep: &dyn Fn(usize) -> bool| {
682 let mut inverse = FlatMatrix::zeros(n, n);
683 for a in 0..k {
684 for b in 0..k {
685 *inverse.get_mut(kept[a], kept[b]) = (0..k)
686 .filter(|&d| keep(d))
687 .map(|d| vectors[(a, d)] * vectors[(b, d)] / values[d])
688 .sum::<f64>()
689 / (scale[a] * scale[b]);
690 }
691 }
692 inverse
693 };
694 let is_spanned = |d: usize| values[d] > rank_floor;
695 let is_determined = |d: usize| is_spanned(d) && values[d] >= DEGENERATE_EIGENVALUE;
696 let mut resolved = vec![false; n];
697 for a in 0..k {
698 let sensitivity: f64 = (0..k)
699 .filter(|&d| is_determined(d))
700 .map(|d| vectors[(a, d)].abs() / singular[d])
701 .sum();
702 let rounding = f64::EPSILON * rows.max(k) as f64 * largest * sensitivity;
703 resolved[kept[a]] = (0..k)
704 .filter(|&d| !is_determined(d))
705 .map(|d| vectors[(a, d)].powi(2))
706 .sum::<f64>()
707 <= rounding.powi(2);
708 }
709 let undetermined = (0..k)
710 .filter(|&d| !is_determined(d))
711 .map(|d| {
712 let mut direction = vec![0.0; n];
713 for a in 0..k {
714 direction[kept[a]] = vectors[(a, d)] / scale[a];
715 }
716 direction
717 })
718 .collect();
719 Ok(InformationInverse {
720 spanned: inverse(&is_spanned),
721 determined: inverse(&is_determined),
722 resolved,
723 undetermined,
724 })
725}
726
727fn withheld(n_free: usize) -> (FlatMatrix, Vec<Option<f64>>) {
728 let mut covariance = FlatMatrix::zeros(n_free, n_free);
729 covariance.data.fill(f64::NAN);
730 (covariance, vec![None; n_free])
731}
732
733fn on_bound(param: &FitParameter) -> bool {
734 param.value == param.lower || param.value == param.upper
735}
736
737fn held_by_bound(param: &FitParameter, gradient: f64) -> bool {
738 (param.value == param.lower && gradient > 0.0) || (param.value == param.upper && gradient < 0.0)
739}
740
741pub fn poisson_fit(
789 model: &dyn FitModel,
790 y_obs: &[f64],
791 priors: &[Prior],
792 params: &mut ParameterSet,
793 config: &PoissonConfig,
794) -> Result<PoissonResult, FittingError> {
795 if let Some((bin, &obs)) = y_obs
796 .iter()
797 .enumerate()
798 .find(|(_, obs)| !(obs.is_finite() && **obs >= 0.0))
799 {
800 return Err(FittingError::InvalidConfig(format!(
801 "observed counts must be finite and non-negative, got {obs} in bin {bin}"
802 )));
803 }
804 if y_obs.is_empty() {
805 return Err(FittingError::EmptyData);
806 }
807 if let Some(p) = params.params.iter().find(|p| {
808 !p.value.is_finite()
809 || (!p.fixed
810 && (p.lower.is_nan()
811 || p.upper.is_nan()
812 || p.lower > p.upper
813 || p.lower == f64::INFINITY
814 || p.upper == f64::NEG_INFINITY))
815 }) {
816 return Err(FittingError::InvalidConfig(format!(
817 "parameter {} = {} with bounds [{}, {}]",
818 p.name, p.value, p.lower, p.upper
819 )));
820 }
821 let covered: Vec<usize> = priors.iter().flat_map(|p| p.parameters.clone()).collect();
822 if let Some((i, prior)) = priors.iter().enumerate().find(|(_, p)| {
823 p.parameters.iter().any(|¶meter| {
824 params.params.get(parameter).is_none_or(|param| param.fixed)
825 || covered.iter().filter(|&&c| c == parameter).count() > 1
826 }) || !p.is_valid()
827 }) {
828 return Err(FittingError::InvalidConfig(format!(
829 "prior {i} must be on free parameters without another prior, with a finite mean \
830 and finite positive sds; got parameters {:?} with mean {:?}",
831 prior.parameters, prior.mean
832 )));
833 }
834 params.set_free_values(¶ms.free_values());
835 let mut y_model = model.evaluate(¶ms.all_values())?;
836 if y_model.len() != y_obs.len() {
837 return Err(FittingError::LengthMismatch {
838 expected: y_model.len(),
839 actual: y_obs.len(),
840 field: "y_obs",
841 });
842 }
843 let free = params.free_indices();
844 let mut value = objective(y_obs, &y_model, priors, params);
845 let mut iterations = 0;
846 let mut at_minimum = None;
847 let mut damping = INITIAL_DAMPING;
848 while value.is_finite() {
849 let linear = linearize(model, params, &free, priors, y_obs, &y_model)?;
850 if !linear
851 .weighted
852 .data
853 .iter()
854 .chain(&linear.zero_slope)
855 .all(|v| v.is_finite())
856 {
857 break;
858 }
859 let movable: Vec<usize> = (0..free.len())
860 .filter(|&j| !held_by_bound(¶ms.params[free[j]], linear.gradient[j]))
861 .collect();
862 let Some(decomposition) = Decomposition::new(&linear.weighted, &movable) else {
863 break;
864 };
865 if decomposition.newton_decrement(&linear) < NEWTON_DECREMENT_TOL {
866 at_minimum = Some(linear);
867 break;
868 }
869 if iterations == config.max_iter {
870 break;
871 }
872 let start = params.free_values();
873 let mut accepted = None;
874 for _ in 0..MAX_REJECTIONS {
875 let direction =
876 decomposition.step(decomposition.spanned(), &linear, free.len(), damping);
877 let unprojected: Vec<f64> = start.iter().zip(&direction).map(|(x, d)| x - d).collect();
878 params.set_free_values(&unprojected);
879 if let Some(trial_model) = model
880 .evaluate(¶ms.all_values())
881 .ok()
882 .filter(|trial| trial.len() == y_obs.len())
883 {
884 let trial_value = objective(y_obs, &trial_model, priors, params);
885 if trial_value < value {
886 damping /= DAMPING_FACTOR;
887 accepted = Some((trial_model, trial_value));
888 break;
889 }
890 }
891 params.set_free_values(&start);
892 damping = (damping * DAMPING_FACTOR).max(INITIAL_DAMPING);
893 }
894 let Some((trial_model, trial_value)) = accepted else {
895 break;
896 };
897 y_model = trial_model;
898 value = trial_value;
899 iterations += 1;
900 }
901
902 let bounded: Vec<bool> = free
903 .iter()
904 .map(|&idx| on_bound(¶ms.params[idx]))
905 .collect();
906 let (covariance, uncertainties, leverage, unbounded) = match &at_minimum {
907 Some(linear) if config.compute_covariance => {
908 let interior: Vec<usize> = (0..free.len()).filter(|&j| !bounded[j]).collect();
909 let decomposition = Decomposition::new(&linear.weighted, &interior);
910 let (covariance, errors) = decomposition.as_ref().map_or_else(
911 || withheld(free.len()),
912 |decomposition| decomposition.error_bars(free.len()),
913 );
914 let leverage = decomposition
915 .as_ref()
916 .map(|decomposition| decomposition.leverage(y_obs.len()));
917 let unbounded = unbounded(linear, ¶ms.free_values(), &bounded);
918 (Some(covariance), Some(errors), leverage, unbounded)
919 }
920 _ => (None, None, None, None),
921 };
922 Ok(PoissonResult {
923 deviance: deviance(y_obs, &y_model),
924 iterations,
925 converged: at_minimum.is_some(),
926 params: params.all_values(),
927 covariance,
928 uncertainties,
929 on_bound: bounded,
930 leverage,
931 unbounded,
932 })
933}
934
935fn unbounded(linear: &Linearization, fitted: &[f64], bounded: &[bool]) -> Option<Unbounded> {
936 if (0..fitted.len()).any(|j| bounded[j] && linear.zero_slope[j] != 0.0) {
937 return None;
938 }
939 let columns: Vec<usize> = (0..fitted.len()).collect();
940 let decomposition = Decomposition::new(&linear.weighted, &columns)?;
941 let step = decomposition.step(decomposition.determined(), linear, fitted.len(), 0.0);
942 let covariance = decomposition.error_bars(fitted.len()).0;
943 let mean = (0..fitted.len())
944 .map(|i| {
945 if covariance.get(i, i).is_nan() {
946 f64::NAN
947 } else {
948 fitted[i] - step[i]
949 }
950 })
951 .collect();
952 Some(Unbounded { mean, covariance })
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 struct ExponentialModel {
962 x: Vec<f64>,
963 flux: Vec<f64>,
964 }
965
966 impl FitModel for ExponentialModel {
967 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
968 let b = params[0]; Ok(self
970 .x
971 .iter()
972 .zip(self.flux.iter())
973 .map(|(&xi, &fi)| fi * (-b * xi).exp())
974 .collect())
975 }
976 }
977
978 struct Decay {
979 t: Vec<f64>,
980 jacobian_factor: f64,
981 }
982
983 impl FitModel for Decay {
984 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
985 Ok(self
986 .t
987 .iter()
988 .map(|&t| params[0] * (-params[1] * t).exp())
989 .collect())
990 }
991
992 fn analytical_jacobian(
993 &self,
994 params: &[f64],
995 free_param_indices: &[usize],
996 _y_current: &[f64],
997 ) -> Option<FlatMatrix> {
998 let mut jacobian = FlatMatrix::zeros(self.t.len(), free_param_indices.len());
999 for (row, &t) in self.t.iter().enumerate() {
1000 let e = (-params[1] * t).exp();
1001 for (col, &index) in free_param_indices.iter().enumerate() {
1002 let slope = [e, -params[0] * t * e][index];
1003 *jacobian.get_mut(row, col) = slope * self.jacobian_factor;
1004 }
1005 }
1006 Some(jacobian)
1007 }
1008 }
1009
1010 fn decay(jacobian_factor: f64) -> Decay {
1011 Decay {
1012 t: (0..25).map(|i| 0.2 * f64::from(i)).collect(),
1013 jacobian_factor,
1014 }
1015 }
1016
1017 fn decay_params(a: f64, b: f64, a_upper: f64) -> ParameterSet {
1018 ParameterSet::new(vec![
1019 FitParameter {
1020 name: "a".into(),
1021 value: a,
1022 lower: 0.0,
1023 upper: a_upper,
1024 fixed: false,
1025 },
1026 FitParameter::non_negative("b", b),
1027 ])
1028 }
1029
1030 fn fit_decay(
1031 model: &Decay,
1032 observed: &[f64],
1033 start: (f64, f64),
1034 max_iter: usize,
1035 ) -> PoissonResult {
1036 let mut params = decay_params(start.0, start.1, f64::INFINITY);
1037 let config = PoissonConfig {
1038 max_iter,
1039 ..PoissonConfig::default()
1040 };
1041 poisson_fit(model, observed, &[], &mut params, &config).unwrap()
1042 }
1043
1044 #[test]
1045 fn half_deviance_matches_high_precision_values() {
1046 for (obs, mean, exact) in [
1047 (1_000_001.0, 1_000_000.0, 4.999_998_333_334_167e-7),
1048 (3.0, 2.5, 0.046_964_670_381_863_88),
1049 (0.0, 2.5, 2.5),
1050 (1.0e308, 9.0e307, 5.360_515_657_826_301e305),
1051 (1.7e308, 1.65e308, 7.500_373_544_579_622e304),
1052 (1.0, 1.0e-310, 712.801_378_828_154_2),
1053 ] {
1054 let got = half_deviance(obs, mean);
1055 assert!(
1056 ((got - exact) / exact).abs() < 1e-12,
1057 "{obs}, {mean}: {got}"
1058 );
1059 }
1060 }
1061
1062 #[test]
1063 fn inputs_that_are_not_counts_for_this_model_are_refused() {
1064 let model = decay(1.0);
1065 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1066 let fit = |model: &dyn FitModel, observed: &[f64]| {
1067 poisson_fit(
1068 model,
1069 observed,
1070 &[],
1071 &mut decay_params(50.0, 1.0, f64::INFINITY),
1072 &PoissonConfig::default(),
1073 )
1074 };
1075 let mut bad = observed.clone();
1076 bad[3] = f64::NAN;
1077 assert!(fit(&model, &bad).is_err());
1078 bad[3] = -1.0;
1079 assert!(fit(&model, &bad).is_err());
1080 assert!(fit(&model, &observed[1..]).is_err());
1081 let empty = Decay {
1082 t: vec![],
1083 jacobian_factor: 1.0,
1084 };
1085 assert!(fit(&empty, &[]).is_err());
1086 let no_jacobian = ExponentialModel {
1087 x: model.t.clone(),
1088 flux: vec![100.0; model.t.len()],
1089 };
1090 assert!(fit(&no_jacobian, &observed).is_err());
1091 }
1092
1093 #[test]
1094 fn a_start_with_a_zero_prediction_is_not_converged() {
1095 let model = decay(1.0);
1096 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1097 let result = fit_decay(&model, &observed, (0.0, 1.0), 200);
1098 assert!(!result.converged && result.iterations == 0, "{result:?}");
1099 }
1100
1101 #[test]
1102 fn a_non_finite_slope_ends_the_fit_unconverged() {
1103 let model = decay(f64::NAN);
1104 let observed = decay(1.0).evaluate(&[100.0, 1.5]).unwrap();
1105 let result = fit_decay(&model, &observed, (50.0, 1.0), 200);
1106 assert!(
1107 !result.converged && result.uncertainties.is_none(),
1108 "{result:?}"
1109 );
1110 }
1111
1112 #[test]
1113 fn a_fit_whose_step_cannot_lower_the_deviance_is_not_converged() {
1114 let model = decay(-1.0);
1115 let observed = decay(1.0).evaluate(&[100.0, 1.5]).unwrap();
1116 let result = fit_decay(&model, &observed, (50.0, 1.0), 200);
1117 assert!(
1118 !result.converged && result.iterations == 0 && result.uncertainties.is_none(),
1119 "{result:?}"
1120 );
1121 }
1122
1123 #[test]
1124 fn a_start_outside_the_bounds_ends_inside_them() {
1125 let model = decay(1.0);
1126 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1127 let mut params = decay_params(100.0, 1.5, 80.0);
1128 let result = poisson_fit(
1129 &model,
1130 &observed,
1131 &[],
1132 &mut params,
1133 &PoissonConfig::default(),
1134 )
1135 .unwrap();
1136 assert!(result.converged && result.params[0] == 80.0, "{result:?}");
1137 assert_eq!(result.on_bound, vec![true, false]);
1138 }
1139
1140 #[test]
1141 fn a_fit_that_reaches_the_minimum_on_its_last_allowed_step_has_converged() {
1142 let model = decay(1.0);
1143 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1144 let steps = fit_decay(&model, &observed, (20.0, 0.3), 200).iterations;
1145 assert!(steps > 1);
1146 assert!(fit_decay(&model, &observed, (20.0, 0.3), steps).converged);
1147 assert!(!fit_decay(&model, &observed, (20.0, 0.3), steps - 1).converged);
1148 }
1149
1150 struct Scaled {
1151 x: Vec<f64>,
1152 slope: f64,
1153 tiny: f64,
1154 }
1155
1156 impl FitModel for Scaled {
1157 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1158 Ok(self
1159 .x
1160 .iter()
1161 .map(|&x| 100.0 * (self.slope * params[0] * x).exp() + self.tiny * params[1] * x)
1162 .collect())
1163 }
1164
1165 fn analytical_jacobian(
1166 &self,
1167 params: &[f64],
1168 free_param_indices: &[usize],
1169 _y_current: &[f64],
1170 ) -> Option<FlatMatrix> {
1171 let mut jacobian = FlatMatrix::zeros(self.x.len(), free_param_indices.len());
1172 for (row, &x) in self.x.iter().enumerate() {
1173 let slopes = [
1174 100.0 * self.slope * x * (self.slope * params[0] * x).exp(),
1175 self.tiny * x,
1176 ];
1177 for (col, &index) in free_param_indices.iter().enumerate() {
1178 *jacobian.get_mut(row, col) = slopes[index];
1179 }
1180 }
1181 Some(jacobian)
1182 }
1183 }
1184
1185 fn fit_scaled(slope: f64, tiny: f64) -> PoissonResult {
1186 let model = Scaled {
1187 x: vec![1.0, 2.0, 3.0],
1188 slope,
1189 tiny,
1190 };
1191 let observed = model.evaluate(&[0.3 / slope, 0.0]).unwrap();
1192 let mut params = ParameterSet::new(vec![
1193 FitParameter::unbounded("theta", 0.0),
1194 FitParameter::unbounded("weak", 0.0),
1195 ]);
1196 poisson_fit(
1197 &model,
1198 &observed,
1199 &[],
1200 &mut params,
1201 &PoissonConfig::default(),
1202 )
1203 .unwrap()
1204 }
1205
1206 #[test]
1207 fn a_slope_too_large_to_square_is_fitted() {
1208 let result = fit_scaled(1.0e160, 1.0);
1209 assert!(
1210 result.converged && result.deviance < NEWTON_DECREMENT_TOL,
1211 "{result:?}"
1212 );
1213 }
1214
1215 #[test]
1216 fn a_slope_too_small_to_square_does_not_fake_convergence() {
1217 let result = fit_scaled(1.0, 1.0e-310);
1218 assert!(
1219 !result.converged || result.deviance < NEWTON_DECREMENT_TOL,
1220 "{result:?}"
1221 );
1222 }
1223
1224 struct Line {
1225 x: Vec<f64>,
1226 }
1227
1228 impl FitModel for Line {
1229 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1230 Ok(self.x.iter().map(|&x| 1.0 + params[0] * x).collect())
1231 }
1232
1233 fn analytical_jacobian(
1234 &self,
1235 _params: &[f64],
1236 free_param_indices: &[usize],
1237 _y_current: &[f64],
1238 ) -> Option<FlatMatrix> {
1239 let mut jacobian = FlatMatrix::zeros(self.x.len(), free_param_indices.len());
1240 jacobian.data.copy_from_slice(&self.x);
1241 Some(jacobian)
1242 }
1243 }
1244
1245 struct Proportional {
1246 a: Vec<f64>,
1247 }
1248
1249 impl FitModel for Proportional {
1250 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1251 Ok(self.a.iter().map(|a| a * params[0]).collect())
1252 }
1253
1254 fn analytical_jacobian(
1255 &self,
1256 _params: &[f64],
1257 free_param_indices: &[usize],
1258 _y_current: &[f64],
1259 ) -> Option<FlatMatrix> {
1260 let mut jacobian = FlatMatrix::zeros(self.a.len(), free_param_indices.len());
1261 jacobian.data.copy_from_slice(&self.a);
1262 Some(jacobian)
1263 }
1264 }
1265
1266 #[test]
1267 fn a_prior_moves_the_fit_to_the_maximum_of_the_posterior() {
1268 let model = Proportional {
1269 a: vec![1.0, 2.0, 3.0, 4.0],
1270 };
1271 let observed = [3.0, 5.0, 9.0, 12.0];
1272 let (a_sum, y_sum): (f64, f64) = (10.0, 29.0);
1273 let (mean, sd) = (2.0, 0.3);
1274 let fit = |priors: &[Prior]| {
1275 let mut params = ParameterSet::new(vec![FitParameter::non_negative("theta", 1.0)]);
1276 poisson_fit(
1277 &model,
1278 &observed,
1279 priors,
1280 &mut params,
1281 &PoissonConfig::default(),
1282 )
1283 .unwrap()
1284 };
1285 let result = fit(&[Prior::measured(0, mean, sd)]);
1286 assert!(result.converged);
1287 let theta = result.params[0];
1288 let b = a_sum - mean / sd.powi(2);
1289 let root = sd.powi(2) * 0.5 * (-b + (b * b + 4.0 * y_sum / sd.powi(2)).sqrt());
1290 let variance = 1.0 / (a_sum / theta + 1.0 / sd.powi(2));
1291 assert!(
1292 (theta - root).abs() <= 2e-3 * variance.sqrt(),
1293 "{theta} vs {root}"
1294 );
1295 let covariance = result.covariance.expect("covariance").get(0, 0);
1296 assert!((covariance / variance - 1.0).abs() <= 1e-12, "{covariance}");
1297 let deviance: f64 = observed
1298 .iter()
1299 .zip(&model.a)
1300 .map(|(y, a)| a * theta - y + y * (y / (a * theta)).ln())
1301 .sum();
1302 assert!((result.deviance / deviance - 1.0).abs() <= 1e-12);
1303 let leverage = result.leverage.expect("leverage");
1304 for (h, a) in leverage.iter().zip(&model.a) {
1305 assert!((h / (a / theta * variance) - 1.0).abs() <= 1e-12, "{h}");
1306 }
1307 let unmeasured = fit(&[]).leverage.expect("leverage");
1308 assert!((unmeasured.iter().sum::<f64>() - 1.0).abs() <= 1e-12);
1309 }
1310
1311 #[test]
1312 fn priors_that_are_not_measurements_of_one_free_parameter_are_refused() {
1313 let model = decay(1.0);
1314 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1315 let fit = |priors: &[Prior], fixed: bool| {
1316 let mut params = decay_params(50.0, 1.0, f64::INFINITY);
1317 params.params[1].fixed = fixed;
1318 poisson_fit(
1319 &model,
1320 &observed,
1321 priors,
1322 &mut params,
1323 &PoissonConfig::default(),
1324 )
1325 };
1326 let prior = Prior::measured;
1327 assert!(fit(&[prior(1, 1.5, 0.1)], false).is_ok());
1328 for priors in [
1329 vec![prior(2, 1.5, 0.1)],
1330 vec![prior(1, 1.5, 0.1), prior(1, 1.4, 0.1)],
1331 vec![prior(1, f64::NAN, 0.1)],
1332 vec![prior(1, 1.5, 0.0)],
1333 vec![prior(1, 1.5, f64::INFINITY)],
1334 vec![block(&[0, 1]), prior(1, 1.5, 0.1)],
1335 ] {
1336 assert!(fit(&priors, false).is_err(), "{priors:?}");
1337 }
1338 assert!(fit(&[prior(1, 1.5, 0.1)], true).is_err());
1339 assert!(fit(&[block(&[0, 1])], false).is_ok());
1340 }
1341
1342 fn two_by_two(data: [f64; 4]) -> FlatMatrix {
1343 FlatMatrix {
1344 data: data.to_vec(),
1345 nrows: 2,
1346 ncols: 2,
1347 }
1348 }
1349
1350 fn block(parameters: &[usize]) -> Prior {
1351 Prior::correlated(
1352 parameters,
1353 &[100.0, 1.5],
1354 &two_by_two([1.0, 0.0, 0.0, 0.01]),
1355 )
1356 .unwrap()
1357 }
1358
1359 #[test]
1360 fn covariances_that_are_not_symmetric_and_well_conditioned_are_refused() {
1361 let correlated = |data| Prior::correlated(&[0, 1], &[0.0, 0.0], &two_by_two(data));
1362 let scaled = |rho: f64| correlated([1.0, rho * 1e3, rho * 1e3, 1e6]);
1363 assert!(scaled(1.0 - 1e-9).is_ok());
1364 assert!(correlated([1.0, 1e-6, 1e-6 + 1e-17, 1.0]).is_ok());
1365 for result in [
1366 scaled(1.0 - 1e-11),
1367 scaled(1.0 + 1e-9),
1368 correlated([1.0, 0.5, 0.5 + 1e-10, 1.0]),
1369 correlated([1.0, 1e-6, 1e-6 + 1e-11, 1.0]),
1370 correlated([1.0, f64::NAN, f64::NAN, 1.0]),
1371 correlated([0.0, 0.0, 0.0, 1.0]),
1372 Prior::correlated(&[0, 1], &[0.0], &two_by_two([1.0, 0.0, 0.0, 1.0])),
1373 Prior::correlated(
1374 &[0, 1],
1375 &[0.0, 0.0],
1376 &FlatMatrix {
1377 data: vec![1.0, 0.0, 0.0],
1378 nrows: 2,
1379 ncols: 2,
1380 },
1381 ),
1382 Prior::correlated(&[], &[], &FlatMatrix::zeros(0, 0)),
1383 Prior::correlated(&[0, 1], &[f64::NAN, 0.0], &two_by_two([1.0, 0.0, 0.0, 1.0])),
1384 Prior::correlated(&[1, 1], &[0.0, 0.0], &two_by_two([1.0, 0.0, 0.0, 1.0])),
1385 ] {
1386 assert!(result.is_err(), "{result:?}");
1387 }
1388 }
1389
1390 #[test]
1391 fn a_measured_prior_divides_by_its_sd() {
1392 for (value, mean, sd) in [
1393 (1.234_567, 0.987_654_3, 0.031_4),
1394 (1e-7, -3.3e5, 7.0e3),
1395 (2.0, 2.0, 0.3),
1396 ] {
1397 let prior = Prior::measured(0, mean, sd);
1398 assert_eq!(
1399 prior.residual(&[value])[0].to_bits(),
1400 ((value - mean) / sd).to_bits()
1401 );
1402 assert_eq!(prior.inverse_factor()[0].to_bits(), (1.0 / sd).to_bits());
1403 }
1404 }
1405
1406 struct Mixture;
1407
1408 const MIXTURE: [[f64; 2]; 5] = [[1.0, 5.0], [2.0, 3.0], [3.0, 2.0], [4.0, 1.0], [5.0, 0.5]];
1409
1410 const MIXTURE_COUNTS: [f64; 5] = [21.0, 20.0, 15.0, 18.0, 16.0];
1411
1412 impl FitModel for Mixture {
1413 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1414 Ok(MIXTURE
1415 .iter()
1416 .map(|a| a[0] * params[0] + a[1] * params[1])
1417 .collect())
1418 }
1419
1420 fn analytical_jacobian(
1421 &self,
1422 _params: &[f64],
1423 free_param_indices: &[usize],
1424 _y_current: &[f64],
1425 ) -> Option<FlatMatrix> {
1426 let mut jacobian = FlatMatrix::zeros(MIXTURE.len(), free_param_indices.len());
1427 for (row, a) in MIXTURE.iter().enumerate() {
1428 for (col, &index) in free_param_indices.iter().enumerate() {
1429 *jacobian.get_mut(row, col) = a[index];
1430 }
1431 }
1432 Some(jacobian)
1433 }
1434 }
1435
1436 fn fit_mixture(priors: &[Prior]) -> PoissonResult {
1437 let mut params = ParameterSet::new(vec![
1438 FitParameter::non_negative("x", 1.0),
1439 FitParameter::non_negative("y", 1.0),
1440 ]);
1441 poisson_fit(
1442 &Mixture,
1443 &MIXTURE_COUNTS,
1444 priors,
1445 &mut params,
1446 &PoissonConfig::default(),
1447 )
1448 .unwrap()
1449 }
1450
1451 fn inverse(m: [[f64; 2]; 2]) -> [[f64; 2]; 2] {
1452 let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
1453 [
1454 [m[1][1] / det, -m[0][1] / det],
1455 [-m[1][0] / det, m[0][0] / det],
1456 ]
1457 }
1458
1459 #[test]
1460 fn a_correlated_prior_adds_its_inverse_covariance_to_the_information() {
1461 let (mean, c) = ([3.0, 4.0], [[0.25, 0.15], [0.15, 0.36]]);
1462 let prior = Prior::correlated(&[0, 1], &mean, &two_by_two(c.concat().try_into().unwrap()));
1463 let result = fit_mixture(&[prior.unwrap()]);
1464 assert!(result.converged);
1465 let precision = inverse(c);
1466 let information = |theta: [f64; 2], observed: bool| {
1467 let mut information = precision;
1468 for (a, y) in MIXTURE.iter().zip(MIXTURE_COUNTS) {
1469 let mu = a[0] * theta[0] + a[1] * theta[1];
1470 let weight = if observed { y / mu.powi(2) } else { 1.0 / mu };
1471 for k in 0..2 {
1472 for l in 0..2 {
1473 information[k][l] += weight * a[k] * a[l];
1474 }
1475 }
1476 }
1477 information
1478 };
1479 let mut mode = mean;
1480 for _ in 0..50 {
1481 let gradient: Vec<f64> = (0..2)
1482 .map(|k| {
1483 (0..2)
1484 .map(|l| precision[k][l] * (mode[l] - mean[l]))
1485 .sum::<f64>()
1486 + MIXTURE
1487 .iter()
1488 .zip(MIXTURE_COUNTS)
1489 .map(|(a, y)| a[k] * (1.0 - y / (a[0] * mode[0] + a[1] * mode[1])))
1490 .sum::<f64>()
1491 })
1492 .collect();
1493 let step = inverse(information(mode, true));
1494 for k in 0..2 {
1495 mode[k] -= (0..2).map(|l| step[k][l] * gradient[l]).sum::<f64>();
1496 }
1497 }
1498 let fitted = [result.params[0], result.params[1]];
1499 let expected = information(fitted, false);
1500 let shift: Vec<f64> = (0..2).map(|k| fitted[k] - mode[k]).collect();
1501 let distance = (0..2)
1502 .flat_map(|k| (0..2).map(move |l| (k, l)))
1503 .map(|(k, l)| shift[k] * expected[k][l] * shift[l])
1504 .sum::<f64>()
1505 .sqrt();
1506 assert!(distance <= 2e-3, "{fitted:?} vs {mode:?}");
1507 let covariance = inverse(expected);
1508 let reported = result.covariance.expect("covariance");
1509 for k in 0..2 {
1510 for l in 0..2 {
1511 let scale = (covariance[k][k] * covariance[l][l]).sqrt();
1512 assert!(
1513 (reported.get(k, l) - covariance[k][l]).abs() <= 1e-12 * scale,
1514 "{k}{l}: {} vs {}",
1515 reported.get(k, l),
1516 covariance[k][l]
1517 );
1518 }
1519 }
1520 let unbounded = result.unbounded.expect("unbounded");
1521 assert_eq!(unbounded.covariance.data, reported.data);
1522 for k in 0..2 {
1523 assert!((unbounded.mean[k] - fitted[k]).abs() <= 2e-3 * covariance[k][k].sqrt());
1524 }
1525 }
1526
1527 const FAR_COUNTS: [f64; 5] = [9743.0, 20046.0, 30110.0, 39769.0, 49761.0];
1528
1529 fn fit_far(params: Vec<FitParameter>, priors: &[Prior]) -> PoissonResult {
1530 let mut params = ParameterSet::new(params);
1531 poisson_fit(
1532 &Mixture,
1533 &FAR_COUNTS,
1534 priors,
1535 &mut params,
1536 &PoissonConfig::default(),
1537 )
1538 .unwrap()
1539 }
1540
1541 #[test]
1542 fn the_unbounded_gaussian_is_one_newton_step_past_the_bounds() {
1543 let (x, y) = (
1544 FitParameter::non_negative("x", 9000.0),
1545 FitParameter::non_negative("y", 1.0),
1546 );
1547 let capped = FitParameter {
1548 upper: 9000.0,
1549 ..x.clone()
1550 };
1551 let cases = [
1552 (vec![x.clone(), y.clone()], None, 1),
1553 (vec![capped, FitParameter::unbounded("y", 1.0)], None, 0),
1554 (vec![x, y], Some((10.0, 20.0)), 1),
1555 ];
1556 for (params, prior, held) in cases {
1557 let priors: Vec<Prior> = prior
1558 .iter()
1559 .map(|&(mean, sd)| Prior::measured(1, mean, sd))
1560 .collect();
1561 let result = fit_far(params, &priors);
1562 assert!(result.converged && result.on_bound[held] && !result.on_bound[1 - held]);
1563 let theta = [result.params[0], result.params[1]];
1564 let (mut information, mut gradient) = ([[0.0; 2]; 2], [0.0; 2]);
1565 for (a, n) in MIXTURE.iter().zip(FAR_COUNTS) {
1566 let mu = a[0] * theta[0] + a[1] * theta[1];
1567 for k in 0..2 {
1568 gradient[k] += a[k] * (1.0 - n / mu);
1569 for l in 0..2 {
1570 information[k][l] += a[k] * a[l] / mu;
1571 }
1572 }
1573 }
1574 if let Some((mean, sd)) = prior {
1575 information[1][1] += sd.powi(-2);
1576 gradient[1] += (theta[1] - mean) / sd.powi(2);
1577 }
1578 let covariance = inverse(information);
1579 let unbounded = result.unbounded.expect("unbounded");
1580 for k in 0..2 {
1581 let mean = theta[k] - (0..2).map(|l| covariance[k][l] * gradient[l]).sum::<f64>();
1582 let sd = covariance[k][k].sqrt();
1583 assert!(
1584 (unbounded.mean[k] - mean).abs() <= 1e-12 * sd,
1585 "{held} {k}: {} vs {mean}",
1586 unbounded.mean[k]
1587 );
1588 for (l, expected) in covariance[k].iter().enumerate() {
1589 let scale = sd * covariance[l][l].sqrt();
1590 assert!((unbounded.covariance.get(k, l) - expected).abs() <= 1e-12 * scale);
1591 }
1592 }
1593 }
1594 }
1595
1596 struct Unused;
1597
1598 impl FitModel for Unused {
1599 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1600 Ok(vec![params[0]])
1601 }
1602
1603 fn analytical_jacobian(
1604 &self,
1605 _params: &[f64],
1606 free_param_indices: &[usize],
1607 _y_current: &[f64],
1608 ) -> Option<FlatMatrix> {
1609 let mut jacobian = FlatMatrix::zeros(1, free_param_indices.len());
1610 for (col, &index) in free_param_indices.iter().enumerate() {
1611 *jacobian.get_mut(0, col) = f64::from(u8::from(index == 0));
1612 }
1613 Some(jacobian)
1614 }
1615 }
1616
1617 #[test]
1618 fn a_parameter_on_a_bound_the_counts_do_not_determine_leaves_the_others_their_gaussian() {
1619 let mut params = ParameterSet::new(vec![
1620 FitParameter::unbounded("x", 90.0),
1621 FitParameter::non_negative("y", 0.0),
1622 ]);
1623 let result = poisson_fit(
1624 &Unused,
1625 &[100.0],
1626 &[],
1627 &mut params,
1628 &PoissonConfig::default(),
1629 )
1630 .unwrap();
1631 assert!(result.converged && result.on_bound == [false, true]);
1632 let unbounded = result.unbounded.expect("unbounded");
1633 let variance = unbounded.covariance.get(0, 0);
1634 assert!((variance / result.params[0] - 1.0).abs() <= 1e-12);
1635 assert!((unbounded.mean[0] - 100.0).abs() <= 2e-3 * variance.sqrt());
1636 assert!(unbounded.covariance.get(1, 1).is_nan() && unbounded.mean[1].is_nan());
1637 }
1638
1639 #[test]
1640 fn the_unbounded_gaussian_is_the_fit_without_its_bound_to_second_order() {
1641 let x = FitParameter::non_negative("x", 9000.0);
1642 let bounded = fit_far(vec![x.clone(), FitParameter::non_negative("y", 1.0)], &[]);
1643 let free = fit_far(vec![x, FitParameter::unbounded("y", 1.0)], &[]);
1644 let covariance = free.covariance.expect("covariance");
1645 assert!(bounded.on_bound[1] && !free.on_bound[1]);
1646 assert!(free.params[1] <= -1.5 * covariance.get(1, 1).sqrt());
1647 let unbounded = bounded.unbounded.expect("unbounded");
1648 for k in 0..2 {
1649 let variance = covariance.get(k, k);
1650 assert!((unbounded.mean[k] - free.params[k]).abs() <= 0.05 * variance.sqrt());
1651 assert!((unbounded.covariance.get(k, k) / variance - 1.0).abs() <= 0.05);
1652 }
1653 }
1654
1655 #[test]
1656 fn a_correlated_prior_with_a_diagonal_covariance_is_two_measured_priors() {
1657 let (mean, sd) = ([3.0, 4.0], [0.5, 0.6]);
1658 let diagonal = two_by_two([sd[0] * sd[0], 0.0, 0.0, sd[1] * sd[1]]);
1659 let bits = |result: PoissonResult| -> Vec<u64> {
1660 result
1661 .params
1662 .iter()
1663 .chain(&result.covariance.expect("covariance").data)
1664 .chain([&result.deviance])
1665 .map(|v| v.to_bits())
1666 .collect()
1667 };
1668 assert_eq!(
1669 bits(fit_mixture(&[
1670 Prior::correlated(&[0, 1], &mean, &diagonal).unwrap()
1671 ])),
1672 bits(fit_mixture(&[
1673 Prior::measured(0, mean[0], sd[0]),
1674 Prior::measured(1, mean[1], sd[1]),
1675 ]))
1676 );
1677 }
1678
1679 #[test]
1680 fn predictions_stay_positive_where_nothing_was_counted() {
1681 let model = Line {
1682 x: vec![1.0, 2.0, 3.0],
1683 };
1684 let mut params = ParameterSet::new(vec![FitParameter::unbounded("a", 0.0)]);
1685 let result = poisson_fit(
1686 &model,
1687 &[0.0; 3],
1688 &[],
1689 &mut params,
1690 &PoissonConfig::default(),
1691 )
1692 .unwrap();
1693 let predicted = model.evaluate(&result.params).unwrap();
1694 assert!(predicted.iter().all(|&mean| mean > 0.0), "{result:?}");
1695 }
1696
1697 struct WrongShape;
1698
1699 impl FitModel for WrongShape {
1700 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1701 Ok(vec![params[0]; 3])
1702 }
1703
1704 fn analytical_jacobian(
1705 &self,
1706 _params: &[f64],
1707 free_param_indices: &[usize],
1708 _y_current: &[f64],
1709 ) -> Option<FlatMatrix> {
1710 Some(FlatMatrix::zeros(2, free_param_indices.len()))
1711 }
1712 }
1713
1714 #[test]
1715 fn a_jacobian_of_the_wrong_shape_is_refused() {
1716 let mut params = ParameterSet::new(vec![FitParameter::non_negative("a", 1.0)]);
1717 let result = poisson_fit(
1718 &WrongShape,
1719 &[1.0; 3],
1720 &[],
1721 &mut params,
1722 &PoissonConfig::default(),
1723 );
1724 assert!(
1725 matches!(result, Err(FittingError::LengthMismatch { .. })),
1726 "{result:?}"
1727 );
1728 }
1729
1730 #[test]
1731 fn inverted_or_nan_bounds_are_refused() {
1732 let model = decay(1.0);
1733 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
1734 for (lower, upper) in [
1735 (1.0, 0.0),
1736 (f64::NAN, 1.0),
1737 (0.0, f64::NAN),
1738 (f64::INFINITY, f64::INFINITY),
1739 (f64::NEG_INFINITY, f64::NEG_INFINITY),
1740 ] {
1741 let mut params = ParameterSet::new(vec![
1742 FitParameter {
1743 name: "a".into(),
1744 value: 50.0,
1745 lower,
1746 upper,
1747 fixed: false,
1748 },
1749 FitParameter::non_negative("b", 1.0),
1750 ]);
1751 let result = poisson_fit(
1752 &model,
1753 &observed,
1754 &[],
1755 &mut params,
1756 &PoissonConfig::default(),
1757 );
1758 assert!(
1759 matches!(result, Err(FittingError::InvalidConfig(_))),
1760 "{lower}, {upper}"
1761 );
1762 }
1763 }
1764
1765 struct Additive;
1766
1767 impl FitModel for Additive {
1768 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1769 Ok(vec![2.0 * params[0], 1.0 - params[0]])
1770 }
1771
1772 fn analytical_jacobian(
1773 &self,
1774 _params: &[f64],
1775 free_param_indices: &[usize],
1776 _y_current: &[f64],
1777 ) -> Option<FlatMatrix> {
1778 let mut jacobian = FlatMatrix::zeros(2, free_param_indices.len());
1779 jacobian.data.copy_from_slice(&[2.0, -1.0]);
1780 Some(jacobian)
1781 }
1782 }
1783
1784 #[test]
1785 fn a_bin_predicted_zero_keeps_its_slope_in_the_gradient() {
1786 let mut params = ParameterSet::new(vec![FitParameter {
1787 name: "theta".into(),
1788 value: 0.0,
1789 lower: 0.0,
1790 upper: 1.0,
1791 fixed: false,
1792 }]);
1793 let result = poisson_fit(
1794 &Additive,
1795 &[0.0, 0.0],
1796 &[],
1797 &mut params,
1798 &PoissonConfig::default(),
1799 )
1800 .unwrap();
1801 assert!(
1802 result.converged
1803 && result.params[0] == 0.0
1804 && result.on_bound == vec![true]
1805 && result.unbounded.is_none(),
1806 "{result:?}"
1807 );
1808 }
1809
1810 struct Line2 {
1811 offset: [f64; 2],
1812 slope: [f64; 2],
1813 }
1814
1815 impl FitModel for Line2 {
1816 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1817 Ok((0..2)
1818 .map(|i| self.offset[i] + self.slope[i] * params[0])
1819 .collect())
1820 }
1821
1822 fn analytical_jacobian(
1823 &self,
1824 _params: &[f64],
1825 free_param_indices: &[usize],
1826 _y_current: &[f64],
1827 ) -> Option<FlatMatrix> {
1828 let mut jacobian = FlatMatrix::zeros(2, free_param_indices.len());
1829 jacobian.data.copy_from_slice(&self.slope);
1830 Some(jacobian)
1831 }
1832 }
1833
1834 fn bounded(value: f64, upper: f64) -> ParameterSet {
1835 ParameterSet::new(vec![FitParameter {
1836 name: "theta".into(),
1837 value,
1838 lower: 0.0,
1839 upper,
1840 fixed: false,
1841 }])
1842 }
1843
1844 #[test]
1845 fn a_zero_bin_slope_enters_the_convergence_test() {
1846 let model = Line2 {
1847 offset: [0.0, 4.0],
1848 slope: [1.0, -4.0],
1849 };
1850 let result = poisson_fit(
1851 &model,
1852 &[0.0, 3.0],
1853 &[],
1854 &mut bounded(0.0, 1.5),
1855 &PoissonConfig::default(),
1856 )
1857 .unwrap();
1858 assert!(
1859 result.converged && result.iterations == 0 && result.params[0] == 0.0,
1860 "{result:?}"
1861 );
1862 }
1863
1864 struct Affine {
1865 offset: Vec<f64>,
1866 jacobian: Vec<Vec<f64>>,
1867 }
1868
1869 impl FitModel for Affine {
1870 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
1871 Ok(self
1872 .offset
1873 .iter()
1874 .zip(&self.jacobian)
1875 .map(|(offset, row)| {
1876 offset + row.iter().zip(params).map(|(j, p)| j * p).sum::<f64>()
1877 })
1878 .collect())
1879 }
1880
1881 fn analytical_jacobian(
1882 &self,
1883 _params: &[f64],
1884 free_param_indices: &[usize],
1885 _y_current: &[f64],
1886 ) -> Option<FlatMatrix> {
1887 let mut jacobian = FlatMatrix::zeros(self.offset.len(), free_param_indices.len());
1888 jacobian.data.copy_from_slice(&self.jacobian.concat());
1889 Some(jacobian)
1890 }
1891 }
1892
1893 fn two_free() -> ParameterSet {
1894 ParameterSet::new(vec![
1895 FitParameter::unbounded("a", 0.0),
1896 FitParameter::unbounded("b", 0.0),
1897 ])
1898 }
1899
1900 fn at_the_start() -> PoissonConfig {
1901 PoissonConfig {
1902 max_iter: 0,
1903 ..PoissonConfig::default()
1904 }
1905 }
1906
1907 #[test]
1908 fn a_weak_direction_keeps_its_share_of_the_newton_decrement() {
1909 let model = Affine {
1910 offset: vec![1.0; 2],
1911 jacobian: vec![vec![1.0, 1.0], vec![1.0e-14, -1.0e-14]],
1912 };
1913 for observed in [[0.999, 0.998999], [0.999, 0.999001]] {
1914 let decrement = 0.5
1915 * observed
1916 .iter()
1917 .map(|y: &f64| (1.0 - y).powi(2))
1918 .sum::<f64>();
1919 let result =
1920 poisson_fit(&model, &observed, &[], &mut two_free(), &at_the_start()).unwrap();
1921 assert_eq!(
1922 result.converged,
1923 decrement < NEWTON_DECREMENT_TOL,
1924 "{decrement:e}: {result:?}"
1925 );
1926 }
1927 }
1928
1929 #[test]
1930 fn the_information_inverse_is_poisson_fits_covariance() {
1931 for weak in [0.0, 1.0e-6] {
1932 let jacobian = vec![
1933 vec![1.0, 1.0 + weak, 0.0],
1934 vec![2.0, 2.0, 1.0],
1935 vec![1.0, 1.0, 3.0],
1936 vec![3.0, 3.0 - weak, 1.0],
1937 ];
1938 let model = Affine {
1939 offset: vec![5.0; 4],
1940 jacobian: jacobian.clone(),
1941 };
1942 let mut parameters = ParameterSet::new(
1943 ["a", "b", "c"]
1944 .map(|name| FitParameter::unbounded(name, 0.0))
1945 .to_vec(),
1946 );
1947 let result =
1948 poisson_fit(&model, &[5.0; 4], &[], &mut parameters, &at_the_start()).unwrap();
1949 let covariance = result.covariance.expect("covariance");
1950 let information = FlatMatrix {
1951 data: (0..9)
1952 .map(|e| {
1953 jacobian
1954 .iter()
1955 .map(|row| row[e / 3] * row[e % 3] / 5.0)
1956 .sum()
1957 })
1958 .collect(),
1959 nrows: 3,
1960 ncols: 3,
1961 };
1962 let diagonal: Vec<f64> = (0..3).map(|i| information.get(i, i)).collect();
1963 let inverse = information_inverse(&information, &diagonal, 4).unwrap();
1964 for i in 0..3 {
1965 assert_eq!(
1966 inverse.resolved[i],
1967 !covariance.get(i, i).is_nan(),
1968 "{weak}: {i}"
1969 );
1970 for j in (0..3).filter(|&j| inverse.resolved[i] && inverse.resolved[j]) {
1971 let (got, expected) = (inverse.determined.get(i, j), covariance.get(i, j));
1972 assert!(
1973 (got - expected).abs() <= 1e-9 * expected.abs().max(1.0),
1974 "{weak}: {got} vs {expected}"
1975 );
1976 }
1977 }
1978 assert_eq!(inverse.spanned.get(0, 0) > 1e12, weak > 0.0, "{weak}");
1979 }
1980 }
1981
1982 #[test]
1983 fn an_information_with_nothing_determined_inverts_to_zero() {
1984 let inverse = information_inverse(&FlatMatrix::zeros(2, 2), &[0.0; 2], 4).unwrap();
1985 assert!(inverse.spanned.data.iter().all(|&x| x == 0.0));
1986 assert!(inverse.determined.data.iter().all(|&x| x == 0.0));
1987 assert_eq!(inverse.resolved, [false; 2]);
1988 let empty = information_inverse(&FlatMatrix::zeros(0, 0), &[], 4).unwrap();
1989 assert!(empty.resolved.is_empty());
1990 }
1991
1992 #[test]
1993 fn a_direction_the_steps_leave_out_has_no_error_bar() {
1994 let weak = 2.0e-12;
1995 let information = FlatMatrix {
1996 data: vec![1.0, 1.0 - weak, 1.0 - weak, 1.0],
1997 nrows: 2,
1998 ncols: 2,
1999 };
2000 for (rows, determined) in [(4, true), (100_000, false)] {
2001 let inverse = information_inverse(&information, &[1.0; 2], rows).unwrap();
2002 assert_eq!(inverse.resolved, [determined; 2], "{rows}");
2003 assert_eq!(
2004 inverse.undetermined.len(),
2005 usize::from(!determined),
2006 "{rows}"
2007 );
2008 }
2009 }
2010
2011 #[test]
2012 fn a_zero_bin_slope_enters_every_direction_of_the_decrement() {
2013 let jacobian = vec![vec![1.0, 2.0], vec![1.0, 3.0], vec![2.0, -1.0]];
2014 let offset: Vec<f64> = vec![0.0, 4.0, 9.0];
2015 let root = [offset[1].sqrt(), offset[2].sqrt()];
2016 let w = [
2017 [jacobian[1][0] / root[0], jacobian[1][1] / root[0]],
2018 [jacobian[2][0] / root[1], jacobian[2][1] / root[1]],
2019 ];
2020 let information = |a: usize, b: usize| w[0][a] * w[0][b] + w[1][a] * w[1][b];
2021 let inverse_00 =
2022 information(1, 1) / (information(0, 0) * information(1, 1) - information(0, 1).powi(2));
2023 let transpose_det = w[0][0] * w[1][1] - w[1][0] * w[0][1];
2024 for decrement in [0.99e-6_f64, 1.01e-6] {
2025 let gradient = [(2.0 * decrement / inverse_00).sqrt(), 0.0];
2026 let rhs = [gradient[0] - jacobian[0][0], gradient[1] - jacobian[0][1]];
2027 let residual = [
2028 (rhs[0] * w[1][1] - w[1][0] * rhs[1]) / transpose_det,
2029 (w[0][0] * rhs[1] - w[0][1] * rhs[0]) / transpose_det,
2030 ];
2031 let observed = [
2032 0.0,
2033 offset[1] - residual[0] * root[0],
2034 offset[2] - residual[1] * root[1],
2035 ];
2036 let model = Affine {
2037 offset: offset.clone(),
2038 jacobian: jacobian.clone(),
2039 };
2040 let result =
2041 poisson_fit(&model, &observed, &[], &mut two_free(), &at_the_start()).unwrap();
2042 assert_eq!(
2043 result.converged,
2044 decrement < NEWTON_DECREMENT_TOL,
2045 "{decrement:e}: {result:?}"
2046 );
2047 }
2048 }
2049
2050 #[test]
2051 fn a_fit_with_hundreds_of_parameters_has_the_error_bars_of_its_inverse_information() {
2052 use rand::SeedableRng;
2053 use rand_chacha::ChaCha12Rng;
2054 use rand_distr::{Distribution, StandardNormal};
2055 let (bins, count, level) = (300, 150, 1.0e4);
2056 let mut rng = ChaCha12Rng::seed_from_u64(806);
2057 let jacobian: Vec<Vec<f64>> = (0..bins)
2058 .map(|_| {
2059 (0..count)
2060 .map(|_| StandardNormal.sample(&mut rng))
2061 .collect()
2062 })
2063 .collect();
2064 let model = Affine {
2065 offset: vec![level; bins],
2066 jacobian: jacobian.clone(),
2067 };
2068 let mut params = ParameterSet::new(
2069 (0..count)
2070 .map(|j| FitParameter::unbounded(format!("c{j}"), 0.0))
2071 .collect(),
2072 );
2073 let result = poisson_fit(
2074 &model,
2075 &vec![level; bins],
2076 &[],
2077 &mut params,
2078 &PoissonConfig::default(),
2079 )
2080 .unwrap();
2081 assert!(result.converged && result.iterations == 0, "{result:?}");
2082 let mut cholesky = vec![vec![0.0; count]; count];
2083 for a in 0..count {
2084 for b in 0..=a {
2085 let information: f64 = jacobian.iter().map(|row| row[a] * row[b] / level).sum();
2086 let rest =
2087 information - (0..b).map(|c| cholesky[a][c] * cholesky[b][c]).sum::<f64>();
2088 cholesky[a][b] = if a == b {
2089 rest.sqrt()
2090 } else {
2091 rest / cholesky[b][b]
2092 };
2093 }
2094 }
2095 for (j, error) in result.uncertainties.unwrap().into_iter().enumerate() {
2096 let mut solved = vec![0.0; count];
2097 for a in 0..count {
2098 let unit = if a == j { 1.0 } else { 0.0 };
2099 solved[a] = (unit - (0..a).map(|c| cholesky[a][c] * solved[c]).sum::<f64>())
2100 / cholesky[a][a];
2101 }
2102 let expected = solved.iter().map(|v| v * v).sum::<f64>().sqrt();
2103 let error = error.unwrap();
2104 assert!(
2105 (error / expected - 1.0).abs() < 1e-10,
2106 "{j}: {error} vs {expected}"
2107 );
2108 }
2109 }
2110
2111 struct SilentZeroBin;
2112
2113 impl FitModel for SilentZeroBin {
2114 fn evaluate(&self, _params: &[f64]) -> Result<Vec<f64>, FittingError> {
2115 Ok(vec![0.0])
2116 }
2117
2118 fn analytical_jacobian(
2119 &self,
2120 _params: &[f64],
2121 free_param_indices: &[usize],
2122 _y_current: &[f64],
2123 ) -> Option<FlatMatrix> {
2124 let mut jacobian = FlatMatrix::zeros(1, free_param_indices.len());
2125 jacobian.data[0] = f64::NAN;
2126 Some(jacobian)
2127 }
2128 }
2129
2130 #[test]
2131 fn a_non_finite_slope_in_a_zero_bin_ends_the_fit_unconverged() {
2132 let mut params = ParameterSet::new(vec![FitParameter::unbounded("theta", 0.0)]);
2133 let result = poisson_fit(
2134 &SilentZeroBin,
2135 &[0.0],
2136 &[],
2137 &mut params,
2138 &PoissonConfig::default(),
2139 )
2140 .unwrap();
2141 assert!(!result.converged, "{result:?}");
2142 }
2143
2144 struct Wall;
2145
2146 impl FitModel for Wall {
2147 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
2148 let x = params[0];
2149 Ok(vec![(100.0 - x + (x - 80.0).max(0.0).powi(2)).exp()])
2150 }
2151
2152 fn analytical_jacobian(
2153 &self,
2154 params: &[f64],
2155 free_param_indices: &[usize],
2156 _y_current: &[f64],
2157 ) -> Option<FlatMatrix> {
2158 let x = params[0];
2159 let mean = (100.0 - x + (x - 80.0).max(0.0).powi(2)).exp();
2160 let mut jacobian = FlatMatrix::zeros(1, free_param_indices.len());
2161 jacobian.data[0] = mean * (2.0 * (x - 80.0).max(0.0) - 1.0);
2162 Some(jacobian)
2163 }
2164 }
2165
2166 #[test]
2167 fn damping_recovers_after_many_accepted_steps_to_reach_the_minimum() {
2168 let mut params = ParameterSet::new(vec![FitParameter::unbounded("x", 0.0)]);
2169 let result =
2170 poisson_fit(&Wall, &[0.0], &[], &mut params, &PoissonConfig::default()).unwrap();
2171 assert!((result.params[0] - 80.5).abs() < 1e-3, "{result:?}");
2172 }
2173
2174 #[test]
2175 fn non_finite_parameter_values_are_refused() {
2176 let model = decay(1.0);
2177 let observed = model.evaluate(&[100.0, 1.5]).unwrap();
2178 for (a, b) in [(f64::NAN, 1.0), (50.0, f64::INFINITY)] {
2179 let mut params = ParameterSet::new(vec![
2180 FitParameter::non_negative("a", a),
2181 FitParameter::fixed("b", b),
2182 ]);
2183 let result = poisson_fit(
2184 &model,
2185 &observed,
2186 &[],
2187 &mut params,
2188 &PoissonConfig::default(),
2189 );
2190 assert!(
2191 matches!(result, Err(FittingError::InvalidConfig(_))),
2192 "{a}, {b}"
2193 );
2194 }
2195 }
2196
2197 struct Shrinking;
2198
2199 impl FitModel for Shrinking {
2200 fn evaluate(&self, params: &[f64]) -> Result<Vec<f64>, FittingError> {
2201 let bins = if params[0] < 0.5 { 3 } else { 2 };
2202 Ok(vec![1.0 + params[0]; bins])
2203 }
2204
2205 fn analytical_jacobian(
2206 &self,
2207 params: &[f64],
2208 free_param_indices: &[usize],
2209 _y_current: &[f64],
2210 ) -> Option<FlatMatrix> {
2211 let bins = if params[0] < 0.5 { 3 } else { 2 };
2212 let mut jacobian = FlatMatrix::zeros(bins, free_param_indices.len());
2213 jacobian.data.fill(1.0);
2214 Some(jacobian)
2215 }
2216 }
2217
2218 #[test]
2219 fn a_trial_prediction_of_the_wrong_length_is_rejected() {
2220 let mut params = ParameterSet::new(vec![FitParameter::non_negative("theta", 0.0)]);
2221 let result = poisson_fit(
2222 &Shrinking,
2223 &[3.0; 3],
2224 &[],
2225 &mut params,
2226 &PoissonConfig::default(),
2227 )
2228 .unwrap();
2229 assert_eq!(
2230 Shrinking.evaluate(&result.params).unwrap().len(),
2231 3,
2232 "{result:?}"
2233 );
2234 }
2235}