Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38054a1
Include via-to-pad errors in repair candidate safety checks
imrishabh18 Sep 4, 2026
1652fc9
Preserve rotated pad geometry in DRC evaluation
imrishabh18 Sep 4, 2026
07e4564
Preserve quantitative repair scores when via-pad errors coexist
imrishabh18 Sep 4, 2026
b78282d
Add before-and-after pad geometry regression snapshots
imrishabh18 Sep 4, 2026
00aa660
Demonstrate unsafe via rejection with real repair snapshots
imrishabh18 Sep 4, 2026
e6487d0
Make rotated-pad visual snapshots portable across runtimes
imrishabh18 Sep 4, 2026
43c700e
Apply CI formatting to snapshot angle serialization
imrishabh18 Sep 4, 2026
d3c92e9
Complete safe layer-change candidates and reuse identical DRC evaluat…
imrishabh18 Sep 4, 2026
d047208
Compare cached DRC geometry directly without serialized keys
imrishabh18 Sep 4, 2026
c682dfd
Align new repair regressions with CI formatting
imrishabh18 Sep 4, 2026
bee41e4
Score completed layer moves and preserve via attachment provenance
imrishabh18 Sep 4, 2026
b9b38db
Match CI formatting for via provenance regressions
imrishabh18 Sep 4, 2026
137497d
Construct pad-clear layer transitions and distinct repair candidates
imrishabh18 Sep 4, 2026
c1d564a
Match CI layout for construction regression tests
imrishabh18 Sep 4, 2026
594622e
Preserve physical via topology in layer repair candidates
imrishabh18 Sep 4, 2026
2e9c510
Match CI layout for via topology regressions
imrishabh18 Sep 4, 2026
7badad2
Replace explanatory tests with real SRJ repair snapshots
imrishabh18 Sep 5, 2026
eccb978
Compare SRJ routing snapshots with the SVG image matcher
imrishabh18 Sep 5, 2026
735853d
Capture platform routing snapshots in test CI
imrishabh18 Sep 5, 2026
b0d851a
Focus sample 9 regression on its captured layer repair stage
imrishabh18 Sep 5, 2026
72290c3
Record the verified Linux sample 9 routing snapshot
imrishabh18 Sep 5, 2026
2534a3d
Stack pad geometry fix on the sample 9 snapshot baseline
imrishabh18 Sep 5, 2026
ce14e8b
Keep pad repair focused on geometry and via construction
imrishabh18 Sep 5, 2026
ce1ff3f
Reuse math-utils for pad clearance geometry
imrishabh18 Sep 5, 2026
d1aa2d7
Represent obstacle geometry with standard coordinate transforms
imrishabh18 Sep 5, 2026
9aec8e6
Preserve upstream staged repair and via span behavior
imrishabh18 Sep 5, 2026
9190c33
Merge branch 'codex/srj18-sample9-snapshot-baseline' into codex/simpl…
imrishabh18 Sep 5, 2026
d2c7dae
Use pad transforms and preserve terminal escape candidates
imrishabh18 Sep 5, 2026
c4f58c4
Repair constrained via clearances without changing routing geometry i…
imrishabh18 Sep 5, 2026
37d2b7a
Apply CI-requested line wrapping to clearance helpers
imrishabh18 Sep 5, 2026
498afe9
Name pad coordinate transforms explicitly
imrishabh18 Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 65 additions & 14 deletions lib/drc/AutoroutingDrcEngine.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import {
getBoundsFromPoints,
getSegmentIntersection,
pointToSegmentClosestPoint,
segmentToBoundsMinDistance,
segmentToCircleMinDistance,
segmentToSegmentMinDistance,
} from "@tscircuit/math-utils"
import type { ConnectivityMap } from "circuit-json-to-connectivity-map"
import {
applyToPoint,
applyToPoints,
compose,
inverse,
type Matrix,
rotateDEG,
translate,
} from "transformation-matrix"
import type {
SimpleRouteJson,
SimplifiedPcbTrace,
Expand Down Expand Up @@ -61,6 +71,8 @@ type StaticObstacle = {
width: number
height: number
radius?: number
padToBoardTransform: Matrix
boardToPadTransform: Matrix
layers: string[]
pcbPortId?: string
}
Expand Down Expand Up @@ -162,11 +174,21 @@ const getViaBounds = (via: Via): Bounds => {
}
}

const getObstacleBounds = (obstacle: StaticObstacle): Bounds => ({
minX: obstacle.x - obstacle.width / 2,
minY: obstacle.y - obstacle.height / 2,
maxX: obstacle.x + obstacle.width / 2,
maxY: obstacle.y + obstacle.height / 2,
const getObstacleBounds = (obstacle: StaticObstacle): Bounds =>
getBoundsFromPoints(
applyToPoints(obstacle.padToBoardTransform, [
{ x: -obstacle.width / 2, y: -obstacle.height / 2 },
{ x: obstacle.width / 2, y: -obstacle.height / 2 },
{ x: obstacle.width / 2, y: obstacle.height / 2 },
{ x: -obstacle.width / 2, y: obstacle.height / 2 },
]),
)!

const getObstacleLocalBounds = (obstacle: StaticObstacle): Bounds => ({
minX: -obstacle.width / 2,
minY: -obstacle.height / 2,
maxX: obstacle.width / 2,
maxY: obstacle.height / 2,
})

const getCellKey = (cellX: number, cellY: number) => `${cellX}:${cellY}`
Expand Down Expand Up @@ -523,8 +545,19 @@ export class AutoroutingDrcEngine {
if (addedIds.has(obstacleId)) continue
addedIds.add(obstacleId)

const hasRotation =
typeof obstacle.ccwRotationDegrees === "number" &&
Number.isFinite(obstacle.ccwRotationDegrees)
const padToBoardTransform = compose(
translate(obstacle.center.x, obstacle.center.y),
rotateDEG(hasRotation ? obstacle.ccwRotationDegrees! : 0),
)
// Explicit rotation describes a rectangular pad, including square pads.
// Only legacy, unrotated multilayer obstacles use the circular inference.
const isCircular =
isMultiLayer && Math.abs(obstacle.width - obstacle.height) < 0.001
!hasRotation &&
isMultiLayer &&
Math.abs(obstacle.width - obstacle.height) < 0.001
obstacles.push({
kind: "obstacle",
obstacleType,
Expand All @@ -534,6 +567,8 @@ export class AutoroutingDrcEngine {
y: obstacle.center.y,
width: obstacle.width,
height: obstacle.height,
padToBoardTransform,
boardToPadTransform: inverse(padToBoardTransform),
...(isCircular
? { radius: Math.max(obstacle.width, obstacle.height) / 2 }
: {}),
Expand Down Expand Up @@ -752,10 +787,19 @@ export class AutoroutingDrcEngine {
if (this.obstacleSharesNet(segment.netId, obstacle)) return undefined
this.lastRunStats.exactCheckCount += 1

const obstacleBounds = getObstacleBounds(obstacle)
const obstacleBounds = getObstacleLocalBounds(obstacle)
const localSegment = {
...segment,
start: applyToPoint(obstacle.boardToPadTransform, segment.start),
end: applyToPoint(obstacle.boardToPadTransform, segment.end),
}
const shapeDistance =
obstacle.radius === undefined
? segmentToBoundsMinDistance(segment.start, segment.end, obstacleBounds)
? segmentToBoundsMinDistance(
localSegment.start,
localSegment.end,
obstacleBounds,
)
: segmentToCircleMinDistance(segment.start, segment.end, {
x: obstacle.x,
y: obstacle.y,
Expand Down Expand Up @@ -788,7 +832,13 @@ export class AutoroutingDrcEngine {
],
center:
obstacle.radius === undefined
? getClosestPointBetweenSegmentAndBounds(segment, obstacleBounds)
? applyToPoint(
obstacle.padToBoardTransform,
getClosestPointBetweenSegmentAndBounds(
localSegment,
obstacleBounds,
),
)
: getClosestPointBetweenSegmentAndPoint(segment, obstacle),
}
}
Expand All @@ -800,19 +850,20 @@ export class AutoroutingDrcEngine {
if (this.obstacleSharesNet(via.netId, obstacle)) return undefined
this.lastRunStats.exactCheckCount += 1

const obstacleBounds = getObstacleBounds(obstacle)
const obstacleBounds = getObstacleLocalBounds(obstacle)
const localVia = applyToPoint(obstacle.boardToPadTransform, via)
const pointToObstacleDistance =
obstacle.radius === undefined
? Math.hypot(
Math.max(
obstacleBounds.minX - via.x,
obstacleBounds.minX - localVia.x,
0,
via.x - obstacleBounds.maxX,
localVia.x - obstacleBounds.maxX,
),
Math.max(
obstacleBounds.minY - via.y,
obstacleBounds.minY - localVia.y,
0,
via.y - obstacleBounds.maxY,
localVia.y - obstacleBounds.maxY,
),
)
: Math.hypot(via.x - obstacle.x, via.y - obstacle.y) - obstacle.radius
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,13 @@ const TRACE_LAYER_CORRIDOR_VARIANTS = ([1, -1] as const).flatMap(

const TRACE_LAYER_CORRIDOR_MAX_DRC_COUNT = 8

const TRACE_PAIR_DISPLACEMENT_VARIANTS = ([0, 1] as const).map((routeSide) => ({
kind: "displacementChain" as const,
routeSide,
}))

const LOW_COUNT_TRACE_TOPOLOGY_VARIANTS = [
...([0, 1] as const).map((routeSide) => ({
kind: "displacementChain" as const,
routeSide,
})),
...TRACE_PAIR_DISPLACEMENT_VARIANTS,
...([-1, 1] as const).flatMap((directionSign) =>
([0, 1] as const).map((routeSide) => ({
kind: "segment" as const,
Expand Down Expand Up @@ -391,7 +393,7 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
}

private getViaIssueCount(snapshot: DrcSnapshot) {
return getViaDrcIssueCount(snapshot, false)
return getViaDrcIssueCount(snapshot)
}

private getRepairIssueCount(snapshot: DrcSnapshot) {
Expand Down Expand Up @@ -676,6 +678,42 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
error,
bestSnapshot.traceRouteIndexById,
)
if (
this.enableTraceViaOwnerTargeting &&
traceRouteIndex !== undefined &&
Array.isArray(error.pcb_via_ids) &&
error.pcb_via_ids.length === 1 &&
candidateAttemptsThisStep < maxCandidateAttemptsThisStep
) {
const candidateRoutes = cloneRoutes(bestRoutes)
if (
applyViaOnlyDisplacementForTraceError(
this.srj,
candidateRoutes,
error,
bestSnapshot.traceRouteIndexById,
traceRouteIndex,
this.connMap,
)
) {
const routes = materializeRoutes(candidateRoutes)
const snapshot = this.getSnapshot(routes)
const viaIssueCount = this.getViaIssueCount(snapshot)
candidateAttemptsThisStep += 1
this.candidateAttempts += 1
if (
viaIssueCount <= bestViaIssueCount &&
isDrcSnapshotCountBetter(snapshot, bestSnapshot)
) {
bestTopologyCandidate = {
routes,
snapshot,
viaIssueCount,
usesViaInPad: false,
}
}
}
}
if (this.enableSafeTraceLayerMoves && traceRouteIndex !== undefined) {
const safeRouteIndexes = traceRoutePair ?? [traceRouteIndex]
const localSpanVariantCount =
Expand Down Expand Up @@ -727,48 +765,62 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
? Math.floor(layerVariant / this.srj.layerCount)
: 0
const changedRouteIndex = safeRouteIndexes[routeSide]!
const candidateRoutes = cloneRoutesForIndexes(bestRoutes, [
changedRouteIndex,
])
const changed = applySafeTraceLayerMoveForError(
this.srj,
candidateRoutes,
error,
changedRouteIndex,
targetZ,
spanExpansion,
this.connMap,
directionVariant,
)
if (!changed) continue
// Compare both placements before accepting a direction. The helper
// skips the adjusted candidate when its via positions are unchanged.
let evaluatedDirection = false
for (const adjustViaClearance of [false, true]) {
const candidateRoutes = cloneRoutesForIndexes(bestRoutes, [
changedRouteIndex,
])
const changed = applySafeTraceLayerMoveForError(
this.srj,
candidateRoutes,
error,
changedRouteIndex,
targetZ,
spanExpansion,
this.connMap,
directionVariant,
adjustViaClearance,
)
if (!changed) continue

const materializedCandidateRoutes = materializeRoutesForIndexes(
candidateRoutes,
[changedRouteIndex],
)
safeTraceLayerCandidateAttemptsThisStep += 1
this.candidateAttempts += 1
const candidateSnapshot = this.getSnapshot(
materializedCandidateRoutes,
)
const candidateViaIssueCount =
this.getViaIssueCount(candidateSnapshot)
const comparisonSnapshot =
bestTopologyCandidate?.snapshot ?? bestSnapshot
const comparisonViaIssueCount =
bestTopologyCandidate?.viaIssueCount ?? bestViaIssueCount
const materializedCandidateRoutes = materializeRoutesForIndexes(
candidateRoutes,
[changedRouteIndex],
)
evaluatedDirection = true
this.candidateAttempts += 1
const candidateSnapshot = this.getSnapshot(
materializedCandidateRoutes,
)
const candidateViaIssueCount =
this.getViaIssueCount(candidateSnapshot)
const comparisonSnapshot =
bestTopologyCandidate?.snapshot ?? bestSnapshot
const comparisonViaIssueCount =
bestTopologyCandidate?.viaIssueCount ?? bestViaIssueCount

if (
candidateViaIssueCount <= comparisonViaIssueCount &&
isDrcSnapshotCountBetter(candidateSnapshot, comparisonSnapshot)
) {
bestTopologyCandidate = {
routes: materializedCandidateRoutes,
snapshot: candidateSnapshot,
viaIssueCount: candidateViaIssueCount,
usesViaInPad: false,
if (
candidateViaIssueCount <= comparisonViaIssueCount &&
(isDrcSnapshotCountBetter(
candidateSnapshot,
comparisonSnapshot,
) ||
(bestTopologyCandidate !== undefined &&
getRepairDrcIssueCount(candidateSnapshot) ===
getRepairDrcIssueCount(comparisonSnapshot) &&
candidateSnapshot.count < comparisonSnapshot.count))
) {
bestTopologyCandidate = {
routes: materializedCandidateRoutes,
snapshot: candidateSnapshot,
viaIssueCount: candidateViaIssueCount,
usesViaInPad: false,
}
}
}
if (evaluatedDirection) safeTraceLayerCandidateAttemptsThisStep += 1
}
if (traceErrorKey) {
this.safeTraceLayerCursorByErrorId.set(
Expand Down Expand Up @@ -849,7 +901,6 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
}
if (
this.enableSafeTraceLayerMoves &&
shouldTryTracePairTopology &&
traceErrorKey &&
((this.initialLowCountErrorsHaveMovableTraces &&
(traceRoutePair || traceRouteIndex !== undefined)) ||
Expand All @@ -858,7 +909,12 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
const traceTopologyVariants = this
.initialLowCountErrorsHaveMovableTraces
? LOW_COUNT_TRACE_TOPOLOGY_VARIANTS
: TRACE_PAIR_DETOUR_VARIANTS
: shouldTryTracePairTopology
? [
...TRACE_PAIR_DISPLACEMENT_VARIANTS,
...TRACE_PAIR_DETOUR_VARIANTS,
]
: TRACE_PAIR_DISPLACEMENT_VARIANTS
const detourRouteIndexes = traceRoutePair ?? [traceRouteIndex!]
let detourCursor =
this.tracePairDetourCursorByErrorId.get(traceErrorKey) ?? 0
Expand Down Expand Up @@ -906,7 +962,6 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {

if (
chainIssueCount > 0 &&
chainIssueCount <= 3 &&
candidateAttemptsThisStep < maxCandidateAttemptsThisStep
) {
const propagatedRoutes = cloneRoutes(materializedChainRoutes)
Expand Down Expand Up @@ -959,6 +1014,7 @@ export class GlobalDrcForceImproveSolver extends BaseSolver {
const comparisonViaIssueCount =
bestTopologyCandidate?.viaIssueCount ?? bestViaIssueCount
if (
chainViaIssueCount <= comparisonViaIssueCount &&
isBetterDrcSnapshot(
chainSnapshot,
chainViaIssueCount,
Expand Down
Loading
Loading