use pyo3::exceptions::{PyRuntimeError, PyValueError, PyTypeError, PyArithmeticError};
use pyo3::PyErr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PoliastroError {
#[error("Convergence failure: {method} failed to converge after {iterations} iterations (tolerance: {tolerance})")]
ConvergenceFailure {
method: String,
iterations: usize,
tolerance: f64,
},
#[error("Invalid numerical value encountered: {context} = {value}")]
InvalidNumericalValue {
context: String,
value: f64,
},
#[error("Division by zero or near-zero value in {context}: divisor = {divisor}")]
DivisionByZero {
context: String,
divisor: f64,
},
#[error("Numerical instability detected in {operation}: {details}")]
NumericalInstability {
operation: String,
details: String,
},
#[error("Singular matrix encountered in {context}: determinant = {determinant}")]
SingularMatrix {
context: String,
determinant: f64,
},
#[error("Invalid parameter '{parameter}': value {value} {constraint}")]
InvalidParameter {
parameter: String,
value: f64,
constraint: String,
},
#[error("Invalid eccentricity: {value} (must be >= 0)")]
InvalidEccentricity {
value: f64,
},
#[error("Invalid semi-major axis: {value} km (must be > 0 for elliptic orbits)")]
InvalidSemiMajorAxis {
value: f64,
},
#[error("Invalid inclination: {value} rad (must be in [0, π])")]
InvalidInclination {
value: f64,
},
#[error("Invalid angle '{name}': {value} rad (expected range: [{min}, {max}])")]
InvalidAngle {
name: String,
value: f64,
min: f64,
max: f64,
},
#[error("Energy conservation violated: ΔE = {delta_energy} (tolerance: {tolerance})")]
EnergyNotConserved {
delta_energy: f64,
tolerance: f64,
},
#[error("Angular momentum conservation violated: ΔL = {delta_momentum} (tolerance: {tolerance})")]
MomentumNotConserved {
delta_momentum: f64,
tolerance: f64,
},
#[error("Invalid state vector: {reason}")]
InvalidStateVector {
reason: String,
},
#[error("Zero or near-zero position vector: |r| = {magnitude} km")]
ZeroPosition {
magnitude: f64,
},
#[error("Zero or near-zero velocity vector: |v| = {magnitude} km/s")]
ZeroVelocity {
magnitude: f64,
},
#[error("Coordinate transformation failed from {from_frame} to {to_frame}: {reason}")]
TransformationFailure {
from_frame: String,
to_frame: String,
reason: String,
},
#[error("Singularity in orbital elements: {singularity_type} (consider using {alternative})")]
OrbitalSingularity {
singularity_type: String,
alternative: String,
},
#[error("Integration failure in {integrator}: {reason}")]
IntegrationFailure {
integrator: String,
reason: String,
},
#[error("Time step too large: {step_size} s (recommended: < {max_recommended} s for {orbit_type})")]
TimeStepTooLarge {
step_size: f64,
max_recommended: f64,
orbit_type: String,
},
#[error("Propagation diverged after {time} s: {reason}")]
PropagationDivergence {
time: f64,
reason: String,
},
#[error("Propagation failed in {context}: {source}")]
PropagationFailed {
context: String,
source: Box<PoliastroError>,
},
#[error("Value out of range for '{parameter}': {value} (expected: [{min}, {max}])")]
OutOfRange {
parameter: String,
value: f64,
min: f64,
max: f64,
},
#[error("Incompatible units: expected {expected}, got {actual}")]
IncompatibleUnits {
expected: String,
actual: String,
},
#[error("Invalid time value: {reason}")]
InvalidTime {
reason: String,
},
#[error("Missing required parameter: {parameter}")]
MissingParameter {
parameter: String,
},
#[error("Operation '{operation}' not supported for {orbit_type} orbits")]
UnsupportedOrbitType {
operation: String,
orbit_type: String,
},
#[error("Cannot determine orbit type: {reason}")]
AmbiguousOrbitType {
reason: String,
},
#[error("Computation error: {message}")]
ComputationError {
message: String,
},
#[error("Not implemented: {feature}")]
NotImplemented {
feature: String,
},
#[error("Internal error: {message} (this is a bug, please report it)")]
InternalError {
message: String,
},
}
pub type PoliastroResult<T> = Result<T, PoliastroError>;
impl From<PoliastroError> for PyErr {
fn from(err: PoliastroError) -> PyErr {
use PoliastroError::*;
match err {
ConvergenceFailure { .. }
| DivisionByZero { .. }
| NumericalInstability { .. }
| SingularMatrix { .. } => PyArithmeticError::new_err(err.to_string()),
InvalidNumericalValue { .. } => PyValueError::new_err(err.to_string()),
InvalidParameter { .. }
| InvalidEccentricity { .. }
| InvalidSemiMajorAxis { .. }
| InvalidInclination { .. }
| InvalidAngle { .. }
| EnergyNotConserved { .. }
| MomentumNotConserved { .. } => PyValueError::new_err(err.to_string()),
InvalidStateVector { .. }
| ZeroPosition { .. }
| ZeroVelocity { .. }
| OrbitalSingularity { .. } => PyValueError::new_err(err.to_string()),
TransformationFailure { .. } => PyRuntimeError::new_err(err.to_string()),
IntegrationFailure { .. }
| PropagationDivergence { .. }
| PropagationFailed { .. } => PyRuntimeError::new_err(err.to_string()),
OutOfRange { .. }
| InvalidTime { .. }
| MissingParameter { .. }
| TimeStepTooLarge { .. } => PyValueError::new_err(err.to_string()),
IncompatibleUnits { .. } => PyTypeError::new_err(err.to_string()),
UnsupportedOrbitType { .. }
| AmbiguousOrbitType { .. } => PyValueError::new_err(err.to_string()),
ComputationError { .. }
| NotImplemented { .. }
| InternalError { .. } => PyRuntimeError::new_err(err.to_string()),
}
}
}
impl PoliastroError {
pub fn convergence_failure(method: impl Into<String>, iterations: usize, tolerance: f64) -> Self {
Self::ConvergenceFailure {
method: method.into(),
iterations,
tolerance,
}
}
pub fn invalid_parameter(
parameter: impl Into<String>,
value: f64,
constraint: impl Into<String>,
) -> Self {
Self::InvalidParameter {
parameter: parameter.into(),
value,
constraint: constraint.into(),
}
}
pub fn out_of_range(parameter: impl Into<String>, value: f64, min: f64, max: f64) -> Self {
Self::OutOfRange {
parameter: parameter.into(),
value,
min,
max,
}
}
pub fn invalid_state(reason: impl Into<String>) -> Self {
Self::InvalidStateVector {
reason: reason.into(),
}
}
pub fn not_implemented(feature: impl Into<String>) -> Self {
Self::NotImplemented {
feature: feature.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::InternalError {
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convergence_failure() {
let err = PoliastroError::convergence_failure("Newton-Raphson", 100, 1e-10);
assert!(err.to_string().contains("Newton-Raphson"));
assert!(err.to_string().contains("100"));
}
#[test]
fn test_invalid_parameter() {
let err = PoliastroError::invalid_parameter("mass", -10.0, "must be positive");
assert!(err.to_string().contains("mass"));
assert!(err.to_string().contains("-10"));
assert!(err.to_string().contains("positive"));
}
#[test]
fn test_out_of_range() {
let err = PoliastroError::out_of_range("eccentricity", 1.5, 0.0, 1.0);
assert!(err.to_string().contains("eccentricity"));
assert!(err.to_string().contains("1.5"));
}
#[test]
fn test_invalid_eccentricity() {
let err = PoliastroError::InvalidEccentricity { value: -0.5 };
assert!(err.to_string().contains("eccentricity"));
assert!(err.to_string().contains("-0.5"));
}
#[test]
fn test_division_by_zero() {
let err = PoliastroError::DivisionByZero {
context: "orbital period calculation".to_string(),
divisor: 0.0,
};
assert!(err.to_string().contains("Division by zero"));
assert!(err.to_string().contains("orbital period"));
}
#[test]
fn test_orbital_singularity() {
let err = PoliastroError::OrbitalSingularity {
singularity_type: "circular orbit (e=0)".to_string(),
alternative: "equinoctial elements".to_string(),
};
assert!(err.to_string().contains("Singularity"));
assert!(err.to_string().contains("circular"));
assert!(err.to_string().contains("equinoctial"));
}
#[test]
fn test_not_implemented() {
let err = PoliastroError::not_implemented("GPU acceleration");
assert!(err.to_string().contains("Not implemented"));
assert!(err.to_string().contains("GPU"));
}
#[test]
fn test_internal_error() {
let err = PoliastroError::internal("unexpected state in propagator");
assert!(err.to_string().contains("Internal error"));
assert!(err.to_string().contains("bug"));
}
#[test]
fn test_energy_conservation() {
let err = PoliastroError::EnergyNotConserved {
delta_energy: 1e-6,
tolerance: 1e-10,
};
let msg = err.to_string();
assert!(msg.contains("Energy"));
assert!(msg.contains("0.000001") || msg.contains("1e-6"));
}
#[test]
fn test_incompatible_units() {
let err = PoliastroError::IncompatibleUnits {
expected: "meters".to_string(),
actual: "radians".to_string(),
};
assert!(err.to_string().contains("units"));
assert!(err.to_string().contains("meters"));
assert!(err.to_string().contains("radians"));
}
}