Floorplan Graph RL: Beam reads real apartment floor plans by writing code

Beam can't see an image. So it learned to write its own eyes.

A text-only Beam agent writes its own computer-vision pipeline, learns from exact grader feedback, and improves without weight updates. Its best 10-round run reaches 0.179 held-out partial credit on 64 unseen real apartment plans and 0.803 room F1 on clean plans.

Beam actually learned: after the grader reported zero rooms on scans, Beam diagnosed its thresholding failure and adopted adaptive thresholding. Public partial credit jumped from 0.022 to 0.179.

The problem. Many floor plans exist only as images or scans. A room graph (rooms + doors) answers "can a wheelchair reach every room?", "how do I get out?" and "how many rooms?". This environment teaches a text-only model to build that graph from a real plan, and grades it exactly.

In one screen.

Data, grader, Docker files and every run: huggingface.co/datasets/hamoodey/floorplan-graph-rl

input plan, true room graph, Beam graph, route to exit

Plan p17 (clean style). Panel 3: rooms recovered by Beam's own program (Beam low, round 9). Panel 4: the step-free route to the exit, a networkx shortest path over the true door graph; this is what a recovered graph is for. Beam's doors are not right yet (door F1 0), so its own route would not be trustworthy.

EvidenceResult
Beam low, 10 feedback rounds0.000 → 0.182 public partial; 0.179 held-out partial
General tips playbook, 3 repeats each0.032 → 0.075 held-out partial
Clean-plan room recovery, best 10-round run0.803 room F1
Strict held-out reward0.000 for every Beam setup; 0.242 reference
Beam rooms per round
held-out partial credit by setup

score per round

Use it for RL

from code.env import FloorplanEnv, extract_code
env = FloorplanEnv(set="public")        # 24 practice plans; "hidden" only for the final score
prompt = env.reset()                    # instruction + contract + style guide
reward, partial, feedback, info = env.step(extract_code(model_reply))
next_prompt = prompt + previous_code + feedback   # the in-context loop used here

def floorplan_reward(completions, **kw):   # GRPO-style reward (e.g. TRL GRPOTrainer)
    return [sum(FloorplanEnv("public").step(extract_code(c if isinstance(c, str) else c[-1]["content"]))[:2])
            for c in completions]          # strict + dense partial credit

Unsaturated: strict reward is still 0.000 for every model loop here, while the hand-written reference reaches 0.242. Lots of room to train.

What generalizes for anyone using Beam on code-writing tasks

Beam reading its failure report (reasoning_content, Beam low, 10-round run)

Round 5 (practice-plan partial credit 0.022 → 0.179): “My previous approach using simple thresholding produced 0 rooms on scan styles and poor results. I need a more robust approach.”
Round 7 (practice-plan partial credit 0.078 → 0.174): “The previous attempt has low IoU due to morphological closing causing over-merge and room splitting. Let me focus on better wall detection and not over-engineer doors yet.”

Why trust these numbers

The grader is exact (rooms at IoU 0.7, doors within 0.5 m). Feedback uses only the 24 practice plans; the 64 held-out plans are from other buildings and never feed back. Every prompt, reasoning trace, program and score is in runs/.

Known limits. Kimi K3 did not finish (no reply to its round-1 request in 31 min); Beam high effort finished (held-out 0.016 in a single 3-round run; 0.176 with best of 4 samples per round). Most numbers are one run; the playbook result has 3 repeats with a wide spread. Doors are nearly unsolved (door F1 0.000 on every setup but one). We did not update Beam's weights.

What this entry is

We did not update Beam's weights; in these runs Beam improves only in-context, by retrying with the environment's feedback. The environment is ready for RL fine-tuning of Beam (open weights).

Results
ModelRoundsBest public strictBest public partialHeld-out strict (64 plans)Held-out partialBest round
Beam low effort, 3 rounds (repeat 1)30.0000.0000.0000.000round 1
Beam low effort, 3 rounds (repeat 2)30.0000.0260.0000.035round 3
Beam low effort, 3 rounds (repeat 3)30.0000.0580.0000.061round 2
Beam low + playbook, 3 rounds (repeat 1)30.0000.1010.0000.102round 1
Beam low + playbook, 3 rounds (repeat 2)30.0000.0000.0000.000round 1
Beam low + playbook, 3 rounds (repeat 3)30.0000.1360.0000.123round 1
Beam high effort, 3 rounds30.0000.0100.0000.016round 2
Beam high + playbook, 3 rounds30.0000.0000.0000.000round 1
Beam low, 10 rounds, each revises the best code so far100.0000.1820.0000.179round 9
Beam high, 5 rounds, best of 4 samples per round50.0000.1810.0000.176round 4
Kimi K3: no reply to round 1 in 31 min; did not finish.
Earlier baseline: Opus 5.5, one 2-hour agent run with tools (a different setup; strict score)---0.032--
Reference solution (hand-written, for scale)---0.2420.586-

Strict = the official score: plan score = room F1 x door F1 (rooms matched one to one at IoU ≥ 0.7, a door counts when its rooms match and its centre is within 0.5 m); gate g = min(1, mean clean / 0.6); strict = g x mean over the 48 hard plans. Partial = mean over plans of (room F1 + door F1) / 2, the dense RL signal. Public scores drive the feedback; held-out plans are used only for the final score.

Held-out results per style (64 plans, 16 per style)
RunRoom F1 cleanoutline+clutterclutter+scanoutline+scanDoor F1 (mean)StrictPartial
Beam low effort, 3 rounds (repeat 1)0.0000.0000.0000.0000.0000.0000.000
Beam low effort, 3 rounds (repeat 2)0.0450.0280.0000.2030.0000.0000.035
Beam low effort, 3 rounds (repeat 3)0.1490.0110.0360.2910.0000.0000.061
Beam low + playbook, 3 rounds (repeat 1)0.6950.0250.0130.0870.0000.0000.102
Beam low + playbook, 3 rounds (repeat 2)0.0000.0000.0000.0000.0000.0000.000
Beam low + playbook, 3 rounds (repeat 3)0.8130.0350.0130.1050.0050.0000.123
Beam high effort, 3 rounds0.0590.0240.0240.0180.0000.0000.016
Beam high + playbook, 3 rounds0.0000.0000.0000.0000.0000.0000.000
Beam low, 10 rounds, each revises the best code so far0.8030.0430.0360.5500.0000.0000.179
Beam high, 5 rounds, best of 4 samples per round0.7890.0100.0500.5600.0000.0000.176
Reference solution0.9690.5240.7790.6510.4410.2420.586
Does a playbook in the prompt help? (held-out, 3 repeats each)

The "playbook" is a 46-line list of general computer-vision techniques added to the prompt (code/playbook.md); no answers, no plan-specific hints. With tips the mean is about 2.4x higher, but n = 3 and one repeat in each group scored 0, so this is not a firm result.

GroupRunsHeld-out strictHeld-out partial
beam-low (no playbook)3mean 0.000, spread 0.000 to 0.000 (runs: 0.000, 0.000, 0.000)mean 0.032, spread 0.000 to 0.061 (runs: 0.000, 0.035, 0.061)
beam-low + playbook3mean 0.000, spread 0.000 to 0.000 (runs: 0.000, 0.000, 0.000)mean 0.075, spread 0.000 to 0.123 (runs: 0.102, 0.000, 0.123)
Score per round (public plans) 0.00 0.04 0.08 0.13 0.17 0.21 1 2 3 4 5 6 7 8 9 10 round public partial credit Beam low effort, 3 rounds (repeat 1) round 1: partial 0.000, strict 0.000 Beam low effort, 3 rounds (repeat 1) round 2: partial 0.000, strict 0.000 Beam low effort, 3 rounds (repeat 1) round 3: partial 0.000, strict 0.000 Beam low Beam low + playbook, 3 rounds (repeat 1) round 1: partial 0.101, strict 0.000 Beam low + playbook, 3 rounds (repeat 1) round 2: partial 0.000, strict 0.000 Beam low + playbook, 3 rounds (repeat 1) round 3: partial 0.000, strict 0.000 Beam low + playbook Beam high effort, 3 rounds round 1: partial 0.010, strict 0.000 Beam high effort, 3 rounds round 2: partial 0.010, strict 0.000 Beam high effort, 3 rounds round 3: partial 0.004, strict 0.000 Beam high Beam high + playbook, 3 rounds round 1: partial 0.000, strict 0.000 Beam high + playbook, 3 rounds round 2: partial 0.000, strict 0.000 Beam high + playbook, 3 rounds round 3: partial 0.000, strict 0.000 Beam high + playbook Beam low, 10 rounds, each revises the best code so far round 1: partial 0.000, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 2: partial 0.025, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 3: partial 0.018, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 4: partial 0.022, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 5: partial 0.179, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 6: partial 0.078, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 7: partial 0.174, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 8: partial 0.176, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 9: partial 0.182, strict 0.000 Beam low, 10 rounds, each revises the best code so far round 10: partial 0.180, strict 0.000 Beam low, best-so-far, 10 rounds Beam high, 5 rounds, best of 4 samples per round round 1: partial 0.155, strict 0.000 Beam high, 5 rounds, best of 4 samples per round round 2: partial 0.140, strict 0.000 Beam high, 5 rounds, best of 4 samples per round round 3: partial 0.093, strict 0.000 Beam high, 5 rounds, best of 4 samples per round round 4: partial 0.181, strict 0.000 Beam high, 5 rounds, best of 4 samples per round round 5: partial 0.175, strict 0.000 Beam high, best of 4, 5 rounds
How Beam used the feedback (Beam low, 10 rounds, each revises the best code so far)

Each round Beam gets its previous program and the grader's report (per-style room F1 / door F1, top failures from the public plans) and writes a new run.py. Below: Beam's own reasoning (reasoning_content) in the rounds where its score rose, with the score before and after.

RoundPublic partialPublic strictRoom F1 clean / outline+clutter / clutter+scan / outline+scanLines of codeOutput tokens (reasoning)Files
10.0000.0000.000 / 0.000 / 0.000 / 0.0001091696 (577)run.py · reasoning
20.0250.0000.151 / 0.050 / 0.000 / 0.0001011136 (154)run.py · reasoning
30.0180.0000.118 / 0.022 / 0.000 / 0.0001011701 (710)run.py · reasoning
40.0220.0000.000 / 0.000 / 0.000 / 0.1781051312 (229)run.py · reasoning
50.1790.0000.707 / 0.117 / 0.123 / 0.4861071216 (152)run.py · reasoning
60.0780.0000.373 / 0.085 / 0.125 / 0.042971058 (103)run.py · reasoning
70.1740.0000.739 / 0.110 / 0.088 / 0.458971173 (171)run.py · reasoning
80.1760.0000.708 / 0.116 / 0.122 / 0.4591602329 (696)run.py · reasoning
90.1820.0000.739 / 0.116 / 0.107 / 0.4971282067 (708)run.py · reasoning
100.1800.0000.739 / 0.116 / 0.097 / 0.4911681911 (74)run.py · reasoning
Round 2 (public partial 0.000 → 0.025): “The previous submission failed because it wrote no rooms? Actually it says pred 0 rooms. Probably adaptive threshold or contour issue due to image dimensions mismatch? cv2.imread on grayscale 8-bit should work. But maybe plan.png is larger? We resized. The wall mask maybe all white? Let's not overthink; output empty likely due to exception? But status ok. Could be min_area too high? No. Given time budget, I'll provide a slightly improved but stil ...”
Round 5 (public partial 0.022 → 0.179): “My previous approach using simple thresholding produced 0 rooms on scan styles and poor results. I need a more robust approach. Let me think about what's needed: detect walls, close small gaps, find free regions. The previous code closed gaps with morphology but maybe threshold was wrong, or morphology too aggressive. Given the complexity and time, I'll improve: use Otsu threshold, invert properly, handle scan styles with adaptive thresholding. A ...”
Round 7 (public partial 0.078 → 0.174): “The previous attempt has low IoU due to morphological closing causing over-merge and room splitting. Let me focus on better wall detection and not over-engineer doors yet. Key: rooms IoU low. Need better thresholding, keep wall gaps. Let me write improved version using Otsu + closing small gaps under 0.5m only, proper free space. Also handle scan rotation? The polygons move with image, so no need to unrotate. Let me improve: use adaptive threshol ...”
What Beam's program sees, including where it still fails (p06: a rotated, noisy scan)

Left: the rooms (colour), doors (red dot; green = door to outside) and door edges recovered by Beam low, 10 rounds, each revises the best code so far (round 9), drawn over the plan. Right: the true room graph. The blue line is a wheelchair route to the exit: a networkx shortest path over the door graph from the room farthest from the exit to the outside door, computed only from the recovered map. Blue dot = start.

p17 (clean)

Beam recovered graph for p17
Beam
true graph for p17
Truth
Rooms matched 4/4 (pred 5), doors matched 0/4 (pred 0), plan score 0.000, partial 0.444. Beam map: no route: no recovered room has a door path to outside. True route: 2 door(s), narrowest 1.04 m, all at least 0.8 m wide.

p01 (outline+clutter)

Beam recovered graph for p01
Beam
true graph for p01
Truth
Rooms matched 3/5 (pred 10), doors matched 0/5 (pred 0), plan score 0.000, partial 0.200. Beam map: no route: no recovered room has a door path to outside. True route: 2 door(s), narrowest 0.84 m, all at least 0.8 m wide.

p06 (clutter+scan)

Beam recovered graph for p06
Beam
true graph for p06
Truth
Rooms matched 2/5 (pred 10), doors matched 0/5 (pred 6), plan score 0.000, partial 0.133. Beam map: route found through 1 door(s); 1 of 10 recovered rooms reach the exit. True route: 3 door(s), narrowest 0.68 m, 1 under 0.8 m (too narrow for many wheelchairs).

p12 (outline+scan)

Beam recovered graph for p12
Beam
true graph for p12
Truth
Rooms matched 2/4 (pred 2), doors matched 0/4 (pred 0), plan score 0.000, partial 0.333. Beam map: no route: no recovered room has a door path to outside. True route: 2 door(s), narrowest 0.80 m, 1 under 0.8 m (too narrow for many wheelchairs).
Why this is useful

Floor plans exist mostly as images. A room graph turns one into something a program can answer questions about: is every room reachable from the exit, what is the step-free route for a wheelchair, how many bedrooms, which rooms are interior. The environment is a ready-made, exactly graded RL task for teaching a text-only model to do visual work through code, with dense partial credit so early programs still get a learning signal.

Credit

Plan geometry: Swiss Dwellings v3.0.0, Zenodo, DOI 10.5281/zenodo.7788422, CC BY 4.0. Plans re-drawn from that geometry. Built by Hamood (hamoodey) for Reflection's Build with Beam contest. Beam: Beam-1.0-501B-A23B via the Reflection API.

Can the reward be gamed?