A pure-Rust orbital-dynamics simulation framework — a port of the physics in NASA JEOD v5.4 (the JSC Engineering Orbital Dynamics package). It models what a high-fidelity space-mission propagator needs: spherical-harmonics gravity, Earth rotation (RNP precession/nutation), multiple time scales, reference-frame trees, atmosphere and drag, solar radiation pressure, third-body and gravity-gradient effects, and multi-body rigid-body dynamics with a family of integrators.
The framework is engine-agnostic: the physics is plain Rust with no game
engine, ECS, or async runtime baked in, so it can be driven from a batch
propagator, a custom integrator loop, an ECS, or any other host. It is split
into a stack of small, single-responsibility astrodyn_* physics crates (each
independently publishable), an orchestration gateway crate (astrodyn,
this crate) that any host depends on, and optional reference consumers. See
The astrodyn workspace below for the full crate map.
astrodyn is the orchestration gateway: it composes the astrodyn_* physics
crates into pipeline stages and re-exports their types, so a host needs only
astrodyn to reach the entire physics surface. It also provides the typestate
VehicleBuilder and the recipes module of mission presets. The per-stage
pipeline functions are borrow-based and storage-agnostic — the host owns state
and decides how to store it. No game-engine or runtime dependency.
Status: pre-1.0. Tier 3 cross-validated against JEOD Trick simulations (see the Tier3-Regeneration wiki page). API may change before 1.0.
The framework is organized into architectural layers. Every consumer of
physics reads through astrodyn and only astrodyn; the astrodyn_* crates
below it are plain Rust and can also be used à la carte.
Foundations
| Crate | Responsibility |
|---|---|
astrodyn_quantities |
Phantom-tagged typed quantities (Position<F>, Velocity<F>, Quat<L,T>, …) — frame/unit safety at API boundaries |
astrodyn_math |
Quaternion, Euler, geodetic, orbital-element, and LVLH math kernels |
astrodyn_time |
Time scales (TAI/UTC/UT1/TDB/TT/GMST) and converters |
Environment & physics
| Crate | Responsibility |
|---|---|
astrodyn_frames |
Reference-frame tree and Earth rotation (RNP, nutation, precession) |
astrodyn_planet |
Planet definitions and presets (Earth, Moon, Sun, Mars) |
astrodyn_ephemeris |
DE4xx (SPICE) binary ephemeris reader |
astrodyn_gravity |
Spherical-harmonics gravity (Gottlieb), tides, and third-body |
astrodyn_atmosphere |
Atmospheric density models (exponential, MET) |
astrodyn_interactions |
Aerodynamic drag, SRP, gravity-gradient torque, shadow, and contact |
astrodyn_dynamics |
Rigid-body dynamics, integrators (RK4, RKF45, GJ, ABM4), mass tree, body initialization |
Orchestration & consumers
| Crate | Responsibility |
|---|---|
astrodyn |
This crate. Pipeline orchestration, VehicleBuilder, recipes — the single API surface every host depends on |
astrodyn_runner |
Standalone arena-state harness that owns all state and drives the pipeline — batch propagation and the Tier 3 test harness |
astrodyn_bevy |
Optional adapter for hosts built on the Bevy ECS: component derives, systems, plugin registration |
astrodyn_runner and astrodyn_bevy are the two reference consumers shipped
in this workspace; a host with different storage or scheduling needs depends on
astrodyn directly and supplies its own state container.
Verification (not published to crates.io). The astrodyn_verif_jeod,
astrodyn_verif_jeod_fixtures, astrodyn_verif_nesc, and
astrodyn_verif_parity crates hold the JEOD Tier 3 cross-validation rigs, the
NESC GN&C Lunar Check Cases track, and the parity tests asserting the two
reference consumers stay bit-identical. They live in the
workspace but stay
in-tree as publish = false.
Many users want one of the reference consumer crates rather than this orchestration layer directly:
astrodyn_runnerfor plain-Rust batch propagation and Tier 3 tests,astrodyn_bevyif your host is built on the Bevy ECS.
If you are wiring the pipeline into your own host (a batch tool, an integrator
loop, a different ECS, …), depend on astrodyn directly:
[dependencies]
astrodyn = "0.1"use astrodyn::{
recipes::{earth, orbital_elements, vehicle},
F64Ext, GravityControl, VehicleBuilder,
};
let mu = earth::point_mass().source.mu.m3_per_s2();
let cfg = VehicleBuilder::new()
.from_orbital_elements(orbital_elements::iss(), mu)
.three_dof_point_mass(vehicle::iss_mass())
.rk4()
.gravity(GravityControl::new_spherical(0_usize, false))
.build();
// `cfg` is a `VehicleConfig` ready to hand to any host — e.g.
// `astrodyn_runner::Simulation::add_vehicle(...)` for batch propagation,
// or `astrodyn_bevy::spawn_bevy::<Earth>(...)` for a Bevy host.
# let _ = cfg;The typestate VehicleBuilder rejects misuse at compile time
(no integrator chosen, no state set, mismatched coordinate frames).
host (astrodyn_runner, astrodyn_bevy, or your own)
↓
astrodyn ← this crate (the single gateway, no engine dependency)
↓
astrodyn_dynamics, astrodyn_gravity, astrodyn_time, astrodyn_frames,
astrodyn_atmosphere, astrodyn_interactions, astrodyn_ephemeris,
astrodyn_planet, astrodyn_math, astrodyn_quantities
astrodyn (this crate) sits at the workspace root and is the only physics
dependency a host needs. astrodyn_runner is the standalone arena-state host
used by the Tier 3 harness; astrodyn_bevy is the optional Bevy-ECS adapter.
Any host shares the same API surface. See the
Strategy wiki page
for the layered-architecture rules.
VehicleBuilder— typestate builder that refuses.build()until state, mass, and integrator are set.recipes::*—earth,moon,mars,sun,orbital_elements,vehicle,scenarios. Mission-facing only; the JEOD-source-backed Tier 3 verification scaffolding lives inastrodyn_verif_jeod.- Per-stage pipeline functions (
accumulate_gravity,validate_body, …), one per stage of the per-step pipeline, that a host schedules in order. (The Bevy adapter maps these onto itsAstrodynSetschedule slots; a batch host just calls them in sequence.) - Frame-tree orchestration helpers shared by the reference consumers:
SourceFrameIds(root + per-source frame IDs),sync_pfix_rotation(writes a planet-fixed child's rotation + angular velocity into aFrameTree),evaluate_and_apply_frame_switch::<SourceId, F>(generic on-approach/on-departure switch driver), and the source-state mutatorsset_source_position/set_source_state. Lifted out ofastrodyn_runnerin #71 so both consumers share one implementation.
Three criterion microbenches measure the hot path under cargo bench:
cargo bench -p astrodyn_gravity --bench accumulate— spherical-harmonics kernel at degree 4 / 20 / 60.cargo bench -p astrodyn_gravity --bench integration— RK4 6-DOF step, with and without realistic Moon LP150Q gravity.cargo bench -p astrodyn_verif_jeod --bench step— fullSimulation::stepfor the Earth–Moon Clementine scenario.
Flamegraph SVGs land under target/criterion/<group>/<bench>/profile/
via pprof's criterion integration.
For steady-state per-step measurement with JSON output (used by CI's
perf-baseline-track job), the tier3_perf_runner binary wraps the
canonical scenarios:
cargo xtask perf-baseline # default Earth–Moon run
cargo xtask perf-baseline --phase-timing # adds per-phase µs/step
cargo xtask perf-baseline --help # see all optionsDirect invocation (skipping the xtask wrapper) is also available:
cargo run --profile release-with-debug \
-p astrodyn_verif_jeod --bin tier3_perf_runner -- \
--scenario earth_moon_clem --steps 100000 --warmup 1000 --repeat 5astrodyn requires Rust 1.89 or newer. The rust-version field is
declared in [workspace.package] so every published member crate
inherits it; users on older toolchains get a clean
error: package <name> requires Rust 1.89 from cargo rather than a
deep dependency-tree compile error.
The binding constraint is the workspace's bevy = "0.18" dependency (used
only by the optional astrodyn_bevy adapter — astrodyn and the astrodyn_*
physics crates do not depend on it): bevy 0.18.1 declares its own
rust-version = "1.89", which MSRV-aware resolution in cargo 1.85+ enforces
transitively, and the floor is shared workspace-wide via [workspace.package].
Our own direct usage of recent stdlib features (u{32,usize}::is_multiple_of,
stabilized in 1.87; #[diagnostic::on_unimplemented]; recent const generics)
sits comfortably below this floor.
Bump policy. Raising the MSRV is treated as a minor-version event, not a patch. The project tracks the last two to three stable Rust releases as the supported window; updates land when a dependency forces the floor higher or when a stdlib feature with no clean polyfill becomes load-bearing. Bumps are called out in the changelog.
Source of truth. The msrv job in
.github/workflows/tooling.yml pins
dtolnay/rust-toolchain@1.89 and runs
cargo check --workspace --all-targets. That CI gate — not the
rust-version field alone — is what every PR has to clear. Stdlib drift
beyond clippy::incompatible_msrv (cfg-gated syntax, transitive-dep MSRV
bumps) surfaces here.
Dual-licensed under either LICENSE-MIT or
LICENSE-APACHE at your option (SPDX: MIT OR Apache-2.0).
The repository also redistributes a verbatim mirror of NASA JEOD v5.4 source
files under crates/astrodyn_verif_jeod/test_data/jeod_inputs/ as
verification fixtures only. Those files remain governed by NASA's Open Source
Agreement (NOSA) v1.3, not astrodyn's dual license. See
NOTICE.md for the full attribution chain and the architectural
distinction (astrodyn is an independent reimplementation, not a JEOD fork).
- Project README and
CLAUDE.md— workspace-level architecture. crates/astrodyn_runner/examples/batch_propagation.rs— engine-agnostic worked example (plain-Rust host).crates/astrodyn_bevy/examples/typed_mission.rs— the same pipeline driven from a Bevy ECS host.- Rendered rustdoc: https://docs.rs/astrodyn