From 6f3db3e0e32d67e0789614445fb3aa4d971c9850 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 27 Sep 2026 13:04:23 -0600 Subject: [PATCH] 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. --- README.md | 144 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index d4f9236..e7e47f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # freeroute -A native Python PCB autorouter — a Java-free reimplementation of the +A native Python PCB autorouter: a Java-free reimplementation of the [FreeRouting](https://github.com/freerouting/freerouting) engine. Specctra DSN in, Specctra SES out, no JVM anywhere in the pipeline. @@ -8,8 +8,8 @@ in, Specctra SES out, no JVM anywhere in the pipeline. Toolchains that autoroute (`kicad-mcp` among them) do it by shelling out to `freerouting.jar`, which drags a JVM into every install and container image. -`freeroute` reimplements the routing engine in pure Python — no dependencies at -all, standard library only — so the JAR step becomes a `pip install`. +`freeroute` reimplements the routing engine in pure Python, with no dependencies +beyond the standard library, so the JAR step becomes a `pip install`. ## The contract @@ -21,7 +21,7 @@ board.dsn ──▶ freeroute ──▶ board.ses ``` `kicad-cli` already exports `.dsn` and imports `.ses`, so `freeroute` drops in -wherever `java -jar freerouting.jar` was invoked — nothing else in the pipeline +wherever `java -jar freerouting.jar` was invoked, and nothing else in the pipeline changes. The `-de` / `-do` / `-mp` flags are the JAR's, on purpose. ## Install @@ -33,27 +33,31 @@ uv tool install freeroute # or: pipx install freeroute ## Usage ```bash -freeroute board.dsn -o board.ses # grid engine, the default -freeroute -de board.dsn -do board.ses # the FreeRouting JAR spelling +# a KiCad board: the exact engine, with the board's copper-to-edge rule +freeroute board.dsn --engine exact --shove --edge-clearance 0.5 -do board.ses +freeroute -de board.dsn -do board.ses # the FreeRouting JAR spelling (grid engine) freeroute board.dsn --engine room --pack --shove -do board.ses -freeroute board.dsn --engine exact --diagonal --shove -do board.ses ``` -With no `-do` / `-o`, the SES goes to stdout. +With no `-do` / `-o`, the SES goes to stdout. Connections freeroute could not +make are listed on stderr; the SES is still written and the exit code stays 0, +as with the JAR. Connections whose two ends sit in one zone of their net are +listed separately, as left to that zone's fill. ### Engines Three routing tracks, selected with `--engine`: -- **`grid`** (default) — multi-layer maze router over a uniform occupancy grid, - with rip-up-and-retry and via search. Highest raw connectivity of the three; - its geometry is grid-quantised, so it is the least precise about clearance. -- **`exact`** — orthogonal router whose output is verified against exact integer - geometry (tile/octagon clearance, not bounding boxes). Cleaner, DRC-clean - output where it succeeds; it will drop a net rather than emit a violation. -- **`room`** — continuous expansion-room router: an exact decomposition of free - space, so it routes off-grid channels that the grid quantisation cannot see. +- **`exact`** (recommended for real boards): orthogonal grid search whose every + accepted connection is verified against exact integer geometry. It drops a + connection rather than emit a violation. Defaults to `--grid-resolution 2`. +- **`grid`** (the default, for JAR-compatible invocation): the same multi-layer + maze router with rip-up and via search, without the exact check, so its + geometry is only as precise as the grid. +- **`room`**: continuous expansion-room router, an exact decomposition of free + space. It routes off-grid channels the grid cannot see, but it is much slower + on real boards and does not yet use zones. ### Flags @@ -62,6 +66,9 @@ Three routing tracks, selected with `--engine`: | `--engine {grid,exact,room}` | routing track (default: `grid`) | | `-de`, `--design` | input `.dsn` (FreeRouting-compatible alias) | | `-do`, `--output` | output `.ses` (default: stdout) | +| `--edge-clearance MM` | copper to board-edge clearance, e.g. KiCad's `min_copper_edge_clearance`, which the DSN does not carry (default 0: copper only has to stay inside the outline) | +| `--grid-resolution K` | grid cells K times finer than trace + clearance (default 2 for exact, 1 for grid); finer reaches fine-pitch pins at a cost in time | +| `--heuristic-weight W` | exact: weighted A*, routes at most W times the shortest (default 1) | | `-mp`, `--max-passes` | rip-up-and-retry pass bound (default 10) | | `--no-rip-up` | single greedy pass, no rip-up | | `--layers` | comma-separated signal layer indices to route on | @@ -74,65 +81,90 @@ unsupported combination is a hard error (exit 2) rather than a silent no-op: | | `--pack` | `--shove` | `--diagonal` | `--no-rip-up` | `-mp` | |---|---|---|---|---|---| -| **grid** | — | — | — | yes | yes | -| **exact** | — | yes | yes | yes | yes | -| **room** | yes | yes | — | — | — | +| **grid** | no | no | no | yes | yes | +| **exact** | no | yes | yes | yes | yes | +| **room** | yes | yes | no | no | no | -`--layers` works on all three. +`--layers` and `--edge-clearance` work on all three; `--grid-resolution` on grid +and exact. What the opt-in passes buy you: -- **`--pack`** (room) — when several 2-pin nets have to cross the same obstacle - gap, they are assigned consistent parallel lanes and routed together, so a - channel physically wide enough for all of them stops dropping nets to greedy - per-net gate collisions. -- **`--shove`** (exact, room) — a net that would be dropped nudges the blocking +- **`--pack`** (room): when several 2-pin nets have to cross the same obstacle + gap, they are assigned consistent parallel lanes and routed together. +- **`--shove`** (exact, room): a net that would be dropped nudges the blocking committed traces aside and re-routes, rolling every moved trace back if the - retry fails. On the room engine it also turns on occupancy-aware gate - placement. It recovers *ordering* drops; it cannot manufacture space that is - not there. -- **`--diagonal`** (exact) — a still-dropped 2-pin net is retried as a 45° trace - whose diagonal copper is covered by an exact integer octagon, so it fits - corridors the orthogonal L cannot, at the ideal hypotenuse length. + retry fails. It recovers *ordering* drops; it cannot make space that is not + there. +- **`--diagonal`** (exact): a still-dropped 2-pin net is retried as a 45° trace + covered by an exact integer octagon. + +## What freeroute reads from a KiCad DSN + +These were each checked against KiCad itself, pad by pad or with its DRC: + +- **Pads** placed as KiCad places them: back-side parts on the mirrored copper + layer, outlines rotated with the part, oval pads at their full width, the + largest shape of a multi-layer padstack. +- **Vias** at the size of the DSN's via padstack. +- **Keepouts**: `keepout` blocks tracks and vias, `wire_keepout` (KiCad's "no + tracks" rule area) tracks only, `via_keepout` vias only; a rule area with a + hole (a no-track ring along the edge) is split into convex pieces rather than + filled in; footprint-level keepouts (a mounting hole's plated ring) are placed + with their part; `place_keepout` does not affect routing. +- **The real board outline**, rounded corners included, with `--edge-clearance`. +- **Copper zones** (`plane`): a pad inside a zone connects through it, and a + route can end on it with a via. A zone is only counted on to connect when a + no-tracks rule area covers it; a board-wide pour can be split into islands by + other nets' tracks, so its pads are routed with tracks instead. +- **Fine-pitch pins**: a finer grid, and exact escape stubs for pins the grid + cannot reach. + +SES coordinates are written in resolution units, exactly as the JAR writes them. ## Library use ```python from freeroute.route import route -ses_text = route(dsn_text, engine="room", shove=True, pack=True) +ses_text = route(dsn_text, engine="exact", shove=True, edge_clearance_mm=0.5) ``` ## Status and honest scope -Alpha, version 0.1.0. It parses real `kicad-cli` DSN output, routes, and writes -SES that KiCad imports. What is actually implemented: +Alpha. Measured on two real KiCad 10 boards (the TerraHawk charge node's power +stage and sense board, 2026-09-27), with every track stripped, then routed, then +checked with `kicad-cli pcb drc --refill-zones`: -- Specctra DSN parser (structure, library, placement, network, rules, keepouts) - and SES writer. -- Multi-layer routing with via search and rip-up-and-retry. -- Exact integer geometry (tiles, octagons) with a spatial index, so the `exact` - and `room` engines verify their own output is clearance-clean. -- Continuous expansion-room free-space decomposition. -- Channel packing, shove, and 45° routing as described above. +| board | freeroute exact | FreeRouting JAR | DRC errors from freeroute | +|---|---|---|---| +| power stage (40 x 30 mm) | 7 unconnected, ~5 s | 12 | 0 | +| sense board (55 x 25 mm, 0.5 mm pitch MCU) | 41 unconnected, ~2.5 min | 32 | 0 | + +In the board owner's production flow (hand-routed power lanes kept, pours added +after routing), freeroute completed the power stage with no unconnected items. What it is **not**, stated plainly: -- **Not at FreeRouting/JAR density parity on dense commercial boards.** The JAR - remains the better router when the board is hard. `freeroute` is honest about - drops: it leaves a net unrouted rather than emit a DRC violation, so on a - crowded board expect fewer completed nets than the JAR would manage. -- **`--diagonal` is a shortening/recovery pass, not diagonal-native search.** - The maze search is orthogonal; 45° geometry is applied to nets the orthogonal - pass dropped or to shorten a staircase. A true 45°-native expansion-room maze - is not implemented. -- No fanout pass, no post-route optimiser, no interactive/incremental routing, - no net classes with per-class widths beyond the default rules, no plane/pour - handling. +- **Not yet at JAR completion on a dense board.** The legality of what it routes + holds up; how much it routes does not match the JAR yet. The remaining gap is + congestion: routes committed one at a time block later ones. The JAR's + negotiated rip-up loop handles that better. Negotiated congestion routing is + the planned next step. +- **Slower than the JAR** on a dense board (minutes against seconds), being + pure Python. +- **`--diagonal` is a recovery pass, not diagonal-native search.** +- The `room` engine does not use zones, the outline edge rule is only enforced + approximately there, and it is slow on real boards. +- No fanout pass, no post-route optimiser, no incremental routing, no per-class + track widths beyond the default rules. +- Copper that KiCad draws as graphics (a net tie's polygon, a logo) is not in + the DSN at all, so no router sees it; mckicad adds footprint copper to the DSN + as keepouts. -If you need maximum completion on a dense board today, use the JAR. If you need -routing without a JVM — CI, containers, an MCP server, a library call — this is -that. +For maximum completion on a dense board today, the JAR is still the stronger +router. For routing without a JVM (CI, containers, an MCP server, a library +call), freeroute routes legally and says what it could not do. ## Development @@ -144,7 +176,7 @@ uv run ruff check src/ tests/ The `oracle`-marked tests run the real `freerouting.jar` as a differential reference and skip cleanly when Java or the JAR is absent. The JAR is never -vendored, and neither is the upstream Java source — see +vendored, and neither is the upstream Java source; see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the port map and [`docs/PORTING_PLAN.md`](docs/PORTING_PLAN.md) for the roadmap.