diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c157739 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# 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.