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.
197 lines
8.6 KiB
Markdown
197 lines
8.6 KiB
Markdown
# rpmroute
|
|
|
|
A native Python PCB autorouter. Specctra DSN in, Specctra SES out, no JVM
|
|
anywhere in the pipeline.
|
|
|
|
It began as a Java-free port of the
|
|
[FreeRouting](https://github.com/freerouting/freerouting) engine and has grown
|
|
its own router since: negotiated congestion before routing, multi-pin nets
|
|
routed as trees, every route checked against exact pad and copper shapes, and
|
|
pin escapes along the pad's own axis for parts placed at an angle. On dense
|
|
boards it now finishes more connections than the FreeRouting JAR, with no
|
|
rule violations.
|
|
|
|
It was called `freeroute` until 2026-10-07; that command still works.
|
|
|
|
## Why
|
|
|
|
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.
|
|
`rpmroute` reimplements the routing engine in pure Python, with no dependencies
|
|
beyond the standard library, so the JAR step becomes a `pip install`.
|
|
|
|
## The contract
|
|
|
|
The entire integration surface is two Specctra files:
|
|
|
|
```
|
|
board.dsn ──▶ rpmroute ──▶ board.ses
|
|
(unrouted) (routed session: traces + vias)
|
|
```
|
|
|
|
`kicad-cli` already exports `.dsn` and imports `.ses`, so `rpmroute` drops in
|
|
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
|
|
|
|
```bash
|
|
uv tool install rpmroute # or: pipx install rpmroute
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# a KiCad board: the exact engine, with the board's copper-to-edge rule
|
|
rpmroute board.dsn --engine exact --shove --edge-clearance 0.5 -do board.ses
|
|
|
|
rpmroute -de board.dsn -do board.ses # the FreeRouting JAR spelling (grid engine)
|
|
rpmroute board.dsn --engine room --pack --shove -do board.ses
|
|
```
|
|
|
|
With no `-do` / `-o`, the SES goes to stdout. Connections rpmroute 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`:
|
|
|
|
- **`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
|
|
|
|
| flag | effect |
|
|
|---|---|
|
|
| `--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 |
|
|
| `--pack` | coordinated multi-trace channel packing |
|
|
| `--shove` | move committed traces aside to recover dropped nets |
|
|
| `--diagonal` | 45° recovery pass for dropped 2-pin nets |
|
|
|
|
Not every engine implements every pass. This is the supported matrix, and an
|
|
unsupported combination is a hard error (exit 2) rather than a silent no-op:
|
|
|
|
| | `--pack` | `--shove` | `--diagonal` | `--no-rip-up` | `-mp` |
|
|
|---|---|---|---|---|---|
|
|
| **grid** | no | no | no | yes | yes |
|
|
| **exact** | no | yes | yes | yes | yes |
|
|
| **room** | yes | yes | no | no | no |
|
|
|
|
`--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.
|
|
- **`--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. 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 rpmroute 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 rpmroute.route import route
|
|
|
|
ses_text = route(dsn_text, engine="exact", shove=True, edge_clearance_mm=0.5)
|
|
```
|
|
|
|
## Status and honest scope
|
|
|
|
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`:
|
|
|
|
| board | rpmroute exact | FreeRouting JAR | DRC errors from rpmroute |
|
|
|---|---|---|---|
|
|
| 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), rpmroute completed the power stage with no unconnected items.
|
|
|
|
What it is **not**, stated plainly:
|
|
|
|
- **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.
|
|
|
|
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), rpmroute routes legally and says what it could not do.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
uv sync
|
|
uv run pytest -m "not oracle" # the suite; oracle tests need Java + the JAR
|
|
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
|
|
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the port map and
|
|
[`docs/PORTING_PLAN.md`](docs/PORTING_PLAN.md) for the roadmap.
|
|
|
|
## License
|
|
|
|
GPL-3.0-or-later, matching the FreeRouting source it is ported from. No upstream
|
|
Java code is distributed with this package; the reference clone used during
|
|
porting is git-ignored and excluded from the sdist.
|