@tscircuit/fanout-solver 0.0.48 → 0.0.50

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 { type GraphicsObject, mergeGraphics } 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"
@@ -25,11 +25,13 @@ import {
25
25
  } from "./prepare-buses"
26
26
  import {
27
27
  fanoutPlansAreClear,
28
+ fanoutPlansAreMutuallyClear,
28
29
  type RouteBusStaticClearanceCache,
29
30
  routeBus,
30
31
  routeBusAlternatives,
32
+ routeBusAlternativesSteps,
31
33
  } from "./route-bus"
32
- import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
34
+ import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
33
35
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
34
36
  import type {
35
37
  AssignmentAttempt,
@@ -54,6 +56,8 @@ interface ResolvedFanoutConfig {
54
56
  compactBusTracks: boolean
55
57
  allowBlindAndBuriedVias: boolean
56
58
  allowSameNetMerges: boolean
59
+ densePlaneReservationBusIds: readonly string[]
60
+ denseUnrestrictedPlaneRoutingBusIds: readonly string[]
57
61
  singleLayerPushAndShove: boolean
58
62
  singleLayerAdaptiveExits: boolean
59
63
  borderDistribution: FanoutBorderDistribution
@@ -79,6 +83,94 @@ interface MixedTerminationState {
79
83
 
80
84
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
81
85
 
86
+ interface FanoutSubsolverRequest {
87
+ type: "subsolver"
88
+ solver: BaseSolver
89
+ }
90
+
91
+ type FanoutWorkYield = undefined | FanoutSubsolverRequest
92
+
93
+ class FanoutWorkSolver<T> extends BaseSolver {
94
+ private output: T | undefined
95
+ private hasOutput = false
96
+ private nextInput: unknown
97
+
98
+ constructor(
99
+ private readonly solverName: string,
100
+ private readonly generator: Generator<unknown, T, unknown>,
101
+ private readonly getVisualization: () => GraphicsObject,
102
+ private readonly getStats: () => Record<string, unknown>,
103
+ private readonly getProgress: () => number,
104
+ ) {
105
+ super()
106
+ this.MAX_ITERATIONS = 1_000_000
107
+ }
108
+
109
+ override getSolverName(): string {
110
+ return this.solverName
111
+ }
112
+
113
+ override _step(): void {
114
+ if (this.activeSubSolver) {
115
+ this.activeSubSolver.step()
116
+ if (this.activeSubSolver.failed) {
117
+ this.failedSubSolvers = [
118
+ ...(this.failedSubSolvers ?? []),
119
+ this.activeSubSolver,
120
+ ]
121
+ this.error = this.activeSubSolver.error
122
+ this.failed = true
123
+ this.activeSubSolver = null
124
+ return
125
+ }
126
+ if (this.activeSubSolver.solved) {
127
+ this.nextInput = this.activeSubSolver.getOutput()
128
+ this.activeSubSolver = null
129
+ }
130
+ this.stats = this.getStats()
131
+ return
132
+ }
133
+
134
+ const result = this.generator.next(this.nextInput)
135
+ this.nextInput = undefined
136
+ this.stats = this.getStats()
137
+ if (result.done) {
138
+ this.output = result.value
139
+ this.hasOutput = true
140
+ this.solved = true
141
+ return
142
+ }
143
+ const yielded = result.value as Partial<FanoutSubsolverRequest> | undefined
144
+ if (yielded?.type === "subsolver" && yielded.solver instanceof BaseSolver) {
145
+ this.activeSubSolver = yielded.solver
146
+ }
147
+ }
148
+
149
+ computeProgress(): number {
150
+ return this.getProgress()
151
+ }
152
+
153
+ override getConstructorParams(): [] {
154
+ return []
155
+ }
156
+
157
+ override getOutput(): T {
158
+ if (!this.solved || !this.hasOutput) {
159
+ throw new Error(`${this.solverName}: output requested before completion`)
160
+ }
161
+ return this.output as T
162
+ }
163
+
164
+ override visualize(): GraphicsObject {
165
+ return this.activeSubSolver?.visualize() ?? this.getVisualization()
166
+ }
167
+ }
168
+
169
+ interface ActiveFanoutOperation<T> {
170
+ solver: FanoutWorkSolver<T>
171
+ onSolved: (output: T) => void
172
+ }
173
+
82
174
  function resolvePositiveNumber(label: string, value: number): number {
83
175
  if (!Number.isFinite(value) || value <= 0) {
84
176
  throw new Error(
@@ -151,6 +243,9 @@ function resolveConfig(
151
243
  compactBusTracks: options.compactBusTracks ?? false,
152
244
  allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
153
245
  allowSameNetMerges: options.allowSameNetMerges ?? false,
246
+ densePlaneReservationBusIds: options.densePlaneReservationBusIds ?? [],
247
+ denseUnrestrictedPlaneRoutingBusIds:
248
+ options.denseUnrestrictedPlaneRoutingBusIds ?? [],
154
249
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
155
250
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
156
251
  borderDistribution,
@@ -357,8 +452,14 @@ function createInitialLayerAssignment(params: {
357
452
  buses: PreparedBus[]
358
453
  escapeLayers: string[]
359
454
  escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
455
+ preferOrderedCoordinatedWindingLayers: boolean
360
456
  }): Readonly<Record<string, string>> {
361
- const { buses, escapeLayers, escapeLayersByBusId } = params
457
+ const {
458
+ buses,
459
+ escapeLayers,
460
+ escapeLayersByBusId,
461
+ preferOrderedCoordinatedWindingLayers,
462
+ } = params
362
463
  const assignment: Record<string, string> = {}
363
464
  const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
364
465
  let nextViaLayerIndex = 0
@@ -394,6 +495,16 @@ function createInitialLayerAssignment(params: {
394
495
  ) {
395
496
  assignment[bus.busId] = sourceLayer
396
497
  } else if (viaLayers.length > 0) {
498
+ if (
499
+ preferOrderedCoordinatedWindingLayers &&
500
+ busUsesCoordinatedWinding(bus)
501
+ ) {
502
+ // Coordinated winding treats allowedLayers as an ordered preference.
503
+ // A global round-robin index can otherwise skip a bus's first choice
504
+ // just because a previous bus had a different set of legal layers.
505
+ assignment[bus.busId] = viaLayers[0]!
506
+ continue
507
+ }
397
508
  const componentDirections = directionsByComponent.get(bus.componentId)!
398
509
  const hasOpposingDirection =
399
510
  (componentDirections.has("left") && componentDirections.has("right")) ||
@@ -756,12 +867,12 @@ function getCandidateEscapeLayersForBus(params: {
756
867
  export class FanoutSolver extends BaseSolver {
757
868
  readonly preparedBuses: PreparedBus[]
758
869
  readonly attempts: FanoutAttemptSummary[] = []
759
- readonly layerAssignments: Array<Readonly<Record<string, string>>>
870
+ readonly layerAssignments: Array<Readonly<Record<string, string>>> = []
760
871
  readonly config: ResolvedFanoutConfig
761
872
  private readonly routingSrj: SimpleRouteJson
762
- private readonly escapeLayersByBusId: Readonly<
763
- Record<string, readonly string[]>
764
- >
873
+ private readonly escapeLayersByBusId: Record<string, readonly string[]> = {}
874
+ private readonly boundaryBuses: PreparedBus[]
875
+ private readonly fixedPlaneAssignments: Readonly<Record<string, string>>
765
876
  private readonly evaluatedAssignmentKeys = new Set<string>()
766
877
  private readonly queuedAssignmentKeys = new Set<string>()
767
878
  private readonly assignmentRepairDepthByKey = new Map<string, number>()
@@ -779,8 +890,14 @@ export class FanoutSolver extends BaseSolver {
779
890
  }
780
891
  >()
781
892
  private groupedBeamEvaluated = false
893
+ private routingInitialized = false
894
+ private nextCandidateLayerBusIndex = 0
782
895
  private nextAssignmentIndex = 0
783
896
  private nextGeneratedAssignmentIndex = 0
897
+ private activeOperation: ActiveFanoutOperation<unknown> | null = null
898
+ private inProgressPlans: FanoutRoutePlan[] = []
899
+ private activeRoutingVisualization: GraphicsObject | null = null
900
+ private activeAdaptiveVisualization: GraphicsObject | null = null
784
901
  private bestAttempt: AssignmentAttempt | null = null
785
902
  private lengthMatchingFailure: FanoutValidationIssue | null = null
786
903
  private endpointCompletion: CompleteOriginalEndpointsResult | null = null
@@ -857,56 +974,333 @@ export class FanoutSolver extends BaseSolver {
857
974
  )
858
975
  }
859
976
  }
860
- const boundaryBusIds = this.preparedBuses
861
- .filter((bus) => bus.termination.type === "boundary")
862
- .map((bus) => bus.busId)
863
- const fixedPlaneAssignments = Object.fromEntries(
977
+ this.boundaryBuses = this.preparedBuses.filter(
978
+ (bus) => bus.termination.type === "boundary",
979
+ )
980
+ this.fixedPlaneAssignments = Object.fromEntries(
864
981
  this.preparedBuses.flatMap((bus) =>
865
982
  bus.termination.type === "plane"
866
983
  ? [[bus.busId, bus.termination.layer] as const]
867
984
  : [],
868
985
  ),
869
986
  )
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
987
+ const workUnitsPerAssignment = this.preparedBuses.length * 3 + 8
988
+ const estimatedWorkUnitCount =
989
+ this.boundaryBuses.length +
990
+ 1 +
991
+ this.config.maxLayerCombinations * workUnitsPerAssignment +
992
+ this.preparedBuses.length * 2 +
993
+ 20
994
+ this.MAX_ITERATIONS = Math.max(10_000, estimatedWorkUnitCount)
995
+ }
996
+
997
+ override getSolverName(): string {
998
+ return "FanoutSolver"
999
+ }
1000
+
1001
+ private stepRoutingInitialization(): void {
1002
+ const bus = this.boundaryBuses[this.nextCandidateLayerBusIndex]
1003
+ if (bus) {
1004
+ this.escapeLayersByBusId[bus.busId] = getCandidateEscapeLayersForBus({
1005
+ bus,
1006
+ srj: this.routingSrj,
1007
+ config: this.config,
1008
+ staticClearanceCache: this.routeStaticClearanceCache,
1009
+ })
1010
+ this.nextCandidateLayerBusIndex++
1011
+ this.stats = {
1012
+ phase: "discover-candidate-layers",
1013
+ bus: bus.busId,
1014
+ busIndex: this.nextCandidateLayerBusIndex,
1015
+ busCount: this.boundaryBuses.length,
1016
+ }
1017
+ return
1018
+ }
1019
+
887
1020
  const generatedAssignments = generateLayerAssignments({
888
- busIds: boundaryBusIds,
1021
+ busIds: this.boundaryBuses.map((candidate) => candidate.busId),
889
1022
  layers: this.config.escapeLayers,
890
- layersByBusId: escapeLayersByBusId,
1023
+ layersByBusId: this.escapeLayersByBusId,
891
1024
  maxAssignments: this.config.maxLayerCombinations,
892
1025
  }).map((assignment) => ({
893
1026
  ...assignment,
894
- ...fixedPlaneAssignments,
1027
+ ...this.fixedPlaneAssignments,
895
1028
  }))
896
- this.layerAssignments = prioritizeLayerAssignment({
897
- initialAssignment: createInitialLayerAssignment({
898
- buses: this.preparedBuses,
899
- escapeLayers: this.config.escapeLayers,
900
- escapeLayersByBusId,
1029
+ this.layerAssignments.push(
1030
+ ...prioritizeLayerAssignment({
1031
+ initialAssignment: createInitialLayerAssignment({
1032
+ buses: this.preparedBuses,
1033
+ escapeLayers: this.config.escapeLayers,
1034
+ escapeLayersByBusId: this.escapeLayersByBusId,
1035
+ preferOrderedCoordinatedWindingLayers:
1036
+ this.config.densePlaneReservationBusIds.length > 0 ||
1037
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0,
1038
+ }),
1039
+ generatedAssignments,
1040
+ maxAssignments: this.config.maxLayerCombinations,
901
1041
  }),
902
- generatedAssignments,
903
- maxAssignments: this.config.maxLayerCombinations,
904
- })
905
- this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
1042
+ )
1043
+ this.routingInitialized = true
1044
+ this.stats = {
1045
+ phase: "prepare-layer-assignments",
1046
+ assignmentCount: this.layerAssignments.length,
1047
+ }
906
1048
  }
907
1049
 
908
- override getSolverName(): string {
909
- return "FanoutSolver"
1050
+ private *initializeRoutingSteps(): Generator<FanoutWorkYield, void, unknown> {
1051
+ while (!this.routingInitialized) {
1052
+ this.stepRoutingInitialization()
1053
+ if (!this.routingInitialized) yield
1054
+ }
1055
+ }
1056
+
1057
+ private setInProgressPlans(params: {
1058
+ phase: string
1059
+ plans: readonly FanoutRoutePlan[]
1060
+ strategy?: RoutingStrategy | "grouped-beam"
1061
+ unitIndex?: number
1062
+ unitCount?: number
1063
+ busId?: string
1064
+ }): void {
1065
+ this.inProgressPlans = [...params.plans]
1066
+ this.stats = {
1067
+ ...this.stats,
1068
+ phase: params.phase,
1069
+ ...(params.strategy ? { routingStrategy: params.strategy } : {}),
1070
+ ...(params.unitIndex !== undefined ? { workUnit: params.unitIndex } : {}),
1071
+ ...(params.unitCount !== undefined
1072
+ ? { workUnitCount: params.unitCount }
1073
+ : {}),
1074
+ ...(params.busId ? { bus: params.busId } : {}),
1075
+ routedConnections: `${params.plans.length}/${this.inputSrj.connections.length}`,
1076
+ }
1077
+ }
1078
+
1079
+ private visualizeCurrentState(): GraphicsObject {
1080
+ const visualizedSrj =
1081
+ this.endpointCompletion?.simpleRouteJson ??
1082
+ (!this.solved && !this.failed && this.inProgressPlans.length > 0
1083
+ ? buildOutputSimpleRouteJson({
1084
+ inputSrj: this.inputSrj,
1085
+ plans: this.inProgressPlans,
1086
+ layerNames: this.config.layerNames,
1087
+ })
1088
+ : undefined) ??
1089
+ this.bestAttempt?.outputSrj ??
1090
+ this.inputSrj
1091
+ return visualizeSimpleRouteJson(visualizedSrj)
1092
+ }
1093
+
1094
+ private visualizeWorkState(solverName: string): GraphicsObject {
1095
+ const base = this.visualizeCurrentState()
1096
+ const boundary =
1097
+ this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds
1098
+ const activeBusId =
1099
+ typeof this.stats.bus === "string" ? this.stats.bus : undefined
1100
+ const activeBus = activeBusId
1101
+ ? this.preparedBuses.find((bus) => bus.busId === activeBusId)
1102
+ : undefined
1103
+ const width = boundary.maxX - boundary.minX
1104
+ const height = boundary.maxY - boundary.minY
1105
+ const annotationSize = Math.max(Math.min(width, height) * 0.025, 0.25)
1106
+ const phase =
1107
+ typeof this.stats.phase === "string" ? this.stats.phase : "starting"
1108
+ const detail = [
1109
+ typeof this.stats.routeConnection === "string"
1110
+ ? `connection ${this.stats.routeConnection}`
1111
+ : undefined,
1112
+ typeof this.stats.searchBatch === "number"
1113
+ ? `batch ${this.stats.searchBatch}`
1114
+ : undefined,
1115
+ typeof this.stats.expandedStates === "number"
1116
+ ? `${this.stats.expandedStates.toLocaleString()} states`
1117
+ : undefined,
1118
+ ]
1119
+ .filter(Boolean)
1120
+ .join(" · ")
1121
+ const title = `${solverName}: ${phase}`
1122
+ return {
1123
+ ...mergeGraphics(base, {
1124
+ rects: [
1125
+ {
1126
+ center: {
1127
+ x: (boundary.minX + boundary.maxX) / 2,
1128
+ y: (boundary.minY + boundary.maxY) / 2,
1129
+ },
1130
+ width,
1131
+ height,
1132
+ fill: "rgba(0, 0, 0, 0)",
1133
+ stroke: "rgba(14, 165, 233, 0.8)",
1134
+ label: `${solverName} working boundary`,
1135
+ },
1136
+ ],
1137
+ circles: (activeBus?.connections ?? []).map((connection) => ({
1138
+ center: connection.sourcePoint,
1139
+ radius: Math.max(
1140
+ annotationSize,
1141
+ Math.min(
1142
+ connection.sourceObstacle.width,
1143
+ connection.sourceObstacle.height,
1144
+ ) * 0.6,
1145
+ ),
1146
+ fill: "rgba(250, 204, 21, 0.25)",
1147
+ stroke: "#f59e0b",
1148
+ label: `active bus ${activeBusId}: ${connection.connection.name}`,
1149
+ })),
1150
+ texts: [
1151
+ {
1152
+ x: boundary.minX,
1153
+ y: boundary.maxY + annotationSize * 2,
1154
+ text: `${solverName} · ${phase}${detail ? ` · ${detail}` : ""}`,
1155
+ color: "#0f172a",
1156
+ fontSize: annotationSize * 1.5,
1157
+ anchorSide: "bottom_left",
1158
+ },
1159
+ ],
1160
+ }),
1161
+ title,
1162
+ }
1163
+ }
1164
+
1165
+ private visualizeBoundaryRoutingState(): GraphicsObject {
1166
+ if (!this.activeRoutingVisualization) {
1167
+ return this.visualizeWorkState("BoundaryBusRoutingSolver")
1168
+ }
1169
+ return {
1170
+ ...mergeGraphics(
1171
+ this.visualizeCurrentState(),
1172
+ this.activeRoutingVisualization,
1173
+ ),
1174
+ title: this.activeRoutingVisualization.title,
1175
+ }
1176
+ }
1177
+
1178
+ private visualizeAdaptiveRoutingState(): GraphicsObject {
1179
+ if (this.activeAdaptiveVisualization) {
1180
+ return this.activeAdaptiveVisualization
1181
+ }
1182
+ const boundary =
1183
+ this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds
1184
+ const width = boundary.maxX - boundary.minX
1185
+ const height = boundary.maxY - boundary.minY
1186
+ const annotationSize = Math.max(Math.min(width, height) * 0.02, 0.2)
1187
+ return {
1188
+ title: "SingleLayerAdaptiveExitSolver: preparing flow grid",
1189
+ rects: [
1190
+ {
1191
+ center: {
1192
+ x: (boundary.minX + boundary.maxX) / 2,
1193
+ y: (boundary.minY + boundary.maxY) / 2,
1194
+ },
1195
+ width,
1196
+ height,
1197
+ fill: "rgba(0, 0, 0, 0)",
1198
+ stroke: "rgba(14, 165, 233, 0.9)",
1199
+ label: "adaptive flow grid boundary",
1200
+ },
1201
+ ],
1202
+ points: this.preparedBuses.flatMap((bus) =>
1203
+ bus.connections.map((connection) => ({
1204
+ ...connection.sourcePoint,
1205
+ color: "#f97316",
1206
+ label: "adaptive route source",
1207
+ })),
1208
+ ),
1209
+ texts: [
1210
+ {
1211
+ x: boundary.minX,
1212
+ y: boundary.maxY + annotationSize * 2,
1213
+ text: "preparing adaptive flow grid",
1214
+ color: "#0f172a",
1215
+ fontSize: annotationSize * 1.5,
1216
+ anchorSide: "bottom_left",
1217
+ },
1218
+ ],
1219
+ }
1220
+ }
1221
+
1222
+ private startOperation<T>(params: {
1223
+ name: string
1224
+ generator: Generator<unknown, T, unknown>
1225
+ onSolved: (output: T) => void
1226
+ getProgress?: () => number
1227
+ }): void {
1228
+ const solver = this.createWorkSolver(
1229
+ params.name,
1230
+ params.generator,
1231
+ params.getProgress,
1232
+ )
1233
+ this.activeOperation = {
1234
+ solver,
1235
+ onSolved: params.onSolved,
1236
+ } as ActiveFanoutOperation<unknown>
1237
+ this.activeSubSolver = solver
1238
+ }
1239
+
1240
+ private createWorkSolver<T>(
1241
+ name: string,
1242
+ generator: Generator<unknown, T, unknown>,
1243
+ getProgress?: () => number,
1244
+ getVisualization?: () => GraphicsObject,
1245
+ ): FanoutWorkSolver<T> {
1246
+ return new FanoutWorkSolver(
1247
+ name,
1248
+ generator,
1249
+ getVisualization ?? (() => this.visualizeWorkState(name)),
1250
+ () => ({ ...this.stats }),
1251
+ getProgress ?? (() => 0),
1252
+ )
1253
+ }
1254
+
1255
+ private *routeBusAlternativesWorkSteps(
1256
+ params: Parameters<typeof routeBusAlternativesSteps>[0],
1257
+ maximumAlternatives: number,
1258
+ ): Generator<FanoutWorkYield, FanoutRoutePlan[][], unknown> {
1259
+ const steps = routeBusAlternativesSteps(params, maximumAlternatives, true)
1260
+ let result = steps.next()
1261
+ while (!result.done) {
1262
+ const { winding } = result.value
1263
+ if (winding.visualization) {
1264
+ this.activeRoutingVisualization = winding.visualization
1265
+ }
1266
+ this.stats = {
1267
+ ...this.stats,
1268
+ phase: "route-boundary-bus-connection",
1269
+ bus: result.value.busId,
1270
+ targetLayer: result.value.targetLayer,
1271
+ routeOrderAttempt: winding.routeOrderAttempt,
1272
+ routeConnection: `${winding.connectionIndex + 1}/${winding.connectionCount}`,
1273
+ connection: winding.connectionName,
1274
+ searchBatch: winding.searchBatch,
1275
+ expandedStates: winding.expandedStateCount,
1276
+ connectionComplete: winding.connectionComplete,
1277
+ }
1278
+ yield
1279
+ result = steps.next()
1280
+ }
1281
+ return result.value
1282
+ }
1283
+
1284
+ private stepActiveOperation(): void {
1285
+ const operation = this.activeOperation
1286
+ if (!operation) return
1287
+ operation.solver.step()
1288
+ if (operation.solver.failed) {
1289
+ this.failedSubSolvers = [
1290
+ ...(this.failedSubSolvers ?? []),
1291
+ operation.solver,
1292
+ ]
1293
+ this.error = operation.solver.error
1294
+ this.failed = true
1295
+ this.activeOperation = null
1296
+ this.activeSubSolver = null
1297
+ return
1298
+ }
1299
+ if (!operation.solver.solved) return
1300
+ const output = operation.solver.getOutput()
1301
+ this.activeOperation = null
1302
+ this.activeSubSolver = null
1303
+ operation.onSolved(output)
910
1304
  }
911
1305
 
912
1306
  private completeBestAttemptEndpoints(): void {
@@ -983,15 +1377,43 @@ export class FanoutSolver extends BaseSolver {
983
1377
  * dogbones. This is intentionally bounded independently of the number of
984
1378
  * plane drops so dense power fields cannot explode the general beam search.
985
1379
  */
986
- private routeDenseThroughAllMixedTerminations(params: {
1380
+ private *routeDenseThroughAllMixedTerminationSteps(params: {
987
1381
  busLayerAssignments: Readonly<Record<string, string>>
988
1382
  busesInRoutingOrder: readonly PreparedBus[]
989
- }): MixedTerminationState | null {
1383
+ }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
990
1384
  if (this.config.allowBlindAndBuriedVias) return null
1385
+ const debugDense = (...values: unknown[]) => {
1386
+ if (process.env.FANOUT_DEBUG_DENSE === "1") {
1387
+ if (
1388
+ process.env.FANOUT_DEBUG_DENSE_SUMMARY === "1" &&
1389
+ ![
1390
+ "start",
1391
+ "plane-match:preflight-failed",
1392
+ "plane-match:incremental-complete",
1393
+ "plane-match:incremental-failed",
1394
+ "plane-route:alternate-candidate-counts",
1395
+ "plane-route:alternate-search",
1396
+ "plane-route:promote-failed",
1397
+ "plane-route:alternate-choice",
1398
+ "plane-route:promote-zero-candidates",
1399
+ "length-match:complete",
1400
+ "length-match:start",
1401
+ "plane-route:failed",
1402
+ "dense-validation",
1403
+ ].includes(String(values[0]))
1404
+ ) {
1405
+ return
1406
+ }
1407
+ console.error("dense:", ...values)
1408
+ }
1409
+ }
991
1410
 
992
1411
  const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
993
1412
  (bus) => bus.termination.type === "boundary",
994
1413
  )
1414
+ const useConfiguredDensePlaneRouting =
1415
+ this.config.densePlaneReservationBusIds.length > 0 ||
1416
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
995
1417
  const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
996
1418
  unsortedBoundaryBuses.map((bus) => bus.connections.length),
997
1419
  )
@@ -1013,84 +1435,181 @@ export class FanoutSolver extends BaseSolver {
1013
1435
  ]),
1014
1436
  )
1015
1437
  : null
1016
- const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
1017
- // Reserve the dense escape field for the widest buses first. Small
1018
- // control groups can usually route around their copper, while routing
1019
- // a two-line corner bus first can consume a critical channel needed by
1020
- // an eight-line winding bus and force the expensive fallback search.
1021
- if (useJointBoundaryViaReservation) {
1022
- const connectionCountDifference =
1023
- second.connections.length - first.connections.length
1024
- if (connectionCountDifference !== 0) return connectionCountDifference
1025
- }
1026
- const firstLayer = params.busLayerAssignments[first.busId]
1027
- const secondLayer = params.busLayerAssignments[second.busId]
1028
- const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1029
- first.busId,
1030
- )
1031
- const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1032
- second.busId,
1033
- )
1034
- if (
1035
- firstPairRoutingPriority !== undefined &&
1036
- secondPairRoutingPriority !== undefined &&
1037
- firstPairRoutingPriority !== secondPairRoutingPriority
1038
- ) {
1039
- // The third pair can be fenced off by two earlier pair windings. Let
1040
- // the pair with the shortest farthest-lane boundary reach claim its
1041
- // channel first without relying on caller-specific bus identifiers.
1042
- return firstPairRoutingPriority - secondPairRoutingPriority
1438
+ const wideBoundaryBuses = unsortedBoundaryBuses.filter(
1439
+ (bus) => bus.connections.length >= 8,
1440
+ )
1441
+ const hasThreeWideBoundaryBuses =
1442
+ useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
1443
+ const getBoundaryTargetSpan = (bus: PreparedBus) => {
1444
+ const coordinates = bus.connections.map((connection) => {
1445
+ const target = connection.exitTargetPoint ?? connection.targetPoint
1446
+ return bus.exitEdge === "top" || bus.exitEdge === "bottom"
1447
+ ? target.x
1448
+ : target.y
1449
+ })
1450
+ return {
1451
+ minimum: Math.min(...coordinates),
1452
+ maximum: Math.max(...coordinates),
1043
1453
  }
1044
- const cornerBandDifference =
1045
- Number(
1046
- Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
1047
- ) -
1048
- Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
1049
- if (cornerBandDifference !== 0) return cornerBandDifference
1050
- const firstIsCorner = Boolean(
1051
- getCornerBandSide(first.exitEdge, first.preferredExit),
1052
- )
1053
- if (!firstIsCorner) {
1054
- const getSourceSpan = (bus: PreparedBus): number => {
1055
- const xCoordinates = bus.connections.map(
1056
- (connection) => connection.sourcePoint.x,
1057
- )
1058
- const yCoordinates = bus.connections.map(
1059
- (connection) => connection.sourcePoint.y,
1060
- )
1061
- return (
1062
- Math.max(...xCoordinates) -
1063
- Math.min(...xCoordinates) +
1064
- Math.max(...yCoordinates) -
1065
- Math.min(...yCoordinates)
1454
+ }
1455
+ const narrowBusOverlapsWideTargetSpan = (bus: PreparedBus): boolean => {
1456
+ if (bus.connections.length >= 8) return false
1457
+ const span = getBoundaryTargetSpan(bus)
1458
+ return wideBoundaryBuses.some((wideBus) => {
1459
+ if (wideBus.exitEdge !== bus.exitEdge) return false
1460
+ const wideSpan = getBoundaryTargetSpan(wideBus)
1461
+ return (
1462
+ span.maximum >= wideSpan.minimum - 1e-9 &&
1463
+ span.minimum <= wideSpan.maximum + 1e-9
1464
+ )
1465
+ })
1466
+ }
1467
+ const getContainingWideSourceField = (
1468
+ bus: PreparedBus,
1469
+ ): PreparedBus | undefined => {
1470
+ if (bus.connections.length >= 8) return undefined
1471
+ return wideBoundaryBuses.find((wideBus) => {
1472
+ const wideXCoordinates = wideBus.connections.map(
1473
+ (connection) => connection.sourcePoint.x,
1474
+ )
1475
+ const wideYCoordinates = wideBus.connections.map(
1476
+ (connection) => connection.sourcePoint.y,
1477
+ )
1478
+ const minimumX = Math.min(...wideXCoordinates)
1479
+ const maximumX = Math.max(...wideXCoordinates)
1480
+ const minimumY = Math.min(...wideYCoordinates)
1481
+ const maximumY = Math.max(...wideYCoordinates)
1482
+ return bus.connections.every(
1483
+ (connection) =>
1484
+ connection.sourcePoint.x >= minimumX - 1e-9 &&
1485
+ connection.sourcePoint.x <= maximumX + 1e-9 &&
1486
+ connection.sourcePoint.y >= minimumY - 1e-9 &&
1487
+ connection.sourcePoint.y <= maximumY + 1e-9,
1488
+ )
1489
+ })
1490
+ }
1491
+ const narrowBusIsEmbeddedInWideSourceField = (bus: PreparedBus): boolean =>
1492
+ Boolean(getContainingWideSourceField(bus))
1493
+ const getThreeWideRoutingPriority = (bus: PreparedBus): number =>
1494
+ narrowBusOverlapsWideTargetSpan(bus) &&
1495
+ !narrowBusIsEmbeddedInWideSourceField(bus)
1496
+ ? 0
1497
+ : bus.connections.length >= 8
1498
+ ? 1
1499
+ : bus.connections.length > 1
1500
+ ? 2
1501
+ : 3
1502
+ const initiallySortedBoundaryBuses = unsortedBoundaryBuses.toSorted(
1503
+ (first, second) => {
1504
+ // Reserve the dense escape field for the widest buses first. Small
1505
+ // control groups can usually route around their copper, while routing
1506
+ // a two-line corner bus first can consume a critical channel needed by
1507
+ // an eight-line winding bus and force the expensive fallback search.
1508
+ if (useJointBoundaryViaReservation || wideBoundaryBuses.length > 0) {
1509
+ if (hasThreeWideBoundaryBuses) {
1510
+ const threeWidePriorityDifference =
1511
+ getThreeWideRoutingPriority(first) -
1512
+ getThreeWideRoutingPriority(second)
1513
+ if (threeWidePriorityDifference !== 0) {
1514
+ return threeWidePriorityDifference
1515
+ }
1516
+ }
1517
+ const connectionCountDifference =
1518
+ unsortedBoundaryBuses.length === 2 &&
1519
+ Math.max(
1520
+ ...unsortedBoundaryBuses.map((bus) => bus.connections.length),
1521
+ ) >= 8
1522
+ ? first.connections.length - second.connections.length
1523
+ : second.connections.length - first.connections.length
1524
+ if (connectionCountDifference !== 0) return connectionCountDifference
1525
+ }
1526
+ const firstLayer = params.busLayerAssignments[first.busId]
1527
+ const secondLayer = params.busLayerAssignments[second.busId]
1528
+ const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1529
+ first.busId,
1530
+ )
1531
+ const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1532
+ second.busId,
1533
+ )
1534
+ if (
1535
+ firstPairRoutingPriority !== undefined &&
1536
+ secondPairRoutingPriority !== undefined &&
1537
+ firstPairRoutingPriority !== secondPairRoutingPriority
1538
+ ) {
1539
+ // The third pair can be fenced off by two earlier pair windings. Let
1540
+ // the pair with the shortest farthest-lane boundary reach claim its
1541
+ // channel first without relying on caller-specific bus identifiers.
1542
+ return firstPairRoutingPriority - secondPairRoutingPriority
1543
+ }
1544
+ const cornerBandDifference =
1545
+ Number(
1546
+ Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
1547
+ ) -
1548
+ Number(
1549
+ Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)),
1066
1550
  )
1551
+ if (cornerBandDifference !== 0) return cornerBandDifference
1552
+ const firstIsCorner = Boolean(
1553
+ getCornerBandSide(first.exitEdge, first.preferredExit),
1554
+ )
1555
+ if (!firstIsCorner) {
1556
+ const getSourceSpan = (bus: PreparedBus): number => {
1557
+ const xCoordinates = bus.connections.map(
1558
+ (connection) => connection.sourcePoint.x,
1559
+ )
1560
+ const yCoordinates = bus.connections.map(
1561
+ (connection) => connection.sourcePoint.y,
1562
+ )
1563
+ return (
1564
+ Math.max(...xCoordinates) -
1565
+ Math.min(...xCoordinates) +
1566
+ Math.max(...yCoordinates) -
1567
+ Math.min(...yCoordinates)
1568
+ )
1569
+ }
1570
+ const sourceSpanDifference =
1571
+ getSourceSpan(second) - getSourceSpan(first)
1572
+ if (Math.abs(sourceSpanDifference) > 1e-9) {
1573
+ return sourceSpanDifference
1574
+ }
1067
1575
  }
1068
- const sourceSpanDifference =
1069
- getSourceSpan(second) - getSourceSpan(first)
1070
- if (Math.abs(sourceSpanDifference) > 1e-9) {
1071
- return sourceSpanDifference
1576
+ const layerDifference =
1577
+ this.config.layerNames.indexOf(firstLayer ?? "") -
1578
+ this.config.layerNames.indexOf(secondLayer ?? "")
1579
+ if (layerDifference !== 0) return -layerDifference
1580
+ if (
1581
+ unsortedBoundaryBuses.length !== 6 &&
1582
+ unsortedBoundaryBuses.length !== 7 &&
1583
+ unsortedBoundaryBuses.length !== 8 &&
1584
+ unsortedBoundaryBuses.length !== 9
1585
+ ) {
1586
+ return 0
1072
1587
  }
1073
- }
1074
- const layerDifference =
1075
- this.config.layerNames.indexOf(firstLayer ?? "") -
1076
- this.config.layerNames.indexOf(secondLayer ?? "")
1077
- if (layerDifference !== 0) return -layerDifference
1078
- if (
1079
- unsortedBoundaryBuses.length !== 6 &&
1080
- unsortedBoundaryBuses.length !== 7 &&
1081
- unsortedBoundaryBuses.length !== 8 &&
1082
- unsortedBoundaryBuses.length !== 9
1083
- ) {
1084
- return 0
1085
- }
1086
- // The general routing order can differ across the two components as a
1087
- // function of local pad geometry. Keep otherwise-equivalent corner buses
1088
- // in one deterministic order for the six- through nine-bus paths so their
1089
- // boundary lanes do not swap between the two ends of a direct
1090
- // interconnect. Leave the released four- and five-bus tie behavior
1091
- // unchanged.
1092
- return first.busId.localeCompare(second.busId)
1093
- })
1588
+ // The general routing order can differ across the two components as a
1589
+ // function of local pad geometry. Keep otherwise-equivalent corner buses
1590
+ // in one deterministic order for the six- through nine-bus paths so their
1591
+ // boundary lanes do not swap between the two ends of a direct
1592
+ // interconnect. Leave the released four- and five-bus tie behavior
1593
+ // unchanged.
1594
+ return first.busId.localeCompare(second.busId)
1595
+ },
1596
+ )
1597
+ const debugBoundaryOrder =
1598
+ process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ??
1599
+ (process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS
1600
+ ? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS]
1601
+ : [])
1602
+ const boundaryBuses =
1603
+ debugBoundaryOrder.length > 0
1604
+ ? initiallySortedBoundaryBuses.toSorted((first, second) => {
1605
+ const firstIndex = debugBoundaryOrder.indexOf(first.busId)
1606
+ const secondIndex = debugBoundaryOrder.indexOf(second.busId)
1607
+ return (
1608
+ (firstIndex < 0 ? Number.POSITIVE_INFINITY : firstIndex) -
1609
+ (secondIndex < 0 ? Number.POSITIVE_INFINITY : secondIndex)
1610
+ )
1611
+ })
1612
+ : initiallySortedBoundaryBuses
1094
1613
  // Preserve the caller/input order for the dense singleton fill. The
1095
1614
  // general routing sort is useful for heterogeneous buses, but ordering a
1096
1615
  // regular BGA power field by obstacle depth creates artificial local
@@ -1099,6 +1618,43 @@ export class FanoutSolver extends BaseSolver {
1099
1618
  const planeBuses = this.preparedBuses.filter(
1100
1619
  (bus) => bus.termination.type === "plane",
1101
1620
  )
1621
+ const denseAdditionalObstacles = useConfiguredDensePlaneRouting
1622
+ ? this.routingSrj.obstacles
1623
+ : undefined
1624
+ const initialPlaneReservationCount = Number.parseInt(
1625
+ process.env.FANOUT_INITIAL_PLANE_RESERVATIONS ?? "8",
1626
+ 10,
1627
+ )
1628
+ const debugInitialPlaneIndices =
1629
+ process.env.FANOUT_DEBUG_INITIAL_PLANE_INDICES?.split(",").map(
1630
+ (index) => Number(index) - 1,
1631
+ )
1632
+ const debugInitialPlaneBusIds =
1633
+ process.env.FANOUT_DEBUG_INITIAL_PLANE_BUS_IDS?.split(",")
1634
+ let activeBoundaryReservationPlaneBuses = debugInitialPlaneBusIds
1635
+ ? planeBuses.filter((bus) => debugInitialPlaneBusIds.includes(bus.busId))
1636
+ : debugInitialPlaneIndices
1637
+ ? debugInitialPlaneIndices.flatMap((index) =>
1638
+ planeBuses[index] ? [planeBuses[index]!] : [],
1639
+ )
1640
+ : this.config.densePlaneReservationBusIds.length > 0
1641
+ ? planeBuses.filter((bus) =>
1642
+ this.config.densePlaneReservationBusIds.includes(bus.busId),
1643
+ )
1644
+ : useConfiguredDensePlaneRouting
1645
+ ? planeBuses.slice(
1646
+ 0,
1647
+ Number.isFinite(initialPlaneReservationCount)
1648
+ ? initialPlaneReservationCount
1649
+ : 8,
1650
+ )
1651
+ : planeBuses
1652
+ debugDense(
1653
+ "start",
1654
+ boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
1655
+ `planes:${planeBuses.length}`,
1656
+ `joint:${useJointBoundaryViaReservation}`,
1657
+ )
1102
1658
  if (
1103
1659
  boundaryBuses.length === 0 ||
1104
1660
  boundaryBuses.length > 9 ||
@@ -1129,18 +1685,40 @@ export class FanoutSolver extends BaseSolver {
1129
1685
  singletonBoundaryBusCount > 1 &&
1130
1686
  shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
1131
1687
  const preferredBoundaryPerpendicularSideByBusId = new Map(
1132
- boundaryBuses.map((bus) => [bus.busId, 1 as const]),
1688
+ boundaryBuses.map((bus) => [
1689
+ bus.busId,
1690
+ hasThreeWideBoundaryBuses &&
1691
+ bus.connections.length < 8 &&
1692
+ narrowBusIsEmbeddedInWideSourceField(bus)
1693
+ ? (-1 as const)
1694
+ : (1 as const),
1695
+ ]),
1133
1696
  )
1134
1697
  const preferBoundaryOutwardByBusId = new Map(
1135
1698
  boundaryBuses.map((bus) => [
1136
1699
  bus.busId,
1137
- useGeometryAwareSingletonOutwardPreference &&
1138
- bus.connections.length === 1 &&
1139
- getCornerBandSide(bus.exitEdge, bus.preferredExit)
1140
- ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1141
- : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1700
+ bus.connections.length === 1
1701
+ ? hasThreeWideBoundaryBuses &&
1702
+ narrowBusIsEmbeddedInWideSourceField(bus)
1703
+ ? true
1704
+ : useGeometryAwareSingletonOutwardPreference &&
1705
+ getCornerBandSide(bus.exitEdge, bus.preferredExit)
1706
+ ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1707
+ : getExitEdgeForDirection(bus.direction) !== bus.exitEdge
1708
+ : bus.exitEdge === "top" && bus.connections.length >= 8
1709
+ ? false
1710
+ : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1142
1711
  ]),
1143
1712
  )
1713
+ const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
1714
+ if (debugFlippedBoundaryBus) {
1715
+ preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
1716
+ }
1717
+ const debugOutwardBoundaryBus =
1718
+ process.env.FANOUT_DEBUG_OUTWARD_BOUNDARY_BUS
1719
+ if (debugOutwardBoundaryBus) {
1720
+ preferBoundaryOutwardByBusId.set(debugOutwardBoundaryBus, true)
1721
+ }
1144
1722
  const canShareCopper = (
1145
1723
  firstConnectionIndex: number,
1146
1724
  secondConnectionIndex: number,
@@ -1176,6 +1754,7 @@ export class FanoutSolver extends BaseSolver {
1176
1754
  (bus) => bus.connections.length === 1,
1177
1755
  )
1178
1756
  const singletonDeferralCandidates =
1757
+ !hasThreeWideBoundaryBuses &&
1179
1758
  shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
1180
1759
  ? singletonBoundaryBuses
1181
1760
  .toSorted(
@@ -1190,8 +1769,8 @@ export class FanoutSolver extends BaseSolver {
1190
1769
  ),
1191
1770
  )
1192
1771
  : []
1193
- const leadingWideSingletonBuses =
1194
- boundaryBuses.length === 9
1772
+ const multiLayerLeadingSingletonBuses =
1773
+ boundaryBuses.length === 8 || boundaryBuses.length === 9
1195
1774
  ? singletonDeferralCandidates.filter((singletonBus) => {
1196
1775
  const singletonTargetLayer =
1197
1776
  params.busLayerAssignments[singletonBus.busId]
@@ -1205,6 +1784,53 @@ export class FanoutSolver extends BaseSolver {
1205
1784
  )
1206
1785
  })
1207
1786
  : []
1787
+ const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses
1788
+ ? singletonBoundaryBuses.filter((singletonBus) => {
1789
+ const containingWideBus = getContainingWideSourceField(singletonBus)
1790
+ const singletonTargetLayer =
1791
+ params.busLayerAssignments[singletonBus.busId]
1792
+ const containingWideLayers =
1793
+ containingWideBus?.routableEscapeLayers ??
1794
+ containingWideBus?.allowedLayers ??
1795
+ []
1796
+ return Boolean(
1797
+ containingWideBus &&
1798
+ singletonTargetLayer &&
1799
+ !containingWideLayers.includes(singletonTargetLayer),
1800
+ )
1801
+ })
1802
+ : []
1803
+ const throughAllLeadingCompanionBuses =
1804
+ throughAllLeadingSingletonBuses.flatMap((singletonBus) => {
1805
+ const containingWideBus = getContainingWideSourceField(singletonBus)
1806
+ const singletonTargetLayer =
1807
+ params.busLayerAssignments[singletonBus.busId]
1808
+ return boundaryBuses.filter(
1809
+ (candidate) =>
1810
+ candidate.connections.length > 1 &&
1811
+ candidate.connections.length < 8 &&
1812
+ candidate.direction === singletonBus.direction &&
1813
+ params.busLayerAssignments[candidate.busId] ===
1814
+ singletonTargetLayer &&
1815
+ getContainingWideSourceField(candidate) === containingWideBus,
1816
+ )
1817
+ })
1818
+ const throughAllLeadingBuses =
1819
+ process.env.FANOUT_DEBUG_DISABLE_LEADING_NARROW === "1"
1820
+ ? []
1821
+ : throughAllLeadingSingletonBuses.flatMap((singletonBus) => [
1822
+ singletonBus,
1823
+ ...throughAllLeadingCompanionBuses.filter(
1824
+ (candidate) =>
1825
+ candidate.direction === singletonBus.direction &&
1826
+ params.busLayerAssignments[candidate.busId] ===
1827
+ params.busLayerAssignments[singletonBus.busId],
1828
+ ),
1829
+ ])
1830
+ const leadingWideSingletonBuses = [
1831
+ ...multiLayerLeadingSingletonBuses,
1832
+ ...throughAllLeadingBuses,
1833
+ ].filter((bus, index, buses) => buses.indexOf(bus) === index)
1208
1834
  const leadingLaneCountByWideCornerBand = new Map<string, number>()
1209
1835
  if (boundaryBuses.length === 9 && leadingWideSingletonBuses.length > 0) {
1210
1836
  for (const bus of leadingWideSingletonBuses) {
@@ -1229,11 +1855,22 @@ export class FanoutSolver extends BaseSolver {
1229
1855
  )
1230
1856
  }
1231
1857
  }
1232
- const viaProvisionalSingletonBusSet = new Set(
1233
- singletonDeferralCandidates.filter(
1858
+ const viaProvisionalBoundaryBusSet = new Set([
1859
+ ...(process.env.FANOUT_DEBUG_PROVISIONAL_NARROW === "1"
1860
+ ? boundaryBuses.filter((bus) => bus.connections.length < 8)
1861
+ : []),
1862
+ ...singletonDeferralCandidates.filter(
1234
1863
  (bus) => !leadingWideSingletonBuses.includes(bus),
1235
1864
  ),
1236
- )
1865
+ ...(hasThreeWideBoundaryBuses
1866
+ ? boundaryBuses.filter(
1867
+ (bus) =>
1868
+ bus.connections.length === 2 &&
1869
+ !narrowBusOverlapsWideTargetSpan(bus) &&
1870
+ !leadingWideSingletonBuses.includes(bus),
1871
+ )
1872
+ : []),
1873
+ ])
1237
1874
  const getCornerBandTargetTrackOffset = (bus: PreparedBus): number => {
1238
1875
  const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1239
1876
  if (!bus.exitEdge || !side) return 0
@@ -1247,11 +1884,14 @@ export class FanoutSolver extends BaseSolver {
1247
1884
  })
1248
1885
  }
1249
1886
  const initiallyMatchedBoundaryBuses = boundaryBuses.filter(
1250
- (bus) => !viaProvisionalSingletonBusSet.has(bus),
1887
+ (bus) => !viaProvisionalBoundaryBusSet.has(bus),
1251
1888
  )
1252
1889
  const jointViaPoints = useJointBoundaryViaReservation
1253
1890
  ? matchComponentDogboneViaSites(
1254
- [...planeBuses, ...initiallyMatchedBoundaryBuses],
1891
+ [
1892
+ ...activeBoundaryReservationPlaneBuses,
1893
+ ...initiallyMatchedBoundaryBuses,
1894
+ ],
1255
1895
  {
1256
1896
  viaDiameter: this.config.viaDiameter,
1257
1897
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1260,28 +1900,133 @@ export class FanoutSolver extends BaseSolver {
1260
1900
  maximumSearchStates: 100_000,
1261
1901
  preferredBoundaryPerpendicularSideByBusId,
1262
1902
  preferBoundaryOutwardByBusId,
1903
+ additionalObstacles: denseAdditionalObstacles,
1904
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1263
1905
  canShareCopper,
1264
1906
  },
1265
1907
  )
1266
1908
  : null
1267
- const seedViaPoints =
1909
+ let seedViaPoints =
1268
1910
  jointViaPoints ??
1269
- matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
1270
- viaDiameter: this.config.viaDiameter,
1271
- viaHoleDiameter: this.config.viaHoleDiameter,
1272
- traceWidth: this.config.traceWidth,
1273
- clearance: this.config.clearance,
1274
- maximumSearchStates: 20_000,
1275
- preferredBoundaryPerpendicularSideByBusId,
1276
- preferBoundaryOutwardByBusId,
1277
- canShareCopper,
1278
- })
1911
+ matchComponentDogboneViaSites(
1912
+ [...activeBoundaryReservationPlaneBuses, boundaryBuses[0]!],
1913
+ {
1914
+ viaDiameter: this.config.viaDiameter,
1915
+ viaHoleDiameter: this.config.viaHoleDiameter,
1916
+ traceWidth: this.config.traceWidth,
1917
+ clearance: this.config.clearance,
1918
+ maximumSearchStates: 20_000,
1919
+ preferredBoundaryPerpendicularSideByBusId,
1920
+ preferBoundaryOutwardByBusId,
1921
+ additionalObstacles: denseAdditionalObstacles,
1922
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1923
+ canShareCopper,
1924
+ },
1925
+ )
1926
+ const debugFixedVias = process.env.FANOUT_DEBUG_FIXED_VIAS
1927
+ if (seedViaPoints && debugFixedVias) {
1928
+ seedViaPoints = new Map(seedViaPoints)
1929
+ for (const entry of debugFixedVias.split(",")) {
1930
+ const parts = entry.split(":")
1931
+ const rawY = parts.pop()
1932
+ const rawX = parts.pop()
1933
+ const connectionName = parts.join(":")
1934
+ const connectionIndex = [...connectionNameByIndex].find(
1935
+ ([, name]) => name === connectionName,
1936
+ )?.[0]
1937
+ const x = Number(rawX)
1938
+ const y = Number(rawY)
1939
+ if (
1940
+ connectionIndex !== undefined &&
1941
+ Number.isFinite(x) &&
1942
+ Number.isFinite(y)
1943
+ ) {
1944
+ seedViaPoints.set(connectionIndex, { x, y })
1945
+ }
1946
+ }
1947
+ }
1948
+ const debugStagedPlaneBuses = process.env.FANOUT_DEBUG_STAGED_PLANE_BUS_IDS
1949
+ ? planeBuses.filter((bus) =>
1950
+ process.env
1951
+ .FANOUT_DEBUG_STAGED_PLANE_BUS_IDS!.split(",")
1952
+ .includes(bus.busId),
1953
+ )
1954
+ : (process.env.FANOUT_DEBUG_STAGED_PLANE_INDICES?.split(",").flatMap(
1955
+ (index) =>
1956
+ planeBuses[Number(index) - 1]
1957
+ ? [planeBuses[Number(index) - 1]!]
1958
+ : [],
1959
+ ) ?? [])
1960
+ if (seedViaPoints && debugStagedPlaneBuses.length > 0) {
1961
+ for (const stagedPlaneBus of debugStagedPlaneBuses) {
1962
+ if (activeBoundaryReservationPlaneBuses.includes(stagedPlaneBus)) {
1963
+ continue
1964
+ }
1965
+ const stagedViaPoints = matchComponentDogboneViaSites(
1966
+ [
1967
+ ...activeBoundaryReservationPlaneBuses,
1968
+ stagedPlaneBus,
1969
+ ...initiallyMatchedBoundaryBuses,
1970
+ ],
1971
+ {
1972
+ viaDiameter: this.config.viaDiameter,
1973
+ viaHoleDiameter: this.config.viaHoleDiameter,
1974
+ traceWidth: this.config.traceWidth,
1975
+ clearance: this.config.clearance,
1976
+ maximumSearchStates: 100_000,
1977
+ preferredBoundaryPerpendicularSideByBusId,
1978
+ preferBoundaryOutwardByBusId,
1979
+ fixedViaPointsByConnectionIndex: seedViaPoints,
1980
+ additionalObstacles: denseAdditionalObstacles,
1981
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1982
+ canShareCopper,
1983
+ },
1984
+ )
1985
+ debugDense(
1986
+ "plane-reservation:staged",
1987
+ stagedPlaneBus.busId,
1988
+ stagedViaPoints?.size ?? "failed",
1989
+ )
1990
+ if (!stagedViaPoints) break
1991
+ seedViaPoints = new Map([...seedViaPoints, ...stagedViaPoints])
1992
+ activeBoundaryReservationPlaneBuses.push(stagedPlaneBus)
1993
+ }
1994
+ }
1995
+ debugDense("seed", seedViaPoints?.size ?? "failed")
1996
+ if (seedViaPoints && process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
1997
+ console.error(
1998
+ "dense: seed-points",
1999
+ [...seedViaPoints].map(([connectionIndex, point]) => ({
2000
+ connection: connectionNameByIndex.get(connectionIndex),
2001
+ point,
2002
+ })),
2003
+ )
2004
+ }
2005
+ let denseWorkUnitIndex = 1
2006
+ const denseWorkUnitCount = boundaryBuses.length + planeBuses.length + 3
2007
+ this.setInProgressPlans({
2008
+ phase: "reserve-dense-via-sites",
2009
+ plans: [],
2010
+ strategy: "default",
2011
+ unitIndex: denseWorkUnitIndex,
2012
+ unitCount: denseWorkUnitCount,
2013
+ })
2014
+ yield
1279
2015
  if (seedViaPoints) {
1280
2016
  const denseBoundaryBusesInRoutingOrder = [
1281
- ...leadingWideSingletonBuses,
1282
- ...boundaryBuses.filter(
1283
- (bus) => !leadingWideSingletonBuses.includes(bus),
1284
- ),
2017
+ ...multiLayerLeadingSingletonBuses,
2018
+ ...boundaryBuses
2019
+ .filter(
2020
+ (bus) =>
2021
+ !multiLayerLeadingSingletonBuses.includes(bus) &&
2022
+ !throughAllLeadingBuses.includes(bus),
2023
+ )
2024
+ .flatMap((bus) => [
2025
+ ...throughAllLeadingBuses.filter(
2026
+ (candidate) => getContainingWideSourceField(candidate) === bus,
2027
+ ),
2028
+ bus,
2029
+ ]),
1285
2030
  ]
1286
2031
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
1287
2032
  number,
@@ -1321,7 +2066,24 @@ export class FanoutSolver extends BaseSolver {
1321
2066
  })
1322
2067
  })
1323
2068
  }
1324
- const routeMatchedBoundaryBus = (bus: PreparedBus): boolean => {
2069
+ const routeMatchedBoundaryBusSteps = function* (
2070
+ this: FanoutSolver,
2071
+ bus: PreparedBus,
2072
+ ): Generator<FanoutWorkYield, boolean, unknown> {
2073
+ debugDense("route:start", bus.busId, matchedPlans.length)
2074
+ if (process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
2075
+ console.error(
2076
+ "dense: points",
2077
+ bus.busId,
2078
+ bus.connections.map((connection) => ({
2079
+ source: connection.sourcePoint,
2080
+ via: fixedViaPointsByConnectionIndex.get(
2081
+ connection.connectionIndex,
2082
+ ),
2083
+ target: connection.exitTargetPoint ?? connection.targetPoint,
2084
+ })),
2085
+ )
2086
+ }
1325
2087
  const targetLayer = params.busLayerAssignments[bus.busId]
1326
2088
  if (!targetLayer) {
1327
2089
  return false
@@ -1342,10 +2104,60 @@ export class FanoutSolver extends BaseSolver {
1342
2104
  staticClearanceCache: this.routeStaticClearanceCache,
1343
2105
  fixedViaPointsByConnectionIndex,
1344
2106
  reservedVias: getReservedVias(bus),
1345
- viaMinimalOnly: true,
2107
+ viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2108
+ fixedViaFallbackRouteOrderAttempts: useConfiguredDensePlaneRouting
2109
+ ? 6
2110
+ : 24,
1346
2111
  cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
1347
2112
  } as const
1348
- let busPlans = routeBusAlternatives(routeParams, 1)[0]
2113
+ const routeAlternatives = function* (
2114
+ this: FanoutSolver,
2115
+ candidateRouteParams: Parameters<typeof routeBusAlternativesSteps>[0],
2116
+ maximumAlternatives: number,
2117
+ ): Generator<FanoutWorkYield, FanoutRoutePlan[][], unknown> {
2118
+ this.activeRoutingVisualization = null
2119
+ const solver = this.createWorkSolver(
2120
+ "BoundaryBusRoutingSolver",
2121
+ this.routeBusAlternativesWorkSteps(
2122
+ candidateRouteParams,
2123
+ maximumAlternatives,
2124
+ ),
2125
+ undefined,
2126
+ () => this.visualizeBoundaryRoutingState(),
2127
+ )
2128
+ return (yield { type: "subsolver", solver }) as FanoutRoutePlan[][]
2129
+ }.bind(this)
2130
+ const routableEscapeLayers =
2131
+ bus.routableEscapeLayers ?? bus.allowedLayers ?? []
2132
+ const singleLayerBus = routableEscapeLayers.some(
2133
+ (layer) => layer !== targetLayer,
2134
+ )
2135
+ ? { ...bus, routableEscapeLayers: [targetLayer] }
2136
+ : bus
2137
+ const embeddedNarrowBusAlreadyRouted = boundaryBuses.some(
2138
+ (candidate) =>
2139
+ candidate.connections.length < 8 &&
2140
+ getContainingWideSourceField(candidate) === bus &&
2141
+ matchedPlans.some((plan) => plan.busId === candidate.busId),
2142
+ )
2143
+ // In the configured dense-plane mode, a second allowed layer is an
2144
+ // optional winding crossover channel rather than a requirement. Try
2145
+ // the simpler single-layer route first unless an already-routed narrow
2146
+ // bus occupies that direct winding channel. Preserve the released
2147
+ // multi-layer search order for callers that did not opt into this mode.
2148
+ const preferSingleLayerWinding =
2149
+ useConfiguredDensePlaneRouting &&
2150
+ singleLayerBus !== bus &&
2151
+ !embeddedNarrowBusAlreadyRouted
2152
+ let busPlans = (yield* routeAlternatives(
2153
+ preferSingleLayerWinding
2154
+ ? { ...routeParams, bus: singleLayerBus }
2155
+ : routeParams,
2156
+ 1,
2157
+ ))[0]
2158
+ if (!busPlans && preferSingleLayerWinding) {
2159
+ busPlans = (yield* routeAlternatives(routeParams, 1))[0]
2160
+ }
1349
2161
  if (busPlans && bus.maxLengthSkew !== undefined) {
1350
2162
  const lengths = busPlans.map((plan) => plan.length)
1351
2163
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -1360,7 +2172,7 @@ export class FanoutSolver extends BaseSolver {
1360
2172
  // skewed that compact meanders are unlikely to absorb the deficit.
1361
2173
  // This keeps already-near-matched buses on the single-attempt path.
1362
2174
  if (needsRouteDiversity) {
1363
- busPlans = routeBusAlternatives(routeParams, 3).toSorted(
2175
+ busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
1364
2176
  (first, second) => {
1365
2177
  const firstLengths = first.map((plan) => plan.length)
1366
2178
  const secondLengths = second.map((plan) => plan.length)
@@ -1374,16 +2186,43 @@ export class FanoutSolver extends BaseSolver {
1374
2186
  }
1375
2187
  }
1376
2188
  if (!busPlans) {
2189
+ debugDense("route:failed", bus.busId)
1377
2190
  return false
1378
2191
  }
1379
2192
  matchedPlans.push(...busPlans)
2193
+ debugDense("route:complete", bus.busId, busPlans.length)
1380
2194
  return true
1381
- }
2195
+ }.bind(this)
1382
2196
 
1383
2197
  const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0]!
1384
2198
  const routedBoundaryBuses: PreparedBus[] = []
1385
- if (routeMatchedBoundaryBus(firstBoundaryBus)) {
2199
+ const reserveAllPlaneDogbonesAfterFirstWideBus = (
2200
+ bus: PreparedBus,
2201
+ ): void => {
2202
+ if (
2203
+ bus.connections.length >= 8 &&
2204
+ !useConfiguredDensePlaneRouting &&
2205
+ process.env.FANOUT_DEBUG_NO_PLANE_EXPANSION !== "1" &&
2206
+ activeBoundaryReservationPlaneBuses.length < planeBuses.length
2207
+ ) {
2208
+ activeBoundaryReservationPlaneBuses = planeBuses
2209
+ debugDense("plane-reservations:expanded", planeBuses.length)
2210
+ }
2211
+ }
2212
+ const firstBoundaryBusRouted =
2213
+ yield* routeMatchedBoundaryBusSteps(firstBoundaryBus)
2214
+ this.setInProgressPlans({
2215
+ phase: "route-dense-boundary-buses",
2216
+ plans: matchedPlans,
2217
+ strategy: "default",
2218
+ unitIndex: ++denseWorkUnitIndex,
2219
+ unitCount: denseWorkUnitCount,
2220
+ busId: firstBoundaryBus.busId,
2221
+ })
2222
+ yield
2223
+ if (firstBoundaryBusRouted) {
1386
2224
  routedBoundaryBuses.push(firstBoundaryBus)
2225
+ reserveAllPlaneDogbonesAfterFirstWideBus(firstBoundaryBus)
1387
2226
  } else {
1388
2227
  matchedRoutingSucceeded = false
1389
2228
  }
@@ -1402,18 +2241,51 @@ export class FanoutSolver extends BaseSolver {
1402
2241
  candidateIndex++
1403
2242
  ) {
1404
2243
  const candidateBus = remainingBoundaryBuses[candidateIndex]!
2244
+ debugDense("candidate:start", candidateBus.busId)
2245
+ const candidateMatchingBase = new Map(fixedViaPointsByConnectionIndex)
2246
+ const debugLateFixedVias = process.env.FANOUT_DEBUG_LATE_FIXED_VIAS
2247
+ if (debugLateFixedVias) {
2248
+ const candidateConnectionIndices = new Set(
2249
+ candidateBus.connections.map(
2250
+ (connection) => connection.connectionIndex,
2251
+ ),
2252
+ )
2253
+ for (const entry of debugLateFixedVias.split(",")) {
2254
+ const parts = entry.split(":")
2255
+ const rawY = parts.pop()
2256
+ const rawX = parts.pop()
2257
+ const connectionName = parts.join(":")
2258
+ const connectionIndex = [...connectionNameByIndex].find(
2259
+ ([, name]) => name === connectionName,
2260
+ )?.[0]
2261
+ const x = Number(rawX)
2262
+ const y = Number(rawY)
2263
+ if (
2264
+ connectionIndex !== undefined &&
2265
+ candidateConnectionIndices.has(connectionIndex) &&
2266
+ Number.isFinite(x) &&
2267
+ Number.isFinite(y)
2268
+ ) {
2269
+ candidateMatchingBase.set(connectionIndex, { x, y })
2270
+ }
2271
+ }
2272
+ }
1405
2273
  const candidateHasFixedViaPoints = candidateBus.connections.every(
1406
2274
  (connection) =>
1407
- fixedViaPointsByConnectionIndex.has(connection.connectionIndex),
2275
+ candidateMatchingBase.has(connection.connectionIndex),
1408
2276
  )
1409
- const extendedViaPoints =
2277
+ const newlyMatchedViaPoints =
1410
2278
  jointViaPoints && candidateHasFixedViaPoints
1411
2279
  ? // The joint map is deliberately kept intact so getReservedVias()
1412
2280
  // blocks every already-reserved future through-barrel during A*.
1413
2281
  // Provisional singleton and plane dogbones are rematched later.
1414
- new Map(fixedViaPointsByConnectionIndex)
2282
+ new Map(candidateMatchingBase)
1415
2283
  : matchComponentDogboneViaSites(
1416
- [...planeBuses, ...routedBoundaryBuses, candidateBus],
2284
+ [
2285
+ ...activeBoundaryReservationPlaneBuses,
2286
+ ...routedBoundaryBuses,
2287
+ candidateBus,
2288
+ ],
1417
2289
  {
1418
2290
  viaDiameter: this.config.viaDiameter,
1419
2291
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1422,12 +2294,31 @@ export class FanoutSolver extends BaseSolver {
1422
2294
  maximumSearchStates: 100_000,
1423
2295
  preferredBoundaryPerpendicularSideByBusId,
1424
2296
  preferBoundaryOutwardByBusId,
1425
- fixedViaPointsByConnectionIndex:
1426
- fixedViaPointsByConnectionIndex,
2297
+ fixedViaPointsByConnectionIndex: candidateMatchingBase,
1427
2298
  blockingSegments,
2299
+ additionalObstacles: denseAdditionalObstacles,
2300
+ preferPlaneCheckerboardSites:
2301
+ useConfiguredDensePlaneRouting,
1428
2302
  canShareCopper,
1429
2303
  },
1430
2304
  )
2305
+ const extendedViaPoints = newlyMatchedViaPoints
2306
+ ? new Map([...candidateMatchingBase, ...newlyMatchedViaPoints])
2307
+ : null
2308
+ debugDense(
2309
+ "candidate:matched",
2310
+ candidateBus.busId,
2311
+ extendedViaPoints?.size ?? "failed",
2312
+ )
2313
+ this.setInProgressPlans({
2314
+ phase: "reserve-next-dense-boundary-bus",
2315
+ plans: matchedPlans,
2316
+ strategy: "default",
2317
+ unitIndex: denseWorkUnitIndex,
2318
+ unitCount: denseWorkUnitCount,
2319
+ busId: candidateBus.busId,
2320
+ })
2321
+ yield
1431
2322
  if (!extendedViaPoints) continue
1432
2323
  const previousFixedViaPoints = fixedViaPointsByConnectionIndex
1433
2324
  const previousPlanCount = matchedPlans.length
@@ -1441,7 +2332,12 @@ export class FanoutSolver extends BaseSolver {
1441
2332
  if (laterBuses.length === 1) {
1442
2333
  const laterBus = laterBuses[0]!
1443
2334
  const futureAssignment = matchComponentDogboneViaSites(
1444
- [...planeBuses, ...routedBoundaryBuses, candidateBus, laterBus],
2335
+ [
2336
+ ...activeBoundaryReservationPlaneBuses,
2337
+ ...routedBoundaryBuses,
2338
+ candidateBus,
2339
+ laterBus,
2340
+ ],
1445
2341
  {
1446
2342
  viaDiameter: this.config.viaDiameter,
1447
2343
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1452,9 +2348,16 @@ export class FanoutSolver extends BaseSolver {
1452
2348
  preferBoundaryOutwardByBusId,
1453
2349
  fixedViaPointsByConnectionIndex: extendedViaPoints,
1454
2350
  blockingSegments,
2351
+ additionalObstacles: denseAdditionalObstacles,
2352
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1455
2353
  canShareCopper,
1456
2354
  },
1457
2355
  )
2356
+ debugDense(
2357
+ "future:matched",
2358
+ laterBus.busId,
2359
+ futureAssignment?.size ?? "failed",
2360
+ )
1458
2361
  if (futureAssignment) {
1459
2362
  const candidateCountByConnectionIndex = new Map<number, number>()
1460
2363
  for (const candidate of getComponentDogboneViaSiteCandidates(
@@ -1465,6 +2368,8 @@ export class FanoutSolver extends BaseSolver {
1465
2368
  traceWidth: this.config.traceWidth,
1466
2369
  clearance: this.config.clearance,
1467
2370
  blockingSegments,
2371
+ additionalObstacles: denseAdditionalObstacles,
2372
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1468
2373
  canShareCopper,
1469
2374
  },
1470
2375
  )) {
@@ -1499,7 +2404,8 @@ export class FanoutSolver extends BaseSolver {
1499
2404
  }
1500
2405
  }
1501
2406
  fixedViaPointsByConnectionIndex = candidateFixedViaPoints
1502
- if (routeMatchedBoundaryBus(candidateBus)) {
2407
+ if (yield* routeMatchedBoundaryBusSteps(candidateBus)) {
2408
+ debugDense("lookahead:start", candidateBus.busId)
1503
2409
  const candidateLeavesAFeasibleExtension =
1504
2410
  laterBuses.length === 0 ||
1505
2411
  laterBuses.some((laterBus) => {
@@ -1512,7 +2418,7 @@ export class FanoutSolver extends BaseSolver {
1512
2418
  return Boolean(
1513
2419
  matchComponentDogboneViaSites(
1514
2420
  [
1515
- ...planeBuses,
2421
+ ...activeBoundaryReservationPlaneBuses,
1516
2422
  ...routedBoundaryBuses,
1517
2423
  candidateBus,
1518
2424
  laterBus,
@@ -1528,39 +2434,828 @@ export class FanoutSolver extends BaseSolver {
1528
2434
  fixedViaPointsByConnectionIndex:
1529
2435
  fixedViaPointsByConnectionIndex,
1530
2436
  blockingSegments: lookaheadBlockingSegments,
2437
+ additionalObstacles: denseAdditionalObstacles,
2438
+ preferPlaneCheckerboardSites:
2439
+ useConfiguredDensePlaneRouting,
1531
2440
  canShareCopper,
1532
2441
  },
1533
2442
  ),
1534
2443
  )
1535
- })
1536
- if (candidateLeavesAFeasibleExtension) {
1537
- selectedBusIndex = candidateIndex
1538
- routedBoundaryBuses.push(candidateBus)
1539
- break
2444
+ })
2445
+ debugDense(
2446
+ "lookahead:complete",
2447
+ candidateBus.busId,
2448
+ candidateLeavesAFeasibleExtension,
2449
+ )
2450
+ if (candidateLeavesAFeasibleExtension) {
2451
+ selectedBusIndex = candidateIndex
2452
+ routedBoundaryBuses.push(candidateBus)
2453
+ reserveAllPlaneDogbonesAfterFirstWideBus(candidateBus)
2454
+ this.setInProgressPlans({
2455
+ phase: "route-dense-boundary-buses",
2456
+ plans: matchedPlans,
2457
+ strategy: "default",
2458
+ unitIndex: ++denseWorkUnitIndex,
2459
+ unitCount: denseWorkUnitCount,
2460
+ busId: candidateBus.busId,
2461
+ })
2462
+ yield
2463
+ break
2464
+ }
2465
+ matchedPlans.splice(previousPlanCount)
2466
+ }
2467
+ fixedViaPointsByConnectionIndex = previousFixedViaPoints
2468
+ this.setInProgressPlans({
2469
+ phase: "retry-dense-boundary-bus",
2470
+ plans: matchedPlans,
2471
+ strategy: "default",
2472
+ unitIndex: denseWorkUnitIndex,
2473
+ unitCount: denseWorkUnitCount,
2474
+ busId: candidateBus.busId,
2475
+ })
2476
+ yield
2477
+ }
2478
+ if (selectedBusIndex < 0) {
2479
+ matchedRoutingSucceeded = false
2480
+ break
2481
+ }
2482
+ remainingBoundaryBuses.splice(selectedBusIndex, 1)
2483
+ }
2484
+ let matchedPlaneBusesInRoutingOrder: PreparedBus[] | null = null
2485
+ if (matchedRoutingSucceeded) {
2486
+ let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
2487
+ null
2488
+ let feasibleAlternatePlanePlans: FanoutRoutePlan[] = []
2489
+ const matchViaPointsAroundPlans = (
2490
+ candidatePlans: readonly FanoutRoutePlan[],
2491
+ ): Map<number, { x: number; y: number }> | null => {
2492
+ feasibleAlternatePlanePlans = []
2493
+ const fixedBoundaryViaPoints = new Map(
2494
+ candidatePlans.flatMap((plan) =>
2495
+ plan.via
2496
+ ? [[plan.connectionIndex, plan.via.center] as const]
2497
+ : [],
2498
+ ),
2499
+ )
2500
+ const blockingSegments = candidatePlans.flatMap((plan) =>
2501
+ plan.segments.map((segment) => ({
2502
+ connectionIndex: plan.connectionIndex,
2503
+ segment,
2504
+ })),
2505
+ )
2506
+ if (
2507
+ useConfiguredDensePlaneRouting ||
2508
+ process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
2509
+ ) {
2510
+ let incrementalViaPoints = new Map(fixedBoundaryViaPoints)
2511
+ const matchedPlaneBuses = [...activeBoundaryReservationPlaneBuses]
2512
+ for (const planeBus of matchedPlaneBuses) {
2513
+ for (const connection of planeBus.connections) {
2514
+ const reservedPoint = fixedViaPointsByConnectionIndex.get(
2515
+ connection.connectionIndex,
2516
+ )
2517
+ if (reservedPoint) {
2518
+ incrementalViaPoints.set(
2519
+ connection.connectionIndex,
2520
+ reservedPoint,
2521
+ )
2522
+ }
2523
+ }
2524
+ }
2525
+ for (const planeBus of matchedPlaneBuses) {
2526
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
2527
+ if (!targetLayer) return null
2528
+ const reservedPlanePlans = routeBus({
2529
+ srj: this.routingSrj,
2530
+ bus: planeBus,
2531
+ targetLayer,
2532
+ acceptedPlans: [
2533
+ ...candidatePlans,
2534
+ ...feasibleAlternatePlanePlans,
2535
+ ],
2536
+ layerNames: this.config.layerNames,
2537
+ traceWidth: this.config.traceWidth,
2538
+ viaDiameter: this.config.viaDiameter,
2539
+ viaHoleDiameter: this.config.viaHoleDiameter,
2540
+ clearance: this.config.clearance,
2541
+ compactBusTracks: this.config.compactBusTracks,
2542
+ allowBlindAndBuriedVias: false,
2543
+ allowSameNetMerges: this.config.allowSameNetMerges,
2544
+ staticClearanceCache: this.routeStaticClearanceCache,
2545
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
2546
+ })
2547
+ if (!reservedPlanePlans) return null
2548
+ feasibleAlternatePlanePlans.push(...reservedPlanePlans)
2549
+ }
2550
+ const independentlyUnmatchablePlaneBuses = planeBuses.filter(
2551
+ (planeBus) =>
2552
+ !matchedPlaneBuses.includes(planeBus) &&
2553
+ !matchComponentDogboneViaSites(
2554
+ [...matchedPlaneBuses, planeBus, ...boundaryBuses],
2555
+ {
2556
+ viaDiameter: this.config.viaDiameter,
2557
+ viaHoleDiameter: this.config.viaHoleDiameter,
2558
+ traceWidth: this.config.traceWidth,
2559
+ clearance: this.config.clearance,
2560
+ maximumSearchStates: 100_000,
2561
+ preferredBoundaryPerpendicularSideByBusId,
2562
+ preferBoundaryOutwardByBusId,
2563
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
2564
+ blockingSegments,
2565
+ additionalObstacles: denseAdditionalObstacles,
2566
+ preferPlaneCheckerboardSites:
2567
+ useConfiguredDensePlaneRouting,
2568
+ canShareCopper,
2569
+ },
2570
+ ),
2571
+ )
2572
+ const shallowestPlaneLayerIndex = Math.min(
2573
+ ...planeBuses.map((bus) =>
2574
+ this.config.layerNames.indexOf(
2575
+ params.busLayerAssignments[bus.busId] ?? "",
2576
+ ),
2577
+ ),
2578
+ )
2579
+ const deeperPlaneBuses =
2580
+ useConfiguredDensePlaneRouting ||
2581
+ process.env.FANOUT_DEBUG_ROUTE_DEEP_PLANES_FIRST === "1"
2582
+ ? planeBuses.filter(
2583
+ (bus) =>
2584
+ !matchedPlaneBuses.includes(bus) &&
2585
+ this.config.layerNames.indexOf(
2586
+ params.busLayerAssignments[bus.busId] ?? "",
2587
+ ) > shallowestPlaneLayerIndex,
2588
+ )
2589
+ : []
2590
+ const additionalAlternatePlaneBusIds = new Set([
2591
+ ...this.config.denseUnrestrictedPlaneRoutingBusIds,
2592
+ ...(process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(
2593
+ ",",
2594
+ ) ?? []),
2595
+ ])
2596
+ const additionalAlternatePlaneBuses = planeBuses.filter(
2597
+ (bus) =>
2598
+ !matchedPlaneBuses.includes(bus) &&
2599
+ additionalAlternatePlaneBusIds.has(bus.busId),
2600
+ )
2601
+ const alternatePlaneBuses = [
2602
+ ...deeperPlaneBuses,
2603
+ ...independentlyUnmatchablePlaneBuses.filter(
2604
+ (bus) => !deeperPlaneBuses.includes(bus),
2605
+ ),
2606
+ ...additionalAlternatePlaneBuses.filter(
2607
+ (bus) =>
2608
+ !deeperPlaneBuses.includes(bus) &&
2609
+ !independentlyUnmatchablePlaneBuses.includes(bus),
2610
+ ),
2611
+ ]
2612
+ const debugAlternatePlaneOrder =
2613
+ process.env.FANOUT_DEBUG_ALTERNATE_PLANE_ORDER?.split(",") ?? []
2614
+ const orderedAlternatePlaneBuses = alternatePlaneBuses.toSorted(
2615
+ (first, second) => {
2616
+ const firstLayerIndex = this.config.layerNames.indexOf(
2617
+ params.busLayerAssignments[first.busId] ?? "",
2618
+ )
2619
+ const secondLayerIndex = this.config.layerNames.indexOf(
2620
+ params.busLayerAssignments[second.busId] ?? "",
2621
+ )
2622
+ if (
2623
+ firstLayerIndex !== secondLayerIndex &&
2624
+ process.env.FANOUT_DEBUG_ALTERNATE_IGNORE_LAYERS !== "1"
2625
+ ) {
2626
+ return process.env.FANOUT_DEBUG_ALTERNATE_SHALLOW_FIRST ===
2627
+ "1"
2628
+ ? firstLayerIndex - secondLayerIndex
2629
+ : secondLayerIndex - firstLayerIndex
2630
+ }
2631
+ const firstPriority = debugAlternatePlaneOrder.indexOf(
2632
+ first.busId,
2633
+ )
2634
+ const secondPriority = debugAlternatePlaneOrder.indexOf(
2635
+ second.busId,
2636
+ )
2637
+ return (
2638
+ (firstPriority < 0
2639
+ ? debugAlternatePlaneOrder.length
2640
+ : firstPriority) -
2641
+ (secondPriority < 0
2642
+ ? debugAlternatePlaneOrder.length
2643
+ : secondPriority) ||
2644
+ first.connections[0]!.connectionIndex -
2645
+ second.connections[0]!.connectionIndex
2646
+ )
2647
+ },
2648
+ )
2649
+ if (alternatePlaneBuses.length > 0) {
2650
+ debugDense(
2651
+ "plane-match:preflight-failed",
2652
+ independentlyUnmatchablePlaneBuses.map((bus) => bus.busId),
2653
+ )
2654
+ if (
2655
+ !useConfiguredDensePlaneRouting &&
2656
+ process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1"
2657
+ ) {
2658
+ return null
2659
+ }
2660
+ let alternatePlaneSearchStates = 0
2661
+ const maximumAlternatePlaneSearchStates = Number(
2662
+ process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ??
2663
+ (useConfiguredDensePlaneRouting ? 3_000_000 : 1_000),
2664
+ )
2665
+ const maximumAlternatePlaneRoutes = Number(
2666
+ process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
2667
+ (useConfiguredDensePlaneRouting ? 128 : 8),
2668
+ )
2669
+ let deepestAlternatePlaneSearchIndex = 0
2670
+ const alternatePlaneFailureCountByBusId = new Map<
2671
+ string,
2672
+ number
2673
+ >()
2674
+ const getPlaneRouteAlternatives = (
2675
+ planeBus: PreparedBus,
2676
+ additionalAcceptedPlans: FanoutRoutePlan[],
2677
+ maximumRoutes = maximumAlternatePlaneRoutes,
2678
+ ): FanoutRoutePlan[][] => {
2679
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
2680
+ if (!targetLayer) return []
2681
+ return routeBusAlternatives(
2682
+ {
2683
+ srj: this.routingSrj,
2684
+ bus: planeBus,
2685
+ targetLayer,
2686
+ acceptedPlans: [
2687
+ ...candidatePlans,
2688
+ ...additionalAcceptedPlans,
2689
+ ],
2690
+ layerNames: this.config.layerNames,
2691
+ traceWidth: this.config.traceWidth,
2692
+ viaDiameter: this.config.viaDiameter,
2693
+ viaHoleDiameter: this.config.viaHoleDiameter,
2694
+ clearance: this.config.clearance,
2695
+ compactBusTracks: this.config.compactBusTracks,
2696
+ allowBlindAndBuriedVias: false,
2697
+ allowSameNetMerges: this.config.allowSameNetMerges,
2698
+ staticClearanceCache: this.routeStaticClearanceCache,
2699
+ },
2700
+ maximumRoutes,
2701
+ )
2702
+ }
2703
+ const routeAlternatePlaneBuses = (
2704
+ remainingPlaneBuses: PreparedBus[],
2705
+ acceptedAlternatePlans: FanoutRoutePlan[],
2706
+ ): FanoutRoutePlan[] | null => {
2707
+ if (remainingPlaneBuses.length === 0) {
2708
+ return acceptedAlternatePlans
2709
+ }
2710
+ if (
2711
+ alternatePlaneSearchStates >=
2712
+ maximumAlternatePlaneSearchStates
2713
+ ) {
2714
+ return null
2715
+ }
2716
+ deepestAlternatePlaneSearchIndex = Math.max(
2717
+ deepestAlternatePlaneSearchIndex,
2718
+ orderedAlternatePlaneBuses.length -
2719
+ remainingPlaneBuses.length,
2720
+ )
2721
+ const alternativesByBus = remainingPlaneBuses.map(
2722
+ (planeBus) => ({
2723
+ planeBus,
2724
+ alternatives: getPlaneRouteAlternatives(
2725
+ planeBus,
2726
+ acceptedAlternatePlans,
2727
+ ),
2728
+ }),
2729
+ )
2730
+ const orderedSelections =
2731
+ process.env.FANOUT_DEBUG_DYNAMIC_ALTERNATE_ORDER === "1"
2732
+ ? alternativesByBus.toSorted(
2733
+ (first, second) =>
2734
+ first.alternatives.length -
2735
+ second.alternatives.length ||
2736
+ orderedAlternatePlaneBuses.indexOf(first.planeBus) -
2737
+ orderedAlternatePlaneBuses.indexOf(second.planeBus),
2738
+ )
2739
+ : alternativesByBus
2740
+ const selected = orderedSelections[0]!
2741
+ const { planeBus, alternatives } = selected
2742
+ if (
2743
+ process.env.FANOUT_DEBUG_ALTERNATE_CHOICES === "1" &&
2744
+ alternatePlaneSearchStates < 64
2745
+ ) {
2746
+ debugDense(
2747
+ "plane-route:alternate-choice",
2748
+ `depth:${orderedAlternatePlaneBuses.length - remainingPlaneBuses.length}`,
2749
+ planeBus.busId,
2750
+ alternativesByBus.map((entry) => [
2751
+ entry.planeBus.busId,
2752
+ entry.alternatives.length,
2753
+ ]),
2754
+ )
2755
+ }
2756
+ if (alternatives.length === 0) {
2757
+ alternatePlaneFailureCountByBusId.set(
2758
+ planeBus.busId,
2759
+ (alternatePlaneFailureCountByBusId.get(planeBus.busId) ??
2760
+ 0) + 1,
2761
+ )
2762
+ return null
2763
+ }
2764
+ const selectionsToSearch =
2765
+ process.env.FANOUT_DEBUG_BRANCH_ALTERNATE_ORDER === "1"
2766
+ ? orderedSelections
2767
+ : [selected]
2768
+ const alternateActions = selectionsToSearch.flatMap(
2769
+ (selection) =>
2770
+ selection.alternatives.map((alternative) => ({
2771
+ selection,
2772
+ alternative,
2773
+ remaining: remainingPlaneBuses.filter(
2774
+ (candidate) => candidate !== selection.planeBus,
2775
+ ),
2776
+ })),
2777
+ )
2778
+ const orderedActions =
2779
+ process.env.FANOUT_DEBUG_LEAST_CONSTRAINING_ALTERNATES === "1"
2780
+ ? alternateActions
2781
+ .map((action) => {
2782
+ const acceptedPlans = [
2783
+ ...acceptedAlternatePlans,
2784
+ ...action.alternative,
2785
+ ]
2786
+ const remainingOptionCounts = action.remaining.map(
2787
+ (remainingBus) =>
2788
+ getPlaneRouteAlternatives(
2789
+ remainingBus,
2790
+ acceptedPlans,
2791
+ Math.min(4, maximumAlternatePlaneRoutes),
2792
+ ).length,
2793
+ )
2794
+ return {
2795
+ ...action,
2796
+ remainingOptionCounts,
2797
+ minimumRemainingOptions:
2798
+ remainingOptionCounts.length === 0
2799
+ ? Number.POSITIVE_INFINITY
2800
+ : Math.min(...remainingOptionCounts),
2801
+ totalRemainingOptions: remainingOptionCounts.reduce(
2802
+ (total, count) => total + count,
2803
+ 0,
2804
+ ),
2805
+ }
2806
+ })
2807
+ .filter(
2808
+ (action) => action.minimumRemainingOptions !== 0,
2809
+ )
2810
+ .toSorted(
2811
+ (first, second) =>
2812
+ second.minimumRemainingOptions -
2813
+ first.minimumRemainingOptions ||
2814
+ second.totalRemainingOptions -
2815
+ first.totalRemainingOptions,
2816
+ )
2817
+ : alternateActions
2818
+ for (const action of orderedActions) {
2819
+ alternatePlaneSearchStates++
2820
+ const completedPlans = routeAlternatePlaneBuses(
2821
+ action.remaining,
2822
+ [...acceptedAlternatePlans, ...action.alternative],
2823
+ )
2824
+ if (completedPlans) return completedPlans
2825
+ if (
2826
+ alternatePlaneSearchStates >=
2827
+ maximumAlternatePlaneSearchStates
2828
+ ) {
2829
+ break
2830
+ }
2831
+ }
2832
+ alternatePlaneFailureCountByBusId.set(
2833
+ planeBus.busId,
2834
+ (alternatePlaneFailureCountByBusId.get(planeBus.busId) ?? 0) +
2835
+ 1,
2836
+ )
2837
+ return null
2838
+ }
2839
+ let alternatePlanePlans: FanoutRoutePlan[] | null
2840
+ if (
2841
+ useConfiguredDensePlaneRouting ||
2842
+ process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1"
2843
+ ) {
2844
+ type IndependentPlaneRouteCandidate = {
2845
+ key: string
2846
+ planeBus: PreparedBus
2847
+ plans: FanoutRoutePlan[]
2848
+ }
2849
+ const candidateSets = orderedAlternatePlaneBuses.map(
2850
+ (planeBus) => ({
2851
+ planeBus,
2852
+ candidates: getPlaneRouteAlternatives(
2853
+ planeBus,
2854
+ feasibleAlternatePlanePlans,
2855
+ ).map((plans, index) => ({
2856
+ key: `${planeBus.busId}:${index}`,
2857
+ planeBus,
2858
+ plans,
2859
+ })),
2860
+ }),
2861
+ )
2862
+ debugDense(
2863
+ "plane-route:alternate-candidate-counts",
2864
+ candidateSets.map((candidateSet) => [
2865
+ candidateSet.planeBus.busId,
2866
+ candidateSet.candidates.length,
2867
+ ]),
2868
+ )
2869
+ const compatibilityByCandidatePair = new Map<string, boolean>()
2870
+ const candidatesAreCompatible = (
2871
+ first: IndependentPlaneRouteCandidate,
2872
+ second: IndependentPlaneRouteCandidate,
2873
+ ): boolean => {
2874
+ const cacheKey = [first.key, second.key].toSorted().join("|")
2875
+ const cached = compatibilityByCandidatePair.get(cacheKey)
2876
+ if (cached !== undefined) return cached
2877
+ const compatible = fanoutPlansAreMutuallyClear({
2878
+ plans: [...first.plans, ...second.plans],
2879
+ srj: this.routingSrj,
2880
+ clearance: this.config.clearance,
2881
+ allowSameNetMerges: this.config.allowSameNetMerges,
2882
+ })
2883
+ compatibilityByCandidatePair.set(cacheKey, compatible)
2884
+ return compatible
2885
+ }
2886
+ const selectCompatiblePlaneRoutes = (
2887
+ remainingCandidateSets: typeof candidateSets,
2888
+ selectedCandidates: IndependentPlaneRouteCandidate[],
2889
+ ): IndependentPlaneRouteCandidate[] | null => {
2890
+ if (remainingCandidateSets.length === 0) {
2891
+ return selectedCandidates
2892
+ }
2893
+ if (
2894
+ alternatePlaneSearchStates >=
2895
+ maximumAlternatePlaneSearchStates
2896
+ ) {
2897
+ return null
2898
+ }
2899
+ deepestAlternatePlaneSearchIndex = Math.max(
2900
+ deepestAlternatePlaneSearchIndex,
2901
+ orderedAlternatePlaneBuses.length -
2902
+ remainingCandidateSets.length,
2903
+ )
2904
+ const selectedSet = remainingCandidateSets.toSorted(
2905
+ (first, second) =>
2906
+ first.candidates.length - second.candidates.length,
2907
+ )[0]!
2908
+ if (selectedSet.candidates.length === 0) {
2909
+ alternatePlaneFailureCountByBusId.set(
2910
+ selectedSet.planeBus.busId,
2911
+ (alternatePlaneFailureCountByBusId.get(
2912
+ selectedSet.planeBus.busId,
2913
+ ) ?? 0) + 1,
2914
+ )
2915
+ return null
2916
+ }
2917
+ const otherSets = remainingCandidateSets.filter(
2918
+ (candidateSet) => candidateSet !== selectedSet,
2919
+ )
2920
+ const candidateBatchSize = Number(
2921
+ process.env.FANOUT_DEBUG_ALTERNATE_CANDIDATE_BATCH_SIZE ??
2922
+ 16,
2923
+ )
2924
+ for (
2925
+ let batchStart = 0;
2926
+ batchStart < selectedSet.candidates.length;
2927
+ batchStart += candidateBatchSize
2928
+ ) {
2929
+ const actions = selectedSet.candidates
2930
+ .slice(batchStart, batchStart + candidateBatchSize)
2931
+ .map((candidate) => {
2932
+ const projectedSets = otherSets.map((candidateSet) => ({
2933
+ ...candidateSet,
2934
+ candidates: candidateSet.candidates.filter(
2935
+ (otherCandidate) =>
2936
+ candidatesAreCompatible(
2937
+ candidate,
2938
+ otherCandidate,
2939
+ ),
2940
+ ),
2941
+ }))
2942
+ const projectedCounts = projectedSets.map(
2943
+ (candidateSet) => candidateSet.candidates.length,
2944
+ )
2945
+ return {
2946
+ candidate,
2947
+ projectedSets,
2948
+ minimumProjectedCount:
2949
+ projectedCounts.length === 0
2950
+ ? Number.POSITIVE_INFINITY
2951
+ : Math.min(...projectedCounts),
2952
+ totalProjectedCount: projectedCounts.reduce(
2953
+ (total, count) => total + count,
2954
+ 0,
2955
+ ),
2956
+ }
2957
+ })
2958
+ .filter((action) => action.minimumProjectedCount !== 0)
2959
+ .toSorted(
2960
+ (first, second) =>
2961
+ second.minimumProjectedCount -
2962
+ first.minimumProjectedCount ||
2963
+ second.totalProjectedCount -
2964
+ first.totalProjectedCount,
2965
+ )
2966
+ for (const action of actions) {
2967
+ alternatePlaneSearchStates++
2968
+ const selected = selectCompatiblePlaneRoutes(
2969
+ action.projectedSets,
2970
+ [...selectedCandidates, action.candidate],
2971
+ )
2972
+ if (selected) return selected
2973
+ if (
2974
+ alternatePlaneSearchStates >=
2975
+ maximumAlternatePlaneSearchStates
2976
+ ) {
2977
+ break
2978
+ }
2979
+ }
2980
+ if (
2981
+ alternatePlaneSearchStates >=
2982
+ maximumAlternatePlaneSearchStates
2983
+ ) {
2984
+ break
2985
+ }
2986
+ }
2987
+ alternatePlaneFailureCountByBusId.set(
2988
+ selectedSet.planeBus.busId,
2989
+ (alternatePlaneFailureCountByBusId.get(
2990
+ selectedSet.planeBus.busId,
2991
+ ) ?? 0) + 1,
2992
+ )
2993
+ return null
2994
+ }
2995
+ const selectedCandidates = selectCompatiblePlaneRoutes(
2996
+ candidateSets,
2997
+ [],
2998
+ )
2999
+ alternatePlanePlans = selectedCandidates
3000
+ ? [
3001
+ ...feasibleAlternatePlanePlans,
3002
+ ...selectedCandidates.flatMap(
3003
+ (candidate) => candidate.plans,
3004
+ ),
3005
+ ]
3006
+ : null
3007
+ } else {
3008
+ alternatePlanePlans = routeAlternatePlaneBuses(
3009
+ orderedAlternatePlaneBuses,
3010
+ feasibleAlternatePlanePlans,
3011
+ )
3012
+ }
3013
+ debugDense(
3014
+ "plane-route:alternate-search",
3015
+ alternatePlanePlans ? "complete" : "failed",
3016
+ alternatePlaneSearchStates,
3017
+ `depth:${deepestAlternatePlaneSearchIndex}/${orderedAlternatePlaneBuses.length}`,
3018
+ [...alternatePlaneFailureCountByBusId].toSorted(
3019
+ ([, first], [, second]) => second - first,
3020
+ ),
3021
+ )
3022
+ if (!alternatePlanePlans) return null
3023
+ feasibleAlternatePlanePlans = alternatePlanePlans
3024
+ }
3025
+ let planeBusIdsRoutedWithoutDogbones = new Set<string>()
3026
+ let allBlockingSegments = [...blockingSegments]
3027
+ let alternateBlockingVias: {
3028
+ connectionIndex: number
3029
+ center: { x: number; y: number }
3030
+ diameter: number
3031
+ spanLayers: readonly string[]
3032
+ }[] = []
3033
+ let planeBusesToMatch = [...planeBuses]
3034
+ let candidateCountByConnectionIndex = new Map<number, number>()
3035
+ let candidatePointsByConnectionIndex = new Map<
3036
+ number,
3037
+ { x: number; y: number }[]
3038
+ >()
3039
+ const refreshPlaneDogboneCandidates = (): void => {
3040
+ planeBusIdsRoutedWithoutDogbones = new Set(
3041
+ feasibleAlternatePlanePlans.map((plan) => plan.busId),
3042
+ )
3043
+ const alternateBlockingSegments =
3044
+ feasibleAlternatePlanePlans.flatMap((plan) =>
3045
+ [...plan.segments, ...(plan.planeEndpointSegments ?? [])].map(
3046
+ (segment) => ({
3047
+ connectionIndex: plan.connectionIndex,
3048
+ segment,
3049
+ }),
3050
+ ),
3051
+ )
3052
+ allBlockingSegments = [
3053
+ ...blockingSegments,
3054
+ ...alternateBlockingSegments,
3055
+ ]
3056
+ alternateBlockingVias = feasibleAlternatePlanePlans.flatMap(
3057
+ (plan) =>
3058
+ [
3059
+ plan.via,
3060
+ ...(plan.additionalVias ?? []),
3061
+ plan.planeEndpointVia,
3062
+ ].flatMap((via) =>
3063
+ via
3064
+ ? [
3065
+ {
3066
+ connectionIndex: plan.connectionIndex,
3067
+ center: via.center,
3068
+ diameter: via.diameter,
3069
+ spanLayers: via.spanLayers,
3070
+ },
3071
+ ]
3072
+ : [],
3073
+ ),
3074
+ )
3075
+ planeBusesToMatch = planeBuses.filter(
3076
+ (bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId),
3077
+ )
3078
+ candidateCountByConnectionIndex = new Map()
3079
+ candidatePointsByConnectionIndex = new Map()
3080
+ for (const candidate of getComponentDogboneViaSiteCandidates(
3081
+ planeBusesToMatch,
3082
+ {
3083
+ viaDiameter: this.config.viaDiameter,
3084
+ viaHoleDiameter: this.config.viaHoleDiameter,
3085
+ traceWidth: this.config.traceWidth,
3086
+ clearance: this.config.clearance,
3087
+ blockingSegments: allBlockingSegments,
3088
+ blockingVias: alternateBlockingVias,
3089
+ additionalObstacles: denseAdditionalObstacles,
3090
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3091
+ canShareCopper,
3092
+ },
3093
+ )) {
3094
+ candidateCountByConnectionIndex.set(
3095
+ candidate.connectionIndex,
3096
+ (candidateCountByConnectionIndex.get(
3097
+ candidate.connectionIndex,
3098
+ ) ?? 0) + 1,
3099
+ )
3100
+ const points =
3101
+ candidatePointsByConnectionIndex.get(
3102
+ candidate.connectionIndex,
3103
+ ) ?? []
3104
+ points.push(candidate.point)
3105
+ candidatePointsByConnectionIndex.set(
3106
+ candidate.connectionIndex,
3107
+ points,
3108
+ )
3109
+ }
3110
+ }
3111
+ refreshPlaneDogboneCandidates()
3112
+ for (
3113
+ let promotionPass = 0;
3114
+ promotionPass < planeBuses.length;
3115
+ promotionPass++
3116
+ ) {
3117
+ const zeroCandidatePlaneBuses = planeBusesToMatch.filter(
3118
+ (bus) =>
3119
+ !candidateCountByConnectionIndex.has(
3120
+ bus.connections[0]!.connectionIndex,
3121
+ ),
3122
+ )
3123
+ if (zeroCandidatePlaneBuses.length === 0) break
3124
+ debugDense(
3125
+ "plane-route:promote-zero-candidates",
3126
+ zeroCandidatePlaneBuses.map((bus) => bus.busId),
3127
+ )
3128
+ if (
3129
+ zeroCandidatePlaneBuses.some((bus) =>
3130
+ matchedPlaneBuses.includes(bus),
3131
+ )
3132
+ ) {
3133
+ return null
3134
+ }
3135
+ for (const planeBus of zeroCandidatePlaneBuses) {
3136
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
3137
+ if (!targetLayer) return null
3138
+ const promotedPlans = routeBusAlternatives(
3139
+ {
3140
+ srj: this.routingSrj,
3141
+ bus: planeBus,
3142
+ targetLayer,
3143
+ acceptedPlans: [
3144
+ ...candidatePlans,
3145
+ ...feasibleAlternatePlanePlans,
3146
+ ],
3147
+ layerNames: this.config.layerNames,
3148
+ traceWidth: this.config.traceWidth,
3149
+ viaDiameter: this.config.viaDiameter,
3150
+ viaHoleDiameter: this.config.viaHoleDiameter,
3151
+ clearance: this.config.clearance,
3152
+ compactBusTracks: this.config.compactBusTracks,
3153
+ allowBlindAndBuriedVias: false,
3154
+ allowSameNetMerges: this.config.allowSameNetMerges,
3155
+ staticClearanceCache: this.routeStaticClearanceCache,
3156
+ },
3157
+ 8,
3158
+ )[0]
3159
+ if (!promotedPlans) {
3160
+ debugDense("plane-route:promote-failed", planeBus.busId)
3161
+ return null
3162
+ }
3163
+ feasibleAlternatePlanePlans.push(...promotedPlans)
3164
+ }
3165
+ refreshPlaneDogboneCandidates()
3166
+ }
3167
+ const debugPlaneMatchOrder =
3168
+ process.env.FANOUT_DEBUG_PLANE_MATCH_ORDER?.split(",") ?? []
3169
+ const incrementalPlaneBuses = planeBusesToMatch.toSorted(
3170
+ (first, second) => {
3171
+ const candidateCountDifference =
3172
+ (candidateCountByConnectionIndex.get(
3173
+ first.connections[0]!.connectionIndex,
3174
+ ) ?? 0) -
3175
+ (candidateCountByConnectionIndex.get(
3176
+ second.connections[0]!.connectionIndex,
3177
+ ) ?? 0)
3178
+ if (candidateCountDifference !== 0) {
3179
+ return candidateCountDifference
3180
+ }
3181
+ const firstPriority = debugPlaneMatchOrder.indexOf(first.busId)
3182
+ const secondPriority = debugPlaneMatchOrder.indexOf(
3183
+ second.busId,
3184
+ )
3185
+ const priorityDifference =
3186
+ (firstPriority < 0
3187
+ ? debugPlaneMatchOrder.length
3188
+ : firstPriority) -
3189
+ (secondPriority < 0
3190
+ ? debugPlaneMatchOrder.length
3191
+ : secondPriority)
3192
+ if (priorityDifference !== 0) return priorityDifference
3193
+ return (
3194
+ first.connections[0]!.connectionIndex -
3195
+ second.connections[0]!.connectionIndex
3196
+ )
3197
+ },
3198
+ )
3199
+ for (const planeBus of incrementalPlaneBuses) {
3200
+ if (matchedPlaneBuses.includes(planeBus)) continue
3201
+ if (
3202
+ process.env.FANOUT_DEBUG_PLANE_CANDIDATES?.split(",").includes(
3203
+ planeBus.busId,
3204
+ )
3205
+ ) {
3206
+ debugDense(
3207
+ "plane-match:candidates",
3208
+ planeBus.busId,
3209
+ candidatePointsByConnectionIndex.get(
3210
+ planeBus.connections[0]!.connectionIndex,
3211
+ ),
3212
+ )
3213
+ }
3214
+ const nextViaPoints = matchComponentDogboneViaSites(
3215
+ [...matchedPlaneBuses, planeBus, ...boundaryBuses],
3216
+ {
3217
+ viaDiameter: this.config.viaDiameter,
3218
+ viaHoleDiameter: this.config.viaHoleDiameter,
3219
+ traceWidth: this.config.traceWidth,
3220
+ clearance: this.config.clearance,
3221
+ maximumSearchStates: 100_000,
3222
+ preferredBoundaryPerpendicularSideByBusId,
3223
+ preferBoundaryOutwardByBusId,
3224
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
3225
+ blockingSegments: allBlockingSegments,
3226
+ blockingVias: alternateBlockingVias,
3227
+ additionalObstacles: denseAdditionalObstacles,
3228
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3229
+ canShareCopper,
3230
+ },
3231
+ )
3232
+ debugDense(
3233
+ nextViaPoints
3234
+ ? "plane-match:incremental"
3235
+ : "plane-match:incremental-failed",
3236
+ planeBus.busId,
3237
+ candidateCountByConnectionIndex.get(
3238
+ planeBus.connections[0]!.connectionIndex,
3239
+ ) ?? 0,
3240
+ nextViaPoints?.get(planeBus.connections[0]!.connectionIndex),
3241
+ nextViaPoints?.size ?? "failed",
3242
+ )
3243
+ if (!nextViaPoints) return null
3244
+ incrementalViaPoints = new Map([
3245
+ ...incrementalViaPoints,
3246
+ ...nextViaPoints,
3247
+ ])
3248
+ matchedPlaneBuses.push(planeBus)
1540
3249
  }
1541
- matchedPlans.splice(previousPlanCount)
3250
+ matchedPlaneBusesInRoutingOrder = matchedPlaneBuses.filter(
3251
+ (bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId),
3252
+ )
3253
+ debugDense(
3254
+ "plane-match:incremental-complete",
3255
+ incrementalViaPoints.size,
3256
+ )
3257
+ return incrementalViaPoints
1542
3258
  }
1543
- fixedViaPointsByConnectionIndex = previousFixedViaPoints
1544
- }
1545
- if (selectedBusIndex < 0) {
1546
- matchedRoutingSucceeded = false
1547
- break
1548
- }
1549
- remainingBoundaryBuses.splice(selectedBusIndex, 1)
1550
- }
1551
- if (matchedRoutingSucceeded) {
1552
- let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
1553
- null
1554
- const matchViaPointsAroundPlans = (
1555
- candidatePlans: readonly FanoutRoutePlan[],
1556
- ): Map<number, { x: number; y: number }> | null => {
1557
- const fixedBoundaryViaPoints = new Map(
1558
- candidatePlans.flatMap((plan) =>
1559
- plan.via
1560
- ? [[plan.connectionIndex, plan.via.center] as const]
1561
- : [],
1562
- ),
1563
- )
1564
3259
  return matchComponentDogboneViaSites(
1565
3260
  [...planeBuses, ...boundaryBuses],
1566
3261
  {
@@ -1572,16 +3267,14 @@ export class FanoutSolver extends BaseSolver {
1572
3267
  preferredBoundaryPerpendicularSideByBusId,
1573
3268
  preferBoundaryOutwardByBusId,
1574
3269
  fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
1575
- blockingSegments: candidatePlans.flatMap((plan) =>
1576
- plan.segments.map((segment) => ({
1577
- connectionIndex: plan.connectionIndex,
1578
- segment,
1579
- })),
1580
- ),
3270
+ blockingSegments,
3271
+ additionalObstacles: denseAdditionalObstacles,
3272
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1581
3273
  canShareCopper,
1582
3274
  },
1583
3275
  )
1584
3276
  }
3277
+ debugDense("length-match:start", matchedPlans.length)
1585
3278
  const matchedLengthResult = matchBusPlanLengths({
1586
3279
  plans: matchedPlans,
1587
3280
  preparedBuses: this.preparedBuses,
@@ -1598,22 +3291,45 @@ export class FanoutSolver extends BaseSolver {
1598
3291
  return true
1599
3292
  },
1600
3293
  })
3294
+ debugDense(
3295
+ "length-match:complete",
3296
+ matchedLengthResult.plans?.length ?? "failed",
3297
+ )
3298
+ this.setInProgressPlans({
3299
+ phase: "match-dense-boundary-lengths",
3300
+ plans: matchedLengthResult.plans ?? matchedPlans,
3301
+ strategy: "default",
3302
+ unitIndex: ++denseWorkUnitIndex,
3303
+ unitCount: denseWorkUnitCount,
3304
+ })
3305
+ yield
1601
3306
  if (matchedLengthResult.plans) {
1602
3307
  matchedPlans = matchedLengthResult.plans
1603
3308
  const rematchedViaPoints =
1604
3309
  feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans)
1605
3310
  if (rematchedViaPoints) {
1606
3311
  fixedViaPointsByConnectionIndex = rematchedViaPoints
3312
+ matchedPlans.push(...feasibleAlternatePlanePlans)
1607
3313
  } else {
1608
3314
  matchedRoutingSucceeded = false
1609
3315
  }
1610
3316
  } else {
1611
3317
  matchedRoutingSucceeded = false
1612
3318
  }
3319
+ this.setInProgressPlans({
3320
+ phase: "rematch-dense-via-sites",
3321
+ plans: matchedPlans,
3322
+ strategy: "default",
3323
+ unitIndex: denseWorkUnitIndex,
3324
+ unitCount: denseWorkUnitCount,
3325
+ })
3326
+ yield
1613
3327
  }
1614
3328
  if (matchedRoutingSucceeded) {
1615
- for (const bus of planeBuses) {
3329
+ for (const bus of matchedPlaneBusesInRoutingOrder ?? planeBuses) {
3330
+ debugDense("plane-route:start", bus.busId)
1616
3331
  const targetLayer = params.busLayerAssignments[bus.busId]
3332
+ const blockingBusCounts = new Map<string, number>()
1617
3333
  const busPlans = targetLayer
1618
3334
  ? routeBus({
1619
3335
  srj: this.routingSrj,
@@ -1629,17 +3345,36 @@ export class FanoutSolver extends BaseSolver {
1629
3345
  allowBlindAndBuriedVias: false,
1630
3346
  allowSameNetMerges: this.config.allowSameNetMerges,
1631
3347
  staticClearanceCache: this.routeStaticClearanceCache,
3348
+ blockingBusCounts,
1632
3349
  fixedViaPointsByConnectionIndex,
1633
3350
  })
1634
3351
  : null
1635
3352
  if (!busPlans) {
3353
+ debugDense(
3354
+ "plane-route:failed",
3355
+ bus.busId,
3356
+ fixedViaPointsByConnectionIndex.get(
3357
+ bus.connections[0]!.connectionIndex,
3358
+ ),
3359
+ [...blockingBusCounts],
3360
+ )
1636
3361
  matchedRoutingSucceeded = false
1637
3362
  break
1638
3363
  }
1639
3364
  matchedPlans.push(...busPlans)
3365
+ debugDense("plane-route:complete", bus.busId)
3366
+ this.setInProgressPlans({
3367
+ phase: "route-dense-plane-buses",
3368
+ plans: matchedPlans,
3369
+ strategy: "default",
3370
+ unitIndex: ++denseWorkUnitIndex,
3371
+ unitCount: denseWorkUnitCount,
3372
+ busId: bus.busId,
3373
+ })
3374
+ yield
1640
3375
  }
1641
3376
  }
1642
- if (
3377
+ const densePlansAreClear =
1643
3378
  matchedRoutingSucceeded &&
1644
3379
  fanoutPlansAreClear({
1645
3380
  plans: matchedPlans,
@@ -1649,11 +3384,19 @@ export class FanoutSolver extends BaseSolver {
1649
3384
  allowBlindAndBuriedVias: false,
1650
3385
  allowSameNetMerges: this.config.allowSameNetMerges,
1651
3386
  })
1652
- ) {
3387
+ debugDense(
3388
+ "dense-validation",
3389
+ matchedRoutingSucceeded,
3390
+ matchedPlans.length,
3391
+ densePlansAreClear,
3392
+ )
3393
+ if (densePlansAreClear) {
1653
3394
  return { plans: matchedPlans, failedBusIds: [] }
1654
3395
  }
1655
3396
  }
1656
3397
 
3398
+ if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1") return null
3399
+
1657
3400
  const maximumStates = 8
1658
3401
  const getBoundaryStates = (
1659
3402
  alternativesPerBoundaryBus: number,
@@ -2003,11 +3746,11 @@ export class FanoutSolver extends BaseSolver {
2003
3746
  return bestState
2004
3747
  }
2005
3748
 
2006
- private evaluateAssignmentWithStrategy(
3749
+ private *evaluateAssignmentWithStrategySteps(
2007
3750
  assignmentIndex: number,
2008
3751
  busLayerAssignments: Readonly<Record<string, string>>,
2009
3752
  routingStrategy: RoutingStrategy,
2010
- ): EvaluatedAssignment {
3753
+ ): Generator<FanoutWorkYield, EvaluatedAssignment, unknown> {
2011
3754
  let plans: AssignmentAttempt["plans"] = []
2012
3755
  let failedBusIds: string[] = []
2013
3756
  let blockingBusCounts = new Map<string, number>()
@@ -2026,21 +3769,53 @@ export class FanoutSolver extends BaseSolver {
2026
3769
  clearance: this.config.clearance,
2027
3770
  borderDistribution: this.config.borderDistribution,
2028
3771
  }
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)
3772
+ let singleLayerPlans = routeSingleLayerWithPushAndShove(singleLayerParams)
3773
+ if (!singleLayerPlans && this.config.singleLayerAdaptiveExits) {
3774
+ this.setInProgressPlans({
3775
+ phase: "prepare-single-layer-adaptive-exits",
3776
+ plans,
3777
+ strategy: routingStrategy,
3778
+ })
3779
+ yield
3780
+ this.setInProgressPlans({
3781
+ phase: "route-single-layer-adaptive-exits",
3782
+ plans,
3783
+ strategy: routingStrategy,
3784
+ })
3785
+ this.activeAdaptiveVisualization = null
3786
+ const adaptiveSolver = this.createWorkSolver(
3787
+ "SingleLayerAdaptiveExitSolver",
3788
+ routeSingleLayerWithAdaptiveExitsSteps({
3789
+ ...singleLayerParams,
3790
+ availableBoundaryRegions: resolveAvailableBoundaryRegions(
3791
+ this.options.availableCornersAndSides,
3792
+ ),
3793
+ onProgress: (visualization, adaptiveStats) => {
3794
+ this.activeAdaptiveVisualization = visualization
3795
+ this.stats = { ...this.stats, ...adaptiveStats }
3796
+ },
3797
+ }),
3798
+ undefined,
3799
+ () => this.visualizeAdaptiveRoutingState(),
3800
+ )
3801
+ singleLayerPlans = (yield {
3802
+ type: "subsolver",
3803
+ solver: adaptiveSolver,
3804
+ }) as FanoutRoutePlan[] | null
3805
+ }
2039
3806
  if (singleLayerPlans) {
2040
3807
  plans.push(...singleLayerPlans)
2041
3808
  } else {
2042
3809
  failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId))
2043
3810
  }
3811
+ this.setInProgressPlans({
3812
+ phase: "route-single-layer",
3813
+ plans,
3814
+ strategy: routingStrategy,
3815
+ unitIndex: 1,
3816
+ unitCount: 1,
3817
+ })
3818
+ yield
2044
3819
  }
2045
3820
  const busesInRoutingOrder = [...this.preparedBuses].sort((a, b) => {
2046
3821
  const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
@@ -2076,23 +3851,56 @@ export class FanoutSolver extends BaseSolver {
2076
3851
  )
2077
3852
  })
2078
3853
 
2079
- const mixedTerminationState =
2080
- !useSingleLayerPushAndShove && routingStrategy === "default"
2081
- ? this.routeDenseThroughAllMixedTerminations({
2082
- busLayerAssignments,
2083
- busesInRoutingOrder,
2084
- })
2085
- : null
3854
+ let mixedTerminationState: MixedTerminationState | null = null
3855
+ if (!useSingleLayerPushAndShove && routingStrategy === "default") {
3856
+ const denseSolver = this.createWorkSolver(
3857
+ "DenseMixedTerminationSolver",
3858
+ this.routeDenseThroughAllMixedTerminationSteps({
3859
+ busLayerAssignments,
3860
+ busesInRoutingOrder,
3861
+ }),
3862
+ () => {
3863
+ const workUnit = Number(this.stats.workUnit ?? 0)
3864
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
3865
+ return workUnitCount > 0 ? workUnit / workUnitCount : 0
3866
+ },
3867
+ )
3868
+ mixedTerminationState = (yield {
3869
+ type: "subsolver",
3870
+ solver: denseSolver,
3871
+ }) as MixedTerminationState | null
3872
+ }
3873
+ if (
3874
+ !mixedTerminationState &&
3875
+ !useSingleLayerPushAndShove &&
3876
+ routingStrategy === "default" &&
3877
+ process.env.FANOUT_DEBUG_DENSE_ONLY === "1"
3878
+ ) {
3879
+ mixedTerminationState = {
3880
+ plans: [],
3881
+ failedBusIds: this.preparedBuses.map((bus) => bus.busId),
3882
+ }
3883
+ }
2086
3884
 
2087
3885
  if (mixedTerminationState) {
2088
3886
  plans = mixedTerminationState.plans
2089
3887
  failedBusIds = mixedTerminationState.failedBusIds
3888
+ this.setInProgressPlans({
3889
+ phase: "route-dense-mixed-terminations",
3890
+ plans,
3891
+ strategy: routingStrategy,
3892
+ unitIndex: this.preparedBuses.length,
3893
+ unitCount: this.preparedBuses.length,
3894
+ })
3895
+ yield
2090
3896
  }
2091
3897
 
2092
3898
  let routingPrefixKey = `${routingStrategy}|`
3899
+ let routedBusIndex = 0
2093
3900
  for (const bus of useSingleLayerPushAndShove || mixedTerminationState
2094
3901
  ? []
2095
3902
  : busesInRoutingOrder) {
3903
+ routedBusIndex++
2096
3904
  const targetLayer = busLayerAssignments[bus.busId]
2097
3905
  if (!targetLayer) {
2098
3906
  throw new Error(
@@ -2110,6 +3918,15 @@ export class FanoutSolver extends BaseSolver {
2110
3918
  plans = [...cachedPrefix.plans]
2111
3919
  failedBusIds = [...cachedPrefix.failedBusIds]
2112
3920
  blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
3921
+ this.setInProgressPlans({
3922
+ phase: "route-assignment",
3923
+ plans,
3924
+ strategy: routingStrategy,
3925
+ unitIndex: routedBusIndex,
3926
+ unitCount: busesInRoutingOrder.length,
3927
+ busId: bus.busId,
3928
+ })
3929
+ yield
2113
3930
  continue
2114
3931
  }
2115
3932
  const currentBusBlockingCounts = new Map<string, number>()
@@ -2145,6 +3962,15 @@ export class FanoutSolver extends BaseSolver {
2145
3962
  failedBusIds: [...failedBusIds],
2146
3963
  blockingBusCounts: new Map(blockingBusCounts),
2147
3964
  })
3965
+ this.setInProgressPlans({
3966
+ phase: "route-assignment",
3967
+ plans,
3968
+ strategy: routingStrategy,
3969
+ unitIndex: routedBusIndex,
3970
+ unitCount: busesInRoutingOrder.length,
3971
+ busId: bus.busId,
3972
+ })
3973
+ yield
2148
3974
  }
2149
3975
 
2150
3976
  let validationIssues: FanoutAttemptSummary["validationIssues"]
@@ -2218,6 +4044,11 @@ export class FanoutSolver extends BaseSolver {
2218
4044
  score,
2219
4045
  ...(validationIssues ? { validationIssues } : {}),
2220
4046
  }
4047
+ this.setInProgressPlans({
4048
+ phase: "finalize-assignment-strategy",
4049
+ plans,
4050
+ strategy: routingStrategy,
4051
+ })
2221
4052
  return {
2222
4053
  summary,
2223
4054
  plans,
@@ -2228,15 +4059,16 @@ export class FanoutSolver extends BaseSolver {
2228
4059
  }
2229
4060
  }
2230
4061
 
2231
- private evaluateAssignment(
4062
+ private *evaluateAssignmentSteps(
2232
4063
  assignmentIndex: number,
2233
4064
  busLayerAssignments: Readonly<Record<string, string>>,
2234
- ): EvaluatedAssignment {
2235
- let bestAttempt = this.evaluateAssignmentWithStrategy(
4065
+ ): Generator<FanoutWorkYield, EvaluatedAssignment, unknown> {
4066
+ let bestAttempt = yield* this.evaluateAssignmentWithStrategySteps(
2236
4067
  assignmentIndex,
2237
4068
  busLayerAssignments,
2238
4069
  "default",
2239
4070
  )
4071
+ if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1") return bestAttempt
2240
4072
  if (
2241
4073
  bestAttempt.summary.routedConnectionCount ===
2242
4074
  this.inputSrj.connections.length &&
@@ -2246,7 +4078,7 @@ export class FanoutSolver extends BaseSolver {
2246
4078
  }
2247
4079
 
2248
4080
  for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
2249
- const attempt = this.evaluateAssignmentWithStrategy(
4081
+ const attempt = yield* this.evaluateAssignmentWithStrategySteps(
2250
4082
  assignmentIndex,
2251
4083
  busLayerAssignments,
2252
4084
  routingStrategy,
@@ -2274,10 +4106,10 @@ export class FanoutSolver extends BaseSolver {
2274
4106
  * power or signal lane cannot starve a later bus before the solver explores
2275
4107
  * an alternate layer/track combination.
2276
4108
  */
2277
- private evaluateGroupedBeam(
4109
+ private *evaluateGroupedBeamSteps(
2278
4110
  assignmentIndex: number,
2279
4111
  groupByDirection = false,
2280
- ): EvaluatedAssignment | null {
4112
+ ): Generator<FanoutWorkYield, EvaluatedAssignment | null, unknown> {
2281
4113
  if (this.config.escapeLayers.length < 2) return null
2282
4114
  if (this.preparedBuses.length > 56) return null
2283
4115
  const totalConnections = this.inputSrj.connections.length
@@ -2386,7 +4218,9 @@ export class FanoutSolver extends BaseSolver {
2386
4218
  )
2387
4219
  }
2388
4220
 
4221
+ let searchedBusIndex = 0
2389
4222
  for (const bus of busesInSearchOrder) {
4223
+ searchedBusIndex++
2390
4224
  const nextStates: GroupedBeamState[] = []
2391
4225
  for (const state of states) {
2392
4226
  const candidateLayers =
@@ -2474,6 +4308,15 @@ export class FanoutSolver extends BaseSolver {
2474
4308
  states.push(state)
2475
4309
  if (states.length >= beamWidth) break
2476
4310
  }
4311
+ this.setInProgressPlans({
4312
+ phase: "route-grouped-beam",
4313
+ plans: states[0]?.plans ?? [],
4314
+ strategy: "grouped-beam",
4315
+ unitIndex: searchedBusIndex,
4316
+ unitCount: busesInSearchOrder.length,
4317
+ busId: bus.busId,
4318
+ })
4319
+ yield
2477
4320
  }
2478
4321
 
2479
4322
  let bestState: GroupedBeamState | undefined
@@ -2493,9 +4336,15 @@ export class FanoutSolver extends BaseSolver {
2493
4336
  (bus) => bus.maxLengthSkew !== undefined,
2494
4337
  )
2495
4338
  for (const state of states) {
2496
- if (state.plans.length !== this.inputSrj.connections.length) continue
4339
+ if (state.plans.length !== this.inputSrj.connections.length) {
4340
+ yield
4341
+ continue
4342
+ }
2497
4343
  const lengthMatching = this.matchCompletePlanLengths(state.plans)
2498
- if (!lengthMatching.plans) continue
4344
+ if (!lengthMatching.plans) {
4345
+ yield
4346
+ continue
4347
+ }
2499
4348
  const lengthMatchedPlans = lengthMatching.plans
2500
4349
  const candidateOutput = buildOutputSimpleRouteJson({
2501
4350
  inputSrj: this.inputSrj,
@@ -2505,6 +4354,7 @@ export class FanoutSolver extends BaseSolver {
2505
4354
  if (
2506
4355
  !this.validateCompletePlans(lengthMatchedPlans, candidateOutput).valid
2507
4356
  ) {
4357
+ yield
2508
4358
  continue
2509
4359
  }
2510
4360
  const candidateState = { ...state, plans: lengthMatchedPlans }
@@ -2523,6 +4373,12 @@ export class FanoutSolver extends BaseSolver {
2523
4373
  bestAdditionalViaCount = candidateAdditionalViaCount
2524
4374
  }
2525
4375
  if (!hasLengthConstraints) break
4376
+ this.setInProgressPlans({
4377
+ phase: "validate-grouped-beam",
4378
+ plans: lengthMatchedPlans,
4379
+ strategy: "grouped-beam",
4380
+ })
4381
+ yield
2526
4382
  }
2527
4383
  if (!bestState || !outputSrj) return null
2528
4384
  const score = bestMatchedScore
@@ -2544,6 +4400,14 @@ export class FanoutSolver extends BaseSolver {
2544
4400
  }
2545
4401
  }
2546
4402
 
4403
+ private *evaluateGroupedBeamAlternativesSteps(
4404
+ assignmentIndex: number,
4405
+ ): Generator<FanoutWorkYield, EvaluatedAssignment | null, unknown> {
4406
+ const primaryAttempt = yield* this.evaluateGroupedBeamSteps(assignmentIndex)
4407
+ if (primaryAttempt) return primaryAttempt
4408
+ return yield* this.evaluateGroupedBeamSteps(assignmentIndex, true)
4409
+ }
4410
+
2547
4411
  private prioritizeFailedBusRepairs(
2548
4412
  assignment: Readonly<Record<string, string>>,
2549
4413
  failedBusIds: readonly string[],
@@ -2710,52 +4574,10 @@ export class FanoutSolver extends BaseSolver {
2710
4574
  return targetedRepairSearchFinished
2711
4575
  }
2712
4576
 
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
- }
4577
+ private commitGroupedBeamAttempt(
4578
+ beamAttempt: EvaluatedAssignment | null,
4579
+ ): void {
4580
+ if (!beamAttempt) {
2759
4581
  if (
2760
4582
  this.hasCompleteBestAttempt() &&
2761
4583
  this.bestAttempt &&
@@ -2763,10 +4585,75 @@ export class FanoutSolver extends BaseSolver {
2763
4585
  ) {
2764
4586
  this.completeBestAttemptEndpoints()
2765
4587
  this.solved = true
2766
- return
2767
4588
  }
4589
+ return
4590
+ }
4591
+ this.attempts.push(beamAttempt.summary)
4592
+ if (
4593
+ !this.bestAttempt ||
4594
+ this.isAttemptBetter(beamAttempt, this.bestAttempt)
4595
+ ) {
4596
+ this.bestAttempt = beamAttempt
4597
+ }
4598
+ const bestSummary = this.bestAttempt.summary
4599
+ this.stats = {
4600
+ phase: "complete-grouped-beam",
4601
+ assignment:
4602
+ bestSummary.assignmentIndex < 0 ? 0 : bestSummary.assignmentIndex + 1,
4603
+ assignmentCount: this.config.maxLayerCombinations,
4604
+ routedBuses: `${bestSummary.routedBusCount}/${this.preparedBuses.length}`,
4605
+ routedConnections: `${bestSummary.routedConnectionCount}/${this.inputSrj.connections.length}`,
4606
+ failedBuses: "none",
4607
+ bestScore: bestSummary.score,
4608
+ }
4609
+ if (this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0) {
4610
+ this.completeBestAttemptEndpoints()
4611
+ this.solved = true
4612
+ }
4613
+ }
4614
+
4615
+ private commitAssignmentAttempt(
4616
+ assignment: Readonly<Record<string, string>>,
4617
+ attempt: EvaluatedAssignment,
4618
+ ): void {
4619
+ this.nextAssignmentIndex++
4620
+ this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
4621
+ if (
4622
+ !this.bestAttempt ||
4623
+ attempt.summary.routedConnectionCount >=
4624
+ this.bestAttempt.summary.routedConnectionCount
4625
+ ) {
4626
+ this.prioritizeFailedBusRepairs(
4627
+ assignment,
4628
+ attempt.summary.failedBusIds,
4629
+ attempt.blockingBusIds,
4630
+ )
4631
+ }
4632
+ this.attempts.push(attempt.summary)
4633
+ if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
4634
+ this.bestAttempt = attempt
4635
+ }
4636
+ this.stats = {
4637
+ phase: "complete-assignment",
4638
+ assignment: attempt.summary.assignmentIndex + 1,
4639
+ assignmentCount: this.config.maxLayerCombinations,
4640
+ routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
4641
+ routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
4642
+ failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
4643
+ bestScore: this.bestAttempt.summary.score,
4644
+ }
4645
+ if (
4646
+ this.groupedBeamEvaluated &&
4647
+ attempt.summary.routedConnectionCount ===
4648
+ this.inputSrj.connections.length &&
4649
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
4650
+ ) {
4651
+ this.completeBestAttemptEndpoints()
4652
+ this.solved = true
2768
4653
  }
4654
+ }
2769
4655
 
4656
+ private getNextAssignment(): Readonly<Record<string, string>> | undefined {
2770
4657
  let assignment: Readonly<Record<string, string>> | undefined
2771
4658
  while (
2772
4659
  !assignment &&
@@ -2802,68 +4689,138 @@ export class FanoutSolver extends BaseSolver {
2802
4689
  if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
2803
4690
  assignment = candidate
2804
4691
  }
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
- }
4692
+ return assignment
4693
+ }
4694
+
4695
+ private finishWithoutAnotherAssignment(): void {
4696
+ if (this.hasCompleteBestAttempt()) {
4697
+ this.completeBestAttemptEndpoints()
4698
+ this.solved = true
2821
4699
  return
2822
4700
  }
4701
+ this.failed = true
4702
+ const validationMessage =
4703
+ this.lengthMatchingFailure?.message ??
4704
+ this.bestAttempt?.summary.validationIssues?.[0]?.message
4705
+ this.error = validationMessage
4706
+ ? `FanoutSolver: ${validationMessage}`
4707
+ : this.bestAttempt
4708
+ ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
4709
+ : "FanoutSolver: no layer assignment could be evaluated"
4710
+ }
2823
4711
 
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
4712
+ override _step(): void {
4713
+ if (this.activeOperation) {
4714
+ this.stepActiveOperation()
4715
+ return
2844
4716
  }
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,
4717
+
4718
+ if (!this.routingInitialized) {
4719
+ this.startOperation({
4720
+ name: "FanoutCandidateLayerSolver",
4721
+ generator: this.initializeRoutingSteps(),
4722
+ onSolved: () => {},
4723
+ getProgress: () =>
4724
+ this.nextCandidateLayerBusIndex /
4725
+ Math.max(1, this.boundaryBuses.length + 1),
4726
+ })
4727
+ return
2852
4728
  }
4729
+
2853
4730
  if (
2854
- this.groupedBeamEvaluated &&
2855
- attempt.summary.routedConnectionCount ===
2856
- this.inputSrj.connections.length &&
2857
- this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
4731
+ this.nextAssignmentIndex > 0 &&
4732
+ this.hasGloballyViaMinimalBestAttempt()
2858
4733
  ) {
2859
4734
  this.completeBestAttemptEndpoints()
2860
4735
  this.solved = true
4736
+ return
4737
+ }
4738
+ // Try the deterministic assignment and only its targeted repair queue
4739
+ // before paying for the grouped beam. If the beam cannot solve, continue
4740
+ // with the broader generated-assignment search below.
4741
+ if (this.shouldEvaluateGroupedBeam()) {
4742
+ this.groupedBeamEvaluated = true
4743
+ this.startOperation({
4744
+ name: "FanoutGroupedBeamSolver",
4745
+ generator: this.evaluateGroupedBeamAlternativesSteps(-1),
4746
+ onSolved: (attempt) => this.commitGroupedBeamAttempt(attempt),
4747
+ getProgress: () => {
4748
+ const workUnit = Number(this.stats.workUnit ?? 0)
4749
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
4750
+ return workUnitCount > 0 ? workUnit / workUnitCount : 0
4751
+ },
4752
+ })
4753
+ this.stats = { ...this.stats, phase: "prepare-grouped-beam" }
4754
+ return
4755
+ }
4756
+
4757
+ const assignment = this.getNextAssignment()
4758
+ if (!assignment && !this.groupedBeamEvaluated) return
4759
+ if (!assignment) {
4760
+ this.finishWithoutAnotherAssignment()
4761
+ return
4762
+ }
4763
+
4764
+ this.startOperation({
4765
+ name: "FanoutAssignmentSolver",
4766
+ generator: this.evaluateAssignmentSteps(
4767
+ this.nextAssignmentIndex,
4768
+ assignment,
4769
+ ),
4770
+ onSolved: (attempt) => this.commitAssignmentAttempt(assignment, attempt),
4771
+ getProgress: () => {
4772
+ const strategyIndex =
4773
+ this.stats.routingStrategy === "group-by-layer"
4774
+ ? 1
4775
+ : this.stats.routingStrategy === "deep-first"
4776
+ ? 2
4777
+ : 0
4778
+ const workUnit = Number(this.stats.workUnit ?? 0)
4779
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
4780
+ const strategyFraction =
4781
+ workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0
4782
+ return (strategyIndex + strategyFraction) / 3
4783
+ },
4784
+ })
4785
+ this.stats = {
4786
+ ...this.stats,
4787
+ phase: "prepare-assignment",
4788
+ assignment: this.nextAssignmentIndex + 1,
4789
+ assignmentCount: this.config.maxLayerCombinations,
4790
+ routedConnections: `0/${this.inputSrj.connections.length}`,
2861
4791
  }
2862
4792
  }
2863
4793
 
2864
4794
  computeProgress(): number {
2865
4795
  if (this.solved || this.failed) return 1
2866
- return this.nextAssignmentIndex / this.config.maxLayerCombinations
4796
+ if (!this.routingInitialized) {
4797
+ return (
4798
+ 0.05 *
4799
+ (this.nextCandidateLayerBusIndex /
4800
+ Math.max(1, this.boundaryBuses.length + 1))
4801
+ )
4802
+ }
4803
+ let activeAssignmentFraction = 0
4804
+ if (this.activeSubSolver?.getSolverName() === "FanoutAssignmentSolver") {
4805
+ const strategyIndex =
4806
+ this.stats.routingStrategy === "group-by-layer"
4807
+ ? 1
4808
+ : this.stats.routingStrategy === "deep-first"
4809
+ ? 2
4810
+ : 0
4811
+ const workUnit = Number(this.stats.workUnit ?? 0)
4812
+ const workUnitCount = Number(this.stats.workUnitCount ?? 0)
4813
+ const strategyFraction =
4814
+ workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0
4815
+ activeAssignmentFraction = (strategyIndex + strategyFraction) / 3
4816
+ }
4817
+ return Math.min(
4818
+ 0.99,
4819
+ 0.05 +
4820
+ 0.95 *
4821
+ ((this.nextAssignmentIndex + activeAssignmentFraction) /
4822
+ this.config.maxLayerCombinations),
4823
+ )
2867
4824
  }
2868
4825
 
2869
4826
  override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
@@ -2937,10 +4894,6 @@ export class FanoutSolver extends BaseSolver {
2937
4894
  }
2938
4895
 
2939
4896
  override visualize(): GraphicsObject {
2940
- const visualizedSrj =
2941
- this.endpointCompletion?.simpleRouteJson ??
2942
- this.bestAttempt?.outputSrj ??
2943
- this.inputSrj
2944
- return visualizeSimpleRouteJson(visualizedSrj)
4897
+ return this.activeSubSolver?.visualize() ?? this.visualizeCurrentState()
2945
4898
  }
2946
4899
  }