nereids_core/validation.rs
1//! Small numeric-invariant helpers shared across NEREIDS crates.
2//!
3//! These live in `nereids-core` (the dependency-free foundation crate) so that
4//! the same invariant is enforced identically everywhere instead of being
5//! re-implemented per crate. Each helper returns the *first* offending
6//! `(index, value)` rather than a formatted error, so the calling crate can
7//! map the failure onto its own error type / message wording without
8//! `nereids-core` having to know about `IoError`, `FittingError`, etc.
9
10/// Locate the first element that is not finite-and-non-negative.
11///
12/// Detector counts (and other count-like quantities) are non-negative by
13/// construction — zero is legitimate, but a NaN, ±∞, or negative entry signals
14/// an upstream loader / normalisation bug. A bare `v < 0.0` test is *not*
15/// sufficient because `NaN < 0.0` is `false`; this helper therefore rejects on
16/// `!v.is_finite() || v < 0.0`, pairing the order comparison with a finiteness
17/// check (NaN bypasses `<`).
18///
19/// Returns `Err((i, v))` for the first offending element at flat index `i`, or
20/// `Ok(())` if every element is finite and `>= 0.0`. An empty iterator is
21/// vacuously `Ok(())`.
22pub fn first_non_finite_or_negative<I>(values: I) -> Result<(), (usize, f64)>
23where
24 I: IntoIterator<Item = f64>,
25{
26 for (i, v) in values.into_iter().enumerate() {
27 if !v.is_finite() || v < 0.0 {
28 return Err((i, v));
29 }
30 }
31 Ok(())
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37
38 #[test]
39 fn empty_is_ok() {
40 assert!(first_non_finite_or_negative(std::iter::empty::<f64>()).is_ok());
41 }
42
43 #[test]
44 fn all_finite_non_negative_is_ok() {
45 assert!(first_non_finite_or_negative([0.0, 1.0, 1e9, 2.5]).is_ok());
46 }
47
48 #[test]
49 fn rejects_negative_with_index() {
50 assert_eq!(
51 first_non_finite_or_negative([1.0, 2.0, -3.0, 4.0]),
52 Err((2, -3.0))
53 );
54 }
55
56 #[test]
57 fn rejects_nan_negative_does_not_bypass() {
58 // `NaN < 0.0` is `false`; the `is_finite()` half of the guard is what
59 // catches it. Reported value compares unequal to itself, so match on
60 // index + NaN-ness rather than `assert_eq!` on the tuple.
61 let err = first_non_finite_or_negative([1.0, f64::NAN, 3.0]).unwrap_err();
62 assert_eq!(err.0, 1);
63 assert!(err.1.is_nan());
64 }
65
66 #[test]
67 fn rejects_positive_and_negative_infinity() {
68 assert_eq!(
69 first_non_finite_or_negative([1.0, f64::INFINITY]),
70 Err((1, f64::INFINITY))
71 );
72 assert_eq!(
73 first_non_finite_or_negative([f64::NEG_INFINITY]),
74 Err((0, f64::NEG_INFINITY))
75 );
76 }
77
78 #[test]
79 fn reports_first_offender_only() {
80 // -1.0 at index 1 comes before NaN at index 3.
81 assert_eq!(
82 first_non_finite_or_negative([1.0, -1.0, 2.0, f64::NAN]),
83 Err((1, -1.0))
84 );
85 }
86}