Commit Graph
80 Commits
Author SHA1 Message Date
rsp2k 245cd91c0e Don't cost a pin stub for a route that ends on a plane
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).
2026-09-28 19:55:07 -06:00
rsp2k 019b043cd5 Merge feat/ripup-rollback: undo rip-ups that lose connections (on by default) 2026-09-28 16:17:30 -06:00
rsp2k a2e108bc51 Rollback on by default; --no-rollback keeps every rip-up
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.
2026-09-28 16:17:28 -06:00
rsp2k 3462089e09 --rollback: undo a rip-up that leaves fewer connections routed
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.
2026-09-28 14:54:29 -06:00
rsp2k 35891c02ff Merge feat/rule-area-keepouts: keepout exits, via-only walled pins, rule-area support 2026-09-28 10:32:44 -06:00
rsp2k 3e4f53a469 Keepout exits and rule areas
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.
2026-09-28 10:08:02 -06:00
rsp2k bb75dc5067 DSN reader: only the declared string_quote character opens a string
KiCad declares " and writes a keycap part number as a bare ', which the
tokenizer took for an unterminated string (PCBench f.60_keyboard).
2026-09-28 09:41:27 -06:00
rsp2k 9de8329027 Merge feat/pin-access: vias in pads inside keepouts, exact stub checks, --access-cost 2026-09-28 09:38:18 -06:00
rsp2k f1d0f8df70 Pin access: --access-cost, vias in pads inside wire keepouts, exact stub checks
--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.
2026-09-28 09:15:11 -06:00
rsp2k c44ff272ae bench/diagnose.py: sort an exact-engine run's failures by cause
rejected by the exact check, unroutable even alone, walled in (a pin's
free region is closed off by other copper, with the nets that close it),
or congestion. The board state is the best pass's, rebuilt by replaying
its paths.
2026-09-28 00:30:05 -06:00
rsp2k 05f31fc828 Merge feat/promote-failures: reroute with failed connections first; --orders 2026-09-27 22:53:47 -06:00
rsp2k 01ca108831 --promote-failures: reroute with the connections that failed moved first
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.
2026-09-27 22:34:06 -06:00
rsp2k 0f5dacbd88 --orders: route several connection orders in parallel, keep the best
The default order is always one of the candidates, so the result is
never worse than a plain run; ties go to the earlier candidate.
2026-09-27 22:14:59 -06:00
rsp2k e67ccc04a0 Keep benchmark board lists and results out of git 2026-09-27 15:07:13 -06:00
rsp2k ed2e4c3fac bench: compare routing variants over several connection orders
Runs every board with every variant under the same seeds and compares
them seed by seed, so a change is judged against the routing-order
noise rather than one lucky or unlucky run.
2026-09-27 15:06:30 -06:00
rsp2k 76c3af94b8 --seed: shuffle the order connections are routed in
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.
2026-09-27 15:04:35 -06:00
rsp2k cf8b7ece60 uv.lock: record the CalVer version from pyproject 2026-09-27 15:04:35 -06:00
rsp2k f8804512ac Merge feat/plane-layers: route to inner plane layers with a via 2026-09-27 15:01:38 -06:00
rsp2k 56d576e613 Route to inner plane layers with a via, not pad to pad
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.
2026-09-27 14:19:13 -06:00
rsp2k 6f3db3e0e3 README: engines, flags and zones as they now are, and measured results
Recommend the exact engine for KiCad boards; document --edge-clearance,
--grid-resolution, --heuristic-weight and the unrouted report; list what is
read from a KiCad DSN (pads, vias, keepout kinds, outline, zones, fine
pitch), each checked against KiCad; replace the scope section with the
TerraHawk measurements and what still trails the JAR.
2026-09-27 13:04:23 -06:00
rsp2k 3253e6fa43 Escape stubs for pins walled in on the grid
On the rc5 sense board at its real 0.2 mm clearance, ten pins of the
0.5 mm pitch STM32 could only reach a pocket of seven or eight cells
inside their own pads: the grid columns miss the narrow legal band past
the pad tip, so twelve connections could not route even on an empty
board. For a crowded pin (a neighbouring pad of another net closer than a
trace can pass) whose nearby cells are such a pocket against pads and
keepouts, offer straight stubs along each axis to the first open cell,
accepted only if the stub clears every other net's pad by the full
margin, measured exactly. All twelve then route alone; on the full board
unrouted connections drop from 61 to 59, the rest being congestion.
2026-09-27 13:01:27 -06:00
rsp2k cdb3c7b568 Say which connections were not routed
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.
2026-09-27 12:34:20 -06:00
rsp2k c49c79604d Exact engine defaults to --grid-resolution 2
Resolution 2 reaches 0.5 mm pitch pins and cut unconnected items on both
rc4 boards; since lazy via checks it costs about 2x in time, not 4x.
The grid engine keeps 1, the old grid exactly.
2026-09-27 12:00:19 -06:00
rsp2k 19b17378fa Count on a zone's connectivity only when no track can cross it
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.
2026-09-27 11:59:59 -06:00
rsp2k 354c89a8d5 Route with copper zones, and let vias into no-tracks areas
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.
2026-09-27 11:54:04 -06:00
rsp2k 83b2248a97 Check via footprints lazily in the grid search
Profiling the rc4 sense board at resolution 2 put half the time in
_via_placeable: every expanded cell checked a 5x5 via footprint on both
layers, for vias the search almost never takes. Queue a layer change as a
candidate and check its footprint (memoised per search) only when it is
popped. A check can only rule a via out, never make it cheaper, so this
cannot change which route is found.
2026-09-27 11:41:42 -06:00
rsp2k 67c6c205f1 Tighter A* estimate on the orthogonal track, and --heuristic-weight
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).
2026-09-27 10:55:15 -06:00
rsp2k 8929fd367e Finer routing grid: --grid-resolution K with trace halos
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.
2026-09-27 07:59:05 -06:00
rsp2k 9fa5432953 Give pin stubs clearance, and let a route leave a pin by any open side
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.
2026-09-27 07:37:50 -06:00
rsp2k c1df284147 Route to the real board outline, per connection, and out of fine-pitch pins
- The exact engine verified copper per net and dropped a whole net on any
  conflict: a 25-pin +3V3 net lost every connection to one bad segment.
  Verify and accept per ratsnest connection instead.
- Pad and keepout cells were rounded outward, blocking cells up to half a
  step clear of the copper; 62 of the 64 pins of a 0.5 mm pitch LQFP could
  not start a route. Block only cells whose centre is inside the clearance
  zone, and let a net walk along inside its own pad, which adds no copper.
- Routing was bounded by the outline's bounding box, so tracks cut across
  rounded corners. Keep the DSN boundary as a polygon; the grid blocks cells
  too close to it (scanline plus per-edge, precomputed for vias) and the
  exact and room engines reject copper that is.
- --edge-clearance MM sets copper-to-edge clearance, e.g. KiCad's
  min_copper_edge_clearance, which the DSN does not carry. Default 0.

On the TerraHawk rc4 power stage the exact engine now matches the
FreeRouting JAR: same open count, no DRC errors, with a 0.5 mm edge rule.
2026-09-27 07:26:32 -06:00
rsp2k 96c212466a Tests for pads placed as KiCad places them, and the via size 2026-09-27 06:41:13 -06:00
rsp2k f1df60fc31 Model pads and vias as KiCad places them
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.
2026-09-27 06:40:34 -06:00
rsp2k 3ad5c983f5 Honour KiCad's keepouts: keyhole rings, wire_keepout, per-tile rasterizing
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.
2026-09-27 06:30:43 -06:00
rsp2k b26fdd96d5 Write SES coordinates in resolution units, as FreeRouting does
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.
2026-09-26 14:26:18 -06:00
rsp2k fd3bee1085 Add CLAUDE.md: invariants, architecture, and hard-won lessons
Captures the three non-negotiable invariants (DRC-clean output, exact integer
geometry, determinism), the engine matrix, the reference/ GPL rule, the JAR
oracle strategy, and the lessons that cost real debugging: KiCad's /*52 net
names, exact clearance being load-bearing, shove not being a densifier, and the
falsifiable-fixture bar for any density claim.

Force-added past the global ignore: this is a private repo and the file is
excluded from the sdist, so it never reaches PyPI.
2026-07-14 07:28:46 -06:00
rsp2k f653c33e01 Version 2026.07.14 for the first public release
CalVer per project convention: the date communicates when the router was last
validated against the reference FreeRouting engine and real KiCad output. The
Alpha classifier and README carry the maturity signal.
2026-07-14 07:26:41 -06:00
rsp2k bf868b7b05 Harden packaging for a public release
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).
2026-07-13 18:17:13 -06:00
rsp2k 758ba1fbe3 Expose the exact and room engines in the CLI
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.
2026-07-13 18:15:02 -06:00
rsp2k accec686f8 Prove the 45-degree win: 29% shorter, DRC-clean, exact clearance load-bearing
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.
2026-07-13 12:57:13 -06:00
rsp2k 700ed06843 Route 45-degree traces on the exact track (opt-in diagonal mode)
Add diagonal=True to the exact router: after orthogonal routing, each 2-pin net
is retried as the shortest exactly-clean octilinear (0/45/90/135-degree) trace.
A dropped net is recovered; an orthogonal route is replaced only when the
diagonal is strictly shorter. Candidates are the direct 45-degree segment or the
two diagonal-plus-axis two-benders; each segment's diagonal copper is covered by
the exact octagon and its clearance checked with clearance_conflict_shape, so a
trace fits a diagonal corridor the bounding-box cover would reject.

Endpoints stay on the pads; diagonal copper is stored with its exact octagon so
later nets clear it precisely; same-net copper is ignored during the check so the
candidate is verified against every other net before the orthogonal copper is
ripped. With diagonal=False the pass is skipped and output is byte-for-byte the
orthogonal router. Threaded through route_dsn_board_exact /
build_exact_routing_result / route(engine="exact").
2026-07-13 12:57:05 -06:00
rsp2k 20dc5fd96a Support exact octagon tiles in the search tree clearance checks
Add optional exact/seg fields to TreeShape for a diagonal trace tile: tile holds
the bounding box for broad-phase bucketing, exact holds the tight octagon copper,
and seg=(a,b,half_width) rebuilds the clearance region on demand. Every
orthogonal tile leaves both None and takes the identical box code path, so the
box-only tree stays byte-for-byte unchanged (whole suite green).

clearance_conflict now tests a stored diagonal tile with an exact 2-D overlap;
clearance_conflict_shape checks a diagonal trace's clearance octagon against
stored copper; has_violation runs the exact octagon test whenever either tile is
diagonal and the plain box overlap otherwise. All exact-integer via Simplex
intersection.
2026-07-13 12:47:06 -06:00
rsp2k 8380318d3c Add exact integer octagon cover for 45-degree trace copper
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.
2026-07-13 12:46:02 -06:00
rsp2k 7f81c1f4ce Prove channel packing: connects all where greedy drops, feasibility-bounded
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.
2026-07-13 12:27:31 -06:00
rsp2k c28223ea9c Add coordinated multi-trace channel packing to the room track
Greedy per-net room routing sends each net through a shared obstacle gap on its
own independently chosen path; the paths collide and some nets drop even when the
gap is physically wide enough for all of them. shove cannot fix this -- it
relocates blockers into existing space, it does not pack lanes.

Add an opt-in pre-pass (pack=True, independent of shove) that groups 2-pin nets
which must cross a common gap, assigns each a parallel lane across the gap's
usable width (pad-ordered so lanes never cross, spaced >= width + clearance) with
staggered fan-in trunks so the converge/diverge jogs never overlap, and realizes
each as an orthogonal trace verified exactly DRC-clean. Packing is all-or-nothing
per group, so it never leaves a channel worse than greedy. With pack=False the
pre-pass is skipped and the output is unchanged.

Scope: single-layer 2-pin nets through one axis-aligned gap, with enough fan
depth to stack the trunks. Threaded through route_dsn_board_rooms /
build_rooms_routing_result / route(engine="room").
2026-07-13 12:27:23 -06:00
rsp2k 99c3d20de5 Prove shove-in-rooms recovers ordering drops but does not create density
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.
2026-07-13 11:34:31 -06:00
rsp2k 9cf2392c4b Fix multi-pin wire/owner desync in shove-in-rooms recovery
_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.
2026-07-13 11:08:02 -06:00
rsp2k a06adbee13 Add shove-in-rooms and gate-placement fixtures and tests
rooms_shove_channel is the headline density win: a sealed NET_A wall
leaves no top<->bottom door, so plain room routing drops NET_C; with
shove=True NET_A is nudged aside and both nets route, DRC-clean and
deterministic. rooms_wide_door seats two nets through one wide gap at
distinct projected gate points with no shove.

Cover the occupancy-aware projection math directly, assert the four
foundation fixtures stay byte-identical under shove=True (shoves==0), and
extend the room DRC-clean parametrize with both new fixtures.
2026-07-13 10:27:17 -06:00
rsp2k 4042f80b81 Add gate-optimal placement and shove-in-rooms to the room router
Both upgrades sit behind an opt-in shove=False flag, so the default code
path is byte-for-byte the foundation router (existing room tests are
unchanged by construction).

With shove=True:

- Gate placement becomes occupancy-aware: each edge gate is projected
  into the largest other-net-free sub-span of the door, with the reserved
  intervals read straight from the search tree. With margin = clearance +
  half_width the room decomposition already keeps doors clearance-clear,
  so this reduces exactly to the old midpoint clamp where no other-net
  copper crosses the door.

- When a net would be dropped, a transactional shove-in-rooms recovery
  nudges the committed trace(s) blocking it aside with the exact shove
  primitive and re-verifies against the clearance oracle, so both nets fit
  through one channel. Every moved trace is journaled and rolled back to
  its exact original tiles and wires on any failure -- the room engine
  mutates already-committed nets, so the rollback is real rather than the
  exact track's no-op.

The board-wide has_violation(clearance) gate stays the final assertion.
Thread the flag through pipeline.route_dsn_board_rooms /
build_rooms_routing_result / route(engine='room', shove=...).
2026-07-13 10:27:10 -06:00
rsp2k e67a11c833 Add shared orthogonal shove primitives module
Factor the exact router's shove geometry into route/shove.py: the
conflicting-segment finder and displacement ladder are re-exported from
exact_router (kept byte-unchanged so its shove count and geometry stay
identical), and a room-oriented trace placement helper is added that
returns the inserted tile owner ids for a journaled rollback.
2026-07-13 10:27:01 -06:00
rsp2k faf2df42bc Add continuity-win fixture and room-router tests
Adds narrow_channel.dsn: a keepout wall with a narrow gap positioned off the
routing grid, so no grid cell centre falls inside the gap's free centreline.
The exact/grid track drops the net (cell quantization can't fit a centreline
in the channel); the continuous room track routes it. This is the headline
result that continuous free-space beats the grid.

Tests: the room router's output is DRC-clean (exact has_violation is None) on
simple/crossing/kicad/narrow_channel; it connects simple/crossing (with a via)
and all four multi-pin KiCad nets; traces are orthogonal and end on pads; and
the continuity contrast asserts exact routes 0 while rooms routes 1, both
clean. Oracle-gated connectivity parity on simple/crossing.
2026-07-13 09:38:52 -06:00