Skip to main content

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}