outfit 3.0.0

Orbit determination toolkit in Rust. Provides astrometric parsing, observer management, and initial orbit determination (Gauss method) with JPL ephemeris support.
Documentation
//! Ephemeris computation results.
//!
//! [`EphemerisResult<T>`] is the return type of
//! [`OrbitalElements::compute`](crate::OrbitalElements::compute).
//! It holds a flat sequence of [`EphemerisEntry<T>`] values, each pairing an
//! epoch, the observer for which the computation was requested, and the
//! per-epoch result (`Ok` or `Err`).
//!
//! Errors are collected *per entry*: a failure at one epoch does not prevent
//! the remaining epochs or observers from being computed.  The total entry
//! count always equals the sum of epochs generated across all observers in
//! the request.
//!
//! # Iteration
//!
//! ```rust,ignore
//! for entry in &result {
//!     match &entry.result {
//!         Ok(pos) => println!("{} {:?}: RA={:.4}", entry.epoch, entry.observer.name, pos.coord.ra),
//!         Err(e)  => eprintln!("{} {:?}: {e}", entry.epoch, entry.observer.name),
//!     }
//! }
//! ```
//!
//! Three focused convenience iterators narrow the view:
//!
//! - [`EphemerisResult::successes`] — only entries whose result is `Ok`.
//! - [`EphemerisResult::errors`] — only entries whose result is `Err`.
//! - [`EphemerisResult::by_observer`] — only entries for a specific observer.

use hifitime::Epoch;
use photom::observer::Observer;

use crate::OutfitError;

// ---------------------------------------------------------------------------
// EphemerisEntry
// ---------------------------------------------------------------------------

/// A single row in an [`EphemerisResult`]: one `(epoch, observer, result)`.
///
/// The `observer` field records which site was used for this computation,
/// which is essential when a request spans several observers.
///
/// # Ordering
///
/// Entries follow the order in which observers were added to the request: all
/// epochs for the first observer appear first, then all epochs for the second
/// observer, and so on.  Within each observer the epoch order matches the
/// [`EphemerisMode`](super::request::EphemerisMode) used for that observer.
#[derive(Debug)]
pub struct EphemerisEntry<T> {
    /// Observation epoch.
    pub epoch: Epoch,
    /// Observing site for which this entry was computed.
    pub observer: Observer,
    /// Computed value, or the error that prevented computation.
    pub result: Result<T, OutfitError>,
}

// ---------------------------------------------------------------------------
// EphemerisResult
// ---------------------------------------------------------------------------

/// The result of an [`EphemerisRequest`](super::request::EphemerisRequest)
/// computation.
///
/// Contains one [`EphemerisEntry`] per `(epoch, observer)` pair generated by
/// the request.  Use [`successes`](Self::successes), [`errors`](Self::errors),
/// or [`by_observer`](Self::by_observer) to filter the entries, or iterate
/// over all of them with [`iter`](Self::iter) or the [`IntoIterator`] impl.
///
/// # Example
///
/// ```rust,ignore
/// let result = elements.compute(&request, &jpl, &ut1);
///
/// println!("{} entries, {} errors", result.len(), result.error_count());
///
/// for entry in result.successes() {
///     println!("{}: {:?}", entry.epoch, entry.result);
/// }
/// ```
#[derive(Debug)]
pub struct EphemerisResult<T> {
    entries: Vec<EphemerisEntry<T>>,
}

impl<T> EphemerisResult<T> {
    /// Create an empty result with pre-allocated capacity.
    pub(crate) fn with_capacity(n: usize) -> Self {
        Self {
            entries: Vec::with_capacity(n),
        }
    }

    /// Append a single entry.
    pub(crate) fn push(
        &mut self,
        epoch: Epoch,
        observer: Observer,
        result: Result<T, OutfitError>,
    ) {
        self.entries.push(EphemerisEntry {
            epoch,
            observer,
            result,
        });
    }

    // -----------------------------------------------------------------------
    // Accessors
    // -----------------------------------------------------------------------

    /// Total number of entries (successes + errors).
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Returns `true` if there are no entries.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Number of entries whose computation succeeded.
    ///
    /// Equals `self.len() - self.error_count()`.
    pub fn success_count(&self) -> usize {
        self.entries.iter().filter(|e| e.result.is_ok()).count()
    }

    /// Number of entries whose computation failed.
    ///
    /// A value of `0` means every `(epoch, observer)` pair succeeded.
    pub fn error_count(&self) -> usize {
        self.entries.iter().filter(|e| e.result.is_err()).count()
    }

    // -----------------------------------------------------------------------
    // Iterators
    // -----------------------------------------------------------------------

    /// Iterate over all entries in order (successes and errors).
    pub fn iter(&self) -> impl Iterator<Item = &EphemerisEntry<T>> {
        self.entries.iter()
    }

    /// Iterate over entries whose computation succeeded, in order.
    pub fn successes(&self) -> impl Iterator<Item = &EphemerisEntry<T>> {
        self.entries.iter().filter(|e| e.result.is_ok())
    }

    /// Iterate over entries whose computation failed, in order.
    pub fn errors(&self) -> impl Iterator<Item = &EphemerisEntry<T>> {
        self.entries.iter().filter(|e| e.result.is_err())
    }

    /// Iterate over entries for a specific observer.
    ///
    /// Two [`Observer`] values are considered equal when all their fields
    /// match — geodetic coordinates ($\rho\cos\phi'$, $\rho\sin\phi'$,
    /// longitude), optional site name, and optional measurement accuracies.
    /// For reliable filtering, pass the exact same `Observer` instance that
    /// was given to [`EphemerisRequest::add`](super::request::EphemerisRequest::add).
    pub fn by_observer<'s, 'o>(
        &'s self,
        observer: &'o Observer,
    ) -> impl Iterator<Item = &'s EphemerisEntry<T>> + 'o
    where
        's: 'o,
    {
        self.entries.iter().filter(move |e| &e.observer == observer)
    }
}

// ---------------------------------------------------------------------------
// IntoIterator
// ---------------------------------------------------------------------------

/// Consuming iterator over all entries in order.
impl<T> IntoIterator for EphemerisResult<T> {
    type Item = EphemerisEntry<T>;
    type IntoIter = std::vec::IntoIter<EphemerisEntry<T>>;

    fn into_iter(self) -> Self::IntoIter {
        self.entries.into_iter()
    }
}

/// Borrowing iterator over all entries in order.
impl<'a, T> IntoIterator for &'a EphemerisResult<T> {
    type Item = &'a EphemerisEntry<T>;
    type IntoIter = std::slice::Iter<'a, EphemerisEntry<T>>;

    fn into_iter(self) -> Self::IntoIter {
        self.entries.iter()
    }
}