@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.
@@ -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
- (boundaryBusConnectionCounts.length === 5 ||
460
- boundaryBusConnectionCounts.length === 6 ||
461
- boundaryBusConnectionCounts.length === 7) &&
462
- boundaryBusConnectionCounts.filter((count) => count === 1).length === 1
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 > 7) {
479
+ if (params.boundaryBusCount === 5 || params.boundaryBusCount > 8) {
473
480
  return false
474
481
  }
475
- if (params.boundaryBusCount === 6 || params.boundaryBusCount === 7) {
476
- // Six or seven buses remove enough meander space that a severely skewed wide
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 getDenseSevenBusPairRoutingPriorityKeys(params: {
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 getMeanSourceToExitTargetDistance = (
600
+ const getMaximumSourceToExitTargetDistance = (
539
601
  bus: DensePairRoutingPriorityBus,
540
602
  ): number =>
541
- bus.connections.reduce((total, connection) => {
542
- const exitTarget = connection.exitTargetPoint!
543
- return (
544
- total +
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
- }, 0) / bus.connections.length
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(getMeanSourceToExitTargetDistance(bus).toFixed(9)),
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 = getDenseSevenBusPairRoutingPriorityKeys({
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 explicit boundary reach claim its channel
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- and seven-bus paths so their
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 > 7 ||
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
- getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
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 seven boundary buses, and heterogeneous four-bus groups, leave too little
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. A single one-line bus stays provisional
1001
- // because its flexible site can be rematched around the wide-bus copper.
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 provisionalSingletonBus = shouldDeferSingletonBoundaryViaReservation(
1007
- boundaryBusConnectionCounts,
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<number, ViaSiteCandidate>()
556
+ const assignedCandidates = new Map(
557
+ forcedCandidates.map((candidate) => [candidate.connectionIndex, candidate]),
558
+ )
530
559
  const remaining = new Set(
531
- entries.map((entry) => entry.connection.preparedConnection.connectionIndex),
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",