Skip to content

Bevy Adapter

Test User edited this page May 30, 2026 · 2 revisions

Bevy Adapter (astrodyn_bevy)

astrodyn_bevy is one host for the engine-agnostic astrodyn pipeline — the reference Bevy ECS driver. It owns no physics: every system queries components and delegates to an astrodyn function. This page collects the Bevy/ECS-specific material (the JEOD→ECS mapping, component wrappers, the schedule, and plugin composition) that used to be threaded through Strategy. The pipeline those things drive is described engine-neutrally in Strategy §2–§5; the standalone non-Bevy driver is astrodyn_runner.

If you are looking for the engine-neutral architecture — the pipeline stages, the core astrodyn_* types, the three-layer rule — read Strategy. Everything below is how the Bevy adapter realizes that architecture; a different host wires the same stages differently.

Why Bevy for the reference adapter?

The physics does not depend on this choice (astrodyn_runner drives the same pipeline with a plain arena and an explicit step loop). Bevy was chosen for the reference runtime because, relative to JEOD's Trick host, it offers:

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

Mapping the pipeline onto ECS

JEOD is built on deep OOP hierarchies with manager god-objects. The engine-neutral half of the translation — god-objects and manager singletons becoming focused pipeline stages and per-body functions — is described in Strategy §2. This section is the ECS half: how the Bevy adapter realizes those stages on ECS primitives.

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

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. ECS: an entity gets the components it needs, no inheritance. A "DynBody" is just an entity with TranslationalState + RotationalState + MassProperties + etc.

Tree structures → Bevy's entity hierarchy. JEOD's RefFrame tree and MassBody tree use raw pointers. Bevy's built-in Parent/Children give the same tree structure with safe entity references.

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

Bevy components

Component wrappers live in crates/astrodyn_bevy/src/components/ (split across state.rs, gravity.rs, mass_tree.rs, frame_tree.rs, integration.rs, …). Each wraps a typed astrodyn quantity with #[derive(Component)] and a C suffix. The frame/planet phantom rides inside the wrapped typed sibling, 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 TranslationalStateTyped<PlanetInertial<P>>);

#[derive(Component, Deref, DerefMut)]
pub struct RotationalStateC(pub RotationalStateTyped<SelfRef>);

#[derive(Component, Deref, DerefMut)]
pub struct MassPropertiesC(pub MassPropertiesTyped<SelfRef>);

#[derive(Component, Deref, DerefMut)]
pub struct GravityAccelerationC(pub GravityAccelerationTyped<RootInertial>);

#[derive(Component, Deref, DerefMut)]
pub struct TotalForceC(pub TotalForceTyped<SelfRef, RootInertial>);

#[derive(Component, Deref, DerefMut)]
pub struct DynamicsConfigC(pub DynamicsConfig);            // config, not a typed quantity

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

Per-body integrator selection and multistep state are themselves components: IntegratorTypeC(astrodyn::IntegratorType) picks the method (RK4, RKF45, Gauss-Jackson, ABM4, LSODE), and GaussJacksonStateC / LsodeStateC carry the persistent multistep state across ticks.

Vehicle bodies are typically spawned through astrodyn::VehicleConfig's spawn_bevy extension, which inserts the right *C components for the chosen DOF and integrator; planet / gravity-source entities use the #[derive(Bundle)] helpers PlanetBundle<P> and SunBundle. The typed-quantity-first, wrap-as-component-second boundary is the "core vs. Bevy split" described in Strategy §3.1.

Execution schedule

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.

The adapter realizes the pipeline's canonical stage order (the engine-neutral PIPELINE_ORDER, see Strategy §4) as 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 (RK4 has 4 sub-steps per timestep, each requiring a fresh force evaluation; Gauss-Jackson and LSODE are multi-step). In the adapter:

  • the integrator is selected per body by the IntegratorTypeC(astrodyn::IntegratorType) component;
  • the timestep is the bit-exact IntegrationDtR(f64) resource — the single source of dt for the whole pipeline;
  • multistep methods keep their persistent state in GaussJacksonStateC / LsodeStateC components across ticks.

integration_system runs the full multi-stage loop internally — for each sub-step it re-evaluates gravity and forces, computes derivatives, and advances the stage — so the multi-stage logic stays inside one system rather than spreading across the schedule.

Key system signatures

Bevy systems are thin wrappers: each is generic over the planet phantom <P: Planet>, queries the *C components, and delegates to a gateway astrodyn::* function — never to an astrodyn_* physics crate directly (the three-layer rule). gravity_computation_system builds a per-body input iterator plus a source resolver and hands both to astrodyn::run_gravity_stage; integration_system routes 6-DOF/3-DOF integration through astrodyn::integrate_body (and integrate_body_coupled for kinematic chains).

A simplified integration_system — incidental frame-origin plumbing and the detached-subtree / kinematic-child filters are elided; see crates/astrodyn_bevy/src/systems/integration.rs for the full signature:

pub fn integration_system<P: Planet>(
    // frame-origin / root-frame plumbing …
    mut bodies: Query<(
        Entity,
        &DynamicsConfigC,
        &mut TranslationalStateC<P>,
        Option<&mut RotationalStateC>,
        Option<&MassPropertiesC>,
        &GravityControlsC,
        &mut TotalForceC,
        Option<&IntegratorTypeC>,
        // … persistent multistep state (Gauss-Jackson / LSODE) …
    )>,
    dt: Res<IntegrationDtR>,
) {
    // For each body: re-evaluate forces per RK sub-step, then
    // `astrodyn::integrate_body(…)` routes the method and DOF.
}

The system owns no algorithm — only the ECS plumbing around the gateway call — which keeps the physics testable without standing up a Bevy App.

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.

See also

  • Strategy — engine-neutral architecture: pipeline stages, core types, three-layer rule, verification.
  • Integration-Groups — Bevy schedule ↔ JEOD JeodIntegrationGroup mapping.
  • Type-System — the typed quantities / phantom tags the component wrappers carry.
  • Dependency-Graph — where astrodyn_bevy and astrodyn_runner sit relative to astrodyn.

Clone this wiki locally