@tscircuit/fanout-solver 0.0.58 → 0.0.59

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/README.md CHANGED
@@ -330,6 +330,11 @@ the solver commit, dataset revision, configuration, solve totals, every sample's
330
330
  status and timing, and partial routing/validation counts. Reports are saved after
331
331
  every completed sample, including the total selected count to identify incomplete runs.
332
332
  Timed-out workers do not retain their in-flight routing counts.
333
+ Every solved case also writes `benchmark-results/<sample-id>.svg`. These SVGs
334
+ are committed so route changes can be reviewed in Git. A run replaces the selected
335
+ cases' snapshots and removes their stale SVGs if they no longer solve; filtered
336
+ runs preserve unselected snapshots. JSON reports and captured inputs remain
337
+ ignored. CI includes the SVGs in its benchmark artifacts.
333
338
  Compare reports with the same budgets to track progress. Solved means validated
334
339
  AM62L fanout, not RAM fanout or downstream inter-chip routing. Partial, error,
335
340
  and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
@@ -1397,9 +1397,14 @@ export class FanoutSolver extends BaseSolver {
1397
1397
  denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
1398
1398
  lengthMatchingStage?: "before-planes" | "after-planes"
1399
1399
  promotedPlaneReservationBusIds?: readonly string[]
1400
+ preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
1400
1401
  planeReservationRetryCount?: number
1401
1402
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1402
1403
  if (this.config.allowBlindAndBuriedVias) return null
1404
+ // An outside-package singleton escape can depend on the completed signal
1405
+ // field. Keep those sites as preferences when plane reservations change;
1406
+ // actual conflicts may still move.
1407
+ let boundaryViaPointsForRetry = params.preferredBoundaryViaPoints
1403
1408
  const usePadAlignedDenseRouting =
1404
1409
  params.denseRoutingStrategy !== "boundary-aligned"
1405
1410
  const debugDense = (...values: unknown[]) => {
@@ -1444,9 +1449,12 @@ export class FanoutSolver extends BaseSolver {
1444
1449
  )
1445
1450
  const useConfiguredDensePlaneRouting =
1446
1451
  configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
1452
+ // A completed boundary assignment remains worth repairing jointly after
1453
+ // plane reservations change; a greedy refill can discard that assignment.
1447
1454
  const useAdaptiveJointPlaneSelection =
1448
1455
  useAdaptiveDensePlaneRouting &&
1449
- (params.planeReservationRetryCount ?? 0) === 0
1456
+ ((params.planeReservationRetryCount ?? 0) === 0 ||
1457
+ Boolean(params.preferredBoundaryViaPoints))
1450
1458
  const matchLengthsAfterPlanes =
1451
1459
  useConfiguredDensePlaneRouting &&
1452
1460
  params.lengthMatchingStage !== "before-planes"
@@ -1871,19 +1879,45 @@ export class FanoutSolver extends BaseSolver {
1871
1879
  )
1872
1880
  })
1873
1881
  : []
1882
+ // A corner singleton on the outward source row must leave before its
1883
+ // surrounding wide bus closes the shared target-layer corridor.
1874
1884
  const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses
1875
1885
  ? singletonBoundaryBuses.filter((singletonBus) => {
1876
1886
  const containingWideBus = getContainingWideSourceField(singletonBus)
1877
1887
  const singletonTargetLayer =
1878
1888
  params.busLayerAssignments[singletonBus.busId]
1889
+ if (!containingWideBus || !singletonTargetLayer) return false
1879
1890
  const containingWideLayers =
1880
- containingWideBus?.routableEscapeLayers ??
1881
- containingWideBus?.allowedLayers ??
1891
+ containingWideBus.routableEscapeLayers ??
1892
+ containingWideBus.allowedLayers ??
1882
1893
  []
1894
+ const sourceAxis =
1895
+ singletonBus.exitEdge === "left" ||
1896
+ singletonBus.exitEdge === "right"
1897
+ ? "x"
1898
+ : "y"
1899
+ const wideCoordinates = containingWideBus.connections.map(
1900
+ (connection) => connection.sourcePoint[sourceAxis],
1901
+ )
1902
+ const outwardSourceCoordinate =
1903
+ singletonBus.exitEdge === "left" ||
1904
+ singletonBus.exitEdge === "bottom"
1905
+ ? Math.min(...wideCoordinates)
1906
+ : Math.max(...wideCoordinates)
1907
+ const isOnOutwardSourceEdge = singletonBus.connections.every(
1908
+ (connection) =>
1909
+ Math.abs(
1910
+ connection.sourcePoint[sourceAxis] - outwardSourceCoordinate,
1911
+ ) < 1e-9,
1912
+ )
1883
1913
  return Boolean(
1884
- containingWideBus &&
1885
- singletonTargetLayer &&
1886
- !containingWideLayers.includes(singletonTargetLayer),
1914
+ !containingWideLayers.includes(singletonTargetLayer) ||
1915
+ (useAdaptiveDensePlaneRouting &&
1916
+ isOnOutwardSourceEdge &&
1917
+ getCornerBandSide(
1918
+ singletonBus.exitEdge,
1919
+ singletonBus.preferredExit,
1920
+ )),
1887
1921
  )
1888
1922
  })
1889
1923
  : []
@@ -1994,6 +2028,8 @@ export class FanoutSolver extends BaseSolver {
1994
2028
  traceWidth: this.config.traceWidth,
1995
2029
  clearance: this.config.clearance,
1996
2030
  maximumSearchStates: 100_000,
2031
+ preferredViaPointsByConnectionIndex:
2032
+ params.preferredBoundaryViaPoints,
1997
2033
  preferredBoundaryPerpendicularSideByBusId,
1998
2034
  preferBoundaryOutwardByBusId,
1999
2035
  additionalObstacles: denseAdditionalObstacles,
@@ -2012,6 +2048,8 @@ export class FanoutSolver extends BaseSolver {
2012
2048
  traceWidth: this.config.traceWidth,
2013
2049
  clearance: this.config.clearance,
2014
2050
  maximumSearchStates: 20_000,
2051
+ preferredViaPointsByConnectionIndex:
2052
+ params.preferredBoundaryViaPoints,
2015
2053
  preferredBoundaryPerpendicularSideByBusId,
2016
2054
  preferBoundaryOutwardByBusId,
2017
2055
  additionalObstacles: denseAdditionalObstacles,
@@ -2070,6 +2108,8 @@ export class FanoutSolver extends BaseSolver {
2070
2108
  traceWidth: this.config.traceWidth,
2071
2109
  clearance: this.config.clearance,
2072
2110
  maximumSearchStates: 100_000,
2111
+ preferredViaPointsByConnectionIndex:
2112
+ params.preferredBoundaryViaPoints,
2073
2113
  preferredBoundaryPerpendicularSideByBusId,
2074
2114
  preferBoundaryOutwardByBusId,
2075
2115
  fixedViaPointsByConnectionIndex: seedViaPoints,
@@ -2221,8 +2261,10 @@ export class FanoutSolver extends BaseSolver {
2221
2261
  allowBoundarySideViaFallback: bus.connections.length === 1,
2222
2262
  preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
2223
2263
  adaptiveWindingRouteOrder,
2264
+ // Retain pad-aligned channels even when plane sites are reserved
2265
+ // adaptively; the boundary grid can fence off a turning wide bus.
2224
2266
  alignWindingGridToPads:
2225
- usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting,
2267
+ usePadAlignedDenseRouting && !configuredDensePlaneRouting,
2226
2268
  fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
2227
2269
  ? 60
2228
2270
  : useConfiguredDensePlaneRouting
@@ -2407,6 +2449,31 @@ export class FanoutSolver extends BaseSolver {
2407
2449
  return false
2408
2450
  }
2409
2451
  matchedPlans.push(...busPlans)
2452
+ if (
2453
+ matchedPlans.length ===
2454
+ boundaryBuses.reduce(
2455
+ (sum, candidate) => sum + candidate.connections.length,
2456
+ 0,
2457
+ ) &&
2458
+ matchedPlans.some((plan) => {
2459
+ const bus = boundaryBuses.find(
2460
+ (candidate) => candidate.busId === plan.busId,
2461
+ )
2462
+ if (bus?.connections.length !== 1 || !plan.via) return false
2463
+ const { center } = plan.via
2464
+ return (
2465
+ center.x < bus.componentBounds.minX ||
2466
+ center.x > bus.componentBounds.maxX ||
2467
+ center.y < bus.componentBounds.minY ||
2468
+ center.y > bus.componentBounds.maxY
2469
+ )
2470
+ })
2471
+ )
2472
+ boundaryViaPointsForRetry = new Map(
2473
+ matchedPlans
2474
+ .filter((plan) => plan.via)
2475
+ .map((plan) => [plan.connectionIndex, plan.via!.center]),
2476
+ )
2410
2477
  debugDense("route:complete", bus.busId, busPlans.length)
2411
2478
  return true
2412
2479
  }.bind(this)
@@ -2932,7 +2999,14 @@ export class FanoutSolver extends BaseSolver {
2932
2999
  let alternatePlaneSearchStates = 0
2933
3000
  const maximumAlternatePlaneSearchStates = Number(
2934
3001
  process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ??
2935
- (useConfiguredDensePlaneRouting ? 3_000_000 : 1_000),
3002
+ // Keep the initial search short before reserving blocked
3003
+ // plane sites. Later joint repairs retain the full budget.
3004
+ (useAdaptiveDensePlaneRouting &&
3005
+ (params.planeReservationRetryCount ?? 0) === 0
3006
+ ? 10_000
3007
+ : useConfiguredDensePlaneRouting
3008
+ ? 3_000_000
3009
+ : 1_000),
2936
3010
  )
2937
3011
  const maximumAlternatePlaneRoutes = Number(
2938
3012
  process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
@@ -3838,6 +3912,7 @@ export class FanoutSolver extends BaseSolver {
3838
3912
  allowSameNetMerges: this.config.allowSameNetMerges,
3839
3913
  allowMatchingInsideDenseBounds: true,
3840
3914
  allowPairLaneSpreading: true,
3915
+ allowUnconstrainedLaneRerouting: true,
3841
3916
  }
3842
3917
  let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
3843
3918
  const shortenedBusIds = new Set<string>()
@@ -3927,6 +4002,7 @@ export class FanoutSolver extends BaseSolver {
3927
4002
  debugDense("promote-reservations", refinedPromotions)
3928
4003
  return yield* this.routeDenseThroughAllMixedTerminationSteps({
3929
4004
  ...params,
4005
+ preferredBoundaryViaPoints: boundaryViaPointsForRetry,
3930
4006
  promotedPlaneReservationBusIds: [
3931
4007
  ...(params.promotedPlaneReservationBusIds ?? []),
3932
4008
  ...refinedPromotions,
@@ -8,7 +8,9 @@ import {
8
8
  distanceSegmentToSegment,
9
9
  segmentsAreClear,
10
10
  } from "./geometry"
11
- import { fanoutPlansAreClear } from "./route-bus"
11
+ import { getCopperLayerNames } from "./layer-names"
12
+ import { fanoutPlansAreClear, fanoutPlansAreMutuallyClear } from "./route-bus"
13
+ import { routeViaMinimalWinding } from "./route-via-minimal-winding"
12
14
  import type {
13
15
  Bounds,
14
16
  FanoutRoutePlan,
@@ -647,6 +649,8 @@ export function matchBusPlanLengths(params: {
647
649
  allowMatchingInsideDenseBounds?: boolean
648
650
  /** Allow a differential pair's longer lane to move aside before tuning its mate. */
649
651
  allowPairLaneSpreading?: boolean
652
+ /** Allow one unconstrained boundary lane to move around a tuning meander. */
653
+ allowUnconstrainedLaneRerouting?: boolean
650
654
  /**
651
655
  * Rejects a geometrically clear candidate when it would make a caller-owned
652
656
  * downstream assignment (such as pending plane dogbones) infeasible.
@@ -866,6 +870,124 @@ export function matchBusPlanLengths(params: {
866
870
  if (++attempts >= 4) break
867
871
  }
868
872
  }
873
+ if (!acceptedPlans && params.allowUnconstrainedLaneRerouting) {
874
+ // A neighboring singleton may occupy the only tuning window. Keep its
875
+ // source dogbone, via and boundary endpoint fixed, and reroute only its
876
+ // target-layer copper around a complete meander. Constrained buses and
877
+ // plane routes are never displaced by this bounded repair.
878
+ let rerouteAttempts = 0
879
+ candidateSearch: for (const targetAddedLength of targetAddedLengths) {
880
+ const candidates = createTunedPlanCandidates({
881
+ plan: shortest,
882
+ bus,
883
+ targetAddedLength,
884
+ clearance,
885
+ sharedBoundary: bus.sharedBoundary,
886
+ allowInsideDenseBounds: allowMatchingInsideDenseBounds,
887
+ })
888
+ for (const candidate of candidates) {
889
+ if (
890
+ !fanoutPlansAreClear({
891
+ plans: [candidate],
892
+ srj: inputSrj,
893
+ sharedBoundary,
894
+ clearance,
895
+ allowBlindAndBuriedVias,
896
+ allowSameNetMerges,
897
+ })
898
+ )
899
+ continue
900
+ const blockers = matchedPlans.filter(
901
+ (plan) =>
902
+ plan !== shortest &&
903
+ !fanoutPlansAreMutuallyClear({
904
+ plans: [candidate, plan],
905
+ srj: inputSrj,
906
+ clearance,
907
+ allowSameNetMerges,
908
+ }),
909
+ )
910
+ if (blockers.length !== 1) continue
911
+ const blocker = blockers[0]!
912
+ const blockerBus = preparedBuses.find(
913
+ (prepared) => prepared.busId === blocker.busId,
914
+ )
915
+ if (
916
+ !blockerBus ||
917
+ blockerBus.termination.type !== "boundary" ||
918
+ blockerBus.maxLengthSkew !== undefined ||
919
+ blockerBus.connections.length !== 1 ||
920
+ !blocker.via ||
921
+ blocker.additionalVias?.length ||
922
+ blocker.planeEndpointVia ||
923
+ blocker.segments.filter(
924
+ (segment) => segment.layer === blocker.sourceLayer,
925
+ ).length !== 1
926
+ )
927
+ continue
928
+ const nextPlans = matchedPlans.map((plan) =>
929
+ plan === shortest ? candidate : plan,
930
+ )
931
+ for (const alignGridToPads of [true, false]) {
932
+ if (rerouteAttempts++ >= 4) break candidateSearch
933
+ const rerouted = routeViaMinimalWinding({
934
+ srj: inputSrj,
935
+ bus: blockerBus,
936
+ terminals: [
937
+ {
938
+ connection: blockerBus.connections[0]!,
939
+ viaPoint: blocker.via.center,
940
+ exitPoint: blocker.exitPoint,
941
+ },
942
+ ],
943
+ targetLayer: blocker.targetLayer,
944
+ acceptedPlans: nextPlans.filter((plan) => plan !== blocker),
945
+ layerNames: getCopperLayerNames(inputSrj.layerCount),
946
+ traceWidth: blocker.segments[0]!.width,
947
+ viaDiameter: blocker.via.diameter,
948
+ viaHoleDiameter: blocker.via.holeDiameter,
949
+ clearance,
950
+ allowBlindAndBuriedVias,
951
+ allowSameNetMerges,
952
+ gridStepDivisor: 2,
953
+ alignGridToPads,
954
+ maximumRouteOrderAttempts: 1,
955
+ preferTargetDirectedLaneBias: true,
956
+ })?.[0]
957
+ if (!rerouted) continue
958
+ // Rebuild from the original plan to preserve its route identity,
959
+ // endpoint metadata and physical via span exactly.
960
+ const repaired = createPlanWithSegments(blocker, [
961
+ blocker.segments[0]!,
962
+ ...rerouted.segments.slice(1),
963
+ ])
964
+ if (!repaired) continue
965
+ const repairedPlans = nextPlans.map((plan) =>
966
+ plan === blocker ? repaired : plan,
967
+ )
968
+ if (
969
+ getBusSkew(
970
+ repairedPlans.filter((plan) => plan.busId === bus.busId),
971
+ ) >
972
+ maxLengthSkew + EPSILON ||
973
+ !fanoutPlansAreClear({
974
+ plans: repairedPlans,
975
+ srj: inputSrj,
976
+ sharedBoundary,
977
+ clearance,
978
+ allowBlindAndBuriedVias,
979
+ allowSameNetMerges,
980
+ }) ||
981
+ (candidatePlansAreFeasible &&
982
+ !candidatePlansAreFeasible(repairedPlans))
983
+ )
984
+ continue
985
+ acceptedPlans = repairedPlans
986
+ break candidateSearch
987
+ }
988
+ }
989
+ }
990
+ }
869
991
  if (!acceptedPlans) return { plans: null, failedBus: bus }
870
992
  matchedPlans = acceptedPlans
871
993
  }
package/lib/route-bus.ts CHANGED
@@ -3060,9 +3060,9 @@ export function* routeBusAlternativesSteps(
3060
3060
  // A through-via does not have to sit next to the source pad. A narrow bus
3061
3061
  // embedded in another bus's source field can have every local dogbone site
3062
3062
  // occupied while still having a clear source-layer escape. A pair first
3063
- // relocates its two vias just outside the nearest package edge; a singleton
3064
- // retains the boundary-side-via fallback. Both forms preserve one via per
3065
- // signal without weakening any clearance rule.
3063
+ // relocates its two vias just outside the nearest package edge. A singleton
3064
+ // tries the existing boundary-side vias before searching outside the package.
3065
+ // Both forms preserve one via per signal without weakening clearance rules.
3066
3066
  if (
3067
3067
  alternatives.length === 0 &&
3068
3068
  allowBoundarySideViaFallback &&
@@ -3182,6 +3182,46 @@ export function* routeBusAlternativesSteps(
3182
3182
  ),
3183
3183
  )
3184
3184
  : []
3185
+ const singletonMatchedVia =
3186
+ bus.connections.length === 1
3187
+ ? fixedViaPointsByConnectionIndex.get(
3188
+ bus.connections[0]!.connectionIndex,
3189
+ )
3190
+ : undefined
3191
+ const packageEdgeViaCandidates = singletonMatchedVia
3192
+ ? [
3193
+ {
3194
+ distance: sourceCenter.x - bus.componentBounds.minX,
3195
+ point: {
3196
+ x: bus.componentBounds.minX - 2 * insetStep,
3197
+ y: singletonMatchedVia.y,
3198
+ },
3199
+ },
3200
+ {
3201
+ distance: bus.componentBounds.maxX - sourceCenter.x,
3202
+ point: {
3203
+ x: bus.componentBounds.maxX + 2 * insetStep,
3204
+ y: singletonMatchedVia.y,
3205
+ },
3206
+ },
3207
+ {
3208
+ distance: sourceCenter.y - bus.componentBounds.minY,
3209
+ point: {
3210
+ x: singletonMatchedVia.x,
3211
+ y: bus.componentBounds.minY - 2 * insetStep,
3212
+ },
3213
+ },
3214
+ {
3215
+ distance: bus.componentBounds.maxY - sourceCenter.y,
3216
+ point: {
3217
+ x: singletonMatchedVia.x,
3218
+ y: bus.componentBounds.maxY + 2 * insetStep,
3219
+ },
3220
+ },
3221
+ ]
3222
+ .toSorted((first, second) => first.distance - second.distance)
3223
+ .map(({ point }) => [point])
3224
+ : []
3185
3225
  const viaCandidates = [
3186
3226
  ...displacedViaCandidates.map((points) => ({
3187
3227
  points,
@@ -3191,6 +3231,13 @@ export function* routeBusAlternativesSteps(
3191
3231
  points,
3192
3232
  boundarySide: true,
3193
3233
  })),
3234
+ // Preserve existing singleton escapes before trying a short source-layer
3235
+ // route beyond the package. The target layer can then wind to the exit
3236
+ // without a local via being fenced in by an already-routed wide bus.
3237
+ ...packageEdgeViaCandidates.map((points) => ({
3238
+ points,
3239
+ boundarySide: false,
3240
+ })),
3194
3241
  ]
3195
3242
  for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
3196
3243
  const boundaryVias = bus.connections.map((connection, index) => ({
@@ -3355,7 +3402,9 @@ export function* routeBusAlternativesSteps(
3355
3402
  )
3356
3403
  return {
3357
3404
  ...sourceLayerPlan,
3358
- ...(preferCornerBoundaryVia || bus.connections.length > 1
3405
+ ...(preferCornerBoundaryVia ||
3406
+ bus.connections.length > 1 ||
3407
+ !boundarySide
3359
3408
  ? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
3360
3409
  : {}),
3361
3410
  targetLayer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.58",
3
+ "version": "0.0.59",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",