Skip to main content

gam_problem/
monotone_root_error.rs

1/// Typed errors emitted by the monotone-root solver. `Display` preserves the
2/// exact pre-refactor error-string shapes so log expectations are unchanged.
3#[derive(Clone, Debug)]
4pub enum MonotoneRootError {
5    /// `eval(a)` returned an inner error message at the bracketing / refine step.
6    EvalFailed {
7        label: String,
8        a: f64,
9        source: String,
10    },
11    /// `eval(a)` returned a non-finite tuple component (f, f', or f'').
12    NonFiniteEval {
13        label: String,
14        a: f64,
15        f: f64,
16        fp: f64,
17        fpp: f64,
18    },
19    /// Derivative at the initial / current point is zero or non-finite —
20    /// monotonicity hypothesis violated.
21    DegenerateDerivative { label: String, a: f64, fp: f64 },
22    /// Bracketing failed to find a sign change within `max_bracket_iters`.
23    BracketingExhausted {
24        label: String,
25        iters: usize,
26        a_lo: f64,
27        a_hi: f64,
28    },
29    /// Newton refinement did not meet `convergence_tol` within `max_refine_iters`.
30    RefinementDidNotConverge {
31        label: String,
32        iters: usize,
33        last_residual: f64,
34    },
35}
36
37/// Internal: which exact textual shape a given error site emitted.
38/// These are folded into the enum variants above via Display so callers see
39/// byte-identical strings to the pre-refactor format!() output.
40impl MonotoneRootError {
41    pub fn exact_root_degenerate(label: &str, a: f64) -> Self {
42        // Tagged via `iters = usize::MAX` to select the "exact root" Display arm.
43        MonotoneRootError::RefinementDidNotConverge {
44            label: format!("__EXACT_ROOT__{label}"),
45            iters: usize::MAX,
46            last_residual: a,
47        }
48    }
49
50    pub fn converged_root_degenerate(label: &str, a: f64) -> Self {
51        MonotoneRootError::RefinementDidNotConverge {
52            label: format!("__CONVERGED__{label}"),
53            iters: 0,
54            last_residual: a,
55        }
56    }
57
58    pub fn analytic_bracket_invalid(label: &str, lo: f64, hi: f64) -> Self {
59        MonotoneRootError::BracketingExhausted {
60            label: format!("__ANALYTIC_INVALID__{label}"),
61            iters: 0,
62            a_lo: lo,
63            a_hi: hi,
64        }
65    }
66
67    pub fn analytic_bracket_no_straddle(label: &str, f_lo: f64, f_hi: f64) -> Self {
68        MonotoneRootError::BracketingExhausted {
69            label: format!("__ANALYTIC_NOSTRADDLE__{label}"),
70            iters: 0,
71            a_lo: f_lo,
72            a_hi: f_hi,
73        }
74    }
75
76    pub fn search_exhausted(label: &str, step_sign: f64, a_init: f64) -> Self {
77        MonotoneRootError::BracketingExhausted {
78            label: format!("__SEARCH__{label}"),
79            iters: 0,
80            a_lo: a_init,
81            a_hi: step_sign,
82        }
83    }
84}
85
86impl std::fmt::Display for MonotoneRootError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            MonotoneRootError::EvalFailed { source, .. } => f.write_str(source),
90            MonotoneRootError::NonFiniteEval { label, a, .. } => {
91                write!(f, "{label}: non-finite evaluation at a={a:.6}")
92            }
93            MonotoneRootError::DegenerateDerivative { label, a, .. } => {
94                write!(
95                    f,
96                    "{label}: initial derivative is zero or non-finite at a={a:.6}"
97                )
98            }
99            MonotoneRootError::BracketingExhausted {
100                label, a_lo, a_hi, ..
101            } => {
102                if let Some(real_label) = label.strip_prefix("__ANALYTIC_INVALID__") {
103                    write!(
104                        f,
105                        "{real_label}: invalid analytic bracket [{a_lo:.6}, {a_hi:.6}]"
106                    )
107                } else if let Some(real_label) = label.strip_prefix("__ANALYTIC_NOSTRADDLE__") {
108                    let f_lo = a_lo;
109                    let f_hi = a_hi;
110                    write!(
111                        f,
112                        "{real_label}: analytic bracket does not straddle root (f_lo={f_lo:.3e}, f_hi={f_hi:.3e})"
113                    )
114                } else if let Some(real_label) = label.strip_prefix("__SEARCH__") {
115                    let step_sign = *a_hi;
116                    let a_init = *a_lo;
117                    write!(
118                        f,
119                        "{real_label}: failed to bracket root (searched {step_sign:+.0} from a={a_init:.6})"
120                    )
121                } else {
122                    write!(
123                        f,
124                        "{label}: failed to bracket root (a_lo={a_lo:.6}, a_hi={a_hi:.6})"
125                    )
126                }
127            }
128            MonotoneRootError::RefinementDidNotConverge {
129                label,
130                last_residual,
131                ..
132            } => {
133                if let Some(real_label) = label.strip_prefix("__EXACT_ROOT__") {
134                    let a = last_residual;
135                    write!(
136                        f,
137                        "{real_label}: zero or non-finite derivative at exact root a={a:.6}"
138                    )
139                } else if let Some(real_label) = label.strip_prefix("__CONVERGED__") {
140                    let a = last_residual;
141                    write!(
142                        f,
143                        "{real_label}: zero or non-finite derivative at converged root a={a:.6}"
144                    )
145                } else {
146                    write!(
147                        f,
148                        "{label}: refinement did not converge (last residual={last_residual:.3e})"
149                    )
150                }
151            }
152        }
153    }
154}
155
156impl std::error::Error for MonotoneRootError {}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn eval_failed_shows_source() {
164        let err = MonotoneRootError::EvalFailed {
165            label: "test".to_string(),
166            a: 1.0,
167            source: "inner error msg".to_string(),
168        };
169        assert_eq!(err.to_string(), "inner error msg");
170    }
171
172    #[test]
173    fn non_finite_eval_shows_label_and_a() {
174        let err = MonotoneRootError::NonFiniteEval {
175            label: "myroot".to_string(),
176            a: 3.5,
177            f: f64::NAN,
178            fp: 0.0,
179            fpp: 0.0,
180        };
181        let msg = err.to_string();
182        assert!(msg.contains("myroot"), "message: {msg}");
183        assert!(msg.contains("3.5") || msg.contains("a=3"), "message: {msg}");
184    }
185
186    #[test]
187    fn degenerate_derivative_shows_label_and_a() {
188        let err = MonotoneRootError::DegenerateDerivative {
189            label: "solver".to_string(),
190            a: -1.0,
191            fp: 0.0,
192        };
193        let msg = err.to_string();
194        assert!(msg.contains("solver"), "message: {msg}");
195        assert!(msg.to_lowercase().contains("derivative") || msg.contains("zero"), "message: {msg}");
196    }
197
198    #[test]
199    fn bracketing_exhausted_default_shows_a_lo_and_a_hi() {
200        let err = MonotoneRootError::BracketingExhausted {
201            label: "mysolve".to_string(),
202            iters: 10,
203            a_lo: -1.0,
204            a_hi: 2.0,
205        };
206        let msg = err.to_string();
207        assert!(msg.contains("mysolve"), "message: {msg}");
208        assert!(msg.contains("-1") && msg.contains("2"), "message: {msg}");
209    }
210
211    #[test]
212    fn analytic_bracket_invalid_factory_shows_real_label() {
213        let err = MonotoneRootError::analytic_bracket_invalid("myfunc", 0.5, 1.5);
214        let msg = err.to_string();
215        assert!(msg.contains("myfunc"), "message: {msg}");
216        assert!(msg.contains("invalid") || msg.contains("bracket"), "message: {msg}");
217    }
218
219    #[test]
220    fn analytic_bracket_no_straddle_factory_shows_f_values() {
221        let err = MonotoneRootError::analytic_bracket_no_straddle("myfunc", 0.1, 0.2);
222        let msg = err.to_string();
223        assert!(msg.contains("myfunc"), "message: {msg}");
224        assert!(msg.to_lowercase().contains("straddle") || msg.contains("f_lo"), "message: {msg}");
225    }
226
227    #[test]
228    fn search_exhausted_factory_shows_real_label() {
229        let err = MonotoneRootError::search_exhausted("myfunc", 1.0, 0.0);
230        let msg = err.to_string();
231        assert!(msg.contains("myfunc"), "message: {msg}");
232        assert!(msg.to_lowercase().contains("bracket") || msg.contains("search"), "message: {msg}");
233    }
234
235    #[test]
236    fn refinement_did_not_converge_default_shows_residual() {
237        let err = MonotoneRootError::RefinementDidNotConverge {
238            label: "fit".to_string(),
239            iters: 50,
240            last_residual: 1.23e-4,
241        };
242        let msg = err.to_string();
243        assert!(msg.contains("fit"), "message: {msg}");
244        assert!(msg.to_lowercase().contains("converge"), "message: {msg}");
245    }
246
247    #[test]
248    fn exact_root_degenerate_factory_shows_real_label() {
249        let err = MonotoneRootError::exact_root_degenerate("myroot", 0.75);
250        let msg = err.to_string();
251        assert!(msg.contains("myroot"), "message: {msg}");
252        assert!(msg.contains("0.75") || msg.contains("a=0"), "message: {msg}");
253    }
254
255    #[test]
256    fn converged_root_degenerate_factory_shows_real_label() {
257        let err = MonotoneRootError::converged_root_degenerate("conv_root", 2.0);
258        let msg = err.to_string();
259        assert!(msg.contains("conv_root"), "message: {msg}");
260        assert!(msg.contains("2.0") || msg.contains("a=2"), "message: {msg}");
261    }
262}