Skip to main content

gam_problem/
finite_validation.rs

1//! Shared finite-value validation helpers for estimation/result contracts.
2
3use crate::EstimationError;
4use ndarray::Array1;
5
6pub fn ensure_finite_scalar_estimation(name: &str, value: f64) -> Result<(), EstimationError> {
7    if value.is_finite() {
8        Ok(())
9    } else {
10        Err(EstimationError::InvalidInput(format!(
11            "{name} must be finite, got {value}"
12        )))
13    }
14}
15
16pub fn validate_all_finite_estimation<I>(label: &str, values: I) -> Result<(), EstimationError>
17where
18    I: IntoIterator<Item = f64>,
19{
20    for (idx, value) in values.into_iter().enumerate() {
21        if !value.is_finite() {
22            return Err(EstimationError::InvalidInput(format!(
23                "{label}[{idx}] must be finite, got {value}"
24            )));
25        }
26    }
27    Ok(())
28}
29
30/// Same check as [`validate_all_finite_estimation`], for a quantity whose
31/// non-finiteness is a statement about THIS TRIAL POINT rather than about the
32/// configuration.
33///
34/// An inference-only matrix derived from `H⁻¹` at the fitted mode -- the
35/// frequentist covariance, the influence matrix, the weighted Gram -- goes
36/// non-finite when the curvature at this rho is singular or unstable. That
37/// becomes true or false by moving rho, so the outer search should retreat from
38/// the point rather than abort, and the difference is carried by the variant
39/// instead of being recovered downstream by matching `"must be finite"` against
40/// a list of field names (#2593).
41///
42/// The rendered message is identical to `validate_all_finite_estimation`'s --
43/// `TrialPointRefused` displays the bare reason -- so nothing that reads the
44/// text sees a change.
45pub fn validate_all_finite_trial_point<I>(label: &str, values: I) -> Result<(), EstimationError>
46where
47    I: IntoIterator<Item = f64>,
48{
49    for (idx, value) in values.into_iter().enumerate() {
50        if !value.is_finite() {
51            return Err(EstimationError::TrialPointRefused {
52                reason: format!("{label}[{idx}] must be finite, got {value}"),
53            });
54        }
55    }
56    Ok(())
57}
58
59#[inline]
60pub fn bail_if_cached_beta_non_finite(beta: &Array1<f64>) -> Result<(), EstimationError> {
61    if beta.iter().any(|v| !v.is_finite()) {
62        return Err(EstimationError::InvalidInput(
63            "cached inner beta contains non-finite entries".to_string(),
64        ));
65    }
66    Ok(())
67}
68
69/// Public wrapper returning `String` errors for use outside the estimation module.
70pub fn ensure_finite_scalar(name: &str, value: f64) -> Result<(), String> {
71    ensure_finite_scalar_estimation(name, value).map_err(|err| err.to_string())
72}
73
74/// Public wrapper returning `String` errors for use outside the estimation module.
75pub fn validate_all_finite<I: IntoIterator<Item = f64>>(
76    label: &str,
77    values: I,
78) -> Result<(), String> {
79    validate_all_finite_estimation(label, values).map_err(|err| err.to_string())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn ensure_finite_scalar_ok_for_finite() {
88        assert!(ensure_finite_scalar("x", 0.0).is_ok());
89        assert!(ensure_finite_scalar("x", -1.5).is_ok());
90        assert!(ensure_finite_scalar("x", f64::MIN).is_ok());
91    }
92
93    #[test]
94    fn ensure_finite_scalar_err_for_nan() {
95        let e = ensure_finite_scalar("my_value", f64::NAN).unwrap_err();
96        assert!(e.contains("my_value"), "error should mention the name: {e}");
97    }
98
99    #[test]
100    fn ensure_finite_scalar_err_for_inf() {
101        assert!(ensure_finite_scalar("v", f64::INFINITY).is_err());
102        assert!(ensure_finite_scalar("v", f64::NEG_INFINITY).is_err());
103    }
104
105    #[test]
106    fn validate_all_finite_ok_for_finite_slice() {
107        assert!(validate_all_finite("vec", [1.0, 2.0, 3.0]).is_ok());
108        assert!(validate_all_finite("empty", std::iter::empty()).is_ok());
109    }
110
111    #[test]
112    fn validate_all_finite_err_reports_index() {
113        let e = validate_all_finite("arr", [1.0, f64::NAN, 3.0]).unwrap_err();
114        assert!(e.contains("arr[1]"), "error should mention arr[1]: {e}");
115    }
116
117    #[test]
118    fn validate_all_finite_err_reports_inf() {
119        let e = validate_all_finite("data", [0.0, f64::INFINITY]).unwrap_err();
120        assert!(e.contains("data[1]"), "error should mention data[1]: {e}");
121    }
122
123    #[test]
124    fn bail_if_cached_beta_ok_for_finite() {
125        let beta = ndarray::array![1.0, -2.0, 3.0];
126        assert!(bail_if_cached_beta_non_finite(&beta).is_ok());
127    }
128
129    #[test]
130    fn bail_if_cached_beta_err_for_nan() {
131        let beta = ndarray::array![1.0, f64::NAN];
132        assert!(bail_if_cached_beta_non_finite(&beta).is_err());
133    }
134}