apt-a1 · validation · MuJoCo free-walk replay

Does the A1 actually track its own gait library?

Every claim so far was that these trajectories are periodic under a lumped single-rigid-body model and inside the A1's measured actuator limits. Neither says a real 12-DOF articulated robot can follow them. This replays library trajectories on the full MuJoCo A1, unrestrained on flat ground — which also makes it the free-walk baseline CONTEXT.md §5 says is missing, since every hardware run so far was on a hoist.

83%of slow trot trajectories (0.25–0.75 m/s) walk six full gait cycles unassisted, tracking height to 15 mm
4%survive at 1.75–2.05 m/s. The library degrades sharply with speed, and torque saturation is the mechanism
53.4%of the A1's mass is in its legs. SRBD assumes they are massless — the leading suspect for the speed ceiling
−JTfthe replay caught an inverted torque sign that had been in the library all along

What the replay found first

The initial run failed everywhere, and the trace showed why: the body sank at close to free fall for 150 ms, then got catapulted to 0.56 m and flipped. Feeding the stored feedforward torque was pulling the robot into the ground.

The bug. a1.py computed joint torque as τ = JTf with f the ground reaction force. That is the work the ground does on the leg; the actuator torque is τ = −JTf. The feasibility filter never noticed because it only ever reads |τ| ≤ 33.5 — but the stored torques are exactly what a torque decoder would be trained on. Confirmed statically too: a leg holding mg/4 returned −5.70 N·m at the knee where extension needs +5.70.

Success rate across the library

Success turned out to be trajectory-dependent, not speed-dependent alone — two draws at the same commanded speed can behave completely differently. So this is 24 randomly drawn feasible trajectories per speed bin, not a hand-picked example.

Speed (m/s)trot survives 6 cycbound median z RMSmedian travelledτ saturated
0.25 – 0.7583%71%0.015 m121%4%
0.75 – 1.2538%62%0.016 m87%54%
1.25 – 1.758%21%0.013 m88%79%
1.75 – 2.054%29%0.026 m50%92%
The mechanism is torque saturation. The fraction of runs pinning |τ| at the 33.5 N·m ceiling climbs 4% → 92% in lockstep with the survival collapse. Note what this means for the feasibility filter: it currently checks that the feedforward torque fits under 33.5, but at run time the PD term adds on top of it. The filter needs to reserve headroom, not spend the whole budget.

Replay detail, six gait cycles each

ff is stored feedforward torque alone, pd is position tracking on the stored joint angles, ff+pd is both — which is what a real controller does, and what APT-RL's decoder-plus-corrections structure amounts to. Gains are the deployed Kp 40, Kd 1.0 from deploy/parkour_run.cpp:396.

Gains are not a free parameter, and softer won. A sim-tuned Kp 150 tracked a cherry-picked case beautifully but is undeployable — CONTEXT.md §4 says higher Kp trips PowerProtect earlier (kp20 needs L3, kp40 needs L5). Rerun at the real Kp 40, results got better: slow trot went from saturating the torque ceiling to peaking at 26.6 N·m with the same tracking. Stiff gains were fighting the feedforward instead of letting it do the work.
CaseModeCycles survivedz RMS (m) pitch RMS (rad)Travelled / planned (m)peak |τ|
trot v=+0.50ff6.000.08740.14430.41 / 1.8421.2
trot v=+0.50pd6.000.03930.15920.24 / 1.8422.5
trot v=+0.50ff+pd6.000.01520.08261.92 / 1.8426.6
trot v=+1.20ff2.580.14010.09430.51 / 1.9121.7
trot v=+1.20pd1.420.09580.49500.43 / 1.0533.5
trot v=+1.20ff+pd1.680.04950.15511.10 / 1.2433.5
trot v=+1.90ff1.020.07970.11380.81 / 0.9723.6
trot v=+1.90pd1.370.09620.32900.68 / 1.3133.5
trot v=+1.90ff+pd1.530.05890.29511.14 / 1.4633.5
bound v=+0.50ff1.610.05470.59560.65 / 0.3223.0
bound v=+0.50pd6.000.03500.2096-0.21 / 1.2233.5
bound v=+0.50ff+pd4.320.03150.38470.95 / 0.8832.7
bound v=+1.20ff1.050.05590.17790.43 / 0.4623.3
bound v=+1.20pd2.790.06870.39740.88 / 1.2533.5
bound v=+1.20ff+pd2.290.05590.29351.18 / 1.0233.5

Video — ff+pd, 6 cycles, real time

trot 0.50 m/s tracked 6/6
cycles survived
6.00 / 6
z RMS
0.0152 m
travelled
1.92 / 1.84 m
peak |τ|
26.6 / 33.5 N·m
trot 1.20 m/s fell at 1.68
cycles survived
1.68 / 6
z RMS
0.0495 m
travelled
1.10 / 1.24 m
peak |τ|
33.5 / 33.5 N·m
trot 1.90 m/s fell at 1.53
cycles survived
1.53 / 6
z RMS
0.0589 m
travelled
1.14 / 1.46 m
peak |τ|
33.5 / 33.5 N·m
bound 0.50 m/s fell at 4.32
cycles survived
4.32 / 6
z RMS
0.0315 m
travelled
0.95 / 0.88 m
peak |τ|
32.7 / 33.5 N·m
bound 1.20 m/s fell at 2.29
cycles survived
2.29 / 6
z RMS
0.0559 m
travelled
1.18 / 1.02 m
peak |τ|
33.5 / 33.5 N·m

Reading it

Not yet established

Every number here is one hand-written replay harness away from being wrong, and it caught two of my own errors already. This is sim, on flat ground, with a model whose contact parameters were never calibrated against the real robot. It says the library is trackable in principle at low speed; it says nothing about sim2real, and CONTEXT.md §4's warnings — PowerProtect tripping earlier at higher Kp, the 15 ms transport delay — all still apply. Peak torque hits the 33.5 N·m ceiling in every pd and ff+pd run, which on hardware is precisely the condition that trips PowerProtect level 1 about 7 s in.