@tscircuit/fanout-solver 0.0.17 → 0.0.19

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.
@@ -5,20 +5,26 @@ import {
5
5
  import { BaseSolver } from "@tscircuit/solver-utils"
6
6
  import type { GraphicsObject } from "graphics-debug"
7
7
  import { buildOutputSimpleRouteJson } from "./build-output"
8
- import { distanceSegmentToObstacle } from "./geometry"
9
8
  import { getCopperLayerColor } from "./layer-colors"
10
9
  import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
11
10
  import {
12
11
  prepareFanoutBuses,
13
12
  resolveAvailableBoundaryRegions,
14
13
  } from "./prepare-buses"
15
- import { routeBus } from "./route-bus"
14
+ import {
15
+ routeBus,
16
+ routeBusAlternatives,
17
+ type RouteBusStaticClearanceCache,
18
+ } from "./route-bus"
16
19
  import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
17
20
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
21
+ import { validateFanoutSolution } from "./validate-fanout-solution"
18
22
  import type {
19
23
  AssignmentAttempt,
24
+ Bounds,
20
25
  FanoutAttemptSummary,
21
26
  FanoutBorderDistribution,
27
+ FanoutRoutePlan,
22
28
  FanoutSolverOptions,
23
29
  FanoutSolverOutput,
24
30
  PreparedBus,
@@ -30,6 +36,7 @@ interface ResolvedFanoutConfig {
30
36
  viaHoleDiameter: number
31
37
  clearance: number
32
38
  compactBusTracks: boolean
39
+ allowSameNetMerges: boolean
33
40
  singleLayerPushAndShove: boolean
34
41
  singleLayerAdaptiveExits: boolean
35
42
  borderDistribution: FanoutBorderDistribution
@@ -38,6 +45,17 @@ interface ResolvedFanoutConfig {
38
45
  maxLayerCombinations: number
39
46
  }
40
47
 
48
+ interface EvaluatedAssignment extends AssignmentAttempt {
49
+ blockingBusIds: string[]
50
+ }
51
+
52
+ interface GroupedBeamState {
53
+ assignment: Readonly<Record<string, string>>
54
+ plans: FanoutRoutePlan[]
55
+ }
56
+
57
+ type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
58
+
41
59
  function resolvePositiveNumber(label: string, value: number): number {
42
60
  if (!Number.isFinite(value) || value <= 0) {
43
61
  throw new Error(
@@ -108,6 +126,7 @@ function resolveConfig(
108
126
  viaHoleDiameter,
109
127
  clearance,
110
128
  compactBusTracks: options.compactBusTracks ?? false,
129
+ allowSameNetMerges: options.allowSameNetMerges ?? false,
111
130
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
112
131
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
113
132
  borderDistribution,
@@ -199,62 +218,12 @@ function getBusDepthInRows(bus: PreparedBus): number {
199
218
  )
200
219
  }
201
220
 
202
- function sourceLayerEscapeIsBlocked(params: {
203
- bus: PreparedBus
204
- srj: SimpleRouteJson
205
- traceWidth: number
206
- clearance: number
207
- }): boolean {
208
- const { bus, srj, traceWidth, clearance } = params
209
- for (const connection of bus.connections) {
210
- const source = {
211
- x: connection.sourcePoint.x,
212
- y: connection.sourcePoint.y,
213
- }
214
- const boundaryPoint = (() => {
215
- switch (bus.direction) {
216
- case "left":
217
- return { x: bus.sharedBoundary.minX, y: source.y }
218
- case "right":
219
- return { x: bus.sharedBoundary.maxX, y: source.y }
220
- case "up":
221
- return { x: source.x, y: bus.sharedBoundary.maxY }
222
- case "down":
223
- return { x: source.x, y: bus.sharedBoundary.minY }
224
- }
225
- })()
226
- const directEscapeSegment = {
227
- start: source,
228
- end: boundaryPoint,
229
- width: traceWidth,
230
- layer: connection.sourceLayer,
231
- }
232
- for (const obstacle of srj.obstacles) {
233
- if (
234
- obstacle === connection.sourceObstacle ||
235
- !obstacle.layers.includes(connection.sourceLayer)
236
- ) {
237
- continue
238
- }
239
- if (
240
- distanceSegmentToObstacle(directEscapeSegment, obstacle) <
241
- traceWidth / 2 + clearance - 1e-9
242
- ) {
243
- return true
244
- }
245
- }
246
- }
247
- return false
248
- }
249
-
250
221
  function createPreferredLayerAssignment(params: {
251
222
  buses: PreparedBus[]
252
223
  escapeLayers: string[]
253
- srj: SimpleRouteJson
254
- traceWidth: number
255
- clearance: number
224
+ escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
256
225
  }): Readonly<Record<string, string>> {
257
- const { buses, escapeLayers, srj, traceWidth, clearance } = params
226
+ const { buses, escapeLayers, escapeLayersByBusId } = params
258
227
  const assignment: Record<string, string> = {}
259
228
  const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
260
229
  let nextViaLayerIndex = 0
@@ -273,16 +242,13 @@ function createPreferredLayerAssignment(params: {
273
242
  assignment[bus.busId] = bus.termination.layer
274
243
  continue
275
244
  }
276
- const viaLayers = escapeLayers.filter((layer) => layer !== sourceLayer)
245
+ const routableEscapeLayers = escapeLayersByBusId[bus.busId] ?? escapeLayers
246
+ const viaLayers = routableEscapeLayers.filter(
247
+ (layer) => layer !== sourceLayer,
248
+ )
277
249
  if (
278
- escapeLayers.includes(sourceLayer) &&
279
- busIsOnOutwardComponentEdge(bus) &&
280
- !sourceLayerEscapeIsBlocked({
281
- bus,
282
- srj,
283
- traceWidth,
284
- clearance,
285
- })
250
+ routableEscapeLayers.includes(sourceLayer) &&
251
+ busIsOnOutwardComponentEdge(bus)
286
252
  ) {
287
253
  assignment[bus.busId] = sourceLayer
288
254
  } else if (viaLayers.length > 0) {
@@ -321,12 +287,67 @@ function prioritizeLayerAssignment(params: {
321
287
  ].slice(0, maxAssignments)
322
288
  }
323
289
 
290
+ function getCandidateEscapeLayersForBus(params: {
291
+ bus: PreparedBus
292
+ srj: SimpleRouteJson
293
+ config: ResolvedFanoutConfig
294
+ staticClearanceCache: RouteBusStaticClearanceCache
295
+ }): string[] {
296
+ const { bus, srj, config, staticClearanceCache } = params
297
+ const individuallyRoutableLayers = config.escapeLayers.filter(
298
+ (targetLayer) =>
299
+ routeBus({
300
+ srj,
301
+ bus,
302
+ targetLayer,
303
+ acceptedPlans: [],
304
+ layerNames: config.layerNames,
305
+ traceWidth: config.traceWidth,
306
+ viaDiameter: config.viaDiameter,
307
+ viaHoleDiameter: config.viaHoleDiameter,
308
+ clearance: config.clearance,
309
+ compactBusTracks: config.compactBusTracks,
310
+ allowSameNetMerges: config.allowSameNetMerges,
311
+ staticClearanceCache,
312
+ }) !== null,
313
+ )
314
+
315
+ // Existing plans only add clearance constraints, so a layer that cannot
316
+ // route this bus by itself cannot become viable later in an assignment.
317
+ // Preserve the original candidates when none route so impossible problems
318
+ // still produce the usual failed-solver result instead of throwing here.
319
+ return individuallyRoutableLayers.length > 0
320
+ ? individuallyRoutableLayers
321
+ : config.escapeLayers
322
+ }
323
+
324
324
  export class FanoutSolver extends BaseSolver {
325
325
  readonly preparedBuses: PreparedBus[]
326
326
  readonly attempts: FanoutAttemptSummary[] = []
327
327
  readonly layerAssignments: Array<Readonly<Record<string, string>>>
328
328
  readonly config: ResolvedFanoutConfig
329
+ private readonly escapeLayersByBusId: Readonly<
330
+ Record<string, readonly string[]>
331
+ >
332
+ private readonly evaluatedAssignmentKeys = new Set<string>()
333
+ private readonly queuedAssignmentKeys = new Set<string>()
334
+ private readonly assignmentRepairDepthByKey = new Map<string, number>()
335
+ private readonly pendingRepairAssignments: Array<
336
+ Readonly<Record<string, string>>
337
+ > = []
338
+ private readonly routeStaticClearanceCache: RouteBusStaticClearanceCache =
339
+ new Map()
340
+ private readonly routingPrefixCache = new Map<
341
+ string,
342
+ {
343
+ plans: AssignmentAttempt["plans"]
344
+ failedBusIds: string[]
345
+ blockingBusCounts: Map<string, number>
346
+ }
347
+ >()
348
+ private groupedBeamEvaluated = false
329
349
  private nextAssignmentIndex = 0
350
+ private nextGeneratedAssignmentIndex = 0
330
351
  private bestAttempt: AssignmentAttempt | null = null
331
352
 
332
353
  constructor(
@@ -364,9 +385,27 @@ export class FanoutSolver extends BaseSolver {
364
385
  : [],
365
386
  ),
366
387
  )
388
+ const escapeLayersByBusId = Object.fromEntries(
389
+ this.preparedBuses.flatMap((bus) => {
390
+ if (bus.termination.type === "plane") return []
391
+ return [
392
+ [
393
+ bus.busId,
394
+ getCandidateEscapeLayersForBus({
395
+ bus,
396
+ srj: inputSrj,
397
+ config: this.config,
398
+ staticClearanceCache: this.routeStaticClearanceCache,
399
+ }),
400
+ ] as const,
401
+ ]
402
+ }),
403
+ )
404
+ this.escapeLayersByBusId = escapeLayersByBusId
367
405
  const generatedAssignments = generateLayerAssignments({
368
406
  busIds: boundaryBusIds,
369
407
  layers: this.config.escapeLayers,
408
+ layersByBusId: escapeLayersByBusId,
370
409
  maxAssignments: this.config.maxLayerCombinations,
371
410
  }).map((assignment) => ({
372
411
  ...assignment,
@@ -376,26 +415,55 @@ export class FanoutSolver extends BaseSolver {
376
415
  preferredAssignment: createPreferredLayerAssignment({
377
416
  buses: this.preparedBuses,
378
417
  escapeLayers: this.config.escapeLayers,
379
- srj: inputSrj,
380
- traceWidth: this.config.traceWidth,
381
- clearance: this.config.clearance,
418
+ escapeLayersByBusId,
382
419
  }),
383
420
  generatedAssignments,
384
421
  maxAssignments: this.config.maxLayerCombinations,
385
422
  })
386
- this.MAX_ITERATIONS = this.layerAssignments.length + 2
423
+ this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
387
424
  }
388
425
 
389
426
  override getSolverName(): string {
390
427
  return "FanoutSolver"
391
428
  }
392
429
 
393
- private evaluateAssignment(
430
+ private getValidationBoundary(): Bounds {
431
+ if (this.options.sharedBoundary) return this.options.sharedBoundary
432
+ const firstBoundary = this.preparedBuses[0]?.sharedBoundary
433
+ if (!firstBoundary) return this.inputSrj.bounds
434
+ return this.preparedBuses.slice(1).reduce<Bounds>(
435
+ (boundary, bus) => ({
436
+ minX: Math.min(boundary.minX, bus.sharedBoundary.minX),
437
+ maxX: Math.max(boundary.maxX, bus.sharedBoundary.maxX),
438
+ minY: Math.min(boundary.minY, bus.sharedBoundary.minY),
439
+ maxY: Math.max(boundary.maxY, bus.sharedBoundary.maxY),
440
+ }),
441
+ { ...firstBoundary },
442
+ )
443
+ }
444
+
445
+ private validateCompletePlans(
446
+ plans: readonly FanoutRoutePlan[],
447
+ outputSrj: SimpleRouteJson,
448
+ ) {
449
+ return validateFanoutSolution({
450
+ inputSrj: this.inputSrj,
451
+ outputSrj,
452
+ plans,
453
+ preparedBuses: this.preparedBuses,
454
+ sharedBoundary: this.getValidationBoundary(),
455
+ clearance: this.config.clearance,
456
+ })
457
+ }
458
+
459
+ private evaluateAssignmentWithStrategy(
394
460
  assignmentIndex: number,
395
461
  busLayerAssignments: Readonly<Record<string, string>>,
396
- ): AssignmentAttempt {
397
- const plans: AssignmentAttempt["plans"] = []
398
- const failedBusIds: string[] = []
462
+ routingStrategy: RoutingStrategy,
463
+ ): EvaluatedAssignment {
464
+ let plans: AssignmentAttempt["plans"] = []
465
+ let failedBusIds: string[] = []
466
+ let blockingBusCounts = new Map<string, number>()
399
467
  const isSingleLayerFanout = this.config.escapeLayers.length === 1
400
468
  if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
401
469
  const singleLayerParams = {
@@ -425,13 +493,21 @@ export class FanoutSolver extends BaseSolver {
425
493
  (a, b) =>
426
494
  Number(a.termination.type === "plane") -
427
495
  Number(b.termination.type === "plane") ||
496
+ (routingStrategy === "group-by-layer"
497
+ ? (busLayerAssignments[a.busId] ?? "").localeCompare(
498
+ busLayerAssignments[b.busId] ?? "",
499
+ )
500
+ : 0) ||
428
501
  b.componentObstacles.length - a.componentObstacles.length ||
429
502
  (isSingleLayerFanout
430
503
  ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
431
504
  : b.connections.length - a.connections.length ||
432
- getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)),
505
+ (routingStrategy === "deep-first"
506
+ ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
507
+ : getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b))),
433
508
  )
434
509
 
510
+ let routingPrefixKey = `${routingStrategy}|`
435
511
  for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
436
512
  ? []
437
513
  : busesInRoutingOrder) {
@@ -441,6 +517,20 @@ export class FanoutSolver extends BaseSolver {
441
517
  `FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
442
518
  )
443
519
  }
520
+ // The routing order can change between assignments (notably for
521
+ // group-by-layer search). A layer-only key can therefore replay a
522
+ // prefix belonging to a different bus and duplicate or drop plans when
523
+ // buses contain multiple connections. Include the bus identity so the
524
+ // cache remains valid for grouped power/signal lanes.
525
+ routingPrefixKey += `${bus.busId.length}:${bus.busId};${targetLayer.length}:${targetLayer};`
526
+ const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey)
527
+ if (cachedPrefix) {
528
+ plans = [...cachedPrefix.plans]
529
+ failedBusIds = [...cachedPrefix.failedBusIds]
530
+ blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
531
+ continue
532
+ }
533
+ const currentBusBlockingCounts = new Map<string, number>()
444
534
  const busPlans = routeBus({
445
535
  srj: this.inputSrj,
446
536
  bus,
@@ -452,12 +542,50 @@ export class FanoutSolver extends BaseSolver {
452
542
  viaHoleDiameter: this.config.viaHoleDiameter,
453
543
  clearance: this.config.clearance,
454
544
  compactBusTracks: this.config.compactBusTracks,
545
+ allowSameNetMerges: this.config.allowSameNetMerges,
546
+ staticClearanceCache: this.routeStaticClearanceCache,
547
+ blockingBusCounts: currentBusBlockingCounts,
455
548
  })
456
549
  if (!busPlans) {
457
550
  failedBusIds.push(bus.busId)
458
- continue
551
+ for (const [blockingBusId, count] of currentBusBlockingCounts) {
552
+ blockingBusCounts.set(
553
+ blockingBusId,
554
+ (blockingBusCounts.get(blockingBusId) ?? 0) + count,
555
+ )
556
+ }
557
+ } else {
558
+ plans.push(...busPlans)
459
559
  }
460
- plans.push(...busPlans)
560
+ this.routingPrefixCache.set(routingPrefixKey, {
561
+ plans: [...plans],
562
+ failedBusIds: [...failedBusIds],
563
+ blockingBusCounts: new Map(blockingBusCounts),
564
+ })
565
+ }
566
+
567
+ let validationIssues: FanoutAttemptSummary["validationIssues"]
568
+ let outputSrj = buildOutputSimpleRouteJson({
569
+ inputSrj: this.inputSrj,
570
+ plans,
571
+ layerNames: this.config.layerNames,
572
+ })
573
+ const validation =
574
+ plans.length === this.inputSrj.connections.length
575
+ ? this.validateCompletePlans(plans, outputSrj)
576
+ : null
577
+ if (validation && !validation.valid) {
578
+ // Every route-producing strategy must pass the same final layer-aware,
579
+ // same-net-aware copper validation before it can be scored as complete.
580
+ validationIssues = validation.issues
581
+ plans = []
582
+ failedBusIds = this.preparedBuses.map((bus) => bus.busId)
583
+ blockingBusCounts.clear()
584
+ outputSrj = buildOutputSimpleRouteJson({
585
+ inputSrj: this.inputSrj,
586
+ plans,
587
+ layerNames: this.config.layerNames,
588
+ })
461
589
  }
462
590
 
463
591
  const routedBusCount = this.preparedBuses.length - failedBusIds.length
@@ -477,21 +605,372 @@ export class FanoutSolver extends BaseSolver {
477
605
  routedConnectionCount: plans.length,
478
606
  failedBusIds,
479
607
  score,
608
+ ...(validationIssues ? { validationIssues } : {}),
480
609
  }
481
610
 
482
611
  return {
483
612
  summary,
484
613
  plans,
485
- outputSrj: buildOutputSimpleRouteJson({
486
- inputSrj: this.inputSrj,
487
- plans,
488
- layerNames: this.config.layerNames,
489
- }),
614
+ blockingBusIds: [...blockingBusCounts.entries()]
615
+ .toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
616
+ .map(([busId]) => busId),
617
+ outputSrj,
490
618
  }
491
619
  }
492
620
 
621
+ private evaluateAssignment(
622
+ assignmentIndex: number,
623
+ busLayerAssignments: Readonly<Record<string, string>>,
624
+ ): EvaluatedAssignment {
625
+ let bestAttempt = this.evaluateAssignmentWithStrategy(
626
+ assignmentIndex,
627
+ busLayerAssignments,
628
+ "default",
629
+ )
630
+ if (
631
+ bestAttempt.summary.routedConnectionCount ===
632
+ this.inputSrj.connections.length
633
+ ) {
634
+ return bestAttempt
635
+ }
636
+
637
+ for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
638
+ const attempt = this.evaluateAssignmentWithStrategy(
639
+ assignmentIndex,
640
+ busLayerAssignments,
641
+ routingStrategy,
642
+ )
643
+ if (attempt.summary.score < bestAttempt.summary.score) {
644
+ bestAttempt = attempt
645
+ }
646
+ if (
647
+ bestAttempt.summary.routedConnectionCount ===
648
+ this.inputSrj.connections.length
649
+ ) {
650
+ return bestAttempt
651
+ }
652
+ }
653
+ return bestAttempt
654
+ }
655
+
656
+ /**
657
+ * Search layer assignments and track alternatives together. The regular
658
+ * assignment loop commits to one route per bus before the next bus is
659
+ * considered, so a locally-valid track can still starve a later bus. A
660
+ * bounded beam keeps several grouped-layer route prefixes alive. It also
661
+ * evaluates multi-connection buses atomically, so one promising route for a
662
+ * power or signal lane cannot starve a later bus before the solver explores
663
+ * an alternate layer/track combination.
664
+ */
665
+ private evaluateGroupedBeam(
666
+ assignmentIndex: number,
667
+ groupByDirection = false,
668
+ ): EvaluatedAssignment | null {
669
+ if (this.config.escapeLayers.length < 2) return null
670
+ if (this.preparedBuses.length > 56) return null
671
+ const totalConnections = this.inputSrj.connections.length
672
+ // Multi-pin alternatives grow with both the bus width and the number of
673
+ // layer prefixes. Keep the new search bounded on the small/medium grouped
674
+ // problems it can improve, then let the regular assignment/repair search
675
+ // handle the very large benchmark samples without starving them.
676
+ if (totalConnections > 64) return null
677
+ if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
678
+ return null
679
+ }
680
+
681
+ const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
682
+ const aLayerCount =
683
+ a.termination.type === "plane"
684
+ ? 1
685
+ : (this.escapeLayersByBusId[a.busId]?.length ??
686
+ this.config.escapeLayers.length)
687
+ const bLayerCount =
688
+ b.termination.type === "plane"
689
+ ? 1
690
+ : (this.escapeLayersByBusId[b.busId]?.length ??
691
+ this.config.escapeLayers.length)
692
+ return (
693
+ Number(a.termination.type === "plane") -
694
+ Number(b.termination.type === "plane") ||
695
+ (groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
696
+ aLayerCount - bLayerCount ||
697
+ b.componentObstacles.length - a.componentObstacles.length ||
698
+ b.connections.length - a.connections.length ||
699
+ getBusDepthInRows(b) - getBusDepthInRows(a) ||
700
+ a.busId.localeCompare(b.busId)
701
+ )
702
+ })
703
+
704
+ const isSmallProblem = totalConnections <= 24
705
+ const hasMultiConnectionBus = this.preparedBuses.some(
706
+ (bus) => bus.connections.length > 1,
707
+ )
708
+ // Preserve the broad track search for small singleton problems. Grouped
709
+ // power buses already branch across every candidate layer and make each
710
+ // route-alternative expansion combinatorial, so retain layer diversity but
711
+ // only one atomic route per layer for those buses.
712
+ const beamWidth = isSmallProblem ? 48 : totalConnections <= 32 ? 24 : 12
713
+ const alternativesPerLayer =
714
+ isSmallProblem && !hasMultiConnectionBus ? 4 : 1
715
+ let states: GroupedBeamState[] = [{ assignment: {}, plans: [] }]
716
+
717
+ const getStateScore = (state: GroupedBeamState): number => {
718
+ const routeLength = state.plans.reduce(
719
+ (total, plan) => total + plan.length,
720
+ 0,
721
+ )
722
+ const viaCount = state.plans.filter((plan) => plan.via).length
723
+ return (
724
+ routeLength +
725
+ viaCount * 0.1 +
726
+ assignmentLoadPenalty(state.assignment) * 0.01
727
+ )
728
+ }
729
+
730
+ for (const bus of busesInSearchOrder) {
731
+ const nextStates: GroupedBeamState[] = []
732
+ for (const state of states) {
733
+ const candidateLayers =
734
+ bus.termination.type === "plane"
735
+ ? [bus.termination.layer]
736
+ : (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
737
+ const layerLoads = new Map<string, number>()
738
+ for (const layer of Object.values(state.assignment)) {
739
+ layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + 1)
740
+ }
741
+ const sourceLayer = bus.connections[0]?.sourceLayer
742
+ const orderedLayers = candidateLayers.toSorted(
743
+ (first, second) =>
744
+ (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
745
+ Number(first === sourceLayer) - Number(second === sourceLayer) ||
746
+ first.localeCompare(second),
747
+ )
748
+
749
+ for (const targetLayer of orderedLayers) {
750
+ const busAlternatives = routeBusAlternatives(
751
+ {
752
+ srj: this.inputSrj,
753
+ bus,
754
+ targetLayer,
755
+ acceptedPlans: state.plans,
756
+ layerNames: this.config.layerNames,
757
+ traceWidth: this.config.traceWidth,
758
+ viaDiameter: this.config.viaDiameter,
759
+ viaHoleDiameter: this.config.viaHoleDiameter,
760
+ clearance: this.config.clearance,
761
+ compactBusTracks: this.config.compactBusTracks,
762
+ allowSameNetMerges: this.config.allowSameNetMerges,
763
+ staticClearanceCache: this.routeStaticClearanceCache,
764
+ },
765
+ alternativesPerLayer,
766
+ )
767
+ for (const busPlans of busAlternatives) {
768
+ nextStates.push({
769
+ assignment: {
770
+ ...state.assignment,
771
+ [bus.busId]: targetLayer,
772
+ },
773
+ plans: [...state.plans, ...busPlans],
774
+ })
775
+ }
776
+ }
777
+ }
778
+
779
+ if (nextStates.length === 0) return null
780
+ nextStates.sort((first, second) => {
781
+ const scoreDifference = getStateScore(first) - getStateScore(second)
782
+ if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
783
+ return JSON.stringify(first.assignment).localeCompare(
784
+ JSON.stringify(second.assignment),
785
+ )
786
+ })
787
+ const statesByAssignment = new Map<string, number>()
788
+ states = []
789
+ for (const state of nextStates) {
790
+ const key = JSON.stringify(state.assignment)
791
+ const sameAssignmentCount = statesByAssignment.get(key) ?? 0
792
+ if (sameAssignmentCount >= 2) continue
793
+ statesByAssignment.set(key, sameAssignmentCount + 1)
794
+ states.push(state)
795
+ if (states.length >= beamWidth) break
796
+ }
797
+ }
798
+
799
+ const bestState = states[0]
800
+ if (!bestState) return null
801
+ const outputSrj = buildOutputSimpleRouteJson({
802
+ inputSrj: this.inputSrj,
803
+ plans: bestState.plans,
804
+ layerNames: this.config.layerNames,
805
+ })
806
+ if (
807
+ bestState.plans.length === this.inputSrj.connections.length &&
808
+ !this.validateCompletePlans(bestState.plans, outputSrj).valid
809
+ ) {
810
+ return null
811
+ }
812
+ const score =
813
+ bestState.plans.length === this.inputSrj.connections.length
814
+ ? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
815
+ bestState.plans.filter((plan) => plan.via).length * 0.1 +
816
+ assignmentLoadPenalty(bestState.assignment) * 0.01
817
+ : Number.POSITIVE_INFINITY
818
+ if (!Number.isFinite(score)) return null
819
+
820
+ const summary: FanoutAttemptSummary = {
821
+ assignmentIndex,
822
+ busLayerAssignments: bestState.assignment,
823
+ routedBusCount: this.preparedBuses.length,
824
+ routedConnectionCount: bestState.plans.length,
825
+ failedBusIds: [],
826
+ score,
827
+ }
828
+ return {
829
+ summary,
830
+ plans: bestState.plans,
831
+ blockingBusIds: [],
832
+ outputSrj,
833
+ }
834
+ }
835
+
836
+ private prioritizeFailedBusRepairs(
837
+ assignment: Readonly<Record<string, string>>,
838
+ failedBusIds: readonly string[],
839
+ blockingBusIds: readonly string[],
840
+ ): void {
841
+ const assignmentKey = JSON.stringify(assignment)
842
+ const repairDepth = this.assignmentRepairDepthByKey.get(assignmentKey) ?? 0
843
+ if (repairDepth >= 2) return
844
+
845
+ const maximumRepairs = 8
846
+ const repairs: Array<Readonly<Record<string, string>>> = []
847
+ const repairKeys = new Set<string>()
848
+ const addRepair = (repair: Readonly<Record<string, string>>): void => {
849
+ const key = JSON.stringify(repair)
850
+ if (
851
+ repairKeys.has(key) ||
852
+ this.evaluatedAssignmentKeys.has(key) ||
853
+ this.queuedAssignmentKeys.has(key)
854
+ ) {
855
+ return
856
+ }
857
+ repairKeys.add(key)
858
+ this.queuedAssignmentKeys.add(key)
859
+ this.assignmentRepairDepthByKey.set(key, repairDepth + 1)
860
+ repairs.push(repair)
861
+ }
862
+ const repairBusIds: string[] = []
863
+ for (
864
+ let index = 0;
865
+ index < Math.max(failedBusIds.length, blockingBusIds.length);
866
+ index++
867
+ ) {
868
+ const failedBusId = failedBusIds[index]
869
+ const blockingBusId = blockingBusIds[index]
870
+ if (failedBusId && !repairBusIds.includes(failedBusId)) {
871
+ repairBusIds.push(failedBusId)
872
+ }
873
+ if (blockingBusId && !repairBusIds.includes(blockingBusId)) {
874
+ repairBusIds.push(blockingBusId)
875
+ }
876
+ }
877
+
878
+ for (const failedBusId of failedBusIds) {
879
+ const failedLayer = assignment[failedBusId]
880
+ const failedCandidateLayers = this.escapeLayersByBusId[failedBusId]
881
+ if (!failedLayer || !failedCandidateLayers) continue
882
+ for (const blockingBusId of blockingBusIds.slice(0, 4)) {
883
+ const blockingLayer = assignment[blockingBusId]
884
+ const blockingCandidateLayers = this.escapeLayersByBusId[blockingBusId]
885
+ if (
886
+ !blockingLayer ||
887
+ !blockingCandidateLayers ||
888
+ !failedCandidateLayers.includes(blockingLayer) ||
889
+ !blockingCandidateLayers.includes(failedLayer)
890
+ ) {
891
+ continue
892
+ }
893
+ addRepair({
894
+ ...assignment,
895
+ [failedBusId]: blockingLayer,
896
+ [blockingBusId]: failedLayer,
897
+ })
898
+ if (repairs.length >= maximumRepairs) break
899
+ }
900
+ if (repairs.length >= maximumRepairs) break
901
+ }
902
+
903
+ for (const busId of repairBusIds) {
904
+ const currentLayer = assignment[busId]
905
+ const candidateLayers = this.escapeLayersByBusId[busId]
906
+ if (!currentLayer || !candidateLayers) continue
907
+ const currentLayerIndex = candidateLayers.indexOf(currentLayer)
908
+ for (let shift = 1; shift < candidateLayers.length; shift++) {
909
+ const candidateLayer =
910
+ candidateLayers[
911
+ (Math.max(currentLayerIndex, 0) + shift) % candidateLayers.length
912
+ ]!
913
+ if (candidateLayer === currentLayer) continue
914
+ addRepair({ ...assignment, [busId]: candidateLayer })
915
+ if (repairs.length >= maximumRepairs) break
916
+ }
917
+ if (repairs.length >= maximumRepairs) break
918
+ }
919
+ this.pendingRepairAssignments.push(...repairs)
920
+ }
921
+
493
922
  override _step(): void {
494
- const assignment = this.layerAssignments[this.nextAssignmentIndex]
923
+ if (!this.groupedBeamEvaluated) {
924
+ this.groupedBeamEvaluated = true
925
+ let beamAttempt = this.evaluateGroupedBeam(-1)
926
+ if (!beamAttempt) {
927
+ beamAttempt = this.evaluateGroupedBeam(-1, true)
928
+ }
929
+ if (beamAttempt) {
930
+ this.attempts.push(beamAttempt.summary)
931
+ this.bestAttempt = beamAttempt
932
+ this.stats = {
933
+ assignment: 0,
934
+ assignmentCount: this.config.maxLayerCombinations,
935
+ routedBuses: `${beamAttempt.summary.routedBusCount}/${this.preparedBuses.length}`,
936
+ routedConnections: `${beamAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
937
+ failedBuses: "none",
938
+ bestScore: beamAttempt.summary.score,
939
+ }
940
+ this.solved = true
941
+ return
942
+ }
943
+ }
944
+
945
+ let assignment: Readonly<Record<string, string>> | undefined
946
+ while (
947
+ !assignment &&
948
+ this.nextAssignmentIndex < this.config.maxLayerCombinations
949
+ ) {
950
+ const preferGeneratedAssignment = this.nextAssignmentIndex % 3 === 0
951
+ let candidate: Readonly<Record<string, string>> | undefined
952
+ let candidateCameFromRepairQueue = false
953
+ if (preferGeneratedAssignment) {
954
+ candidate = this.layerAssignments[this.nextGeneratedAssignmentIndex++]
955
+ } else {
956
+ candidate = this.pendingRepairAssignments.pop()
957
+ candidateCameFromRepairQueue = candidate !== undefined
958
+ }
959
+ if (!candidate) {
960
+ candidate = preferGeneratedAssignment
961
+ ? this.pendingRepairAssignments.pop()
962
+ : this.layerAssignments[this.nextGeneratedAssignmentIndex++]
963
+ candidateCameFromRepairQueue =
964
+ preferGeneratedAssignment && candidate !== undefined
965
+ }
966
+ if (!candidate) break
967
+ const candidateKey = JSON.stringify(candidate)
968
+ if (candidateCameFromRepairQueue) {
969
+ this.queuedAssignmentKeys.delete(candidateKey)
970
+ }
971
+ if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
972
+ assignment = candidate
973
+ }
495
974
  if (!assignment) {
496
975
  if (
497
976
  this.bestAttempt &&
@@ -513,6 +992,18 @@ export class FanoutSolver extends BaseSolver {
513
992
  assignment,
514
993
  )
515
994
  this.nextAssignmentIndex++
995
+ this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
996
+ if (
997
+ !this.bestAttempt ||
998
+ attempt.summary.routedConnectionCount >=
999
+ this.bestAttempt.summary.routedConnectionCount
1000
+ ) {
1001
+ this.prioritizeFailedBusRepairs(
1002
+ assignment,
1003
+ attempt.summary.failedBusIds,
1004
+ attempt.blockingBusIds,
1005
+ )
1006
+ }
516
1007
  this.attempts.push(attempt.summary)
517
1008
  if (
518
1009
  !this.bestAttempt ||
@@ -522,7 +1013,7 @@ export class FanoutSolver extends BaseSolver {
522
1013
  }
523
1014
  this.stats = {
524
1015
  assignment: attempt.summary.assignmentIndex + 1,
525
- assignmentCount: this.layerAssignments.length,
1016
+ assignmentCount: this.config.maxLayerCombinations,
526
1017
  routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
527
1018
  routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
528
1019
  failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
@@ -537,7 +1028,7 @@ export class FanoutSolver extends BaseSolver {
537
1028
 
538
1029
  computeProgress(): number {
539
1030
  if (this.solved || this.failed) return 1
540
- return this.nextAssignmentIndex / this.layerAssignments.length
1031
+ return this.nextAssignmentIndex / this.config.maxLayerCombinations
541
1032
  }
542
1033
 
543
1034
  override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
@@ -550,6 +1041,15 @@ export class FanoutSolver extends BaseSolver {
550
1041
  "FanoutSolver: getOutput() called before a complete fanout was solved",
551
1042
  )
552
1043
  }
1044
+ const validation = this.validateCompletePlans(
1045
+ this.bestAttempt.plans,
1046
+ this.bestAttempt.outputSrj,
1047
+ )
1048
+ if (!validation.valid) {
1049
+ throw new Error(
1050
+ `FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
1051
+ )
1052
+ }
553
1053
  return {
554
1054
  simpleRouteJson: this.bestAttempt.outputSrj,
555
1055
  fanoutTraces: this.bestAttempt.plans.map((plan) => plan.trace),
@@ -570,6 +1070,7 @@ export class FanoutSolver extends BaseSolver {
570
1070
  this.preparedBuses.map((bus) => [bus.busId, bus.direction]),
571
1071
  ),
572
1072
  attempts: [...this.attempts],
1073
+ validation,
573
1074
  }
574
1075
  }
575
1076