Instructions to use Histochemichael/microduck-electric-slide-policy with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Microduck
How to use Histochemichael/microduck-electric-slide-policy with Microduck:
# Replace SLOT with the slot specified in the model card (walk, stand, sitstand, ground_pick, kick_left, kick_right, roulade). sudo robotctl policy load SLOT Histochemichael/microduck-electric-slide-policy
- Notebooks
- Google Colab
- Kaggle
|
Download code/AGENTS.md from Histochemichael/microduck-electric-slide-policy: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/AGENTS.md
- Command line
-
hf download hf://Histochemichael/microduck-electric-slide-policy/code/AGENTS.md
-
curl -L -o AGENTS.md https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/AGENTS.md
16 kB
AGENTS.md
RL training environments for Microduck β a ~800 g, ~25 cm tall bipedal
robot with 14 Dynamixel XL330 servos β built on mjlab
(MuJoCo Warp) with PPO (rsl_rl). Policies are trained here at 50 Hz, exported to
ONNX, and deployed by the runtime in the pollen-robotics/microduck repo on
the real robot. Sim2real transfer
is the whole point: every convention below exists because breaking it produced a
policy that worked in the viewer and failed on hardware.
Commands
uv run list-envs # live task registry
uv run train <TASK_ID> --env.scene.num-envs 4096 # train (add --hf-jobs for Hugging Face Jobs)
uv run train <TASK_ID> --env.scene.num-envs 64 --agent.max_iterations 5 # SMOKE TEST β always run first
uv run play <TASK_ID> --wandb-run-path <entity/project/run_id>
uv run scripts/export.py <TASK_ID> --wandb-run-path <...> # β ONNX (bakes obs normalizer β mandatory path)
uv run scripts/infer_policy.py --walking out.onnx # CPU MuJoCo deployment rehearsal
uv run --with pytest pytest tests/
A 5-iteration smoke test at 64 envs catches ~95% of config errors for cents. Never launch a long run without one.
Repo map
src/mjlab_microduck/tasks/mdp.pyβ ALL custom MDP functions (rewards, events, observations, commands, curricula). Add new functions here, grouped by task.src/mjlab_microduck/tasks/microduck_*_env_cfg.pyβ one cfg module per task family.microduck_velocity_env_cfg.pyis the main walking recipe AND the shared base (robot, DR, obs, commands) other envs build on or mirror.src/mjlab_microduck/tasks/__init__.pyβ task registration (base +-Backlash-variants).src/mjlab_microduck/tasks/backlash.pyβ wraps any env cfg into its backlash twin.src/mjlab_microduck/robot/microduck_constants.pyβ robot cfgs, HOME frame, BAM actuator cfg.src/mjlab_microduck/robot/microduck/β MJCF exports from Onshape (onshape-to-robot, oneconfig_mjcf_*.jsonper model) + scenes +add_backlash.py.src/mjlab_microduck/actuator/friction_dr_bam.pyβ BAM actuator + friction DR + backlash encoder.scripts/β export, infer, sim2real comparison, wandb helpers.tests/β cfg-invariant and mdp-function regression tests (CPU, no GPU needed).
Invariants β do not break these
- Obs layout is 61D (actor) and shared across the whole policy family so
policies are hot-swappable in the runtime: 48 base proprioception +
13D command block
[twist(3), head_pose(4), body_pose(6)], in that order. An env that doesn't use a command slot ZERO-PADS it (keep the obs term, sample tiny ranges) β never delete a slot. - Joint layout (14 servos, ctrl idx = joint idx on walk/allcollisions
models): 0β4 left leg (hip_yaw, hip_roll, hip_pitch, knee, ankle), 5β8
neck/head (neck_pitch, head_pitch, head_yaw, head_roll), 9β13 right leg.
On roller/backlash models, passive joints INTERLEAVE β never hardcode joint
indices in mdp functions; use the
_servo_joint_ids/_servo_joint_poshelpers in mdp.py (identity on plain models, correct everywhere else). - Unactuated joints are all named
passive_*(wheels, backlash hinges). Every actuator/obs/reward selector uses^(?!passive_).*β keep the prefix convention when adding joints, and newpassive_regexes must not accidentally match backlash joints (^passive_.*wheel, not^passive_.*). - Actuators are BAM (voltage-controlled XL330 model, friction computed by
the actuator). Two consequences: any STANDALONE env cfg must register the
expand_bam_friction_fieldsstartup event, and joint-friction DR must scale the actuator'sfriction_scaleβdof_frictionlossis zeroed under BAM, so randomizing it is a silent no-op. - Obs normalization is ON β the normalizer must be baked into the ONNX.
scripts/export.pydoes this; in-sim play hides the bug (it applies the normalizer anyway), so never hand-convert a checkpoint. - Policies are UNFILTERED (no action low-pass in training). Don't add EMA filtering without a matched runtime flag and a transfer test β trained-with / deployed-without (either direction) breaks transfer.
- Domain randomization must not accumulate across resets. mjlab 1.3.0's
dr.*ops withoperation="add"/"scale"are natively non-accumulating (they re-read compile-time defaults); custom DR functions must restore-then-apply. An accumulating CoM randomizer once degraded every long run for months. - If an obs is remapped to a sensor view (backlash encoder, bias), any tracking REWARD on the same quantity must measure the same view β otherwise the policy is punished for correcting what it sees.
-Backlash-task variants must mirror their base task's robot model (walk / allcollisions / rollers) so backlash A/B comparisons are unconfounded.
Building a new env β the workflow
- Pick the closest template and build on it, don't start from scratch:
locomotion β the velocity recipe; episodic trick ending in a pose β
standup; commanded two-state β sitstand; dynamic maneuver β roulade
(read its cfg docstring β it encodes a 5-run lesson arc). Building on
make_microduck_velocity*_env_cfgkeeps DR / obs / noise / delays in sync for free; if you build standalone from mjlab's base template, you must port the whole DR + obs-noise + NaN-guard stack yourself (grep for what velocity wires:_safecritic obs terms,nan_statetermination with sensor_names,expand_bam_friction_fields, encoder bias, IMU misalignment). - Verify physics assumptions in sim BEFORE training β this is the single
biggest time-saver:
- A target/rest pose must be a stable equilibrium: hold its ctrl for 3 s from noisy inits and check TILT, not just height (a settle test that only records z reports fallen states as "resting fine").
- Measure target heights off the actual robot in sim (e.g. trunk z under a standing policy), never carry them across model revisions. A 5 mm-wrong STAND_Z once turned the goal into an impossible target for days.
- Config conventions:
ENABLE_*toggles + tuned constants at the top of the cfg file; factorymake_..._env_cfg(play: bool, rough: bool); register intasks/__init__.py(+ the_BACKLASH_TASKStable if applicable); ownRslRl...RunnerCfgwith a distinctexperiment_name. Symmetry mirror-loss is available (61D table insymmetry.py) β OFF by default, never for asymmetric tasks. - Write cfg tests (see
tests/test_*_cfg.py): joint indices resolve on the actual model, reward weights have the intended sign, gates open/closed where expected. These run on CPU and lock in the invariants. - Smoke test (64 envs, 5 iters): builds, steps NaN-free, obs is 61D, every reward term computes, ONNX exports.
- Train, watch the log (below), and expect 2β5 iterations of reward-hacking whack-a-mole β that's normal, the lessons below shortcut most of it.
Reward design β rules that were each learned the hard way
- Sign convention (bit four envs): mdp.py has two penalty styles. mjlab-base
cost functions return β₯ 0 β negative weight. Self-negating microduck functions
(
*_penalty,*_l1returning β€ 0) β POSITIVE weight. A negative weight on a self-negating penalty double-negates into a reward for the violation, and the policy will farm it (butt-hopping, crash-sits). The infallible check: on every run, everyEpisode_Reward/<penalty>in wandb must be β€ 0. - RL optimizes the letter of the reward. Every under-specified degree of freedom will be exploited (ballistic whip instead of a roll, shoulder-roll instead of sagittal, head-tripod instead of standing). Encode what counts as the maneuver in hard state-based gates (support contact, orientation-axis checks, latches), not in small penalty nudges.
- No jackpots: any "reach X" reward must be rate-limited or slewed. Arriving early at a goal state that then pays per-step is a jackpot that buys arbitrary violence. For commanded transitions, track a slewed internal target (constant-rate blend) β being ahead of the ramp pays zero, so slow IS the argmax. Speed-cap penalties alone integrate to a bounded cost and lose.
- Never gate a positive reward on being in a bad state (fallen, low) β the policy parks in the cheapest qualifying pose and farms it. Use potential-based shaping instead (pay Ξprogress, e.g. Ξcos(tilt): rising pays, holding pays zero, unfarmable). For rest tasks, audit each positive term against every stable flop (on back / face / side): if flopping keeps most of the stack, the policy will flop.
- Episodic pose-landing tasks: single fixed target from t=0 (Gaussian + L1 on joints and height, generous std) + |a_z| impact penalty + two-layer upright β NOT keyframe/waypoint trajectories (the policy camps at waypoints). The path is what RL is supposed to discover.
- Regularizers come in two kinds. Motion-blockers (body_ang_vel, angular_momentum, pose std) penalize what a dynamic motion physically requires β keep them LOW for dynamic tasks. Smoothness (action_rate, joint_torque_rate) damps jitter without blocking slow big motions β safe to weight, but introduce it AFTER skill discovery (curriculum from ~0): any attempt-tax active while a hard skill is being explored makes "do nothing" win. Slow careful tasks (reaching) want heavier smoothness than walking.
- Compare reward mass, not weights, when copying regularizers between envs. PPO sees relative advantage: the same action_rate weight is 4Γ weaker under a 4Γ-larger positive task stack.
- Tracking Gaussian std: β the error you still care about, not the max error β too loose has no gradient at small errors. BUT before tightening, ask whether the error is escapable by the policy or inherent to the behavior you want (a 38%-of-body-mass head MUST oscillate while walking; a tight instantaneous head-tracking std taxed walking so hard the policy stood still). Price only the escapable part β e.g. L1 on a 1 s EMA charges DC bias and lets oscillation cancel.
- Multiplicative composites beat additive sums at goal states: when an additive stack has a compromise basin (80% of every term via a lean), a product of Gaussians collapses on any single deficient factor β but pick stds wide enough that the CURRENT policy scores visibly, or the gradient is invisible and nothing changes.
- Joints parking on hard limits: fix with a qpos-side limit-proximity
penalty on the offending joints; the stock
dof_pos_limitsonly fires in the last ~7.5% of range, and command-side penalties don't work (wide ctrlrange is intentional β low-kp servos need overshoot).
Commands, observations, dead weights
- A command input that is never non-zero has dead weights forever. Every command slot keeps a small non-zero sampling range from step 0 (even at reward weight 0) so its input neurons stay alive for later curricula.
- Zero-command behavior must be explicitly trained (
zero_command_prob-style exact-zero sampling): uniform sampling essentially never produces the all-zero command, which is exactly the deployment idle state. - Rare-but-important command regions need explicit buckets β e.g. turn-in-place
(
rel_turn_in_place_envs): independent uniform sampling made spinning ~2% of experience and it never trained.
Curricula
- Steps are env steps:
iteration Γ 24(NUM_STEPS_PER_ENV = 24). - Use the proven split:
microduck_mdp.reward_weightfor weight schedules, a dedicated params-curriculum for command/event ranges.mdp.reward_weightis a step function, not an interpolation β discretize ramps into stages. - Mutate term cfgs via the managers (
env.event_manager.get_term_cfg(...)), neverenv.cfg.events[...]β managers deepcopy their cfg at init, so writes toenv.cfgare silent no-ops (this also bites eval scripts that force spawn states). - Phase-align every stage with what the policy has actually learned: don't harden spawn mixes before the current slice consolidates; don't introduce taxes before the skill exists. When a wandb metric steps DOWN exactly at curriculum stage boundaries, the pacing is wrong β stretch stages or move the introduction later, never earlier.
- Reverse-curriculum spawns (starting episodes partway through the maneuver, including nearly-done) are the reliable fix for "learns the start, never the last mile" β the frontier otherwise gets no on-policy data.
Training ops & reading a run
- wandb project
mjlab_microduck; logs inlogs/<experiment_name>/; resume with--agent.load-checkpoint model_XXXX.pt --agent.resume True. - Watch per-iteration: mean reward rising AND episode length behaving as the
task demands; every penalty term β€ 0; the MAIN task term actually growing
(total reward can rise purely on regularizers while the trick never happens).
Episode_Reward/<term>logs the WEIGHTED value β a term at weight 0 reads 0 regardless of behavior, so interpret against the weight schedule. - Budgets: simple episodic tricks β 1000 iters at 4096 envs; gaits and curriculum-heavy recovery need 4000β6000.
- Measure before theorizing. When a run "fails", run a headless eval of the actual checkpoint (per-spawn-type batteries, end-state clusters, angular-rate profiles) before changing rewards: past "failures" turned out to be early checkpoints, a success criterion splitting one behavior cluster in half, and a pay cap fighting measured physics. Sim metrics can pass while the video fails the human eye β watch the video AND check which geom/axis touches.
- Report what rollouts actually show ("rolls but face-plants 1 in 3"), not "it works!". The user decides when it's good enough.
Sim2real footguns (cost real debugging weeks)
- A fresh
uv syncis the ground truth (HF Jobs run one): anything that only works via manually-installed local packages will die remotely. Keeppyproject.tomlhonest. - Wheels are per-architecture. On linux-
aarch64(DGX Spark / GB10) PyPI's torch wheel is CPU-ONLY (2.9.1+cpu,torch.version.cuda is None), sotorch.cuda.device_count() == 0and mjlab'sselect_gpus()indexes an empty list βIndexErrorbefore iteration 0.[tool.uv.sources]routes torch to the cu129 index foraarch64only (cu129 matches the CUDA toolkit warp bundles; x86_64/HF Jobs stay on PyPI). Two silent break points, both locked bytests/test_aarch64_cuda_torch.py: torch must stay a DIRECT dependency (uv applies[tool.uv.sources]to direct deps only β deleting the redundant-lookingtorch==pin makes the routing a no-op), and the pin must stay==, since the CUDA index carries newer builds than PyPI (a>=silently dragged torch 2.9.1 β 2.13.0). - Physics-aligned limits: a 25 cm robot tumbles at 3.5β5.5 rad/s NATURALLY β don't impose human-scale speed intuitions via caps; put anti-violence pressure on impacts and thrash (|a_z|, action_rate, support gates), not on rotation speed.
- IMU DR is zero-centered β it trains tolerance to misalignment magnitude, and CANNOT compensate a systematic mounting bias (that's a runtime calibration).
- Real deployments hot-swap ONNX policies (walk / stand / trick) with a shared
obs contract β rehearse in
scripts/infer_policy.pybefore touching the robot, with the correct command-slot writes (a posture flag lives in the twist vx slot; feeding all-zeros means "stand", which looks like "policy ignores the button").