@tscircuit/fanout-solver 0.0.20 → 0.0.22
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 +25 -4
- package/lib/build-output.ts +30 -12
- package/lib/complete-original-endpoints.ts +1208 -0
- package/lib/fanout-solver.ts +150 -20
- package/lib/index.ts +15 -0
- package/lib/route-bus.ts +339 -87
- package/lib/route-single-layer-adaptive-exits.ts +1 -0
- package/lib/route-single-layer-push-shove.ts +1 -0
- package/lib/types.ts +50 -0
- package/lib/validate-fanout-solution.ts +152 -79
- package/lib/validate-original-endpoint-connectivity.ts +415 -0
- package/lib/validate-routed-copper-drc.ts +463 -0
- package/package.json +2 -2
|
@@ -0,0 +1,415 @@
|
|
|
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 {
|
|
23
|
+
Point2D,
|
|
24
|
+
RoutedSegment,
|
|
25
|
+
SimpleRouteJsonWithFanoutPlanes,
|
|
26
|
+
} from "./types"
|
|
27
|
+
|
|
28
|
+
const EPSILON = 1e-6
|
|
29
|
+
|
|
30
|
+
export interface OriginalEndpointConnectivityIssue {
|
|
31
|
+
code: "original-endpoints-disconnected"
|
|
32
|
+
connectionName: string
|
|
33
|
+
disconnectedEndpointIndices: number[]
|
|
34
|
+
message: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OriginalEndpointConnectivityReport {
|
|
38
|
+
valid: boolean
|
|
39
|
+
checkedConnectionCount: number
|
|
40
|
+
connectedConnectionCount: number
|
|
41
|
+
checkedEndpointCount: number
|
|
42
|
+
connectedEndpointCount: number
|
|
43
|
+
issues: OriginalEndpointConnectivityIssue[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface EndpointCopper {
|
|
47
|
+
type: "endpoint"
|
|
48
|
+
connectionName: string
|
|
49
|
+
endpointIndex: number
|
|
50
|
+
point: ConnectionPoint
|
|
51
|
+
layers: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ObstacleCopper {
|
|
55
|
+
type: "obstacle"
|
|
56
|
+
obstacle: Obstacle
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SegmentCopper {
|
|
60
|
+
type: "segment"
|
|
61
|
+
segment: RoutedSegment
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ViaCopper {
|
|
65
|
+
type: "via"
|
|
66
|
+
center: Point2D
|
|
67
|
+
diameter: number
|
|
68
|
+
layers: string[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PlaneCopper {
|
|
72
|
+
type: "plane"
|
|
73
|
+
layer: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type CopperPrimitive =
|
|
77
|
+
| EndpointCopper
|
|
78
|
+
| ObstacleCopper
|
|
79
|
+
| SegmentCopper
|
|
80
|
+
| ViaCopper
|
|
81
|
+
| PlaneCopper
|
|
82
|
+
|
|
83
|
+
function getPointLayers(point: ConnectionPoint): string[] {
|
|
84
|
+
return "layer" in point ? [point.layer] : point.layers
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function layersOverlap(first: readonly string[], second: readonly string[]) {
|
|
88
|
+
return first.some((layer) => second.includes(layer))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractTraceCopper(
|
|
92
|
+
trace: SimplifiedPcbTrace,
|
|
93
|
+
layerNames: string[],
|
|
94
|
+
): Array<SegmentCopper | ViaCopper> {
|
|
95
|
+
const copper: Array<SegmentCopper | ViaCopper> = []
|
|
96
|
+
let previousWire:
|
|
97
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
98
|
+
| undefined
|
|
99
|
+
|
|
100
|
+
for (const routePoint of trace.route) {
|
|
101
|
+
if (routePoint.route_type === "via") {
|
|
102
|
+
copper.push({
|
|
103
|
+
type: "via",
|
|
104
|
+
center: { x: routePoint.x, y: routePoint.y },
|
|
105
|
+
diameter: routePoint.via_diameter ?? 0,
|
|
106
|
+
layers: getLayerSpan(
|
|
107
|
+
routePoint.from_layer,
|
|
108
|
+
routePoint.to_layer,
|
|
109
|
+
layerNames,
|
|
110
|
+
),
|
|
111
|
+
})
|
|
112
|
+
previousWire = undefined
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (routePoint.route_type !== "wire") continue
|
|
116
|
+
if (
|
|
117
|
+
previousWire &&
|
|
118
|
+
previousWire.layer === routePoint.layer &&
|
|
119
|
+
distance(previousWire, routePoint) > EPSILON
|
|
120
|
+
) {
|
|
121
|
+
copper.push({
|
|
122
|
+
type: "segment",
|
|
123
|
+
segment: {
|
|
124
|
+
start: { x: previousWire.x, y: previousWire.y },
|
|
125
|
+
end: { x: routePoint.x, y: routePoint.y },
|
|
126
|
+
width: routePoint.width,
|
|
127
|
+
layer: routePoint.layer,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
previousWire = routePoint
|
|
132
|
+
}
|
|
133
|
+
return copper
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function primitivesTouch(first: CopperPrimitive, second: CopperPrimitive) {
|
|
137
|
+
if (first.type === "plane" && second.type === "plane") {
|
|
138
|
+
return first.layer === second.layer
|
|
139
|
+
}
|
|
140
|
+
if (first.type === "plane" && second.type === "via") {
|
|
141
|
+
return second.layers.includes(first.layer)
|
|
142
|
+
}
|
|
143
|
+
if (first.type === "via" && second.type === "plane") {
|
|
144
|
+
return primitivesTouch(second, first)
|
|
145
|
+
}
|
|
146
|
+
if (first.type === "plane" && second.type === "segment") {
|
|
147
|
+
return first.layer === second.segment.layer
|
|
148
|
+
}
|
|
149
|
+
if (first.type === "segment" && second.type === "plane") {
|
|
150
|
+
return primitivesTouch(second, first)
|
|
151
|
+
}
|
|
152
|
+
if (first.type === "endpoint" && second.type === "endpoint") {
|
|
153
|
+
return (
|
|
154
|
+
layersOverlap(first.layers, second.layers) &&
|
|
155
|
+
distance(first.point, second.point) <= EPSILON
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
if (first.type === "endpoint" && second.type === "obstacle") {
|
|
159
|
+
return (
|
|
160
|
+
layersOverlap(first.layers, second.obstacle.layers) &&
|
|
161
|
+
pointIsInsideObstacle(first.point, second.obstacle, EPSILON)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
if (first.type === "obstacle" && second.type === "endpoint") {
|
|
165
|
+
return primitivesTouch(second, first)
|
|
166
|
+
}
|
|
167
|
+
if (first.type === "endpoint" && second.type === "segment") {
|
|
168
|
+
return (
|
|
169
|
+
first.layers.includes(second.segment.layer) &&
|
|
170
|
+
distancePointToSegment(
|
|
171
|
+
first.point,
|
|
172
|
+
second.segment.start,
|
|
173
|
+
second.segment.end,
|
|
174
|
+
) <=
|
|
175
|
+
second.segment.width / 2 + EPSILON
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
if (first.type === "segment" && second.type === "endpoint") {
|
|
179
|
+
return primitivesTouch(second, first)
|
|
180
|
+
}
|
|
181
|
+
if (first.type === "endpoint" && second.type === "via") {
|
|
182
|
+
return (
|
|
183
|
+
layersOverlap(first.layers, second.layers) &&
|
|
184
|
+
distance(first.point, second.center) <= second.diameter / 2 + EPSILON
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
if (first.type === "via" && second.type === "endpoint") {
|
|
188
|
+
return primitivesTouch(second, first)
|
|
189
|
+
}
|
|
190
|
+
if (first.type === "obstacle" && second.type === "segment") {
|
|
191
|
+
return (
|
|
192
|
+
first.obstacle.layers.includes(second.segment.layer) &&
|
|
193
|
+
distanceSegmentToObstacle(second.segment, first.obstacle) <=
|
|
194
|
+
second.segment.width / 2 + EPSILON
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
if (first.type === "segment" && second.type === "obstacle") {
|
|
198
|
+
return primitivesTouch(second, first)
|
|
199
|
+
}
|
|
200
|
+
if (first.type === "obstacle" && second.type === "via") {
|
|
201
|
+
return (
|
|
202
|
+
layersOverlap(first.obstacle.layers, second.layers) &&
|
|
203
|
+
distancePointToObstacle(second.center, first.obstacle) <=
|
|
204
|
+
second.diameter / 2 + EPSILON
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
if (first.type === "via" && second.type === "obstacle") {
|
|
208
|
+
return primitivesTouch(second, first)
|
|
209
|
+
}
|
|
210
|
+
if (first.type === "segment" && second.type === "segment") {
|
|
211
|
+
return (
|
|
212
|
+
first.segment.layer === second.segment.layer &&
|
|
213
|
+
distanceSegmentToSegment(
|
|
214
|
+
first.segment.start,
|
|
215
|
+
first.segment.end,
|
|
216
|
+
second.segment.start,
|
|
217
|
+
second.segment.end,
|
|
218
|
+
) <=
|
|
219
|
+
(first.segment.width + second.segment.width) / 2 + EPSILON
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
if (first.type === "segment" && second.type === "via") {
|
|
223
|
+
return (
|
|
224
|
+
second.layers.includes(first.segment.layer) &&
|
|
225
|
+
distancePointToSegment(
|
|
226
|
+
second.center,
|
|
227
|
+
first.segment.start,
|
|
228
|
+
first.segment.end,
|
|
229
|
+
) <=
|
|
230
|
+
second.diameter / 2 + first.segment.width / 2 + EPSILON
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
if (first.type === "via" && second.type === "segment") {
|
|
234
|
+
return primitivesTouch(second, first)
|
|
235
|
+
}
|
|
236
|
+
if (first.type === "via" && second.type === "via") {
|
|
237
|
+
return (
|
|
238
|
+
layersOverlap(first.layers, second.layers) &&
|
|
239
|
+
distance(first.center, second.center) <=
|
|
240
|
+
(first.diameter + second.diameter) / 2 + EPSILON
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class DisjointSet {
|
|
247
|
+
private readonly parent: number[]
|
|
248
|
+
|
|
249
|
+
constructor(size: number) {
|
|
250
|
+
this.parent = Array.from({ length: size }, (_, index) => index)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
find(index: number): number {
|
|
254
|
+
const parent = this.parent[index]!
|
|
255
|
+
if (parent === index) return index
|
|
256
|
+
const root = this.find(parent)
|
|
257
|
+
this.parent[index] = root
|
|
258
|
+
return root
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
union(first: number, second: number): void {
|
|
262
|
+
const firstRoot = this.find(first)
|
|
263
|
+
const secondRoot = this.find(second)
|
|
264
|
+
if (firstRoot !== secondRoot) this.parent[secondRoot] = firstRoot
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function traceBelongsToNet(
|
|
269
|
+
inputSrj: SimpleRouteJson,
|
|
270
|
+
trace: SimplifiedPcbTrace,
|
|
271
|
+
representativeConnection: SimpleRouteConnection,
|
|
272
|
+
): boolean {
|
|
273
|
+
return Boolean(
|
|
274
|
+
trace.connection_name &&
|
|
275
|
+
connectionsShareElectricalNet(
|
|
276
|
+
inputSrj,
|
|
277
|
+
trace.connection_name,
|
|
278
|
+
representativeConnection.name,
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Independently proves that emitted copper connects every original SRJ
|
|
285
|
+
* endpoint. Merely reaching a boundary or retaining an endpoint in the output
|
|
286
|
+
* connection metadata does not count as connectivity.
|
|
287
|
+
*/
|
|
288
|
+
export function validateOriginalEndpointConnectivity(params: {
|
|
289
|
+
inputSrj: SimpleRouteJson
|
|
290
|
+
routedSrj: SimpleRouteJson
|
|
291
|
+
}): OriginalEndpointConnectivityReport {
|
|
292
|
+
const { inputSrj, routedSrj } = params
|
|
293
|
+
const planeConnectivity = (routedSrj as SimpleRouteJsonWithFanoutPlanes)
|
|
294
|
+
.fanoutPlaneConnectivity
|
|
295
|
+
const layerNames = getCopperLayerNames(routedSrj.layerCount)
|
|
296
|
+
const connectionsByNet = new Map<string, SimpleRouteConnection[]>()
|
|
297
|
+
for (const connection of inputSrj.connections) {
|
|
298
|
+
const netKey = getConnectionNetKey(connection)
|
|
299
|
+
const connections = connectionsByNet.get(netKey) ?? []
|
|
300
|
+
connections.push(connection)
|
|
301
|
+
connectionsByNet.set(netKey, connections)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const issues: OriginalEndpointConnectivityIssue[] = []
|
|
305
|
+
let connectedConnectionCount = 0
|
|
306
|
+
let connectedEndpointCount = 0
|
|
307
|
+
|
|
308
|
+
for (const connections of connectionsByNet.values()) {
|
|
309
|
+
const representativeConnection = connections[0]!
|
|
310
|
+
const endpointCopper: EndpointCopper[] = connections.flatMap((connection) =>
|
|
311
|
+
connection.pointsToConnect.map((point, endpointIndex) => ({
|
|
312
|
+
type: "endpoint" as const,
|
|
313
|
+
connectionName: connection.name,
|
|
314
|
+
endpointIndex,
|
|
315
|
+
point,
|
|
316
|
+
layers: getPointLayers(point),
|
|
317
|
+
})),
|
|
318
|
+
)
|
|
319
|
+
const obstacleCopper: ObstacleCopper[] = inputSrj.obstacles
|
|
320
|
+
.filter((obstacle) =>
|
|
321
|
+
obstacleSharesElectricalNet(
|
|
322
|
+
inputSrj,
|
|
323
|
+
obstacle,
|
|
324
|
+
representativeConnection.name,
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
.map((obstacle) => ({ type: "obstacle", obstacle }))
|
|
328
|
+
const routeCopper = (routedSrj.traces ?? [])
|
|
329
|
+
.filter((trace) =>
|
|
330
|
+
traceBelongsToNet(inputSrj, trace, representativeConnection),
|
|
331
|
+
)
|
|
332
|
+
.flatMap((trace) => extractTraceCopper(trace, layerNames))
|
|
333
|
+
const planeCopper: PlaneCopper[] = [
|
|
334
|
+
...new Set(
|
|
335
|
+
(planeConnectivity ?? []).flatMap((plane) =>
|
|
336
|
+
connectionsShareElectricalNet(
|
|
337
|
+
inputSrj,
|
|
338
|
+
plane.connectionName,
|
|
339
|
+
representativeConnection.name,
|
|
340
|
+
)
|
|
341
|
+
? [plane.layer]
|
|
342
|
+
: [],
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
].map((layer) => ({ type: "plane", layer }))
|
|
346
|
+
const copper: CopperPrimitive[] = [
|
|
347
|
+
...endpointCopper,
|
|
348
|
+
...obstacleCopper,
|
|
349
|
+
...routeCopper,
|
|
350
|
+
...planeCopper,
|
|
351
|
+
]
|
|
352
|
+
const connectedCopper = new DisjointSet(copper.length)
|
|
353
|
+
for (let firstIndex = 0; firstIndex < copper.length; firstIndex++) {
|
|
354
|
+
for (
|
|
355
|
+
let secondIndex = firstIndex + 1;
|
|
356
|
+
secondIndex < copper.length;
|
|
357
|
+
secondIndex++
|
|
358
|
+
) {
|
|
359
|
+
if (primitivesTouch(copper[firstIndex]!, copper[secondIndex]!)) {
|
|
360
|
+
connectedCopper.union(firstIndex, secondIndex)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const endpointIndexByConnection = new Map<string, number[]>()
|
|
366
|
+
for (let index = 0; index < endpointCopper.length; index++) {
|
|
367
|
+
const endpoint = endpointCopper[index]!
|
|
368
|
+
const indices =
|
|
369
|
+
endpointIndexByConnection.get(endpoint.connectionName) ?? []
|
|
370
|
+
indices[endpoint.endpointIndex] = index
|
|
371
|
+
endpointIndexByConnection.set(endpoint.connectionName, indices)
|
|
372
|
+
}
|
|
373
|
+
for (const connection of connections) {
|
|
374
|
+
const endpointIndices =
|
|
375
|
+
endpointIndexByConnection.get(connection.name) ?? []
|
|
376
|
+
const firstEndpointIndex = endpointIndices[0]
|
|
377
|
+
const firstRoot =
|
|
378
|
+
firstEndpointIndex === undefined
|
|
379
|
+
? undefined
|
|
380
|
+
: connectedCopper.find(firstEndpointIndex)
|
|
381
|
+
const disconnectedEndpointIndices = endpointIndices.flatMap(
|
|
382
|
+
(endpointIndex, index) =>
|
|
383
|
+
firstRoot === undefined ||
|
|
384
|
+
connectedCopper.find(endpointIndex) !== firstRoot
|
|
385
|
+
? [index]
|
|
386
|
+
: [],
|
|
387
|
+
)
|
|
388
|
+
connectedEndpointCount +=
|
|
389
|
+
endpointIndices.length - disconnectedEndpointIndices.length
|
|
390
|
+
if (disconnectedEndpointIndices.length === 0) {
|
|
391
|
+
connectedConnectionCount++
|
|
392
|
+
} else {
|
|
393
|
+
issues.push({
|
|
394
|
+
code: "original-endpoints-disconnected",
|
|
395
|
+
connectionName: connection.name,
|
|
396
|
+
disconnectedEndpointIndices,
|
|
397
|
+
message: `Connection ${connection.name} has no physical copper path from endpoint 0 to original endpoint${disconnectedEndpointIndices.length === 1 ? "" : "s"} ${disconnectedEndpointIndices.join(", ")}`,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const checkedEndpointCount = inputSrj.connections.reduce(
|
|
404
|
+
(count, connection) => count + connection.pointsToConnect.length,
|
|
405
|
+
0,
|
|
406
|
+
)
|
|
407
|
+
return {
|
|
408
|
+
valid: issues.length === 0,
|
|
409
|
+
checkedConnectionCount: inputSrj.connections.length,
|
|
410
|
+
connectedConnectionCount,
|
|
411
|
+
checkedEndpointCount,
|
|
412
|
+
connectedEndpointCount,
|
|
413
|
+
issues,
|
|
414
|
+
}
|
|
415
|
+
}
|