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.
6.9 KiB
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 engine. Specctra DSN
in, Specctra SES out, no JVM anywhere. Published on PyPI as freeroute
(GPL-3.0-or-later, CalVer).
Commands
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:
- DRC-clean output. For every different-net item pair, the exact
TileShapeclearance intersection must be empty (ShapeSearchTree.has_violation()returnsNone). Theexactandroomengines 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 reporteddrc_clean=Trueover a real short) and was caught only by an independent adversarial check. - Exact integer arithmetic in every geometric decision. No floats in
clearance or intersection tests. Points, vectors, lines and tiles use Python's
unbounded
intand projective rationals, mirroring FreeRouting's use ofBigInteger.FloatPointexists but is quarantined to distances, rounding and heuristics. Where an irrational is unavoidable (thesqrt(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. - 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.exactroutes orthogonally and verifies every trace against exact geometry. DRC-clean where it succeeds. Supports--shoveand--diagonal.roomis 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--packand--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'sSpecCharASCIIincludes/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
LoadBoardis headless-safe;ImportSpecctraSESis not. UseLoadBoardto 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.