Skip to main content

pykep_core/
error.rs

1// Copyright (c) 2026 pykep-rust contributors
2// SPDX-License-Identifier: MPL-2.0
3
4//! Error types shared by the numerical core.
5
6use core::fmt;
7
8/// Error returned by a pykep numerical operation.
9#[derive(Clone, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum PykepError {
12    /// A finite input lies outside the mathematical domain.
13    InvalidInput {
14        /// Public parameter name.
15        parameter: &'static str,
16        /// Human-readable domain requirement.
17        reason: String,
18    },
19    /// A public input is NaN or infinite.
20    NonFiniteInput {
21        /// Public parameter name.
22        parameter: &'static str,
23    },
24    /// The requested operation is undefined for the supplied geometry.
25    SingularGeometry {
26        /// Operation that detected the singularity.
27        operation: &'static str,
28    },
29    /// An iterative algorithm exhausted its iteration limit.
30    ConvergenceFailure {
31        /// Iterative operation that failed.
32        operation: &'static str,
33        /// Number of iterations attempted.
34        iterations: usize,
35    },
36    /// A dynamically sized value has an invalid length.
37    DimensionMismatch {
38        /// Required number of scalar values.
39        expected: usize,
40        /// Supplied number of scalar values.
41        actual: usize,
42    },
43    /// An ephemeris or backend does not implement a requested capability.
44    UnsupportedCapability {
45        /// Provider or backend name.
46        provider: String,
47        /// Unsupported capability name.
48        capability: &'static str,
49    },
50    /// A finite input produced a value outside the binary64 range.
51    NumericalOverflow {
52        /// Numerical operation that overflowed.
53        operation: &'static str,
54    },
55    /// A numerical integrator could not complete a propagation.
56    IntegrationFailure {
57        /// Dynamics model being integrated.
58        model: &'static str,
59        /// Human-readable failure context.
60        reason: String,
61    },
62    /// A required embedded or external dataset is unavailable or corrupt.
63    DataUnavailable {
64        /// Stable dataset name.
65        dataset: &'static str,
66    },
67}
68
69impl fmt::Display for PykepError {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::InvalidInput { parameter, reason } => {
73                write!(formatter, "invalid {parameter}: {reason}")
74            }
75            Self::NonFiniteInput { parameter } => {
76                write!(formatter, "{parameter} must be finite")
77            }
78            Self::SingularGeometry { operation } => {
79                write!(formatter, "singular geometry in {operation}")
80            }
81            Self::ConvergenceFailure {
82                operation,
83                iterations,
84            } => write!(
85                formatter,
86                "{operation} did not converge after {iterations} iterations"
87            ),
88            Self::DimensionMismatch { expected, actual } => {
89                write!(
90                    formatter,
91                    "dimension mismatch: expected {expected}, got {actual}"
92                )
93            }
94            Self::UnsupportedCapability {
95                provider,
96                capability,
97            } => write!(formatter, "{provider} does not support {capability}"),
98            Self::NumericalOverflow { operation } => {
99                write!(formatter, "floating-point overflow in {operation}")
100            }
101            Self::IntegrationFailure { model, reason } => {
102                write!(formatter, "integration of {model} failed: {reason}")
103            }
104            Self::DataUnavailable { dataset } => {
105                write!(formatter, "required dataset is unavailable: {dataset}")
106            }
107        }
108    }
109}
110
111impl std::error::Error for PykepError {}
112
113/// Result returned by fallible pykep operations.
114pub type Result<T> = core::result::Result<T, PykepError>;
115
116pub(crate) fn ensure_finite(parameter: &'static str, value: f64) -> Result<()> {
117    if value.is_finite() {
118        Ok(())
119    } else {
120        Err(PykepError::NonFiniteInput { parameter })
121    }
122}
123
124pub(crate) fn ensure_finite_values(names_and_values: &[(&'static str, f64)]) -> Result<()> {
125    for &(name, value) in names_and_values {
126        ensure_finite(name, value)?;
127    }
128    Ok(())
129}
130
131pub(crate) fn ensure_finite_output(operation: &'static str, value: f64) -> Result<f64> {
132    if value.is_finite() {
133        Ok(value)
134    } else {
135        Err(PykepError::NumericalOverflow { operation })
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::PykepError;
142
143    #[test]
144    fn errors_have_stable_useful_messages() {
145        let cases = [
146            (
147                PykepError::InvalidInput {
148                    parameter: "x",
149                    reason: "must be positive".into(),
150                },
151                "invalid x: must be positive",
152            ),
153            (
154                PykepError::NonFiniteInput { parameter: "x" },
155                "x must be finite",
156            ),
157            (
158                PykepError::SingularGeometry { operation: "orbit" },
159                "singular geometry in orbit",
160            ),
161            (
162                PykepError::ConvergenceFailure {
163                    operation: "solver",
164                    iterations: 10,
165                },
166                "solver did not converge after 10 iterations",
167            ),
168            (
169                PykepError::DimensionMismatch {
170                    expected: 3,
171                    actual: 2,
172                },
173                "dimension mismatch: expected 3, got 2",
174            ),
175            (
176                PykepError::UnsupportedCapability {
177                    provider: "minimal".into(),
178                    capability: "acceleration",
179                },
180                "minimal does not support acceleration",
181            ),
182            (
183                PykepError::NumericalOverflow {
184                    operation: "multiply",
185                },
186                "floating-point overflow in multiply",
187            ),
188            (
189                PykepError::IntegrationFailure {
190                    model: "model",
191                    reason: "step limit".into(),
192                },
193                "integration of model failed: step limit",
194            ),
195            (
196                PykepError::DataUnavailable { dataset: "series" },
197                "required dataset is unavailable: series",
198            ),
199        ];
200        for (error, expected) in cases {
201            assert_eq!(error.to_string(), expected);
202        }
203    }
204}