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, 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()),
));

The plain-Rust-struct-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 (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 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,
        );
    }
}

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