Skip to content

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:

  1. Read MuJoCo sensors for pose, velocity, attitude, and body rates.
  2. Build the controller state vector expected by FlightController.
  3. Convert target and state into total thrust plus body moments.
  4. Pass that wrench through Mixer to get rotor RPM commands.
  5. Pass rotor RPM commands through RotorDynamics.
  6. 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 is passed through Mixer via apply_thrust_curve(...) and then injected directly into the mixer input, instead of being generated by the outer position and velocity loops. That is why these modes behave as nested attitude or body-rate control with throttle pass-through, rather than 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. It is intentionally simpler than the flight cascade because the vehicle is under planar ground constraints rather than full 6-DoF flight.

Other Controller Families

The package also includes other controller layouts built on top of ControllerBase:

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. This matters because PX4 fundamentally works with normalized thrust and torque style inputs rather than direct physical wrench commands, so the mixer is where LAV2 translates 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 currently 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, but if you compare exact allocation behavior across backends you should expect this current difference.

Outputs are clipped in squared-RPM space before taking the square root.

Helper Modules

Two supporting modules are worth knowing when you work above the raw control law itself:

  • lav2.controller.utils provides logging and visualization helpers for controller response, actuator behavior, and data export.
  • lav2.controller.mapping aligns 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.main is the CLI entrypoint behind uv run lav2.
  • control_callback switches between flight and track mode at runtime.
  • The flight and track execution paths are implemented in the helper functions inside lav2.controller.run, immediately below control_callback, and are the best place to read the full local data path.

Where To Go Next

Use this guide when you want to understand the local simulator or tune gains. Move to Isaac Lab Tasks when the next step is RL training or large batched environments rather than interactive MuJoCo debugging.

API Cross-References