Captures the three non-negotiable invariants (DRC-clean output, exact integer geometry, determinism), the engine matrix, the reference/ GPL rule, the JAR oracle strategy, and the lessons that cost real debugging: KiCad's /*52 net names, exact clearance being load-bearing, shove not being a densifier, and the falsifiable-fixture bar for any density claim. Force-added past the global ignore: this is a private repo and the file is excluded from the sdist, so it never reaches PyPI.
137 lines
6.9 KiB
Markdown
137 lines
6.9 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code working in the `freeroute` 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 `freeroute`
|
|
(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 freeroute 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 `freeroute.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.
|