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

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:

  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.