siderust 0.7.0

High-precision astronomy and satellite mechanics in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Vallés Puig, Ramon

//! # Astronomical Transform Context
//!
//! ## Scientific scope
//!
//! Every time-dependent coordinate transformation in siderust needs access to
//! three external providers: an *ephemeris* that supplies planetary positions,
//! an *Earth Orientation Parameters* (EOP) table that supplies the observed
//! offset between UT1 and UTC and the pole coordinates, and a *nutation /
//! precession model* that determines which IAU series is used to orient the
//! terrestrial frame in the celestial reference system. These three concerns
//! are bundled in this module so that transform call sites carry no ambient
//! state and the same transform path can be reused with any combination of
//! backends.
//!
//! ## Technical scope
//!
//! - [`AstroContext<Eph, Eop>`] — the primary runtime provider container.
//!   - `Eph` selects the ephemeris backend (default: [`DefaultEphemeris`],
//!     VSOP87/ELP2000; DE440/DE441 selectable via Cargo features).
//!   - `Eop` selects the EOP source (default: [`DefaultEop`] = [`IersEop`],
//!     backed by the build-time `finals2000A.all` table; substitute
//!     [`NullEop`](crate::astro::eop::NullEop) for zero overhead).
//! - [`AstroContext::with_model::<Nut>()`] — attaches a compile-time nutation
//!   model marker, returning a zero-cost [`ModelContext`]. The nutation model
//!   is always a **phantom type**: use [`Iau2006A`](crate::astro::nutation::Iau2006A)
//!   (default, full 1365-term MHB2000 + P03 correction),
//!   [`Iau2000B`](crate::astro::nutation::Iau2000B) (77-term abridged),
//!   [`Iau2000A`](crate::astro::nutation::Iau2000A) (uncorrected 2000A), or
//!   [`Iau2006`](crate::astro::nutation::Iau2006) (precession-only). This is
//!   the canonical model-selection mechanism — no runtime enum dispatch.
//! - [`ModelContext<Eph, Eop, Nut>`] — lightweight borrow wrapper that satisfies
//!   [`TransformContext`] and exposes the chosen model to the transform
//!   providers via a single trait.
//! - [`DynAstroContext<Eop>`] — dynamic variant carrying a `Box<dyn DynEphemeris>`
//!   for cases where the BSP file is loaded at runtime.
//!
//! ## Model-selection example
//!
//! ```rust
//! use siderust::coordinates::transform::context::AstroContext;
//! use siderust::astro::nutation::Iau2000B;
//!
//! // Default context: IAU 2006A nutation, VSOP87 ephemeris, IERS EOP.
//! let ctx = AstroContext::new();
//!
//! // Abridged 77-term IAU 2000B nutation — zero runtime overhead.
//! let low_cost = ctx.with_model::<Iau2000B>();
//! // low_cost can now be passed to any _with transform variant.
//! ```
//!
//! ## References
//!
//! - IAU 2006 Resolution B1 (precession) and Resolution B1.6 (nutation).
//! - IERS Conventions (2010), §5.4 and §5.5.
//! - Wallace, P. T., & Capitaine, N. (2006). *A&A* 459, 981 (P03 correction).
//! - SOFA software collection.

use std::marker::PhantomData;

use crate::astro::eop::{EopError, EopProvider, EopValues, IersEop};
use crate::astro::nutation::NutationModel;
use crate::time::JulianDate;

#[cfg(not(any(feature = "de440", feature = "de441")))]
use crate::calculus::ephemeris::Vsop87Ephemeris;

/// Default ephemeris type.
///
/// - Without a DE feature: [`Vsop87Ephemeris`] (VSOP87 + ELP2000-82B).
/// - With `de441` feature (and real data): `De441Ephemeris` (JPL DE441, compile-time).
/// - With `de440` feature (and real data): `De440Ephemeris` (JPL DE440, compile-time).
/// - With a DE feature but matching `SIDERUST_JPL_STUB` set: falls back to
///   [`Vsop87Ephemeris`] so tests run without downloading the BSP.
/// - For other large datasets: use
///   `DataManager` (from the `runtime-data` feature) with a BSP file loaded at runtime.
///
/// This type alias is used as the default `Eph` parameter in [`AstroContext`],
/// so all code using `AstroContext::default()` will automatically use the
/// selected backend.
#[cfg(not(any(feature = "de440", feature = "de441")))]
pub type DefaultEphemeris = Vsop87Ephemeris;

#[cfg(all(feature = "de441", not(siderust_mock_de441)))]
pub type DefaultEphemeris = crate::calculus::ephemeris::De441Ephemeris;

#[cfg(all(feature = "de440", not(feature = "de441"), not(siderust_mock_de440)))]
pub type DefaultEphemeris = crate::calculus::ephemeris::De440Ephemeris;

// Stub: DE feature is on but SIDERUST_JPL_STUB is set, fall back to VSOP87 so
// tests work without the BSP download. DE441 takes precedence when both DE
// features are enabled.
#[cfg(any(
    all(feature = "de441", siderust_mock_de441),
    all(feature = "de440", not(feature = "de441"), siderust_mock_de440)
))]
pub type DefaultEphemeris = crate::calculus::ephemeris::Vsop87Ephemeris;

/// Default Earth orientation model: [`IersEop`], backed by the
/// build-time embedded `finals2000A.all` table.
///
/// For zero-overhead use (no EOP corrections), substitute
/// [`NullEop`](crate::astro::eop::NullEop) as the `Eop` type parameter.
pub type DefaultEop = IersEop;

/// Default nutation/precession model marker.
///
/// Uses the full IAU 2006/2000A nutation model for SOFA-grade accuracy.
/// For the abridged 77-term model, substitute
/// [`Iau2000B`](crate::astro::nutation::Iau2000B).
pub type DefaultNutationModel = crate::astro::nutation::Iau2006A;

/// Astronomical context for coordinate transformations.
///
/// This structure holds the runtime configuration for time-dependent
/// transformations:
/// - Ephemeris source for planetary positions
/// - Earth Orientation Parameters (EOP) for polar motion and UT1-UTC
///
/// Compile-time model selection is layered on top through
/// [`ModelContext`], keeping the base context focused on providers/data.
///
/// # Type Parameters
///
/// - `Eph`: Ephemeris provider type (default: [`DefaultEphemeris`]).
/// - `Eop`: Earth Orientation Parameters type (default: [`DefaultEop`]).
///
/// To override the default nutation model, call [`AstroContext::with_model`]
/// and pass the resulting [`ModelContext`] into the `_with` transform APIs.
///
/// # Example
///
/// ```rust
/// use siderust::coordinates::transform::context::AstroContext;
///
/// // Use defaults for typical applications
/// let ctx = AstroContext::new();
/// ```
#[derive(Debug, Clone)]
pub struct AstroContext<Eph = DefaultEphemeris, Eop = DefaultEop> {
    _ephemeris: PhantomData<Eph>,
    /// Earth Orientation Parameters provider.
    eop: Eop,
}

impl<Eph, Eop: Default> Default for AstroContext<Eph, Eop> {
    fn default() -> Self {
        Self {
            _ephemeris: PhantomData,
            eop: Eop::default(),
        }
    }
}

impl AstroContext {
    /// Creates a new context with default configuration.
    ///
    /// This is equivalent to `AstroContext::default()` but provides a named
    /// constructor for clarity.
    #[inline]
    pub fn new() -> Self {
        Self::default()
    }
}

impl<Eph, Eop: Default> AstroContext<Eph, Eop> {
    /// Creates a context with custom type parameters.
    ///
    /// Use this when you need to specify custom ephemeris or EOP types.
    #[inline]
    pub fn with_types() -> Self {
        Self::default()
    }
}

impl<Eph, Eop> AstroContext<Eph, Eop> {
    /// Binds a compile-time nutation model to this context.
    ///
    /// This returns a lightweight wrapper carrying only a reference to the
    /// base context plus a [`PhantomData`] marker for the selected model.
    #[inline]
    pub fn with_model<Nut: NutationModel>(&self) -> ModelContext<'_, Eph, Eop, Nut> {
        ModelContext {
            ctx: self,
            _nutation: PhantomData,
        }
    }
}

impl<Eph, Eop: EopProvider> AstroContext<Eph, Eop> {
    /// Fallibly look up EOP values for the given **UTC** Julian Date.
    #[inline]
    pub fn try_eop_at(&self, jd_utc: JulianDate) -> Result<EopValues, EopError> {
        self.eop.try_eop_at(jd_utc)
    }

    /// Fallibly look up EOP values for a **TT** observation epoch.
    ///
    /// This converts TT to UTC before querying the provider, preserving the
    /// EOP provider contract while keeping transform call sites ergonomic.
    #[inline]
    pub fn try_eop_at_tt(&self, jd_tt: JulianDate) -> Result<EopValues, EopError> {
        let jd_utc = crate::astro::earth_rotation::jd_utc_from_tt(jd_tt);
        self.try_eop_at(jd_utc)
    }

    /// Look up EOP values for a **TT** observation epoch.
    #[inline]
    pub fn eop_at_tt(&self, jd_tt: JulianDate) -> EopValues {
        let jd_utc = crate::astro::earth_rotation::jd_utc_from_tt(jd_tt);
        self.eop_at(jd_utc)
    }

    /// Look up EOP values for the given **UTC** Julian Date.
    ///
    /// # Time-scale contract
    /// `jd_utc` **must** be a UTC Julian Date.  Passing TT or UT1 values will
    /// corrupt the interpolated `dUT1`, `xp`, and `yp` values because the IERS
    /// tables are indexed by UTC civil date (see [`crate::astro::eop`] for
    /// details).
    ///
    /// Delegates to the context's [`EopProvider`].
    #[inline]
    pub fn eop_at(&self, jd_utc: JulianDate) -> EopValues {
        self.eop.eop_at(jd_utc)
    }

    /// Reference to the underlying EOP provider.
    #[inline]
    pub fn eop(&self) -> &Eop {
        &self.eop
    }
}

// Marker that this context uses default ephemeris
impl<Eop> AstroContext<DefaultEphemeris, Eop> {
    /// Returns true if this context uses the default ephemeris.
    #[inline]
    pub const fn uses_default_ephemeris(&self) -> bool {
        true
    }
}

/// Context wrapper that binds a compile-time nutation model to an
/// [`AstroContext`] without duplicating the runtime provider state.
#[derive(Debug, Clone, Copy)]
pub struct ModelContext<'a, Eph = DefaultEphemeris, Eop = DefaultEop, Nut = DefaultNutationModel> {
    ctx: &'a AstroContext<Eph, Eop>,
    _nutation: PhantomData<Nut>,
}

impl<'a, Eph, Eop, Nut> ModelContext<'a, Eph, Eop, Nut> {
    /// Returns the underlying runtime context.
    #[inline]
    pub fn astro_context(&self) -> &'a AstroContext<Eph, Eop> {
        self.ctx
    }
}

/// Common interface for transformation contexts accepted by the ergonomic
/// `_with` APIs.
///
/// A plain [`AstroContext`] uses [`DefaultNutationModel`], while a
/// [`ModelContext`] overrides that compile-time model through its phantom
/// type parameter.
pub trait TransformContext {
    /// Ephemeris provider type carried by the context.
    type Eph;
    /// EOP provider type carried by the context.
    type Eop: EopProvider;
    /// Compile-time nutation model associated with this context value.
    type Nut: NutationModel;

    /// Returns the underlying runtime context.
    fn astro_context(&self) -> &AstroContext<Self::Eph, Self::Eop>;
}

impl<Eph, Eop: EopProvider> TransformContext for AstroContext<Eph, Eop> {
    type Eph = Eph;
    type Eop = Eop;
    type Nut = DefaultNutationModel;

    #[inline]
    fn astro_context(&self) -> &AstroContext<Self::Eph, Self::Eop> {
        self
    }
}

impl<'a, Eph, Eop: EopProvider, Nut: NutationModel> TransformContext
    for ModelContext<'a, Eph, Eop, Nut>
{
    type Eph = Eph;
    type Eop = Eop;
    type Nut = Nut;

    #[inline]
    fn astro_context(&self) -> &AstroContext<Self::Eph, Self::Eop> {
        self.ctx
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// DynAstroContext, runtime-selected ephemeris via DynEphemeris trait object
// ═══════════════════════════════════════════════════════════════════════════

use crate::calculus::ephemeris::DynEphemeris;

/// Astronomical context with a runtime-selected ephemeris backend.
///
/// This is the dynamic counterpart to [`AstroContext`]. Instead of selecting
/// an ephemeris backend at compile time via type parameters, it stores a
/// `Box<dyn DynEphemeris>` and dispatches via virtual calls.
///
/// Use this when:
/// - You load BSP files at runtime via [`RuntimeEphemeris`](crate::calculus::ephemeris::RuntimeEphemeris)
/// - You need to switch between backends without recompiling
/// - You want to override the default ephemeris at runtime
///
/// # Example
///
/// ```rust,ignore
/// use siderust::calculus::ephemeris::{RuntimeEphemeris, DynEphemeris};
/// use siderust::coordinates::transform::context::DynAstroContext;
///
/// let eph = RuntimeEphemeris::from_bsp("path/to/de441.bsp")?;
/// let ctx = DynAstroContext::with_ephemeris(Box::new(eph));
/// ```
pub struct DynAstroContext<Eop = DefaultEop> {
    ephemeris: Box<dyn DynEphemeris>,
    eop: Eop,
}

impl DynAstroContext<DefaultEop> {
    /// Create a dynamic context from a `DynEphemeris` implementor.
    pub fn with_ephemeris(eph: Box<dyn DynEphemeris>) -> Self {
        Self {
            ephemeris: eph,
            eop: DefaultEop::default(),
        }
    }
}

impl<Eop: Default> DynAstroContext<Eop> {
    /// Create a dynamic context with a custom EOP provider.
    pub fn with_ephemeris_and_eop(eph: Box<dyn DynEphemeris>, eop: Eop) -> Self {
        Self {
            ephemeris: eph,
            eop,
        }
    }
}

impl<Eop: EopProvider> DynAstroContext<Eop> {
    /// Reference to the runtime ephemeris backend.
    #[inline]
    pub fn ephemeris(&self) -> &dyn DynEphemeris {
        &*self.ephemeris
    }

    /// Look up EOP values for the given **UTC** Julian Date.
    #[inline]
    pub fn eop_at(&self, jd_utc: JulianDate) -> EopValues {
        self.eop.eop_at(jd_utc)
    }

    /// Fallibly look up EOP values for the given **UTC** Julian Date.
    #[inline]
    pub fn try_eop_at(&self, jd_utc: JulianDate) -> Result<EopValues, EopError> {
        self.eop.try_eop_at(jd_utc)
    }

    /// Fallibly look up EOP values for a **TT** observation epoch.
    #[inline]
    pub fn try_eop_at_tt(&self, jd_tt: JulianDate) -> Result<EopValues, EopError> {
        let jd_utc = crate::astro::earth_rotation::jd_utc_from_tt(jd_tt);
        self.try_eop_at(jd_utc)
    }

    /// Look up EOP values for a **TT** observation epoch.
    #[inline]
    pub fn eop_at_tt(&self, jd_tt: JulianDate) -> EopValues {
        let jd_utc = crate::astro::earth_rotation::jd_utc_from_tt(jd_tt);
        self.eop_at(jd_utc)
    }

    /// Reference to the underlying EOP provider.
    #[inline]
    pub fn eop(&self) -> &Eop {
        &self.eop
    }
}

impl<Eop> std::fmt::Debug for DynAstroContext<Eop>
where
    Eop: std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DynAstroContext")
            .field("ephemeris", &"<dyn DynEphemeris>")
            .field("eop", &self.eop)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_context_creation() {
        let ctx = AstroContext::new();
        assert!(ctx.uses_default_ephemeris());
    }

    #[test]
    fn test_context_default() {
        let ctx: AstroContext = Default::default();
        assert!(ctx.uses_default_ephemeris());
    }

    #[test]
    fn test_context_with_types() {
        let ctx: AstroContext<DefaultEphemeris, DefaultEop> = AstroContext::with_types();
        assert!(ctx.uses_default_ephemeris());
    }

    #[test]
    fn test_context_with_model() {
        let ctx = AstroContext::new();
        let model_ctx = ctx.with_model::<DefaultNutationModel>();
        let _ = model_ctx.astro_context();
    }

    #[test]
    fn test_eop_at_returns_values() {
        let ctx = AstroContext::new();
        let jd = JulianDate::J2000;
        let eop = ctx.eop_at_tt(jd);
        // EOP values should be finite
        assert!(eop.dut1.is_finite());
    }

    #[test]
    fn test_eop_ref() {
        let ctx = AstroContext::new();
        let _eop = ctx.eop();
    }

    #[test]
    fn test_dyn_context_creation() {
        use crate::calculus::ephemeris::Vsop87Ephemeris;

        // Vsop87Ephemeris implements DynEphemeris via blanket impl
        let dyn_ctx = DynAstroContext::with_ephemeris(Box::new(Vsop87Ephemeris));
        let _eph = dyn_ctx.ephemeris();
        let _eop = dyn_ctx.eop_at(JulianDate::J2000);
        let _eop_ref = dyn_ctx.eop();

        // Debug should work
        let debug_str = format!("{:?}", dyn_ctx);
        assert!(debug_str.contains("DynAstroContext"));
    }

    #[test]
    fn test_dyn_context_with_eop() {
        use crate::calculus::ephemeris::Vsop87Ephemeris;

        let dyn_ctx = DynAstroContext::with_ephemeris_and_eop(
            Box::new(Vsop87Ephemeris),
            DefaultEop::default(),
        );
        let eop = dyn_ctx.eop_at(JulianDate::J2000);
        assert!(eop.dut1.is_finite());
    }
}