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 session of 12 rows: 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. Per row: 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]

Not adopted: 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.

The repeat (4,800 rows, load median 16, 0.05 s a frame):

So it is not adopted and not shipped. The code stays, unused unless a spec asks for it. A later candidate that keeps the mat will be pre-registered as new, with fresh seeds.

A control: the shipped loop, run again

The mat's repeat failed on 8 falls, judged against one run of the shipped loop (/1) that had none. So /1 was run again on the same machine type with fresh seeds, 4,800 rows, its rule written down first: 3 or more falls would mean the falls criterion cannot tell a candidate from the simulator's own falls.

A secondary analysis was written then, before any row of the next candidate was read. It compares a candidate with both /1 runs pooled, by a permutation test over sessions, with all 80 "worse" tests held to 5% together (Holm). Run on /1 against /1 it finds nothing, both ways. Looking back, as exploration only, with verdicts unchanged: calibration is still worse with no drift at all. The mat's falls are within /1's own. Its real cost is Walk back rows under stress (32% of rows miss on position, against 20%), and its gain on long rows is clear (93% against 31%). A later mat candidate has that to fix, not falls.

Kept, then built on: merge cards (duck-card-loop/5)

Long rows fail because the sensed pose drifts with time, so a row that takes less time should drift less. Consecutive Walk cards run as one walk, up to a cap, and two quarter turns the same way run as one half turn. The board, the cards and every sentence the duck says are unchanged. Pre-registered before any row, with the same rule as before.

Choosing the cap, on development rows only (60 rows a condition, not a result): long rows at randomized 10 Hz passed on position 8 of 15 with no merging, 11 with two walks at a time, 7 with three, 2 with four, and 3 with every walk merged. Every stop lets the next card re-aim at the board; one long walk loses that. The cap is 2.

The evaluation (4,800 rows on Hugging Face Jobs, fresh seeds, no rows lost the pose):

The shipped app keeps /1. /5 is where the next candidate starts, not a change to what Studio runs.

The new base: the mat on the nest (duck-card-loop/7)

The mat as a compass (above) lifted long rows more than anything else, and was not adopted because Walk back rows under stress got worse. Read after the control, those rows split on one thing: the simulated camera's tilt error. With the camera tilted 2° one way, 89 of 214 rows missed by more than 10 cm; tilted 2° the other way, 1 of 142. A camera tilted other than the duck believes stretches the floor along its view, so the mat's fix lands a few centimetres short along the walk. That was a hypothesis, so it was tested as one.

The evaluation (4,800 rows on Hugging Face Jobs, fresh seeds, no row lost the pose):

Long rows, position within 10 cm/6/7
nominal120 of 120119 of 120
randomized79 of 120117 of 120
randomized, 10 Hz76 of 120117 of 120
stressed, 10 Hz23 of 12057 of 120

Not in the app yet. The app ships /6. /7 needs the duck's head camera read in the app, the mat reader ported and held to the same test vectors as the sim's, and a printed mat. None of this shows a real lens, real light or a real taped mat.

Kept, then built on: a nest and chapters (duck-card-loop/6)

The first three fixes tried to make the duck drift less. This one accepts the drift and shortens what drifts. A long challenge plays in chapters, and between chapters the child puts the duck back on a nest square, as they already seat it at the start. Drift then builds over a chapter, not a whole row.

The evaluation (4,800 rows on Hugging Face Jobs, fresh seeds, 1 row lost the pose):

Long rows, position within 10 cm/5/6
nominal119 of 120120 of 120
randomized48 of 12079 of 120
randomized, 10 Hz50 of 12076 of 120
stressed, 10 Hz3 of 12023 of 120

In the app, from build 87. Both card readers now carry /6. In Packs, a row of more than 7 Walk and Turn cards plays in chapters: the board rings each nest and pauses on it, and Run on the duck stops at each nest until someone presses "The duck is on the nest". Closed-loop cards stay off on real ducks, so a real duck's cards are still timed; there the nest is simply where the child puts the duck back on the board, a game rule that nothing here measured. Measure my duck still runs each card on its own. The sim cannot show that a child will re-seat a duck this well, or will want to.

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. Not adopted: long rows far better, but Walk back under stress got worse (control).
  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. Kept as the base: a small gain, 37% to 42% on long rows.
  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. The new base, then built on: long rows 42% to 63%.
  5. The mat on the nest. Back to the compass, now that the nest gives it a place to lie, with the camera's tilt measured by the mat. The new base in the sim: long rows 63% to 97.5%. Next, porting it to the app.

Separately, Measure my duck now has a 30 second Stand still step that records 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.