LIVE DEMO  //  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.

repo ebootheee/palletballet · api palletballet-api.boothe.io · stack python · mujoco · fastapi

Generate a pallet, see what it survives

Each click randomly configures a pallet — items, weights, wrap, temperature — and asks the live API to compute its safe operating envelope. The work is real: a MuJoCo simulation runs several times behind a Brent's-method search to find the failure boundary on each axis.

palletballet — analyze idle
$ click "ANALYZE" to spawn a pallet and run the safety solver.
  the solver runs 5–8 MuJoCo simulations under a brentq search.
  expected wall time: ~400–800 ms.
0 runs this session
Where the data flows when you click that button
browser
  └─ POST https://palletballet-api.boothe.io/pallet/random
       └─ Cloudflare edge → Tunnel → home server → MockRandomAdapter → PalletConfig
  └─ POST https://palletballet-api.boothe.io/safety/analyze
       └─ ThresholdAnalyzer (brentq sweep on speed + accel)
            └─ MuJoCo solver × N runs → (mu, phi, w, ω) trace per run
                 └─ failure detector → FailureMode + tip angle
       └─ SafetyResult{ max_speed_mps, max_accel_mps2, governing_failure, ... }
back to browser → rendered above

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.

Failures in slow motion

Trust comes from watching things go wrong. The UI ships with a rogues' gallery of known-bad scenarios; 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.

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.

  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 47-body MJCF: base, items, contact pairs with the right friction coefficients, a moving conveyor surface. 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 contact forces every step. Failure detectors look for tip angle past threshold, pairwise distance drift between items, and pallet-vs-belt slip.

  5. 05

    The threshold analyzer searches the boundary

    A brentq root-finder 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 v2uvDockerCloudflare TunnelGitHub Actions → GHCRWatchtower

The whole runtime self-hosts on a Ryzen 1700X with 16 GB RAM. Cold start (API + MuJoCo) is ~3 seconds; a single safety analysis lands in 400–800 ms; cache hits return in ~1 ms. 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.