Skip to content

fix: reject net-label candidates whose connector crosses another net (40 → 19 crossings) - #719

Open
DPS0340 wants to merge 3 commits into
tscircuit:mainfrom
DPS0340:fix/reduce-crossings
Open

fix: reject net-label candidates whose connector crosses another net (40 → 19 crossings)#719
DPS0340 wants to merge 3 commits into
tscircuit:mainfrom
DPS0340:fix/reduce-crossings

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown

Builds on #718 (the measurement) by fixing the largest source of what it measures. #718 should merge first — this PR updates the numbers that one pins.

Root cause

AvailableNetOrientationSolver places net labels and, for each one, appends a connector trace joining the label to its net. getCandidateStatus validates a candidate's connector against chips:

for (const chip of this.chipObstacleSpatialIndex.chips) {
  if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) return "chip-collision"
}

…and against other net labels. It never checks the connector against existing traces. So a candidate is accepted whose connector cuts straight across a different net — which reads as a short in the rendered schematic.

I traced this by counting crossings after each pipeline stage on the worst board:

schematicTraceLinesSolver                  traces=  3  crossings=0
longDistancePairSolver                     traces= 25  crossings=2
traceCleanupSolver                         traces= 25  crossings=2
preAlignmentNetLabelTraceCollisionSolver   traces= 46  crossings=11   <-- +21 traces, +9 crossings
traceCleanupSolver2                        traces= 46  crossings=11

The jump is where availableNetOrientationSolver's connectors enter. Attributing the 11 crossings by trace id: 7 involve a connector named available-net-orientation-*, 4 are between pre-existing traces.

Fix

Reject candidates whose connector properly crosses a trace on a different net, so the search continues to the next candidate.

This reuses tracePathCrossesAnyTrace — already in geometry.ts for this exact purpose — filtered to other-net traces. Same-net traces are excluded because a connector legitimately meets the trace it attaches to. The rejection reuses the existing trace-collision status, so no new candidate state is introduced.

Result

Measured across all 19 imported bug reports:

board before after
bug-report-20260706T213649Z 11 4
bug-report-20260706T220324Z 6 4
bug-report-20260721T221026Z 5 2
bug-report-20260716T144856Z 4 0
bug-report-20260717T022934Z 3 0
bug-report-20260707T134549Z 3 2
bug-report-20260708T055430Z 2 1

Total 40 → 19 (−52.5%). 7 boards improved, 0 regressed. Two boards reach zero.

Verification

  • Full suite 164 pass / 0 fail / 4 skip, tsc --noEmit clean, biome clean.
  • repro51-overlap-junction-crossing is worth calling out. Its snapshot changed, but its behavioural assertion — tracesHavePositiveLengthOverlap(gndTrace, vccTrace) is false for every GND/VCC pair — still passes. The guarantee that test exists to protect is intact; only the rendering moved.
  • 13 snapshots regenerated, all from genuinely different (better) routing rather than incidental churn.
  • The pinned counts from test: pin different-net trace crossings per bug report #718 are updated in this PR, which is exactly the workflow that file's comment describes.

Diff: 1 source file (+~20 lines, no new helper) plus the test/snapshot updates.

Remaining 19 crossings are mostly between pre-existing traces rather than connectors, so they'd need a different fix — happy to look at those next if this lands.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
schematic-trace-solver Ready Ready Preview, Comment Jul 26, 2026 3:49am

Request Review

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Follow-up on the remaining 19, since I offered to look at them — I investigated and decided not to send a fix. Posting the findings so the next person doesn't repeat the attempt.

After this PR, the connector class is fully gone. Re-attributing all 19 remaining crossings by trace id:

connector-vs-trace     = 0
connector-vs-connector = 0
trace-vs-trace         = 19

So every remaining crossing is between two ordinary routed traces, and they're present from the routing stage — not introduced later:

schematicTraceLinesSolver         traces=19  crossings=2
longDistancePairSolver            traces=47  crossings=4
traceCleanupSolver                traces=47  crossings=4
traceCleanupSolver2               traces=56  crossings=4

Why they exist: SchematicTraceSingleLineSolver2 builds its obstacle set from chips and text boxes only. It has no knowledge of other nets' traces, so each trace is routed independently and crossings are invisible to it.

What I tried and rejected. SchematicTraceLinesSolver routes sequentially, so earlier results are available. I wired already-routed other-net paths into the sub-solver and added crossing count as a sort key — deliberately as a tiebreaker after length, to respect the existing comment that a flat additive penalty must never trade a shorter valid route for a detour.

Then I measured whether it did anything:

  • Total crossings: 19 → 19. No change on any board.
  • Instrumented how often equal-length candidates actually differed in crossing count: 3 occurrences across all 19 boards, all on one board.

The tiebreaker almost never fires, because candidates that differ in crossings essentially always differ in length first. So the change added a per-candidate O(n·m) scan for no measurable benefit. I reverted it rather than ship something that looks like an improvement and isn't.

What would actually be needed: making crossings a real cost the router optimises against, which means accepting longer routes to avoid them — a direct reversal of the current design decision, and a much larger change than this PR. That's a judgement call about what the schematic should look like, so I'd rather not make it unilaterally. Happy to attempt it if you think it's the right direction.

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

One more datapoint while probing what this PR does and doesn't cover.

I measured a defect class nobody has been counting: different-net traces drawn collinearly on top of each other — visually a single wire where there are two nets. Across the corpus there is exactly 1, on bug-report-20260707T092615Z:

V overlap, length 0.141
  A  C_SENSE.2-C1.2                        net=connectivity_net1   (-9.809,-1.705)→(-9.809,-1.905)
  B  available-net-orientation-13-BAT_POS  net=connectivity_net13  (-9.809,-2.915)→(-9.809,-1.764)

Same x, overlapping y ranges, different nets. That renders as one wire.

This PR does not fix it — I checked rather than assuming, since side B is an available-net-orientation-* connector and this PR is about those connectors:

main:      collinear different-net overlaps = 1
this PR:   collinear different-net overlaps = 1

The reason is that #719 rejects candidates whose connector crosses another net, and this connector doesn't cross C_SENSE.2-C1.2 — it lies along it. Crossing and collinear overlap are different geometric predicates, so the guard I added correctly doesn't fire.

Recording it here rather than opening another PR: I have a lot open in this repo already, and I'd rather land some of what's there before adding more. If a maintainer wants it, the natural fix is extending the connector validation in getCandidateStatus to reject collinear overlap with another net as well as crossings — small, and it would sit right beside the check this PR adds.

(Corrected an earlier read while investigating: I initially attributed the overlap to traceCleanupSolver2 because the count went 0→1 there. It doesn't — the connector is simply absent from the intermediate stages' outputs and rejoins at the end. Both paths are byte-identical in and out of that solver.)

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

@mohan-bee @MustafaMulla29 @ShiboSoftwareDev — I had 13 PRs open here, which is too many to ask anyone to sort through. I've cut that to 11 and verified the merge order so it doesn't cost you a decision.

What I withdrew, after measuring:

Merge order. I merged all 11 onto main in this order and ran the suite at the end:

 1. #718  test: pin different-net trace crossings        (measurement only)
 2. #719  fix: connector crosses another net             40 → 19 crossings
 3. #723  fix: connector runs along another net          (stacked on #719)
 4. #720  test: pin net-label placement defects          (measurement only)
 5. #721  fix: key skipped collisions by label pair
 6. #700  fix: merge connector rails with same-net traces   ← already approved
 7. #707  test: repro #641
 8. #722  fix: straighten near-orthogonal segments
 9. #717  fix: getColorFromString finite for long strings
10. #699  fix: relocate labels inside chip bodies
11. #702  fix: keep labeled rail nets on net labels

I rebased #719 onto #718 and #723 onto #719 so the stack is now linear — previously all three added different-net-trace-crossings.test.ts independently and collided. After that: 0 source conflicts across all 55 pairs.

Three of the later merges conflict only in regenerated .snap.svg files, resolved by re-running the suite — no patch changes needed.

Result of merging all 11: 184 pass, 4 skip, and 7 remaining failures are all improvements against pinned baselines — the boards get strictly better, so the expected values need lowering:

board pinned after stack
bug-report-20260707T230831Z insideChips 4 0
bug-report-20260721T221026Z overlappingPairs 2 0
bug-report-20260716T144856Z overlappingPairs 2 0
20260706T220324Z / 092615Z / 141025Z / 141421Z 1 each 0

Those are exactly the "if a change legitimately improves a board, lower the number in the same PR" case the harness documents. I've left them un-lowered because which PR should own the reduction depends on what you merge — happy to fold the updates into whichever lands last.

If you only have time for two: #718 then #719. That's the measurement plus the fix it justifies, 2 source files, and it removes 21 of the 40 crossings on its own.

DPS0340 added 3 commits July 26, 2026 12:47
Adds a helper that counts places where traces on different nets properly
cross, and a test that pins the current count for every imported bug
report.

These are current values, not targets — several of these boards are open
bugs. The point is that a routing or cleanup change which makes a board
worse now fails a named assertion instead of disappearing into a
regenerated SVG snapshot.

Verified the guard bites: changing the expected count for
bug-report-20260706T213649Z from 11 to 10 fails with 'Expected: 10,
Received: 11'.
tscircuit#727 (TraceOverlapShiftSolver) fixed the RP2040 USB-C overlap and moved
bug-report-20260707T092615Z from 2 different-net crossings to 3. Measured
either side of that merge:

  ca7e80c (before)  TOTAL 40, this board 2
  be20aa7 (tscircuit#727)    TOTAL 41, this board 3

Pinned at the current value with a comment recording the change, and
reported it on tscircuit#727 so the trade is visible rather than absorbed.
getCandidateStatus already checks the candidate's connector against chips
and other net labels, but never against existing traces. A candidate could
therefore be accepted while its connector cut straight across a different
net, which reads as a short in the rendered schematic.

Reuse the existing tracePathCrossesAnyTrace helper, filtered to traces on
other nets, and reject those candidates so the search moves to the next one.

Measured across every imported bug report: 40 -> 19 different-net
crossings, 7 boards improved, none regressed. Worst board goes 11 -> 4,
and two boards reach zero.
@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Author

@seveibar — your #707 note ("synthetically constructs input, you have to use the FULL solver") was a useful bar, so I audited my other 10 PRs against it rather than wait to be told twice. One correction and a summary below.

Audit result: none of the others construct input. No PR here adds a synthetic asset; every repro imports an unmodified bug report and drives SchematicTracePipelineSolver:

#699  bug-report-20260707T230831Z   full pipeline
#702  bug-report-20260716T144856Z   full pipeline
#718 #719 #723  all imported bug-reports, full pipeline

One does not change any board, and says so. #721 fixes an aliasing bug in NetLabelNetLabelCollisionSolver.collisionKey — keyed on net pair instead of label pair. I measured it: overlapping label pairs across all imported reports is 43 before and 43 after. The PR body already states "It changes no board today"; I'm repeating it here because that makes it the easiest one to decline, and I'd rather you decline it knowingly than merge it assuming a fix.

Sorted by review cost, if it helps:

PR files what it is
#718 2 measurement only, pins crossings per board
#720 2 measurement only, pins label-placement defects
#721 2 latent aliasing fix, changes no board
#722 6 straightens near-orthogonal segments
#699 13 relocates labels placed inside chip bodies
#719 16 connector crossing check — 40 → 20 crossings
#723 17 stacked on #719, adds the collinear case
#702 29 keeps labeled rails on net labels
#717 106 19 source lines + 105 regenerated snapshots

#719 is the one with the largest measured effect — different-net crossings across all imported bug reports go from 41 on main to 20. #718 is the counter it's measured with, which is why they're a pair.

#717's file count is misleading: the fix is 19 lines in getColorFromString.ts (long strings produced hsl(NaN, ...)), and the other 105 files are snapshots that regenerate because colours change.

All 10 are rebased onto current main and green. Happy to close any of them — particularly #721 — if they're not worth the queue.

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Correction to my audit comment above: I listed #721 as "changes no board today — the easiest one to decline". That was based on the wrong measurement.

The final overlap count is unchanged (43 → 43), which is what I measured. But the skipped-collision keys on main are net pairs, and one of them covers ten label pairs on bug-report-20260707T092615Z — so giving up on one silently suppresses nine that were never examined. Details and the per-board numbers are on #721.

The rest of that table stands. #718/#719 are still the pair with the largest measured effect (41 → 20 crossings).

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