@tscircuit/fanout-solver 0.0.36 → 0.0.38
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 +16 -2
- package/lib/build-output.ts +32 -10
- package/lib/fanout-solver.ts +294 -44
- package/lib/match-bus-lengths.ts +571 -0
- package/lib/prepare-buses.ts +32 -2
- package/lib/route-bus.ts +378 -81
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +18 -3
- package/lib/validate-fanout-solution.ts +40 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,10 @@ and treats each bus-layer decision atomically.
|
|
|
73
73
|
visibly continuous pad connections.
|
|
74
74
|
- Chamfers orthogonal routing corners into 45° segments before validating and
|
|
75
75
|
emitting the fanout.
|
|
76
|
+
- Honors a boundary bus `maxLengthSkew` as a hard local-fanout constraint. It
|
|
77
|
+
adds straight/45° meanders only after the dense component escape, keeps the
|
|
78
|
+
original endpoints and vias, and atomically rejects an assignment when the
|
|
79
|
+
requested skew cannot fit inside that bus's shared boundary.
|
|
76
80
|
- Verifies oriented-pad, via, trace, and already-routed fanout clearance on
|
|
77
81
|
every complete candidate, independent of the routing strategy that produced
|
|
78
82
|
it.
|
|
@@ -189,6 +193,7 @@ The canonical bus input is the current `SimpleRouteJson` bus structure:
|
|
|
189
193
|
busId: "ddr",
|
|
190
194
|
connectionNames: ["BUS_DDR_01", "BUS_DDR_02", "BUS_DDR_03"],
|
|
191
195
|
preferredExit: "right",
|
|
196
|
+
maxLengthSkew: 0.25,
|
|
192
197
|
},
|
|
193
198
|
],
|
|
194
199
|
}
|
|
@@ -201,6 +206,13 @@ routed cleanly, the solver rejects that bus for the current layer assignment and
|
|
|
201
206
|
tries another combination. `busExitPreferences` provides the same override
|
|
202
207
|
without modifying the input object.
|
|
203
208
|
|
|
209
|
+
`maxLengthSkew` is measured in millimeters of planar routed copper within this
|
|
210
|
+
fanout phase. It is supported for multi-connection boundary buses. A loose or
|
|
211
|
+
omitted constraint leaves the routed geometry unchanged; an impossible
|
|
212
|
+
constraint fails instead of returning a fanout that violates the declared skew.
|
|
213
|
+
Plane-terminated buses reject `maxLengthSkew` because they do not have a
|
|
214
|
+
boundary tuning corridor.
|
|
215
|
+
|
|
204
216
|
`availableCornersAndSides` is a solver-wide hard constraint. Its directed
|
|
205
217
|
corner names distinguish the two edges meeting at a corner: `top_left` exits
|
|
206
218
|
through the top edge, while `left_top` exits through the left edge. The complete
|
|
@@ -473,5 +485,7 @@ label.
|
|
|
473
485
|
## Scope
|
|
474
486
|
|
|
475
487
|
This package owns the BGA pad-to-breakout prefix. It does not replace the
|
|
476
|
-
board-level autorouter
|
|
477
|
-
|
|
488
|
+
board-level autorouter or route arbitrary obstacles between the breakout
|
|
489
|
+
boundary and the final destination. Its `maxLengthSkew` matching applies to the
|
|
490
|
+
local fanout prefix; end-to-end delay matching across multiple routing phases
|
|
491
|
+
still belongs to a board-level coordinator.
|
package/lib/build-output.ts
CHANGED
|
@@ -5,10 +5,9 @@ import type { FanoutRoutePlan, SimpleRouteJsonWithFanoutPlanes } from "./types"
|
|
|
5
5
|
function createViaObstacle(
|
|
6
6
|
plan: FanoutRoutePlan,
|
|
7
7
|
layerNames: string[],
|
|
8
|
-
|
|
8
|
+
via: NonNullable<FanoutRoutePlan["via"]>,
|
|
9
|
+
viaIndex: number | "endpoint",
|
|
9
10
|
): Obstacle | null {
|
|
10
|
-
const via = endpoint ? plan.planeEndpointVia : plan.via
|
|
11
|
-
if (!via) return null
|
|
12
11
|
const zLayers = via.spanLayers.map((layer) => {
|
|
13
12
|
const layerIndex = layerNames.indexOf(layer)
|
|
14
13
|
if (layerIndex < 0) {
|
|
@@ -20,9 +19,12 @@ function createViaObstacle(
|
|
|
20
19
|
})
|
|
21
20
|
const outputIds = createFanoutOutputIds(plan)
|
|
22
21
|
return {
|
|
23
|
-
obstacleId:
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
obstacleId:
|
|
23
|
+
viaIndex === "endpoint"
|
|
24
|
+
? outputIds.planeEndpointViaObstacleId
|
|
25
|
+
: viaIndex === 0
|
|
26
|
+
? outputIds.viaObstacleId
|
|
27
|
+
: `${outputIds.viaObstacleId}:${viaIndex}`,
|
|
26
28
|
type: "rect",
|
|
27
29
|
center: via.center,
|
|
28
30
|
width: via.diameter,
|
|
@@ -67,10 +69,30 @@ export function buildOutputSimpleRouteJson(params: {
|
|
|
67
69
|
if (plan.termination.type === "plane") {
|
|
68
70
|
planeTerminatedConnectionNames.add(plan.connectionName)
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (plan.via) {
|
|
73
|
+
const viaObstacle = createViaObstacle(plan, layerNames, plan.via, 0)
|
|
74
|
+
if (viaObstacle) viaObstacles.push(viaObstacle)
|
|
75
|
+
}
|
|
76
|
+
for (const [additionalViaIndex, via] of (
|
|
77
|
+
plan.additionalVias ?? []
|
|
78
|
+
).entries()) {
|
|
79
|
+
const viaObstacle = createViaObstacle(
|
|
80
|
+
plan,
|
|
81
|
+
layerNames,
|
|
82
|
+
via,
|
|
83
|
+
additionalViaIndex + 1,
|
|
84
|
+
)
|
|
85
|
+
if (viaObstacle) viaObstacles.push(viaObstacle)
|
|
86
|
+
}
|
|
87
|
+
if (plan.planeEndpointVia) {
|
|
88
|
+
const endpointViaObstacle = createViaObstacle(
|
|
89
|
+
plan,
|
|
90
|
+
layerNames,
|
|
91
|
+
plan.planeEndpointVia,
|
|
92
|
+
"endpoint",
|
|
93
|
+
)
|
|
94
|
+
if (endpointViaObstacle) viaObstacles.push(endpointViaObstacle)
|
|
95
|
+
}
|
|
74
96
|
}
|
|
75
97
|
const planTraces = plans.flatMap((plan) => [
|
|
76
98
|
plan.trace,
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
completeOriginalEndpoints,
|
|
9
9
|
} from "./complete-original-endpoints"
|
|
10
10
|
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
11
|
+
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
11
12
|
import {
|
|
12
13
|
prepareFanoutBuses,
|
|
13
14
|
resolveAvailableBoundaryRegions,
|
|
@@ -27,6 +28,7 @@ import type {
|
|
|
27
28
|
FanoutRoutePlan,
|
|
28
29
|
FanoutSolverOptions,
|
|
29
30
|
FanoutSolverOutput,
|
|
31
|
+
FanoutValidationIssue,
|
|
30
32
|
PreparedBus,
|
|
31
33
|
} from "./types"
|
|
32
34
|
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
@@ -219,6 +221,7 @@ function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
|
|
|
219
221
|
(count, plan) =>
|
|
220
222
|
count +
|
|
221
223
|
Number(Boolean(plan.via)) +
|
|
224
|
+
(plan.additionalVias?.length ?? 0) +
|
|
222
225
|
Number(Boolean(plan.planeEndpointVia)),
|
|
223
226
|
0,
|
|
224
227
|
)
|
|
@@ -257,6 +260,27 @@ function busUsesDestinationGuidedTracks(bus: PreparedBus): boolean {
|
|
|
257
260
|
})
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
function getCommonExplicitExitTargetLayer(
|
|
264
|
+
bus: PreparedBus,
|
|
265
|
+
): string | undefined {
|
|
266
|
+
if (
|
|
267
|
+
bus.connections.length === 0 ||
|
|
268
|
+
bus.connections.some(
|
|
269
|
+
(connection) =>
|
|
270
|
+
!connection.hasExplicitLayeredExitTarget ||
|
|
271
|
+
!connection.exitTargetPoint?.layer,
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
const targetLayers = new Set(
|
|
277
|
+
bus.connections.map((connection) => connection.exitTargetPoint!.layer!),
|
|
278
|
+
)
|
|
279
|
+
if (targetLayers.size !== 1) return undefined
|
|
280
|
+
const [targetLayer] = targetLayers
|
|
281
|
+
return targetLayer
|
|
282
|
+
}
|
|
283
|
+
|
|
260
284
|
function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
|
|
261
285
|
const isHorizontal = bus.direction === "left" || bus.direction === "right"
|
|
262
286
|
const directionalCoordinates = isHorizontal
|
|
@@ -327,7 +351,14 @@ function createInitialLayerAssignment(params: {
|
|
|
327
351
|
const viaLayers = routableEscapeLayers.filter(
|
|
328
352
|
(layer) => layer !== sourceLayer,
|
|
329
353
|
)
|
|
354
|
+
const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus)
|
|
330
355
|
if (
|
|
356
|
+
commonExitTargetLayer &&
|
|
357
|
+
routableEscapeLayers.includes(commonExitTargetLayer)
|
|
358
|
+
) {
|
|
359
|
+
assignment[bus.busId] = commonExitTargetLayer
|
|
360
|
+
} else if (
|
|
361
|
+
!busUsesCoordinatedWinding(bus) &&
|
|
331
362
|
routableEscapeLayers.includes(sourceLayer) &&
|
|
332
363
|
(busUsesDestinationGuidedTracks(bus) || busIsOnOutwardComponentEdge(bus))
|
|
333
364
|
) {
|
|
@@ -368,6 +399,17 @@ function prioritizeLayerAssignment(params: {
|
|
|
368
399
|
].slice(0, maxAssignments)
|
|
369
400
|
}
|
|
370
401
|
|
|
402
|
+
function busUsesCoordinatedWinding(bus: PreparedBus): boolean {
|
|
403
|
+
return Boolean(
|
|
404
|
+
bus.exitEdge &&
|
|
405
|
+
bus.termination.type === "boundary" &&
|
|
406
|
+
bus.connections.length > 0 &&
|
|
407
|
+
bus.connections.every(
|
|
408
|
+
(connection) => connection.hasExplicitLayeredExitTarget === true,
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
371
413
|
function getCandidateEscapeLayersForBus(params: {
|
|
372
414
|
bus: PreparedBus
|
|
373
415
|
srj: SimpleRouteJson
|
|
@@ -380,6 +422,10 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
380
422
|
busAllowedLayers === undefined
|
|
381
423
|
? config.escapeLayers
|
|
382
424
|
: config.escapeLayers.filter((layer) => busAllowedLayers.includes(layer))
|
|
425
|
+
// A coordinated winding route is deliberately planned with the other buses'
|
|
426
|
+
// committed escape vias present. Testing it in isolation is both expensive
|
|
427
|
+
// and can reject a layer whose shared via field guides a valid bus ordering.
|
|
428
|
+
if (busUsesCoordinatedWinding(bus)) return allowedEscapeLayers
|
|
383
429
|
const individuallyRoutableLayers = allowedEscapeLayers.filter(
|
|
384
430
|
(targetLayer) =>
|
|
385
431
|
routeBus({
|
|
@@ -438,6 +484,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
438
484
|
private nextAssignmentIndex = 0
|
|
439
485
|
private nextGeneratedAssignmentIndex = 0
|
|
440
486
|
private bestAttempt: AssignmentAttempt | null = null
|
|
487
|
+
private lengthMatchingFailure: FanoutValidationIssue | null = null
|
|
441
488
|
private endpointCompletion: CompleteOriginalEndpointsResult | null = null
|
|
442
489
|
|
|
443
490
|
constructor(
|
|
@@ -453,6 +500,19 @@ export class FanoutSolver extends BaseSolver {
|
|
|
453
500
|
this.preparedBuses = prepareFanoutBuses(this.routingSrj, options)
|
|
454
501
|
validateCornerBandCapacities(this.preparedBuses, this.config)
|
|
455
502
|
for (const bus of this.preparedBuses) {
|
|
503
|
+
for (const connection of bus.connections) {
|
|
504
|
+
if (!connection.hasExplicitLayeredExitTarget) continue
|
|
505
|
+
const targetLayer = connection.exitTargetPoint?.layer
|
|
506
|
+
if (
|
|
507
|
+
typeof targetLayer !== "string" ||
|
|
508
|
+
targetLayer.length === 0 ||
|
|
509
|
+
!this.config.layerNames.includes(targetLayer)
|
|
510
|
+
) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`FanoutSolver: connection exit target for "${connection.connection.name}" uses unavailable layer "${String(targetLayer)}"`,
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
456
516
|
for (const allowedLayer of bus.allowedLayers ?? []) {
|
|
457
517
|
if (!this.config.layerNames.includes(allowedLayer)) {
|
|
458
518
|
throw new Error(
|
|
@@ -471,6 +531,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
471
531
|
`FanoutSolver: bus "${bus.busId}" has no allowed layer in escapeLayers`,
|
|
472
532
|
)
|
|
473
533
|
}
|
|
534
|
+
bus.routableEscapeLayers = this.config.escapeLayers.filter(
|
|
535
|
+
(layer) => bus.allowedLayers?.includes(layer) ?? true,
|
|
536
|
+
)
|
|
474
537
|
if (bus.termination.type !== "plane") continue
|
|
475
538
|
const planeLayer = bus.termination.layer
|
|
476
539
|
if (!this.config.layerNames.includes(planeLayer)) {
|
|
@@ -598,6 +661,19 @@ export class FanoutSolver extends BaseSolver {
|
|
|
598
661
|
})
|
|
599
662
|
}
|
|
600
663
|
|
|
664
|
+
private matchCompletePlanLengths(
|
|
665
|
+
plans: readonly FanoutRoutePlan[],
|
|
666
|
+
): ReturnType<typeof matchBusPlanLengths> {
|
|
667
|
+
return matchBusPlanLengths({
|
|
668
|
+
plans,
|
|
669
|
+
preparedBuses: this.preparedBuses,
|
|
670
|
+
inputSrj: this.inputSrj,
|
|
671
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
672
|
+
clearance: this.config.clearance,
|
|
673
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
601
677
|
private evaluateAssignmentWithStrategy(
|
|
602
678
|
assignmentIndex: number,
|
|
603
679
|
busLayerAssignments: Readonly<Record<string, string>>,
|
|
@@ -637,10 +713,22 @@ export class FanoutSolver extends BaseSolver {
|
|
|
637
713
|
failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId))
|
|
638
714
|
}
|
|
639
715
|
}
|
|
640
|
-
const busesInRoutingOrder = [...this.preparedBuses].sort(
|
|
641
|
-
(a
|
|
716
|
+
const busesInRoutingOrder = [...this.preparedBuses].sort((a, b) => {
|
|
717
|
+
const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
|
|
718
|
+
const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b)
|
|
719
|
+
const aLayerIndex = this.config.layerNames.indexOf(
|
|
720
|
+
busLayerAssignments[a.busId] ?? "",
|
|
721
|
+
)
|
|
722
|
+
const bLayerIndex = this.config.layerNames.indexOf(
|
|
723
|
+
busLayerAssignments[b.busId] ?? "",
|
|
724
|
+
)
|
|
725
|
+
return (
|
|
642
726
|
Number(b.termination.type === "plane") -
|
|
643
727
|
Number(a.termination.type === "plane") ||
|
|
728
|
+
Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
|
|
729
|
+
(aUsesCoordinatedWinding && bUsesCoordinatedWinding
|
|
730
|
+
? bLayerIndex - aLayerIndex
|
|
731
|
+
: 0) ||
|
|
644
732
|
(routingStrategy === "group-by-layer"
|
|
645
733
|
? (busLayerAssignments[a.busId] ?? "").localeCompare(
|
|
646
734
|
busLayerAssignments[b.busId] ?? "",
|
|
@@ -652,8 +740,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
652
740
|
: b.connections.length - a.connections.length ||
|
|
653
741
|
(routingStrategy === "deep-first"
|
|
654
742
|
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
655
|
-
: getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)))
|
|
656
|
-
|
|
743
|
+
: getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)))
|
|
744
|
+
)
|
|
745
|
+
})
|
|
657
746
|
|
|
658
747
|
let routingPrefixKey = `${routingStrategy}|`
|
|
659
748
|
for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
|
|
@@ -711,6 +800,29 @@ export class FanoutSolver extends BaseSolver {
|
|
|
711
800
|
}
|
|
712
801
|
|
|
713
802
|
let validationIssues: FanoutAttemptSummary["validationIssues"]
|
|
803
|
+
if (plans.length === this.inputSrj.connections.length) {
|
|
804
|
+
const lengthMatching = this.matchCompletePlanLengths(plans)
|
|
805
|
+
if (lengthMatching.plans) {
|
|
806
|
+
plans = lengthMatching.plans
|
|
807
|
+
} else {
|
|
808
|
+
const constrainedBus = lengthMatching.failedBus
|
|
809
|
+
const lengthMatchingIssue: FanoutValidationIssue = {
|
|
810
|
+
code: "bus-length-skew",
|
|
811
|
+
message: `Bus ${constrainedBus.busId} could not satisfy its ${constrainedBus.maxLengthSkew!.toFixed(6)}mm routed-length skew within the fanout boundary`,
|
|
812
|
+
busId: constrainedBus.busId,
|
|
813
|
+
}
|
|
814
|
+
validationIssues = [lengthMatchingIssue]
|
|
815
|
+
this.lengthMatchingFailure ??= lengthMatchingIssue
|
|
816
|
+
plans = []
|
|
817
|
+
failedBusIds = [
|
|
818
|
+
constrainedBus.busId,
|
|
819
|
+
...this.preparedBuses
|
|
820
|
+
.map((bus) => bus.busId)
|
|
821
|
+
.filter((busId) => busId !== constrainedBus.busId),
|
|
822
|
+
]
|
|
823
|
+
blockingBusCounts.clear()
|
|
824
|
+
}
|
|
825
|
+
}
|
|
714
826
|
let outputSrj = buildOutputSimpleRouteJson({
|
|
715
827
|
inputSrj: this.inputSrj,
|
|
716
828
|
plans,
|
|
@@ -780,7 +892,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
780
892
|
)
|
|
781
893
|
if (
|
|
782
894
|
bestAttempt.summary.routedConnectionCount ===
|
|
783
|
-
|
|
895
|
+
this.inputSrj.connections.length &&
|
|
896
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
|
|
784
897
|
) {
|
|
785
898
|
return bestAttempt
|
|
786
899
|
}
|
|
@@ -791,12 +904,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
791
904
|
busLayerAssignments,
|
|
792
905
|
routingStrategy,
|
|
793
906
|
)
|
|
794
|
-
if (attempt
|
|
907
|
+
if (this.isAttemptBetter(attempt, bestAttempt)) {
|
|
795
908
|
bestAttempt = attempt
|
|
796
909
|
}
|
|
797
910
|
if (
|
|
798
911
|
bestAttempt.summary.routedConnectionCount ===
|
|
799
|
-
|
|
912
|
+
this.inputSrj.connections.length &&
|
|
913
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
|
|
800
914
|
) {
|
|
801
915
|
return bestAttempt
|
|
802
916
|
}
|
|
@@ -829,7 +943,24 @@ export class FanoutSolver extends BaseSolver {
|
|
|
829
943
|
return null
|
|
830
944
|
}
|
|
831
945
|
|
|
946
|
+
const getMaximumViaSpan = (bus: PreparedBus): number => {
|
|
947
|
+
const sourceLayerIndex = this.config.layerNames.indexOf(
|
|
948
|
+
bus.connections[0]?.sourceLayer ?? "",
|
|
949
|
+
)
|
|
950
|
+
const candidateLayers =
|
|
951
|
+
bus.termination.type === "plane"
|
|
952
|
+
? [bus.termination.layer]
|
|
953
|
+
: (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
|
|
954
|
+
return Math.max(
|
|
955
|
+
0,
|
|
956
|
+
...candidateLayers.map((layer) =>
|
|
957
|
+
Math.abs(this.config.layerNames.indexOf(layer) - sourceLayerIndex),
|
|
958
|
+
),
|
|
959
|
+
)
|
|
960
|
+
}
|
|
832
961
|
const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
|
|
962
|
+
const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
|
|
963
|
+
const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b)
|
|
833
964
|
const aLayerCount =
|
|
834
965
|
a.termination.type === "plane"
|
|
835
966
|
? 1
|
|
@@ -843,6 +974,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
843
974
|
return (
|
|
844
975
|
Number(b.termination.type === "plane") -
|
|
845
976
|
Number(a.termination.type === "plane") ||
|
|
977
|
+
Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
|
|
978
|
+
(aUsesCoordinatedWinding && bUsesCoordinatedWinding
|
|
979
|
+
? getMaximumViaSpan(b) - getMaximumViaSpan(a)
|
|
980
|
+
: 0) ||
|
|
846
981
|
(groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
|
|
847
982
|
aLayerCount - bLayerCount ||
|
|
848
983
|
b.componentObstacles.length - a.componentObstacles.length ||
|
|
@@ -880,7 +1015,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
880
1015
|
return count
|
|
881
1016
|
}
|
|
882
1017
|
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
883
|
-
|
|
1018
|
+
const preferredLayer =
|
|
1019
|
+
getCommonExplicitExitTargetLayer(bus) ?? sourceLayer
|
|
1020
|
+
return state.assignment[bus.busId] === preferredLayer
|
|
884
1021
|
? count
|
|
885
1022
|
: count + bus.connections.length
|
|
886
1023
|
},
|
|
@@ -920,10 +1057,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
920
1057
|
)
|
|
921
1058
|
}
|
|
922
1059
|
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
1060
|
+
const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus)
|
|
923
1061
|
const preferSourceLayer = busUsesDestinationGuidedTracks(bus)
|
|
924
1062
|
const orderedLayers = candidateLayers.toSorted(
|
|
925
1063
|
(first, second) =>
|
|
926
1064
|
(layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
|
|
1065
|
+
Number(second === commonExitTargetLayer) -
|
|
1066
|
+
Number(first === commonExitTargetLayer) ||
|
|
927
1067
|
(preferSourceLayer
|
|
928
1068
|
? Number(second === sourceLayer) - Number(first === sourceLayer)
|
|
929
1069
|
: Number(first === sourceLayer) -
|
|
@@ -963,6 +1103,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
963
1103
|
|
|
964
1104
|
if (nextStates.length === 0) return null
|
|
965
1105
|
nextStates.sort((first, second) => {
|
|
1106
|
+
const additionalViaDifference =
|
|
1107
|
+
this.getCoordinatedAdditionalViaCount(first.plans) -
|
|
1108
|
+
this.getCoordinatedAdditionalViaCount(second.plans)
|
|
1109
|
+
if (additionalViaDifference !== 0) return additionalViaDifference
|
|
966
1110
|
const scoreDifference = getStateScore(first) - getStateScore(second)
|
|
967
1111
|
if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
|
|
968
1112
|
return JSON.stringify(first.assignment).localeCompare(
|
|
@@ -981,30 +1125,56 @@ export class FanoutSolver extends BaseSolver {
|
|
|
981
1125
|
}
|
|
982
1126
|
}
|
|
983
1127
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1128
|
+
let bestState: GroupedBeamState | undefined
|
|
1129
|
+
let outputSrj: SimpleRouteJson | undefined
|
|
1130
|
+
let bestMatchedScore = Number.POSITIVE_INFINITY
|
|
1131
|
+
let bestAdditionalViaCount = Number.POSITIVE_INFINITY
|
|
1132
|
+
const getCompleteStateScore = (state: GroupedBeamState): number =>
|
|
1133
|
+
state.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
1134
|
+
getPlanViaCount(state.plans) * 0.1 +
|
|
1135
|
+
assignmentLoadPenalty(
|
|
1136
|
+
state.assignment,
|
|
1137
|
+
this.preparedBuses,
|
|
1138
|
+
this.config.balanceLayerLoadByConnectionCount,
|
|
1139
|
+
) *
|
|
1140
|
+
getLayerLoadPenaltyWeight(this.config)
|
|
1141
|
+
const hasLengthConstraints = this.preparedBuses.some(
|
|
1142
|
+
(bus) => bus.maxLengthSkew !== undefined,
|
|
1143
|
+
)
|
|
1144
|
+
for (const state of states) {
|
|
1145
|
+
if (state.plans.length !== this.inputSrj.connections.length) continue
|
|
1146
|
+
const lengthMatching = this.matchCompletePlanLengths(state.plans)
|
|
1147
|
+
if (!lengthMatching.plans) continue
|
|
1148
|
+
const lengthMatchedPlans = lengthMatching.plans
|
|
1149
|
+
const candidateOutput = buildOutputSimpleRouteJson({
|
|
1150
|
+
inputSrj: this.inputSrj,
|
|
1151
|
+
plans: lengthMatchedPlans,
|
|
1152
|
+
layerNames: this.config.layerNames,
|
|
1153
|
+
})
|
|
1154
|
+
if (
|
|
1155
|
+
!this.validateCompletePlans(lengthMatchedPlans, candidateOutput).valid
|
|
1156
|
+
) {
|
|
1157
|
+
continue
|
|
1158
|
+
}
|
|
1159
|
+
const candidateState = { ...state, plans: lengthMatchedPlans }
|
|
1160
|
+
const candidateAdditionalViaCount =
|
|
1161
|
+
this.getCoordinatedAdditionalViaCount(lengthMatchedPlans)
|
|
1162
|
+
const candidateScore = getCompleteStateScore(candidateState)
|
|
1163
|
+
if (
|
|
1164
|
+
!bestState ||
|
|
1165
|
+
candidateAdditionalViaCount < bestAdditionalViaCount ||
|
|
1166
|
+
(candidateAdditionalViaCount === bestAdditionalViaCount &&
|
|
1167
|
+
candidateScore < bestMatchedScore)
|
|
1168
|
+
) {
|
|
1169
|
+
bestState = candidateState
|
|
1170
|
+
outputSrj = candidateOutput
|
|
1171
|
+
bestMatchedScore = candidateScore
|
|
1172
|
+
bestAdditionalViaCount = candidateAdditionalViaCount
|
|
1173
|
+
}
|
|
1174
|
+
if (!hasLengthConstraints) break
|
|
996
1175
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
1000
|
-
getPlanViaCount(bestState.plans) * 0.1 +
|
|
1001
|
-
assignmentLoadPenalty(
|
|
1002
|
-
bestState.assignment,
|
|
1003
|
-
this.preparedBuses,
|
|
1004
|
-
this.config.balanceLayerLoadByConnectionCount,
|
|
1005
|
-
) *
|
|
1006
|
-
getLayerLoadPenaltyWeight(this.config)
|
|
1007
|
-
: Number.POSITIVE_INFINITY
|
|
1176
|
+
if (!bestState || !outputSrj) return null
|
|
1177
|
+
const score = bestMatchedScore
|
|
1008
1178
|
if (!Number.isFinite(score)) return null
|
|
1009
1179
|
|
|
1010
1180
|
const summary: FanoutAttemptSummary = {
|
|
@@ -1116,6 +1286,66 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1116
1286
|
)
|
|
1117
1287
|
}
|
|
1118
1288
|
|
|
1289
|
+
private getCoordinatedAdditionalViaCount(
|
|
1290
|
+
plans: readonly FanoutRoutePlan[],
|
|
1291
|
+
): number {
|
|
1292
|
+
const coordinatedBusIds = new Set(
|
|
1293
|
+
this.preparedBuses
|
|
1294
|
+
.filter(busUsesCoordinatedWinding)
|
|
1295
|
+
.map((bus) => bus.busId),
|
|
1296
|
+
)
|
|
1297
|
+
return plans.reduce(
|
|
1298
|
+
(count, plan) =>
|
|
1299
|
+
count +
|
|
1300
|
+
(coordinatedBusIds.has(plan.busId)
|
|
1301
|
+
? (plan.additionalVias?.length ?? 0)
|
|
1302
|
+
: 0),
|
|
1303
|
+
0,
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
private isAttemptBetter(
|
|
1308
|
+
candidate: AssignmentAttempt,
|
|
1309
|
+
current: AssignmentAttempt,
|
|
1310
|
+
): boolean {
|
|
1311
|
+
if (
|
|
1312
|
+
candidate.summary.routedConnectionCount !==
|
|
1313
|
+
current.summary.routedConnectionCount
|
|
1314
|
+
) {
|
|
1315
|
+
return (
|
|
1316
|
+
candidate.summary.routedConnectionCount >
|
|
1317
|
+
current.summary.routedConnectionCount
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1320
|
+
if (candidate.summary.routedBusCount !== current.summary.routedBusCount) {
|
|
1321
|
+
return candidate.summary.routedBusCount > current.summary.routedBusCount
|
|
1322
|
+
}
|
|
1323
|
+
const candidateAdditionalVias = this.getCoordinatedAdditionalViaCount(
|
|
1324
|
+
candidate.plans,
|
|
1325
|
+
)
|
|
1326
|
+
const currentAdditionalVias = this.getCoordinatedAdditionalViaCount(
|
|
1327
|
+
current.plans,
|
|
1328
|
+
)
|
|
1329
|
+
if (candidateAdditionalVias !== currentAdditionalVias) {
|
|
1330
|
+
return candidateAdditionalVias < currentAdditionalVias
|
|
1331
|
+
}
|
|
1332
|
+
return candidate.summary.score < current.summary.score
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
private hasGloballyViaMinimalBestAttempt(): boolean {
|
|
1336
|
+
if (!this.hasCompleteBestAttempt() || !this.bestAttempt) return false
|
|
1337
|
+
if (
|
|
1338
|
+
this.preparedBuses.length === 0 ||
|
|
1339
|
+
!this.preparedBuses.every(busUsesCoordinatedWinding)
|
|
1340
|
+
) {
|
|
1341
|
+
return false
|
|
1342
|
+
}
|
|
1343
|
+
return this.bestAttempt.plans.every(
|
|
1344
|
+
(plan) =>
|
|
1345
|
+
plan.via !== undefined && (plan.additionalVias?.length ?? 0) === 0,
|
|
1346
|
+
)
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1119
1349
|
private shouldEvaluateGroupedBeam(): boolean {
|
|
1120
1350
|
if (this.groupedBeamEvaluated || this.nextAssignmentIndex === 0) {
|
|
1121
1351
|
return false
|
|
@@ -1130,6 +1360,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1130
1360
|
}
|
|
1131
1361
|
|
|
1132
1362
|
override _step(): void {
|
|
1363
|
+
if (
|
|
1364
|
+
this.nextAssignmentIndex > 0 &&
|
|
1365
|
+
this.hasGloballyViaMinimalBestAttempt()
|
|
1366
|
+
) {
|
|
1367
|
+
this.completeBestAttemptEndpoints()
|
|
1368
|
+
this.solved = true
|
|
1369
|
+
return
|
|
1370
|
+
}
|
|
1133
1371
|
// Try the deterministic assignment and only its targeted repair queue
|
|
1134
1372
|
// before paying for the grouped beam. If the beam cannot solve, continue
|
|
1135
1373
|
// with the broader generated-assignment search below.
|
|
@@ -1143,7 +1381,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1143
1381
|
this.attempts.push(beamAttempt.summary)
|
|
1144
1382
|
if (
|
|
1145
1383
|
!this.bestAttempt ||
|
|
1146
|
-
beamAttempt
|
|
1384
|
+
this.isAttemptBetter(beamAttempt, this.bestAttempt)
|
|
1147
1385
|
) {
|
|
1148
1386
|
this.bestAttempt = beamAttempt
|
|
1149
1387
|
}
|
|
@@ -1159,11 +1397,19 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1159
1397
|
failedBuses: "none",
|
|
1160
1398
|
bestScore: bestSummary.score,
|
|
1161
1399
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1400
|
+
if (
|
|
1401
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
|
|
1402
|
+
) {
|
|
1403
|
+
this.completeBestAttemptEndpoints()
|
|
1404
|
+
this.solved = true
|
|
1405
|
+
return
|
|
1406
|
+
}
|
|
1165
1407
|
}
|
|
1166
|
-
if (
|
|
1408
|
+
if (
|
|
1409
|
+
this.hasCompleteBestAttempt() &&
|
|
1410
|
+
this.bestAttempt &&
|
|
1411
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
|
|
1412
|
+
) {
|
|
1167
1413
|
this.completeBestAttemptEndpoints()
|
|
1168
1414
|
this.solved = true
|
|
1169
1415
|
return
|
|
@@ -1212,9 +1458,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1212
1458
|
this.solved = true
|
|
1213
1459
|
} else {
|
|
1214
1460
|
this.failed = true
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1461
|
+
const validationMessage =
|
|
1462
|
+
this.lengthMatchingFailure?.message ??
|
|
1463
|
+
this.bestAttempt?.summary.validationIssues?.[0]?.message
|
|
1464
|
+
this.error = validationMessage
|
|
1465
|
+
? `FanoutSolver: ${validationMessage}`
|
|
1466
|
+
: this.bestAttempt
|
|
1467
|
+
? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
|
|
1468
|
+
: "FanoutSolver: no layer assignment could be evaluated"
|
|
1218
1469
|
}
|
|
1219
1470
|
return
|
|
1220
1471
|
}
|
|
@@ -1237,10 +1488,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1237
1488
|
)
|
|
1238
1489
|
}
|
|
1239
1490
|
this.attempts.push(attempt.summary)
|
|
1240
|
-
if (
|
|
1241
|
-
!this.bestAttempt ||
|
|
1242
|
-
attempt.summary.score < this.bestAttempt.summary.score
|
|
1243
|
-
) {
|
|
1491
|
+
if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
|
|
1244
1492
|
this.bestAttempt = attempt
|
|
1245
1493
|
}
|
|
1246
1494
|
this.stats = {
|
|
@@ -1253,7 +1501,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1253
1501
|
}
|
|
1254
1502
|
if (
|
|
1255
1503
|
this.groupedBeamEvaluated &&
|
|
1256
|
-
attempt.summary.routedConnectionCount ===
|
|
1504
|
+
attempt.summary.routedConnectionCount ===
|
|
1505
|
+
this.inputSrj.connections.length &&
|
|
1506
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
|
|
1257
1507
|
) {
|
|
1258
1508
|
this.completeBestAttemptEndpoints()
|
|
1259
1509
|
this.solved = true
|