@tscircuit/fanout-solver 0.0.45 → 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 +115 -35
- package/lib/match-component-dogbone-via-sites.ts +44 -2
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -447,6 +447,7 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
447
447
|
boundaryBusConnectionCounts.length === 5 ||
|
|
448
448
|
boundaryBusConnectionCounts.length === 6 ||
|
|
449
449
|
boundaryBusConnectionCounts.length === 7 ||
|
|
450
|
+
boundaryBusConnectionCounts.length === 8 ||
|
|
450
451
|
(boundaryBusConnectionCounts.length === 4 &&
|
|
451
452
|
new Set(boundaryBusConnectionCounts).size > 1)
|
|
452
453
|
)
|
|
@@ -455,11 +456,17 @@ export function shouldUseJointBoundaryViaReservation(
|
|
|
455
456
|
export function shouldDeferSingletonBoundaryViaReservation(
|
|
456
457
|
boundaryBusConnectionCounts: readonly number[],
|
|
457
458
|
): boolean {
|
|
459
|
+
const boundaryBusCount = boundaryBusConnectionCounts.length
|
|
460
|
+
const singletonBusCount = boundaryBusConnectionCounts.filter(
|
|
461
|
+
(count) => count === 1,
|
|
462
|
+
).length
|
|
458
463
|
return (
|
|
459
|
-
(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
464
|
+
((boundaryBusCount === 5 ||
|
|
465
|
+
boundaryBusCount === 6 ||
|
|
466
|
+
boundaryBusCount === 7) &&
|
|
467
|
+
singletonBusCount === 1) ||
|
|
468
|
+
(boundaryBusCount === 8 &&
|
|
469
|
+
(singletonBusCount === 1 || singletonBusCount === 2))
|
|
463
470
|
)
|
|
464
471
|
}
|
|
465
472
|
|
|
@@ -469,11 +476,15 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
|
469
476
|
rawSkew: number
|
|
470
477
|
maximumSkew: number
|
|
471
478
|
}): boolean {
|
|
472
|
-
if (params.boundaryBusCount === 5 || params.boundaryBusCount >
|
|
479
|
+
if (params.boundaryBusCount === 5 || params.boundaryBusCount > 8) {
|
|
473
480
|
return false
|
|
474
481
|
}
|
|
475
|
-
if (
|
|
476
|
-
|
|
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
|
|
477
488
|
// topology can be impossible to tune. Keep the retry away from narrow
|
|
478
489
|
// differential/control groups and from modest deficits that the atomic
|
|
479
490
|
// length matcher can absorb directly.
|
|
@@ -488,6 +499,57 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
|
488
499
|
)
|
|
489
500
|
}
|
|
490
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
|
+
|
|
491
553
|
interface DensePairRoutingPriorityBus {
|
|
492
554
|
componentId: string
|
|
493
555
|
exitEdge?: PreparedBus["exitEdge"]
|
|
@@ -499,7 +561,7 @@ interface DensePairRoutingPriorityBus {
|
|
|
499
561
|
}[]
|
|
500
562
|
}
|
|
501
563
|
|
|
502
|
-
export function
|
|
564
|
+
export function getDenseBoundaryPairRoutingPriorityKeys(params: {
|
|
503
565
|
boundaryBusCount: number
|
|
504
566
|
pairBuses: readonly DensePairRoutingPriorityBus[]
|
|
505
567
|
}): number[] | null {
|
|
@@ -507,7 +569,7 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
|
|
|
507
569
|
const firstBus = pairBuses[0]
|
|
508
570
|
const firstSourceLayer = firstBus?.connections[0]?.sourceLayer
|
|
509
571
|
if (
|
|
510
|
-
params.boundaryBusCount !== 7 ||
|
|
572
|
+
(params.boundaryBusCount !== 7 && params.boundaryBusCount !== 8) ||
|
|
511
573
|
pairBuses.length !== 3 ||
|
|
512
574
|
firstBus === undefined ||
|
|
513
575
|
firstBus.exitEdge === undefined ||
|
|
@@ -535,23 +597,24 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
|
|
|
535
597
|
return null
|
|
536
598
|
}
|
|
537
599
|
|
|
538
|
-
const
|
|
600
|
+
const getMaximumSourceToExitTargetDistance = (
|
|
539
601
|
bus: DensePairRoutingPriorityBus,
|
|
540
602
|
): number =>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
Math.hypot(
|
|
603
|
+
Math.max(
|
|
604
|
+
...bus.connections.map((connection) => {
|
|
605
|
+
const exitTarget = connection.exitTargetPoint!
|
|
606
|
+
return Math.hypot(
|
|
546
607
|
exitTarget.x - connection.sourcePoint.x,
|
|
547
608
|
exitTarget.y - connection.sourcePoint.y,
|
|
548
609
|
)
|
|
549
|
-
)
|
|
550
|
-
|
|
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.
|
|
551
614
|
// Quantize once so equality is transitive. An epsilon-based pairwise
|
|
552
615
|
// comparator can otherwise produce A = B, B = C, but A != C.
|
|
553
616
|
return pairBuses.map((bus) =>
|
|
554
|
-
Number(
|
|
617
|
+
Number(getMaximumSourceToExitTargetDistance(bus).toFixed(9)),
|
|
555
618
|
)
|
|
556
619
|
}
|
|
557
620
|
|
|
@@ -846,7 +909,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
846
909
|
const twoConnectionBoundaryBuses = unsortedBoundaryBuses.filter(
|
|
847
910
|
(bus) => bus.connections.length === 2,
|
|
848
911
|
)
|
|
849
|
-
const pairRoutingPriorityKeys =
|
|
912
|
+
const pairRoutingPriorityKeys = getDenseBoundaryPairRoutingPriorityKeys({
|
|
850
913
|
boundaryBusCount: unsortedBoundaryBuses.length,
|
|
851
914
|
pairBuses: twoConnectionBoundaryBuses.map((bus) => ({
|
|
852
915
|
...bus,
|
|
@@ -885,8 +948,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
885
948
|
firstPairRoutingPriority !== secondPairRoutingPriority
|
|
886
949
|
) {
|
|
887
950
|
// 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.
|
|
951
|
+
// the pair with the shortest farthest-lane boundary reach claim its
|
|
952
|
+
// channel first without relying on caller-specific bus identifiers.
|
|
890
953
|
return firstPairRoutingPriority - secondPairRoutingPriority
|
|
891
954
|
}
|
|
892
955
|
const cornerBandDifference =
|
|
@@ -925,13 +988,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
925
988
|
if (layerDifference !== 0) return -layerDifference
|
|
926
989
|
if (
|
|
927
990
|
unsortedBoundaryBuses.length !== 6 &&
|
|
928
|
-
unsortedBoundaryBuses.length !== 7
|
|
991
|
+
unsortedBoundaryBuses.length !== 7 &&
|
|
992
|
+
unsortedBoundaryBuses.length !== 8
|
|
929
993
|
) {
|
|
930
994
|
return 0
|
|
931
995
|
}
|
|
932
996
|
// The general routing order can differ across the two components as a
|
|
933
997
|
// function of local pad geometry. Keep otherwise-equivalent corner buses
|
|
934
|
-
// in one deterministic order for the six-
|
|
998
|
+
// in one deterministic order for the six- through eight-bus paths so their
|
|
935
999
|
// boundary lanes do not swap between the two ends of a direct
|
|
936
1000
|
// interconnect. Leave the released four- and five-bus tie behavior
|
|
937
1001
|
// unchanged.
|
|
@@ -947,7 +1011,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
947
1011
|
)
|
|
948
1012
|
if (
|
|
949
1013
|
boundaryBuses.length === 0 ||
|
|
950
|
-
boundaryBuses.length >
|
|
1014
|
+
boundaryBuses.length > 8 ||
|
|
951
1015
|
planeBuses.length < 8 ||
|
|
952
1016
|
boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
|
|
953
1017
|
planeBuses.some((bus) => bus.connections.length !== 1) ||
|
|
@@ -965,13 +1029,22 @@ export class FanoutSolver extends BaseSolver {
|
|
|
965
1029
|
),
|
|
966
1030
|
),
|
|
967
1031
|
)
|
|
1032
|
+
const singletonBoundaryBusCount = boundaryBuses.filter(
|
|
1033
|
+
(bus) => bus.connections.length === 1,
|
|
1034
|
+
).length
|
|
1035
|
+
const useGeometryAwareSingletonOutwardPreference =
|
|
1036
|
+
boundaryBuses.length === 8 && singletonBoundaryBusCount === 2
|
|
968
1037
|
const preferredBoundaryPerpendicularSideByBusId = new Map(
|
|
969
1038
|
boundaryBuses.map((bus) => [bus.busId, 1 as const]),
|
|
970
1039
|
)
|
|
971
1040
|
const preferBoundaryOutwardByBusId = new Map(
|
|
972
1041
|
boundaryBuses.map((bus) => [
|
|
973
1042
|
bus.busId,
|
|
974
|
-
|
|
1043
|
+
useGeometryAwareSingletonOutwardPreference &&
|
|
1044
|
+
bus.connections.length === 1 &&
|
|
1045
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1046
|
+
? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
|
|
1047
|
+
: getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
975
1048
|
]),
|
|
976
1049
|
)
|
|
977
1050
|
const canShareCopper = (
|
|
@@ -994,23 +1067,30 @@ export class FanoutSolver extends BaseSolver {
|
|
|
994
1067
|
),
|
|
995
1068
|
)
|
|
996
1069
|
}
|
|
997
|
-
// Five through
|
|
1070
|
+
// Five through eight boundary buses, and heterogeneous four-bus groups, leave too little
|
|
998
1071
|
// slack for incremental site allocation: a valid early trace can consume
|
|
999
1072
|
// the last dogbone site of a later narrow bus. Reserve the multi-line bus
|
|
1000
|
-
// barrels before routing copper.
|
|
1001
|
-
// 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.
|
|
1002
1077
|
// Plane sites are likewise rematched around completed boundary plans.
|
|
1003
1078
|
const boundaryBusConnectionCounts = boundaryBuses.map(
|
|
1004
1079
|
(bus) => bus.connections.length,
|
|
1005
1080
|
)
|
|
1006
|
-
const
|
|
1007
|
-
|
|
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),
|
|
1008
1093
|
)
|
|
1009
|
-
? boundaryBuses.find((bus) => bus.connections.length === 1)
|
|
1010
|
-
: undefined
|
|
1011
|
-
const initiallyMatchedBoundaryBuses = provisionalSingletonBus
|
|
1012
|
-
? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
|
|
1013
|
-
: boundaryBuses
|
|
1014
1094
|
const jointViaPoints = useJointBoundaryViaReservation
|
|
1015
1095
|
? matchComponentDogboneViaSites(
|
|
1016
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[] =>
|