LIVE PHYSICS  //  checking…

PalletBallet

A pallet enters the conveyor zone. Geometry, mass, wrap, temperature, friction — all of it shoved through MuJoCo. Out the other side: the speed it can actually survive, the failure mode that would otherwise govern, and a replay of the moment it would have toppled. Below, you get the dials before the solver shows its answer. Beat it if you can.

repo ebootheee/palletballet · engine mujoco · wasm · fp64 · stack python · mujoco · fastapi · three.js

You set the belt. The physics decides.

Every dispatch is a live simulation: your dial settings go to the API, MuJoCo runs the conveyor profile against the actual pallet, and the 3D replay you watch is the solver's own trace — every box pose, every frame, straight from the physics engine. After the dust settles, the solver reveals the envelope it would have set. Score by shipping fast without shipping it onto the floor.

waking the solver…
loading scenarios…
3D stage loads when you scroll here
t = 0.00 s belt 0.00 m/s
Solver calls — every request the game makes, in the API's shape, copyable as curl

No canned outcomes: the replay frames are MuJoCo's integrator output at 30 Hz, and the envelope reveal is a fresh bisection search (5–8 sims) per pallet. If the numbers feel conservative, read how the failure detectors work — then argue with the physics.

A static speed table is conservative on good pallets and blind to weird ones

Cold-storage warehouses move pallets across long conveyors at speeds set once, usually pessimistically, by a person guessing at the worst case. A perfectly stacked dairy pallet gets the same 1.0 m/s as a top-heavy unwrapped tower of frozen meat that should be capped at 0.4 m/s. The first is being throttled. The second is being launched.

PalletBallet skips the table. For each pallet — described by its actual geometry, mass distribution, wrap, and thermal state — it runs a small batch of physics simulations and answers a narrower question:

Given this pallet, in this thermal state, what conveyor motion profile is safe right now?

The result is per-pallet decisioning: speed, acceleration, deceleration, lateral g-tolerance, and the failure mode that would otherwise govern — slip, top-item slide, load shift, or tip-over. A PLC can throttle the next conveyor zone with it. A WMS can flag the pallet for re-wrap. A human can look at the replay and agree. You just did, above.

Failures in slow motion

Trust comes from watching things go wrong. Here's a top-heavy unwrapped pallet hitting an aggressive start ramp. Every frame is from a real solver run, not a render — same physics, same engine, same trace the API returns to the game above.

Six stages, end to end

  1. 01

    Inputs land in a single contract

    Scanner output, WMS export, a manual stack builder, or the random adapter all converge on the same RawInputs Pydantic schema. The configurator turns it into a PalletConfig — items with positions, masses, fragility, wrap, temperature. The curated pallets you just played live at GET /scenarios.

  2. 02

    Friction is temperature-aware

    Cold-chain pallets move through frost-melt regimes where mu drops by half. The friction model interpolates a calibration table and applies a transition penalty when the pallet is near 0 °C with recent thermal motion — the regime that actually causes slips on real lines.

  3. 03

    The MJCF builder emits MuJoCo XML

    Each pallet becomes a free-body MJCF scene: base, items, contact pairs with the right friction coefficients, a velocity-actuated conveyor. Wrap becomes weld constraints with stiffness per wrap type. No GUI, no rendering — pure physics.

  4. 04

    The solver runs the conveyor profile

    Acceleration ramp, target speed, dwell, deceleration. The trace records pallet pose, item poses, and belt state every step. Failure detectors look for tip angle past threshold, item slide in the pallet frame, pairwise load shift, and pallet-vs-belt slip. With include_replay, the API hands the whole 6-DoF pose history to your browser — that's what the 3D stage plays back.

  5. 05

    The threshold analyzer searches the boundary

    A bisection search walks the speed and acceleration axes. Five to eight simulations later, you have a tight upper bound on each — the line between "survives" and "fails." Results are cached by config fingerprint, so duplicates cost nothing.

  6. 06

    FastAPI hands back a SafetyResult

    Plus the sweep points, the dominant failure mode, the cache hit count, and a confidence score. That is the shape the API returns — run the repo and it is self-documented at http://localhost:8000/docs. Cache hits return in ~1 ms.

How this page actually serves you a physics result

It used to call a FastAPI service on a home server through a Cloudflare Tunnel. As of August 2026 the solver ships to you instead: the same MuJoCo build, at the same fp64 precision, compiled to WebAssembly and run in a worker thread in this tab. The API still exists in the repo — it is just no longer in the path.

 your browser
 ┌──────────────────────────────────────────────┐
 │ boothe.io/palletballet                       │
 │  (Astro on Cloudflare Workers, static)       │
 │                                              │
 │   main thread          worker thread         │
 │  ┌──────────────┐     ┌────────────────────┐ │
 │  │ game UI      │────►│ @palletballet/     │ │
 │  │ three.js     │◄────│   engine           │ │
 │  └──────────────┘     │  ├── mujoco.wasm   │ │
 │                       │  │   (fp64, 2.5 MB)│ │
 │                       │  ├── solver        │ │
 │                       │  ├── detectors     │ │
 │                       │  └── envelope      │ │
 │                       │      search        │ │
 │                       └────────────────────┘ │
 └──────────────────────────────────────────────┘
       no network · no server · works offline

 the same engine, still packaged as a service:

       docker compose up  ──►  localhost:8000
                               ├── /solve
                               ├── /safety/analyze
                               └── /docs
                          ghcr.io/ebootheee/palletballet

Stack

Python 3.12MuJoCo 3.xFastAPIPydantic v2three.jsAstrouvDockerCloudflare TunnelGitHub Actions → GHCRWatchtower

Those numbers are from the Python service on a Ryzen 9 box: a single conveyor sim in 60–200 ms, a full envelope search in 400–800 ms, cache hits in ~1 ms, and ~7 pallets/second in batch mode across all cores. In your browser the same engine runs about 1.4–2.2× slower per step — typically a few hundred milliseconds for the search above, which is what the console is reporting.

+~
~+

> Read the source. Run your own.

The code is open. The Dockerfile and tunnel config are in infra/. Cloning and running docker compose up -d gets you the same engine this page is running, with the HTTP API in front of it — and the launch post walks through every endpoint, the batch runner, and the physics.