@tscircuit/fanout-solver 0.0.37 → 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,18 +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"
16
+ import { matchBusPlanLengths } from "./match-bus-lengths"
17
+ import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
18
+ import { connectionsShareElectricalNet } from "./net-identity"
11
19
  import {
12
20
  prepareFanoutBuses,
13
21
  resolveAvailableBoundaryRegions,
14
22
  } from "./prepare-buses"
15
23
  import {
24
+ fanoutPlansAreClear,
16
25
  type RouteBusStaticClearanceCache,
17
26
  routeBus,
18
27
  routeBusAlternatives,
@@ -27,7 +36,9 @@ import type {
27
36
  FanoutRoutePlan,
28
37
  FanoutSolverOptions,
29
38
  FanoutSolverOutput,
39
+ FanoutValidationIssue,
30
40
  PreparedBus,
41
+ SimpleRouteJsonWithFanoutPlanes,
31
42
  } from "./types"
32
43
  import { validateFanoutSolution } from "./validate-fanout-solution"
33
44
  import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
@@ -38,6 +49,7 @@ interface ResolvedFanoutConfig {
38
49
  viaHoleDiameter: number
39
50
  clearance: number
40
51
  compactBusTracks: boolean
52
+ allowBlindAndBuriedVias: boolean
41
53
  allowSameNetMerges: boolean
42
54
  singleLayerPushAndShove: boolean
43
55
  singleLayerAdaptiveExits: boolean
@@ -57,6 +69,11 @@ interface GroupedBeamState {
57
69
  plans: FanoutRoutePlan[]
58
70
  }
59
71
 
72
+ interface MixedTerminationState {
73
+ plans: FanoutRoutePlan[]
74
+ failedBusIds: string[]
75
+ }
76
+
60
77
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
61
78
 
62
79
  function resolvePositiveNumber(label: string, value: number): number {
@@ -129,6 +146,7 @@ function resolveConfig(
129
146
  viaHoleDiameter,
130
147
  clearance,
131
148
  compactBusTracks: options.compactBusTracks ?? false,
149
+ allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
132
150
  allowSameNetMerges: options.allowSameNetMerges ?? false,
133
151
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
134
152
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
@@ -214,6 +232,17 @@ function getLayerLoadPenaltyWeight(config: ResolvedFanoutConfig): number {
214
232
  return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01
215
233
  }
216
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
+
217
246
  function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
218
247
  return plans.reduce(
219
248
  (count, plan) =>
@@ -437,6 +466,7 @@ function getCandidateEscapeLayersForBus(params: {
437
466
  viaHoleDiameter: config.viaHoleDiameter,
438
467
  clearance: config.clearance,
439
468
  compactBusTracks: config.compactBusTracks,
469
+ allowBlindAndBuriedVias: config.allowBlindAndBuriedVias,
440
470
  allowSameNetMerges: config.allowSameNetMerges,
441
471
  staticClearanceCache,
442
472
  }) !== null,
@@ -482,7 +512,12 @@ export class FanoutSolver extends BaseSolver {
482
512
  private nextAssignmentIndex = 0
483
513
  private nextGeneratedAssignmentIndex = 0
484
514
  private bestAttempt: AssignmentAttempt | null = null
515
+ private lengthMatchingFailure: FanoutValidationIssue | null = null
485
516
  private endpointCompletion: CompleteOriginalEndpointsResult | null = null
517
+ private denseDogboneViaPoints:
518
+ | Map<number, { x: number; y: number }>
519
+ | null
520
+ | undefined
486
521
 
487
522
  constructor(
488
523
  public readonly inputSrj: SimpleRouteJson,
@@ -624,6 +659,7 @@ export class FanoutSolver extends BaseSolver {
624
659
  viaDiameter: this.config.viaDiameter,
625
660
  viaHoleDiameter: this.config.viaHoleDiameter,
626
661
  clearance: this.config.clearance,
662
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
627
663
  effort: this.options.endpointCompletionEffort,
628
664
  routeDownstreamConnections: this.options.routeDownstreamConnections,
629
665
  })
@@ -655,9 +691,584 @@ export class FanoutSolver extends BaseSolver {
655
691
  preparedBuses: this.preparedBuses,
656
692
  sharedBoundary: this.getValidationBoundary(),
657
693
  clearance: this.config.clearance,
694
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
658
695
  })
659
696
  }
660
697
 
698
+ private matchCompletePlanLengths(
699
+ plans: readonly FanoutRoutePlan[],
700
+ ): ReturnType<typeof matchBusPlanLengths> {
701
+ return matchBusPlanLengths({
702
+ plans,
703
+ preparedBuses: this.preparedBuses,
704
+ inputSrj: this.inputSrj,
705
+ sharedBoundary: this.getValidationBoundary(),
706
+ clearance: this.config.clearance,
707
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
708
+ allowSameNetMerges: this.config.allowSameNetMerges,
709
+ })
710
+ }
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
+
661
1272
  private evaluateAssignmentWithStrategy(
662
1273
  assignmentIndex: number,
663
1274
  busLayerAssignments: Readonly<Record<string, string>>,
@@ -707,8 +1318,11 @@ export class FanoutSolver extends BaseSolver {
707
1318
  busLayerAssignments[b.busId] ?? "",
708
1319
  )
709
1320
  return (
710
- Number(b.termination.type === "plane") -
711
- Number(a.termination.type === "plane") ||
1321
+ comparePlaneRoutingPriority(
1322
+ a,
1323
+ b,
1324
+ this.config.allowBlindAndBuriedVias,
1325
+ ) ||
712
1326
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
713
1327
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
714
1328
  ? bLayerIndex - aLayerIndex
@@ -728,8 +1342,23 @@ export class FanoutSolver extends BaseSolver {
728
1342
  )
729
1343
  })
730
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
+
731
1358
  let routingPrefixKey = `${routingStrategy}|`
732
- for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
1359
+ for (const bus of useSingleLayerPushAndShove || mixedTerminationState
1360
+ ? []
1361
+ : busesInRoutingOrder) {
733
1362
  const targetLayer = busLayerAssignments[bus.busId]
734
1363
  if (!targetLayer) {
735
1364
  throw new Error(
@@ -761,6 +1390,7 @@ export class FanoutSolver extends BaseSolver {
761
1390
  viaHoleDiameter: this.config.viaHoleDiameter,
762
1391
  clearance: this.config.clearance,
763
1392
  compactBusTracks: this.config.compactBusTracks,
1393
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
764
1394
  allowSameNetMerges: this.config.allowSameNetMerges,
765
1395
  staticClearanceCache: this.routeStaticClearanceCache,
766
1396
  blockingBusCounts: currentBusBlockingCounts,
@@ -784,6 +1414,29 @@ export class FanoutSolver extends BaseSolver {
784
1414
  }
785
1415
 
786
1416
  let validationIssues: FanoutAttemptSummary["validationIssues"]
1417
+ if (plans.length === this.inputSrj.connections.length) {
1418
+ const lengthMatching = this.matchCompletePlanLengths(plans)
1419
+ if (lengthMatching.plans) {
1420
+ plans = lengthMatching.plans
1421
+ } else {
1422
+ const constrainedBus = lengthMatching.failedBus
1423
+ const lengthMatchingIssue: FanoutValidationIssue = {
1424
+ code: "bus-length-skew",
1425
+ message: `Bus ${constrainedBus.busId} could not satisfy its ${constrainedBus.maxLengthSkew!.toFixed(6)}mm routed-length skew within the fanout boundary`,
1426
+ busId: constrainedBus.busId,
1427
+ }
1428
+ validationIssues = [lengthMatchingIssue]
1429
+ this.lengthMatchingFailure ??= lengthMatchingIssue
1430
+ plans = []
1431
+ failedBusIds = [
1432
+ constrainedBus.busId,
1433
+ ...this.preparedBuses
1434
+ .map((bus) => bus.busId)
1435
+ .filter((busId) => busId !== constrainedBus.busId),
1436
+ ]
1437
+ blockingBusCounts.clear()
1438
+ }
1439
+ }
787
1440
  let outputSrj = buildOutputSimpleRouteJson({
788
1441
  inputSrj: this.inputSrj,
789
1442
  plans,
@@ -831,7 +1484,6 @@ export class FanoutSolver extends BaseSolver {
831
1484
  score,
832
1485
  ...(validationIssues ? { validationIssues } : {}),
833
1486
  }
834
-
835
1487
  return {
836
1488
  summary,
837
1489
  plans,
@@ -933,8 +1585,11 @@ export class FanoutSolver extends BaseSolver {
933
1585
  : (this.escapeLayersByBusId[b.busId]?.length ??
934
1586
  this.config.escapeLayers.length)
935
1587
  return (
936
- Number(b.termination.type === "plane") -
937
- Number(a.termination.type === "plane") ||
1588
+ comparePlaneRoutingPriority(
1589
+ a,
1590
+ b,
1591
+ this.config.allowBlindAndBuriedVias,
1592
+ ) ||
938
1593
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
939
1594
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
940
1595
  ? getMaximumViaSpan(b) - getMaximumViaSpan(a)
@@ -1045,6 +1700,7 @@ export class FanoutSolver extends BaseSolver {
1045
1700
  viaHoleDiameter: this.config.viaHoleDiameter,
1046
1701
  clearance: this.config.clearance,
1047
1702
  compactBusTracks: this.config.compactBusTracks,
1703
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
1048
1704
  allowSameNetMerges: this.config.allowSameNetMerges,
1049
1705
  staticClearanceCache: this.routeStaticClearanceCache,
1050
1706
  },
@@ -1088,32 +1744,54 @@ export class FanoutSolver extends BaseSolver {
1088
1744
 
1089
1745
  let bestState: GroupedBeamState | undefined
1090
1746
  let outputSrj: SimpleRouteJson | undefined
1747
+ let bestMatchedScore = Number.POSITIVE_INFINITY
1748
+ let bestAdditionalViaCount = Number.POSITIVE_INFINITY
1749
+ const getCompleteStateScore = (state: GroupedBeamState): number =>
1750
+ state.plans.reduce((total, plan) => total + plan.length, 0) +
1751
+ getPlanViaCount(state.plans) * 0.1 +
1752
+ assignmentLoadPenalty(
1753
+ state.assignment,
1754
+ this.preparedBuses,
1755
+ this.config.balanceLayerLoadByConnectionCount,
1756
+ ) *
1757
+ getLayerLoadPenaltyWeight(this.config)
1758
+ const hasLengthConstraints = this.preparedBuses.some(
1759
+ (bus) => bus.maxLengthSkew !== undefined,
1760
+ )
1091
1761
  for (const state of states) {
1092
1762
  if (state.plans.length !== this.inputSrj.connections.length) continue
1763
+ const lengthMatching = this.matchCompletePlanLengths(state.plans)
1764
+ if (!lengthMatching.plans) continue
1765
+ const lengthMatchedPlans = lengthMatching.plans
1093
1766
  const candidateOutput = buildOutputSimpleRouteJson({
1094
1767
  inputSrj: this.inputSrj,
1095
- plans: state.plans,
1768
+ plans: lengthMatchedPlans,
1096
1769
  layerNames: this.config.layerNames,
1097
1770
  })
1098
- if (!this.validateCompletePlans(state.plans, candidateOutput).valid) {
1771
+ if (
1772
+ !this.validateCompletePlans(lengthMatchedPlans, candidateOutput).valid
1773
+ ) {
1099
1774
  continue
1100
1775
  }
1101
- bestState = state
1102
- outputSrj = candidateOutput
1103
- break
1776
+ const candidateState = { ...state, plans: lengthMatchedPlans }
1777
+ const candidateAdditionalViaCount =
1778
+ this.getCoordinatedAdditionalViaCount(lengthMatchedPlans)
1779
+ const candidateScore = getCompleteStateScore(candidateState)
1780
+ if (
1781
+ !bestState ||
1782
+ candidateAdditionalViaCount < bestAdditionalViaCount ||
1783
+ (candidateAdditionalViaCount === bestAdditionalViaCount &&
1784
+ candidateScore < bestMatchedScore)
1785
+ ) {
1786
+ bestState = candidateState
1787
+ outputSrj = candidateOutput
1788
+ bestMatchedScore = candidateScore
1789
+ bestAdditionalViaCount = candidateAdditionalViaCount
1790
+ }
1791
+ if (!hasLengthConstraints) break
1104
1792
  }
1105
1793
  if (!bestState || !outputSrj) return null
1106
- const score =
1107
- bestState.plans.length === this.inputSrj.connections.length
1108
- ? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
1109
- getPlanViaCount(bestState.plans) * 0.1 +
1110
- assignmentLoadPenalty(
1111
- bestState.assignment,
1112
- this.preparedBuses,
1113
- this.config.balanceLayerLoadByConnectionCount,
1114
- ) *
1115
- getLayerLoadPenaltyWeight(this.config)
1116
- : Number.POSITIVE_INFINITY
1794
+ const score = bestMatchedScore
1117
1795
  if (!Number.isFinite(score)) return null
1118
1796
 
1119
1797
  const summary: FanoutAttemptSummary = {
@@ -1397,9 +2075,14 @@ export class FanoutSolver extends BaseSolver {
1397
2075
  this.solved = true
1398
2076
  } else {
1399
2077
  this.failed = true
1400
- this.error = this.bestAttempt
1401
- ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
1402
- : "FanoutSolver: no layer assignment could be evaluated"
2078
+ const validationMessage =
2079
+ this.lengthMatchingFailure?.message ??
2080
+ this.bestAttempt?.summary.validationIssues?.[0]?.message
2081
+ this.error = validationMessage
2082
+ ? `FanoutSolver: ${validationMessage}`
2083
+ : this.bestAttempt
2084
+ ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
2085
+ : "FanoutSolver: no layer assignment could be evaluated"
1403
2086
  }
1404
2087
  return
1405
2088
  }
@@ -1468,14 +2151,17 @@ export class FanoutSolver extends BaseSolver {
1468
2151
  `FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
1469
2152
  )
1470
2153
  }
1471
- const finalSrj =
1472
- 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
+ })
1473
2160
  const finalTraceById = new Map(
1474
2161
  (finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]),
1475
2162
  )
1476
2163
  return {
1477
- simpleRouteJson:
1478
- this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
2164
+ simpleRouteJson: finalSrj,
1479
2165
  fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
1480
2166
  finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
1481
2167
  ...(plan.planeEndpointTrace
@@ -1485,7 +2171,9 @@ export class FanoutSolver extends BaseSolver {
1485
2171
  ]
1486
2172
  : []),
1487
2173
  ]),
1488
- completionTraces: this.endpointCompletion?.traces ?? [],
2174
+ completionTraces: (this.endpointCompletion?.traces ?? []).map(
2175
+ (trace) => finalTraceById.get(trace.pcb_trace_id) ?? trace,
2176
+ ),
1489
2177
  ...(this.endpointCompletion
1490
2178
  ? { endpointCompletion: this.endpointCompletion.report }
1491
2179
  : {}),
@@ -1510,7 +2198,7 @@ export class FanoutSolver extends BaseSolver {
1510
2198
  }
1511
2199
  }
1512
2200
 
1513
- getOutputSimpleRouteJson(): SimpleRouteJson {
2201
+ getOutputSimpleRouteJson(): SimpleRouteJsonWithFanoutPlanes {
1514
2202
  return this.getOutput().simpleRouteJson
1515
2203
  }
1516
2204