@tscircuit/fanout-solver 0.0.44 → 0.0.46
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 +213 -24
- package/lib/match-component-dogbone-via-sites.ts +44 -2
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -446,6 +446,8 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
446
446
|
return (
|
|
447
447
|
boundaryBusConnectionCounts.length === 5 ||
|
|
448
448
|
boundaryBusConnectionCounts.length === 6 ||
|
|
449
|
+
boundaryBusConnectionCounts.length === 7 ||
|
|
450
|
+
boundaryBusConnectionCounts.length === 8 ||
|
|
449
451
|
(boundaryBusConnectionCounts.length === 4 &&
|
|
450
452
|
new Set(boundaryBusConnectionCounts).size > 1)
|
|
451
453
|
)
|
|
@@ -454,10 +456,17 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
454
456
|
export function shouldDeferSingletonBoundaryViaReservation(
|
|
455
457
|
boundaryBusConnectionCounts: readonly number[],
|
|
456
458
|
): boolean {
|
|
459
|
+
const boundaryBusCount = boundaryBusConnectionCounts.length
|
|
460
|
+
const singletonBusCount = boundaryBusConnectionCounts.filter(
|
|
461
|
+
(count) => count === 1,
|
|
462
|
+
).length
|
|
457
463
|
return (
|
|
458
|
-
(
|
|
459
|
-
|
|
460
|
-
|
|
464
|
+
((boundaryBusCount === 5 ||
|
|
465
|
+
boundaryBusCount === 6 ||
|
|
466
|
+
boundaryBusCount === 7) &&
|
|
467
|
+
singletonBusCount === 1) ||
|
|
468
|
+
(boundaryBusCount === 8 &&
|
|
469
|
+
(singletonBusCount === 1 || singletonBusCount === 2))
|
|
461
470
|
)
|
|
462
471
|
}
|
|
463
472
|
|
|
@@ -467,11 +476,15 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
|
467
476
|
rawSkew: number
|
|
468
477
|
maximumSkew: number
|
|
469
478
|
}): boolean {
|
|
470
|
-
if (params.boundaryBusCount === 5 || params.boundaryBusCount >
|
|
479
|
+
if (params.boundaryBusCount === 5 || params.boundaryBusCount > 8) {
|
|
471
480
|
return false
|
|
472
481
|
}
|
|
473
|
-
if (
|
|
474
|
-
|
|
482
|
+
if (
|
|
483
|
+
params.boundaryBusCount === 6 ||
|
|
484
|
+
params.boundaryBusCount === 7 ||
|
|
485
|
+
params.boundaryBusCount === 8
|
|
486
|
+
) {
|
|
487
|
+
// Six through eight buses remove enough meander space that a severely skewed wide
|
|
475
488
|
// topology can be impossible to tune. Keep the retry away from narrow
|
|
476
489
|
// differential/control groups and from modest deficits that the atomic
|
|
477
490
|
// length matcher can absorb directly.
|
|
@@ -486,6 +499,125 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
|
486
499
|
)
|
|
487
500
|
}
|
|
488
501
|
|
|
502
|
+
interface DenseSingletonBoundaryGeometryBus {
|
|
503
|
+
busId: string
|
|
504
|
+
direction: PreparedBus["direction"]
|
|
505
|
+
exitEdge?: PreparedBus["exitEdge"]
|
|
506
|
+
preferredExit?: PreparedBus["preferredExit"]
|
|
507
|
+
connections: readonly {
|
|
508
|
+
sourcePoint: { x: number; y: number }
|
|
509
|
+
exitTargetPoint?: { x: number; y: number }
|
|
510
|
+
}[]
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function getDenseSingletonBoundaryGeometry(
|
|
514
|
+
bus: DenseSingletonBoundaryGeometryBus,
|
|
515
|
+
): { isCorner: boolean; targetProjection: number } {
|
|
516
|
+
const connection = bus.connections[0]
|
|
517
|
+
const target = connection?.exitTargetPoint
|
|
518
|
+
let targetProjection = 0
|
|
519
|
+
if (connection && target) {
|
|
520
|
+
const deltaX = target.x - connection.sourcePoint.x
|
|
521
|
+
const deltaY = target.y - connection.sourcePoint.y
|
|
522
|
+
targetProjection =
|
|
523
|
+
bus.direction === "right"
|
|
524
|
+
? deltaX
|
|
525
|
+
: bus.direction === "left"
|
|
526
|
+
? -deltaX
|
|
527
|
+
: bus.direction === "up"
|
|
528
|
+
? deltaY
|
|
529
|
+
: -deltaY
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
isCorner: Boolean(getCornerBandSide(bus.exitEdge, bus.preferredExit)),
|
|
533
|
+
targetProjection,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function compareDenseSingletonBoundaryDeferralPriority(
|
|
538
|
+
first: DenseSingletonBoundaryGeometryBus,
|
|
539
|
+
second: DenseSingletonBoundaryGeometryBus,
|
|
540
|
+
): number {
|
|
541
|
+
const firstGeometry = getDenseSingletonBoundaryGeometry(first)
|
|
542
|
+
const secondGeometry = getDenseSingletonBoundaryGeometry(second)
|
|
543
|
+
return (
|
|
544
|
+
Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) ||
|
|
545
|
+
(firstGeometry.isCorner && secondGeometry.isCorner
|
|
546
|
+
? Number(firstGeometry.targetProjection > 0) -
|
|
547
|
+
Number(secondGeometry.targetProjection > 0)
|
|
548
|
+
: 0) ||
|
|
549
|
+
first.busId.localeCompare(second.busId)
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
interface DensePairRoutingPriorityBus {
|
|
554
|
+
componentId: string
|
|
555
|
+
exitEdge?: PreparedBus["exitEdge"]
|
|
556
|
+
assignedLayer?: string
|
|
557
|
+
connections: readonly {
|
|
558
|
+
sourceLayer: string
|
|
559
|
+
sourcePoint: { x: number; y: number }
|
|
560
|
+
exitTargetPoint?: { x: number; y: number; layer?: string }
|
|
561
|
+
}[]
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function getDenseBoundaryPairRoutingPriorityKeys(params: {
|
|
565
|
+
boundaryBusCount: number
|
|
566
|
+
pairBuses: readonly DensePairRoutingPriorityBus[]
|
|
567
|
+
}): number[] | null {
|
|
568
|
+
const { pairBuses } = params
|
|
569
|
+
const firstBus = pairBuses[0]
|
|
570
|
+
const firstSourceLayer = firstBus?.connections[0]?.sourceLayer
|
|
571
|
+
if (
|
|
572
|
+
(params.boundaryBusCount !== 7 && params.boundaryBusCount !== 8) ||
|
|
573
|
+
pairBuses.length !== 3 ||
|
|
574
|
+
firstBus === undefined ||
|
|
575
|
+
firstBus.exitEdge === undefined ||
|
|
576
|
+
firstBus.assignedLayer === undefined ||
|
|
577
|
+
firstSourceLayer === undefined ||
|
|
578
|
+
pairBuses.some(
|
|
579
|
+
(bus) =>
|
|
580
|
+
bus.connections.length !== 2 ||
|
|
581
|
+
bus.componentId !== firstBus.componentId ||
|
|
582
|
+
bus.exitEdge !== firstBus.exitEdge ||
|
|
583
|
+
bus.assignedLayer !== firstBus.assignedLayer ||
|
|
584
|
+
bus.connections.some((connection) => {
|
|
585
|
+
const exitTarget = connection.exitTargetPoint
|
|
586
|
+
return (
|
|
587
|
+
connection.sourceLayer !== firstSourceLayer ||
|
|
588
|
+
exitTarget?.layer !== firstBus.assignedLayer ||
|
|
589
|
+
!Number.isFinite(connection.sourcePoint.x) ||
|
|
590
|
+
!Number.isFinite(connection.sourcePoint.y) ||
|
|
591
|
+
!Number.isFinite(exitTarget?.x) ||
|
|
592
|
+
!Number.isFinite(exitTarget?.y)
|
|
593
|
+
)
|
|
594
|
+
}),
|
|
595
|
+
)
|
|
596
|
+
) {
|
|
597
|
+
return null
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const getMaximumSourceToExitTargetDistance = (
|
|
601
|
+
bus: DensePairRoutingPriorityBus,
|
|
602
|
+
): number =>
|
|
603
|
+
Math.max(
|
|
604
|
+
...bus.connections.map((connection) => {
|
|
605
|
+
const exitTarget = connection.exitTargetPoint!
|
|
606
|
+
return Math.hypot(
|
|
607
|
+
exitTarget.x - connection.sourcePoint.x,
|
|
608
|
+
exitTarget.y - connection.sourcePoint.y,
|
|
609
|
+
)
|
|
610
|
+
}),
|
|
611
|
+
)
|
|
612
|
+
// A pair is constrained by the lane with the farthest explicit reach. A
|
|
613
|
+
// mean can hide that lane behind its shorter mate and reverse channel order.
|
|
614
|
+
// Quantize once so equality is transitive. An epsilon-based pairwise
|
|
615
|
+
// comparator can otherwise produce A = B, B = C, but A != C.
|
|
616
|
+
return pairBuses.map((bus) =>
|
|
617
|
+
Number(getMaximumSourceToExitTargetDistance(bus).toFixed(9)),
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
|
|
489
621
|
function getCandidateEscapeLayersForBus(params: {
|
|
490
622
|
bus: PreparedBus
|
|
491
623
|
srj: SimpleRouteJson
|
|
@@ -774,6 +906,24 @@ export class FanoutSolver extends BaseSolver {
|
|
|
774
906
|
const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
|
|
775
907
|
unsortedBoundaryBuses.map((bus) => bus.connections.length),
|
|
776
908
|
)
|
|
909
|
+
const twoConnectionBoundaryBuses = unsortedBoundaryBuses.filter(
|
|
910
|
+
(bus) => bus.connections.length === 2,
|
|
911
|
+
)
|
|
912
|
+
const pairRoutingPriorityKeys = getDenseBoundaryPairRoutingPriorityKeys({
|
|
913
|
+
boundaryBusCount: unsortedBoundaryBuses.length,
|
|
914
|
+
pairBuses: twoConnectionBoundaryBuses.map((bus) => ({
|
|
915
|
+
...bus,
|
|
916
|
+
assignedLayer: params.busLayerAssignments[bus.busId],
|
|
917
|
+
})),
|
|
918
|
+
})
|
|
919
|
+
const pairRoutingPriorityKeyByBusId = pairRoutingPriorityKeys
|
|
920
|
+
? new Map(
|
|
921
|
+
twoConnectionBoundaryBuses.map((bus, index) => [
|
|
922
|
+
bus.busId,
|
|
923
|
+
pairRoutingPriorityKeys[index]!,
|
|
924
|
+
]),
|
|
925
|
+
)
|
|
926
|
+
: null
|
|
777
927
|
const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
|
|
778
928
|
// Reserve the dense escape field for the widest buses first. Small
|
|
779
929
|
// control groups can usually route around their copper, while routing
|
|
@@ -784,6 +934,24 @@ export class FanoutSolver extends BaseSolver {
|
|
|
784
934
|
second.connections.length - first.connections.length
|
|
785
935
|
if (connectionCountDifference !== 0) return connectionCountDifference
|
|
786
936
|
}
|
|
937
|
+
const firstLayer = params.busLayerAssignments[first.busId]
|
|
938
|
+
const secondLayer = params.busLayerAssignments[second.busId]
|
|
939
|
+
const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
|
|
940
|
+
first.busId,
|
|
941
|
+
)
|
|
942
|
+
const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
|
|
943
|
+
second.busId,
|
|
944
|
+
)
|
|
945
|
+
if (
|
|
946
|
+
firstPairRoutingPriority !== undefined &&
|
|
947
|
+
secondPairRoutingPriority !== undefined &&
|
|
948
|
+
firstPairRoutingPriority !== secondPairRoutingPriority
|
|
949
|
+
) {
|
|
950
|
+
// The third pair can be fenced off by two earlier pair windings. Let
|
|
951
|
+
// the pair with the shortest farthest-lane boundary reach claim its
|
|
952
|
+
// channel first without relying on caller-specific bus identifiers.
|
|
953
|
+
return firstPairRoutingPriority - secondPairRoutingPriority
|
|
954
|
+
}
|
|
787
955
|
const cornerBandDifference =
|
|
788
956
|
Number(
|
|
789
957
|
Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
|
|
@@ -814,18 +982,23 @@ export class FanoutSolver extends BaseSolver {
|
|
|
814
982
|
return sourceSpanDifference
|
|
815
983
|
}
|
|
816
984
|
}
|
|
817
|
-
const firstLayer = params.busLayerAssignments[first.busId]
|
|
818
|
-
const secondLayer = params.busLayerAssignments[second.busId]
|
|
819
985
|
const layerDifference =
|
|
820
986
|
this.config.layerNames.indexOf(firstLayer ?? "") -
|
|
821
987
|
this.config.layerNames.indexOf(secondLayer ?? "")
|
|
822
988
|
if (layerDifference !== 0) return -layerDifference
|
|
823
|
-
if (
|
|
989
|
+
if (
|
|
990
|
+
unsortedBoundaryBuses.length !== 6 &&
|
|
991
|
+
unsortedBoundaryBuses.length !== 7 &&
|
|
992
|
+
unsortedBoundaryBuses.length !== 8
|
|
993
|
+
) {
|
|
994
|
+
return 0
|
|
995
|
+
}
|
|
824
996
|
// The general routing order can differ across the two components as a
|
|
825
997
|
// function of local pad geometry. Keep otherwise-equivalent corner buses
|
|
826
|
-
// in one deterministic order for the six-bus
|
|
827
|
-
// lanes do not swap between the two ends of a direct
|
|
828
|
-
// the released four- and five-bus tie behavior
|
|
998
|
+
// in one deterministic order for the six- through eight-bus paths so their
|
|
999
|
+
// boundary lanes do not swap between the two ends of a direct
|
|
1000
|
+
// interconnect. Leave the released four- and five-bus tie behavior
|
|
1001
|
+
// unchanged.
|
|
829
1002
|
return first.busId.localeCompare(second.busId)
|
|
830
1003
|
})
|
|
831
1004
|
// Preserve the caller/input order for the dense singleton fill. The
|
|
@@ -838,7 +1011,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
838
1011
|
)
|
|
839
1012
|
if (
|
|
840
1013
|
boundaryBuses.length === 0 ||
|
|
841
|
-
boundaryBuses.length >
|
|
1014
|
+
boundaryBuses.length > 8 ||
|
|
842
1015
|
planeBuses.length < 8 ||
|
|
843
1016
|
boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
|
|
844
1017
|
planeBuses.some((bus) => bus.connections.length !== 1) ||
|
|
@@ -856,13 +1029,22 @@ export class FanoutSolver extends BaseSolver {
|
|
|
856
1029
|
),
|
|
857
1030
|
),
|
|
858
1031
|
)
|
|
1032
|
+
const singletonBoundaryBusCount = boundaryBuses.filter(
|
|
1033
|
+
(bus) => bus.connections.length === 1,
|
|
1034
|
+
).length
|
|
1035
|
+
const useGeometryAwareSingletonOutwardPreference =
|
|
1036
|
+
boundaryBuses.length === 8 && singletonBoundaryBusCount === 2
|
|
859
1037
|
const preferredBoundaryPerpendicularSideByBusId = new Map(
|
|
860
1038
|
boundaryBuses.map((bus) => [bus.busId, 1 as const]),
|
|
861
1039
|
)
|
|
862
1040
|
const preferBoundaryOutwardByBusId = new Map(
|
|
863
1041
|
boundaryBuses.map((bus) => [
|
|
864
1042
|
bus.busId,
|
|
865
|
-
|
|
1043
|
+
useGeometryAwareSingletonOutwardPreference &&
|
|
1044
|
+
bus.connections.length === 1 &&
|
|
1045
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1046
|
+
? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
|
|
1047
|
+
: getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
866
1048
|
]),
|
|
867
1049
|
)
|
|
868
1050
|
const canShareCopper = (
|
|
@@ -885,23 +1067,30 @@ export class FanoutSolver extends BaseSolver {
|
|
|
885
1067
|
),
|
|
886
1068
|
)
|
|
887
1069
|
}
|
|
888
|
-
// Five
|
|
1070
|
+
// Five through eight boundary buses, and heterogeneous four-bus groups, leave too little
|
|
889
1071
|
// slack for incremental site allocation: a valid early trace can consume
|
|
890
1072
|
// the last dogbone site of a later narrow bus. Reserve the multi-line bus
|
|
891
|
-
// barrels before routing copper.
|
|
892
|
-
// because its flexible site can be rematched around
|
|
1073
|
+
// barrels before routing copper. The least-constrained eligible one-line
|
|
1074
|
+
// bus stays provisional because its flexible site can be rematched around
|
|
1075
|
+
// the wide-bus copper. Prefer a centered singleton, then a corner singleton
|
|
1076
|
+
// whose explicit target lies inward along its local escape direction.
|
|
893
1077
|
// Plane sites are likewise rematched around completed boundary plans.
|
|
894
1078
|
const boundaryBusConnectionCounts = boundaryBuses.map(
|
|
895
1079
|
(bus) => bus.connections.length,
|
|
896
1080
|
)
|
|
897
|
-
const
|
|
898
|
-
|
|
1081
|
+
const singletonBoundaryBuses = boundaryBuses.filter(
|
|
1082
|
+
(bus) => bus.connections.length === 1,
|
|
1083
|
+
)
|
|
1084
|
+
const provisionalSingletonBuses =
|
|
1085
|
+
shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
|
|
1086
|
+
? singletonBoundaryBuses
|
|
1087
|
+
.toSorted(compareDenseSingletonBoundaryDeferralPriority)
|
|
1088
|
+
.slice(0, 1)
|
|
1089
|
+
: []
|
|
1090
|
+
const provisionalSingletonBusSet = new Set(provisionalSingletonBuses)
|
|
1091
|
+
const initiallyMatchedBoundaryBuses = boundaryBuses.filter(
|
|
1092
|
+
(bus) => !provisionalSingletonBusSet.has(bus),
|
|
899
1093
|
)
|
|
900
|
-
? boundaryBuses.find((bus) => bus.connections.length === 1)
|
|
901
|
-
: undefined
|
|
902
|
-
const initiallyMatchedBoundaryBuses = provisionalSingletonBus
|
|
903
|
-
? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
|
|
904
|
-
: boundaryBuses
|
|
905
1094
|
const jointViaPoints = useJointBoundaryViaReservation
|
|
906
1095
|
? matchComponentDogboneViaSites(
|
|
907
1096
|
[...planeBuses, ...initiallyMatchedBoundaryBuses],
|
|
@@ -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[] =>
|