Commit Graph
20 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 6a24ffc540 Add continuous expansion-room routing track
A third engine (engine='room') that replaces the fixed routing grid with
FreeRouting's free-space rooms. Per layer, the space not occupied by items
(inflated by the routing clearance) is decomposed into exact rectangles via
coordinate compression (the orthogonal analogue of
CompleteFreeSpaceExpansionRoom); adjacent rooms share an edge door, and a via
door joins overlapping rooms on two layers. The maze search is an A* over
rooms-through-doors (cost = geometric distance + via cost), so a path may
cross a door anywhere along its width rather than at a quantized cell. The
found room sequence is realized into an exact orthogonal Polyline through the
door mid-gates and verified DRC-clean against the ShapeSearchTree, dropping
the net if it cannot be made exactly clean.

Maps to autoroute/: ExpansionRoom / CompleteFreeSpaceExpansionRoom (rooms),
ExpansionDoor / TargetItemExpansionDoor (doors), MazeSearchAlgo /
AutorouteEngine (search), LocateFoundConnectionAlgo (realization).

Foundation scope: rooms are axis-aligned rectangles (not general Simplex
tiles); realization uses an L-connector through door mid-gates. Any-angle
rooms, optimal gate placement (true sub-cell shove), and optimization passes
are follow-ups. The grid and exact tracks are unchanged; this is opt-in.

Routes simple/crossing (with a via) and all four multi-pin nets of the real
KiCad board, DRC-clean.
2026-07-13 09:38:40 -06:00
rsp2k f581f5b1e2 Add shove-and-retry to the exact router
Before a dropped net is abandoned, the exact router (with shove=True)
tries to make room by moving an existing trace aside instead: it forms a
straight orthogonal candidate for the dropped net, finds the axis-aligned
trace segments that cross it, and shoves each one perpendicular (via
shove_segment) far enough to restore clearance. A shove is accepted only
if the moved trace still connects its pads, clears every other item
exactly against the ShapeSearchTree, and stays inside the board outline,
so DRC-cleanliness is preserved by construction. Shoves are bounded
(max_shove_depth, per-trace shove_cap) and deterministic. The dropped set
now includes nets the grid failed to route, not only exact-clearance
rejections, so shove can recover them.

The pipeline exposes shove (and rip_up) through route_dsn_board_exact /
build_exact_routing_result / route(engine='exact', shove=True); default
off, so the grid track and the no-shove exact track are unchanged.
2026-07-13 02:43:19 -06:00
rsp2k 0bee385e71 Add exact-geometry (orthogonal, DRC-clean) routing track
Adds route/exact_router.py: routes with the grid track's rip-up/multi-layer/
via topology search in orthogonal mode (axis-aligned segments -> exact IntBox
copper), then verifies clearance exactly against a ShapeSearchTree. A net
whose copper would come closer than the clearance to any accepted item is
dropped, so the emitted geometry is DRC-clean by construction; endpoints stay
exactly on pads and vias at real layer transitions.

GridRouter gains a non-breaking orthogonal option (4-connected). The pipeline
gains an engine= selector: 'grid' (default, unchanged, highest coverage) or
'exact' (DRC-verified). The grid MVP stays the fallback.

On the designed boards the exact track routes every net DRC-clean; on the
real KiCad board it matches the grid's connectivity (4/4 multi-pin nets) and
is DRC-clean where the grid MVP's widened centrelines are not. Deferred:
shove, and 45-degree/IntOctagon trace caps.

Tests: the exact router's output has no clearance violation (exact IntBox
check) on every fixture; endpoints on pads; traces orthogonal; crossing uses
a via; and a direct comparison showing the exact track is clean where the
grid MVP violates on the real board. Oracle-gated parity for engine='exact'.
2026-07-13 02:16:23 -06:00
rsp2k d44275f07b Add rip-up-and-retry to the maze router
Restructures the router so occupancy is tracked per connection, then adds
a rip-up-and-retry loop that recovers from bad greedy net orderings.

When a connection cannot reach its target through free space, a rip-up
search may pass through other nets' traces at an escalating penalty; the
router rips up the connections that path crosses (fully removing their
occupancy), routes the failing connection, and re-queues the ripped
connections. Passes iterate up to max_passes, keeping the best
(fewest-unrouted) result and stopping on full success or when a pass
changes nothing.

Thrash prevention: each connection may be ripped at most rip_cap times,
and the rip penalty escalates with a connection's rip count, so
repeatedly-ripped connections harden into walls. Fully deterministic
(sorted rip sets, connection-creation order, tuple-keyed A* — no RNG).

route()/route_dsn_board()/route_board() take rip_up and max_passes;
rip_up=False reproduces the previous greedy single-pass behaviour for
comparison. RouteResult now reports the unrouted connection count.
2026-07-12 13:38:40 -06:00
rsp2k 992bba82a0 Add multi-layer routing with vias
Extends the grid router with a layer axis: A* nodes are (col, row, layer),
in-plane moves stay on a layer, and a via move transitions between layers
at a cell for a configurable via_cost (so the router prefers one layer but
changes layers to get through). Occupancy is tracked per (cell, layer);
a through via must be clear on every signal layer and then blocks all of
them for other nets.

RouteResult now carries per-layer wire segments and per-net via locations.
The pipeline emits each segment on its layer and each via as
(via <padstack> x y), using the DSN's via padstack; wire and via
coordinates are converted from board units back to DSN units. route() and
route_dsn_board() take an optional layers= to restrict routing (e.g. a
single layer) for comparison.
2026-07-12 13:20:52 -06:00
rsp2k 89c0481ccc Add MVP grid maze router and DSN->SES routing pipeline
Implements a working autorouter and the Java-free replacement for the
freerouting.jar step: dsn_text -> parse_dsn -> build_board -> route ->
write_ses.

GridRouter is a single-layer A* maze search over a uniform occupancy
grid: a cell is blocked by another net's pad (inflated by clearance +
half trace width) or a keepout; each net's ratsnest is connected pin to
pin; a routed trace then blocks other nets. The cell path becomes a
trace polyline whose endpoints are the exact pin locations. Board-unit
paths are converted back to DSN units for the SES (wire (path ...))
scopes.

This is an MVP, not a port of FreeRouting's expansion-room maze:
free-space rooms, rip-up-and-retry, multi-layer via search, and shove
are deferred (they raise quality/coverage, not the connectivity
milestone). It reaches connectivity on boards whose nets route on one
layer without crossing.

Adds tests/dsn/fixtures/simple_2net.dsn (a guaranteed-routable 2-net
board). Invariant tests: both nets route, the emitted SES parses, every
trace stays on a valid layer and within the board outline, and every
routed net's trace endpoints sit exactly on its two pads. An oracle-gated
test asserts connectivity parity with the reference FreeRouting JAR on
the simple board.
2026-07-12 11:25:03 -06:00