lox-space 0.1.0-alpha.48

The Lox toolbox for space mission analysis and design
Documentation
// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
//
// SPDX-License-Identifier: MPL-2.0

use crate::frames::{
    dynamic::{DynFrame, UnknownFrameError},
    traits::ReferenceFrame,
};
use lox_frames::rotations::DynRotationError;
use pyo3::{
    PyErr, PyResult, create_exception,
    exceptions::{PyException, PyValueError},
    prelude::*,
    pyclass, pymethods,
};

/// PyO3 error wrapper for [`lox_frames::dynamic::UnknownFrameError`].
pub struct PyUnknownFrameError(pub UnknownFrameError);

impl From<PyUnknownFrameError> for PyErr {
    fn from(err: PyUnknownFrameError) -> Self {
        PyValueError::new_err(err.0.to_string())
    }
}

create_exception!(
    lox_space,
    FrameTransformationError,
    PyException,
    "Python exception raised when a frame transformation cannot be performed."
);

/// PyO3 error wrapper for [`lox_frames::rotations::DynRotationError`].
pub struct PyDynRotationError(pub DynRotationError);

impl From<PyDynRotationError> for PyErr {
    fn from(err: PyDynRotationError) -> Self {
        FrameTransformationError::new_err(err.0.to_string())
    }
}

/// Represents a reference frame for positioning and transformations.
///
/// Reference frames define coordinate systems for expressing positions and
/// velocities. Lox supports both inertial (non-rotating) and rotating frames.
///
/// Supported frames:
///
/// - **ICRF**: International Celestial Reference Frame (inertial)
/// - **J2000** / **EME2000**: J2000 Mean Equator and Equinox (inertial)
/// - **CIRF**: Celestial Intermediate Reference Frame
/// - **TIRF**: Terrestrial Intermediate Reference Frame
/// - **ITRF**: International Terrestrial Reference Frame (Earth-fixed)
/// - **MOD**, **TOD**, **PEF**: Equinox-based frames (default: IERS1996,
///   e.g., ``MOD(IERS2003)``, ``TOD(IERS2010)``)
/// - **TEME**: True Equator Mean Equinox (SGP4/TLE)
/// - **Body-fixed frames**: IAU_EARTH, IAU_MOON, IAU_MARS, etc.
///
/// Args:
///     abbreviation: Frame abbreviation (e.g., "ICRF", "ITRF", "IAU_MOON").
///
/// Raises:
///     ValueError: If the frame abbreviation is not recognized.
#[pyclass(name = "Frame", module = "lox_space", frozen, from_py_object)]
#[pyo3(eq)]
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct PyFrame(pub DynFrame);

#[pymethods]
impl PyFrame {
    #[new]
    fn new(abbreviation: &str) -> PyResult<Self> {
        Ok(Self(abbreviation.parse().map_err(PyUnknownFrameError)?))
    }

    fn __getnewargs__(&self) -> (String,) {
        (self.abbreviation(),)
    }

    /// Return the full name of this reference frame.
    ///
    /// Returns:
    ///     The descriptive name (e.g., "International Celestial Reference Frame").
    fn name(&self) -> String {
        self.0.name()
    }

    /// Return the abbreviation of this reference frame.
    ///
    /// Returns:
    ///     The short abbreviation (e.g., "ICRF", "ITRF").
    fn abbreviation(&self) -> String {
        self.0.abbreviation()
    }

    /// Return the string representation of this frame.
    pub fn __repr__(&self) -> String {
        format!("Frame(\"{}\")", self.0.abbreviation())
    }
}

impl TryFrom<&Bound<'_, PyAny>> for PyFrame {
    type Error = PyErr;

    fn try_from(value: &Bound<'_, PyAny>) -> Result<Self, Self::Error> {
        if let Ok(frame) = value.extract::<PyFrame>() {
            return Ok(frame);
        }
        if let Ok(name) = value.extract::<&str>() {
            return Ok(Self(name.parse().map_err(PyUnknownFrameError)?));
        }
        Err(PyValueError::new_err(
            "'frame' argument must be a string or a 'Frame' instance",
        ))
    }
}