@tscircuit/fanout-solver 0.0.17 → 0.0.18

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,24 @@ 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"
18
21
  import type {
19
22
  AssignmentAttempt,
20
23
  FanoutAttemptSummary,
21
24
  FanoutBorderDistribution,
25
+ FanoutRoutePlan,
22
26
  FanoutSolverOptions,
23
27
  FanoutSolverOutput,
24
28
  PreparedBus,
@@ -38,6 +42,17 @@ interface ResolvedFanoutConfig {
38
42
  maxLayerCombinations: number
39
43
  }
40
44
 
45
+ interface EvaluatedAssignment extends AssignmentAttempt {
46
+ blockingBusIds: string[]
47
+ }
48
+
49
+ interface GroupedBeamState {
50
+ assignment: Readonly<Record<string, string>>
51
+ plans: FanoutRoutePlan[]
52
+ }
53
+
54
+ type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
55
+
41
56
  function resolvePositiveNumber(label: string, value: number): number {
42
57
  if (!Number.isFinite(value) || value <= 0) {
43
58
  throw new Error(
@@ -199,62 +214,12 @@ function getBusDepthInRows(bus: PreparedBus): number {
199
214
  )
200
215
  }
201
216
 
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
217
  function createPreferredLayerAssignment(params: {
251
218
  buses: PreparedBus[]
252
219
  escapeLayers: string[]
253
- srj: SimpleRouteJson
254
- traceWidth: number
255
- clearance: number
220
+ escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
256
221
  }): Readonly<Record<string, string>> {
257
- const { buses, escapeLayers, srj, traceWidth, clearance } = params
222
+ const { buses, escapeLayers, escapeLayersByBusId } = params
258
223
  const assignment: Record<string, string> = {}
259
224
  const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
260
225
  let nextViaLayerIndex = 0
@@ -273,16 +238,13 @@ function createPreferredLayerAssignment(params: {
273
238
  assignment[bus.busId] = bus.termination.layer
274
239
  continue
275
240
  }
276
- const viaLayers = escapeLayers.filter((layer) => layer !== sourceLayer)
241
+ const routableEscapeLayers = escapeLayersByBusId[bus.busId] ?? escapeLayers
242
+ const viaLayers = routableEscapeLayers.filter(
243
+ (layer) => layer !== sourceLayer,
244
+ )
277
245
  if (
278
- escapeLayers.includes(sourceLayer) &&
279
- busIsOnOutwardComponentEdge(bus) &&
280
- !sourceLayerEscapeIsBlocked({
281
- bus,
282
- srj,
283
- traceWidth,
284
- clearance,
285
- })
246
+ routableEscapeLayers.includes(sourceLayer) &&
247
+ busIsOnOutwardComponentEdge(bus)
286
248
  ) {
287
249
  assignment[bus.busId] = sourceLayer
288
250
  } else if (viaLayers.length > 0) {
@@ -321,12 +283,66 @@ function prioritizeLayerAssignment(params: {
321
283
  ].slice(0, maxAssignments)
322
284
  }
323
285
 
286
+ function getCandidateEscapeLayersForBus(params: {
287
+ bus: PreparedBus
288
+ srj: SimpleRouteJson
289
+ config: ResolvedFanoutConfig
290
+ staticClearanceCache: RouteBusStaticClearanceCache
291
+ }): string[] {
292
+ const { bus, srj, config, staticClearanceCache } = params
293
+ const individuallyRoutableLayers = config.escapeLayers.filter(
294
+ (targetLayer) =>
295
+ routeBus({
296
+ srj,
297
+ bus,
298
+ targetLayer,
299
+ acceptedPlans: [],
300
+ layerNames: config.layerNames,
301
+ traceWidth: config.traceWidth,
302
+ viaDiameter: config.viaDiameter,
303
+ viaHoleDiameter: config.viaHoleDiameter,
304
+ clearance: config.clearance,
305
+ compactBusTracks: config.compactBusTracks,
306
+ staticClearanceCache,
307
+ }) !== null,
308
+ )
309
+
310
+ // Existing plans only add clearance constraints, so a layer that cannot
311
+ // route this bus by itself cannot become viable later in an assignment.
312
+ // Preserve the original candidates when none route so impossible problems
313
+ // still produce the usual failed-solver result instead of throwing here.
314
+ return individuallyRoutableLayers.length > 0
315
+ ? individuallyRoutableLayers
316
+ : config.escapeLayers
317
+ }
318
+
324
319
  export class FanoutSolver extends BaseSolver {
325
320
  readonly preparedBuses: PreparedBus[]
326
321
  readonly attempts: FanoutAttemptSummary[] = []
327
322
  readonly layerAssignments: Array<Readonly<Record<string, string>>>
328
323
  readonly config: ResolvedFanoutConfig
324
+ private readonly escapeLayersByBusId: Readonly<
325
+ Record<string, readonly string[]>
326
+ >
327
+ private readonly evaluatedAssignmentKeys = new Set<string>()
328
+ private readonly queuedAssignmentKeys = new Set<string>()
329
+ private readonly assignmentRepairDepthByKey = new Map<string, number>()
330
+ private readonly pendingRepairAssignments: Array<
331
+ Readonly<Record<string, string>>
332
+ > = []
333
+ private readonly routeStaticClearanceCache: RouteBusStaticClearanceCache =
334
+ new Map()
335
+ private readonly routingPrefixCache = new Map<
336
+ string,
337
+ {
338
+ plans: AssignmentAttempt["plans"]
339
+ failedBusIds: string[]
340
+ blockingBusCounts: Map<string, number>
341
+ }
342
+ >()
343
+ private groupedBeamEvaluated = false
329
344
  private nextAssignmentIndex = 0
345
+ private nextGeneratedAssignmentIndex = 0
330
346
  private bestAttempt: AssignmentAttempt | null = null
331
347
 
332
348
  constructor(
@@ -364,9 +380,27 @@ export class FanoutSolver extends BaseSolver {
364
380
  : [],
365
381
  ),
366
382
  )
383
+ const escapeLayersByBusId = Object.fromEntries(
384
+ this.preparedBuses.flatMap((bus) => {
385
+ if (bus.termination.type === "plane") return []
386
+ return [
387
+ [
388
+ bus.busId,
389
+ getCandidateEscapeLayersForBus({
390
+ bus,
391
+ srj: inputSrj,
392
+ config: this.config,
393
+ staticClearanceCache: this.routeStaticClearanceCache,
394
+ }),
395
+ ] as const,
396
+ ]
397
+ }),
398
+ )
399
+ this.escapeLayersByBusId = escapeLayersByBusId
367
400
  const generatedAssignments = generateLayerAssignments({
368
401
  busIds: boundaryBusIds,
369
402
  layers: this.config.escapeLayers,
403
+ layersByBusId: escapeLayersByBusId,
370
404
  maxAssignments: this.config.maxLayerCombinations,
371
405
  }).map((assignment) => ({
372
406
  ...assignment,
@@ -376,26 +410,26 @@ export class FanoutSolver extends BaseSolver {
376
410
  preferredAssignment: createPreferredLayerAssignment({
377
411
  buses: this.preparedBuses,
378
412
  escapeLayers: this.config.escapeLayers,
379
- srj: inputSrj,
380
- traceWidth: this.config.traceWidth,
381
- clearance: this.config.clearance,
413
+ escapeLayersByBusId,
382
414
  }),
383
415
  generatedAssignments,
384
416
  maxAssignments: this.config.maxLayerCombinations,
385
417
  })
386
- this.MAX_ITERATIONS = this.layerAssignments.length + 2
418
+ this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
387
419
  }
388
420
 
389
421
  override getSolverName(): string {
390
422
  return "FanoutSolver"
391
423
  }
392
424
 
393
- private evaluateAssignment(
425
+ private evaluateAssignmentWithStrategy(
394
426
  assignmentIndex: number,
395
427
  busLayerAssignments: Readonly<Record<string, string>>,
396
- ): AssignmentAttempt {
397
- const plans: AssignmentAttempt["plans"] = []
398
- const failedBusIds: string[] = []
428
+ routingStrategy: RoutingStrategy,
429
+ ): EvaluatedAssignment {
430
+ let plans: AssignmentAttempt["plans"] = []
431
+ let failedBusIds: string[] = []
432
+ let blockingBusCounts = new Map<string, number>()
399
433
  const isSingleLayerFanout = this.config.escapeLayers.length === 1
400
434
  if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
401
435
  const singleLayerParams = {
@@ -425,13 +459,21 @@ export class FanoutSolver extends BaseSolver {
425
459
  (a, b) =>
426
460
  Number(a.termination.type === "plane") -
427
461
  Number(b.termination.type === "plane") ||
462
+ (routingStrategy === "group-by-layer"
463
+ ? (busLayerAssignments[a.busId] ?? "").localeCompare(
464
+ busLayerAssignments[b.busId] ?? "",
465
+ )
466
+ : 0) ||
428
467
  b.componentObstacles.length - a.componentObstacles.length ||
429
468
  (isSingleLayerFanout
430
469
  ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
431
470
  : b.connections.length - a.connections.length ||
432
- getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)),
471
+ (routingStrategy === "deep-first"
472
+ ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
473
+ : getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b))),
433
474
  )
434
475
 
476
+ let routingPrefixKey = `${routingStrategy}|`
435
477
  for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
436
478
  ? []
437
479
  : busesInRoutingOrder) {
@@ -441,6 +483,15 @@ export class FanoutSolver extends BaseSolver {
441
483
  `FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
442
484
  )
443
485
  }
486
+ routingPrefixKey += `${targetLayer.length}:${targetLayer};`
487
+ const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey)
488
+ if (cachedPrefix) {
489
+ plans = [...cachedPrefix.plans]
490
+ failedBusIds = [...cachedPrefix.failedBusIds]
491
+ blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
492
+ continue
493
+ }
494
+ const currentBusBlockingCounts = new Map<string, number>()
444
495
  const busPlans = routeBus({
445
496
  srj: this.inputSrj,
446
497
  bus,
@@ -452,12 +503,25 @@ export class FanoutSolver extends BaseSolver {
452
503
  viaHoleDiameter: this.config.viaHoleDiameter,
453
504
  clearance: this.config.clearance,
454
505
  compactBusTracks: this.config.compactBusTracks,
506
+ staticClearanceCache: this.routeStaticClearanceCache,
507
+ blockingBusCounts: currentBusBlockingCounts,
455
508
  })
456
509
  if (!busPlans) {
457
510
  failedBusIds.push(bus.busId)
458
- continue
511
+ for (const [blockingBusId, count] of currentBusBlockingCounts) {
512
+ blockingBusCounts.set(
513
+ blockingBusId,
514
+ (blockingBusCounts.get(blockingBusId) ?? 0) + count,
515
+ )
516
+ }
517
+ } else {
518
+ plans.push(...busPlans)
459
519
  }
460
- plans.push(...busPlans)
520
+ this.routingPrefixCache.set(routingPrefixKey, {
521
+ plans: [...plans],
522
+ failedBusIds: [...failedBusIds],
523
+ blockingBusCounts: new Map(blockingBusCounts),
524
+ })
461
525
  }
462
526
 
463
527
  const routedBusCount = this.preparedBuses.length - failedBusIds.length
@@ -482,6 +546,9 @@ export class FanoutSolver extends BaseSolver {
482
546
  return {
483
547
  summary,
484
548
  plans,
549
+ blockingBusIds: [...blockingBusCounts.entries()]
550
+ .toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
551
+ .map(([busId]) => busId),
485
552
  outputSrj: buildOutputSimpleRouteJson({
486
553
  inputSrj: this.inputSrj,
487
554
  plans,
@@ -490,8 +557,337 @@ export class FanoutSolver extends BaseSolver {
490
557
  }
491
558
  }
492
559
 
560
+ private evaluateAssignment(
561
+ assignmentIndex: number,
562
+ busLayerAssignments: Readonly<Record<string, string>>,
563
+ ): EvaluatedAssignment {
564
+ let bestAttempt = this.evaluateAssignmentWithStrategy(
565
+ assignmentIndex,
566
+ busLayerAssignments,
567
+ "default",
568
+ )
569
+ if (
570
+ bestAttempt.summary.routedConnectionCount ===
571
+ this.inputSrj.connections.length
572
+ ) {
573
+ return bestAttempt
574
+ }
575
+
576
+ for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
577
+ const attempt = this.evaluateAssignmentWithStrategy(
578
+ assignmentIndex,
579
+ busLayerAssignments,
580
+ routingStrategy,
581
+ )
582
+ if (attempt.summary.score < bestAttempt.summary.score) {
583
+ bestAttempt = attempt
584
+ }
585
+ if (
586
+ bestAttempt.summary.routedConnectionCount ===
587
+ this.inputSrj.connections.length
588
+ ) {
589
+ return bestAttempt
590
+ }
591
+ }
592
+ return bestAttempt
593
+ }
594
+
595
+ /**
596
+ * Search layer assignments and track alternatives together. The regular
597
+ * assignment loop commits to one route per bus before the next bus is
598
+ * considered, so a locally-valid track can still starve a later bus. A
599
+ * bounded beam keeps several grouped-layer route prefixes alive and is
600
+ * especially useful for the many singleton buses in the SRJ19 samples.
601
+ */
602
+ private evaluateGroupedBeam(
603
+ assignmentIndex: number,
604
+ groupByDirection = false,
605
+ ): EvaluatedAssignment | null {
606
+ if (this.config.escapeLayers.length < 2) return null
607
+ if (this.preparedBuses.length > 56) return null
608
+ if (this.preparedBuses.some((bus) => bus.connections.length !== 1)) {
609
+ return null
610
+ }
611
+ if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
612
+ return null
613
+ }
614
+
615
+ const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
616
+ const aLayerCount =
617
+ a.termination.type === "plane"
618
+ ? 1
619
+ : (this.escapeLayersByBusId[a.busId]?.length ??
620
+ this.config.escapeLayers.length)
621
+ const bLayerCount =
622
+ b.termination.type === "plane"
623
+ ? 1
624
+ : (this.escapeLayersByBusId[b.busId]?.length ??
625
+ this.config.escapeLayers.length)
626
+ return (
627
+ Number(a.termination.type === "plane") -
628
+ Number(b.termination.type === "plane") ||
629
+ (groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
630
+ aLayerCount - bLayerCount ||
631
+ b.componentObstacles.length - a.componentObstacles.length ||
632
+ b.connections.length - a.connections.length ||
633
+ getBusDepthInRows(b) - getBusDepthInRows(a) ||
634
+ a.busId.localeCompare(b.busId)
635
+ )
636
+ })
637
+
638
+ const beamWidth = 128
639
+ const alternativesPerLayer = 4
640
+ let states: GroupedBeamState[] = [{ assignment: {}, plans: [] }]
641
+
642
+ const getStateScore = (state: GroupedBeamState): number => {
643
+ const routeLength = state.plans.reduce(
644
+ (total, plan) => total + plan.length,
645
+ 0,
646
+ )
647
+ const viaCount = state.plans.filter((plan) => plan.via).length
648
+ return (
649
+ routeLength +
650
+ viaCount * 0.1 +
651
+ assignmentLoadPenalty(state.assignment) * 0.01
652
+ )
653
+ }
654
+
655
+ for (const bus of busesInSearchOrder) {
656
+ const nextStates: GroupedBeamState[] = []
657
+ for (const state of states) {
658
+ const candidateLayers =
659
+ bus.termination.type === "plane"
660
+ ? [bus.termination.layer]
661
+ : (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
662
+ const layerLoads = new Map<string, number>()
663
+ for (const layer of Object.values(state.assignment)) {
664
+ layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + 1)
665
+ }
666
+ const sourceLayer = bus.connections[0]?.sourceLayer
667
+ const orderedLayers = candidateLayers.toSorted(
668
+ (first, second) =>
669
+ (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
670
+ Number(first === sourceLayer) - Number(second === sourceLayer) ||
671
+ first.localeCompare(second),
672
+ )
673
+
674
+ for (const targetLayer of orderedLayers) {
675
+ const busAlternatives = routeBusAlternatives(
676
+ {
677
+ srj: this.inputSrj,
678
+ bus,
679
+ targetLayer,
680
+ acceptedPlans: state.plans,
681
+ layerNames: this.config.layerNames,
682
+ traceWidth: this.config.traceWidth,
683
+ viaDiameter: this.config.viaDiameter,
684
+ viaHoleDiameter: this.config.viaHoleDiameter,
685
+ clearance: this.config.clearance,
686
+ compactBusTracks: this.config.compactBusTracks,
687
+ staticClearanceCache: this.routeStaticClearanceCache,
688
+ },
689
+ alternativesPerLayer,
690
+ )
691
+ for (const busPlans of busAlternatives) {
692
+ nextStates.push({
693
+ assignment: {
694
+ ...state.assignment,
695
+ [bus.busId]: targetLayer,
696
+ },
697
+ plans: [...state.plans, ...busPlans],
698
+ })
699
+ }
700
+ }
701
+ }
702
+
703
+ if (nextStates.length === 0) return null
704
+ nextStates.sort((first, second) => {
705
+ const scoreDifference = getStateScore(first) - getStateScore(second)
706
+ if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
707
+ return JSON.stringify(first.assignment).localeCompare(
708
+ JSON.stringify(second.assignment),
709
+ )
710
+ })
711
+ const statesByAssignment = new Map<string, number>()
712
+ states = []
713
+ for (const state of nextStates) {
714
+ const key = JSON.stringify(state.assignment)
715
+ const sameAssignmentCount = statesByAssignment.get(key) ?? 0
716
+ if (sameAssignmentCount >= 2) continue
717
+ statesByAssignment.set(key, sameAssignmentCount + 1)
718
+ states.push(state)
719
+ if (states.length >= beamWidth) break
720
+ }
721
+ }
722
+
723
+ const bestState = states[0]
724
+ if (!bestState) return null
725
+ const score =
726
+ bestState.plans.length === this.inputSrj.connections.length
727
+ ? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
728
+ bestState.plans.filter((plan) => plan.via).length * 0.1 +
729
+ assignmentLoadPenalty(bestState.assignment) * 0.01
730
+ : Number.POSITIVE_INFINITY
731
+ if (!Number.isFinite(score)) return null
732
+
733
+ const summary: FanoutAttemptSummary = {
734
+ assignmentIndex,
735
+ busLayerAssignments: bestState.assignment,
736
+ routedBusCount: this.preparedBuses.length,
737
+ routedConnectionCount: bestState.plans.length,
738
+ failedBusIds: [],
739
+ score,
740
+ }
741
+ return {
742
+ summary,
743
+ plans: bestState.plans,
744
+ blockingBusIds: [],
745
+ outputSrj: buildOutputSimpleRouteJson({
746
+ inputSrj: this.inputSrj,
747
+ plans: bestState.plans,
748
+ layerNames: this.config.layerNames,
749
+ }),
750
+ }
751
+ }
752
+
753
+ private prioritizeFailedBusRepairs(
754
+ assignment: Readonly<Record<string, string>>,
755
+ failedBusIds: readonly string[],
756
+ blockingBusIds: readonly string[],
757
+ ): void {
758
+ const assignmentKey = JSON.stringify(assignment)
759
+ const repairDepth = this.assignmentRepairDepthByKey.get(assignmentKey) ?? 0
760
+ if (repairDepth >= 2) return
761
+
762
+ const maximumRepairs = 8
763
+ const repairs: Array<Readonly<Record<string, string>>> = []
764
+ const repairKeys = new Set<string>()
765
+ const addRepair = (repair: Readonly<Record<string, string>>): void => {
766
+ const key = JSON.stringify(repair)
767
+ if (
768
+ repairKeys.has(key) ||
769
+ this.evaluatedAssignmentKeys.has(key) ||
770
+ this.queuedAssignmentKeys.has(key)
771
+ ) {
772
+ return
773
+ }
774
+ repairKeys.add(key)
775
+ this.queuedAssignmentKeys.add(key)
776
+ this.assignmentRepairDepthByKey.set(key, repairDepth + 1)
777
+ repairs.push(repair)
778
+ }
779
+ const repairBusIds: string[] = []
780
+ for (
781
+ let index = 0;
782
+ index < Math.max(failedBusIds.length, blockingBusIds.length);
783
+ index++
784
+ ) {
785
+ const failedBusId = failedBusIds[index]
786
+ const blockingBusId = blockingBusIds[index]
787
+ if (failedBusId && !repairBusIds.includes(failedBusId)) {
788
+ repairBusIds.push(failedBusId)
789
+ }
790
+ if (blockingBusId && !repairBusIds.includes(blockingBusId)) {
791
+ repairBusIds.push(blockingBusId)
792
+ }
793
+ }
794
+
795
+ for (const failedBusId of failedBusIds) {
796
+ const failedLayer = assignment[failedBusId]
797
+ const failedCandidateLayers = this.escapeLayersByBusId[failedBusId]
798
+ if (!failedLayer || !failedCandidateLayers) continue
799
+ for (const blockingBusId of blockingBusIds.slice(0, 4)) {
800
+ const blockingLayer = assignment[blockingBusId]
801
+ const blockingCandidateLayers = this.escapeLayersByBusId[blockingBusId]
802
+ if (
803
+ !blockingLayer ||
804
+ !blockingCandidateLayers ||
805
+ !failedCandidateLayers.includes(blockingLayer) ||
806
+ !blockingCandidateLayers.includes(failedLayer)
807
+ ) {
808
+ continue
809
+ }
810
+ addRepair({
811
+ ...assignment,
812
+ [failedBusId]: blockingLayer,
813
+ [blockingBusId]: failedLayer,
814
+ })
815
+ if (repairs.length >= maximumRepairs) break
816
+ }
817
+ if (repairs.length >= maximumRepairs) break
818
+ }
819
+
820
+ for (const busId of repairBusIds) {
821
+ const currentLayer = assignment[busId]
822
+ const candidateLayers = this.escapeLayersByBusId[busId]
823
+ if (!currentLayer || !candidateLayers) continue
824
+ const currentLayerIndex = candidateLayers.indexOf(currentLayer)
825
+ for (let shift = 1; shift < candidateLayers.length; shift++) {
826
+ const candidateLayer =
827
+ candidateLayers[
828
+ (Math.max(currentLayerIndex, 0) + shift) % candidateLayers.length
829
+ ]!
830
+ if (candidateLayer === currentLayer) continue
831
+ addRepair({ ...assignment, [busId]: candidateLayer })
832
+ if (repairs.length >= maximumRepairs) break
833
+ }
834
+ if (repairs.length >= maximumRepairs) break
835
+ }
836
+ this.pendingRepairAssignments.push(...repairs)
837
+ }
838
+
493
839
  override _step(): void {
494
- const assignment = this.layerAssignments[this.nextAssignmentIndex]
840
+ if (!this.groupedBeamEvaluated) {
841
+ this.groupedBeamEvaluated = true
842
+ let beamAttempt = this.evaluateGroupedBeam(-1)
843
+ if (!beamAttempt) {
844
+ beamAttempt = this.evaluateGroupedBeam(-1, true)
845
+ }
846
+ if (beamAttempt) {
847
+ this.attempts.push(beamAttempt.summary)
848
+ this.bestAttempt = beamAttempt
849
+ this.stats = {
850
+ assignment: 0,
851
+ assignmentCount: this.config.maxLayerCombinations,
852
+ routedBuses: `${beamAttempt.summary.routedBusCount}/${this.preparedBuses.length}`,
853
+ routedConnections: `${beamAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
854
+ failedBuses: "none",
855
+ bestScore: beamAttempt.summary.score,
856
+ }
857
+ this.solved = true
858
+ return
859
+ }
860
+ }
861
+
862
+ let assignment: Readonly<Record<string, string>> | undefined
863
+ while (
864
+ !assignment &&
865
+ this.nextAssignmentIndex < this.config.maxLayerCombinations
866
+ ) {
867
+ const preferGeneratedAssignment = this.nextAssignmentIndex % 3 === 0
868
+ let candidate: Readonly<Record<string, string>> | undefined
869
+ let candidateCameFromRepairQueue = false
870
+ if (preferGeneratedAssignment) {
871
+ candidate = this.layerAssignments[this.nextGeneratedAssignmentIndex++]
872
+ } else {
873
+ candidate = this.pendingRepairAssignments.pop()
874
+ candidateCameFromRepairQueue = candidate !== undefined
875
+ }
876
+ if (!candidate) {
877
+ candidate = preferGeneratedAssignment
878
+ ? this.pendingRepairAssignments.pop()
879
+ : this.layerAssignments[this.nextGeneratedAssignmentIndex++]
880
+ candidateCameFromRepairQueue =
881
+ preferGeneratedAssignment && candidate !== undefined
882
+ }
883
+ if (!candidate) break
884
+ const candidateKey = JSON.stringify(candidate)
885
+ if (candidateCameFromRepairQueue) {
886
+ this.queuedAssignmentKeys.delete(candidateKey)
887
+ }
888
+ if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
889
+ assignment = candidate
890
+ }
495
891
  if (!assignment) {
496
892
  if (
497
893
  this.bestAttempt &&
@@ -513,6 +909,18 @@ export class FanoutSolver extends BaseSolver {
513
909
  assignment,
514
910
  )
515
911
  this.nextAssignmentIndex++
912
+ this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
913
+ if (
914
+ !this.bestAttempt ||
915
+ attempt.summary.routedConnectionCount >=
916
+ this.bestAttempt.summary.routedConnectionCount
917
+ ) {
918
+ this.prioritizeFailedBusRepairs(
919
+ assignment,
920
+ attempt.summary.failedBusIds,
921
+ attempt.blockingBusIds,
922
+ )
923
+ }
516
924
  this.attempts.push(attempt.summary)
517
925
  if (
518
926
  !this.bestAttempt ||
@@ -522,7 +930,7 @@ export class FanoutSolver extends BaseSolver {
522
930
  }
523
931
  this.stats = {
524
932
  assignment: attempt.summary.assignmentIndex + 1,
525
- assignmentCount: this.layerAssignments.length,
933
+ assignmentCount: this.config.maxLayerCombinations,
526
934
  routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
527
935
  routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
528
936
  failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
@@ -537,7 +945,7 @@ export class FanoutSolver extends BaseSolver {
537
945
 
538
946
  computeProgress(): number {
539
947
  if (this.solved || this.failed) return 1
540
- return this.nextAssignmentIndex / this.layerAssignments.length
948
+ return this.nextAssignmentIndex / this.config.maxLayerCombinations
541
949
  }
542
950
 
543
951
  override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
@@ -37,9 +37,10 @@ export function getLayerSpan(
37
37
  export function generateLayerAssignments(params: {
38
38
  busIds: string[]
39
39
  layers: string[]
40
+ layersByBusId?: Readonly<Record<string, readonly string[]>>
40
41
  maxAssignments: number
41
42
  }): Array<Readonly<Record<string, string>>> {
42
- const { busIds, layers, maxAssignments } = params
43
+ const { busIds, layers, layersByBusId, maxAssignments } = params
43
44
  if (layers.length === 0) {
44
45
  throw new Error("FanoutSolver: no escape layers are available")
45
46
  }
@@ -49,7 +50,22 @@ export function generateLayerAssignments(params: {
49
50
  )
50
51
  }
51
52
 
52
- const rawCombinationCount = layers.length ** busIds.length
53
+ const availableLayersByBus = busIds.map(
54
+ (busId) => layersByBusId?.[busId] ?? layers,
55
+ )
56
+ const busWithoutLayersIndex = availableLayersByBus.findIndex(
57
+ (availableLayers) => availableLayers.length === 0,
58
+ )
59
+ if (busWithoutLayersIndex >= 0) {
60
+ throw new Error(
61
+ `FanoutSolver: no escape layers are available for bus "${busIds[busWithoutLayersIndex]}"`,
62
+ )
63
+ }
64
+
65
+ const rawCombinationCount = availableLayersByBus.reduce(
66
+ (count, availableLayers) => count * availableLayers.length,
67
+ 1,
68
+ )
53
69
  const combinationCount = Math.min(
54
70
  maxAssignments,
55
71
  Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments,
@@ -60,7 +76,8 @@ export function generateLayerAssignments(params: {
60
76
  function addAssignment(layerIndexes: number[]): void {
61
77
  const assignment: Record<string, string> = {}
62
78
  for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
63
- assignment[busIds[busIndex]!] = layers[layerIndexes[busIndex]!]!
79
+ assignment[busIds[busIndex]!] =
80
+ availableLayersByBus[busIndex]![layerIndexes[busIndex]!]!
64
81
  }
65
82
  const key = JSON.stringify(assignment)
66
83
  if (seenAssignments.has(key)) return
@@ -73,9 +90,10 @@ export function generateLayerAssignments(params: {
73
90
  const layerIndexes: number[] = []
74
91
  let remaining = ordinal
75
92
  for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
76
- const digit = remaining % layers.length
77
- remaining = Math.floor(remaining / layers.length)
78
- layerIndexes.push((digit + busIndex) % layers.length)
93
+ const layerCount = availableLayersByBus[busIndex]!.length
94
+ const digit = remaining % layerCount
95
+ remaining = Math.floor(remaining / layerCount)
96
+ layerIndexes.push((digit + busIndex) % layerCount)
79
97
  }
80
98
  addAssignment(layerIndexes)
81
99
  }
@@ -90,17 +108,22 @@ export function generateLayerAssignments(params: {
90
108
  }
91
109
 
92
110
  const balancedLayerIndexes = busIds.map(
93
- (_, busIndex) => busIndex % layers.length,
111
+ (_, busIndex) => busIndex % availableLayersByBus[busIndex]!.length,
94
112
  )
95
113
  addAssignment(balancedLayerIndexes)
114
+ const maximumAvailableLayerCount = Math.max(
115
+ ...availableLayersByBus.map((availableLayers) => availableLayers.length),
116
+ )
96
117
  for (
97
118
  let globalShift = 1;
98
- globalShift < layers.length && assignments.length < combinationCount;
119
+ globalShift < maximumAvailableLayerCount &&
120
+ assignments.length < combinationCount;
99
121
  globalShift++
100
122
  ) {
101
123
  addAssignment(
102
124
  balancedLayerIndexes.map(
103
- (layerIndex) => (layerIndex + globalShift) % layers.length,
125
+ (layerIndex, busIndex) =>
126
+ (layerIndex + globalShift) % availableLayersByBus[busIndex]!.length,
104
127
  ),
105
128
  )
106
129
  }
@@ -111,12 +134,14 @@ export function generateLayerAssignments(params: {
111
134
  ) {
112
135
  for (
113
136
  let shift = 1;
114
- shift < layers.length && assignments.length < combinationCount;
137
+ shift < availableLayersByBus[busIndex]!.length &&
138
+ assignments.length < combinationCount;
115
139
  shift++
116
140
  ) {
117
141
  const layerIndexes = [...balancedLayerIndexes]
118
142
  layerIndexes[busIndex] =
119
- (balancedLayerIndexes[busIndex]! + shift) % layers.length
143
+ (balancedLayerIndexes[busIndex]! + shift) %
144
+ availableLayersByBus[busIndex]!.length
120
145
  addAssignment(layerIndexes)
121
146
  }
122
147
  }
@@ -128,7 +153,8 @@ export function generateLayerAssignments(params: {
128
153
  addAssignment(
129
154
  busIds.map(
130
155
  (_, busIndex) =>
131
- mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) % layers.length,
156
+ mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
157
+ availableLayersByBus[busIndex]!.length,
132
158
  ),
133
159
  )
134
160
  }
package/lib/route-bus.ts CHANGED
@@ -21,7 +21,9 @@ import type {
21
21
  RoutedSegment,
22
22
  } from "./types"
23
23
 
24
- interface RouteBusParams {
24
+ export type RouteBusStaticClearanceCache = Map<string, boolean>
25
+
26
+ export interface RouteBusParams {
25
27
  srj: SimpleRouteJson
26
28
  bus: PreparedBus
27
29
  targetLayer: string
@@ -32,6 +34,8 @@ interface RouteBusParams {
32
34
  viaHoleDiameter: number
33
35
  clearance: number
34
36
  compactBusTracks: boolean
37
+ staticClearanceCache?: RouteBusStaticClearanceCache
38
+ blockingBusCounts?: Map<string, number>
35
39
  }
36
40
 
37
41
  interface TrackCandidate {
@@ -682,14 +686,13 @@ function segmentIsClearOfObstacles(params: {
682
686
  return true
683
687
  }
684
688
 
685
- function planIsClear(params: {
689
+ function planIsStaticallyClear(params: {
686
690
  plan: FanoutRoutePlan
687
- otherPlans: FanoutRoutePlan[]
688
691
  srj: SimpleRouteJson
689
692
  sharedBoundary: Bounds
690
693
  clearance: number
691
694
  }): boolean {
692
- const { plan, otherPlans, srj, sharedBoundary, clearance } = params
695
+ const { plan, srj, sharedBoundary, clearance } = params
693
696
  const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
694
697
  if (
695
698
  !pointIsInsideBounds(plan.exitPoint, routableBounds) ||
@@ -730,10 +733,36 @@ function planIsClear(params: {
730
733
  }
731
734
  }
732
735
 
736
+ return true
737
+ }
738
+
739
+ function planIsClearOfPlans(params: {
740
+ plan: FanoutRoutePlan
741
+ otherPlans: FanoutRoutePlan[]
742
+ clearance: number
743
+ blockingBusCounts?: Map<string, number>
744
+ }): boolean {
745
+ const { plan, otherPlans, clearance, blockingBusCounts } = params
733
746
  for (const otherPlan of otherPlans) {
747
+ const plansShareSourcePort =
748
+ (plan.sourcePoint.pcb_port_id &&
749
+ plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id) ||
750
+ (plan.sourcePoint.pointId &&
751
+ plan.sourcePoint.pointId === otherPlan.sourcePoint.pointId)
752
+ if (plansShareSourcePort) continue
753
+ const recordBlocker = (): void => {
754
+ if (otherPlan.busId === plan.busId) return
755
+ blockingBusCounts?.set(
756
+ otherPlan.busId,
757
+ (blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
758
+ )
759
+ }
734
760
  for (const segment of plan.segments) {
735
761
  for (const otherSegment of otherPlan.segments) {
736
- if (!segmentsAreClear(segment, otherSegment, clearance)) return false
762
+ if (!segmentsAreClear(segment, otherSegment, clearance)) {
763
+ recordBlocker()
764
+ return false
765
+ }
737
766
  }
738
767
  if (
739
768
  otherPlan.via?.spanLayers.includes(segment.layer) &&
@@ -744,6 +773,7 @@ function planIsClear(params: {
744
773
  ) <
745
774
  otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
746
775
  ) {
776
+ recordBlocker()
747
777
  return false
748
778
  }
749
779
  }
@@ -758,6 +788,7 @@ function planIsClear(params: {
758
788
  ) <
759
789
  plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
760
790
  ) {
791
+ recordBlocker()
761
792
  return false
762
793
  }
763
794
  }
@@ -769,6 +800,7 @@ function planIsClear(params: {
769
800
  distance(plan.via.center, otherPlan.via.center) <
770
801
  (plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
771
802
  ) {
803
+ recordBlocker()
772
804
  return false
773
805
  }
774
806
  }
@@ -776,6 +808,47 @@ function planIsClear(params: {
776
808
  return true
777
809
  }
778
810
 
811
+ function planIsClear(params: {
812
+ plan: FanoutRoutePlan
813
+ otherPlans: FanoutRoutePlan[]
814
+ staticClearanceCache?: RouteBusStaticClearanceCache
815
+ blockingBusCounts?: Map<string, number>
816
+ cacheKey: string
817
+ srj: SimpleRouteJson
818
+ sharedBoundary: Bounds
819
+ clearance: number
820
+ }): boolean {
821
+ const {
822
+ plan,
823
+ otherPlans,
824
+ staticClearanceCache,
825
+ blockingBusCounts,
826
+ cacheKey,
827
+ srj,
828
+ sharedBoundary,
829
+ clearance,
830
+ } = params
831
+ let staticallyClear = staticClearanceCache?.get(cacheKey)
832
+ if (staticallyClear === undefined) {
833
+ staticallyClear = planIsStaticallyClear({
834
+ plan,
835
+ srj,
836
+ sharedBoundary,
837
+ clearance,
838
+ })
839
+ staticClearanceCache?.set(cacheKey, staticallyClear)
840
+ }
841
+ return (
842
+ staticallyClear &&
843
+ planIsClearOfPlans({
844
+ plan,
845
+ otherPlans,
846
+ clearance,
847
+ blockingBusCounts,
848
+ })
849
+ )
850
+ }
851
+
779
852
  function routePlaneTerminatedBus(
780
853
  params: RouteBusParams,
781
854
  ): FanoutRoutePlan[] | null {
@@ -789,6 +862,8 @@ function routePlaneTerminatedBus(
789
862
  viaDiameter,
790
863
  viaHoleDiameter,
791
864
  clearance,
865
+ staticClearanceCache,
866
+ blockingBusCounts,
792
867
  } = params
793
868
  const sourceObstacle = bus.connections[0]?.sourceObstacle
794
869
  if (!sourceObstacle || bus.termination.type !== "plane") return null
@@ -833,6 +908,9 @@ function routePlaneTerminatedBus(
833
908
  !planIsClear({
834
909
  plan,
835
910
  otherPlans: [...acceptedPlans, ...candidatePlans],
911
+ staticClearanceCache,
912
+ blockingBusCounts,
913
+ cacheKey: `plane:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}`,
836
914
  srj,
837
915
  sharedBoundary: bus.sharedBoundary,
838
916
  clearance,
@@ -850,7 +928,10 @@ function routePlaneTerminatedBus(
850
928
  return null
851
929
  }
852
930
 
853
- export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
931
+ export function routeBusAlternatives(
932
+ params: RouteBusParams,
933
+ maxAlternatives = 1,
934
+ ): FanoutRoutePlan[][] {
854
935
  const {
855
936
  srj,
856
937
  bus,
@@ -862,13 +943,21 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
862
943
  viaHoleDiameter,
863
944
  clearance,
864
945
  compactBusTracks,
946
+ staticClearanceCache,
947
+ blockingBusCounts,
865
948
  } = params
949
+ if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
950
+ throw new Error(
951
+ `FanoutSolver: maxAlternatives must be a positive integer, received ${maxAlternatives}`,
952
+ )
953
+ }
866
954
  if (bus.termination.type === "plane") {
867
- return routePlaneTerminatedBus(params)
955
+ const plan = routePlaneTerminatedBus(params)
956
+ return plan ? [plan] : []
868
957
  }
869
958
  const exitAxis = getExitAxis(bus)
870
959
  const sourceObstacle = bus.connections[0]?.sourceObstacle
871
- if (!sourceObstacle) return []
960
+ if (!sourceObstacle) return [[]]
872
961
  const directionalPadSize = isHorizontal(bus.direction)
873
962
  ? sourceObstacle.width
874
963
  : sourceObstacle.height
@@ -886,71 +975,111 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
886
975
  : [1, -1]
887
976
  : [0]
888
977
 
889
- for (const viaHandedness of viaHandednesses) {
890
- for (const connectionOrder of getConnectionOrders(bus)) {
891
- const candidatePlans: FanoutRoutePlan[] = []
892
- let orderIsClear = true
893
- for (const preparedConnection of connectionOrder) {
894
- let acceptedPlan: FanoutRoutePlan | null = null
895
- for (const track of getTrackCandidates({
896
- bus,
897
- connection: preparedConnection,
898
- preferredTrack: getPreferredTrack({
899
- bus,
900
- connection: preparedConnection,
901
- targetUsesVia,
902
- interstitialEscape,
903
- compactBusTracks,
904
- traceWidth,
905
- viaDiameter,
906
- clearance,
907
- }),
908
- traceWidth,
978
+ const alternatives: FanoutRoutePlan[][] = []
979
+ const seenAlternativeKeys = new Set<string>()
980
+
981
+ const addAlternative = (plans: FanoutRoutePlan[]): void => {
982
+ const key = plans
983
+ .map(
984
+ (plan) =>
985
+ `${plan.connectionIndex}:${plan.targetLayer}:${plan.exitPoint.x}:${plan.exitPoint.y}:${plan.segments.map((segment) => `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`).join(";")}`,
986
+ )
987
+ .join("|")
988
+ if (seenAlternativeKeys.has(key)) return
989
+ seenAlternativeKeys.add(key)
990
+ alternatives.push(plans)
991
+ }
992
+
993
+ const searchConnectionOrder = (
994
+ connectionOrder: PreparedConnection[],
995
+ viaHandedness: ViaHandedness,
996
+ connectionIndex: number,
997
+ candidatePlans: FanoutRoutePlan[],
998
+ ): void => {
999
+ if (alternatives.length >= maxAlternatives) return
1000
+ if (connectionIndex >= connectionOrder.length) {
1001
+ addAlternative(candidatePlans)
1002
+ return
1003
+ }
1004
+
1005
+ const preparedConnection = connectionOrder[connectionIndex]!
1006
+ const connectionRank = getConnectionRank(bus, preparedConnection)
1007
+ const trackCandidates = getTrackCandidates({
1008
+ bus,
1009
+ connection: preparedConnection,
1010
+ preferredTrack: getPreferredTrack({
1011
+ bus,
1012
+ connection: preparedConnection,
1013
+ targetUsesVia,
1014
+ interstitialEscape,
1015
+ compactBusTracks,
1016
+ traceWidth,
1017
+ viaDiameter,
1018
+ clearance,
1019
+ }),
1020
+ traceWidth,
1021
+ clearance,
1022
+ })
1023
+ for (
1024
+ let trackIndex = 0;
1025
+ trackIndex < trackCandidates.length;
1026
+ trackIndex++
1027
+ ) {
1028
+ const track = trackCandidates[trackIndex]!
1029
+ const plan = buildPlan({
1030
+ preparedConnection,
1031
+ bus,
1032
+ targetLayer,
1033
+ track: track.value,
1034
+ exitAxis,
1035
+ layerNames,
1036
+ traceWidth,
1037
+ viaDiameter,
1038
+ viaHoleDiameter,
1039
+ viaHandedness,
1040
+ interstitialEscape,
1041
+ spreadLaneIndex: Math.min(
1042
+ connectionRank,
1043
+ bus.connections.length - connectionRank - 1,
1044
+ ),
1045
+ clearance,
1046
+ terminateAtVia: false,
1047
+ })
1048
+ if (
1049
+ !planIsClear({
1050
+ plan,
1051
+ otherPlans: [...acceptedPlans, ...candidatePlans],
1052
+ staticClearanceCache,
1053
+ blockingBusCounts,
1054
+ cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}`,
1055
+ srj,
1056
+ sharedBoundary: bus.sharedBoundary,
909
1057
  clearance,
910
- })) {
911
- const plan = buildPlan({
912
- preparedConnection,
913
- bus,
914
- targetLayer,
915
- track: track.value,
916
- exitAxis,
917
- layerNames,
918
- traceWidth,
919
- viaDiameter,
920
- viaHoleDiameter,
921
- viaHandedness,
922
- interstitialEscape,
923
- spreadLaneIndex: Math.min(
924
- getConnectionRank(bus, preparedConnection),
925
- bus.connections.length -
926
- getConnectionRank(bus, preparedConnection) -
927
- 1,
928
- ),
929
- clearance,
930
- terminateAtVia: false,
931
- })
932
- if (
933
- planIsClear({
934
- plan,
935
- otherPlans: [...acceptedPlans, ...candidatePlans],
936
- srj,
937
- sharedBoundary: bus.sharedBoundary,
938
- clearance,
939
- })
940
- ) {
941
- acceptedPlan = plan
942
- break
943
- }
944
- }
945
- if (!acceptedPlan) {
946
- orderIsClear = false
947
- break
948
- }
949
- candidatePlans.push(acceptedPlan)
1058
+ })
1059
+ ) {
1060
+ continue
950
1061
  }
951
- if (orderIsClear) return candidatePlans
1062
+ searchConnectionOrder(
1063
+ connectionOrder,
1064
+ viaHandedness,
1065
+ connectionIndex + 1,
1066
+ [...candidatePlans, plan],
1067
+ )
1068
+ if (alternatives.length >= maxAlternatives) return
1069
+ if (maxAlternatives === 1) return
952
1070
  }
953
1071
  }
954
1072
 
955
- return null
1073
+ for (const viaHandedness of viaHandednesses) {
1074
+ for (const connectionOrder of getConnectionOrders(bus)) {
1075
+ searchConnectionOrder(connectionOrder, viaHandedness, 0, [])
1076
+ if (alternatives.length >= maxAlternatives) return alternatives
1077
+ }
1078
+ }
1079
+
1080
+ return alternatives
1081
+ }
1082
+
1083
+ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
1084
+ return routeBusAlternatives(params, 1)[0] ?? null
956
1085
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",