Duck Cards

Duck Cards: the evidence

Can a Microduck walk one square and turn a quarter turn per card, reliably enough for a classroom? We wrote down what reliable means before running anything, ran it in Pollen Robotics' own simulator, ran it again on a different machine, and publish every row. So far the answer is no, and this page says exactly where and why.

Summary

The question

Duck Cards are printed coding cards: a child lays Walk, Turn and Repeat cards in a row and the duck acts them out on a 30 cm grid. A timed card (walk for two seconds) does not land on a square: walking speed varies with the floor, the policy and the battery. A closed-loop card walks until odometry says one square and turns until the heading says a quarter turn. The question is whether that is good enough, measured, before any child relies on it.

System under test

Pre-registered criteria

Written into docs/PACKS.md on 2026-10-15, before any result was in, and not changed since. A condition passes only if every criterion passes.

Every share on this page is the observed share with its Wilson 95% interval in brackets.

Conditions

Each condition is fixed before its rows run
ConditionWhat is perturbed
nominalNothing. State at 20 Hz, the reader's rate.
randomizedPer row: floor friction ×U(0.5, 1.2); masses and inertias ×U(0.95, 1.05); IMU heading bias U(-5, 5)° drifting at U(-0.5, 0.5)°/s; added state latency U(0, 0.15) s; odometry noise 5 mm and 0.5° per frame; odometry scale U(0.95, 1.05). State at 20 Hz.
randomized-10hzAs randomized, with state at 10 Hz, the rate Studio asks for.
stressed-10hzThe far end of every range at once: friction ×0.5, mass ×1.05, bias 5° and drift 0.5°/s, latency 0.15 s, odometry scale 0.95 or 1.05. State at 10 Hz.

Evidence ladder

  1. Rung 1: Pollen's duck-sim, Monte Carlo. Every criterion in every condition, for the pre-registered rows and separately for rows with a Walk back card. Not passed.
  2. Rung 2: a second, clean machine. The same commit and conditions from a fresh checkout on different hardware, matching every verdict. Run on 2026-10-16 on Hugging Face Jobs (x86, AMD EPYC); 38 of 40 verdicts matched, so not reproduced.
  3. Rung 3: a real duck on a taped 30 cm grid. Studio's Measure my duck protocol, tape-measured and filmed. Not started; any fall sends the work back to rung 1.

Only when all three pass does a card pack turn closed-loop cards on for real ducks.

Results

Pre-registered rows, duck-card-loop/1. First machine: a Raspberry Pi 5. Second machine: Hugging Face Jobs cpu-upgrade. 455 rows per condition on each.

Pre-registered rows: share of rows within the bar
ConditionCriterionFirst machineSecond machine
nominalpositionPASS 98.7% [97.2, 99.4]PASS 99.1% [97.8, 99.7]
nominalheadingPASS 98.7% [97.2, 99.4]PASS 98.9% [97.5, 99.5]
randomizedpositionPASS 96.9% [94.9, 98.2]PASS 97.8% [96.0, 98.8]
randomizedheadingPASS 96.7% [94.6, 98.0]PASS 98.0% [96.3, 99.0]
randomized-10hzpositionPASS 97.1% [95.2, 98.3]PASS 96.0% [93.8, 97.5]
randomized-10hzheadingPASS 96.9% [94.9, 98.2]FAIL 94.9% [92.5, 96.6]
stressed-10hzpositionFAIL 86.8% [83.4, 89.6]FAIL 84.4% [80.8, 87.4]
stressed-10hzheadingFAIL 87.3% [83.9, 90.0]FAIL 85.9% [82.4, 88.8]

Two of the 40 rung 1 verdicts differ, both heading at randomized 10 Hz: this row, and the same criterion for Walk back rows below. The intervals overlap; a repeat on the first machine got 93.8%. We read it as a true rate near 95%, on the line, and count it as failing. Falls were 0 in every condition on both machines, and every Time and Ending badly criterion passed.

Walk back rows (403 per condition) and the packs' long rows (60 to 70 per condition)
Rows, conditionCriterionFirst machineSecond machine
Walk back, randomized-10hzpositionFAIL 92.8% [89.9, 94.9]FAIL 93.1% [90.1, 95.1]
Walk back, randomized-10hzheadingPASS 96.0% [93.6, 97.5]FAIL 93.8% [91.0, 95.8]
Walk back, stressed-10hzpositionFAIL 79.9% [75.7, 83.5]FAIL 79.9% [75.7, 83.5]
10 cards or more, randomizedposition41.7% [30.1, 54.3]33.3% [22.7, 45.9]
10 cards or more, randomized-10hzposition41.7% [30.1, 54.3]36.7% [25.6, 49.3]
10 cards or more, stressed-10hzposition0.0% [0.0, 5.2]0.0% [0.0, 6.0]

In evaluation: the mat as a compass (duck-card-loop/4)

A taped grid on the floor, 30 cm squares, sits outside the duck and does not drift. After each Walk or Turn card the duck takes one frame from its head camera, keeps the dark tape pixels whose rays meet the floor 0.2 to 1 m ahead, and finds the small turn and shift that put them back on the grid. Heading comes back to the nearest quarter turn and position to the nearest square. In the simulator the mat is rendered from the duck's true pose, and each condition adds errors in what the duck believes about its camera (pitch, roll, mount, height, brightness, noise). Pre-registered with /3's rule before any row ran.

Run 1 (4,800 rows on Hugging Face Jobs), against /1 on the same machine type:

Rejected by its own rule. The machine looks like the cause. This is not proven. 170 rows stopped because the duck's pose stopped arriving for half a second; the /1 run had 2. 17 falls came before the mat had changed anything. The host's load median was 260, against 21 for /3's run. The mat was rendered in software, which by default takes every core for each frame, beside four simulated ducks on 8 vCPUs. On the Pi, with a GPU, development rows had no falls.

The repeat was pre-registered after run 1 and before any row of it. It changes only the instrument: one render thread, at a lower priority than the duck. The seeds, code, criteria and rule are run 1's. It counts only if 10 or fewer of its 4,800 rows lose the pose. If more do, it is called inconclusive, and the next idea starts from /1. It is running now. Its result goes here, beside run 1, whatever it is.

Rejected candidates

The Walk lead (duck-card-loop/2)

duck-card-loop/2 gave Walk cards the lead Turn cards have: stop short by the measured coast, settle, and lead later Walk cards by the row's mean coast. Its parameters were chosen on development seeds only, then evaluated once on fresh seeds (4,800 rows), on both machines. Against /1 on the same kind of machine:

It trades one failing criterion for another and makes long rows worse. Not adopted.

Calibrate at every stop (duck-card-loop/3)

The duck stands still for 5 s before a row and between cards; the yaw rate it reports while standing is taken as drift (a pooled least-squares slope over every still window) and taken out of heading and position. Pre-registered with its decision rule before any row ran; chosen on development seeds; evaluated once on fresh seeds, 4,800 rows on Hugging Face Jobs. The rule: it replaces /1 only if no verdict /1 passed becomes a fail and long rows' position at randomized 10 Hz rises. Against /1 on the same machine type:

Rejected by its own rule. The rows say why: the estimate follows the injected drift but sits about 0.15°/s low in every condition, nominal included. A standing duck's yaw really moves (the policy creeps or settles), and a sensor inside the duck cannot tell that from drift. Subtracting it for the whole row turns a small true creep into a heading error that grows with time. Heading needs a reference outside the duck, which is the next idea.

Why long rows fail

The controller already aims every card at an absolute place on the board, so its own errors do not stack. What stacks is what the duck believes about itself. A heading sensor drifting at up to 0.5°/s is off by up to 30° after a minute, and a minute is about a ten-card row; odometry that reads 5% long or short is off by 5% of every metre walked. Neither is visible to the controller. The enemy is elapsed time and distance, not the number of cards, and no per-card tuning removes a wrong sensor reading.

What is being tried next

Each is a new controller version, held to the same criteria above, evaluated on fresh seeds on a quiet machine, and published here whether it passes or not. One at a time, in this order:

  1. Calibrate at every stop. Tried and rejected: a standing duck is not still enough for its own sensor to measure drift.
  2. The mat as a compass. Grid lines seen by the duck's camera fix heading to the nearest quarter turn, and position to the nearest square. In evaluation: run 1 rejected on a starved machine, the repeat running.
  3. Spend less time. Merge consecutive Walk cards into one walk and two quarter turns into a half turn, so a row takes less time to drift.
  4. A reset in the game. A nest square where the child re-seats the duck, and long challenges split into chapters no longer than the length that passes.

Separately, Measure my duck will record how much a real duck's heading drifts standing still. If that argues for a gentler simulated condition, the condition is added and pre-registered as new; the conditions above are not edited.

Reproduce it

From a fresh checkout of duck-studio on Linux (x86 or arm64), with nothing else running:

sh cardreader/sim/validate.sh
python3 cardreader/sim/mc/mc_summary.py cardreader/sim/results/raw \
  cardreader/sim/results/raw-acceptance cardreader/sim/results/raw-back

validate.sh builds Pollen's stack at pinned commits, runs system identification and every Monte Carlo batch, and writes HOST.txt (machine, commit, Pollen SHAs) and a per-minute load log. Each session's randomness is seeded from the batch seed and the rows already done, so a rerun is a fresh statistical sample, not a replay: compare verdicts and intervals, not rows. A loaded machine makes simulated bodies fall behind real time and fall; check the logs for "behind real time" before trusting a fall.

Limits

Data and code

Questions, a second reproduction or a correction: email or open an issue on duck-studio.