# astra/errors.py
"""ASTRA Core exception hierarchy.
All custom exceptions for the ASTRA library. Every error condition
maps to a typed exception that carries full diagnostic information.
Hierarchy:
AstraError (base)
├── InvalidTLEError — TLE parsing and validation failures
├── PropagationError — Orbit propagation failures (SGP4, Cowell IVP, etc.)
├── FilterError — Invalid filter configuration
├── CoordinateError — Coordinate frame conversion failures
├── ManeuverError — Maneuver definition or execution failures
├── SpaceWeatherError — Solar flux/Ap data unavailable in STRICT_MODE
├── EphemerisError — JPL ephemeris data cannot be loaded in STRICT_MODE
└── SpacebookError — COMSPOC Spacebook API failures
└── SpacebookLookupError — NORAD ID cannot be resolved to a COMSPOC GUID
"""
from __future__ import annotations
from typing import Any, Optional
from enum import IntEnum
[docs]
class SGP4ErrorCode(IntEnum):
"""SGP4 propagation error codes as documented by the sgp4 library.
These codes indicate why SGP4 propagation failed. Error code 0 means success.
Reference: https://github.com/brandon-rhodes/python-sgp4/blob/master/sgp4/api.py
Attributes:
OK: No error, propagation succeeded.
MEAN_ELEMENTS_INVALID: Mean elements are invalid (eccentricity >= 1.0, etc.).
MEAN_MOTION_TOO_SMALL: Mean motion is too small for the orbital model.
PEMEG: Perigee altitude < 156 km (atmospheric drag model breaks down).
INITIAL_POSITION_ERROR: Error computing initial position from elements.
FUTURE_POSITION_ERROR: Error computing future position (decayed, etc.).
SATELLITE_DECAYED: Satellite has decayed below 156 km altitude.
"""
OK = 0
MEAN_ELEMENTS_INVALID = 1
MEAN_MOTION_TOO_SMALL = 2
PEMEG = 3 # Perigee error
INITIAL_POSITION_ERROR = 4
FUTURE_POSITION_ERROR = 5
SATELLITE_DECAYED = 6
[docs]
def describe(self) -> str:
"""Return human-readable description of the error code."""
descriptions = {
SGP4ErrorCode.OK: "Propagation succeeded",
SGP4ErrorCode.MEAN_ELEMENTS_INVALID: "Mean elements are invalid (eccentricity >= 1.0 or inclination < 0° or > 180°)",
SGP4ErrorCode.MEAN_MOTION_TOO_SMALL: "Mean motion is too small for the orbital model",
SGP4ErrorCode.PEMEG: "Perigee altitude below 156 km - atmospheric drag model breaks down",
SGP4ErrorCode.INITIAL_POSITION_ERROR: "Error computing initial position from orbital elements",
SGP4ErrorCode.FUTURE_POSITION_ERROR: "Error computing future position (satellite may have decayed)",
SGP4ErrorCode.SATELLITE_DECAYED: "Satellite has decayed below 156 km altitude",
}
return descriptions.get(self, f"Unknown SGP4 error code: {self.value}")
[docs]
@classmethod
def is_success(cls, code: int) -> bool:
"""Check if an error code indicates successful propagation."""
return code == cls.OK
[docs]
class AstraError(Exception):
"""Base class for all ASTRA Core exceptions.
All ASTRA exceptions derive from this class, allowing callers
to catch ``AstraError`` for broad handling or specific subclasses
for targeted handling.
Args:
message: Human-readable error description.
**context: Arbitrary diagnostic key-value pairs attached to the error.
"""
def __init__(self, message: str, **context: Any) -> None:
super().__init__(message)
self.message: str = message
self.context: dict[str, Any] = context
def __str__(self) -> str:
if self.context:
ctx = ", ".join(
f"{k}={v!r}" for k, v in self.context.items() if v is not None
)
return f"{self.message} [{ctx}]" if ctx else self.message
return self.message
[docs]
class InvalidTLEError(AstraError):
"""Raised when TLE parsing or validation fails.
Args:
message: Human-readable error description.
norad_id: NORAD catalog number of the offending object, if known.
object_name: Human-readable name, if known.
invalid_line: The raw TLE line that caused the failure.
reason: Machine-readable failure reason code (see docs).
"""
def __init__(
self,
message: str,
norad_id: Optional[str] = None,
object_name: Optional[str] = None,
invalid_line: Optional[str] = None,
reason: Optional[str] = None,
) -> None:
super().__init__(
message,
norad_id=norad_id,
object_name=object_name,
invalid_line=invalid_line,
reason=reason,
)
self.norad_id: Optional[str] = norad_id
self.object_name: Optional[str] = object_name
self.invalid_line: Optional[str] = invalid_line
self.reason: Optional[str] = reason
[docs]
class PropagationError(AstraError):
"""Raised when numerical orbit propagation fails (SGP4 or Cowell integrator).
Args:
message: Human-readable error description.
norad_id: NORAD catalog number of the failing satellite (SGP4 paths).
error_code: SGP4 internal error code when applicable (0 = success, 1–6 = failure).
t_jd: Julian Date at which propagation failed.
"""
def __init__(
self,
message: str,
norad_id: Optional[str] = None,
error_code: Optional[int] = None,
t_jd: Optional[float] = None,
) -> None:
super().__init__(
message,
norad_id=norad_id,
error_code=error_code,
t_jd=t_jd,
)
self.norad_id: Optional[str] = norad_id
self.error_code: Optional[int] = error_code
self.t_jd: Optional[float] = t_jd
[docs]
class FilterError(AstraError):
"""Raised when filter parameters are invalid or contradictory.
Args:
message: Human-readable error description.
parameter: Name of the offending parameter.
value: The invalid value that was supplied.
"""
def __init__(
self,
message: str,
parameter: Optional[str] = None,
value: Optional[Any] = None,
) -> None:
super().__init__(message, parameter=parameter, value=value)
self.parameter: Optional[str] = parameter
self.value: Optional[Any] = value
[docs]
class CoordinateError(AstraError):
"""Raised when coordinate frame conversion fails.
Args:
message: Human-readable error description.
frame: The coordinate frame involved (e.g. ``"TEME"``, ``"GCRS"``).
"""
def __init__(
self,
message: str,
frame: Optional[str] = None,
) -> None:
super().__init__(message, frame=frame)
self.frame: Optional[str] = frame
[docs]
class ManeuverError(AstraError):
"""Raised when maneuver definition or execution fails.
Covers validation issues (non-unit direction vector, negative Isp,
mass depletion exceeding available propellant) and runtime failures
during the 7-DOF powered integration phase.
Args:
message: Human-readable error description.
parameter: Name of the offending parameter, if applicable.
value: The invalid value, if applicable.
"""
def __init__(
self,
message: str,
parameter: Optional[str] = None,
value: Optional[Any] = None,
) -> None:
super().__init__(message, parameter=parameter, value=value)
self.parameter: Optional[str] = parameter
self.value: Optional[Any] = value
[docs]
class SpaceWeatherError(AstraError):
"""Raised when solar flux/Ap data is unavailable in STRICT_MODE.
Triggered by ``get_space_weather()`` when the CelesTrak cache is empty
or stale and ASTRA_STRICT_MODE is True. In relaxed mode, synthetic
defaults (F10.7=150, Ap=15) are substituted with a WARNING log.
"""
[docs]
class EphemerisError(AstraError):
"""Raised when JPL ephemeris data cannot be loaded in STRICT_MODE.
Triggered by Sun/Moon position functions when the DE421 BSP file is
missing and ASTRA_STRICT_MODE is True. In relaxed mode, the low-fidelity
analytical approximation is substituted with a WARNING log.
"""
[docs]
class SpacebookError(AstraError):
"""Raised when a COMSPOC Spacebook API call fails or returns unexpected data.
This is the base class for all Spacebook-related failures. It is raised
when an HTTP request to a Spacebook endpoint fails with a non-200 status,
times out, or returns a response body that cannot be parsed by the
expected format (TLE, fixed-width text, JSON, or STK ephemeris).
Callers that wish to catch any Spacebook data error but still allow
general ASTRA errors to propagate should catch ``SpacebookError`` directly.
Callers that want to tolerate all data failures should catch ``AstraError``.
Args:
message: Human-readable error description.
url: The Spacebook API URL that triggered the failure, if known.
status_code: HTTP status code returned, if the request reached the server.
"""
def __init__(
self,
message: str,
url: Optional[str] = None,
status_code: Optional[int] = None,
) -> None:
super().__init__(message, url=url, status_code=status_code)
self.url: Optional[str] = url
self.status_code: Optional[int] = status_code
[docs]
class SpacebookLookupError(SpacebookError):
"""Raised when a NORAD Catalog ID cannot be resolved to a COMSPOC GUID.
COMSPOC assigns each tracked space object a UUID-based GUID (distinct
from the public NORAD ID). Several Spacebook per-object API endpoints
require this GUID. This error is raised when a NORAD ID passed to
``get_norad_guid()`` is not found in the Spacebook satellite catalog.
Common causes:
- The satellite is tracked by Space-Track but not yet by COMSPOC.
- The satellite has decayed and was removed from the active catalog.
- The catalog cache is stale; calling ``refresh_satcat_cache()``
to force a re-download may resolve the issue.
Args:
message: Human-readable error description.
norad_id: The NORAD ID that could not be resolved.
"""
def __init__(
self,
message: str,
norad_id: Optional[int] = None,
) -> None:
super().__init__(message)
self.norad_id: Optional[int] = norad_id