@tscircuit/fanout-solver 0.0.38 → 0.0.39

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.
@@ -1,19 +1,27 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
3
  import type { GraphicsObject } from "graphics-debug"
4
- import { getCornerBandSide } from "./boundary-exit"
4
+ import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
5
+ import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
5
6
  import { buildOutputSimpleRouteJson } from "./build-output"
6
7
  import {
7
8
  type CompleteOriginalEndpointsResult,
8
9
  completeOriginalEndpoints,
9
10
  } from "./complete-original-endpoints"
10
- import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
11
+ import {
12
+ generateLayerAssignments,
13
+ getCopperLayerNames,
14
+ getViaSpanLayers,
15
+ } from "./layer-names"
11
16
  import { matchBusPlanLengths } from "./match-bus-lengths"
17
+ import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
18
+ import { connectionsShareElectricalNet } from "./net-identity"
12
19
  import {
13
20
  prepareFanoutBuses,
14
21
  resolveAvailableBoundaryRegions,
15
22
  } from "./prepare-buses"
16
23
  import {
24
+ fanoutPlansAreClear,
17
25
  type RouteBusStaticClearanceCache,
18
26
  routeBus,
19
27
  routeBusAlternatives,
@@ -30,6 +38,7 @@ import type {
30
38
  FanoutSolverOutput,
31
39
  FanoutValidationIssue,
32
40
  PreparedBus,
41
+ SimpleRouteJsonWithFanoutPlanes,
33
42
  } from "./types"
34
43
  import { validateFanoutSolution } from "./validate-fanout-solution"
35
44
  import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
@@ -40,6 +49,7 @@ interface ResolvedFanoutConfig {
40
49
  viaHoleDiameter: number
41
50
  clearance: number
42
51
  compactBusTracks: boolean
52
+ allowBlindAndBuriedVias: boolean
43
53
  allowSameNetMerges: boolean
44
54
  singleLayerPushAndShove: boolean
45
55
  singleLayerAdaptiveExits: boolean
@@ -59,6 +69,11 @@ interface GroupedBeamState {
59
69
  plans: FanoutRoutePlan[]
60
70
  }
61
71
 
72
+ interface MixedTerminationState {
73
+ plans: FanoutRoutePlan[]
74
+ failedBusIds: string[]
75
+ }
76
+
62
77
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
63
78
 
64
79
  function resolvePositiveNumber(label: string, value: number): number {
@@ -131,6 +146,7 @@ function resolveConfig(
131
146
  viaHoleDiameter,
132
147
  clearance,
133
148
  compactBusTracks: options.compactBusTracks ?? false,
149
+ allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
134
150
  allowSameNetMerges: options.allowSameNetMerges ?? false,
135
151
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
136
152
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
@@ -216,6 +232,17 @@ function getLayerLoadPenaltyWeight(config: ResolvedFanoutConfig): number {
216
232
  return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01
217
233
  }
218
234
 
235
+ function comparePlaneRoutingPriority(
236
+ first: PreparedBus,
237
+ second: PreparedBus,
238
+ allowBlindAndBuriedVias: boolean,
239
+ ): number {
240
+ const planeDifference =
241
+ Number(first.termination.type === "plane") -
242
+ Number(second.termination.type === "plane")
243
+ return allowBlindAndBuriedVias ? -planeDifference : planeDifference
244
+ }
245
+
219
246
  function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
220
247
  return plans.reduce(
221
248
  (count, plan) =>
@@ -439,6 +466,7 @@ function getCandidateEscapeLayersForBus(params: {
439
466
  viaHoleDiameter: config.viaHoleDiameter,
440
467
  clearance: config.clearance,
441
468
  compactBusTracks: config.compactBusTracks,
469
+ allowBlindAndBuriedVias: config.allowBlindAndBuriedVias,
442
470
  allowSameNetMerges: config.allowSameNetMerges,
443
471
  staticClearanceCache,
444
472
  }) !== null,
@@ -486,6 +514,10 @@ export class FanoutSolver extends BaseSolver {
486
514
  private bestAttempt: AssignmentAttempt | null = null
487
515
  private lengthMatchingFailure: FanoutValidationIssue | null = null
488
516
  private endpointCompletion: CompleteOriginalEndpointsResult | null = null
517
+ private denseDogboneViaPoints:
518
+ | Map<number, { x: number; y: number }>
519
+ | null
520
+ | undefined
489
521
 
490
522
  constructor(
491
523
  public readonly inputSrj: SimpleRouteJson,
@@ -627,6 +659,7 @@ export class FanoutSolver extends BaseSolver {
627
659
  viaDiameter: this.config.viaDiameter,
628
660
  viaHoleDiameter: this.config.viaHoleDiameter,
629
661
  clearance: this.config.clearance,
662
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
630
663
  effort: this.options.endpointCompletionEffort,
631
664
  routeDownstreamConnections: this.options.routeDownstreamConnections,
632
665
  })
@@ -658,6 +691,7 @@ export class FanoutSolver extends BaseSolver {
658
691
  preparedBuses: this.preparedBuses,
659
692
  sharedBoundary: this.getValidationBoundary(),
660
693
  clearance: this.config.clearance,
694
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
661
695
  })
662
696
  }
663
697
 
@@ -670,10 +704,571 @@ export class FanoutSolver extends BaseSolver {
670
704
  inputSrj: this.inputSrj,
671
705
  sharedBoundary: this.getValidationBoundary(),
672
706
  clearance: this.config.clearance,
707
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
673
708
  allowSameNetMerges: this.config.allowSameNetMerges,
674
709
  })
675
710
  }
676
711
 
712
+ /**
713
+ * Through-all source vias from a wide boundary bus can consume the only
714
+ * legal dogbone channel for nearby plane pads. Conversely, routing hundreds
715
+ * of singleton plane drops first can strand the boundary bus. Search a tiny
716
+ * number of whole-bus boundary alternatives, then fill the remaining plane
717
+ * dogbones. This is intentionally bounded independently of the number of
718
+ * plane drops so dense power fields cannot explode the general beam search.
719
+ */
720
+ private routeDenseThroughAllMixedTerminations(params: {
721
+ busLayerAssignments: Readonly<Record<string, string>>
722
+ busesInRoutingOrder: readonly PreparedBus[]
723
+ }): MixedTerminationState | null {
724
+ if (this.config.allowBlindAndBuriedVias) return null
725
+
726
+ const boundaryBuses = params.busesInRoutingOrder.filter(
727
+ (bus) => bus.termination.type === "boundary",
728
+ )
729
+ // Preserve the caller/input order for the dense singleton fill. The
730
+ // general routing sort is useful for heterogeneous buses, but ordering a
731
+ // regular BGA power field by obstacle depth creates artificial local
732
+ // dead-ends and needlessly triggers the widened boundary beam. The local
733
+ // rip-up/retry below handles the genuinely constrained drops.
734
+ const planeBuses = this.preparedBuses.filter(
735
+ (bus) => bus.termination.type === "plane",
736
+ )
737
+ if (
738
+ boundaryBuses.length === 0 ||
739
+ boundaryBuses.length > 4 ||
740
+ planeBuses.length < 8 ||
741
+ boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
742
+ planeBuses.some((bus) => bus.connections.length !== 1) ||
743
+ boundaryBuses.length + planeBuses.length !==
744
+ params.busesInRoutingOrder.length
745
+ ) {
746
+ return null
747
+ }
748
+
749
+ const connectionNameByIndex = new Map(
750
+ this.preparedBuses.flatMap((bus) =>
751
+ bus.connections.map(
752
+ (connection) =>
753
+ [connection.connectionIndex, connection.connection.name] as const,
754
+ ),
755
+ ),
756
+ )
757
+ const preferredBoundaryPerpendicularSideByBusId = new Map(
758
+ boundaryBuses.map((bus) => [bus.busId, 1 as const]),
759
+ )
760
+ const preferBoundaryOutwardByBusId = new Map(
761
+ boundaryBuses.map((bus) => [
762
+ bus.busId,
763
+ getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
764
+ ]),
765
+ )
766
+ if (this.denseDogboneViaPoints === undefined) {
767
+ this.denseDogboneViaPoints = matchComponentDogboneViaSites(
768
+ this.preparedBuses,
769
+ {
770
+ viaDiameter: this.config.viaDiameter,
771
+ viaHoleDiameter: this.config.viaHoleDiameter,
772
+ traceWidth: this.config.traceWidth,
773
+ clearance: this.config.clearance,
774
+ maximumSearchStates: 20_000,
775
+ preferredBoundaryPerpendicularSideByBusId,
776
+ 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
+ ),
792
+ )
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
+ },
832
+ })
833
+ }
834
+ reservedViasByComponentId.set(bus.componentId, componentReservedVias)
835
+ }
836
+
837
+ const matchedPlans: FanoutRoutePlan[] = []
838
+ let matchedRoutingSucceeded = true
839
+ for (const bus of boundaryBuses) {
840
+ const targetLayer = params.busLayerAssignments[bus.busId]
841
+ if (!targetLayer) {
842
+ matchedRoutingSucceeded = false
843
+ break
844
+ }
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,
871
+ },
872
+ 1,
873
+ )[0]
874
+ if (!busPlans) {
875
+ matchedRoutingSucceeded = false
876
+ break
877
+ }
878
+ matchedPlans.push(...busPlans)
879
+ }
880
+ if (matchedRoutingSucceeded) {
881
+ for (const bus of planeBuses) {
882
+ const targetLayer = params.busLayerAssignments[bus.busId]
883
+ const busPlans = targetLayer
884
+ ? routeBus({
885
+ srj: this.routingSrj,
886
+ bus,
887
+ targetLayer,
888
+ acceptedPlans: matchedPlans,
889
+ layerNames: this.config.layerNames,
890
+ traceWidth: this.config.traceWidth,
891
+ viaDiameter: this.config.viaDiameter,
892
+ viaHoleDiameter: this.config.viaHoleDiameter,
893
+ clearance: this.config.clearance,
894
+ compactBusTracks: this.config.compactBusTracks,
895
+ allowBlindAndBuriedVias: false,
896
+ allowSameNetMerges: this.config.allowSameNetMerges,
897
+ staticClearanceCache: this.routeStaticClearanceCache,
898
+ fixedViaPointsByConnectionIndex,
899
+ })
900
+ : null
901
+ if (!busPlans) {
902
+ matchedRoutingSucceeded = false
903
+ break
904
+ }
905
+ matchedPlans.push(...busPlans)
906
+ }
907
+ }
908
+ if (
909
+ matchedRoutingSucceeded &&
910
+ fanoutPlansAreClear({
911
+ plans: matchedPlans,
912
+ srj: this.routingSrj,
913
+ sharedBoundary: boundaryBuses[0]!.sharedBoundary,
914
+ clearance: this.config.clearance,
915
+ allowBlindAndBuriedVias: false,
916
+ allowSameNetMerges: this.config.allowSameNetMerges,
917
+ })
918
+ ) {
919
+ return { plans: matchedPlans, failedBusIds: [] }
920
+ }
921
+ }
922
+
923
+ const maximumStates = 8
924
+ const getBoundaryStates = (
925
+ alternativesPerBoundaryBus: number,
926
+ initialPlans: readonly FanoutRoutePlan[] = [],
927
+ ): MixedTerminationState[] | null => {
928
+ let states: MixedTerminationState[] = [
929
+ { plans: [...initialPlans], failedBusIds: [] },
930
+ ]
931
+ for (const bus of boundaryBuses) {
932
+ const targetLayer = params.busLayerAssignments[bus.busId]
933
+ if (!targetLayer) return null
934
+ const nextStates: MixedTerminationState[] = []
935
+ const alternativesByState = states.map((state) => ({
936
+ state,
937
+ alternatives: routeBusAlternatives(
938
+ {
939
+ srj: this.routingSrj,
940
+ bus,
941
+ targetLayer,
942
+ acceptedPlans: state.plans,
943
+ layerNames: this.config.layerNames,
944
+ traceWidth: this.config.traceWidth,
945
+ viaDiameter: this.config.viaDiameter,
946
+ viaHoleDiameter: this.config.viaHoleDiameter,
947
+ clearance: this.config.clearance,
948
+ compactBusTracks: this.config.compactBusTracks,
949
+ allowBlindAndBuriedVias: false,
950
+ allowSameNetMerges: this.config.allowSameNetMerges,
951
+ staticClearanceCache: this.routeStaticClearanceCache,
952
+ },
953
+ alternativesPerBoundaryBus,
954
+ ),
955
+ }))
956
+ for (
957
+ let alternativeIndex = 0;
958
+ alternativeIndex < alternativesPerBoundaryBus;
959
+ alternativeIndex++
960
+ ) {
961
+ for (const { state, alternatives } of alternativesByState) {
962
+ const alternative = alternatives[alternativeIndex]
963
+ if (!alternative) continue
964
+ nextStates.push({
965
+ plans: [...state.plans, ...alternative],
966
+ failedBusIds: [],
967
+ })
968
+ if (nextStates.length >= maximumStates) break
969
+ }
970
+ if (nextStates.length >= maximumStates) break
971
+ }
972
+ if (nextStates.length === 0) return null
973
+ states = nextStates
974
+ }
975
+ return states
976
+ }
977
+
978
+ const getJointReservedBoundaryState = (
979
+ initialPlans: readonly FanoutRoutePlan[],
980
+ ): MixedTerminationState | null => {
981
+ if (boundaryBuses.length !== 2) return null
982
+ const [firstBus, secondBus] = boundaryBuses
983
+ if (!firstBus || !secondBus) return null
984
+ const routeBoundaryBus = (
985
+ bus: PreparedBus,
986
+ acceptedPlans: FanoutRoutePlan[],
987
+ rejectedViaMinimalCandidates?: FanoutRoutePlan[][],
988
+ ): FanoutRoutePlan[] | null => {
989
+ const targetLayer = params.busLayerAssignments[bus.busId]
990
+ if (!targetLayer) return null
991
+ return (
992
+ routeBusAlternatives(
993
+ {
994
+ srj: this.routingSrj,
995
+ bus,
996
+ targetLayer,
997
+ acceptedPlans,
998
+ layerNames: this.config.layerNames,
999
+ traceWidth: this.config.traceWidth,
1000
+ viaDiameter: this.config.viaDiameter,
1001
+ viaHoleDiameter: this.config.viaHoleDiameter,
1002
+ clearance: this.config.clearance,
1003
+ compactBusTracks: this.config.compactBusTracks,
1004
+ allowBlindAndBuriedVias: false,
1005
+ allowSameNetMerges: this.config.allowSameNetMerges,
1006
+ staticClearanceCache: this.routeStaticClearanceCache,
1007
+ rejectedViaMinimalCandidates,
1008
+ stopAfterFirstRejectedViaMinimalCandidate:
1009
+ rejectedViaMinimalCandidates !== undefined,
1010
+ },
1011
+ 1,
1012
+ )[0] ?? null
1013
+ )
1014
+ }
1015
+
1016
+ const firstPlans = routeBoundaryBus(firstBus, [...initialPlans])
1017
+ if (!firstPlans) return null
1018
+ const rejectedSecondCandidates: FanoutRoutePlan[][] = []
1019
+ const secondPlans = routeBoundaryBus(
1020
+ secondBus,
1021
+ [...initialPlans, ...firstPlans],
1022
+ rejectedSecondCandidates,
1023
+ )
1024
+ if (secondPlans) {
1025
+ return {
1026
+ plans: [...initialPlans, ...firstPlans, ...secondPlans],
1027
+ failedBusIds: [],
1028
+ }
1029
+ }
1030
+
1031
+ const rejectedSecondPlans = rejectedSecondCandidates[0]
1032
+ if (!rejectedSecondPlans) return null
1033
+ // Keep the candidate's exact copper/vias as immutable blockers while
1034
+ // rerouting the earlier bus, but exclude it from corner-slot allocation.
1035
+ const reservedSecondPlans = rejectedSecondPlans.map((plan) => ({
1036
+ ...plan,
1037
+ termination: {
1038
+ type: "plane" as const,
1039
+ layer: plan.targetLayer,
1040
+ },
1041
+ }))
1042
+ const reroutedFirstPlans = routeBoundaryBus(firstBus, [
1043
+ ...initialPlans,
1044
+ ...reservedSecondPlans,
1045
+ ])
1046
+ if (!reroutedFirstPlans) return null
1047
+ const combinedPlans = [
1048
+ ...initialPlans,
1049
+ ...reroutedFirstPlans,
1050
+ ...rejectedSecondPlans,
1051
+ ]
1052
+ if (
1053
+ !fanoutPlansAreClear({
1054
+ plans: combinedPlans,
1055
+ srj: this.routingSrj,
1056
+ sharedBoundary: firstBus.sharedBoundary,
1057
+ clearance: this.config.clearance,
1058
+ allowBlindAndBuriedVias: false,
1059
+ allowSameNetMerges: this.config.allowSameNetMerges,
1060
+ })
1061
+ ) {
1062
+ return null
1063
+ }
1064
+ return { plans: combinedPlans, failedBusIds: [] }
1065
+ }
1066
+
1067
+ const planeBusById = new Map(planeBuses.map((bus) => [bus.busId, bus]))
1068
+ const routePlaneBus = (
1069
+ bus: PreparedBus,
1070
+ acceptedPlans: FanoutRoutePlan[],
1071
+ blockingBusCounts?: Map<string, number>,
1072
+ ): FanoutRoutePlan[] | null => {
1073
+ const targetLayer = params.busLayerAssignments[bus.busId]
1074
+ if (!targetLayer) return null
1075
+ return routeBus({
1076
+ srj: this.routingSrj,
1077
+ bus,
1078
+ targetLayer,
1079
+ acceptedPlans,
1080
+ layerNames: this.config.layerNames,
1081
+ traceWidth: this.config.traceWidth,
1082
+ viaDiameter: this.config.viaDiameter,
1083
+ viaHoleDiameter: this.config.viaHoleDiameter,
1084
+ clearance: this.config.clearance,
1085
+ compactBusTracks: this.config.compactBusTracks,
1086
+ allowBlindAndBuriedVias: false,
1087
+ allowSameNetMerges: this.config.allowSameNetMerges,
1088
+ staticClearanceCache: this.routeStaticClearanceCache,
1089
+ blockingBusCounts,
1090
+ })
1091
+ }
1092
+
1093
+ const routePlaneOrder = (
1094
+ boundaryPlans: readonly FanoutRoutePlan[],
1095
+ planeOrder: readonly PreparedBus[],
1096
+ ): MixedTerminationState => {
1097
+ const state: MixedTerminationState = {
1098
+ plans: [...boundaryPlans],
1099
+ failedBusIds: [],
1100
+ }
1101
+ for (const bus of planeOrder) {
1102
+ const blockingBusCounts = new Map<string, number>()
1103
+ let busPlans = routePlaneBus(bus, state.plans, blockingBusCounts)
1104
+
1105
+ // A plane dogbone can lose its only local channel to one or two
1106
+ // earlier singleton drops. Rip up only the strongest local blockers,
1107
+ // route the constrained drop first, then put the displaced drops back.
1108
+ // Boundary buses are immutable here and the search is capped at 36
1109
+ // blocker pairs, so dense fields remain predictable.
1110
+ if (!busPlans) {
1111
+ const blockerIds = [...blockingBusCounts.entries()]
1112
+ .filter(([busId]) =>
1113
+ state.plans.some((plan) => plan.busId === busId),
1114
+ )
1115
+ .filter(([busId]) => planeBusById.has(busId))
1116
+ .toSorted(
1117
+ ([, firstCount], [, secondCount]) => secondCount - firstCount,
1118
+ )
1119
+ .slice(0, 8)
1120
+ .map(([busId]) => busId)
1121
+ const ripupSets = [
1122
+ ...blockerIds.map((busId) => [busId]),
1123
+ ...blockerIds.flatMap((first, firstIndex) =>
1124
+ blockerIds.slice(firstIndex + 1).map((second) => [first, second]),
1125
+ ),
1126
+ ]
1127
+ for (const ripupIds of ripupSets) {
1128
+ const ripupIdSet = new Set(ripupIds)
1129
+ const candidatePlans = state.plans.filter(
1130
+ (plan) => !ripupIdSet.has(plan.busId),
1131
+ )
1132
+ const constrainedPlans = routePlaneBus(bus, candidatePlans)
1133
+ if (!constrainedPlans) continue
1134
+ candidatePlans.push(...constrainedPlans)
1135
+ let repairSucceeded = true
1136
+ for (const blockerId of ripupIds) {
1137
+ const blockerBus = planeBusById.get(blockerId)
1138
+ const replacementPlans = blockerBus
1139
+ ? routePlaneBus(blockerBus, candidatePlans)
1140
+ : null
1141
+ if (!replacementPlans) {
1142
+ repairSucceeded = false
1143
+ break
1144
+ }
1145
+ candidatePlans.push(...replacementPlans)
1146
+ }
1147
+ if (!repairSucceeded) continue
1148
+ state.plans = candidatePlans
1149
+ busPlans = []
1150
+ break
1151
+ }
1152
+ }
1153
+
1154
+ if (busPlans) state.plans.push(...busPlans)
1155
+ else state.failedBusIds.push(bus.busId)
1156
+ }
1157
+ return state
1158
+ }
1159
+
1160
+ let bestState: MixedTerminationState | null = null
1161
+ let mostRecentBoundaryBestState: MixedTerminationState | null = null
1162
+ const evaluateBoundaryStates = (
1163
+ states: readonly MixedTerminationState[],
1164
+ initialPlaneOrder: readonly PreparedBus[],
1165
+ ): MixedTerminationState | null => {
1166
+ let localBestState: MixedTerminationState | null = null
1167
+ for (const boundaryState of states) {
1168
+ let planeOrder = [...initialPlaneOrder]
1169
+ const seenPlaneOrders = new Set<string>()
1170
+ for (let retryIndex = 0; retryIndex < 3; retryIndex++) {
1171
+ const orderKey = planeOrder.map((bus) => bus.busId).join("\u0000")
1172
+ if (seenPlaneOrders.has(orderKey)) break
1173
+ seenPlaneOrders.add(orderKey)
1174
+ const state = routePlaneOrder(boundaryState.plans, planeOrder)
1175
+ if (
1176
+ !bestState ||
1177
+ state.plans.length > bestState.plans.length ||
1178
+ (state.plans.length === bestState.plans.length &&
1179
+ state.failedBusIds.length < bestState.failedBusIds.length)
1180
+ ) {
1181
+ bestState = state
1182
+ }
1183
+ if (
1184
+ !localBestState ||
1185
+ state.plans.length > localBestState.plans.length ||
1186
+ (state.plans.length === localBestState.plans.length &&
1187
+ state.failedBusIds.length < localBestState.failedBusIds.length)
1188
+ ) {
1189
+ localBestState = state
1190
+ }
1191
+ if (state.failedBusIds.length === 0) return state
1192
+ const failedBusIds = new Set(state.failedBusIds)
1193
+ planeOrder = [
1194
+ ...state.failedBusIds.flatMap((busId) => {
1195
+ const bus = planeBusById.get(busId)
1196
+ return bus ? [bus] : []
1197
+ }),
1198
+ ...planeOrder.filter((bus) => !failedBusIds.has(bus.busId)),
1199
+ ]
1200
+ }
1201
+ }
1202
+ mostRecentBoundaryBestState = localBestState
1203
+ return null
1204
+ }
1205
+
1206
+ // Most dense packages route with the first deterministic boundary choice.
1207
+ // Try that cheap path before widening the beam; eagerly generating four
1208
+ // complete A* variants per state can otherwise dominate runtime and memory.
1209
+ for (const alternativesPerBoundaryBus of [1, 4]) {
1210
+ const states = getBoundaryStates(alternativesPerBoundaryBus)
1211
+ if (!states) continue
1212
+ const completeState = evaluateBoundaryStates(states, planeBuses)
1213
+ if (completeState) return completeState
1214
+
1215
+ // If a completed signal escape encloses one especially constrained
1216
+ // power pad, seed just that failed singleton first and recompute the
1217
+ // small boundary-bus set around its physical through barrel. This keeps
1218
+ // the search local without routing hundreds of plane drops before the
1219
+ // signal buses.
1220
+ const seedFailureIds = (
1221
+ bestState as MixedTerminationState | null
1222
+ )?.failedBusIds.slice(0, 3)
1223
+ if (alternativesPerBoundaryBus === 1 && seedFailureIds) {
1224
+ for (const failedBusId of seedFailureIds) {
1225
+ const seededPlanePlans: FanoutRoutePlan[] = []
1226
+ const seededPlaneBusIds = new Set<string>()
1227
+ let nextFailedBusId: string | undefined = failedBusId
1228
+ for (
1229
+ let seedDepth = 0;
1230
+ seedDepth < 3 && nextFailedBusId;
1231
+ seedDepth++
1232
+ ) {
1233
+ const failedPlaneBus = planeBusById.get(nextFailedBusId)
1234
+ if (!failedPlaneBus) break
1235
+ const nextSeedPlans = routePlaneBus(
1236
+ failedPlaneBus,
1237
+ seededPlanePlans,
1238
+ )
1239
+ if (!nextSeedPlans) break
1240
+ seededPlanePlans.push(...nextSeedPlans)
1241
+ seededPlaneBusIds.add(nextFailedBusId)
1242
+ const jointReservedBoundaryState =
1243
+ getJointReservedBoundaryState(seededPlanePlans)
1244
+ if (!jointReservedBoundaryState) break
1245
+ const jointCompleteState = evaluateBoundaryStates(
1246
+ [jointReservedBoundaryState],
1247
+ planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
1248
+ )
1249
+ if (jointCompleteState) return jointCompleteState
1250
+ const recentFailedBusIds = (
1251
+ mostRecentBoundaryBestState as MixedTerminationState | null
1252
+ )?.failedBusIds
1253
+ nextFailedBusId = recentFailedBusIds?.find(
1254
+ (busId) => !seededPlaneBusIds.has(busId),
1255
+ )
1256
+ }
1257
+ if (seededPlanePlans.length === 0) continue
1258
+ const seededBoundaryStates = getBoundaryStates(1, seededPlanePlans)
1259
+ if (!seededBoundaryStates) continue
1260
+ const seededCompleteState = evaluateBoundaryStates(
1261
+ seededBoundaryStates,
1262
+ planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
1263
+ )
1264
+ if (seededCompleteState) return seededCompleteState
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ return bestState
1270
+ }
1271
+
677
1272
  private evaluateAssignmentWithStrategy(
678
1273
  assignmentIndex: number,
679
1274
  busLayerAssignments: Readonly<Record<string, string>>,
@@ -723,8 +1318,11 @@ export class FanoutSolver extends BaseSolver {
723
1318
  busLayerAssignments[b.busId] ?? "",
724
1319
  )
725
1320
  return (
726
- Number(b.termination.type === "plane") -
727
- Number(a.termination.type === "plane") ||
1321
+ comparePlaneRoutingPriority(
1322
+ a,
1323
+ b,
1324
+ this.config.allowBlindAndBuriedVias,
1325
+ ) ||
728
1326
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
729
1327
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
730
1328
  ? bLayerIndex - aLayerIndex
@@ -744,8 +1342,23 @@ export class FanoutSolver extends BaseSolver {
744
1342
  )
745
1343
  })
746
1344
 
1345
+ const mixedTerminationState =
1346
+ !useSingleLayerPushAndShove && routingStrategy === "default"
1347
+ ? this.routeDenseThroughAllMixedTerminations({
1348
+ busLayerAssignments,
1349
+ busesInRoutingOrder,
1350
+ })
1351
+ : null
1352
+
1353
+ if (mixedTerminationState) {
1354
+ plans = mixedTerminationState.plans
1355
+ failedBusIds = mixedTerminationState.failedBusIds
1356
+ }
1357
+
747
1358
  let routingPrefixKey = `${routingStrategy}|`
748
- for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
1359
+ for (const bus of useSingleLayerPushAndShove || mixedTerminationState
1360
+ ? []
1361
+ : busesInRoutingOrder) {
749
1362
  const targetLayer = busLayerAssignments[bus.busId]
750
1363
  if (!targetLayer) {
751
1364
  throw new Error(
@@ -777,6 +1390,7 @@ export class FanoutSolver extends BaseSolver {
777
1390
  viaHoleDiameter: this.config.viaHoleDiameter,
778
1391
  clearance: this.config.clearance,
779
1392
  compactBusTracks: this.config.compactBusTracks,
1393
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
780
1394
  allowSameNetMerges: this.config.allowSameNetMerges,
781
1395
  staticClearanceCache: this.routeStaticClearanceCache,
782
1396
  blockingBusCounts: currentBusBlockingCounts,
@@ -870,7 +1484,6 @@ export class FanoutSolver extends BaseSolver {
870
1484
  score,
871
1485
  ...(validationIssues ? { validationIssues } : {}),
872
1486
  }
873
-
874
1487
  return {
875
1488
  summary,
876
1489
  plans,
@@ -972,8 +1585,11 @@ export class FanoutSolver extends BaseSolver {
972
1585
  : (this.escapeLayersByBusId[b.busId]?.length ??
973
1586
  this.config.escapeLayers.length)
974
1587
  return (
975
- Number(b.termination.type === "plane") -
976
- Number(a.termination.type === "plane") ||
1588
+ comparePlaneRoutingPriority(
1589
+ a,
1590
+ b,
1591
+ this.config.allowBlindAndBuriedVias,
1592
+ ) ||
977
1593
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
978
1594
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
979
1595
  ? getMaximumViaSpan(b) - getMaximumViaSpan(a)
@@ -1084,6 +1700,7 @@ export class FanoutSolver extends BaseSolver {
1084
1700
  viaHoleDiameter: this.config.viaHoleDiameter,
1085
1701
  clearance: this.config.clearance,
1086
1702
  compactBusTracks: this.config.compactBusTracks,
1703
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
1087
1704
  allowSameNetMerges: this.config.allowSameNetMerges,
1088
1705
  staticClearanceCache: this.routeStaticClearanceCache,
1089
1706
  },
@@ -1534,14 +2151,17 @@ export class FanoutSolver extends BaseSolver {
1534
2151
  `FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
1535
2152
  )
1536
2153
  }
1537
- const finalSrj =
1538
- this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj
2154
+ const finalSrj = addViaLayerMetadataToSrj({
2155
+ srj:
2156
+ this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
2157
+ layerNames: this.config.layerNames,
2158
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
2159
+ })
1539
2160
  const finalTraceById = new Map(
1540
2161
  (finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]),
1541
2162
  )
1542
2163
  return {
1543
- simpleRouteJson:
1544
- this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
2164
+ simpleRouteJson: finalSrj,
1545
2165
  fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
1546
2166
  finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
1547
2167
  ...(plan.planeEndpointTrace
@@ -1551,7 +2171,9 @@ export class FanoutSolver extends BaseSolver {
1551
2171
  ]
1552
2172
  : []),
1553
2173
  ]),
1554
- completionTraces: this.endpointCompletion?.traces ?? [],
2174
+ completionTraces: (this.endpointCompletion?.traces ?? []).map(
2175
+ (trace) => finalTraceById.get(trace.pcb_trace_id) ?? trace,
2176
+ ),
1555
2177
  ...(this.endpointCompletion
1556
2178
  ? { endpointCompletion: this.endpointCompletion.report }
1557
2179
  : {}),
@@ -1576,7 +2198,7 @@ export class FanoutSolver extends BaseSolver {
1576
2198
  }
1577
2199
  }
1578
2200
 
1579
- getOutputSimpleRouteJson(): SimpleRouteJson {
2201
+ getOutputSimpleRouteJson(): SimpleRouteJsonWithFanoutPlanes {
1580
2202
  return this.getOutput().simpleRouteJson
1581
2203
  }
1582
2204