@tscircuit/fanout-solver 0.0.45 → 0.0.47
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/lib/fanout-solver.ts +284 -39
- package/lib/match-component-dogbone-via-sites.ts +44 -2
- package/lib/route-bus.ts +39 -10
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -447,6 +447,8 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
447
447
|
boundaryBusConnectionCounts.length === 5 ||
|
|
448
448
|
boundaryBusConnectionCounts.length === 6 ||
|
|
449
449
|
boundaryBusConnectionCounts.length === 7 ||
|
|
450
|
+
boundaryBusConnectionCounts.length === 8 ||
|
|
451
|
+
boundaryBusConnectionCounts.length === 9 ||
|
|
450
452
|
(boundaryBusConnectionCounts.length === 4 &&
|
|
451
453
|
new Set(boundaryBusConnectionCounts).size > 1)
|
|
452
454
|
)
|
|
@@ -455,25 +457,106 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
455
457
|
export function shouldDeferSingletonBoundaryViaReservation(
|
|
456
458
|
boundaryBusConnectionCounts: readonly number[],
|
|
457
459
|
): boolean {
|
|
460
|
+
const boundaryBusCount = boundaryBusConnectionCounts.length
|
|
461
|
+
const singletonBusCount = boundaryBusConnectionCounts.filter(
|
|
462
|
+
(count) => count === 1,
|
|
463
|
+
).length
|
|
458
464
|
return (
|
|
459
|
-
(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
465
|
+
((boundaryBusCount === 5 ||
|
|
466
|
+
boundaryBusCount === 6 ||
|
|
467
|
+
boundaryBusCount === 7) &&
|
|
468
|
+
singletonBusCount === 1) ||
|
|
469
|
+
(boundaryBusCount === 8 &&
|
|
470
|
+
(singletonBusCount === 1 || singletonBusCount === 2)) ||
|
|
471
|
+
(boundaryBusCount === 9 && singletonBusCount >= 1 && singletonBusCount <= 3)
|
|
463
472
|
)
|
|
464
473
|
}
|
|
465
474
|
|
|
475
|
+
export function getDenseSingletonDeferralCandidateCount(
|
|
476
|
+
boundaryBusConnectionCounts: readonly number[],
|
|
477
|
+
): number {
|
|
478
|
+
if (
|
|
479
|
+
!shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
|
|
480
|
+
) {
|
|
481
|
+
return 0
|
|
482
|
+
}
|
|
483
|
+
const singletonBusCount = boundaryBusConnectionCounts.filter(
|
|
484
|
+
(count) => count === 1,
|
|
485
|
+
).length
|
|
486
|
+
return Math.max(1, singletonBusCount - 1)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
interface DenseSourceFieldBus {
|
|
490
|
+
componentId: string
|
|
491
|
+
exitEdge?: PreparedBus["exitEdge"]
|
|
492
|
+
allowedLayers?: readonly string[]
|
|
493
|
+
routableEscapeLayers?: readonly string[]
|
|
494
|
+
connections: readonly { sourcePoint: { x: number; y: number } }[]
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function isDenseSingletonEmbeddedInMultiLayerWideBus(params: {
|
|
498
|
+
singletonBus: DenseSourceFieldBus
|
|
499
|
+
singletonTargetLayer: string
|
|
500
|
+
wideBuses: readonly DenseSourceFieldBus[]
|
|
501
|
+
}): boolean {
|
|
502
|
+
const sourcePoint = params.singletonBus.connections[0]?.sourcePoint
|
|
503
|
+
if (params.singletonBus.connections.length !== 1 || !sourcePoint) return false
|
|
504
|
+
return params.wideBuses.some((wideBus) => {
|
|
505
|
+
const wideLayers =
|
|
506
|
+
wideBus.routableEscapeLayers ?? wideBus.allowedLayers ?? []
|
|
507
|
+
if (
|
|
508
|
+
wideBus.connections.length < 8 ||
|
|
509
|
+
wideBus.componentId !== params.singletonBus.componentId ||
|
|
510
|
+
wideBus.exitEdge !== params.singletonBus.exitEdge ||
|
|
511
|
+
wideLayers.length < 2 ||
|
|
512
|
+
!wideLayers.includes(params.singletonTargetLayer)
|
|
513
|
+
) {
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
516
|
+
const sourceXs = wideBus.connections.map(
|
|
517
|
+
(connection) => connection.sourcePoint.x,
|
|
518
|
+
)
|
|
519
|
+
const sourceYs = wideBus.connections.map(
|
|
520
|
+
(connection) => connection.sourcePoint.y,
|
|
521
|
+
)
|
|
522
|
+
return (
|
|
523
|
+
sourcePoint.x >= Math.min(...sourceXs) - 1e-9 &&
|
|
524
|
+
sourcePoint.x <= Math.max(...sourceXs) + 1e-9 &&
|
|
525
|
+
sourcePoint.y >= Math.min(...sourceYs) - 1e-9 &&
|
|
526
|
+
sourcePoint.y <= Math.max(...sourceYs) + 1e-9
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function getDenseLeadingCornerBandTargetTrackOffset(params: {
|
|
532
|
+
leadingLaneCount: number
|
|
533
|
+
traceWidth: number
|
|
534
|
+
viaDiameter: number
|
|
535
|
+
clearance: number
|
|
536
|
+
}): number {
|
|
537
|
+
const cornerPitch = Math.max(
|
|
538
|
+
params.traceWidth + params.clearance,
|
|
539
|
+
params.viaDiameter + params.clearance,
|
|
540
|
+
)
|
|
541
|
+
return (-params.leadingLaneCount * cornerPitch) / 2
|
|
542
|
+
}
|
|
543
|
+
|
|
466
544
|
export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
467
545
|
boundaryBusCount: number
|
|
468
546
|
connectionCount: number
|
|
469
547
|
rawSkew: number
|
|
470
548
|
maximumSkew: number
|
|
471
549
|
}): boolean {
|
|
472
|
-
if (params.boundaryBusCount === 5 || params.boundaryBusCount >
|
|
550
|
+
if (params.boundaryBusCount === 5 || params.boundaryBusCount > 9) {
|
|
473
551
|
return false
|
|
474
552
|
}
|
|
475
|
-
if (
|
|
476
|
-
|
|
553
|
+
if (
|
|
554
|
+
params.boundaryBusCount === 6 ||
|
|
555
|
+
params.boundaryBusCount === 7 ||
|
|
556
|
+
params.boundaryBusCount === 8 ||
|
|
557
|
+
params.boundaryBusCount === 9
|
|
558
|
+
) {
|
|
559
|
+
// Six through nine buses remove enough meander space that a severely skewed wide
|
|
477
560
|
// topology can be impossible to tune. Keep the retry away from narrow
|
|
478
561
|
// differential/control groups and from modest deficits that the atomic
|
|
479
562
|
// length matcher can absorb directly.
|
|
@@ -488,6 +571,72 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
|
488
571
|
)
|
|
489
572
|
}
|
|
490
573
|
|
|
574
|
+
interface DenseSingletonBoundaryGeometryBus {
|
|
575
|
+
busId: string
|
|
576
|
+
direction: PreparedBus["direction"]
|
|
577
|
+
exitEdge?: PreparedBus["exitEdge"]
|
|
578
|
+
preferredExit?: PreparedBus["preferredExit"]
|
|
579
|
+
connections: readonly {
|
|
580
|
+
sourcePoint: { x: number; y: number }
|
|
581
|
+
exitTargetPoint?: { x: number; y: number }
|
|
582
|
+
}[]
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function getDenseSingletonBoundaryGeometry(
|
|
586
|
+
bus: DenseSingletonBoundaryGeometryBus,
|
|
587
|
+
): { isCorner: boolean; targetProjection: number } {
|
|
588
|
+
const connection = bus.connections[0]
|
|
589
|
+
const target = connection?.exitTargetPoint
|
|
590
|
+
let targetProjection = 0
|
|
591
|
+
if (connection && target) {
|
|
592
|
+
const deltaX = target.x - connection.sourcePoint.x
|
|
593
|
+
const deltaY = target.y - connection.sourcePoint.y
|
|
594
|
+
targetProjection =
|
|
595
|
+
bus.direction === "right"
|
|
596
|
+
? deltaX
|
|
597
|
+
: bus.direction === "left"
|
|
598
|
+
? -deltaX
|
|
599
|
+
: bus.direction === "up"
|
|
600
|
+
? deltaY
|
|
601
|
+
: -deltaY
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
isCorner: Boolean(getCornerBandSide(bus.exitEdge, bus.preferredExit)),
|
|
605
|
+
targetProjection,
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function compareDenseSingletonBoundaryDeferralPriority(
|
|
610
|
+
first: DenseSingletonBoundaryGeometryBus,
|
|
611
|
+
second: DenseSingletonBoundaryGeometryBus,
|
|
612
|
+
): number {
|
|
613
|
+
const firstGeometry = getDenseSingletonBoundaryGeometry(first)
|
|
614
|
+
const secondGeometry = getDenseSingletonBoundaryGeometry(second)
|
|
615
|
+
return (
|
|
616
|
+
Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) ||
|
|
617
|
+
(firstGeometry.isCorner && secondGeometry.isCorner
|
|
618
|
+
? firstGeometry.targetProjection - secondGeometry.targetProjection
|
|
619
|
+
: 0) ||
|
|
620
|
+
first.busId.localeCompare(second.busId)
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function compareReleasedDenseSingletonBoundaryDeferralPriority(
|
|
625
|
+
first: DenseSingletonBoundaryGeometryBus,
|
|
626
|
+
second: DenseSingletonBoundaryGeometryBus,
|
|
627
|
+
): number {
|
|
628
|
+
const firstGeometry = getDenseSingletonBoundaryGeometry(first)
|
|
629
|
+
const secondGeometry = getDenseSingletonBoundaryGeometry(second)
|
|
630
|
+
return (
|
|
631
|
+
Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) ||
|
|
632
|
+
(firstGeometry.isCorner && secondGeometry.isCorner
|
|
633
|
+
? Number(firstGeometry.targetProjection > 0) -
|
|
634
|
+
Number(secondGeometry.targetProjection > 0)
|
|
635
|
+
: 0) ||
|
|
636
|
+
first.busId.localeCompare(second.busId)
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
491
640
|
interface DensePairRoutingPriorityBus {
|
|
492
641
|
componentId: string
|
|
493
642
|
exitEdge?: PreparedBus["exitEdge"]
|
|
@@ -499,7 +648,7 @@ interface DensePairRoutingPriorityBus {
|
|
|
499
648
|
}[]
|
|
500
649
|
}
|
|
501
650
|
|
|
502
|
-
export function
|
|
651
|
+
export function getDenseBoundaryPairRoutingPriorityKeys(params: {
|
|
503
652
|
boundaryBusCount: number
|
|
504
653
|
pairBuses: readonly DensePairRoutingPriorityBus[]
|
|
505
654
|
}): number[] | null {
|
|
@@ -507,7 +656,9 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
|
|
|
507
656
|
const firstBus = pairBuses[0]
|
|
508
657
|
const firstSourceLayer = firstBus?.connections[0]?.sourceLayer
|
|
509
658
|
if (
|
|
510
|
-
params.boundaryBusCount !== 7
|
|
659
|
+
(params.boundaryBusCount !== 7 &&
|
|
660
|
+
params.boundaryBusCount !== 8 &&
|
|
661
|
+
params.boundaryBusCount !== 9) ||
|
|
511
662
|
pairBuses.length !== 3 ||
|
|
512
663
|
firstBus === undefined ||
|
|
513
664
|
firstBus.exitEdge === undefined ||
|
|
@@ -535,23 +686,24 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
|
|
|
535
686
|
return null
|
|
536
687
|
}
|
|
537
688
|
|
|
538
|
-
const
|
|
689
|
+
const getMaximumSourceToExitTargetDistance = (
|
|
539
690
|
bus: DensePairRoutingPriorityBus,
|
|
540
691
|
): number =>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
Math.hypot(
|
|
692
|
+
Math.max(
|
|
693
|
+
...bus.connections.map((connection) => {
|
|
694
|
+
const exitTarget = connection.exitTargetPoint!
|
|
695
|
+
return Math.hypot(
|
|
546
696
|
exitTarget.x - connection.sourcePoint.x,
|
|
547
697
|
exitTarget.y - connection.sourcePoint.y,
|
|
548
698
|
)
|
|
549
|
-
)
|
|
550
|
-
|
|
699
|
+
}),
|
|
700
|
+
)
|
|
701
|
+
// A pair is constrained by the lane with the farthest explicit reach. A
|
|
702
|
+
// mean can hide that lane behind its shorter mate and reverse channel order.
|
|
551
703
|
// Quantize once so equality is transitive. An epsilon-based pairwise
|
|
552
704
|
// comparator can otherwise produce A = B, B = C, but A != C.
|
|
553
705
|
return pairBuses.map((bus) =>
|
|
554
|
-
Number(
|
|
706
|
+
Number(getMaximumSourceToExitTargetDistance(bus).toFixed(9)),
|
|
555
707
|
)
|
|
556
708
|
}
|
|
557
709
|
|
|
@@ -846,7 +998,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
846
998
|
const twoConnectionBoundaryBuses = unsortedBoundaryBuses.filter(
|
|
847
999
|
(bus) => bus.connections.length === 2,
|
|
848
1000
|
)
|
|
849
|
-
const pairRoutingPriorityKeys =
|
|
1001
|
+
const pairRoutingPriorityKeys = getDenseBoundaryPairRoutingPriorityKeys({
|
|
850
1002
|
boundaryBusCount: unsortedBoundaryBuses.length,
|
|
851
1003
|
pairBuses: twoConnectionBoundaryBuses.map((bus) => ({
|
|
852
1004
|
...bus,
|
|
@@ -885,8 +1037,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
885
1037
|
firstPairRoutingPriority !== secondPairRoutingPriority
|
|
886
1038
|
) {
|
|
887
1039
|
// The third pair can be fenced off by two earlier pair windings. Let
|
|
888
|
-
// the pair with the shortest
|
|
889
|
-
// first without relying on caller-specific bus identifiers.
|
|
1040
|
+
// the pair with the shortest farthest-lane boundary reach claim its
|
|
1041
|
+
// channel first without relying on caller-specific bus identifiers.
|
|
890
1042
|
return firstPairRoutingPriority - secondPairRoutingPriority
|
|
891
1043
|
}
|
|
892
1044
|
const cornerBandDifference =
|
|
@@ -925,13 +1077,15 @@ export class FanoutSolver extends BaseSolver {
|
|
|
925
1077
|
if (layerDifference !== 0) return -layerDifference
|
|
926
1078
|
if (
|
|
927
1079
|
unsortedBoundaryBuses.length !== 6 &&
|
|
928
|
-
unsortedBoundaryBuses.length !== 7
|
|
1080
|
+
unsortedBoundaryBuses.length !== 7 &&
|
|
1081
|
+
unsortedBoundaryBuses.length !== 8 &&
|
|
1082
|
+
unsortedBoundaryBuses.length !== 9
|
|
929
1083
|
) {
|
|
930
1084
|
return 0
|
|
931
1085
|
}
|
|
932
1086
|
// The general routing order can differ across the two components as a
|
|
933
1087
|
// function of local pad geometry. Keep otherwise-equivalent corner buses
|
|
934
|
-
// in one deterministic order for the six-
|
|
1088
|
+
// in one deterministic order for the six- through nine-bus paths so their
|
|
935
1089
|
// boundary lanes do not swap between the two ends of a direct
|
|
936
1090
|
// interconnect. Leave the released four- and five-bus tie behavior
|
|
937
1091
|
// unchanged.
|
|
@@ -947,7 +1101,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
947
1101
|
)
|
|
948
1102
|
if (
|
|
949
1103
|
boundaryBuses.length === 0 ||
|
|
950
|
-
boundaryBuses.length >
|
|
1104
|
+
boundaryBuses.length > 9 ||
|
|
951
1105
|
planeBuses.length < 8 ||
|
|
952
1106
|
boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
|
|
953
1107
|
planeBuses.some((bus) => bus.connections.length !== 1) ||
|
|
@@ -965,13 +1119,26 @@ export class FanoutSolver extends BaseSolver {
|
|
|
965
1119
|
),
|
|
966
1120
|
),
|
|
967
1121
|
)
|
|
1122
|
+
const boundaryBusConnectionCounts = boundaryBuses.map(
|
|
1123
|
+
(bus) => bus.connections.length,
|
|
1124
|
+
)
|
|
1125
|
+
const singletonBoundaryBusCount = boundaryBusConnectionCounts.filter(
|
|
1126
|
+
(connectionCount) => connectionCount === 1,
|
|
1127
|
+
).length
|
|
1128
|
+
const useGeometryAwareSingletonOutwardPreference =
|
|
1129
|
+
singletonBoundaryBusCount > 1 &&
|
|
1130
|
+
shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
|
|
968
1131
|
const preferredBoundaryPerpendicularSideByBusId = new Map(
|
|
969
1132
|
boundaryBuses.map((bus) => [bus.busId, 1 as const]),
|
|
970
1133
|
)
|
|
971
1134
|
const preferBoundaryOutwardByBusId = new Map(
|
|
972
1135
|
boundaryBuses.map((bus) => [
|
|
973
1136
|
bus.busId,
|
|
974
|
-
|
|
1137
|
+
useGeometryAwareSingletonOutwardPreference &&
|
|
1138
|
+
bus.connections.length === 1 &&
|
|
1139
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1140
|
+
? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
|
|
1141
|
+
: getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
975
1142
|
]),
|
|
976
1143
|
)
|
|
977
1144
|
const canShareCopper = (
|
|
@@ -994,23 +1161,94 @@ export class FanoutSolver extends BaseSolver {
|
|
|
994
1161
|
),
|
|
995
1162
|
)
|
|
996
1163
|
}
|
|
997
|
-
// Five through
|
|
1164
|
+
// Five through nine boundary buses, and heterogeneous four-bus groups, leave too little
|
|
998
1165
|
// slack for incremental site allocation: a valid early trace can consume
|
|
999
1166
|
// the last dogbone site of a later narrow bus. Reserve the multi-line bus
|
|
1000
|
-
// barrels before routing copper.
|
|
1001
|
-
//
|
|
1167
|
+
// barrels before routing copper. Classify the least-constrained eligible
|
|
1168
|
+
// one-line buses as deferral candidates, preferring a centered singleton,
|
|
1169
|
+
// then a corner singleton whose explicit target lies inward along its local
|
|
1170
|
+
// escape direction. A candidate embedded in a multi-layer wide-bus source
|
|
1171
|
+
// field must instead reserve an outward dogbone and route before that wide
|
|
1172
|
+
// bus; the remaining candidates stay provisional and can be rematched
|
|
1173
|
+
// around completed copper.
|
|
1002
1174
|
// Plane sites are likewise rematched around completed boundary plans.
|
|
1003
|
-
const
|
|
1004
|
-
(bus) => bus.connections.length,
|
|
1175
|
+
const singletonBoundaryBuses = boundaryBuses.filter(
|
|
1176
|
+
(bus) => bus.connections.length === 1,
|
|
1005
1177
|
)
|
|
1006
|
-
const
|
|
1007
|
-
boundaryBusConnectionCounts
|
|
1178
|
+
const singletonDeferralCandidates =
|
|
1179
|
+
shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
|
|
1180
|
+
? singletonBoundaryBuses
|
|
1181
|
+
.toSorted(
|
|
1182
|
+
boundaryBuses.length === 9
|
|
1183
|
+
? compareDenseSingletonBoundaryDeferralPriority
|
|
1184
|
+
: compareReleasedDenseSingletonBoundaryDeferralPriority,
|
|
1185
|
+
)
|
|
1186
|
+
.slice(
|
|
1187
|
+
0,
|
|
1188
|
+
getDenseSingletonDeferralCandidateCount(
|
|
1189
|
+
boundaryBusConnectionCounts,
|
|
1190
|
+
),
|
|
1191
|
+
)
|
|
1192
|
+
: []
|
|
1193
|
+
const leadingWideSingletonBuses =
|
|
1194
|
+
boundaryBuses.length === 9
|
|
1195
|
+
? singletonDeferralCandidates.filter((singletonBus) => {
|
|
1196
|
+
const singletonTargetLayer =
|
|
1197
|
+
params.busLayerAssignments[singletonBus.busId]
|
|
1198
|
+
return Boolean(
|
|
1199
|
+
singletonTargetLayer &&
|
|
1200
|
+
isDenseSingletonEmbeddedInMultiLayerWideBus({
|
|
1201
|
+
singletonBus,
|
|
1202
|
+
singletonTargetLayer,
|
|
1203
|
+
wideBuses: boundaryBuses,
|
|
1204
|
+
}),
|
|
1205
|
+
)
|
|
1206
|
+
})
|
|
1207
|
+
: []
|
|
1208
|
+
const leadingLaneCountByWideCornerBand = new Map<string, number>()
|
|
1209
|
+
if (boundaryBuses.length === 9 && leadingWideSingletonBuses.length > 0) {
|
|
1210
|
+
for (const bus of leadingWideSingletonBuses) {
|
|
1211
|
+
preferBoundaryOutwardByBusId.set(bus.busId, true)
|
|
1212
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1213
|
+
if (!bus.exitEdge || !side) continue
|
|
1214
|
+
const bandKey = `${bus.exitEdge}:${side}`
|
|
1215
|
+
const sharesBandWithWideBus = boundaryBuses.some((candidate) => {
|
|
1216
|
+
if (candidate === bus || candidate.connections.length < 8)
|
|
1217
|
+
return false
|
|
1218
|
+
return (
|
|
1219
|
+
candidate.exitEdge === bus.exitEdge &&
|
|
1220
|
+
getCornerBandSide(candidate.exitEdge, candidate.preferredExit) ===
|
|
1221
|
+
side
|
|
1222
|
+
)
|
|
1223
|
+
})
|
|
1224
|
+
if (!sharesBandWithWideBus) continue
|
|
1225
|
+
leadingLaneCountByWideCornerBand.set(
|
|
1226
|
+
bandKey,
|
|
1227
|
+
(leadingLaneCountByWideCornerBand.get(bandKey) ?? 0) +
|
|
1228
|
+
bus.connections.length,
|
|
1229
|
+
)
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const viaProvisionalSingletonBusSet = new Set(
|
|
1233
|
+
singletonDeferralCandidates.filter(
|
|
1234
|
+
(bus) => !leadingWideSingletonBuses.includes(bus),
|
|
1235
|
+
),
|
|
1236
|
+
)
|
|
1237
|
+
const getCornerBandTargetTrackOffset = (bus: PreparedBus): number => {
|
|
1238
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1239
|
+
if (!bus.exitEdge || !side) return 0
|
|
1240
|
+
const leadingLaneCount =
|
|
1241
|
+
leadingLaneCountByWideCornerBand.get(`${bus.exitEdge}:${side}`) ?? 0
|
|
1242
|
+
return getDenseLeadingCornerBandTargetTrackOffset({
|
|
1243
|
+
leadingLaneCount,
|
|
1244
|
+
traceWidth: this.config.traceWidth,
|
|
1245
|
+
viaDiameter: this.config.viaDiameter,
|
|
1246
|
+
clearance: this.config.clearance,
|
|
1247
|
+
})
|
|
1248
|
+
}
|
|
1249
|
+
const initiallyMatchedBoundaryBuses = boundaryBuses.filter(
|
|
1250
|
+
(bus) => !viaProvisionalSingletonBusSet.has(bus),
|
|
1008
1251
|
)
|
|
1009
|
-
? boundaryBuses.find((bus) => bus.connections.length === 1)
|
|
1010
|
-
: undefined
|
|
1011
|
-
const initiallyMatchedBoundaryBuses = provisionalSingletonBus
|
|
1012
|
-
? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
|
|
1013
|
-
: boundaryBuses
|
|
1014
1252
|
const jointViaPoints = useJointBoundaryViaReservation
|
|
1015
1253
|
? matchComponentDogboneViaSites(
|
|
1016
1254
|
[...planeBuses, ...initiallyMatchedBoundaryBuses],
|
|
@@ -1039,6 +1277,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1039
1277
|
canShareCopper,
|
|
1040
1278
|
})
|
|
1041
1279
|
if (seedViaPoints) {
|
|
1280
|
+
const denseBoundaryBusesInRoutingOrder = [
|
|
1281
|
+
...leadingWideSingletonBuses,
|
|
1282
|
+
...boundaryBuses.filter(
|
|
1283
|
+
(bus) => !leadingWideSingletonBuses.includes(bus),
|
|
1284
|
+
),
|
|
1285
|
+
]
|
|
1042
1286
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
1043
1287
|
number,
|
|
1044
1288
|
{ x: number; y: number }
|
|
@@ -1099,6 +1343,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1099
1343
|
fixedViaPointsByConnectionIndex,
|
|
1100
1344
|
reservedVias: getReservedVias(bus),
|
|
1101
1345
|
viaMinimalOnly: true,
|
|
1346
|
+
cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
|
|
1102
1347
|
} as const
|
|
1103
1348
|
let busPlans = routeBusAlternatives(routeParams, 1)[0]
|
|
1104
1349
|
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
@@ -1135,14 +1380,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1135
1380
|
return true
|
|
1136
1381
|
}
|
|
1137
1382
|
|
|
1138
|
-
const firstBoundaryBus =
|
|
1383
|
+
const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0]!
|
|
1139
1384
|
const routedBoundaryBuses: PreparedBus[] = []
|
|
1140
1385
|
if (routeMatchedBoundaryBus(firstBoundaryBus)) {
|
|
1141
1386
|
routedBoundaryBuses.push(firstBoundaryBus)
|
|
1142
1387
|
} else {
|
|
1143
1388
|
matchedRoutingSucceeded = false
|
|
1144
1389
|
}
|
|
1145
|
-
const remainingBoundaryBuses =
|
|
1390
|
+
const remainingBoundaryBuses = denseBoundaryBusesInRoutingOrder.slice(1)
|
|
1146
1391
|
while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
|
|
1147
1392
|
const blockingSegments = matchedPlans.flatMap((plan) =>
|
|
1148
1393
|
plan.segments.map((segment) => ({
|
|
@@ -525,10 +525,43 @@ function matchComponent(params: {
|
|
|
525
525
|
}),
|
|
526
526
|
)
|
|
527
527
|
if (entries.some((entry) => entry.candidates.length === 0)) return null
|
|
528
|
+
// Every solution must include each sole candidate. Seed and validate those
|
|
529
|
+
// forced choices once so recursive matching only explores genuine choices.
|
|
530
|
+
const forcedCandidates = entries.flatMap((entry) =>
|
|
531
|
+
entry.candidates.length === 1 ? [entry.candidates[0]!] : [],
|
|
532
|
+
)
|
|
533
|
+
for (
|
|
534
|
+
let candidateIndex = 0;
|
|
535
|
+
candidateIndex < forcedCandidates.length;
|
|
536
|
+
candidateIndex++
|
|
537
|
+
) {
|
|
538
|
+
const candidate = forcedCandidates[candidateIndex]!
|
|
539
|
+
for (
|
|
540
|
+
let previousIndex = 0;
|
|
541
|
+
previousIndex < candidateIndex;
|
|
542
|
+
previousIndex++
|
|
543
|
+
) {
|
|
544
|
+
if (
|
|
545
|
+
!candidatesAreMutuallyClear({
|
|
546
|
+
first: candidate,
|
|
547
|
+
second: forcedCandidates[previousIndex]!,
|
|
548
|
+
rules,
|
|
549
|
+
})
|
|
550
|
+
) {
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
528
555
|
|
|
529
|
-
const assignedCandidates = new Map
|
|
556
|
+
const assignedCandidates = new Map(
|
|
557
|
+
forcedCandidates.map((candidate) => [candidate.connectionIndex, candidate]),
|
|
558
|
+
)
|
|
530
559
|
const remaining = new Set(
|
|
531
|
-
entries.
|
|
560
|
+
entries.flatMap((entry) =>
|
|
561
|
+
entry.candidates.length > 1
|
|
562
|
+
? [entry.connection.preparedConnection.connectionIndex]
|
|
563
|
+
: [],
|
|
564
|
+
),
|
|
532
565
|
)
|
|
533
566
|
const entryByConnectionIndex = new Map(
|
|
534
567
|
entries.map((entry) => [
|
|
@@ -537,6 +570,15 @@ function matchComponent(params: {
|
|
|
537
570
|
]),
|
|
538
571
|
)
|
|
539
572
|
|
|
573
|
+
if (remaining.size === 0) {
|
|
574
|
+
return new Map(
|
|
575
|
+
[...assignedCandidates.entries()].map(([connectionIndex, candidate]) => [
|
|
576
|
+
connectionIndex,
|
|
577
|
+
{ ...candidate.point },
|
|
578
|
+
]),
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
540
582
|
const getViableCandidates = (
|
|
541
583
|
entry: ConnectionCandidates,
|
|
542
584
|
): ViaSiteCandidate[] =>
|
package/lib/route-bus.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface RouteBusParams {
|
|
|
60
60
|
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
61
61
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
62
62
|
viaMinimalOnly?: boolean
|
|
63
|
+
/** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
|
|
64
|
+
cornerBandTargetTrackOffset?: number
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
interface TrackCandidate {
|
|
@@ -287,6 +289,24 @@ function getWindingCrossoverLayer(params: {
|
|
|
287
289
|
)
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
export function getBoundaryTargetTrack(params: {
|
|
293
|
+
bus: PreparedBus
|
|
294
|
+
connection: PreparedConnection
|
|
295
|
+
boundaryDirection: FanoutDirection
|
|
296
|
+
}): number {
|
|
297
|
+
const requestedTrack = getPerpendicularAxis(
|
|
298
|
+
params.connection.exitTargetPoint ?? params.connection.targetPoint,
|
|
299
|
+
params.boundaryDirection,
|
|
300
|
+
)
|
|
301
|
+
const boundaryMinimum = isHorizontal(params.boundaryDirection)
|
|
302
|
+
? params.bus.sharedBoundary.minY
|
|
303
|
+
: params.bus.sharedBoundary.minX
|
|
304
|
+
const boundaryMaximum = isHorizontal(params.boundaryDirection)
|
|
305
|
+
? params.bus.sharedBoundary.maxY
|
|
306
|
+
: params.bus.sharedBoundary.maxX
|
|
307
|
+
return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
|
|
308
|
+
}
|
|
309
|
+
|
|
290
310
|
function getCornerTargetTrack(params: {
|
|
291
311
|
bus: PreparedBus
|
|
292
312
|
connection: PreparedConnection
|
|
@@ -297,6 +317,7 @@ function getCornerTargetTrack(params: {
|
|
|
297
317
|
layerNames: readonly string[]
|
|
298
318
|
targetLayer: string
|
|
299
319
|
windingOrderIndex?: number
|
|
320
|
+
cornerBandTargetTrackOffset?: number
|
|
300
321
|
}): number {
|
|
301
322
|
const {
|
|
302
323
|
bus,
|
|
@@ -308,6 +329,7 @@ function getCornerTargetTrack(params: {
|
|
|
308
329
|
layerNames,
|
|
309
330
|
targetLayer,
|
|
310
331
|
windingOrderIndex,
|
|
332
|
+
cornerBandTargetTrackOffset = 0,
|
|
311
333
|
} = params
|
|
312
334
|
const side = getCornerSide(bus)
|
|
313
335
|
if (!side || !bus.exitEdge) {
|
|
@@ -326,7 +348,8 @@ function getCornerTargetTrack(params: {
|
|
|
326
348
|
const pitch = Math.max(traceWidth + clearance, viaDiameter + clearance)
|
|
327
349
|
const baseBandCenter =
|
|
328
350
|
boundaryMinimum +
|
|
329
|
-
(boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75)
|
|
351
|
+
(boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75) +
|
|
352
|
+
cornerBandTargetTrackOffset
|
|
330
353
|
const windingTarget = busUsesCoordinatedWindingChannel(bus)
|
|
331
354
|
? getWindingTargetRank({
|
|
332
355
|
bus,
|
|
@@ -945,6 +968,7 @@ function buildPlan(params: {
|
|
|
945
968
|
allowBlindAndBuriedVias: boolean
|
|
946
969
|
initialViaPoint?: Point2D
|
|
947
970
|
sourceEscapePath?: readonly Point2D[]
|
|
971
|
+
cornerBandTargetTrackOffset?: number
|
|
948
972
|
}): FanoutRoutePlan {
|
|
949
973
|
const {
|
|
950
974
|
preparedConnection,
|
|
@@ -967,6 +991,7 @@ function buildPlan(params: {
|
|
|
967
991
|
allowBlindAndBuriedVias,
|
|
968
992
|
initialViaPoint,
|
|
969
993
|
sourceEscapePath,
|
|
994
|
+
cornerBandTargetTrackOffset,
|
|
970
995
|
} = params
|
|
971
996
|
const sourcePoint = {
|
|
972
997
|
x: preparedConnection.sourcePoint.x,
|
|
@@ -1048,13 +1073,14 @@ function buildPlan(params: {
|
|
|
1048
1073
|
clearance,
|
|
1049
1074
|
layerNames,
|
|
1050
1075
|
targetLayer,
|
|
1076
|
+
cornerBandTargetTrackOffset,
|
|
1051
1077
|
})
|
|
1052
1078
|
: usesLayeredWindingChannel
|
|
1053
|
-
?
|
|
1054
|
-
|
|
1055
|
-
|
|
1079
|
+
? getBoundaryTargetTrack({
|
|
1080
|
+
bus,
|
|
1081
|
+
connection: preparedConnection,
|
|
1056
1082
|
boundaryDirection,
|
|
1057
|
-
)
|
|
1083
|
+
})
|
|
1058
1084
|
: track
|
|
1059
1085
|
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
1060
1086
|
const localCornerSide = getLocalCornerSide(bus)
|
|
@@ -2365,6 +2391,7 @@ export function routeBusAlternatives(
|
|
|
2365
2391
|
fixedViaPointsByConnectionIndex,
|
|
2366
2392
|
reservedVias = [],
|
|
2367
2393
|
viaMinimalOnly = false,
|
|
2394
|
+
cornerBandTargetTrackOffset,
|
|
2368
2395
|
} = params
|
|
2369
2396
|
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
2370
2397
|
throw new Error(
|
|
@@ -2698,12 +2725,13 @@ export function routeBusAlternatives(
|
|
|
2698
2725
|
layerNames,
|
|
2699
2726
|
targetLayer,
|
|
2700
2727
|
windingOrderIndex: terminalPattern.windingOrderIndex,
|
|
2728
|
+
cornerBandTargetTrackOffset,
|
|
2701
2729
|
})
|
|
2702
|
-
:
|
|
2703
|
-
|
|
2704
|
-
|
|
2730
|
+
: getBoundaryTargetTrack({
|
|
2731
|
+
bus,
|
|
2732
|
+
connection: preparedConnection,
|
|
2705
2733
|
boundaryDirection,
|
|
2706
|
-
)
|
|
2734
|
+
})
|
|
2707
2735
|
return {
|
|
2708
2736
|
connection: preparedConnection,
|
|
2709
2737
|
viaPoint: terminalPattern.getViaPoint
|
|
@@ -2882,6 +2910,7 @@ export function routeBusAlternatives(
|
|
|
2882
2910
|
clearance,
|
|
2883
2911
|
terminateAtVia: false,
|
|
2884
2912
|
allowBlindAndBuriedVias,
|
|
2913
|
+
cornerBandTargetTrackOffset,
|
|
2885
2914
|
})
|
|
2886
2915
|
if (
|
|
2887
2916
|
!planIsClear({
|
|
@@ -2889,7 +2918,7 @@ export function routeBusAlternatives(
|
|
|
2889
2918
|
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
2890
2919
|
staticClearanceCache,
|
|
2891
2920
|
blockingBusCounts,
|
|
2892
|
-
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}`,
|
|
2921
|
+
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}:${cornerBandTargetTrackOffset ?? 0}`,
|
|
2893
2922
|
srj,
|
|
2894
2923
|
sharedBoundary: bus.sharedBoundary,
|
|
2895
2924
|
clearance,
|