@tscircuit/fanout-solver 0.0.39 → 0.0.41

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.
@@ -14,7 +14,10 @@ import {
14
14
  getViaSpanLayers,
15
15
  } from "./layer-names"
16
16
  import { matchBusPlanLengths } from "./match-bus-lengths"
17
- import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
17
+ import {
18
+ getComponentDogboneViaSiteCandidates,
19
+ matchComponentDogboneViaSites,
20
+ } from "./match-component-dogbone-via-sites"
18
21
  import { connectionsShareElectricalNet } from "./net-identity"
19
22
  import {
20
23
  prepareFanoutBuses,
@@ -514,10 +517,6 @@ export class FanoutSolver extends BaseSolver {
514
517
  private bestAttempt: AssignmentAttempt | null = null
515
518
  private lengthMatchingFailure: FanoutValidationIssue | null = null
516
519
  private endpointCompletion: CompleteOriginalEndpointsResult | null = null
517
- private denseDogboneViaPoints:
518
- | Map<number, { x: number; y: number }>
519
- | null
520
- | undefined
521
520
 
522
521
  constructor(
523
522
  public readonly inputSrj: SimpleRouteJson,
@@ -723,9 +722,60 @@ export class FanoutSolver extends BaseSolver {
723
722
  }): MixedTerminationState | null {
724
723
  if (this.config.allowBlindAndBuriedVias) return null
725
724
 
726
- const boundaryBuses = params.busesInRoutingOrder.filter(
725
+ const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
727
726
  (bus) => bus.termination.type === "boundary",
728
727
  )
728
+ const useJointFourBusReservation =
729
+ unsortedBoundaryBuses.length === 4 &&
730
+ new Set(unsortedBoundaryBuses.map((bus) => bus.connections.length)).size >
731
+ 1
732
+ const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
733
+ // Reserve the dense escape field for the widest buses first. Small
734
+ // control groups can usually route around their copper, while routing
735
+ // a two-line corner bus first can consume a critical channel needed by
736
+ // an eight-line winding bus and force the expensive fallback search.
737
+ if (useJointFourBusReservation) {
738
+ const connectionCountDifference =
739
+ second.connections.length - first.connections.length
740
+ if (connectionCountDifference !== 0) return connectionCountDifference
741
+ }
742
+ const cornerBandDifference =
743
+ Number(
744
+ Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
745
+ ) -
746
+ Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
747
+ if (cornerBandDifference !== 0) return cornerBandDifference
748
+ const firstIsCorner = Boolean(
749
+ getCornerBandSide(first.exitEdge, first.preferredExit),
750
+ )
751
+ if (!firstIsCorner) {
752
+ const getSourceSpan = (bus: PreparedBus): number => {
753
+ const xCoordinates = bus.connections.map(
754
+ (connection) => connection.sourcePoint.x,
755
+ )
756
+ const yCoordinates = bus.connections.map(
757
+ (connection) => connection.sourcePoint.y,
758
+ )
759
+ return (
760
+ Math.max(...xCoordinates) -
761
+ Math.min(...xCoordinates) +
762
+ Math.max(...yCoordinates) -
763
+ Math.min(...yCoordinates)
764
+ )
765
+ }
766
+ const sourceSpanDifference =
767
+ getSourceSpan(second) - getSourceSpan(first)
768
+ if (Math.abs(sourceSpanDifference) > 1e-9) {
769
+ return sourceSpanDifference
770
+ }
771
+ }
772
+ const firstLayer = params.busLayerAssignments[first.busId]
773
+ const secondLayer = params.busLayerAssignments[second.busId]
774
+ const layerDifference =
775
+ this.config.layerNames.indexOf(firstLayer ?? "") -
776
+ this.config.layerNames.indexOf(secondLayer ?? "")
777
+ return -layerDifference
778
+ })
729
779
  // Preserve the caller/input order for the dense singleton fill. The
730
780
  // general routing sort is useful for heterogeneous buses, but ordering a
731
781
  // regular BGA power field by obstacle depth creates artificial local
@@ -763,119 +813,372 @@ export class FanoutSolver extends BaseSolver {
763
813
  getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
764
814
  ]),
765
815
  )
766
- if (this.denseDogboneViaPoints === undefined) {
767
- this.denseDogboneViaPoints = matchComponentDogboneViaSites(
768
- this.preparedBuses,
769
- {
816
+ const canShareCopper = (
817
+ firstConnectionIndex: number,
818
+ secondConnectionIndex: number,
819
+ ): boolean => {
820
+ if (!this.config.allowSameNetMerges) return false
821
+ const firstConnectionName =
822
+ connectionNameByIndex.get(firstConnectionIndex)
823
+ const secondConnectionName = connectionNameByIndex.get(
824
+ secondConnectionIndex,
825
+ )
826
+ return Boolean(
827
+ firstConnectionName &&
828
+ secondConnectionName &&
829
+ connectionsShareElectricalNet(
830
+ this.routingSrj,
831
+ firstConnectionName,
832
+ secondConnectionName,
833
+ ),
834
+ )
835
+ }
836
+ // Four boundary buses leave too little slack for incremental site
837
+ // allocation: a valid early trace can consume the last dogbone site of a
838
+ // later narrow bus. Reserve every physical barrel before routing copper.
839
+ // Plane sites remain provisional and are rematched around the completed
840
+ // boundary plans below.
841
+ const jointViaPoints = useJointFourBusReservation
842
+ ? matchComponentDogboneViaSites([...planeBuses, ...boundaryBuses], {
770
843
  viaDiameter: this.config.viaDiameter,
771
844
  viaHoleDiameter: this.config.viaHoleDiameter,
772
845
  traceWidth: this.config.traceWidth,
773
846
  clearance: this.config.clearance,
774
- maximumSearchStates: 20_000,
847
+ maximumSearchStates: 100_000,
775
848
  preferredBoundaryPerpendicularSideByBusId,
776
849
  preferBoundaryOutwardByBusId,
777
- canShareCopper: (firstConnectionIndex, secondConnectionIndex) => {
778
- if (!this.config.allowSameNetMerges) return false
779
- const firstConnectionName =
780
- connectionNameByIndex.get(firstConnectionIndex)
781
- const secondConnectionName = connectionNameByIndex.get(
782
- secondConnectionIndex,
783
- )
784
- return Boolean(
785
- firstConnectionName &&
786
- secondConnectionName &&
787
- connectionsShareElectricalNet(
788
- this.routingSrj,
789
- firstConnectionName,
790
- secondConnectionName,
791
- ),
850
+ canShareCopper,
851
+ })
852
+ : null
853
+ const seedViaPoints =
854
+ jointViaPoints ??
855
+ matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
856
+ viaDiameter: this.config.viaDiameter,
857
+ viaHoleDiameter: this.config.viaHoleDiameter,
858
+ traceWidth: this.config.traceWidth,
859
+ clearance: this.config.clearance,
860
+ maximumSearchStates: 20_000,
861
+ preferredBoundaryPerpendicularSideByBusId,
862
+ preferBoundaryOutwardByBusId,
863
+ canShareCopper,
864
+ })
865
+ if (seedViaPoints) {
866
+ let fixedViaPointsByConnectionIndex: ReadonlyMap<
867
+ number,
868
+ { x: number; y: number }
869
+ > = seedViaPoints
870
+ let matchedPlans: FanoutRoutePlan[] = []
871
+ let matchedRoutingSucceeded = true
872
+ const getReservedVias = (bus: PreparedBus) => {
873
+ const currentConnectionNames = new Set(
874
+ bus.connections.map((connection) => connection.connection.name),
875
+ )
876
+ return this.preparedBuses.flatMap((preparedBus) => {
877
+ const targetLayer = params.busLayerAssignments[preparedBus.busId]
878
+ if (!targetLayer) return []
879
+ return preparedBus.connections.flatMap((connection) => {
880
+ if (currentConnectionNames.has(connection.connection.name))
881
+ return []
882
+ const center = fixedViaPointsByConnectionIndex.get(
883
+ connection.connectionIndex,
792
884
  )
793
- },
794
- },
795
- )
796
- }
797
- const fixedViaPointsByConnectionIndex = this.denseDogboneViaPoints
798
- if (fixedViaPointsByConnectionIndex) {
799
- const reservedViasByComponentId = new Map<
800
- string,
801
- Array<{
802
- connectionName: string
803
- via: {
804
- center: { x: number; y: number }
805
- diameter: number
806
- spanLayers: string[]
807
- }
808
- }>
809
- >()
810
- for (const bus of this.preparedBuses) {
811
- const targetLayer = params.busLayerAssignments[bus.busId]
812
- if (!targetLayer) continue
813
- const componentReservedVias =
814
- reservedViasByComponentId.get(bus.componentId) ?? []
815
- for (const connection of bus.connections) {
816
- const center = fixedViaPointsByConnectionIndex.get(
817
- connection.connectionIndex,
818
- )
819
- if (!center) continue
820
- componentReservedVias.push({
821
- connectionName: connection.connection.name,
822
- via: {
823
- center,
824
- diameter: this.config.viaDiameter,
825
- spanLayers: getViaSpanLayers({
826
- fromLayer: connection.sourceLayer,
827
- toLayer: targetLayer,
828
- layerNames: this.config.layerNames,
829
- allowBlindAndBuriedVias: false,
830
- }),
831
- },
885
+ if (!center) return []
886
+ return [
887
+ {
888
+ connectionName: connection.connection.name,
889
+ via: {
890
+ center,
891
+ diameter: this.config.viaDiameter,
892
+ spanLayers: getViaSpanLayers({
893
+ fromLayer: connection.sourceLayer,
894
+ toLayer: targetLayer,
895
+ layerNames: this.config.layerNames,
896
+ allowBlindAndBuriedVias: false,
897
+ }),
898
+ },
899
+ },
900
+ ]
832
901
  })
833
- }
834
- reservedViasByComponentId.set(bus.componentId, componentReservedVias)
902
+ })
835
903
  }
836
-
837
- const matchedPlans: FanoutRoutePlan[] = []
838
- let matchedRoutingSucceeded = true
839
- for (const bus of boundaryBuses) {
904
+ const routeMatchedBoundaryBus = (bus: PreparedBus): boolean => {
840
905
  const targetLayer = params.busLayerAssignments[bus.busId]
841
906
  if (!targetLayer) {
907
+ return false
908
+ }
909
+ const routeParams = {
910
+ srj: this.routingSrj,
911
+ bus,
912
+ targetLayer,
913
+ acceptedPlans: matchedPlans,
914
+ layerNames: this.config.layerNames,
915
+ traceWidth: this.config.traceWidth,
916
+ viaDiameter: this.config.viaDiameter,
917
+ viaHoleDiameter: this.config.viaHoleDiameter,
918
+ clearance: this.config.clearance,
919
+ compactBusTracks: this.config.compactBusTracks,
920
+ allowBlindAndBuriedVias: false,
921
+ allowSameNetMerges: this.config.allowSameNetMerges,
922
+ staticClearanceCache: this.routeStaticClearanceCache,
923
+ fixedViaPointsByConnectionIndex,
924
+ reservedVias: getReservedVias(bus),
925
+ viaMinimalOnly: true,
926
+ } as const
927
+ let busPlans = routeBusAlternatives(routeParams, 1)[0]
928
+ if (busPlans && bus.maxLengthSkew !== undefined) {
929
+ const lengths = busPlans.map((plan) => plan.length)
930
+ const rawSkew = Math.max(...lengths) - Math.min(...lengths)
931
+ const needsRouteDiversity =
932
+ rawSkew - bus.maxLengthSkew > Math.max(1, bus.maxLengthSkew * 0.25)
933
+ // Only pay for additional A* variants when the first topology is so
934
+ // skewed that compact meanders are unlikely to absorb the deficit.
935
+ // This keeps already-near-matched buses on the single-attempt path.
936
+ if (needsRouteDiversity) {
937
+ busPlans = routeBusAlternatives(routeParams, 3).toSorted(
938
+ (first, second) => {
939
+ const firstLengths = first.map((plan) => plan.length)
940
+ const secondLengths = second.map((plan) => plan.length)
941
+ return (
942
+ Math.max(...firstLengths) -
943
+ Math.min(...firstLengths) -
944
+ (Math.max(...secondLengths) - Math.min(...secondLengths))
945
+ )
946
+ },
947
+ )[0]
948
+ }
949
+ }
950
+ if (!busPlans) {
951
+ return false
952
+ }
953
+ matchedPlans.push(...busPlans)
954
+ return true
955
+ }
956
+
957
+ const firstBoundaryBus = boundaryBuses[0]!
958
+ const routedBoundaryBuses: PreparedBus[] = []
959
+ if (routeMatchedBoundaryBus(firstBoundaryBus)) {
960
+ routedBoundaryBuses.push(firstBoundaryBus)
961
+ } else {
962
+ matchedRoutingSucceeded = false
963
+ }
964
+ const remainingBoundaryBuses = boundaryBuses.slice(1)
965
+ while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
966
+ const blockingSegments = matchedPlans.flatMap((plan) =>
967
+ plan.segments.map((segment) => ({
968
+ connectionIndex: plan.connectionIndex,
969
+ segment,
970
+ })),
971
+ )
972
+ let selectedBusIndex = -1
973
+ for (
974
+ let candidateIndex = 0;
975
+ candidateIndex < remainingBoundaryBuses.length;
976
+ candidateIndex++
977
+ ) {
978
+ const candidateBus = remainingBoundaryBuses[candidateIndex]!
979
+ const extendedViaPoints = jointViaPoints
980
+ ? // The joint map is deliberately kept intact so getReservedVias()
981
+ // blocks every future through-barrel during A*. Plane dogbones are
982
+ // validated/rematched once the boundary copper is complete.
983
+ new Map(fixedViaPointsByConnectionIndex)
984
+ : matchComponentDogboneViaSites(
985
+ [...planeBuses, ...routedBoundaryBuses, candidateBus],
986
+ {
987
+ viaDiameter: this.config.viaDiameter,
988
+ viaHoleDiameter: this.config.viaHoleDiameter,
989
+ traceWidth: this.config.traceWidth,
990
+ clearance: this.config.clearance,
991
+ maximumSearchStates: 100_000,
992
+ preferredBoundaryPerpendicularSideByBusId,
993
+ preferBoundaryOutwardByBusId,
994
+ fixedViaPointsByConnectionIndex:
995
+ fixedViaPointsByConnectionIndex,
996
+ blockingSegments,
997
+ canShareCopper,
998
+ },
999
+ )
1000
+ if (!extendedViaPoints) continue
1001
+ const previousFixedViaPoints = fixedViaPointsByConnectionIndex
1002
+ const previousPlanCount = matchedPlans.length
1003
+ const laterBuses = remainingBoundaryBuses.filter(
1004
+ (_, laterIndex) => laterIndex !== candidateIndex,
1005
+ )
1006
+ let candidateFixedViaPoints: ReadonlyMap<
1007
+ number,
1008
+ { x: number; y: number }
1009
+ > = extendedViaPoints
1010
+ if (laterBuses.length === 1) {
1011
+ const laterBus = laterBuses[0]!
1012
+ const futureAssignment = matchComponentDogboneViaSites(
1013
+ [...planeBuses, ...routedBoundaryBuses, candidateBus, laterBus],
1014
+ {
1015
+ viaDiameter: this.config.viaDiameter,
1016
+ viaHoleDiameter: this.config.viaHoleDiameter,
1017
+ traceWidth: this.config.traceWidth,
1018
+ clearance: this.config.clearance,
1019
+ maximumSearchStates: 100_000,
1020
+ preferredBoundaryPerpendicularSideByBusId,
1021
+ preferBoundaryOutwardByBusId,
1022
+ fixedViaPointsByConnectionIndex: extendedViaPoints,
1023
+ blockingSegments,
1024
+ canShareCopper,
1025
+ },
1026
+ )
1027
+ if (futureAssignment) {
1028
+ const candidateCountByConnectionIndex = new Map<number, number>()
1029
+ for (const candidate of getComponentDogboneViaSiteCandidates(
1030
+ [laterBus],
1031
+ {
1032
+ viaDiameter: this.config.viaDiameter,
1033
+ viaHoleDiameter: this.config.viaHoleDiameter,
1034
+ traceWidth: this.config.traceWidth,
1035
+ clearance: this.config.clearance,
1036
+ blockingSegments,
1037
+ canShareCopper,
1038
+ },
1039
+ )) {
1040
+ candidateCountByConnectionIndex.set(
1041
+ candidate.connectionIndex,
1042
+ (candidateCountByConnectionIndex.get(
1043
+ candidate.connectionIndex,
1044
+ ) ?? 0) + 1,
1045
+ )
1046
+ }
1047
+ const constrainedConnections = laterBus.connections.toSorted(
1048
+ (first, second) =>
1049
+ (candidateCountByConnectionIndex.get(first.connectionIndex) ??
1050
+ 0) -
1051
+ (candidateCountByConnectionIndex.get(
1052
+ second.connectionIndex,
1053
+ ) ?? 0) || first.connectionIndex - second.connectionIndex,
1054
+ )
1055
+ const repairedViaPoints = new Map(extendedViaPoints)
1056
+ for (const connection of constrainedConnections) {
1057
+ const criticalPoint = futureAssignment.get(
1058
+ connection.connectionIndex,
1059
+ )
1060
+ if (criticalPoint) {
1061
+ repairedViaPoints.set(
1062
+ connection.connectionIndex,
1063
+ criticalPoint,
1064
+ )
1065
+ }
1066
+ }
1067
+ candidateFixedViaPoints = repairedViaPoints
1068
+ }
1069
+ }
1070
+ fixedViaPointsByConnectionIndex = candidateFixedViaPoints
1071
+ if (routeMatchedBoundaryBus(candidateBus)) {
1072
+ const candidateLeavesAFeasibleExtension =
1073
+ laterBuses.length === 0 ||
1074
+ laterBuses.some((laterBus) => {
1075
+ const lookaheadBlockingSegments = matchedPlans.flatMap((plan) =>
1076
+ plan.segments.map((segment) => ({
1077
+ connectionIndex: plan.connectionIndex,
1078
+ segment,
1079
+ })),
1080
+ )
1081
+ return Boolean(
1082
+ matchComponentDogboneViaSites(
1083
+ [
1084
+ ...planeBuses,
1085
+ ...routedBoundaryBuses,
1086
+ candidateBus,
1087
+ laterBus,
1088
+ ],
1089
+ {
1090
+ viaDiameter: this.config.viaDiameter,
1091
+ viaHoleDiameter: this.config.viaHoleDiameter,
1092
+ traceWidth: this.config.traceWidth,
1093
+ clearance: this.config.clearance,
1094
+ maximumSearchStates: 100_000,
1095
+ preferredBoundaryPerpendicularSideByBusId,
1096
+ preferBoundaryOutwardByBusId,
1097
+ fixedViaPointsByConnectionIndex:
1098
+ fixedViaPointsByConnectionIndex,
1099
+ blockingSegments: lookaheadBlockingSegments,
1100
+ canShareCopper,
1101
+ },
1102
+ ),
1103
+ )
1104
+ })
1105
+ if (candidateLeavesAFeasibleExtension) {
1106
+ selectedBusIndex = candidateIndex
1107
+ routedBoundaryBuses.push(candidateBus)
1108
+ break
1109
+ }
1110
+ matchedPlans.splice(previousPlanCount)
1111
+ }
1112
+ fixedViaPointsByConnectionIndex = previousFixedViaPoints
1113
+ }
1114
+ if (selectedBusIndex < 0) {
842
1115
  matchedRoutingSucceeded = false
843
1116
  break
844
1117
  }
845
- const currentConnectionNames = new Set(
846
- bus.connections.map((connection) => connection.connection.name),
847
- )
848
- const reservedVias = (
849
- reservedViasByComponentId.get(bus.componentId) ?? []
850
- ).filter(
851
- ({ connectionName }) => !currentConnectionNames.has(connectionName),
852
- )
853
- const busPlans = routeBusAlternatives(
854
- {
855
- srj: this.routingSrj,
856
- bus,
857
- targetLayer,
858
- acceptedPlans: matchedPlans,
859
- layerNames: this.config.layerNames,
860
- traceWidth: this.config.traceWidth,
861
- viaDiameter: this.config.viaDiameter,
862
- viaHoleDiameter: this.config.viaHoleDiameter,
863
- clearance: this.config.clearance,
864
- compactBusTracks: this.config.compactBusTracks,
865
- allowBlindAndBuriedVias: false,
866
- allowSameNetMerges: this.config.allowSameNetMerges,
867
- staticClearanceCache: this.routeStaticClearanceCache,
868
- fixedViaPointsByConnectionIndex,
869
- reservedVias,
870
- viaMinimalOnly: true,
1118
+ remainingBoundaryBuses.splice(selectedBusIndex, 1)
1119
+ }
1120
+ if (matchedRoutingSucceeded) {
1121
+ let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
1122
+ null
1123
+ const matchViaPointsAroundPlans = (
1124
+ candidatePlans: readonly FanoutRoutePlan[],
1125
+ ): Map<number, { x: number; y: number }> | null => {
1126
+ const fixedBoundaryViaPoints = new Map(
1127
+ candidatePlans.flatMap((plan) =>
1128
+ plan.via
1129
+ ? [[plan.connectionIndex, plan.via.center] as const]
1130
+ : [],
1131
+ ),
1132
+ )
1133
+ return matchComponentDogboneViaSites(
1134
+ [...planeBuses, ...boundaryBuses],
1135
+ {
1136
+ viaDiameter: this.config.viaDiameter,
1137
+ viaHoleDiameter: this.config.viaHoleDiameter,
1138
+ traceWidth: this.config.traceWidth,
1139
+ clearance: this.config.clearance,
1140
+ maximumSearchStates: 100_000,
1141
+ preferredBoundaryPerpendicularSideByBusId,
1142
+ preferBoundaryOutwardByBusId,
1143
+ fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
1144
+ blockingSegments: candidatePlans.flatMap((plan) =>
1145
+ plan.segments.map((segment) => ({
1146
+ connectionIndex: plan.connectionIndex,
1147
+ segment,
1148
+ })),
1149
+ ),
1150
+ canShareCopper,
1151
+ },
1152
+ )
1153
+ }
1154
+ const matchedLengthResult = matchBusPlanLengths({
1155
+ plans: matchedPlans,
1156
+ preparedBuses: this.preparedBuses,
1157
+ inputSrj: this.inputSrj,
1158
+ sharedBoundary: this.getValidationBoundary(),
1159
+ clearance: this.config.clearance,
1160
+ allowBlindAndBuriedVias: false,
1161
+ allowSameNetMerges: this.config.allowSameNetMerges,
1162
+ allowMatchingInsideDenseBounds: true,
1163
+ candidatePlansAreFeasible: (candidatePlans) => {
1164
+ const candidateViaPoints = matchViaPointsAroundPlans(candidatePlans)
1165
+ if (!candidateViaPoints) return false
1166
+ feasibleViaPoints = candidateViaPoints
1167
+ return true
871
1168
  },
872
- 1,
873
- )[0]
874
- if (!busPlans) {
1169
+ })
1170
+ if (matchedLengthResult.plans) {
1171
+ matchedPlans = matchedLengthResult.plans
1172
+ const rematchedViaPoints =
1173
+ feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans)
1174
+ if (rematchedViaPoints) {
1175
+ fixedViaPointsByConnectionIndex = rematchedViaPoints
1176
+ } else {
1177
+ matchedRoutingSucceeded = false
1178
+ }
1179
+ } else {
875
1180
  matchedRoutingSucceeded = false
876
- break
877
1181
  }
878
- matchedPlans.push(...busPlans)
879
1182
  }
880
1183
  if (matchedRoutingSucceeded) {
881
1184
  for (const bus of planeBuses) {
@@ -448,6 +448,7 @@ function createTunedPlanCandidates(params: {
448
448
  targetAddedLength: number
449
449
  clearance: number
450
450
  sharedBoundary: Bounds
451
+ allowInsideDenseBounds?: boolean
451
452
  denseBoundarySplitApplied?: boolean
452
453
  }): FanoutRoutePlan[] {
453
454
  const {
@@ -456,6 +457,7 @@ function createTunedPlanCandidates(params: {
456
457
  targetAddedLength,
457
458
  clearance,
458
459
  sharedBoundary,
460
+ allowInsideDenseBounds = false,
459
461
  denseBoundarySplitApplied = false,
460
462
  } = params
461
463
  const candidates: FanoutRoutePlan[] = []
@@ -497,6 +499,7 @@ function createTunedPlanCandidates(params: {
497
499
  continue
498
500
  }
499
501
  if (
502
+ !allowInsideDenseBounds &&
500
503
  points
501
504
  .slice(1, -1)
502
505
  .some(
@@ -575,6 +578,17 @@ export function matchBusPlanLengths(params: {
575
578
  clearance: number
576
579
  allowBlindAndBuriedVias?: boolean
577
580
  allowSameNetMerges?: boolean
581
+ /**
582
+ * Allows tuning on target-layer copper inside the component pad envelope.
583
+ * Intended only for coordinated dense routing whose complete copper is
584
+ * revalidated and whose remaining dogbone capacity is checked atomically.
585
+ */
586
+ allowMatchingInsideDenseBounds?: boolean
587
+ /**
588
+ * Rejects a geometrically clear candidate when it would make a caller-owned
589
+ * downstream assignment (such as pending plane dogbones) infeasible.
590
+ */
591
+ candidatePlansAreFeasible?: (plans: readonly FanoutRoutePlan[]) => boolean
578
592
  }):
579
593
  | { plans: FanoutRoutePlan[]; failedBus?: never }
580
594
  | { plans: null; failedBus: PreparedBus } {
@@ -585,6 +599,8 @@ export function matchBusPlanLengths(params: {
585
599
  clearance,
586
600
  allowBlindAndBuriedVias = true,
587
601
  allowSameNetMerges = false,
602
+ allowMatchingInsideDenseBounds = false,
603
+ candidatePlansAreFeasible,
588
604
  } = params
589
605
  let matchedPlans = [...params.plans]
590
606
  const constrainedBuses = preparedBuses.filter(
@@ -633,6 +649,95 @@ export function matchBusPlanLengths(params: {
633
649
  )
634
650
  .toSorted((first, second) => first - second)
635
651
  let acceptedPlans: FanoutRoutePlan[] | null = null
652
+ const acceptCandidate = (
653
+ candidate: FanoutRoutePlan,
654
+ ): FanoutRoutePlan[] | null => {
655
+ const nextPlans = matchedPlans.map((plan) =>
656
+ plan === shortest ? candidate : plan,
657
+ )
658
+ const nextBusPlans = nextPlans.filter(
659
+ (plan) => plan.busId === bus.busId,
660
+ )
661
+ if (getBusSkew(nextBusPlans) > skew + EPSILON) return null
662
+ if (
663
+ !fanoutPlansAreClear({
664
+ plans: nextPlans,
665
+ srj: inputSrj,
666
+ sharedBoundary,
667
+ clearance,
668
+ allowBlindAndBuriedVias,
669
+ allowSameNetMerges,
670
+ })
671
+ ) {
672
+ return null
673
+ }
674
+ if (
675
+ candidatePlansAreFeasible &&
676
+ !candidatePlansAreFeasible(nextPlans)
677
+ ) {
678
+ return null
679
+ }
680
+ return nextPlans
681
+ }
682
+ const findMultiSpanCandidate = (
683
+ targetAddedLength: number,
684
+ ): FanoutRoutePlan[] | null => {
685
+ const maximumSearchStates = 320
686
+ const maximumCandidatesPerState = 48
687
+ let searchedStateCount = 0
688
+ const sampleCandidates = (
689
+ candidates: readonly FanoutRoutePlan[],
690
+ ): FanoutRoutePlan[] => {
691
+ if (candidates.length <= maximumCandidatesPerState) {
692
+ return [...candidates]
693
+ }
694
+ return Array.from(
695
+ { length: maximumCandidatesPerState },
696
+ (_, sampleIndex) =>
697
+ candidates[
698
+ Math.floor(
699
+ (sampleIndex * candidates.length) / maximumCandidatesPerState,
700
+ )
701
+ ]!,
702
+ )
703
+ }
704
+ const search = (
705
+ currentPlan: FanoutRoutePlan,
706
+ stagesRemaining: number,
707
+ ): FanoutRoutePlan[] | null => {
708
+ const addedLength = currentPlan.length - shortest.length
709
+ const remainingAddition = targetAddedLength - addedLength
710
+ if (remainingAddition <= EPSILON) {
711
+ return acceptCandidate(currentPlan)
712
+ }
713
+ if (stagesRemaining <= 0) return null
714
+ const stageAddedLength = remainingAddition / stagesRemaining
715
+ const candidates = sampleCandidates(
716
+ createTunedPlanCandidates({
717
+ plan: currentPlan,
718
+ bus,
719
+ targetAddedLength: stageAddedLength,
720
+ clearance,
721
+ sharedBoundary: bus.sharedBoundary,
722
+ allowInsideDenseBounds: allowMatchingInsideDenseBounds,
723
+ }),
724
+ )
725
+ for (const candidate of candidates) {
726
+ searchedStateCount++
727
+ if (searchedStateCount > maximumSearchStates) return null
728
+ if (!acceptCandidate(candidate)) continue
729
+ const result = search(candidate, stagesRemaining - 1)
730
+ if (result) return result
731
+ }
732
+ return null
733
+ }
734
+ for (let stageCount = 2; stageCount <= 8; stageCount++) {
735
+ const result = search(shortest, stageCount)
736
+ if (result) return result
737
+ if (searchedStateCount > maximumSearchStates) break
738
+ }
739
+ return null
740
+ }
636
741
  for (const targetAddedLength of targetAddedLengths) {
637
742
  const candidates = createTunedPlanCandidates({
638
743
  plan: shortest,
@@ -640,31 +745,19 @@ export function matchBusPlanLengths(params: {
640
745
  targetAddedLength,
641
746
  clearance,
642
747
  sharedBoundary: bus.sharedBoundary,
748
+ allowInsideDenseBounds: allowMatchingInsideDenseBounds,
643
749
  })
644
750
  for (const candidate of candidates) {
645
- const nextPlans = matchedPlans.map((plan) =>
646
- plan === shortest ? candidate : plan,
647
- )
648
- const nextBusPlans = nextPlans.filter(
649
- (plan) => plan.busId === bus.busId,
650
- )
651
- const nextSkew = getBusSkew(nextBusPlans)
652
- if (nextSkew > skew + EPSILON) continue
653
- if (
654
- !fanoutPlansAreClear({
655
- plans: nextPlans,
656
- srj: inputSrj,
657
- sharedBoundary,
658
- clearance,
659
- allowBlindAndBuriedVias,
660
- allowSameNetMerges,
661
- })
662
- ) {
663
- continue
664
- }
665
- acceptedPlans = nextPlans
751
+ acceptedPlans = acceptCandidate(candidate)
752
+ if (!acceptedPlans) continue
666
753
  break
667
754
  }
755
+ if (
756
+ !acceptedPlans &&
757
+ Math.abs(targetAddedLength - minimumRequiredAddition) <= EPSILON
758
+ ) {
759
+ acceptedPlans = findMultiSpanCandidate(targetAddedLength)
760
+ }
668
761
  if (acceptedPlans) break
669
762
  }
670
763
  if (!acceptedPlans) return { plans: null, failedBus: bus }
@@ -33,6 +33,13 @@ export interface DogboneViaSiteGeometryRules {
33
33
  preferredBoundaryPerpendicularSideByBusId?: ReadonlyMap<string, -1 | 1>
34
34
  /** Prefer the local outward or inward half-pitch row for a boundary bus. */
35
35
  preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
36
+ /** Existing assignments that must be preserved while matching other pads. */
37
+ fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
38
+ /** Routed copper that every newly assigned through-via/dogbone must clear. */
39
+ blockingSegments?: readonly {
40
+ connectionIndex: number
41
+ segment: RoutedSegment
42
+ }[]
36
43
  /** True only when the two connections are allowed to merge copper. */
37
44
  canShareCopper?: (
38
45
  firstConnectionIndex: number,
@@ -69,6 +76,11 @@ interface ConnectionCandidates {
69
76
  candidates: ViaSiteCandidate[]
70
77
  }
71
78
 
79
+ export interface ComponentDogboneViaSiteCandidate {
80
+ connectionIndex: number
81
+ point: Point2D
82
+ }
83
+
72
84
  function assertGeometryRules(rules: DogboneViaSiteGeometryRules): number {
73
85
  for (const [name, value] of [
74
86
  ["viaDiameter", rules.viaDiameter],
@@ -340,9 +352,13 @@ function getConnectionCandidates(params: {
340
352
  uniquePoints.push(point)
341
353
  }
342
354
  }
355
+ const fixedViaPoint = rules.fixedViaPointsByConnectionIndex?.get(
356
+ preparedConnection.connectionIndex,
357
+ )
358
+ const candidatePoints = fixedViaPoint ? [fixedViaPoint] : uniquePoints
343
359
 
344
360
  const candidates: ViaSiteCandidate[] = []
345
- for (const point of uniquePoints) {
361
+ for (const point of candidatePoints) {
346
362
  if (
347
363
  connection.terminationType === "plane" &&
348
364
  !directSegmentIsStraightOr45(source, point)
@@ -375,6 +391,38 @@ function getConnectionCandidates(params: {
375
391
  ) {
376
392
  continue
377
393
  }
394
+ const candidateClearsRoutedCopper = (rules.blockingSegments ?? []).every(
395
+ (blocker) => {
396
+ if (blocker.connectionIndex === preparedConnection.connectionIndex) {
397
+ return true
398
+ }
399
+ if (
400
+ rules.canShareCopper?.(
401
+ preparedConnection.connectionIndex,
402
+ blocker.connectionIndex,
403
+ )
404
+ ) {
405
+ return true
406
+ }
407
+ const viaToTraceClearance =
408
+ rules.viaDiameter / 2 + blocker.segment.width / 2 + rules.clearance
409
+ if (
410
+ distancePointToSegment(
411
+ point,
412
+ blocker.segment.start,
413
+ blocker.segment.end,
414
+ ) <
415
+ viaToTraceClearance - EPSILON
416
+ ) {
417
+ return false
418
+ }
419
+ return (
420
+ blocker.segment.layer !== sourceSegment.layer ||
421
+ segmentsAreClear(sourceSegment, blocker.segment, rules.clearance)
422
+ )
423
+ },
424
+ )
425
+ if (!candidateClearsRoutedCopper) continue
378
426
  candidates.push({
379
427
  connectionIndex: preparedConnection.connectionIndex,
380
428
  point,
@@ -582,3 +630,26 @@ export function matchComponentDogboneViaSites(
582
630
  }
583
631
  return result
584
632
  }
633
+
634
+ /**
635
+ * Enumerates the same statically legal sites used by the component matcher.
636
+ * This is useful to preserve future dogbone capacity while another bus is
637
+ * being routed; callers must still run the full matcher afterward because
638
+ * these candidates are not mutually assigned.
639
+ */
640
+ export function getComponentDogboneViaSiteCandidates(
641
+ preparedBuses: readonly PreparedBus[],
642
+ rules: DogboneViaSiteGeometryRules,
643
+ ): ComponentDogboneViaSiteCandidate[] {
644
+ assertGeometryRules(rules)
645
+ return getComponentMatchingInputs(preparedBuses).flatMap((component) =>
646
+ component.connections.flatMap((connection) =>
647
+ getConnectionCandidates({ connection, component, rules }).map(
648
+ (candidate) => ({
649
+ connectionIndex: candidate.connectionIndex,
650
+ point: { ...candidate.point },
651
+ }),
652
+ ),
653
+ ),
654
+ )
655
+ }
package/lib/route-bus.ts CHANGED
@@ -131,14 +131,22 @@ function getStableConnectionIdentity(
131
131
  return connection.name
132
132
  }
133
133
 
134
- function getWindingTargetRank(params: {
134
+ function getWindingTargetOrders(params: {
135
135
  bus: PreparedBus
136
- connection: PreparedConnection
137
136
  boundaryDirection: FanoutDirection
138
137
  layerNames: readonly string[]
139
- }): { rank: number; connectionCount: number } {
140
- const { bus, connection, boundaryDirection, layerNames } = params
141
- const orderedConnections = bus.connections.toSorted((first, second) => {
138
+ targetLayer: string
139
+ }): {
140
+ orders: PreparedConnection[][]
141
+ legacyOrder: PreparedConnection[]
142
+ } {
143
+ const { bus, boundaryDirection, layerNames, targetLayer } = params
144
+ const getTargetLayer = (candidate: PreparedConnection): string =>
145
+ candidate.exitTargetPoint?.layer ?? getPointLayer(candidate.targetPoint)
146
+ const compareWithinLayer = (
147
+ first: PreparedConnection,
148
+ second: PreparedConnection,
149
+ ): number => {
142
150
  const axisDifference =
143
151
  getPerpendicularAxis(
144
152
  first.exitTargetPoint ?? first.targetPoint,
@@ -150,28 +158,114 @@ function getWindingTargetRank(params: {
150
158
  )
151
159
  if (axisDifference !== 0) return axisDifference
152
160
 
153
- // Equal target coordinates came from distinct target layers and do not
154
- // impose a physical order after this bus is collapsed onto one escape
155
- // layer. Keep that free choice independent of allowed-layer ordering so
156
- // the local fanout can choose a deterministic, via-minimal permutation.
157
- const identityDifference = first.connection.name.localeCompare(
158
- second.connection.name,
159
- )
160
- if (identityDifference !== 0) return identityDifference
161
-
162
161
  const firstStableId = getStableConnectionIdentity(first.connection)
163
162
  const secondStableId = getStableConnectionIdentity(second.connection)
164
163
  const stableIdentityDifference = firstStableId.localeCompare(secondStableId)
165
164
  if (stableIdentityDifference !== 0) return stableIdentityDifference
166
165
 
167
- const firstLayer = first.exitTargetPoint?.layer
168
- const secondLayer = second.exitTargetPoint?.layer
169
166
  return (
170
- layerNames.indexOf(firstLayer ?? "") -
171
- layerNames.indexOf(secondLayer ?? "") ||
167
+ first.connection.name.localeCompare(second.connection.name) ||
168
+ first.connectionIndex - second.connectionIndex
169
+ )
170
+ }
171
+ const legacyOrderedConnections = bus.connections.toSorted((first, second) => {
172
+ const axisDifference =
173
+ getPerpendicularAxis(
174
+ first.exitTargetPoint ?? first.targetPoint,
175
+ boundaryDirection,
176
+ ) -
177
+ getPerpendicularAxis(
178
+ second.exitTargetPoint ?? second.targetPoint,
179
+ boundaryDirection,
180
+ )
181
+ if (axisDifference !== 0) return axisDifference
182
+ return (
183
+ first.connection.name.localeCompare(second.connection.name) ||
184
+ getStableConnectionIdentity(first.connection).localeCompare(
185
+ getStableConnectionIdentity(second.connection),
186
+ ) ||
187
+ layerNames.indexOf(getTargetLayer(first)) -
188
+ layerNames.indexOf(getTargetLayer(second)) ||
172
189
  first.connectionIndex - second.connectionIndex
173
190
  )
174
191
  })
192
+ const connectionsByLayer = new Map<string, PreparedConnection[]>()
193
+ for (const candidate of bus.connections) {
194
+ const layer = getTargetLayer(candidate)
195
+ const layerConnections = connectionsByLayer.get(layer) ?? []
196
+ layerConnections.push(candidate)
197
+ connectionsByLayer.set(layer, layerConnections)
198
+ }
199
+ const orderedLayers = [...connectionsByLayer.keys()].toSorted(
200
+ (first, second) =>
201
+ Number(second === targetLayer) - Number(first === targetLayer) ||
202
+ layerNames.indexOf(first) - layerNames.indexOf(second) ||
203
+ first.localeCompare(second),
204
+ )
205
+ const layerOrderByName = new Map(
206
+ orderedLayers.map((layer, layerOrder) => [layer, layerOrder]),
207
+ )
208
+ const rankWithinLayerByConnectionIndex = new Map<number, number>()
209
+ for (const layer of orderedLayers) {
210
+ for (const [rank, candidate] of connectionsByLayer
211
+ .get(layer)!
212
+ .toSorted(compareWithinLayer)
213
+ .entries()) {
214
+ rankWithinLayerByConnectionIndex.set(candidate.connectionIndex, rank)
215
+ }
216
+ }
217
+ // Winding targets define an order within each copper layer. Their absolute
218
+ // offsets across different layers are not an ordering constraint: unrelated
219
+ // buses can move those layer bands without changing this bus's topology.
220
+ // Build a canonical interleave plus its adjacent linear extensions so a
221
+ // via-minimal search can choose a locally routable merge without violating
222
+ // any same-layer winding order.
223
+ const canonicalOrderedConnections = bus.connections.toSorted(
224
+ (first, second) =>
225
+ (rankWithinLayerByConnectionIndex.get(first.connectionIndex) ?? 0) -
226
+ (rankWithinLayerByConnectionIndex.get(second.connectionIndex) ?? 0) ||
227
+ (layerOrderByName.get(getTargetLayer(first)) ?? 0) -
228
+ (layerOrderByName.get(getTargetLayer(second)) ?? 0) ||
229
+ compareWithinLayer(first, second),
230
+ )
231
+ const candidateOrders: PreparedConnection[][] = [canonicalOrderedConnections]
232
+ for (let index = 0; index + 1 < canonicalOrderedConnections.length; index++) {
233
+ const first = canonicalOrderedConnections[index]!
234
+ const second = canonicalOrderedConnections[index + 1]!
235
+ if (getTargetLayer(first) === getTargetLayer(second)) continue
236
+ const adjacentExtension = [...canonicalOrderedConnections]
237
+ adjacentExtension[index] = second
238
+ adjacentExtension[index + 1] = first
239
+ candidateOrders.push(adjacentExtension)
240
+ }
241
+ // Retain the coordinate-total-order behavior as a compatibility fallback,
242
+ // but do not let sub-nanometer noise between unrelated layer bands choose
243
+ // the primary topology.
244
+ candidateOrders.push(legacyOrderedConnections)
245
+ const seenOrders = new Set<string>()
246
+ const orders = candidateOrders.filter((order) => {
247
+ const key = order.map((candidate) => candidate.connectionIndex).join(",")
248
+ if (seenOrders.has(key)) return false
249
+ seenOrders.add(key)
250
+ return true
251
+ })
252
+ return { orders, legacyOrder: legacyOrderedConnections }
253
+ }
254
+
255
+ function getWindingTargetRank(params: {
256
+ bus: PreparedBus
257
+ connection: PreparedConnection
258
+ boundaryDirection: FanoutDirection
259
+ layerNames: readonly string[]
260
+ targetLayer: string
261
+ windingOrderIndex?: number
262
+ }): { rank: number; connectionCount: number } {
263
+ const { connection, windingOrderIndex = 0 } = params
264
+ const { orders, legacyOrder } = getWindingTargetOrders(params)
265
+ const orderedConnections =
266
+ params.windingOrderIndex === undefined
267
+ ? legacyOrder
268
+ : (orders[windingOrderIndex] ?? orders[0] ?? legacyOrder)
175
269
  const rank = orderedConnections.findIndex(
176
270
  (candidate) => candidate.connectionIndex === connection.connectionIndex,
177
271
  )
@@ -201,6 +295,8 @@ function getCornerTargetTrack(params: {
201
295
  viaDiameter: number
202
296
  clearance: number
203
297
  layerNames: readonly string[]
298
+ targetLayer: string
299
+ windingOrderIndex?: number
204
300
  }): number {
205
301
  const {
206
302
  bus,
@@ -210,6 +306,8 @@ function getCornerTargetTrack(params: {
210
306
  viaDiameter,
211
307
  clearance,
212
308
  layerNames,
309
+ targetLayer,
310
+ windingOrderIndex,
213
311
  } = params
214
312
  const side = getCornerSide(bus)
215
313
  if (!side || !bus.exitEdge) {
@@ -235,6 +333,8 @@ function getCornerTargetTrack(params: {
235
333
  connection,
236
334
  boundaryDirection,
237
335
  layerNames,
336
+ targetLayer,
337
+ windingOrderIndex,
238
338
  })
239
339
  : undefined
240
340
  const bandConnectionCount = Math.max(
@@ -947,6 +1047,7 @@ function buildPlan(params: {
947
1047
  viaDiameter,
948
1048
  clearance,
949
1049
  layerNames,
1050
+ targetLayer,
950
1051
  })
951
1052
  : usesLayeredWindingChannel
952
1053
  ? getPerpendicularAxis(
@@ -2376,8 +2477,18 @@ export function routeBusAlternatives(
2376
2477
  ) => ViaHandedness
2377
2478
  getViaPoint?: (preparedConnection: PreparedConnection) => Point2D
2378
2479
  maximumRouteOrderAttempts?: number
2480
+ windingOrderIndex?: number
2481
+ preferTargetDirectedLaneBias?: boolean
2379
2482
  }
2380
2483
  const maximumThroughAllRouteOrderAttempts = 24
2484
+ const windingTargetOrderCount = cornerSide
2485
+ ? getWindingTargetOrders({
2486
+ bus,
2487
+ boundaryDirection,
2488
+ layerNames,
2489
+ targetLayer,
2490
+ }).orders.length
2491
+ : 1
2381
2492
  const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2382
2493
  viaHandednesses.map((viaHandedness) => ({
2383
2494
  label: `uniform-${viaHandedness}`,
@@ -2519,21 +2630,40 @@ export function routeBusAlternatives(
2519
2630
  useViaInPad: true,
2520
2631
  getViaHandedness: () => 0 as const,
2521
2632
  }
2522
- const fixedViaTerminalPattern: CoordinatedTerminalPattern | undefined =
2633
+ const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
2523
2634
  fixedViaPointsByConnectionIndex
2524
- ? {
2525
- label: "component-matched-vias",
2526
- useViaInPad: false,
2527
- getViaHandedness: () => 0,
2528
- getViaPoint: (connection) =>
2529
- fixedViaPointsByConnectionIndex.get(connection.connectionIndex)!,
2530
- // A fixed component-wide dogbone assignment is a bounded fast
2531
- // path. Keep enough order/bias attempts for the eight-lane DDR
2532
- // cases without allowing route-order rotations to grow with an
2533
- // arbitrarily wide bus.
2534
- maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2535
- }
2536
- : undefined
2635
+ ? [
2636
+ ...Array.from(
2637
+ { length: windingTargetOrderCount },
2638
+ (_, windingOrderIndex) => ({
2639
+ label: `component-matched-vias-winding-${windingOrderIndex}`,
2640
+ useViaInPad: false,
2641
+ getViaHandedness: () => 0 as const,
2642
+ getViaPoint: (connection: PreparedConnection) =>
2643
+ fixedViaPointsByConnectionIndex.get(
2644
+ connection.connectionIndex,
2645
+ )!,
2646
+ maximumRouteOrderAttempts: 1,
2647
+ windingOrderIndex,
2648
+ preferTargetDirectedLaneBias: true,
2649
+ }),
2650
+ ),
2651
+ {
2652
+ label: "component-matched-vias-fallback",
2653
+ useViaInPad: false,
2654
+ getViaHandedness: () => 0,
2655
+ getViaPoint: (connection) =>
2656
+ fixedViaPointsByConnectionIndex.get(
2657
+ connection.connectionIndex,
2658
+ )!,
2659
+ // Preserve the existing bounded search after every inexpensive
2660
+ // layer-interleave candidate has had one deterministic attempt.
2661
+ maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2662
+ windingOrderIndex: 0,
2663
+ preferTargetDirectedLaneBias: true,
2664
+ },
2665
+ ]
2666
+ : []
2537
2667
  const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
2538
2668
  (plan) => plan.termination.type === "plane",
2539
2669
  )
@@ -2545,8 +2675,8 @@ export function routeBusAlternatives(
2545
2675
  ? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
2546
2676
  : [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
2547
2677
  const terminalPatterns: CoordinatedTerminalPattern[] =
2548
- fixedViaTerminalPattern
2549
- ? [fixedViaTerminalPattern]
2678
+ fixedViaTerminalPatterns.length > 0
2679
+ ? fixedViaTerminalPatterns
2550
2680
  : canUseViaInPadTerminals
2551
2681
  ? planeTerminationsAlreadyOccupyTheFanout
2552
2682
  ? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
@@ -2566,6 +2696,8 @@ export function routeBusAlternatives(
2566
2696
  viaDiameter,
2567
2697
  clearance,
2568
2698
  layerNames,
2699
+ targetLayer,
2700
+ windingOrderIndex: terminalPattern.windingOrderIndex,
2569
2701
  })
2570
2702
  : getPerpendicularAxis(
2571
2703
  preparedConnection.exitTargetPoint ??
@@ -2597,12 +2729,12 @@ export function routeBusAlternatives(
2597
2729
  ),
2598
2730
  }
2599
2731
  })
2600
- const terminalSignature = terminals
2732
+ const terminalSignature = `${terminals
2601
2733
  .map(
2602
2734
  (terminal) =>
2603
- `${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}`,
2735
+ `${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
2604
2736
  )
2605
- .join("|")
2737
+ .join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
2606
2738
  if (seenTerminalSignatures.has(terminalSignature)) continue
2607
2739
  seenTerminalSignatures.add(terminalSignature)
2608
2740
  const viaMinimalAlternatives = routeViaMinimalWindingAlternatives(
@@ -2628,10 +2760,14 @@ export function routeBusAlternatives(
2628
2760
  traceWidth + clearance
2629
2761
  ? 2
2630
2762
  : 1,
2763
+ preferTargetDirectedLaneBias:
2764
+ terminalPattern.preferTargetDirectedLaneBias,
2631
2765
  },
2632
- terminalPattern.maximumRouteOrderAttempts === undefined
2633
- ? Math.min(2, maxAlternatives - alternatives.length)
2634
- : 2,
2766
+ fixedViaPointsByConnectionIndex && viaMinimalOnly
2767
+ ? Math.min(2, Math.max(1, maxAlternatives - alternatives.length))
2768
+ : terminalPattern.maximumRouteOrderAttempts === undefined
2769
+ ? Math.min(2, maxAlternatives - alternatives.length)
2770
+ : 2,
2635
2771
  )
2636
2772
  for (const viaMinimalPlans of viaMinimalAlternatives) {
2637
2773
  const combinedPlansAreClear = fanoutPlansAreClear({
@@ -60,6 +60,8 @@ export interface RouteViaMinimalWindingParams {
60
60
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
61
61
  /** Use a finer uniform grid for narrow channels between reserved vias. */
62
62
  gridStepDivisor?: 1 | 2
63
+ /** Bias bounded fixed-site searches toward the remote target band. */
64
+ preferTargetDirectedLaneBias?: boolean
63
65
  }
64
66
 
65
67
  interface GridNode {
@@ -600,6 +602,7 @@ export function routeViaMinimalWindingAlternatives(
600
602
  maximumRouteOrderAttempts,
601
603
  reservedVias = [],
602
604
  gridStepDivisor = 1,
605
+ preferTargetDirectedLaneBias = false,
603
606
  } = params
604
607
  if (
605
608
  maximumRouteOrderAttempts !== undefined &&
@@ -723,7 +726,6 @@ export function routeViaMinimalWindingAlternatives(
723
726
  }
724
727
  return low
725
728
  }
726
-
727
729
  const segmentIsClear = (params: {
728
730
  segment: RoutedSegment
729
731
  terminal: ViaMinimalWindingTerminal
@@ -842,7 +844,7 @@ export function routeViaMinimalWindingAlternatives(
842
844
  nodeIndex,
843
845
  points,
844
846
  radialDistance: connectorDistance,
845
- length: getSegments(points, traceWidth, targetLayer).reduce(
847
+ length: segments.reduce(
846
848
  (total, segment) => total + distance(segment.start, segment.end),
847
849
  0,
848
850
  ),
@@ -1073,15 +1075,31 @@ export function routeViaMinimalWindingAlternatives(
1073
1075
  const targetTracks = targetOrderedTerminals.map((terminal) =>
1074
1076
  getPerpendicularAxis(terminal.exitPoint, boundaryDirection),
1075
1077
  )
1078
+ const meanViaTrack =
1079
+ viaTracks.reduce((sum, track) => sum + track, 0) / viaTracks.length
1080
+ const meanTargetTrack =
1081
+ targetTracks.reduce((sum, track) => sum + track, 0) / targetTracks.length
1076
1082
  const viasAreBeforeTargets =
1077
1083
  Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON
1078
1084
  const viasAreAfterTargets =
1079
1085
  Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON
1080
- const laneBiases = viasAreBeforeTargets
1081
- ? ([1, 0, -1] as const)
1082
- : viasAreAfterTargets
1083
- ? ([-1, 0, 1] as const)
1084
- : ([0, 1, -1] as const)
1086
+ const laneBiases = preferTargetDirectedLaneBias
1087
+ ? viasAreBeforeTargets
1088
+ ? ([0, 1, -1] as const)
1089
+ : viasAreAfterTargets
1090
+ ? ([0, -1, 1] as const)
1091
+ : bus.direction === boundaryDirection &&
1092
+ meanTargetTrack > meanViaTrack + EPSILON
1093
+ ? ([1, 0, -1] as const)
1094
+ : bus.direction === boundaryDirection &&
1095
+ meanTargetTrack < meanViaTrack - EPSILON
1096
+ ? ([-1, 0, 1] as const)
1097
+ : ([0, 1, -1] as const)
1098
+ : viasAreBeforeTargets
1099
+ ? ([1, 0, -1] as const)
1100
+ : viasAreAfterTargets
1101
+ ? ([-1, 0, 1] as const)
1102
+ : ([0, 1, -1] as const)
1085
1103
  const initialRouteOrderFactories: Array<
1086
1104
  () => readonly ViaMinimalWindingTerminal[]
1087
1105
  > = []
@@ -1094,8 +1112,19 @@ export function routeViaMinimalWindingAlternatives(
1094
1112
  initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse())
1095
1113
  }
1096
1114
  initialRouteOrderFactories.push(
1097
- () => targetOrderedTerminals,
1098
- () => [...targetOrderedTerminals].reverse(),
1115
+ ...(preferTargetDirectedLaneBias &&
1116
+ bus.direction === boundaryDirection &&
1117
+ meanTargetTrack < meanViaTrack - EPSILON &&
1118
+ !viasAreBeforeTargets &&
1119
+ !viasAreAfterTargets
1120
+ ? [
1121
+ () => [...targetOrderedTerminals].reverse(),
1122
+ () => targetOrderedTerminals,
1123
+ ]
1124
+ : [
1125
+ () => targetOrderedTerminals,
1126
+ () => [...targetOrderedTerminals].reverse(),
1127
+ ]),
1099
1128
  () =>
1100
1129
  terminals.toSorted(
1101
1130
  (first, second) =>
@@ -1201,7 +1230,9 @@ export function routeViaMinimalWindingAlternatives(
1201
1230
  if (seenAlternativeKeys.has(alternativeKey)) continue
1202
1231
  seenAlternativeKeys.add(alternativeKey)
1203
1232
  alternatives.push(plans)
1204
- if (alternatives.length >= maximumAlternatives) return alternatives
1233
+ if (alternatives.length >= maximumAlternatives) {
1234
+ return alternatives
1235
+ }
1205
1236
  }
1206
1237
  }
1207
1238
  return alternatives
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",