fix(router): enforce via hole-to-hole spacing at probe time#672
Conversation
The session's legality oracle only compared copper, and copper is indexed per layer. Two vias whose layer spans do not overlap share no copper element, so `RouteSession::probe` never compared them — while their DRILL barrels, which are layer-independent, could physically collide. The 2026-07-24 CM5 fab campaign found 9 such cross-net pairs on a routed board (worst: -0.024mm overlap, /MIPI0.D3_N vs /USB3-0.DM); they were caught only by a post-hoc scan. Give the session a second index over drilled holes — via barrels and through-hole pad drills — with `probe_hole` / `commit_hole`, mirroring `drc::check_hole_to_hole` exactly: illegal when `dist(centers) - rA - rB < rules.hole_to_hole - 1e-6`, regardless of layer span. Hole ids share the span-id space, so rip-up removes a via's barrel with its copper. Same-net holes stay exempt (coincident same-net drills are the post-route merge case); net ties are not exempt, since a tie is electrical and a drill is mechanical — exactly as the DRC treats them. Wired into every via-placement path: the maze's in-search `via_ok`, the escape/stitch `via_legal`, `validate_and_commit`'s commit gate, and the complete window router's via edges. The hole query is one small R-tree lookup per candidate via; a 40-net CM5 subset routes byte-identically to before at the same wall-clock. The diff-pair repro board's rules were internally inconsistent — a 0.5mm hole-to-hole rule with 0.2mm drills on legs 0.45mm apart makes every pair transition unmanufacturable — so it now declares the 0.2mm rule its own geometry implies, and both transition repros assert the twins' drills keep it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Choji has used up today's reviewsYour team has reached the free plan's limit of 5 reviews per day, so Choji stepped aside on this pull request. That's a plan limit, not a reflection of the change itself. Your free reviews reset tomorrow. To have Choji review every push without waiting, upgrade to Pro for unlimited reviews. You're on the free plan · 5 reviews/day. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Acceptance criterion 2: full CM5 route — zero cross-net via pairsFresh full-board route on this branch, no post-pass: Scan over the 0 violations across 741 vias. The 2026-07-24 fab campaign found 9 such pairs on a comparable route — worst Routability 0.987 is at or above the 0.985 recorded best (#669), so the extra constraint did not cost coverage. The 40-net iteration subset also routes byte-identically to the pre-fix baseline (1292 segments, 84 vias, same single pre-existing plane-stitch failure), which is the wall-clock evidence: the hole query is one small R-tree lookup per candidate via. Also merged current main in — the three unrelated phyz breaks (#673, #675) that were blocking every job before this branch's code could even compile. 🤖 Posted by Claude Code |
main gained its own drill index in #677 (`DrillElement` / `probe_drill` / `commit_drill`), so this branch's parallel `SessionHole` / `probe_hole` / `commit_hole` is dropped and its wiring re-expressed on main's API. What survives is the part #677 left unfinished: - `commit_drill` now returns a `SpanId` and the barrel is tombstoned like any other span, so rip-up-and-reroute takes the hole out with the copper. Without this the router walls itself in — every via it tried and abandoned would keep its ground forever. `probe_drill` skips dead barrels; the index compacts on the same half-tombstone rule as copper. - The check is wired into every via-placement path, not just the complete window router: the maze's in-search `via_ok` (so A* avoids bad sites instead of failing at commit), the escape/stitch `via_legal`, and `validate_and_commit`'s gate. `commit_via` indexes the barrel, which is what makes router-vs-router collisions visible at all — on main `commit_drill` had no production caller, so only pre-existing board holes were ever seen, and the CM5 failure mode (two *router-placed* vias) was still open. - `via_drill_for` / `probe_via_drill` size a candidate from its net class the way `legalize::via_geom_for` does on write-back, so no call site has to plumb drill diameters. Adopts main's net-agnostic semantics: the hole rule ignores nets, which matches `drc::check_hole_to_hole` exactly and is stricter than this branch's earlier same-net exemption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hole
RouteSession::probefilters candidates by layer (e.layer != layer) because copper is indexed per layer. Two vias whose layer spans do not overlap share no copper element, so the oracle never compared them — while their drill barrels, which are layer-independent, could physically collide. Every clearance check passed; the board was unmanufacturable.Evidence from the 2026-07-24 CM5 fab campaign: a routed board carried 9 cross-net via pairs closer than the hole-to-hole rule, worst
-0.024mmoverlap (/MIPI0.D3_Nvs/USB3-0.DM). They were caught only by a post-hoc python scan and fixed by strip-and-reroute.router::legalizedoes have a fail-closedHoleToHoledemote path, but it fixes one violation per round underMAX_FIX_ROUNDS = 4— nine of them outran it.The fix
A second index in the session, over drilled holes rather than copper:
probe_hole/probe_via_hole— mirrorsdrc::check_hole_to_holeexactly: illegal whendist(centers) - rA - rB < rules.hole_to_hole - 1e-6, regardless of layer span. Seeded frompcb.vias(any span) and through-hole pad drills.commit_hole— hole ids share theSpanIdspace, soremoverips a via's barrel out with its copper and rip-up-and-reroute keeps working. Separate tombstone counter and compaction per index.via_drill_for— per-net-class drill diameter, mirroringlegalize::via_geom_for, i.e. how the router sizes a via on write-back. No drill plumbing needed at call sites.Wired into every via-placement path:
maze::via_okauto::escape_endpoint::via_legalauto::validate_and_commitcomplete::edge_okauto::commit_viaTwo deliberate semantic choices, both documented in the code:
legalize::merge_same_net_vias— refusing them at probe time would break at-pad stitching.Pad drills are included, not just vias. The task's evidence was via-vs-via, but it's the same rule and the DRC already flags via-vs-pad — excluding pads would leave router and DRC disagreeing, which is the failure mode being fixed.
Verification
via_refused_when_drills_collide_across_disjoint_layer_spans: a blindFCu..In1Cuvia and a buriedIn2Cu..BCuvia 0.5mm apart on a 4-layer board. Asserts every copper probe passes (that is the hole), the hole probe reports the 0.1mm gap and names the blocker,validate_and_commitrefuses andsession.len()is unchanged, a 1.0mm placement is accepted, andcheck_drcflags the same pair asHoleToHole— so the session and the DRC agree.cargo test -p vcad-ecad-pcb --features gpu— 255 passed, 0 failed.cargo clippy -p vcad-ecad-pcb --features gpu -- -D warnings— clean. (--all-targetshas 2 pre-existingitems-after-test-moduleerrors, present onmainat1e69b5a.)--release --features gpu: output is identical to the pre-fix baseline — 1292 segments, 84 vias, 39 routed / 1 unrouted (the same pre-existing plane-stitch failure onNet-(C240-Pad2)) — with zero cross-net via pairs under the rule either way. No routability or wall-clock regression; the hole query is one small R-tree lookup per candidate via./tmp/out.pcb.jsonon this PR when it lands.Reviewer note: the diff-pair test board changed
router::pair'spair_boarddeclaredhole_to_hole: 0.5withvia_drill: 0.2on legsgap + width = 0.45mmapart. Two 0.2mm drills 0.45mm apart leave a 0.25mm gap — under that rule no pair layer transition on that board is manufacturable, and the two transition repros only passed because the oracle couldn't see it. The board now declares the 0.2mm rule its own geometry implies, and both repros assertassert_twin_holes_clearso the invariant is checked rather than assumed. CM5 itself usesholeToHole: 0.25, so real pair transitions are unaffected.🤖 Generated with Claude Code