Skip to main content

astrodyn_quantities/
dims.rs

1// JEOD_INV: TS.01 — `<SelfRef>` / `<SelfPlanet>` are runtime-resolved storage-boundary wildcards; see `docs/JEOD_invariants.md` row TS.01 and the lint at `tests/self_ref_self_planet_discipline.rs`.
2//! Custom quantity dimensions not already present in `uom::si`.
3//!
4//! Dimensions are compile-time 7-tuples of `typenum` integers indexing
5//! `ISQ<L, M, T, I, Th, N, J>`. Each alias below names a physical quantity
6//! used by orbital mechanics that `uom::si` does not predefine.
7//!
8//! The `*Dim` aliases can be used as the dimension parameter of `Qty3<D, F>`;
9//! the companion scalar aliases (`SpecificAngMom`, …) model the
10//! corresponding `Quantity<_, SI<f64>, f64>` values used in scalar math.
11//!
12//! # Planet-tagged gravitational parameter
13//!
14//! [`GravParam<P>`] is the planet-phantom-tagged gravitational parameter:
15//! `GravParam<Earth>` and `GravParam<Sun>` are distinct types. Mixing them
16//! across a planet boundary is a compile error. The witness-gated
17//! constructor pattern mirrors [`crate::BodyAttitude<V>`]: callers reach
18//! the type only through factories that fix the planet (e.g.
19//! `f64.m3_per_s2_for::<Earth>()`, `mu_ggm05c()` returning
20//! `GravParam<Earth>`).
21
22use core::marker::PhantomData;
23
24use typenum::{N1, N2, P1, P2, P3, Z0};
25use uom::si::{Quantity, ISQ, SI};
26
27use crate::frame::{Planet, SelfPlanet};
28
29/// Gravitational parameter μ = GM (L³T⁻²). Base SI unit: m³/s².
30pub type GravParamDim = ISQ<P3, Z0, N2, Z0, Z0, Z0, Z0>;
31
32/// Specific angular momentum h = |r × v| / m (L²T⁻¹). Base SI unit: m²/s.
33pub type SpecificAngMomDim = ISQ<P2, Z0, N1, Z0, Z0, Z0, Z0>;
34
35/// Specific energy ε (L²T⁻²). Base SI unit: J/kg = m²/s².
36pub type SpecificEnergyDim = ISQ<P2, Z0, N2, Z0, Z0, Z0, Z0>;
37
38/// Mass flow rate ṁ (MT⁻¹). Base SI unit: kg/s.
39pub type MassFlowRateDim = ISQ<Z0, P1, N1, Z0, Z0, Z0, Z0>;
40
41// --- Scalar `Quantity` aliases -----------------------------------------------
42
43/// Scalar specific angular momentum.
44pub type SpecificAngMom = Quantity<SpecificAngMomDim, SI<f64>, f64>;
45
46/// Scalar specific energy.
47pub type SpecificEnergy = Quantity<SpecificEnergyDim, SI<f64>, f64>;
48
49/// Scalar mass-flow rate.
50pub type MassFlowRate = Quantity<MassFlowRateDim, SI<f64>, f64>;
51
52// --- Planet-tagged gravitational parameter -----------------------------------
53
54/// Gravitational parameter μ = GM tagged with the source planet `P`.
55///
56/// `GravParam<Earth>` and `GravParam<Sun>` are distinct types so the
57/// compiler refuses to silently feed `μ_Sun` into a function expecting
58/// `μ_Earth`. The numeric value is stored in SI base units (m³/s²) and
59/// reachable via [`GravParam::value`] (also a public field for the
60/// kernel-level call sites that already do `mu.value`).
61///
62/// ## Construction
63///
64/// Mission code constructs a `GravParam<P>` through the inferred-planet
65/// factory `f64::m3_per_s2()` (planet `<P>` inferred from the
66/// expected-type context at the call site — see
67/// [`crate::ext::F64Ext::m3_per_s2`] for the full story; a bare
68/// `let mu = 1.0.m3_per_s2();` with no expected-type context fails to
69/// compile), the explicit-planet factory `f64::m3_per_s2_for::<P>()`, or
70/// one of the curated `mu_*()` constants in
71/// `astrodyn::recipes::constants`. Calling a typed consumer with a
72/// [`SelfPlanet`]-tagged μ in a planet-pinned slot is rejected at compile
73/// time, so a planet-erased μ only flows through `SelfPlanet`-typed
74/// surfaces (the registry-side boundary code, `GravitySource`, etc.).
75///
76/// ```
77/// use astrodyn_quantities::prelude::*;
78///
79/// // Planet-pinned construction (Earth):
80/// let mu_earth: GravParam<Earth> = 3.986_004_415e14_f64.m3_per_s2_for::<Earth>();
81/// // Type ascription supplies the inference context for `<P>`:
82/// let mu_sun: GravParam<Sun> = 1.327_124_400_18e20_f64.m3_per_s2();
83/// // The numeric SI value in m³/s² is reachable via `.value`:
84/// assert!(mu_earth.value > 0.0);
85/// assert!(mu_sun.value > 0.0);
86/// ```
87///
88/// ## Compile-fail: cross-planet construction is rejected
89///
90/// Building a planet-pinned `GravParam<Earth>` directly from a
91/// `GravParam<Sun>` is a type error — the compiler refuses the
92/// assignment.
93///
94/// ```compile_fail
95/// use astrodyn_quantities::prelude::*;
96/// let mu_sun: GravParam<Sun> = 1.327e20.m3_per_s2_for::<Sun>();
97/// let _bad: GravParam<Earth> = mu_sun;   // planet phantom mismatch
98/// ```
99///
100/// A planet-erased `GravParam<SelfPlanet>` cannot be assigned to a
101/// planet-pinned slot either — `SelfPlanet` is not `Earth`:
102///
103/// ```compile_fail
104/// use astrodyn_quantities::prelude::*;
105/// let mu_any: GravParam<SelfPlanet> = 3.986e14.m3_per_s2();
106/// let _bad: GravParam<Earth> = mu_any;   // SelfPlanet vs Earth mismatch
107/// ```
108///
109/// ## Compile-fail: there is no `<P = SelfPlanet>` default
110///
111/// `GravParam<P>` carries no default planet — every call site must
112/// commit to a planet via turbofish, type ascription, or argument
113/// inference. A bare `GravParam::from_si(...)` with no inference
114/// context is rejected. There is deliberately no `<P = SelfPlanet>`
115/// fallback: a default would silently relax to `<SelfPlanet>` whenever
116/// inference had no constraint, hiding missing planet-pinning
117/// decisions. The type system is meant to surface those at compile
118/// time, not satisfy them with a wildcard:
119///
120/// ```compile_fail
121/// use astrodyn_quantities::prelude::*;
122/// // No type context for `<P>`, no turbofish, no default — type
123/// // annotations needed.
124/// let _mu = GravParam::from_si(3.986_004_415e14);
125/// ```
126///
127/// The fix is to commit to a planet at the call site:
128///
129/// ```
130/// use astrodyn_quantities::prelude::*;
131/// let _mu = GravParam::<Earth>::from_si(3.986_004_415e14);
132/// ```
133#[repr(C)]
134pub struct GravParam<P: Planet> {
135    /// Numeric value in SI base units (m³/s²).
136    ///
137    /// The field is public so internal physics kernels can read it via
138    /// `mu.value` without going through an accessor — the typed planet
139    /// phantom is the load-bearing part, not the wrapping ceremony.
140    pub value: f64,
141    _p: PhantomData<P>,
142}
143
144impl<P: Planet> GravParam<P> {
145    /// Construct a `GravParam<P>` from its SI base value (m³/s²).
146    ///
147    /// This is the witness-gated constructor: the planet phantom is
148    /// fixed by the turbofish or by the surrounding type context.
149    /// Mission code typically reaches this through
150    /// [`crate::ext::F64Ext::m3_per_s2_for`] or the curated `mu_*()`
151    /// constants rather than calling `from_si` directly.
152    // JEOD_INV: RF.11 — planet-pinned witness constructor; the phantom
153    // `P` ties the resulting μ to its source body so downstream typed
154    // consumers refuse a μ-vs-frame mismatch at compile time.
155    #[inline]
156    pub const fn from_si(value: f64) -> Self {
157        Self {
158            value,
159            _p: PhantomData,
160        }
161    }
162
163    /// Numeric value in SI base units (m³/s²).
164    #[inline]
165    pub const fn raw_si(&self) -> f64 {
166        self.value
167    }
168}
169
170impl GravParam<SelfPlanet> {
171    /// Relabel a planet-erased ([`SelfPlanet`]) μ as belonging to a
172    /// specific planet `Q`.
173    ///
174    /// Restricted to `impl GravParam<SelfPlanet>` so it can only retag a
175    /// μ that is already planet-erased — a planet-pinned `GravParam<Sun>`
176    /// cannot accidentally be relabeled as `GravParam<Earth>` via this
177    /// method. Provided **only** for the registry-side boundary code
178    /// that stores μ values keyed by a runtime source ID — the gravity
179    /// source registry, `GravitySourceTyped`, the dynamic mu carried on
180    /// `PlanetShape`, and similar surfaces where the planet identity is
181    /// determined at runtime. Mission code that knows the planet at
182    /// compile time should construct a planet-tagged `GravParam<Q>`
183    /// directly via `m3_per_s2_for::<Q>()` or one of the `mu_*()`
184    /// constants.
185    ///
186    /// A genuine `<P>` → `<Q>` retag for two distinct named planets is
187    /// almost never the right operation (μ is per-body); if you need it
188    /// for a different reason, add a separate, clearly-named escape
189    /// hatch instead of widening this impl block.
190    #[inline]
191    pub fn relabel<Q: Planet>(self) -> GravParam<Q> {
192        GravParam::<Q>::from_si(self.value)
193    }
194}
195
196// Manual impls — the derive macros would demand `P: Default`/`P: Copy`/...
197// which is wrong for a phantom-only type parameter.
198impl<P: Planet> Copy for GravParam<P> {}
199
200impl<P: Planet> Clone for GravParam<P> {
201    #[inline]
202    fn clone(&self) -> Self {
203        *self
204    }
205}
206
207impl<P: Planet> Default for GravParam<P> {
208    #[inline]
209    fn default() -> Self {
210        Self::from_si(0.0)
211    }
212}
213
214impl<P: Planet> PartialEq for GravParam<P> {
215    #[inline]
216    fn eq(&self, other: &Self) -> bool {
217        self.value == other.value
218    }
219}
220
221impl<P: Planet> core::fmt::Debug for GravParam<P> {
222    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
223        // `Planet::NAME` carries the per-planet identifier (e.g. "Earth").
224        write!(f, "GravParam<{}>({} m^3/s^2)", P::NAME, self.value)
225    }
226}