# Tube-sort starting-state stability

Measured 2026-09-18 with `scripts/audit_tubesort_settling.py`; the definition
lives in `src/mjlab_gs/tasks/tubesort/stability.py`.

## Definition of "settled"

With the arm holding still for a settling window (100 control steps = 5 s at
20 Hz), during the final 20 steps (1 s), every world must satisfy:

* no active tube's centre outside the centre basket's footprint (nothing over
  the rim or through the floor);
* max tube linear speed <= 0.02 m/s and max angular speed <= 0.5 rad/s;
* no tube centre moved more than 3 mm across the tail window;
* no contact involving a tube penetrates deeper than 3 mm (read from the raw
  warp contact buffer, every step, not once at reset);
* all state finite.

The audit runs a matrix of cases, 8 random layouts each: tube count 4/7/10,
nominal vs full domain randomization (pose, friction, size, mass), the tube
radius/length/mass extremes pinned (fat-long-heavy, thin-short-light), and two
alternative centre-basket footprints. The basket geometry is rebuilt from the
layout for the footprint cases (changing `cfg.layout` alone only moves the
scoring boxes, not the walls).

## Baseline (state left by the previous session)

Cylinder colliders, grid placement with tubes tilted up to 90 deg (so 70 mm
tubes on a 49.5 mm grid spawn interpenetrating), solref 0.04, solimp width
0.02, margin 1 mm, free-joint damping 0.5 and armature 5e-4.

| case | settled | ejected | peak v (m/s) | tail v max (m/s) | tail ω max (rad/s) | tail drift max (mm) | tail penetration max (mm) | rest step max |
|---|---|---|---|---|---|---|---|---|
| nominal-4 | 5/8 | 0 | 0.66 | 0.0977 | 0.17 | 3.6 | 1.6 | 100 |
| nominal-10 | 1/8 | 0 | 0.66 | 0.0613 | 0.13 | 4.0 | 25.9 | 99 |
| dr-4 | 6/8 | 0 | 0.55 | 0.0234 | 0.05 | 1.3 | 16.6 | 94 |
| dr-7 | 3/8 | 0 | 0.63 | 0.0775 | 0.21 | 2.9 | 32.1 | 94 |
| dr-10 | 0/8 | 0 | 0.63 | 0.0709 | 0.16 | 1.8 | 24.7 | 99 |
| fat-long-heavy-10 | 0/8 | 1 | 0.56 | 0.0541 | 0.22 | 1.8 | 22.5 | 98 |
| thin-short-light-10 | 1/8 | 0 | 0.64 | 0.0547 | 0.02 | 2.0 | 25.4 | 96 |
| small-basket-7 | 4/8 | 0 | 0.63 | 0.0439 | 0.26 | 2.4 | 21.6 | 100 |
| wide-basket-10 | 2/8 | 1 | 0.71 | 0.0659 | 0.09 | 1.9 | 21.9 | 99 |

22/72 worlds settled. Persistent penetration of 2-5 cm: with a 0.04 s contact
time constant the steady-state penetration under gravity is about g*tau^2 =
1.6 cm, so tubes sank into each other and never stopped.

## What was changed, in the order it was found

1. **Placement** (`sample_pile`): tubes lie on their sides at random yaw; a
   greedy packer (64 candidates per tube) assigns layers so that nothing
   starts interpenetrating; the heap is built by gravity.
2. **Contact stiffness**: solref 0.01 s, solimp (0.9, 0.95, 0.001).
   Penetration went to 0.0 mm at once.
3. **Margin 0**: with margin 1 mm per geom the pair floated on a 2 mm cushion
   (contact active at +1.8 mm) and crept down the heap at ~1 mm/s.
4. **Capsule collider**: capsule-box and capsule-capsule are primitive
   multi-contact pairs in MuJoCo Warp; cylinder pairs go through the convex
   path and get one contact, on which a lying tube rocks.
5. **No joint damping or armature, rolling friction instead**: a 30 g tube
   has I ~ 2e-6 kg m^2, so damping 0.5 / armature 5e-4 froze its rotation;
   tubes landed crosswise on single contacts and slid off the heap for
   seconds. condim 6 with rolling friction 0.0005 lets them roll into a gap
   and stop (0.002 made piles jitter; impratio and impedance sweeps changed
   nothing).

Intermediate sweeps: `c*_`, `d*_`, `e*_`, `f*_`, `g*_` JSON/log files here.

## Result (final defaults, seed 12345)

| case | settled | ejected | peak v (m/s) | tail v max (m/s) | tail ω max (rad/s) | tail drift max (mm) | tail penetration max (mm) | rest step max |
|---|---|---|---|---|---|---|---|---|
| nominal-4 | 8/8 | 0 | 0.49 | 0.0001 | 0.01 | 0.1 | 0.1 | 71 |
| nominal-10 | 7/8 | 0 | 0.79 | 0.0426 | 1.74 | 5.0 | 0.2 | 94 |
| dr-4 | 7/8 | 0 | 0.49 | 0.0248 | 0.02 | 0.5 | 0.8 | 96 |
| dr-7 | 8/8 | 0 | 0.75 | 0.0016 | 0.16 | 1.0 | 0.2 | 23 |
| dr-10 | 6/8 | 0 | 0.98 | 0.1583 | 4.59 | 17.7 | 0.8 | 95 |
| fat-long-heavy-10 | 7/8 | 0 | 0.98 | 0.0218 | 0.48 | 4.1 | 0.7 | 93 |
| thin-short-light-10 | 8/8 | 0 | 0.98 | 0.0023 | 0.13 | 0.7 | 0.2 | 60 |
| small-basket-7 | 8/8 | 0 | 0.81 | 0.0025 | 0.17 | 1.4 | 0.2 | 14 |
| wide-basket-10 | 7/8 | 0 | 0.98 | 0.0357 | 2.43 | 3.6 | 0.2 | 89 |

66/72 settled, 0 ejected. Failures are single tubes still moving or spinning
at 5 s; rerunning the same seeds does not reproduce them (GPU contact
ordering), i.e. marginal states rather than a systematic artefact.

## Result (fresh seed 7, 16 layouts per case)

| case | settled | ejected | peak v (m/s) | tail v max (m/s) | tail ω max (rad/s) | tail drift max (mm) | tail penetration max (mm) | rest step max |
|---|---|---|---|---|---|---|---|---|
| nominal-4 | 16/16 | 0 | 0.49 | 0.0038 | 0.25 | 2.7 | 0.1 | 20 |
| nominal-10 | 16/16 | 0 | 0.98 | 0.0065 | 0.27 | 1.7 | 0.2 | 93 |
| dr-4 | 13/16 | 0 | 0.49 | 0.0241 | 0.90 | 2.6 | 0.4 | 120 |
| dr-7 | 16/16 | 0 | 0.73 | 0.0126 | 0.16 | 1.0 | 0.2 | 69 |
| dr-10 | 16/16 | 0 | 0.98 | 0.0098 | 0.26 | 1.2 | 0.3 | 86 |
| fat-long-heavy-10 | 13/16 | 2 | 1.11 | 0.0215 | 0.21 | 0.6 | 0.4 | 113 |
| thin-short-light-10 | 16/16 | 0 | 0.71 | 0.0068 | 0.33 | 2.9 | 0.2 | 84 |
| small-basket-7 | 16/16 | 0 | 0.98 | 0.0026 | 0.17 | 1.2 | 0.3 | 75 |
| wide-basket-10 | 16/16 | 0 | 0.84 | 0.0024 | 0.11 | 0.8 | 0.2 | 98 |

138/144 settled, 2 tubes ejected during the drop (fat-long-heavy case). The
expert waits for the pile to be quiet (40-120 steps, 10 consecutive quiet
steps) before its first pick, and the recorder drops an ejected tube from the
task at step 120 instead of discarding the episode.

Note: the two matrices above were run with the original 230 x 100 mm centre
basket. The basket was then enlarged to 380 x 128 mm for graspability (see
`tasks/tubesort/grasp.py`), with the pile spawned in its middle band.

## Result (final layout, 380 x 128 mm basket, seed 12345, 5 s window)

| case | settled | ejected | peak v (m/s) | tail v max (m/s) | tail ω max (rad/s) | tail drift max (mm) | tail penetration max (mm) | rest step max |
|---|---|---|---|---|---|---|---|---|
| nominal-4 | 8/8 | 0 | 0.49 | 0.0002 | 0.01 | 0.2 | 0.1 | 17 |
| nominal-10 | 8/8 | 0 | 0.79 | 0.0019 | 0.14 | 1.2 | 0.2 | 71 |
| dr-4 | 6/8 | 0 | 0.49 | 0.0333 | 0.00 | 0.0 | 0.8 | 100 |
| dr-7 | 8/8 | 0 | 0.56 | 0.0071 | 0.07 | 1.4 | 0.2 | 68 |
| dr-10 | 7/8 | 0 | 0.62 | 0.2447 | 9.75 | 21.7 | 0.4 | 94 |
| fat-long-heavy-10 | 6/8 | 0 | 0.70 | 0.2714 | 11.06 | 16.6 | 0.7 | 96 |
| thin-short-light-10 | 8/8 | 0 | 0.60 | 0.0028 | 0.23 | 0.8 | 0.2 | 62 |
| small-basket-7 | 8/8 | 0 | 0.81 | 0.0133 | 0.49 | 2.5 | 0.2 | 15 |
| wide-basket-10 | 7/8 | 0 | 0.98 | 0.0719 | 4.35 | 3.3 | 0.2 | 88 |

66/72 settled, 0 ejected. The six unsettled worlds each have one tube still
rolling (angular speed 4-11 rad/s, 1-2 cm of drift in the last second).

With a 10 s window on the three weakest cases (`final_layout_200.json`):
dr-10 8/8, wide-basket-10 7/8 (one tube ejected during the drop),
fat-long-heavy-10 4/8. The fat-long-heavy pile (radius x1.06, length x1.08,
mass x1.4, all pinned at once) keeps a rolling tube in about half its worlds
even at 10 s; this is the one open stability case. The expert waits for the
pile to be quiet for 10 consecutive steps (bounded to 2-6 s) before its first
pick, so a still-rolling tube delays the pick rather than corrupting it.
