Binary release — prebuilt
klayoutandkplaceexecutables for macOS on Apple Silicon (arm64). Links against the system libSystem only; no other dependencies. Other platforms and distributions: build from the source repository when authorized by Seagull Work, Inc. This package contains executables and one example KiCad board — no source code is included or implied. Theklayoutandkplacebinaries, this README, theLICENSEand theexample/board they route are the entire distribution: there is no source tree, no Makefile, and nothing here that can be compiled, rebuilt, or modified. The example is design input for the tool, not program source. The binaries are the only form of the program you receive. Nothing to build here: route the included board with./klayout --input-dir example --output-dir out --resize-pcb. Keep both binaries in the same directory: klayout invokes kplace for auto-placement and re-invokes itself for parallel attempts (install on PATH or run from their directory). kicad-cli from KiCad 10, found via PATH or KICAD_CLI, enables the DRC acceptance gates, zone-fill verification for the pour stitching, and the fab exports (gerbers, drill, pick-and-place, IPC-D-356). Trial software, no warranty — free to evaluate, and the layouts it produces are yours to use for anything, commercially included. Nothing it outputs is guaranteed or has been reviewed by an engineer: a clean DRC report is this program grading its own work, not an assurance that a board is manufacturable or safe. Verify every layout before you build it. Full terms inLICENSE. The build instructions carried by the source repository are omitted from this README: there is nothing here to compile.
A batch place-and-route tool for KiCad boards, written in portable C99 with no
dependencies beyond libm. It takes .kicad_pcb files — placed, or with
footprints still staged outside the outline — places them if needed,
routes the unconnected nets, and writes complete KiCad projects back
out: board, project file, and schematic. Everything in
the board file that the router doesn't understand is preserved verbatim, so
it works across KiCad versions (developed and DRC-validated against
KiCad 10).
boards/ routed/
amp.kicad_pcb klayout amp.kicad_pcb (+ tracks, vias)
amp.kicad_pro -------> amp.kicad_pro (copied)
amp.kicad_sch amp.kicad_sch (copied)
Boards that route completely (0 DRC error violations, 0 unconnected):
- CANNONBALL (dense 4-layer, 58 nets):
--layers 4 --plane GND:In2.Cu --grid 0.05— zero unrouted, zero DRC errors, class 0.6/0.3 vias only, the auto-detected multi-drop USB pair routed as MST spans within its budgets. Also solves the ORIGINAL human-rule variant (0.15 mm clearance — twice as strict as the regression copy): 0/0 with the USB pair at 0.425 mm skew, in ~32 minutes wall with parallel attempts. - xlator (197×132 mm, 0.076 mm rules, 7 differential pairs, MIPI CSI-2 + Ethernet): placed or staged-for-placement — every pair budget met (0.000 mm skew, matched lanes, co-layer members, balanced flips, solved 100-ohm geometry), zero DRC errors, zero unconnected. The 8-attempt hunt runs in ~36 s wall in parallel.
- regulator (single-sided):
--layers 1 --resize-pcb— ground as flood fill, no vias, planar-routed on a re-tried placement, board shrunk to the minimum DRC-legal outline. With itspower.json(1.25 A in, 3 A out, 100 mV drop budgets) the rails route at the IPC-2221 widths (0.409 / 1.367 mm), IR drop verified in budget — in the regression corpus with exactly those flags.
klayout ships with a vendored simulated-annealing placer (kplace,
built alongside it). When an input board's footprints sit with pads
outside the board outline — the standard "parts staged for placement"
convention — klayout detects it, runs the placement stage (minimising
weighted wirelength, 90° rotations, courtyard-overlap avoidance, with
pad rotations rewritten in world frame as KiCad requires), reloads the
placed board, and routes it. A fully unplaced netlist becomes a placed,
routed, DRC-clean board in one command; boards that are already placed
skip the stage entirely.
Written boards get their silkscreen designators straightened: KiCad
text angles rotate with the part, so a customer-rotated footprint
arrives with its Reference label turned sideways and would stay that
way through placement. Every Reference angle is normalized to 0 in
the output — parts keep their rotations, labels read horizontally
(straightened N turned designator(s) reports it).
With no diffpairs.json, pairs are detected from net names: two nets
pair when their names are the same length and differ only at
positions where one has P/p/+ and the other N/n/- —
DP/DN, D+/D-, X_P/X_N, and names carrying the polarity
twice all match. Both members need routable pads; battery/voltage
terminals (B+/B-, V+/V-) are excluded; the standard is
inferred from the base name where it is unambiguous (DP/DN and
USB-ish names get USB at 90 ohm). Detections are announced, and a
diffpairs.json always overrides.
Each pair is judged against what ITS spec demands, not the strictest: MIPI CSI/DSI and unknown standards 0.1 mm intra-pair, USB 1.25 mm, Ethernet MDI 1.3 mm. The SI itemization prints each pair's own budget with its shortfall.
A DDR memory interface is detected from net names alone: byte lanes
(DQ by index/8 with their DM/DQM/LDM/UDM masks) assemble around their
DQS strobe pairs, and the address/command group (A*/BA/RAS/CAS/WE/
CS/CKE/ODT/ACT/PAR/RESET, token-boundary matched so CKE never reads
as CK) assembles around the CK pair. The detected spec is written to
the output as diffpairs.json — promote it next to the input to make
it authoritative; a hand-written file disables detection. Groups are
length-matched after pair matching (0.635 mm members-to-strobe,
2.0 mm addr/cmd-to-CK), pairs route at 100 ohm differential, and the
DDR report itemizes each group's worst skew in the SI gates.
Boards with pairs or DDR groups print a titled legend of every
matched signal, then one Y/N row per placement attempt as it
lands — which signals routed, which missed, misses named. On a
72-bit ECC DDR that is a 160-column live view of the campaign.
Ground detection understands the common dialects: GND/AGND/DGND,
the CMOS VSS/AVSS/DVSS family, EARTH. All of auto-plane
selection, the "ground with no plane" note, the stackup role labels
and the power-net exclusion share one recognizer, so an avss-only
board gets its ground plane (or 1/2-layer flood fill) automatically.
An SMD pad's via drop targets only the LIVE pour: per plane layer, the largest connected free region — under a dense BGA via field the foreign clearance halos overlap into a moat where the zone fill islands and is removed, and a via dropped there lands on bare laminate. The drop search targets every plane layer the net owns (a 6+ stack offers both its GND planes) and walks the escape out of the moat on whatever layer is cheapest before dropping.
When a fab sweep, --fab, or explicit flags resolve rules below the
project's, the OUTPUT copy of the .kicad_pro gets its DRC floors
(rules minimums and netclass floors, lowered only) rewritten to what
was actually routed — KiCad's DRC judges the board that exists, not
the board the project imagined.
The place-route-measure loop engages for EVERY board that goes
through placement, pairs or not: an attempt whose routing cannot
complete (a single-sided board fenced in by an unlucky arrangement,
say) is re-placed with the next seed until everything routes with
zero DRC errors, up to --place-tries. When no attempt makes it,
the best one is still written but the failure is loud: GATES FAILED: no placement attempt of 8 met the completion/DRC gates,
a *** GATE WARNING *** in the run summary with the count of
boards below the gates, and a nonzero exit status — a written
board is never silently passed off as a good one.
Class vias only, by default: the human reference layouts close their
boards entirely with net-class vias, and that is the bar. The
endgame's fallback to the project's min_via_diameter is an explicit
opt-in (--allow-min-vias), and even then never goes below what the
.kicad_pro allows. --exact-vias (experimental, slow) extends
analytically validated off-grid via hunting from the endgame to the
whole search. Via-site verdicts persist across searches in a memo
tagged by net and invalidated when the copper changes, so repeated
negotiation rounds stop re-probing the same sites — this is what made
--exact-vias finish at all; it remains experimental because even
memoized it does not beat a well-aligned fine grid.
Differential-pair quality drives placement, not the other way around.
Whenever the board has pairs — from diffpairs.json OR auto-detected
from net names — klayout hands the list to the placer, so boards that
never shipped a pair file still place with exit corridors and facing
costs (on the camera benchmark this alone took six failing pairs down
to two). Two things happen:
- The placer's diff-pair cost terms engage: pads on pair nets get reserved exit corridors and lane-width fields, so parts are placed with room for the pairs to escape and run.
- The layout is done and redone until every pair meets budget.
Boards that place here vary the placement seed per attempt
(
--seed, default 1; attempt t uses seed+t). Boards that arrive already placed iterate on the ROUTING instead — the net order rotates and the rip-round penalty phase shifts per attempt — because the placement is fixed but the negotiation is not. An attempt is accepted only when all connections routed, there are zero DRC error violations, every pair's intra-pair skew is within 0.1 mm, every matched-standard group's inter-lane spread is within 1.0 mm, both members of every pair have the same layer-flip count, and the members' per-layer copper matches within the standard's co-layer budget. That budget scales with the standard's timing tolerance: off-layer copper skews delay at the microstrip-vs- stripline velocity difference (~0.85 ps/mm), about 8x more gently than plain length skew, so each standard gets 8x its intra-pair budget with a 2.0 mm floor — CSI/unknown 2.0 mm, USB 10 mm, Ethernet MDI 10.4 mm. The floor stays even where timing forgives (Ethernet tolerates nanoseconds) because off-layer copper also means the members ride different impedance classes. Otherwise the next attempt runs, up to--place-tries(default 8), keeping the best attempt if none fully meets budget — in which case the run printsPAIRS FAILED, the summary counts the board under "differential pairs missing budgets", and the exit status is nonzero. The verdict line (pairs meet/miss budgets: ...) is printed for every attempt on every board with pairs, and the final SI warning ITEMIZES every shortfall of the written board — the measured value, the budget, and how far over:MIPI CSI-2 group: inter-lane spread 3.656 mm, budget 1.0 -- 2.656 mm over (extend or shorten lanes to match). Output is unbuffered, so a long run streams its progress through pipes and logs in real time.
Fixed seeds also make runs reproducible: the same inputs and options
give the same board every time, on any platform. The placer draws from
an explicit generator (src/place/rng.c, SplitMix64) rather than libc
rand(), whose algorithm is implementation-defined — glibc and macOS
walk different sequences from the same seed, which used to put the
annealer on a different placement per machine and made a recorded
baseline unreproducible anywhere else. One caveat remains: the annealer
compares against exp() and scales its schedule with pow(), and libm
transcendentals are not bit-identical across platforms, so a divergence
is improbable rather than impossible. On the xlator board the loop
rejects seed 1 (off-layer 2.8 mm) and accepts seed 2 with every
budget met: all 7 pairs at 0.000 mm intra-pair skew, CSI inter-lane
spread 0.000 mm, worst off-layer copper 0.416 mm, balanced flips,
zero DRC errors, zero unconnected — differential pairs done first,
frozen, and everything else routed around them.
./klayout --input-dir boards/ --output-dir routed/ [options]Routes every .kicad_pcb in the input directory and writes a board with
the same name to the output directory (created if needed). For each board
the output directory also receives:
<name>.kicad_pro— copied from the input directory; if the input has none, a minimal valid project file is generated so KiCad opens the result as a proper project.*.kicad_sch— every schematic in the input directory is copied across, so hierarchical designs keep their sub-sheets.
The exit status is 0 only if every connection on every board routed.
At the end of the run, each output board is checked with
kicad-cli pcb drc --severity-error and a summary is printed: the number
of DRC error violations and unconnected items in the output directory,
per board and in total. kicad-cli is found via $KICAD_CLI, PATH, or
the standard macOS install location; if it is missing the DRC report is
skipped with a notice.
The binary package ships one board to route, in example/: Doorbell_PS,
the 2-layer revision of an 8–24 VAC doorbell-camera UPS — 48 nets, 184
pads, every one of them staged outside the outline, so the run places the
board before it routes it. Beside the board sit a power.json (12 rails,
up to 6 A, widened to their IPC-2221 ampacity) and a .kicad_kds design
file carrying 9 placement constraints, so one command exercises placement,
ampacity widening, KDS constraints and flood-fill grounds together:
./klayout --input-dir example --output-dir out --resize-pcbUp to eight placement attempts are made — one fewer at a time than the
machine has cores, since the parent keeps one — and the first to meet
every gate is kept, which on this board is normally attempt 1.
--resize-pcb then shrinks the outline to the minimum DRC-legal rectangle
around what was placed. Around a minute and a half on a two-core x86-64
box. out/ receives the routed
board, its project and schematic, the footprint library, and
Doorbell_PS-klayout-result.json with the run verdict. The expected
finish is 0 error violation(s), 0 unconnected item(s).
Because a .kicad_kds sits next to the input, the fab exports are left to
the KiChad flow — pass --fab-outputs to get gerbers and drill files from
this run anyway.
./klayout board.kicad_pcb -o routed.kicad_pcb [options]Routes one board. No project/schematic copying is done in this mode.
| Option | Default | Meaning |
|---|---|---|
--layers N |
2 | Copper layers the router may use (1–32). --layers 1 is true single-sided: everything on F.Cu, no vias, the ground net poured as a flood fill the tracks route through (the classic single-sided technique), and placement re-tried until the remaining nets route planar. |
| (no plane flags) | With only --layers N (4+), the tool chooses the ground plane itself: the ground net feeding the most pads goes on the inner layer adjacent to the denser SMD face (6+ layers add a second GND plane by the other face; 8+ layers add a dedicated power plane beside a ground — interplane capacitance — with the net auto-picked), reported with its reason at run start — stackup: chose In1.Cu for ground plane "GND" (adjacent to F.Cu, the denser face: 198 vs 0 SMD pads). On 1/2 layers the ground is instead poured as a flood fill (both faces on 2) and never routed as tracks; if the fill fragments — live islands with no copper path back to the net — each stranded fragment is bonded to the opposite face's pour with a stitching via placed inside it, and a fine-pitch ground pin the fill cannot reach gets a via drop lapping the pad or, when no via fits, a short same-layer spur to the nearest ground copper (stitch: lines in the log; all of it is taken back out if DRC does not improve). --plane/--stack override. |
|
--stack SPEC |
Stackup by role, outermost first: SIG-GND-SIG-GND:AGND-PWR-SIG sets 6 layers, reserves a plane for every GND/PWR layer. GND defaults to net "GND"; GND:AGND names another ground; GND:GND+AGND groups split grounds (joined only at their ferrite bead) on one reserved layer; bare PWR lets the tool pick the biggest power-looking net and print its choice. Outer layers must be SIG. Use , separators when net names contain dashes. |
|
--plane NETS:LAYER |
Reserve an inner layer for one net (pours a zone; SMD pads get via drops) or a comma-separated group (routed as tracks). Repeatable. Other nets' vias still pass through. Inner layers only, so 1/2-layer boards have no reservation. | |
--route-all-layers |
Ignore --plane specs: every layer open to every net. |
|
--eight-angles-routing |
default | Octilinear copper (90/45-degree turns only) is the DEFAULT: the full campaign runs on-compass first, retries any-angle only when gates or pair budgets are missed, and restores the eight-angle result when the retry does no better. The explicit flag pins eight angles with no fallback; KLAYOUT_ANGLES=any skips straight to any-angle. Short pad-attach stubs may still angle as a connectivity last resort. |
--square-wiggles / --sine-wiggles / --arc-wiggles |
arc | Length-matching meander shape. Arc serpentines (straight risers, true KiCad arc caps) are the default; square restores the rectangular teeth, sine the chord-sampled sinusoids. Every family caps amplitude at 3 track widths (clamped 0.15–2.5 mm) — length is bought with more periods, not tall teeth — and a meander leg may never touch its own net's copper beyond its chained neighbors (a self-crossing meander is a length lie). |
--net NAME |
all | Route only the named net |
--grid MM |
0.1 | Routing grid pitch. Over the 80M-cell budget (big outlines × many layers) the pitch auto-coarsens through the standard steps with a log line; an explicit --grid pins the pitch exactly — the run refuses rather than coarsen. |
--clearance MM |
project | Copper-to-copper clearance |
--track MM |
project | Track width |
--via-size MM |
project | Via pad diameter |
--via-drill MM |
project | Via drill diameter |
--edge-clearance MM |
project | Copper to board edge clearance |
--hole-clearance MM |
project | Copper to hole edge clearance |
--via-cost MM |
5.0 | Detour length the router will accept to avoid one via |
--place-tries N |
8 | Attempts in the iterate-until-done loop (placement seeds for staged boards, routing seeds for placed ones). Attempts run in PARALLEL, one process per attempt, defaulting to installed cores − 1; KLAYOUT_PARALLEL=k caps the width (12-layer grids run hundreds of MB each — oversubscribed memory page-fault-crawls), KLAYOUT_SERIAL=1 forces one at a time. Children inherit the coordinator's RESOLVED design rules as explicit flags, so fab-floor and agent-adjusted runs behave identically in and out of the workers. |
--seed N |
1 | Base seed; attempt t uses N+t, so every run is reproducible |
--resize-pcb |
After routing, shrink the outline to the minimum DRC-legal rectangle around the design: copper, courtyards, silkscreen text, edge clearance | |
--board NAME |
all | Directory mode: process only this board (used by the parallel workers; handy standalone) |
--allow-min-vias |
Let the endgame shrink stragglers' vias to the project's min_via_diameter. Default is class vias only, matching the human reference layouts. |
|
--exact-vias |
Experimental: analytically validated off-grid via hunting during the whole search, not just the endgame. Slower; the human's gridless vias prove the architecture, but a well-aligned fine grid still wins. | |
--no-widen |
Ignore power.json: route power rails at the class track width (no IPC-2221 ampacity widening). |
|
--no-fab |
Skip gerber + drill file generation. | |
--no-research |
On an SI miss, skip the online research step (also skipped when offline or KLAYOUT_OFFLINE=1). |
|
--agent N |
0 | Strategy escalation: on a below-gates finish, consult a claude CLI for one whitelisted knob change and re-run, up to N rounds (decisions logged to agent-log.md). |
--gerber-dir DIR |
OUT/gerber-drill | Where to write the fab files. |
-h, --help |
Usage |
Layers are indexed 0 = F.Cu, 1…N−2 = In1.Cu…, N−1 = B.Cu. All layer
changes use through vias, which occupy every copper layer.
Every routed board also gets manufacturing outputs by default,
written to gerber-drill/ in the output directory (--gerber-dir
redirects, --no-fab skips): one gerber per copper, mask, silk and
paste layer plus the board outline, and Excellon drill files in mm
with decimal zeros, plated and non-plated holes in separate files.
The copper layer list is read from the written board itself, so
4/6/8-layer boards export their inner plane layers too. Generation
runs on the DRC'd output file — zone fills were saved into it by the
DRC's refill, so the gerber copper is exactly the copper DRC
validated. Like DRC, this shells out to kicad-cli ($KICAD_CLI,
PATH, or the standard macOS install).
--fab NAME (or a (fab "NAME") clause anywhere in the
.kicad_kds next to the input board — the design file is the
natural home for "this board is made at JLCPCB") declares the
vendor up front. Their capability JSON becomes the hard floor for
every rule: project rules below a floor are raised and called out,
no reduction (agent rounds, endgame vias, stitch copper) ever goes
under them, a --layers count beyond the vendor's range is
flagged, and the post-failure capability sweep below is skipped —
the fab is already chosen, so there is nothing to shop around.
A .kicad_kds next to the input board changes what klayout is: not a
standalone tool but the layout stage of the KiChad flow, whose
contract is exactly two artifacts in the output directory — the
routed board and the run verdict.
- Placement constraints. The KDS's
(board (layout (placement ...)))section —(near A B (maximum Nmm)),(edge REF side (maximum Nmm)),(group name (members ...) (maximum_span Nmm)),(board (maximum_width/maximum_height))— is parsed and enforced end to end: kplace carries a cost pull for every constraint (zero inside spec, steep past the maximum), each placement attempt is measured and gated on them, and every check prints measured-vs-max. Measurements use the footprint's authored anchor (its(at X Y)), matching KiChad's analyzer; board maxima measure the Edge.Cuts bounding box, strict comparison, no tolerance. - Run verdict.
<board>-klayout-result.jsonlands next to every output board: DRC error/unconnected counts, routing failures, SI budget results, stitch bonds,gates_met, the fab target, the per-constraint measured-vs-max table, and the generated outputs (ornullwhen fabrication belongs to KiChad). The DRC counts and the constraint table are stability-guaranteed — KiChad's gates consume them verbatim. - Fab outputs stay KiChad's. With a KDS present the gerber/drill/
pos/IPC-D-356 generation is skipped (
fab outputs: left to KiChad's fabricatein the log) — theirfabricatebuilds the package of record from the reconciled board.--fab-outputsforces klayout's own set as a cross-check; boards without a KDS always get the complete standalone package, which now includes pick-and-place (<board>-pos.csv) and the IPC-D-356 test netlist (<board>.d356) beside gerbers + drill. - Physical stackup. Boards whose input carries no
(setup (stackup ...))get one written into the output, matching the geometry the SI model assumed (35 um outer copper, FR4 er 4.3, the impedance model's dielectric heights;stackup.jsonnext to the input overrides). An input stackup is the designer's and is left untouched. - Attempt gating meets reality. Attempt children stitch their own pours before reporting, so selection judges what a placement can be bonded into; and the parent's final gate verdict is decided by the SHIPPED board's measurements, never by stale attempt-stage readings.
Below-gates boards no longer just report — they learn, automatically
(disable with --no-learn; the regression suite disables via
KLAYOUT_NO_LEARN so references stay deterministic):
- Consulted parameter rounds. A board below its gates triggers up
to three consultation rounds (a claude CLI with the banked research
and the agent log): one whitelisted knob change per round — seed,
place-tries, grid, track/clearance inside the legal band, plane
spec — applied and re-run. Across rounds the BEST board wins, never
the last: a failed experiment cannot displace an earlier better
result.
--agent Nstill sets an explicit budget. - Remembered tuning. When a consulted round verifiably beats the
baseline run, the winning knobs are persisted to
klayout-tuning.jsonnext to the input board — applied on every future run at the lowest precedence (project rules and the command line always win), with the reason recorded. - Code evolution. When no parameter fixes a board, the
consultation may propose an actual code change: a unified diff
restricted to the router's algorithm files (never the gates, DRC
calls, Makefile or test suite), applied in a git worktree, built,
and judged twice — the triggering board must meet its gates AND
the full regression suite must pass — before being adopted as its
own commit with provenance. Failures feed their evidence back for
a revised patch, three attempts per run, transcripts kept in
.evolve/. Evolution refuses to run on a dirty tree: it builds on committed ground only.
Bare-PCB fab capability files (one JSON per vendor — min trace width
and spacing, via drill/diameter, edge clearance, layer counts) plug
in via --fab-dir DIR, KLAYOUT_FAB_DIR, or the house default
../designer-parts/fab when present. When a board finishes below
ANY gate under the project's own rules — incomplete, DRC, or SI
budgets — each fab's envelope gets its own attempt with the rules
opened to that fab's minimums, and the run ends with a verdict per
vendor (completes / completes and meets SI / still incomplete) plus
a recommendation preferring the first fab that meets SI outright:
fab sweep: JLCPCB (trace 0.127, space 0.127, via 0.25/0.15)... COMPLETES the board, SI budgets MET
*** FAB RECOMMENDATION *** the board completes AND meets every SI budget
within JLCPCB's capabilities. To adopt: rerun with --track 0.127
--clearance 0.127 --via-size 0.25 --via-drill 0.15 ... then order at JLCPCB.
Sweep boards are judged by the router's exact geometry model, not
the KiCad DRC gate — copper below the project's own netclass values
SHOULD flag under the old rules, so adopting a sweep result means
updating the project rules to the reported numbers. Sweep outputs
stay in OUT/.fab-<vendor>/; the main output is never displaced.
The inner loop (placement seeds, routing attempts, honest gates) has a fixed strategy. Agent mode adds the outer brain for boards that finish below their gates:
- Problem write-up — the failing pairs with their numbers plus a
parts inventory (footprint library ids, tagged with the pairs they
carry) goes to
OUT/research-request.txt. Reference layouts for the EXACT parts on the board are what a human would search for, so that is what the request asks for. - Research — when the internet is reachable (one ~0.7 s TCP
probe;
KLAYOUT_OFFLINE=1or--no-researchskips) and aclaudeCLI is on PATH, the request is handed to it and the findings land inOUT/research-notes.md. Advisory only. - Escalation (
--agent N) — the consultation must answer with ONE next action as strict JSON from a whitelisted vocabulary — newseed,place_tries,grid,plane,route_all_layers,track/clearance(reductions allowed only inside the legal band between the project's HARD minimums and the current values; pair geometry is impedance-solved and untouchable; and the DRC gate remains the judge either way), orstop— which is validated, clamped to legal ranges, logged with its reasoning toOUT/agent-log.md, applied, and the board re-run, up to N rounds. The model picks among the tool's own legal knobs; it can never emit shell, paths, or file edits.
Agent mode is inherently nondeterministic and is never used by the regression suite; every decision is on the record in the log.
KiCad stores design rules in the .kicad_pro project file, not in the
board file. For each board, values are resolved in this order:
- Command line —
--clearance,--track,--via-size,--via-drillalways win. - Project file next to the input board: the Default net class value,
floored by the board's
min_*rule (min_clearance,min_track_width,min_via_diameter,min_through_hole_diameter) — whichever is larger wins. Edge clearance comes frommin_copper_edge_clearance. - Built-in defaults — 0.2 mm clearance, 0.25 mm track, 0.6/0.3 mm via.
A .kicad_dru custom-rules file next to the board is honored too:
its UNCONDITIONAL constraints (clearance, track_width, hole_to_hole,
edge/hole clearance, via_diameter, hole_size, annular_width — the
last checked against the router's own via ring) override the project
values in whichever direction the customer chose, and the file is
copied to the output so DRC judges the result under the same law.
Conditional rules carry KiCad's full expression language and are
announced by name as "not modeled in routing" — the DRC gate still
enforces them on the output. Command-line pins beat everything.
The resolved rules are printed per board before routing.
The program is four modules plus a CLI, each a .c/.h pair under src/.
Data flows: sexpr parses the file → board builds a geometric model →
route adds copper to both the model and the parse tree → sexpr writes
the tree back out.
.kicad_pcb files are s-expressions. The parser reads the whole file into
a tree of nodes (atoms with a quoted flag, or lists), and the writer
serializes the tree back with KiCad-style indentation. Round-tripping the
full tree — rather than picking out known fields — is the compatibility
strategy: zones, graphics, properties, and any nodes introduced by newer
KiCad versions pass through untouched. Helpers (sx_find, sx_num,
sx_str) give the rest of the program keyed access into lists.
The writer reproduces KiCad's own layout, not merely a valid one: a list's
leading atoms stay on the head line ((pad "1" thru_hole circle — not one
atom per line), and millimetres are written with trailing zeros trimmed
(23.68, never 23.6800) through the one formatter in numfmt.c that
every emitter shares — the tree writer, the designator nudge in board.c,
and the placer's in-place text patches. The point is that content we did
not touch comes back byte-for-byte: an output diff then shows only what
klayout actually changed, and the regression corpus's baseline comparison
carries signal instead of whitespace. Parse-and-write of an untouched
KiCad board is byte-identical except for (xy …) packing inside (pts …),
where KiCad fits several per line and we emit one.
Walks the tree once and extracts only what routing needs:
- Net table: KiCad ≤ 9 declares
(net N "name")nodes and references nets by index; KiCad 10 references nets by name only, so names are interned into indices as they are encountered. New copper is written in whichever style the file uses. - Pads: each footprint's
(at x y rot)is applied to its pads' local offsets (KiCad's y axis points down; positive angles rotate counter-clockwise on screen) to get absolute positions. As an obstacle, a pad is a bounding circle of radiushypot(w,h)/2, widened by the copper-shape offset when the pad has(drill (offset …)). As a connection target, the exact copper shape is kept — a rounded rectangle in the pad's rotated frame, which covers rect, roundrect, oval and circle pads exactly — so routes attach only on real copper. SMD pads carry a copper layer; thru-hole pads occupy every layer. - Holes: every drill is a keep-out on all layers with its own
clearance rule (
min_hole_clearance), separate from — and often larger than — the copper clearance.np_thru_holepads have no copper but their hole still blocks. - Copper rectangles: KiCad 10 allows filled graphics on copper to
carry a net (e.g. battery contact areas). A net'd
gr_rectbecomes an exact rectangular obstacle for other nets and, for its own net, a connection target (a disc inscribed in the rect, so anything reaching it lands on real copper). - Board outline:
Edge.Cutsgraphics (lines, rects, arcs, circles, polygons) are decomposed into segments; arcs and circles are sampled into ~0.2 mm chords. - Existing copper:
(segment)and(via)nodes become obstacles.
board_add_segment / board_add_via are the write path: they append to
the obstacle arrays and build the corresponding s-expression node
(including a fresh v4 UUID) onto the tree root, keeping the model and the
output file in sync by construction.
Derives <board>.kicad_pro from the board path and scans the JSON for the
relevant keys with a first-match "key": number search rather than a full
JSON parser. That shortcut is sound for .kicad_pro files because the
net-class keys (clearance, track_width, via_diameter, via_drill)
first appear in the Default class — KiCad writes it first — and the
min_* rule keys are unique in the file. Per-class rules would need a
real parser.
For each net (skipping nets that already have tracks):
-
Connection ordering. A minimum spanning tree (Prim's) over the net's pads picks which pad pairs to connect — n−1 connections, shortest overall, in MST growth order.
-
Obstacle grid. A uniform grid (
--gridpitch) covers the board outline's bounding box (or the pads' plus a margin if there is no outline), one plane of cells per copper layer. Before each connection, all foreign copper is rasterized into it: pads as their exact copper shape (a rounded rectangle in the rotated pad frame — a signed-distance test that covers rect, roundrect, oval and circle pads; only trapezoid and custom pads fall back to the bounding circle), vias as discs, tracks as thick lines sampled along their length, net'd copper rectangles exactly, and the board edge with the edge clearance. Exact pad shapes matter: a fine-pitch connector's neighbours stay routable where bounding circles would blanket them. Two maps are kept: one inflated byclearance + track/2(where a track center may go) and one byclearance + via_size/2(where a via center may go — vias are fatter than tracks). Both get half a cell of slack so copper that merely grazes a cell still blocks it (conservative, never unsafe). Same-net copper is left free, so nets may T off their own tracks. The grid is rebuilt per connection, which keeps tracks routed moments earlier correctly up to date; rebuild cost is linear in the number of obstacles. -
Search. A* over
(layer, y, x)cells. Moves are the 8 neighbours (so finished tracks run at 0/45/90°) with Euclidean step costs, plus a layer change: a through via needs the via-clearance map free on every copper layer, and then connects this layer to any other in one hop at--via-cost. The heuristic is straight-line distance to the target pad (admissible; via cost excluded). The search is seeded from every free cell under the source pad and terminates on any cell of the target pad, so connections attach anywhere on the pad, not just at its center. The heap admits duplicate entries; stale ones are detected and skipped on pop. -
Emission. The cell path is compressed into maximal collinear runs, each becoming one
(segment); layer changes become(via)nodes. Short stubs connect the exact pad centers to the first/last grid points.
Failures are per-connection: an unroutable pair is reported on stderr with its coordinates and the router moves on.
Complexity. Memory is layers x (bbox/grid)² bytes for the grid plus
~8 bytes/cell during a search; a 100×100 mm board at 0.1 mm pitch and two
layers is 2M cells. The cell count is capped at 80M — for large boards,
coarsen --grid.
Quality bar: kicad-cli pcb drc on every output — zero error violations
introduced is non-negotiable; completion is the metric being pushed.
CANNONBALL (dense 4-layer board, 58 nets / 179 connections; a human layout of the same design routes it fully) — progression of unrouted connections, all at 0 introduced DRC violations:
| Router state | Failed connections |
|---|---|
| 2 layers, bounding-circle obstacles | 71 |
--layers 4 |
52 |
--layers 4 + exact pad-shape obstacles |
25 |
| + rip-up-and-retry with history cost | 7 |
+ EP anchor-pad adoption, --via-size 0.5 |
0 |
Recommended invocation for dense boards:
./klayout --input-dir boards/ --output-dir routed/ --layers 4 \
--via-size 0.5 --via-drill 0.3 # if min_via_diameter allowsVia size is the scarce resource on dense 4-layer boards — a through
via's keep-out spans every layer — so if the board's min_via_diameter
permits a smaller via than the Default net class uses, passing it can be
the difference between a few stubborn nets and full completion.
Simpler boards (the two in test/regression/) route completely.
The router now negotiates congestion: a greedy first pass, a hard-nets-first reroute, then rip-up rounds in which a failed connection finds the cheapest path across other nets' copper (exact static obstacles stay hard), rips the owning nets, and requeues them — with a PathFinder-style history cost that makes repeatedly contested cells more expensive for everyone, so oscillating fights settle. Fine-pitch parts (0.35 mm LGA) attach through zero-margin escape stubs validated against exact geometry rather than the (slack-carrying) raster. The best state seen across all rounds is kept and restored.
With those flags CANNONBALL routes completely: 192/192 connections, 0 DRC error violations, 0 unconnected items — matching the human reference layout's connectivity.
--plane NET:LAYER pours a real zone: klayout emits the KiCad zone
(solid pad connections, island removal), gives every SMD pad of the net
a via drop to the plane, lets thru-hole pads connect through the fill,
and reserves the layer against other nets' tracks (their vias still pass
through — the fill pours around them). The DRC step runs with
--refill-zones --save-board, so the output board is verified and
saved with its fill computed. Measured configurations on CANNONBALL,
all at 0 DRC error violations:
| Configuration | Unrouted |
|---|---|
| no pour, netclass 0.6 vias | 5 |
--plane GND:In2.Cu, 0.6 vias |
3 |
--plane GND:In2.Cu --via-size 0.5 |
2 |
no pour, --via-size 0.5 --via-drill 0.3 |
0 |
| GND pour + power group on In1, 0.6 vias | 14 |
The complete-board recipe — zero unrouted, zero DRC error violations, netclass 0.6/0.3 vias, a real verified GND plane, copper reuse and manufacturing-correct same-net via spacing:
klayout --input-dir boards/ --output-dir routed/ \
--layers 4 --plane GND:In2.Cu --grid 0.05The 0.05 grid matters: this board's footprints sit on mixed mil/metric coordinates (origins largely on a 1-mil grid, pad offsets metric), and 0.05 mm resolves enough of the legal track corridors and via gaps that the router closes every connection — 0.1 mm leaves ~5 unrouted and 0.04 mm (finer but worse aligned) does not help. The endgame can also place analytically validated off-grid vias and, when the rules allow, minimum-size vias for stragglers. Grouping the voltage nets onto In1 starves the signals of that layer (14), the same lesson as two pours. Pour zones flood every copper layer like hand layouts do; the reserved layer is the guaranteed plane and the other fills are additive.
Two pours (GND + 3V3) starve the signal layers and do worse — one ground plane is the sweet spot on a 4-layer board this dense.
Pour layers are not walled off: signal nets may route through them as a
last resort (a 4x cost multiplier keeps traffic away). Fill integrity is
preserved by construction — on a pour layer a foreign track must keep a
plane rib (two zone clearances plus the zone minimum thickness, 0.85 mm)
between itself and any static copper, the board edge, or another net's
incursion, so every incursion stays an isolated slot that cannot cut the
plane edge-to-edge. Vias passing through the plane need no such guard (a
via barrel is a small closed slot) and are unrestricted beyond normal
clearance. KiCad's refill + DRC in the verification step remains the
ground truth that no fill island is ever orphaned. When
connections remain after the rip-up rounds and the board's
min_via_diameter allows a smaller via than the net class, an endgame
pass automatically retries the stragglers with minimum-size vias
(snapshot-protected, so it can only improve the result).
Future quality work: per-netclass track widths/clearances.
Debugging aid: set KLAYOUT_DEBUG=1 to print, for each failed
connection, the seed/target cell counts of both pads — seeds=0 means
the pad's copper is entirely blanketed by obstacles (a modeling
problem), while nonzero counts mean the maze search genuinely found no
corridor (a congestion problem).
- Only through vias; no blind/buried vias or microvias.
- Nets that already have some copper in the input are skipped, not completed.
- Trapezoid and custom-shape pads are still bounding circles (their true outline is not parsed), so channels next to them may be refused.
- The outline is enforced as "stay clear of the edge geometry"; a track cannot cross Edge.Cuts, but exotic outlines with interior cutouts rely on that blocking alone.
- Zones/pours present in the input board are not treated as obstacles (pours emitted by klayout are fully modeled).
- One clearance/track width for ordinary nets; differential pairs get
their own impedance-solved width and gap, and power rails from
power.jsonget IPC-2221 ampacity widths, but there are no general per-net-class rules. - Ampacity widening sizes tracks only; via thermal capacity is not checked (the IR-drop pass does model via barrel resistance, so a starved single-via neck shows up as excess drop, but a via running hot within the drop budget does not).
- Differential pairs need equal P/N pad counts (2–8 per member); unequal or larger fan-outs route as ordinary nets with a note.
If a power.json sits next to the input board — written by hand or
generated by the schematic tool — its power rails are routed at the
width their current demands. The format is the layout tool's
power-delivery model; klayout reads the subset it needs:
{
"gnd_nets": ["GND"],
"nets": {
"VBUS": {
"voltage": 5,
"supply": "J1.1",
"loads": [{ "pad": "U2.3", "current_mA": 1500 },
{ "pad": "U4.8", "current_mA": 500 }]
},
"3V3": { "current_mA": 800 }
}
}Each rail's draw is the sum of its declared load currents (the
net-level current_mA/current_A shorthand works when no loads are
listed; rated supply currents are capacity, not consumption, and are
ignored). The minimum track width for that draw comes from the
IPC-2221 external-layer ampacity formula — I = 0.048 · ΔT^0.44 · A^0.725, ΔT = 10 °C, copper thickness from stackup.json (default
35 µm) — and the rail routes at max(class track, IPC width):
widening only, never narrower than the class. The substitution happens
at the router's single choke point, so search halos, stub validation
and emission all see the wide track; DRC clearances are honored at the
new width automatically.
Every decision is printed at run start:
power: VBUS draws 2000 mA -> track 0.781 mm (IPC-2221, 35 um copper, dT 10 C; class 0.254 mm)
A rail asking for more than 2 mm of copper gets a note suggesting
--plane NET:LAYER instead — a maze-routed trace that wide rarely
fits. --no-widen turns the whole pass off for routability
comparisons. Nets named in power.json but absent from the board are
reported, not silently ignored.
A power-supply board with no power.json still declares its own
maximum currents if you read the copper's paperwork, and klayout
does: the fuse's rating (from its Value, e.g. "1.1A HOLD") bounds
the input; the series chain — fuse, protection diode, regulator,
inductor, output — says which nets carry it (walked through two-pin
F/D/L parts and bridged through small converter ICs, never into a
ground net, so catch-diode and TVS shunt legs stay out); and a buck
MULTIPLIES current downstream: with the input voltage from a
connector Value ("12V INPUT") and rail voltages from net names
("+5V_OUT"), I_out = I_in x Vin/Vout at eta = 1 — conservative for
sizing. Every inference prints its reasoning, the rails feed the
same IPC-2221 machinery, and the model is written to
OUT/auto-power.json in the real power.json format — review it and
copy it next to the input board to make it authoritative. A real
power.json disables all inference; --no-widen disables this too.
Ampacity says the trace won't overheat; the IR-drop check says the
load still sees its voltage. When a rail declares a supply pad and
per-load draws, the routed copper is measured after routing: DC nodal
analysis of the rail's own network — every segment contributes
R = ρ·L/(w·t), vias and thru-pads join layers as plated barrels,
the supply pad anchors 0 V, each load draws its declared current. No
SPICE involved: the network is a few dozen resistors and klayout
solves it directly.
The worst load's drop is judged against max_drop_mv (per net, or
file-level for all rails; absent = report only):
power: V5 IR drop 37.8 mV worst, at J2.1 (2000 mA), budget 20 mV — OVER
power: V5 widening 0.781 -> 1.552 mm for the drop budget, re-routing
power: V5 IR drop 19.0 mV worst, at J2.1 (2000 mA), budget 20 mV — OK
A miss widens the rail (drop scales with 1/width) and re-routes it
with the full machinery — frozen differential pairs respected, up to
three rounds, capped at 3 mm. If the wider trace no longer fits, the
previous copper is restored and the shortfall is reported as a
*** POWER WARNING *** with the --plane suggestion — never hidden.
Rails poured as planes, fed through unmodeled copper, or hand-routed
in the input are reported as such and left alone.
If diffpairs.json sits next to the input board ({"pairs": [{"p": "NETP", "n": "NETN", ...}]}), klayout length-matches each pair after
routing using full wiggles — single-sided serpentine meanders (arc
caps by default; --square-wiggles/--sine-wiggles restore the
older shapes) inserted along the shorter member's longest straight
segments (falling through to shorter segments when a run is
corridor-locked beside its partner), every leg validated against the
exact geometry, amplitude capped at 3 track widths, self-crossings
banned — and reports aligned columns of P/N routed lengths and
intra-pair skew.
Pairs are first attempted coupled: one centerline search per pair (partner nets merged so neither blocks, virtual track wide enough for both members plus the intra-pair gap, single layer), with P and N emitted as mitered parallel offsets — exact-validated segment by segment — and the ends trimmed clear of the pin fields so each pad joins by an independent breakout stub. Through-hole endpoints (Ethernet magnetics, jacks) couple too: they exist on every copper layer, so the working layer comes from the SMD side. Pairs that don't fit fall to the flip-symmetric machinery below, and the wiggle matcher covers the lengths either way. On xlator this puts all three MIPI lanes at identical 39.489 mm — 0.000 intra-pair skew and 0.000 inter-lane spread.
When BOTH the coupled attempt and every flip-symmetric escalation fail, a rescue coupling gets one last shot with three extra freedoms — it runs strictly last, so it can never take a working pair away. (1) Gather rings: a double-width corridor cannot exist inside a fine-pitch pin field, so the centerline may start and end at candidate gather points ringing each end 1.8–3 mm out, the way a human gathers a pair outside the breakout zone. (2) Symmetric layer flips: the centerline may via (doubled cost — one flip is two barrels); at each flip the P via sits on its polyline, the N via staggers ~0.9 mm along its own, both jog outward when the barrel outgrows the pair pitch, and every site passes the exact analytic via check before any copper is emitted. (3) Routed breakouts: each member reaches its gather point as a normal validated route; the second member's via count is pinned to the first's (the counted-via search), so flip symmetry survives the pin field. Twisted results and unreachable gathers retry with the offending endpoints removed; whatever still fails falls back to ordinary routing, honestly reported.
Pairs that cannot couple fall back to flip-symmetric, co-layer
routing: one member routes freely, and its partner is routed with a
constrained search — the A* state carries a via counter, the goal is
accepted only at the member's exact flip count, via hops may land only
on the member's layer sequence, and cells more than ~2 mm from the
member's path carry a shadow-corridor penalty so the partner flies
beside it and flips at nearby spots. If the partner cannot follow, the
roles swap and the search runs the other way round. The point of all
of this is DELAY matching, not just length matching: stripline and
microstrip layers propagate at different speeds, so a pair is only
truly matched when both members put the same length on the same
layers. The report measures that directly as off-layer mm (half the
summed per-layer length difference between the members; 0 = co-layer),
and the placement loop budgets it at 2.0 mm.
The skew wiggles are layer-aware for the same reason: intra-pair bumps go on the layer where the short member lacks copper relative to its partner, and inter-lane extensions go on the layer both members share, so length matching does not un-balance the layers.
Once a pair is routed, it is frozen. Raul: the differential pairs are the most important copper on the board — iterate until they are done, then don't touch them. Routed pair copper is static for every other net: included in the rip-search's obstacle maps, never soft-annotated as rippable, never ignorable in the analytic checks. Everything else routes around the pairs, never through them.
The symmetric search shadows per stretch: while the partner is between its i-th and i+1-th vias, only cells within ~2 mm of the member's i-th stretch are penalty-free, so the flips land at nearby spots and both members split their length over the layers the same way (an xy-only corridor let the partner flip anywhere along it). Both role orders (P-first and N-first) are tried, and a success that is lopsided — partner much longer than its member, or split across the propagation classes differently — loses to the better order. When the natural flip count strands the partner entirely (a member's 0-via path can foreclose the only same-layer route back for its partner), the search ESCALATES: the member is re-routed forcing 2, then 4 flips, each opening a fresh layer for the partner to shadow. Off-layer copper itself is measured per propagation CLASS, not per layer: one member on F.Cu while the other runs on B.Cu is electrically symmetric (same microstrip height over the same plane) and does not count; microstrip-vs-stripline copper does. Pair order rotates with the routing seed — the first pair laid gets first pick of the corridors, so each attempt gives a different pair the advantage.
After the rip-up rounds settle, a pair repair pass re-checks every
pair: any that ended off-layer or flip-unbalanced (rounds re-route
ripped members with no layer discipline) is ripped and routed again as
a pair in the settled board, one pair at a time, each under its own
snapshot — kept only if it strictly improves and never at the cost of
a routed connection. The hard-nets-first reorder pass also exempts
intact pair nets from its wholesale rip. Known limit: pair members
ripped as blockers during the rounds can still end off-layer when the
repair search finds no constrained path in the settled board — rip
protection for pair copper is the open work item (see the
pair-protection-wip branch).
A differential pair is not always two pads a side: CANNONBALL's USB pair fans from the connector's mirrored pads to the MCU — four pads per member — and used to be silently skipped by the pair machinery. Members with equal pad counts (up to 8) are decomposed into PORTS (each P pad grouped with its nearest N pad), the ports spanned by a minimum spanning tree, and every span routed through the coupled and symmetric machinery with the full budget treatment. A span's fallback may only rip copper that span itself added, and the connection records the router trusts are written per span and die with their copper — the audit trail behind both rules is a false-complete bug where the router declared victory over missing copper. The DRC gate now also requires ZERO UNCONNECTED items per attempt, so that class of lie cannot reach an accepted board.
The iterate-until-done attempts are independent, so they run as
concurrent child processes (one per attempt, waves sized to the CPU
count) with the winner selected exactly as the serial loop would:
the lowest attempt that meets every gate, else the best score —
deterministic, byte-identical winners at the wall time of the
slowest single attempt instead of the sum. xlator's 8-attempt hunt:
36 seconds instead of ~6 minutes; CANNONBALL's: ~32 minutes instead
of an hour or more. --board NAME restricts a directory run to one
board (used by the workers; handy standalone too). KLAYOUT_SERIAL=1
forces the serial loop.
Copper already in the input file is the user's: any net with existing tracks is left untouched by routing ("already has tracks, skipping"), and its copper is static — nothing may rip it. A hand-routed differential pair is therefore a hard corridor the rest of the board routes around. It is still MEASURED and held to the same budgets as router-laid pairs — lengths, skew, via counts and off-layer copper all include input copper — so hand-laid skew cannot evade the SI verdict by reporting as zero.
One modification IS sanctioned, because it is the point of laying the
pair by hand: pre-routed pairs are length-matched FIRST, before
anything else routes. The skew matcher may split the user's own
straight runs to insert meanders on the short member (pre-routed pair P / N: length-matched N, skew 2.001 -> 0.000 mm), and the
matched pair is then frozen with the whole board routing around it.
A hand route carrying a LONG DETOUR — lane length beyond ~1.35x the
direct pad-to-pad run — is re-laid instead (pre-routed pair P / N: 54.956 mm is a long detour over the 34.377 mm direct run; re-laying the pair): matching it would force the whole detour into every
lane-mate's meanders, so the tool rips the hand copper and routes the
pair to length with everything else. Group-level budgets still apply
to what remains: a long-but-legal hand route fails inter-lane spread
honestly rather than burying tens of millimetres in meanders. Feeding a routed board back in also does
not stack a second pour: an existing zone for the plane net is
detected and reused.
Pair copper is not routed at the net-class track width: each pair's
width and gap are solved from its standard's differential impedance —
100 ohms for MIPI CSI/DSI, LVDS and Ethernet MDI, 90 for USB, 85 for
PCIe — using Hammerstad-Jensen microstrip and IPC-2141 edge-coupled
corrections on the stackup. A stackup.json next to the board
supplies the fab's real numbers — { "er": 4.2, "copper_um": 35, "microstrip_h_mm": 0.2104, "stripline_b_mm": 1.1 } — and is
reported when loaded; without one the assumptions are 35 um copper,
er 4.3, 0.21 mm microstrip height, 1.0 mm stripline spacing (printed
with every
solution so a wrong assumption is visible, e.g. impedance (MIPI CSI-2): 100 ohm differential -> track 0.172 mm, gap 0.101 mm). The
pair report then shows every pair's modeled impedance weighted by
where its copper actually runs — impedance: target 100 ohm, ~100.0 ohm length-weighted (100% microstrip @ 100.0, 0% stripline @ 102.9)
— and a hand-routed pair honestly reports "geometry as laid, not
modeled" rather than claiming numbers for copper the solver did not
shape. The
gap floor is the board clearance plus margin, so the solved geometry
is always DRC-legal. Coupled sections, symmetric routes, breakout
stubs and skew wiggles all use the pair's solved width.
The wiggle length-matcher refuses to fake it: bump copper is marked and never used as the base for further bumps (compounding bumps produced self-crossing copper lattices), and asks beyond a sane bound (25% of lane length inter-lane, 5 mm intra-pair) are declined with a message — a member that long relative to its partner is a routing problem, and hiding it in wiggles would only make the copper worse while the skew number lied.
Each placement attempt's board is checked with kicad-cli pcb drc
inside the loop: zero error-severity violations is part of the
acceptance bar, so pair metrics can never select a board with shorts
as "best".
Pairs whose "standard" requires it (MIPI/CSI/DSI, LVDS, PCIe, USB3)
are also inter-lane matched: every lane in the group is extended
toward the longest lane, tighter member first with the partner matched
to what it actually achieved — so intra-pair skew (the strict budget)
always wins over inter-lane spread when a corridor-locked lane cannot
take the full extension. A group summary line reports the longest lane
and the residual spread. Ethernet MDI pairs are intra-matched only
(pair-to-pair skew is a non-issue at board scale for the PHY). On the xlator board, 6 of 7
pairs match to 0.000 mm (the seventh is limited by available straight
runs at 0.783 mm).
Routed paths are also straightened by string-pulling: within each same-layer stretch the emitter pulls the longest exactly-legal straight segments through the grid path's corners, eliminating the staircase wobble grid searches produce (6× fewer segments on large boards).
KiCad's command-line DRC is the ground truth. The regression suite in the source repository
closes the loop automatically: after checking each routed board against
its baseline, it runs kicad-cli pcb drc --severity-error on both the
input and the output, and fails unless the routed board has 0
unconnected items and no violations beyond those already present in
the input (pre-existing footprint defects are not the router's fault,
but anything the router adds is). kicad-cli is found on PATH or at the
standard macOS location; override with the KICAD_CLI environment variable.
Baselines require kicad-cli, and are only comparable against runs that had it. Without kicad-cli the suite still runs and still compares baselines — the DRC and SI steps are skipped with a notice — but DRC is also part of the acceptance gate that picks which attempt gets kept (zero error violations and zero unconnected, see DRC-gated acceptance). With no DRC to fail, the gate collapses to the router's own metrics and the first attempt is accepted immediately. The board that comes out is therefore a different board, and baselines recorded with kicad-cli will not match a run without it, in either direction. Record and check baselines on a machine that has it. For the same reason, baselines are pinned to a KiCad version: an upgrade that reports one more violation can change which attempt wins.
To check a board manually:
kicad-cli pcb drc --severity-error -o report.txt routed/board.kicad_pcbTrial software, free to evaluate. LICENSE carries the terms; the two
that matter most in practice:
- What it produces is yours. Routed boards, gerbers, drill files, pick-and-place, verdict JSON — use them for anything, personal or commercial, no royalty and no attribution. Your input designs stay yours too. The included example board is free to use and modify as well.
- Nothing is guaranteed. The software comes with no warranty, and a run that reports zero DRC violations and every gate met is this program grading its own work — not an assurance that a board is correct, manufacturable, or safe. Placement, routing, ampacity widening and impedance geometry are heuristics and can be confidently wrong. Verify every layout independently before fabricating, energizing, or selling it.
Copyright (c) 2026 Seagull Work, Inc. Redistributing the executables, and use beyond evaluation, need separate terms — ask.