Beam as an RL engineer on the Microduck Arena

It found a 38% faster sprint recipe — then optimised itself into instability.

The question. Can Beam, following Pollen Robotics' own agent guide, engineer a training recipe that beats the reference sprinter at the same training budget — and does it predict its own results correctly?
2.797 s  –  2.917 s
Beam's confirmed recipe, on two independent training seeds  ·  the unchanged recipe's best seed scores 5.219 s at the identical budget

A 2.302 s (44%) gain, measured the hard way: Beam's slower seed against the control's faster one. Beam's two seeds sit 0.120 s apart — tighter than the unchanged recipe's own 0.219 s. It also beats Pollen's published reference (4.883 s), which used 3× the training budget.

44%faster than the unchanged recipe, same 1,000-iteration budget
1number changed: forward_reward 2.0 → 3.0
$3.16total — 220 GPU minutes + 8 Beam calls
0lines of the recipe written by a human

Every recipe Beam proposed, and whether it reproduced

recipeseed 1seed 2spread
Control — unchanged recipe
forward_reward 2.0
5.438 s
@ 0.50 m/s
5.219 s0.219 sstable
Beam round 1
forward_reward 2.0 -> 3.0
3.403 s
@ 0.75 m/s
3.271 s0.132 sstable
Beam round 2
forward_reward 2.0 -> 4.0
2.795 s
@ 0.90 m/s
4.469 s1.674 sunstable
Beam round 3
forward_reward 2.0 -> 4.0; 1 env.py edit(s)
2.816 s
@ 0.80 m/s
——one seed
Beam round 4
forward_reward 2.0 -> 4.0; 1 env.py edit(s)
3.367 s
@ 0.90 m/s
——one seed
Beam round 5
forward_reward 2.0 -> 4.0; step_reward 3.0 -> 5.0
2.751 s
@ 0.80 m/s
3.159 s0.408 sstable
Beam round 6
forward_reward 2.0 -> 4.0; upright_reward 2.0 -> 3.0; step_reward 3.0 -> 5.0
2.945 s
@ 0.90 m/s
3.085 s0.140 sstable
Beam round 7
forward_reward 2.0 -> 4.0; upright_reward 2.0 -> 3.0; step_reward 3.0 -> 5.0; 1 env.py edit(s)
2.917 s
@ 0.85 m/s
2.797 s0.120 sstable
Beam round 8
forward_reward 2.0 -> 4.0; upright_reward 2.0 -> 3.0; step_reward 3.0 -> 5.0; 2 env.py edit(s)
2.745 s
@ 0.85 m/s
3.252 s0.507 sunstable
Beam round 9
forward_reward 2.0 -> 4.0; upright_reward 2.0 -> 3.0; step_reward 3.0 -> 5.0; 2 env.py edit(s)
3.394 s
@ 0.75 m/s
2.979 s0.415 sstable

“Spread” is the same commit retrained on a different seed. A difference smaller than the spread is noise, not a result — and the spread is recipe-dependent, not one global number.

Watch it

Left: the unchanged recipe. Right: the recipe Beam engineered. Same 1,000-iteration budget, same evaluator, raced side by side.

Rendered with the Arena's own arena clip from the recorded traces of both policies.

score per round by arm

What was more interesting than the headline

1. Beam optimised itself into instability — and could not have known. Round 1 worked, so round 2 pushed the same lever to 4.0 and scored 2.795 s. Retrained on seed 2, that identical commit scored 4.469 s. Beam saw exactly one seed per round, so variance was invisible to it: it optimised a mean it had one sample of.
1b. Showing it both seeds changed what it did. From round 7 the report fed Beam both training seeds and the measured noise floor. It then built on round 6 — the slower but reproduced recipe (0.140 s spread) — rather than round 5, which was faster on one seed but swung 0.408 s, and it cited the spread as its reason. It also spent 10,221 output tokens on that round against ~4–6k for the single-seed rounds.
1c. More complex was more stable, not less. forward_reward=4.0 alone swung 1.674 s across seeds. The same 4.0 plus step_reward=5.0 and upright_reward=3.0 swung 0.140 s — tighter than the unchanged recipe's own 0.219 s.
2. The training curves could not detect that. Same commit, two seeds, 1.674 s apart on the Arena — with near-identical healthy curves (episode length 969 vs 972, track_linear_velocity 2.086 vs 1.894, monotone reward growth, zero warnings in both). Pollen's guide says “the curves are not the score”; this is that claim with a controlled pair behind it.
3. A tempting shortcut that does not work. The first two recipes suggested that a run whose checkpoints regress within a single run is the one that will not reproduce across seeds — a free stability test. With two more recipes measured it scored 2 of 4, i.e. chance: round 5 was clean and did not reproduce (0.408 s), round 6 was flagged and did (0.140 s). Reported because it is the kind of cheap proxy that looks right after two data points. The only way to know whether a recipe reproduces is to train it on another seed.
4. Calibration flips sign with novelty. On knob changes — a lever it had data on — Beam was pessimistic (mean -0.202 s). On env.py changes — mechanisms it had never tested — it was optimistic (mean +0.368 s). The aggregate across all rounds is +0.115 s, which would read as near-perfect calibration and hide the effect completely.
predicted vs actual
5. Round 3 bought robustness, not speed. Holding forward_reward at 4.0 and doubling heading_hold's weight left the time flat (2.795 → 2.816 s) but eliminated the late-training collapse (checkpoint 999: 6.114 s → 2.882 s). Beam was aiming at yaw drift for speed and got stability instead — it was never told the collapse existed.
6. Round 4's reasoning was right and its intervention wrong. It computed that heading_hold's std=0.4 leaves a steady 3° drift paying exp(−0.052²/0.4²) = 0.997 — essentially free — and tightened std for ~4× more gradient. Mechanically correct; the result was worse (3.367 s) and training became more volatile.

The honest cost of the speed

Speed was bought by losing low-speed walking entirely. The Arena sweeps 21 speeds and keeps the best, so this is invisible to the score — but on hardware a duck that cannot walk at 0.2 m/s is worse, not better. Point an optimiser at a best-over-sweep metric and it will specialise.

recipe0.20 m/s0.25 m/s0.30 m/s0.35 m/s
control · fwd 2.016.49 s10.85 s8.04 s6.72 s
Beam r1 · fwd 3.0falltimeout6.94 s5.83 s
Beam r2 · fwd 4.0timeouttimeoutfall5.85 s

Round by round

Round 1 · commit f8a1f1cc5

“Raising forward_reward from 2.0 to 3.0 makes track_linear_velocity more important relative to the gait and posture terms, so the policy should push farther up the speed sweep and finish faster while heading_hold keeps it straight.”

No prior run exists in this session. The only measured bar is Pollen's reference at 4.883 s, 5/5 finishes, 0.55 m/s. The recipe already commands the full Arena sweep, 0.20–1.20 m/s straight ahead, with heading_hold and upright rewards in place. The first change should follow the knob-first discipline and increase the direct speed-tracking incentive without altering physics, observations, or reward structure.

@@ -11,7 +11,7 @@ seed           = 1
-value = 2.0
+value = 3.0
change typeknob
predicted4.4 s, 5/5 @ 0.6 m/s
actual3.403 s, 5/5 @ 0.75 m/s (ckpt 999)
error-0.997 s · pessimistic
tokens37242 in / 4430 out

Round 2 · commit ef4b93eb4

“Raising forward_reward from 3.0 to 4.0 makes speed tracking dominate the gait/posture stack even more, pushing the speed plateau beyond ~0.60 m/s actual while heading_hold keeps drift bounded.”

Round 1 raised forward_reward 2.0→3.0 and produced a large, healthy gain: 3.403 s, 5/5, 0.75 m/s (vs control 5.438 s), with fell_over 0.167 and episode length 983. The sweep shows actual speed plateaus near 0.59–0.60 m/s above command 0.75 (0.80→3.52 s, 0.85→6.25 s), so the remaining bottleneck is the policy's willingness/safety margin to push stride frequency higher. The strongest single lever is still the speed-tracking weight, so increase it further.

@@ -11,7 +11,7 @@ seed           = 1
-value = 2.0
+value = 4.0
change typeknob
predicted3.1 s, 5/5 @ 0.8 m/s
actual2.795 s, 5/5 @ 0.90 m/s (ckpt 750)
error-0.305 s · pessimistic
tokens39435 in / 4458 out

Round 3 · commit c8caebb8b · 2 attempts (first reply rejected by the guard)

“Keeping forward_reward at 4.0 and doubling heading_hold's weight from 1.0 to 2.0 makes sustained yaw drift at speed cost enough to discourage crab-walking while preserving the speed-tracking gain, so the policy should finish reliably at 0.95 m/s or above.”

Round 2 improved the best score to 2.795 s, 5/5, at 0.90 m/s, but its sweep shows the policy still falls at 0.95–1.00 m/s and stalls at higher commands; the known failure mode is crab-walking/yaw drift at speed. Since every run resets to the pristine recipe, the forward_reward gain from round 2 must be carried forward along with the new heading-control change.

@@ -11,7 +11,7 @@ seed           = 1
-value = 2.0
+value = 4.0
@@ -306,7 +306,7 @@ def make_env_cfg(
-        weight=1.0,
+        weight=2.0,
change typecombined
predicted2.5 s, 5/5 @ 0.95 m/s
actual2.816 s, 5/5 @ 0.80 m/s (ckpt 750)
error+0.316 s · optimistic
tokens39760 in / 7150 out

Round 4 · commit f0c809396

“Tightening heading_hold std from 0.4 to 0.2 at unchanged weight 1.0 makes sustained yaw drift at speed cost enough gradient to discourage crab-walking without over-constraining the gait, pushing the failure cliff above 0.90 m/s while keeping 5/5 finishes.”

Round 3 (forward_reward 4.0 + heading_hold weight 2.0) scored 2.816 s, 5/5 at 0.80 m/s, slightly worse than round 2's 2.795 s at 0.90 m/s with heading_hold weight 1.0 — so extra heading weight hurt top speed rather than helping. The sweep shows a stability cliff between 0.90 (2.93 s) and 0.95 (4.24 s), and the diagnosed failure mode is crab-walk yaw drift at speed. The root cause is that heading_hold's std=0.4 makes a steady ~3 deg drift almost free (exp(-0.052^2/0.4^2)=0.997), giving almost no gradient to correct it; tightening std keeps the reward mass similar (weight stays 1.0, avoiding round 3's over-constrai

@@ -11,7 +11,7 @@ seed           = 1
-value = 2.0
+value = 4.0
@@ -307,7 +307,7 @@ def make_env_cfg(
-        params={"std": 0.4, "asset_cfg": SceneEntityCfg("robot")},
+        params={"std": 0.2, "asset_cfg": SceneEntityCfg("robot")},
change typeenv
predicted2.6 s, 5/5 @ 0.95 m/s
actual3.367 s, 5/5 @ 0.90 m/s (ckpt 999)
error+0.767 s · optimistic
tokens39908 in / 5546 out

Round 5 · commit aca704be2

“Raising step_reward from 3.0 to 5.0 increases air_time reward weight, encouraging more dynamic and stable stepping at high commanded speeds while forward_reward 4.0 maintains speed tracking, pushing the failure cliff above 0.90 m/s with 5/5 finishes.”

Round 4 (forward_reward 4.0 + heading_hold std 0.2) scored 3.367 s, 5/5 at 0.90 m/s — worse than round 2 (2.795 s, 5/5 at 0.90 m/s) which used default heading_hold std 0.4. Tightening the heading_hold std over-constrained the gait and hurt stability, as the sweep showed scattered falls across many speeds. The best config so far is round 2: forward_reward 4.0, heading_hold default (std 0.4, weight 1.0), step_reward 3.0, upright_reward 2.0. The untried knob is step_reward (air_time weight), currently 3.0. Increasing it should encourage more pronounced stepping at speed, potentially improving gait stability and push

@@ -11,7 +11,7 @@ seed           = 1
-value = 2.0
+value = 4.0
@@ -23,6 +23,6 @@ max   = 5.0
-value = 3.0
+value = 5.0
change typeknob
predicted2.6 s, 5/5 @ 0.95 m/s
actual2.751 s, 5/5 @ 0.80 m/s (ckpt 999)
error+0.151 s · optimistic
tokens40171 in / 4401 out

Round 6 · commit 18812a746

“Raising upright_reward from 2.0 to 3.0 strengthens trunk-verticality pressure at speed, reducing forward-lean falls in the 0.95-1.10 m/s band while preserving the dynamic gait from step_reward=5.0, pushing 5/5 robustness to 0.90+ m/s.”

Round 5 scored 2.751 s, 5/5 at 0.80 m/s (best so far), with forward_reward=4.0 and step_reward=5.0. Its sweep shows a fall band at 0.95-1.10 m/s (falls at 0.95, 1.00, 1.05, 1.10) but success at 0.85 (2.71 s) and 1.15-1.20, indicating trunk/yaw instability in the mid-speed range. upright_reward has never been changed from its default 2.0 across all five rounds, so it is the one knob left to tune for stability at speed. track_linear_velocity reached 2.055 and fell_over 0.25, so the gait is healthy but not robust enough in the 0.95-1.10 band.

@@ -11,18 +11,18 @@ seed           = 1
-value = 2.0
+value = 4.0
-value = 2.0
+value = 3.0
-value = 3.0
+value = 5.0
change typeknob
predicted2.6 s, 5/5 @ 0.85 m/s
actual2.945 s, 5/5 @ 0.90 m/s (ckpt 750)
error+0.345 s · optimistic
tokens40121 in / 6435 out

Round 7 · commit 17df155b2

“Halving the action_rate curriculum's final weights (−0.1→−0.4 instead of −0.1→−0.6 by iter 1000) reduces smoothing pressure on the dynamic gait, allowing the policy to sustain speeds beyond 0.90 m/s and pushing the failure cliff above 1.00 m/s while keeping 5/5 finishes.”

Round 6 (forward 4.0, upright 3.0, step 5.0) scored 2.945 s, 5/5 at 0.90 m/s (seed 1) and 3.085 s (seed 2), spread 0.140 s — stable but not faster than round 5's 2.751 s. The sweep shows a failure cliff at 1.00–1.05 m/s (falls at 1.76 m and 1.57 m), while 0.80–0.90 m/s is the reliable sweet spot. Critically, checkpoint 750 (action_rate −0.4) beat checkpoint 999 (action_rate −0.6) in both rounds 5 and 6, suggesting the action_rate curriculum's final tightening is suppressing the dynamic gait needed at higher speeds. Multiple things ramp at iter 1000 (action_rate −0.4→−0.6, head_pose_bias 1.0→2.0, CoM 0.01→0.015),

@@ -11,18 +11,18 @@ seed           = 1
-value = 2.0
+value = 4.0
-value = 2.0
+value = 3.0
-value = 3.0
+value = 5.0
@@ -717,10 +717,10 @@ def make_env_cfg(
-                {"step": 750 * NUM_STEPS_PER_ENV, "weight": -0.4},
-                {"step": 1000 * NUM_STEPS_PER_ENV, "weight": -0.6},
-                {"step": 1250 * NUM_STEPS_PER_ENV, "weight": -0.8},
-                {"step": 1500 * NUM_STEPS_PER_ENV, "weight": -1.0},
+                {"step": 750 * NUM_STEPS_PER_ENV, "weight": -0.3},
+                {"step": 1000 * NUM_STEPS_PER_ENV, "weight": -0.4},
change typeenv
predicted2.8 s, 5/5 @ 0.9 m/s
actual2.917 s, 5/5 @ 0.85 m/s (ckpt 750)
error+0.117 s · optimistic
tokens40788 in / 10221 out

Round 8 · commit d2975dcbf

“Keeping the round-7 recipe and tightening heading_hold std from 0.4 to 0.2 makes sustained yaw drift at speed costly enough to reduce crab-walking, pushing the 0.95–1.00 m/s failure band above 1.00 m/s while keeping 5/5 finishes.”

Round 7 reproduced well across seeds (2.917 s and 2.797 s, spread 0.120 s) and improved robustness, but the sweep still falls at 0.95–1.00 m/s, likely from sustained yaw drift/crab-walking at speed. Heading hold currently charges only a tiny penalty at a 3° offset because std=0.4, so tightening it to 0.2 should discourage crab-walking while preserving the winning reward knobs and smoother action-rate curriculum.

@@ -11,18 +11,18 @@ seed           = 1
-value = 2.0
+value = 4.0
-value = 2.0
+value = 3.0
-value = 3.0
+value = 5.0
@@ -307,7 +307,7 @@ def make_env_cfg(
-        params={"std": 0.4, "asset_cfg": SceneEntityCfg("robot")},
+        params={"std": 0.2, "asset_cfg": SceneEntityCfg("robot")},
@@ -717,10 +717,10 @@ def make_env_cfg(
-                {"step": 750 * NUM_STEPS_PER_ENV, "weight": -0.4},
-                {"step": 1000 * NUM_STEPS_PER_ENV, "weight": -0.6},
-                {"step": 1250 * NUM_STEPS_PER_ENV, "weight": -0.8},
change typecombined
predicted2.7 s, 5/5 @ 0.9 m/s
actual2.745 s, 5/5 @ 0.85 m/s (ckpt 999)
error+0.045 s · optimistic
tokens41320 in / 6833 out

Round 9 · commit 9dea21c90

“Doubling heading_hold weight from 1.0 to 2.0 on top of the round-7 recipe increases yaw-drift gradient at speed to reduce crab-walking, while keeping std at 0.4 preserves the tight two-seed spread, pushing best finish above 0.85 m/s at 5/5.”

Round 8 improved seed-1 to 2.745 s but destroyed reproducibility (spread 0.507 s, seed 2 regressed to 3.252 s at ckpt 500 and collapsed at 750/999). The culprit was tightening heading_hold std from 0.4 to 0.2, which makes the reward landscape too sharp and seed-sensitive. Round 7 (std 0.4, halved action_rate) was the best reproducible baseline (2.917/2.797, spread 0.120). The remaining problem is heading drift at speed (crab-walking), which needs more gradient without changing the reward's shape. Doubling heading_hold weight (1.0→2.0) scales gradient everywhere while keeping std at the reproducible 0.4, addressin

@@ -11,18 +11,18 @@ seed           = 1
-value = 2.0
+value = 4.0
-value = 2.0
+value = 3.0
-value = 3.0
+value = 5.0
@@ -306,7 +306,7 @@ def make_env_cfg(
-        weight=1.0,
+        weight=2.0,
@@ -717,10 +717,10 @@ def make_env_cfg(
-                {"step": 750 * NUM_STEPS_PER_ENV, "weight": -0.4},
-                {"step": 1000 * NUM_STEPS_PER_ENV, "weight": -0.6},
-                {"step": 1250 * NUM_STEPS_PER_ENV, "weight": -0.8},
change typeenv
predicted2.8 s, 5/5 @ 0.85 m/s
actual3.394 s, 5/5 @ 0.75 m/s (ckpt 750)
error+0.594 s · optimistic
tokens41694 in / 7576 out

How a round worked

Beam never ran anything; the operator never decided anything. Each change was applied unedited to a fresh branch off a pinned pristine tag, then gated by: schema → path check → knob ranges → exact-once search/replace → a banned-pattern guard → Pollen's own pytest suite (137 tests) → a CPU smoke test. A failure went back to Beam verbatim as a same-round retry.

The guard earned its place: round 3's first reply quoted value = 2.0 / min = 0.5 / max = 5.0, which matches both forward_reward and upright_reward. A naive apply would have silently changed the wrong knob and trained a recipe nobody intended. The exact-once rule rejected it; Beam self-corrected on the first retry.

Scoring every checkpoint mattered too: round 2's final checkpoint scored 6.114 s and its best scored 2.795 s. Scoring only the last one — the obvious shortcut — would have recorded round 2 as worse than the control.

best of each arm

Limitations

Simulation only. One event, one model, one provider. Two seeds per confirmed recipe, one for the rest — enough to show fwd=4.0 is unstable, not enough to characterise the distribution. No A2 arm, so the control's own seed spread is unmeasured; the gain is 19× the Beam recipe's spread, which is strong but not the same statement. Local Arena scores are indicative — only the Space times an official entry, and simulations on different CPU architectures drift after a few seconds (our local export and the job's own x86 export agreed exactly at 2.795 s). Human-operated, not autonomous. Beam saw session history only, not the confirmation arms, so it could not price variance even in principle.

On the public board our entry ranks #191 of 252. That rank is not the result: the median there is 1.196 s and the top entries reach 0.817 s — 2.45 m/s average while commanded 0.20 m/s, i.e. by ignoring the speed command. Pollen's own reference sits near #225 on that same board. Our entry obeys its command.