@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,463 @@
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
+ | "via-at-endpoint"
27
+ | "trace-obstacle-clearance"
28
+ | "via-obstacle-clearance"
29
+ | "different-net-trace-clearance"
30
+ | "different-net-trace-via-clearance"
31
+ | "different-net-via-clearance"
32
+
33
+ export interface RoutedCopperDrcIssue {
34
+ code: RoutedCopperDrcIssueCode
35
+ message: string
36
+ traceId: string
37
+ connectionName?: string
38
+ otherTraceId?: string
39
+ otherConnectionName?: string
40
+ layer?: string
41
+ obstacleId?: string
42
+ }
43
+
44
+ export interface RoutedCopperDrcReport {
45
+ valid: boolean
46
+ checkedTraceCount: number
47
+ checkedSegmentCount: number
48
+ checkedViaCount: number
49
+ issues: RoutedCopperDrcIssue[]
50
+ }
51
+
52
+ interface RoutedVia {
53
+ center: Point2D
54
+ diameter: number
55
+ spanLayers: string[]
56
+ }
57
+
58
+ interface TraceCopper {
59
+ trace: SimplifiedPcbTrace
60
+ connectionName: string
61
+ segments: RoutedSegment[]
62
+ vias: RoutedVia[]
63
+ }
64
+
65
+ export function segmentIsLegalTerminalBodyEscape(params: {
66
+ inputSrj: SimpleRouteJson
67
+ segment: RoutedSegment
68
+ bodyObstacle: SimpleRouteJson["obstacles"][number]
69
+ connectionName: string
70
+ }): boolean {
71
+ const { inputSrj, segment, bodyObstacle, connectionName } = params
72
+ if (bodyObstacle.connectedTo.length > 0 || !bodyObstacle.componentId) {
73
+ return false
74
+ }
75
+ const sameNetComponentPads = inputSrj.obstacles.filter(
76
+ (obstacle) =>
77
+ obstacle.componentId === bodyObstacle.componentId &&
78
+ obstacle.layers.includes(segment.layer) &&
79
+ obstacleSharesElectricalNet(inputSrj, obstacle, connectionName),
80
+ )
81
+ for (const pad of sameNetComponentPads) {
82
+ for (const [terminal, other] of [
83
+ [segment.start, segment.end],
84
+ [segment.end, segment.start],
85
+ ] as const) {
86
+ if (!pointIsInsideObstacle(terminal, pad, EPSILON)) continue
87
+ const outwardFromBody = {
88
+ x: terminal.x - bodyObstacle.center.x,
89
+ y: terminal.y - bodyObstacle.center.y,
90
+ }
91
+ const terminalToOther = {
92
+ x: other.x - terminal.x,
93
+ y: other.y - terminal.y,
94
+ }
95
+ if (
96
+ outwardFromBody.x * terminalToOther.x +
97
+ outwardFromBody.y * terminalToOther.y >
98
+ EPSILON
99
+ ) {
100
+ return true
101
+ }
102
+ }
103
+ }
104
+ return false
105
+ }
106
+
107
+ function pointsMatch(first: Point2D, second: Point2D): boolean {
108
+ return distance(first, second) <= EPSILON
109
+ }
110
+
111
+ function addIssue(
112
+ issues: RoutedCopperDrcIssue[],
113
+ issue: RoutedCopperDrcIssue,
114
+ ): void {
115
+ issues.push(issue)
116
+ }
117
+
118
+ function extractTraceCopper(params: {
119
+ srj: SimpleRouteJson
120
+ trace: SimplifiedPcbTrace
121
+ connectionName: string
122
+ layerNames: string[]
123
+ issues: RoutedCopperDrcIssue[]
124
+ }): TraceCopper {
125
+ const { srj, trace, connectionName, layerNames, issues } = params
126
+ const segments: RoutedSegment[] = []
127
+ const vias: RoutedVia[] = []
128
+ let previousWire:
129
+ | Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
130
+ | undefined
131
+ let pendingVia:
132
+ | Extract<SimplifiedPcbTrace["route"][number], { route_type: "via" }>
133
+ | undefined
134
+
135
+ for (const routePoint of trace.route) {
136
+ if (routePoint.route_type === "via") {
137
+ const spanLayers = getLayerSpan(
138
+ routePoint.from_layer,
139
+ routePoint.to_layer,
140
+ layerNames,
141
+ )
142
+ vias.push({
143
+ center: { x: routePoint.x, y: routePoint.y },
144
+ diameter:
145
+ routePoint.via_diameter ?? srj.minViaPadDiameter ?? srj.minTraceWidth,
146
+ spanLayers,
147
+ })
148
+ if (
149
+ !previousWire ||
150
+ !pointsMatch(previousWire, routePoint) ||
151
+ previousWire.layer !== routePoint.from_layer
152
+ ) {
153
+ addIssue(issues, {
154
+ code: "disconnected-trace",
155
+ traceId: trace.pcb_trace_id,
156
+ connectionName,
157
+ message: `Trace ${trace.pcb_trace_id} reaches a via without a matching ${routePoint.from_layer} wire endpoint`,
158
+ })
159
+ }
160
+ pendingVia = routePoint
161
+ continue
162
+ }
163
+
164
+ if (routePoint.route_type !== "wire") {
165
+ addIssue(issues, {
166
+ code: "unsupported-route-point",
167
+ traceId: trace.pcb_trace_id,
168
+ connectionName,
169
+ message: `Trace ${trace.pcb_trace_id} contains unsupported ${routePoint.route_type} geometry`,
170
+ })
171
+ previousWire = undefined
172
+ pendingVia = undefined
173
+ continue
174
+ }
175
+
176
+ if (pendingVia) {
177
+ if (
178
+ !pointsMatch(routePoint, pendingVia) ||
179
+ routePoint.layer !== pendingVia.to_layer
180
+ ) {
181
+ addIssue(issues, {
182
+ code: "disconnected-trace",
183
+ traceId: trace.pcb_trace_id,
184
+ connectionName,
185
+ message: `Trace ${trace.pcb_trace_id} does not continue from its via on ${pendingVia.to_layer}`,
186
+ })
187
+ }
188
+ previousWire = routePoint
189
+ pendingVia = undefined
190
+ continue
191
+ }
192
+
193
+ if (previousWire) {
194
+ if (previousWire.layer !== routePoint.layer) {
195
+ addIssue(issues, {
196
+ code: "disconnected-trace",
197
+ traceId: trace.pcb_trace_id,
198
+ connectionName,
199
+ message: `Trace ${trace.pcb_trace_id} changes from ${previousWire.layer} to ${routePoint.layer} without a via`,
200
+ })
201
+ } else if (!pointsMatch(previousWire, routePoint)) {
202
+ segments.push({
203
+ start: { x: previousWire.x, y: previousWire.y },
204
+ end: { x: routePoint.x, y: routePoint.y },
205
+ width: routePoint.width,
206
+ layer: routePoint.layer,
207
+ })
208
+ }
209
+ }
210
+ previousWire = routePoint
211
+ }
212
+
213
+ if (pendingVia) {
214
+ addIssue(issues, {
215
+ code: "disconnected-trace",
216
+ traceId: trace.pcb_trace_id,
217
+ connectionName,
218
+ message: `Trace ${trace.pcb_trace_id} ends at a via without a wire on ${pendingVia.to_layer}`,
219
+ })
220
+ }
221
+
222
+ return { trace, connectionName, segments, vias }
223
+ }
224
+
225
+ /**
226
+ * Independently audits every emitted trace and via against the original SRJ
227
+ * obstacle field and every other emitted net. This deliberately does not rely
228
+ * on the solver's accepted route-plan objects.
229
+ */
230
+ export function validateRoutedCopperDrc(params: {
231
+ inputSrj: SimpleRouteJson
232
+ routedSrj: SimpleRouteJson
233
+ clearance: number
234
+ }): RoutedCopperDrcReport {
235
+ const { inputSrj, routedSrj, clearance } = params
236
+ const issues: RoutedCopperDrcIssue[] = []
237
+ const layerNames = getCopperLayerNames(routedSrj.layerCount)
238
+ const traceCopper: TraceCopper[] = []
239
+ const originalAndRoutedEndpoints = [inputSrj, routedSrj].flatMap((srj) =>
240
+ srj.connections.flatMap((connection) =>
241
+ connection.pointsToConnect.map((point) => ({
242
+ connectionName: connection.name,
243
+ point,
244
+ })),
245
+ ),
246
+ )
247
+
248
+ for (const trace of routedSrj.traces ?? []) {
249
+ const connectionName = trace.connection_name
250
+ if (
251
+ !connectionName ||
252
+ !inputSrj.connections.some(
253
+ (connection) => connection.name === connectionName,
254
+ )
255
+ ) {
256
+ addIssue(issues, {
257
+ code: "unknown-trace-connection",
258
+ traceId: trace.pcb_trace_id,
259
+ ...(connectionName ? { connectionName } : {}),
260
+ message: `Trace ${trace.pcb_trace_id} does not identify an input connection`,
261
+ })
262
+ continue
263
+ }
264
+ traceCopper.push(
265
+ extractTraceCopper({
266
+ srj: routedSrj,
267
+ trace,
268
+ connectionName,
269
+ layerNames,
270
+ issues,
271
+ }),
272
+ )
273
+ }
274
+
275
+ for (const copper of traceCopper) {
276
+ for (const segment of copper.segments) {
277
+ for (const obstacle of inputSrj.obstacles) {
278
+ if (
279
+ !obstacle.layers.includes(segment.layer) ||
280
+ obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)
281
+ ) {
282
+ continue
283
+ }
284
+ if (
285
+ segmentIsLegalTerminalBodyEscape({
286
+ inputSrj,
287
+ segment,
288
+ bodyObstacle: obstacle,
289
+ connectionName: copper.connectionName,
290
+ })
291
+ ) {
292
+ continue
293
+ }
294
+ const actual = distanceSegmentToObstacle(segment, obstacle)
295
+ const required = segment.width / 2 + clearance
296
+ if (actual < required - EPSILON) {
297
+ addIssue(issues, {
298
+ code: "trace-obstacle-clearance",
299
+ traceId: copper.trace.pcb_trace_id,
300
+ connectionName: copper.connectionName,
301
+ layer: segment.layer,
302
+ obstacleId: obstacle.obstacleId,
303
+ 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`,
304
+ })
305
+ }
306
+ }
307
+ }
308
+ for (const via of copper.vias) {
309
+ const coincidentEndpoint = originalAndRoutedEndpoints.find(
310
+ ({ point }) => distance(via.center, point) <= EPSILON,
311
+ )
312
+ if (coincidentEndpoint) {
313
+ addIssue(issues, {
314
+ code: "via-at-endpoint",
315
+ traceId: copper.trace.pcb_trace_id,
316
+ connectionName: copper.connectionName,
317
+ otherConnectionName: coincidentEndpoint.connectionName,
318
+ message: `Via in ${copper.trace.pcb_trace_id} is placed directly at an original or routed connection endpoint`,
319
+ })
320
+ }
321
+ for (const obstacle of inputSrj.obstacles) {
322
+ if (
323
+ !obstacle.layers.some((layer) => via.spanLayers.includes(layer)) ||
324
+ obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)
325
+ ) {
326
+ continue
327
+ }
328
+ const actual = distancePointToObstacle(via.center, obstacle)
329
+ const required = via.diameter / 2 + clearance
330
+ if (actual < required - EPSILON) {
331
+ addIssue(issues, {
332
+ code: "via-obstacle-clearance",
333
+ traceId: copper.trace.pcb_trace_id,
334
+ connectionName: copper.connectionName,
335
+ obstacleId: obstacle.obstacleId,
336
+ 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`,
337
+ })
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ for (let firstIndex = 0; firstIndex < traceCopper.length; firstIndex++) {
344
+ const first = traceCopper[firstIndex]!
345
+ for (
346
+ let secondIndex = firstIndex + 1;
347
+ secondIndex < traceCopper.length;
348
+ secondIndex++
349
+ ) {
350
+ const second = traceCopper[secondIndex]!
351
+ if (
352
+ connectionsShareElectricalNet(
353
+ inputSrj,
354
+ first.connectionName,
355
+ second.connectionName,
356
+ )
357
+ ) {
358
+ continue
359
+ }
360
+
361
+ for (const firstSegment of first.segments) {
362
+ for (const secondSegment of second.segments) {
363
+ if (segmentsAreClear(firstSegment, secondSegment, clearance)) continue
364
+ addIssue(issues, {
365
+ code: "different-net-trace-clearance",
366
+ traceId: first.trace.pcb_trace_id,
367
+ connectionName: first.connectionName,
368
+ otherTraceId: second.trace.pcb_trace_id,
369
+ otherConnectionName: second.connectionName,
370
+ layer: firstSegment.layer,
371
+ message: `Different-net traces ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} intersect or violate clearance on ${firstSegment.layer}`,
372
+ })
373
+ }
374
+ for (const secondVia of second.vias) {
375
+ if (
376
+ !secondVia.spanLayers.includes(firstSegment.layer) ||
377
+ distancePointToSegment(
378
+ secondVia.center,
379
+ firstSegment.start,
380
+ firstSegment.end,
381
+ ) >=
382
+ secondVia.diameter / 2 +
383
+ firstSegment.width / 2 +
384
+ clearance -
385
+ EPSILON
386
+ ) {
387
+ continue
388
+ }
389
+ addIssue(issues, {
390
+ code: "different-net-trace-via-clearance",
391
+ traceId: first.trace.pcb_trace_id,
392
+ connectionName: first.connectionName,
393
+ otherTraceId: second.trace.pcb_trace_id,
394
+ otherConnectionName: second.connectionName,
395
+ layer: firstSegment.layer,
396
+ message: `Trace ${first.trace.pcb_trace_id} violates via clearance to ${second.trace.pcb_trace_id} on ${firstSegment.layer}`,
397
+ })
398
+ }
399
+ }
400
+
401
+ for (const firstVia of first.vias) {
402
+ for (const secondSegment of second.segments) {
403
+ if (
404
+ !firstVia.spanLayers.includes(secondSegment.layer) ||
405
+ distancePointToSegment(
406
+ firstVia.center,
407
+ secondSegment.start,
408
+ secondSegment.end,
409
+ ) >=
410
+ firstVia.diameter / 2 +
411
+ secondSegment.width / 2 +
412
+ clearance -
413
+ EPSILON
414
+ ) {
415
+ continue
416
+ }
417
+ addIssue(issues, {
418
+ code: "different-net-trace-via-clearance",
419
+ traceId: first.trace.pcb_trace_id,
420
+ connectionName: first.connectionName,
421
+ otherTraceId: second.trace.pcb_trace_id,
422
+ otherConnectionName: second.connectionName,
423
+ layer: secondSegment.layer,
424
+ message: `Via in ${first.trace.pcb_trace_id} violates trace clearance to ${second.trace.pcb_trace_id} on ${secondSegment.layer}`,
425
+ })
426
+ }
427
+ for (const secondVia of second.vias) {
428
+ if (
429
+ !firstVia.spanLayers.some((layer) =>
430
+ secondVia.spanLayers.includes(layer),
431
+ ) ||
432
+ distance(firstVia.center, secondVia.center) >=
433
+ (firstVia.diameter + secondVia.diameter) / 2 + clearance - EPSILON
434
+ ) {
435
+ continue
436
+ }
437
+ addIssue(issues, {
438
+ code: "different-net-via-clearance",
439
+ traceId: first.trace.pcb_trace_id,
440
+ connectionName: first.connectionName,
441
+ otherTraceId: second.trace.pcb_trace_id,
442
+ otherConnectionName: second.connectionName,
443
+ message: `Different-net vias in ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} violate clearance on an overlapping layer span`,
444
+ })
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ return {
451
+ valid: issues.length === 0,
452
+ checkedTraceCount: routedSrj.traces?.length ?? 0,
453
+ checkedSegmentCount: traceCopper.reduce(
454
+ (count, copper) => count + copper.segments.length,
455
+ 0,
456
+ ),
457
+ checkedViaCount: traceCopper.reduce(
458
+ (count, copper) => count + copper.vias.length,
459
+ 0,
460
+ ),
461
+ issues,
462
+ }
463
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@biomejs/biome": "2.5.5",
43
43
  "@tsci/tscircuit.dataset-srj19-bga-passive-overlays": "github:tscircuit/dataset-srj19#c1206f3",
44
- "@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#a8a1e74",
44
+ "@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#bfdf7bc",
45
45
  "@tscircuit/footprinter": "0.0.394",
46
46
  "@types/bun": "1.3.14",
47
47
  "@types/react": "19.2.14",