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.
137 lines
6.9 KiB
Markdown
137 lines
6.9 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code working in the `rpmroute` repository.
|
|
|
|
## What this is
|
|
|
|
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. Published on PyPI as `rpmroute`
|
|
(GPL-3.0-or-later, CalVer).
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
uv sync
|
|
uv run pytest -m "not oracle" # the fast suite (this is what you run)
|
|
uv run pytest -m oracle # differential tests against the real JAR (needs Java)
|
|
uv run ruff check src/ tests/
|
|
uv run rpmroute board.dsn --engine room --pack -do board.ses
|
|
```
|
|
|
|
## The three non-negotiable invariants
|
|
|
|
Break any of these and the change is wrong, regardless of how good the routing
|
|
looks:
|
|
|
|
1. **DRC-clean output.** For every different-net item pair, the exact
|
|
`TileShape` clearance intersection must be empty
|
|
(`ShapeSearchTree.has_violation()` returns `None`). The `exact` and `room`
|
|
engines are clean *by construction*: they drop a net rather than emit a
|
|
violation. A router that packs traces densely and quietly shorts two nets is
|
|
the failure mode this codebase exists to prevent. It has already happened
|
|
once (a multi-pin wire/owner desync in shove recovery reported `drc_clean=True`
|
|
over a real short) and was caught only by an independent adversarial check.
|
|
2. **Exact integer arithmetic in every geometric decision.** No floats in
|
|
clearance or intersection tests. Points, vectors, lines and tiles use Python's
|
|
unbounded `int` and projective rationals, mirroring FreeRouting's use of
|
|
`BigInteger`. `FloatPoint` exists but is quarantined to distances, rounding
|
|
and heuristics. Where an irrational is unavoidable (the `sqrt(2)` in the 45°
|
|
octagon cover), it is bounded to an exact integer **once** at tile
|
|
construction (`math.isqrt`, rounded outward so the cover is a provable
|
|
superset) and never appears in a pairwise test.
|
|
3. **Determinism.** Same input, byte-identical output, every run. Sorted
|
|
iteration, fixed candidate ladders, no RNG. Rip-up and shove are bounded
|
|
(pass limits, rip caps, escalating penalties) so they provably terminate.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
dsn/ Specctra DSN parser (tokenizer -> sexp -> typed DsnBoard)
|
|
ses/ Specctra SES writer (the routed session file)
|
|
geometry/ exact planar geometry: point, vector, line, box, simplex,
|
|
polygon, polyline (trace copper), octagon (45 degree copper)
|
|
board/ BasicBoard, items, nets, clearance, build_board (DSN -> board),
|
|
search_tree (ShapeSearchTree: the exact clearance oracle)
|
|
route/ grid_router | exact_router | room_router, shove, pipeline, cli
|
|
```
|
|
|
|
### The three engines (`--engine`)
|
|
|
|
- **`grid`** (default) is the MVP maze router over a uniform occupancy grid.
|
|
Highest raw connectivity, but grid-quantised, so its clearance is approximate
|
|
and its traces are staircases.
|
|
- **`exact`** routes orthogonally and verifies every trace against exact
|
|
geometry. DRC-clean where it succeeds. Supports `--shove` and `--diagonal`.
|
|
- **`room`** is the continuous expansion-room router: free space is exactly
|
|
decomposed into convex rooms joined by doors, so it routes off-grid channels
|
|
the grid literally cannot see. DRC-clean. Supports `--pack` and `--shove`.
|
|
|
|
The engine/option matrix lives in `rpmroute.cli.ENGINE_OPTIONS` and is
|
|
importable, so consumers can assert against it. **An unsupported combination is
|
|
a hard error (exit 2), never a silent no-op.** Downstream code (`mckicad`) reads
|
|
this matrix to avoid sending an invalid pair.
|
|
|
|
## `reference/` is GPL Java. Never commit it, never ship it.
|
|
|
|
`reference/freerouting` is a clone of the upstream Java source, used only as a
|
|
porting reference. It is gitignored and excluded from the sdist. Study it and
|
|
reimplement; do not paste it. Before any release, confirm the built sdist
|
|
contains zero `.java` / `.jar` / `.class` / `.flex` files and no `reference/`
|
|
directory. Redistributing upstream source inside this package would be a
|
|
licensing problem.
|
|
|
|
## The oracle: validate behaviour, not vibes
|
|
|
|
FreeRouting ships **no unit tests for its geometry or router**, so there are no
|
|
value-level oracles to port against. Instead, `tests/oracle.py` runs the real
|
|
`freerouting.jar` as a differential reference: route the same DSN with both, and
|
|
compare connectivity (`routed_net_set`). Oracle tests are marked
|
|
`@pytest.mark.oracle` and skip cleanly without Java, so the normal suite stays
|
|
JVM-free.
|
|
|
|
When you cannot get an oracle, use **invariants and falsifiable fixtures**:
|
|
- `split_to_convex`: tiles union to the polygon, interiors disjoint, a point is
|
|
inside the polygon iff inside exactly one tile.
|
|
- Routing: endpoints exactly on pads, traces on valid layers, inside the
|
|
outline, no different-net crossing, vias at real layer transitions.
|
|
|
|
## Hard-won lessons (do not relearn these)
|
|
|
|
- **KiCad emits net names like `/*52`.** Specctra's `SpecCharASCII` includes `/`
|
|
and `*`, so an unclosed `/*` is a *name*, not an unterminated block comment.
|
|
The tokenizer must fall through to a token when no closing `*/` exists.
|
|
- **Exact clearance is load-bearing, not decoration.** A 45° trace whose
|
|
*bounding box* hits an obstacle can still be legal, because its true octagon
|
|
copper misses. Coarse box checks reject legal routes. See
|
|
`test_exact_clearance_is_load_bearing`.
|
|
- **Shove is not a densifier.** It relocates a blocker into space that already
|
|
exists; it cannot compress copper. Proven with an order-independent fixture
|
|
(`true_density.dsn`) plus a parameter sweep: shove flips from "recovers" to
|
|
"powerless" exactly when detour headroom runs out. The real densifier is
|
|
**lane/gate packing** (`--pack`).
|
|
- **A density claim needs a falsifiable fixture.** "Plain routing drops a net"
|
|
is worthless if the drop is an *ordering artifact*. The bar: the drop must
|
|
hold under **every** net ordering (enumerate the permutations), and the
|
|
pack/fail boundary must sit at the true geometric feasibility width. This bar
|
|
killed one over-claim (`rooms_shove_channel`) and validated a real win
|
|
(`channel_pack.dsn`).
|
|
- **KiCad's `LoadBoard` is headless-safe; `ImportSpecctraSES` is not.** Use
|
|
`LoadBoard` to verify KiCad accepts routed output without needing a display.
|
|
|
|
## Honest scope
|
|
|
|
Alpha. It routes real KiCad boards, multi-layer, with vias, rip-up, shove,
|
|
channel packing, 45° shortening, and DRC-clean output. It is **not** at
|
|
FreeRouting/JAR density parity on dense commercial boards: the maze search is
|
|
orthogonal (45° is a recovery pass, not diagonal-native search), there is no
|
|
fanout pass and no post-route optimiser, and on a crowded board it will complete
|
|
fewer nets than the JAR because it drops rather than violates. Say this plainly;
|
|
do not oversell it in code comments, the README, or release notes.
|
|
|
|
## Conventions
|
|
|
|
`uv` for everything, src-layout, ruff, pytest. Small single-purpose modules.
|
|
Commit in logical chunks with professional messages and no AI attribution. Cite
|
|
the upstream Java file in a module docstring when porting one.
|