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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
//! Reference-frame phantom markers.
//!
//! JEOD distinguishes reference frames at runtime via `RefFrameKind` + string
//! names. We lift that distinction to compile time: `Position<RootInertial>` and
//! `Position<Ecef>` are distinct types and cannot be added.
//!
//! JEOD_INV: TS.01 — this file defines the runtime-resolved-boundary
//! wildcards [`SelfRef`] and [`SelfPlanet`]. The full description of
//! their boundary discipline lives in `docs/JEOD_invariants.md` row
//! TS.01 and is enforced by the workspace lint at
//! `tests/self_ref_self_planet_discipline.rs`.
//!
//! ## Extension model
//!
//! - **Frame *kinds*** (`RootInertial`, `Ecef`, `BodyFrame<V>`, …) are sealed
//! to this crate. Adding a new kind requires editing this file.
//! - **Vehicle and Planet *parameter* tags** (the `V` in `BodyFrame<V>`,
//! the `P` in `PlanetFixed<P>`) are extensible from downstream crates
//! via the [`define_vehicle!`](crate::define_vehicle) and
//! [`define_planet!`](crate::define_planet) macros, defined at the
//! bottom of this file. Mission crates that model multiple vehicles
//! should use those macros so each vehicle gets a distinct
//! compile-time identity.
use PhantomData;
use crate;
/// Compile-time reference frame tag.
///
/// Sealed at the type-system level: only `astrodyn_quantities` can implement
/// this trait. The seal trait `FrameSealed` is private to this crate, so
/// downstream code cannot satisfy the supertrait bound.
///
/// # The seal is type-system enforced
///
/// Unlike [`Vehicle`] and [`Planet`] (which expose their seal traits via
/// `__macro_support` so the `define_*!` macros work cross-crate),
/// `Frame`'s seal is fully closed. Downstream code cannot impl `Frame`
/// even by reaching into the macro infrastructure:
///
/// ```compile_fail
/// // `FrameSealed` is private to astrodyn_quantities and is NOT re-exported
/// // via __macro_support, so the supertrait bound cannot be satisfied
/// // from outside the crate.
/// struct EvilFrame;
/// impl astrodyn_quantities::Frame for EvilFrame {
/// const NAME: &'static str = "Evil";
/// }
/// ```
/// Compile-time planet tag used to parameterize planet-fixed frames.
///
/// Convention-sealed: the seal trait `PlanetSealed` is re-exported via
/// the crate's `__macro_support` module so [`define_planet!`](crate::define_planet)
/// can satisfy the bound from downstream call sites. Direct
/// `impl Planet for X` outside the macro is technically possible but
/// unsupported. Use the macro.
///
/// `Send + Sync` are required so `PhantomData<P>`-tagged ECS components
/// (e.g. `astrodyn_bevy::TranslationalStateC<P>`) can be moved between
/// threads under Bevy's parallel scheduler. Every Planet marker is a
/// zero-sized empty struct that trivially satisfies both — the
/// supertrait bound just makes the contract explicit.
/// Compile-time vehicle tag used to parameterize vehicle-relative frames.
///
/// Convention-sealed: the seal trait `VehicleSealed` is re-exported via
/// the crate's `__macro_support` module so [`define_vehicle!`](crate::define_vehicle)
/// can satisfy the bound from downstream call sites. Direct
/// `impl Vehicle for X` outside the macro is technically possible but
/// unsupported. Use the macro.
///
/// Vehicle marker types are used with [`BodyFrame`], [`StructuralFrame`],
/// [`Lvlh`], and [`Ned`].
///
/// `Send + Sync` are required so `PhantomData<V>`-tagged ECS components
/// can be moved between threads under Bevy's parallel scheduler. Every
/// Vehicle marker is a zero-sized empty struct that trivially satisfies
/// both — the supertrait bound just makes the contract explicit and
/// mirrors the parallel symmetry with [`Planet`].
// --- Planet markers -----------------------------------------------------------
planet_marker!;
planet_marker!;
planet_marker!;
planet_marker!;
/// Phantom marker for "this entity's own planet" — used by ECS adapters
/// whose per-entity components carry `PlanetFixed<P>` phantoms but whose
/// planet identity is determined at runtime by the entity itself.
///
/// Planet-side analog of [`SelfRef`] (which serves the same role for
/// `Vehicle`-parameterized frames). Use when wrapping a per-entity
/// rotation Component such as `PlanetFixedRotationC` whose Bevy entity
/// already carries a `PlanetC` discriminator — the typed `FrameTransform`
/// inside the Component encodes the *direction* (RootInertial → PlanetFixed)
/// while the planet identity stays at the entity level.
// JEOD_INV: TS.01 — `SelfPlanet` is the per-entity storage-boundary
// wildcard for runtime-resolved planet identity (Bevy components, runner
// `SimBody`/`VehicleOutput` slots, dynamic-registry-erased returns). All
// system code paths and APIs use `<P: Planet>` instead. See the lint at
// `tests/self_ref_self_planet_discipline.rs`.
;
// --- Frame markers ------------------------------------------------------------
/// The simulation's **root inertial frame** — the unique inertial node
/// at the top of the frame tree.
///
/// In all current sims this is Earth-centered (ICRF / J2000) because
/// Earth is the central body, but the semantic is "whatever the
/// simulation's root inertial frame is" — for an SSB-rooted setup it
/// would be heliocentric/barycentric.
///
/// Distinct from [`PlanetInertial<P>`] (a particular planet's inertial
/// frame, which may be a non-root child of `RootInertial` in the tree) and
/// from [`IntegrationFrame`] (a body's integration frame, which equals
/// `RootInertial` when the body integrates in root and equals some
/// `PlanetInertial<P>` when it integrates in a child).
///
/// Consumers that mix body state with root-inertial source positions
/// (gravity sources, Sun, Moon) — gravity, relativistic, SRP, solar
/// beta, earth lighting — require their position arguments in this
/// frame. See issue #255 / RF.10.
;
/// A particular planet's inertial (non-rotating, J2000-aligned) frame,
/// centered at the planet's CoM.
///
/// Distinct from [`RootInertial`] (the simulation root) and from
/// [`PlanetFixed<P>`] (the planet's rotating, body-fixed frame).
/// `PlanetInertial<P>` is the parent frame of `PlanetFixed<P>` in JEOD's
/// frame tree convention, so the rotation matrix
/// `PlanetFixed<P>::t_parent_this` rotates `PlanetInertial<P> →
/// PlanetFixed<P>`.
///
/// Consumers that operate around a single planet (atmosphere, drag
/// velocity vs corotation wind, LVLH, geodetic, orbital elements) take
/// their position/velocity arguments in this frame. In realistic
/// configs the body's integration frame *is* `PlanetInertial<P>` for
/// the planet the body orbits, so `body.trans.position` (typed
/// `Position<IntegrationFrame>`) can be relabeled to
/// `Position<PlanetInertial<P>>` at the call site without arithmetic.
///
/// The runner currently invokes consumers with raw `DVec3` (without
/// static `P` dispatch) and asserts the runtime invariant
/// `body.integ_source == consumer_planet_source`. Mission-crate code
/// that knows the planet at compile time should prefer the typed
/// `_typed_for_planet` siblings.
;
/// Earth-centered Earth-fixed frame (ITRF-like). Rotates with Earth.
;
/// A body's integration frame — a non-rotating quasi-inertial frame whose
/// origin generally differs from the root inertial origin.
///
/// `IntegrationFrame` is *kind-distinct* from [`RootInertial`] so that APIs
/// requiring root-inertial coordinates refuse `Position<IntegrationFrame>`
/// at compile time. **Root-inertial consumers** (the "shift sites" — they
/// mix body state with root-inertial source positions for Sun, Moon, or
/// gravity sources): gravity, relativistic corrections, SRP, solar beta,
/// earth lighting. **Planet-inertial consumers** (the "non-shift sites"
/// — they operate within a single planet's inertial frame, which equals
/// the body's integration frame in realistic configs): atmosphere, drag
/// velocity, LVLH, geodetic, orbital elements; these take
/// [`PlanetInertial<P>`] inputs and must NOT receive a root-inertial
/// shift, since shifting would change the planet-relative coordinates.
///
/// The only safe transition from `Position<IntegrationFrame>` to
/// `Position<RootInertial>` is the integration-origin shift, exposed via
/// [`IntegOrigin::shift_position`](crate::integ_origin::IntegOrigin::shift_position)
/// and the `to_inertial` method on the typed translational-state wrapper
/// in `astrodyn_dynamics`. There is no implicit `From`/`Into` — forgetting
/// the shift at a root-inertial consumer is the bug class this phantom
/// catches (issue #255 / `RF.10` in `docs/JEOD_invariants.md`).
///
/// For all sims that integrate in the root frame, the shift is by
/// `IntegOrigin::zero()` and is a no-op (bit-identical numerics).
;
/// Mass-tree wildcard inertial-flavor frame for kinematic-propagation
/// scratch state.
///
/// The kinematic-propagation kernel (`propagate_state_via_storage`)
/// walks a heterogeneous mass tree where different nodes may live in
/// different integration frames — a parent body in [`RootInertial`],
/// a child in `PlanetInertial<Earth>`, a sibling somewhere else. The
/// per-edge `KinematicEdge.t_parent_child` matrix already captures
/// the cross-frame transition, so per-node translational scratch state
/// has no single concrete frame it can be tagged with.
///
/// `MassNode` is the inertial-flavor sibling of [`SelfRef`] /
/// [`SelfPlanet`] for that boundary: storage callers
/// (`Simulation::propagate_kinematic_state` in `astrodyn_runner` and
/// `propagate_state_from_root_system` in `astrodyn_bevy`) lift their
/// concrete-frame typed state into `<MassNode>` at the kernel-walk
/// boundary via [`Qty3::relabel_to`](crate::qty3::Qty3::relabel_to)
/// and re-pin to a concrete frame on writeback. Inside the walk every
/// node carries the same `<MassNode>` tag, so the kernel composes
/// states across heterogeneous nodes without needing a per-node frame
/// generic that the type system cannot satisfy.
///
/// The convention is the same as [`SelfRef`] / [`SelfPlanet`] — the
/// wildcard is **a runtime-resolved storage-boundary tag, not a
/// physical frame**. Mixing a `Position<MassNode>` with a
/// `Position<RootInertial>` is a compile error by design; the only
/// way to bridge is the explicit `relabel_to` at the boundary, which
/// the storage caller is responsible for routing through the
/// per-body integration-origin shift first when its body lives in a
/// non-root integration frame (RF.10 shift discipline).
// JEOD_INV: TS.01 — `MassNode` is the kinematic-propagation
// storage-boundary wildcard; the per-node trans is mid-walk scratch
// for a heterogeneous mass tree (parent in `RootInertial`, child in
// `PlanetInertial<P>`, …) whose per-edge rotation already carries
// the cross-frame transition. System code paths and APIs use
// concrete `<F: Frame>` parameters; `MassNode` appears only at the
// `KinematicNodeState.trans` storage boundary and the matching
// storage-caller lift sites. See the lint at
// `tests/self_ref_self_planet_discipline.rs`.
;
// NOTE on `NAME`: each frame's `NAME` const identifies the *frame kind*
// (e.g. "BodyFrame") rather than the embedded planet/vehicle tag. This is
// a `const &'static str`, so we can't splice `V::NAME` into it at compile
// time. Callers that need a fully-qualified string (including the vehicle
// or planet) should use `std::any::type_name::<F>()`, which `Qty3`'s
// `Debug` impl does.
/// Planet-fixed frame for any planet `P`. Rotates with that planet.
;
// Manual `Clone` / `Copy` impls so `FrameTransform<_, PlanetFixed<P>>` can
// be cloned/copied even when `P` itself doesn't bound `Clone` / `Copy`.
// `PlanetFixed<P>` is just `PhantomData<P>` — zero-sized — so both impls
// are trivially correct.
/// Body (CoM-centered) frame of vehicle `V`. Rotates with the vehicle.
;
/// Structural (geometric-origin) frame of vehicle `V`. Rotates with vehicle.
;
/// Local Vertical / Local Horizontal frame relative to chief vehicle `Chief`.
/// Z axis points planet-ward; Y opposes orbital angular momentum; X completes
/// the right-handed triad (approximately along-track in near-circular orbits).
;
/// North-East-Down topocentric frame relative to chief vehicle `Chief`.
;
// --- Self-referential vehicle marker ----------------------------------------
//
// `Vehicle` is sealed, so downstream crates cannot mint their own phantom
// tags. The Bevy adapter (and any other ECS adapter) needs *some* tag to
// instantiate vehicle-parameterized frames (`BodyFrame`, `StructuralFrame`)
// on per-entity components. `SelfRef` is the canonical "this entity's own
// vehicle frame" tag — it stands in for the (compile-time-unknown) entity
// without leaking generics into user-facing `Query`s.
//
// Distinct from `TestVehicle`: `SelfRef` is part of the production API
// surface (always compiled in), while `TestVehicle` is feature-gated for
// test harnesses only.
/// Phantom marker for "this entity's own vehicle frame" — used by ECS
/// adapters whose per-entity components carry frame phantoms but whose
/// vehicle identity is determined at runtime by the entity itself.
///
/// Use with [`BodyFrame`], [`StructuralFrame`], [`Lvlh`], [`Ned`], or any
/// `Vehicle`-parameterized type when the runtime entity *is* the vehicle.
/// Never appears in user-facing `Query`s — components wrap concrete
/// monomorphizations like `Position<RootInertial>` or
/// `Torque<BodyFrame<SelfRef>>`, so the user sees only the wrapper newtype.
// JEOD_INV: TS.01 — `SelfRef` is the per-entity storage-boundary wildcard
// for runtime-resolved vehicle identity (Bevy components, `AttachEvent`
// canonical registration, runner `SimBody.flat_plate_state`,
// `compute_relative_state::<SelfRef, SelfRef>`). System code paths and
// public APIs use `<V: Vehicle>` parameters; the wildcard appears only at
// per-entity storage boundaries. See the lint at
// `tests/self_ref_self_planet_discipline.rs`.
;
// --- Test-only vehicle marker ------------------------------------------------
//
// Tests that exercise vehicle-parameterized frames (`Lvlh`, `Ned`,
// `BodyFrame`, `StructuralFrame`) sometimes need *another* tag distinct
// from `SelfRef`, so we expose a single test-only vehicle behind the
// `test-utils` feature. It is never compiled into production builds.
/// A no-op vehicle phantom marker for use in downstream test harnesses.
///
/// Available only when the crate is built with `--features test-utils`.
;
// --- Macros for downstream marker types --------------------------------------
//
// Mission crates that model multiple vehicles (e.g., a chief + deputy
// formation, a tug + payload, the ISS plus a visiting Soyuz) need
// distinct compile-time `Vehicle` markers so `BodyFrame<Iss>` and
// `BodyFrame<Soyuz>` are type-distinct. The same applies to multi-planet
// scenarios (e.g., a Mars sample-return mission carrying state in both
// Mars-fixed and Earth-fixed frames).
//
// These macros generate the marker struct + per-domain sealed impl +
// trait impl in one statement. They are the canonical way for a
// downstream crate to add a `Vehicle` or `Planet` marker.
//
// The seal traits `VehicleSealed` and `PlanetSealed` are re-exported via
// `$crate::__macro_support` so the macros can satisfy the bounds from
// downstream call sites. The other three seal traits — `FrameSealed`,
// `TimeScaleSealed`, and `QuatSealed` (which gates both `Layout` and
// `Transform`) — are *not* re-exported, so the four public traits they
// guard (`Frame`, `TimeScale`, `Layout`, `Transform`) remain
// type-system-sealed and downstream code cannot impl them at all.
// Direct `impl Vehicle for X` outside the macro is technically possible
// but unsupported.
//
// Per-instance names (`"Iss"`, `"Soyuz"`, …) come from `stringify!($name)`.
// `Frame::NAME` cannot splice `V::NAME` (it is a `&'static str` const, not
// a `const fn`); callers that need a fully-qualified name should use
// `std::any::type_name::<F>()`, which is what `Qty3`'s `Debug` impl does.
/// Define a new compile-time `Vehicle` marker type.
///
/// Generates `pub struct $name;` plus the sealed `Vehicle` impl. The
/// resulting type is zero-sized and `Copy`. After `define_vehicle!(Iss);`
/// you can use `BodyFrame<Iss>`, `StructuralFrame<Iss>`, `Lvlh<Iss>`, and
/// `Ned<Iss>` as distinct frame phantoms.
///
/// # Example
///
/// ```
/// use astrodyn_quantities::define_vehicle;
/// use astrodyn_quantities::prelude::*;
///
/// define_vehicle!(Iss);
/// define_vehicle!(Soyuz);
///
/// // Position<BodyFrame<Iss>> and Position<BodyFrame<Soyuz>> are distinct
/// // types — adding one to the other is a compile error.
/// let _iss_pos: Position<BodyFrame<Iss>> = Qty3::zero();
/// let _soyuz_pos: Position<BodyFrame<Soyuz>> = Qty3::zero();
/// ```
///
/// # Frame mismatches are caught at compile time
///
/// ```compile_fail
/// use astrodyn_quantities::define_vehicle;
/// use astrodyn_quantities::prelude::*;
///
/// define_vehicle!(Iss);
/// define_vehicle!(Soyuz);
///
/// let iss: Position<BodyFrame<Iss>> = Qty3::zero();
/// let soyuz: Position<BodyFrame<Soyuz>> = Qty3::zero();
/// // Mixing frames is a compile error with a physics-language diagnostic:
/// let _bad = iss + soyuz;
/// ```
///
/// # Sealing
///
/// `Vehicle` has a `VehicleSealed` super-bound. The seal trait is
/// re-exported via `__macro_support` so this macro can satisfy it from
/// a downstream call site. The other sealed-trait domains
/// (`FrameSealed`, `TimeScaleSealed`, `QuatSealed`) are not re-exported,
/// so `Frame`, `TimeScale`, `Layout`, and `Transform` remain
/// type-system-sealed.
///
/// Direct `impl Vehicle for X {}` outside this macro is technically
/// possible (the seal trait is reachable) but is unsupported and may
/// break in any release. Use the macro.
/// Define a new compile-time `Planet` marker type.
///
/// Generates `pub struct $name;` plus the sealed `Planet` impl. The
/// resulting type is zero-sized and `Copy`. After `define_planet!(Pluto);`
/// you can use `PlanetFixed<Pluto>` as a distinct frame phantom.
///
/// # Example
///
/// ```
/// use astrodyn_quantities::define_planet;
/// use astrodyn_quantities::prelude::*;
///
/// define_planet!(Pluto);
///
/// // PlanetFixed<Pluto> is a distinct frame from PlanetFixed<Earth>.
/// let _pluto_fixed: Position<PlanetFixed<Pluto>> = Qty3::zero();
/// ```
///
/// # Sealing
///
/// Same per-domain seal as [`define_vehicle!`]: `Planet`'s super-bound
/// `PlanetSealed` is re-exported via `__macro_support` so this macro
/// can satisfy it from a downstream call site. Direct
/// `impl Planet for X {}` outside this macro is technically possible
/// but unsupported. Use the macro.