@tscircuit/fanout-solver 0.0.48 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,17 @@ export interface DogboneViaSiteGeometryRules {
40
40
  connectionIndex: number
41
41
  segment: RoutedSegment
42
42
  }[]
43
+ /** Routed vias that every newly assigned through-via/dogbone must clear. */
44
+ blockingVias?: readonly {
45
+ connectionIndex: number
46
+ center: Point2D
47
+ diameter: number
48
+ spanLayers: readonly string[]
49
+ }[]
50
+ /** Board obstacles outside the source component that dogbones must clear. */
51
+ additionalObstacles?: readonly Obstacle[]
52
+ /** Alternate horizontal plane dogbones across BGA rows. */
53
+ preferPlaneCheckerboardSites?: boolean
43
54
  /** True only when the two connections are allowed to merge copper. */
44
55
  canShareCopper?: (
45
56
  firstConnectionIndex: number,
@@ -148,6 +159,7 @@ function getComponentMatchingInputs(
148
159
  ): ComponentMatchingInput[] {
149
160
  const byComponent = new Map<string, ComponentMatchingInput>()
150
161
  const componentByConnectionIndex = new Map<number, string>()
162
+ const obstacleSetByComponentId = new Map<string, Set<Obstacle>>()
151
163
 
152
164
  for (const bus of preparedBuses) {
153
165
  let component = byComponent.get(bus.componentId)
@@ -162,6 +174,7 @@ function getComponentMatchingInputs(
162
174
  pitchY: Number.POSITIVE_INFINITY,
163
175
  }
164
176
  byComponent.set(bus.componentId, component)
177
+ obstacleSetByComponentId.set(bus.componentId, new Set())
165
178
  }
166
179
 
167
180
  component.xCoordinates.push(...bus.xCoordinates)
@@ -172,8 +185,10 @@ function getComponentMatchingInputs(
172
185
  if (Number.isFinite(bus.pitchY) && bus.pitchY > EPSILON) {
173
186
  component.pitchY = Math.min(component.pitchY, bus.pitchY)
174
187
  }
188
+ const obstacleSet = obstacleSetByComponentId.get(bus.componentId)!
175
189
  for (const obstacle of bus.componentObstacles) {
176
- if (!component.obstacles.includes(obstacle)) {
190
+ if (!obstacleSet.has(obstacle)) {
191
+ obstacleSet.add(obstacle)
177
192
  component.obstacles.push(obstacle)
178
193
  }
179
194
  }
@@ -325,6 +340,9 @@ function getConnectionCandidates(params: {
325
340
  }): ViaSiteCandidate[] {
326
341
  const { connection, component, rules } = params
327
342
  const { preparedConnection, direction } = connection
343
+ const obstacles = rules.additionalObstacles
344
+ ? [...new Set([...component.obstacles, ...rules.additionalObstacles])]
345
+ : component.obstacles
328
346
  const source = {
329
347
  x: preparedConnection.sourcePoint.x,
330
348
  y: preparedConnection.sourcePoint.y,
@@ -368,7 +386,7 @@ function getConnectionCandidates(params: {
368
386
  if (
369
387
  !viaSiteClearsObstacles({
370
388
  point,
371
- obstacles: component.obstacles,
389
+ obstacles,
372
390
  viaDiameter: rules.viaDiameter,
373
391
  clearance: rules.clearance,
374
392
  })
@@ -385,7 +403,7 @@ function getConnectionCandidates(params: {
385
403
  !sourceSegmentClearsOtherObstacles({
386
404
  segment: sourceSegment,
387
405
  sourceObstacle: preparedConnection.sourceObstacle,
388
- obstacles: component.obstacles,
406
+ obstacles,
389
407
  clearance: rules.clearance,
390
408
  })
391
409
  ) {
@@ -423,6 +441,35 @@ function getConnectionCandidates(params: {
423
441
  },
424
442
  )
425
443
  if (!candidateClearsRoutedCopper) continue
444
+ const candidateClearsRoutedVias = (rules.blockingVias ?? []).every(
445
+ (blocker) => {
446
+ if (blocker.connectionIndex === preparedConnection.connectionIndex) {
447
+ return true
448
+ }
449
+ if (
450
+ distance(point, blocker.center) <
451
+ (rules.viaDiameter + blocker.diameter) / 2 + rules.clearance - EPSILON
452
+ ) {
453
+ return false
454
+ }
455
+ if (
456
+ blocker.spanLayers.includes(sourceSegment.layer) &&
457
+ distancePointToSegment(
458
+ blocker.center,
459
+ sourceSegment.start,
460
+ sourceSegment.end,
461
+ ) <
462
+ blocker.diameter / 2 +
463
+ sourceSegment.width / 2 +
464
+ rules.clearance -
465
+ EPSILON
466
+ ) {
467
+ return false
468
+ }
469
+ return true
470
+ },
471
+ )
472
+ if (!candidateClearsRoutedVias) continue
426
473
  candidates.push({
427
474
  connectionIndex: preparedConnection.connectionIndex,
428
475
  point,
@@ -431,10 +478,30 @@ function getConnectionCandidates(params: {
431
478
  })
432
479
  }
433
480
 
481
+ const perpendicularCoordinates =
482
+ direction === "left" || direction === "right"
483
+ ? component.yCoordinates
484
+ : component.xCoordinates
485
+ const sourcePerpendicularCoordinate =
486
+ direction === "left" || direction === "right" ? source.y : source.x
487
+ const perpendicularGridIndex = perpendicularCoordinates.findIndex(
488
+ (coordinate) =>
489
+ Math.abs(coordinate - sourcePerpendicularCoordinate) <= EPSILON,
490
+ )
491
+ const planeCheckerboardSide =
492
+ perpendicularGridIndex >= 0
493
+ ? perpendicularGridIndex % 2 === 0
494
+ ? 1
495
+ : -1
496
+ : undefined
434
497
  const preferredPerpendicularSide =
435
498
  connection.terminationType === "boundary"
436
499
  ? rules.preferredBoundaryPerpendicularSideByBusId?.get(connection.busId)
437
- : undefined
500
+ : rules.preferPlaneCheckerboardSites &&
501
+ connection.terminationType === "plane" &&
502
+ (direction === "left" || direction === "right")
503
+ ? planeCheckerboardSide
504
+ : undefined
438
505
  const preferOutward =
439
506
  connection.terminationType === "boundary"
440
507
  ? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
@@ -525,6 +592,25 @@ function matchComponent(params: {
525
592
  }),
526
593
  )
527
594
  if (entries.some((entry) => entry.candidates.length === 0)) return null
595
+ const compatibilityCache = new Map<
596
+ ViaSiteCandidate,
597
+ Map<ViaSiteCandidate, boolean>
598
+ >()
599
+ const candidatesAreCompatible = (
600
+ first: ViaSiteCandidate,
601
+ second: ViaSiteCandidate,
602
+ ): boolean => {
603
+ const cached = compatibilityCache.get(first)?.get(second)
604
+ if (cached !== undefined) return cached
605
+ const compatible = candidatesAreMutuallyClear({ first, second, rules })
606
+ const firstCache = compatibilityCache.get(first) ?? new Map()
607
+ firstCache.set(second, compatible)
608
+ compatibilityCache.set(first, firstCache)
609
+ const secondCache = compatibilityCache.get(second) ?? new Map()
610
+ secondCache.set(first, compatible)
611
+ compatibilityCache.set(second, secondCache)
612
+ return compatible
613
+ }
528
614
  // Every solution must include each sole candidate. Seed and validate those
529
615
  // forced choices once so recursive matching only explores genuine choices.
530
616
  const forcedCandidates = entries.flatMap((entry) =>
@@ -542,11 +628,7 @@ function matchComponent(params: {
542
628
  previousIndex++
543
629
  ) {
544
630
  if (
545
- !candidatesAreMutuallyClear({
546
- first: candidate,
547
- second: forcedCandidates[previousIndex]!,
548
- rules,
549
- })
631
+ !candidatesAreCompatible(candidate, forcedCandidates[previousIndex]!)
550
632
  ) {
551
633
  return null
552
634
  }
@@ -584,11 +666,7 @@ function matchComponent(params: {
584
666
  ): ViaSiteCandidate[] =>
585
667
  entry.candidates.filter((candidate) =>
586
668
  [...assignedCandidates.values()].every((assignedCandidate) =>
587
- candidatesAreMutuallyClear({
588
- first: candidate,
589
- second: assignedCandidate,
590
- rules,
591
- }),
669
+ candidatesAreCompatible(candidate, assignedCandidate),
592
670
  ),
593
671
  )
594
672
 
package/lib/route-bus.ts CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  obstacleSharesElectricalNet,
25
25
  } from "./net-identity"
26
26
  import {
27
- routeViaMinimalWindingAlternatives,
27
+ type RouteViaMinimalWindingProgress,
28
+ routeViaMinimalWindingAlternativesSteps,
28
29
  type ViaMinimalWindingReservedVia,
29
30
  } from "./route-via-minimal-winding"
30
31
  import type {
@@ -60,10 +61,21 @@ export interface RouteBusParams {
60
61
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
61
62
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
62
63
  viaMinimalOnly?: boolean
64
+ /** Bounds the final fixed-via winding fallback after ordered attempts. */
65
+ fixedViaFallbackRouteOrderAttempts?: number
66
+ /** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
67
+ planeCandidateSkipCount?: number
63
68
  /** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
64
69
  cornerBandTargetTrackOffset?: number
65
70
  }
66
71
 
72
+ export interface RouteBusAlternativesProgress {
73
+ phase: "via-minimal-winding"
74
+ busId: string
75
+ targetLayer: string
76
+ winding: RouteViaMinimalWindingProgress
77
+ }
78
+
67
79
  interface TrackCandidate {
68
80
  value: number
69
81
  kind: "corridor" | "gap" | "margin" | "preferred"
@@ -1865,6 +1877,24 @@ function planIsClearOfPlans(params: {
1865
1877
  return true
1866
1878
  }
1867
1879
 
1880
+ export function fanoutPlansAreMutuallyClear(params: {
1881
+ plans: readonly FanoutRoutePlan[]
1882
+ srj: SimpleRouteJson
1883
+ clearance: number
1884
+ allowSameNetMerges?: boolean
1885
+ }): boolean {
1886
+ const { plans, srj, clearance, allowSameNetMerges = false } = params
1887
+ return plans.every((plan, index) =>
1888
+ planIsClearOfPlans({
1889
+ plan,
1890
+ otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
1891
+ srj,
1892
+ allowSameNetMerges,
1893
+ clearance,
1894
+ }),
1895
+ )
1896
+ }
1897
+
1868
1898
  function planIsClear(params: {
1869
1899
  plan: FanoutRoutePlan
1870
1900
  otherPlans: FanoutRoutePlan[]
@@ -1983,11 +2013,27 @@ function routePlaneTerminatedBus(
1983
2013
  allowBlindAndBuriedVias = true,
1984
2014
  allowSameNetMerges = false,
1985
2015
  fixedViaPointsByConnectionIndex,
2016
+ planeCandidateSkipCount = 0,
1986
2017
  } = params
1987
2018
  const sourceObstacle = bus.connections[0]?.sourceObstacle
1988
2019
  if (!sourceObstacle || bus.termination.type !== "plane") return null
1989
2020
  const sourceLayer = bus.connections[0]!.sourceLayer
1990
2021
  if (targetLayer === sourceLayer) return null
2022
+ let remainingPlaneCandidatesToSkip = planeCandidateSkipCount
2023
+ const selectClearPlanePlan = (
2024
+ plans: FanoutRoutePlan[],
2025
+ isClear: (plan: FanoutRoutePlan, index: number) => boolean,
2026
+ ): FanoutRoutePlan | undefined => {
2027
+ for (const [index, plan] of plans.entries()) {
2028
+ if (!isClear(plan, index)) continue
2029
+ if (remainingPlaneCandidatesToSkip > 0) {
2030
+ remainingPlaneCandidatesToSkip--
2031
+ continue
2032
+ }
2033
+ return plan
2034
+ }
2035
+ return undefined
2036
+ }
1991
2037
 
1992
2038
  if (fixedViaPointsByConnectionIndex) {
1993
2039
  const fixedPlans: FanoutRoutePlan[] = []
@@ -2044,19 +2090,21 @@ function routePlaneTerminatedBus(
2044
2090
  ),
2045
2091
  basePlan,
2046
2092
  ]
2047
- const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2048
- planIsClear({
2049
- plan: candidatePlan,
2050
- otherPlans: [...acceptedPlans, ...fixedPlans],
2051
- staticClearanceCache,
2052
- blockingBusCounts,
2053
- cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
2054
- srj,
2055
- sharedBoundary: bus.sharedBoundary,
2056
- clearance,
2057
- allowBlindAndBuriedVias,
2058
- allowSameNetMerges,
2059
- }),
2093
+ const clearPlan = selectClearPlanePlan(
2094
+ plansToTry,
2095
+ (candidatePlan, candidateIndex) =>
2096
+ planIsClear({
2097
+ plan: candidatePlan,
2098
+ otherPlans: [...acceptedPlans, ...fixedPlans],
2099
+ staticClearanceCache,
2100
+ blockingBusCounts,
2101
+ cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
2102
+ srj,
2103
+ sharedBoundary: bus.sharedBoundary,
2104
+ clearance,
2105
+ allowBlindAndBuriedVias,
2106
+ allowSameNetMerges,
2107
+ }),
2060
2108
  )
2061
2109
  if (!clearPlan) return null
2062
2110
  fixedPlans.push(clearPlan)
@@ -2120,19 +2168,21 @@ function routePlaneTerminatedBus(
2120
2168
  ),
2121
2169
  viaInPadPlan,
2122
2170
  ]
2123
- const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2124
- planIsClear({
2125
- plan: candidatePlan,
2126
- otherPlans: acceptedPlans,
2127
- staticClearanceCache,
2128
- blockingBusCounts,
2129
- cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
2130
- srj,
2131
- sharedBoundary: bus.sharedBoundary,
2132
- clearance,
2133
- allowBlindAndBuriedVias,
2134
- allowSameNetMerges,
2135
- }),
2171
+ const clearPlan = selectClearPlanePlan(
2172
+ plansToTry,
2173
+ (candidatePlan, candidateIndex) =>
2174
+ planIsClear({
2175
+ plan: candidatePlan,
2176
+ otherPlans: acceptedPlans,
2177
+ staticClearanceCache,
2178
+ blockingBusCounts,
2179
+ cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
2180
+ srj,
2181
+ sharedBoundary: bus.sharedBoundary,
2182
+ clearance,
2183
+ allowBlindAndBuriedVias,
2184
+ allowSameNetMerges,
2185
+ }),
2136
2186
  )
2137
2187
  if (clearPlan) return [clearPlan]
2138
2188
  }
@@ -2195,7 +2245,7 @@ function routePlaneTerminatedBus(
2195
2245
  )
2196
2246
  const perpendicularPitch = getPerpendicularPitch(directionalBus)
2197
2247
  const sourceEscapePaths: Point2D[][] = []
2198
- const maximumCandidatePaths = 32
2248
+ const maximumCandidatePaths = 128
2199
2249
  for (
2200
2250
  let totalSteps = 0;
2201
2251
  totalSteps <= maximumEscapeSteps &&
@@ -2337,19 +2387,21 @@ function routePlaneTerminatedBus(
2337
2387
  ),
2338
2388
  basePlan,
2339
2389
  ]
2340
- plan = plansToTry.find((candidatePlan, candidateIndex) =>
2341
- planIsClear({
2342
- plan: candidatePlan,
2343
- otherPlans: [...acceptedPlans, ...candidatePlans],
2344
- staticClearanceCache,
2345
- blockingBusCounts,
2346
- cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
2347
- srj,
2348
- sharedBoundary: bus.sharedBoundary,
2349
- clearance,
2350
- allowBlindAndBuriedVias,
2351
- allowSameNetMerges,
2352
- }),
2390
+ plan = selectClearPlanePlan(
2391
+ plansToTry,
2392
+ (candidatePlan, candidateIndex) =>
2393
+ planIsClear({
2394
+ plan: candidatePlan,
2395
+ otherPlans: [...acceptedPlans, ...candidatePlans],
2396
+ staticClearanceCache,
2397
+ blockingBusCounts,
2398
+ cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
2399
+ srj,
2400
+ sharedBoundary: bus.sharedBoundary,
2401
+ clearance,
2402
+ allowBlindAndBuriedVias,
2403
+ allowSameNetMerges,
2404
+ }),
2353
2405
  )
2354
2406
  if (plan) break
2355
2407
  }
@@ -2367,10 +2419,11 @@ function routePlaneTerminatedBus(
2367
2419
  return null
2368
2420
  }
2369
2421
 
2370
- export function routeBusAlternatives(
2422
+ export function* routeBusAlternativesSteps(
2371
2423
  params: RouteBusParams,
2372
2424
  maxAlternatives = 1,
2373
- ): FanoutRoutePlan[][] {
2425
+ includeVisualization = false,
2426
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[][], void> {
2374
2427
  const {
2375
2428
  srj,
2376
2429
  bus,
@@ -2391,6 +2444,7 @@ export function routeBusAlternatives(
2391
2444
  fixedViaPointsByConnectionIndex,
2392
2445
  reservedVias = [],
2393
2446
  viaMinimalOnly = false,
2447
+ fixedViaFallbackRouteOrderAttempts = 24,
2394
2448
  cornerBandTargetTrackOffset,
2395
2449
  } = params
2396
2450
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
@@ -2408,8 +2462,20 @@ export function routeBusAlternatives(
2408
2462
  return []
2409
2463
  }
2410
2464
  if (bus.termination.type === "plane") {
2411
- const plan = routePlaneTerminatedBus(params)
2412
- return plan ? [plan] : []
2465
+ const alternatives: FanoutRoutePlan[][] = []
2466
+ for (
2467
+ let planeCandidateSkipCount = 0;
2468
+ planeCandidateSkipCount < maxAlternatives;
2469
+ planeCandidateSkipCount++
2470
+ ) {
2471
+ const plan = routePlaneTerminatedBus({
2472
+ ...params,
2473
+ planeCandidateSkipCount,
2474
+ })
2475
+ if (!plan) break
2476
+ alternatives.push(plan)
2477
+ }
2478
+ return alternatives
2413
2479
  }
2414
2480
  const exitAxis = getExitAxis(bus)
2415
2481
  const sourceObstacle = bus.connections[0]?.sourceObstacle
@@ -2685,7 +2751,7 @@ export function routeBusAlternatives(
2685
2751
  )!,
2686
2752
  // Preserve the existing bounded search after every inexpensive
2687
2753
  // layer-interleave candidate has had one deterministic attempt.
2688
- maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2754
+ maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
2689
2755
  windingOrderIndex: 0,
2690
2756
  preferTargetDirectedLaneBias: true,
2691
2757
  },
@@ -2765,7 +2831,7 @@ export function routeBusAlternatives(
2765
2831
  .join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
2766
2832
  if (seenTerminalSignatures.has(terminalSignature)) continue
2767
2833
  seenTerminalSignatures.add(terminalSignature)
2768
- const viaMinimalAlternatives = routeViaMinimalWindingAlternatives(
2834
+ const windingSteps = routeViaMinimalWindingAlternativesSteps(
2769
2835
  {
2770
2836
  srj,
2771
2837
  bus,
@@ -2796,7 +2862,19 @@ export function routeBusAlternatives(
2796
2862
  : terminalPattern.maximumRouteOrderAttempts === undefined
2797
2863
  ? Math.min(2, maxAlternatives - alternatives.length)
2798
2864
  : 2,
2865
+ includeVisualization,
2799
2866
  )
2867
+ let windingResult = windingSteps.next()
2868
+ while (!windingResult.done) {
2869
+ yield {
2870
+ phase: "via-minimal-winding",
2871
+ busId: bus.busId,
2872
+ targetLayer,
2873
+ winding: windingResult.value,
2874
+ }
2875
+ windingResult = windingSteps.next()
2876
+ }
2877
+ const viaMinimalAlternatives = windingResult.value
2800
2878
  for (const viaMinimalPlans of viaMinimalAlternatives) {
2801
2879
  const combinedPlansAreClear = fanoutPlansAreClear({
2802
2880
  plans: [...acceptedPlans, ...viaMinimalPlans],
@@ -2949,6 +3027,16 @@ export function routeBusAlternatives(
2949
3027
  return alternatives
2950
3028
  }
2951
3029
 
3030
+ export function routeBusAlternatives(
3031
+ params: RouteBusParams,
3032
+ maxAlternatives = 1,
3033
+ ): FanoutRoutePlan[][] {
3034
+ const steps = routeBusAlternativesSteps(params, maxAlternatives)
3035
+ let result = steps.next()
3036
+ while (!result.done) result = steps.next()
3037
+ return result.value
3038
+ }
3039
+
2952
3040
  export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
2953
3041
  return routeBusAlternatives(params, 1)[0] ?? null
2954
3042
  }