Fidelity, banging, and what humans actually do: the reference-engine investigation

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.


1. Is 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

The defect the user was sensing was real

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.

The banging is 0% the reference

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.


2. Does a proper approach policy exist anywhere public?

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:

  1. Every "expert" Push-T dataset in the world-model literature is the same 206 human demonstrations, replayed with noise. There is no scripted expert behind any of them. The gentle, contact-holding motion people have in mind is a human moving a mouse.
  2. The one scripted heuristic that does exist is the same naive rule the workbench's policy started from — go behind the block on the goal→block axis and push through — and it fails every episode.

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.


3. How humans push the T

Data and a provenance decision

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.

The headline: humans do not retreat and ram

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.

Touching

Pushing — the strongest result

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 — when to let go

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.

Choosing the next contact

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.

What this means for a controller

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.


4. Which findings are physics and which are the human

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 split is measurable, not arguable

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}}$$

What the physics contributes (fixed by the reference; would change on any other engine)

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.

What the human contributes (portable across engines)

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

Consequences for the controller


5. Where this leaves the work

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