@tscircuit/fanout-solver 0.0.17 → 0.0.19

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,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
+ }