@tscircuit/fanout-solver 0.0.53 → 0.0.54

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,5 +1,6 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
+ import { shortenBusPlans } from "./shorten-bus-plans"
3
4
  import { type GraphicsObject, mergeGraphics } from "graphics-debug"
4
5
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
5
6
  import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
@@ -1380,8 +1381,12 @@ export class FanoutSolver extends BaseSolver {
1380
1381
  private *routeDenseThroughAllMixedTerminationSteps(params: {
1381
1382
  busLayerAssignments: Readonly<Record<string, string>>
1382
1383
  busesInRoutingOrder: readonly PreparedBus[]
1384
+ denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
1385
+ lengthMatchingStage?: "before-planes" | "after-planes"
1383
1386
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1384
1387
  if (this.config.allowBlindAndBuriedVias) return null
1388
+ const usePadAlignedDenseRouting =
1389
+ params.denseRoutingStrategy !== "boundary-aligned"
1385
1390
  const debugDense = (...values: unknown[]) => {
1386
1391
  if (process.env.FANOUT_DEBUG_DENSE === "1") {
1387
1392
  if (
@@ -1414,6 +1419,9 @@ export class FanoutSolver extends BaseSolver {
1414
1419
  const useConfiguredDensePlaneRouting =
1415
1420
  this.config.densePlaneReservationBusIds.length > 0 ||
1416
1421
  this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
1422
+ const matchLengthsAfterPlanes =
1423
+ useConfiguredDensePlaneRouting &&
1424
+ params.lengthMatchingStage !== "before-planes"
1417
1425
  const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
1418
1426
  unsortedBoundaryBuses.map((bus) => bus.connections.length),
1419
1427
  )
@@ -1902,6 +1910,7 @@ export class FanoutSolver extends BaseSolver {
1902
1910
  ...singletonDeferralCandidates.filter((bus) => {
1903
1911
  const containingBus = getContainingWideSourceField(bus)
1904
1912
  const sharesContainingBusLayer =
1913
+ usePadAlignedDenseRouting &&
1905
1914
  !useConfiguredDensePlaneRouting &&
1906
1915
  containingBus &&
1907
1916
  params.busLayerAssignments[containingBus.busId] ===
@@ -2160,9 +2169,11 @@ export class FanoutSolver extends BaseSolver {
2160
2169
  fixedViaPointsByConnectionIndex,
2161
2170
  reservedVias: getReservedVias(bus),
2162
2171
  viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2163
- allowBoundarySideViaFallback: !useConfiguredDensePlaneRouting,
2172
+ allowBoundarySideViaFallback: true,
2173
+ preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
2164
2174
  adaptiveWindingRouteOrder,
2165
- alignWindingGridToPads: !useConfiguredDensePlaneRouting,
2175
+ alignWindingGridToPads:
2176
+ usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting,
2166
2177
  fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
2167
2178
  ? 60
2168
2179
  : useConfiguredDensePlaneRouting
@@ -2218,7 +2229,7 @@ export class FanoutSolver extends BaseSolver {
2218
2229
  if (!busPlans && preferSingleLayerWinding) {
2219
2230
  busPlans = (yield* routeAlternatives(routeParams, 1))[0]
2220
2231
  }
2221
- if (!busPlans && !useConfiguredDensePlaneRouting) {
2232
+ if (!busPlans) {
2222
2233
  const originalPoints = fixedViaPointsByConnectionIndex
2223
2234
  const originalOutward =
2224
2235
  preferBoundaryOutwardByBusId.get(bus.busId) ?? true
@@ -2292,6 +2303,7 @@ export class FanoutSolver extends BaseSolver {
2292
2303
  ...routeParams,
2293
2304
  fixedViaPointsByConnectionIndex: rematchedPoints,
2294
2305
  reservedVias: getReservedVias(bus),
2306
+ alignWindingGridToPads: useConfiguredDensePlaneRouting,
2295
2307
  fixedViaFallbackRouteOrderAttempts: 3,
2296
2308
  },
2297
2309
  1,
@@ -2313,7 +2325,7 @@ export class FanoutSolver extends BaseSolver {
2313
2325
  // Only pay for additional A* variants when the first topology is so
2314
2326
  // skewed that compact meanders are unlikely to absorb the deficit.
2315
2327
  // This keeps already-near-matched buses on the single-attempt path.
2316
- if (needsRouteDiversity) {
2328
+ if (needsRouteDiversity && !matchLengthsAfterPlanes) {
2317
2329
  busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
2318
2330
  (first, second) => {
2319
2331
  const firstLengths = first.map((plan) => plan.length)
@@ -2630,6 +2642,7 @@ export class FanoutSolver extends BaseSolver {
2630
2642
  let feasibleAlternatePlanePlans: FanoutRoutePlan[] = []
2631
2643
  const matchViaPointsAroundPlans = (
2632
2644
  candidatePlans: readonly FanoutRoutePlan[],
2645
+ promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
2633
2646
  ): Map<number, { x: number; y: number }> | null => {
2634
2647
  feasibleAlternatePlanePlans = []
2635
2648
  const fixedBoundaryViaPoints = new Map(
@@ -2645,10 +2658,18 @@ export class FanoutSolver extends BaseSolver {
2645
2658
  segment,
2646
2659
  })),
2647
2660
  )
2648
- const boundaryBusesToMatch = useConfiguredDensePlaneRouting
2661
+ const preserveBoundaryCopper =
2662
+ !useConfiguredDensePlaneRouting ||
2663
+ candidatePlans.some(
2664
+ (plan) =>
2665
+ plan.segments.filter(
2666
+ (segment) => segment.layer === plan.sourceLayer,
2667
+ ).length > 1,
2668
+ )
2669
+ const boundaryBusesToMatch = !preserveBoundaryCopper
2649
2670
  ? boundaryBuses
2650
2671
  : []
2651
- const blockingVias = useConfiguredDensePlaneRouting
2672
+ const blockingVias = !preserveBoundaryCopper
2652
2673
  ? []
2653
2674
  : candidatePlans.flatMap((plan) =>
2654
2675
  [
@@ -2667,7 +2688,7 @@ export class FanoutSolver extends BaseSolver {
2667
2688
  // Completed boundary paths can reach their via with several source-
2668
2689
  // layer segments. Treat their actual copper as fixed obstacles instead
2669
2690
  // of reinterpreting each as a straight pad-to-via dogbone.
2670
- const retainedViaPoints = useConfiguredDensePlaneRouting
2691
+ const retainedViaPoints = !preserveBoundaryCopper
2671
2692
  ? null
2672
2693
  : matchComponentDogboneViaSites(planeBuses, {
2673
2694
  viaDiameter: this.config.viaDiameter,
@@ -2775,6 +2796,7 @@ export class FanoutSolver extends BaseSolver {
2775
2796
  )
2776
2797
  : []
2777
2798
  const additionalAlternatePlaneBusIds = new Set([
2799
+ ...promotedAlternatePlaneBusIds,
2778
2800
  ...this.config.denseUnrestrictedPlaneRoutingBusIds,
2779
2801
  ...(process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(
2780
2802
  ",",
@@ -3345,7 +3367,16 @@ export class FanoutSolver extends BaseSolver {
3345
3367
  )[0]
3346
3368
  if (!promotedPlans) {
3347
3369
  debugDense("plane-route:promote-failed", planeBus.busId)
3348
- return null
3370
+ // Let these newly blocked drops participate in the joint
3371
+ // choice. Each retry adds previously excluded plane buses,
3372
+ // so the recursion is bounded by the number of plane buses.
3373
+ return matchViaPointsAroundPlans(
3374
+ candidatePlans,
3375
+ new Set([
3376
+ ...promotedAlternatePlaneBusIds,
3377
+ ...zeroCandidatePlaneBuses.map((bus) => bus.busId),
3378
+ ]),
3379
+ )
3349
3380
  }
3350
3381
  feasibleAlternatePlanePlans.push(...promotedPlans)
3351
3382
  }
@@ -3466,22 +3497,25 @@ export class FanoutSolver extends BaseSolver {
3466
3497
  : null
3467
3498
  }
3468
3499
  debugDense("length-match:start", matchedPlans.length)
3469
- const matchedLengthResult = matchBusPlanLengths({
3470
- plans: matchedPlans,
3471
- preparedBuses: this.preparedBuses,
3472
- inputSrj: this.inputSrj,
3473
- sharedBoundary: this.getValidationBoundary(),
3474
- clearance: this.config.clearance,
3475
- allowBlindAndBuriedVias: false,
3476
- allowSameNetMerges: this.config.allowSameNetMerges,
3477
- allowMatchingInsideDenseBounds: true,
3478
- candidatePlansAreFeasible: (candidatePlans) => {
3479
- const candidateViaPoints = matchViaPointsAroundPlans(candidatePlans)
3480
- if (!candidateViaPoints) return false
3481
- feasibleViaPoints = candidateViaPoints
3482
- return true
3483
- },
3484
- })
3500
+ const matchedLengthResult = matchLengthsAfterPlanes
3501
+ ? { plans: matchedPlans }
3502
+ : matchBusPlanLengths({
3503
+ plans: matchedPlans,
3504
+ preparedBuses: this.preparedBuses,
3505
+ inputSrj: this.inputSrj,
3506
+ sharedBoundary: this.getValidationBoundary(),
3507
+ clearance: this.config.clearance,
3508
+ allowBlindAndBuriedVias: false,
3509
+ allowSameNetMerges: this.config.allowSameNetMerges,
3510
+ allowMatchingInsideDenseBounds: true,
3511
+ candidatePlansAreFeasible: (candidatePlans) => {
3512
+ const candidateViaPoints =
3513
+ matchViaPointsAroundPlans(candidatePlans)
3514
+ if (!candidateViaPoints) return false
3515
+ feasibleViaPoints = candidateViaPoints
3516
+ return true
3517
+ },
3518
+ })
3485
3519
  debugDense(
3486
3520
  "length-match:complete",
3487
3521
  matchedLengthResult.plans?.length ?? "failed",
@@ -3565,6 +3599,65 @@ export class FanoutSolver extends BaseSolver {
3565
3599
  yield
3566
3600
  }
3567
3601
  }
3602
+ if (matchedRoutingSucceeded && matchLengthsAfterPlanes) {
3603
+ // With the configured plane escape strategy, route those dogbones
3604
+ // before tuning. Length matching can then check the actual complete
3605
+ // copper instead of repeatedly searching for a new plane assignment
3606
+ // for every prospective meander.
3607
+ const lengthMatchingParams = {
3608
+ plans: matchedPlans,
3609
+ preparedBuses: this.preparedBuses,
3610
+ inputSrj: this.inputSrj,
3611
+ sharedBoundary: this.getValidationBoundary(),
3612
+ clearance: this.config.clearance,
3613
+ allowBlindAndBuriedVias: false,
3614
+ allowSameNetMerges: this.config.allowSameNetMerges,
3615
+ allowMatchingInsideDenseBounds: true,
3616
+ allowPairLaneSpreading: true,
3617
+ }
3618
+ let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
3619
+ const shortenedBusIds = new Set<string>()
3620
+ while (
3621
+ !matchedLengthResult.plans &&
3622
+ matchedLengthResult.failedBus &&
3623
+ !shortenedBusIds.has(matchedLengthResult.failedBus.busId)
3624
+ ) {
3625
+ const bus = matchedLengthResult.failedBus
3626
+ shortenedBusIds.add(bus.busId)
3627
+ const shortened = shortenBusPlans({
3628
+ plans: matchedPlans,
3629
+ bus,
3630
+ srj: this.inputSrj,
3631
+ sharedBoundary: this.getValidationBoundary(),
3632
+ layerNames: this.config.layerNames,
3633
+ traceWidth: this.config.traceWidth,
3634
+ viaDiameter: this.config.viaDiameter,
3635
+ viaHoleDiameter: this.config.viaHoleDiameter,
3636
+ clearance: this.config.clearance,
3637
+ allowSameNetMerges: this.config.allowSameNetMerges,
3638
+ })
3639
+ if (shortened.every((plan, index) => plan === matchedPlans[index]))
3640
+ break
3641
+ matchedPlans = shortened
3642
+ matchedLengthResult = matchBusPlanLengths({
3643
+ ...lengthMatchingParams,
3644
+ plans: matchedPlans,
3645
+ })
3646
+ }
3647
+ if (matchedLengthResult.plans) {
3648
+ matchedPlans = matchedLengthResult.plans
3649
+ } else {
3650
+ matchedRoutingSucceeded = false
3651
+ }
3652
+ this.setInProgressPlans({
3653
+ phase: "match-dense-complete-lengths",
3654
+ plans: matchedPlans,
3655
+ strategy: "default",
3656
+ unitIndex: ++denseWorkUnitIndex,
3657
+ unitCount: denseWorkUnitCount,
3658
+ })
3659
+ yield
3660
+ }
3568
3661
  const densePlansAreClear =
3569
3662
  matchedRoutingSucceeded &&
3570
3663
  fanoutPlansAreClear({
@@ -3586,7 +3679,30 @@ export class FanoutSolver extends BaseSolver {
3586
3679
  }
3587
3680
  }
3588
3681
 
3589
- if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1") return null
3682
+ if (matchLengthsAfterPlanes) {
3683
+ return yield* this.routeDenseThroughAllMixedTerminationSteps({
3684
+ ...params,
3685
+ lengthMatchingStage: "before-planes",
3686
+ })
3687
+ }
3688
+
3689
+ // Pad-aligned windings can fence off a same-layer singleton even when
3690
+ // every wide bus routes successfully. Before widening the search, retry
3691
+ // the coordinated reservation with a boundary-aligned grid and let those
3692
+ // singleton sites remain provisional until surrounding copper is fixed.
3693
+ if (usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting) {
3694
+ const boundaryAlignedState =
3695
+ yield* this.routeDenseThroughAllMixedTerminationSteps({
3696
+ ...params,
3697
+ denseRoutingStrategy: "boundary-aligned",
3698
+ })
3699
+ if (boundaryAlignedState) return boundaryAlignedState
3700
+ }
3701
+ if (
3702
+ !usePadAlignedDenseRouting ||
3703
+ process.env.FANOUT_DEBUG_DENSE_ONLY === "1"
3704
+ )
3705
+ return null
3590
3706
 
3591
3707
  const maximumStates = 8
3592
3708
  const getBoundaryStates = (
@@ -565,6 +565,67 @@ function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
565
565
  return Math.max(...lengths) - Math.min(...lengths)
566
566
  }
567
567
 
568
+ function* createSpreadLaneCandidates(
569
+ plan: FanoutRoutePlan,
570
+ clearance: number,
571
+ ): Generator<FanoutRoutePlan> {
572
+ for (const { segment, index } of plan.segments
573
+ .map((segment, index) => ({ segment, index }))
574
+ .filter(({ segment }) => segment.layer === plan.targetLayer)
575
+ .toSorted(
576
+ (a, b) =>
577
+ distance(b.segment.start, b.segment.end) -
578
+ distance(a.segment.start, a.segment.end),
579
+ )) {
580
+ const length = distance(segment.start, segment.end)
581
+ if (length <= EPSILON) continue
582
+ const dx = Math.abs(segment.end.x - segment.start.x)
583
+ const dy = Math.abs(segment.end.y - segment.start.y)
584
+ if (dx > EPSILON && dy > EPSILON && Math.abs(dx - dy) > EPSILON) continue
585
+ const tangent = {
586
+ x: (segment.end.x - segment.start.x) / length,
587
+ y: (segment.end.y - segment.start.y) / length,
588
+ }
589
+ const pitch = segment.width + clearance
590
+ for (const multiple of [2, 3, 4, 6]) {
591
+ const offset = pitch * multiple
592
+ if (length < 2 * offset + pitch) continue
593
+ for (const sign of [1, -1]) {
594
+ const normal = { x: -tangent.y * sign, y: tangent.x * sign }
595
+ const points = [
596
+ segment.start,
597
+ addScaled(addScaled(segment.start, tangent, offset), normal, offset),
598
+ addScaled(addScaled(segment.end, tangent, -offset), normal, offset),
599
+ segment.end,
600
+ ]
601
+ const replacement = points.slice(1).map((end, i) => ({
602
+ ...segment,
603
+ start: points[i]!,
604
+ end,
605
+ }))
606
+ const segments = [
607
+ ...plan.segments.slice(0, index),
608
+ ...replacement,
609
+ ...plan.segments.slice(index + 1),
610
+ ]
611
+ if (hasNonAdjacentSelfIntersection(segments)) continue
612
+ if (
613
+ !replacementCopperIsSelfClear({
614
+ plan,
615
+ segments,
616
+ replacementStartIndex: index,
617
+ replacementSegmentCount: replacement.length,
618
+ clearance,
619
+ })
620
+ )
621
+ continue
622
+ const candidate = createPlanWithSegments(plan, segments)
623
+ if (candidate) yield candidate
624
+ }
625
+ }
626
+ }
627
+ }
628
+
568
629
  /**
569
630
  * Adds straight/45-degree meanders after the dense component escape. Matching
570
631
  * is atomic: a constrained bus either satisfies its declared skew with the
@@ -584,6 +645,8 @@ export function matchBusPlanLengths(params: {
584
645
  * revalidated and whose remaining dogbone capacity is checked atomically.
585
646
  */
586
647
  allowMatchingInsideDenseBounds?: boolean
648
+ /** Allow a differential pair's longer lane to move aside before tuning its mate. */
649
+ allowPairLaneSpreading?: boolean
587
650
  /**
588
651
  * Rejects a geometrically clear candidate when it would make a caller-owned
589
652
  * downstream assignment (such as pending plane dogbones) infeasible.
@@ -760,6 +823,49 @@ export function matchBusPlanLengths(params: {
760
823
  }
761
824
  if (acceptedPlans) break
762
825
  }
826
+ if (
827
+ !acceptedPlans &&
828
+ params.allowPairLaneSpreading &&
829
+ bus.connections.length === 2
830
+ ) {
831
+ // A tightly packed pair may leave no space to lengthen the inner lane.
832
+ // Move the outer lane, then retune the complete pair atomically. Only
833
+ // four geometrically clear placements may start another matching pass.
834
+ const longer = busPlans.find((plan) => plan !== shortest)!
835
+ let attempts = 0
836
+ for (const candidate of createSpreadLaneCandidates(longer, clearance)) {
837
+ const nextPlans = matchedPlans.map((plan) =>
838
+ plan === longer ? candidate : plan,
839
+ )
840
+ if (
841
+ !fanoutPlansAreClear({
842
+ plans: nextPlans,
843
+ srj: inputSrj,
844
+ sharedBoundary,
845
+ clearance,
846
+ allowBlindAndBuriedVias,
847
+ allowSameNetMerges,
848
+ })
849
+ )
850
+ continue
851
+ if (
852
+ candidatePlansAreFeasible &&
853
+ !candidatePlansAreFeasible(nextPlans)
854
+ )
855
+ continue
856
+ const result = matchBusPlanLengths({
857
+ ...params,
858
+ plans: nextPlans,
859
+ preparedBuses: [bus],
860
+ allowPairLaneSpreading: false,
861
+ })
862
+ if (result.plans) {
863
+ acceptedPlans = result.plans
864
+ break
865
+ }
866
+ if (++attempts >= 4) break
867
+ }
868
+ }
763
869
  if (!acceptedPlans) return { plans: null, failedBus: bus }
764
870
  matchedPlans = acceptedPlans
765
871
  }
package/lib/route-bus.ts CHANGED
@@ -64,6 +64,8 @@ export interface RouteBusParams {
64
64
  viaMinimalOnly?: boolean
65
65
  /** Permit a singleton to move its provisional via near the boundary. */
66
66
  allowBoundarySideViaFallback?: boolean
67
+ /** Reserve corner tuning space when selecting a boundary-side via. */
68
+ preferCornerBoundaryVia?: boolean
67
69
  /** Retry blocked winding terminals ahead of already-routed terminals. */
68
70
  adaptiveWindingRouteOrder?: boolean
69
71
  /** Preserve pad-lattice channels in the automatic dense routing path. */
@@ -1611,8 +1613,10 @@ function segmentIsClearOfObstacles(params: {
1611
1613
  ) {
1612
1614
  continue
1613
1615
  }
1616
+ // A winding escape can turn more than once while leaving its own pad.
1614
1617
  if (
1615
- segmentIndex === 0 &&
1618
+ segmentIndex >= 0 &&
1619
+ segmentIndex < (plan.sourceEscapeSegmentCount ?? 1) &&
1616
1620
  obstacle === plan.sourceObstacle &&
1617
1621
  segment.layer === plan.sourceLayer
1618
1622
  ) {
@@ -2003,7 +2007,9 @@ export function fanoutPlansAreClear(params: {
2003
2007
  }
2004
2008
 
2005
2009
  function routePlaneTerminatedBus(
2006
- params: RouteBusParams,
2010
+ params: RouteBusParams & {
2011
+ collectAlternative?: (plan: FanoutRoutePlan) => boolean
2012
+ },
2007
2013
  ): FanoutRoutePlan[] | null {
2008
2014
  const {
2009
2015
  srj,
@@ -2037,6 +2043,8 @@ function routePlaneTerminatedBus(
2037
2043
  remainingPlaneCandidatesToSkip--
2038
2044
  continue
2039
2045
  }
2046
+ if (params.collectAlternative && !params.collectAlternative(plan))
2047
+ continue
2040
2048
  return plan
2041
2049
  }
2042
2050
  return undefined
@@ -2452,6 +2460,7 @@ export function* routeBusAlternativesSteps(
2452
2460
  reservedVias = [],
2453
2461
  viaMinimalOnly = false,
2454
2462
  allowBoundarySideViaFallback = false,
2463
+ preferCornerBoundaryVia = false,
2455
2464
  adaptiveWindingRouteOrder = false,
2456
2465
  alignWindingGridToPads = false,
2457
2466
  fixedViaFallbackRouteOrderAttempts = 24,
@@ -2473,6 +2482,19 @@ export function* routeBusAlternativesSteps(
2473
2482
  }
2474
2483
  if (bus.termination.type === "plane") {
2475
2484
  const alternatives: FanoutRoutePlan[][] = []
2485
+ if (bus.connections.length === 1 && maxAlternatives > 1) {
2486
+ // Enumerate once instead of rebuilding and skipping every earlier route
2487
+ // for each successive alternative. Keep the original candidate order.
2488
+ routePlaneTerminatedBus({
2489
+ ...params,
2490
+ planeCandidateSkipCount: 0,
2491
+ collectAlternative: (plan) => {
2492
+ alternatives.push([plan])
2493
+ return alternatives.length >= maxAlternatives
2494
+ },
2495
+ })
2496
+ return alternatives
2497
+ }
2476
2498
  for (
2477
2499
  let planeCandidateSkipCount = 0;
2478
2500
  planeCandidateSkipCount < maxAlternatives;
@@ -2801,14 +2823,19 @@ export function* routeBusAlternativesSteps(
2801
2823
  acceptedBoundaryPlansExist && !allowBlindAndBuriedVias
2802
2824
  ? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
2803
2825
  : [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
2826
+ const unreservedTerminalPatterns: CoordinatedTerminalPattern[] =
2827
+ canUseViaInPadTerminals
2828
+ ? planeTerminationsAlreadyOccupyTheFanout
2829
+ ? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
2830
+ : [...dogboneTerminalPatterns, viaInPadTerminalPattern]
2831
+ : dogboneTerminalPatterns
2832
+ // Automatically matched sites are candidates, not caller reservations.
2833
+ // Keep the ordinary dogbone patterns available before adding crossover
2834
+ // vias when that first site assignment cannot route the complete bus.
2804
2835
  const terminalPatterns: CoordinatedTerminalPattern[] =
2805
- fixedViaTerminalPatterns.length > 0
2836
+ fixedViaPointsByConnectionIndex
2806
2837
  ? fixedViaTerminalPatterns
2807
- : canUseViaInPadTerminals
2808
- ? planeTerminationsAlreadyOccupyTheFanout
2809
- ? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
2810
- : [...dogboneTerminalPatterns, viaInPadTerminalPattern]
2811
- : dogboneTerminalPatterns
2838
+ : [...fixedViaTerminalPatterns, ...unreservedTerminalPatterns]
2812
2839
  const seenTerminalSignatures = new Set<string>()
2813
2840
  for (const terminalPattern of terminalPatterns) {
2814
2841
  const terminals = bus.connections.map((preparedConnection) => {
@@ -2857,12 +2884,24 @@ export function* routeBusAlternativesSteps(
2857
2884
  ),
2858
2885
  }
2859
2886
  })
2887
+ const alignGridToPads =
2888
+ alignWindingGridToPads ||
2889
+ Boolean(terminalPattern.getViaPoint && !fixedViaPointsByConnectionIndex)
2890
+ const gridStepDivisor =
2891
+ terminalPattern.getViaPoint &&
2892
+ Math.min(bus.pitchX, bus.pitchY) -
2893
+ 2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
2894
+ traceWidth + clearance
2895
+ ? 2
2896
+ : 1
2860
2897
  const terminalSignature = `${terminals
2861
2898
  .map(
2862
2899
  (terminal) =>
2863
2900
  `${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
2864
2901
  )
2865
- .join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
2902
+ .join(
2903
+ "|",
2904
+ )}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}`
2866
2905
  if (seenTerminalSignatures.has(terminalSignature)) continue
2867
2906
  seenTerminalSignatures.add(terminalSignature)
2868
2907
  const windingSteps = routeViaMinimalWindingAlternativesSteps(
@@ -2881,17 +2920,9 @@ export function* routeBusAlternativesSteps(
2881
2920
  allowSameNetMerges,
2882
2921
  maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
2883
2922
  adaptiveRouteOrder: adaptiveWindingRouteOrder,
2884
- alignGridToPads:
2885
- alignWindingGridToPads ||
2886
- Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
2923
+ alignGridToPads,
2887
2924
  reservedVias,
2888
- gridStepDivisor:
2889
- coordinatedViaPoints &&
2890
- Math.min(bus.pitchX, bus.pitchY) -
2891
- 2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
2892
- traceWidth + clearance
2893
- ? 2
2894
- : 1,
2925
+ gridStepDivisor,
2895
2926
  preferTargetDirectedLaneBias:
2896
2927
  terminalPattern.preferTargetDirectedLaneBias,
2897
2928
  },
@@ -2962,11 +2993,24 @@ export function* routeBusAlternativesSteps(
2962
2993
  const preparedConnection = bus.connections[0]!
2963
2994
  const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
2964
2995
  const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
2965
- const finalTrack = getBoundaryTargetTrack({
2966
- bus,
2967
- connection: preparedConnection,
2968
- boundaryDirection,
2969
- })
2996
+ const finalTrack =
2997
+ preferCornerBoundaryVia && getCornerSide(bus)
2998
+ ? getCornerTargetTrack({
2999
+ bus,
3000
+ connection: preparedConnection,
3001
+ cornerExitLaneOffset: cornerLaneOffsets.exit,
3002
+ traceWidth,
3003
+ viaDiameter,
3004
+ clearance,
3005
+ layerNames,
3006
+ targetLayer,
3007
+ cornerBandTargetTrackOffset,
3008
+ })
3009
+ : getBoundaryTargetTrack({
3010
+ bus,
3011
+ connection: preparedConnection,
3012
+ boundaryDirection,
3013
+ })
2970
3014
  const finalExitPoint = makePoint(
2971
3015
  boundaryExitAxis,
2972
3016
  finalTrack,
@@ -2992,13 +3036,27 @@ export function* routeBusAlternativesSteps(
2992
3036
  viaDiameter / 2 + clearance,
2993
3037
  Math.min(bus.pitchX, bus.pitchY) / 2,
2994
3038
  )
2995
- for (const multiple of [1, 2, 3, 4, 5]) {
3039
+ const cornerSide = preferCornerBoundaryVia ? getCornerSide(bus) : undefined
3040
+ const boundaryViaCandidates = [1, 2, 3, 4, 5].flatMap((multiple) => {
2996
3041
  const inset = multiple * insetStep
2997
- const boundaryViaPoint = makePoint(
3042
+ const straight = makePoint(
2998
3043
  boundaryExitAxis - directionSign(boundaryDirection) * inset,
2999
3044
  finalTrack,
3000
3045
  boundaryDirection,
3001
3046
  )
3047
+ if (!cornerSide) return [straight]
3048
+ // Approach corner exits diagonally to leave the adjacent pair's tuning
3049
+ // lane free of the through-via barrel. Retain the straight fallback.
3050
+ return [
3051
+ makePoint(
3052
+ boundaryExitAxis - directionSign(boundaryDirection) * inset,
3053
+ finalTrack + (cornerSide === "maximum" ? inset : -inset),
3054
+ boundaryDirection,
3055
+ ),
3056
+ straight,
3057
+ ]
3058
+ })
3059
+ for (const boundaryViaPoint of boundaryViaCandidates) {
3002
3060
  const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps(
3003
3061
  {
3004
3062
  srj: sourceLayerSrj,
@@ -3065,6 +3123,9 @@ export function* routeBusAlternativesSteps(
3065
3123
  const plans: FanoutRoutePlan[] = [
3066
3124
  {
3067
3125
  ...sourceLayerPlan,
3126
+ ...(preferCornerBoundaryVia
3127
+ ? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
3128
+ : {}),
3068
3129
  targetLayer,
3069
3130
  exitPoint: finalExitPoint,
3070
3131
  via,
@@ -1075,6 +1075,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
1075
1075
  endByNode.set(end.nodeIndex, values)
1076
1076
  }
1077
1077
  const stateCount = nodeCount * 9
1078
+ // A* visits a grid edge with several incoming directions. Copper does not
1079
+ // change while routing this terminal, so check each edge only once. Keep
1080
+ // this cache local: later terminals and route-order attempts add blockers.
1081
+ const edgeClearance = new Uint8Array(nodeCount * 8)
1078
1082
  const distances = new Float64Array(stateCount).fill(
1079
1083
  Number.POSITIVE_INFINITY,
1080
1084
  )
@@ -1205,21 +1209,6 @@ export function* routeViaMinimalWindingAlternativesSteps(
1205
1209
  }
1206
1210
  const nextNode = row * columnCount + column
1207
1211
  const nextPoint = nodes[nextNode]!.point
1208
- const segment: RoutedSegment = {
1209
- start: node.point,
1210
- end: nextPoint,
1211
- width: traceWidth,
1212
- layer: targetLayer,
1213
- }
1214
- if (
1215
- !segmentIsClear({
1216
- segment,
1217
- terminal,
1218
- acceptedAttemptSegments,
1219
- })
1220
- ) {
1221
- continue
1222
- }
1223
1212
  const addsTurn =
1224
1213
  current.direction !== 8 && current.direction !== directionIndex
1225
1214
  const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection)
@@ -1242,6 +1231,24 @@ export function* routeViaMinimalWindingAlternativesSteps(
1242
1231
  lanePenalty
1243
1232
  const nextState = nextNode * 9 + directionIndex
1244
1233
  if (nextDistance >= distances[nextState]! - EPSILON) continue
1234
+ const edgeIndex = current.node * 8 + directionIndex
1235
+ if (edgeClearance[edgeIndex] === 0) {
1236
+ const clear = segmentIsClear({
1237
+ segment: {
1238
+ start: node.point,
1239
+ end: nextPoint,
1240
+ width: traceWidth,
1241
+ layer: targetLayer,
1242
+ },
1243
+ terminal,
1244
+ acceptedAttemptSegments,
1245
+ })
1246
+ edgeClearance[edgeIndex] = clear ? 1 : 2
1247
+ edgeClearance[nextNode * 8 + ((directionIndex + 4) % 8)] = clear
1248
+ ? 1
1249
+ : 2
1250
+ }
1251
+ if (edgeClearance[edgeIndex] === 2) continue
1245
1252
  distances[nextState] = nextDistance
1246
1253
  previous[nextState] = state
1247
1254
  const remaining = heuristic(nextPoint)
@@ -0,0 +1,75 @@
1
+ import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
+ import { fanoutPlansAreClear } from "./route-bus"
3
+ import { routeViaMinimalWinding } from "./route-via-minimal-winding"
4
+ import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
5
+
6
+ /** Revisit excessive winding detours after provisional reservations are gone. */
7
+ export function shortenBusPlans(params: {
8
+ plans: readonly FanoutRoutePlan[]
9
+ bus: PreparedBus
10
+ srj: SimpleRouteJson
11
+ sharedBoundary: Bounds
12
+ layerNames: string[]
13
+ traceWidth: number
14
+ viaDiameter: number
15
+ viaHoleDiameter: number
16
+ clearance: number
17
+ allowSameNetMerges: boolean
18
+ }): FanoutRoutePlan[] {
19
+ const { bus } = params
20
+ let plans = [...params.plans]
21
+ if (bus.maxLengthSkew === undefined) return plans
22
+ const busPlans = plans
23
+ .filter((plan) => plan.busId === bus.busId)
24
+ .toSorted((first, second) => second.length - first.length)
25
+ for (const plan of busPlans) {
26
+ const minimumLength = Math.min(
27
+ ...plans.filter((p) => p.busId === bus.busId).map((p) => p.length),
28
+ )
29
+ if (plan.length - minimumLength <= bus.maxLengthSkew) continue
30
+ // Retain the original source dogbone, through-via and boundary endpoint.
31
+ if (
32
+ !plan.via ||
33
+ plan.additionalVias?.length ||
34
+ plan.planeEndpointVia ||
35
+ plan.segments.filter((segment) => segment.layer === plan.sourceLayer)
36
+ .length !== 1
37
+ )
38
+ continue
39
+ const connection = bus.connections.find(
40
+ (c) => c.connectionIndex === plan.connectionIndex,
41
+ )!
42
+ for (const alignGridToPads of [true, false]) {
43
+ const candidate = routeViaMinimalWinding({
44
+ ...params,
45
+ bus: {
46
+ ...bus,
47
+ connections: [connection],
48
+ routableEscapeLayers: [plan.targetLayer],
49
+ },
50
+ terminals: [
51
+ { connection, viaPoint: plan.via.center, exitPoint: plan.exitPoint },
52
+ ],
53
+ targetLayer: plan.targetLayer,
54
+ acceptedPlans: plans.filter((p) => p !== plan),
55
+ allowBlindAndBuriedVias: false,
56
+ gridStepDivisor: 2,
57
+ alignGridToPads,
58
+ maximumRouteOrderAttempts: 1,
59
+ })?.[0]
60
+ if (!candidate || candidate.length >= plan.length - 1e-6) continue
61
+ const nextPlans = plans.map((p) => (p === plan ? candidate : p))
62
+ if (
63
+ !fanoutPlansAreClear({
64
+ ...params,
65
+ plans: nextPlans,
66
+ allowBlindAndBuriedVias: false,
67
+ })
68
+ )
69
+ continue
70
+ plans = nextPlans
71
+ break
72
+ }
73
+ }
74
+ return plans
75
+ }
package/lib/types.ts CHANGED
@@ -388,6 +388,8 @@ export interface FanoutRoutePlan {
388
388
  exitPoint: Point2D
389
389
  trace: FanoutSimplifiedPcbTrace
390
390
  segments: RoutedSegment[]
391
+ /** Validated winding segments that leave this route's own source pad. */
392
+ sourceEscapeSegmentCount?: number
391
393
  via?: RoutedVia
392
394
  /** Additional layer transitions used by an explicit winding channel. */
393
395
  additionalVias?: RoutedVia[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.53",
3
+ "version": "0.0.54",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",