1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//! Error taxonomy for `semiflow`.
//!
//! See `contracts/semiflow-core.errors.yaml` for the authoritative listing.
//! Every fallible operation returns `Result<T, SemiflowError>`; the library
//! never panics in release mode (errors-as-values, suckless principle).
//!
//! ## Recovery patterns
//!
//! | Variant | Recovery |
//! |---------|----------|
//! | [`SemiflowError::DomainViolation`] | fix the offending input value |
//! | [`SemiflowError::GridUnderresolved`] | use `suggested_n` nodes |
//! | [`SemiflowError::CflViolated`] | use `tau_safe = 0.45 * dx² / a_norm_bound` |
//! | [`SemiflowError::AdaptiveStepRejected`] | loosen `tol_rel`/`tol_abs`, coarsen grid |
//! | [`SemiflowError::ConvergenceFailed`] | raise the cap that was hit (PCG `max_iter`, `RationalOptions::m_max`, `KrylovPhiOptions::max_substeps`, `RosenbrockOptions::max_steps`), loosen `tol`, or for PCG use a stronger `Precond` / the direct `LDLᵀ` path |
//! | [`SemiflowError::Unsupported`] | enable the named feature flag |
use core::fmt;
/// Errors returned by `semiflow` operations.
///
/// `#[non_exhaustive]` — new variants may be added in minor versions without
/// a semver break. Match with a catch-all `_ => {}` arm when forward-compat
/// is required.
///
/// All `f64` payload fields (`value`, `tau`, etc.) may be `NaN` / `Inf` when
/// the offending input was itself non-finite.
#[derive(Debug, Clone)]
#[non_exhaustive]
#[allow(clippy::module_name_repetitions)]
pub enum SemiflowError {
/// Caller passed input that violates a documented precondition.
///
/// Examples: `n < 4` in `Grid1D::new`, `tau < 0` in `apply`,
/// `xmin >= xmax`.
DomainViolation {
/// Static label for the violated invariant (stable, grep-friendly).
what: &'static str,
/// Offending numeric value. `NaN`/`Inf` permitted.
value: f64,
},
/// The Chernoff shift exceeds the grid spacing — accuracy may degrade.
///
/// v0.1.0 policy: soft signal only; `apply` still returns `Ok`.
GridUnderresolved {
/// Recommended grid size to resolve the shift.
suggested_n: usize,
/// `(max shift) / dx` ratio that triggered the warning (> 1.0).
shift_dx_ratio: f64,
},
/// Iterative solver did not converge within the iteration cap.
///
/// Returned when an iterative method hits its cap: the PCG solvers
/// (`ImplicitEuler` Krylov path, `SpdResolvent` PCG path, including its
/// gross-residual guard), rational Krylov (`m_max`), Krylov φ / `expmv`
/// (substep or rejection cap) and ROS2 kinetics (`max_steps`).
ConvergenceFailed {
/// Final residual when the solver gave up. `ImplicitEuler`: absolute recursive CG
/// residual norm. `SpdResolvent`: relative true residual `‖b − Sx‖₂/‖b‖₂`.
last_residual: f64,
/// Configured iteration cap (not necessarily the iterations performed: the
/// `SpdResolvent` residual guard can fire before the cap).
max_iter: usize,
},
/// A feature is declared in the public API but not yet implemented.
///
/// v0.1.0: returned by `BoundaryPolicy::{ZeroExtend, Periodic,
/// LinearExtrapolate}` and `InterpKind::Linear` (without the
/// `linear-interp` feature).
Unsupported {
/// Which optional feature is unsupported. Stable label.
feature: &'static str,
},
/// CFL bound violated for the truncated-exp K=4 power series.
///
/// Returned by `TruncatedExpDiffusionChernoff::apply` when
/// `2·tau·a_norm_bound ≥ dx²`. The K=4 partial sum diverges in
/// operator norm when this bound is violated (BCOR 2009 §3.1).
///
/// Caller pattern: retry with `tau_safe = 0.45 * dx_squared / a_norm_bound`.
///
/// **v0.7.0 Block C note (ADR-0016, math.md §10.7-bis.4)**: for
/// `NonSeparable2DChernoff`, `dx_squared` semantically holds `dx · dy`
/// (cell area) and `a_norm_bound` holds `‖c‖_∞`. The variant is
/// REUSED — no new variant was added.
CflViolated {
/// The Chernoff step that violated the CFL bound.
tau: f64,
/// `dx² = grid.dx() * grid.dx()` at the point of failure.
/// For `NonSeparable2DChernoff`: `dx · dy` (cell area).
dx_squared: f64,
/// The `a_norm_bound` of the `TruncatedExpDiffusionChernoff` instance.
/// For `NonSeparable2DChernoff`: `‖c‖_∞`.
a_norm_bound: f64,
},
/// Adaptive PI step controller failed to converge within `max_substeps`.
///
/// Returned exclusively by [`crate::AdaptivePI::evolve_adaptive`] when
/// `steps_attempted >= max_substeps` (runaway protection). This indicates
/// a pathological tolerance mismatch — the requested `tol_rel` / `tol_abs`
/// is tighter than what the underlying `ChernoffFunction` can achieve for
/// the given grid and time step configuration.
///
/// Caller pattern: increase `tol_rel`, coarsen the grid, or reduce `t`.
///
/// See: ADR-0014, `contracts/semiflow-core.errors.yaml`.
AdaptiveStepRejected {
/// Last substep size attempted before the cap was hit.
last_tau: f64,
/// Richardson error estimate on the last attempted substep.
last_err: f64,
/// Total substeps attempted (accepted + rejected); equals `max_substeps`.
steps_attempted: usize,
},
/// Operation requires a primitive not yet available for this inner type.
///
/// Returned by [`crate::adjoint::AdjointChernoff`] `apply_into` when called via
/// `new_general` on an inner that does not implement
/// [`crate::adjoint::AdjointApply`] (no transpose-apply primitive).
///
/// Caller options: (a) use `new_self_adjoint` for symmetric inners;
/// (b) use an inner that implements `AdjointApply` (e.g.,
/// `DriftReactionChernoff`). See ADR-0114.
UnsupportedOperation {
/// What operation was requested and why it cannot be fulfilled.
what: &'static str,
},
/// Magnus convergence radius violated: `ρ̄_max · τ ≥ π/2`.
///
/// Returned by [`crate::magnus_graph::MagnusGraphHeatChernoff`] `apply_into` when
/// `rho_bar_max * tau >= π/2` (50% safety margin vs. theoretical `< π`).
///
/// Caller MUST reduce `τ` or supply a tighter `rho_bar_max` bound.
///
/// See: ADR-0051, math.md §12.9 (NORMATIVE library policy).
OutOfMagnusRadius {
/// The Chernoff step `τ` that violated the convergence radius.
tau: f64,
/// The caller-supplied Gershgorin spectral-radius estimate `ρ̄_max`.
rho_estimate: f64,
},
/// Input lies outside the proven operator class for an S³ POC evolver.
///
/// Returned by the `S3*` constructor family (ADR-0169, v9.2.0) when the
/// supplied parameters cannot represent an in-class operator — for example,
/// a diffusion coefficient `a[j] ≤ 0` (non-parabolic), a CP-rank coefficient
/// that violates parabolicity on the grid, or `u0` outside `(0,1)` for
/// [`crate::S3BurgersColeHopf`] logistic reaction.
///
/// `detail` is a stable, grep-friendly static label naming the violated class
/// boundary. See the `## Proven boundary` stanza on each S³ type for the
/// full class description.
///
/// **Recovery:** adjust the input to lie within the proven class, or choose
/// a different evolver that covers the desired operator.
#[cfg(feature = "s3-poc")]
S3OutOfClass {
/// Stable label for the violated class boundary.
detail: &'static str,
},
/// Input lies outside the proven operator class for [`crate::VarCoefTt`].
///
/// Returned by [`crate::VarCoefTt::new`] when the supplied parameters
/// cannot represent an in-class additive-separable variable-coefficient
/// operator — for example, `a_axis[j][i] ≤ 0` (non-parabolic), shape
/// mismatch between axes, or `d == 0`.
///
/// `detail` is a stable, grep-friendly static label naming the violated
/// class boundary.
///
/// **Recovery:** adjust the input to lie within the additive-separable
/// parabolic class described in [`crate::VarCoefTt`].
VarCoefOutOfClass {
/// Stable label for the violated class boundary.
detail: &'static str,
},
}
/// Format helpers — each formats one variant, keeping `fmt` under 50 lines.
fn fmt_grid_underresolved(
f: &mut fmt::Formatter<'_>,
suggested_n: usize,
shift_dx_ratio: f64,
) -> fmt::Result {
write!(
f,
"grid under-resolved: shift/dx = {shift_dx_ratio:.3}, \
suggested N >= {suggested_n}"
)
}
fn fmt_cfl_violated(
f: &mut fmt::Formatter<'_>,
tau: f64,
dx_squared: f64,
a_norm_bound: f64,
) -> fmt::Result {
write!(
f,
"CFL violated for truncated-exp K=4: tau = {tau:.3e}, \
dx\u{00B2} = {dx_squared:.3e}, \
\u{2016}a\u{2016}_\u{221E} = {a_norm_bound:.3e} \
(need 2\u{00B7}tau\u{00B7}\u{2016}a\u{2016}_\u{221E} < dx\u{00B2})"
)
}
fn fmt_adaptive_rejected(
f: &mut fmt::Formatter<'_>,
last_tau: f64,
last_err: f64,
steps_attempted: usize,
) -> fmt::Result {
write!(
f,
"adaptive PI: step rejected after {steps_attempted} substeps; \
last_tau = {last_tau:.3e}, last_err = {last_err:.3e} \
(increase tol_rel/tol_abs or reduce t)"
)
}
fn fmt_out_of_magnus_radius(
f: &mut fmt::Formatter<'_>,
tau: f64,
rho_estimate: f64,
) -> fmt::Result {
write!(
f,
"Magnus convergence radius violated: \
rho_bar_max = {rho_estimate:.3e}, tau = {tau:.3e}, \
product = {:.3e} >= pi/2 \
(reduce tau or supply tighter rho_bar_max)",
rho_estimate * tau
)
}
impl fmt::Display for SemiflowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DomainViolation { what, value } => {
write!(f, "domain violation: {what} (value = {value})")
}
Self::GridUnderresolved {
suggested_n,
shift_dx_ratio,
} => fmt_grid_underresolved(f, *suggested_n, *shift_dx_ratio),
Self::ConvergenceFailed {
last_residual,
max_iter,
} => {
write!(
f,
"convergence failed: last_residual = {last_residual:.3e}, \
max_iter = {max_iter}"
)
}
Self::Unsupported { feature } => {
write!(f, "feature not supported in this build: {feature}")
}
Self::UnsupportedOperation { what } => {
write!(f, "unsupported operation: {what}")
}
Self::CflViolated {
tau,
dx_squared,
a_norm_bound,
} => fmt_cfl_violated(f, *tau, *dx_squared, *a_norm_bound),
Self::AdaptiveStepRejected {
last_tau,
last_err,
steps_attempted,
} => fmt_adaptive_rejected(f, *last_tau, *last_err, *steps_attempted),
Self::OutOfMagnusRadius { tau, rho_estimate } => {
fmt_out_of_magnus_radius(f, *tau, *rho_estimate)
}
#[cfg(feature = "s3-poc")]
Self::S3OutOfClass { detail } => {
write!(f, "S³ out-of-class: {detail}")
}
Self::VarCoefOutOfClass { detail } => {
write!(f, "VarCoefTt out-of-class: {detail}")
}
}
}
}
// `core::error::Error` (Rust 1.81) is `std::error::Error`: `?` into
// `Box<dyn Error>` works in every build, `std` or not (ADR-0201).
impl core::error::Error for SemiflowError {}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
use alloc::string::ToString;
include!("error_tests_mod.rs");
}