-
Notifications
You must be signed in to change notification settings - Fork 0
Bevy Adapter
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.
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 |
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).
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.
See also: Integration-Groups — how the Bevy schedule maps to JEOD's
JeodIntegrationGroupconcept; 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
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),
));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.
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,
);
}
}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.
- Strategy — engine-neutral architecture: pipeline stages, core types, three-layer rule, verification.
-
Integration-Groups — Bevy schedule ↔ JEOD
JeodIntegrationGroupmapping. - Type-System — the typed quantities / phantom tags the component wrappers carry.
-
Dependency-Graph — where
astrodyn_bevyandastrodyn_runnersit relative toastrodyn.