Controllers
The controller stack turns high-level targets into actuator-level commands for the MuJoCo demo. In practice, the local runner is the best place to read this flow end-to-end because it wires the target state, controller, mixer, dynamics, and MuJoCo actuators in one file.
At the package level, concrete controllers are organized around the common interface ControllerBase, which provides the shared lifecycle and target-management pattern used by PID, geometric, and learned or adaptive controller variants.
flowchart LR
Target[Target or agent output] --> Mapping[Mapping]
Mapping --> Controller[Controller]
Controller --> Mixer[Mixer]
Mixer --> Rotor[Rotor dynamics]
Rotor --> MuJoCo[MuJoCo actuators]
Mapping --> TrackController[Track controller]
TrackController --> TrackDynamics[Track dynamics]
TrackDynamics --> MuJoCo
End-To-End Control Flow
The flight loop in lav2.controller.run follows this order:
- Read MuJoCo sensors for pose, velocity, attitude, and body rates.
- Build the controller state vector expected by FlightController.
- Convert target and state into total thrust plus body moments.
- Pass that wrench through Mixer to get rotor RPM commands.
- Pass rotor RPM commands through RotorDynamics.
- Write the resulting thrust and torque values into MuJoCo actuators.
The track loop is analogous, but uses TrackController and TrackDynamics instead of the mixer-plus-rotor path.
PID Controllers
lav2.controller.pid contains the reusable PID block plus two composed controllers:
Flight Controller
The flight controller is organized as a cascaded PID stack:
- position loop produces velocity targets
- velocity loop produces roll/pitch commands and thrust
- attitude loop produces angular-rate targets
- angular-rate loop produces body moments
flowchart LR
TPos[Position target]
TVel[Velocity target]
TAtt[Attitude target]
TRate[Body-rate target]
TThr[Collective thrust target]
subgraph Cascade[Flight control cascade]
direction LR
Pos[Position loop] --> Vel[Velocity loop] --> Att[Attitude loop] --> Rate[Body-rate loop] --> Mixer[Mixer] --> Rotor[Rotor dynamics] --> Act[MuJoCo actuators]
end
TPos --> Pos
TVel --> Vel
TAtt --> Att
TRate --> Rate
TThr --> Mixer
The control_mask determines which target channels are externally supplied and which ones are synthesized inside the cascade. That is the key hook that lets the same controller support position-style or lower-level command modes.
In lav2.controller.run, the lower-level flight modes also override how collective thrust enters the stack. For cmd_ctatt and cmd_ctbr, the thrust-related target channel passes through Mixer via apply_thrust_curve(...) and is then injected directly into the mixer input. The outer position and velocity loops do not generate this term. As a result, these modes behave as nested attitude or body-rate control with throttle pass-through, not as a full outer-loop position controller.
Track Controller
The track controller works on the planar state layout [x, y, yaw, u, v, r] and outputs left/right track acceleration-style commands. The structure is intentionally simpler than the flight cascade because the vehicle is subject to planar ground constraints, not full 6-DoF flight.
Other Controller Families
The package also includes other controller layouts built on top of ControllerBase:
lav2.controller.geocontains the geometric control family for flight.lav2.controller.xadapcontains the adaptive or learned controller path used for more specialized control experiments.
They follow the same package-level organization even when their internal control laws differ from the PID cascade.
Mixer And Command Allocation
Mixer is specific to the rotor vehicle path. It allocates total thrust and roll/pitch/yaw moments into four rotor commands by inverting a geometry-aware allocation matrix derived from VehicleParams.
The mixer also handles normalized thrust conversion for PX4-aligned command paths. PX4 fundamentally works with normalized thrust and torque style inputs, so the mixer performs the translation between normalized thrust conventions and total thrust in Newtons.
Two practical details matter here.
First, the NumPy and Torch mixer implementations are intentionally not fully behavior-identical. The NumPy version adds an extra allocation strategy that preserves roll and pitch authority before allocating yaw when saturation occurs. The Torch version keeps the more basic allocation path for efficiency and broader batched use.
Second, both implementations still serve the same high-level role of mapping desired thrust and moments into rotor commands. Exact allocation behavior can still differ across backends.
Outputs are clipped in squared-RPM space before taking the square root.
Helper Modules
Two supporting modules are worth knowing when work extends above the raw control law itself:
lav2.controller.utilsprovides logging and visualization helpers for controller response, actuator behavior, and data export.lav2.controller.mappingaligns agent outputs with the expected control-loop inputs, which is useful for layered RL control where different agents may drive different parts of the cascade.
Runtime Entry Points
lav2.controller.run.mainis the CLI entrypoint behinduv run lav2.control_callbackswitches between flight and track mode at runtime.- The flight and track execution paths are implemented in the helper functions inside
lav2.controller.run, immediately belowcontrol_callback, and are the best place to read the full local data path.
API Cross-References
- Base interface: ControllerBase
- PID module: lav2.controller.pid
- Flight controller: FlightController
- Track controller: TrackController
- Geometric controllers: lav2.controller.geo
- Adaptive controller path: lav2.controller.xadap
- Mixer: Mixer
- Mapping helpers: lav2.controller.mapping
- Logging and plotting helpers: lav2.controller.utils
- Local runner: lav2.controller.run