@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.
@@ -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
+ }