-
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, 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.
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 (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 ofdtfor the whole pipeline; - multistep methods keep their persistent state in
GaussJacksonStateC/LsodeStateCcomponents 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.
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.
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.