@tscircuit/fanout-solver 0.0.18 → 0.0.20
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 +41 -22
- package/lib/fanout-solver.ts +111 -18
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +108 -3
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionPoint,
|
|
3
|
+
SimpleRouteJson,
|
|
4
|
+
SimplifiedPcbTrace,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import {
|
|
7
|
+
distance,
|
|
8
|
+
distancePointToObstacle,
|
|
9
|
+
distancePointToSegment,
|
|
10
|
+
distanceSegmentToObstacle,
|
|
11
|
+
distanceSegmentToSegment,
|
|
12
|
+
segmentsAreClear,
|
|
13
|
+
} from "./geometry"
|
|
14
|
+
import {
|
|
15
|
+
connectionsShareElectricalNet,
|
|
16
|
+
obstacleSharesElectricalNet,
|
|
17
|
+
} from "./net-identity"
|
|
18
|
+
import type {
|
|
19
|
+
Bounds,
|
|
20
|
+
FanoutRoutePlan,
|
|
21
|
+
FanoutValidationIssue,
|
|
22
|
+
FanoutValidationReport,
|
|
23
|
+
Point2D,
|
|
24
|
+
PreparedBus,
|
|
25
|
+
RoutedSegment,
|
|
26
|
+
} from "./types"
|
|
27
|
+
|
|
28
|
+
const EPSILON = 1e-6
|
|
29
|
+
|
|
30
|
+
function pointsMatch(first: Point2D, second: Point2D): boolean {
|
|
31
|
+
return distance(first, second) <= EPSILON
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getPointLayers(point: ConnectionPoint): string[] {
|
|
35
|
+
return "layer" in point ? [point.layer] : point.layers
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function connectionPointsMatch(
|
|
39
|
+
first: ConnectionPoint,
|
|
40
|
+
second: ConnectionPoint,
|
|
41
|
+
): boolean {
|
|
42
|
+
return (
|
|
43
|
+
pointsMatch(first, second) &&
|
|
44
|
+
getPointLayers(first).join("\0") === getPointLayers(second).join("\0") &&
|
|
45
|
+
first.pointId === second.pointId &&
|
|
46
|
+
first.pcb_port_id === second.pcb_port_id
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pointIsOnBoundary(point: Point2D, boundary: Bounds): boolean {
|
|
51
|
+
const inside =
|
|
52
|
+
point.x >= boundary.minX - EPSILON &&
|
|
53
|
+
point.x <= boundary.maxX + EPSILON &&
|
|
54
|
+
point.y >= boundary.minY - EPSILON &&
|
|
55
|
+
point.y <= boundary.maxY + EPSILON
|
|
56
|
+
const onEdge =
|
|
57
|
+
Math.abs(point.x - boundary.minX) <= EPSILON ||
|
|
58
|
+
Math.abs(point.x - boundary.maxX) <= EPSILON ||
|
|
59
|
+
Math.abs(point.y - boundary.minY) <= EPSILON ||
|
|
60
|
+
Math.abs(point.y - boundary.maxY) <= EPSILON
|
|
61
|
+
return inside && onEdge
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pointIsInsideBounds(point: Point2D, bounds: Bounds): boolean {
|
|
65
|
+
return (
|
|
66
|
+
point.x >= bounds.minX - EPSILON &&
|
|
67
|
+
point.x <= bounds.maxX + EPSILON &&
|
|
68
|
+
point.y >= bounds.minY - EPSILON &&
|
|
69
|
+
point.y <= bounds.maxY + EPSILON
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addIssue(
|
|
74
|
+
issues: FanoutValidationIssue[],
|
|
75
|
+
code: FanoutValidationIssue["code"],
|
|
76
|
+
message: string,
|
|
77
|
+
plan?: FanoutRoutePlan,
|
|
78
|
+
otherConnectionName?: string,
|
|
79
|
+
): void {
|
|
80
|
+
issues.push({
|
|
81
|
+
code,
|
|
82
|
+
message,
|
|
83
|
+
...(plan
|
|
84
|
+
? {
|
|
85
|
+
connectionName: plan.connectionName,
|
|
86
|
+
busId: plan.busId,
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
...(otherConnectionName ? { otherConnectionName } : {}),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractTraceSegments(params: {
|
|
94
|
+
trace: SimplifiedPcbTrace
|
|
95
|
+
plan: FanoutRoutePlan
|
|
96
|
+
issues: FanoutValidationIssue[]
|
|
97
|
+
}): RoutedSegment[] {
|
|
98
|
+
const { trace, plan, issues } = params
|
|
99
|
+
const segments: RoutedSegment[] = []
|
|
100
|
+
let previousWire:
|
|
101
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
102
|
+
| undefined
|
|
103
|
+
let pendingVia:
|
|
104
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "via" }>
|
|
105
|
+
| undefined
|
|
106
|
+
|
|
107
|
+
for (const routePoint of trace.route) {
|
|
108
|
+
if (routePoint.route_type === "via") {
|
|
109
|
+
if (
|
|
110
|
+
!previousWire ||
|
|
111
|
+
!pointsMatch(previousWire, routePoint) ||
|
|
112
|
+
previousWire.layer !== routePoint.from_layer
|
|
113
|
+
) {
|
|
114
|
+
addIssue(
|
|
115
|
+
issues,
|
|
116
|
+
"disconnected-trace",
|
|
117
|
+
`Trace ${trace.pcb_trace_id} reaches a via without a matching ${routePoint.from_layer} wire endpoint`,
|
|
118
|
+
plan,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
pendingVia = routePoint
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (routePoint.route_type !== "wire") {
|
|
125
|
+
addIssue(
|
|
126
|
+
issues,
|
|
127
|
+
"unsupported-route-point",
|
|
128
|
+
`Trace ${trace.pcb_trace_id} contains unsupported ${routePoint.route_type} geometry`,
|
|
129
|
+
plan,
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (pendingVia) {
|
|
135
|
+
if (
|
|
136
|
+
!pointsMatch(routePoint, pendingVia) ||
|
|
137
|
+
routePoint.layer !== pendingVia.to_layer
|
|
138
|
+
) {
|
|
139
|
+
addIssue(
|
|
140
|
+
issues,
|
|
141
|
+
"disconnected-trace",
|
|
142
|
+
`Trace ${trace.pcb_trace_id} does not continue from its via on ${pendingVia.to_layer}`,
|
|
143
|
+
plan,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
previousWire = routePoint
|
|
147
|
+
pendingVia = undefined
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (previousWire) {
|
|
152
|
+
if (previousWire.layer !== routePoint.layer) {
|
|
153
|
+
addIssue(
|
|
154
|
+
issues,
|
|
155
|
+
"disconnected-trace",
|
|
156
|
+
`Trace ${trace.pcb_trace_id} changes from ${previousWire.layer} to ${routePoint.layer} without a via`,
|
|
157
|
+
plan,
|
|
158
|
+
)
|
|
159
|
+
} else if (!pointsMatch(previousWire, routePoint)) {
|
|
160
|
+
segments.push({
|
|
161
|
+
start: { x: previousWire.x, y: previousWire.y },
|
|
162
|
+
end: { x: routePoint.x, y: routePoint.y },
|
|
163
|
+
width: routePoint.width,
|
|
164
|
+
layer: routePoint.layer,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
previousWire = routePoint
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (pendingVia) {
|
|
172
|
+
addIssue(
|
|
173
|
+
issues,
|
|
174
|
+
"disconnected-trace",
|
|
175
|
+
`Trace ${trace.pcb_trace_id} ends at a via without a wire on ${pendingVia.to_layer}`,
|
|
176
|
+
plan,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return segments
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validatePlanStructure(params: {
|
|
183
|
+
plan: FanoutRoutePlan
|
|
184
|
+
preparedBus: PreparedBus | undefined
|
|
185
|
+
inputSrj: SimpleRouteJson
|
|
186
|
+
outputSrj: SimpleRouteJson
|
|
187
|
+
sharedBoundary: Bounds
|
|
188
|
+
issues: FanoutValidationIssue[]
|
|
189
|
+
}): void {
|
|
190
|
+
const { plan, preparedBus, inputSrj, outputSrj, sharedBoundary, issues } =
|
|
191
|
+
params
|
|
192
|
+
const inputConnection = inputSrj.connections[plan.connectionIndex]
|
|
193
|
+
if (!inputConnection || inputConnection.name !== plan.connectionName) {
|
|
194
|
+
addIssue(
|
|
195
|
+
issues,
|
|
196
|
+
"connection-mismatch",
|
|
197
|
+
`Plan index ${plan.connectionIndex} does not identify connection ${plan.connectionName}`,
|
|
198
|
+
plan,
|
|
199
|
+
)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const preparedConnection = preparedBus?.connections.find(
|
|
203
|
+
(connection) => connection.connectionIndex === plan.connectionIndex,
|
|
204
|
+
)
|
|
205
|
+
if (
|
|
206
|
+
!preparedConnection ||
|
|
207
|
+
preparedConnection.sourcePointIndex !== plan.sourcePointIndex ||
|
|
208
|
+
!connectionPointsMatch(preparedConnection.sourcePoint, plan.sourcePoint) ||
|
|
209
|
+
preparedConnection.sourceObstacle.obstacleId !==
|
|
210
|
+
plan.sourceObstacle.obstacleId
|
|
211
|
+
) {
|
|
212
|
+
addIssue(
|
|
213
|
+
issues,
|
|
214
|
+
"source-mismatch",
|
|
215
|
+
`Plan ${plan.connectionName} does not start at its prepared component endpoint`,
|
|
216
|
+
plan,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
if (preparedBus?.termination.type !== plan.termination.type) {
|
|
220
|
+
addIssue(
|
|
221
|
+
issues,
|
|
222
|
+
"termination-mismatch",
|
|
223
|
+
`Plan ${plan.connectionName} does not use its bus termination`,
|
|
224
|
+
plan,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
if (plan.segments.length === 0 || plan.length <= EPSILON) {
|
|
228
|
+
addIssue(
|
|
229
|
+
issues,
|
|
230
|
+
"not-broken-out",
|
|
231
|
+
`Plan ${plan.connectionName} has no non-zero escape geometry`,
|
|
232
|
+
plan,
|
|
233
|
+
)
|
|
234
|
+
} else {
|
|
235
|
+
const routableBounds = {
|
|
236
|
+
minX: Math.min(inputSrj.bounds.minX, sharedBoundary.minX),
|
|
237
|
+
maxX: Math.max(inputSrj.bounds.maxX, sharedBoundary.maxX),
|
|
238
|
+
minY: Math.min(inputSrj.bounds.minY, sharedBoundary.minY),
|
|
239
|
+
maxY: Math.max(inputSrj.bounds.maxY, sharedBoundary.maxY),
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
plan.segments.some(
|
|
243
|
+
(segment) =>
|
|
244
|
+
!pointIsInsideBounds(segment.start, routableBounds) ||
|
|
245
|
+
!pointIsInsideBounds(segment.end, routableBounds),
|
|
246
|
+
)
|
|
247
|
+
) {
|
|
248
|
+
addIssue(
|
|
249
|
+
issues,
|
|
250
|
+
"outside-routing-bounds",
|
|
251
|
+
`Plan ${plan.connectionName} leaves the routable SRJ/shared-boundary area`,
|
|
252
|
+
plan,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
if (!pointsMatch(plan.segments[0]!.start, plan.sourcePoint)) {
|
|
256
|
+
addIssue(
|
|
257
|
+
issues,
|
|
258
|
+
"disconnected-trace",
|
|
259
|
+
`Plan ${plan.connectionName} does not start at its source pad`,
|
|
260
|
+
plan,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
if (!pointsMatch(plan.segments.at(-1)!.end, plan.exitPoint)) {
|
|
264
|
+
addIssue(
|
|
265
|
+
issues,
|
|
266
|
+
"disconnected-trace",
|
|
267
|
+
`Plan ${plan.connectionName} does not end at its declared exit`,
|
|
268
|
+
plan,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
for (let index = 1; index < plan.segments.length; index++) {
|
|
272
|
+
const previous = plan.segments[index - 1]!
|
|
273
|
+
const current = plan.segments[index]!
|
|
274
|
+
if (!pointsMatch(previous.end, current.start)) {
|
|
275
|
+
addIssue(
|
|
276
|
+
issues,
|
|
277
|
+
"disconnected-trace",
|
|
278
|
+
`Plan ${plan.connectionName} has a gap between route segments`,
|
|
279
|
+
plan,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
previous.layer !== current.layer &&
|
|
284
|
+
(!plan.via ||
|
|
285
|
+
!pointsMatch(previous.end, plan.via.center) ||
|
|
286
|
+
!plan.via.spanLayers.includes(previous.layer) ||
|
|
287
|
+
!plan.via.spanLayers.includes(current.layer))
|
|
288
|
+
) {
|
|
289
|
+
addIssue(
|
|
290
|
+
issues,
|
|
291
|
+
"disconnected-trace",
|
|
292
|
+
`Plan ${plan.connectionName} changes layers without a connecting via`,
|
|
293
|
+
plan,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const traceSegments = extractTraceSegments({
|
|
300
|
+
trace: plan.trace,
|
|
301
|
+
plan,
|
|
302
|
+
issues,
|
|
303
|
+
})
|
|
304
|
+
if (
|
|
305
|
+
traceSegments.length !== plan.segments.length ||
|
|
306
|
+
traceSegments.some((segment, index) => {
|
|
307
|
+
const declared = plan.segments[index]
|
|
308
|
+
return (
|
|
309
|
+
!declared ||
|
|
310
|
+
segment.layer !== declared.layer ||
|
|
311
|
+
Math.abs(segment.width - declared.width) > EPSILON ||
|
|
312
|
+
!pointsMatch(segment.start, declared.start) ||
|
|
313
|
+
!pointsMatch(segment.end, declared.end)
|
|
314
|
+
)
|
|
315
|
+
})
|
|
316
|
+
) {
|
|
317
|
+
addIssue(
|
|
318
|
+
issues,
|
|
319
|
+
"trace-plan-mismatch",
|
|
320
|
+
`Trace ${plan.trace.pcb_trace_id} does not encode its declared route segments`,
|
|
321
|
+
plan,
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const firstRoutePoint = plan.trace.route.find(
|
|
326
|
+
(routePoint): routePoint is Extract<typeof routePoint, { x: number }> =>
|
|
327
|
+
"x" in routePoint && "y" in routePoint,
|
|
328
|
+
)
|
|
329
|
+
const lastRoutePoint = [...plan.trace.route]
|
|
330
|
+
.reverse()
|
|
331
|
+
.find(
|
|
332
|
+
(routePoint): routePoint is Extract<typeof routePoint, { x: number }> =>
|
|
333
|
+
"x" in routePoint && "y" in routePoint,
|
|
334
|
+
)
|
|
335
|
+
if (
|
|
336
|
+
!firstRoutePoint ||
|
|
337
|
+
!lastRoutePoint ||
|
|
338
|
+
!pointsMatch(firstRoutePoint, plan.sourcePoint) ||
|
|
339
|
+
!pointsMatch(lastRoutePoint, plan.exitPoint)
|
|
340
|
+
) {
|
|
341
|
+
addIssue(
|
|
342
|
+
issues,
|
|
343
|
+
"disconnected-trace",
|
|
344
|
+
`Trace ${plan.trace.pcb_trace_id} does not span its source and exit`,
|
|
345
|
+
plan,
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const outputConnection = outputSrj.connections.find(
|
|
350
|
+
(connection) => connection.name === plan.connectionName,
|
|
351
|
+
)
|
|
352
|
+
if (plan.termination.type === "boundary") {
|
|
353
|
+
if (!outputConnection) {
|
|
354
|
+
addIssue(
|
|
355
|
+
issues,
|
|
356
|
+
"output-connection-missing",
|
|
357
|
+
`Boundary connection ${plan.connectionName} was removed from the output`,
|
|
358
|
+
plan,
|
|
359
|
+
)
|
|
360
|
+
} else {
|
|
361
|
+
const outputSource =
|
|
362
|
+
outputConnection.pointsToConnect[plan.sourcePointIndex]
|
|
363
|
+
if (
|
|
364
|
+
!outputSource ||
|
|
365
|
+
!pointsMatch(outputSource, plan.exitPoint) ||
|
|
366
|
+
!("layer" in outputSource) ||
|
|
367
|
+
outputSource.layer !== plan.targetLayer
|
|
368
|
+
) {
|
|
369
|
+
addIssue(
|
|
370
|
+
issues,
|
|
371
|
+
"output-exit-mismatch",
|
|
372
|
+
`Output connection ${plan.connectionName} is not attached to its fanout exit`,
|
|
373
|
+
plan,
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
for (
|
|
377
|
+
let index = 0;
|
|
378
|
+
index < inputConnection.pointsToConnect.length;
|
|
379
|
+
index++
|
|
380
|
+
) {
|
|
381
|
+
if (index === plan.sourcePointIndex) continue
|
|
382
|
+
const inputPoint = inputConnection.pointsToConnect[index]
|
|
383
|
+
const outputPoint = outputConnection.pointsToConnect[index]
|
|
384
|
+
if (
|
|
385
|
+
!inputPoint ||
|
|
386
|
+
!outputPoint ||
|
|
387
|
+
!connectionPointsMatch(inputPoint, outputPoint)
|
|
388
|
+
) {
|
|
389
|
+
addIssue(
|
|
390
|
+
issues,
|
|
391
|
+
"downstream-endpoint-lost",
|
|
392
|
+
`Output connection ${plan.connectionName} did not retain downstream endpoint ${index}`,
|
|
393
|
+
plan,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} else if (outputConnection) {
|
|
399
|
+
addIssue(
|
|
400
|
+
issues,
|
|
401
|
+
"plane-connection-retained",
|
|
402
|
+
`Plane-terminated connection ${plan.connectionName} remains in the output`,
|
|
403
|
+
plan,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function plansHaveConnectedCopper(
|
|
409
|
+
first: FanoutRoutePlan,
|
|
410
|
+
second: FanoutRoutePlan,
|
|
411
|
+
): boolean {
|
|
412
|
+
for (const firstSegment of first.segments) {
|
|
413
|
+
for (const secondSegment of second.segments) {
|
|
414
|
+
if (
|
|
415
|
+
firstSegment.layer === secondSegment.layer &&
|
|
416
|
+
distanceSegmentToSegment(
|
|
417
|
+
firstSegment.start,
|
|
418
|
+
firstSegment.end,
|
|
419
|
+
secondSegment.start,
|
|
420
|
+
secondSegment.end,
|
|
421
|
+
) <=
|
|
422
|
+
(firstSegment.width + secondSegment.width) / 2 + EPSILON
|
|
423
|
+
) {
|
|
424
|
+
return true
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (
|
|
428
|
+
second.via?.spanLayers.includes(firstSegment.layer) &&
|
|
429
|
+
distancePointToSegment(
|
|
430
|
+
second.via.center,
|
|
431
|
+
firstSegment.start,
|
|
432
|
+
firstSegment.end,
|
|
433
|
+
) <=
|
|
434
|
+
second.via.diameter / 2 + firstSegment.width / 2 + EPSILON
|
|
435
|
+
) {
|
|
436
|
+
return true
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (first.via) {
|
|
440
|
+
for (const secondSegment of second.segments) {
|
|
441
|
+
if (
|
|
442
|
+
first.via.spanLayers.includes(secondSegment.layer) &&
|
|
443
|
+
distancePointToSegment(
|
|
444
|
+
first.via.center,
|
|
445
|
+
secondSegment.start,
|
|
446
|
+
secondSegment.end,
|
|
447
|
+
) <=
|
|
448
|
+
first.via.diameter / 2 + secondSegment.width / 2 + EPSILON
|
|
449
|
+
) {
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
second.via &&
|
|
455
|
+
first.via.spanLayers.some((layer) =>
|
|
456
|
+
second.via!.spanLayers.includes(layer),
|
|
457
|
+
) &&
|
|
458
|
+
distance(first.via.center, second.via.center) <=
|
|
459
|
+
(first.via.diameter + second.via.diameter) / 2 + EPSILON
|
|
460
|
+
) {
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validateBreakoutConnectivity(params: {
|
|
468
|
+
plans: readonly FanoutRoutePlan[]
|
|
469
|
+
inputSrj: SimpleRouteJson
|
|
470
|
+
sharedBoundary: Bounds
|
|
471
|
+
issues: FanoutValidationIssue[]
|
|
472
|
+
}): Set<FanoutRoutePlan> {
|
|
473
|
+
const { plans, inputSrj, sharedBoundary, issues } = params
|
|
474
|
+
const connectedPlans = new Set<FanoutRoutePlan>()
|
|
475
|
+
const neighboringPlans = new Map<FanoutRoutePlan, FanoutRoutePlan[]>()
|
|
476
|
+
for (const plan of plans) neighboringPlans.set(plan, [])
|
|
477
|
+
|
|
478
|
+
for (let firstIndex = 0; firstIndex < plans.length; firstIndex++) {
|
|
479
|
+
const first = plans[firstIndex]!
|
|
480
|
+
if (
|
|
481
|
+
first.termination.type === "plane"
|
|
482
|
+
? Boolean(first.via)
|
|
483
|
+
: first.segments.some(
|
|
484
|
+
(segment) =>
|
|
485
|
+
pointIsOnBoundary(segment.start, sharedBoundary) ||
|
|
486
|
+
pointIsOnBoundary(segment.end, sharedBoundary),
|
|
487
|
+
)
|
|
488
|
+
) {
|
|
489
|
+
connectedPlans.add(first)
|
|
490
|
+
}
|
|
491
|
+
for (
|
|
492
|
+
let secondIndex = firstIndex + 1;
|
|
493
|
+
secondIndex < plans.length;
|
|
494
|
+
secondIndex++
|
|
495
|
+
) {
|
|
496
|
+
const second = plans[secondIndex]!
|
|
497
|
+
if (
|
|
498
|
+
!connectionsShareElectricalNet(
|
|
499
|
+
inputSrj,
|
|
500
|
+
first.connectionName,
|
|
501
|
+
second.connectionName,
|
|
502
|
+
) ||
|
|
503
|
+
!plansHaveConnectedCopper(first, second)
|
|
504
|
+
) {
|
|
505
|
+
continue
|
|
506
|
+
}
|
|
507
|
+
neighboringPlans.get(first)!.push(second)
|
|
508
|
+
neighboringPlans.get(second)!.push(first)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const queue = [...connectedPlans]
|
|
513
|
+
while (queue.length > 0) {
|
|
514
|
+
const plan = queue.shift()!
|
|
515
|
+
for (const neighbor of neighboringPlans.get(plan) ?? []) {
|
|
516
|
+
if (connectedPlans.has(neighbor)) continue
|
|
517
|
+
connectedPlans.add(neighbor)
|
|
518
|
+
queue.push(neighbor)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (const plan of plans) {
|
|
523
|
+
if (connectedPlans.has(plan)) continue
|
|
524
|
+
addIssue(
|
|
525
|
+
issues,
|
|
526
|
+
"not-broken-out",
|
|
527
|
+
plan.termination.type === "boundary"
|
|
528
|
+
? `Connection ${plan.connectionName} has no continuous same-net copper path to the shared boundary`
|
|
529
|
+
: `Plane connection ${plan.connectionName} has no terminating via`,
|
|
530
|
+
plan,
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
return connectedPlans
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function validateClearances(params: {
|
|
537
|
+
plans: readonly FanoutRoutePlan[]
|
|
538
|
+
inputSrj: SimpleRouteJson
|
|
539
|
+
clearance: number
|
|
540
|
+
issues: FanoutValidationIssue[]
|
|
541
|
+
}): void {
|
|
542
|
+
const { plans, inputSrj, clearance, issues } = params
|
|
543
|
+
for (const plan of plans) {
|
|
544
|
+
for (
|
|
545
|
+
let segmentIndex = 0;
|
|
546
|
+
segmentIndex < plan.segments.length;
|
|
547
|
+
segmentIndex++
|
|
548
|
+
) {
|
|
549
|
+
const segment = plan.segments[segmentIndex]!
|
|
550
|
+
for (const obstacle of inputSrj.obstacles) {
|
|
551
|
+
if (!obstacle.layers.includes(segment.layer)) continue
|
|
552
|
+
if (
|
|
553
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, plan.connectionName)
|
|
554
|
+
) {
|
|
555
|
+
continue
|
|
556
|
+
}
|
|
557
|
+
if (
|
|
558
|
+
segmentIndex === 0 &&
|
|
559
|
+
obstacle.obstacleId === plan.sourceObstacle.obstacleId &&
|
|
560
|
+
segment.layer === plan.sourceLayer
|
|
561
|
+
) {
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
const actual = distanceSegmentToObstacle(segment, obstacle)
|
|
565
|
+
const required = segment.width / 2 + clearance
|
|
566
|
+
if (actual < required - 1e-9) {
|
|
567
|
+
addIssue(
|
|
568
|
+
issues,
|
|
569
|
+
"obstacle-clearance",
|
|
570
|
+
`Trace ${plan.connectionName} on ${segment.layer} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId}; ${required.toFixed(4)}mm is required`,
|
|
571
|
+
plan,
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (plan.via) {
|
|
577
|
+
for (const obstacle of inputSrj.obstacles) {
|
|
578
|
+
if (
|
|
579
|
+
!obstacle.layers.some((layer) =>
|
|
580
|
+
plan.via!.spanLayers.includes(layer),
|
|
581
|
+
) ||
|
|
582
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, plan.connectionName)
|
|
583
|
+
) {
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
const actual = distancePointToObstacle(plan.via.center, obstacle)
|
|
587
|
+
const required = plan.via.diameter / 2 + clearance
|
|
588
|
+
if (actual < required - 1e-9) {
|
|
589
|
+
addIssue(
|
|
590
|
+
issues,
|
|
591
|
+
"via-obstacle-clearance",
|
|
592
|
+
`Via ${plan.connectionName} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId} on its layer span; ${required.toFixed(4)}mm is required`,
|
|
593
|
+
plan,
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (let firstIndex = 0; firstIndex < plans.length; firstIndex++) {
|
|
601
|
+
const first = plans[firstIndex]!
|
|
602
|
+
for (
|
|
603
|
+
let secondIndex = firstIndex + 1;
|
|
604
|
+
secondIndex < plans.length;
|
|
605
|
+
secondIndex++
|
|
606
|
+
) {
|
|
607
|
+
const second = plans[secondIndex]!
|
|
608
|
+
if (
|
|
609
|
+
connectionsShareElectricalNet(
|
|
610
|
+
inputSrj,
|
|
611
|
+
first.connectionName,
|
|
612
|
+
second.connectionName,
|
|
613
|
+
)
|
|
614
|
+
) {
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
for (const firstSegment of first.segments) {
|
|
618
|
+
for (const secondSegment of second.segments) {
|
|
619
|
+
if (!segmentsAreClear(firstSegment, secondSegment, clearance)) {
|
|
620
|
+
addIssue(
|
|
621
|
+
issues,
|
|
622
|
+
"different-net-trace-clearance",
|
|
623
|
+
`Different-net traces ${first.connectionName} and ${second.connectionName} intersect or violate clearance on ${firstSegment.layer}`,
|
|
624
|
+
first,
|
|
625
|
+
second.connectionName,
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
second.via?.spanLayers.includes(firstSegment.layer) &&
|
|
631
|
+
distancePointToSegment(
|
|
632
|
+
second.via.center,
|
|
633
|
+
firstSegment.start,
|
|
634
|
+
firstSegment.end,
|
|
635
|
+
) <
|
|
636
|
+
second.via.diameter / 2 + firstSegment.width / 2 + clearance - 1e-9
|
|
637
|
+
) {
|
|
638
|
+
addIssue(
|
|
639
|
+
issues,
|
|
640
|
+
"different-net-trace-via-clearance",
|
|
641
|
+
`Trace ${first.connectionName} violates via clearance to ${second.connectionName} on ${firstSegment.layer}`,
|
|
642
|
+
first,
|
|
643
|
+
second.connectionName,
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (first.via) {
|
|
648
|
+
for (const secondSegment of second.segments) {
|
|
649
|
+
if (
|
|
650
|
+
first.via.spanLayers.includes(secondSegment.layer) &&
|
|
651
|
+
distancePointToSegment(
|
|
652
|
+
first.via.center,
|
|
653
|
+
secondSegment.start,
|
|
654
|
+
secondSegment.end,
|
|
655
|
+
) <
|
|
656
|
+
first.via.diameter / 2 +
|
|
657
|
+
secondSegment.width / 2 +
|
|
658
|
+
clearance -
|
|
659
|
+
1e-9
|
|
660
|
+
) {
|
|
661
|
+
addIssue(
|
|
662
|
+
issues,
|
|
663
|
+
"different-net-trace-via-clearance",
|
|
664
|
+
`Via ${first.connectionName} violates trace clearance to ${second.connectionName} on ${secondSegment.layer}`,
|
|
665
|
+
first,
|
|
666
|
+
second.connectionName,
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
second.via &&
|
|
672
|
+
first.via.spanLayers.some((layer) =>
|
|
673
|
+
second.via!.spanLayers.includes(layer),
|
|
674
|
+
) &&
|
|
675
|
+
distance(first.via.center, second.via.center) <
|
|
676
|
+
(first.via.diameter + second.via.diameter) / 2 + clearance - 1e-9
|
|
677
|
+
) {
|
|
678
|
+
addIssue(
|
|
679
|
+
issues,
|
|
680
|
+
"different-net-via-clearance",
|
|
681
|
+
`Vias ${first.connectionName} and ${second.connectionName} violate clearance on an overlapping layer span`,
|
|
682
|
+
first,
|
|
683
|
+
second.connectionName,
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function validateFanoutSolution(params: {
|
|
692
|
+
inputSrj: SimpleRouteJson
|
|
693
|
+
outputSrj: SimpleRouteJson
|
|
694
|
+
plans: readonly FanoutRoutePlan[]
|
|
695
|
+
preparedBuses: readonly PreparedBus[]
|
|
696
|
+
sharedBoundary: Bounds
|
|
697
|
+
clearance: number
|
|
698
|
+
}): FanoutValidationReport {
|
|
699
|
+
const {
|
|
700
|
+
inputSrj,
|
|
701
|
+
outputSrj,
|
|
702
|
+
plans,
|
|
703
|
+
preparedBuses,
|
|
704
|
+
sharedBoundary,
|
|
705
|
+
clearance,
|
|
706
|
+
} = params
|
|
707
|
+
const issues: FanoutValidationIssue[] = []
|
|
708
|
+
const plansByConnection = new Map<string, FanoutRoutePlan[]>()
|
|
709
|
+
const preparedBusById = new Map(preparedBuses.map((bus) => [bus.busId, bus]))
|
|
710
|
+
for (const plan of plans) {
|
|
711
|
+
const connectionPlans = plansByConnection.get(plan.connectionName) ?? []
|
|
712
|
+
connectionPlans.push(plan)
|
|
713
|
+
plansByConnection.set(plan.connectionName, connectionPlans)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
for (const connection of inputSrj.connections) {
|
|
717
|
+
const connectionPlans = plansByConnection.get(connection.name) ?? []
|
|
718
|
+
if (connectionPlans.length === 0) {
|
|
719
|
+
addIssue(
|
|
720
|
+
issues,
|
|
721
|
+
"missing-plan",
|
|
722
|
+
`Connection ${connection.name} has no fanout plan`,
|
|
723
|
+
)
|
|
724
|
+
} else if (connectionPlans.length > 1) {
|
|
725
|
+
addIssue(
|
|
726
|
+
issues,
|
|
727
|
+
"duplicate-plan",
|
|
728
|
+
`Connection ${connection.name} has ${connectionPlans.length} fanout plans`,
|
|
729
|
+
connectionPlans[0],
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const plan of plans) {
|
|
734
|
+
if (
|
|
735
|
+
!inputSrj.connections.some(
|
|
736
|
+
(connection) => connection.name === plan.connectionName,
|
|
737
|
+
)
|
|
738
|
+
) {
|
|
739
|
+
addIssue(
|
|
740
|
+
issues,
|
|
741
|
+
"unknown-plan",
|
|
742
|
+
`Plan ${plan.connectionName} is not an input connection`,
|
|
743
|
+
plan,
|
|
744
|
+
)
|
|
745
|
+
continue
|
|
746
|
+
}
|
|
747
|
+
validatePlanStructure({
|
|
748
|
+
plan,
|
|
749
|
+
preparedBus: preparedBusById.get(plan.busId),
|
|
750
|
+
inputSrj,
|
|
751
|
+
outputSrj,
|
|
752
|
+
sharedBoundary,
|
|
753
|
+
issues,
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
const connectedPlans = validateBreakoutConnectivity({
|
|
757
|
+
plans,
|
|
758
|
+
inputSrj,
|
|
759
|
+
sharedBoundary,
|
|
760
|
+
issues,
|
|
761
|
+
})
|
|
762
|
+
validateClearances({ plans, inputSrj, clearance, issues })
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
valid: issues.length === 0,
|
|
766
|
+
checkedConnectionCount: inputSrj.connections.length,
|
|
767
|
+
brokenOutConnectionCount: new Set(
|
|
768
|
+
[...connectedPlans].map((plan) => plan.connectionName),
|
|
769
|
+
).size,
|
|
770
|
+
issues,
|
|
771
|
+
}
|
|
772
|
+
}
|