feat(router): synthesize copper pours for high-current nets#679
Merged
Conversation
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. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ecto
force-pushed
the
claude/loving-solomon-7300a6
branch
from
July 25, 2026 12:56
327544c to
a4f3715
Compare
The router synthesized traces only. copper_pour::fill_zones could fill a
zone that already existed, but nothing ever created a zone *definition* —
so a board whose power nets carry tens of amps got hairline copper where a
human designer floods a region. The CM5 fixture hid this behind 121
hand-authored zones; the moteus fixture (Eagle, whose polygons the importer
drops) exposes it.
New `vcad_ecad_pcb::pour_synth`, split so the judgement is inspectable
separately from the drawing:
* Policy (`PourPolicy` / `decide_pours`) — which nets, which layers, and
why. A declared current is sized through the *same* IPC-2221 closed form
the `size_trace_for_current` MCP tool evaluates; a wide net class and a
power-rail name are the fallbacks for imported boards, which carry no
current annotation. Pad count *or* pad copper qualifies a net, since a
battery input is two big terminals rather than twenty small ones. A layer
is chosen to minimise stitching: pour where the net already lives, and
never flood a layer fewer than half its pads sit on.
* Geometry (`synthesize_outlines`) — region floods, promoted to a whole
layer once a net spans the board, clipped to the outline eroded by edge
clearance. A whole-layer flood owns its layer; local pours share one,
smallest-claims-first. One plane per candidate, and if any pad cannot
reach it the candidate is dropped and that net keeps its traces: the
router treats a poured net as connected *through* its plane, so a pour
that missed a pad would silently strand it.
Zones come back in `RouteAllResult::zones` and every caller (cm5_bench, the
WASM binding, `route_nets`) applies them — the routing assumes them.
Three legality fixes the pours surfaced, each of which stands on its own:
* Escape/stitch vias are now checked against the board's drilled holes when
they are placed, instead of being committed and cleaned up afterwards.
* Post-route legalization demoted a whole net on one bad drill. That is
right for a routed path and catastrophic for a plane-stitched net, whose
copper is dozens of independent pad->plane vias: on moteus a single hole
conflict cost all 32 GND stitches and left the pour connected to nothing.
A stitch via is now dropped on its own, with its dog-bone stub.
* `fill_zones` now prunes orphan islands — fill fragments that reach none of
their own net's copper. Floating metal carries no current and is a
`NetIslands` error; the largest piece is always kept so a zone poured
before its net is routed still renders.
`is_power_net` folds away word separators, so `V_SUPPLY` reads as the rail
it is.
Measured on this base (VCAD_POUR_SYNTH=0 is the A/B control):
moteus routability 0.872 -> 0.891, vias 256 -> 140 (0.73x -> 0.40x
human), copper 1.19x -> 0.78x human, Clearance 100 -> 76, Short
177 -> 121, SameNetBypass 3 -> 0, NetIslands 0 -> 0,
UnstitchedPad 0 -> 0; 445 -> 359 violations overall. GND floods
F.Cu as one plane, reached by 3 stitching vias.
VESC bit-identical with synthesis on and off (0.794, 223 violations).
Its 13 hand-authored zones already cover ~80% of both layers, so
the geometry correctly drops the one candidate the policy picks.
CM5 (top 150 nets) flat: routability 0.982 -> 0.981, 2912 -> 2927
violations. Synthesis adds one 49mm^2 zone for a rail that owned
none and duplicates nothing.
On moteus the pour's own copper is provably legal: the Clearance count is
identical with and without the zone (76 both), so it touches no foreign
copper. It does raise Short against the same routed board with zones
stripped (97 -> 121) — that is the plane unifying GND's copper, so nets
already shorted to GND get reported pairwise. Against the real control the
net effect is still strongly positive (177 -> 121).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ecto
force-pushed
the
claude/loving-solomon-7300a6
branch
from
July 25, 2026 14:04
a4f3715 to
8ddf5db
Compare
phyz 0.3 deprecated `contact_forces_implicit`, which turned `cargo clippy -- -D warnings` into a hard error and took main (and every open PR) red. The deprecation points at the convex solve (`assemble` + `solve_contacts`), which couples contacts through the Delassus operator instead of resolving each in isolation. That is a different contact model, not a rename: adopting it would move every drift number `fit_physics` measures and silently re-grade the mecheval F-suite, whose task tolerances were baselined against the penalty path. So this pins the existing behaviour with `#[allow(deprecated)]` and records why, rather than swapping solvers inside a CI fix. Migrating is worth doing on its own, with the F-suite re-baselined against the new numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`a_wandering_loop_never_returns_a_worse_board_than_it_found` has been failing on main since the fix loop learned to restore the best board it saw. The test's contract is its name: however badly the loop wanders, the board it hands back must never be worse than the best one it found. The assertion encoded one particular way of honouring that — regress, then name the regression — and required `connectivity.regression() > 0`. With the restore in place the loop no longer regresses at all (0 on arrival, 0 on completion) and fails closed with "the fix loop stopped improving ... the best board the loop saw was restored". That satisfies the contract more strongly than the assertion it trips. So the assertion now allows either shape — no regression, or a regression that is named — and still forbids the one thing that matters: clearing violations by deleting copper and reporting it as progress. The added `blocker.is_some()` check keeps a silent pass from slipping through the weaker predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
vcad's router synthesizes traces only.
copper_pour::fill_zonescan fill a zone that already exists, but nothing ever created a zone definition — so a board whose power nets carry tens of amps got hairline copper where a human designer floods a region.The CM5 benchmark hid this: its fixture carries 121 hand-authored zones. The motor-controller fixtures expose it —
moteus.brdis Eagle, whose copper polygons the importer drops entirely, so its power nets arrived with no pour at all.What this adds
New crate module
vcad_ecad_pcb::pour_synth, deliberately split so the judgement is inspectable separately from the drawing — per the brief's design note, so it can later be driven by explicit design intent rather than heuristics.Policy —
PourPolicy+decide_pours: which nets, which layers, and why (PourReason). No geometry, no board mutation, every knob a number you can argue with.net_current_a) is sized through the same IPC-2221 closed formsize_trace_for_currentevaluates —ipc2221_width_mmis unit-tested against that tool's own numbers to 1e-9, so "how wide for this current?" means the same thing to the router as to an agent asking the tool.Geometry —
synthesize_outlines: region floods (a margined box), promoted to a whole-layer flood once a net spans the board, clipped to the outline eroded by edge clearance. Erosion and dilation reuse the pour engine's own capsule primitive rather than adding a second offsetting path.Stitching, DRC and fill are all existing machinery: a synthesized zone is an ordinary
Zone. Zones come back inRouteAllResult::zones, and every caller applies them (cm5_bench, the WASM binding,route_nets) — the routing assumes them, so dropping them would leave those nets connected to nothing.cargo run -p vcad-ecad-pcb --example pour_report -- <board>shows the decisions and the resulting outlines without routing.Three legality fixes the pours surfaced
Each stands on its own, independent of synthesis:
UnstitchedPad.fill_zonesprunes orphan islands — fill fragments reaching none of their own net's copper. Floating metal carries no current and is aNetIslandserror; every EDA tool drops it before plotting. The largest piece is always kept, so a zone poured before its net is routed still renders.Plus:
is_power_netfolds away word separators, soV_SUPPLYreads as the rail it is.Measured
VCAD_POUR_SYNTH=0oncm5_benchis the A/B control. Re-measured after rebasing onto #676/#677/#678/#680/#681..scratch/moteus.brd)GND floods F.Cu as a single plane — 40 of its 45 pads flood directly, 3 stitching vias reach the rest. Fewer vias, less copper, better routability, fewer violations in every category.
VESC (
.scratch/VESC_4.kicad_pcb): bit-identical with synthesis on and off (0.794, 223 violations). Its 13 hand-authored zones already cover ~80% of both copper layers, so the policy's one candidate (VCCon F.Cu) is correctly dropped by the geometry — what is left of the layer is not a plane every pad can reach.CM5 (
.scratch/CM5RevEng.kicad_pcb, top 150 nets): flat — routability 0.982 -> 0.981, 2912 -> 2927 violations (+0.5%, within this router's run-to-run variation; the delta is almost entirelyMinTraceWidth254 -> 270, which a zone cannot cause directly). Synthesis adds exactly one 49 mm2 zone, for a rail that owned none, and duplicates nothing.The one number that goes up, honestly
On moteus the pour's own copper is provably legal: the
Clearancecount is identical with and without the zone (76 both), so it touches no foreign copper — and the DRC models a plane as never bonding to other-net copper. Against the same routed board with zones stripped,Shortdoes rise 97 -> 121: that is the plane unifying GND's copper, so nets already shorted to GND get reported pairwise rather than individually. Against the real control the net effect is still strongly positive (177 -> 121).Neither fixture is DRC-clean at import — VESC arrives with 72 shorts and 33 clearance violations before any routing — so absolute cleanliness was never on the table; the bar is no new violations attributable to the pours, which holds.
Notes
VESC_4.kicad_pcbimports as a 2-layer board: its inner layers are namedGND/GND2, andparse_layeronly knowsIn1.Cu…In8.Cu, so both the layers and their zones are dropped. Left alone here — fixing it would change the fixture's routing problem entirely — but it is a real importer gap.min_pads: 6andmin_pad_area_mm2: 20.0keep synthesis off nets that a trace already serves, and all 270 existing crate tests pass unchanged.Verification
cargo test -p vcad-ecad-pcb— 282 passed (18 new, covering ampacity vs. the shared model, each policy signal, outline synthesis, layer arbitration, orphan pruning, and surgical stitch removal)cargo clippy -p vcad-ecad-pcb --features gpu -- -D warnings— cleancargo fmt --all --check— cleannpm test -w @vcad/mcp— 253 ecad tests pass;tsc --noEmitclean🤖 Generated with Claude Code