p.enthalabs

GitHub - Koppar-AI/autorouter-bench: Measuring Koppar PCB autorouter against other solutions - benchmark on real open-hardware boards, scored by KiCad

A benchmark for PCB autorouters on real, professionally routed open-hardware boards, with KiCad as the referee.

Each router gets the same eight boards with their routing removed, the same placement, the same design rules and the same wall-clock cap. What comes back is scored by KiCad, never by the router:

- **Completion**: `100 × (1 − unconnected / connections)`, both counted by KiCad's ratsnest on the board the router returned.

- **DRC**: `kicad-cli pcb drc --severity-error` with the board's benchmark rules, with any zones the router drew refilled. Reported as the errors a router added over the human board's own findings (two upstream boards carry an unclosed outline or a courtyard overlap).

- **Vias, track length and wall-clock seconds**, as measured.

A router that reports 100% and leaves a pad stranded scores the stranded pad.

The boards

[](https://github.com/Koppar-AI/autorouter-bench#the-boards) | Board | Layers | Source | Licence | | --- | --- | --- | --- | | pic-programmer | 2 | KiCad demos, tag 10.0.4 | GPL-3.0-or-later | | rp2040-minimal | 2 | Raspberry Pi RP2040 minimal design, revision 2 (archived download) | Raspberry Pi design files | | pico-vga | 4 (ground planes inside) | Raspberry Pi Pico VGA, SD and audio base (archived download) | Raspberry Pi design files | | cm5io | 4 | Raspberry Pi CM5 IO board, revision 2 | Raspberry Pi design files | | rk3328-som-evb | 4 | OLIMEX RK3328-SOM-EVB | GPL-3.0 / CERN-OHL-1.2 | | scalenode-cm4 | 4 | Antmicro Scalenode CM4 baseboard | Apache-2.0 | | stmp157-som-evb | 4 | OLIMEX STMP157-BASE-SOM-EVB | GPL-3.0 | | imx8mp-som-evb | 4 | OLIMEX iMX8MP-SOM-EVB | GPL-3.0 |

No board is stored in this repository. `scripts/fetch_boards.py` downloads each one from its pinned upstream source and checks it against `boards/checksums.json`. The two older Raspberry Pi archives are fetched from the Internet Archive, because the live URLs now serve a later revision.

The fetched board is loaded and saved once in KiCad 10, which upgrades the file format and leaves the geometry alone. That is the human reference. The input every router gets is its placement only: the tracks, vias and copper zones are deleted, and the outline, footprints and keepout areas stay. Every net is the router's to connect, ground included; a router may pour planes of its own.

The 25 public boards (set `zap25`)

[](https://github.com/Koppar-AI/autorouter-bench#the-25-public-boards-set-zap25)

The 25 boards Zap! (Mutual Solutions) published its comparison on (zap.mutual.solutions/compare, measured 12-13 October 2026), pinned to the same commits and listed in `boards/manifest-zap25.json`. They have no benchmark rules file: each board runs under its own project's rules, as in that comparison. They are selected with `BOARD_SET=zap25`; without it every script sees the eight boards above. Two of them ship custom design rules (`.kicad_dru`: seven, stm32h7-hdmi-sodimm); they are fetched beside the board as `circuit.kicad_dru`, so every router and the referee judge by them. Koppar's run on this set, beside the numbers Zap! published for every router, is `results/2026-10-zap25/summary.md`.

| Board | Layers | Source | Licence | | --- | --- | --- | --- | | cm4-lvds-adapter | 6 | Antmicro CM4 baseboard LVDS adapter | Apache-2.0 | | ecp5-dc-scm | 8 | Antmicro ECP5 DC-SCM | Apache-2.0 | | ftdi-jtag-programmer | 4 | FTDI JTAG programmer | CERN-OHL-P-2.0 | | trellis-core | 4 | Trellis Core | CERN-OHL-P-2.0 | | bitaxe-ultra | 4 | Bitaxe Ultra (fork) | CERN-OHL-S-2.0 | | ender3-loadcell | 2 | Ender-3 load-cell bed-levelling sensor | MIT | | rp2040-devboard | 2 | RP2040 development board | MIT | | discretesim-hwtest | 2 | discretesim test board | MIT | | water-level-monitor | 2 | Arduino water-level monitor | MIT | | ecp5-bga381-ft2232 | 4 | ECP5 BGA-381 FT2232HQ FIFO board | MIT | | emberone | 6 | EmberOne | CERN-OHL-S-2.0 | | argos-demo | 4 | Argos demo board | CERN-OHL-S-2.0 | | rp2350-pico2-bb48 | 4 | RP2350-PICO2-BB48 | MIT | | cvbs-mipi-bridge | 4 | Antmicro CVBS-to-MIPI bridge | Apache-2.0 | | dual-ipex-csi-interposer | 4 | Antmicro dual IPEX CSI interposer | Apache-2.0 | | gmsl-serializer | 4 | Antmicro GMSL serializer | Apache-2.0 | | pmod-i3c-sensor | 4 | Antmicro PMOD I3C sensor board | Apache-2.0 | | si-test-board | 4 | Antmicro signal-integrity test board | Apache-2.0 | | stm32h7-hdmi-sodimm | 6 | Antmicro STM32H7 HDMI SO-DIMM board | Apache-2.0 | | tiliqua-motherboard | 6 | Tiliqua (apfaudio) | CERN-OHL-S-2.0 | | buildbotics-controller | 4 | Buildbotics controller | CERN-OHL-S-2.0 | | hackrf-pro | 6 | HackRF Pro | CERN-OHL-P-2.0 | | seven | 4 | Seven | CERN-OHL-P-2.0 | | kintex-pcie | 8 | Kintex PCIe board | Apache-2.0 | | xerxes | 6 | Xerxes | Apache-2.0 |

The benchmark rules

[](https://github.com/Koppar-AI/autorouter-bench#the-benchmark-rules)

`boards/rules/<board>.json` lists every change from the upstream project's rules; the fetch script applies exactly that, and every router and the DRC referee get the result. The changes set one floor (0.1 mm clearance and tracks on the 4-layer carriers) and drop the impedance classes and cm5io's PoE spacing, so the table measures routing under that floor, not each design's own constraints. The 25-board set runs under each board's own rules.

PCBWorld-Bench

[](https://github.com/Koppar-AI/autorouter-bench#pcbworld-bench)

The real-board test sets of PCBWorld (LG AI Research): D3-A (99 boards) and D3-B (10), built from the public PCBench collection by PCBWorld's own preparation chain. `pcbworld/` holds the script that builds them and the clean-pass scorer; `pcbworld/README.md` says how.

Running it

[](https://github.com/Koppar-AI/autorouter-bench#running-it) Needs Linux, KiCad 10 (`kicad-cli` and a python that imports `pcbnew`), Python 3.10+ and Node 22+.

export KICAD_PYTHON=/usr/bin/python3 # a python that imports pcbnew python3 scripts/fetch_boards.py # the eight boards, checked and prepared BOARD_SET=zap25 python3 scripts/fetch_boards.py # the 25 public boards of Zap!'s comparison

tools/install.sh freerouting # Freerouting 2.5.0 and Java 25 tools/install.sh node # tscircuit 0.0.979, KiCad Routing Tools 0.1.9 tools/install.sh tracemaker # TraceMaker 0.9.0, built from source (CUDA)

python3 harness/run.py freerouting # or tscircuit, krt, tracemaker, koppar, koppar_filled, zap; optional board names python3 scripts/collect.py # results/<campaign>/summary.md

`CAP_S` sets the wall-clock cap per board (default 1200 s). Results merge by board into `results/<campaign>/<tool>.json`.

How each router is run

[](https://github.com/Koppar-AI/autorouter-bench#how-each-router-is-run)

- **Freerouting 2.5.0**: the board exported to Specctra DSN by KiCad, routed with `-mp 100 -mt 8`, and the session imported back by KiCad. Freerouting writes its result only when it finishes, so a run past the cap has no board to score.

- **tscircuit capacity autorouter 0.0.979**: the board as a SimpleRouteJson (KiCad's bounding box for every pad, hole, copper graphic and keepout; one connection per net, ground included), routed with `AutoroutingPipelineSolver`, and the returned traces written back onto the board as returned.

- **KiCad Routing Tools 0.1.9** (`@tscircuit/krt-wasm`): the same input, on a 0.05 mm grid with the board's default clearance. The package routes a whole board or nothing: when it gives up inside the cap, the harness routes one net at a time with the copper routed so far as obstacles and keeps what closes. The package does not include the project's rip-up and reroute, which is Python; results here are for the WASM router core only.

- **TraceMaker 0.9.0**: reads and writes KiCad boards directly, given the cap as its time limit.

- **Koppar**: a hosted service. Its results are produced by the service on the same machine, with the same inputs and cap, and scored by the same collector. `harness/run.py koppar` sends a board and its project to the public endpoint at tools.koppar.ai; a board past 300 connections needs an account API key, read from `KOPPAR_API_KEY`. `koppar` keeps every via out of pin pads (the default); `koppar_filled` lets vias sit in pin pads, for a board ordered with filled and capped vias. The service holds a route to the 1200 s cap itself: a step that would end past it is skipped or undone, so the board returned is the last one inside the cap. The harness polls the job for up to the cap + 600 s, which is waiting time, not routing time.

- **Zap!** (Mutual Solutions): a hosted service. `harness/run.py zap` sends the board and its project to zap.mutual.solutions with `fab_floor=board` (the board's rules, as every other tool gets) and the service defaults otherwise (its own pours, no vias in pads), and `minutes` = the cap (20). Time is the job's run time on the service, without its queue, upload or download, so the whole cap is routing time, as it is for Koppar. The job is polled for up to the cap + 600 s, like Koppar's; a board that comes back after the cap is scored and marked late, and a job still running then is cancelled. Boards run one at a time. The API key is read from `ZAP_KEY_FILE` (default `~/.zap-key`).

Every router runs with its defaults and the board's rules. A router's author may know settings that do better; the harness is here so anyone can show it.

Results

[](https://github.com/Koppar-AI/autorouter-bench#results)

`results/2026-10-placement/summary.md` is the October 2026 campaign: one workstation (24 cores, one RTX 5090), 1200 s per board, KiCad 10.0.4 as referee. How each column was produced, and what it does not score, is in `results/2026-10-placement/NOTES.md`.

Boards fully routed (100% completion) with no DRC error added, out of eight:

| Router | Boards | | --- | --- | | Koppar | 8 | | Koppar (filled vias) | 8 | | TraceMaker 0.9.0 | 5 | | Zap! 2026.10.14.78 | 3 | | Freerouting 2.5.0 | 0 | | tscircuit 0.0.979 | 0 | | KiCad Routing Tools 0.1.9 | 0 |

The 25 public boards

[](https://github.com/Koppar-AI/autorouter-bench#the-25-public-boards) `results/2026-10-zap25/summary.md`: Koppar, one run per board (measured 2026-10-21), beside the numbers Zap! published for itself, TraceMaker, Freerouting and tscircuit (which we did not re-run). Each board under its own rules.

| Router | Fully connected | Clean (0 open, 0 errors) | Connected, no more errors than the human board | | --- | --- | --- | --- | | Koppar | 14 | 11 | 14 | | Zap! | 14 | 6 | 9 | | TraceMaker | 7 | 3 | 4 | | Freerouting | 11 | 2 | 2 | | tscircuit | 10 | 0 | 0 |

PCBWorld-Bench

[](https://github.com/Koppar-AI/autorouter-bench#pcbworld-bench-1)

`results/pcbworld-2026-10/summary.md`: clean-pass rate, one attempt per board. Koppar 1.000 on D3-A and 1.000 on D3-B; DeepPCB reports 0.929 and 0.800, and the PCBWorld paper's baselines are lower (the full table, as DeepPCB published it, is in the summary).

Electrical quality

[](https://github.com/Koppar-AI/autorouter-bench#electrical-quality)

Completion is not quality. `scripts/quality.py` measures, on every routed board and the human reference alike: the share of high-speed track over a ground plane (and over any plane), ground vias beside high-speed layer changes, differential pair length mismatch and the share routed side by side, the narrowest track on each power net, and the routed path from each decoupling capacitor to its part (length and vias). `scripts/quality_campaign.py` runs it over a campaign: `results/2026-10-placement/quality.md`.

Nets are classified by name (pairs by `_P`/`_N`-style suffixes, clocks by `CLK`, rails by voltage-style names); the patterns are in the script.

What this does not measure

[](https://github.com/Koppar-AI/autorouter-bench#what-this-does-not-measure) Current is not stated on these boards, so rail widths are reported, not judged against a current. Impedance needs each board's stackup and is not scored. No router in the table matches the human layouts on every measure.

Licence

[](https://github.com/Koppar-AI/autorouter-bench#licence) The code in this repository is MIT licensed. The boards are not part of it; each keeps its upstream licence.