Skip to content

Strategy

Claude edited this page May 10, 2026 · 5 revisions

astrodyn: Reimplementation Strategy

Reimplementing NASA JEOD (JSC Engineering Orbital Dynamics) in Rust, using the Bevy game engine's Entity Component System as the simulation framework — replacing NASA's Trick.

Table of Contents


1. Project Overview

What is JEOD?

JEOD 5.4 is a C++ orbital dynamics library developed at NASA JSC. It models:

  • Dynamics: 6-DOF rigid body propagation, multi-body attachment/detachment, mass trees
  • Environment: Spherical harmonics gravity (GGM05C, GRAIL150, MRO110B2), JPL DE4xx ephemerides, MET atmosphere, time scales (TAI/UTC/UT1/TDB/TT/GMST/MET)
  • Interactions: Aerodynamic drag, solar radiation pressure, gravity gradient torque, contact mechanics
  • Utilities: Reference frame trees, integration methods (RK4, RKF45, Gauss-Jackson, LSODE), quaternion/orbital-element math

JEOD runs inside NASA's Trick simulation framework, which provides job scheduling, data recording, checkpoint/restart, and Python-based configuration.

What are we building?

A Rust reimplementation where Bevy's ECS replaces Trick as the simulation framework. Bevy provides:

  • Entities in place of Trick simulation objects
  • Components in place of C++ class member data
  • Systems in place of Trick scheduled jobs
  • Resources in place of global manager state
  • Schedules in place of Trick's job ordering
  • Plugins in place of Trick's S_modules

Portability Goal

While Bevy is the primary executor, the physics and math must not depend on Bevy. The codebase is split into three layers:

  • astrodyn_* crates — Pure Rust libraries containing all physics, math, algorithms, data models, and domain types. Zero Bevy dependency.

  • astrodyn crate — ECS-agnostic orchestration layer (the workspace root package, src/ at workspace root). Composes astrodyn_* functions into pipeline stages, owns the VehicleBuilder typestate, provides the recipes::* catalog, and re-exports every type that ECS adapters need. Zero Bevy dependency. The parallel astrodyn_runner crate (under crates/astrodyn_runner/) is a non-Bevy consumer of this layer that owns its own state container — it is the test harness used by Tier 3 cross-validation tests, not part of the production path.

  • astrodyn_bevy crate (crates/astrodyn_bevy/) — Thin Bevy integration layer that depends only on astrodyn + bevy. Defines component wrappers, systems that delegate to astrodyn functions (zero math), schedule sets, and plugin registration. All Bevy glue lives in one unified crate, not separate per-domain crates.

This separation means:

  1. Portability: Swap Bevy for another ECS by writing a new thin glue layer. The physics code doesn't change.
  2. Testability: Core algorithms are tested as pure functions — no need to spin up a Bevy App for unit tests.
  3. Embeddability: astrodyn_* crates can be used in non-ECS contexts (batch trajectory computation, optimization loops, Monte Carlo analysis) without pulling in Bevy.
  4. Stability: Physics crates are insulated from Bevy's rapid release cycle and breaking API changes.

Why Bevy?

Concern Trick Bevy
Language C++/Python Rust (memory safety, no UB)
Architecture OOP with manager objects Data-oriented ECS
Parallelism Manual thread management Automatic system parallelism
Ecosystem NASA-internal Open source, active community
Visualization External tools Built-in rendering, egui integration
Distribution Complex build chain cargo build

2. ECS Architecture Mapping

The Core Translation

JEOD is built on deep OOP hierarchies with manager god-objects. The translation is not mechanical — it requires rethinking how state flows through the simulation.

JEOD (OOP)                          Bevy (ECS)
─────────────────────────────────   ─────────────────────────────────
DynBody class (1200 lines)      →   ~10 focused Components on an Entity
DynManager.gravitation()         →   gravity_computation_system
GravityManager (singleton)       →   Resource + System
TimeManager (singleton)          →   Resource + System
RefFrame tree (pointer graph)    →   Entity hierarchy (Parent/Children)
BodyAction subclasses            →   Events or Commands
Virtual dispatch (GravitySource) →   Trait objects or enum dispatch
Method call ordering             →   System ordering constraints

Pattern-by-Pattern Mapping

Manager Pattern → Resource + Systems

JEOD's DynManager, GravityManager, TimeManager, and EphemerisManager are singletons that coordinate subsystems. In ECS:

  • Manager state becomes a Resource (e.g., SimulationTime, EphemerisData)
  • Manager behavior becomes one or more Systems
  • Manager coordination becomes system ordering via configure_sets()

Class Hierarchy → Component Composition

JEOD: DynBody : RefFrameOwner, IntegrableObject — deep inheritance tree. ECS: An entity gets the components it needs. No inheritance. A "DynBody" is just an entity that has TranslationalState + RotationalState + MassProperties + etc.

Tree Structures → Bevy's Entity Hierarchy

JEOD's RefFrame tree and MassBody tree use raw pointers. Bevy has built-in Parent/Children components that give us the same tree structure with safe entity references.

Virtual Dispatch → Enum or Trait Objects

JEOD uses virtual base classes (GravitySource, Atmosphere, etc.) for extensibility. In Rust: use an enum for the closed set of known models, or Box<dyn Trait> for user-extensible models. Prefer enums where the model set is fixed (gravity, atmosphere).

Core → Orchestration → Glue Separation

All of the above mappings happen in three layers:

astrodyn_dynamics        (plain Rust structs, pure functions)
    ↕ used by
astrodyn             (orchestration: composes astrodyn_* functions, re-exports types)
    ↕ used by
astrodyn_bevy        (derives Component/Resource, defines systems that delegate to astrodyn)

The astrodyn_* crates define algorithms. astrodyn composes them into pipeline stages and re-exports all types. astrodyn_bevy depends only on astrodyn — switching to another ECS means writing a new glue crate that calls the same astrodyn functions. astrodyn_runner is a parallel non-Bevy consumer of astrodyn that owns its own state container (used as the Tier 3 test harness, not in the production path).


3. Component Design

3.1 Core vs. Bevy Split

Every data type exists first as a plain Rust struct in a astrodyn_* crate, then gets wrapped or re-derived as a Bevy component in astrodyn_bevy (crates/astrodyn_bevy/src/components/).

// ── astrodyn_dynamics/src/state.rs (pure Rust, no Bevy) ─────────────
/// Translational state in the integration frame.
pub struct TranslationalState {
    pub position: DVec3,    // m
    pub velocity: DVec3,    // m/s
}

// ── crates/astrodyn_bevy/src/components/state.rs (Bevy glue) ────────
use bevy::prelude::*;
use astrodyn_dynamics::TranslationalState;

// Option A: newtype wrapper
#[derive(Component, Deref, DerefMut)]
pub struct TranslationalStateComponent(pub TranslationalState);

// Option B: feature-gated derive (if we control the core crate)
//   #[derive(Component)]  // behind #[cfg(feature = "bevy")]
//   pub struct TranslationalState { ... }

Option B (feature-gated derive) is preferred when practical — it avoids wrapper boilerplate. Option A is the fallback when the core type can't carry Bevy derives (e.g., when it contains non-Reflect fields).

3.2 Core State Types

These are the ECS equivalent of DynBody's member data, decomposed by access pattern — components that are read/written together by the same systems stay together.

The structs below live in astrodyn_dynamics (pure Rust). In the Bevy layer they gain #[derive(Component)].

// ── astrodyn_dynamics/src/state.rs ──────────────────────────────────
// All types are plain Rust. In the Bevy layer they gain #[derive(Component)].

/// Translational state in the integration frame.
pub struct TranslationalState {
    pub position: DVec3,    // m
    pub velocity: DVec3,    // m/s
}

/// Rotational state (body orientation and angular velocity).
pub struct RotationalState {
    pub quaternion: DQuat,       // left transformation, parent-to-body
    pub ang_vel_body: DVec3,     // rad/s, expressed in body frame
}

/// Rigid body mass properties.
pub struct MassProperties {
    pub mass: f64,                // kg
    pub inertia: DMat3,           // kg*m^2, in body frame
    pub inertia_inverse: DMat3,   // precomputed I^-1
    pub center_of_mass: DVec3,    // m, in structural frame
}

/// Dynamics configuration flags.
pub struct DynamicsConfig {
    pub translational: bool,      // integrate translation?
    pub rotational: bool,         // integrate rotation?
    pub three_dof: bool,          // translation-only mode?
}

Note: IntegrationFrameRef(Entity) only exists in the Bevy layer since Entity is a Bevy type. In a non-ECS context, the integration frame is identified by name or index.

3.3 Force/Torque Types

Each interaction system writes its own force output. The force collection system reads all of them and produces TotalForce. All types live in astrodyn_dynamics (pure Rust).

// ── astrodyn_dynamics/src/forces.rs ─────────────────────────────────

/// Gravitational acceleration and gradient at the body's position.
pub struct GravityAcceleration {
    pub accel: DVec3,       // m/s^2, in integration frame
    pub gradient: DMat3,    // 1/s^2, tidal gradient tensor
    pub potential: f64,     // m^2/s^2
}

/// Aerodynamic drag force and torque.
pub struct AerodynamicForce {
    pub force: DVec3,       // N, in body frame
    pub torque: DVec3,      // N*m, in body frame
}

/// Solar radiation pressure force and torque.
pub struct RadiationForce {
    pub force: DVec3,       // N, in body frame
    pub torque: DVec3,      // N*m, in body frame
}

/// Gravity gradient torque on an extended body.
pub struct GravityTorque {
    pub torque: DVec3,      // N*m, in body frame
}

/// Sum of all forces and torques acting on the body.
pub struct TotalForce {
    pub force: DVec3,       // N, in integration frame
    pub torque: DVec3,      // N*m, in body frame
}

/// Computed accelerations (output of F=ma).
pub struct FrameDerivatives {
    pub trans_accel: DVec3,  // m/s^2
    pub rot_accel: DVec3,    // rad/s^2
}

3.4 Reference Frame Types

JEOD's RefFrame tree is the backbone of all coordinate transformations. The state types live in astrodyn_frames (pure Rust). The tree structure is ECS-specific.

// ── astrodyn_frames/src/state.rs (pure Rust) ────────────────────────

/// State of a reference frame relative to its parent frame.
/// Mirrors JEOD's RefFrameState = RefFrameTrans + RefFrameRot.
pub struct RefFrameState {
    pub trans: RefFrameTrans,
    pub rot: RefFrameRot,
}

pub struct RefFrameTrans {
    pub position: DVec3,    // m, in parent frame
    pub velocity: DVec3,    // m/s, in parent frame
}

pub struct RefFrameRot {
    pub q_parent_this: DQuat,       // left transformation quaternion
    pub t_parent_this: DMat3,       // transformation matrix (redundant with quat)
    pub ang_vel_this: DVec3,        // rad/s, in this frame
}

/// Frame identity and classification.
pub struct RefFrameInfo {
    pub name: String,               // "Earth.inertial", "ISS.structure", etc.
    pub kind: RefFrameKind,
}

pub enum RefFrameKind {
    Inertial,       // non-rotating, valid as integration frame
    PlanetFixed,    // rotating with planet
    Body,           // attached to a dynamic body
}
// ── crates/astrodyn_bevy/src/components/frame_tree.rs ───────────────

// The frame tree uses Bevy's built-in `ChildOf` / `Children` relationship
// (Bevy 0.18 renamed `Parent` to `ChildOf`). Marker components identify
// frame types for queries: `InertialFrameMarker`, `PlanetFixedFrameMarker`,
// `BodyFrameMarker`, `IntegrationFrameMarker`.

// Frame tree example (every node is a Bevy `Entity` with `FrameTransC` /
// `FrameRotC` / `FrameAngVelC` components and one or more frame markers):
//
// root.frame [InertialFrameMarker]                              ← RootFrameEntityR
//   +-- Sun.frame.inertial [InertialFrameMarker]
//   +-- Earth.frame.inertial [InertialFrameMarker]
//   |     +-- Earth.frame.pfix [PlanetFixedFrameMarker]
//   |     +-- ISS.frame [BodyFrameMarker]                       ← FrameEntityC on body
//   +-- Moon.frame.inertial [InertialFrameMarker]
//   +-- Mars.frame.inertial [InertialFrameMarker]

The arena-shaped FrameTree lives in astrodyn_frames (used by the non-Bevy astrodyn_runner harness). The Bevy adapter does not mirror it: state lives exclusively on the ECS frame entities, and cross-frame queries go through the RelativeFrameState / FrameOrigin SystemParams. Both consumers share the read-only graph-traversal algorithms via the astrodyn::FrameStorage trait. See the Frame-Tree-ECS-Native wiki page for the full design.

3.5 Planet Types

Planets and vehicles share the same state types (TranslationalState, RotationalState) — differentiated by marker components in the ECS layer.

// ── astrodyn_planet/src/lib.rs (pure Rust) ──────────────────────────

/// Planetary shape parameters (reference ellipsoid).
pub struct PlanetShape {
    pub r_eq: f64,          // equatorial radius, m
    pub r_pol: f64,         // polar radius, m
    pub flattening: f64,    // flattening coefficient (1/298.257 for Earth)
}

// ── astrodyn_gravity/src/source.rs (pure Rust) ──────────────────────

/// Gravity source definition.
pub struct GravitySource {
    pub mu: f64,            // gravitational parameter, m^3/s^2
    pub model: GravityModel,
}

pub enum GravityModel {
    PointMass,
    SphericalHarmonics {
        degree: usize,
        order: usize,
        radius: f64,                   // reference radius, m
        cnm: Vec<Vec<f64>>,            // cosine coefficients [n][m]
        snm: Vec<Vec<f64>>,            // sine coefficients [n][m]
    },
}

// ── astrodyn_gravity/src/compute.rs (pure Rust) ─────────────────────

/// Pure function: compute gravity acceleration at a position.
/// No ECS dependency — callable from any context.
pub fn compute_gravity(
    source: &GravitySource,
    position: DVec3,         // in source-centered frame
) -> GravityAcceleration { ... }

3.6 Gravity Controls (Vehicle-to-Planet Link)

Each vehicle specifies which planets affect it and how. The core type lives in astrodyn_gravity and uses a generic identifier for the source (string name or index). The Bevy layer maps this to Entity.

// ── astrodyn_gravity/src/controls.rs (pure Rust) ────────────────────

/// Per-vehicle specification of gravitational interactions.
pub struct GravityControls<SourceId = String> {
    pub controls: Vec<GravityControl<SourceId>>,
}

pub struct GravityControl<SourceId = String> {
    pub source_id: SourceId,       // planet identifier (generic)
    pub spherical_only: bool,      // point-mass vs full harmonics
    pub max_degree: Option<usize>, // truncation override
    pub max_order: Option<usize>,
    pub compute_gradient: bool,    // tidal gradient needed?
}

// ── crates/astrodyn_bevy/src/components/gravity.rs ──────────────────

/// In Bevy, SourceId = Entity for efficient queries.
pub type BevyGravityControls = GravityControls<Entity>;

3.7 Derived State Types

Optional data that computes secondary state representations. The computation functions live in astrodyn_math (pure Rust). The ECS layer attaches these as components and runs systems that call the pure functions.

// ── astrodyn_math/src/orbital_elements.rs (pure Rust) ───────────────

pub struct OrbitalElements {
    pub semi_major_axis: f64,      // m
    pub eccentricity: f64,
    pub inclination: f64,          // rad
    pub raan: f64,                 // rad, right ascension of ascending node
    pub arg_periapsis: f64,        // rad
    pub true_anomaly: f64,         // rad
    pub mean_anomaly: f64,         // rad
    pub mean_motion: f64,          // rad/s
    pub orbital_energy: f64,       // m^2/s^2
    pub ang_momentum: f64,         // m^2/s
}

/// Pure function — no ECS dependency.
pub fn cartesian_to_elements(pos: DVec3, vel: DVec3, mu: f64) -> OrbitalElements { ... }
pub fn elements_to_cartesian(elems: &OrbitalElements, mu: f64) -> (DVec3, DVec3) { ... }

// ── astrodyn_math/src/orientation.rs (pure Rust) ────────────────────

pub struct EulerAngles {
    pub sequence: EulerSequence,
    pub ref_body_angles: DVec3,        // rad
    pub body_ref_angles: DVec3,        // rad
}

pub fn decompose_euler(matrix: &DMat3, seq: EulerSequence) -> EulerAngles { ... }

// ── astrodyn_math/src/planet_fixed.rs (pure Rust) ───────────────────

pub struct PlanetFixedPosition {
    pub latitude: f64,       // rad, geodetic
    pub longitude: f64,      // rad
    pub altitude: f64,       // m, above ellipsoid
}

pub fn cartesian_to_geodetic(pos: DVec3, shape: &PlanetShape) -> PlanetFixedPosition { ... }

3.8 Bevy Components

Component wrappers live in crates/astrodyn_bevy/src/components/ (split across state.rs, gravity.rs, frame_tree.rs, mass_tree.rs, …). Each wraps an astrodyn type with #[derive(Component)] and a C suffix. Most are now also parameterized by a planet phantom (e.g. TranslationalStateC<P: Planet>) so the compiler refuses to mix bodies integrated in different planet-inertial frames:

// ── crates/astrodyn_bevy/src/components/state.rs ────────────────────

#[derive(Component, Deref, DerefMut)]
pub struct TranslationalStateC<P: Planet>(pub TranslationalState, PhantomData<P>);

#[derive(Component, Deref, DerefMut)]
pub struct RotationalStateC(pub RotationalState);

#[derive(Component, Deref, DerefMut)]
pub struct MassPropertiesC(pub MassProperties);

#[derive(Component, Deref, DerefMut)]
pub struct DynamicsConfigC(pub DynamicsConfig);

#[derive(Component, Deref, DerefMut)]
pub struct GravityControlsC(pub GravityControls<Entity>);
// ... etc.

Entities are spawned with individual components (no bundle struct):

commands.spawn((
    TranslationalStateC(state),
    RotationalStateC(rot),
    MassPropertiesC(mass),
    DynamicsConfigC(config),
    GravityControlsC(controls),
    GravityAccelerationC(GravityAcceleration::default()),
    TotalForceC(TotalForce::default()),
    FrameDerivativesC(FrameDerivatives::default()),
));

4. System Pipeline

See also: Integration-Groups — how the Bevy schedule maps to JEOD's JeodIntegrationGroup concept; multi-body coordination, multi-stage integrators, and the separate-group escape hatch.

Execution Schedule

JEOD's integration loop translates to Bevy's FixedUpdate schedule, partitioned into the seven AstrodynSet variants defined in crates/astrodyn_bevy/src/sets.rs. The ordering matches JEOD's DynManager sequencing — JEOD's nine init/update steps collapse to seven sets because gravity + atmosphere both run in Environment, and frame propagation rides inside the integration system as its post-step (issue #362).

FixedUpdate
 |
 |-- AstrodynSet::TimeUpdate
 |     '-- time_advance_system            // advance TAI, compute UTC/UT1/TDB/GMST
 |
 |-- AstrodynSet::EphemerisUpdate         // .after(TimeUpdate)
 |     |-- ephemeris_update_system        // update planet positions from DE4xx
 |     '-- planet_fixed_rotation_system   // update planet-fixed frame rotations (RNP)
 |
 |-- AstrodynSet::Environment             // .after(EphemerisUpdate)
 |     |-- gravity_computation_system     // for each body: spherical harmonics accel
 |     '-- atmosphere_update_system       // compute density at body positions
 |
 |-- AstrodynSet::Interaction             // .after(Environment)
 |     |-- aero_drag_system               // F_drag = 0.5 * rho * v^2 * Cd * A
 |     |-- flat_plate_srp_system          // solar radiation pressure (flat plate)
 |     |-- cannonball_srp_system          // solar radiation pressure (cannonball)
 |     '-- gravity_torque_system          // gravity gradient torque
 |
 |-- AstrodynSet::ForceCollection         // .after(Interaction)
 |     |-- force_collection_system        // sum all force components -> TotalForce
 |     '-- wrench_aggregation_system      // composite-rigid-body wrench accumulation
 |
 |-- AstrodynSet::Integration             // .after(ForceCollection)
 |     |-- integration_system             // propagate state via RK4/GJ/ABM4
 |     |-- sync_body_to_frame_system      // mirror body state into frame entity
 |     |-- frame_switch_system            // distance-triggered re-parenting
 |     '-- propagate_state_from_root_post_integration_system  // kinematic walk
 |
 '-- AstrodynSet::DerivedState            // .after(Integration)
       |-- orbital_elements_system        // Cartesian -> Keplerian
       |-- euler_angles_system            // quaternion -> Euler angles
       |-- geodetic_system                // inertial -> geodetic coords
       |-- lvlh_system                    // compute LVLH frame state
       |-- solar_beta_system              // solar beta angle
       '-- earth_lighting_system          // shadow / illumination geometry

System Ordering in Code

app.configure_sets(FixedUpdate, (
    AstrodynSet::TimeUpdate,
    AstrodynSet::EphemerisUpdate.after(AstrodynSet::TimeUpdate),
    AstrodynSet::Environment.after(AstrodynSet::EphemerisUpdate),
    AstrodynSet::Interaction.after(AstrodynSet::Environment),
    AstrodynSet::ForceCollection.after(AstrodynSet::Interaction),
    AstrodynSet::Integration.after(AstrodynSet::ForceCollection),
    AstrodynSet::DerivedState.after(AstrodynSet::Integration),
));

Multi-Stage Integration

JEOD uses multi-stage integrators (e.g., RK4 has 4 stages per timestep, each requiring a fresh force evaluation). This is handled with a resource tracking stage state:

#[derive(Resource)]
pub struct IntegrationState {
    pub method: IntegrationMethod,
    pub current_stage: usize,
    pub total_stages: usize,
    pub dt: f64,
}

pub enum IntegrationMethod {
    Rk4,                    // 4 stages, fixed step
    Rkf45 { tol: f64 },    // adaptive step
    GaussJackson { order: usize },  // multi-step
}

The integration_system runs the full multi-stage loop internally: for each stage it re-evaluates forces, computes derivatives, and advances the stage. This keeps the multi-stage logic contained rather than spreading it across the schedule.

Key System Signatures

Bevy systems are thin wrappers that query components and delegate to astrodyn_* pure functions. This keeps the physics testable without Bevy.

// ── crates/astrodyn_bevy/src/systems/ ──────────────────────────

fn gravity_computation_system(
    mut bodies: Query<(&TranslationalState, &BevyGravityControls, &mut GravityAcceleration)>,
    sources: Query<(&GravitySource, &RefFrameState), With<Planet>>,
) {
    for (state, controls, mut accel) in &mut bodies {
        // Delegate to pure function from astrodyn_gravity
        *accel = astrodyn_gravity::compute_all_gravity(
            state.position, controls, |entity| sources.get(entity),
        );
    }
}

// ── crates/astrodyn_bevy/src/systems/ ──────────────────────────

fn integration_system(
    mut bodies: Query<(
        &TotalForce, &MassProperties, &DynamicsConfig,
        &mut TranslationalState, &mut RotationalState,
        &mut FrameDerivatives,
    )>,
    integ_state: Res<IntegrationState>,
) {
    for (force, mass, config, mut trans, mut rot, mut derivs) in &mut bodies {
        // Delegate to pure function from astrodyn_dynamics
        astrodyn_dynamics::integrate_step(
            &force, mass, config, &mut trans, &mut rot, &mut derivs,
            integ_state.method, integ_state.dt,
        );
    }
}

5. Plugin Architecture

Crate Organization

The workspace has three layers: core physics crates (astrodyn_*), the orchestration crate astrodyn (the workspace root package under src/), and the Bevy glue crate astrodyn_bevy (under crates/astrodyn_bevy/). astrodyn_bevy depends only on astrodyn + bevy — see Section 1: Portability Goal. The parallel astrodyn_runner (under crates/astrodyn_runner/) is a non-Bevy consumer of astrodyn used as the in-workspace test harness for Tier 3 cross-validation and offline propagation.

astrodyn/                               # workspace root
|
+-- crates/
|   |
|   | ── CORE LAYER (pure Rust, no Bevy dependency) ───────────────
|   |
|   +-- astrodyn_math/                       # f64 math, quaternions, orbital elements
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- quaternion.rs            # JEOD quaternion conventions (scalar-first)
|   |       +-- orbital_elements.rs      # Cartesian <-> Keplerian, Kepler equation
|   |       +-- orientation.rs           # Euler angle decomposition, rotation matrices
|   |       +-- planet_fixed.rs          # geodetic coordinate conversions
|   |       +-- lvlh.rs                  # LVLH frame computation
|   |
|   +-- astrodyn_time/                       # Time scales and conversions
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- scales.rs               # TAI, UTC, UT1, TDB, TT, GMST, MET types
|   |       +-- converters.rs           # time scale conversions (leap seconds, UT1-TAI)
|   |       +-- sim_time.rs             # SimulationTime state struct
|   |
|   +-- astrodyn_frames/                     # Reference frame state and transformations
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- state.rs                # RefFrameState, RefFrameTrans, RefFrameRot
|   |       +-- transform.rs           # relative state computation, frame composition
|   |       +-- tree.rs                 # arena-based frame tree (for non-ECS use)
|   |
|   +-- astrodyn_gravity/                    # Gravity models and computation
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- source.rs              # GravitySource, GravityModel
|   |       +-- controls.rs            # GravityControls<SourceId>
|   |       +-- compute.rs             # compute_gravity() pure function
|   |       +-- spherical_harmonics.rs  # Legendre polynomials, coefficient evaluation
|   |   +-- data/                       # coefficient files (binary or RON)
|   |       +-- earth_ggm05c.bin
|   |       +-- moon_grail150.bin
|   |       +-- mars_mro110b2.bin
|   |
|   +-- astrodyn_ephemeris/                  # Ephemeris readers
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- de4xx.rs               # JPL DE4xx binary reader (Chebyshev interpolation)
|   |
|   +-- astrodyn_atmosphere/                 # Atmosphere models
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- model.rs              # Atmosphere trait
|   |       +-- met.rs                # Marshall Engineering Thermosphere tables
|   |
|   +-- astrodyn_dynamics/                   # State types, integration methods, force collection
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- state.rs              # TranslationalState, RotationalState
|   |       +-- mass.rs               # MassProperties, composite mass computation
|   |       +-- forces.rs             # GravityAcceleration, AeroForce, TotalForce, etc.
|   |       +-- integration.rs        # RK4, RKF45, Gauss-Jackson, LSODE (pure functions)
|   |       +-- body_action.rs        # initialization from orbital elements, LVLH, NED
|   |
|   +-- astrodyn_interactions/               # Force/torque computation
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- aerodynamics.rs       # drag computation (pure function)
|   |       +-- radiation.rs          # SRP computation (pure function)
|   |       +-- gravity_torque.rs     # gradient torque (pure function)
|   |
|   +-- astrodyn_planet/                     # Planet data and presets
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- shape.rs             # PlanetShape
|   |       +-- presets.rs           # Earth, Moon, Mars, Sun constants
|   |       +-- rnp.rs              # precession, nutation, polar motion
|   |
|   | ── BEVY GLUE LAYER (thin, delegates to astrodyn) ───────────
|   |
|   +-- astrodyn_bevy/                   # Bevy ECS adapter (depends on astrodyn + bevy only)
|   |   +-- src/
|   |       +-- lib.rs                   # AstrodynPlugin, resources, schedule set ordering
|   |       +-- components/              # Component wrappers (state, gravity, mass_tree, frame_tree, ...)
|   |       +-- systems/                 # All Bevy systems delegating to astrodyn
|   |       +-- sets.rs                  # AstrodynSet schedule sets
|   |       +-- frame_param.rs           # RelativeFrameState / FrameOrigin SystemParams
|   |       +-- frame_attach_system.rs   # frame-attached body propagation
|   |       +-- mass_tree.rs             # MassChildOf walks, composite_mass_system
|   |       +-- kinematic_propagation.rs # JEOD-style propagate_state_from_structure
|   |       +-- recipes.rs               # Re-export of astrodyn::recipes
|   |       +-- validation.rs            # Runtime invariant checks
|   |
|   | ── PARALLEL NON-BEVY CONSUMER (in-workspace test harness) ──
|   |
|   +-- astrodyn_runner/                 # Standalone arena-state Simulation harness
|   |   +-- src/                         # depends on astrodyn + each astrodyn_* crate it uses
|   |       +-- lib.rs                   # Simulation, SimBody, FrameTree (arena), ...
|   |
|   | ── VERIFICATION / FIXTURES ────────────────────────────────
|   |
|   +-- astrodyn_verif_jeod/             # JEOD parsers, fixtures, Tier 3 trajectory tests
|   +-- astrodyn_verif_parity/           # runner ↔ Bevy parity tests + KNOWN_PARITY_GAPS
|
| ── ORCHESTRATION LAYER (workspace root, ECS-agnostic pipeline) ──
+-- src/                                 # the `astrodyn` crate (`Cargo.toml` at workspace root)
|   +-- lib.rs                           # public re-exports, prelude
|   +-- atmosphere.rs                    # evaluate_atmosphere()
|   +-- gravity.rs                       # accumulate_gravity()
|   +-- forces.rs                        # collect_and_resolve_forces()
|   +-- integration.rs                   # integrate_body()
|   +-- validation.rs                    # validate_body() -> Result<(), Vec<ValidationError>>
|   +-- pipeline.rs                      # PipelineStage enum, PIPELINE_ORDER
|   +-- frame_orchestration.rs           # frame-tree compose / origin shifts
|   +-- vehicle_builder.rs               # typestate VehicleBuilder
|   +-- recipes/                         # constants, earth, mars, moon, orbital_elements, ...
|   +-- ...
|
+-- xtask/                               # cargo xtask regenerate-tier3 driver
+-- trick/                               # Docker-based JEOD reference-CSV regen tooling

JEOD's data files are parsed inside astrodyn_verif_jeod (parsers + fixtures were redistributed there in #387; the older standalone jeod_test_data crate has been retired). The verbatim NASA JEOD source mirror under crates/astrodyn_verif_jeod/test_data/jeod_inputs/ is committed to the repository, so all three tiers run on a fresh clone with no $JEOD_HOME.

Dependency graph:

                     astrodyn_quantities
                  (uom + phantom tags + Qty3<D, F> +
                   NormalizedQuat + FrameTransform<From, To>)
                            ^
                            |
                  ──────────┴──────────
                  | every astrodyn_* crate |
                  ─────────────────────
                            |
astrodyn_math  <── astrodyn_dynamics  <── astrodyn_interactions
   ^              ^                      |
   |              |                      v
astrodyn_time    astrodyn_gravity          astrodyn_atmosphere
   ^              ^
   |              |
astrodyn_frames  astrodyn_ephemeris    astrodyn_planet
                                     |
     All astrodyn_* are pure Rust        |
     ────────────────────────────────┘
              |
              v
     astrodyn (workspace root: composes astrodyn_* functions, zero Bevy dep)
              |
        ┌─────┴─────────────────────────────────┐
        v                                       v
  astrodyn_bevy (Bevy glue, depends on        astrodyn_runner (in-workspace
   astrodyn + bevy)                            test harness; Tier 3 + offline)

astrodyn_quantities (added in #101 Phase 0) sits at the bottom of the DAG. Every other astrodyn_* crate depends on it for typed quantities, phantom frame/time-scale tags, and the F64Ext facade. See §8 "Phase 8: Type-System Refactor" and Type-System for the architecture and rationale.

Three-Layer Architecture

The codebase has three layers:

  1. astrodyn_* crates — Pure physics algorithms and data types. Zero Bevy dependency. Define per-function operations (gravity evaluation, RK4 step, drag computation, etc.).

  2. astrodyn crate — ECS-agnostic orchestration. Zero Bevy dependency. Composes astrodyn_* functions into pipeline stages and provides:

    • Per-body functions (primary API for ECS adapters): accumulate_gravity(), evaluate_atmosphere(), collect_and_resolve_forces(), integrate_body(), validate_body(). All borrow-based — the ECS world remains the source of truth.
    • Simulation runner (for non-ECS use): standalone struct for batch propagation, scripting, and tests. Owns state internally.
    • PipelineStage enum and PIPELINE_ORDER: canonical stage ordering that any adapter must respect.
  3. astrodyn_bevy crate (crates/astrodyn_bevy/) — Thin Bevy glue. All system functions, component definitions, schedule sets, plugin registration, and validation live in a single unified crate. Each system queries components and delegates to astrodyn per-body functions.

Why three layers? The original two-layer design (astrodyn_* + Bevy glue) kept physics portable, but the orchestration logic — pipeline ordering, gravity accumulation, frame transform composition, force contribution assembly, integration routing, and validation — lived exclusively in Bevy system code. A non-Bevy ECS user would have had to reverse-engineer ~10 systems to build a working simulation loop. The astrodyn layer extracts this orchestration into a single, Bevy-free crate that any ECS (or no ECS) can use directly.

astrodyn_bevy depends only on astrodyn and bevy — never on astrodyn_* crates directly. astrodyn re-exports all types that ECS adapters need, making it the single API surface for the production path (every Bevy system, every mission crate, every downstream consumer that ships in a real simulation reads the workspace through astrodyn and only through astrodyn; #360 scoped this rule explicitly). The astrodyn_* and astrodyn crates have no Bevy dependency and can be used standalone.

The workspace also contains the parallel non-Bevy consumer astrodyn_runner (crates/astrodyn_runner/), which owns its own state container and does depend directly on the physics crates it constructs by hand. The runner is a test harness — Tier 3 cross-validation tests, batch propagation, scripting — and is never published as a mission consumer. See CLAUDE.md for the full astrodyn_bevy vs astrodyn_runner asymmetry rules.

Top-Level Plugin Composition

All Bevy glue lives in a single AstrodynPlugin in crates/astrodyn_bevy/src/lib.rs (renamed from JeodPlugin in #392). The plugin registers resources, configures schedule set ordering, and adds all systems inline — there are no separate sub-plugins per domain.

pub struct AstrodynPlugin;

impl Plugin for AstrodynPlugin {
    fn build(&self, app: &mut App) {
        // Insert resources (SimulationTime, EphemerisData, etc.)
        // Configure AstrodynSet schedule set ordering in FixedUpdate
        // Add all systems (time, gravity, integration, derived states, etc.)
        //   each assigned to the appropriate AstrodynSet
    }
}

Users add AstrodynPlugin to get the full simulation pipeline. Since all systems live in one plugin, selective opt-in is done by which components are spawned on entities, not by choosing sub-plugins.


6. Verification Strategy

Four-Tier Verification Plan

Tier 1: Analytical Unit Tests

Test pure math with known exact solutions. No JEOD data needed. Implement these alongside each module.

#[test]
fn kepler_equation_circular() {
    // For circular orbit (e=0), mean anomaly = eccentric anomaly
    assert_f64_eq!(solve_kepler(0.0, PI / 4.0), PI / 4.0);
}

#[test]
fn orbital_elements_roundtrip() {
    let state = CartesianState { r: dvec3(...), v: dvec3(...) };
    let elems = OrbitalElements::from_cartesian(&state, MU_EARTH);
    let back = elems.to_cartesian(MU_EARTH);
    assert_dvec3_near!(state.r, back.r, 1e-10);
}

#[test]
fn quaternion_rotation_matrix_consistency() { ... }
#[test]
fn frame_composition_is_identity_for_self() { ... }
#[test]
fn kepler_orbit_conserves_energy() { ... }
#[test]
fn kepler_orbit_period_matches_analytical() {
    // T = 2*pi*sqrt(a^3/mu) — must match to integrator precision
}

Tier 2: Component Tests Against JEOD Reference Values

Use JEOD's own test data files via the parsers in astrodyn_verif_jeod. The JEOD input mirror lives in crates/astrodyn_verif_jeod/test_data/jeod_inputs/, committed to the repository so all three tiers run on a fresh clone with no $JEOD_HOME. See Section 7 for parser details.

use astrodyn_verif_jeod::{reference_states, gravity_test_cases, orbital_init_data};

#[test]
fn iss_reference_inertial_state() {
    // Source: models/dynamics/body_action/verif/SIM_orbinit/
    //         Modified_data/ISS/reference_inertial_trans_state.py
    // (read from crates/astrodyn_verif_jeod/test_data/jeod_inputs/)
    let expected = reference_states("ISS", "inertial");
    // expected.position = [1244540.53, 5655938.85, 3425643.22]
    // expected.velocity = [-6003.833051, -1469.496044, 4590.511776]

    let init = orbital_init_data("ISS", "trans_Orbit_inertial_body_set01");
    let state = propagate_from_elements(&init);
    assert_dvec3_near!(state.position, expected.position, 1.0);  // 1m tolerance
}

#[test]
fn earth_gravity_at_known_positions() {
    // Source: models/environment/gravity/verif/unit_tests/
    //         grav_geospherical/data/verif_out.txt
    for case in gravity_test_cases() {
        let result = gravity_source.compute(case.position, case.degree, case.order);
        assert_dvec3_near!(result.accel, case.expected_accel, 1e-12);
        assert_near!(result.potential, case.expected_potential, 1e-6);
    }
}

#[test]
fn euler_angle_decomposition() {
    // Source: models/dynamics/derived_state/verif/unit_tests/
    //         euler_derived_state_ut.cc
    // 6 test cases with rotation matrix -> expected Euler angles
    for case in euler_test_cases() {
        let result = euler_decompose(case.matrix, case.sequence);
        assert_dvec3_near!(result, case.expected_angles, 1e-14);
    }
}

Tier 3: Trajectory Cross-Validation

Generate reference trajectories by running JEOD's verification sims inside a Rocky 9 Docker container with Trick 25, then compare against astrodyn propagation from identical initial conditions.

Docker workflow:

# Build the container (from astrodyn root, with trick/ and jeod/ as siblings)
docker build -f trick/Dockerfile -t jeod-trick ..

# Generate reference CSVs (runs JEOD sims, exports to test_data/)
mkdir -p test_data
docker run --rm -v $(pwd)/test_data:/output jeod-trick

The container builds Trick and JEOD from source using the exact package list from Trick's CI (test_linux.yml Rocky 9 matrix entry), runs verification sims, and exports ASCII CSV trajectories. The CSV files are gitignored — they are generated locally and consumed by cargo test.

Cross-validation test pattern (implemented):

#[test]
fn tier3_cross_validate_against_jeod_dyncomp() {
    let csv_path = Path::new("../../test_data/dyncomp_run2_state.csv");
    assert!(csv_path.exists(), "Generate with: docker run ...");

    let jeod_trajectory = load_jeod_trajectory(&csv_path);
    // ... propagate using OUR ported code, collect StateLog entries ...

    let report = CrossvalReport::compute("tier3_...", &our_states, &ref_states);
    report.write();  // errors to target/tier3_crossval/<name>.json

    // Per-component tolerances at 5% above observed max error
    report.assert_position([1.37e-6, 2.154e-6, 1.826e-6]);
    report.assert_velocity([1.446e-9, 2.389e-9, 1.814e-9]);
}

CrossvalReport computes per-component max errors and writes them to JSON. It has no tolerance fields — tolerances live exclusively in the test source as literal values passed to assert_position, assert_velocity, assert_quat_angle, assert_ang_vel, or via assert!(var < literal, "name") for scalar extras. The report binary extracts all tolerances from source for display — JSON contains only errors.

Key rules:

  • Tests assert on missing data — never skip gracefully.
  • All computation (gravity, Earth rotation, time conversion) is our own ported code. JEOD CSV data is used only for comparison, never as input to our computation. JEOD data must never be injected into intermediate computation steps — the Simulation propagates entirely under its own physics from initial conditions.
  • Every Tier 3 test must exercise the full Simulation::step() pipeline end-to-end. Tests that call per-body functions directly or evaluate static data points bypass the pipeline and must be upgraded.
  • Tier 3 tests are part of the definition of done for every phase, not optional.
  • Tolerances are literal per-test values (5% above observed error), never runtime-computed or conditional. JEOD CSVs are static, our code is deterministic.

Two complementary Tier 3 test paths:

  1. Simulation-vs-JEOD (crates/astrodyn_verif_jeod/tests/tier3_sim_*.rs): runs astrodyn_runner::Simulation::step() from JEOD initial conditions and compares against JEOD Trick CSV output at each checkpoint. Validates the astrodyn pipeline (driven by the runner harness) against NASA's reference.

  2. Bevy-vs-Simulation parity (crates/astrodyn_verif_parity/tests/bevy_parity_*.rs): runs both a Bevy App (with full AstrodynPlugin pipeline) and an astrodyn_runner::Simulation from the same initial conditions and asserts f64::to_bits() equality. The [VerificationCaseParityExt::run_and_assert_parity] trait + the [SimulationBuilderBevyExt::populate_app] bridge (issue #389) drive both paths from one VerificationCase. The parity_coverage.rs meta-test enforces the superset invariant tier3_topics ⊂ bevy_parity_topics ∪ KNOWN_PARITY_GAPS, so a new tier3_* topic that lands without either a parity wrapper or a KNOWN_PARITY_GAPS exemption fails CI.

Together: Bevy ≡ Simulation ≈ JEOD. The Bevy adapter inherits Tier 3 coverage transitively for every topic that has a runner ↔ bevy parity sibling; the long tail (multi-planet scenarios, pre-recipe siblings, analytical-only tests, scenarios with pre_step ephemeris updates that need a Bevy-side SimContext impl — see issue #395) is documented in KNOWN_PARITY_GAPS for incremental closure.

Results:

Phase Run Gravity Pos Error (8h) Attitude
1 RUN_2 Point-mass 0.4 m
2 RUN_3A 4×4 harmonics + our RNP 15.6 m
2 RUN_3B 8×8 harmonics + our RNP 28.8 m
3 RUN_2 Point-mass, 6-DOF 0.32 m 4.21e-8 rad

Available JEOD sims for cross-validation:

Sim Run Duration Gravity Validates
SIM_dyncomp RUN_2 28800s Spherical Phase 1: point-mass dynamics
SIM_dyncomp RUN_7A 28800s 4x4 harmonics Phase 2: spherical harmonics
SIM_orbinit RUN_0001 instant Orbital element initialization
SIM_Euler RUN_inc 86400s GGM05C Phase 3: Euler angles
SIM_integ_test RUN_rk4 28800s Integrator accuracy
SIM_Earth_Moon RUN_clem days multi-body Phase 5: Earth-Moon dynamics

Tier 4: Regression Suite

Automated CI that tracks error budgets across all scenarios:

Scenario                         | Quantity  | Tolerance | Phase | Status
---------------------------------|----------|-----------|-------|--------
Kepler 2-body (1 orbit)          | position | 1e-6 m    |   1   | [x] 0.017 m
Energy conservation (10 orbits)  | energy   | 1e-8 rel  |   1   | [x] 3.2e-10
Period accuracy                  | period   | 1e-4 rel  |   1   | [x] 2.3e-12
ISS 24h point-mass               | altitude | 1 km      |   1   | [x] exact
JEOD trajectory (8h spherical)   | position | 5 km      |   1   | [x] 0.4 m
Orbital elements roundtrip       | position | 1e-6 m    |   1   | [x] <1e-6
Euler angle decomposition        | angles   | 1e-12 rad |   1   | [x] <1e-15
Gravity acceleration             | accel    | 1e-12 m/s²|   1   | [x] exact
LEO + J2 (24h)                   | position | 1.0 m     |   2   | [ ]
ISS full gravity (24h)           | position | 10.0 m    |   2   | [ ]
Earth-Moon 3-body (7 days)       | position | 100.0 m   |   5   | [ ]

6b. JEOD Invariant Tracking

JEOD's C++ architecture enforces ~120 invariants through MessageHandler::fail() (fatal), MessageHandler::error() (non-fatal auto-correction), structural guarantees (value members, deleted copy constructors), and flag-gated code paths. In ECS, components are optional and can be added/removed freely, so these invariants must be tracked and enforced explicitly.

Three-part system

1. CatalogJEOD_invariants.md

A table of every known JEOD invariant, organized by section (DB=DynBody, MA=Mass, GV=Gravity, etc.). Each row has:

Field Purpose
Tag Unique ID like GV.04 for cross-referencing
Invariant What the invariant requires
Enforcement How JEOD enforces it (fatal, error, structural, flag-gate)
Category When it applies (initialization, runtime, structural, consistency, ordering)
Our Status How we enforce it (enforced, partial, deferred, n/a, structural)

2. Source tags// JEOD_INV: XX.YY comments

Every enforcement site in our Rust source is tagged:

// JEOD_INV: GV.04 — degree <= source degree
assert!(
    self.degree <= data.degree,
    "Gravity field degree requested ({}) exceeds source ({})",
    self.degree, data.degree
);

Tag text must describe what our code does. When we diverge from JEOD's approach, note the divergence:

// JEOD_INV: DB.18 — F=ma via precomputed inverse_mass (matches JEOD MassPointState.inverse_mass)

3. CI coveragetests/invariant_coverage.rs

Bidirectional consistency test:

  • Every catalog entry marked enforced, partial, or structural (with a file reference) must have at least one // JEOD_INV: tag in source.
  • Every source tag must reference a valid catalog entry.
  • No duplicate IDs in the catalog.

Workflow: adding a new invariant

When reading JEOD source and encountering a MessageHandler::fail(), error(), assert, or structural guarantee not already in the catalog:

  1. Add a row to JEOD_invariants.md with the next tag in the section (e.g., DB.28).
  2. Add // JEOD_INV: DB.28 — description at the enforcement site in our code, or mark the catalog entry deferred/n/a if we don't enforce it yet.
  3. Run cargo test --test invariant_coverage to verify consistency.

Workflow: tagging an untagged enforcement site

If our code already enforces a JEOD invariant but lacks a // JEOD_INV tag:

  1. Find or create the catalog entry.
  2. Add the tag at the enforcement site.
  3. Run the coverage test.

Current state

The catalog has 287 invariants across 19 sections (AT, BA, DB, DM, DS, EP, FD, GV, IG, IN, LV, MA, OE, PF, QT, RF, SM, TM, TS). 101 are enforced, 14 partial, 49 structural (guaranteed by the type system or Bevy ECS), 26 deferred (typically to Phase-5 articulated-bodies work), and 97 n/a for the ECS architecture (rationale recorded inline). Source carries roughly 460 tag sites matching // JEOD_INV: XX.YY; the bidirectional CI test (crates/astrodyn_bevy/tests/invariant_coverage.rs) keeps the catalog and the tag sites consistent.


7. JEOD Data Ingestion

The astrodyn_verif_jeod crate

The JEOD parsers and fixtures live in astrodyn_verif_jeod (crates/astrodyn_verif_jeod/). They were originally housed in a standalone jeod_test_data crate; #387 redistributed them into astrodyn_verif_jeod (and per-domain regen binaries — extract_grav_coeffs, extract_planet_pfixposn, etc. — into the owner crates).

The verbatim NASA JEOD source mirror lives under crates/astrodyn_verif_jeod/test_data/jeod_inputs/ and is committed to the repository, so all three test tiers run on a fresh clone with no $JEOD_HOME set. The standard NASA JEOD_HOME env var (the older JEOD_PATH alias was retired in #239) is required only for fixture regeneration after a JEOD upgrade.

File Parsability Assessment

JEOD's data files fall into three categories:

Directly parseable (no modification needed)

All paths below are relative to the in-repo mirror at crates/astrodyn_verif_jeod/test_data/jeod_inputs/ (which mirrors the upstream JEOD layout one-for-one).

File Location Format Parser
Leap_Second.dat models/environment/time/data/ # comments + whitespace columns Line parser, skip #
verif_out.txt models/environment/gravity/verif/unit_tests/grav_geospherical/data/ 18 space-separated numeric fields, 40 rows sscanf-equivalent
reference_*_trans_state.py models/dynamics/body_action/verif/SIM_orbinit/Modified_data/ISS/ vehicle.expected_state.trans.position = [x, y, z] Regex on RHS arrays
iss_rate_def.py same ISS directory return [0.002, 0.006, -0.003] Regex on return literal
lvlh_rate_def.py same ISS directory return -0.06556131568278 Regex on return literal
earth_discrep.txt models/environment/spice/verif/compare/ Angle = 1.26e-07Axis = -0.92 0.38 0.07 Regex

Parseable with trick.attach_units() stripping

Orbital element, mass property, and attitude files wrap numeric data in Trick calls. A single regex handles all of them:

# What the files look like:
vehicle_reference.orb_init.inclination = trick.attach_units("degree", 51.670450765)
vehicle_reference.orb_init.semi_major_axis = trick.attach_units("km", 6732.90120152)
vehicle_reference.orb_init.eccentricity = 0.00129073350
vehicle_reference.mass_init.properties.mass = 100000.0
vehicle_reference.mass_init.properties.inertia[0] = [7e12, 0.0, 0.0]
vehicle_reference.att_init.orientation.euler_angles = trick.attach_units("degree", [77.59, -30.60, -46.10])

The parser extracts (dotted_key, unit_or_none, value) tuples:

pub struct JeodPyValue {
    pub key: String,              // "orb_init.inclination"
    pub unit: Option<String>,     // Some("degree")
    pub value: JeodValue,         // Scalar(51.670450765) or Vec([...])
}

pub enum JeodValue {
    Scalar(f64),
    Vec(Vec<f64>),
    Str(String),
    Bool(bool),
}

Files covered by this parser:

Pattern Count Content
trans_Orbit_*_body_set*.py ~20 Orbital elements (a, e, i, RAAN, omega, tp)
mass.py per vehicle Mass, inertia tensor, center of mass, attach points
att_RotState_*.py ~10 Euler angles, quaternions
rate_RotState_*.py (some) ~5 Angular velocity

Unit conversions are trivial — the parser applies them automatically:

JEOD Unit Conversion
"degree" multiply by PI/180
"km" multiply by 1000
"s" no conversion

Not parseable (orchestration logic)

~30% of files contain exec() chains, eval(), or complex control flow. These are orchestration files that wire together the data files above — they don't contain unique data.

File Why unparseable
single_vehicle_run.py exec() chains, eval("set_" + name + "_mass(...)")
earth.py Method calls: set_date_and_time(2005, 7, 28, 10, 9, 59.0)
system.py Pure Trick API calls
run_files.py Dynamic file loading with exec()

These don't need to be parsed. The data they reference lives in the parseable files. The scenario configuration (start date, integration method, stop time) can be hardcoded in Rust test functions since there are a finite number of scenarios.

C++ Unit Test Extraction

Of JEOD's 262 C++ unit test files, only 2 contain extractable numerical test vectors:

Source Test Cases Content
euler_derived_state_ut.cc 6 Rotation matrix → expected Euler angles
verif_out.txt 40 Position → expected gravity acceleration, gradient, potential

The rest are structural tests (empty bodies, mock verification, boolean checks) with no hardcoded numerical assertions. Not worth parsing.

For euler_derived_state_ut.cc, values can be extracted with:

regex: double\s+(\w+)\[3\]\s*=\s*\{([^}]+)\}

Complete Parser Inventory

The parser surface lives in astrodyn_verif_jeod. By default each helper reads from the in-repo mirror under crates/astrodyn_verif_jeod/test_data/jeod_inputs/; callers can override with an explicit path or $JEOD_HOME for fixture regeneration.

// crates/astrodyn_verif_jeod/src/lib.rs (and submodules)

/// Parse JEOD Python data files with optional trick.attach_units() stripping.
/// Works for orbital elements, mass properties, attitude, rate definitions.
pub fn parse_py_data(path: &Path) -> Vec<JeodPyValue>;

/// Parse reference state vectors from reference_*_trans_state.py files.
pub fn reference_states(vehicle: &str, frame: &str) -> TranslationalState;

/// Parse orbital initialization data from trans_Orbit_*.py files.
/// Returns orbital elements with units already converted (deg->rad, km->m).
pub fn orbital_init_data(vehicle: &str, init_name: &str) -> OrbitalInitData;

/// Parse gravity verification test cases from verif_out.txt.
/// Returns 40 test cases with (position, degree, order, expected accel/grad/pot).
pub fn gravity_test_cases() -> Vec<GravityTestCase>;

/// Parse Leap_Second.dat into a leap second table.
pub fn leap_second_table() -> Vec<LeapSecondEntry>;

/// Parse Euler angle test vectors from euler_derived_state_ut.cc.
pub fn euler_test_cases() -> Vec<EulerTestCase>;

8. Implementation Phases

Phase 1: Foundation

Goal: A dot orbiting a point mass in Bevy's FixedUpdate.

Core crates: astrodyn_math, astrodyn_dynamics (minimal), astrodyn_gravity (point mass only), astrodyn_frames (minimal)

Bevy glue: astrodyn_bevy (crates/astrodyn_bevy/)

Deliver:

  • DVec3/DQuat/DMat3 math operations (using glam f64 types)
  • Orbital element ↔ Cartesian conversions with Kepler equation solver
  • TranslationalState, MassProperties, TotalForce types (core) + components (Bevy)
  • RK4 integrator as pure function + Bevy integration system
  • Point-mass gravity computation + Bevy gravity system
  • Minimal reference frame hierarchy (inertial root + one body frame)
  • OrbitalElements derived state
  • batch_propagation.rs example using astrodyn_* crates with no Bevy

Verify with:

  • Kepler orbit conserves energy and angular momentum to machine precision
  • Orbital period matches analytical T = 2*pi*sqrt(a^3/mu)
  • Orbital elements round-trip test

Phase 2: Realistic Environment

Goal: J2+ spherical harmonics gravity, time system, basic ephemeris.

Core crates: astrodyn_gravity (spherical harmonics), astrodyn_time, astrodyn_ephemeris, astrodyn_planet, astrodyn_verif_jeod

Bevy glue: astrodyn_bevy (crates/astrodyn_bevy/)

Deliver:

  • Full spherical harmonics gravity engine (port of spherical_harmonics_calc_nonspherical.cc)
  • Earth GGM05C, Moon GRAIL150 coefficient data
  • TAI, UTC, UT1, TDB, TT time scales with converters
  • Leap second table (from JEOD data)
  • DE421 binary ephemeris reader
  • Planet position updates from ephemeris
  • Earth, Moon, Sun planet presets with shapes and gravity

Verify with:

  • Tier 2 gravity tests: 40 test vectors from verif_out.txt
  • LEO + J2 nodal regression rate matches analytical prediction
  • Time conversion tests against known epochs

Phase 3: Full Dynamics

Goal: 6-DOF dynamics with rotational state and multi-body attachment.

Core crates: astrodyn_dynamics (full), astrodyn_frames (full), astrodyn_math (derived states)

Bevy glue: astrodyn_bevy (crates/astrodyn_bevy/)

Deliver:

  • Rotational integration (Lie group technique for quaternion propagation)
  • Force and torque collection system
  • Mass tree with composite property updates on attach/detach
  • Full reference frame propagation (structure → composite → core body)
  • Body initialization actions (orbital elements, LVLH, NED)
  • Euler angles, LVLH, NED, planet-fixed derived states

Verify with:

  • Tier 2 ISS reference state tests
  • Tier 2 Euler angle decomposition tests (6 vectors from euler_derived_state_ut.cc)
  • Tier 3 cross-validation against JEOD's SIM_dyncomp (6-DOF attitude: 4.21e-8 rad/8h)

Phase 3a: Cross-Validation Closure

Goal: Tier 2/3 cross-validation for every Phase 3 capability. No new physics.

Deliver:

  • Wire planet-fixed frame into gravity pipeline (fix 15–29 m RNP residual)
  • Cross-validate structure/core_body frame propagation against existing CSV data
  • Docker sims: SIM_OrbElem, SIM_LVLH, SIM_NED, SIM_SolarBeta, SIM_Euler, SIM_orbinit
  • Trajectory-level validation of orbital elements, LVLH, geodetic, NED, solar beta, Euler angles
  • Bevy system integration test (wiring parity)

Verify with:

  • Tier 3 spherical harmonics with correct RNP (target < 5 m, down from 15.6 m)
  • Tier 3 frame propagation (structure/core match JEOD CSV)
  • Tier 3 derived states (each validated against its own JEOD sim)
  • Tier 2 body initialization (ISS reference state < 1 m)

Phase 4: Interactions

Goal: Aerodynamic drag, radiation pressure, gravity gradient torque.

Core crates: astrodyn_atmosphere, astrodyn_interactions

Bevy glue: astrodyn_bevy (crates/astrodyn_bevy/)

Deliver:

  • MET atmosphere model (density/temperature/pressure tables)
  • Aerodynamic drag system (ballistic coefficient and flat-plate models)
  • Solar radiation pressure system
  • Gravity gradient torque system
  • Solar beta angle derived state

Verify with:

  • Tier 1: LEO with drag orbital decay rate matches expected behavior
  • Tier 1: SRP magnitude matches analytical P = L_sun / (4*pi*r^2*c)
  • Tier 1: Gravity torque on known inertia tensor matches analytical gradient torque
  • Tier 2: MET atmosphere density at 400 km matches JEOD tables to < 5%
  • Tier 3: Gravity torque trajectory (RUN_9A/9B) attitude < 0.01 rad/8h
  • Tier 3: Drag trajectory (SIM_dyncomp with drag) position < 100 m/24h
  • Tier 3: SRP trajectory position < 10 m/24h
  • Tier 3: Eclipse entry/exit times match JEOD to < 10 s

Phase 5: High-Fidelity Parity

Goal: Feature parity with JEOD's verified capabilities.

Crates: All — advanced features added to existing crates

Deliver:

  • Advanced integrators: Gauss-Jackson, LSODE, RKF45 (adaptive step)
  • Solid body tides in gravity model
  • Full RNP model for Earth rotation (precession, nutation, polar motion)
  • SPICE integration (via FFI to cspice, or native Rust reader)
  • Contact dynamics
  • Full regression suite against JEOD (Tier 4)
  • Multi-body scenarios: Apollo trans-lunar, Earth-Moon, Mars

Verify with (Tier 3 required for each new physics):

  • Tier 3 LEO 24h high-fidelity gravity (GGM05C deg 20 + polar motion) < 10 m
  • Tier 3 LEO with drag (MET + ballistic drag) < 100 m/24h
  • Tier 3 Earth-Moon multi-body (Sun/Moon differential accel) < 100 m/7d
  • Tier 3 Mars orbit (MRO110B2 gravity) < 100 m/7d
  • Tier 3 Gauss-Jackson trajectory < 1 m/24h
  • Tier 3 RKF45 trajectory < 10 m/24h with adaptive stepping
  • Tier 3 polar motion: Earth-fixed frame < 0.1 arcsecond/24h
  • Tier 3 solid tides: ON vs OFF position delta matches JEOD delta to < 10%
  • Tier 4 automated regression suite with error budget tracking

Phase 6: Comprehensive JEOD Parity Validation

Goal: Full-breadth cross-validation against every major JEOD verification sim category. No new physics — validates Phase 5 capabilities across broader parameter spaces, edge cases, and specialized scenarios.

Crates: All — new reference data (Docker) and Tier 3 tests added.

Deliver:

  • 9 new reference-data sims generated via the Docker pipeline: SIM_Relative, SIM_Planetary, SIM_LIGHT_CIR, SIM_5_all_inclusive, SIM_MET, SIM_7_time_reversal, SIM_orb_elem, SIM_mercury, SIM_LvlhRelative (derived- state edge-case data was reused from Phase 4b)
  • Full tier3_simulation_* and tier3_bevy_* coverage for each category (69/69 simulation/Bevy pairs, 100%)
  • Feature-parity audit: 15/17 astrodyn public functions have a Bevy system counterpart; the 2 by-design exceptions are compute_relative_state/compute_lvlh_relative_state (on-demand utilities, not per-entity systems — covered in parity tests) and integrate_body_coupled (future-only coupled SRP thermal path)

Verify with (Tier 3):

  • Earth-Moon multi-body: 0.93 m over 7 days (< 1 m target)
  • Mars orbit: 3.8 m over 3 hours (< 100 m target)
  • Relative dynamics: 8.0e-14 m over 100 s (< 1e-6 m target)
  • Planetary derived states (LEO/GEO/polar): 1.0 m over 24 h
  • Earth lighting: 1e-10 shadow geometry match across 10 geometries
  • Time scales: TAI/TT/TDB < 2e-6 s, GMST < 1e-4 s over 2 h
  • MET atmosphere: machine precision after TJT epoch fix
  • Comprehensive orbital elements: 7 orbit families to < 1e-6 per element
  • Mercury GR perihelion: 42.97 arcsec/century (~0.02% vs JEOD's ~43)
  • LVLH-relative: 2.1e-14 m (< 1e-6 m target)

Phase 7: Polish, Examples, and Hardening

Goal: Close remaining open issues, harden the test suite, and document irreducible numerical differences vs JEOD. No new physics beyond one small function port that surfaced during the audit.

Crates: Docs (docs/), tests (crates/*/tests/, tests/), and a new astrodyn_dynamics::init_from_time_periapsis function matching JEOD's dyn_body_init_orbit.cc:295.

Deliver:

  • Numerical-Differences: five catalogued irreducible differences (GCC vs LLVM FP, DE421 Anise vs cspice, SRP thermal residual, host libm on drag velocity schedule, geodetic longitude at poles)
  • Earth-Lighting-Validation: Tier 3 gap rationale and enumeration of the 11 tier3_bevy_earth_lighting_* static-geometry tests (no propagating JEOD sim ships in JEOD 5.4, so full trajectory cross-validation is not possible upstream)
  • Test-suite audit: 41 fixes across 30 files; hardcoded planet constants (mu, R_earth, rotation rate, shadow radius) replaced with astrodyn::{EARTH, SUN, MOON} presets; inline orbit-init math replaced with the new init_from_time_periapsis port
  • Showcase examples kept with the runner harness they exercise: apollo.rs, batch_propagation.rs, leo_drag.rs under crates/astrodyn_runner/examples/; the longer earth_moon.rs / mars_orbit.rs cross-validation examples under crates/astrodyn_verif_jeod/examples/. Bevy mission examples (kepler_orbit.rs, typed_mission.rs) live under crates/astrodyn_bevy/examples/.

Verify with:

  • cargo nextest run --workspace: all tests pass (including the slow earth_moon run on main-push CI)
  • cargo clippy --workspace --tests -- -D warnings: clean
  • Phase 7 exit-gate issues closed: #6, #49, #50, #60 (#13 already closed); DynManager multi-integrable-object work split into #114 as follow-up

Phase 8: Type-System Refactor (#101)

Goal: Shift correctness guarantees from runtime/documentation into the type system. Mission-crate code never sees DVec3, PhantomData, or uom::si::f64::Length::new::<kilometer> — it composes typed building blocks and gets compiler errors in physics language when conventions are violated.

Context: The original Phase 1–7 plan closed in April 2026. Phase 8 is the type-system refactor (issue #101), planned and shipped April 2026 across 12 sub-issues #102#113. It is additive to the original plan: physics is unchanged (Tier 3 baselines frozen at Phase 0 and held to within 1e-12 · magnitude through every refactor-only phase).

Architectural outcome — three-layer facade:

┌──────────────────────────────────────────────────────────┐
│ Facade  (astrodyn_bevy::prelude, astrodyn::recipes)          │
│   F64Ext: 400.0.km(), 51.6.deg(), 420_000.0.kg()         │
│   Concrete Component wrappers (no visible generics)      │
│   Custom #[diagnostic::on_unimplemented] messages        │
├──────────────────────────────────────────────────────────┤
│ Typed astrodyn_* siblings                                     │
│   Position<F: Frame>, SecondsSince<S: TimeScale>,        │
│   Quat<L, T>, NormalizedQuat, FrameTransform<From, To>   │
├──────────────────────────────────────────────────────────┤
│ astrodyn_quantities (bottom of dep graph)                    │
│   uom re-exports, Qty3<D, F>, phantom frames/scales,     │
│   NormalizedQuat witness, F64Ext / Vec3Ext / Array3Ext   │
└──────────────────────────────────────────────────────────┘

Phase summary table:

Phase Issue PR Scope
0 #102 #126 astrodyn_quantities crate, F64Ext, custom diagnostics, baselines.json capture
1 #103 #130#134 Migrate leaf crates: astrodyn_time, astrodyn_planet, astrodyn_atmosphere, astrodyn_ephemeris, jeod_test_data
2 #104 #129, #135#140 Migrate astrodyn_math (commitment-point gate)
3 #105 #141 Migrate astrodyn_frames + astrodyn_dynamics typed siblings
4 #106 #142 Migrate astrodyn_gravity + astrodyn_interactions typed siblings
5 #107 #143 Migrate astrodyn public API + typestate VehicleBuilder
6 #108 #145 Migrate astrodyn_runner + introduce astrodyn::recipes (building_blocks/scenarios/verification)
7 #109 #147 Tier 3 wave A: VerificationCase one-liners + ExtrasComparator dispatch
8 #110 #146 Tier 3 wave B: archetype-B classification + recipes::helpers/ infrastructure
9 #111 #148 Migrate root package + Bevy integration: typed Components + 5/20 systems
10 #112 #149 Purge deprecated APIs + complete typed-sibling coverage + retire sim_test_helpers
11 #113 (this PR) Documentation, CI polish, compile-time budget enforcement

Acceptance: mission code reads like physics — let altitude = 400.0.km() — and the compiler rejects frame mismatches, scalar-vs-vector quaternion confusion, and unit-dimensional errors at compile time. See Type-System for the contributor primer and crates/astrodyn_bevy/examples/typed_mission.rs for the canonical worked example.

Post-Phase-8 ongoing work

The original Phase 1–7 plan and Phase 8 type-system refactor closed in April 2026. Subsequent work is tracked as individual GitHub issues rather than as numbered phases. Notable workstreams that have landed since:

  • Workspace rebrand (#387, #392) — the bevy_jeodastrodyn / astrodyn_* rename plus JeodPlugin / JeodSetAstrodynPlugin / AstrodynSet. Fixtures and parsers were redistributed (the standalone jeod_test_data crate retired in favor of astrodyn_verif_jeod).
  • Schedule simplification (#362) — JEOD's 9 init/update steps collapsed to 7 AstrodynSet variants; multi-stage integrators run as an inner loop inside AstrodynSet::Integration.
  • Frame-tree ECS-native shape (#268, PRs #260 / #263 / #280) — the arena-shaped FrameTreeR Resource and *FrameIdC components were replaced by ECS frame entities (FrameEntityC, PfixFrameEntityC, RootFrameEntityR) with state on FrameTransC/FrameRotC/FrameAngVelC. Cross-frame queries go through the RelativeFrameState / FrameOrigin SystemParams. See Frame-Tree-ECS-Native.
  • Articulated bodies (frame-attached integration #198 / #206; cross-integ-frame attach #299/#312/#314/#319/#350; mass-tree dual-write #308; per-body kernels #358/#363; vehicle phantoms on FlatPlate/AttachEvent/LvlhRelativeState #332).
  • Single-API-surface scoping (#360) — clarified that the production-path single-API rule applies to astrodyn_bevy and downstream mission crates only; astrodyn_runner is exempt because it owns its own state container.
  • Bevy parity infrastructure (#389, #395) — VerificationCaseParityExt::run_and_assert_parity + SimulationBuilderBevyExt::populate_app drive both astrodyn_runner and a Bevy App from one VerificationCase; parity_coverage.rs enforces tier3_topics ⊂ bevy_parity_topics ∪ KNOWN_PARITY_GAPS. Follow-on work added 9 parity wrappers (#414) and migrated the deferred parity test set to the recipe trait (#406, #415); the dyncomp_run6 wrapper was reinstated (#416).
  • Typed gateway (#388, #397) — drove typed-quantity bypass constructors out of the astrodyn gateway and discontinued untyped construction across the public surface so every mission-facing builder path is uniformly typed. Round-trip property tests now cover every typed sibling (#398 / #410).
  • Single-API-surface enforcement (#390 / #404) — astrodyn_runner and astrodyn_bevy declare astrodyn as their sole physics dep; the scripts/check_no_bypass_deps.sh lint enforces the rule at CI time.
  • Self-contained data assets (#144 / #403) — gravity coefficient binaries and DE/PCK ephemeris kernels are bundled into astrodyn_gravity / astrodyn_ephemeris via include_bytes!, so a fresh clone needs no external download to run any test tier. Spherical harmonics recipes were promoted to the mission API as part of this work.
  • Mass-tree wildcard typing (#396 / #417), typed Simulation::body_mass (#408 / #409) — the typed-quantity surface continues to expand into runner internals.

Deferred items (tracked separately):

Topic Tracker
Session types for integrator stage ordering (design spike) #150
Capability tokens vs Bevy SystemSets (design spike) #151
Branded Simulation<'sim> lifetimes (design spike) #152
Docker CSV regeneration pipeline modernization #153
Bevy Reflect impls for uom-backed types #154
Type-erase FrameTransform-shaped Bevy Components #155
pre_step hook on VerificationCaseExt #156
EvaluationCase recipe shape for non-propagating tests #157
Const-generic dimensional analysis obviated by uom adoption (Phase 0)
Spherical harmonics evaluation performance tracked under #65

9. Key Architectural Decisions

Decision Choice Rationale
ECS portability Three-layer split: astrodyn_* (pure physics) + astrodyn (orchestration, workspace root) + astrodyn_bevy (thin Bevy glue) Physics algorithms in astrodyn_* are reusable anywhere. Pipeline orchestration in astrodyn codifies stage ordering, gravity accumulation, force collection, and integration routing without ECS dependency. astrodyn_bevy's systems delegate to astrodyn per-body functions. A non-Bevy ECS writes its own thin glue calling the same astrodyn functions, guaranteed bit-identical by bevy_parity_* tests. The parallel non-Bevy consumer astrodyn_runner (test harness) is allowed to depend directly on physics crates because it owns its own state container.
Floating-point precision f64 everywhere via custom components (not Bevy's Transform) Orbital mechanics requires ~15 significant digits. f32 loses km-scale accuracy at Earth-orbit distances.
Math library glam with f64 features (DVec3, DQuat, DMat3) + nalgebra for NxN matrices glam provides f64 types with no Bevy dependency (it's a standalone crate). nalgebra is better for variable-size matrices needed by spherical harmonics coefficient arrays. Both work in astrodyn_* crates.
Reference frame tree astrodyn_frames provides an arena-based tree; astrodyn maps it to Bevy's Parent/Children Core tree is portable. Bevy layer adds ECS hierarchy for efficient queries. Other ECS layers can use their own hierarchy mechanism.
Integration loop Custom inner loop within FixedUpdate with stage-tracking resource Multi-stage integrators (RK4 = 4 stages) need multiple force evaluations per timestep. An inner loop keeps this self-contained.
Gravity coefficient data Binary asset files loaded at runtime via Bevy's AssetServer (or direct file I/O in non-Bevy contexts) Keeps multi-MB coefficient arrays out of the compiled binary. Enables runtime model swapping (e.g., switch from GGM05C to GEMT1). astrodyn_gravity provides a load_from_file() function independent of Bevy's asset system.
Ephemeris data Standard JPL DE421 binary files Well-documented format. Existing parsers available. Same files JEOD uses. astrodyn_ephemeris reads them directly; astrodyn wraps via AssetServer.
Plugin granularity Separate astrodyn_* core crates per domain + one unified astrodyn_bevy glue crate Core crates are fine-grained for modularity and parallel compilation. Bevy glue is unified in a single AstrodynPlugin — selective behavior comes from which components are spawned, not plugin selection. Non-Bevy users depend only on astrodyn_* / astrodyn crates.
Quaternion convention JEOD's left-quaternion, scalar-first [q0, q1, q2, q3] Must match JEOD exactly for verification. Document any conversions needed at the glam boundary (glam uses [x, y, z, w] ordering).
Testing approach #[cfg(test)] unit tests + integration test binaries + criterion benchmarks Core physics tested as pure functions (no Bevy App needed). Bevy integration tested separately. Matches JEOD's tiered verification.
JEOD data access Verbatim NASA JEOD source mirror committed under crates/astrodyn_verif_jeod/test_data/jeod_inputs/; parsers in astrodyn_verif_jeod; $JEOD_HOME env var only required for fixture regeneration All three test tiers run on a fresh clone with no $JEOD_HOME set. Tests assert on missing data rather than skipping.

10. Risks and Mitigations

Risk Impact Mitigation
Numerical precision drift vs. JEOD Tests fail despite correct implementation Use per-component tolerances at 5% above observed error. Document known precision differences between GCC and Rust's LLVM backend. Tolerances are literal values in test source — tighten after each code improvement.
Spherical harmonics performance Degree-2190 GGM05C is computationally expensive Implement with cache-friendly memory layout. Benchmark early. Provide degree/order truncation as a runtime option. Consider SIMD for inner loops.
JEOD verification data requires Trick Cannot produce Tier 3 baseline trajectories without Trick installed Start with Tier 1 (analytical) and Tier 2 (reference values) — these need no Trick. Generate Tier 3 baselines once, store as CSV.
Bevy's FixedUpdate assumes fixed timestep Adaptive integrators (RKF45, LSODE) need variable dt Use inner sub-stepping loop within FixedUpdate. The outer schedule provides a maximum dt; the integrator may take smaller steps internally.
Quaternion convention mismatch Subtle rotation bugs that pass simple tests but fail complex scenarios Document JEOD's convention (scalar-first, left-transform) explicitly. Write conversion functions at the glam boundary. Test with non-trivial rotations (not just identity or 90-degree).
Mass tree / attachment complexity Rigid body attachment/detachment is intricate and error-prone Implement incrementally: single body first (Phase 1-2), then parent-child attachment (Phase 3), then multi-level trees (Phase 5). Test each level before proceeding.
Scope creep JEOD has 714 source files; reimplementing everything is years of work Strict phasing. Each phase is independently useful and verifiable. Phase 1 alone enables two-body mission analysis. Resist adding features ahead of schedule.
glam vs nalgebra friction Two math libraries with different conventions, conversion overhead Standardize on glam for 3-vectors and quaternions (hot path). Use nalgebra only for NxN matrices in gravity coefficients and similar. Define clear boundary types.
Glue layer overhead Extra indirection between ECS and physics astrodyn_bevy is intentionally a thin glue crate (component derives, systems that delegate to astrodyn, plugin registration). The physics code only exists once. The overhead pays for itself in testability and portability.
Bevy breaking changes Bevy's rapid release cycle breaks the glue layer Only astrodyn_bevy needs updating. Physics code in astrodyn_* and orchestration in astrodyn are untouched. Pin Bevy version in workspace; upgrade the glue crate when a new Bevy release lands.

Clone this wiki locally