Skip to content

feat(router): prune dangling copper automatically at the end of route_all#676

Merged
ecto merged 2 commits into
mainfrom
feat/router-auto-prune-dangling
Jul 25, 2026
Merged

feat(router): prune dangling copper automatically at the end of route_all#676
ecto merged 2 commits into
mainfrom
feat/router-auto-prune-dangling

Conversation

@ecto

@ecto ecto commented Jul 25, 2026

Copy link
Copy Markdown
Owner

vcad_ecad_pcb::drc::prune_dangling_copper removes traces and vias whose galvanic island touches no pad and no pour fragment — debris left behind by rip-up/restore. Until now it only ran when invoked by hand via the prune_copper example. It now runs as a post-pass inside route_all, so a fresh route never emits dead copper.

What changed

  • drc.rs — split dangling_copper_mask out of prune_dangling_copper, so a caller owning only part of a board's copper can judge its own pieces without touching copper it did not place. Behavior of the public pruner is unchanged.
  • router/legalize.rs — new prune_dangling, running the mask over the candidate board (existing copper + router output) and dropping only the new dead traces/vias. Reuses candidate_pcb, the same subject legalize already judges.
  • router/auto.rs — called at the end of route_all_with_opts, after the final negotiation/rip-up phases and after legalization, with a log::info! naming what it removed. New RouteOptions::prune_dangling_copper (default true).

Interaction with pending and the session — audited

  • Islands are maximal connected components, so they are disjoint. Deleting a whole unanchored island cannot disconnect copper that stays, and the single pass is already the fixpoint.
  • Every connection the router counts as placed runs pad to pad, so its copper lives in a pad-anchored island. still_unrouted (and the routability, unrouted_nets and diagnostics derived from it) is computed before the prune and is unaffected by it.
  • Only new copper is dropped. Board copper — including hand-placed traces — is judged (it anchors islands) but never removed. That also neutralizes the connectivity model's arc blind spot: it does not model trace_arcs, but the router emits none and never deletes the board's.
  • The RouteSession does not escape route_all_with_opts and is not consulted after this point, so there is no session state left to keep in step with the pruned copper.
  • The invariant is enforced, not just argued: if the prune empties a net out entirely, that net is reported unrouted with a diagnostic rather than claimed as routed with no copper behind it.

Measurements — CM5 40-net subset

VCAD_GPU_BATCH=1 VCAD_GPU_NEGOTIATE=1 cm5_bench .scratch/CM5RevEng.kicad_pcb 1 40

segments vias routability routed/unrouted nets copper only, no pads
before 1292 84 1.000 39 / 1 24
after 789 84 1.000 39 / 1 13

Routability does not regress (1.000 either way, identical net split); 503 dead segments removed. Prune cost ~0.4s.

The 13 remaining islands are pour fragments, not routed copper

Acceptance asked for grep -c "copper only, no pads" → 0. That count cannot reach zero by pruning traces and vias, because it also counts zone-fill fragments, which are design intent and not the router's to delete. Evidence:

  • The bare unrouted board (same fixture, traces/vias stripped, zones intact) already reports 12 of the 13. They are pours whose fill is split from their own pads, e.g. Net-(U5-LX1), which has zero traces and zero vias on the routed board.
  • The 13th, Net-(C240-Pad2), persists when that net's router copper is deleted from the finished board (6 islands, the pad-less one still there) — it is a pour fragment carved off by a foreign-net trace's anti-pad, not router copper.
  • Diffing the island net-lists: 12 router-attributable island nets in the baseline → 0 with the prune (the one remaining name is the pour fragment above).
  • Running prune_copper on the freshly routed board removes 0 traces, 0 vias — the router's output is at the prune fixpoint.

Follow-up worth its own change: orphaned zone-fill fragments (KiCad's per-zone island-removal mode) are the remaining source of NetIslands noise.

Checks

  • cargo test -p vcad-ecad-pcb --features gpu — 256 passed, 0 failed (4 new tests: dangling island pruned / live copper kept, board copper never pruned, via-chained copper kept, and an end-to-end route_all invariant test asserting its output is at the prune fixpoint and every net it reports routed owns copper).
  • cargo clippy -p vcad-ecad-pcb --features gpu -- -D warnings — clean.
  • cargo fmt --all --check — clean.
  • Full-board run (1 999999) is in progress; results will be posted here.

🤖 Generated with Claude Code

…_all

The autorouter's speculative emitters (fan-out escape vias, stubs whose
route rip-up later replaced, stitching vias for a plane that never
materialized) left copper stranded in islands touching no pad and no
pour — DRC's NetIslands "copper only, no pads". Removing it required
running the standalone prune_copper example by hand.

route_all now runs the prune itself, after legalization, so the copper it
returns is final and never dead. Opt out via
RouteOptions::prune_dangling_copper (default on).

Soundness: islands are maximal connected components, so dropping a whole
unanchored island cannot disconnect copper that stays, and one pass is
already the fixpoint. Every connection the router counts as placed runs
pad to pad and therefore lives in a pad-anchored island — routability,
the routed/unrouted split, and the pending diagnostics are computed
before the prune and mean exactly what they meant. Only new copper is
dropped; board copper is judged (it anchors islands) but never removed.
The session is not consulted after this point and never escapes the
function, so there is no session state left to keep in step. The
invariant is enforced rather than only argued: a net the prune empties
out is reported unrouted with a diagnostic instead of being claimed as
routed with no copper behind it.

CM5 40-net subset (VCAD_GPU_BATCH=1 VCAD_GPU_NEGOTIATE=1, effort 1):

  before: 1292 segments, 84 vias, routability 1.000, 24 island violations
  after:   789 segments, 84 vias, routability 1.000, 13 island violations

All 13 remaining islands are zone-pour fragments, not routed copper: 12
are present on the bare unrouted board, and the 13th (Net-(C240-Pad2))
survives deletion of that net's router copper. Router-attributable
copper-only islands go 12 -> 0, and re-running prune_copper on the
finished board removes nothing. Prune cost: ~0.4s on the subset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chojiai

chojiai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Choji has used up today's reviews

Your 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.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Jul 25, 2026 1:05pm
vcad Ignored Ignored Jul 25, 2026 1:05pm
vcad-docs Ignored Ignored Jul 25, 2026 1:05pm
vcad-mcp Ignored Ignored Jul 25, 2026 1:05pm

Request Review

@ecto

ecto commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Full-board CM5 run — confirmed at scale

VCAD_GPU_BATCH=1 VCAD_GPU_NEGOTIATE=1 cm5_bench .scratch/CM5RevEng.kicad_pcb 1 999999, 6809s:

vcad:  3661 segments, 693 vias, 4171 mm copper, 342 routed / 66 unrouted nets
score: routability 0.985, via ratio 0.24x human, length ratio 0.54x human

Routability 0.985 — matches the CM5 record set in #669, no regression at scale.

Zero dangling copper in the output

prune_copper re-run on the finished board: 0 dangling traces, 0 dangling vias. The route is at the prune fixpoint.

DRC reports 7 copper only, no pads islands, and every one is zone-pour fill, not routed copper:

net verdict
Net-(U5-LX5), Net-(U5-VC_LX1), Net-(U5-VC_LX3) present on the bare unrouted board (these nets have no traces and no vias at all)
/ETH.1_N, /GPIO5, /PCIe.CLK_nREQ, Net-(C240-Pad2) pour fragments carved off by foreign-net anti-pads — routing new copper voids other nets' pours and can split a fill

Proof for the second row, which is the interesting one: re-running the prune with the anchor rule tightened from "pad or pour" to pad only still removes 0 traces and 0 vias. Those islands contain no router copper of any kind under either rule — they are fill polygons. Deleting the four nets' router copper makes the islands vanish only because it removes the anti-pads that were splitting the pours (verified: doing so also re-merges Net-(C240-Pad2) from 6 pad islands back down).

The bare unrouted board reports 12 such islands; the finished route reports 7 — routing reduced pour fragmentation on net.

Where the acceptance criterion landed

grep -c "copper only, no pads" → 7, not 0, and it cannot reach 0 by pruning traces and vias: that DRC message also counts zone-fill fragments, which are design intent. The equivalent check that does pass is the prune fixpoint above (0 traces / 0 vias removable from the finished board), on both the 40-net subset and the full board.

Follow-up worth its own change: orphaned pour fragments — KiCad models this as a per-zone island-removal mode, and fill_zones has no equivalent today.

Picks up the phyz sibling-crate version bump to 0.3 (#675), without which
CI cannot resolve phyz-md against phyz's main tip.
@ecto
ecto merged commit 3236258 into main Jul 25, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant