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 · api palletballet-api.boothe.io · 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
API traffic — every call this page makes, 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. Available at https://palletballet-api.boothe.io/docs. Cache hits return in ~1 ms.

How this page actually serves you a physics result

 your browser
       │
       ▼
 ┌─────────────────────┐         ┌───────────────────────────────┐
 │ boothe.io/          │         │ palletballet-api.boothe.io    │
 │ palletballet        │         │                               │
 │  (Astro on          │ fetch   │  ┌─────────────────────────┐  │
 │   Cloudflare        │────────►│  │ Cloudflare Tunnel       │  │
 │   Workers)          │  CORS   │  └─────────────┬───────────┘  │
 └─────────────────────┘         │                ▼              │
                                 │  ┌─────────────────────────┐  │
                                 │  │ home server (docker)    │  │
                                 │  │  ├── palletballet:8000  │  │
                                 │  │  ├── cloudflared        │  │
                                 │  │  └── watchtower         │  │
                                 │  └─────────────────────────┘  │
                                 └───────────────────────────────┘
                                            ▲
                                            │ pull on every push to main
                                            │
                                  ghcr.io/ebootheee/palletballet
                                            ▲
                                            │ build & push
                                            │
                                       GitHub Actions

Stack

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

The whole runtime self-hosts on a Ryzen 9 home server. A single conveyor sim lands in 60–200 ms; a full envelope search in 400–800 ms; cache hits in ~1 ms. Batch mode chews through ~7 pallets/second across all cores. You're hitting that machine through a Cloudflare Tunnel right now.

+~
~+

> 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 a working copy of the same system this page is talking to — and the launch post walks through every endpoint, the batch runner, and the physics.