@tscircuit/fanout-solver 0.0.49 → 0.0.51

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.
package/lib/route-bus.ts CHANGED
@@ -19,13 +19,14 @@ import {
19
19
  } from "./geometry"
20
20
  import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
21
21
  import { getViaSpanLayers } from "./layer-names"
22
+ import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
22
23
  import {
23
24
  connectionsShareElectricalNet,
24
25
  obstacleSharesElectricalNet,
25
26
  } from "./net-identity"
26
27
  import {
27
- routeViaMinimalWindingAlternativesSteps,
28
28
  type RouteViaMinimalWindingProgress,
29
+ routeViaMinimalWindingAlternativesSteps,
29
30
  type ViaMinimalWindingReservedVia,
30
31
  } from "./route-via-minimal-winding"
31
32
  import type {
@@ -61,6 +62,16 @@ export interface RouteBusParams {
61
62
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
62
63
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
63
64
  viaMinimalOnly?: boolean
65
+ /** Permit a singleton to move its provisional via near the boundary. */
66
+ allowBoundarySideViaFallback?: boolean
67
+ /** Retry blocked winding terminals ahead of already-routed terminals. */
68
+ adaptiveWindingRouteOrder?: boolean
69
+ /** Preserve pad-lattice channels in the automatic dense routing path. */
70
+ alignWindingGridToPads?: boolean
71
+ /** Bounds the final fixed-via winding fallback after ordered attempts. */
72
+ fixedViaFallbackRouteOrderAttempts?: number
73
+ /** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
74
+ planeCandidateSkipCount?: number
64
75
  /** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
65
76
  cornerBandTargetTrackOffset?: number
66
77
  }
@@ -1873,6 +1884,24 @@ function planIsClearOfPlans(params: {
1873
1884
  return true
1874
1885
  }
1875
1886
 
1887
+ export function fanoutPlansAreMutuallyClear(params: {
1888
+ plans: readonly FanoutRoutePlan[]
1889
+ srj: SimpleRouteJson
1890
+ clearance: number
1891
+ allowSameNetMerges?: boolean
1892
+ }): boolean {
1893
+ const { plans, srj, clearance, allowSameNetMerges = false } = params
1894
+ return plans.every((plan, index) =>
1895
+ planIsClearOfPlans({
1896
+ plan,
1897
+ otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
1898
+ srj,
1899
+ allowSameNetMerges,
1900
+ clearance,
1901
+ }),
1902
+ )
1903
+ }
1904
+
1876
1905
  function planIsClear(params: {
1877
1906
  plan: FanoutRoutePlan
1878
1907
  otherPlans: FanoutRoutePlan[]
@@ -1991,11 +2020,27 @@ function routePlaneTerminatedBus(
1991
2020
  allowBlindAndBuriedVias = true,
1992
2021
  allowSameNetMerges = false,
1993
2022
  fixedViaPointsByConnectionIndex,
2023
+ planeCandidateSkipCount = 0,
1994
2024
  } = params
1995
2025
  const sourceObstacle = bus.connections[0]?.sourceObstacle
1996
2026
  if (!sourceObstacle || bus.termination.type !== "plane") return null
1997
2027
  const sourceLayer = bus.connections[0]!.sourceLayer
1998
2028
  if (targetLayer === sourceLayer) return null
2029
+ let remainingPlaneCandidatesToSkip = planeCandidateSkipCount
2030
+ const selectClearPlanePlan = (
2031
+ plans: FanoutRoutePlan[],
2032
+ isClear: (plan: FanoutRoutePlan, index: number) => boolean,
2033
+ ): FanoutRoutePlan | undefined => {
2034
+ for (const [index, plan] of plans.entries()) {
2035
+ if (!isClear(plan, index)) continue
2036
+ if (remainingPlaneCandidatesToSkip > 0) {
2037
+ remainingPlaneCandidatesToSkip--
2038
+ continue
2039
+ }
2040
+ return plan
2041
+ }
2042
+ return undefined
2043
+ }
1999
2044
 
2000
2045
  if (fixedViaPointsByConnectionIndex) {
2001
2046
  const fixedPlans: FanoutRoutePlan[] = []
@@ -2052,19 +2097,21 @@ function routePlaneTerminatedBus(
2052
2097
  ),
2053
2098
  basePlan,
2054
2099
  ]
2055
- const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2056
- planIsClear({
2057
- plan: candidatePlan,
2058
- otherPlans: [...acceptedPlans, ...fixedPlans],
2059
- staticClearanceCache,
2060
- blockingBusCounts,
2061
- cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
2062
- srj,
2063
- sharedBoundary: bus.sharedBoundary,
2064
- clearance,
2065
- allowBlindAndBuriedVias,
2066
- allowSameNetMerges,
2067
- }),
2100
+ const clearPlan = selectClearPlanePlan(
2101
+ plansToTry,
2102
+ (candidatePlan, candidateIndex) =>
2103
+ planIsClear({
2104
+ plan: candidatePlan,
2105
+ otherPlans: [...acceptedPlans, ...fixedPlans],
2106
+ staticClearanceCache,
2107
+ blockingBusCounts,
2108
+ cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
2109
+ srj,
2110
+ sharedBoundary: bus.sharedBoundary,
2111
+ clearance,
2112
+ allowBlindAndBuriedVias,
2113
+ allowSameNetMerges,
2114
+ }),
2068
2115
  )
2069
2116
  if (!clearPlan) return null
2070
2117
  fixedPlans.push(clearPlan)
@@ -2128,19 +2175,21 @@ function routePlaneTerminatedBus(
2128
2175
  ),
2129
2176
  viaInPadPlan,
2130
2177
  ]
2131
- const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2132
- planIsClear({
2133
- plan: candidatePlan,
2134
- otherPlans: acceptedPlans,
2135
- staticClearanceCache,
2136
- blockingBusCounts,
2137
- cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
2138
- srj,
2139
- sharedBoundary: bus.sharedBoundary,
2140
- clearance,
2141
- allowBlindAndBuriedVias,
2142
- allowSameNetMerges,
2143
- }),
2178
+ const clearPlan = selectClearPlanePlan(
2179
+ plansToTry,
2180
+ (candidatePlan, candidateIndex) =>
2181
+ planIsClear({
2182
+ plan: candidatePlan,
2183
+ otherPlans: acceptedPlans,
2184
+ staticClearanceCache,
2185
+ blockingBusCounts,
2186
+ cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
2187
+ srj,
2188
+ sharedBoundary: bus.sharedBoundary,
2189
+ clearance,
2190
+ allowBlindAndBuriedVias,
2191
+ allowSameNetMerges,
2192
+ }),
2144
2193
  )
2145
2194
  if (clearPlan) return [clearPlan]
2146
2195
  }
@@ -2203,7 +2252,7 @@ function routePlaneTerminatedBus(
2203
2252
  )
2204
2253
  const perpendicularPitch = getPerpendicularPitch(directionalBus)
2205
2254
  const sourceEscapePaths: Point2D[][] = []
2206
- const maximumCandidatePaths = 32
2255
+ const maximumCandidatePaths = 128
2207
2256
  for (
2208
2257
  let totalSteps = 0;
2209
2258
  totalSteps <= maximumEscapeSteps &&
@@ -2345,19 +2394,21 @@ function routePlaneTerminatedBus(
2345
2394
  ),
2346
2395
  basePlan,
2347
2396
  ]
2348
- plan = plansToTry.find((candidatePlan, candidateIndex) =>
2349
- planIsClear({
2350
- plan: candidatePlan,
2351
- otherPlans: [...acceptedPlans, ...candidatePlans],
2352
- staticClearanceCache,
2353
- blockingBusCounts,
2354
- cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
2355
- srj,
2356
- sharedBoundary: bus.sharedBoundary,
2357
- clearance,
2358
- allowBlindAndBuriedVias,
2359
- allowSameNetMerges,
2360
- }),
2397
+ plan = selectClearPlanePlan(
2398
+ plansToTry,
2399
+ (candidatePlan, candidateIndex) =>
2400
+ planIsClear({
2401
+ plan: candidatePlan,
2402
+ otherPlans: [...acceptedPlans, ...candidatePlans],
2403
+ staticClearanceCache,
2404
+ blockingBusCounts,
2405
+ cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
2406
+ srj,
2407
+ sharedBoundary: bus.sharedBoundary,
2408
+ clearance,
2409
+ allowBlindAndBuriedVias,
2410
+ allowSameNetMerges,
2411
+ }),
2361
2412
  )
2362
2413
  if (plan) break
2363
2414
  }
@@ -2400,6 +2451,10 @@ export function* routeBusAlternativesSteps(
2400
2451
  fixedViaPointsByConnectionIndex,
2401
2452
  reservedVias = [],
2402
2453
  viaMinimalOnly = false,
2454
+ allowBoundarySideViaFallback = false,
2455
+ adaptiveWindingRouteOrder = false,
2456
+ alignWindingGridToPads = false,
2457
+ fixedViaFallbackRouteOrderAttempts = 24,
2403
2458
  cornerBandTargetTrackOffset,
2404
2459
  } = params
2405
2460
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
@@ -2417,8 +2472,20 @@ export function* routeBusAlternativesSteps(
2417
2472
  return []
2418
2473
  }
2419
2474
  if (bus.termination.type === "plane") {
2420
- const plan = routePlaneTerminatedBus(params)
2421
- return plan ? [plan] : []
2475
+ const alternatives: FanoutRoutePlan[][] = []
2476
+ for (
2477
+ let planeCandidateSkipCount = 0;
2478
+ planeCandidateSkipCount < maxAlternatives;
2479
+ planeCandidateSkipCount++
2480
+ ) {
2481
+ const plan = routePlaneTerminatedBus({
2482
+ ...params,
2483
+ planeCandidateSkipCount,
2484
+ })
2485
+ if (!plan) break
2486
+ alternatives.push(plan)
2487
+ }
2488
+ return alternatives
2422
2489
  }
2423
2490
  const exitAxis = getExitAxis(bus)
2424
2491
  const sourceObstacle = bus.connections[0]?.sourceObstacle
@@ -2666,8 +2733,36 @@ export function* routeBusAlternativesSteps(
2666
2733
  useViaInPad: true,
2667
2734
  getViaHandedness: () => 0 as const,
2668
2735
  }
2736
+ const coordinatedViaPoints =
2737
+ fixedViaPointsByConnectionIndex ??
2738
+ (!allowBlindAndBuriedVias &&
2739
+ bus.connections.length >= 8 &&
2740
+ reservedVias.length === 0
2741
+ ? matchComponentDogboneViaSites([bus], {
2742
+ viaDiameter,
2743
+ viaHoleDiameter,
2744
+ traceWidth,
2745
+ clearance,
2746
+ additionalObstacles: srj.obstacles,
2747
+ blockingSegments: acceptedPlans.flatMap((plan) =>
2748
+ getPlanSegments(plan).map((segment) => ({
2749
+ connectionIndex: plan.connectionIndex,
2750
+ segment,
2751
+ })),
2752
+ ),
2753
+ blockingVias: acceptedPlans.flatMap((plan) =>
2754
+ getPlanVias(plan).map((via) => ({
2755
+ connectionIndex: plan.connectionIndex,
2756
+ ...via.center,
2757
+ center: via.center,
2758
+ diameter: via.diameter,
2759
+ spanLayers: via.spanLayers,
2760
+ })),
2761
+ ),
2762
+ })
2763
+ : null)
2669
2764
  const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
2670
- fixedViaPointsByConnectionIndex
2765
+ coordinatedViaPoints
2671
2766
  ? [
2672
2767
  ...Array.from(
2673
2768
  { length: windingTargetOrderCount },
@@ -2676,9 +2771,7 @@ export function* routeBusAlternativesSteps(
2676
2771
  useViaInPad: false,
2677
2772
  getViaHandedness: () => 0 as const,
2678
2773
  getViaPoint: (connection: PreparedConnection) =>
2679
- fixedViaPointsByConnectionIndex.get(
2680
- connection.connectionIndex,
2681
- )!,
2774
+ coordinatedViaPoints.get(connection.connectionIndex)!,
2682
2775
  maximumRouteOrderAttempts: 1,
2683
2776
  windingOrderIndex,
2684
2777
  preferTargetDirectedLaneBias: true,
@@ -2689,12 +2782,10 @@ export function* routeBusAlternativesSteps(
2689
2782
  useViaInPad: false,
2690
2783
  getViaHandedness: () => 0,
2691
2784
  getViaPoint: (connection) =>
2692
- fixedViaPointsByConnectionIndex.get(
2693
- connection.connectionIndex,
2694
- )!,
2785
+ coordinatedViaPoints.get(connection.connectionIndex)!,
2695
2786
  // Preserve the existing bounded search after every inexpensive
2696
2787
  // layer-interleave candidate has had one deterministic attempt.
2697
- maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2788
+ maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
2698
2789
  windingOrderIndex: 0,
2699
2790
  preferTargetDirectedLaneBias: true,
2700
2791
  },
@@ -2789,9 +2880,13 @@ export function* routeBusAlternativesSteps(
2789
2880
  allowBlindAndBuriedVias,
2790
2881
  allowSameNetMerges,
2791
2882
  maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
2883
+ adaptiveRouteOrder: adaptiveWindingRouteOrder,
2884
+ alignGridToPads:
2885
+ alignWindingGridToPads ||
2886
+ Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
2792
2887
  reservedVias,
2793
2888
  gridStepDivisor:
2794
- fixedViaPointsByConnectionIndex &&
2889
+ coordinatedViaPoints &&
2795
2890
  Math.min(bus.pitchX, bus.pitchY) -
2796
2891
  2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
2797
2892
  traceWidth + clearance
@@ -2850,6 +2945,204 @@ export function* routeBusAlternativesSteps(
2850
2945
  }
2851
2946
  }
2852
2947
 
2948
+ // A through-via does not have to sit next to the source pad. A singleton
2949
+ // embedded in another bus's source field can have every local dogbone site
2950
+ // occupied while still having a clear top-layer escape. In that case, route
2951
+ // to one via near the boundary and make the short final hop on the requested
2952
+ // escape layer. This retains the one-via invariant and avoids weakening any
2953
+ // clearance rule.
2954
+ if (
2955
+ alternatives.length === 0 &&
2956
+ allowBoundarySideViaFallback &&
2957
+ fixedViaPointsByConnectionIndex &&
2958
+ bus.connections.length === 1 &&
2959
+ bus.exitEdge &&
2960
+ bus.connections[0]!.sourceLayer !== targetLayer
2961
+ ) {
2962
+ const preparedConnection = bus.connections[0]!
2963
+ const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
2964
+ const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
2965
+ const finalTrack = getBoundaryTargetTrack({
2966
+ bus,
2967
+ connection: preparedConnection,
2968
+ boundaryDirection,
2969
+ })
2970
+ const finalExitPoint = makePoint(
2971
+ boundaryExitAxis,
2972
+ finalTrack,
2973
+ boundaryDirection,
2974
+ )
2975
+ const sourceLayerSrj: SimpleRouteJson = {
2976
+ ...srj,
2977
+ obstacles: srj.obstacles.map((obstacle) =>
2978
+ obstacle === preparedConnection.sourceObstacle ||
2979
+ (distance(obstacle.center, preparedConnection.sourcePoint) <= 1e-7 &&
2980
+ obstacle.layers.includes(preparedConnection.sourceLayer))
2981
+ ? {
2982
+ ...obstacle,
2983
+ connectedTo: [
2984
+ ...obstacle.connectedTo,
2985
+ preparedConnection.connection.name,
2986
+ ],
2987
+ }
2988
+ : obstacle,
2989
+ ),
2990
+ }
2991
+ const insetStep = Math.max(
2992
+ viaDiameter / 2 + clearance,
2993
+ Math.min(bus.pitchX, bus.pitchY) / 2,
2994
+ )
2995
+ for (const multiple of [1, 2, 3, 4, 5]) {
2996
+ const inset = multiple * insetStep
2997
+ const boundaryViaPoint = makePoint(
2998
+ boundaryExitAxis - directionSign(boundaryDirection) * inset,
2999
+ finalTrack,
3000
+ boundaryDirection,
3001
+ )
3002
+ const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps(
3003
+ {
3004
+ srj: sourceLayerSrj,
3005
+ bus,
3006
+ targetLayer: preparedConnection.sourceLayer,
3007
+ terminals: [
3008
+ {
3009
+ connection: preparedConnection,
3010
+ viaPoint: preparedConnection.sourcePoint,
3011
+ exitPoint: boundaryViaPoint,
3012
+ },
3013
+ ],
3014
+ acceptedPlans,
3015
+ layerNames,
3016
+ traceWidth,
3017
+ viaDiameter,
3018
+ viaHoleDiameter,
3019
+ clearance,
3020
+ allowBlindAndBuriedVias,
3021
+ allowSameNetMerges,
3022
+ maximumRouteOrderAttempts: 3,
3023
+ reservedVias,
3024
+ gridStepDivisor: 2,
3025
+ allowSourceLayerRouting: true,
3026
+ alignGridToPads: true,
3027
+ },
3028
+ 1,
3029
+ includeVisualization,
3030
+ )
3031
+ let sourceLayerResult = sourceLayerSteps.next()
3032
+ while (!sourceLayerResult.done) {
3033
+ yield {
3034
+ phase: "via-minimal-winding",
3035
+ busId: bus.busId,
3036
+ targetLayer: preparedConnection.sourceLayer,
3037
+ winding: sourceLayerResult.value,
3038
+ }
3039
+ sourceLayerResult = sourceLayerSteps.next()
3040
+ }
3041
+ const sourceLayerPlan = sourceLayerResult.value[0]?.[0]
3042
+ if (!sourceLayerPlan) continue
3043
+ const targetSegment: RoutedSegment = {
3044
+ start: boundaryViaPoint,
3045
+ end: finalExitPoint,
3046
+ width: traceWidth,
3047
+ layer: targetLayer,
3048
+ }
3049
+ const via = {
3050
+ center: boundaryViaPoint,
3051
+ diameter: viaDiameter,
3052
+ holeDiameter: viaHoleDiameter,
3053
+ fromLayer: preparedConnection.sourceLayer,
3054
+ toLayer: targetLayer,
3055
+ spanLayers: getViaSpanLayers({
3056
+ fromLayer: preparedConnection.sourceLayer,
3057
+ toLayer: targetLayer,
3058
+ layerNames,
3059
+ allowBlindAndBuriedVias,
3060
+ }),
3061
+ }
3062
+ const sourceRoute = sourceLayerPlan.trace.route.filter(
3063
+ (item) => item.route_type === "wire",
3064
+ )
3065
+ const plans: FanoutRoutePlan[] = [
3066
+ {
3067
+ ...sourceLayerPlan,
3068
+ targetLayer,
3069
+ exitPoint: finalExitPoint,
3070
+ via,
3071
+ segments: [...sourceLayerPlan.segments, targetSegment],
3072
+ length:
3073
+ sourceLayerPlan.length + distance(boundaryViaPoint, finalExitPoint),
3074
+ trace: {
3075
+ ...sourceLayerPlan.trace,
3076
+ route: [
3077
+ ...sourceRoute,
3078
+ {
3079
+ route_type: "via",
3080
+ ...boundaryViaPoint,
3081
+ from_layer: preparedConnection.sourceLayer,
3082
+ to_layer: targetLayer,
3083
+ via_diameter: viaDiameter,
3084
+ via_hole_diameter: viaHoleDiameter,
3085
+ },
3086
+ {
3087
+ route_type: "wire",
3088
+ ...boundaryViaPoint,
3089
+ width: traceWidth,
3090
+ layer: targetLayer,
3091
+ },
3092
+ {
3093
+ route_type: "wire",
3094
+ ...finalExitPoint,
3095
+ width: traceWidth,
3096
+ layer: targetLayer,
3097
+ },
3098
+ ],
3099
+ },
3100
+ },
3101
+ ]
3102
+ const boundaryViaPlansAreClear = fanoutPlansAreClear({
3103
+ plans: [...acceptedPlans, ...plans],
3104
+ srj,
3105
+ sharedBoundary: bus.sharedBoundary,
3106
+ clearance,
3107
+ allowBlindAndBuriedVias,
3108
+ allowSameNetMerges,
3109
+ })
3110
+ const reservedViasAreClear = reservedVias.every((reserved) => {
3111
+ if (
3112
+ reserved.connectionName === preparedConnection.connection.name ||
3113
+ (allowSameNetMerges &&
3114
+ connectionsShareElectricalNet(
3115
+ srj,
3116
+ reserved.connectionName,
3117
+ preparedConnection.connection.name,
3118
+ ))
3119
+ )
3120
+ return true
3121
+ if (
3122
+ via.spanLayers.some((layer) =>
3123
+ reserved.via.spanLayers.includes(layer),
3124
+ ) &&
3125
+ distance(via.center, reserved.via.center) <
3126
+ (via.diameter + reserved.via.diameter) / 2 + clearance - 1e-9
3127
+ )
3128
+ return false
3129
+ return (
3130
+ !reserved.via.spanLayers.includes(targetLayer) ||
3131
+ distancePointToSegment(
3132
+ reserved.via.center,
3133
+ targetSegment.start,
3134
+ targetSegment.end,
3135
+ ) >=
3136
+ reserved.via.diameter / 2 + traceWidth / 2 + clearance - 1e-9
3137
+ )
3138
+ })
3139
+ if (boundaryViaPlansAreClear && reservedViasAreClear) {
3140
+ addAlternative(plans)
3141
+ return alternatives
3142
+ }
3143
+ }
3144
+ }
3145
+
2853
3146
  if (viaMinimalOnly) return alternatives
2854
3147
 
2855
3148
  const searchConnectionOrder = (