@tscircuit/fanout-solver 0.0.38 → 0.0.40

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,30 @@
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 {
18
+ getComponentDogboneViaSiteCandidates,
19
+ matchComponentDogboneViaSites,
20
+ } from "./match-component-dogbone-via-sites"
21
+ import { connectionsShareElectricalNet } from "./net-identity"
12
22
  import {
13
23
  prepareFanoutBuses,
14
24
  resolveAvailableBoundaryRegions,
15
25
  } from "./prepare-buses"
16
26
  import {
27
+ fanoutPlansAreClear,
17
28
  type RouteBusStaticClearanceCache,
18
29
  routeBus,
19
30
  routeBusAlternatives,
@@ -30,6 +41,7 @@ import type {
30
41
  FanoutSolverOutput,
31
42
  FanoutValidationIssue,
32
43
  PreparedBus,
44
+ SimpleRouteJsonWithFanoutPlanes,
33
45
  } from "./types"
34
46
  import { validateFanoutSolution } from "./validate-fanout-solution"
35
47
  import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
@@ -40,6 +52,7 @@ interface ResolvedFanoutConfig {
40
52
  viaHoleDiameter: number
41
53
  clearance: number
42
54
  compactBusTracks: boolean
55
+ allowBlindAndBuriedVias: boolean
43
56
  allowSameNetMerges: boolean
44
57
  singleLayerPushAndShove: boolean
45
58
  singleLayerAdaptiveExits: boolean
@@ -59,6 +72,11 @@ interface GroupedBeamState {
59
72
  plans: FanoutRoutePlan[]
60
73
  }
61
74
 
75
+ interface MixedTerminationState {
76
+ plans: FanoutRoutePlan[]
77
+ failedBusIds: string[]
78
+ }
79
+
62
80
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
63
81
 
64
82
  function resolvePositiveNumber(label: string, value: number): number {
@@ -131,6 +149,7 @@ function resolveConfig(
131
149
  viaHoleDiameter,
132
150
  clearance,
133
151
  compactBusTracks: options.compactBusTracks ?? false,
152
+ allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
134
153
  allowSameNetMerges: options.allowSameNetMerges ?? false,
135
154
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
136
155
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
@@ -216,6 +235,17 @@ function getLayerLoadPenaltyWeight(config: ResolvedFanoutConfig): number {
216
235
  return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01
217
236
  }
218
237
 
238
+ function comparePlaneRoutingPriority(
239
+ first: PreparedBus,
240
+ second: PreparedBus,
241
+ allowBlindAndBuriedVias: boolean,
242
+ ): number {
243
+ const planeDifference =
244
+ Number(first.termination.type === "plane") -
245
+ Number(second.termination.type === "plane")
246
+ return allowBlindAndBuriedVias ? -planeDifference : planeDifference
247
+ }
248
+
219
249
  function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
220
250
  return plans.reduce(
221
251
  (count, plan) =>
@@ -439,6 +469,7 @@ function getCandidateEscapeLayersForBus(params: {
439
469
  viaHoleDiameter: config.viaHoleDiameter,
440
470
  clearance: config.clearance,
441
471
  compactBusTracks: config.compactBusTracks,
472
+ allowBlindAndBuriedVias: config.allowBlindAndBuriedVias,
442
473
  allowSameNetMerges: config.allowSameNetMerges,
443
474
  staticClearanceCache,
444
475
  }) !== null,
@@ -627,6 +658,7 @@ export class FanoutSolver extends BaseSolver {
627
658
  viaDiameter: this.config.viaDiameter,
628
659
  viaHoleDiameter: this.config.viaHoleDiameter,
629
660
  clearance: this.config.clearance,
661
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
630
662
  effort: this.options.endpointCompletionEffort,
631
663
  routeDownstreamConnections: this.options.routeDownstreamConnections,
632
664
  })
@@ -658,6 +690,7 @@ export class FanoutSolver extends BaseSolver {
658
690
  preparedBuses: this.preparedBuses,
659
691
  sharedBoundary: this.getValidationBoundary(),
660
692
  clearance: this.config.clearance,
693
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
661
694
  })
662
695
  }
663
696
 
@@ -670,10 +703,841 @@ export class FanoutSolver extends BaseSolver {
670
703
  inputSrj: this.inputSrj,
671
704
  sharedBoundary: this.getValidationBoundary(),
672
705
  clearance: this.config.clearance,
706
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
673
707
  allowSameNetMerges: this.config.allowSameNetMerges,
674
708
  })
675
709
  }
676
710
 
711
+ /**
712
+ * Through-all source vias from a wide boundary bus can consume the only
713
+ * legal dogbone channel for nearby plane pads. Conversely, routing hundreds
714
+ * of singleton plane drops first can strand the boundary bus. Search a tiny
715
+ * number of whole-bus boundary alternatives, then fill the remaining plane
716
+ * dogbones. This is intentionally bounded independently of the number of
717
+ * plane drops so dense power fields cannot explode the general beam search.
718
+ */
719
+ private routeDenseThroughAllMixedTerminations(params: {
720
+ busLayerAssignments: Readonly<Record<string, string>>
721
+ busesInRoutingOrder: readonly PreparedBus[]
722
+ }): MixedTerminationState | null {
723
+ if (this.config.allowBlindAndBuriedVias) return null
724
+
725
+ const boundaryBuses = params.busesInRoutingOrder
726
+ .filter((bus) => bus.termination.type === "boundary")
727
+ .toSorted((first, second) => {
728
+ const cornerBandDifference =
729
+ Number(
730
+ Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
731
+ ) -
732
+ Number(
733
+ Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)),
734
+ )
735
+ if (cornerBandDifference !== 0) return cornerBandDifference
736
+ const firstIsCorner = Boolean(
737
+ getCornerBandSide(first.exitEdge, first.preferredExit),
738
+ )
739
+ if (!firstIsCorner) {
740
+ const getSourceSpan = (bus: PreparedBus): number => {
741
+ const xCoordinates = bus.connections.map(
742
+ (connection) => connection.sourcePoint.x,
743
+ )
744
+ const yCoordinates = bus.connections.map(
745
+ (connection) => connection.sourcePoint.y,
746
+ )
747
+ return (
748
+ Math.max(...xCoordinates) -
749
+ Math.min(...xCoordinates) +
750
+ Math.max(...yCoordinates) -
751
+ Math.min(...yCoordinates)
752
+ )
753
+ }
754
+ const sourceSpanDifference =
755
+ getSourceSpan(second) - getSourceSpan(first)
756
+ if (Math.abs(sourceSpanDifference) > 1e-9) {
757
+ return sourceSpanDifference
758
+ }
759
+ }
760
+ const firstLayer = params.busLayerAssignments[first.busId]
761
+ const secondLayer = params.busLayerAssignments[second.busId]
762
+ const layerDifference =
763
+ this.config.layerNames.indexOf(firstLayer ?? "") -
764
+ this.config.layerNames.indexOf(secondLayer ?? "")
765
+ return -layerDifference
766
+ })
767
+ // Preserve the caller/input order for the dense singleton fill. The
768
+ // general routing sort is useful for heterogeneous buses, but ordering a
769
+ // regular BGA power field by obstacle depth creates artificial local
770
+ // dead-ends and needlessly triggers the widened boundary beam. The local
771
+ // rip-up/retry below handles the genuinely constrained drops.
772
+ const planeBuses = this.preparedBuses.filter(
773
+ (bus) => bus.termination.type === "plane",
774
+ )
775
+ if (
776
+ boundaryBuses.length === 0 ||
777
+ boundaryBuses.length > 4 ||
778
+ planeBuses.length < 8 ||
779
+ boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
780
+ planeBuses.some((bus) => bus.connections.length !== 1) ||
781
+ boundaryBuses.length + planeBuses.length !==
782
+ params.busesInRoutingOrder.length
783
+ ) {
784
+ return null
785
+ }
786
+
787
+ const connectionNameByIndex = new Map(
788
+ this.preparedBuses.flatMap((bus) =>
789
+ bus.connections.map(
790
+ (connection) =>
791
+ [connection.connectionIndex, connection.connection.name] as const,
792
+ ),
793
+ ),
794
+ )
795
+ const preferredBoundaryPerpendicularSideByBusId = new Map(
796
+ boundaryBuses.map((bus) => [bus.busId, 1 as const]),
797
+ )
798
+ const preferBoundaryOutwardByBusId = new Map(
799
+ boundaryBuses.map((bus) => [
800
+ bus.busId,
801
+ getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
802
+ ]),
803
+ )
804
+ const canShareCopper = (
805
+ firstConnectionIndex: number,
806
+ secondConnectionIndex: number,
807
+ ): boolean => {
808
+ if (!this.config.allowSameNetMerges) return false
809
+ const firstConnectionName =
810
+ connectionNameByIndex.get(firstConnectionIndex)
811
+ const secondConnectionName = connectionNameByIndex.get(
812
+ secondConnectionIndex,
813
+ )
814
+ return Boolean(
815
+ firstConnectionName &&
816
+ secondConnectionName &&
817
+ connectionsShareElectricalNet(
818
+ this.routingSrj,
819
+ firstConnectionName,
820
+ secondConnectionName,
821
+ ),
822
+ )
823
+ }
824
+ const seedViaPoints = matchComponentDogboneViaSites(
825
+ [...planeBuses, boundaryBuses[0]!],
826
+ {
827
+ viaDiameter: this.config.viaDiameter,
828
+ viaHoleDiameter: this.config.viaHoleDiameter,
829
+ traceWidth: this.config.traceWidth,
830
+ clearance: this.config.clearance,
831
+ maximumSearchStates: 20_000,
832
+ preferredBoundaryPerpendicularSideByBusId,
833
+ preferBoundaryOutwardByBusId,
834
+ canShareCopper,
835
+ },
836
+ )
837
+ if (seedViaPoints) {
838
+ let fixedViaPointsByConnectionIndex: ReadonlyMap<
839
+ number,
840
+ { x: number; y: number }
841
+ > = seedViaPoints
842
+ let matchedPlans: FanoutRoutePlan[] = []
843
+ let matchedRoutingSucceeded = true
844
+ const getReservedVias = (bus: PreparedBus) => {
845
+ const currentConnectionNames = new Set(
846
+ bus.connections.map((connection) => connection.connection.name),
847
+ )
848
+ return this.preparedBuses.flatMap((preparedBus) => {
849
+ const targetLayer = params.busLayerAssignments[preparedBus.busId]
850
+ if (!targetLayer) return []
851
+ return preparedBus.connections.flatMap((connection) => {
852
+ if (currentConnectionNames.has(connection.connection.name))
853
+ return []
854
+ const center = fixedViaPointsByConnectionIndex.get(
855
+ connection.connectionIndex,
856
+ )
857
+ if (!center) return []
858
+ return [
859
+ {
860
+ connectionName: connection.connection.name,
861
+ via: {
862
+ center,
863
+ diameter: this.config.viaDiameter,
864
+ spanLayers: getViaSpanLayers({
865
+ fromLayer: connection.sourceLayer,
866
+ toLayer: targetLayer,
867
+ layerNames: this.config.layerNames,
868
+ allowBlindAndBuriedVias: false,
869
+ }),
870
+ },
871
+ },
872
+ ]
873
+ })
874
+ })
875
+ }
876
+ const routeMatchedBoundaryBus = (bus: PreparedBus): boolean => {
877
+ const targetLayer = params.busLayerAssignments[bus.busId]
878
+ if (!targetLayer) {
879
+ return false
880
+ }
881
+ const routeParams = {
882
+ srj: this.routingSrj,
883
+ bus,
884
+ targetLayer,
885
+ acceptedPlans: matchedPlans,
886
+ layerNames: this.config.layerNames,
887
+ traceWidth: this.config.traceWidth,
888
+ viaDiameter: this.config.viaDiameter,
889
+ viaHoleDiameter: this.config.viaHoleDiameter,
890
+ clearance: this.config.clearance,
891
+ compactBusTracks: this.config.compactBusTracks,
892
+ allowBlindAndBuriedVias: false,
893
+ allowSameNetMerges: this.config.allowSameNetMerges,
894
+ staticClearanceCache: this.routeStaticClearanceCache,
895
+ fixedViaPointsByConnectionIndex,
896
+ reservedVias: getReservedVias(bus),
897
+ viaMinimalOnly: true,
898
+ } as const
899
+ let busPlans = routeBusAlternatives(routeParams, 1)[0]
900
+ if (busPlans && bus.maxLengthSkew !== undefined) {
901
+ const lengths = busPlans.map((plan) => plan.length)
902
+ const rawSkew = Math.max(...lengths) - Math.min(...lengths)
903
+ const needsRouteDiversity =
904
+ rawSkew - bus.maxLengthSkew > Math.max(1, bus.maxLengthSkew * 0.25)
905
+ // Only pay for additional A* variants when the first topology is so
906
+ // skewed that compact meanders are unlikely to absorb the deficit.
907
+ // This keeps already-near-matched buses on the single-attempt path.
908
+ if (needsRouteDiversity) {
909
+ busPlans = routeBusAlternatives(routeParams, 3).toSorted(
910
+ (first, second) => {
911
+ const firstLengths = first.map((plan) => plan.length)
912
+ const secondLengths = second.map((plan) => plan.length)
913
+ return (
914
+ Math.max(...firstLengths) -
915
+ Math.min(...firstLengths) -
916
+ (Math.max(...secondLengths) - Math.min(...secondLengths))
917
+ )
918
+ },
919
+ )[0]
920
+ }
921
+ }
922
+ if (!busPlans) {
923
+ return false
924
+ }
925
+ matchedPlans.push(...busPlans)
926
+ return true
927
+ }
928
+
929
+ const firstBoundaryBus = boundaryBuses[0]!
930
+ const routedBoundaryBuses: PreparedBus[] = []
931
+ if (routeMatchedBoundaryBus(firstBoundaryBus)) {
932
+ routedBoundaryBuses.push(firstBoundaryBus)
933
+ } else {
934
+ matchedRoutingSucceeded = false
935
+ }
936
+ const remainingBoundaryBuses = boundaryBuses.slice(1)
937
+ while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
938
+ const blockingSegments = matchedPlans.flatMap((plan) =>
939
+ plan.segments.map((segment) => ({
940
+ connectionIndex: plan.connectionIndex,
941
+ segment,
942
+ })),
943
+ )
944
+ let selectedBusIndex = -1
945
+ for (
946
+ let candidateIndex = 0;
947
+ candidateIndex < remainingBoundaryBuses.length;
948
+ candidateIndex++
949
+ ) {
950
+ const candidateBus = remainingBoundaryBuses[candidateIndex]!
951
+ const extendedViaPoints = matchComponentDogboneViaSites(
952
+ [...planeBuses, ...routedBoundaryBuses, candidateBus],
953
+ {
954
+ viaDiameter: this.config.viaDiameter,
955
+ viaHoleDiameter: this.config.viaHoleDiameter,
956
+ traceWidth: this.config.traceWidth,
957
+ clearance: this.config.clearance,
958
+ maximumSearchStates: 100_000,
959
+ preferredBoundaryPerpendicularSideByBusId,
960
+ preferBoundaryOutwardByBusId,
961
+ fixedViaPointsByConnectionIndex: fixedViaPointsByConnectionIndex,
962
+ blockingSegments,
963
+ canShareCopper,
964
+ },
965
+ )
966
+ if (!extendedViaPoints) continue
967
+ const previousFixedViaPoints = fixedViaPointsByConnectionIndex
968
+ const previousPlanCount = matchedPlans.length
969
+ const laterBuses = remainingBoundaryBuses.filter(
970
+ (_, laterIndex) => laterIndex !== candidateIndex,
971
+ )
972
+ let candidateFixedViaPoints: ReadonlyMap<
973
+ number,
974
+ { x: number; y: number }
975
+ > = extendedViaPoints
976
+ if (laterBuses.length === 1) {
977
+ const laterBus = laterBuses[0]!
978
+ const futureAssignment = matchComponentDogboneViaSites(
979
+ [...planeBuses, ...routedBoundaryBuses, candidateBus, laterBus],
980
+ {
981
+ viaDiameter: this.config.viaDiameter,
982
+ viaHoleDiameter: this.config.viaHoleDiameter,
983
+ traceWidth: this.config.traceWidth,
984
+ clearance: this.config.clearance,
985
+ maximumSearchStates: 100_000,
986
+ preferredBoundaryPerpendicularSideByBusId,
987
+ preferBoundaryOutwardByBusId,
988
+ fixedViaPointsByConnectionIndex: extendedViaPoints,
989
+ blockingSegments,
990
+ canShareCopper,
991
+ },
992
+ )
993
+ if (futureAssignment) {
994
+ const candidateCountByConnectionIndex = new Map<number, number>()
995
+ for (const candidate of getComponentDogboneViaSiteCandidates(
996
+ [laterBus],
997
+ {
998
+ viaDiameter: this.config.viaDiameter,
999
+ viaHoleDiameter: this.config.viaHoleDiameter,
1000
+ traceWidth: this.config.traceWidth,
1001
+ clearance: this.config.clearance,
1002
+ blockingSegments,
1003
+ canShareCopper,
1004
+ },
1005
+ )) {
1006
+ candidateCountByConnectionIndex.set(
1007
+ candidate.connectionIndex,
1008
+ (candidateCountByConnectionIndex.get(
1009
+ candidate.connectionIndex,
1010
+ ) ?? 0) + 1,
1011
+ )
1012
+ }
1013
+ const constrainedConnections = laterBus.connections.toSorted(
1014
+ (first, second) =>
1015
+ (candidateCountByConnectionIndex.get(first.connectionIndex) ??
1016
+ 0) -
1017
+ (candidateCountByConnectionIndex.get(
1018
+ second.connectionIndex,
1019
+ ) ?? 0) || first.connectionIndex - second.connectionIndex,
1020
+ )
1021
+ const repairedViaPoints = new Map(extendedViaPoints)
1022
+ for (const connection of constrainedConnections) {
1023
+ const criticalPoint = futureAssignment.get(
1024
+ connection.connectionIndex,
1025
+ )
1026
+ if (criticalPoint) {
1027
+ repairedViaPoints.set(
1028
+ connection.connectionIndex,
1029
+ criticalPoint,
1030
+ )
1031
+ }
1032
+ }
1033
+ candidateFixedViaPoints = repairedViaPoints
1034
+ }
1035
+ }
1036
+ fixedViaPointsByConnectionIndex = candidateFixedViaPoints
1037
+ if (routeMatchedBoundaryBus(candidateBus)) {
1038
+ const candidateLeavesAFeasibleExtension =
1039
+ laterBuses.length === 0 ||
1040
+ laterBuses.some((laterBus) => {
1041
+ const lookaheadBlockingSegments = matchedPlans.flatMap((plan) =>
1042
+ plan.segments.map((segment) => ({
1043
+ connectionIndex: plan.connectionIndex,
1044
+ segment,
1045
+ })),
1046
+ )
1047
+ return Boolean(
1048
+ matchComponentDogboneViaSites(
1049
+ [
1050
+ ...planeBuses,
1051
+ ...routedBoundaryBuses,
1052
+ candidateBus,
1053
+ laterBus,
1054
+ ],
1055
+ {
1056
+ viaDiameter: this.config.viaDiameter,
1057
+ viaHoleDiameter: this.config.viaHoleDiameter,
1058
+ traceWidth: this.config.traceWidth,
1059
+ clearance: this.config.clearance,
1060
+ maximumSearchStates: 100_000,
1061
+ preferredBoundaryPerpendicularSideByBusId,
1062
+ preferBoundaryOutwardByBusId,
1063
+ fixedViaPointsByConnectionIndex:
1064
+ fixedViaPointsByConnectionIndex,
1065
+ blockingSegments: lookaheadBlockingSegments,
1066
+ canShareCopper,
1067
+ },
1068
+ ),
1069
+ )
1070
+ })
1071
+ if (candidateLeavesAFeasibleExtension) {
1072
+ selectedBusIndex = candidateIndex
1073
+ routedBoundaryBuses.push(candidateBus)
1074
+ break
1075
+ }
1076
+ matchedPlans.splice(previousPlanCount)
1077
+ }
1078
+ fixedViaPointsByConnectionIndex = previousFixedViaPoints
1079
+ }
1080
+ if (selectedBusIndex < 0) {
1081
+ matchedRoutingSucceeded = false
1082
+ break
1083
+ }
1084
+ remainingBoundaryBuses.splice(selectedBusIndex, 1)
1085
+ }
1086
+ if (matchedRoutingSucceeded) {
1087
+ let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
1088
+ null
1089
+ const matchViaPointsAroundPlans = (
1090
+ candidatePlans: readonly FanoutRoutePlan[],
1091
+ ): Map<number, { x: number; y: number }> | null => {
1092
+ const fixedBoundaryViaPoints = new Map(
1093
+ candidatePlans.flatMap((plan) =>
1094
+ plan.via
1095
+ ? [[plan.connectionIndex, plan.via.center] as const]
1096
+ : [],
1097
+ ),
1098
+ )
1099
+ return matchComponentDogboneViaSites(
1100
+ [...planeBuses, ...boundaryBuses],
1101
+ {
1102
+ viaDiameter: this.config.viaDiameter,
1103
+ viaHoleDiameter: this.config.viaHoleDiameter,
1104
+ traceWidth: this.config.traceWidth,
1105
+ clearance: this.config.clearance,
1106
+ maximumSearchStates: 100_000,
1107
+ preferredBoundaryPerpendicularSideByBusId,
1108
+ preferBoundaryOutwardByBusId,
1109
+ fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
1110
+ blockingSegments: candidatePlans.flatMap((plan) =>
1111
+ plan.segments.map((segment) => ({
1112
+ connectionIndex: plan.connectionIndex,
1113
+ segment,
1114
+ })),
1115
+ ),
1116
+ canShareCopper,
1117
+ },
1118
+ )
1119
+ }
1120
+ const matchedLengthResult = matchBusPlanLengths({
1121
+ plans: matchedPlans,
1122
+ preparedBuses: this.preparedBuses,
1123
+ inputSrj: this.inputSrj,
1124
+ sharedBoundary: this.getValidationBoundary(),
1125
+ clearance: this.config.clearance,
1126
+ allowBlindAndBuriedVias: false,
1127
+ allowSameNetMerges: this.config.allowSameNetMerges,
1128
+ allowMatchingInsideDenseBounds: true,
1129
+ candidatePlansAreFeasible: (candidatePlans) => {
1130
+ const candidateViaPoints = matchViaPointsAroundPlans(candidatePlans)
1131
+ if (!candidateViaPoints) return false
1132
+ feasibleViaPoints = candidateViaPoints
1133
+ return true
1134
+ },
1135
+ })
1136
+ if (matchedLengthResult.plans) {
1137
+ matchedPlans = matchedLengthResult.plans
1138
+ const rematchedViaPoints =
1139
+ feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans)
1140
+ if (rematchedViaPoints) {
1141
+ fixedViaPointsByConnectionIndex = rematchedViaPoints
1142
+ } else {
1143
+ matchedRoutingSucceeded = false
1144
+ }
1145
+ } else {
1146
+ matchedRoutingSucceeded = false
1147
+ }
1148
+ }
1149
+ if (matchedRoutingSucceeded) {
1150
+ for (const bus of planeBuses) {
1151
+ const targetLayer = params.busLayerAssignments[bus.busId]
1152
+ const busPlans = targetLayer
1153
+ ? routeBus({
1154
+ srj: this.routingSrj,
1155
+ bus,
1156
+ targetLayer,
1157
+ acceptedPlans: matchedPlans,
1158
+ layerNames: this.config.layerNames,
1159
+ traceWidth: this.config.traceWidth,
1160
+ viaDiameter: this.config.viaDiameter,
1161
+ viaHoleDiameter: this.config.viaHoleDiameter,
1162
+ clearance: this.config.clearance,
1163
+ compactBusTracks: this.config.compactBusTracks,
1164
+ allowBlindAndBuriedVias: false,
1165
+ allowSameNetMerges: this.config.allowSameNetMerges,
1166
+ staticClearanceCache: this.routeStaticClearanceCache,
1167
+ fixedViaPointsByConnectionIndex,
1168
+ })
1169
+ : null
1170
+ if (!busPlans) {
1171
+ matchedRoutingSucceeded = false
1172
+ break
1173
+ }
1174
+ matchedPlans.push(...busPlans)
1175
+ }
1176
+ }
1177
+ if (
1178
+ matchedRoutingSucceeded &&
1179
+ fanoutPlansAreClear({
1180
+ plans: matchedPlans,
1181
+ srj: this.routingSrj,
1182
+ sharedBoundary: boundaryBuses[0]!.sharedBoundary,
1183
+ clearance: this.config.clearance,
1184
+ allowBlindAndBuriedVias: false,
1185
+ allowSameNetMerges: this.config.allowSameNetMerges,
1186
+ })
1187
+ ) {
1188
+ return { plans: matchedPlans, failedBusIds: [] }
1189
+ }
1190
+ }
1191
+
1192
+ const maximumStates = 8
1193
+ const getBoundaryStates = (
1194
+ alternativesPerBoundaryBus: number,
1195
+ initialPlans: readonly FanoutRoutePlan[] = [],
1196
+ ): MixedTerminationState[] | null => {
1197
+ let states: MixedTerminationState[] = [
1198
+ { plans: [...initialPlans], failedBusIds: [] },
1199
+ ]
1200
+ for (const bus of boundaryBuses) {
1201
+ const targetLayer = params.busLayerAssignments[bus.busId]
1202
+ if (!targetLayer) return null
1203
+ const nextStates: MixedTerminationState[] = []
1204
+ const alternativesByState = states.map((state) => ({
1205
+ state,
1206
+ alternatives: routeBusAlternatives(
1207
+ {
1208
+ srj: this.routingSrj,
1209
+ bus,
1210
+ targetLayer,
1211
+ acceptedPlans: state.plans,
1212
+ layerNames: this.config.layerNames,
1213
+ traceWidth: this.config.traceWidth,
1214
+ viaDiameter: this.config.viaDiameter,
1215
+ viaHoleDiameter: this.config.viaHoleDiameter,
1216
+ clearance: this.config.clearance,
1217
+ compactBusTracks: this.config.compactBusTracks,
1218
+ allowBlindAndBuriedVias: false,
1219
+ allowSameNetMerges: this.config.allowSameNetMerges,
1220
+ staticClearanceCache: this.routeStaticClearanceCache,
1221
+ },
1222
+ alternativesPerBoundaryBus,
1223
+ ),
1224
+ }))
1225
+ for (
1226
+ let alternativeIndex = 0;
1227
+ alternativeIndex < alternativesPerBoundaryBus;
1228
+ alternativeIndex++
1229
+ ) {
1230
+ for (const { state, alternatives } of alternativesByState) {
1231
+ const alternative = alternatives[alternativeIndex]
1232
+ if (!alternative) continue
1233
+ nextStates.push({
1234
+ plans: [...state.plans, ...alternative],
1235
+ failedBusIds: [],
1236
+ })
1237
+ if (nextStates.length >= maximumStates) break
1238
+ }
1239
+ if (nextStates.length >= maximumStates) break
1240
+ }
1241
+ if (nextStates.length === 0) return null
1242
+ states = nextStates
1243
+ }
1244
+ return states
1245
+ }
1246
+
1247
+ const getJointReservedBoundaryState = (
1248
+ initialPlans: readonly FanoutRoutePlan[],
1249
+ ): MixedTerminationState | null => {
1250
+ if (boundaryBuses.length !== 2) return null
1251
+ const [firstBus, secondBus] = boundaryBuses
1252
+ if (!firstBus || !secondBus) return null
1253
+ const routeBoundaryBus = (
1254
+ bus: PreparedBus,
1255
+ acceptedPlans: FanoutRoutePlan[],
1256
+ rejectedViaMinimalCandidates?: FanoutRoutePlan[][],
1257
+ ): FanoutRoutePlan[] | null => {
1258
+ const targetLayer = params.busLayerAssignments[bus.busId]
1259
+ if (!targetLayer) return null
1260
+ return (
1261
+ routeBusAlternatives(
1262
+ {
1263
+ srj: this.routingSrj,
1264
+ bus,
1265
+ targetLayer,
1266
+ acceptedPlans,
1267
+ layerNames: this.config.layerNames,
1268
+ traceWidth: this.config.traceWidth,
1269
+ viaDiameter: this.config.viaDiameter,
1270
+ viaHoleDiameter: this.config.viaHoleDiameter,
1271
+ clearance: this.config.clearance,
1272
+ compactBusTracks: this.config.compactBusTracks,
1273
+ allowBlindAndBuriedVias: false,
1274
+ allowSameNetMerges: this.config.allowSameNetMerges,
1275
+ staticClearanceCache: this.routeStaticClearanceCache,
1276
+ rejectedViaMinimalCandidates,
1277
+ stopAfterFirstRejectedViaMinimalCandidate:
1278
+ rejectedViaMinimalCandidates !== undefined,
1279
+ },
1280
+ 1,
1281
+ )[0] ?? null
1282
+ )
1283
+ }
1284
+
1285
+ const firstPlans = routeBoundaryBus(firstBus, [...initialPlans])
1286
+ if (!firstPlans) return null
1287
+ const rejectedSecondCandidates: FanoutRoutePlan[][] = []
1288
+ const secondPlans = routeBoundaryBus(
1289
+ secondBus,
1290
+ [...initialPlans, ...firstPlans],
1291
+ rejectedSecondCandidates,
1292
+ )
1293
+ if (secondPlans) {
1294
+ return {
1295
+ plans: [...initialPlans, ...firstPlans, ...secondPlans],
1296
+ failedBusIds: [],
1297
+ }
1298
+ }
1299
+
1300
+ const rejectedSecondPlans = rejectedSecondCandidates[0]
1301
+ if (!rejectedSecondPlans) return null
1302
+ // Keep the candidate's exact copper/vias as immutable blockers while
1303
+ // rerouting the earlier bus, but exclude it from corner-slot allocation.
1304
+ const reservedSecondPlans = rejectedSecondPlans.map((plan) => ({
1305
+ ...plan,
1306
+ termination: {
1307
+ type: "plane" as const,
1308
+ layer: plan.targetLayer,
1309
+ },
1310
+ }))
1311
+ const reroutedFirstPlans = routeBoundaryBus(firstBus, [
1312
+ ...initialPlans,
1313
+ ...reservedSecondPlans,
1314
+ ])
1315
+ if (!reroutedFirstPlans) return null
1316
+ const combinedPlans = [
1317
+ ...initialPlans,
1318
+ ...reroutedFirstPlans,
1319
+ ...rejectedSecondPlans,
1320
+ ]
1321
+ if (
1322
+ !fanoutPlansAreClear({
1323
+ plans: combinedPlans,
1324
+ srj: this.routingSrj,
1325
+ sharedBoundary: firstBus.sharedBoundary,
1326
+ clearance: this.config.clearance,
1327
+ allowBlindAndBuriedVias: false,
1328
+ allowSameNetMerges: this.config.allowSameNetMerges,
1329
+ })
1330
+ ) {
1331
+ return null
1332
+ }
1333
+ return { plans: combinedPlans, failedBusIds: [] }
1334
+ }
1335
+
1336
+ const planeBusById = new Map(planeBuses.map((bus) => [bus.busId, bus]))
1337
+ const routePlaneBus = (
1338
+ bus: PreparedBus,
1339
+ acceptedPlans: FanoutRoutePlan[],
1340
+ blockingBusCounts?: Map<string, number>,
1341
+ ): FanoutRoutePlan[] | null => {
1342
+ const targetLayer = params.busLayerAssignments[bus.busId]
1343
+ if (!targetLayer) return null
1344
+ return routeBus({
1345
+ srj: this.routingSrj,
1346
+ bus,
1347
+ targetLayer,
1348
+ acceptedPlans,
1349
+ layerNames: this.config.layerNames,
1350
+ traceWidth: this.config.traceWidth,
1351
+ viaDiameter: this.config.viaDiameter,
1352
+ viaHoleDiameter: this.config.viaHoleDiameter,
1353
+ clearance: this.config.clearance,
1354
+ compactBusTracks: this.config.compactBusTracks,
1355
+ allowBlindAndBuriedVias: false,
1356
+ allowSameNetMerges: this.config.allowSameNetMerges,
1357
+ staticClearanceCache: this.routeStaticClearanceCache,
1358
+ blockingBusCounts,
1359
+ })
1360
+ }
1361
+
1362
+ const routePlaneOrder = (
1363
+ boundaryPlans: readonly FanoutRoutePlan[],
1364
+ planeOrder: readonly PreparedBus[],
1365
+ ): MixedTerminationState => {
1366
+ const state: MixedTerminationState = {
1367
+ plans: [...boundaryPlans],
1368
+ failedBusIds: [],
1369
+ }
1370
+ for (const bus of planeOrder) {
1371
+ const blockingBusCounts = new Map<string, number>()
1372
+ let busPlans = routePlaneBus(bus, state.plans, blockingBusCounts)
1373
+
1374
+ // A plane dogbone can lose its only local channel to one or two
1375
+ // earlier singleton drops. Rip up only the strongest local blockers,
1376
+ // route the constrained drop first, then put the displaced drops back.
1377
+ // Boundary buses are immutable here and the search is capped at 36
1378
+ // blocker pairs, so dense fields remain predictable.
1379
+ if (!busPlans) {
1380
+ const blockerIds = [...blockingBusCounts.entries()]
1381
+ .filter(([busId]) =>
1382
+ state.plans.some((plan) => plan.busId === busId),
1383
+ )
1384
+ .filter(([busId]) => planeBusById.has(busId))
1385
+ .toSorted(
1386
+ ([, firstCount], [, secondCount]) => secondCount - firstCount,
1387
+ )
1388
+ .slice(0, 8)
1389
+ .map(([busId]) => busId)
1390
+ const ripupSets = [
1391
+ ...blockerIds.map((busId) => [busId]),
1392
+ ...blockerIds.flatMap((first, firstIndex) =>
1393
+ blockerIds.slice(firstIndex + 1).map((second) => [first, second]),
1394
+ ),
1395
+ ]
1396
+ for (const ripupIds of ripupSets) {
1397
+ const ripupIdSet = new Set(ripupIds)
1398
+ const candidatePlans = state.plans.filter(
1399
+ (plan) => !ripupIdSet.has(plan.busId),
1400
+ )
1401
+ const constrainedPlans = routePlaneBus(bus, candidatePlans)
1402
+ if (!constrainedPlans) continue
1403
+ candidatePlans.push(...constrainedPlans)
1404
+ let repairSucceeded = true
1405
+ for (const blockerId of ripupIds) {
1406
+ const blockerBus = planeBusById.get(blockerId)
1407
+ const replacementPlans = blockerBus
1408
+ ? routePlaneBus(blockerBus, candidatePlans)
1409
+ : null
1410
+ if (!replacementPlans) {
1411
+ repairSucceeded = false
1412
+ break
1413
+ }
1414
+ candidatePlans.push(...replacementPlans)
1415
+ }
1416
+ if (!repairSucceeded) continue
1417
+ state.plans = candidatePlans
1418
+ busPlans = []
1419
+ break
1420
+ }
1421
+ }
1422
+
1423
+ if (busPlans) state.plans.push(...busPlans)
1424
+ else state.failedBusIds.push(bus.busId)
1425
+ }
1426
+ return state
1427
+ }
1428
+
1429
+ let bestState: MixedTerminationState | null = null
1430
+ let mostRecentBoundaryBestState: MixedTerminationState | null = null
1431
+ const evaluateBoundaryStates = (
1432
+ states: readonly MixedTerminationState[],
1433
+ initialPlaneOrder: readonly PreparedBus[],
1434
+ ): MixedTerminationState | null => {
1435
+ let localBestState: MixedTerminationState | null = null
1436
+ for (const boundaryState of states) {
1437
+ let planeOrder = [...initialPlaneOrder]
1438
+ const seenPlaneOrders = new Set<string>()
1439
+ for (let retryIndex = 0; retryIndex < 3; retryIndex++) {
1440
+ const orderKey = planeOrder.map((bus) => bus.busId).join("\u0000")
1441
+ if (seenPlaneOrders.has(orderKey)) break
1442
+ seenPlaneOrders.add(orderKey)
1443
+ const state = routePlaneOrder(boundaryState.plans, planeOrder)
1444
+ if (
1445
+ !bestState ||
1446
+ state.plans.length > bestState.plans.length ||
1447
+ (state.plans.length === bestState.plans.length &&
1448
+ state.failedBusIds.length < bestState.failedBusIds.length)
1449
+ ) {
1450
+ bestState = state
1451
+ }
1452
+ if (
1453
+ !localBestState ||
1454
+ state.plans.length > localBestState.plans.length ||
1455
+ (state.plans.length === localBestState.plans.length &&
1456
+ state.failedBusIds.length < localBestState.failedBusIds.length)
1457
+ ) {
1458
+ localBestState = state
1459
+ }
1460
+ if (state.failedBusIds.length === 0) return state
1461
+ const failedBusIds = new Set(state.failedBusIds)
1462
+ planeOrder = [
1463
+ ...state.failedBusIds.flatMap((busId) => {
1464
+ const bus = planeBusById.get(busId)
1465
+ return bus ? [bus] : []
1466
+ }),
1467
+ ...planeOrder.filter((bus) => !failedBusIds.has(bus.busId)),
1468
+ ]
1469
+ }
1470
+ }
1471
+ mostRecentBoundaryBestState = localBestState
1472
+ return null
1473
+ }
1474
+
1475
+ // Most dense packages route with the first deterministic boundary choice.
1476
+ // Try that cheap path before widening the beam; eagerly generating four
1477
+ // complete A* variants per state can otherwise dominate runtime and memory.
1478
+ for (const alternativesPerBoundaryBus of [1, 4]) {
1479
+ const states = getBoundaryStates(alternativesPerBoundaryBus)
1480
+ if (!states) continue
1481
+ const completeState = evaluateBoundaryStates(states, planeBuses)
1482
+ if (completeState) return completeState
1483
+
1484
+ // If a completed signal escape encloses one especially constrained
1485
+ // power pad, seed just that failed singleton first and recompute the
1486
+ // small boundary-bus set around its physical through barrel. This keeps
1487
+ // the search local without routing hundreds of plane drops before the
1488
+ // signal buses.
1489
+ const seedFailureIds = (
1490
+ bestState as MixedTerminationState | null
1491
+ )?.failedBusIds.slice(0, 3)
1492
+ if (alternativesPerBoundaryBus === 1 && seedFailureIds) {
1493
+ for (const failedBusId of seedFailureIds) {
1494
+ const seededPlanePlans: FanoutRoutePlan[] = []
1495
+ const seededPlaneBusIds = new Set<string>()
1496
+ let nextFailedBusId: string | undefined = failedBusId
1497
+ for (
1498
+ let seedDepth = 0;
1499
+ seedDepth < 3 && nextFailedBusId;
1500
+ seedDepth++
1501
+ ) {
1502
+ const failedPlaneBus = planeBusById.get(nextFailedBusId)
1503
+ if (!failedPlaneBus) break
1504
+ const nextSeedPlans = routePlaneBus(
1505
+ failedPlaneBus,
1506
+ seededPlanePlans,
1507
+ )
1508
+ if (!nextSeedPlans) break
1509
+ seededPlanePlans.push(...nextSeedPlans)
1510
+ seededPlaneBusIds.add(nextFailedBusId)
1511
+ const jointReservedBoundaryState =
1512
+ getJointReservedBoundaryState(seededPlanePlans)
1513
+ if (!jointReservedBoundaryState) break
1514
+ const jointCompleteState = evaluateBoundaryStates(
1515
+ [jointReservedBoundaryState],
1516
+ planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
1517
+ )
1518
+ if (jointCompleteState) return jointCompleteState
1519
+ const recentFailedBusIds = (
1520
+ mostRecentBoundaryBestState as MixedTerminationState | null
1521
+ )?.failedBusIds
1522
+ nextFailedBusId = recentFailedBusIds?.find(
1523
+ (busId) => !seededPlaneBusIds.has(busId),
1524
+ )
1525
+ }
1526
+ if (seededPlanePlans.length === 0) continue
1527
+ const seededBoundaryStates = getBoundaryStates(1, seededPlanePlans)
1528
+ if (!seededBoundaryStates) continue
1529
+ const seededCompleteState = evaluateBoundaryStates(
1530
+ seededBoundaryStates,
1531
+ planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
1532
+ )
1533
+ if (seededCompleteState) return seededCompleteState
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ return bestState
1539
+ }
1540
+
677
1541
  private evaluateAssignmentWithStrategy(
678
1542
  assignmentIndex: number,
679
1543
  busLayerAssignments: Readonly<Record<string, string>>,
@@ -723,8 +1587,11 @@ export class FanoutSolver extends BaseSolver {
723
1587
  busLayerAssignments[b.busId] ?? "",
724
1588
  )
725
1589
  return (
726
- Number(b.termination.type === "plane") -
727
- Number(a.termination.type === "plane") ||
1590
+ comparePlaneRoutingPriority(
1591
+ a,
1592
+ b,
1593
+ this.config.allowBlindAndBuriedVias,
1594
+ ) ||
728
1595
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
729
1596
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
730
1597
  ? bLayerIndex - aLayerIndex
@@ -744,8 +1611,23 @@ export class FanoutSolver extends BaseSolver {
744
1611
  )
745
1612
  })
746
1613
 
1614
+ const mixedTerminationState =
1615
+ !useSingleLayerPushAndShove && routingStrategy === "default"
1616
+ ? this.routeDenseThroughAllMixedTerminations({
1617
+ busLayerAssignments,
1618
+ busesInRoutingOrder,
1619
+ })
1620
+ : null
1621
+
1622
+ if (mixedTerminationState) {
1623
+ plans = mixedTerminationState.plans
1624
+ failedBusIds = mixedTerminationState.failedBusIds
1625
+ }
1626
+
747
1627
  let routingPrefixKey = `${routingStrategy}|`
748
- for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
1628
+ for (const bus of useSingleLayerPushAndShove || mixedTerminationState
1629
+ ? []
1630
+ : busesInRoutingOrder) {
749
1631
  const targetLayer = busLayerAssignments[bus.busId]
750
1632
  if (!targetLayer) {
751
1633
  throw new Error(
@@ -777,6 +1659,7 @@ export class FanoutSolver extends BaseSolver {
777
1659
  viaHoleDiameter: this.config.viaHoleDiameter,
778
1660
  clearance: this.config.clearance,
779
1661
  compactBusTracks: this.config.compactBusTracks,
1662
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
780
1663
  allowSameNetMerges: this.config.allowSameNetMerges,
781
1664
  staticClearanceCache: this.routeStaticClearanceCache,
782
1665
  blockingBusCounts: currentBusBlockingCounts,
@@ -870,7 +1753,6 @@ export class FanoutSolver extends BaseSolver {
870
1753
  score,
871
1754
  ...(validationIssues ? { validationIssues } : {}),
872
1755
  }
873
-
874
1756
  return {
875
1757
  summary,
876
1758
  plans,
@@ -972,8 +1854,11 @@ export class FanoutSolver extends BaseSolver {
972
1854
  : (this.escapeLayersByBusId[b.busId]?.length ??
973
1855
  this.config.escapeLayers.length)
974
1856
  return (
975
- Number(b.termination.type === "plane") -
976
- Number(a.termination.type === "plane") ||
1857
+ comparePlaneRoutingPriority(
1858
+ a,
1859
+ b,
1860
+ this.config.allowBlindAndBuriedVias,
1861
+ ) ||
977
1862
  Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
978
1863
  (aUsesCoordinatedWinding && bUsesCoordinatedWinding
979
1864
  ? getMaximumViaSpan(b) - getMaximumViaSpan(a)
@@ -1084,6 +1969,7 @@ export class FanoutSolver extends BaseSolver {
1084
1969
  viaHoleDiameter: this.config.viaHoleDiameter,
1085
1970
  clearance: this.config.clearance,
1086
1971
  compactBusTracks: this.config.compactBusTracks,
1972
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
1087
1973
  allowSameNetMerges: this.config.allowSameNetMerges,
1088
1974
  staticClearanceCache: this.routeStaticClearanceCache,
1089
1975
  },
@@ -1534,14 +2420,17 @@ export class FanoutSolver extends BaseSolver {
1534
2420
  `FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
1535
2421
  )
1536
2422
  }
1537
- const finalSrj =
1538
- this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj
2423
+ const finalSrj = addViaLayerMetadataToSrj({
2424
+ srj:
2425
+ this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
2426
+ layerNames: this.config.layerNames,
2427
+ allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
2428
+ })
1539
2429
  const finalTraceById = new Map(
1540
2430
  (finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]),
1541
2431
  )
1542
2432
  return {
1543
- simpleRouteJson:
1544
- this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
2433
+ simpleRouteJson: finalSrj,
1545
2434
  fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
1546
2435
  finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
1547
2436
  ...(plan.planeEndpointTrace
@@ -1551,7 +2440,9 @@ export class FanoutSolver extends BaseSolver {
1551
2440
  ]
1552
2441
  : []),
1553
2442
  ]),
1554
- completionTraces: this.endpointCompletion?.traces ?? [],
2443
+ completionTraces: (this.endpointCompletion?.traces ?? []).map(
2444
+ (trace) => finalTraceById.get(trace.pcb_trace_id) ?? trace,
2445
+ ),
1555
2446
  ...(this.endpointCompletion
1556
2447
  ? { endpointCompletion: this.endpointCompletion.report }
1557
2448
  : {}),
@@ -1576,7 +2467,7 @@ export class FanoutSolver extends BaseSolver {
1576
2467
  }
1577
2468
  }
1578
2469
 
1579
- getOutputSimpleRouteJson(): SimpleRouteJson {
2470
+ getOutputSimpleRouteJson(): SimpleRouteJsonWithFanoutPlanes {
1580
2471
  return this.getOutput().simpleRouteJson
1581
2472
  }
1582
2473