kinavis_kernel/error.rs
1//! Errors of the value types and numeric kernels.
2//!
3//! [`KernelError`] uses only the kernel's vocabulary: non-finite or
4//! out-of-range numbers, undersized buffers or stores, unsolvable systems,
5//! undefined quantities, missing inputs, reversed clocks. Algorithm-specific
6//! errors (deviation tables, sailings) belong to the crate that owns the
7//! algorithm, which wraps this type via `From`.
8//!
9//! `#[non_exhaustive]`: variants may be added in a minor release; match with a
10//! wildcard arm.
11//!
12//! [`ensure_finite`] and [`ensure_range`] are public so adapters validate
13//! sensor input the same way the value types do.
14
15use crate::inline::InlineStr;
16use core::fmt;
17
18/// Result alias.
19pub type Result<T> = core::result::Result<T, KernelError>;
20
21/// Maximum bytes of offending input carried by an error.
22///
23/// Enough to identify the input; bounded so errors need no allocator and cannot
24/// be inflated by input.
25pub const EXCERPT_BYTES: usize = 32;
26
27/// Excerpt of offending input.
28pub type Excerpt = InlineStr<EXCERPT_BYTES>;
29
30/// Kernel operation failure.
31///
32/// No operation panics on caller data; every failure is reported through this
33/// type.
34#[derive(Debug, Clone, PartialEq)]
35#[non_exhaustive]
36pub enum KernelError {
37 /// Parameter is `NaN` or infinite.
38 NotFinite {
39 /// Parameter name.
40 parameter: &'static str,
41 /// Value supplied.
42 value: f64,
43 },
44
45 /// Parameter is finite but out of range.
46 OutOfRange {
47 /// Parameter name.
48 parameter: &'static str,
49 /// Value supplied.
50 value: f64,
51 /// Minimum, inclusive.
52 min: f64,
53 /// Maximum, inclusive.
54 max: f64,
55 },
56
57 /// Fewer items than required: interpolation nodes, route waypoints, fit
58 /// samples.
59 InsufficientData {
60 /// Items present.
61 found: usize,
62 /// Items required.
63 required: usize,
64 /// Operation that required them.
65 context: &'static str,
66 },
67
68 /// Not one of the eight cardinal/intercardinal abbreviations.
69 UnknownCardinalDirection {
70 /// Input, truncated.
71 direction: Excerpt,
72 },
73
74 /// Caller-supplied output buffer too small.
75 ///
76 /// Returned by slice-writing calls so a short buffer is an error, not a
77 /// silent truncation.
78 BufferTooSmall {
79 /// Values produced.
80 needed: usize,
81 /// Buffer length.
82 found: usize,
83 },
84
85 /// Inline store capacity exceeded.
86 ///
87 /// Collections are fixed-capacity inline stores; their bounds are public
88 /// constants, so callers can check beforehand.
89 CapacityExceeded {
90 /// What was being built.
91 context: &'static str,
92 /// Items required.
93 needed: usize,
94 /// Capacity.
95 capacity: usize,
96 },
97
98 /// Linear system has no unique solution.
99 ///
100 /// For a parametric fit: the sample courses do not constrain the requested
101 /// coefficients (e.g. five coefficients from nodes on one semicircle).
102 SingularSystem {
103 /// System being solved.
104 context: &'static str,
105 },
106
107 /// Matrix is not a valid covariance (symmetric positive definite): e.g.
108 /// zero observation variance, collapsed innovation covariance.
109 NotCovariance {
110 /// Intended role of the matrix.
111 context: &'static str,
112 },
113
114 /// Iterative solver did not converge.
115 ///
116 /// The deviation inverse fails this way when deviation changes faster than
117 /// 1° per degree of heading, so several compass courses map to one magnetic
118 /// course; the compass needs re-swinging.
119 NotConverged {
120 /// Iterations run.
121 iterations: u32,
122 /// Final residual.
123 residual: f64,
124 },
125
126 /// String could not be parsed.
127 Parse {
128 /// Expected value type.
129 what: &'static str,
130 /// Input, truncated.
131 input: Excerpt,
132 },
133
134 /// Mathematically undefined for the inputs: rhumb line through a pole,
135 /// direction of a zero vector, great circle between antipodes.
136 Indeterminate {
137 /// Undefined quantity.
138 quantity: &'static str,
139 },
140
141 /// Required input unavailable: snapshot without position, environment
142 /// sample without tide, empty estimator history.
143 Missing {
144 /// Missing input.
145 what: &'static str,
146 },
147
148 /// Result exists but is not representable: instant beyond range, counter
149 /// overflow.
150 Unrepresentable {
151 /// Unrepresentable quantity.
152 what: &'static str,
153 },
154
155 /// Instants in the wrong order for the elapsed time requested (clock
156 /// stepped back, or arguments swapped).
157 TimeReversed {
158 /// Amount by which the later instant precedes the earlier.
159 by: core::time::Duration,
160 },
161
162 /// Height referred to a different vertical datum than required.
163 VerticalDatumMismatch {
164 /// Required datum.
165 required: crate::geodesy::VerticalDatum,
166 /// Datum of the height.
167 found: crate::geodesy::VerticalDatum,
168 },
169
170 /// Data requested outside its validity interval (leap-second table past
171 /// expiry, magnetic model past epoch).
172 OutsideValidity {
173 /// Requested data.
174 data: &'static str,
175 },
176}
177
178/// Checks that a value is finite.
179///
180/// # Errors
181///
182/// [`KernelError::NotFinite`] for `NaN` or infinity.
183pub fn ensure_finite(parameter: &'static str, value: f64) -> Result<()> {
184 if value.is_finite() {
185 Ok(())
186 } else {
187 Err(KernelError::NotFinite { parameter, value })
188 }
189}
190
191/// Checks that a value is finite and within `[min, max]`.
192///
193/// # Errors
194///
195/// [`KernelError::NotFinite`] for `NaN` or infinity;
196/// [`KernelError::OutOfRange`] outside the interval.
197pub fn ensure_range(parameter: &'static str, value: f64, min: f64, max: f64) -> Result<()> {
198 ensure_finite(parameter, value)?;
199 if value < min || value > max {
200 return Err(KernelError::OutOfRange {
201 parameter,
202 value,
203 min,
204 max,
205 });
206 }
207 Ok(())
208}
209
210impl fmt::Display for KernelError {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 Self::NotFinite { parameter, value } => {
214 write!(f, "{parameter} must be a finite number, got {value}")
215 }
216 Self::OutOfRange {
217 parameter,
218 value,
219 min,
220 max,
221 } => write!(
222 f,
223 "{parameter} out of range: {value}. Must be between {min} and {max}"
224 ),
225 Self::InsufficientData {
226 found,
227 required,
228 context,
229 } => write!(f, "{context} needs at least {required}, and has {found}"),
230 Self::UnknownCardinalDirection { direction } => write!(
231 f,
232 "unknown cardinal direction: {direction}. Expected one of N, NE, E, SE, S, SW, W, NW"
233 ),
234 Self::CapacityExceeded {
235 context,
236 needed,
237 capacity,
238 } => write!(
239 f,
240 "{context} needs room for {needed}, and the limit is {capacity}"
241 ),
242 Self::BufferTooSmall { needed, found } => write!(
243 f,
244 "output buffer holds {found} values, {needed} are needed"
245 ),
246 Self::SingularSystem { context } => {
247 write!(f, "singular system while solving {context}")
248 }
249 Self::NotCovariance { context } => {
250 write!(f, "{context} is not a covariance matrix")
251 }
252 Self::NotConverged {
253 iterations,
254 residual,
255 } => write!(
256 f,
257 "solver did not converge after {iterations} iterations, residual {residual}"
258 ),
259 Self::Parse { what, input } => {
260 write!(f, "could not read {input:?} as a {what}")
261 }
262 Self::Indeterminate { quantity } => {
263 write!(f, "{quantity} is indeterminate for these inputs")
264 }
265 Self::Missing { what } => write!(f, "{what} is not available"),
266 Self::Unrepresentable { what } => write!(f, "{what} cannot be represented"),
267 Self::TimeReversed { by } => {
268 write!(f, "time ran backwards by {} s", by.as_secs_f64())
269 }
270 Self::VerticalDatumMismatch { required, found } => write!(
271 f,
272 "a height above {found:?} was given where one above {required:?} is required"
273 ),
274 Self::OutsideValidity { data } => {
275 write!(f, "the {data} is not valid for the requested moment")
276 }
277 }
278 }
279}
280
281// `std::error::Error` re-exports `core::error::Error` since Rust 1.81; one impl
282// covers `std` and `no_std`.
283impl core::error::Error for KernelError {}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use alloc::string::ToString;
289
290 #[test]
291 fn every_variant_has_a_message() {
292 let errors = [
293 KernelError::NotFinite {
294 parameter: "course",
295 value: f64::NAN,
296 },
297 KernelError::OutOfRange {
298 parameter: "course",
299 value: 400.0,
300 min: 0.0,
301 max: 360.0,
302 },
303 KernelError::InsufficientData {
304 found: 1,
305 required: 2,
306 context: "interpolation",
307 },
308 KernelError::UnknownCardinalDirection {
309 direction: Excerpt::new("XYZ"),
310 },
311 KernelError::BufferTooSmall {
312 needed: 36,
313 found: 8,
314 },
315 KernelError::CapacityExceeded {
316 context: "a deviation table",
317 needed: 90,
318 capacity: 72,
319 },
320 KernelError::SingularSystem {
321 context: "parametric fit",
322 },
323 KernelError::NotCovariance {
324 context: "the observation noise",
325 },
326 KernelError::TimeReversed {
327 by: core::time::Duration::from_secs(18),
328 },
329 KernelError::OutsideValidity {
330 data: "leap second table",
331 },
332 KernelError::VerticalDatumMismatch {
333 required: crate::geodesy::VerticalDatum::Ellipsoid,
334 found: crate::geodesy::VerticalDatum::MeanSeaLevel,
335 },
336 KernelError::NotConverged {
337 iterations: 64,
338 residual: 1.0,
339 },
340 KernelError::Parse {
341 what: "latitude",
342 input: Excerpt::new("north-ish"),
343 },
344 KernelError::Indeterminate {
345 quantity: "a rhumb line through a pole",
346 },
347 KernelError::Missing {
348 what: "the vessel's position",
349 },
350 KernelError::Unrepresentable {
351 what: "a moment beyond the end of time",
352 },
353 ];
354 for error in errors {
355 assert!(!error.to_string().is_empty(), "{error:?}");
356 }
357 }
358
359 #[test]
360 fn errors_compare_by_value() {
361 let a = KernelError::OutOfRange {
362 parameter: "x",
363 value: 1.0,
364 min: 0.0,
365 max: 0.5,
366 };
367 assert_eq!(a, a.clone());
368 assert_ne!(
369 a,
370 KernelError::Missing {
371 what: "the vessel's position"
372 }
373 );
374 }
375
376 #[test]
377 fn the_checks_report_what_they_reject() {
378 assert!(ensure_finite("x", 1.0).is_ok());
379 assert!(matches!(
380 ensure_finite("x", f64::NAN),
381 Err(KernelError::NotFinite { parameter: "x", .. })
382 ));
383 assert!(matches!(
384 ensure_range("x", 2.0, 0.0, 1.0),
385 Err(KernelError::OutOfRange { parameter: "x", .. })
386 ));
387 assert!(matches!(
388 ensure_range("x", f64::INFINITY, 0.0, 1.0),
389 Err(KernelError::NotFinite { .. })
390 ));
391 }
392}