1#[derive(Clone, Debug)]
4pub enum MonotoneRootError {
5 EvalFailed {
7 label: String,
8 a: f64,
9 source: String,
10 },
11 NonFiniteEval {
13 label: String,
14 a: f64,
15 f: f64,
16 fp: f64,
17 fpp: f64,
18 },
19 DegenerateDerivative { label: String, a: f64, fp: f64 },
22 BracketingExhausted {
24 label: String,
25 iters: usize,
26 a_lo: f64,
27 a_hi: f64,
28 },
29 RefinementDidNotConverge {
31 label: String,
32 iters: usize,
33 last_residual: f64,
34 },
35}
36
37impl MonotoneRootError {
41 pub fn exact_root_degenerate(label: &str, a: f64) -> Self {
42 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}