Flagship 03 · Sim-to-real · Isaac Lab · Unitree GO2

Closing the loop after the first deployment

Phoenix trains a locomotion policy in Isaac Lab, carries it to a physical GO2 through a parity-gated pipeline, captures what breaks on hardware, and feeds those failures back into training as a randomized replay distribution.

32 / 32 stand-v3-h25 sim evaluations passed
3.30% slew saturation against a 5% gate
4.77e-06 max torch to ONNX parity drift
235 unit tests green in CI
Gate 7 on-robot stand: still open

01 The problem

Most open-source quadruped RL stops at "trained in sim, deployed once." The deployment either works on video or it does not, and either way the failures that happen on hardware never make it back into the training distribution. The gap between a policy that scores well in simulation and one that is trustworthy on a robot is exactly where the interesting engineering lives.

02 Why existing approaches fall short

Domain randomization is applied up front, blind, and generously, in the hope of covering whatever reality does. That is expensive and it is not targeted: the sweep is not informed by any failure the robot actually had. Meanwhile the export path from PyTorch to the runtime on the robot is usually taken on faith, so a numerical discrepancy introduced at export is indistinguishable from a policy that simply did not transfer.

03 Core insight

Gate every stage, and make the hardware failure a training input rather than a postmortem. Phoenix records real failures as trajectories, replays them in simulation under a randomized physics sweep seeded from that specific failure, and fine-tunes on the resulting distribution. The policy then goes back to the robot.

04 Architecture

TrainingPPO through rsl_rl on a layered-YAML Isaac Lab environment, driven by roughly ten shell entry points.
Export gateONNX export with a torch / onnxruntime parity check that fails the build rather than warning.
RuntimeA ROS 2 policy node on the Jetson behind three bridges and one shared slew cap, all fail-closed.
CaptureRule-based attitude, collapse, and slip detection writing Parquet trajectory logs.
ReplayFailure-seeded curriculum fine-tune, wired and unit-tested, awaiting real hardware parquets.

05 What I built

The whole loop: the Isaac Lab environment layering, the training entry points, the parity-gated export, the ROS 2 deploy stack and its safety layer, the failure detector and logging format, and the replay and fine-tune path. The evidence ledger that classifies each claim as verified, inferred, or not-validated is also mine, and it is what keeps this page honest.

06 Validation and results

VerifiedSimulation training and the layered environment: complete, with unit tests across the stages.
Verifiedstand-v3-h25 sim evaluation: 32 of 32 successes, 3.30% slew nominal and 2.91% under full domain randomization, against a 5% gate. Simulation only.
VerifiedONNX / torch parity gate: maximum absolute difference 4.77e-06 against a 1e-4 tolerance.
VerifiedThe deploy stack ran end to end on the physical GO2 from the Jetson, with 235 unit tests green in CI.
OpenGate 7, the live on-robot stand. The saturation was root-caused and fixed in simulation; the corrected export has not yet been re-verified on hardware, so no stand has cleared.
PlannedGate 8, live velocity tracking. The two-policy mode-switch runtime is ready but unexercised.

07 What the hardware taught me

The slew saturation is the point of the whole project. A policy with a clean simulation record met a per-step rate limit the moment it touched real actuators, and it took a month to root-cause.

The cause was not the robot. Isaac's randomize_actuator_gains was zeroing the explicit DCMotor actuator gains during training, so the policy had been learning against a silently wrong actuator model the whole time. Replacing it with a custom gain-scaling startup term, plus a per-motor slew hinge penalty, brought the evaluation to 32 of 32 at 3.30% slew.

A related audit finding is worth stating plainly: the parity gate compares ONNX against TorchScript exports of the same wrapper, so it verifies numeric parity but cannot catch a wrong wrapper. It did not: the exporter was searching for normalizer keys that rsl_rl 3.x never writes, so every pre-2026-05-21 export silently dropped observation normalization. A green gate was not the same as a correct export, and finding that is exactly what the ledger exists for.

08 Limitations

LocomotionNo validated on-robot locomotion yet. Every locomotion number on this page is a simulation number and is labeled as one.
Replay loopThe failure-curriculum fine-tune is wired and unit-tested but has never run on real captured parquets, so the loop is not yet closed end to end.
Posture offsetAn unresolved posture offset needs either floating-base randomization or a floor test to diagnose.

09 Reproducibility

The pipeline is YAML-configured with shell entry points per stage, and the parity gate is part of the deploy verification rather than an afterthought.

deploy verification
$ ./scripts/verify_deploy.sh stand-v3 export torch -> onnx ........ ok parity max abs drift ....... 4.77e-06 (tol 1e-4) sim stand-v3-h25 eval ... 32/32 success sim slew nom / full DR .. 3.30% / 2.91% (gate <5%) hw gate 7 stand ........ OPEN (awaiting re-verified export)

10 Next step

Re-export the stand-v3-h25 deliverable through the corrected exporter, re-run the parity gate, and retry the stand on hardware with the slew path instrumented. If it clears, the captured failure parquets finally feed the replay curriculum and the loop closes for the first time on real data.