This document consolidates one thread of the workbench's development: the user reported that the scripted
pusher "bangs" into the T on pymunk, that superdex bangs too, and that pymunk-reference — although
much gentler — still goes back and forth. The underlying question was sharper than the symptom: if the
original code is in hand, how can the values and the behaviour be a bit off? Answering that required
separating three things that had been tangled together: how faithful the reference engine is, what the
scripted policy was doing, and what the human demonstrations that everyone trains on actually contain.
The sections below follow the order in which the questions were settled.
pymunk-reference a 100% replica?Verdict: it was 95%; it is now ~98%. The missing 2% is deliberate and written down.
The audit went element by element against the published gym-pusht source, with each row backed by a measurement rather than a reading.
| element | grade | evidence |
|---|---|---|
PD law, kinematic pusher, dt, substeps |
100 | 1e-9 against an independent re-implementation of the control loop |
| Block mass, inertia typo (3000), CoG (0, 45) | 100 | pinned by tests |
Frictionless shapes, damping = 0, iterations = 10, walls, settle step |
100 | pinned |
| Full trajectories under identical actions | 100 | 0.000e+00 over 120 steps on contact-heavy seeds |
| 5 recorded human episodes replayed | 100 | ≤ 0.002 px over 738 frames |
Angle % 2π, keypoints, contact count, block_cog / damping options |
100 | pinned |
| RNG draw for seed → sampled state | 100 | identical numbers |
| Start pose the workbench actually used | was wrong; fixed | see below |
| Observation dtype (float32 vs float64) | deviation, ~1.5e-5 | documented |
Success >= vs >; truncation inside vs a 300-step TimeLimit outside |
deviation | documented |
| Rendering (OpenCV vs pygame) | deviation | pixel observations are not the dataset's pixels |
gym-pusht's reset(seed=783958) places the block at (231.16, 332.93). The workbench placed it at
(271, 267). The published draw had been reproduced exactly, but the sampled pose was then applied
through the workbench's own round-tripping setter (angle first) instead of gym-pusht's legacy setter
(position, then angle). Because pymunk rotates a body about its centre of gravity, that order shifts the
block by roughly 75 px. Every reference episode watched up to that point had therefore started from a pose
that seed never produces upstream — a genuine "the values are a bit off".
The fix routes a sampled start through the legacy setter, exactly as upstream does, while an explicit
reset(state=...) keeps round-tripping (that path is for everything that is not a published seed).
The proof is on the live server, not only in tests:
live pymunk-reference, seed 783958 -> block (231.162, 332.927) angle 4.229
gym-pusht reset(seed=783958) -> block (231.162, 332.927) angle 4.229
Two tests that had pinned the old, wrong behaviour were rewritten to assert the corrected semantics. The
cross-version parity harness (ours on pymunk 7.3.0, the published env on 6.11.1 in its own virtualenv —
it cannot run on pymunk 7 because it calls a Space API that was removed) still reports 0.000e+00.
Neither gym-pusht nor LeWM ships a controller that approaches the block. gym-pusht's data was driven by
human teleoperation; LeWM's WeakPolicy is seeded random relative offsets clipped to the block's
neighbourhood. The retreat-and-ram cycle is entirely the workbench's own ScriptedPushPolicy. Measured on
seed 783958 over 150 control steps:
| engine | separate contact events | peak approach speed |
|---|---|---|
| pymunk | 25 | 348 px/s |
| superdex | 17 | 364 px/s |
| pymunk-reference | 10 | 234 px/s |
| lewm | 11 | 239 px/s |
pymunk-reference only looks gentler because a kinematic pusher cannot bounce off the block. So "you
have the original code but the behaviour is off" resolves cleanly: the environment is a replica; the
thing pushing in it was never part of the original.
The rule set for the search was strict: a policy counts only if its code is readable or its generated data is inspectable. Hearsay and README claims without artefacts do not count.
Verdict: no public, working scripted approach-and-push controller for the pymunk Push-T exists.
| candidate | kind | code | data | holds contact? | status |
|---|---|---|---|---|---|
LeRobot pusht (206 episodes) |
human teleop | — | yes | yes, human | verified |
DINO-WM pusht_noise (18.5k) |
human demos + action noise | — | yes | inherited | verified (paper App. A.1) |
LeWM lewm-pusht (20k) |
the same DINO-WM data | — | yes | inherited | verified (paper App. E) |
stable-worldmodel WeakPolicy |
random, clipped near the block | yes | yes | no | verified |
jimchen2/pushT-dataset-example |
scripted heuristic | yes | 10 episodes | no | verified — 0/10 success, max coverage 0.47 |
| Xie / Chen / Goldberg, Revisiting Push-T with Agentic Robotics (arXiv 2608.18227) | state-machine controller, 100% over 200 seeds | no | no | claimed | unverified — "will be posted online"; nothing found on GitHub |
interactive_world_sim planner |
scripted, MuJoCo ALOHA, random-direction pushes | yes | yes | yes | verified — different environment, not goal-directed |
| Model-Based Diffusion, CRISP | trajectory optimisers on their own physics | yes | — | — | planners, not heuristics |
Two things are worth stating plainly:
The only claimed working controller (the Goldberg-lab paper) describes plan → approach → push → retreat, a contact library, a quasi-static model, and greedy contact selection by dominant pose error. No code or data yet, so by the rule above it does not exist — but its architecture is the informed design, and it explicitly includes a retreat phase, so "never retreat" is not the target either.
Three options were put to the user: build the controller from the paper's description, fix the retreat-and-ram in the existing heuristic, or replay the human demonstrations as the policy. The user's answer was a fourth: measure the humans first.
The LeWM paper's dataset is the DINO-WM set, which is the Diffusion Policy human teleop episodes replayed
with injected action noise to reach 18.5k–20k trajectories. The pushing skill in that data is a human's;
the noise is synthetic. For velocity, acceleration and jerk statistics the noised copies would be actively
misleading, so the analysis uses the 206 clean demonstrations (LeRobot pusht_keypoints, 8 keypoints
per frame, 10 Hz). Block pose is recovered per frame by a rigid fit to the keypoints (max residual 3.8e-05
px); the dataset's reward column was checked against the repository's coverage() to 2.2e-07.
All 206 episodes contain contact; the first 50 were analysed and the remaining 156 held out to score the
contact-selection rule. Contact is defined as distance(agent centre, T outline) - 15 <= 1 px; the gap
histogram has a spike in (−1, 0.5] px from the collision slop and a flat tail beyond, so the threshold is
not sensitive.
Per-frame roles were assigned from contact state, the agent's velocity split into normal and tangential components at the nearest outline point, and block motion.
| role | share of frames | median run | median speed (px/s) | median |accel| (px/s²) |
|---|---|---|---|---|
| APPROACH | 13% | 4 frames | 69 | 147 |
| TOUCH | 2.5% | 1 | 57 | — |
| PUSH | 39% | 7 | 48 | 88 |
| SLIDE | 7% | 2 | 50 | 145 |
| RETREAT | 2.7% | 2 | 106 | 567 |
| REPLAN / orbit | 35% | 8 | 122 | 305 |
Retreat is 2.7% of all motion. What replaces it is orbiting: 72% of no-contact motion is tangential to the outline at a gap of about 37 px, followed by a four-frame radial approach. A typical episode has three contact bouts of about 17 frames, with 16 frames between them. The dominant role sequence is idle → approach → touch → push → slide → push.
Block rotation follows the lever arm of the push about the CoG: correlation r = 0.87, slope 0.56 °/s per px, and the sign is correct in 100% of frames in which the block rotates faster than 2 °/s. The lever is chosen deliberately, scaled to the angle error:
| |angle error| | median lever from the CoG line | corrective sign |
|---|---|---|
| 0–5° | 5.7 px (through the CoG) | 57% |
| 10–20° | 34 px | 92% |
| 30–45° | 42.5 px | 99% |
A released bout takes the position error from 96 px to 48 px and the angle error from 53° to 5°, with 0.95 of the CoG displacement along the goal direction.
Release is triggered by geometry, not by distance. Push alignment (the angle between the push normal and the CoG→goal direction) is 26° at touch and 89° at release — above 45° in 91% of releases. The block is stopped first (0.4 px/s on the release step); the pusher then swings about 89° around the block and touches again roughly 16 frames later.
The unfitted rule "push where the inward normal points at the goal, as close to the CoG line as possible" predicts the human's edge in 57% of touches under cross-validation and 49% on the external hold-out (540 touches), against 12.5% chance and 23% for always choosing the majority edge. A fitted contact-point model (alignment, torque × angle error, off-centre penalty; weights 1.79, 2.97, 1.92) reaches 56% / 51% and halves the perimeter error (24 px vs 32 px). The torque term does not change which edge is chosen; it moves the contact point along it. A mirrored decision table (goal direction in the block frame × angle error) is in the full report.
Orbit at ~120 px/s at a 37 px gap → four-frame normal approach at ~70 px/s → push at ~48 px/s with a lever of 0 px below 5° error and ~42 px above 10° → slide (two frames) to re-aim while alignment stays below 45° → release when alignment exceeds 45–60° with the block stopped → swing ~90° → next contact from the goal-normal rule.
This was the user's sharpest question: of everything in the analysis, what is contributed by the already
defined elements — PD law, kinematic pusher, dt, inertia, damping — and what by the operator?
The environment is a deterministic function of the action stream, so the two contributions can be separated by computation. The pure PD law was re-simulated in free space from each recorded start, fed only the recorded actions, and compared with the recorded agent path:
| frames | median error | p90 |
|---|---|---|
| no contact (n = 3065) | 0.00 px | 0.00 |
| in contact (n = 3120) | 0.00 px | 0.00 |
Zero in contact is the decisive number: the pusher is kinematic, the block never acts on it, and the entire agent trajectory is the PD law applied to the mouse. Every agent-side statistic therefore factors as
$$\text{agent path} = \underbrace{\text{PD}(k_p{=}100,\ k_v{=}20,\ dt{=}0.01)}{\text{physics, fixed}} \circ \underbrace{\text{mouse target stream}}{\text{human}}$$
The PD response to a 100 px step command is 29.6 → 61.6 → 80.8 → 95.6 px over four control steps, with no overshoot, 90% settled at step four, and a peak of 320 px/s: a critically damped lag of about one control step. Its consequences are not human decisions:
| element | what it puts into the data |
|---|---|
PD law, dt, substeps |
a one-step lag and a ~10 px lead (median |target − agent| = 10.1 px); halves the human's acceleration and quarters the jerk — mouse |accel| medians are 224 / 300 / 500 / 1153 px/s² for push / approach / orbit / retreat, the agent's are 88 / 147 / 305 / 567. About half of the "gentle" look is filter. |
| kinematic pusher | "push depth" carries no force. The block is displaced by a moving wall; "push at 48 px/s, block follows at 32" is kinematic slip, not contact mechanics. |
| inertia typo (3000 vs 7875 geometric), CoG | the 0.56 °/s per px rotation gain is rigid-body kinematics under the understated moment; with the correct inertia the same lever would give roughly 40% of the rotation rate. |
damping = 0 |
the block stops on the release step (0.4 px/s) because velocity is annihilated every substep — not a skill. |
| frictionless shapes | no tangential drag, so slides are cheap. |
Measured on the raw commanded targets before any filtering. In every role the agent goes exactly where the
mouse points (cos(lead, motion) = 1.00), so these are genuinely the operator's decisions:
| decision | evidence from the mouse stream |
|---|---|
| speed by phase: slow in contact, fast when free | 45 px/s (push), 41 (touch), 51 (slide) vs 114 (orbit), 184 (retreat) |
| orbit rather than retreat | 72% tangential free motion; retreat 2.7% of frames; standoff 37 px |
| head-on aim | 17° incidence, 72% within 30° |
| edge selection by goal geometry | goal-normal rule 57% / 49%; nearest edge 9% |
| lever offset scaled to angle error | 5.7 px at < 5°, 42.5 px at 30–45°, corrective 99% |
| release on geometry, not distance | alignment 26° at touch, 89° at release |
| pacing | ~3 bouts of ~17 frames, 16 frames between |
pymunk,
superdex) the mapping is different again.| item | state |
|---|---|
pymunk-reference start pose |
fixed, proven on the live server, pinned on seed 783958 |
| gym-pusht parity | 0.000e+00 across pymunk majors; deviations documented |
| public approach policy | none exists; the only claimed one has no code or data |
| human behaviour analysis | docs/human_pushing_analysis.md, scripts/analyze_human_pushing.py, 15 figures |
| physics-vs-human decomposition | §8 of that report |
| scripted policy rewrite | not started — the design above is the spec; the decision to build it is the user's |