@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.
@@ -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
- (boundaryBusConnectionCounts.length === 5 ||
459
- boundaryBusConnectionCounts.length === 6) &&
460
- 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))
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 > 6) {
479
+ if (params.boundaryBusCount === 5 || params.boundaryBusCount > 8) {
471
480
  return false
472
481
  }
473
- if (params.boundaryBusCount === 6) {
474
- // A sixth bus removes 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
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 (unsortedBoundaryBuses.length !== 6) return 0
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 path so their boundary
827
- // lanes do not swap between the two ends of a direct interconnect. Leave
828
- // the released four- and five-bus tie behavior unchanged.
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 > 6 ||
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
- 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,
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 or six boundary buses, and heterogeneous four-bus groups, leave too little
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. A single one-line bus stays provisional
892
- // 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.
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 provisionalSingletonBus = shouldDeferSingletonBoundaryViaReservation(
898
- 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),
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<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.44",
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",