The physics oracle: an AI-written controller, tested against a compiled plant
Every number on this page came from a real run through plimsoll's docker tier (container) on 2026-09-18. The controller is the only thing the caller sent. The plant (control engineering's word for the thing being controlled, here a cart on a rail with a pole hinged on top of it that the controller keeps upright by pushing the cart) is compiled to WebAssembly, and it and the runner that closes the loop are baked into the sandbox image. The runner ran the controller as a separate process, recorded every 10 ms tick, and the SHA-256 of that record is the fingerprint.
What the plant did, replaying the runner's record
Linear state feedback. Only + and *, so its arithmetic is plain IEEE doubles with no engine-specific libm.
The browser only replays the runner's record; no physics runs here. Loops every 20 s; a fall holds for two seconds, then loops.
The verdict
- Accepted controller, run 1
- Accepted controller, run 2
- IDENTICAL
- The agent's first draft
- DIFFERS
- Where the draft parts from the accepted run
- What happened to the pole
Two different questions. Identity: the same fingerprint means the controller ran identically, to the last bit, and a different one means a different trajectory, which may or may not matter (a controller that changes the last digit of one number gets a different hash and the same balance). Acceptance: whether it balanced, and where the cart ended, are the runner's separate facts above. The record is written by the runner after the controller has exited, so the controller cannot alter it.
The accepted controller, the only file the caller sent
// The accepted controller: linear state feedback. Each line on stdin is the plant
// state "x v theta omega"; the answer is one number, the force on the cart. Only
// + and * so the arithmetic is plain IEEE doubles with no engine-specific libm.
import { createInterface } from 'node:readline';
const kp = 40, kd = 5, kx = 1, kv = 1;
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const [x, v, theta, omega] = line.split(' ').map(Number);
const u = kp * theta + kd * omega + kx * x + kv * v;
process.stdout.write(u + '\n');
});
The agent's first draft
// The agent's first draft: the same structure with the position and velocity
// terms of the wrong sign, a classic mistake (the cart must move under the pole,
// not away from where it stands). It balances the pole for a moment and then
// runs off the rail. The runner records exactly where.
import { createInterface } from 'node:readline';
const kp = 40, kd = 5, kx = -1, kv = -1;
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const [x, v, theta, omega] = line.split(' ').map(Number);
const u = kp * theta + kd * omega + kx * x + kv * v;
process.stdout.write(u + '\n');
});
The two runs, side by side
Left: pole angle, clipped at the horizontal (±1.57 rad). Right: cart position, clipped at ±3 m. The draft balances the pole for a few seconds and then the cart runs away; the dotted line marks the first tick at which its record differs from the accepted one.
How it works
- The plant is image content. A cart-pole simulator in the Reference FMU style, force as an input and no controller inside, compiled to WebAssembly and baked into plimsoll's module image at
/models/cartpole.wasm. Register a simulator = build an image. - The runner is image content too.
/oracle/run.mjsloads the plant through Node's WASI support, spawns the caller's controller as a separate process, and each tick sends the state on its stdin and reads one number back, the force. It records every tick and writes the trajectory after the controller has exited. - The run is an ordinary project run. One file, one step, one artifact, through plimsoll's ordinary project API on the docker tier: read-only root, no network, the syscall allowlist. The daemon knows nothing about physics.
- The fingerprint holds because of where the arithmetic lives. The plant's
sinandcosare wasi-libc code inside the module, WebAssembly has no fused multiply-add and rounds once per operation, and the controller's own arithmetic is plain doubles. Same bytes in, same bytes out: the sister repository's local run on a different Node major produced this exact fingerprint before the image did.
What this does not show
- The controller cannot alter the plant or the record, but it can read them: the runner and the plant sit on the container's read-only root, visible to any process in the run. Nothing here is secret. A hidden plant or a hidden runner needs the controller in its own namespace with only the state-and-force channel crossing, which is a different build of the same idea.
- The runner protects the record from the controller; it does not make the controller's arithmetic portable. A controller that calls
Math.sinties its fingerprint to the JavaScript engine's implementation. - Forward Euler at 1 ms, the Reference FMUs' solver: adequate for a balancing demo, not a claim about accuracy.
- A fingerprint is identity within one build of the plant, and it is not a score. The bytes come from the compiled module's own arithmetic, so recompiling
cartpole.wasmwith another toolchain, or changing the simulator, can move every fingerprint on this page while each controller still balances exactly as well as it did. Two controllers whose gains differ in the last digit balance the same and hash differently. That is what makes the number a reproducibility check and not a ranking. - Nothing here ranks two controllers that both balance. The runner records whether and when the pole fell and where the cart ended; it does not report settling time, peak angle, or control effort, so this page cannot say which of two balancing controllers is better. Those are fuzzy measures over the same trajectory the runner already records: a grader, the component that scores a run, could compute them from that record without any change to the run.
- One plant. A second plant is another simulator file and another image build.
Reproduce: make docker-images && go run ./examples/oracle in the plimsoll repository, with docker. The page is written from the run.