Files
rpmroute/CLAUDE.md
T
rsp2k a6aa1991a1 Rename to rpmroute
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.
2026-10-07 14:10:36 -06:00

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.