twoleg / training /scripts /analyze_gait.py
kenpeter
twoleg: project snapshot 2026-10-07T05:03Z — gate, loop, env, model, audit trail
24fe873
Raw History Blame Contribute Delete
4.48 kB
"""Diagnose the gait: is the TwoLeg policy walking, hopping, or shuffling?
Reward terms can be satisfied by a hop. This rolls out a checkpoint with a fixed
forward command and reports what the feet actually do: the contact distribution
(both down / one down / both up), per-foot contact runs, and whether the two
feet alternate. Alternating single-foot support is what "walking" means.
uv run python training/scripts/analyze_gait.py --checkpoint <pt> --command 0.3
"""
import argparse
import os
from dataclasses import asdict
import numpy as np
import torch
TASK = "Mjlab-Velocity-Flat-TwoLeg"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--checkpoint", required=True)
ap.add_argument("--command", type=float, default=0.3)
ap.add_argument("--steps", type=int, default=400)
ap.add_argument("--envs", type=int, default=8)
ap.add_argument("--device", default="cuda:0")
args = ap.parse_args()
from mjlab.envs import ManagerBasedRlEnv
from mjlab.rl import MjlabOnPolicyRunner, RslRlVecEnvWrapper
from mjlab.tasks.registry import load_env_cfg, load_rl_cfg, load_runner_cls
env_cfg = load_env_cfg(TASK, play=True)
env_cfg.scene.num_envs = args.envs
agent_cfg = load_rl_cfg(TASK)
env = ManagerBasedRlEnv(cfg=env_cfg, device=args.device, render_mode=None)
venv = RslRlVecEnvWrapper(env)
runner_cls = load_runner_cls(TASK) or MjlabOnPolicyRunner
runner = runner_cls(venv, asdict(agent_cfg), device=args.device)
runner.load(args.checkpoint, load_cfg={"actor": True}, strict=True, map_location=args.device)
policy = runner.get_inference_policy(device=args.device)
robot = env.scene["robot"]
# NOTE: do NOT hoist .data.found out of the loop. ContactSensor reallocates
# the tensor every step, so a captured handle stays frozen at zeros and the
# run reads as 100% both-feet-up. Re-read the attribute each step.
twist = env.command_manager.get_term("twist")
obs, _ = venv.reset()
start = robot.data.root_link_pos_w[:, :2].clone()
hist = np.zeros(3, dtype=np.int64) # 0,1,2 feet in contact
lf = [] # left foot contact per step
rf = []
for _ in range(args.steps):
cmd = torch.zeros_like(twist.command)
cmd[:, 1] = -args.command # forward is body -y (face side)
twist.command[:] = cmd
with torch.no_grad():
actions = policy(obs)
obs, _, _, _ = venv.step(actions)
c = env.scene["feet_ground_contact"].data.found[:] # live read, see NOTE above
n_down = (c > 0.5).sum(1) # per env 0/1/2
for k in (0, 1, 2):
hist[k] += int((n_down == k).sum())
lf.append((c[:, 0] > 0.5).float().mean().item())
rf.append((c[:, 1] > 0.5).float().mean().item())
end = robot.data.root_link_pos_w[:, :2]
disp = (end - start).norm(dim=1)
elapsed = args.steps * env.step_dt
lf = np.array(lf)
rf = np.array(rf)
total = hist.sum()
both_down = hist[2] / total
one_down = hist[1] / total
both_up = hist[0] / total
# alternation: correlation of the two feet's contact traces, low = out of phase
alt = float(np.corrcoef(lf, rf)[0, 1]) if lf.std() > 1e-6 and rf.std() > 1e-6 else float("nan")
print(f"checkpoint : {os.path.basename(args.checkpoint)}")
print(f"command : {args.command:.2f} m/s, {args.steps} steps, {args.envs} envs")
print(f"net speed : {disp.mean().item()/elapsed:.3f} m/s")
print()
print("contact distribution over all (env, step) samples:")
print(f" both feet down (standing/slide): {both_down*100:5.1f} %")
print(f" ONE foot down (walking) : {one_down*100:5.1f} %")
print(f" both feet up (hopping/flying): {both_up*100:5.1f} %")
print()
print(f"foot duty factor: left {lf.mean():.2f} right {rf.mean():.2f}")
print(f"foot contact correlation: {alt:+.2f} (near -1 = perfectly alternating = walking)")
print()
if one_down > 0.3 and (alt < -0.3 or np.isnan(alt)):
print("verdict : WALKING pattern (single-foot support, feet alternate)")
elif both_up > 0.25:
print("verdict : HOPPING (both feet leave the ground together)")
elif both_down > 0.6:
print("verdict : SHUFFLING / standing (weight stays on both feet)")
else:
print("verdict : mixed, not a clean walk")
if __name__ == "__main__":
main()