Skip to content

Commit 954c797

Browse files
committed
Fix solver3 micro-triangle output by sanitizing simplified wire points
1 parent b2275bd commit 954c797

File tree

4 files changed

+221
-3
lines changed

4 files changed

+221
-3
lines changed

lib/utils/convertHdRouteToSimplifiedRoute.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,60 @@ import { mapZToLayerName } from "./mapZToLayerName"
44

55
type Point = { x: number; y: number; z: number }
66

7+
const POINT_EPSILON = 1e-3
8+
const MICRO_DETOUR_MAX_CHORD = 0.06
9+
const MICRO_DETOUR_MIN_TINY_EDGE = 0.01
10+
const MICRO_DETOUR_MIN_LARGE_EDGE = 0.03
11+
const MICRO_DETOUR_MAX_PERPENDICULAR = 0.01
12+
13+
const distance = (a: Point, b: Point) => Math.hypot(a.x - b.x, a.y - b.y)
14+
15+
const sanitizeLayerPoints = (points: Point[]) => {
16+
if (points.length < 2) return points
17+
18+
const cleaned = [...points]
19+
let changed = true
20+
21+
while (changed) {
22+
changed = false
23+
24+
for (let i = 1; i < cleaned.length; i++) {
25+
if (distance(cleaned[i - 1], cleaned[i]) < POINT_EPSILON) {
26+
cleaned.splice(i, 1)
27+
changed = true
28+
break
29+
}
30+
}
31+
if (changed) continue
32+
33+
for (let i = 1; i < cleaned.length - 1; i++) {
34+
const a = cleaned[i - 1]
35+
const b = cleaned[i]
36+
const c = cleaned[i + 1]
37+
38+
const chord = distance(a, c)
39+
if (chord >= MICRO_DETOUR_MAX_CHORD) continue
40+
41+
const distAB = distance(a, b)
42+
const distBC = distance(b, c)
43+
if (Math.min(distAB, distBC) >= MICRO_DETOUR_MIN_TINY_EDGE) continue
44+
if (Math.max(distAB, distBC) <= MICRO_DETOUR_MIN_LARGE_EDGE) continue
45+
46+
const doubledArea = Math.abs(
47+
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x),
48+
)
49+
const perpendicularDistance = doubledArea / Math.max(chord, 1e-9)
50+
if (perpendicularDistance >= MICRO_DETOUR_MAX_PERPENDICULAR) continue
51+
52+
cleaned.splice(i, 1)
53+
changed = true
54+
break
55+
}
56+
}
57+
58+
return cleaned
59+
}
60+
761
/**
862
* Extended HD route type that may contain jumpers (from HighDensitySolver)
963
*/
@@ -30,7 +84,7 @@ export const convertHdRouteToSimplifiedRoute = (
3084
if (point.z !== currentZ) {
3185
// Add all wire segments for the current layer
3286
const layerName = mapZToLayerName(currentZ, layerCount)
33-
for (const layerPoint of currentLayerPoints) {
87+
for (const layerPoint of sanitizeLayerPoints(currentLayerPoints)) {
3488
result.push({
3589
route_type: "wire",
3690
x: layerPoint.x,
@@ -72,7 +126,7 @@ export const convertHdRouteToSimplifiedRoute = (
72126

73127
// Add the final layer's wire segments
74128
const layerName = mapZToLayerName(currentZ, layerCount)
75-
for (const layerPoint of currentLayerPoints) {
129+
for (const layerPoint of sanitizeLayerPoints(currentLayerPoints)) {
76130
result.push({
77131
route_type: "wire",
78132
x: layerPoint.x,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"vercel-build": "cosmos-export",
1616
"repomix:lib": "repomix --ignore 'testing/**,**/TwoRouteHighDensitySolver/**,**/RouteStitchingSolver/**,solvers/CapacitySegmentPointOptimizer/CapacitySegmentPointOptimizer.ts' lib",
1717
"bug-report": "bun run scripts/download-bug-report.ts",
18-
"bug-report-with-test": "bun run scripts/create-bug-report-test.ts"
18+
"bug-report-with-test": "bun run scripts/create-bug-report-test.ts",
19+
"debug:dataset01:circuit011:solver3": "bun scripts/debug-dataset01-circuit011-solver3.ts"
1920
},
2021
"devDependencies": {
2122
"@biomejs/biome": "^1.9.4",
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as dataset01 from "@tscircuit/autorouting-dataset-01"
2+
import { AutoroutingPipelineSolver3_HgPortPointPathing } from "../lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver3_HgPortPointPathing"
3+
4+
type Point2D = { x: number; y: number }
5+
6+
type RoutePoint = Point2D & { z?: number; layer?: string; route_type?: string }
7+
8+
const EPS = 1e-6
9+
10+
const distance = (a: Point2D, b: Point2D) => Math.hypot(a.x - b.x, a.y - b.y)
11+
12+
const triangleArea2 = (a: Point2D, b: Point2D, c: Point2D) =>
13+
Math.abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
14+
15+
const describePoint = (p: RoutePoint) =>
16+
`(${p.x.toFixed(6)}, ${p.y.toFixed(6)}${typeof p.z === "number" ? `, z=${p.z}` : ""}${p.layer ? `, layer=${p.layer}` : ""})`
17+
18+
const srj = dataset01.circuit011
19+
const solver = new AutoroutingPipelineSolver3_HgPortPointPathing(srj)
20+
solver.solve()
21+
22+
console.log("=== dataset01 / circuit011 / Solver3 debug ===")
23+
console.log("trace count:", solver.getOutputSimpleRouteJson().traces.length)
24+
25+
const node25 = solver.capacityNodes?.find((n) => n.capacityMeshNodeId === "cmn_25")
26+
if (!node25) {
27+
console.log("cmn_25 not found in capacity nodes")
28+
process.exit(0)
29+
}
30+
31+
const node25Bounds = {
32+
minX: node25.center.x - node25.width / 2,
33+
maxX: node25.center.x + node25.width / 2,
34+
minY: node25.center.y - node25.height / 2,
35+
maxY: node25.center.y + node25.height / 2,
36+
}
37+
38+
console.log("cmn_25 bounds:", node25Bounds)
39+
40+
const solvedRoutes = solver.portPointPathingSolver?.solvedRoutes ?? []
41+
const routesThrough25 = solvedRoutes.filter((route) =>
42+
route.path.some((step) => step.nextRegion?.regionId === "cmn_25"),
43+
)
44+
45+
console.log("connections whose hypergraph path traverses cmn_25:")
46+
for (const route of routesThrough25) {
47+
const name = route.connection.simpleRouteConnection.name
48+
const regionPath = route.path
49+
.map((step) => step.nextRegion?.regionId)
50+
.filter(Boolean)
51+
.join(" -> ")
52+
console.log(`- ${name}: ${regionPath}`)
53+
}
54+
55+
const traces = solver.getOutputSimpleRouteJson().traces
56+
57+
const tinyTriangleCandidates: Array<{
58+
traceId: string
59+
index: number
60+
points: [RoutePoint, RoutePoint, RoutePoint]
61+
chordLength: number
62+
area2: number
63+
}> = []
64+
65+
for (const trace of traces) {
66+
const wirePoints = trace.route.filter((p) => p.route_type === "wire") as RoutePoint[]
67+
68+
for (let i = 0; i < wirePoints.length - 2; i++) {
69+
const a = wirePoints[i]
70+
const b = wirePoints[i + 1]
71+
const c = wirePoints[i + 2]
72+
73+
const chordLength = distance(a, c)
74+
const area2 = triangleArea2(a, b, c)
75+
76+
const hasSmallDetour = chordLength < 0.08 && area2 > EPS
77+
if (hasSmallDetour) {
78+
tinyTriangleCandidates.push({
79+
traceId: trace.pcb_trace_id,
80+
index: i,
81+
points: [a, b, c],
82+
chordLength,
83+
area2,
84+
})
85+
}
86+
}
87+
}
88+
89+
console.log("tiny triangle candidates in final simplified traces:", tinyTriangleCandidates.length)
90+
for (const candidate of tinyTriangleCandidates) {
91+
const [a, b, c] = candidate.points
92+
console.log(
93+
`- ${candidate.traceId} at i=${candidate.index}, chord=${candidate.chordLength.toFixed(6)}, area2=${candidate.area2.toExponential(2)}`,
94+
)
95+
console.log(` a=${describePoint(a)}`)
96+
console.log(` b=${describePoint(b)}`)
97+
console.log(` c=${describePoint(c)}`)
98+
}
99+
100+
// Determine at which stage the most obvious kink appears.
101+
const target = { x: 0.75, y: -4.205 }
102+
const stageRoutes: Array<[string, Array<{ connectionName: string; route: RoutePoint[] }>]> = [
103+
["highDensityRouteSolver", solver.highDensityRouteSolver?.routes ?? []],
104+
["highDensityStitchSolver", solver.highDensityStitchSolver?.mergedHdRoutes ?? []],
105+
["traceSimplificationSolver", solver.traceSimplificationSolver?.simplifiedHdRoutes ?? []],
106+
["traceWidthSolver", solver.traceWidthSolver?.getHdRoutesWithWidths() ?? []],
107+
]
108+
109+
console.log("stage presence of kink point (0.75, -4.205):")
110+
for (const [stageName, routes] of stageRoutes) {
111+
const hits = routes.filter((route) =>
112+
route.route.some(
113+
(point) => Math.abs(point.x - target.x) < EPS && Math.abs(point.y - target.y) < EPS,
114+
),
115+
)
116+
console.log(`- ${stageName}: ${hits.length} route(s)`)
117+
for (const hit of hits) {
118+
console.log(` -> ${hit.connectionName}`)
119+
}
120+
}
121+
122+
console.log("Conclusion: the detour is already present in raw intra-node output from highDensityRouteSolver, not introduced by later stitching/simplification.")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect, test } from "bun:test"
2+
import * as dataset01 from "@tscircuit/autorouting-dataset-01"
3+
import { AutoroutingPipelineSolver3_HgPortPointPathing } from "../../lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver3_HgPortPointPathing"
4+
5+
const EPS = 1e-6
6+
7+
const distance = (a: { x: number; y: number }, b: { x: number; y: number }) =>
8+
Math.hypot(a.x - b.x, a.y - b.y)
9+
10+
const triangleArea2 = (
11+
a: { x: number; y: number },
12+
b: { x: number; y: number },
13+
c: { x: number; y: number },
14+
) => Math.abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
15+
16+
test("dataset01 circuit011 solver3 output should not contain tiny wire triangle detours", () => {
17+
const solver = new AutoroutingPipelineSolver3_HgPortPointPathing(dataset01.circuit011)
18+
solver.solve()
19+
20+
const traces = solver.getOutputSimpleRouteJson().traces
21+
const tinyTriangles: Array<{ traceId: string; index: number }> = []
22+
23+
for (const trace of traces) {
24+
const wirePoints = trace.route.filter((p) => p.route_type === "wire")
25+
for (let i = 0; i < wirePoints.length - 2; i++) {
26+
const a = wirePoints[i]
27+
const b = wirePoints[i + 1]
28+
const c = wirePoints[i + 2]
29+
30+
if (a.layer !== b.layer || b.layer !== c.layer) continue
31+
32+
const chord = distance(a, c)
33+
const area2 = triangleArea2(a, b, c)
34+
if (chord < 0.08 && area2 > EPS) {
35+
tinyTriangles.push({ traceId: trace.pcb_trace_id, index: i })
36+
}
37+
}
38+
}
39+
40+
expect(tinyTriangles).toEqual([])
41+
})

0 commit comments

Comments
 (0)