@tscircuit/fanout-solver 0.0.49 → 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,8 +24,8 @@ import {
24
24
  obstacleSharesElectricalNet,
25
25
  } from "./net-identity"
26
26
  import {
27
- routeViaMinimalWindingAlternativesSteps,
28
27
  type RouteViaMinimalWindingProgress,
28
+ routeViaMinimalWindingAlternativesSteps,
29
29
  type ViaMinimalWindingReservedVia,
30
30
  } from "./route-via-minimal-winding"
31
31
  import type {
@@ -61,6 +61,10 @@ export interface RouteBusParams {
61
61
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
62
62
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
63
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
64
68
  /** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
65
69
  cornerBandTargetTrackOffset?: number
66
70
  }
@@ -1873,6 +1877,24 @@ function planIsClearOfPlans(params: {
1873
1877
  return true
1874
1878
  }
1875
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
+
1876
1898
  function planIsClear(params: {
1877
1899
  plan: FanoutRoutePlan
1878
1900
  otherPlans: FanoutRoutePlan[]
@@ -1991,11 +2013,27 @@ function routePlaneTerminatedBus(
1991
2013
  allowBlindAndBuriedVias = true,
1992
2014
  allowSameNetMerges = false,
1993
2015
  fixedViaPointsByConnectionIndex,
2016
+ planeCandidateSkipCount = 0,
1994
2017
  } = params
1995
2018
  const sourceObstacle = bus.connections[0]?.sourceObstacle
1996
2019
  if (!sourceObstacle || bus.termination.type !== "plane") return null
1997
2020
  const sourceLayer = bus.connections[0]!.sourceLayer
1998
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
+ }
1999
2037
 
2000
2038
  if (fixedViaPointsByConnectionIndex) {
2001
2039
  const fixedPlans: FanoutRoutePlan[] = []
@@ -2052,19 +2090,21 @@ function routePlaneTerminatedBus(
2052
2090
  ),
2053
2091
  basePlan,
2054
2092
  ]
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
- }),
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
+ }),
2068
2108
  )
2069
2109
  if (!clearPlan) return null
2070
2110
  fixedPlans.push(clearPlan)
@@ -2128,19 +2168,21 @@ function routePlaneTerminatedBus(
2128
2168
  ),
2129
2169
  viaInPadPlan,
2130
2170
  ]
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
- }),
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
+ }),
2144
2186
  )
2145
2187
  if (clearPlan) return [clearPlan]
2146
2188
  }
@@ -2203,7 +2245,7 @@ function routePlaneTerminatedBus(
2203
2245
  )
2204
2246
  const perpendicularPitch = getPerpendicularPitch(directionalBus)
2205
2247
  const sourceEscapePaths: Point2D[][] = []
2206
- const maximumCandidatePaths = 32
2248
+ const maximumCandidatePaths = 128
2207
2249
  for (
2208
2250
  let totalSteps = 0;
2209
2251
  totalSteps <= maximumEscapeSteps &&
@@ -2345,19 +2387,21 @@ function routePlaneTerminatedBus(
2345
2387
  ),
2346
2388
  basePlan,
2347
2389
  ]
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
- }),
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
+ }),
2361
2405
  )
2362
2406
  if (plan) break
2363
2407
  }
@@ -2400,6 +2444,7 @@ export function* routeBusAlternativesSteps(
2400
2444
  fixedViaPointsByConnectionIndex,
2401
2445
  reservedVias = [],
2402
2446
  viaMinimalOnly = false,
2447
+ fixedViaFallbackRouteOrderAttempts = 24,
2403
2448
  cornerBandTargetTrackOffset,
2404
2449
  } = params
2405
2450
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
@@ -2417,8 +2462,20 @@ export function* routeBusAlternativesSteps(
2417
2462
  return []
2418
2463
  }
2419
2464
  if (bus.termination.type === "plane") {
2420
- const plan = routePlaneTerminatedBus(params)
2421
- 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
2422
2479
  }
2423
2480
  const exitAxis = getExitAxis(bus)
2424
2481
  const sourceObstacle = bus.connections[0]?.sourceObstacle
@@ -2694,7 +2751,7 @@ export function* routeBusAlternativesSteps(
2694
2751
  )!,
2695
2752
  // Preserve the existing bounded search after every inexpensive
2696
2753
  // layer-interleave candidate has had one deterministic attempt.
2697
- maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2754
+ maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
2698
2755
  windingOrderIndex: 0,
2699
2756
  preferTargetDirectedLaneBias: true,
2700
2757
  },
package/lib/types.ts CHANGED
@@ -197,6 +197,17 @@ export interface FanoutSolverOptions {
197
197
  allowBlindAndBuriedVias?: boolean
198
198
  /** Allow branches belonging to the same electrical net to share copper. */
199
199
  allowSameNetMerges?: boolean
200
+ /**
201
+ * Plane buses whose adjacent dogbone sites must be reserved before routing
202
+ * dense through-via boundary buses. This is an advanced deterministic hint
203
+ * for fields where later signal copper would otherwise consume those sites.
204
+ */
205
+ densePlaneReservationBusIds?: readonly string[]
206
+ /**
207
+ * Plane buses that should participate in the bounded alternate-route CSP
208
+ * instead of using an adjacent component dogbone.
209
+ */
210
+ denseUnrestrictedPlaneRoutingBusIds?: readonly string[]
200
211
  singleLayerPushAndShove?: boolean
201
212
  /**
202
213
  * When preferred single-layer exits cannot coexist, allow a global
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",