freeroute/CLAUDE.md
Ryan Malloy fd3bee1085 Add CLAUDE.md: invariants, architecture, and hard-won lessons
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.
2026-07-14 07:28:46 -06:00

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.