@tscircuit/fanout-solver 0.0.48 → 0.0.49

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,6 +1,6 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
- import type { GraphicsObject } from "graphics-debug"
3
+ import { mergeGraphics, type GraphicsObject } from "graphics-debug"
4
4
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
5
5
  import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
6
6
  import { buildOutputSimpleRouteJson } from "./build-output"
@@ -27,9 +27,10 @@ import {
27
27
  fanoutPlansAreClear,
28
28
  type RouteBusStaticClearanceCache,
29
29
  routeBus,
30
+ routeBusAlternativesSteps,
30
31
  routeBusAlternatives,
31
32
  } from "./route-bus"
32
- import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
33
+ import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
33
34
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
34
35
  import type {
35
36
  AssignmentAttempt,
@@ -79,6 +80,94 @@ interface MixedTerminationState {
79
80
 
80
81
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
81
82
 
83
+ interface FanoutSubsolverRequest {
84
+ type: "subsolver"
85
+ solver: BaseSolver
86
+ }
87
+
88
+ type FanoutWorkYield = void | FanoutSubsolverRequest
89
+
90
+ class FanoutWorkSolver<T> extends BaseSolver {
91
+ private output: T | undefined
92
+ private hasOutput = false
93
+ private nextInput: unknown
94
+
95
+ constructor(
96
+ private readonly solverName: string,
97
+ private readonly generator: Generator<unknown, T, unknown>,
98
+ private readonly getVisualization: () => GraphicsObject,
99
+ private readonly getStats: () => Record<string, unknown>,
100
+ private readonly getProgress: () => number,
101
+ ) {
102
+ super()
103
+ this.MAX_ITERATIONS = 1_000_000
104
+ }
105
+
106
+ override getSolverName(): string {
107
+ return this.solverName
108
+ }
109
+
110
+ override _step(): void {
111
+ if (this.activeSubSolver) {
112
+ this.activeSubSolver.step()
113
+ if (this.activeSubSolver.failed) {
114
+ this.failedSubSolvers = [
115
+ ...(this.failedSubSolvers ?? []),
116
+ this.activeSubSolver,
117
+ ]
118
+ this.error = this.activeSubSolver.error
119
+ this.failed = true
120
+ this.activeSubSolver = null
121
+ return
122
+ }
123
+ if (this.activeSubSolver.solved) {
124
+ this.nextInput = this.activeSubSolver.getOutput()
125
+ this.activeSubSolver = null
126
+ }
127
+ this.stats = this.getStats()
128
+ return
129
+ }
130
+
131
+ const result = this.generator.next(this.nextInput)
132
+ this.nextInput = undefined
133
+ this.stats = this.getStats()
134
+ if (result.done) {
135
+ this.output = result.value
136
+ this.hasOutput = true
137
+ this.solved = true
138
+ return
139
+ }
140
+ const yielded = result.value as Partial<FanoutSubsolverRequest> | undefined
141
+ if (yielded?.type === "subsolver" && yielded.solver instanceof BaseSolver) {
142
+ this.activeSubSolver = yielded.solver
143
+ }
144
+ }
145
+
146
+ computeProgress(): number {
147
+ return this.getProgress()
148
+ }
149
+
150
+ override getConstructorParams(): [] {
151
+ return []
152
+ }
153
+
154
+ override getOutput(): T {
155
+ if (!this.solved || !this.hasOutput) {
156
+ throw new Error(`${this.solverName}: output requested before completion`)
157
+ }
158
+ return this.output as T
159
+ }
160
+
161
+ override visualize(): GraphicsObject {
162
+ return this.activeSubSolver?.visualize() ?? this.getVisualization()
163
+ }
164
+ }
165
+
166
+ interface ActiveFanoutOperation<T> {
167
+ solver: FanoutWorkSolver<T>
168
+ onSolved: (output: T) => void
169
+ }
170
+
82
171
  function resolvePositiveNumber(label: string, value: number): number {
83
172
  if (!Number.isFinite(value) || value <= 0) {
84
173
  throw new Error(
@@ -756,12 +845,12 @@ function getCandidateEscapeLayersForBus(params: {
756
845
  export class FanoutSolver extends BaseSolver {
757
846
  readonly preparedBuses: PreparedBus[]
758
847
  readonly attempts: FanoutAttemptSummary[] = []
759
- readonly layerAssignments: Array<Readonly<Record<string, string>>>
848
+ readonly layerAssignments: Array<Readonly<Record<string, string>>> = []
760
849
  readonly config: ResolvedFanoutConfig
761
850
  private readonly routingSrj: SimpleRouteJson
762
- private readonly escapeLayersByBusId: Readonly<
763
- Record<string, readonly string[]>
764
- >
851
+ private readonly escapeLayersByBusId: Record<string, readonly string[]> = {}
852
+ private readonly boundaryBuses: PreparedBus[]
853
+ private readonly fixedPlaneAssignments: Readonly<Record<string, string>>
765
854
  private readonly evaluatedAssignmentKeys = new Set<string>()
766
855
  private readonly queuedAssignmentKeys = new Set<string>()
767
856
  private readonly assignmentRepairDepthByKey = new Map<string, number>()
@@ -779,8 +868,14 @@ export class FanoutSolver extends BaseSolver {
779
868
  }
780
869
  >()
781
870
  private groupedBeamEvaluated = false
871
+ private routingInitialized = false
872
+ private nextCandidateLayerBusIndex = 0
782
873
  private nextAssignmentIndex = 0
783
874
  private nextGeneratedAssignmentIndex = 0
875
+ private activeOperation: ActiveFanoutOperation<unknown> | null = null
876
+ private inProgressPlans: FanoutRoutePlan[] = []
877
+ private activeRoutingVisualization: GraphicsObject | null = null
878
+ private activeAdaptiveVisualization: GraphicsObject | null = null
784
879
  private bestAttempt: AssignmentAttempt | null = null
785
880
  private lengthMatchingFailure: FanoutValidationIssue | null = null
786
881
  private endpointCompletion: CompleteOriginalEndpointsResult | null = null
@@ -857,56 +952,330 @@ export class FanoutSolver extends BaseSolver {
857
952
  )
858
953
  }
859
954
  }
860
- const boundaryBusIds = this.preparedBuses
861
- .filter((bus) => bus.termination.type === "boundary")
862
- .map((bus) => bus.busId)
863
- const fixedPlaneAssignments = Object.fromEntries(
955
+ this.boundaryBuses = this.preparedBuses.filter(
956
+ (bus) => bus.termination.type === "boundary",
957
+ )
958
+ this.fixedPlaneAssignments = Object.fromEntries(
864
959
  this.preparedBuses.flatMap((bus) =>
865
960
  bus.termination.type === "plane"
866
961
  ? [[bus.busId, bus.termination.layer] as const]
867
962
  : [],
868
963
  ),
869
964
  )
870
- const escapeLayersByBusId = Object.fromEntries(
871
- this.preparedBuses.flatMap((bus) => {
872
- if (bus.termination.type === "plane") return []
873
- return [
874
- [
875
- bus.busId,
876
- getCandidateEscapeLayersForBus({
877
- bus,
878
- srj: this.routingSrj,
879
- config: this.config,
880
- staticClearanceCache: this.routeStaticClearanceCache,
881
- }),
882
- ] as const,
883
- ]
884
- }),
885
- )
886
- this.escapeLayersByBusId = escapeLayersByBusId
965
+ const workUnitsPerAssignment = this.preparedBuses.length * 3 + 8
966
+ const estimatedWorkUnitCount =
967
+ this.boundaryBuses.length +
968
+ 1 +
969
+ this.config.maxLayerCombinations * workUnitsPerAssignment +
970
+ this.preparedBuses.length * 2 +
971
+ 20
972
+ this.MAX_ITERATIONS = Math.max(10_000, estimatedWorkUnitCount)
973
+ }
974
+
975
+ override getSolverName(): string {
976
+ return "FanoutSolver"
977
+ }
978
+
979
+ private stepRoutingInitialization(): void {
980
+ const bus = this.boundaryBuses[this.nextCandidateLayerBusIndex]
981
+ if (bus) {
982
+ this.escapeLayersByBusId[bus.busId] = getCandidateEscapeLayersForBus({
983
+ bus,
984
+ srj: this.routingSrj,
985
+ config: this.config,
986
+ staticClearanceCache: this.routeStaticClearanceCache,
987
+ })
988
+ this.nextCandidateLayerBusIndex++
989
+ this.stats = {
990
+ phase: "discover-candidate-layers",
991
+ bus: bus.busId,
992
+ busIndex: this.nextCandidateLayerBusIndex,
993
+ busCount: this.boundaryBuses.length,
994
+ }
995
+ return
996
+ }
997
+
887
998
  const generatedAssignments = generateLayerAssignments({
888
- busIds: boundaryBusIds,
999
+ busIds: this.boundaryBuses.map((candidate) => candidate.busId),
889
1000
  layers: this.config.escapeLayers,
890
- layersByBusId: escapeLayersByBusId,
1001
+ layersByBusId: this.escapeLayersByBusId,
891
1002
  maxAssignments: this.config.maxLayerCombinations,
892
1003
  }).map((assignment) => ({
893
1004
  ...assignment,
894
- ...fixedPlaneAssignments,
1005
+ ...this.fixedPlaneAssignments,
895
1006
  }))
896
- this.layerAssignments = prioritizeLayerAssignment({
897
- initialAssignment: createInitialLayerAssignment({
898
- buses: this.preparedBuses,
899
- escapeLayers: this.config.escapeLayers,
900
- escapeLayersByBusId,
1007
+ this.layerAssignments.push(
1008
+ ...prioritizeLayerAssignment({
1009
+ initialAssignment: createInitialLayerAssignment({
1010
+ buses: this.preparedBuses,
1011
+ escapeLayers: this.config.escapeLayers,
1012
+ escapeLayersByBusId: this.escapeLayersByBusId,
1013
+ }),
1014
+ generatedAssignments,
1015
+ maxAssignments: this.config.maxLayerCombinations,
901
1016
  }),
902
- generatedAssignments,
903
- maxAssignments: this.config.maxLayerCombinations,
904
- })
905
- this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
1017
+ )
1018
+ this.routingInitialized = true
1019
+ this.stats = {
1020
+ phase: "prepare-layer-assignments",
1021
+ assignmentCount: this.layerAssignments.length,
1022
+ }
906
1023
  }
907
1024
 
908
- override getSolverName(): string {
909
- return "FanoutSolver"
1025
+ private *initializeRoutingSteps(): Generator<FanoutWorkYield, void, unknown> {
1026
+ while (!this.routingInitialized) {
1027
+ this.stepRoutingInitialization()
1028
+ if (!this.routingInitialized) yield
1029
+ }
1030
+ }
1031
+
1032
+ private setInProgressPlans(params: {
1033
+ phase: string
1034
+ plans: readonly FanoutRoutePlan[]
1035
+ strategy?: RoutingStrategy | "grouped-beam"
1036
+ unitIndex?: number
1037
+ unitCount?: number
1038
+ busId?: string
1039
+ }): void {
1040
+ this.inProgressPlans = [...params.plans]
1041
+ this.stats = {
1042
+ ...this.stats,
1043
+ phase: params.phase,
1044
+ ...(params.strategy ? { routingStrategy: params.strategy } : {}),
1045
+ ...(params.unitIndex !== undefined ? { workUnit: params.unitIndex } : {}),
1046
+ ...(params.unitCount !== undefined
1047
+ ? { workUnitCount: params.unitCount }
1048
+ : {}),
1049
+ ...(params.busId ? { bus: params.busId } : {}),
1050
+ routedConnections: `${params.plans.length}/${this.inputSrj.connections.length}`,
1051
+ }
1052
+ }
1053
+
1054
+ private visualizeCurrentState(): GraphicsObject {
1055
+ const visualizedSrj =
1056
+ this.endpointCompletion?.simpleRouteJson ??
1057
+ (!this.solved && !this.failed && this.inProgressPlans.length > 0
1058
+ ? buildOutputSimpleRouteJson({
1059
+ inputSrj: this.inputSrj,
1060
+ plans: this.inProgressPlans,
1061
+ layerNames: this.config.layerNames,
1062
+ })
1063
+ : undefined) ??
1064
+ this.bestAttempt?.outputSrj ??
1065
+ this.inputSrj
1066
+ return visualizeSimpleRouteJson(visualizedSrj)
1067
+ }
1068
+
1069
+ private visualizeWorkState(solverName: string): GraphicsObject {
1070
+ const base = this.visualizeCurrentState()
1071
+ const boundary =
1072
+ this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds
1073
+ const activeBusId =
1074
+ typeof this.stats.bus === "string" ? this.stats.bus : undefined
1075
+ const activeBus = activeBusId
1076
+ ? this.preparedBuses.find((bus) => bus.busId === activeBusId)
1077
+ : undefined
1078
+ const width = boundary.maxX - boundary.minX
1079
+ const height = boundary.maxY - boundary.minY
1080
+ const annotationSize = Math.max(Math.min(width, height) * 0.025, 0.25)
1081
+ const phase =
1082
+ typeof this.stats.phase === "string" ? this.stats.phase : "starting"
1083
+ const detail = [
1084
+ typeof this.stats.routeConnection === "string"
1085
+ ? `connection ${this.stats.routeConnection}`
1086
+ : undefined,
1087
+ typeof this.stats.searchBatch === "number"
1088
+ ? `batch ${this.stats.searchBatch}`
1089
+ : undefined,
1090
+ typeof this.stats.expandedStates === "number"
1091
+ ? `${this.stats.expandedStates.toLocaleString()} states`
1092
+ : undefined,
1093
+ ]
1094
+ .filter(Boolean)
1095
+ .join(" · ")
1096
+ const title = `${solverName}: ${phase}`
1097
+ return {
1098
+ ...mergeGraphics(base, {
1099
+ rects: [
1100
+ {
1101
+ center: {
1102
+ x: (boundary.minX + boundary.maxX) / 2,
1103
+ y: (boundary.minY + boundary.maxY) / 2,
1104
+ },
1105
+ width,
1106
+ height,
1107
+ fill: "rgba(0, 0, 0, 0)",
1108
+ stroke: "rgba(14, 165, 233, 0.8)",
1109
+ label: `${solverName} working boundary`,
1110
+ },
1111
+ ],
1112
+ circles: (activeBus?.connections ?? []).map((connection) => ({
1113
+ center: connection.sourcePoint,
1114
+ radius: Math.max(
1115
+ annotationSize,
1116
+ Math.min(
1117
+ connection.sourceObstacle.width,
1118
+ connection.sourceObstacle.height,
1119
+ ) * 0.6,
1120
+ ),
1121
+ fill: "rgba(250, 204, 21, 0.25)",
1122
+ stroke: "#f59e0b",
1123
+ label: `active bus ${activeBusId}: ${connection.connection.name}`,
1124
+ })),
1125
+ texts: [
1126
+ {
1127
+ x: boundary.minX,
1128
+ y: boundary.maxY + annotationSize * 2,
1129
+ text: `${solverName} · ${phase}${detail ? ` · ${detail}` : ""}`,
1130
+ color: "#0f172a",
1131
+ fontSize: annotationSize * 1.5,
1132
+ anchorSide: "bottom_left",
1133
+ },
1134
+ ],
1135
+ }),
1136
+ title,
1137
+ }
1138
+ }
1139
+
1140
+ private visualizeBoundaryRoutingState(): GraphicsObject {
1141
+ if (!this.activeRoutingVisualization) {
1142
+ return this.visualizeWorkState("BoundaryBusRoutingSolver")
1143
+ }
1144
+ return {
1145
+ ...mergeGraphics(
1146
+ this.visualizeCurrentState(),
1147
+ this.activeRoutingVisualization,
1148
+ ),
1149
+ title: this.activeRoutingVisualization.title,
1150
+ }
1151
+ }
1152
+
1153
+ private visualizeAdaptiveRoutingState(): GraphicsObject {
1154
+ if (this.activeAdaptiveVisualization) {
1155
+ return this.activeAdaptiveVisualization
1156
+ }
1157
+ const boundary =
1158
+ this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds
1159
+ const width = boundary.maxX - boundary.minX
1160
+ const height = boundary.maxY - boundary.minY
1161
+ const annotationSize = Math.max(Math.min(width, height) * 0.02, 0.2)
1162
+ return {
1163
+ title: "SingleLayerAdaptiveExitSolver: preparing flow grid",
1164
+ rects: [
1165
+ {
1166
+ center: {
1167
+ x: (boundary.minX + boundary.maxX) / 2,
1168
+ y: (boundary.minY + boundary.maxY) / 2,
1169
+ },
1170
+ width,
1171
+ height,
1172
+ fill: "rgba(0, 0, 0, 0)",
1173
+ stroke: "rgba(14, 165, 233, 0.9)",
1174
+ label: "adaptive flow grid boundary",
1175
+ },
1176
+ ],
1177
+ points: this.preparedBuses.flatMap((bus) =>
1178
+ bus.connections.map((connection) => ({
1179
+ ...connection.sourcePoint,
1180
+ color: "#f97316",
1181
+ label: "adaptive route source",
1182
+ })),
1183
+ ),
1184
+ texts: [
1185
+ {
1186
+ x: boundary.minX,
1187
+ y: boundary.maxY + annotationSize * 2,
1188
+ text: "preparing adaptive flow grid",
1189
+ color: "#0f172a",
1190
+ fontSize: annotationSize * 1.5,
1191
+ anchorSide: "bottom_left",
1192
+ },
1193
+ ],
1194
+ }
1195
+ }
1196
+
1197
+ private startOperation<T>(params: {
1198
+ name: string
1199
+ generator: Generator<unknown, T, unknown>
1200
+ onSolved: (output: T) => void
1201
+ getProgress?: () => number
1202
+ }): void {
1203
+ const solver = this.createWorkSolver(
1204
+ params.name,
1205
+ params.generator,
1206
+ params.getProgress,
1207
+ )
1208
+ this.activeOperation = {
1209
+ solver,
1210
+ onSolved: params.onSolved,
1211
+ } as ActiveFanoutOperation<unknown>
1212
+ this.activeSubSolver = solver
1213
+ }
1214
+
1215
+ private createWorkSolver<T>(
1216
+ name: string,
1217
+ generator: Generator<unknown, T, unknown>,
1218
+ getProgress?: () => number,
1219
+ getVisualization?: () => GraphicsObject,
1220
+ ): FanoutWorkSolver<T> {
1221
+ return new FanoutWorkSolver(
1222
+ name,
1223
+ generator,
1224
+ getVisualization ?? (() => this.visualizeWorkState(name)),
1225
+ () => ({ ...this.stats }),
1226
+ getProgress ?? (() => 0),
1227
+ )
1228
+ }
1229
+
1230
+ private *routeBusAlternativesWorkSteps(
1231
+ params: Parameters<typeof routeBusAlternativesSteps>[0],
1232
+ maximumAlternatives: number,
1233
+ ): Generator<FanoutWorkYield, FanoutRoutePlan[][], unknown> {
1234
+ const steps = routeBusAlternativesSteps(params, maximumAlternatives, true)
1235
+ let result = steps.next()
1236
+ while (!result.done) {
1237
+ const { winding } = result.value
1238
+ if (winding.visualization) {
1239
+ this.activeRoutingVisualization = winding.visualization
1240
+ }
1241
+ this.stats = {
1242
+ ...this.stats,
1243
+ phase: "route-boundary-bus-connection",
1244
+ bus: result.value.busId,
1245
+ targetLayer: result.value.targetLayer,
1246
+ routeOrderAttempt: winding.routeOrderAttempt,
1247
+ routeConnection: `${winding.connectionIndex + 1}/${winding.connectionCount}`,
1248
+ connection: winding.connectionName,
1249
+ searchBatch: winding.searchBatch,
1250
+ expandedStates: winding.expandedStateCount,
1251
+ connectionComplete: winding.connectionComplete,
1252
+ }
1253
+ yield
1254
+ result = steps.next()
1255
+ }
1256
+ return result.value
1257
+ }
1258
+
1259
+ private stepActiveOperation(): void {
1260
+ const operation = this.activeOperation
1261
+ if (!operation) return
1262
+ operation.solver.step()
1263
+ if (operation.solver.failed) {
1264
+ this.failedSubSolvers = [
1265
+ ...(this.failedSubSolvers ?? []),
1266
+ operation.solver,
1267
+ ]
1268
+ this.error = operation.solver.error
1269
+ this.failed = true
1270
+ this.activeOperation = null
1271
+ this.activeSubSolver = null
1272
+ return
1273
+ }
1274
+ if (!operation.solver.solved) return
1275
+ const output = operation.solver.getOutput()
1276
+ this.activeOperation = null
1277
+ this.activeSubSolver = null
1278
+ operation.onSolved(output)
910
1279
  }
911
1280
 
912
1281
  private completeBestAttemptEndpoints(): void {
@@ -983,10 +1352,10 @@ export class FanoutSolver extends BaseSolver {
983
1352
  * dogbones. This is intentionally bounded independently of the number of
984
1353
  * plane drops so dense power fields cannot explode the general beam search.
985
1354
  */
986
- private routeDenseThroughAllMixedTerminations(params: {
1355
+ private *routeDenseThroughAllMixedTerminationSteps(params: {
987
1356
  busLayerAssignments: Readonly<Record<string, string>>
988
1357
  busesInRoutingOrder: readonly PreparedBus[]
989
- }): MixedTerminationState | null {
1358
+ }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
990
1359
  if (this.config.allowBlindAndBuriedVias) return null
991
1360
 
992
1361
  const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
@@ -1276,6 +1645,16 @@ export class FanoutSolver extends BaseSolver {
1276
1645
  preferBoundaryOutwardByBusId,
1277
1646
  canShareCopper,
1278
1647
  })
1648
+ let denseWorkUnitIndex = 1
1649
+ const denseWorkUnitCount = boundaryBuses.length + planeBuses.length + 3
1650
+ this.setInProgressPlans({
1651
+ phase: "reserve-dense-via-sites",
1652
+ plans: [],
1653
+ strategy: "default",
1654
+ unitIndex: denseWorkUnitIndex,
1655
+ unitCount: denseWorkUnitCount,
1656
+ })
1657
+ yield
1279
1658
  if (seedViaPoints) {
1280
1659
  const denseBoundaryBusesInRoutingOrder = [
1281
1660
  ...leadingWideSingletonBuses,
@@ -1321,7 +1700,10 @@ export class FanoutSolver extends BaseSolver {
1321
1700
  })
1322
1701
  })
1323
1702
  }
1324
- const routeMatchedBoundaryBus = (bus: PreparedBus): boolean => {
1703
+ const routeMatchedBoundaryBusSteps = function* (
1704
+ this: FanoutSolver,
1705
+ bus: PreparedBus,
1706
+ ): Generator<FanoutWorkYield, boolean, unknown> {
1325
1707
  const targetLayer = params.busLayerAssignments[bus.busId]
1326
1708
  if (!targetLayer) {
1327
1709
  return false
@@ -1345,7 +1727,23 @@ export class FanoutSolver extends BaseSolver {
1345
1727
  viaMinimalOnly: true,
1346
1728
  cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
1347
1729
  } as const
1348
- let busPlans = routeBusAlternatives(routeParams, 1)[0]
1730
+ const routeAlternatives = function* (
1731
+ this: FanoutSolver,
1732
+ maximumAlternatives: number,
1733
+ ): Generator<FanoutWorkYield, FanoutRoutePlan[][], unknown> {
1734
+ this.activeRoutingVisualization = null
1735
+ const solver = this.createWorkSolver(
1736
+ "BoundaryBusRoutingSolver",
1737
+ this.routeBusAlternativesWorkSteps(
1738
+ routeParams,
1739
+ maximumAlternatives,
1740
+ ),
1741
+ undefined,
1742
+ () => this.visualizeBoundaryRoutingState(),
1743
+ )
1744
+ return (yield { type: "subsolver", solver }) as FanoutRoutePlan[][]
1745
+ }.bind(this)
1746
+ let busPlans = (yield* routeAlternatives(1))[0]
1349
1747
  if (busPlans && bus.maxLengthSkew !== undefined) {
1350
1748
  const lengths = busPlans.map((plan) => plan.length)
1351
1749
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -1360,7 +1758,7 @@ export class FanoutSolver extends BaseSolver {
1360
1758
  // skewed that compact meanders are unlikely to absorb the deficit.
1361
1759
  // This keeps already-near-matched buses on the single-attempt path.
1362
1760
  if (needsRouteDiversity) {
1363
- busPlans = routeBusAlternatives(routeParams, 3).toSorted(
1761
+ busPlans = (yield* routeAlternatives(3)).toSorted(
1364
1762
  (first, second) => {
1365
1763
  const firstLengths = first.map((plan) => plan.length)
1366
1764
  const secondLengths = second.map((plan) => plan.length)
@@ -1378,11 +1776,22 @@ export class FanoutSolver extends BaseSolver {
1378
1776
  }
1379
1777
  matchedPlans.push(...busPlans)
1380
1778
  return true
1381
- }
1779
+ }.bind(this)
1382
1780
 
1383
1781
  const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0]!
1384
1782
  const routedBoundaryBuses: PreparedBus[] = []
1385
- if (routeMatchedBoundaryBus(firstBoundaryBus)) {
1783
+ const firstBoundaryBusRouted =
1784
+ yield* routeMatchedBoundaryBusSteps(firstBoundaryBus)
1785
+ this.setInProgressPlans({
1786
+ phase: "route-dense-boundary-buses",
1787
+ plans: matchedPlans,
1788
+ strategy: "default",
1789
+ unitIndex: ++denseWorkUnitIndex,
1790
+ unitCount: denseWorkUnitCount,
1791
+ busId: firstBoundaryBus.busId,
1792
+ })
1793
+ yield
1794
+ if (firstBoundaryBusRouted) {
1386
1795
  routedBoundaryBuses.push(firstBoundaryBus)
1387
1796
  } else {
1388
1797
  matchedRoutingSucceeded = false
@@ -1428,7 +1837,27 @@ export class FanoutSolver extends BaseSolver {
1428
1837
  canShareCopper,
1429
1838
  },
1430
1839
  )
1431
- if (!extendedViaPoints) continue
1840
+ if (!extendedViaPoints) {
1841
+ this.setInProgressPlans({
1842
+ phase: "reserve-next-dense-boundary-bus",
1843
+ plans: matchedPlans,
1844
+ strategy: "default",
1845
+ unitIndex: denseWorkUnitIndex,
1846
+ unitCount: denseWorkUnitCount,
1847
+ busId: candidateBus.busId,
1848
+ })
1849
+ yield
1850
+ continue
1851
+ }
1852
+ this.setInProgressPlans({
1853
+ phase: "reserve-next-dense-boundary-bus",
1854
+ plans: matchedPlans,
1855
+ strategy: "default",
1856
+ unitIndex: denseWorkUnitIndex,
1857
+ unitCount: denseWorkUnitCount,
1858
+ busId: candidateBus.busId,
1859
+ })
1860
+ yield
1432
1861
  const previousFixedViaPoints = fixedViaPointsByConnectionIndex
1433
1862
  const previousPlanCount = matchedPlans.length
1434
1863
  const laterBuses = remainingBoundaryBuses.filter(
@@ -1499,7 +1928,7 @@ export class FanoutSolver extends BaseSolver {
1499
1928
  }
1500
1929
  }
1501
1930
  fixedViaPointsByConnectionIndex = candidateFixedViaPoints
1502
- if (routeMatchedBoundaryBus(candidateBus)) {
1931
+ if (yield* routeMatchedBoundaryBusSteps(candidateBus)) {
1503
1932
  const candidateLeavesAFeasibleExtension =
1504
1933
  laterBuses.length === 0 ||
1505
1934
  laterBuses.some((laterBus) => {
@@ -1536,11 +1965,29 @@ export class FanoutSolver extends BaseSolver {
1536
1965
  if (candidateLeavesAFeasibleExtension) {
1537
1966
  selectedBusIndex = candidateIndex
1538
1967
  routedBoundaryBuses.push(candidateBus)
1968
+ this.setInProgressPlans({
1969
+ phase: "route-dense-boundary-buses",
1970
+ plans: matchedPlans,
1971
+ strategy: "default",
1972
+ unitIndex: ++denseWorkUnitIndex,
1973
+ unitCount: denseWorkUnitCount,
1974
+ busId: candidateBus.busId,
1975
+ })
1976
+ yield
1539
1977
  break
1540
1978
  }
1541
1979
  matchedPlans.splice(previousPlanCount)
1542
1980
  }
1543
1981
  fixedViaPointsByConnectionIndex = previousFixedViaPoints
1982
+ this.setInProgressPlans({
1983
+ phase: "retry-dense-boundary-bus",
1984
+ plans: matchedPlans,
1985
+ strategy: "default",
1986
+ unitIndex: denseWorkUnitIndex,
1987
+ unitCount: denseWorkUnitCount,
1988
+ busId: candidateBus.busId,
1989
+ })
1990
+ yield
1544
1991
  }
1545
1992
  if (selectedBusIndex < 0) {
1546
1993
  matchedRoutingSucceeded = false
@@ -1598,6 +2045,14 @@ export class FanoutSolver extends BaseSolver {
1598
2045
  return true
1599
2046
  },
1600
2047
  })
2048
+ this.setInProgressPlans({
2049
+ phase: "match-dense-boundary-lengths",
2050
+ plans: matchedLengthResult.plans ?? matchedPlans,
2051
+ strategy: "default",
2052
+ unitIndex: ++denseWorkUnitIndex,
2053
+ unitCount: denseWorkUnitCount,
2054
+ })
2055
+ yield
1601
2056
  if (matchedLengthResult.plans) {
1602
2057
  matchedPlans = matchedLengthResult.plans
1603
2058
  const rematchedViaPoints =
@@ -1610,6 +2065,14 @@ export class FanoutSolver extends BaseSolver {
1610
2065
  } else {
1611
2066
  matchedRoutingSucceeded = false
1612
2067
  }
2068
+ this.setInProgressPlans({
2069
+ phase: "rematch-dense-via-sites",
2070
+ plans: matchedPlans,
2071
+ strategy: "default",
2072
+ unitIndex: denseWorkUnitIndex,
2073
+ unitCount: denseWorkUnitCount,
2074
+ })
2075
+ yield
1613
2076
  }
1614
2077
  if (matchedRoutingSucceeded) {
1615
2078
  for (const bus of planeBuses) {
@@ -1637,6 +2100,15 @@ export class FanoutSolver extends BaseSolver {
1637
2100
  break
1638
2101
  }
1639
2102
  matchedPlans.push(...busPlans)
2103
+ this.setInProgressPlans({
2104
+ phase: "route-dense-plane-buses",
2105
+ plans: matchedPlans,
2106
+ strategy: "default",
2107
+ unitIndex: ++denseWorkUnitIndex,
2108
+ unitCount: denseWorkUnitCount,
2109
+ busId: bus.busId,
2110
+ })
2111
+ yield
1640
2112
  }
1641
2113
  }
1642
2114
  if (
@@ -2003,11 +2475,11 @@ export class FanoutSolver extends BaseSolver {
2003
2475
  return bestState
2004
2476
  }
2005
2477
 
2006
- private evaluateAssignmentWithStrategy(
2478
+ private *evaluateAssignmentWithStrategySteps(
2007
2479
  assignmentIndex: number,
2008
2480
  busLayerAssignments: Readonly<Record<string, string>>,
2009
2481
  routingStrategy: RoutingStrategy,
2010
- ): EvaluatedAssignment {
2482
+ ): Generator<FanoutWorkYield, EvaluatedAssignment, unknown> {
2011
2483
  let plans: AssignmentAttempt["plans"] = []
2012
2484
  let failedBusIds: string[] = []
2013
2485
  let blockingBusCounts = new Map<string, number>()
@@ -2026,21 +2498,53 @@ export class FanoutSolver extends BaseSolver {
2026
2498
  clearance: this.config.clearance,
2027
2499
  borderDistribution: this.config.borderDistribution,
2028
2500
  }
2029
- const singleLayerPlans =
2030
- routeSingleLayerWithPushAndShove(singleLayerParams) ??
2031
- (this.config.singleLayerAdaptiveExits
2032
- ? routeSingleLayerWithAdaptiveExits({
2033
- ...singleLayerParams,
2034
- availableBoundaryRegions: resolveAvailableBoundaryRegions(
2035
- this.options.availableCornersAndSides,
2036
- ),
2037
- })
2038
- : null)
2501
+ let singleLayerPlans = routeSingleLayerWithPushAndShove(singleLayerParams)
2502
+ if (!singleLayerPlans && this.config.singleLayerAdaptiveExits) {
2503
+ this.setInProgressPlans({
2504
+ phase: "prepare-single-layer-adaptive-exits",
2505
+ plans,
2506
+ strategy: routingStrategy,
2507
+ })
2508
+ yield
2509
+ this.setInProgressPlans({
2510
+ phase: "route-single-layer-adaptive-exits",
2511
+ plans,
2512
+ strategy: routingStrategy,
2513
+ })
2514
+ this.activeAdaptiveVisualization = null
2515
+ const adaptiveSolver = this.createWorkSolver(
2516
+ "SingleLayerAdaptiveExitSolver",
2517
+ routeSingleLayerWithAdaptiveExitsSteps({
2518
+ ...singleLayerParams,
2519
+ availableBoundaryRegions: resolveAvailableBoundaryRegions(
2520
+ this.options.availableCornersAndSides,
2521
+ ),
2522
+ onProgress: (visualization, adaptiveStats) => {
2523
+ this.activeAdaptiveVisualization = visualization
2524
+ this.stats = { ...this.stats, ...adaptiveStats }
2525
+ },
2526
+ }),
2527
+ undefined,
2528
+ () => this.visualizeAdaptiveRoutingState(),
2529
+ )
2530
+ singleLayerPlans = (yield {
2531
+ type: "subsolver",
2532
+ solver: adaptiveSolver,
2533
+ }) as FanoutRoutePlan[] | null
2534
+ }
2039
2535
  if (singleLayerPlans) {
2040
2536
  plans.push(...singleLayerPlans)
2041
2537
  } else {
2042
2538
  failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId))
2043
2539
  }
2540
+ this.setInProgressPlans({
2541
+ phase: "route-single-layer",
2542
+ plans,
2543
+ strategy: routingStrategy,
2544
+ unitIndex: 1,
2545
+ unitCount: 1,
2546
+ })
2547
+ yield
2044
2548
  }
2045
2549
  const busesInRoutingOrder = [...this.preparedBuses].sort((a, b) => {
2046
2550
  const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
@@ -2076,23 +2580,45 @@ export class FanoutSolver extends BaseSolver {
2076
2580
  )
2077
2581
  })
2078
2582
 
2079
- const mixedTerminationState =
2080
- !useSingleLayerPushAndShove && routingStrategy === "default"
2081
- ? this.routeDenseThroughAllMixedTerminations({
2082
- busLayerAssignments,
2083
- busesInRoutingOrder,
2084
- })
2085
- : null
2583
+ let mixedTerminationState: MixedTerminationState | null = null
2584
+ if (!useSingleLayerPushAndShove && routingStrategy === "default") {
2585
+ const denseSolver = this.createWorkSolver(
2586
+ "DenseMixedTerminationSolver",
2587
+ this.routeDenseThroughAllMixedTerminationSteps({
2588
+ busLayerAssignments,
2589
+ busesInRoutingOrder,
2590
+ }),
2591
+ () => {
2592
+ const workUnit = Number(this.stats.workUnit ?? 0)
2593
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
2594
+ return workUnitCount > 0 ? workUnit / workUnitCount : 0
2595
+ },
2596
+ )
2597
+ mixedTerminationState = (yield {
2598
+ type: "subsolver",
2599
+ solver: denseSolver,
2600
+ }) as MixedTerminationState | null
2601
+ }
2086
2602
 
2087
2603
  if (mixedTerminationState) {
2088
2604
  plans = mixedTerminationState.plans
2089
2605
  failedBusIds = mixedTerminationState.failedBusIds
2606
+ this.setInProgressPlans({
2607
+ phase: "route-dense-mixed-terminations",
2608
+ plans,
2609
+ strategy: routingStrategy,
2610
+ unitIndex: this.preparedBuses.length,
2611
+ unitCount: this.preparedBuses.length,
2612
+ })
2613
+ yield
2090
2614
  }
2091
2615
 
2092
2616
  let routingPrefixKey = `${routingStrategy}|`
2617
+ let routedBusIndex = 0
2093
2618
  for (const bus of useSingleLayerPushAndShove || mixedTerminationState
2094
2619
  ? []
2095
2620
  : busesInRoutingOrder) {
2621
+ routedBusIndex++
2096
2622
  const targetLayer = busLayerAssignments[bus.busId]
2097
2623
  if (!targetLayer) {
2098
2624
  throw new Error(
@@ -2110,6 +2636,15 @@ export class FanoutSolver extends BaseSolver {
2110
2636
  plans = [...cachedPrefix.plans]
2111
2637
  failedBusIds = [...cachedPrefix.failedBusIds]
2112
2638
  blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
2639
+ this.setInProgressPlans({
2640
+ phase: "route-assignment",
2641
+ plans,
2642
+ strategy: routingStrategy,
2643
+ unitIndex: routedBusIndex,
2644
+ unitCount: busesInRoutingOrder.length,
2645
+ busId: bus.busId,
2646
+ })
2647
+ yield
2113
2648
  continue
2114
2649
  }
2115
2650
  const currentBusBlockingCounts = new Map<string, number>()
@@ -2145,6 +2680,15 @@ export class FanoutSolver extends BaseSolver {
2145
2680
  failedBusIds: [...failedBusIds],
2146
2681
  blockingBusCounts: new Map(blockingBusCounts),
2147
2682
  })
2683
+ this.setInProgressPlans({
2684
+ phase: "route-assignment",
2685
+ plans,
2686
+ strategy: routingStrategy,
2687
+ unitIndex: routedBusIndex,
2688
+ unitCount: busesInRoutingOrder.length,
2689
+ busId: bus.busId,
2690
+ })
2691
+ yield
2148
2692
  }
2149
2693
 
2150
2694
  let validationIssues: FanoutAttemptSummary["validationIssues"]
@@ -2218,6 +2762,11 @@ export class FanoutSolver extends BaseSolver {
2218
2762
  score,
2219
2763
  ...(validationIssues ? { validationIssues } : {}),
2220
2764
  }
2765
+ this.setInProgressPlans({
2766
+ phase: "finalize-assignment-strategy",
2767
+ plans,
2768
+ strategy: routingStrategy,
2769
+ })
2221
2770
  return {
2222
2771
  summary,
2223
2772
  plans,
@@ -2228,11 +2777,11 @@ export class FanoutSolver extends BaseSolver {
2228
2777
  }
2229
2778
  }
2230
2779
 
2231
- private evaluateAssignment(
2780
+ private *evaluateAssignmentSteps(
2232
2781
  assignmentIndex: number,
2233
2782
  busLayerAssignments: Readonly<Record<string, string>>,
2234
- ): EvaluatedAssignment {
2235
- let bestAttempt = this.evaluateAssignmentWithStrategy(
2783
+ ): Generator<FanoutWorkYield, EvaluatedAssignment, unknown> {
2784
+ let bestAttempt = yield* this.evaluateAssignmentWithStrategySteps(
2236
2785
  assignmentIndex,
2237
2786
  busLayerAssignments,
2238
2787
  "default",
@@ -2246,7 +2795,7 @@ export class FanoutSolver extends BaseSolver {
2246
2795
  }
2247
2796
 
2248
2797
  for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
2249
- const attempt = this.evaluateAssignmentWithStrategy(
2798
+ const attempt = yield* this.evaluateAssignmentWithStrategySteps(
2250
2799
  assignmentIndex,
2251
2800
  busLayerAssignments,
2252
2801
  routingStrategy,
@@ -2274,10 +2823,10 @@ export class FanoutSolver extends BaseSolver {
2274
2823
  * power or signal lane cannot starve a later bus before the solver explores
2275
2824
  * an alternate layer/track combination.
2276
2825
  */
2277
- private evaluateGroupedBeam(
2826
+ private *evaluateGroupedBeamSteps(
2278
2827
  assignmentIndex: number,
2279
2828
  groupByDirection = false,
2280
- ): EvaluatedAssignment | null {
2829
+ ): Generator<FanoutWorkYield, EvaluatedAssignment | null, unknown> {
2281
2830
  if (this.config.escapeLayers.length < 2) return null
2282
2831
  if (this.preparedBuses.length > 56) return null
2283
2832
  const totalConnections = this.inputSrj.connections.length
@@ -2386,7 +2935,9 @@ export class FanoutSolver extends BaseSolver {
2386
2935
  )
2387
2936
  }
2388
2937
 
2938
+ let searchedBusIndex = 0
2389
2939
  for (const bus of busesInSearchOrder) {
2940
+ searchedBusIndex++
2390
2941
  const nextStates: GroupedBeamState[] = []
2391
2942
  for (const state of states) {
2392
2943
  const candidateLayers =
@@ -2474,6 +3025,15 @@ export class FanoutSolver extends BaseSolver {
2474
3025
  states.push(state)
2475
3026
  if (states.length >= beamWidth) break
2476
3027
  }
3028
+ this.setInProgressPlans({
3029
+ phase: "route-grouped-beam",
3030
+ plans: states[0]?.plans ?? [],
3031
+ strategy: "grouped-beam",
3032
+ unitIndex: searchedBusIndex,
3033
+ unitCount: busesInSearchOrder.length,
3034
+ busId: bus.busId,
3035
+ })
3036
+ yield
2477
3037
  }
2478
3038
 
2479
3039
  let bestState: GroupedBeamState | undefined
@@ -2493,9 +3053,15 @@ export class FanoutSolver extends BaseSolver {
2493
3053
  (bus) => bus.maxLengthSkew !== undefined,
2494
3054
  )
2495
3055
  for (const state of states) {
2496
- if (state.plans.length !== this.inputSrj.connections.length) continue
3056
+ if (state.plans.length !== this.inputSrj.connections.length) {
3057
+ yield
3058
+ continue
3059
+ }
2497
3060
  const lengthMatching = this.matchCompletePlanLengths(state.plans)
2498
- if (!lengthMatching.plans) continue
3061
+ if (!lengthMatching.plans) {
3062
+ yield
3063
+ continue
3064
+ }
2499
3065
  const lengthMatchedPlans = lengthMatching.plans
2500
3066
  const candidateOutput = buildOutputSimpleRouteJson({
2501
3067
  inputSrj: this.inputSrj,
@@ -2505,6 +3071,7 @@ export class FanoutSolver extends BaseSolver {
2505
3071
  if (
2506
3072
  !this.validateCompletePlans(lengthMatchedPlans, candidateOutput).valid
2507
3073
  ) {
3074
+ yield
2508
3075
  continue
2509
3076
  }
2510
3077
  const candidateState = { ...state, plans: lengthMatchedPlans }
@@ -2523,6 +3090,12 @@ export class FanoutSolver extends BaseSolver {
2523
3090
  bestAdditionalViaCount = candidateAdditionalViaCount
2524
3091
  }
2525
3092
  if (!hasLengthConstraints) break
3093
+ this.setInProgressPlans({
3094
+ phase: "validate-grouped-beam",
3095
+ plans: lengthMatchedPlans,
3096
+ strategy: "grouped-beam",
3097
+ })
3098
+ yield
2526
3099
  }
2527
3100
  if (!bestState || !outputSrj) return null
2528
3101
  const score = bestMatchedScore
@@ -2544,6 +3117,14 @@ export class FanoutSolver extends BaseSolver {
2544
3117
  }
2545
3118
  }
2546
3119
 
3120
+ private *evaluateGroupedBeamAlternativesSteps(
3121
+ assignmentIndex: number,
3122
+ ): Generator<FanoutWorkYield, EvaluatedAssignment | null, unknown> {
3123
+ const primaryAttempt = yield* this.evaluateGroupedBeamSteps(assignmentIndex)
3124
+ if (primaryAttempt) return primaryAttempt
3125
+ return yield* this.evaluateGroupedBeamSteps(assignmentIndex, true)
3126
+ }
3127
+
2547
3128
  private prioritizeFailedBusRepairs(
2548
3129
  assignment: Readonly<Record<string, string>>,
2549
3130
  failedBusIds: readonly string[],
@@ -2710,52 +3291,10 @@ export class FanoutSolver extends BaseSolver {
2710
3291
  return targetedRepairSearchFinished
2711
3292
  }
2712
3293
 
2713
- override _step(): void {
2714
- if (
2715
- this.nextAssignmentIndex > 0 &&
2716
- this.hasGloballyViaMinimalBestAttempt()
2717
- ) {
2718
- this.completeBestAttemptEndpoints()
2719
- this.solved = true
2720
- return
2721
- }
2722
- // Try the deterministic assignment and only its targeted repair queue
2723
- // before paying for the grouped beam. If the beam cannot solve, continue
2724
- // with the broader generated-assignment search below.
2725
- if (this.shouldEvaluateGroupedBeam()) {
2726
- this.groupedBeamEvaluated = true
2727
- let beamAttempt = this.evaluateGroupedBeam(-1)
2728
- if (!beamAttempt) {
2729
- beamAttempt = this.evaluateGroupedBeam(-1, true)
2730
- }
2731
- if (beamAttempt) {
2732
- this.attempts.push(beamAttempt.summary)
2733
- if (
2734
- !this.bestAttempt ||
2735
- this.isAttemptBetter(beamAttempt, this.bestAttempt)
2736
- ) {
2737
- this.bestAttempt = beamAttempt
2738
- }
2739
- const bestSummary = this.bestAttempt.summary
2740
- this.stats = {
2741
- assignment:
2742
- bestSummary.assignmentIndex < 0
2743
- ? 0
2744
- : bestSummary.assignmentIndex + 1,
2745
- assignmentCount: this.config.maxLayerCombinations,
2746
- routedBuses: `${bestSummary.routedBusCount}/${this.preparedBuses.length}`,
2747
- routedConnections: `${bestSummary.routedConnectionCount}/${this.inputSrj.connections.length}`,
2748
- failedBuses: "none",
2749
- bestScore: bestSummary.score,
2750
- }
2751
- if (
2752
- this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
2753
- ) {
2754
- this.completeBestAttemptEndpoints()
2755
- this.solved = true
2756
- return
2757
- }
2758
- }
3294
+ private commitGroupedBeamAttempt(
3295
+ beamAttempt: EvaluatedAssignment | null,
3296
+ ): void {
3297
+ if (!beamAttempt) {
2759
3298
  if (
2760
3299
  this.hasCompleteBestAttempt() &&
2761
3300
  this.bestAttempt &&
@@ -2763,10 +3302,75 @@ export class FanoutSolver extends BaseSolver {
2763
3302
  ) {
2764
3303
  this.completeBestAttemptEndpoints()
2765
3304
  this.solved = true
2766
- return
2767
3305
  }
3306
+ return
3307
+ }
3308
+ this.attempts.push(beamAttempt.summary)
3309
+ if (
3310
+ !this.bestAttempt ||
3311
+ this.isAttemptBetter(beamAttempt, this.bestAttempt)
3312
+ ) {
3313
+ this.bestAttempt = beamAttempt
3314
+ }
3315
+ const bestSummary = this.bestAttempt.summary
3316
+ this.stats = {
3317
+ phase: "complete-grouped-beam",
3318
+ assignment:
3319
+ bestSummary.assignmentIndex < 0 ? 0 : bestSummary.assignmentIndex + 1,
3320
+ assignmentCount: this.config.maxLayerCombinations,
3321
+ routedBuses: `${bestSummary.routedBusCount}/${this.preparedBuses.length}`,
3322
+ routedConnections: `${bestSummary.routedConnectionCount}/${this.inputSrj.connections.length}`,
3323
+ failedBuses: "none",
3324
+ bestScore: bestSummary.score,
3325
+ }
3326
+ if (this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0) {
3327
+ this.completeBestAttemptEndpoints()
3328
+ this.solved = true
2768
3329
  }
3330
+ }
2769
3331
 
3332
+ private commitAssignmentAttempt(
3333
+ assignment: Readonly<Record<string, string>>,
3334
+ attempt: EvaluatedAssignment,
3335
+ ): void {
3336
+ this.nextAssignmentIndex++
3337
+ this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
3338
+ if (
3339
+ !this.bestAttempt ||
3340
+ attempt.summary.routedConnectionCount >=
3341
+ this.bestAttempt.summary.routedConnectionCount
3342
+ ) {
3343
+ this.prioritizeFailedBusRepairs(
3344
+ assignment,
3345
+ attempt.summary.failedBusIds,
3346
+ attempt.blockingBusIds,
3347
+ )
3348
+ }
3349
+ this.attempts.push(attempt.summary)
3350
+ if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
3351
+ this.bestAttempt = attempt
3352
+ }
3353
+ this.stats = {
3354
+ phase: "complete-assignment",
3355
+ assignment: attempt.summary.assignmentIndex + 1,
3356
+ assignmentCount: this.config.maxLayerCombinations,
3357
+ routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
3358
+ routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
3359
+ failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
3360
+ bestScore: this.bestAttempt.summary.score,
3361
+ }
3362
+ if (
3363
+ this.groupedBeamEvaluated &&
3364
+ attempt.summary.routedConnectionCount ===
3365
+ this.inputSrj.connections.length &&
3366
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
3367
+ ) {
3368
+ this.completeBestAttemptEndpoints()
3369
+ this.solved = true
3370
+ }
3371
+ }
3372
+
3373
+ private getNextAssignment(): Readonly<Record<string, string>> | undefined {
2770
3374
  let assignment: Readonly<Record<string, string>> | undefined
2771
3375
  while (
2772
3376
  !assignment &&
@@ -2802,68 +3406,138 @@ export class FanoutSolver extends BaseSolver {
2802
3406
  if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
2803
3407
  assignment = candidate
2804
3408
  }
2805
- if (!assignment && !this.groupedBeamEvaluated) return
2806
- if (!assignment) {
2807
- if (this.hasCompleteBestAttempt()) {
2808
- this.completeBestAttemptEndpoints()
2809
- this.solved = true
2810
- } else {
2811
- this.failed = true
2812
- const validationMessage =
2813
- this.lengthMatchingFailure?.message ??
2814
- this.bestAttempt?.summary.validationIssues?.[0]?.message
2815
- this.error = validationMessage
2816
- ? `FanoutSolver: ${validationMessage}`
2817
- : this.bestAttempt
2818
- ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
2819
- : "FanoutSolver: no layer assignment could be evaluated"
2820
- }
3409
+ return assignment
3410
+ }
3411
+
3412
+ private finishWithoutAnotherAssignment(): void {
3413
+ if (this.hasCompleteBestAttempt()) {
3414
+ this.completeBestAttemptEndpoints()
3415
+ this.solved = true
2821
3416
  return
2822
3417
  }
3418
+ this.failed = true
3419
+ const validationMessage =
3420
+ this.lengthMatchingFailure?.message ??
3421
+ this.bestAttempt?.summary.validationIssues?.[0]?.message
3422
+ this.error = validationMessage
3423
+ ? `FanoutSolver: ${validationMessage}`
3424
+ : this.bestAttempt
3425
+ ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
3426
+ : "FanoutSolver: no layer assignment could be evaluated"
3427
+ }
2823
3428
 
2824
- const attempt = this.evaluateAssignment(
2825
- this.nextAssignmentIndex,
2826
- assignment,
2827
- )
2828
- this.nextAssignmentIndex++
2829
- this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
2830
- if (
2831
- !this.bestAttempt ||
2832
- attempt.summary.routedConnectionCount >=
2833
- this.bestAttempt.summary.routedConnectionCount
2834
- ) {
2835
- this.prioritizeFailedBusRepairs(
2836
- assignment,
2837
- attempt.summary.failedBusIds,
2838
- attempt.blockingBusIds,
2839
- )
2840
- }
2841
- this.attempts.push(attempt.summary)
2842
- if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
2843
- this.bestAttempt = attempt
3429
+ override _step(): void {
3430
+ if (this.activeOperation) {
3431
+ this.stepActiveOperation()
3432
+ return
2844
3433
  }
2845
- this.stats = {
2846
- assignment: attempt.summary.assignmentIndex + 1,
2847
- assignmentCount: this.config.maxLayerCombinations,
2848
- routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
2849
- routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
2850
- failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
2851
- bestScore: this.bestAttempt.summary.score,
3434
+
3435
+ if (!this.routingInitialized) {
3436
+ this.startOperation({
3437
+ name: "FanoutCandidateLayerSolver",
3438
+ generator: this.initializeRoutingSteps(),
3439
+ onSolved: () => {},
3440
+ getProgress: () =>
3441
+ this.nextCandidateLayerBusIndex /
3442
+ Math.max(1, this.boundaryBuses.length + 1),
3443
+ })
3444
+ return
2852
3445
  }
3446
+
2853
3447
  if (
2854
- this.groupedBeamEvaluated &&
2855
- attempt.summary.routedConnectionCount ===
2856
- this.inputSrj.connections.length &&
2857
- this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
3448
+ this.nextAssignmentIndex > 0 &&
3449
+ this.hasGloballyViaMinimalBestAttempt()
2858
3450
  ) {
2859
3451
  this.completeBestAttemptEndpoints()
2860
3452
  this.solved = true
3453
+ return
3454
+ }
3455
+ // Try the deterministic assignment and only its targeted repair queue
3456
+ // before paying for the grouped beam. If the beam cannot solve, continue
3457
+ // with the broader generated-assignment search below.
3458
+ if (this.shouldEvaluateGroupedBeam()) {
3459
+ this.groupedBeamEvaluated = true
3460
+ this.startOperation({
3461
+ name: "FanoutGroupedBeamSolver",
3462
+ generator: this.evaluateGroupedBeamAlternativesSteps(-1),
3463
+ onSolved: (attempt) => this.commitGroupedBeamAttempt(attempt),
3464
+ getProgress: () => {
3465
+ const workUnit = Number(this.stats.workUnit ?? 0)
3466
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
3467
+ return workUnitCount > 0 ? workUnit / workUnitCount : 0
3468
+ },
3469
+ })
3470
+ this.stats = { ...this.stats, phase: "prepare-grouped-beam" }
3471
+ return
3472
+ }
3473
+
3474
+ const assignment = this.getNextAssignment()
3475
+ if (!assignment && !this.groupedBeamEvaluated) return
3476
+ if (!assignment) {
3477
+ this.finishWithoutAnotherAssignment()
3478
+ return
3479
+ }
3480
+
3481
+ this.startOperation({
3482
+ name: "FanoutAssignmentSolver",
3483
+ generator: this.evaluateAssignmentSteps(
3484
+ this.nextAssignmentIndex,
3485
+ assignment,
3486
+ ),
3487
+ onSolved: (attempt) => this.commitAssignmentAttempt(assignment, attempt),
3488
+ getProgress: () => {
3489
+ const strategyIndex =
3490
+ this.stats.routingStrategy === "group-by-layer"
3491
+ ? 1
3492
+ : this.stats.routingStrategy === "deep-first"
3493
+ ? 2
3494
+ : 0
3495
+ const workUnit = Number(this.stats.workUnit ?? 0)
3496
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
3497
+ const strategyFraction =
3498
+ workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0
3499
+ return (strategyIndex + strategyFraction) / 3
3500
+ },
3501
+ })
3502
+ this.stats = {
3503
+ ...this.stats,
3504
+ phase: "prepare-assignment",
3505
+ assignment: this.nextAssignmentIndex + 1,
3506
+ assignmentCount: this.config.maxLayerCombinations,
3507
+ routedConnections: `0/${this.inputSrj.connections.length}`,
2861
3508
  }
2862
3509
  }
2863
3510
 
2864
3511
  computeProgress(): number {
2865
3512
  if (this.solved || this.failed) return 1
2866
- return this.nextAssignmentIndex / this.config.maxLayerCombinations
3513
+ if (!this.routingInitialized) {
3514
+ return (
3515
+ 0.05 *
3516
+ (this.nextCandidateLayerBusIndex /
3517
+ Math.max(1, this.boundaryBuses.length + 1))
3518
+ )
3519
+ }
3520
+ let activeAssignmentFraction = 0
3521
+ if (this.activeSubSolver?.getSolverName() === "FanoutAssignmentSolver") {
3522
+ const strategyIndex =
3523
+ this.stats.routingStrategy === "group-by-layer"
3524
+ ? 1
3525
+ : this.stats.routingStrategy === "deep-first"
3526
+ ? 2
3527
+ : 0
3528
+ const workUnit = Number(this.stats.workUnit ?? 0)
3529
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
3530
+ const strategyFraction =
3531
+ workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0
3532
+ activeAssignmentFraction = (strategyIndex + strategyFraction) / 3
3533
+ }
3534
+ return Math.min(
3535
+ 0.99,
3536
+ 0.05 +
3537
+ 0.95 *
3538
+ ((this.nextAssignmentIndex + activeAssignmentFraction) /
3539
+ this.config.maxLayerCombinations),
3540
+ )
2867
3541
  }
2868
3542
 
2869
3543
  override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
@@ -2937,10 +3611,6 @@ export class FanoutSolver extends BaseSolver {
2937
3611
  }
2938
3612
 
2939
3613
  override visualize(): GraphicsObject {
2940
- const visualizedSrj =
2941
- this.endpointCompletion?.simpleRouteJson ??
2942
- this.bestAttempt?.outputSrj ??
2943
- this.inputSrj
2944
- return visualizeSimpleRouteJson(visualizedSrj)
3614
+ return this.activeSubSolver?.visualize() ?? this.visualizeCurrentState()
2945
3615
  }
2946
3616
  }