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.