# Dense bin of scanned household objects in MuJoCo: stability and picking

Standalone MuJoCo 3.13 (CPU) study, 2026-09-18. Objects: Google Scanned
Objects via kevinzakka/mujoco_scanned_objects (V-HACD convex parts).
Library: 1030 objects scanned, 281 suitable for a 40 x 28 x 20 cm tote
(max extent 16 cm, min extent 1.5 cm, hull volume <= 1.5 L).

## Model preparation (`binpick/objects.py`, `binpick/scene.py`)

* Mass = 300 kg/m^3 x convex-hull volume (clamped 20 g .. 2 kg); the
  per-object part density is scaled so the V-HACD parts sum to that mass;
  the visual mesh carries zero mass. (Without this the default 1000 kg/m^3
  on overlapping parts plus the visual mesh gave 3-6x too much mass.)
* Degenerate V-HACD parts (thinner than 4 mm, volume < 0.2 cm^3, or fewer
  than 6 vertices) are dropped: 7.5% of parts. A 0.5 mm, five-vertex
  sliver was measured 9 mm into the bin floor.
* Contacts: solref 0.01 s, solimp (0.9, 0.95, 0.001), margin 0, condim 4,
  friction 0.6; free joints with no damping or armature; implicitfast,
  Newton, elliptic cone, impratio 10, timestep 1 ms.

## Filling (`binpick/fill.py`)

Objects are *poured*: released one at a time 6 cm above the lowest cell of a
height map of the heap (with the neighbourhood under the object's bounding
radius cleared), random orientation, next object once the previous is at
rest. Filling stops when the median height-map cell reaches the rim. Two
spawn bugs were found and fixed this way: a bounding-sphere fullness test
that stopped after 3 objects, and half-extent clearance that ignored the
mesh origin sitting at the object's base (launched a plant pot 1 m).

## Stability audit (`scripts/audit_fills.py`)

Settled = with nothing actuated for 8 s, in the last second: no object
centre outside the footprint, max speed <= 20 mm/s, max angular speed
<= 0.5 rad/s, drift <= 3 mm, object contact penetration <= 5 mm, all
finite. 24 random fills per row, ~21 objects each.

| physics | settled | objects | ejected | tail v max (mm/s) | tail pen max (mm) | peak pen (mm) | rest (s) max |
|---|---|---|---|---|---|---|---|
| default (dt 1 ms, implicitfast, Newton, solref 0.01, margin 0) | 23/24 | 20.5 | 0 | 135 | 1.0 | 3.3 | 7.3 |
| timestep 2 ms | 20/24 | 20.4 | 2 | 224 | 4.2 | 9.2 | 8.0 |
| solref 0.005 | 21/24 | 21.0 | 1 | 111 | 1.7 | 2.4 | 7.5 |
| solref 0.02 (MuJoCo default) | 20/24 | 20.2 | 1 | 45 | 9.2 | 23.7 | 7.6 |
| noslip 5 iterations | 20/24 | 21.5 | 4 | 14 | 2.1 | 2.8 | 6.2 |
| Euler integrator | 21/24 | 20.6 | 1 | 42 | 1.1 | 4.9 | 7.3 |

The default row's one failure is an object toppling off the heap at 7.3 s
(tail speed 135 mm/s, then still). Penetration never exceeds 3.3 mm in any
default fill, and no object ever leaves the bin.

## Picking audit (`scripts/audit_picks.py`, `binpick/pick.py`)

A floating parallel gripper welded softly to a mocap target (20 ms time
constant, so the pile can push it back), 60 mm fingers, 100 mm opening,
force-limited 40 N grip. Each pick: highest object whose narrow width fits,
jaws across that width, descend to 55 mm below its top or until pushed back
20 mm, close, lift, carry out, release. 16 bins, 96 picks:

* successes: 27 (28%) -- failures are fine, the point is the physics;
* object-object penetration during picks: max 13.6 mm; gripper-object (the 40 N squeeze): max 19.4 mm;
* neighbours moving faster than free fall from their recent height (plus gripper speed) allows: 2 samples in 1 pick(s) -- i.e. the high neighbour speeds (p95 1.30 m/s) are objects falling after a neighbour is pulled out, not solver energy;
* ejected while picking: 1; non-finite states: False.
