Simulator Overview¶
Worlds and drones¶
Crazyflow organises simulation state into a two-dimensional batch: worlds × drones.
n_worlds— number of independent simulation environments. Each world has its own physics state and evolves independently. Use this to run domain randomisation, parallel RL rollouts, or MPPI sampling.n_drones— number of drones per world. All drones in a world share the same physics tick but have independent states.
Every state array has shape (n_worlds, n_drones, feature_dim). To read the position of drone 0 in world 2:
from crazyflow.sim import Sim
sim = Sim(n_worlds=4, n_drones=2)
sim.reset()
pos = sim.data.states.pos[2, 0] # world 2, drone 0 → shape (3,)
SimData¶
All simulation state is stored in sim.data, a SimData pytree. The main sub-trees are:
| Field | Type | Description |
|---|---|---|
states |
SimState |
Current kinematic state of every drone |
states_deriv |
SimStateDeriv |
Time derivatives computed by the physics model |
controls |
SimControls |
Staged commands and controller state |
params |
SimParams |
Physical parameters (mass, inertia, motor constants, …) |
core |
SimCore |
Metadata: step count, frequency, RNG key, device |
SimState fields¶
| Field | Shape | Units |
|---|---|---|
pos |
(N, M, 3) |
Position in world frame, metres |
quat |
(N, M, 4) |
Orientation quaternion, scalar-last xyzw |
vel |
(N, M, 3) |
Linear velocity, m/s |
ang_vel |
(N, M, 3) |
Angular velocity in body frame, rad/s |
force |
(N, M, 3) |
External force applied to the drone body, N |
torque |
(N, M, 3) |
External torque applied to the drone body, Nm |
rotor_vel |
(N, M, 4) |
Motor angular velocities, RPM |
where N = n_worlds and M = n_drones.
Immutability and data.replace¶
SimData is a JAX pytree. All fields are immutable — operations return a new SimData rather than modifying in place. This is what makes the simulation compatible with jax.jit, jax.grad, and jax.vmap.
To modify a field, use .replace():
from crazyflow.sim import Sim
sim = Sim(n_worlds=1, n_drones=1)
sim.reset()
import jax.numpy as jnp
new_pos = sim.data.states.pos.at[..., 2].add(1.0)
sim.data = sim.data.replace(states=sim.data.states.replace(pos=new_pos))
Simulation frequency and the control stack¶
freq sets the physics update rate in Hz. The control stack is executed as part of each physics step, but controllers fire at their own sub-frequency rather than every tick. For example, with freq=500 and state_freq=100, the state (Mellinger) controller runs every 5 physics steps, and the attitude controller runs at attitude_freq.
This means you can advance multiple physics steps in a single sim.step(n_steps) call and the control stack will execute at the correct rate automatically, with no manual sub-stepping required. This is also what makes fusing many steps into a single compiled call efficient.
import numpy as np
from crazyflow.sim import Sim
from crazyflow.control import Control
sim = Sim(freq=500, control=Control.state)
sim.reset()
cmd = np.zeros((1, 1, 13), dtype=np.float32)
sim.state_control(cmd)
sim.step(sim.freq // sim.control_freq) # 500 // 100 = 5 physics steps, controller fires once
The step and reset pipelines¶
Each call to sim.step() runs sim.step_pipeline, a tuple of pure JAX functions that transforms SimData. By default it contains the control conversion functions, the numerical integrator, a step counter, and a floor clip. Similarly, sim.reset_pipeline is applied during sim.reset() and is empty by default.
Both pipelines can be extended with custom functions for disturbances, domain randomization, or logging without modifying the core simulator.
See Pipelines for full details.
Next steps¶
- Object-Oriented API — all
Simmethods in detail - Functional API — working with
SimDatadirectly inside JAX transformations - Pipelines — extending the step and reset pipelines