The grid and room engines write copper they never check. After a grid or
room run the result is now put through the exact engine's geometry (its
pads, fixed copper and keepouts; wires as boxes or slanted slabs; vias as
their pads) and the report gives the shorts and clearance violations, with
examples, instead of a blanket warning. --strict exits 3 when there are any;
by default the exit status stays 0 so the JAR drop-in contract holds.
The exact engine's static-copper setup moves into insert_static_copper so
both use it; exact runs are byte-identical before and after.
The grid engine was the default and its copper is not checked against exact
shapes: a full reroute of the Labrador lid with no flags came out with 15
tracks_crossing and 87 clearance violations under KiCad's DRC while the
report listed only unrouted connections. The exact engine (what mckicad and
every benchmark already use) is now the default, and grid or room runs end
with a warning that their output needs a DRC.
The router began as a port of FreeRouting and has grown its own engine
(negotiated congestion, tree nets, exact checks, axis escapes), and
'freeroute' was one letter from the project it no longer follows. The
package, module and command are now rpmroute; the freeroute command stays
as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back
to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a
PCBench board routes to a byte-identical session either way.
With items away from the window left off its board, a stub from the window
edge out to a rerouted net's far pin crossed copper the exact check never
saw: 38 shorts on th-eda's flight controller. A connection with an end
outside the bounds is now left unrouted, and a window whose new copper
leaves the window is refused.
A window's board now holds every pin and piece of copper of the nets it
reroutes and, of the rest, only items near the window, instead of the whole
board rebuilt per window. The smallest windows go first, at most
--window-cap per round (40), and no window starts once --window-budget of
the routing's own time is spent (0.25). On th-eda's flight controller the
stage had taken 12x the routing time for no gain.
After the exact engine, every net routed wholly inside a window around a
stuck connection's pins is ripped, the rest of the board is laid down as
fixed copper, and the window alone is routed again at --window-resolution
(default 4) with the stuck net and the ripped nets only. The result is kept
only if fewer connections are left unrouted; each round doubles the margin.
The grid and exact routers gain only_nets and bounds for it. Off by default.
Anchoring a T only on the connection whose copper it touched missed the
connections that tie that copper to the T's own group. When one of those
was ripped, the T stayed routed but cut off: on charge-sense freeroute
counted 15 unrouted while KiCad found 27 opens. Routed connections of a net
form a forest, so the path back is unique; the T now depends on all of it.
KiCad's opens and freeroute's count agree again (18 against 17, the extra
one a GND pour island left to the fill).
A net's pin groups were joined as a chain in pin order, and each
connection had to end on one chosen pin. Groups are now paired by a
minimum spanning tree (Manhattan distance between their nearest pins), and
a connection may start or end on any copper its net has already routed on
that side (path cells, via cells on every layer; not walled cells, whose
via moves into the pad). Plane nets keep their drops to the plane.
A connection that tees onto another records it as an anchor. Ripping a
connection rips what hangs on it, transitively, inside the same rip-up
transaction, so rollback restores all of them. The exact engine gets the
connections in commit order with their anchors and drops a T whose anchor
it rejects. --no-tree restores the chain.
A QFN placed at 45 degrees had no exit on the grid: every horizontal or
vertical step from a pin passes its neighbours inside clearance. A pin whose
pad is turned more than ten degrees off the grid axes now leaves along the
pad's own axis, away from the part, until an L from there to a grid cell
clears every other net's pad, then searches as usual. The slanted leg is
checked and stored as an exact slab (segment_slab), not its bounding box,
in the grid stub checks and in the exact engine.
Where pins leave side by side, a cell names only its first owner, so stubs
are also indexed per cell for every connection near them; the exact
stub-vs-stub check, trace entry and rip-up all consult that index. A pin
with an axis exit and no clear cell reports no way out instead of falling
back to a cell on another net's route. diagnose.py replays with the exits.
Every connection is routed N times on the empty board, where routes may
share cells at a price that doubles each round for every other net on a
cell, and a cell that stays shared gains history. The real routing then
pays that history on free cells, so the first routes stop taking the space
later routes (a fine-pitch part's escapes) have no way round. Off by
default.
The old history term never steered: it only accrued at rip-up fights,
after the first routes had claimed the contested space, and routed
connections are never revisited in later passes.
Two connections of a net can share a via. The grid kept one owner per via
cell next to the claim list, and the search and rip-up read only that owner:
a route of another net through the via ripped the first user and left the
second's via under the new trace. The exact check then threw one of them
out. The search and rip-up now read the claim list, and the single owner is
gone.
The exact check also tested a via against the outline as its square box,
whose corner reaches past a 45-degree chamfer where the round via does not.
The grid keeps the via's centre its radius plus the edge clearance from the
outline, as KiCad measures it; the exact check now does the same.
kitspace_minisumo_v3 seed 4: 4 rejected connections to 0, 11 failed to 7.
Rotated oval pads were built from padded corners that are not in outline
order, so they fell back to their bounding box; neighbouring boxes of a
rotated socket overlap, and the grid's own-pad exemption opened the
overlap to the pin's net, which the exact check then refused. Pads are now
the convex hull of their corners, the exact tree checks a rotated pad
against that outline, and the grid blocks cells against the same outline.
A via reused by a second connection of its net left that connection with
no claim on the via's footprint, so ripping the first owner freed cells
around a via still on the board. Footprint cells now keep every claimant.
A net class wider than the default was searched as a default trace: its
spread was empty below half a cell, halos were circular while the exact
check compares boxes, and a rip-up route only counted what its centre
crossed. The spread now covers the extra width in whole cells, halos are
square, and rip-up victims include what the spread crosses.
bench/diagnose.py now routes with net-class widths and hole spacing, as
the CLI does.
A 1.0 mm trace from a 1.27 mm pitch pin (the RN2483 row on the TTN
Enschede mote) has to stay within 0.06 mm of the pin's centreline until
it is past the neighbouring pads. No grid column is that close, and the
router could not get there:
- A stub ran as one slanted segment to its cell centre. The exact check
sees that as its bounding box, which lands on the next pad. Orthogonal
stubs now leave straight along the pin's axis and jog to the cell
only at the end.
- End nodes were chosen as in free space even in the rip-up search, so
once another net crossed a pin's only exit, the pin had no end left
and rip-up could never take the exit back. In rip-up mode those cells
and stubs are now enterable at the rip penalty, and routes on a stub
are ripped along with the ones the path crosses.
- Two neighbouring pins' stubs each claim the cells a centred trace
would crowd, so their claims overlapped well before their copper
did. Stub against stub is now checked on the copper.
- Escapes also look one column either side, so two pins leaving the
same way can jog apart.
TTN Enschede mote, exact engine with --shove: 12 unrouted before, 0
after; KiCad DRC on the result has no clearance violations and no
unconnected items. The 0.5 mm pitch row test at resolution 1 now
routes all 5 pins (it pinned 2).
Committed vias were checked for hole spacing, but a route's own vias are
not committed until it is done, so a route hopping layers (more of them
on 6 layers) could put two of its vias 0.41 mm apart, as th-eda's
6-layer flight controller showed. A via is now also checked against
layer changes earlier on its own route.
A fixed via sits anywhere, not on a cell centre, and may have a bigger
drill than the router's via; checking hole spacing between cell centres
with one drill let the flight controller's routed 0.2 mm via come 0.19
mm from a hand-placed 0.25 mm one. The board now carries each fixed
via's drill (from KiCad's padstack name), and cells are checked against
fixed vias exactly.
A route could drop a via next to (or onto) a through-hole pad of its own
net, where it changes layer for free, and a via reusing a fixed via's
cell came out a little off the fixed via's centre, beside it. No via now
goes within hole spacing of a through-hole pad of any net; a via in a pad
is checked exactly against every via and hole; and a routed via on a
fixed via of its net is not written. charge-sense: 4 hole_to_hole to 0;
flight controller: 2 to 0, unconnected unchanged.
Fixed tracks and vias only kept other nets away, so a pin fanned out to
its plane was routed to the plane again, beside its own fan-out. Pins,
zones and planes that fixed copper of their net touches now form one
connected group: a fanned-out supply pin needs nothing more, a fanned-out
signal routes from its via, and copper touching no pin or zone adds
nothing to route.
The exact engine rebuilds its output from each connection's own vias,
so connections sharing a plane via each wrote a copy: th-eda's flight
controller had 71 spots carrying 2 to 4 vias of one net. Vias are now
written once per net and spot, and never on a via already on the board.
Every pin of a supply drops its own via to the plane, and nothing kept
two of them apart: the flight controller came back with 199 co-located
holes and 109 hole-to-hole errors, nearly all VDD_3V3 and GND. A via now
keeps its drill plus --hole-to-hole (default 0.25 mm, KiCad's) from
every via centre, fixed ones included, except an own-net via at the same
cell, which it reuses; such a via is written once, and none is written
on top of a via already on the board.
The grid is laid out for the default width; a wider trace also needs the
cells its extra half width reaches to be free, and claims a wider halo.
A connection that does not fit at its class width (typically at a
fine-pitch pin) is routed at the default instead and reported. A net is
written at its class width when all of its connections went in at it,
and the exact check verifies it at that width; nets that shove or the
diagonal pass reshaped go out at the default.
Tracks and vias already on the board were ignored, so other nets could
be routed straight through them (the flight controller's 82 GND
stitching vias and its guard tracks). They are now copper of their net:
other nets keep a pad's distance from them in the grid, the exact check
and the room engine, and they add no connections to route.
A plane end is marked with its zone, not a pin, so a route from a pad
outside the plane's outline crashed looking for the pin's location
(the flight-controller board's first placement).
Over 8 net orders it left 25 to 30 fewer connections unrouted on the
dense sense boards and 16 to 18 fewer on two PCBench boards, and was
never clearly worse. KiCad DRC on the sense harness: 41 opens to 28.
The connections a rip-up route crosses are rerouted at once in free
space. If that leaves fewer connections routed than before, the new
route comes out and every displaced connection goes back on its old
path, with its rip count as it was. Even trades are kept.
A route may not leave a cell inside a wire keepout as a wire, nor reach
a pin there by one: it changes layer instead, and the via goes in the
pad. A pin whose every stub enters a keepout is reached by a via only.
A structure keepout named rule_area... (mckicad names KiCad rule areas
so) is checked for overlap only, as KiCad's DRC does, with the free ends
of each wire tested for their round caps.
--access-cost C makes a route pay C to pass where it would close another
pin's way out (its end cells, the via it may need there, and the
clearance around both) while that pin still has connections to route.
A pin inside a wire keepout can only leave by a via; its stub to the
cell centre was track inside the keepout, which the exact check refused.
The via now goes on the pin itself. Pin stubs are also checked exactly
against wire keepouts, as the exact engine will check them.
bench/diagnose.py reports what each refused connection collided with.
Each round routes every connection that has failed so far ahead of the
rest, which keep their order. The promoted set only grows, so two groups
cannot take turns failing; the best round is kept.
The router is deterministic, but its result depends heavily on which
connection routes first. A seed shuffles that order reproducibly, so a
change can be judged over several orders instead of the one it happened
to get.
On a four-layer board with a GND plane on an inner power layer, freeroute
ignored the plane (it only used zones on routing layers) and chained every
GND pad to the next with tracks across the board. Now a through-hole pad
inside a plane is joined to it already, and for a net with a plane every
other group connects straight to the plane: a route may end on any cell
inside it where a via fits (checked lazily, like other vias), and ending
there places that via. Signal nets are unaffected.
Tested on a four-layer DSN: three SMD GND pads each get a short stub and
their own via, the THT GND pad needs nothing, and no track crosses the
board. Ahead of the TerraHawk ESC, a four-layer board.
freeroute exited 0 and said nothing when it dropped connections, which
cost real diagnosis time: on rc4 two simple two-pin nets came back with no
copper and no explanation. The grid now records each connection it could
not route, the exact check adds each it rejects, and the CLI lists them on
stderr (the SES is still written; the exit code stays 0, as the JAR's).
A connection whose ends both sit in one zone of its net is listed apart,
as left to the zone fill: freeroute does not route through a zone it
cannot trust to be solid, but KiCad's fill often joins those pads anyway.
A zone outline is where copper may go, not where it ends up: on the rc4
sense board the board-wide GND pours break into islands around other
nets' tracks, and treating every pad inside them as connected left 28
GND islands unjoined (63 unconnected against 42 without zones). Trust a
zone only when a no-tracks rule area covers it on its layer, as the
power stage's pours are covered; route the rest with tracks as before.
rc4, exact engine, resolution 2: power stage 5 unconnected (JAR 12),
sense board 41 (JAR 32), no DRC errors on either.
freeroute ignored the DSN's planes (KiCad's zones), so it never knew that
pads in a pour are joined by it. On the rc4 power stage those pads sit
inside "keep router off" areas as well, and every connection to them was
dropped without a word.
- Read (plane <net> <shape>) and put each zone on the board as a
ConductionArea of its net. Zones are not obstacles: KiCad refills them
around other nets' copper.
- The grid groups each net's pads and zone pieces that copper already
joins (a pad in a zone, two zones sharing a pad) and routes only between
groups. A connection may end on a zone by a via inside its copper, or by
a track where tracks are allowed.
- wire_keepout now blocks traces only, as KiCad's "no tracks" rule area
does; via_keepout only vias; keepout both, in the grid and in the exact
verifier.
rc4 power stage, exact engine, resolution 2: 2 unconnected, down from 8
(FreeRouting JAR: 12), no DRC errors.
The grid search estimated the remaining distance as a straight line even
when only orthogonal steps exist; Manhattan is the tighter estimate those
moves admit and still never overestimates. --heuristic-weight W (default
1) makes it weighted A*: routes at most W times the shortest (tested).
A cell was exactly one trace plus its clearance, so a trace owning its cell
was the whole clearance model, and the 0.4 mm grid could not reach most
pins of a 0.5 mm pitch package: between two 0.3 mm pads a trace's centre
has a legal band only 0.15 mm wide. Resolution K makes cells K times
finer and has every trace claim a halo, the cells whose centre is nearer
than trace + clearance to one of its own, released again on rip-up.
K = 1 has an empty halo and is the old grid exactly (tested). On a 0.5 mm
row, K = 4 gets every pin out where K = 1 gets two. On the rc4 boards, K = 2
cuts the sense board's opens from 66 to 41 (JAR: 32) and the power board's
to 7 (JAR: 12), still with no DRC errors from freeroute's own copper.
Every route starts and ends with a stub from the pin to its first grid
cell, and the grid never saw that copper: on the rc4 sense board 24 of 39
connections the exact verifier rejected had a later route pass beside an
earlier stub. A route's stubs now claim the cells they come near, a stub
must be clear of other nets to be used, and a route may start from any
enterable neighbour of the pin cell, costed by stub length (Manhattan on
the orthogonal track, so a pin on a cell centre still leaves straight).
Tests: a route near a rounded corner keeps its edge clearance; and a
strict xfail that pins down the remaining gap, a 0.5 mm pin row the grid
cannot give a column to every pin of.
Measured against KiCad's own pads on the TerraHawk rc4 power stage:
- Back-side parts kept their pads on F.Cu (32 pads). Mirror the copper
layer for a back-side part.
- Pad outlines were copied unrotated (29 pads on rotated parts). Place the
outline through the same chain as the pin: pin rotation, back-side
mirror, part rotation. Oval pads, a path with a width, came out as a
zero-height sliver; pad them out by the aperture.
- Multi-shape padstacks used the first shape; use the largest.
- Footprint-level keepouts were parsed but never placed. KiCad exports a
mounting hole's plated ring that way, so the router could not see it.
- Vias were sized like a trace (0.25 mm square for a 0.6 mm via), and every
remaining short and clearance error was a via. Read the size from the
DSN's via padstack and use it in all three engines; the grid engine now
reserves the cells a via's copper reaches.
With these the exact and room engines route that board with no shorts,
clearance or other DRC errors. Pins now carry their component reference.
On the TerraHawk rc4 power stage every engine misread the board's rule areas:
- A no-track ring along the board edge arrives as one keyhole polygon, and
keepout polygons were wrapped in a single convex tile, so the ring became
the whole board. Split keepout polygons into convex pieces instead.
- The grid engine rasterized an area by its overall bounding box, which for
that ring is again the whole board; it routed nothing. Rasterize per tile.
- "Keep router off" rule areas arrive as wire_keepout, which the reader did
not know, so tracks ran through them (172 DRC items_not_allowed). Read it
at board and image level. For now it blocks vias too, which is stricter
than KiCad and cannot create a violation.
- place_keepout only restricts part placement; stop treating it as copper.
The oracle now runs the JAR headless; it was opening FreeRouting's GUI on
the desktop for every oracle test.
SES lengths and coordinates were written in the DSN's unit while the
session declared (resolution um 10), so readers that follow FreeRouting's
SesWriter (KiCad's importer, mckicad's ses_apply) placed every track at a
tenth of its size. The JAR, run on the same DSN, writes F1 at 65000 where
freeroute wrote 6500.
Scale every length by the resolution factor: placements, via padstack
shapes, wire widths and points, and vias. Rotations stay as they are. The
router tests compared SES output against DSN pad positions and so had the
same mistake built in; they now derive the expected values through
ses_scale.
Add the GPLv3 text (the package declared the licence but never shipped it),
a py.typed marker, and an sdist include/exclude allowlist so the distribution
carries only src/, docs/, README, LICENSE and pyproject. reference/ is a clone
of the GPL FreeRouting Java tree kept purely as a porting reference and must
never be redistributed inside this package; tests, caches and build output are
excluded too.
Switch to the PEP 639 SPDX licence expression with license-files, widen the
classifiers, and add an -o alias for -do. Rewrite the README around the engine
matrix: what each track and pass actually does, and what this is not (no
FreeRouting density parity on dense boards, and --diagonal is a recovery and
shortening pass, not a diagonal-native search).
The grid MVP was the only track reachable from the command line, so the
continuous room engine, channel packing, shove, and 45-degree routing were
unusable by anything shelling out to freeroute.
Add --engine {grid,exact,room} (default grid, unchanged behaviour) plus
--pack / --shove / --diagonal, and honour -mp on the tracks that implement
rip-up passes. Not every engine supports every pass; ENGINE_OPTIONS is the
matrix and an unsupported combination exits 2 with a message naming the
engines that do support the flag, rather than silently no-opping.
Add diagonal_win.dsn: a 45-degree pin-to-pin net with a keepout placed inside the
diagonal's bounding box but clear of the true thin copper. Orthogonal routing
connects it with a Manhattan-length staircase (1600000); diagonal routing
replaces that with a single 45-degree trace at the ideal hypotenuse length
(1131371) -- a 29% reduction -- DRC-clean via the search tree and via an
independent reconstruction of the emitted 45-degree copper's clearance octagon.
test_exact_clearance_is_load_bearing shows the keepout overlaps the coarse
bounding box but not the exact octagon, so the exact clearance (not the box) is
what lets the diagonal route. Determinism, endpoints-on-pads, valid SES, and
diagonal-off-equals-orthogonal (additive, over four fixtures) are covered.
A diagonal segment's copper has no axis-aligned exact tile, so segment_box falls
back to the bounding box -- a huge conservative cover that makes diagonal traces
conflict with everything. segment_octagon builds the tight cover instead: the
copper of a 0/45/90/135-degree segment swept by a radius, as a convex integer
Simplex bounded by the four axis and four x+y / x-y diagonal half-planes.
Diagonal bounds widen by ceil_sqrt2(radius) -- the exact integer ceiling of
radius*sqrt(2) via math.isqrt, no float -- rounded outward, so the octagon is a
provable superset of segment (+) disk(radius) (verified over 20k sampled copper
points). Growing by clearance is just a larger radius, giving both the stored
copper tile and the queried clearance region. overlaps_2d tests a real 2-D
overlap via exact Simplex intersection, accepting mixed IntBox/Simplex tiles.
Add channel_pack.dsn: three nets whose pads spread wider than a wall gap, so they
must converge and pack into lanes. Plain room routing (shove on or off) drops >= 1
net under every net ordering; the packer connects all three under every ordering,
DRC-clean (search tree and independent emitted-copper check).
The falsifiable boundary test sweeps the gap width for N=3 and N=4 and asserts the
packer connects all N iff the gap meets the geometric feasibility width
(N-1)*(width+clearance) + 2*(half_width+clearance), never routes fewer than greedy
below it, and never false-packs. Determinism, endpoints-on-pads, valid SES, and
pack=False-equals-default (additive) are covered.
Add true_density.dsn: NET_C is a full-height wall on the sole signal layer,
so plain room routing drops a net under every net ordering (an order-independent
obstruction, verified over all permutations), unlike rooms_shove_channel whose
plain drop fits under a reorder.
Test that shove recovers nothing on this board (shoves == 0, same routed set as
plain) while staying DRC-clean, and that the only difference from the recoverable
shove_channel is the wall's span. shove relocates a blocker into existing free
space; where the wall leaves no headroom there is nowhere to relocate to, so it
cannot manufacture density. The recover/fail boundary is the free-space boundary.
_shove_recover_room clobbered a net's _Accepted with a fresh one whose
wire_owners held only the just-recovered wire, while its wires list (the
shared result.wires[net_no]) already held earlier committed connections.
That left wire_owners shorter than the parallel wires list, so a later
net's shove either removed the wrong wire's copper from the search tree
(a silent different-net short: copper stayed in the emitted SES but
vanished from the tree, and has_violation falsely certified it clean) or
raised IndexError and aborted the whole route.
Reuse the net's existing _Accepted on recovery so wire_owners stays
parallel to wires. Fixes both the silent DRC violation and the crash
sibling without touching the DRC-clean or determinism invariants.
Also correct the overstated "density win" framing of the shove_channel
fixture: its plain-routing net drop is an artifact of greedy net order,
not a density limit (the same board routes both nets with zero shoves
under a reorder or with the cosmetic seals removed). Reframed the test
and docstring; the genuine sub-cell win remains the wide_door occupancy
packing (shoves == 0).
Regression tests assert the invariant against the emitted output
directly (reconstructed copper tiles), not just the tree that the bug
fooled, plus determinism, no-crash, and the ordering-artifact bound.