@tscircuit/fanout-solver 0.0.40 → 0.0.42

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.
Files changed (2) hide show
  1. package/lib/fanout-solver.ts +148 -61
  2. package/package.json +1 -1
@@ -440,6 +440,36 @@ function busUsesCoordinatedWinding(bus: PreparedBus): boolean {
440
440
  )
441
441
  }
442
442
 
443
+ export function shouldUseJointBoundaryViaReservation(
444
+ boundaryBusConnectionCounts: readonly number[],
445
+ ): boolean {
446
+ return (
447
+ boundaryBusConnectionCounts.length === 5 ||
448
+ (boundaryBusConnectionCounts.length === 4 &&
449
+ new Set(boundaryBusConnectionCounts).size > 1)
450
+ )
451
+ }
452
+
453
+ export function shouldDeferSingletonBoundaryViaReservation(
454
+ boundaryBusConnectionCounts: readonly number[],
455
+ ): boolean {
456
+ return (
457
+ boundaryBusConnectionCounts.length === 5 &&
458
+ boundaryBusConnectionCounts.filter((count) => count === 1).length === 1
459
+ )
460
+ }
461
+
462
+ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
463
+ boundaryBusCount: number
464
+ rawSkew: number
465
+ maximumSkew: number
466
+ }): boolean {
467
+ if (params.boundaryBusCount >= 5) return false
468
+ return (
469
+ params.rawSkew - params.maximumSkew > Math.max(1, params.maximumSkew * 0.25)
470
+ )
471
+ }
472
+
443
473
  function getCandidateEscapeLayersForBus(params: {
444
474
  bus: PreparedBus
445
475
  srj: SimpleRouteJson
@@ -722,48 +752,59 @@ export class FanoutSolver extends BaseSolver {
722
752
  }): MixedTerminationState | null {
723
753
  if (this.config.allowBlindAndBuriedVias) return null
724
754
 
725
- const boundaryBuses = params.busesInRoutingOrder
726
- .filter((bus) => bus.termination.type === "boundary")
727
- .toSorted((first, second) => {
728
- const cornerBandDifference =
729
- Number(
730
- Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
731
- ) -
732
- Number(
733
- Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)),
755
+ const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
756
+ (bus) => bus.termination.type === "boundary",
757
+ )
758
+ const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
759
+ unsortedBoundaryBuses.map((bus) => bus.connections.length),
760
+ )
761
+ const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
762
+ // Reserve the dense escape field for the widest buses first. Small
763
+ // control groups can usually route around their copper, while routing
764
+ // a two-line corner bus first can consume a critical channel needed by
765
+ // an eight-line winding bus and force the expensive fallback search.
766
+ if (useJointBoundaryViaReservation) {
767
+ const connectionCountDifference =
768
+ second.connections.length - first.connections.length
769
+ if (connectionCountDifference !== 0) return connectionCountDifference
770
+ }
771
+ const cornerBandDifference =
772
+ Number(
773
+ Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
774
+ ) -
775
+ Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
776
+ if (cornerBandDifference !== 0) return cornerBandDifference
777
+ const firstIsCorner = Boolean(
778
+ getCornerBandSide(first.exitEdge, first.preferredExit),
779
+ )
780
+ if (!firstIsCorner) {
781
+ const getSourceSpan = (bus: PreparedBus): number => {
782
+ const xCoordinates = bus.connections.map(
783
+ (connection) => connection.sourcePoint.x,
784
+ )
785
+ const yCoordinates = bus.connections.map(
786
+ (connection) => connection.sourcePoint.y,
787
+ )
788
+ return (
789
+ Math.max(...xCoordinates) -
790
+ Math.min(...xCoordinates) +
791
+ Math.max(...yCoordinates) -
792
+ Math.min(...yCoordinates)
734
793
  )
735
- if (cornerBandDifference !== 0) return cornerBandDifference
736
- const firstIsCorner = Boolean(
737
- getCornerBandSide(first.exitEdge, first.preferredExit),
738
- )
739
- if (!firstIsCorner) {
740
- const getSourceSpan = (bus: PreparedBus): number => {
741
- const xCoordinates = bus.connections.map(
742
- (connection) => connection.sourcePoint.x,
743
- )
744
- const yCoordinates = bus.connections.map(
745
- (connection) => connection.sourcePoint.y,
746
- )
747
- return (
748
- Math.max(...xCoordinates) -
749
- Math.min(...xCoordinates) +
750
- Math.max(...yCoordinates) -
751
- Math.min(...yCoordinates)
752
- )
753
- }
754
- const sourceSpanDifference =
755
- getSourceSpan(second) - getSourceSpan(first)
756
- if (Math.abs(sourceSpanDifference) > 1e-9) {
757
- return sourceSpanDifference
758
- }
759
794
  }
760
- const firstLayer = params.busLayerAssignments[first.busId]
761
- const secondLayer = params.busLayerAssignments[second.busId]
762
- const layerDifference =
763
- this.config.layerNames.indexOf(firstLayer ?? "") -
764
- this.config.layerNames.indexOf(secondLayer ?? "")
765
- return -layerDifference
766
- })
795
+ const sourceSpanDifference =
796
+ getSourceSpan(second) - getSourceSpan(first)
797
+ if (Math.abs(sourceSpanDifference) > 1e-9) {
798
+ return sourceSpanDifference
799
+ }
800
+ }
801
+ const firstLayer = params.busLayerAssignments[first.busId]
802
+ const secondLayer = params.busLayerAssignments[second.busId]
803
+ const layerDifference =
804
+ this.config.layerNames.indexOf(firstLayer ?? "") -
805
+ this.config.layerNames.indexOf(secondLayer ?? "")
806
+ return -layerDifference
807
+ })
767
808
  // Preserve the caller/input order for the dense singleton fill. The
768
809
  // general routing sort is useful for heterogeneous buses, but ordering a
769
810
  // regular BGA power field by obstacle depth creates artificial local
@@ -774,7 +815,7 @@ export class FanoutSolver extends BaseSolver {
774
815
  )
775
816
  if (
776
817
  boundaryBuses.length === 0 ||
777
- boundaryBuses.length > 4 ||
818
+ boundaryBuses.length > 5 ||
778
819
  planeBuses.length < 8 ||
779
820
  boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
780
821
  planeBuses.some((bus) => bus.connections.length !== 1) ||
@@ -821,9 +862,41 @@ export class FanoutSolver extends BaseSolver {
821
862
  ),
822
863
  )
823
864
  }
824
- const seedViaPoints = matchComponentDogboneViaSites(
825
- [...planeBuses, boundaryBuses[0]!],
826
- {
865
+ // Five boundary buses, and heterogeneous four-bus groups, leave too little
866
+ // slack for incremental site allocation: a valid early trace can consume
867
+ // the last dogbone site of a later narrow bus. Reserve the multi-line bus
868
+ // barrels before routing copper. A single one-line bus stays provisional
869
+ // because its flexible site can be rematched around the wide-bus copper.
870
+ // Plane sites are likewise rematched around completed boundary plans.
871
+ const boundaryBusConnectionCounts = boundaryBuses.map(
872
+ (bus) => bus.connections.length,
873
+ )
874
+ const provisionalSingletonBus = shouldDeferSingletonBoundaryViaReservation(
875
+ boundaryBusConnectionCounts,
876
+ )
877
+ ? boundaryBuses.find((bus) => bus.connections.length === 1)
878
+ : undefined
879
+ const initiallyMatchedBoundaryBuses = provisionalSingletonBus
880
+ ? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
881
+ : boundaryBuses
882
+ const jointViaPoints = useJointBoundaryViaReservation
883
+ ? matchComponentDogboneViaSites(
884
+ [...planeBuses, ...initiallyMatchedBoundaryBuses],
885
+ {
886
+ viaDiameter: this.config.viaDiameter,
887
+ viaHoleDiameter: this.config.viaHoleDiameter,
888
+ traceWidth: this.config.traceWidth,
889
+ clearance: this.config.clearance,
890
+ maximumSearchStates: 100_000,
891
+ preferredBoundaryPerpendicularSideByBusId,
892
+ preferBoundaryOutwardByBusId,
893
+ canShareCopper,
894
+ },
895
+ )
896
+ : null
897
+ const seedViaPoints =
898
+ jointViaPoints ??
899
+ matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
827
900
  viaDiameter: this.config.viaDiameter,
828
901
  viaHoleDiameter: this.config.viaHoleDiameter,
829
902
  traceWidth: this.config.traceWidth,
@@ -832,8 +905,7 @@ export class FanoutSolver extends BaseSolver {
832
905
  preferredBoundaryPerpendicularSideByBusId,
833
906
  preferBoundaryOutwardByBusId,
834
907
  canShareCopper,
835
- },
836
- )
908
+ })
837
909
  if (seedViaPoints) {
838
910
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
839
911
  number,
@@ -901,7 +973,11 @@ export class FanoutSolver extends BaseSolver {
901
973
  const lengths = busPlans.map((plan) => plan.length)
902
974
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
903
975
  const needsRouteDiversity =
904
- rawSkew - bus.maxLengthSkew > Math.max(1, bus.maxLengthSkew * 0.25)
976
+ shouldSearchAdditionalBoundaryRouteTopologies({
977
+ boundaryBusCount: boundaryBuses.length,
978
+ rawSkew,
979
+ maximumSkew: bus.maxLengthSkew,
980
+ })
905
981
  // Only pay for additional A* variants when the first topology is so
906
982
  // skewed that compact meanders are unlikely to absorb the deficit.
907
983
  // This keeps already-near-matched buses on the single-attempt path.
@@ -948,21 +1024,32 @@ export class FanoutSolver extends BaseSolver {
948
1024
  candidateIndex++
949
1025
  ) {
950
1026
  const candidateBus = remainingBoundaryBuses[candidateIndex]!
951
- const extendedViaPoints = matchComponentDogboneViaSites(
952
- [...planeBuses, ...routedBoundaryBuses, candidateBus],
953
- {
954
- viaDiameter: this.config.viaDiameter,
955
- viaHoleDiameter: this.config.viaHoleDiameter,
956
- traceWidth: this.config.traceWidth,
957
- clearance: this.config.clearance,
958
- maximumSearchStates: 100_000,
959
- preferredBoundaryPerpendicularSideByBusId,
960
- preferBoundaryOutwardByBusId,
961
- fixedViaPointsByConnectionIndex: fixedViaPointsByConnectionIndex,
962
- blockingSegments,
963
- canShareCopper,
964
- },
1027
+ const candidateHasFixedViaPoints = candidateBus.connections.every(
1028
+ (connection) =>
1029
+ fixedViaPointsByConnectionIndex.has(connection.connectionIndex),
965
1030
  )
1031
+ const extendedViaPoints =
1032
+ jointViaPoints && candidateHasFixedViaPoints
1033
+ ? // The joint map is deliberately kept intact so getReservedVias()
1034
+ // blocks every already-reserved future through-barrel during A*.
1035
+ // Provisional singleton and plane dogbones are rematched later.
1036
+ new Map(fixedViaPointsByConnectionIndex)
1037
+ : matchComponentDogboneViaSites(
1038
+ [...planeBuses, ...routedBoundaryBuses, candidateBus],
1039
+ {
1040
+ viaDiameter: this.config.viaDiameter,
1041
+ viaHoleDiameter: this.config.viaHoleDiameter,
1042
+ traceWidth: this.config.traceWidth,
1043
+ clearance: this.config.clearance,
1044
+ maximumSearchStates: 100_000,
1045
+ preferredBoundaryPerpendicularSideByBusId,
1046
+ preferBoundaryOutwardByBusId,
1047
+ fixedViaPointsByConnectionIndex:
1048
+ fixedViaPointsByConnectionIndex,
1049
+ blockingSegments,
1050
+ canShareCopper,
1051
+ },
1052
+ )
966
1053
  if (!extendedViaPoints) continue
967
1054
  const previousFixedViaPoints = fixedViaPointsByConnectionIndex
968
1055
  const previousPlanCount = matchedPlans.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",