@tscircuit/fanout-solver 0.0.50 → 0.0.52
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 +50 -2
- package/lib/fanout-solver.ts +202 -11
- package/lib/match-component-dogbone-via-sites.ts +9 -0
- package/lib/route-bus.ts +244 -8
- package/lib/route-via-minimal-winding.ts +94 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -280,6 +280,54 @@ parameters. Each sample has one shared boundary around all of its footprints,
|
|
|
280
280
|
and component bounds come from the exact footprinter-generated copper pad
|
|
281
281
|
extents.
|
|
282
282
|
|
|
283
|
+
## All-sample benchmark
|
|
284
|
+
|
|
285
|
+
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark every available sample:
|
|
286
|
+
Datasets 01–08, SRJ19, and SRJ29, currently **421 samples**. New samples in these
|
|
287
|
+
catalogs are discovered automatically; no test-file allowlist is involved.
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
./benchmark.sh
|
|
291
|
+
./benchmark.sh --list
|
|
292
|
+
./benchmark.sh --dataset dataset08
|
|
293
|
+
./benchmark.sh --dataset srj19,srj29 --sample sample001
|
|
294
|
+
./benchmark.sh --concurrency 8 --sample-timeout-seconds 300
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Each sample runs in an isolated process, with up to four concurrent processes
|
|
298
|
+
locally and a **120-second hard timeout** by default. A synchronous solver hang,
|
|
299
|
+
exception, or unsolved case does not prevent later samples from running.
|
|
300
|
+
Assignment budgets and circuit constraints remain at each sample's defaults;
|
|
301
|
+
`--max-layer-combinations` explicitly overrides only the search budget.
|
|
302
|
+
|
|
303
|
+
The ordered `benchmark-results/benchmark.json` and `benchmark.md` reports contain
|
|
304
|
+
the commit, configuration, per-dataset solve totals, every sample's status and
|
|
305
|
+
timing, and partial routing/validation counts. Reports are saved after every
|
|
306
|
+
completed sample, including the total selected count to identify incomplete runs.
|
|
307
|
+
Timed-out workers do not retain their in-flight routing counts.
|
|
308
|
+
Compare reports with the same budgets to track progress. Solved means validated
|
|
309
|
+
fanout, not downstream inter-chip routing; SRJ29 additionally requires its
|
|
310
|
+
original-endpoint and emitted-copper DRC checks. Partial, error, and timeout rows
|
|
311
|
+
are benchmark results (exit 0); invalid CLI arguments or report I/O failures are
|
|
312
|
+
command failures (nonzero exit).
|
|
313
|
+
|
|
314
|
+
### PR comment trigger
|
|
315
|
+
|
|
316
|
+
Once `.github/workflows/benchmark.yml` is on the default branch, a repository
|
|
317
|
+
writer can comment **`/benchmark`** on an open PR. The workflow captures that
|
|
318
|
+
PR's exact head SHA, runs all samples on a **32-vCPU Blacksmith ARM** runner,
|
|
319
|
+
then updates a status comment with solve totals, per-sample results, and a link
|
|
320
|
+
to the complete JSON/Markdown artifact. The Actions UI also supports a manual
|
|
321
|
+
run, optionally supplying an open PR number. No custom bot token is required.
|
|
322
|
+
|
|
323
|
+
The runner defaults to 32 processes and a 120-second per-sample deadline; set
|
|
324
|
+
repository variables `BENCHMARK_CONCURRENCY` and
|
|
325
|
+
`BENCHMARK_SAMPLE_TIMEOUT_SECONDS` to change these. PR code runs with a read-only
|
|
326
|
+
token and no persisted checkout credentials. A separate job uses the trusted
|
|
327
|
+
workflow revision to validate report data and post comments; it never executes
|
|
328
|
+
PR code. Only exact commands from non-bot users with current write, maintain, or
|
|
329
|
+
admin access are accepted.
|
|
330
|
+
|
|
283
331
|
## SRJ29 benchmark
|
|
284
332
|
|
|
285
333
|
The repository loads all 200 samples from the derivative
|
|
@@ -293,10 +341,10 @@ direction. Every adapted problem uses the same six-layer stackup (`top`,
|
|
|
293
341
|
`inner1` through `inner4`, and `bottom`) so benchmark improvements are directly
|
|
294
342
|
comparable.
|
|
295
343
|
|
|
296
|
-
|
|
344
|
+
The dedicated legacy SRJ29 report remains available with:
|
|
297
345
|
|
|
298
346
|
```sh
|
|
299
|
-
|
|
347
|
+
bun run benchmark:srj29
|
|
300
348
|
```
|
|
301
349
|
|
|
302
350
|
Use `--sample sample001`, `--limit 10`, or
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -1710,6 +1710,46 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1710
1710
|
: getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
1711
1711
|
]),
|
|
1712
1712
|
)
|
|
1713
|
+
// An outward half-pitch dogbone can enter a neighboring wide bus's pad
|
|
1714
|
+
// field when a corner bus first escapes toward the package center. Keep
|
|
1715
|
+
// those through-barrels on the opposite side before any earlier bus
|
|
1716
|
+
// commits copper; rematching after that point can be too late.
|
|
1717
|
+
for (const bus of wideBoundaryBuses) {
|
|
1718
|
+
if (useConfiguredDensePlaneRouting) continue
|
|
1719
|
+
if (!getCornerBandSide(bus.exitEdge, bus.preferredExit)) continue
|
|
1720
|
+
const entersNeighboringSourceField = wideBoundaryBuses.some((other) => {
|
|
1721
|
+
if (other === bus || other.componentId !== bus.componentId) return false
|
|
1722
|
+
const xs = other.connections.map(
|
|
1723
|
+
(connection) => connection.sourcePoint.x,
|
|
1724
|
+
)
|
|
1725
|
+
const ys = other.connections.map(
|
|
1726
|
+
(connection) => connection.sourcePoint.y,
|
|
1727
|
+
)
|
|
1728
|
+
const minX = Math.min(...xs)
|
|
1729
|
+
const maxX = Math.max(...xs)
|
|
1730
|
+
const minY = Math.min(...ys)
|
|
1731
|
+
const maxY = Math.max(...ys)
|
|
1732
|
+
return bus.connections.some(({ sourcePoint }) => {
|
|
1733
|
+
const x =
|
|
1734
|
+
sourcePoint.x +
|
|
1735
|
+
(bus.direction === "left"
|
|
1736
|
+
? -bus.pitchX / 2
|
|
1737
|
+
: bus.direction === "right"
|
|
1738
|
+
? bus.pitchX / 2
|
|
1739
|
+
: 0)
|
|
1740
|
+
const y =
|
|
1741
|
+
sourcePoint.y +
|
|
1742
|
+
(bus.direction === "down"
|
|
1743
|
+
? -bus.pitchY / 2
|
|
1744
|
+
: bus.direction === "up"
|
|
1745
|
+
? bus.pitchY / 2
|
|
1746
|
+
: 0)
|
|
1747
|
+
return x >= minX && x <= maxX && y >= minY && y <= maxY
|
|
1748
|
+
})
|
|
1749
|
+
})
|
|
1750
|
+
if (entersNeighboringSourceField)
|
|
1751
|
+
preferBoundaryOutwardByBusId.set(bus.busId, false)
|
|
1752
|
+
}
|
|
1713
1753
|
const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
|
|
1714
1754
|
if (debugFlippedBoundaryBus) {
|
|
1715
1755
|
preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
|
|
@@ -1859,9 +1899,17 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1859
1899
|
...(process.env.FANOUT_DEBUG_PROVISIONAL_NARROW === "1"
|
|
1860
1900
|
? boundaryBuses.filter((bus) => bus.connections.length < 8)
|
|
1861
1901
|
: []),
|
|
1862
|
-
...singletonDeferralCandidates.filter(
|
|
1863
|
-
|
|
1864
|
-
|
|
1902
|
+
...singletonDeferralCandidates.filter((bus) => {
|
|
1903
|
+
const containingBus = getContainingWideSourceField(bus)
|
|
1904
|
+
const sharesContainingBusLayer =
|
|
1905
|
+
!useConfiguredDensePlaneRouting &&
|
|
1906
|
+
containingBus &&
|
|
1907
|
+
params.busLayerAssignments[containingBus.busId] ===
|
|
1908
|
+
params.busLayerAssignments[bus.busId]
|
|
1909
|
+
return (
|
|
1910
|
+
!leadingWideSingletonBuses.includes(bus) && !sharesContainingBusLayer
|
|
1911
|
+
)
|
|
1912
|
+
}),
|
|
1865
1913
|
...(hasThreeWideBoundaryBuses
|
|
1866
1914
|
? boundaryBuses.filter(
|
|
1867
1915
|
(bus) =>
|
|
@@ -2088,6 +2136,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2088
2136
|
if (!targetLayer) {
|
|
2089
2137
|
return false
|
|
2090
2138
|
}
|
|
2139
|
+
const adaptiveWindingRouteOrder =
|
|
2140
|
+
!useConfiguredDensePlaneRouting &&
|
|
2141
|
+
!getCornerBandSide(bus.exitEdge, bus.preferredExit) &&
|
|
2142
|
+
bus.connections.length > 2 &&
|
|
2143
|
+
wideBoundaryBuses.some((candidate) =>
|
|
2144
|
+
getCornerBandSide(candidate.exitEdge, candidate.preferredExit),
|
|
2145
|
+
)
|
|
2091
2146
|
const routeParams = {
|
|
2092
2147
|
srj: this.routingSrj,
|
|
2093
2148
|
bus,
|
|
@@ -2105,9 +2160,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2105
2160
|
fixedViaPointsByConnectionIndex,
|
|
2106
2161
|
reservedVias: getReservedVias(bus),
|
|
2107
2162
|
viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2163
|
+
allowBoundarySideViaFallback: !useConfiguredDensePlaneRouting,
|
|
2164
|
+
adaptiveWindingRouteOrder,
|
|
2165
|
+
alignWindingGridToPads: !useConfiguredDensePlaneRouting,
|
|
2166
|
+
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
|
|
2167
|
+
? 60
|
|
2168
|
+
: useConfiguredDensePlaneRouting
|
|
2169
|
+
? 6
|
|
2170
|
+
: 24,
|
|
2111
2171
|
cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
|
|
2112
2172
|
} as const
|
|
2113
2173
|
const routeAlternatives = function* (
|
|
@@ -2158,6 +2218,88 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2158
2218
|
if (!busPlans && preferSingleLayerWinding) {
|
|
2159
2219
|
busPlans = (yield* routeAlternatives(routeParams, 1))[0]
|
|
2160
2220
|
}
|
|
2221
|
+
if (!busPlans && !useConfiguredDensePlaneRouting) {
|
|
2222
|
+
const originalPoints = fixedViaPointsByConnectionIndex
|
|
2223
|
+
const originalOutward =
|
|
2224
|
+
preferBoundaryOutwardByBusId.get(bus.busId) ?? true
|
|
2225
|
+
const originalSide =
|
|
2226
|
+
preferredBoundaryPerpendicularSideByBusId.get(bus.busId) ?? 1
|
|
2227
|
+
for (const [outward, side] of [
|
|
2228
|
+
[!originalOutward, originalSide],
|
|
2229
|
+
[originalOutward, -originalSide],
|
|
2230
|
+
[!originalOutward, -originalSide],
|
|
2231
|
+
] as const) {
|
|
2232
|
+
const rematchedPoints = matchComponentDogboneViaSites(
|
|
2233
|
+
[
|
|
2234
|
+
...new Set([
|
|
2235
|
+
...activeBoundaryReservationPlaneBuses,
|
|
2236
|
+
...initiallyMatchedBoundaryBuses,
|
|
2237
|
+
...this.preparedBuses.filter((candidate) =>
|
|
2238
|
+
matchedPlans.some((plan) => plan.busId === candidate.busId),
|
|
2239
|
+
),
|
|
2240
|
+
bus,
|
|
2241
|
+
]),
|
|
2242
|
+
],
|
|
2243
|
+
{
|
|
2244
|
+
viaDiameter: this.config.viaDiameter,
|
|
2245
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
2246
|
+
traceWidth: this.config.traceWidth,
|
|
2247
|
+
clearance: this.config.clearance,
|
|
2248
|
+
maximumSearchStates: 100_000,
|
|
2249
|
+
preferredBoundaryPerpendicularSideByBusId: new Map([
|
|
2250
|
+
...preferredBoundaryPerpendicularSideByBusId,
|
|
2251
|
+
[bus.busId, side as -1 | 1],
|
|
2252
|
+
]),
|
|
2253
|
+
preferBoundaryOutwardByBusId: new Map([
|
|
2254
|
+
...preferBoundaryOutwardByBusId,
|
|
2255
|
+
[bus.busId, outward],
|
|
2256
|
+
]),
|
|
2257
|
+
fixedViaPointsByConnectionIndex: new Map(
|
|
2258
|
+
matchedPlans
|
|
2259
|
+
.filter((plan) => plan.via)
|
|
2260
|
+
.map((plan) => [plan.connectionIndex, plan.via!.center]),
|
|
2261
|
+
),
|
|
2262
|
+
preferredViaPointsByConnectionIndex: new Map(
|
|
2263
|
+
[...originalPoints].filter(
|
|
2264
|
+
([index]) =>
|
|
2265
|
+
!bus.connections.some(
|
|
2266
|
+
(connection) => connection.connectionIndex === index,
|
|
2267
|
+
),
|
|
2268
|
+
),
|
|
2269
|
+
),
|
|
2270
|
+
blockingSegments: matchedPlans.flatMap((plan) =>
|
|
2271
|
+
plan.segments.map((segment) => ({
|
|
2272
|
+
connectionIndex: plan.connectionIndex,
|
|
2273
|
+
segment,
|
|
2274
|
+
})),
|
|
2275
|
+
),
|
|
2276
|
+
additionalObstacles: this.routingSrj.obstacles,
|
|
2277
|
+
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
2278
|
+
canShareCopper,
|
|
2279
|
+
},
|
|
2280
|
+
)
|
|
2281
|
+
debugDense(
|
|
2282
|
+
"retry-sites",
|
|
2283
|
+
bus.busId,
|
|
2284
|
+
outward,
|
|
2285
|
+
side,
|
|
2286
|
+
rematchedPoints?.size ?? "failed",
|
|
2287
|
+
)
|
|
2288
|
+
if (!rematchedPoints) continue
|
|
2289
|
+
fixedViaPointsByConnectionIndex = rematchedPoints
|
|
2290
|
+
busPlans = (yield* routeAlternatives(
|
|
2291
|
+
{
|
|
2292
|
+
...routeParams,
|
|
2293
|
+
fixedViaPointsByConnectionIndex: rematchedPoints,
|
|
2294
|
+
reservedVias: getReservedVias(bus),
|
|
2295
|
+
fixedViaFallbackRouteOrderAttempts: 3,
|
|
2296
|
+
},
|
|
2297
|
+
1,
|
|
2298
|
+
))[0]
|
|
2299
|
+
if (busPlans) break
|
|
2300
|
+
}
|
|
2301
|
+
if (!busPlans) fixedViaPointsByConnectionIndex = originalPoints
|
|
2302
|
+
}
|
|
2161
2303
|
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
2162
2304
|
const lengths = busPlans.map((plan) => plan.length)
|
|
2163
2305
|
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
@@ -2503,6 +2645,50 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2503
2645
|
segment,
|
|
2504
2646
|
})),
|
|
2505
2647
|
)
|
|
2648
|
+
const boundaryBusesToMatch = useConfiguredDensePlaneRouting
|
|
2649
|
+
? boundaryBuses
|
|
2650
|
+
: []
|
|
2651
|
+
const blockingVias = useConfiguredDensePlaneRouting
|
|
2652
|
+
? []
|
|
2653
|
+
: candidatePlans.flatMap((plan) =>
|
|
2654
|
+
[
|
|
2655
|
+
plan.via,
|
|
2656
|
+
...(plan.additionalVias ?? []),
|
|
2657
|
+
plan.planeEndpointVia,
|
|
2658
|
+
]
|
|
2659
|
+
.filter((via) => via !== undefined)
|
|
2660
|
+
.map((via) => ({
|
|
2661
|
+
connectionIndex: plan.connectionIndex,
|
|
2662
|
+
center: via!.center,
|
|
2663
|
+
diameter: via!.diameter,
|
|
2664
|
+
spanLayers: via!.spanLayers,
|
|
2665
|
+
})),
|
|
2666
|
+
)
|
|
2667
|
+
// Completed boundary paths can reach their via with several source-
|
|
2668
|
+
// layer segments. Treat their actual copper as fixed obstacles instead
|
|
2669
|
+
// of reinterpreting each as a straight pad-to-via dogbone.
|
|
2670
|
+
const retainedViaPoints = useConfiguredDensePlaneRouting
|
|
2671
|
+
? null
|
|
2672
|
+
: matchComponentDogboneViaSites(planeBuses, {
|
|
2673
|
+
viaDiameter: this.config.viaDiameter,
|
|
2674
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
2675
|
+
traceWidth: this.config.traceWidth,
|
|
2676
|
+
clearance: this.config.clearance,
|
|
2677
|
+
maximumSearchStates: 1,
|
|
2678
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
2679
|
+
preferBoundaryOutwardByBusId,
|
|
2680
|
+
fixedViaPointsByConnectionIndex: new Map([
|
|
2681
|
+
...fixedViaPointsByConnectionIndex,
|
|
2682
|
+
...fixedBoundaryViaPoints,
|
|
2683
|
+
]),
|
|
2684
|
+
blockingSegments,
|
|
2685
|
+
blockingVias,
|
|
2686
|
+
additionalObstacles: denseAdditionalObstacles,
|
|
2687
|
+
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
2688
|
+
canShareCopper,
|
|
2689
|
+
})
|
|
2690
|
+
if (retainedViaPoints)
|
|
2691
|
+
return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints])
|
|
2506
2692
|
if (
|
|
2507
2693
|
useConfiguredDensePlaneRouting ||
|
|
2508
2694
|
process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
|
|
@@ -2551,7 +2737,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2551
2737
|
(planeBus) =>
|
|
2552
2738
|
!matchedPlaneBuses.includes(planeBus) &&
|
|
2553
2739
|
!matchComponentDogboneViaSites(
|
|
2554
|
-
[...matchedPlaneBuses, planeBus, ...
|
|
2740
|
+
[...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch],
|
|
2555
2741
|
{
|
|
2556
2742
|
viaDiameter: this.config.viaDiameter,
|
|
2557
2743
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
@@ -2562,6 +2748,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2562
2748
|
preferBoundaryOutwardByBusId,
|
|
2563
2749
|
fixedViaPointsByConnectionIndex: incrementalViaPoints,
|
|
2564
2750
|
blockingSegments,
|
|
2751
|
+
blockingVias,
|
|
2565
2752
|
additionalObstacles: denseAdditionalObstacles,
|
|
2566
2753
|
preferPlaneCheckerboardSites:
|
|
2567
2754
|
useConfiguredDensePlaneRouting,
|
|
@@ -3212,7 +3399,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3212
3399
|
)
|
|
3213
3400
|
}
|
|
3214
3401
|
const nextViaPoints = matchComponentDogboneViaSites(
|
|
3215
|
-
[...matchedPlaneBuses, planeBus, ...
|
|
3402
|
+
[...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch],
|
|
3216
3403
|
{
|
|
3217
3404
|
viaDiameter: this.config.viaDiameter,
|
|
3218
3405
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
@@ -3223,7 +3410,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3223
3410
|
preferBoundaryOutwardByBusId,
|
|
3224
3411
|
fixedViaPointsByConnectionIndex: incrementalViaPoints,
|
|
3225
3412
|
blockingSegments: allBlockingSegments,
|
|
3226
|
-
blockingVias: alternateBlockingVias,
|
|
3413
|
+
blockingVias: [...blockingVias, ...alternateBlockingVias],
|
|
3227
3414
|
additionalObstacles: denseAdditionalObstacles,
|
|
3228
3415
|
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
3229
3416
|
canShareCopper,
|
|
@@ -3256,8 +3443,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3256
3443
|
)
|
|
3257
3444
|
return incrementalViaPoints
|
|
3258
3445
|
}
|
|
3259
|
-
|
|
3260
|
-
[...planeBuses, ...
|
|
3446
|
+
const planeViaPoints = matchComponentDogboneViaSites(
|
|
3447
|
+
[...planeBuses, ...boundaryBusesToMatch],
|
|
3261
3448
|
{
|
|
3262
3449
|
viaDiameter: this.config.viaDiameter,
|
|
3263
3450
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
@@ -3268,11 +3455,15 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3268
3455
|
preferBoundaryOutwardByBusId,
|
|
3269
3456
|
fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
|
|
3270
3457
|
blockingSegments,
|
|
3458
|
+
blockingVias,
|
|
3271
3459
|
additionalObstacles: denseAdditionalObstacles,
|
|
3272
3460
|
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
3273
3461
|
canShareCopper,
|
|
3274
3462
|
},
|
|
3275
3463
|
)
|
|
3464
|
+
return planeViaPoints
|
|
3465
|
+
? new Map([...fixedBoundaryViaPoints, ...planeViaPoints])
|
|
3466
|
+
: null
|
|
3276
3467
|
}
|
|
3277
3468
|
debugDense("length-match:start", matchedPlans.length)
|
|
3278
3469
|
const matchedLengthResult = matchBusPlanLengths({
|
|
@@ -35,6 +35,8 @@ export interface DogboneViaSiteGeometryRules {
|
|
|
35
35
|
preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
|
|
36
36
|
/** Existing assignments that must be preserved while matching other pads. */
|
|
37
37
|
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
38
|
+
/** Prefer these provisional sites, but allow rematching when they conflict. */
|
|
39
|
+
preferredViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
38
40
|
/** Routed copper that every newly assigned through-via/dogbone must clear. */
|
|
39
41
|
blockingSegments?: readonly {
|
|
40
42
|
connectionIndex: number
|
|
@@ -506,6 +508,9 @@ function getConnectionCandidates(params: {
|
|
|
506
508
|
connection.terminationType === "boundary"
|
|
507
509
|
? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
|
|
508
510
|
: true
|
|
511
|
+
const preferredViaPoint = rules.preferredViaPointsByConnectionIndex?.get(
|
|
512
|
+
preparedConnection.connectionIndex,
|
|
513
|
+
)
|
|
509
514
|
const getPerpendicularPreferenceRank = (
|
|
510
515
|
candidate: ViaSiteCandidate,
|
|
511
516
|
): number => {
|
|
@@ -522,6 +527,10 @@ function getConnectionCandidates(params: {
|
|
|
522
527
|
}
|
|
523
528
|
return candidates.toSorted(
|
|
524
529
|
(first, second) =>
|
|
530
|
+
(preferredViaPoint
|
|
531
|
+
? Number(distance(first.point, preferredViaPoint) > EPSILON) -
|
|
532
|
+
Number(distance(second.point, preferredViaPoint) > EPSILON)
|
|
533
|
+
: 0) ||
|
|
525
534
|
(preferOutward
|
|
526
535
|
? first.outwardRank - second.outwardRank
|
|
527
536
|
: second.outwardRank - first.outwardRank) ||
|
package/lib/route-bus.ts
CHANGED
|
@@ -19,6 +19,7 @@ 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,
|
|
@@ -61,6 +62,12 @@ 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
|
|
64
71
|
/** Bounds the final fixed-via winding fallback after ordered attempts. */
|
|
65
72
|
fixedViaFallbackRouteOrderAttempts?: number
|
|
66
73
|
/** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
|
|
@@ -2444,6 +2451,9 @@ export function* routeBusAlternativesSteps(
|
|
|
2444
2451
|
fixedViaPointsByConnectionIndex,
|
|
2445
2452
|
reservedVias = [],
|
|
2446
2453
|
viaMinimalOnly = false,
|
|
2454
|
+
allowBoundarySideViaFallback = false,
|
|
2455
|
+
adaptiveWindingRouteOrder = false,
|
|
2456
|
+
alignWindingGridToPads = false,
|
|
2447
2457
|
fixedViaFallbackRouteOrderAttempts = 24,
|
|
2448
2458
|
cornerBandTargetTrackOffset,
|
|
2449
2459
|
} = params
|
|
@@ -2723,8 +2733,36 @@ export function* routeBusAlternativesSteps(
|
|
|
2723
2733
|
useViaInPad: true,
|
|
2724
2734
|
getViaHandedness: () => 0 as const,
|
|
2725
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)
|
|
2726
2764
|
const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2727
|
-
|
|
2765
|
+
coordinatedViaPoints
|
|
2728
2766
|
? [
|
|
2729
2767
|
...Array.from(
|
|
2730
2768
|
{ length: windingTargetOrderCount },
|
|
@@ -2733,9 +2771,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2733
2771
|
useViaInPad: false,
|
|
2734
2772
|
getViaHandedness: () => 0 as const,
|
|
2735
2773
|
getViaPoint: (connection: PreparedConnection) =>
|
|
2736
|
-
|
|
2737
|
-
connection.connectionIndex,
|
|
2738
|
-
)!,
|
|
2774
|
+
coordinatedViaPoints.get(connection.connectionIndex)!,
|
|
2739
2775
|
maximumRouteOrderAttempts: 1,
|
|
2740
2776
|
windingOrderIndex,
|
|
2741
2777
|
preferTargetDirectedLaneBias: true,
|
|
@@ -2746,9 +2782,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2746
2782
|
useViaInPad: false,
|
|
2747
2783
|
getViaHandedness: () => 0,
|
|
2748
2784
|
getViaPoint: (connection) =>
|
|
2749
|
-
|
|
2750
|
-
connection.connectionIndex,
|
|
2751
|
-
)!,
|
|
2785
|
+
coordinatedViaPoints.get(connection.connectionIndex)!,
|
|
2752
2786
|
// Preserve the existing bounded search after every inexpensive
|
|
2753
2787
|
// layer-interleave candidate has had one deterministic attempt.
|
|
2754
2788
|
maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
|
|
@@ -2846,9 +2880,13 @@ export function* routeBusAlternativesSteps(
|
|
|
2846
2880
|
allowBlindAndBuriedVias,
|
|
2847
2881
|
allowSameNetMerges,
|
|
2848
2882
|
maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
|
|
2883
|
+
adaptiveRouteOrder: adaptiveWindingRouteOrder,
|
|
2884
|
+
alignGridToPads:
|
|
2885
|
+
alignWindingGridToPads ||
|
|
2886
|
+
Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
|
|
2849
2887
|
reservedVias,
|
|
2850
2888
|
gridStepDivisor:
|
|
2851
|
-
|
|
2889
|
+
coordinatedViaPoints &&
|
|
2852
2890
|
Math.min(bus.pitchX, bus.pitchY) -
|
|
2853
2891
|
2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
|
|
2854
2892
|
traceWidth + clearance
|
|
@@ -2907,6 +2945,204 @@ export function* routeBusAlternativesSteps(
|
|
|
2907
2945
|
}
|
|
2908
2946
|
}
|
|
2909
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
|
+
|
|
2910
3146
|
if (viaMinimalOnly) return alternatives
|
|
2911
3147
|
|
|
2912
3148
|
const searchConnectionOrder = (
|
|
@@ -64,6 +64,12 @@ export interface RouteViaMinimalWindingParams {
|
|
|
64
64
|
gridStepDivisor?: 1 | 2
|
|
65
65
|
/** Bias bounded fixed-site searches toward the remote target band. */
|
|
66
66
|
preferTargetDirectedLaneBias?: boolean
|
|
67
|
+
/** Internal path-only mode used before a boundary-side via is appended. */
|
|
68
|
+
allowSourceLayerRouting?: boolean
|
|
69
|
+
/** Promote a blocked terminal while staying within maximumRouteOrderAttempts. */
|
|
70
|
+
adaptiveRouteOrder?: boolean
|
|
71
|
+
/** Align a fine grid with pad/interstice centers instead of the boundary. */
|
|
72
|
+
alignGridToPads?: boolean
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
export interface RouteViaMinimalWindingProgress {
|
|
@@ -493,6 +499,7 @@ function buildPlan(params: {
|
|
|
493
499
|
layer: connection.sourceLayer,
|
|
494
500
|
}
|
|
495
501
|
const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON
|
|
502
|
+
const changesLayer = connection.sourceLayer !== targetLayer
|
|
496
503
|
const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
|
|
497
504
|
const spanLayers = getViaSpanLayers({
|
|
498
505
|
fromLayer: connection.sourceLayer,
|
|
@@ -528,20 +535,24 @@ function buildPlan(params: {
|
|
|
528
535
|
},
|
|
529
536
|
]
|
|
530
537
|
: []),
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
538
|
+
...(changesLayer
|
|
539
|
+
? [
|
|
540
|
+
{
|
|
541
|
+
route_type: "via" as const,
|
|
542
|
+
...terminal.viaPoint,
|
|
543
|
+
from_layer: connection.sourceLayer,
|
|
544
|
+
to_layer: targetLayer,
|
|
545
|
+
via_diameter: viaDiameter,
|
|
546
|
+
via_hole_diameter: viaHoleDiameter,
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
route_type: "wire" as const,
|
|
550
|
+
...terminal.viaPoint,
|
|
551
|
+
width: traceWidth,
|
|
552
|
+
layer: targetLayer,
|
|
553
|
+
},
|
|
554
|
+
]
|
|
555
|
+
: []),
|
|
545
556
|
...targetLayerPoints.slice(1).map((point) => ({
|
|
546
557
|
route_type: "wire" as const,
|
|
547
558
|
...point,
|
|
@@ -589,7 +600,7 @@ function buildPlan(params: {
|
|
|
589
600
|
route,
|
|
590
601
|
},
|
|
591
602
|
segments,
|
|
592
|
-
via,
|
|
603
|
+
via: changesLayer ? via : undefined,
|
|
593
604
|
length: segments.reduce(
|
|
594
605
|
(total, segment) => total + distance(segment.start, segment.end),
|
|
595
606
|
0,
|
|
@@ -624,6 +635,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
624
635
|
reservedVias = [],
|
|
625
636
|
gridStepDivisor = 1,
|
|
626
637
|
preferTargetDirectedLaneBias = false,
|
|
638
|
+
allowSourceLayerRouting = false,
|
|
639
|
+
adaptiveRouteOrder = false,
|
|
640
|
+
alignGridToPads = false,
|
|
627
641
|
} = params
|
|
628
642
|
if (
|
|
629
643
|
maximumRouteOrderAttempts !== undefined &&
|
|
@@ -643,18 +657,33 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
643
657
|
if (
|
|
644
658
|
terminals.length === 0 ||
|
|
645
659
|
!bus.exitEdge ||
|
|
646
|
-
|
|
647
|
-
(
|
|
648
|
-
|
|
660
|
+
(!allowSourceLayerRouting &&
|
|
661
|
+
terminals.some(
|
|
662
|
+
(terminal) => terminal.connection.sourceLayer === targetLayer,
|
|
663
|
+
))
|
|
649
664
|
) {
|
|
650
665
|
return []
|
|
651
666
|
}
|
|
652
667
|
|
|
653
|
-
const
|
|
668
|
+
const baseGridStep = (traceWidth + clearance) / gridStepDivisor
|
|
669
|
+
const pitch = Math.min(bus.pitchX, bus.pitchY)
|
|
670
|
+
const alignGridToPitch =
|
|
671
|
+
alignGridToPads && gridStepDivisor === 2 && Number.isFinite(pitch)
|
|
672
|
+
const gridStep = alignGridToPitch
|
|
673
|
+
? pitch / (2 * Math.ceil(pitch / (2 * baseGridStep)))
|
|
674
|
+
: baseGridStep
|
|
654
675
|
if (!Number.isFinite(gridStep) || gridStep <= 0) return []
|
|
655
676
|
const { minX, maxX, minY, maxY } = bus.sharedBoundary
|
|
656
|
-
const
|
|
657
|
-
const
|
|
677
|
+
const originX = bus.xCoordinates[0] ?? minX
|
|
678
|
+
const originY = bus.yCoordinates[0] ?? minY
|
|
679
|
+
const gridMinX = alignGridToPitch
|
|
680
|
+
? originX + Math.ceil((minX - originX) / gridStep) * gridStep
|
|
681
|
+
: minX
|
|
682
|
+
const gridMinY = alignGridToPitch
|
|
683
|
+
? originY + Math.ceil((minY - originY) / gridStep) * gridStep
|
|
684
|
+
: minY
|
|
685
|
+
const columnCount = Math.floor((maxX - gridMinX) / gridStep) + 1
|
|
686
|
+
const rowCount = Math.floor((maxY - gridMinY) / gridStep) + 1
|
|
658
687
|
const nodeCount = columnCount * rowCount
|
|
659
688
|
if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
|
|
660
689
|
return []
|
|
@@ -665,7 +694,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
665
694
|
return {
|
|
666
695
|
column,
|
|
667
696
|
row,
|
|
668
|
-
point: { x:
|
|
697
|
+
point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
|
|
669
698
|
}
|
|
670
699
|
})
|
|
671
700
|
const sampledGridPoints = includeVisualization
|
|
@@ -797,6 +826,18 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
797
826
|
}
|
|
798
827
|
for (const blocker of acceptedAttemptSegments) {
|
|
799
828
|
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
829
|
+
const margin = (segment.width + blocker.segment.width) / 2 + clearance
|
|
830
|
+
if (
|
|
831
|
+
Math.max(segment.start.x, segment.end.x) + margin <
|
|
832
|
+
Math.min(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
833
|
+
Math.min(segment.start.x, segment.end.x) - margin >
|
|
834
|
+
Math.max(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
835
|
+
Math.max(segment.start.y, segment.end.y) + margin <
|
|
836
|
+
Math.min(blocker.segment.start.y, blocker.segment.end.y) ||
|
|
837
|
+
Math.min(segment.start.y, segment.end.y) - margin >
|
|
838
|
+
Math.max(blocker.segment.start.y, blocker.segment.end.y)
|
|
839
|
+
)
|
|
840
|
+
continue
|
|
800
841
|
if (
|
|
801
842
|
distanceSegmentToSegment(
|
|
802
843
|
segment.start,
|
|
@@ -1283,6 +1324,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1283
1324
|
const initialRouteOrderFactories: Array<
|
|
1284
1325
|
() => readonly ViaMinimalWindingTerminal[]
|
|
1285
1326
|
> = []
|
|
1327
|
+
if (alignGridToPads && preferTargetDirectedLaneBias && viasAreBeforeTargets) {
|
|
1328
|
+
initialRouteOrderFactories.push(() => targetOrderedTerminals)
|
|
1329
|
+
}
|
|
1286
1330
|
if (viasAreBeforeTargets) {
|
|
1287
1331
|
initialRouteOrderFactories.push(() => [
|
|
1288
1332
|
...targetOrderedTerminals.slice(1),
|
|
@@ -1340,11 +1384,28 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1340
1384
|
getItemKey: (terminal) => terminal.connection.connection.name,
|
|
1341
1385
|
maximumOrderCount: maximumRouteOrderCount,
|
|
1342
1386
|
})
|
|
1387
|
+
const pendingRouteOrders: ViaMinimalWindingTerminal[][] = []
|
|
1388
|
+
const seenAdaptiveOrders = new Set<string>()
|
|
1389
|
+
const adaptiveRouteOrders = function* () {
|
|
1390
|
+
while (true) {
|
|
1391
|
+
const pending = pendingRouteOrders.shift()
|
|
1392
|
+
const next = pending
|
|
1393
|
+
? { done: false, value: pending }
|
|
1394
|
+
: routeOrders.next()
|
|
1395
|
+
if (next.done) return
|
|
1396
|
+
const key = next.value
|
|
1397
|
+
.map((terminal) => terminal.connection.connection.name)
|
|
1398
|
+
.join("|")
|
|
1399
|
+
if (seenAdaptiveOrders.has(key)) continue
|
|
1400
|
+
seenAdaptiveOrders.add(key)
|
|
1401
|
+
yield next.value
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1343
1404
|
|
|
1344
1405
|
const alternatives: FanoutRoutePlan[][] = []
|
|
1345
1406
|
const seenAlternativeKeys = new Set<string>()
|
|
1346
1407
|
let routeOrderAttemptCount = 0
|
|
1347
|
-
for (const routeOrder of
|
|
1408
|
+
for (const routeOrder of adaptiveRouteOrders()) {
|
|
1348
1409
|
for (const laneBias of laneBiases) {
|
|
1349
1410
|
if (
|
|
1350
1411
|
maximumRouteOrderAttempts !== undefined &&
|
|
@@ -1415,6 +1476,16 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1415
1476
|
: {}),
|
|
1416
1477
|
}
|
|
1417
1478
|
if (!points) {
|
|
1479
|
+
if (
|
|
1480
|
+
adaptiveRouteOrder &&
|
|
1481
|
+
maximumRouteOrderAttempts !== undefined &&
|
|
1482
|
+
terminalIndex > 0
|
|
1483
|
+
) {
|
|
1484
|
+
const others = routeOrder.filter(
|
|
1485
|
+
(candidate) => candidate !== terminal,
|
|
1486
|
+
)
|
|
1487
|
+
pendingRouteOrders.push([terminal, ...others])
|
|
1488
|
+
}
|
|
1418
1489
|
failed = true
|
|
1419
1490
|
break
|
|
1420
1491
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fanout-solver",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
|
|
5
5
|
"module": "lib/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test": "bun test",
|
|
33
33
|
"benchmark": "bun benchmarks/run-benchmark.ts",
|
|
34
34
|
"generate:repro04": "bun scripts/generate-repro/generate-repro04.tsx",
|
|
35
|
-
"benchmark:srj29": "
|
|
35
|
+
"benchmark:srj29": "bun benchmarks/run-srj29-benchmark.ts",
|
|
36
36
|
"render:dataset": "bun scripts/render-dataset-pngs.ts",
|
|
37
37
|
"format": "biome format --write .",
|
|
38
38
|
"format:check": "biome format ."
|