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}