14 — Placement test plan¶
Part of the TraceMaker plan. Start at
../PLAN.md. Design: 04-placement.md. Gates: 11-roadmap.md M7 and M8. Written 2026-10-03.
This plan says how component placement (tracemaker-place) is tested: what is checked, on which boards, against
which baselines, and what has to be true before placement is called good. It covers the existing modes
(full, refine, auto with --route-check) and the M8 additions (routable, eco).
1. What "good placement" has to mean¶
A placement is good when, in this order:
- It is legal. No new courtyard overlaps, parts inside the board outline, pads keep copper, edge and solder-mask clearances, locked and fixed parts untouched, and the board still opens in KiCad unchanged apart from the moved footprints.
- It routes at least as well as the human placement. Measured by TraceMaker and Freerouting, judged by KiCad's DRC, with the same budget for both placements.
- It respects design intent that the netlist does not state. Decoupling capacitors stay close to their chip, connectors stay at the edge, crystals stay close to their oscillator pins, mechanical parts stay put.
- It is short. Wirelength (HPWL) and airwire crossings go down, with a reported distance from the proven lower bound.
- It is reproducible and reasonably fast. The same seed gives the same board at any thread count, and placement time is recorded.
Wirelength comes fourth on purpose: the M7 evaluation showed that shorter wirelength from denser packing can make a board route worse (13 fully routed boards against 15 for the human placement), so wirelength alone never decides that a placement is better.
2. Test levels¶
| Level | What | Where | Runs |
|---|---|---|---|
| L0 unit | Geometry, legality, cost, solvers, determinism | src/place/test_place.cpp (Catch2, ctest -L place) |
Every build |
| L1 property | Invariants on random and fixture inputs (below) | test_place.cpp, tagged [property] |
Every build |
| L2 board | Place real boards, check legality and the KiCad round trip | src/place/eval_place.py --truth |
Every placement change |
| L3 routability | Route human vs placed boards with a fixed budget; KiCad DRC | bench/place_auto.py, bench/place_bench.py (to build, §7) |
Every placement change (quick set); weekly (full set) |
| L4 baselines | SA-PCB and other placers on the same boards | bench/place_baselines.py (to build) |
Per milestone |
| L5 human review | A designer looks at placed boards in KiCad | Checklist (§6) | Per milestone |
2.1 Unit tests (L0) — existing¶
The 11 tests in test_place.cpp: KiCad rotation convention; convex hull; power-net name detection; translated
closer() agreement with the exact geometry predicate; min-cost flow on a small graph; the HPWL lower bound is
exact on a chain and bounds random placements; the raster fast path is conservative (raster free implies exactly
legal); legalisation removes every overlap; the full pipeline is legal, deterministic and its incremental cost is
exact; rotation descent is monotone; problem extraction from a KiCad board.
2.2 Property tests (L1) — to add¶
Each property is checked on 50 seeded random problems and on 5 fixture boards:
| # | Property |
|---|---|
| P1 | Every output placement is legal under the exact checker (not only the raster). |
| P2 | Locked parts, fixed parts (connectors at the edge, mounting holes, parts with Edge.Cuts or keepouts) and parts touching existing routed copper keep their position, rotation and side exactly. |
| P3 | The reported HPWL equals an independent recomputation from the written board. |
| P4 | Lower bound ≤ HPWL of every placement found (human, full, refine). |
| P5 | Same seed ⇒ byte-identical output board for 1, 3 and 16 threads. |
| P6 | refine never increases the cost it optimises; LNS and parallel tempering never return a worse best state than their start. |
| P7 | --route-check and auto never return a placement with more unrouted connections than the input (same work budget). |
| P8 | eco moves at most the configured number of parts and only parts within the configured distance of failed connections. |
| P9 | Placing an already optimal toy problem (two parts, one net) returns the analytic optimum. |
3. Datasets¶
| Set | Boards | Purpose | Notes |
|---|---|---|---|
| Q — quick | 10 PCBench tier-A/B boards (fixed list below) | Every placement change | 2–4 minutes on 16 threads |
| D — development | The 23 boards of doc 04 §7.3 | Tuning | Already used: never quote results from it as final |
| H — held-out | 40 PCBench boards never used in placement or routing development (seeded sample, §3.1) | Milestone results | Frozen binary; no tuning afterwards |
| DAC | DAC 2020 benchmarks (bench/data/dac2020_prepared, 10 boards with Specctra files, 11 in total) |
Placement benchmark built for this purpose | Locked parts are marked in the files; the authors' metrics are via count, layers and bounding-box area |
| S — from scratch | The H boards with every movable part piled at the board centre (generated) | full mode as a schematic-to-board flow; the M8 gate's "schematic-to-board set" |
Human placement still serves as the reference |
| K — KiCad demos | Demo boards with movable parts | Rule coverage (net classes, custom rules, zones) | Not a quality benchmark |
Quick set Q: ChirpHardware_chirp, IGN01A_IGN01A, LadybugLiteBlue_HW_LadybugBlueLite, PiPlay_SDHat,
Solare-BQ24210_Solare-BQ24210, beast-phat_beast-phat, domotics_out-board, kitspace_hbridge_driver,
scimpy_volumebuffer, uC3Moy_uC3Moy.
3.1 Selecting the held-out set H¶
Sample with a fixed seed (7) from PCBench boards that:
- appear in no
bench/results/*/boards.jsonl, noquality.jsonland none of the D boards; - have at least 10 movable parts and at most 300 parts;
- have a courtyard on at least 80% of their parts (otherwise legality is undefined);
- route completely in their human placement with TraceMaker at a 120 s budget (so routability changes are attributable to placement).
Stratify 20/20 by part count (below and above the median). Record the list in bench/place_sets/H.txt
and never change it; a new held-out set gets a new name.
4. Metrics¶
All metrics are computed from the written .kicad_pcb, never from the placer's internal state.
| Group | Metric | Definition |
|---|---|---|
| Legality | New KiCad DRC errors | courtyards_overlap, pth_inside_courtyard, npth_inside_courtyard, copper_edge_clearance, clearance, shorting_items, solder_mask_bridge, hole_clearance errors in the placed board that the human board does not have |
| Legality | Round trip | kicad_truth.py on the placed board equals tracemaker inspect --json except for moved footprints |
| Legality | Fixed parts | Count of locked or fixed parts whose position, rotation or side changed (must be 0) |
| Routability | Unrouted (TraceMaker) | Unrouted connections after tracemaker route --work W --threads 8, W fixed per board size, and at 120 s |
| Routability | Unrouted (Freerouting) | Same with Freerouting 2.5 and 1.9 via bench/compare.py (independent router: guards against a placement tuned to TraceMaker) |
| Routability | Clean pass | Fully routed and no router-introduced KiCad DRC errors |
| Routability | Route quality | Track length, vias, bends (bench/quality.py) of the routed placed board vs the routed human board |
| Intent | Decoupling distance | For each capacitor whose nets are one power and one ground net shared with an IC, the distance to the nearest such IC's power pin; report median and maximum, human vs placed |
| Intent | Connector edge | Connectors (reference prefix J/P/CN or footprint library "Connector") within 2 mm of the edge, human vs placed (must not drop) |
| Intent | Crystal distance | Crystal/oscillator to the IC pins it connects to (median, maximum) |
| Intent | Orientation | Share of two-pin passives at 0°/90°/180°/270°; mixed 45° rotations count as violations |
| Wirelength | HPWL | Signal HPWL, weighted HPWL (power nets ×0.1) |
| Wirelength | Lower-bound gap | Weighted HPWL ÷ the min-cost-flow lower bound (any rotation) |
| Wirelength | Crossings | Airwire (MST) crossings |
| Wirelength | Area | Bounding box of all parts (the DAC 2020 metric) |
| Process | Time, determinism | Placement wall time and moves; output hash for 1 and 16 threads |
5. Baselines and comparisons¶
| Baseline | How | Why |
|---|---|---|
| Human placement | The fixture as given | The real target: a designer accepted it |
TraceMaker refine, full, auto, routable, eco |
tracemaker-place --mode ... |
Mode-against-mode |
| SA-PCB | Build The-OpenROAD-Project/SA-PCB (BSD-3, archived 2026) under build/tools/, adapter from .kicad_pcb |
The M7 gate: HPWL ≤ SA-PCB |
| Random legal placement | full with the annealer off and a random start (seeded) |
Sanity floor: every metric must beat it |
| Ablations | full without rotations, without crossings term, without the routability term |
Each part of the cost must help or be removed |
6. Human review checklist (L5)¶
For 5 boards per milestone (2 from H, 2 from DAC, 1 from S), a designer opens the human and placed boards side by side in KiCad and scores 1–5:
- Would you manufacture this placement without changes?
- Decoupling capacitors next to their pins?
- Connectors, switches, LEDs and mounting holes where the enclosure expects them?
- Silkscreen readable (references not under parts)?
- Thermal parts (regulators, power transistors) spread and near copper?
- Analogue/digital and high-current/small-signal separation kept?
Record the scores and comments in dev/placement_review.md. A score below 3 on question 1 for any board is a
release blocker for that mode, whatever the numbers say.
7. Tooling to build¶
| Tool | Purpose |
|---|---|
bench/place_bench.py |
One command for L3: place (all modes) → route with TraceMaker (work budget and 120 s) and optionally Freerouting → KiCad DRC → metrics of §4 → bench/results/<run>/place_summary.json and report.md; reuses eval_place.py, quality.py, compare.py |
bench/place_sets/ |
Frozen board lists Q, D, H, DAC, S |
bench/make_scratch_set.py |
Builds set S: movable parts piled at the board centre, everything else unchanged |
bench/place_intent.py |
Design-intent metrics (decoupling, connector edge, crystal, orientation) |
bench/place_baselines.py |
SA-PCB build, adapter and runs; random-placement floor |
| Dashboard panel | Latest placement run: legality, unrouted vs human, HPWL ratio, intent metrics |
8. Gates and pass criteria¶
| Gate | Criterion | Set |
|---|---|---|
| G1 legality (every change) | 0 new KiCad DRC errors of the legality group; 0 moved fixed parts; round trip exact; P1–P9 pass | Q (and H at milestones) |
| G2 routability (every change) | Unrouted (TraceMaker, fixed work) summed over Q not above the human placement; no board worse by more than 1 connection in auto |
Q |
| G3 M7 gate | HPWL ≤ SA-PCB on ≥ 80% of H boards; G1 on H | H |
| G4 M8 gate | Clean pass ≥ 90% of S boards with routable/full (TraceMaker, 120 s); eco closes at least half of the H boards that none mode leaves unrouted |
S, H |
| G5 independence | On H, the placed boards' unrouted count with Freerouting 2.5 is not above the human boards' | H |
| G6 intent | Median decoupling distance and connector-at-edge count not worse than human on H | H |
| G7 human review | No board scored below 3 on question 1 | L5 boards |
A change that fails G1 or G2 is not merged. G3–G7 are reported with every milestone result, pass or fail.
9. Procedure¶
ctest --preset release -L place(L0, L1).bench/place_bench.py --set Q --modes refine auto --work 3000000(L2, L3) on every placement change.- At a milestone: freeze the binary, run
--set H,--set DAC,--set Swith all modes, Freerouting comparison on H, baselines (L4), then the human review (L5). - Publish:
report/section and the dashboard panel; record decisions in doc 12.
10. Current status (2026-10-03)¶
| Item | Status |
|---|---|
| L0 unit tests | 11 tests, passing; M8 adds tests for parallel tempering, LNS and ECO (in progress) |
| L1 property tests | Not yet written as such (P5 and P7 are partly covered by existing tests and place_auto.py) |
| L2 board evaluation | eval_place.py (legality, round trip, HPWL, crossings) on the D set |
| L3 routability | place_auto.py on the D set: HPWL −24.5%, unrouted 61 → 59, 0 new DRC errors |
| Held-out set H, scratch set S | Built and run 2026-10-06 (bench/make_place_sets.py, bench/place_sets/): H human 95 %, S from scratch 85 % clean, gate G4 (90 %) not met (doc 04 §10) |
| DAC placement runs | Not started |
| Design-intent metrics | Not started |
| SA-PCB baseline (M7 gate) | Not started |
| Human review | Not started |