Your first simulation#
Run the complete example:
pixi run example-minimal
It loads a bundled 100 kg free body, initializes a circular reference orbit 400 km above Earth, places the spacecraft 10 m from the reference, and advances one simulated second.
The model#
The bundled FREE_BODY_XML contains:
<mujoco model="free_body">
<!-- Minimal free rigid body. Positions are chief-relative offsets in meters.
Uniform MuJoCo gravity is zero; mjorbit supplies differential gravity
and the configured orbital environment. -->
<option timestep="0.01" gravity="0 0 0"/>
<worldbody>
<body name="spacecraft" pos="0 0 0">
<freejoint/>
<geom type="box" size="0.5 0.5 0.5" mass="100" rgba="0.3 0.5 0.9 1"/>
</body>
</worldbody>
</mujoco>
The free joint permits translation and rotation. The box dimensions, mass, joint positions, and timestep use MuJoCo’s native SI units. Uniform MuJoCo gravity is zero; the orbit layer supplies the orbital environment.
The Python program#
"""Step a free body offset from a circular reference orbit.
Run from the repository root with ``pixi run example-minimal``.
"""
import numpy as np
from mjorbit import MjoModel, OrbitInit, mjo_forward, mjo_step
from mjorbit.constants import GM_EARTH, R_EARTH
from mjorbit.testdata import FREE_BODY_XML
def main() -> None:
radius_km = R_EARTH + 400.0
orbit = OrbitInit(
R_eci=[radius_km, 0.0, 0.0],
V_eci=[0.0, np.sqrt(GM_EARTH / radius_km), 0.0],
)
model = MjoModel.from_xml_path(FREE_BODY_XML, mj_timestep=0.01)
data = model.make_data(orbit=orbit)
# MuJoCo positions are offsets from the chief, in meters.
data.qpos[:3] = [10.0, 0.0, 0.0]
mjo_forward(model, data)
for _ in range(100):
mjo_step(model, data)
print(f"Time: {data.time:.2f} s")
print("Chief position (km):", data.orbit.R_eci)
print("Spacecraft offset (m):", data.qpos[:3])
if __name__ == "__main__":
main()
MjoModel.from_xml_path(...) compiles the XML into a reusable model.
model.make_data(orbit=...) allocates the state of one simulation. The chief
orbit uses kilometers and kilometers per second; the MuJoCo state uses offsets
from that chief in meters and meters per second.
After changing qpos, mjo_forward refreshes derived positions, frames, and
other runtime quantities. mjo_step advances both the coupled spacecraft and
the reference orbit by the model timestep.
Expect output close to:
Time: 1.00 s
Chief position (km): [6778.13265579 7.66855654 0. ]
Spacecraft offset (m): [1.00000129e+01 7.25404685e-09 0.00000000e+00]
The chief moves several kilometers while the spacecraft’s local offset remains approximately 10 m. Last digits may vary with platform and configuration. A body initialized exactly at the chief may keep zero local displacement while its absolute orbit advances.
Initialize in LVLH#
LVLH axes follow the orbit: radial, along-track, and cross-track. Use the data helpers to convert a desired LVLH position and velocity:
position_lvlh = np.array([10.0, 0.0, 5.0]) # m
velocity_lvlh = np.array([0.0, 0.05, 0.0]) # m/s in the rotating frame
data.qpos[:3] = data.world_position_from_lvlh(position_lvlh)
data.qvel[:3] = data.world_velocity_from_lvlh(position_lvlh, velocity_lvlh)
mjo_forward(model, data)
The velocity helper includes frame rotation. Rotating the velocity vector alone is insufficient. See frames and units.
Next steps#
Run pixi run example-free-drift for the analytical Clohessy–Wiltshire
comparison, pixi run example-mppi-arm-reach for a controller, or
pixi run -e warp example-batched for the GPU tutorial.