Routability: the placer learns from the router
Re-measured, 14 September 2026. A network fitted on the corpus labelled by the A* router at its rule under kicad-cli (
.cache/corpus-astar-127, 62,926 placements), with the 2,050 PCBench copies out of training, scores 0.914 [0.861, 0.953] on PCBench (218 placements of 111 boards, held out by content) and 0.916 on the family-held-out split; within a board 0.586. That network is the shipped model; the 3 September figures below it are the previous model's and read as history.Correction, 12 September 2026. The external figure on this page — the shipped network's 0.89 on PCBench's 125 usable boards — was withdrawn until re-measured (above). The training bank held 2,050 of PCBench's boards in files freerouting had vendored and KiCad re-saved: new bytes around the same parts, nets and placement, which the 5 Sep byte-level refusal could not see. 704 were identical to a PCBench board in netlist and placement, 1,339 variants of the same boards, and 6,017 corpus examples of them sat on the training side when the network was fitted. They are quarantined and the door now refuses a board by its DSN content and by the vendored path (
kicad_boards.py, 12 Sep). The mould, the fold and the pitch counts are not fitted and their held-out numbers stand; the Mendicant paper's CNN-baseline comparison on PCBench shares the defect and is marked there. A re-measure needslearn train, which is not run unasked.Correction, 11 September 2026. Two claims from the Mendicant paper that this page's thread rests on are withdrawn. First, that a judge asked which of a board's anneals routes under either router is bounded near chance by the two routers' disagreement. Agreement between two raters bounds nothing about a third predictor of their disjunction, and the merged label is in any case very nearly one router's — it agrees with the A* router's own on 92.9% of placements at κ = 0.83. Measured properly, by scoring a placement from how often its five 0.05mm nudged copies failed and testing against the unnudged verdict, a judge that cannot see below a fifth of a track width orders a board's own anneals at AUC 0.837 [0.737, 0.923]. The label leaves room; what this repository has measured is a run of estimator failures, not a ceiling. Second, the paper's threshold — "a judge would need about 0.70" — was asserted and is now derived: on the held-out pools, one A* route on a random anneal comes back clean 66.6% of the time, a second router takes that to 73.8%, and a perfect choice of anneal to 77.9%, so a judge is worth what the second router is worth at within-board AUC 0.79. Separately, three cached result files were found to hold instrument scores and baselines computed on different placements, because the tools regenerated a placement instead of storing it and both the placer's cost and the KiCad reader's treatment of back-side footprints had changed underneath them; every figure has been re-measured from
.cache/placements_paper.jsonand the affected numbers moved in the third decimal, except the ten-iteration within-board reading, which was 0.548 and is 0.579. Seepaper/mendicant.tex§6.1 andtools/referee.py.
Correction, 5 September 2026. Every number on this page was measured on labels that have since been corrected, and the model that produced them has not been retrained. An edge-clearance defect in this pipeline — the router was told to keep copper 0.5mm from the cut line while the placer seated connector pads on it — failed a fifth of the placer's anneals at the router's first pass, for a reason no placement could fix. 2,502 labels were decided by it; re-routed at the corrected rule, 292 turned from failure to clean and none the other way. Judged today against the corrected labels, the shipped model scores 0.937 held-out and 0.923 among anneals, against the 0.954 and 0.941 below, with expected calibration error rising from 0.02 to 0.07. The human-versus-placer gap this page and the paper reported is withdrawn: it was the defect, not the human, and with the rule fixed the placer matches the human placement on one anneal over 705 boards (74.5% against 74.8%) and beats it over six (80.4% against 74.8%). Those were 726 boards at 74.7% each until 5 September, when an audit of board identity removed 866 copies of the external test set from the training directory — freerouting vendors PCBench inside its own benchmark fixtures — and 21 of the 726 went with them. The conclusion is unchanged; the count and the third decimal are not. See
paper/isonet.tex§1 for the full correction. A retrain on the corrected corpus is the next step and is not done, and it matters more than it did: the external AUC below was measured with about a twelfth of its test set seen during training.
Freerouting is the only judge this pipeline has of whether a placement can be built. It answers in about ten seconds, in one line — 3 unrouted and 9 violations — and until now that line was read once, for a status pill, and thrown away. The placer that decided the outcome never heard it.
Three things changed.
The router's verdict is kept
Every route the pipeline runs is written to a corpus under .cache/corpus/: the board as the router met it, drawn as congestion maps, and what the router said about it. A build in the app, a build on the command line, and every placement the harness below tries all land there. corpus.py owns it.
The label is the router's own count, not ours. Ours had a blind spot: ses.coverage() asked whether a net had any copper, and a router that joins two pins of a three-pin net and gives up on the third passed it. Three of the boards saved under projects/ have exactly that — the B-side ground and VBUS pins of a USB-C receptacle left dangling — and their Route pill said done. ses.incomplete() now walks the copper pin by pin and names what it never reaches (GND misses J1.B1), the workbench reads that, and the corpus takes both counts.
The placer sees what the router will meet
congestion.py draws the board on a 32×32 grid, whatever its size: RUDY (Spindler & Johannes, 2007) spreads each net's expected copper evenly over its bounding box in units of layers of copper this cell would need; pad copper, connected pins, courtyards and through-hole pads get a channel each; 23 scalars carry the size, the wirelength, the peak and 95th-percentile demand and the fraction of the board asking for more than two layers. Wirelength says how much copper a board needs; these maps say where.
A model ranks placements before the router runs
routability.py fits three models on the corpus and judges them on the same held-out boards — held out by board, so a model that has seen the drone controller placed forty ways is not asked about the forty-first:
| model | what it sees | why it is there |
|---|---|---|
rudy | four congestion scalars, logistic | the analytic answer with its scale calibrated: the baseline anything else has to beat |
linear | all 23 scalars, logistic | is the picture worth more than the numbers? |
cnn | the maps, three 3×3 convolutions — the five original channels and no scalars in the shipped lean recipe; --recipe full reads all eight and the scalars | the neural network |
The one the app ships (soldermask/assets/routability.json) is the one that judged best, with the other two's scores written beside it in the file; a learned model that cannot beat the four congestion numbers by 0.02 AUC loses to them, and the file says so. Whether a neural network earns its place here is a measurement, not a position. soldermask learn report prints it.
The shipped model was trained on 7,303 routes of 1,447 boards — 5,995 to fit and 1,308 to choose the epoch and calibrate — out of a corpus of 9,777 examples from 2,080 boards (1,200 real families), every label at the clearance its board's parts allow; 58% route clean. It was judged on 2,407 held-out routes of 606 boards, separately on the 1,298 of those that are annealed placements of real boards, and on PCBench's 125 external boards it was never fitted on:
| model | held-out AUC | Brier | ECE | accuracy | AUC among anneals | first pick clean (228 boards with a clean anneal) |
|---|---|---|---|---|---|---|
rudy | 0.69 | 0.22 | 0.11 | 0.65 | 0.73 | 190 |
linear | 0.84 | 0.14 | 0.06 | 0.81 | 0.83 | 204 |
cnn | 0.95 | 0.08 | 0.02 | 0.89 | 0.94 | 212 |
| wirelength (what the placer used before) | — | — | — | — | — | 190 |
The bold column is the deployment's question — of the anneals of a board it never saw, which route clean? — and the external test, PCBench's boards, scores it at 0.89. The spread of that column over training seeds is about ±0.01 on this corpus (0.931, 0.941, 0.942), and the shipped model is the ensemble of the three. The picks column asks it once per held-out board that has at least one clean anneal among its routes — 228 of the 373 with a choice — and counts the boards whose first pick is clean: 61% of these anneals route clean, so a random first pick would be clean on about 140, wirelength's is on 190, the model's on 212. That is one route per placement on a few hundred boards, and ordering the anneals of one board is a narrower question still, so it is reported as a direction, not a result. On the first corpus rudy scored below 0.5 in that column — the congestion scalars pointed the wrong way, because the boards that failed most were the sparse ones; on this corpus they score 0.73, which is mostly the difference between a crowded board and a loose one, pooled across boards. Ordering the anneals of one board is a narrower question than any column here asks, and on it the scalars carry nothing: which is why the model ranks and the scalars do not.
This is the second corpus. The first, of 2,607 routes with five anneals a board, scored 0.83 on the whole held-out set and at chance among anneals (0.47); see Two distributions below. What moved the number the product rests on from chance was correcting the labels, not the size of the corpus: with the labels fixed, the first corpus already scored 0.80 ±0.04 over seeds among anneals, and six times more anneals of the same 92 boards left it at 0.75 ±0.01 while the whole-set AUC edged up. More placements of the same boards is not the lever; more boards is.
What the number means: p_clean is the probability that freerouting returns every net routed with no violation left behind — the outcome the decision to order is made on. Inference is numpy only; the app carries no torch.
Two distributions, and the one that matters
The corpus holds two kinds of placement. Placements the placer would make — full anneals, the placement as saved, real builds — and placements made to fail: random positions, parts swapped, a 4 mm jitter, an outline squeezed to half its size, an anneal stopped early. The second kind is two fifths of the real-board examples, and it is there so the corpus has failures to learn from. But it makes the whole-corpus AUC flatter the model than it deserves: telling a random scatter from a proper anneal is easy, and it is a distinction the placer never asks for, because every candidate it offers the router is a proper anneal.
So every score is now reported twice: over the whole held-out set, as before, and over the annealed placements of held-out real boards — the distribution the deployment actually scores. The second is the number the product rests on, the model file carries it as held_out_annealed, and the model's own sentence says it last and plainest: among the 1161 placements like the ones the placer makes, AUC 0.93.
When this was first measured, on the first corpus, it was at chance for every model — 0.47 for the network, which fitted its own training boards at 0.97 — and the first reading was that the model had learned to recognise a bad placement and the boards it had seen, and nothing about which of six good anneals the router would prefer. That reading was wrong, and the corpus said so once it was looked at properly. The labels were the problem. Fifty of the first corpus's 2,608 routes carried a verdict the router and our own pin walk disagreed on: the router said every connection was made, ses.incomplete() said a pin was missed, and the example was filed as a failure. Among the 93 annealed placements of held-out real boards, 25 of the 43 "failures" were of exactly this kind. A model cannot learn a label that is not there. The backfill that re-routed every example for the heatmap targets re-ran the walk as it is today, the disagreements fell to 2 of 2,608, and the same network on the same 93 ids scores 0.79, and the first paper's whole recipe, retrained on the corrected corpus, scores 0.76 among anneals. The disagreement count is now something to check on any corpus before believing a number from it: learn status reports it as disputed, learn train says so out loud, and disputed routes are left out of fitting and judging. Eleven remain in the corpus of 6,376 and they survive a second routing — on those boards the router's log says 0 unrouted while its own session file leaves pads unconnected (two islands of VCC and a dangling via, on the one dissected), so there the router misreports and the walk is right, which is the right way round for the Route pill.
A probability that is checked
A model that says 88% is making a claim about frequency, and the first model made it unchecked: it was fitted to the last epoch and its output was read as a probability because it came out of a sigmoid. Now a validation slice — 15% of board families, deterministic by hash and disjoint from the held-out quarter — is kept back from fitting. The network's epoch is chosen on it (the weights kept are the ones with the lowest cross-entropy there, not the last), and every model's output is put through a two-parameter Platt calibration fitted on it. The held-out boards are asked nothing until the end, and then they report a reliability table — in each band of what the model said, how often the board actually routed clean — and its expected calibration error (ECE) beside the AUC. soldermask learn report prints the table.
What the table says is on every board because of it. The shipped model is a little over-confident at the top: placements it rates 90–100% (it says 97% on average) route clean 94% of the time on boards it never saw, and the expected calibration error is 0.05 over the whole held-out set. On the first corpus the same band said 97% and happened 81%, driven by one held-out family a global calibration could not reach; the second corpus, with many more anneals of that family, closed most of that gap. Either way the workbench does not let the number stand alone. The routing note reads rated 98% likely to route clean — in testing, placements it rated 90–100% routed clean 94% of the time (1025 boards). The table it quotes is the held-out one, written into board.json under routing.reliability; it is never used to fit anything.
Eight views of one board
A board turned a quarter turn or seen in a mirror routes exactly as well — a quarter turn swaps its width and height, and nothing else about it changes. The network is trained on the eight orientations of the grid (routability.view), and at inference it is shown all eight and its answers are averaged. The views are a free ensemble, and how far they disagree is reported as a spread on every candidate (routing.candidates[].spread) and in the note when it is large. A model file written before the views existed is read the way it was judged, one view.
Numbers with intervals, and every family held out once
Every AUC now carries a 95% interval, by bootstrap over board families rather than routes: forty placements of one board are one draw, and an interval that resampled routes would be forty times too narrow. The intervals are wide where the evidence is thin — the deployment's question rests on eight held-out families, and its interval says so — and that is the point of printing them.
soldermask learn crossval holds every family out once: four folds by the same hash as the split, each fold scored by models that never saw it, with a slice of the remaining families choosing the epoch and the calibration. The out-of-fold predictions are pooled and judged as one held-out set, per-fold spread beside the pooled number. It is the deployment's question asked of every family in the corpus rather than the quarter that fell on the held-out side, and it is the number to quote when eight families are not enough. Out of fold over 2,056 families: AUC 0.95 (0.93–0.96), and among the annealed placements of real boards 0.93 (0.90–0.96) over 1,035 families.
soldermask learn train --seeds N trains the network N times and ships their ensemble — logits averaged over members and views — with each seed's own held-out scores written beside it. One seed's answer on the placements that matter moves by a few points of AUC; the average of several moves less, and the spread is on record either way. The shipped ensemble is 3 seeds; on their own they scored 0.946, 0.953, 0.955 on the held-out set, the ensemble 0.954.
Three more channels, and a corpus that can be redrawn
The first corpus's picture had five channels. Three more say what it could not: wide is the RUDY of the nets routed wider than the median — a rail's copper, which the picture could not tell from a signal's; degree marks each connected pin with log1p of its net's pin count, so a ground at every part reads as the one net it is rather than a box over the whole board at a density that says nothing; keepout is the antenna zone the router is told to respect and the maps never saw. Because every example keeps its placement, soldermask learn redraw draws the whole corpus again with today's channels from those placements, no route run: a new channel is a retrain, not a new corpus. Whether the three earn their place is an ablation, first five channels, in the paper. With the three channels zeroed the network scores 0.950 ±0.003 against 0.939 ±0.008 with them, and 0.936 against 0.920 among anneals, over three seeds. Whether the three earn their place was an ablation, and they did not: over three seeds the first five channels alone scored 0.950 on the held-out boards against 0.939 with all eight, and the maps without the scalars 0.950 ±0.003. The shipped network therefore reads the five and no scalars (routability.RECIPES["lean"], the default of learn train); the three are still drawn and stored, and --recipe full trains the network that reads them.
Where the copper will crowd
Since the corpus was made whole again (below), every route in it records not only the router's verdict but where the router's copper went and which pins it could not reach, drawn on the same 32×32 grid the maps are drawn on (corpus.targets). Copper is in the RUDY channel's own unit — layers of the cell's area — so the analytic guess and the router's answer can be laid cell on cell. Unreached is 1.0 in a cell holding a pin that ses.incomplete()'s walk never reached.
The network carries a heatmap head on the same features: the last feature map is upsampled back to the grid, joined to the first layer's, and two more convolutions draw the copper it expects per cell and the probability that a pin in the cell is left unreached. Nothing about the verdict changed — the head is judged on its own, on the held-out boards, against RUDY: the correlation cell for cell between the copper predicted and the copper laid, and, on boards where a pin was left unreached, which pin — the AUC over the cells that hold a pin. On the held-out boards the head's copper correlates with the router's at 0.468 against 0.368 for RUDY, and it names the pin that goes unreached at AUC 0.780 against 0.637 for RUDY's demand, over 536 boards. The picture is worth drawing.
The workbench shows it. On a board rated by a model with a head, the Layers tab has one more entry, off by default: Predicted congestion — warm where copper was expected, saturating at two full layers, and ringed in the selection colour where a pin was expected to go unreached. Turn it on over the routed copper and the two can be read against each other. The picture is in board.json under routing.heat.
How the placer uses it
placer.candidates(nl, count=6) anneals six placements from six seeds, the model rates each, and they are returned best first (ties, and every board when there is no model on disk, fall back to wirelength — what the placer always did). The anneal's cost is now incremental — only the moved part's nets and overlaps are re-summed, which is the same number to 1e-12 and eight times fewer operations a step — so six placements cost about what one did. (A no-op move no longer costs a random draw, so a seed reproduces a board under this placer and not one saved by the old one.)
workspace.route_candidates() shows the router the first candidate; if it comes back clean that is the board and the build cost what it always did. If not, the next candidate is tried, up to three, and the least bad is kept: fewest incomplete nets, then fewest violations. The board's notes say routed on the second of 3 placements tried: the first left 1 net incomplete and was rated 61% likely to route clean; the one kept was rated 88% and routed clean. Where the model's first choice was confident and wrong, that is said too, in those words, because a model that is wrong in silence is the failure mode this pipeline exists to avoid — and the route is already in the corpus for the next one.
board.json carries the whole account under routing: the prediction, every candidate's rating and spread, every attempt's outcome, which was kept, the pins each incomplete net misses, the model's picture of the placement kept (heat), and the held-out reliability table the note quotes (reliability).
A second router, and what it says about the labels
Every example in this corpus is freerouting's verdict, which makes one question worth asking directly: how much of a failed label is the board, and how much is the router? tools/router_ab.py answers it by giving a board's human placement to freerouting and to KiCadRoutingTools — grid-based A* with a Rust core, MIT, reading .kicad_pcb rather than Specctra — at one fixed rule, and having KiCad's own DRC judge both. Neither router is asked how it did.
Over 300 boards, 276 of which both routers answered for:
| clean | closed every net | seconds | median DRC errors left | |
|---|---|---|---|---|
| freerouting | 159 | 168 | 8,301 | 31 |
| KiCadRoutingTools | 192 | 193 | 8,466 | 9 |
118 boards were closed by both, 14 by freerouting alone and 47 by the A* router alone; McNemar's chi-squared over the discordant pairs is 16.79, so the difference is not noise. The two take the same time. The finding is not which router is better: it is that 47 of the 117 boards freerouting calls failures — 40% of them — route cleanly under a different router. A label that reads as a fact about a placement is, two times in five, partly a fact about the tool. freerouting additionally crashed on 3 boards and hit its 300-second timeout on 20.
Getting there took two corrections worth recording, because both were the kind of mistake this page exists to catch. The first pass scored freerouting with this repository's own copper walk and the A* router with the A* router's own summary — not the same test, and it flattered the newcomer. The second put both through DRC but counted errors absolutely, which made the newcomer look reckless until the bare board was checked: the same placement with no copper on it at all returns a median of 65 DRC errors, because a courtyard that overlaps or a pad on the outline is the board's own problem and was being charged to whoever routed it. The third reading was wrong in the other direction: scoring what a router adds over that baseline, clamped at zero, returns zero for both and measures nothing, since routing removes the bare board's unconnected errors and 274 of 276 boards finish under their own baseline. What separates them is what is left. Over the 154 boards both routers fully connected, DRC holds a median of 11 errors against freerouting and 2 against the A* router, which is tidier on 104 of them and worse on 6.
None of this replaces freerouting. Twenty-three thousand labels are its opinion and a corpus with two oracles in it has a confound in it; corpus.Outcome records which router produced each label for exactly this reason. It is a second opinion, and what it says is that a meaningful share of the corpus's negatives are about the tool.
A mould routes the board before the router does
Every estimate above stops short of routing. RUDY spreads a net's copper over its bounding box and cannot say whether a path exists; the chord diagram (soldermask/fold.py) counts the crossings a topology forces and, measured, predicts how many vias the router drills and not whether it finishes. soldermask/physarum.py routes. It is Tero, Kobayashi and Nakagaki's model of the slime mould Physarum polycephalum: a network of tubes with a length and a conductivity each, flow through a tube proportional to conductivity times pressure drop over length, pressures from a Poisson solve with one source and the rest sinks, and a tube that carries flow thickening while one that does not wastes away, dD/dt = |Q| − D. Iterated, the tubes that survive are the shortest paths from the food to every mouth — Bonifaci, Mehlhorn and Varma proved it — and the mould has drawn a tree without being told what a tree is.
Here the tubes are the edges of the same 32×32 grid the congestion maps are drawn on, every net is a mould of its own, fed at the pin nearest its middle and drinking at every other pin, and the nets negotiate. An edge across a cell boundary has room for so many tracks — the boundary's length over a track's pitch, times the layers not under a pad in either cell, with a net's own pads put back for that net — and a net whose tube would be one track more than the edge has room for sees the edge as longer, the more so the more others are already on it and the longer it has been full: PathFinder's (base + history) × present cost with the mould as the router. The Poisson solve is the grid Laplacian, block tridiagonal by rows, eliminated for every net at once in numpy — forty iterations over forty nets in six tenths of a second. What comes out is the usage of every edge against its room: a saturation map, the millimetres of track with no room and which nets they belong to, and each net's tree length against its rectilinear spanning tree.
The multi-net form is a deliberate departure from the literature. Bonifaci et al.'s multi-commodity Physarum (2022) grows one tube per edge for every commodity at once and rewards sharing it; copper cannot be shared, so here every net has its own tube and the sharing is priced. The capacitated solvers (Huang et al. 2020, Sonoda et al. 2024) cap a single commodity's tube by scaling its conductivity down; that would move the signal this module exists to read — how much does not fit — into flow with nowhere to go, so it is not used.
tools/physarum_ab.py measures it against the estimators it has to beat, with the label read three ways: clean under either router, and under each alone. The test bed is the 131 boards both routers above answered for, at eight anneal seeds each — the two from the router comparison, routed here, and six more routed on Azure by tools/azure_ab.py under the same KiCad release — 989 placements in all. Scores are read as higher-is-worse; the partial correlation holds the net, pin and footprint counts at once, because on this corpus every raw signal so far has been a size prior.
| score | AUC, either router | ρ holding size | AUC, freerouting | AUC, A* |
|---|---|---|---|---|
| net count | 0.650 | — | 0.629 | 0.679 |
| pin count | 0.638 | — | 0.624 | 0.673 |
| RUDY peak | 0.680 | +0.28 | 0.627 | 0.639 |
| RUDY overflow area | 0.689 | +0.25 | 0.634 | 0.666 |
| pad copper peak | 0.662 | +0.32 | 0.587 | 0.615 |
| fold, chords displaced | 0.660 | +0.22 | 0.633 | 0.697 |
| mould, every net alone, saturation at pins | 0.634 | +0.22 | 0.591 | 0.602 |
| mould, negotiated, saturation at pins (max) | 0.757 | +0.33 | 0.725 | 0.744 |
| mould, negotiated, saturation, 95th percentile | 0.757 | +0.32 | 0.703 | 0.753 |
| mould, negotiated, track with no room, mm | 0.583 | +0.24 | 0.571 | 0.571 |
Three things to read off it. The saturation at the pins — how close the copper comes to full where a net has to get out of a pad field — is the first scalar measured on this corpus that is not the size prior: it beats the net count by a tenth of AUC, holds the three size counts at ρ +0.33 (p 1e-27), and added to the net count in a leave-one-out logistic fit takes it from 0.638 to 0.773 (0.784 with the rotating feed's mean). Second, the negotiation is what makes it: the same mould with every net grown alone scores 0.634, no better than RUDY. Third, the physical overflow — millimetres of track with no room — is weak on its own, because the room model is generous (a 0.84 mm gap between header pins is counted as 1.9 tracks a layer) and the copper rarely runs out; the signal is how near it comes. A finer grid (48×48, 2.8× the time) does not help, and feeding a net at a different pin each iteration, Tero's Tokyo recipe, moves the AUC by a hundredth.
What it does not do, and this is the finding that matters for the placer. The anneal loop does not ask which board is harder; it asks which of this board's placements will route. Eight seeds a board give 42 boards with both verdicts under the merged label (58 under freerouting, 63 under the A* router), and on each such board the score's AUC over that board's own placements is computed and the boards counted. No score orders them: the saturation at pins scores 0.52 within board (21 boards right, 16 wrong), the best of the fifty-odd scores tested reaches 0.60 (25 right, 16 wrong, p 0.21), and RUDY, wirelength and the fold sit in the same band. Under the A* router alone the finest grid's peak saturation gets 41 boards right to 18 (p 0.004), which does not survive fifty tests and does not repeat under the other two labels. So the mould is a board-difficulty estimator — a strong one, and the first — and not an anneal judge, the same shape of result the routability model's audit found. What it does see within a board is cost: against the A* router's via count over each board's own placements, the 95th-percentile saturation runs at median ρ +0.34 (89 boards positive to 29), and +0.19 with wirelength held (p 2e-3), where the fold's chord count gave +0.25 and +0.16 — the same signal, read by routing rather than by counting. Wirelength alone is +0.56. The verdict is something none of these see at 32 cells a side: whether a board's anneal routes turns on geometry finer than any of them draw.
The six extra seeds are also the first labels made off this machine. The 250 placements the laptop had routed before the run moved to Azure were routed there again, and every verdict under both routers and every via count came back identical; the labels do not depend on the machine.
The pad field, drawn
If the verdict turns on geometry finer than the grid, draw it finer. physarum.escape() lays 8 mm windows over the board at 0.25 mm a cell — a 0.9 mm pad is four cells and the 0.84 mm between a header's pins three — draws every window that holds pins of two or more nets, and grows the nets in it as before, except that a net with pins outside the window is not asked to connect them, only to get out: it is fed a unit at every pin inside and the window's rim is where the flow leaves. A track is wider than a cell now, so what a net puts on an edge is spread over the edges its track covers, and the room a net sees on an edge is the least over those same edges, because a track one cell from a pad has no room for its clearance. A pin's escape is read along its own net within a millimetre of it. Eight seconds a placement.
| score | AUC, either router | ρ holding size | within board, 42 boards |
|---|---|---|---|
| coarse mould, saturation at pins | 0.757 | +0.33 | 0.52 (21 right, 16 wrong) |
| windows, every net alone, pins with no way out | 0.784 | +0.38 | 0.55 (22 right, 15 wrong, p 0.32) |
| windows, every net alone, mean saturation at pins | 0.772 | +0.35 | 0.56 (25 right, 16 wrong, p 0.21) |
| windows, negotiated, pins with no way out | 0.769 | +0.39 | 0.48 (15 right, 17 wrong) |
Between boards it is the best estimator yet, and the purer geometry wins: at pad resolution, nets each taking their shortest way out say more than nets that negotiate, because what is being read is how walled in a pin is, not how the fight over a corridor came out. The held-out logistic fit on the net count goes to 0.788 with it. Within a board it is where everything else was.
Held out: 1,774 boards it had never seen
Every number above was tuned and reported on the same 131 boards. So 2,000 boards from the bank that none of it had touched were annealed at four seeds and routed under both routers on Azure (tools/azure_ab.py --boards 2000 --exclude-from ...), and the mould was run on them cold: 6,822 placements of 1,774 boards both routers answered for.
| score | AUC, either router | ρ holding size | AUC, freerouting | AUC, A* |
|---|---|---|---|---|
| net count | 0.713 | — | 0.699 | 0.716 |
| RUDY peak | 0.682 | +0.28 | 0.629 | 0.644 |
| fold, chords displaced | 0.743 | +0.16 | 0.711 | 0.754 |
| coarse mould, saturation, 95th percentile | 0.803 | +0.35 | 0.742 | 0.787 |
| windows, every net alone, pins with no way out | 0.829 | +0.42 | 0.751 | 0.799 |
| pads closer than a track + two clearances (a count, microseconds) | 0.821 | +0.38 | 0.759 | 0.801 |
Correction (2026-09-09). The windows' 0.829 is matched within noise by a count of pads closer to their nearest neighbour than a track plus two clearances (0.821, difference +0.008 with a paired interval over boards of [−0.006, +0.022]); holding size and three such pitch counts the windows keep a partial ρ of +0.105 and the coarse mould +0.206. "Not a size prior" stands; at pad resolution the reading is mostly a pitch prior, and the coarse negotiated mould is the part that reads past the geometry. Measured by tools/pitch_ab.py; the Mendicant paper's §4 carries it.
Held out, it is stronger, not weaker: 0.829 against 0.784 on the boards it was tuned on, ρ +0.42 holding the three size counts (p 1e-309), and the held-out logistic fit on the net count goes from 0.705 to 0.835. The size prior is stronger on this wider set too (0.713 against 0.650), and the mould's margin over it is the same thirteen hundredths.
The ablation holds here too, and it is the claim most worth checking on boards nobody tuned on: the same coarse mould with every net grown alone, negotiating with nothing, scores 0.740 where the negotiated one scores 0.803. Two thirds of the mould's margin over the net count is the nets competing for room, not the trees themselves. (At pad resolution the ordering reverses, as it did on the tuning set — there the unnegotiated windows are the better score, because what they read is how walled in a pin is rather than how a fight over a corridor came out.) The claim stands on boards nobody tuned on.
With 313 mixed boards rather than 42, one within-board signal clears significance: the coarse mould's 95th-percentile saturation orders a board's own anneals at 0.58 (173 boards right, 114 wrong, p 0.001), the score that led the 42-board table too, and it holds under the A* label alone (0.575, p 0.0002). It is real and it is weak — a judge needs 0.70 — and the pad-resolution windows carry none of it (0.47). RUDY's 95th percentile reaches 0.57 (p 0.014). One reading goes the other way: within a board, the anneal whose trees detour more is the one that routes (0.44, p 0.0002), which reads as a placement with room to detour in having room.
Both routers, in the build
Everything above measures two routers; this is the pipeline using them. When
freerouting finishes a placement and our own walk of the copper still finds a
net short of a pin, soldermask/astar.py's second_opinion() routes the same
placement again with the A* router, reads the copper back out of the
.kicad_pcb it writes, and walks it the same way. The A* copper is kept only
when it reaches pins freerouting's did not — never on a tie — so a rescue can
only add connectivity, and the board that ships is the better of the two.
Four things about it are deliberate:
- It costs nothing on a board that routes. A placement freerouting closes never reaches the second router, so the common path is exactly what it was.
- The corpus stays one oracle's. The example recorded is freerouting's
verdict on freerouting's copper, whatever the board ends up carrying. A
heatmap target has to be the copper of the router whose verdict sits beside
it, and two oracles in one corpus is the confound
Outcome.routerexists to keep out. The board is the product; the label is the record. - A board that needs wider copper is refused rather than routed badly. The A* router lays one track width, so a placement with a net designed for more than the convention keeps freerouting's copper and says so.
- A board that could have had a second opinion and did not says why. If the router is not installed and a net is still short of a pin, that goes in the board's gaps with the command that installs it, rather than shipping a dead net in silence.
A worked example, from the held-out set. On daschiller-nfc_sensor, freerouting
reports nothing unrouted and no violation — and leaves GND short of two pins,
which is the blind spot ses.incomplete() was written for. The A* router closes
it: the board goes from 61 wires with a dead ground net to 110 wires and 8 vias
with every pin reached, and its notes say which router laid the copper. Since
the session file is freerouting's and the copper is not, no .ses is left
beside a rescued board to contradict it.
The whole bank
The bank's other 10,089 boards were then annealed at four seeds and routed the same way, on six machines in three regions over nineteen hours. With the tuning and held-out sets that is 42,334 placements of 10,895 boards, every one judged by both routers under one DRC — to our knowledge the largest two-router routability set there is — and 2,045 boards whose own anneals disagree, fifty times the 42 the within-board question was first asked on.
Nothing moves. The coarse mould's 95th-percentile saturation predicts that neither router closes a board at AUC 0.785 against 0.698 for the net count; the fold is 0.719; RUDY's peak 0.667. A ten-fold logistic fit on the net count goes 0.698 → 0.806 with the mould, → 0.819 with its two readings, → 0.821 with the fold. On the 6,822 placements that also carry the pad-resolution windows, net count and both scales together reach 0.856.
Within a board, with 2,045 boards instead of 313, the signal holds and stays weak: 0.574 (1,106 boards right, 753 wrong, p < 1e-15), 0.553 under freerouting alone and 0.549 under the A* router, and the ordering of scores is the same as on the small sets. It is strongest on mid-sized boards (0.58 on 10–49 nets) and disappears under ten nets, where there is little to arrange. Detour keeps running backwards (0.453, p < 1e-9). So the within-board result is not a small-sample artefact in either direction: it is a real effect of about six hundredths of AUC, measured now to three decimal places, and six hundredths is not a judge.
The routers' agreement is the same at bank scale as on 131 boards: freerouting closes 54.0% of these placements, the A* router 67.7%, either 74.3%, and they disagree on 27%. The second-router lift of twenty points is a property of the corpus, not of a sample.
The ceiling
What settles the within-board question is not another estimator but the labels. Over the 989 placements the two routers disagree on 279 — 28% — and over the placements of the 42 boards that have both verdicts they agree on 52%, a coin. Used as a score, one full router's verdict orders the other's within a board at AUC 0.48 (14 boards right, 25 wrong): freerouting does not know which of a board's anneals the A* router will close, and the A* router does not know freerouting's. The strongest within-board signal on the whole table is the A* router's own via count against the merged verdict, 0.60 over 42 boards (26 right, 15 wrong, p 0.12), and that is a full route. So on these boards, whether a given anneal routes is mostly which router ran and how its search went, and a judge that cannot see the router's search cannot order them; the geometry the estimators read — coarse or at pad scale — is not what decides it. The board-level signal is real and now well measured; the placement-level one is not there to be found in the labels as they are.
What the label is made of, measured by nudging. The night after, 100 of the held-out boards below were routed at four seeds with every part nudged by 0.05 mm, five times each, under both routers (router_ab.py --jitter 0.05 --jitters 5; 2,000 placements on Azure). Under freerouting a placement's five copies all route on 41% of placements, all fail on 38%, and split on 21%; under the A* router 56%, 23% and 22%; and the unnudged verdict is the majority of its five copies 92 to 97% of the time. So a router's verdict is mostly a property of the placement and not of its search: two thirds of placements are stable under both routers, and of those the two routers still disagree on 15% — a systematic difference in what each can route, not noise. The remaining third is marginal for at least one router, and that is where "a coin" is literally true. Put against the within-board question: among 88 boards, only 10 have both an anneal that freerouting routes stably and one it fails stably. The differences a judge could learn within a board are real but rare; most of a board's within-board variation is the marginal third and the routers' private disagreements, and that is why nothing ordered it. The nudging also gives every placement a route probability rather than a verdict, and against that the coarse mould's 95th-percentile saturation is the strongest single score: ρ −0.62 over 313 placements, −0.53 holding net count, against −0.40 for the fold and nothing for the net count once held; within a board, −0.32 over 22 boards (p 0.03).
One more measurement settles how the pieces fit. On the verdict between boards, the fold adds nothing to the mould — a held-out logistic fit on the net count and the windows' blocked pins scores 0.778, and 0.775 with the fold's displaced count added — but the two moulds at two scales add to each other: net count, blocked pins at pad resolution and the coarse grid's saturation at pins together reach 0.805. On via cost within a board they are complementary, and each survives the other: the fold holding wirelength and the mould's saturation is +0.15 (72 boards to 47, p 0.03), the saturation holding wirelength and the fold is +0.22 (79 to 40, p 5e-4). So the fold reads topology — how many crossings a placement forces, which is vias — and the mould reads room; a difficulty model wants both moulds, and an anneal's cost term wants wirelength, the fold and the coarse saturation.
Two things follow for the pipeline. The estimator's place is board difficulty — which board will fight, and how much a placement of it has to be steered — and not choosing among a board's anneals. And the cheapest way to lift the clean rate on annealed placements is not a better judge but the second router: on the same 989 placements freerouting closes 539 and either router 746, which is the case for the switch decision the router comparison left open.
Boards from the open-hardware world
Thirty-three real families capped every number in the first two revisions, and the data ablation said more placements of those boards would not move the one that matters. Finished KiCad boards are new families for free. soldermask/kicadpcb.py reads a .kicad_pcb — KiCad 5 module files and KiCad 6+ footprint files alike — into the netlist and placement this pipeline places and routes: pads with their nets, the outline from Edge.Cuts, y mirrored into a frame that runs up, rotations snapped to the quarter turns the placer uses, bottom-side parts brought to the front, holes without copper and footprints without pads dropped, and a report of every one of those decisions with the reason a board is not for this pipeline: not two layers, larger than 150 mm, more than 120 footprints, a ball grid array. The human's traces are not read. The router labels the board like any other, so the corpus stays one oracle's, and the human's own placement becomes one more placement kind — which asks two new questions, answered in the paper and then answered again: does the router route the human's placement clean, and does ISOnet rate it above our anneals? The first answer, that the human was well ahead, turned out to be this pipeline's edge-clearance defect rather than the human (see the correction at the head of this page); the second, that the model does not see any advantage, was right for a reason nobody guessed.
python tools/fetch_kicad.py repos.tsv .cache/boards/kicad/src # every .kicad_pcb from listed repositories
python tools/search_kicad.py .cache/boards/kicad/src # ...or across GitHub, by code search
python tools/exa_boards.py discover .cache/boards/kicad/src # ...or ask Exa, in sentences, and follow the links (EXA_API_KEY)
python tools/exa_boards.py add .cache/boards/kicad/src boards.txt # ...or hand it a list: Kitspace's registry, an awesome list
python tools/exa_boards.py fetch .cache/boards/kicad/src # download what either found, 12 boards a repository, over the API and then over git
python tools/kicad_boards.py .cache/boards/kicad/src .cache/boards/kicad # the usable ones, as DSNs
soldermask learn generate --hours 4 # and the harness finds themboards.jsonl records every file, kept or not, with the reader's report; manifest.jsonl records where each came from and under what licence. The first sweep read 10,982 files and kept 5,643 boards from 1,691 repositories (left out: 2710 not two layers, 1195 too few footprints, 756 too large, 274 other, 231 too many footprints, 173 too small); a median of 20 footprints and 56 mm on the longest side. The corpus went from 33 real board families to 1,200. PCBench's corpus (843 usable boards of 1,182, median 25 footprints) is held out as an external test that is never fitted on — held out by content, since 5 September 2026, and not by path. It had been identified by the path .cache/boards/pcbench alone, and freerouting vendors a copy of PCBench inside its own benchmark fixtures, so the first KiCad sweep put 866 byte-identical copies of the held-out boards into the training directory under ordinary names. 11 of the 125 external boards that have been routed were also routed from the training side, so the 0.89 below was measured with about a twelfth of its test set seen in training; the copies are gone from the training directory, the 309 examples of 60 such boards are out of the corpus, and kicad_boards.py --not-in refuses them at the door from now on: On PCBench's 125 usable boards (256 routes, never fitted on) the shipped network scores AUC 0.89 (0.83–0.93), and 0.85 among their anneals. A further 908 unique boards came from the open-schematics dataset on HuggingFace — this page said 3,449 until the board names were audited on 5 September 2026, and that figure was its boards.jsonl row count, which had the same rows in it several times over because the fetcher had been run more than once against the same parquet files. No board was lost to it and none was double-counted in training, since the harness routes the DSN files on disk and there were only ever 908 of those; the number written down here was simply larger than the thing it described. And the deployment as a user meets it, on 13 held-out boards with the same six anneals and up to three routes: ISOnet's order routed clean first time on 3 and within three on 3, using 33 routes and 2236 router seconds; wirelength's order 2, 2, 35 and 1835.
Code search reaches the end of what it can see at a thousand files a query, on default branches, in repositories GitHub has indexed. tools/exa_boards.py asks Exa instead — a search engine that takes a question in sentences and returns pages with their outbound links, and a findSimilar that returns a URL's neighbours.
The questions come in four families, and only one of them mentions GitHub. g- names a kind of board a two-layer router sees and is pinned to GitHub, where the answer is the repository itself. w- names a site people publish on: Hackaday, Crowd Supply, OSHWA's directory, Zenodo, kbd.news, HardwareX, four forges that are not GitHub. o- pins nothing at all, because most of open hardware is on a personal site, a university lab page, a hackerspace wiki or a paper's supplement, and some of it is not in English — these are asked in six languages. f- looks for the board file itself. Except for g-, each answer is read for its text and its outbound links, so a page listing a hundred projects is a hundred candidates. Where the candidates came from is the argument for asking broadly: 3,363 from the questions that pin nothing to GitHub against 2,306 from the ones that do, measured before the question set was grown to a hundred and forty-eight on the strength of it, with the best single question an open one — awesome list of open source hardware projects designed in KiCad, 275 candidates — and thirty to seventy-five repositories apiece from the Japanese, Chinese and Spanish questions that no English question found. Exa returns nothing for reddit.com, which is recorded rather than retried.
Every question, its cost and every page returned are written under src/exa/; every repository or .kicad_pcb URL in an answer or in a page's links becomes a candidate unless the manifest already has it. A hundred and forty-eight questions and three hundred seeds cost $27.91 at Exa's published prices and found 16,070 candidates; --budget caps a run and --dry-run prints the questions, marking the ones already asked and pricing only the rest. Fetching them took 12,284 files from 5,741 repositories, of which kicad_boards.py kept 6,810 — a 55% yield, and the corpus goes from 4,358 board families to 11,861. What the trees held on the way past is the other measurement: 29,980 .kicad_pcb, 8,944 Eagle .brd and 1,152 Altium, which is the size of what an Eagle reader would add.
Every format kicad-cli imports (11 September 2026). The sweeps above asked for KiCad boards and took .kicad_pcb alone. GitHub's code index, asked the same day, holds 87,808 .kicad_pcb against 45,440 Eagle .brd, 1,800 KiCad pre-2013 .brd, 24,512 Eagle .sch and 160,256 .kicad_sch (default branches, text files under 384 KB — an order of magnitude, not an inventory). SparkFun's 1,417 and Adafruit's 1,868 public repositories hold a board in 65% and 37% of cases (sampled blind, 40 and 60), nearly all Eagle beside its schematic, under CC-BY-SA; the sweeps had reached 29 and 40 of them. kicad-cli pcb import reads Eagle, Altium, PADS and the legacy KiCad board, so the fetch now takes every such file (fetch_kicad.is_board, panels left aside), kicad_boards.py converts through kicadcli.import_board — the format read off the bytes, since three programs use .brd — into SRC/converted/ and reads the conversion, and --merge grows the bank in place without renaming a board already in it. tools/org_boards.py lists a vendor's every public repository as candidates; exa_boards.py fetch re-queues the 1,119 repositories earlier sweeps marked no-kicad while counting Eagle files in them.
python tools/org_boards.py .cache/boards/kicad/src sparkfun adafruit sparkfunX # a vendor's every repository
python tools/exa_boards.py fetch .cache/boards/kicad/src --how clone # boards of every format, over git
python tools/kicad_boards.py .cache/boards/kicad/src .cache/boards/kicad --merge --not-in .cache/boards/pcbenchThe first such sweep (11 September 2026) — three vendors' every repository, the 1,119 re-queued repositories and 888 candidates from 32 new questions costing $3.08 — fetched 5,820 files over git in 31 minutes and kept Eagle 3,259 of 3,986 (82%), Altium 301 of 605 (50%), legacy 52 of 69, and 780 more KiCad boards: the two-layer bank goes from 12,239 to 16,367 boards, 1,680 of them SparkFun's, Adafruit's and SparkFunX's. On the way the reader stopped counting a layer table at its word when it lists more than two: kicad-cli's Eagle importer declares KiCad's full sixteen-layer stack for a two-layer board, so kicadpcb.read counts the two outer layers plus the inner ones copper actually sits on, rounded up to an even stack, and records declared_layers when the two differ. And the boards the two-layer rule turns away are written too, into a second bank: kicad_boards.py --layers 4,6 into .cache/boards/multilayer holds 1,861 boards (1,788 four-layer, 73 six-layer; median 54 footprints against the two-layer bank's 20), routable with learn generate --boards .cache/boards/multilayer and only that way — the bank's layers.json keeps corpus.find_dsns() out of it, so a bare learn generate cannot route four-layer boards into the corpus every model was fitted on. None of the new boards is routed yet.
A caution the professional boards make concrete: SparkFun's own placement of its ADXL313 breakout came back from freerouting with 7 unrouted pins and 14 violations — jumper pads 0.1 mm apart on different nets, a 0.5 mm-pitch LFCSP the 0.25 mm track cannot pass — so under the corpus label (freerouting's log, violations == 0) a shipped product reads as failing. That is the judge confound measured on 6 September (59% agreement between the corpus label and kicad-cli's; the relative bar worth +22.4 points), and a vendor board's clean rate in this corpus is not a statement about the vendor until the corpus carries the kicad-cli judge.
fetch turns candidates into files, and asks a repository three things: the default branch, the file tree, the licence. GitHub's REST API answers them in two calls and allows five thousand an hour, which four thousand candidates do not fit in. A blobless clone — --filter=blob:none --depth 1 --single-branch --no-checkout — answers all three over git in 132 KB and about a second and a half, against nineteen megabytes for the same repository checked out; only the boards are then checked out, so git fetches exactly those blobs. Git has no hourly limit, so the API is used while the hour lasts and git after it, and a sweep of any size finishes in one sitting — 12,031 repositories came through the API and 4,295 over git, with nothing deferred. On the API alone the larger sweep would have spent five hours waiting on the clock; it took forty-eight minutes. Sizes are never asked of git: ls-tree -l pulls every blob to measure it, 2m40s against 0.04s on one repository, so boards are taken by path to a cap and the smallest kept once they are on disk. The API's fork flag is replaced by something better — every board carries the SHA-256 of its bytes, and one already in the corpus is not taken again whoever published it, which catches a vendored copy as well as a fork.
The same door takes a list by hand, and Kitspace's registry through it is the first measurement: 150 URLs, 138 repositories the corpus did not have (126 on GitHub, 12 on GitLab), 88 of them with a KiCad board, 170 files fetched in 102 seconds, 107 kept — a 63% yield against code search's 51%. The 38 repositories with no KiCad board held 95 Eagle .brd files and 3 Altium, which is the first count of what an Eagle reader would add. Reading those 170 also showed the reader a thing it had been getting wrong since the first sweep: KiCad 5 writes a renamed copper layer under its new name — (0 Top signal), (31 Cuivre signal) — and the reader counted copper by names ending in .Cu, so 1,062 files of the first sweep read as having no copper at all. Copper is now counted by the type column and the names mapped back by their place in the stack; read again, 373 of the 1,062 have copper (the other 689 are KiCad's own QA stubs with no layer block), and 207 of those are two-layer boards this pipeline can route — new families that had been on disk since the first sweep.
Building the corpus and the model
pip install -e ".[learn]" # torch, for training only
soldermask learn generate --hours 2 # place and route boards until the time is up
soldermask learn status # how many routes, from how many boards
soldermask learn train # fit the three, judge, ship the winner
soldermask learn report # what the shipped model claims
soldermask learn relabel # route again anything the router never counted
soldermask learn relabel --rules # route again, at the clearance its parts allow, anything labelled at a wider one
soldermask learn backfill --hours 3 # route again anything with no record of where the copper went
soldermask learn ablate # the network with one thing taken away at a time, over seeds
soldermask learn crossval # every family held out once
soldermask learn redraw # the maps again, with today's channels, from stored placements
soldermask learn train --seeds 3 # ship an ensemble, each seed on record
soldermask learn train --recipe full # the scalars and all eight channels, for comparison (lean is the default)backfill made the corpus whole again for the heatmap head: every example recorded before the targets existed was routed once more and rewritten under its own id — maps, verdict and where the copper went, from one route of today's placement. Two things moved when it did, and both are counted in its output. The anneal's random stream had changed since the corpus was first made (its cost became incremental), so most transforms now make a different placement from the same seed; and the router is not perfectly deterministic, so a few verdicts flip on the same placement. The numbers in the table above are from the corpus after this, and they are not the numbers in the first version of this page; the paper says which is which.
ablate is the evidence for what the shipped network is made of: maps without scalars, scalars without maps, no heatmap head, no rotations in training, one view at inference, no calibration, a 16-cell grid, real boards only — each trained over three seeds and judged on the same held-out boards, with the spread over seeds beside the mean. The rows land in .cache/ablations.json and in the paper.
generate reads every DSN under the repository (a DSN is a complete statement of what the router was given, and every saved version carries one), places each board forty ways — other seeds, fewer anneal steps, a squeezed or stretched outline, random positions, a jitter, a swap — and draws four hundred synthetic boards from the package catalogue so the model sees densities the twenty real designs never reach. Four workers, two router threads each, is the load this machine takes without the anneals slowing down; five routers at their default thread count put the load average at 31.
What it does not do
- It never routes. The router runs on every build; the model orders the queue.
- It knows nothing about a board it has not seen the shape of. A 150 mm board with a module the corpus never met is rated by a model trained on boards that were not it, and the note says how many boards that was.
- It is trained on freerouting's verdict, which is not a fab's. A board the router calls clean can still fail KiCad's DRC on silkscreen or courtyard rules the router does not check.
- The corpus was measured on this machine's freerouting 2.3.0 with
-mp 40; routes from 4 September 2026 on are 2.4.1's and say so (Outcome.router). On 120 held-out placements the two routers agree on 119, so the labels were not remade. A router that improves makes the old labels pessimistic. - Every label made before 2026-09-03 was routed at 0.2 mm clearance, and a board whose parts are closer than that could not route clean under it whatever the placement. Since then a board is routed at the clearance its parts allow (
dsn.rules), the label says which (Outcome.clearance_mm,Noneon the older ones), andlearn statuscounts the examples the rule would now be lower for (below_convention).learn relabel --rulesroutes those again at today's rule, the same stored placement and nothing else moved, and says how many verdicts turned; until it has run, the model's verdict on such a board is a verdict on the old rule.