Low-Level Locomotion Policy

The low-level (LL) policy turns a planar base command (vx, vy, ωz) into leg joint targets. It must keep walking while the arm carries the payload and the partner pulls on it. It is trained once and kept frozen while the transport policy is trained.

What we used

We use the perceptive locomotion backbone of Ma et al., RA-L 2022 for ANYmal D with an arm. It observes leg proprioception, a summary of the predicted external wrenches on the base together with the current command and base twist, and a height scan around the feet. This model is internal and not released, so you will need to train your own backbone.

Terrain robustness comes from this policy

The transport policy is trained on rough terrain, so it learns to tolerate terrain-induced disturbances. A weaker backbone still lets you reproduce the high-level training pipeline, but it will limit transport on rough terrain.

Training a replacement in legged_gym

Any velocity-tracking policy that stays stable under arm loading works with the rest of the pipeline. A practical choice is a legged_gym rough-terrain policy trained on the full robot model, including the arm and payload:

  1. Robot. Load the same URDF as the transport task (robot with payload). The policy outputs 12 leg actions; the arm joints track their default carrying posture with a joint PD controller.
  2. Load robustness. Randomize the payload mass (0-10 kg), base mass and friction, push the robot, and apply random horizontal forces (up to 40 N) and yaw torques (up to 10 N·m) to the payload, resampled within each episode. This mimics what the partner will do during transport.
  3. Commands. Sample vx, vy ∈ [−1, 1] m/s and ωz ∈ [−1.5, 1.5] rad/s, which covers what the transport policy sends after clipping.
  4. Terrain. Use the legged_gym rough-terrain curriculum (slopes, stairs, discrete obstacles).

Since legged_gym assumes one action per joint, let the locomotion policy output only the leg actions and use only the leg joints in its observation. Check the joint order in self.dof_names, since Isaac Gym orders joints by the URDF tree. After training, export the policy as TorchScript (legged_gym's play.py with EXPORT_POLICY = True) so the transport environment can load it.

A minimal skeleton of the low-level task:

# legged_gym/envs/paint/paint_env.py
class PaintLocoEnv(LeggedRobot):
    """Low-level task: the policy drives the legs, the arm holds its carrying posture."""

    def _compute_torques(self, actions):
        # TODO: PD towards the default joint positions, with the scaled actions added
        #       to the leg joints only
        ...

    def compute_observations(self):
        # TODO: as LeggedRobot.compute_observations, with leg joints only, plus height scan
        ...

    def _process_rigid_body_props(self, props, env_id):
        # TODO: randomize base and payload mass
        #       (see Transport Environment > Domain randomization)
        ...

    def step(self, actions):
        # TODO: as LeggedRobot.step, with a random payload wrench before every physics step
        ...

Command interface

The first three actions of the transport policy are the base command (vx, vy, ωz). Before it reaches the LL policy, limit it to the range the LL policy was trained on and add a small deadband, so that near-zero commands become zero.

Using it in the transport environment

The transport environment loads the exported LL policy and queries it once per control step (50 Hz). Build its observation exactly as during locomotion training, with the velocity command replaced by the processed base command. Keep the LL policy's own previous leg actions (and internal state, if any) separate from the transport policy's, and reset them together with the environment.