|
Download abstraction.md from kenpeter123/twoleg: direct link, hf CLI and curl.
- Browser
- Download file 30.5 kB
-
https://huggingface.co/kenpeter123/twoleg/resolve/main/abstraction.md
- Command line
-
hf download hf://kenpeter123/twoleg/abstraction.md
-
curl -L -o abstraction.md https://huggingface.co/kenpeter123/twoleg/resolve/main/abstraction.md
30.5 kB
| # TwoLeg training abstraction | |
| ## Scope and how to read this file | |
| This file answers three questions about the training runs under | |
| `training/logs/rsl_rl/velocity/`: | |
| 1. Did the twoleg runs follow the `microduck_rl` recipe? | |
| 2. How much training did `microduck_rl` give its duck before it walked? | |
| 3. Why does the twoleg policy stand still? | |
| Every number below was read from an artifact on disk. Each claim carries a label: | |
| - **measured**: read directly from a log file, `params/*.yaml`, or a rendered video frame. | |
| - **derived**: arithmetic on measured values, with the arithmetic shown. | |
| - **inference**: my reading of the evidence. Not a measurement. | |
| No training was run and no policy was evaluated for this file. The | |
| `preflight_3d.py --capability train` gate reports `BLOCKED` on this host | |
| (Python 3.14.4, no `torch`, `mujoco`, `gymnasium`, or `stable_baselines3`), so | |
| every statement below is a reading of a past run, not a fresh rollout. | |
| ## How training works | |
| ``` | |
| robot_item/chest_both_arms_legs.xml (reference assembly, world-pinned puppet) | |
| β training/scripts/generate_twoleg_model.py | |
| β re-parent limbs under torso, recompute pos/quat from world poses, | |
| β drop the 2 duplicate waist hinges, add freejoint + imu + foot sites | |
| v | |
| twoleg_mjcf/robot_twoleg.xml (connected tree, 121 bodies, 17 joints) | |
| training/assets/twoleg.xml (GENERATED, mjlab-ready, relative meshdir) | |
| β robot/twoleg_constants.py:17-22 | |
| v | |
| EntityCfg -> make_twoleg_velocity_env_cfg(play=False) [tasks/twoleg_velocity_env_cfg.py:33] | |
| ββ Robot: 17 XML <position> servos [generated model] | |
| β 4.4818 kg, 121 bodies, 262 geoms, kp 6 arms / 40 legs | |
| ββ Terrain: plane, extent 2.0 m, no generator | |
| ββ Sensors: foot_height_scan, feet_ground_contact, self_collision | |
| ββ Commands: twist(3) = lin_vel_x 0..0.3, lin_vel_y -0.1..0.1, ang_vel_z -0.4..0.4 | |
| ββ 13 reward terms, 1 curriculum (air-time window ramp) | |
| ββ 3 terminations: time_out, fell_over, nan_state | |
| β | |
| v | |
| MuJoCo Warp via mjlab 1.3.0, 4096 envs, decimation 4, timestep 0.005 -> 50 Hz | |
| β rollout 24 steps per env per iteration | |
| v | |
| PPO via rsl_rl [tasks/twoleg_velocity_env_cfg.py:137-174] | |
| β actor/critic 512-256-128 ELU | |
| v | |
| logs/rsl_rl/velocity/<timestamp>/model_*.pt -> TensorBoard | |
| ``` | |
| ## The runs on disk | |
| `training/logs/rsl_rl/velocity/` holds 8 runs. Six are smoke tests. Two carry | |
| real training. | |
| **measured** (2026-10-03): only `2026-10-03_08-28-43_velocity` remains | |
| on disk; the earlier 2026-10-01/02/03 run dirs were deleted. | |
| | run | max checkpoint | `num_envs` | `max_iterations` | wall clock | | |
| |---|---|---|---|---| | |
| | `2026-09-27_23-59-17_velocity` | `model_24999.pt` | 4096 | 25000 | 6 h 09 m 54 s | | |
| | `2026-09-28_10-37-56_velocity` | `model_299.pt` | 1024 | 300 | about 2 m | | |
| | `2026-09-27_23-48-25_velocity` | `model_149.pt` | not resolved | not resolved | under 1 m | | |
| | `2026-09-27_23-46-05` / `-23-46-25` / `2026-09-27_23-58-35` | `model_2` / `model_9` / `model_4` | not resolved | not resolved | under 1 m | | |
| | `2026-09-28_10-36-51` / `10-37-23` | `model_2.pt` | not resolved | not resolved | under 1 m | | |
| Wall clock for the 25,000-iteration run is derived from the mtime of | |
| `model_0.pt` (2026-09-27 23:59:23) to the mtime of `model_24999.pt` | |
| (2026-09-28 06:09:17). | |
| **derived** sample budget for the 25,000-iteration run: | |
| ``` | |
| 4096 envs x 24 steps/env/iter x 25000 iters = 2,457,600,000 env-steps | |
| 25000 iters x 0.8833 s/iter = 6.13 h (logged Perf/* mean) | |
| ``` | |
| The 0.8833 s/iter is the mean of `Perf/collection_time` (0.816 s) plus | |
| `Perf/learning_time` (0.059 s) over the last 10% of the run. The | |
| `Perf/total_fps` of 1.12e5 agrees: `4096 x 24 / 0.816 = 1.20e5` during | |
| collection. | |
| The run directory also holds 105 training videos and a `git/twoleg.diff` | |
| recording commit `8ee9bc3` as the provenance. | |
| ## What was copied from microduck_rl | |
| `training/README.md:4-6` states the intent: "a small, self-contained first pass | |
| modelled on `microduck_rl`". The logs show the intent was carried into the | |
| agent configuration and stopped there. | |
| **measured**, comparing `params/agent.yaml` in each run directory: | |
| | setting | twoleg 25k run | microduck velocity run | same? | | |
| |---|---|---|---| | |
| | `class_name` | `PPO` via `OnPolicyRunner` | `PPO` via `OnPolicyRunner` | yes | | |
| | `num_steps_per_env` | 24 | 24 | yes | | |
| | `num_learning_epochs` | 5 | 5 | yes | | |
| | `num_mini_batches` | 4 | 4 | yes | | |
| | `learning_rate` | 0.001, `adaptive` | 0.001, `adaptive` | yes | | |
| | `gamma` / `lam` | 0.99 / 0.95 | 0.99 / 0.95 | yes | | |
| | `clip_param` | 0.2 | 0.2 | yes | | |
| | `entropy_coef` | 0.01 | 0.01 | yes | | |
| | `desired_kl` | 0.01 | 0.01 | yes | | |
| | `max_grad_norm` | 1.0 | 1.0 | yes | | |
| | `value_loss_coef` | 1.0 | 1.0 | yes | | |
| | `seed` | 42 | 42 | yes | | |
| | `actor.hidden_dims` | 256, 128, 64 | 512, 256, 128 | no | | |
| | `critic.hidden_dims` | 256, 128, 64 | 512, 256, 128 | no | | |
| | `max_iterations` | 25000 | 2500 | no | | |
| | `save_interval` | 500 | 2500 | no | | |
| | `logger` | `tensorboard` | `wandb` | no | | |
| | `algorithm.symmetry_cfg` | key absent | `null` | no | | |
| Every PPO hyperparameter is identical. The network is half as wide at every | |
| layer and one layer shallower. The iteration budget is ten times larger. | |
| ## What was dropped | |
| **measured**, comparing `params/env.yaml` in each run directory. The twoleg | |
| reward block has 8 terms, the microduck one has 16. | |
| | reward term | twoleg weight | microduck weight | status | | |
| |---|---|---|---| | |
| | `track_linear_velocity` | 2.0 | 2.0 | both | | |
| | `track_angular_velocity` | 1.5 | 2.0 | both, weakened | | |
| | `upright` | 1.0 | 2.0 | both, weakened | | |
| | `pose` | 1.0 | 1.0 | both | | |
| | `action_rate_l2` | -0.1 | -0.1 | both | | |
| | `body_ang_vel` | -0.05 | -0.05 | both | | |
| | `angular_momentum` | -0.02 | -0.02 | both | | |
| | `dof_pos_limits` | -1.0 | -1.0 | both | | |
| | `air_time` | absent | 3.0 | dropped | | |
| | `foot_clearance` | absent | -2.0 | dropped | | |
| | `foot_swing_height` | absent | -0.25 | dropped | | |
| | `foot_slip` | absent | -0.1 | dropped | | |
| | `self_collisions` | absent | -1.0 | dropped | | |
| | `head_pose_tracking` | absent | 2.0 | dropped | | |
| | `head_pose_bias` | absent | 0.0 | dropped | | |
| | `body_pose_tracking` | absent | 0.0 | dropped | | |
| Four of the eight dropped terms are foot-gait shaping. `air_time` at weight | |
| 3.0 is the only positive reward in the microduck velocity task that pays for | |
| lifting a foot. None of the four survive in twoleg. | |
| Other structural differences, **measured** from the same two files: | |
| | | twoleg | microduck | | |
| |---|---|---| | |
| | `terrain_type` | `plane` | `generator`, 4 difficulty levels | | |
| | curricula active in the log | none | 8, logged as 9 `Curriculum/*` tags | | |
| | domain-randomization events | 6 | 13 | | |
| | domain randomization of mass, armature, joint friction | absent | present | | |
| | IMU misalignment (up to 6 deg, random axis) and 0 to 1 step lag | absent | present | | |
| | terminations | 2 (`time_out`, `fell_over`) | 4, plus `nan_state` with a contact-sensor check | | |
| | scalar tags logged | 26 | 46 | | |
| | `nan_guard.enabled` | `false` | not present in the resolved file | | |
| | command ranges | `lin_vel_x 0..0.3`, `ang_vel_z -0.4..0.4` | `lin_vel_x -0.4..0.4`, `ang_vel_z -1.0..1.0` | | |
| `head_pose_tracking`, `head_pose_bias`, and `body_pose_tracking` are not | |
| available to twoleg by construction. Its MJCF has no head-pose command block, | |
| so dropping them is correct. The other five drops are not forced by the robot. | |
| ## How much microduck_rl trained its duck to walk | |
| Three measurements, in increasing order of difficulty. | |
| **The shipped walking policy took about 3,750 iterations.** | |
| `microduck_rl/AGENTS.md:224-225` records the deployed walk and stand policies | |
| by wandb run id and iteration: "walk = 441tzs6d@3750, stand = | |
| 69u48n8l@9750". That line exists because Hub ONNX files ship with | |
| `run_path=None`, so the run had to be recovered by matching the last-layer | |
| weights against the wandb checkpoints. | |
| **The author's own budget for a gait is 4,000 to 6,000 iterations.** | |
| `microduck_rl/AGENTS.md:231-232`: "Budgets: simple episodic tricks ~1000 iters | |
| at 4096 envs; gaits and curriculum-heavy recovery need 4000-6000." The same | |
| file gives the 1 to 2 hour figure that `README.md:34` repeats: "~1-2 h for a | |
| usable gait at 4096 envs". | |
| **The local velocity logs hold 6 runs totalling 19,994 iterations.** | |
| Recovered by taking the highest `model_*.pt` symlink in each | |
| `wandb/offline-run-*/files/` directory, which survives after checkpoints are | |
| pruned. The highest single run reached 4,999 iterations at 2,048 envs. | |
| | task | runs | iterations summed | longest run | | |
| |---|---|---|---| | |
| | `running` | 25 | 901,250 | 76,500 | | |
| | `sprint` | 19 | 395,953 | 34,250 | | |
| | `velstand` | 13 | 31,491 | 4,999 | | |
| | `velocity` (the walking task) | 6 | 19,994 | 4,999 | | |
| | `ground_pick` | 12 | 14,996 | 4,999 | | |
| | all 14 tasks | 99 | 1,413,666 | 76,500 | | |
| **derived**: `1,413,666 x 24 x 2048 = 69,484,511,232` env-steps across the | |
| whole repo. Walking specifically: `19,994 x 24 x 2048 = 982,745,088`. | |
| **Fast gait work went into `running`, not `velocity`.** The longest single run | |
| is `2026-09-12_16-16-33_running-max-speed`: 15,000 iterations from checkpoint | |
| 61,500 to 76,500 in 6 h 26 m 43 s, at 4,096 envs on a flat plane, with | |
| `max_iterations: 80000` configured. | |
| **The only speed number in either repo is a world-displacement measurement.** | |
| `microduck_rl/hf_upload_README.md:21-27` reports, for | |
| `model_76500.pt`, evaluated with `scripts/eval_sprint_speed.py`: | |
| | command | measured forward speed | | |
| |---|---| | |
| | 0.4 m/s | 0.11 m/s (16 resets) | | |
| | 0.8 m/s | 1.27 m/s | | |
| | 1.2 m/s | 1.55 m/s | | |
| | 1.6 m/s | 1.61 m/s | | |
| | 2.0 m/s | 1.66 m/s (p10 1.49, p90 1.85, max 1.93) | | |
| `AGENTS.md:233-238` and `hf_upload_README.md:30-34` both insist on this | |
| method: "Reward/curriculum metrics are NEVER reported as speed." | |
| **measured**, the `running` task's resolved reward block, for contrast with | |
| the `velocity` task: | |
| | term | weight | term | weight | | |
| |---|---|---|---| | |
| | `forward_progress` | 5.0 | `dof_pos_limits` | -1.0 | | |
| | `track_linear_velocity` | 2.0 | `foot_clearance` | -2.0 | | |
| | `heading_hold` | 1.5 | `self_collisions` | -1.0 | | |
| | `upright` | 0.75 | `foot_swing_height` | -0.25 | | |
| | `pose` | 0.15 | `action_rate_l2` | -0.02 | | |
| | `track_angular_velocity` | 0.5 | `foot_slip` | -0.05 | | |
| | `body_ang_vel` | -0.01 | `air_time` | 0.0 | | |
| | `angular_momentum` | -0.005 | | | | |
| `forward_progress` at 5.0 outweighs `track_linear_velocity` at 2.0 by two and a | |
| half times, and it is the only term in either repo that pays for covering | |
| ground rather than for matching a command. `air_time` is present but weighted | |
| 0.0, so the running gait was not bought with a swing-phase bonus. It was bought | |
| with a term that pays for displacement. Twoleg has no equivalent term at all. | |
| ## What the twoleg 25,000-iteration run produced | |
| **measured**, mean of the last 10% of iterations unless noted. | |
| | tag | twoleg 25k | microduck velocity 2.5k | | |
| |---|---|---| | |
| | `Train/mean_reward` | 80.18 (last) | 93.3 (last) | | |
| | `Episode_Reward/track_linear_velocity` | 1.378 | 1.184 | | |
| | `Episode_Reward/track_angular_velocity` | 1.239 | 0.454 | | |
| | `Episode_Reward/upright` | 0.991 | 1.621 | | |
| | `Episode_Reward/pose` | 0.879 | 0.572 | | |
| | `Episode_Reward/air_time` | tag absent | 0.837 | | |
| | `Episode_Termination/fell_over` | 0.027 | 0.441 | | |
| | `Train/mean_episode_length` | 997 | 916 | | |
| | `Metrics/twist/error_vel_xy` | 0.454 | 0.440 | | |
| | `Metrics/twist/error_vel_yaw` | 0.402 | 1.571 | | |
| | `Policy/mean_std` | 0.336 | 0.209 | | |
| | `Loss/entropy` | 4.45 | -2.19 | | |
| | `Perf/collection_time` (s) | 0.816 | 1.939 | | |
| | `Perf/total_fps` | 1.124e5 | 2.456e4 | | |
| | scalar tags | 26 | 46 | | |
| Two rows carry the answer. | |
| `Metrics/twist/error_vel_xy` is 0.454 at iteration 24,999 and was already | |
| 0.460 at iteration 100. The policy did not improve velocity tracking in | |
| 24,899 iterations. | |
| `Train/mean_episode_length` is 997 out of a 1000-step cap, and | |
| `Episode_Termination/fell_over` is 0.027 percent. The robot never falls and | |
| never gets cut short. It holds one pose for the full 20-second episode. | |
| **measured**, the last training video. `videos/train/rl-video-step-599040.mp4` | |
| is 100 frames at 50 fps, 2.0 s, and its step number is `24960 x 24`. Frames 0, | |
| 25, 50, 75, and 99 show the humanoid standing with both feet flat, knees | |
| together, no stepping, and no displacement against the floor grid. The same | |
| check on `rl-video-step-17280.mp4` (iteration 720) shows the same standing | |
| posture with a slight forward lean. | |
| **inference**: the twoleg policy learned to stand still. | |
| ## Why it stands still | |
| Read the reward table against the measured curve. | |
| The policy gets 0.991 of a possible 1.0 on `upright` and 0.879 of 1.0 on | |
| `pose`. Both are Gaussian terms that peak at perfect posture. Standing still | |
| maximises both, and `pose` is weighted 1.0. | |
| `track_linear_velocity` reaches 1.378 out of a possible 2.0. It is a Gaussian | |
| on the velocity error with `std = sqrt(0.1) = 0.316` m/s, so the reward is | |
| already above half scale while the robot sits inside roughly 0.2 m/s of the | |
| command. The twoleg command range is `lin_vel_x 0.0 to 0.3` m/s, half the | |
| magnitude of microduck's `-0.4 to 0.4`. Doing nothing earns most of that | |
| term's mass without moving. | |
| `air_time` is the only term that pays for a swing phase, and it is absent. | |
| `foot_clearance`, `foot_swing_height`, and `foot_slip` are absent too, so | |
| nothing opposes foot drag and nothing demands a lift. A biped with no reward | |
| for unweighting one leg has no reason to unweight one leg. `action_rate_l2` at | |
| -0.1 actively rewards holding still, because a still robot has a zero action | |
| derivative. | |
| That is the whole gap. `track_linear_velocity` is a tracking term, so it is | |
| satisfied by a robot that sits near its command, and a robot that sits still | |
| sits near a `0.0 to 0.3` m/s command. `microduck_rl` learned to run with | |
| `air_time` at weight 0.0, and it still learned a gait, but only because | |
| `forward_progress` at 5.0 was there to pay for ground covered. Neither a | |
| swing-phase bonus nor a displacement bonus exists in the twoleg run. | |
| **inference**: the reward function the twoleg run used has a global optimum at | |
| standing still. Ten times microduck's iteration budget cannot leave that | |
| optimum, because nothing in the gradient points out of it. This is a reward | |
| design problem, not a training budget problem. | |
| Two things make it worse than a neutral result. `upright` and | |
| `track_angular_velocity` were both weakened relative to microduck, which | |
| removes part of the pressure that keeps a biped stepping. The learning rate also | |
| trends down: `Loss/learning_rate` reads 7.6e-5 at iteration 10,000, having | |
| started at 1e-3, because the adaptive schedule hits the 0.01 `desired_kl` | |
| target. Over iterations 10,000 to 24,999 it averages 5.11e-4, about half the | |
| initial rate, and it reaches its 1e-5 floor at every instability step listed | |
| below. It is not monotonic, and it recovers to 1.95e-3, so this is a weaker | |
| signal than the reward table and should not be leaned on. | |
| ## Numerical instability in the same run | |
| **measured**: 120 of 25,000 logged iterations, 0.48 percent, have a negative | |
| `Train/mean_reward`. The worst is `-1.121e9` at iteration 24,417. The | |
| correlated tags at that step are `Episode_Reward/action_rate_l2 = -1.168e8` and | |
| `Loss/value = 1.288e9`. | |
| `action_rate_l2` is the squared change in the action vector, weighted -0.1. | |
| With `clip: null` and 15 actions, a finite bound of about -6.0 exists. A value | |
| of -1.168e8 requires a non-finite or overflowing action. `Loss/value` reaching | |
| 1.13e13 in the same window confirms the value head diverged with the actor. | |
| `Loss/learning_rate` reads 1e-5, its floor, at every one of these steps, then | |
| recovers on the next step. `Episode_Termination/fell_over` stays between 0.04 | |
| and 0.17 percent throughout, so this is not a physical fall. | |
| **inference**: the twoleg env had no guard to catch this. The microduck env | |
| terminates on `nan_state`, checked against the `feet_ground_contact` sensor, and | |
| logs `Episode_Termination/nan_state` every iteration. The twoleg env has | |
| `nan_guard.enabled: false` and `nan_policy: disabled`. These 120 iterations | |
| were silent. | |
| ## Gotchas for anyone reading these logs | |
| - `Episode_Reward/<term>` is the weighted value, not the raw signal. A term at | |
| weight 0 logs 0 whatever the policy does. `AGENTS.md:229-230` says this. | |
| - Total reward can rise on regularizers alone while the task never happens. | |
| `AGENTS.md:226-228` says this. The twoleg `mean_reward` sits at 80 to 86 | |
| from iteration 100 onward, and none of the movement metrics move with it. | |
| - `Metrics/twist/error_vel_xy` is the honest progress signal here, and it is | |
| flat. Watch it, not `mean_reward`. | |
| - Twoleg logs no `air_time`, `foot_clearance`, `foot_slip`, or | |
| `foot_swing_height` tag at all. The terms were removed from the config, not | |
| merely weighted to zero. An absent tag and a zero tag mean different things. | |
| - wandb checkpoint directories in `microduck_rl/wandb/offline-run-*/files/` | |
| are symlinks. When the run directory is pruned, the symlink dangles, but its | |
| target path still records the run name and the final iteration. That is the | |
| only surviving record of iteration counts for pruned runs. | |
| - `Task/mean_reward` for twoleg has 120 outliers spanning -1.1e9 to -3.9e6. Any | |
| statistic over the full run is dominated by them. The last-10% mean of | |
| -4.5e5 is not a training signal, it is the arithmetic of 8 outliers landing | |
| in that window. | |
| ## The training robot is not the reference robot | |
| `training/assets/twoleg.xml` is generated from `twoleg_mjcf/robot_twoleg.xml`, | |
| which is hand-built and has no provenance link to | |
| `robot_item/chest_both_arms_legs.xml`. Git confirms they are independent: | |
| `robot_twoleg.xml` landed in `7931963` and `59a2a98`; | |
| `chest_both_arms_legs.xml` landed later, in `d1591b5`. | |
| **measured**, `training/scripts/check_model_vs_reference.py`: | |
| | | reference | training | | |
| |---|---|---| | |
| | source | `chest_both_arms_legs.xml` via the `chest_move` rig | `twoleg_mjcf/robot_twoleg.xml` | | |
| | bodies | 121 | 32 | | |
| | joints | 19 | 16 (15 actuated plus the freejoint) | | |
| | actuators | 19 | 15 | | |
| | geoms | 262 | 94 | | |
| | mass, compiled | 4.4818 kg | 2.0625 kg | | |
| | world-pinned bodies | 32 | 1 | | |
| | freejoints | 0 | 1 | | |
| | shared joint names | **none** | **none** | | |
| The joint name sets are disjoint. `waist_test`, `hip_test`, `knee_test`, | |
| `ankle_test`, `abduct_*`, `shoulder_test`, `elbow_test`, `wrist_test` in the | |
| reference against `hip_roll`, `hip_pitch`, `knee`, `ankle`, | |
| `shoulder_pitch`, `shoulder_roll`, `elbow`, `head_yaw` in training. | |
| **The reference is a puppet, not a free body.** 32 of its 121 bodies attach | |
| directly to the world, including `L_waist_link`, `R_waist_link`, | |
| `abduct_L_link`, `abduct_R_link`, and `head_link`. Every limb hangs from the | |
| world rather than from the torso. Adding a freejoint to the root `u_beam` | |
| would move only the chest plate and leave the limbs pinned in the air. | |
| **measured**: the reference build emits no `freejoint`. Its physics mode works | |
| because the chains are pinned to the world, which is why | |
| `chest_both_arms_legs_move.py:46` can say `LEG_KP = 40.0` because "the legs | |
| carry every gram of leg under gravity". The legs swing from a fixed world | |
| anchor. Nothing falls over, and no foot ever needs to touch the ground. | |
| **measured**: the reference's collision masks are an inter-leg setup. Left-leg | |
| geoms are `contype=1 conaffinity=2`, right-leg geoms are `contype=2 | |
| conaffinity=1`, so the two legs collide with each other and with nothing else. | |
| A ground plane at `contype=1 conaffinity=1` is not in either mask. Dropped into | |
| a free-body simulation with a floor, the reference robot falls through it. | |
| The reference is authoritative for two things: the world pose of every part, | |
| and the measured pivot of every hinge (`leg_move_all.SIDES`, `chest_move.WAIST`, | |
| commented as measured to 0.000 mm). It is not authoritative for articulation, | |
| because it has none that survives a free body. | |
| **Consequence**: no reward change can make the old training robot move like the | |
| reference video, because they were different mechanisms with disjoint joint | |
| sets. | |
| **The rebuild is done.** `training/scripts/generate_twoleg_model.py` reads the | |
| reference assembly and its rig, re-parents the limb chains under the torso, | |
| recomputes each body's `pos` and `quat` from its world pose at the zero pose, | |
| drops the two duplicate waist hinges, and adds the freejoint, the `imu` site, | |
| and two foot sites at the soles. It writes both consumers in one pass, so | |
| `sync_twoleg_xml.py` is deleted. | |
| **measured**, `training/scripts/check_model_vs_reference.py` after the rebuild: | |
| | | reference | training | | |
| |---|---|---| | |
| | bodies | 121 | 121 | | |
| | geoms | 262 | 262 | | |
| | mass | 4.4818 kg | 4.4818 kg (ratio 1.000) | | |
| | world-pinned bodies | 32 | 1 | | |
| | freejoints | 0 | 1 | | |
| | shared joint anchors | | **0.00 mm on all 17** | | |
| The only joint differences are the two duplicate waist hinges the reference | |
| models twice, and the added freejoint. The training model now has the | |
| mechanism's true 17 actuated joints: 4 per arm (`abduct`, `shoulder_test`, | |
| `elbow_test`, `wrist_test`), 4 per leg (`L_waist_test`, `L_hip_test`, | |
| `L_knee_test`, `L_ankle_test`), and `head`. | |
| Two things had to be fixed that the reference never exercised, because its | |
| puppet never fell over or touched a floor: | |
| * **Collision masks.** The reference's leg geoms are `contype=1 conaffinity=2` | |
| on the left and `2/1` on the right, an inter-leg setup that leaves a ground | |
| plane at `1/1` in neither mask. The generator adds the ground bit, and a drop | |
| test now lands the robot on its feet and settles instead of falling through. | |
| * **Contact capacity.** 26 collision geoms against the old model's two foot | |
| boxes overflows mjlab's default `nconmax`, so the env sets it to 200. | |
| Cost: the faithful model is heavier to simulate. At 64 envs the old model ran | |
| 5,783 steps/s and the rebuilt one runs 878, about 6.5x slower, because it is | |
| 121 bodies and 262 geoms instead of 32 and 94. | |
| ## Result: it walks slowly | |
| **measured**, `training/scripts/eval_walk.py` on the final checkpoint of run | |
| `2026-09-28_23-48-21`, holding a fixed 0.30 m/s forward command for 500 control | |
| steps (10 s) across 16 environments: | |
| | | value | | |
| |---|---| | |
| | net displacement | 2.081 m in 10 s (p10 1.517, p90 2.367) | | |
| | net speed | **0.208 m/s** | | |
| | mean speed, direction ignored | 0.212 m/s | | |
| | tracking of the command | 69 percent | | |
| Mean speed and net speed agree, so the motion is forward travel rather than | |
| oscillation. The video at `videos/train/rl-video-step-142000.mp4` shows a | |
| stepping gait with the floor grid advancing. | |
| **measured**, the same run's last 5 percent of iterations: | |
| | term | value | target | | |
| |---|---|---| | |
| | `Episode_Reward/track_linear_velocity` | 1.689 | β₯ 1.0 | | |
| | `Episode_Reward/air_time` | 1.361 | β₯ 0.5 | | |
| | `Metrics/twist/error_vel_xy` | 0.220 | < 0.40 | | |
| | `Episode_Reward/upright` | 1.913 | β₯ 1.5 | | |
| | `Episode_Termination/fell_over` | 0.058 | < 10 | | |
| | `Train/mean_episode_length` | 984 | > 800 | | |
| For comparison, microduck's own velocity run at iteration 2499 reached | |
| `error_vel_xy` 0.440 and `air_time` 0.837. The twoleg run is better on both. | |
| Two bugs had to be fixed to get here, and both were silent: | |
| 1. **The root frame.** The chest body carries a 90 degree rotation of its own. | |
| Making it the free body's root left mjlab reading `projected_gravity` as | |
| `[9.81, 0, 0]` instead of `[0, 0, -9.81]` while the robot stood, so the | |
| `upright` reward and the `fell_over` termination both measured the wrong | |
| axis. The policy learned to lie down, and reported `upright` 0.065 and | |
| `air_time_mean` 9.76 s. The generator now inserts an empty world-aligned | |
| base body as the root and hangs the chest from it. | |
| 2. **Joint regularisation.** The reference leaves `damping`, `frictionloss` and | |
| `armature` unset and has massless link bodies, so a hinge could rotate | |
| near-zero inertia and the solver returned NaN within milliseconds of a free | |
| fall. The generator sets `armature=0.01`, `damping=0.05`, | |
| `frictionloss=0.02`. | |
| Also deliberate: the 26 mesh collision geoms became one box per foot, because | |
| mesh-mesh contact on the near-parallel leg brackets destabilises the solver. | |
| The kinematics, mass and pivots are untouched. | |
| ## Verify this file | |
| ```bash | |
| cd /home/kenpeter/work/twoleg | |
| # reward and weight diff | |
| python3 - <<'PY' | |
| import re | |
| def weights(p): | |
| out=[];inr=False | |
| for l in open(p).read().splitlines(): | |
| if re.match(r'^rewards:\s*$',l): inr=True; continue | |
| if inr and re.match(r'^[a-z_]+:\s*$',l): break | |
| if inr: | |
| m=re.match(r'^ ([a-z_0-9]+):\s*$',l) | |
| if m: out.append([m.group(1),None]); continue | |
| m=re.match(r'^\s+weight:\s*(\S+)',l) | |
| if m and out: out[-1][1]=m.group(1) | |
| return out | |
| A=weights("training/logs/rsl_rl/velocity/2026-09-27_23-59-17_velocity/params/env.yaml") | |
| B=weights("/home/kenpeter/work/microduck_rl/logs/rsl_rl/velocity/2026-09-05_13-08-20_auto5000/params/env.yaml") | |
| for k in sorted(set(dict(A))|set(dict(B))): | |
| print(f"{k:30s} twoleg={dict(A).get(k,'-'):>6s} microduck={dict(B).get(k,'-'):>6s}") | |
| PY | |
| # the video claim | |
| ffmpeg -v error -i training/logs/rsl_rl/velocity/2026-09-27_23-59-17_velocity/videos/train/rl-video-step-599040.mp4 \ | |
| -vf "select='eq(n\,0)+eq(n\,25)+eq(n\,50)+eq(n\,75)+eq(n\,99)',tile=5x1" -frames:v 1 /tmp/twoleg_last.png | |
| ``` | |
| Scalar tables come from the two tfevents files with each project's own venv, | |
| which already has tensorboard: | |
| ```bash | |
| /home/kenpeter/work/twoleg/training/.venv/bin/python - <<'PY' | |
| from tensorboard.backend.event_processing import event_accumulator as ea | |
| a=ea.EventAccumulator("training/logs/rsl_rl/velocity/2026-09-27_23-59-17_velocity/" | |
| "events.out.tfevents.1790517561.kenpeter-ubuntu.23723.0", size_guidance={'scalars':0}) | |
| a.Reload() | |
| print(sorted(a.Tags()['scalars'])) | |
| for t in ("Train/mean_reward","Metrics/twist/error_vel_xy","Loss/learning_rate"): | |
| s=a.Scalars(t); print(t, "n=",len(s), "last=",s[-1].value, "step100=",s[100].value) | |
| PY | |
| ``` | |
| ## Walk loop (agentic, video-gated) | |
| `/pp` runs this loop. The loop condition is a picture, not a reward: the | |
| policy must walk slowly on two legs in a rendered rollout. Contact numbers | |
| cross-check, the frames decide. | |
| ``` | |
| /pp <walk task> (pp/.opencode/commands/pp.md routes to poteto-mode) | |
| β | |
| v | |
| walk_loop.py (training/scripts/walk_loop.py, strategy registry inside) | |
| β state: .audit/walk_loop_state.json trail: .audit/twoleg-walk.tsv | |
| v | |
| ββ round ββββββββββββββββββββββββββββββββββββββββββββββ | |
| β 1. resume-train, bounded (1000 iters, 2048 envs) β | |
| β 2. render rollout at held 0.1 m/s (MUJOCO_GL=glfw) β | |
| β 3. verify_walk.py watches the frames: β | |
| β pink feet -> left/right clusters per frame β | |
| β vertical traces correlated (out of phase=walk) β | |
| β 4. PASS? stop, report video. FAIL? log row, next β | |
| β strategy. Table empty? stop, do not spin. β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PASS predicate (scripts/verify_walk.py): | |
| speed within 0.05 of command AND each foot down >= 20 pct | |
| AND contact alternation <= -0.2 AND frame alternation <= -0.2 | |
| Anything inconclusive fails. Exit 0 walks, 1 does not, 2 error. | |
| Strategy registry (only these run; the loop invents nothing): | |
| continue-1000: resume-train 1000 iters, same config (H1-H5 stack) | |
| Run it: | |
| uv run python scripts/walk_loop.py --from-run <run> --from-checkpoint <pt> --rounds 1 | |
| uv run python scripts/walk_loop.py --verify-only --from-run <run> --from-checkpoint <pt> | |
| ``` | |
| **measured**: the verifier self-test reads alt -0.998 on synthetic | |
| alternating frames and +1.000 on together frames. On `model_10993.pt` it | |
| reads speed +0.070, duties 0.07/0.95, frame alt +0.42, verdict NO-WALK, | |
| which matches the human-watched training video frame by frame. | |
| ## 2026-10-03: forward-axis bug, fixed; loop still NO-WALK | |
| **measured** (this session) | |
| - The command envelope had the facing axis swapped: it sampled | |
| `lin_vel_x` as the forward command, but on this model forward is +Y. | |
| Evidence, all from `training/assets/twoleg.xml` loaded in the training | |
| venv (`mujoco 3.14.0`) at spawn pose: | |
| - `L_ankle_link` / `R_ankle_link` at (Β±0.0442, -0.0255, -0.2332): | |
| left-right is X. | |
| - Knee world axes are Β±X, so knee flexion steps the foot along Y: | |
| forward is Y. Matches `symmetry.py:55` ("Left-right is X, forward is Y"). | |
| - Consequence of the old config, measured earlier in-session comment: | |
| holding `lin_vel_x = 0.10` moved the policy 0.080 m/s along body+x | |
| and 0.001 m/s along body+y β a side-step, not a walk. | |
| - **Fixed** in `twoleg_velocity_env_cfg.py` (uncommitted): | |
| `LIN_VEL_X = (-0.1, 0.1)` lateral, `LIN_VEL_Y = (0.0, 0.3)` forward. | |
| Run `2026-10-03_08-28-43_velocity/params/env.yaml` already carries the | |
| fixed ranges. | |
| - Stale camera comment at the same file's viewer block was corrected | |
| (it claimed the front is +x; the value `azimuth = 180` was already | |
| correct for a +y-facing robot). | |
| **measured**, walk loop status (`training/scripts/walk_loop.py`, | |
| `/tmp/opencode/loop_infinite3.log`): | |
| ``` | |
| round 5 NO-WALK -> HARD EVIDENCE fails: frame_alt=-1.0 L=1.598 R=0.901 | |
| roll=0.0 -> keep training -> continuing (infinite loop) | |
| ``` | |
| **measured**, `videos/train/rl-video-step-22000.mp4` of the same run, | |
| 8 frames at 2 fps: robot stands in place, sways, one foot lifts slightly. | |
| No stepping, no forward travel. The run is at `model_7500.pt` (~2048 envs, | |
| resumed from the deleted `2026-10-03_07-25-04_velocity` run's | |
| `model_6500.pt`). That resume source directory has been removed from disk, | |
| so restarting the loop process will fail on `--agent.load-run`; only the | |
| live process still holds the checkpoint. | |
| **inference**: the resumed checkpoint carries the old-axis command | |
| distribution, and the NO-WALK metrics are unchanged across 5 loop rounds. | |
| A clean from-scratch run with the fixed axis is the next verifiable unit; | |
| resume state is suspect. | |
| **measured**, actuator inventory (knee question): each leg has 4 actuated | |
| joints β `{L,R}_waist_test`, `{L,R}_hip_test`, `{L,R}_knee_test`, | |
| `{L,R}_ankle_test`. Each knee is a single hinge (`axis=Β±X`, | |
| `ctrlrange=-1.4 1.4`). There is no second knee DOF to wire; if the | |
| physical knee uses two servos, they drive this one knee coordinate. | |