@tscircuit/fanout-solver 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/lib/index.ts +11 -0
- package/lib/validate-original-endpoint-connectivity.ts +374 -0
- package/lib/validate-routed-copper-drc.ts +442 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,10 +239,16 @@ samples in parallel, and `--sample-timeout-seconds 600` prevents a difficult
|
|
|
239
239
|
sample from blocking the remaining work. Each run writes the full ordered
|
|
240
240
|
results to `benchmark-results/srj29.json` and
|
|
241
241
|
`benchmark-results/srj29.md`. A row is marked solved only when every input
|
|
242
|
-
connection has a validated breakout
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
connection has a validated breakout and an independent physical-copper audit
|
|
243
|
+
proves that the emitted wires, vias, and same-net pads connect every original
|
|
244
|
+
endpoint on compatible layers. A second independent audit checks every emitted
|
|
245
|
+
trace and via against different-net pads, obstacles, traces, and vias on every
|
|
246
|
+
physical layer in its span. Reaching an arbitrary boundary point, retaining an
|
|
247
|
+
unrouted endpoint in the output JSON, or emitting copper with a DRC violation
|
|
248
|
+
does not count. The report separates fanout-prefix completion from physically
|
|
249
|
+
connected original connections so partial progress remains visible without
|
|
250
|
+
overstating it as a solution. Partial solutions are reported as benchmark
|
|
251
|
+
results instead of failing the command. `bun run benchmark:srj29` is an alias
|
|
246
252
|
for the same command.
|
|
247
253
|
|
|
248
254
|
The `SRJ29 Benchmark` GitHub Actions workflow runs the complete dataset on a
|
package/lib/index.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
export { FanoutSolver } from "./fanout-solver"
|
|
2
2
|
export { getCopperLayerColor } from "./layer-colors"
|
|
3
3
|
export { getCopperLayerNames } from "./layer-names"
|
|
4
|
+
export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
|
|
5
|
+
export { validateRoutedCopperDrc } from "./validate-routed-copper-drc"
|
|
4
6
|
export { validateFanoutSolution } from "./validate-fanout-solution"
|
|
7
|
+
export type {
|
|
8
|
+
OriginalEndpointConnectivityIssue,
|
|
9
|
+
OriginalEndpointConnectivityReport,
|
|
10
|
+
} from "./validate-original-endpoint-connectivity"
|
|
11
|
+
export type {
|
|
12
|
+
RoutedCopperDrcIssue,
|
|
13
|
+
RoutedCopperDrcIssueCode,
|
|
14
|
+
RoutedCopperDrcReport,
|
|
15
|
+
} from "./validate-routed-copper-drc"
|
|
5
16
|
export type {
|
|
6
17
|
Bounds,
|
|
7
18
|
FanoutAttemptSummary,
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionPoint,
|
|
3
|
+
Obstacle,
|
|
4
|
+
SimpleRouteConnection,
|
|
5
|
+
SimpleRouteJson,
|
|
6
|
+
SimplifiedPcbTrace,
|
|
7
|
+
} from "@tscircuit/capacity-autorouter"
|
|
8
|
+
import {
|
|
9
|
+
distance,
|
|
10
|
+
distancePointToObstacle,
|
|
11
|
+
distancePointToSegment,
|
|
12
|
+
distanceSegmentToObstacle,
|
|
13
|
+
distanceSegmentToSegment,
|
|
14
|
+
pointIsInsideObstacle,
|
|
15
|
+
} from "./geometry"
|
|
16
|
+
import { getCopperLayerNames, getLayerSpan } from "./layer-names"
|
|
17
|
+
import {
|
|
18
|
+
connectionsShareElectricalNet,
|
|
19
|
+
getConnectionNetKey,
|
|
20
|
+
obstacleSharesElectricalNet,
|
|
21
|
+
} from "./net-identity"
|
|
22
|
+
import type { Point2D, RoutedSegment } from "./types"
|
|
23
|
+
|
|
24
|
+
const EPSILON = 1e-6
|
|
25
|
+
|
|
26
|
+
export interface OriginalEndpointConnectivityIssue {
|
|
27
|
+
code: "original-endpoints-disconnected"
|
|
28
|
+
connectionName: string
|
|
29
|
+
disconnectedEndpointIndices: number[]
|
|
30
|
+
message: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OriginalEndpointConnectivityReport {
|
|
34
|
+
valid: boolean
|
|
35
|
+
checkedConnectionCount: number
|
|
36
|
+
connectedConnectionCount: number
|
|
37
|
+
checkedEndpointCount: number
|
|
38
|
+
connectedEndpointCount: number
|
|
39
|
+
issues: OriginalEndpointConnectivityIssue[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EndpointCopper {
|
|
43
|
+
type: "endpoint"
|
|
44
|
+
connectionName: string
|
|
45
|
+
endpointIndex: number
|
|
46
|
+
point: ConnectionPoint
|
|
47
|
+
layers: string[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ObstacleCopper {
|
|
51
|
+
type: "obstacle"
|
|
52
|
+
obstacle: Obstacle
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SegmentCopper {
|
|
56
|
+
type: "segment"
|
|
57
|
+
segment: RoutedSegment
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ViaCopper {
|
|
61
|
+
type: "via"
|
|
62
|
+
center: Point2D
|
|
63
|
+
diameter: number
|
|
64
|
+
layers: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type CopperPrimitive =
|
|
68
|
+
| EndpointCopper
|
|
69
|
+
| ObstacleCopper
|
|
70
|
+
| SegmentCopper
|
|
71
|
+
| ViaCopper
|
|
72
|
+
|
|
73
|
+
function getPointLayers(point: ConnectionPoint): string[] {
|
|
74
|
+
return "layer" in point ? [point.layer] : point.layers
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function layersOverlap(first: readonly string[], second: readonly string[]) {
|
|
78
|
+
return first.some((layer) => second.includes(layer))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractTraceCopper(
|
|
82
|
+
trace: SimplifiedPcbTrace,
|
|
83
|
+
layerNames: string[],
|
|
84
|
+
): Array<SegmentCopper | ViaCopper> {
|
|
85
|
+
const copper: Array<SegmentCopper | ViaCopper> = []
|
|
86
|
+
let previousWire:
|
|
87
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
88
|
+
| undefined
|
|
89
|
+
|
|
90
|
+
for (const routePoint of trace.route) {
|
|
91
|
+
if (routePoint.route_type === "via") {
|
|
92
|
+
copper.push({
|
|
93
|
+
type: "via",
|
|
94
|
+
center: { x: routePoint.x, y: routePoint.y },
|
|
95
|
+
diameter: routePoint.via_diameter ?? 0,
|
|
96
|
+
layers: getLayerSpan(
|
|
97
|
+
routePoint.from_layer,
|
|
98
|
+
routePoint.to_layer,
|
|
99
|
+
layerNames,
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
previousWire = undefined
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (routePoint.route_type !== "wire") continue
|
|
106
|
+
if (
|
|
107
|
+
previousWire &&
|
|
108
|
+
previousWire.layer === routePoint.layer &&
|
|
109
|
+
distance(previousWire, routePoint) > EPSILON
|
|
110
|
+
) {
|
|
111
|
+
copper.push({
|
|
112
|
+
type: "segment",
|
|
113
|
+
segment: {
|
|
114
|
+
start: { x: previousWire.x, y: previousWire.y },
|
|
115
|
+
end: { x: routePoint.x, y: routePoint.y },
|
|
116
|
+
width: routePoint.width,
|
|
117
|
+
layer: routePoint.layer,
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
previousWire = routePoint
|
|
122
|
+
}
|
|
123
|
+
return copper
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function primitivesTouch(first: CopperPrimitive, second: CopperPrimitive) {
|
|
127
|
+
if (first.type === "endpoint" && second.type === "endpoint") {
|
|
128
|
+
return (
|
|
129
|
+
layersOverlap(first.layers, second.layers) &&
|
|
130
|
+
distance(first.point, second.point) <= EPSILON
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
if (first.type === "endpoint" && second.type === "obstacle") {
|
|
134
|
+
return (
|
|
135
|
+
layersOverlap(first.layers, second.obstacle.layers) &&
|
|
136
|
+
pointIsInsideObstacle(first.point, second.obstacle, EPSILON)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
if (first.type === "obstacle" && second.type === "endpoint") {
|
|
140
|
+
return primitivesTouch(second, first)
|
|
141
|
+
}
|
|
142
|
+
if (first.type === "endpoint" && second.type === "segment") {
|
|
143
|
+
return (
|
|
144
|
+
first.layers.includes(second.segment.layer) &&
|
|
145
|
+
distancePointToSegment(
|
|
146
|
+
first.point,
|
|
147
|
+
second.segment.start,
|
|
148
|
+
second.segment.end,
|
|
149
|
+
) <=
|
|
150
|
+
second.segment.width / 2 + EPSILON
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
if (first.type === "segment" && second.type === "endpoint") {
|
|
154
|
+
return primitivesTouch(second, first)
|
|
155
|
+
}
|
|
156
|
+
if (first.type === "endpoint" && second.type === "via") {
|
|
157
|
+
return (
|
|
158
|
+
layersOverlap(first.layers, second.layers) &&
|
|
159
|
+
distance(first.point, second.center) <= second.diameter / 2 + EPSILON
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (first.type === "via" && second.type === "endpoint") {
|
|
163
|
+
return primitivesTouch(second, first)
|
|
164
|
+
}
|
|
165
|
+
if (first.type === "obstacle" && second.type === "segment") {
|
|
166
|
+
return (
|
|
167
|
+
first.obstacle.layers.includes(second.segment.layer) &&
|
|
168
|
+
distanceSegmentToObstacle(second.segment, first.obstacle) <=
|
|
169
|
+
second.segment.width / 2 + EPSILON
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
if (first.type === "segment" && second.type === "obstacle") {
|
|
173
|
+
return primitivesTouch(second, first)
|
|
174
|
+
}
|
|
175
|
+
if (first.type === "obstacle" && second.type === "via") {
|
|
176
|
+
return (
|
|
177
|
+
layersOverlap(first.obstacle.layers, second.layers) &&
|
|
178
|
+
distancePointToObstacle(second.center, first.obstacle) <=
|
|
179
|
+
second.diameter / 2 + EPSILON
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
if (first.type === "via" && second.type === "obstacle") {
|
|
183
|
+
return primitivesTouch(second, first)
|
|
184
|
+
}
|
|
185
|
+
if (first.type === "segment" && second.type === "segment") {
|
|
186
|
+
return (
|
|
187
|
+
first.segment.layer === second.segment.layer &&
|
|
188
|
+
distanceSegmentToSegment(
|
|
189
|
+
first.segment.start,
|
|
190
|
+
first.segment.end,
|
|
191
|
+
second.segment.start,
|
|
192
|
+
second.segment.end,
|
|
193
|
+
) <=
|
|
194
|
+
(first.segment.width + second.segment.width) / 2 + EPSILON
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
if (first.type === "segment" && second.type === "via") {
|
|
198
|
+
return (
|
|
199
|
+
second.layers.includes(first.segment.layer) &&
|
|
200
|
+
distancePointToSegment(
|
|
201
|
+
second.center,
|
|
202
|
+
first.segment.start,
|
|
203
|
+
first.segment.end,
|
|
204
|
+
) <=
|
|
205
|
+
second.diameter / 2 + first.segment.width / 2 + EPSILON
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
if (first.type === "via" && second.type === "segment") {
|
|
209
|
+
return primitivesTouch(second, first)
|
|
210
|
+
}
|
|
211
|
+
if (first.type === "via" && second.type === "via") {
|
|
212
|
+
return (
|
|
213
|
+
layersOverlap(first.layers, second.layers) &&
|
|
214
|
+
distance(first.center, second.center) <=
|
|
215
|
+
(first.diameter + second.diameter) / 2 + EPSILON
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class DisjointSet {
|
|
222
|
+
private readonly parent: number[]
|
|
223
|
+
|
|
224
|
+
constructor(size: number) {
|
|
225
|
+
this.parent = Array.from({ length: size }, (_, index) => index)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
find(index: number): number {
|
|
229
|
+
const parent = this.parent[index]!
|
|
230
|
+
if (parent === index) return index
|
|
231
|
+
const root = this.find(parent)
|
|
232
|
+
this.parent[index] = root
|
|
233
|
+
return root
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
union(first: number, second: number): void {
|
|
237
|
+
const firstRoot = this.find(first)
|
|
238
|
+
const secondRoot = this.find(second)
|
|
239
|
+
if (firstRoot !== secondRoot) this.parent[secondRoot] = firstRoot
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function traceBelongsToNet(
|
|
244
|
+
inputSrj: SimpleRouteJson,
|
|
245
|
+
trace: SimplifiedPcbTrace,
|
|
246
|
+
representativeConnection: SimpleRouteConnection,
|
|
247
|
+
): boolean {
|
|
248
|
+
return Boolean(
|
|
249
|
+
trace.connection_name &&
|
|
250
|
+
connectionsShareElectricalNet(
|
|
251
|
+
inputSrj,
|
|
252
|
+
trace.connection_name,
|
|
253
|
+
representativeConnection.name,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Independently proves that emitted copper connects every original SRJ
|
|
260
|
+
* endpoint. Merely reaching a boundary or retaining an endpoint in the output
|
|
261
|
+
* connection metadata does not count as connectivity.
|
|
262
|
+
*/
|
|
263
|
+
export function validateOriginalEndpointConnectivity(params: {
|
|
264
|
+
inputSrj: SimpleRouteJson
|
|
265
|
+
routedSrj: SimpleRouteJson
|
|
266
|
+
}): OriginalEndpointConnectivityReport {
|
|
267
|
+
const { inputSrj, routedSrj } = params
|
|
268
|
+
const layerNames = getCopperLayerNames(routedSrj.layerCount)
|
|
269
|
+
const connectionsByNet = new Map<string, SimpleRouteConnection[]>()
|
|
270
|
+
for (const connection of inputSrj.connections) {
|
|
271
|
+
const netKey = getConnectionNetKey(connection)
|
|
272
|
+
const connections = connectionsByNet.get(netKey) ?? []
|
|
273
|
+
connections.push(connection)
|
|
274
|
+
connectionsByNet.set(netKey, connections)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const issues: OriginalEndpointConnectivityIssue[] = []
|
|
278
|
+
let connectedConnectionCount = 0
|
|
279
|
+
let connectedEndpointCount = 0
|
|
280
|
+
|
|
281
|
+
for (const connections of connectionsByNet.values()) {
|
|
282
|
+
const representativeConnection = connections[0]!
|
|
283
|
+
const endpointCopper: EndpointCopper[] = connections.flatMap((connection) =>
|
|
284
|
+
connection.pointsToConnect.map((point, endpointIndex) => ({
|
|
285
|
+
type: "endpoint" as const,
|
|
286
|
+
connectionName: connection.name,
|
|
287
|
+
endpointIndex,
|
|
288
|
+
point,
|
|
289
|
+
layers: getPointLayers(point),
|
|
290
|
+
})),
|
|
291
|
+
)
|
|
292
|
+
const obstacleCopper: ObstacleCopper[] = inputSrj.obstacles
|
|
293
|
+
.filter((obstacle) =>
|
|
294
|
+
obstacleSharesElectricalNet(
|
|
295
|
+
inputSrj,
|
|
296
|
+
obstacle,
|
|
297
|
+
representativeConnection.name,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
.map((obstacle) => ({ type: "obstacle", obstacle }))
|
|
301
|
+
const routeCopper = (routedSrj.traces ?? [])
|
|
302
|
+
.filter((trace) =>
|
|
303
|
+
traceBelongsToNet(inputSrj, trace, representativeConnection),
|
|
304
|
+
)
|
|
305
|
+
.flatMap((trace) => extractTraceCopper(trace, layerNames))
|
|
306
|
+
const copper: CopperPrimitive[] = [
|
|
307
|
+
...endpointCopper,
|
|
308
|
+
...obstacleCopper,
|
|
309
|
+
...routeCopper,
|
|
310
|
+
]
|
|
311
|
+
const connectedCopper = new DisjointSet(copper.length)
|
|
312
|
+
for (let firstIndex = 0; firstIndex < copper.length; firstIndex++) {
|
|
313
|
+
for (
|
|
314
|
+
let secondIndex = firstIndex + 1;
|
|
315
|
+
secondIndex < copper.length;
|
|
316
|
+
secondIndex++
|
|
317
|
+
) {
|
|
318
|
+
if (primitivesTouch(copper[firstIndex]!, copper[secondIndex]!)) {
|
|
319
|
+
connectedCopper.union(firstIndex, secondIndex)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const endpointIndexByConnection = new Map<string, number[]>()
|
|
325
|
+
for (let index = 0; index < endpointCopper.length; index++) {
|
|
326
|
+
const endpoint = endpointCopper[index]!
|
|
327
|
+
const indices =
|
|
328
|
+
endpointIndexByConnection.get(endpoint.connectionName) ?? []
|
|
329
|
+
indices[endpoint.endpointIndex] = index
|
|
330
|
+
endpointIndexByConnection.set(endpoint.connectionName, indices)
|
|
331
|
+
}
|
|
332
|
+
for (const connection of connections) {
|
|
333
|
+
const endpointIndices =
|
|
334
|
+
endpointIndexByConnection.get(connection.name) ?? []
|
|
335
|
+
const firstEndpointIndex = endpointIndices[0]
|
|
336
|
+
const firstRoot =
|
|
337
|
+
firstEndpointIndex === undefined
|
|
338
|
+
? undefined
|
|
339
|
+
: connectedCopper.find(firstEndpointIndex)
|
|
340
|
+
const disconnectedEndpointIndices = endpointIndices.flatMap(
|
|
341
|
+
(endpointIndex, index) =>
|
|
342
|
+
firstRoot === undefined ||
|
|
343
|
+
connectedCopper.find(endpointIndex) !== firstRoot
|
|
344
|
+
? [index]
|
|
345
|
+
: [],
|
|
346
|
+
)
|
|
347
|
+
connectedEndpointCount +=
|
|
348
|
+
endpointIndices.length - disconnectedEndpointIndices.length
|
|
349
|
+
if (disconnectedEndpointIndices.length === 0) {
|
|
350
|
+
connectedConnectionCount++
|
|
351
|
+
} else {
|
|
352
|
+
issues.push({
|
|
353
|
+
code: "original-endpoints-disconnected",
|
|
354
|
+
connectionName: connection.name,
|
|
355
|
+
disconnectedEndpointIndices,
|
|
356
|
+
message: `Connection ${connection.name} has no physical copper path from endpoint 0 to original endpoint${disconnectedEndpointIndices.length === 1 ? "" : "s"} ${disconnectedEndpointIndices.join(", ")}`,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const checkedEndpointCount = inputSrj.connections.reduce(
|
|
363
|
+
(count, connection) => count + connection.pointsToConnect.length,
|
|
364
|
+
0,
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
valid: issues.length === 0,
|
|
368
|
+
checkedConnectionCount: inputSrj.connections.length,
|
|
369
|
+
connectedConnectionCount,
|
|
370
|
+
checkedEndpointCount,
|
|
371
|
+
connectedEndpointCount,
|
|
372
|
+
issues,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SimpleRouteJson,
|
|
3
|
+
SimplifiedPcbTrace,
|
|
4
|
+
} from "@tscircuit/capacity-autorouter"
|
|
5
|
+
import {
|
|
6
|
+
distance,
|
|
7
|
+
distancePointToObstacle,
|
|
8
|
+
distancePointToSegment,
|
|
9
|
+
distanceSegmentToObstacle,
|
|
10
|
+
pointIsInsideObstacle,
|
|
11
|
+
segmentsAreClear,
|
|
12
|
+
} from "./geometry"
|
|
13
|
+
import { getCopperLayerNames, getLayerSpan } from "./layer-names"
|
|
14
|
+
import {
|
|
15
|
+
connectionsShareElectricalNet,
|
|
16
|
+
obstacleSharesElectricalNet,
|
|
17
|
+
} from "./net-identity"
|
|
18
|
+
import type { Point2D, RoutedSegment } from "./types"
|
|
19
|
+
|
|
20
|
+
const EPSILON = 1e-6
|
|
21
|
+
|
|
22
|
+
export type RoutedCopperDrcIssueCode =
|
|
23
|
+
| "unknown-trace-connection"
|
|
24
|
+
| "unsupported-route-point"
|
|
25
|
+
| "disconnected-trace"
|
|
26
|
+
| "trace-obstacle-clearance"
|
|
27
|
+
| "via-obstacle-clearance"
|
|
28
|
+
| "different-net-trace-clearance"
|
|
29
|
+
| "different-net-trace-via-clearance"
|
|
30
|
+
| "different-net-via-clearance"
|
|
31
|
+
|
|
32
|
+
export interface RoutedCopperDrcIssue {
|
|
33
|
+
code: RoutedCopperDrcIssueCode
|
|
34
|
+
message: string
|
|
35
|
+
traceId: string
|
|
36
|
+
connectionName?: string
|
|
37
|
+
otherTraceId?: string
|
|
38
|
+
otherConnectionName?: string
|
|
39
|
+
layer?: string
|
|
40
|
+
obstacleId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RoutedCopperDrcReport {
|
|
44
|
+
valid: boolean
|
|
45
|
+
checkedTraceCount: number
|
|
46
|
+
checkedSegmentCount: number
|
|
47
|
+
checkedViaCount: number
|
|
48
|
+
issues: RoutedCopperDrcIssue[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RoutedVia {
|
|
52
|
+
center: Point2D
|
|
53
|
+
diameter: number
|
|
54
|
+
spanLayers: string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TraceCopper {
|
|
58
|
+
trace: SimplifiedPcbTrace
|
|
59
|
+
connectionName: string
|
|
60
|
+
segments: RoutedSegment[]
|
|
61
|
+
vias: RoutedVia[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isLegalTerminalBodyEscape(params: {
|
|
65
|
+
inputSrj: SimpleRouteJson
|
|
66
|
+
segment: RoutedSegment
|
|
67
|
+
bodyObstacle: SimpleRouteJson["obstacles"][number]
|
|
68
|
+
connectionName: string
|
|
69
|
+
}): boolean {
|
|
70
|
+
const { inputSrj, segment, bodyObstacle, connectionName } = params
|
|
71
|
+
if (bodyObstacle.connectedTo.length > 0 || !bodyObstacle.componentId) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
const sameNetComponentPads = inputSrj.obstacles.filter(
|
|
75
|
+
(obstacle) =>
|
|
76
|
+
obstacle.componentId === bodyObstacle.componentId &&
|
|
77
|
+
obstacle.layers.includes(segment.layer) &&
|
|
78
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, connectionName),
|
|
79
|
+
)
|
|
80
|
+
for (const pad of sameNetComponentPads) {
|
|
81
|
+
for (const [terminal, other] of [
|
|
82
|
+
[segment.start, segment.end],
|
|
83
|
+
[segment.end, segment.start],
|
|
84
|
+
] as const) {
|
|
85
|
+
if (!pointIsInsideObstacle(terminal, pad, EPSILON)) continue
|
|
86
|
+
const outwardFromBody = {
|
|
87
|
+
x: terminal.x - bodyObstacle.center.x,
|
|
88
|
+
y: terminal.y - bodyObstacle.center.y,
|
|
89
|
+
}
|
|
90
|
+
const terminalToOther = {
|
|
91
|
+
x: other.x - terminal.x,
|
|
92
|
+
y: other.y - terminal.y,
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
outwardFromBody.x * terminalToOther.x +
|
|
96
|
+
outwardFromBody.y * terminalToOther.y >
|
|
97
|
+
EPSILON
|
|
98
|
+
) {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pointsMatch(first: Point2D, second: Point2D): boolean {
|
|
107
|
+
return distance(first, second) <= EPSILON
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function addIssue(
|
|
111
|
+
issues: RoutedCopperDrcIssue[],
|
|
112
|
+
issue: RoutedCopperDrcIssue,
|
|
113
|
+
): void {
|
|
114
|
+
issues.push(issue)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractTraceCopper(params: {
|
|
118
|
+
srj: SimpleRouteJson
|
|
119
|
+
trace: SimplifiedPcbTrace
|
|
120
|
+
connectionName: string
|
|
121
|
+
layerNames: string[]
|
|
122
|
+
issues: RoutedCopperDrcIssue[]
|
|
123
|
+
}): TraceCopper {
|
|
124
|
+
const { srj, trace, connectionName, layerNames, issues } = params
|
|
125
|
+
const segments: RoutedSegment[] = []
|
|
126
|
+
const vias: RoutedVia[] = []
|
|
127
|
+
let previousWire:
|
|
128
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
129
|
+
| undefined
|
|
130
|
+
let pendingVia:
|
|
131
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "via" }>
|
|
132
|
+
| undefined
|
|
133
|
+
|
|
134
|
+
for (const routePoint of trace.route) {
|
|
135
|
+
if (routePoint.route_type === "via") {
|
|
136
|
+
const spanLayers = getLayerSpan(
|
|
137
|
+
routePoint.from_layer,
|
|
138
|
+
routePoint.to_layer,
|
|
139
|
+
layerNames,
|
|
140
|
+
)
|
|
141
|
+
vias.push({
|
|
142
|
+
center: { x: routePoint.x, y: routePoint.y },
|
|
143
|
+
diameter:
|
|
144
|
+
routePoint.via_diameter ?? srj.minViaPadDiameter ?? srj.minTraceWidth,
|
|
145
|
+
spanLayers,
|
|
146
|
+
})
|
|
147
|
+
if (
|
|
148
|
+
!previousWire ||
|
|
149
|
+
!pointsMatch(previousWire, routePoint) ||
|
|
150
|
+
previousWire.layer !== routePoint.from_layer
|
|
151
|
+
) {
|
|
152
|
+
addIssue(issues, {
|
|
153
|
+
code: "disconnected-trace",
|
|
154
|
+
traceId: trace.pcb_trace_id,
|
|
155
|
+
connectionName,
|
|
156
|
+
message: `Trace ${trace.pcb_trace_id} reaches a via without a matching ${routePoint.from_layer} wire endpoint`,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
pendingVia = routePoint
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (routePoint.route_type !== "wire") {
|
|
164
|
+
addIssue(issues, {
|
|
165
|
+
code: "unsupported-route-point",
|
|
166
|
+
traceId: trace.pcb_trace_id,
|
|
167
|
+
connectionName,
|
|
168
|
+
message: `Trace ${trace.pcb_trace_id} contains unsupported ${routePoint.route_type} geometry`,
|
|
169
|
+
})
|
|
170
|
+
previousWire = undefined
|
|
171
|
+
pendingVia = undefined
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (pendingVia) {
|
|
176
|
+
if (
|
|
177
|
+
!pointsMatch(routePoint, pendingVia) ||
|
|
178
|
+
routePoint.layer !== pendingVia.to_layer
|
|
179
|
+
) {
|
|
180
|
+
addIssue(issues, {
|
|
181
|
+
code: "disconnected-trace",
|
|
182
|
+
traceId: trace.pcb_trace_id,
|
|
183
|
+
connectionName,
|
|
184
|
+
message: `Trace ${trace.pcb_trace_id} does not continue from its via on ${pendingVia.to_layer}`,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
previousWire = routePoint
|
|
188
|
+
pendingVia = undefined
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (previousWire) {
|
|
193
|
+
if (previousWire.layer !== routePoint.layer) {
|
|
194
|
+
addIssue(issues, {
|
|
195
|
+
code: "disconnected-trace",
|
|
196
|
+
traceId: trace.pcb_trace_id,
|
|
197
|
+
connectionName,
|
|
198
|
+
message: `Trace ${trace.pcb_trace_id} changes from ${previousWire.layer} to ${routePoint.layer} without a via`,
|
|
199
|
+
})
|
|
200
|
+
} else if (!pointsMatch(previousWire, routePoint)) {
|
|
201
|
+
segments.push({
|
|
202
|
+
start: { x: previousWire.x, y: previousWire.y },
|
|
203
|
+
end: { x: routePoint.x, y: routePoint.y },
|
|
204
|
+
width: routePoint.width,
|
|
205
|
+
layer: routePoint.layer,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
previousWire = routePoint
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (pendingVia) {
|
|
213
|
+
addIssue(issues, {
|
|
214
|
+
code: "disconnected-trace",
|
|
215
|
+
traceId: trace.pcb_trace_id,
|
|
216
|
+
connectionName,
|
|
217
|
+
message: `Trace ${trace.pcb_trace_id} ends at a via without a wire on ${pendingVia.to_layer}`,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { trace, connectionName, segments, vias }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Independently audits every emitted trace and via against the original SRJ
|
|
226
|
+
* obstacle field and every other emitted net. This deliberately does not rely
|
|
227
|
+
* on the solver's accepted route-plan objects.
|
|
228
|
+
*/
|
|
229
|
+
export function validateRoutedCopperDrc(params: {
|
|
230
|
+
inputSrj: SimpleRouteJson
|
|
231
|
+
routedSrj: SimpleRouteJson
|
|
232
|
+
clearance: number
|
|
233
|
+
}): RoutedCopperDrcReport {
|
|
234
|
+
const { inputSrj, routedSrj, clearance } = params
|
|
235
|
+
const issues: RoutedCopperDrcIssue[] = []
|
|
236
|
+
const layerNames = getCopperLayerNames(routedSrj.layerCount)
|
|
237
|
+
const traceCopper: TraceCopper[] = []
|
|
238
|
+
|
|
239
|
+
for (const trace of routedSrj.traces ?? []) {
|
|
240
|
+
const connectionName = trace.connection_name
|
|
241
|
+
if (
|
|
242
|
+
!connectionName ||
|
|
243
|
+
!inputSrj.connections.some(
|
|
244
|
+
(connection) => connection.name === connectionName,
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
addIssue(issues, {
|
|
248
|
+
code: "unknown-trace-connection",
|
|
249
|
+
traceId: trace.pcb_trace_id,
|
|
250
|
+
...(connectionName ? { connectionName } : {}),
|
|
251
|
+
message: `Trace ${trace.pcb_trace_id} does not identify an input connection`,
|
|
252
|
+
})
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
traceCopper.push(
|
|
256
|
+
extractTraceCopper({
|
|
257
|
+
srj: routedSrj,
|
|
258
|
+
trace,
|
|
259
|
+
connectionName,
|
|
260
|
+
layerNames,
|
|
261
|
+
issues,
|
|
262
|
+
}),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const copper of traceCopper) {
|
|
267
|
+
for (const segment of copper.segments) {
|
|
268
|
+
for (const obstacle of inputSrj.obstacles) {
|
|
269
|
+
if (
|
|
270
|
+
!obstacle.layers.includes(segment.layer) ||
|
|
271
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)
|
|
272
|
+
) {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
isLegalTerminalBodyEscape({
|
|
277
|
+
inputSrj,
|
|
278
|
+
segment,
|
|
279
|
+
bodyObstacle: obstacle,
|
|
280
|
+
connectionName: copper.connectionName,
|
|
281
|
+
})
|
|
282
|
+
) {
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
const actual = distanceSegmentToObstacle(segment, obstacle)
|
|
286
|
+
const required = segment.width / 2 + clearance
|
|
287
|
+
if (actual < required - EPSILON) {
|
|
288
|
+
addIssue(issues, {
|
|
289
|
+
code: "trace-obstacle-clearance",
|
|
290
|
+
traceId: copper.trace.pcb_trace_id,
|
|
291
|
+
connectionName: copper.connectionName,
|
|
292
|
+
layer: segment.layer,
|
|
293
|
+
obstacleId: obstacle.obstacleId,
|
|
294
|
+
message: `Trace ${copper.trace.pcb_trace_id} on ${segment.layer} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId}; ${required.toFixed(4)}mm is required`,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const via of copper.vias) {
|
|
300
|
+
for (const obstacle of inputSrj.obstacles) {
|
|
301
|
+
if (
|
|
302
|
+
!obstacle.layers.some((layer) => via.spanLayers.includes(layer)) ||
|
|
303
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)
|
|
304
|
+
) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const actual = distancePointToObstacle(via.center, obstacle)
|
|
308
|
+
const required = via.diameter / 2 + clearance
|
|
309
|
+
if (actual < required - EPSILON) {
|
|
310
|
+
addIssue(issues, {
|
|
311
|
+
code: "via-obstacle-clearance",
|
|
312
|
+
traceId: copper.trace.pcb_trace_id,
|
|
313
|
+
connectionName: copper.connectionName,
|
|
314
|
+
obstacleId: obstacle.obstacleId,
|
|
315
|
+
message: `Via in ${copper.trace.pcb_trace_id} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId} on its layer span; ${required.toFixed(4)}mm is required`,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (let firstIndex = 0; firstIndex < traceCopper.length; firstIndex++) {
|
|
323
|
+
const first = traceCopper[firstIndex]!
|
|
324
|
+
for (
|
|
325
|
+
let secondIndex = firstIndex + 1;
|
|
326
|
+
secondIndex < traceCopper.length;
|
|
327
|
+
secondIndex++
|
|
328
|
+
) {
|
|
329
|
+
const second = traceCopper[secondIndex]!
|
|
330
|
+
if (
|
|
331
|
+
connectionsShareElectricalNet(
|
|
332
|
+
inputSrj,
|
|
333
|
+
first.connectionName,
|
|
334
|
+
second.connectionName,
|
|
335
|
+
)
|
|
336
|
+
) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const firstSegment of first.segments) {
|
|
341
|
+
for (const secondSegment of second.segments) {
|
|
342
|
+
if (segmentsAreClear(firstSegment, secondSegment, clearance)) continue
|
|
343
|
+
addIssue(issues, {
|
|
344
|
+
code: "different-net-trace-clearance",
|
|
345
|
+
traceId: first.trace.pcb_trace_id,
|
|
346
|
+
connectionName: first.connectionName,
|
|
347
|
+
otherTraceId: second.trace.pcb_trace_id,
|
|
348
|
+
otherConnectionName: second.connectionName,
|
|
349
|
+
layer: firstSegment.layer,
|
|
350
|
+
message: `Different-net traces ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} intersect or violate clearance on ${firstSegment.layer}`,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
for (const secondVia of second.vias) {
|
|
354
|
+
if (
|
|
355
|
+
!secondVia.spanLayers.includes(firstSegment.layer) ||
|
|
356
|
+
distancePointToSegment(
|
|
357
|
+
secondVia.center,
|
|
358
|
+
firstSegment.start,
|
|
359
|
+
firstSegment.end,
|
|
360
|
+
) >=
|
|
361
|
+
secondVia.diameter / 2 +
|
|
362
|
+
firstSegment.width / 2 +
|
|
363
|
+
clearance -
|
|
364
|
+
EPSILON
|
|
365
|
+
) {
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
addIssue(issues, {
|
|
369
|
+
code: "different-net-trace-via-clearance",
|
|
370
|
+
traceId: first.trace.pcb_trace_id,
|
|
371
|
+
connectionName: first.connectionName,
|
|
372
|
+
otherTraceId: second.trace.pcb_trace_id,
|
|
373
|
+
otherConnectionName: second.connectionName,
|
|
374
|
+
layer: firstSegment.layer,
|
|
375
|
+
message: `Trace ${first.trace.pcb_trace_id} violates via clearance to ${second.trace.pcb_trace_id} on ${firstSegment.layer}`,
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const firstVia of first.vias) {
|
|
381
|
+
for (const secondSegment of second.segments) {
|
|
382
|
+
if (
|
|
383
|
+
!firstVia.spanLayers.includes(secondSegment.layer) ||
|
|
384
|
+
distancePointToSegment(
|
|
385
|
+
firstVia.center,
|
|
386
|
+
secondSegment.start,
|
|
387
|
+
secondSegment.end,
|
|
388
|
+
) >=
|
|
389
|
+
firstVia.diameter / 2 +
|
|
390
|
+
secondSegment.width / 2 +
|
|
391
|
+
clearance -
|
|
392
|
+
EPSILON
|
|
393
|
+
) {
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
addIssue(issues, {
|
|
397
|
+
code: "different-net-trace-via-clearance",
|
|
398
|
+
traceId: first.trace.pcb_trace_id,
|
|
399
|
+
connectionName: first.connectionName,
|
|
400
|
+
otherTraceId: second.trace.pcb_trace_id,
|
|
401
|
+
otherConnectionName: second.connectionName,
|
|
402
|
+
layer: secondSegment.layer,
|
|
403
|
+
message: `Via in ${first.trace.pcb_trace_id} violates trace clearance to ${second.trace.pcb_trace_id} on ${secondSegment.layer}`,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
for (const secondVia of second.vias) {
|
|
407
|
+
if (
|
|
408
|
+
!firstVia.spanLayers.some((layer) =>
|
|
409
|
+
secondVia.spanLayers.includes(layer),
|
|
410
|
+
) ||
|
|
411
|
+
distance(firstVia.center, secondVia.center) >=
|
|
412
|
+
(firstVia.diameter + secondVia.diameter) / 2 + clearance - EPSILON
|
|
413
|
+
) {
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
addIssue(issues, {
|
|
417
|
+
code: "different-net-via-clearance",
|
|
418
|
+
traceId: first.trace.pcb_trace_id,
|
|
419
|
+
connectionName: first.connectionName,
|
|
420
|
+
otherTraceId: second.trace.pcb_trace_id,
|
|
421
|
+
otherConnectionName: second.connectionName,
|
|
422
|
+
message: `Different-net vias in ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} violate clearance on an overlapping layer span`,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
valid: issues.length === 0,
|
|
431
|
+
checkedTraceCount: routedSrj.traces?.length ?? 0,
|
|
432
|
+
checkedSegmentCount: traceCopper.reduce(
|
|
433
|
+
(count, copper) => count + copper.segments.length,
|
|
434
|
+
0,
|
|
435
|
+
),
|
|
436
|
+
checkedViaCount: traceCopper.reduce(
|
|
437
|
+
(count, copper) => count + copper.vias.length,
|
|
438
|
+
0,
|
|
439
|
+
),
|
|
440
|
+
issues,
|
|
441
|
+
}
|
|
442
|
+
}
|