@tscircuit/fanout-solver 0.0.52 → 0.0.54
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 +35 -70
- package/lib/fanout-solver.ts +141 -25
- package/lib/match-bus-lengths.ts +106 -0
- package/lib/route-bus.ts +87 -26
- package/lib/route-via-minimal-winding.ts +22 -15
- package/lib/shorten-bus-plans.ts +75 -0
- package/lib/types.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ and treats each bus-layer decision atomically.
|
|
|
16
16
|
- Detects rectangular pad footprints through obstacle `componentId` metadata,
|
|
17
17
|
including perimeter packages and two-pad passives.
|
|
18
18
|
- Handles multiple mixed footprints inside one shared breakout boundary.
|
|
19
|
-
- Routes perimeter and inner-matrix pads
|
|
19
|
+
- Routes perimeter and inner-matrix pads.
|
|
20
20
|
- Uses `sharedBoundary` as the common exit rectangle. Without one, it infers a
|
|
21
21
|
shared rectangle around the source footprints selected for the buses, without
|
|
22
22
|
expanding that boundary to include destination footprints.
|
|
@@ -280,20 +280,32 @@ 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
|
-
##
|
|
283
|
+
## Dataset 31 benchmark
|
|
284
284
|
|
|
285
|
-
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark **only the 12 AM62L
|
|
286
|
+
directional cases** from
|
|
287
|
+
[`tscircuit/dataset-fanout31-am62l`](https://github.com/tscircuit/dataset-fanout31-am62l).
|
|
288
|
+
The upstream revision is pinned in `scripts/generate-repro/package.json` and
|
|
289
|
+
recorded in every report. Other datasets remain available for regression tests
|
|
290
|
+
and the debugger, but have no benchmark commands or workflows.
|
|
288
291
|
|
|
289
292
|
```sh
|
|
290
293
|
./benchmark.sh
|
|
291
294
|
./benchmark.sh --list
|
|
292
|
-
./benchmark.sh --
|
|
293
|
-
./benchmark.sh --dataset srj19,srj29 --sample sample001
|
|
295
|
+
./benchmark.sh --sample 11-left-center
|
|
294
296
|
./benchmark.sh --concurrency 8 --sample-timeout-seconds 300
|
|
295
297
|
```
|
|
296
298
|
|
|
299
|
+
Before timing the solver, the benchmark renders the selected upstream TSX/core
|
|
300
|
+
circuits and captures their exact fanout-solver constructor inputs into
|
|
301
|
+
`benchmark-results/inputs/<sample-id>.json`. Each case retains all 135 AM62L
|
|
302
|
+
connections, 573 pad obstacles, nine DDR buses, 102 plane drops, and the original
|
|
303
|
+
clearance, differential-pair, and length-skew constraints. The timed workers run
|
|
304
|
+
**this checkout's solver**, not the upstream package's released solver.
|
|
305
|
+
To capture the inputs without solving, use `bun run generate:dataset31`.
|
|
306
|
+
The optional `--dataset dataset31` flag is accepted for explicit CI invocation;
|
|
307
|
+
other dataset selections are rejected.
|
|
308
|
+
|
|
297
309
|
Each sample runs in an isolated process, with up to four concurrent processes
|
|
298
310
|
locally and a **120-second hard timeout** by default. A synchronous solver hang,
|
|
299
311
|
exception, or unsolved case does not prevent later samples from running.
|
|
@@ -301,80 +313,33 @@ Assignment budgets and circuit constraints remain at each sample's defaults;
|
|
|
301
313
|
`--max-layer-combinations` explicitly overrides only the search budget.
|
|
302
314
|
|
|
303
315
|
The ordered `benchmark-results/benchmark.json` and `benchmark.md` reports contain
|
|
304
|
-
the commit, configuration,
|
|
305
|
-
timing, and partial routing/validation counts. Reports are saved after
|
|
306
|
-
completed sample, including the total selected count to identify incomplete runs.
|
|
316
|
+
the solver commit, dataset revision, configuration, solve totals, every sample's
|
|
317
|
+
status and timing, and partial routing/validation counts. Reports are saved after
|
|
318
|
+
every completed sample, including the total selected count to identify incomplete runs.
|
|
307
319
|
Timed-out workers do not retain their in-flight routing counts.
|
|
308
320
|
Compare reports with the same budgets to track progress. Solved means validated
|
|
309
|
-
fanout, not downstream inter-chip routing
|
|
310
|
-
|
|
311
|
-
are
|
|
312
|
-
command failures (nonzero exit).
|
|
321
|
+
AM62L fanout, not RAM fanout or downstream inter-chip routing. Partial, error,
|
|
322
|
+
and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
|
323
|
+
I/O failures are command failures (nonzero exit).
|
|
313
324
|
|
|
314
325
|
### PR comment trigger
|
|
315
326
|
|
|
316
327
|
Once `.github/workflows/benchmark.yml` is on the default branch, a repository
|
|
317
328
|
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**
|
|
319
|
-
then updates a status comment with solve totals, per-sample results,
|
|
320
|
-
to the complete JSON/Markdown
|
|
321
|
-
run, optionally supplying an open PR number. No custom
|
|
329
|
+
PR's exact head SHA, runs all 12 dataset 31 samples on a **32-vCPU Blacksmith ARM**
|
|
330
|
+
runner, then updates a status comment with solve totals, per-sample results,
|
|
331
|
+
and a link to the complete JSON/Markdown reports and captured inputs. The Actions
|
|
332
|
+
UI also supports a manual run, optionally supplying an open PR number. No custom
|
|
333
|
+
bot token is required.
|
|
322
334
|
|
|
323
335
|
The runner defaults to 32 processes and a 120-second per-sample deadline; set
|
|
324
336
|
repository variables `BENCHMARK_CONCURRENCY` and
|
|
325
337
|
`BENCHMARK_SAMPLE_TIMEOUT_SECONDS` to change these. PR code runs with a read-only
|
|
326
338
|
token and no persisted checkout credentials. A separate job uses the trusted
|
|
327
339
|
workflow revision to validate report data and post comments; it never executes
|
|
328
|
-
PR code.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
## SRJ29 benchmark
|
|
332
|
-
|
|
333
|
-
The repository loads all 200 samples from the derivative
|
|
334
|
-
[`tscircuit/dataset-srj29-bga-decoupling`](https://github.com/tscircuit/dataset-srj29-bga-decoupling)
|
|
335
|
-
as a pinned development dependency. The adapter keeps the complete obstacle
|
|
336
|
-
field, including opposite-layer capacitor pads and bodies. VCC and GND are
|
|
337
|
-
grouped onto opposite boundary corridors, while the capacitor pad remains the
|
|
338
|
-
downstream endpoint of every power connection; a local capacitor or plane via
|
|
339
|
-
alone cannot count as a solved BGA pin. Remaining edge signals are grouped by
|
|
340
|
-
direction. Every adapted problem uses the same six-layer stackup (`top`,
|
|
341
|
-
`inner1` through `inner4`, and `bottom`) so benchmark improvements are directly
|
|
342
|
-
comparable.
|
|
343
|
-
|
|
344
|
-
The dedicated legacy SRJ29 report remains available with:
|
|
345
|
-
|
|
346
|
-
```sh
|
|
347
|
-
bun run benchmark:srj29
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Use `--sample sample001`, `--limit 10`, or
|
|
351
|
-
`--max-layer-combinations 16` for shorter runs. Samples run one at a time by
|
|
352
|
-
default and print progress as they finish. `--concurrency 8` runs isolated
|
|
353
|
-
samples in parallel, and `--sample-timeout-seconds 600` prevents a difficult
|
|
354
|
-
sample from blocking the remaining work. Each run writes the full ordered
|
|
355
|
-
results to `benchmark-results/srj29.json` and
|
|
356
|
-
`benchmark-results/srj29.md`. A row is marked solved only when every input
|
|
357
|
-
connection has a validated breakout and an independent physical-copper audit
|
|
358
|
-
proves that the emitted wires, vias, and same-net pads connect every original
|
|
359
|
-
endpoint on compatible layers. A second independent audit checks every emitted
|
|
360
|
-
trace and via against different-net pads, obstacles, traces, and vias on every
|
|
361
|
-
physical layer in its span. Reaching an arbitrary boundary point, retaining an
|
|
362
|
-
unrouted endpoint in the output JSON, or emitting copper with a DRC violation
|
|
363
|
-
does not count. The report separates fanout-prefix completion from physically
|
|
364
|
-
connected original connections so partial progress remains visible without
|
|
365
|
-
overstating it as a solution. Partial solutions are reported as benchmark
|
|
366
|
-
results instead of failing the command. `bun run benchmark:srj29` is an alias
|
|
367
|
-
for the same command.
|
|
368
|
-
|
|
369
|
-
The `SRJ29 Benchmark` GitHub Actions workflow runs the complete dataset on a
|
|
370
|
-
Blacksmith 32-vCPU ARM runner with 32 sample processes by default. It can be
|
|
371
|
-
started manually with an optional sample id, or for a pull request by adding
|
|
372
|
-
`[BENCHMARK TEST]` to its title. The workflow publishes the Markdown summary and
|
|
373
|
-
uploads both reports as an artifact.
|
|
374
|
-
|
|
375
|
-
Run `bun run start` and inspect the SRJ29 fixtures to step through the selected
|
|
376
|
-
sample. The derivative dataset also publishes dedicated Cosmos pages for the
|
|
377
|
-
first ten samples.
|
|
340
|
+
PR code. The trusted renderer rejects legacy or mixed-dataset reports, so PR
|
|
341
|
+
comments contain only dataset 31 results. Only exact commands from non-bot users
|
|
342
|
+
with current write, maintain, or admin access are accepted.
|
|
378
343
|
|
|
379
344
|
## Dataset 02
|
|
380
345
|
|
|
@@ -527,8 +492,8 @@ bun run render:dataset
|
|
|
527
492
|
bun run start
|
|
528
493
|
```
|
|
529
494
|
|
|
530
|
-
The benchmark runs
|
|
531
|
-
|
|
495
|
+
The benchmark runs only the 12 dataset 31 AM62L cases and reports solve counts,
|
|
496
|
+
validation, and timing. `bun run start` opens all regression
|
|
532
497
|
datasets in the standard tscircuit solver debugger. `bun run
|
|
533
498
|
render:dataset` writes `graphics-debug` PNGs under one subdirectory per dataset,
|
|
534
499
|
with a red shared boundary, gray component courtyards, and green fanout-exit
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
2
|
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
3
|
+
import { shortenBusPlans } from "./shorten-bus-plans"
|
|
3
4
|
import { type GraphicsObject, mergeGraphics } from "graphics-debug"
|
|
4
5
|
import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
|
|
5
6
|
import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
|
|
@@ -1380,8 +1381,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1380
1381
|
private *routeDenseThroughAllMixedTerminationSteps(params: {
|
|
1381
1382
|
busLayerAssignments: Readonly<Record<string, string>>
|
|
1382
1383
|
busesInRoutingOrder: readonly PreparedBus[]
|
|
1384
|
+
denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
|
|
1385
|
+
lengthMatchingStage?: "before-planes" | "after-planes"
|
|
1383
1386
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1384
1387
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1388
|
+
const usePadAlignedDenseRouting =
|
|
1389
|
+
params.denseRoutingStrategy !== "boundary-aligned"
|
|
1385
1390
|
const debugDense = (...values: unknown[]) => {
|
|
1386
1391
|
if (process.env.FANOUT_DEBUG_DENSE === "1") {
|
|
1387
1392
|
if (
|
|
@@ -1414,6 +1419,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1414
1419
|
const useConfiguredDensePlaneRouting =
|
|
1415
1420
|
this.config.densePlaneReservationBusIds.length > 0 ||
|
|
1416
1421
|
this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
|
|
1422
|
+
const matchLengthsAfterPlanes =
|
|
1423
|
+
useConfiguredDensePlaneRouting &&
|
|
1424
|
+
params.lengthMatchingStage !== "before-planes"
|
|
1417
1425
|
const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
|
|
1418
1426
|
unsortedBoundaryBuses.map((bus) => bus.connections.length),
|
|
1419
1427
|
)
|
|
@@ -1902,6 +1910,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1902
1910
|
...singletonDeferralCandidates.filter((bus) => {
|
|
1903
1911
|
const containingBus = getContainingWideSourceField(bus)
|
|
1904
1912
|
const sharesContainingBusLayer =
|
|
1913
|
+
usePadAlignedDenseRouting &&
|
|
1905
1914
|
!useConfiguredDensePlaneRouting &&
|
|
1906
1915
|
containingBus &&
|
|
1907
1916
|
params.busLayerAssignments[containingBus.busId] ===
|
|
@@ -2160,9 +2169,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2160
2169
|
fixedViaPointsByConnectionIndex,
|
|
2161
2170
|
reservedVias: getReservedVias(bus),
|
|
2162
2171
|
viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
|
|
2163
|
-
allowBoundarySideViaFallback:
|
|
2172
|
+
allowBoundarySideViaFallback: true,
|
|
2173
|
+
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2164
2174
|
adaptiveWindingRouteOrder,
|
|
2165
|
-
alignWindingGridToPads:
|
|
2175
|
+
alignWindingGridToPads:
|
|
2176
|
+
usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting,
|
|
2166
2177
|
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
|
|
2167
2178
|
? 60
|
|
2168
2179
|
: useConfiguredDensePlaneRouting
|
|
@@ -2218,7 +2229,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2218
2229
|
if (!busPlans && preferSingleLayerWinding) {
|
|
2219
2230
|
busPlans = (yield* routeAlternatives(routeParams, 1))[0]
|
|
2220
2231
|
}
|
|
2221
|
-
if (!busPlans
|
|
2232
|
+
if (!busPlans) {
|
|
2222
2233
|
const originalPoints = fixedViaPointsByConnectionIndex
|
|
2223
2234
|
const originalOutward =
|
|
2224
2235
|
preferBoundaryOutwardByBusId.get(bus.busId) ?? true
|
|
@@ -2292,6 +2303,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2292
2303
|
...routeParams,
|
|
2293
2304
|
fixedViaPointsByConnectionIndex: rematchedPoints,
|
|
2294
2305
|
reservedVias: getReservedVias(bus),
|
|
2306
|
+
alignWindingGridToPads: useConfiguredDensePlaneRouting,
|
|
2295
2307
|
fixedViaFallbackRouteOrderAttempts: 3,
|
|
2296
2308
|
},
|
|
2297
2309
|
1,
|
|
@@ -2313,7 +2325,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2313
2325
|
// Only pay for additional A* variants when the first topology is so
|
|
2314
2326
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2315
2327
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2316
|
-
if (needsRouteDiversity) {
|
|
2328
|
+
if (needsRouteDiversity && !matchLengthsAfterPlanes) {
|
|
2317
2329
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2318
2330
|
(first, second) => {
|
|
2319
2331
|
const firstLengths = first.map((plan) => plan.length)
|
|
@@ -2630,6 +2642,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2630
2642
|
let feasibleAlternatePlanePlans: FanoutRoutePlan[] = []
|
|
2631
2643
|
const matchViaPointsAroundPlans = (
|
|
2632
2644
|
candidatePlans: readonly FanoutRoutePlan[],
|
|
2645
|
+
promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
|
|
2633
2646
|
): Map<number, { x: number; y: number }> | null => {
|
|
2634
2647
|
feasibleAlternatePlanePlans = []
|
|
2635
2648
|
const fixedBoundaryViaPoints = new Map(
|
|
@@ -2645,10 +2658,18 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2645
2658
|
segment,
|
|
2646
2659
|
})),
|
|
2647
2660
|
)
|
|
2648
|
-
const
|
|
2661
|
+
const preserveBoundaryCopper =
|
|
2662
|
+
!useConfiguredDensePlaneRouting ||
|
|
2663
|
+
candidatePlans.some(
|
|
2664
|
+
(plan) =>
|
|
2665
|
+
plan.segments.filter(
|
|
2666
|
+
(segment) => segment.layer === plan.sourceLayer,
|
|
2667
|
+
).length > 1,
|
|
2668
|
+
)
|
|
2669
|
+
const boundaryBusesToMatch = !preserveBoundaryCopper
|
|
2649
2670
|
? boundaryBuses
|
|
2650
2671
|
: []
|
|
2651
|
-
const blockingVias =
|
|
2672
|
+
const blockingVias = !preserveBoundaryCopper
|
|
2652
2673
|
? []
|
|
2653
2674
|
: candidatePlans.flatMap((plan) =>
|
|
2654
2675
|
[
|
|
@@ -2667,7 +2688,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2667
2688
|
// Completed boundary paths can reach their via with several source-
|
|
2668
2689
|
// layer segments. Treat their actual copper as fixed obstacles instead
|
|
2669
2690
|
// of reinterpreting each as a straight pad-to-via dogbone.
|
|
2670
|
-
const retainedViaPoints =
|
|
2691
|
+
const retainedViaPoints = !preserveBoundaryCopper
|
|
2671
2692
|
? null
|
|
2672
2693
|
: matchComponentDogboneViaSites(planeBuses, {
|
|
2673
2694
|
viaDiameter: this.config.viaDiameter,
|
|
@@ -2775,6 +2796,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2775
2796
|
)
|
|
2776
2797
|
: []
|
|
2777
2798
|
const additionalAlternatePlaneBusIds = new Set([
|
|
2799
|
+
...promotedAlternatePlaneBusIds,
|
|
2778
2800
|
...this.config.denseUnrestrictedPlaneRoutingBusIds,
|
|
2779
2801
|
...(process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(
|
|
2780
2802
|
",",
|
|
@@ -3345,7 +3367,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3345
3367
|
)[0]
|
|
3346
3368
|
if (!promotedPlans) {
|
|
3347
3369
|
debugDense("plane-route:promote-failed", planeBus.busId)
|
|
3348
|
-
|
|
3370
|
+
// Let these newly blocked drops participate in the joint
|
|
3371
|
+
// choice. Each retry adds previously excluded plane buses,
|
|
3372
|
+
// so the recursion is bounded by the number of plane buses.
|
|
3373
|
+
return matchViaPointsAroundPlans(
|
|
3374
|
+
candidatePlans,
|
|
3375
|
+
new Set([
|
|
3376
|
+
...promotedAlternatePlaneBusIds,
|
|
3377
|
+
...zeroCandidatePlaneBuses.map((bus) => bus.busId),
|
|
3378
|
+
]),
|
|
3379
|
+
)
|
|
3349
3380
|
}
|
|
3350
3381
|
feasibleAlternatePlanePlans.push(...promotedPlans)
|
|
3351
3382
|
}
|
|
@@ -3466,22 +3497,25 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3466
3497
|
: null
|
|
3467
3498
|
}
|
|
3468
3499
|
debugDense("length-match:start", matchedPlans.length)
|
|
3469
|
-
const matchedLengthResult =
|
|
3470
|
-
plans: matchedPlans
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3500
|
+
const matchedLengthResult = matchLengthsAfterPlanes
|
|
3501
|
+
? { plans: matchedPlans }
|
|
3502
|
+
: matchBusPlanLengths({
|
|
3503
|
+
plans: matchedPlans,
|
|
3504
|
+
preparedBuses: this.preparedBuses,
|
|
3505
|
+
inputSrj: this.inputSrj,
|
|
3506
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3507
|
+
clearance: this.config.clearance,
|
|
3508
|
+
allowBlindAndBuriedVias: false,
|
|
3509
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3510
|
+
allowMatchingInsideDenseBounds: true,
|
|
3511
|
+
candidatePlansAreFeasible: (candidatePlans) => {
|
|
3512
|
+
const candidateViaPoints =
|
|
3513
|
+
matchViaPointsAroundPlans(candidatePlans)
|
|
3514
|
+
if (!candidateViaPoints) return false
|
|
3515
|
+
feasibleViaPoints = candidateViaPoints
|
|
3516
|
+
return true
|
|
3517
|
+
},
|
|
3518
|
+
})
|
|
3485
3519
|
debugDense(
|
|
3486
3520
|
"length-match:complete",
|
|
3487
3521
|
matchedLengthResult.plans?.length ?? "failed",
|
|
@@ -3565,6 +3599,65 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3565
3599
|
yield
|
|
3566
3600
|
}
|
|
3567
3601
|
}
|
|
3602
|
+
if (matchedRoutingSucceeded && matchLengthsAfterPlanes) {
|
|
3603
|
+
// With the configured plane escape strategy, route those dogbones
|
|
3604
|
+
// before tuning. Length matching can then check the actual complete
|
|
3605
|
+
// copper instead of repeatedly searching for a new plane assignment
|
|
3606
|
+
// for every prospective meander.
|
|
3607
|
+
const lengthMatchingParams = {
|
|
3608
|
+
plans: matchedPlans,
|
|
3609
|
+
preparedBuses: this.preparedBuses,
|
|
3610
|
+
inputSrj: this.inputSrj,
|
|
3611
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3612
|
+
clearance: this.config.clearance,
|
|
3613
|
+
allowBlindAndBuriedVias: false,
|
|
3614
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3615
|
+
allowMatchingInsideDenseBounds: true,
|
|
3616
|
+
allowPairLaneSpreading: true,
|
|
3617
|
+
}
|
|
3618
|
+
let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
|
|
3619
|
+
const shortenedBusIds = new Set<string>()
|
|
3620
|
+
while (
|
|
3621
|
+
!matchedLengthResult.plans &&
|
|
3622
|
+
matchedLengthResult.failedBus &&
|
|
3623
|
+
!shortenedBusIds.has(matchedLengthResult.failedBus.busId)
|
|
3624
|
+
) {
|
|
3625
|
+
const bus = matchedLengthResult.failedBus
|
|
3626
|
+
shortenedBusIds.add(bus.busId)
|
|
3627
|
+
const shortened = shortenBusPlans({
|
|
3628
|
+
plans: matchedPlans,
|
|
3629
|
+
bus,
|
|
3630
|
+
srj: this.inputSrj,
|
|
3631
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3632
|
+
layerNames: this.config.layerNames,
|
|
3633
|
+
traceWidth: this.config.traceWidth,
|
|
3634
|
+
viaDiameter: this.config.viaDiameter,
|
|
3635
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
3636
|
+
clearance: this.config.clearance,
|
|
3637
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3638
|
+
})
|
|
3639
|
+
if (shortened.every((plan, index) => plan === matchedPlans[index]))
|
|
3640
|
+
break
|
|
3641
|
+
matchedPlans = shortened
|
|
3642
|
+
matchedLengthResult = matchBusPlanLengths({
|
|
3643
|
+
...lengthMatchingParams,
|
|
3644
|
+
plans: matchedPlans,
|
|
3645
|
+
})
|
|
3646
|
+
}
|
|
3647
|
+
if (matchedLengthResult.plans) {
|
|
3648
|
+
matchedPlans = matchedLengthResult.plans
|
|
3649
|
+
} else {
|
|
3650
|
+
matchedRoutingSucceeded = false
|
|
3651
|
+
}
|
|
3652
|
+
this.setInProgressPlans({
|
|
3653
|
+
phase: "match-dense-complete-lengths",
|
|
3654
|
+
plans: matchedPlans,
|
|
3655
|
+
strategy: "default",
|
|
3656
|
+
unitIndex: ++denseWorkUnitIndex,
|
|
3657
|
+
unitCount: denseWorkUnitCount,
|
|
3658
|
+
})
|
|
3659
|
+
yield
|
|
3660
|
+
}
|
|
3568
3661
|
const densePlansAreClear =
|
|
3569
3662
|
matchedRoutingSucceeded &&
|
|
3570
3663
|
fanoutPlansAreClear({
|
|
@@ -3586,7 +3679,30 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3586
3679
|
}
|
|
3587
3680
|
}
|
|
3588
3681
|
|
|
3589
|
-
if (
|
|
3682
|
+
if (matchLengthsAfterPlanes) {
|
|
3683
|
+
return yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3684
|
+
...params,
|
|
3685
|
+
lengthMatchingStage: "before-planes",
|
|
3686
|
+
})
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// Pad-aligned windings can fence off a same-layer singleton even when
|
|
3690
|
+
// every wide bus routes successfully. Before widening the search, retry
|
|
3691
|
+
// the coordinated reservation with a boundary-aligned grid and let those
|
|
3692
|
+
// singleton sites remain provisional until surrounding copper is fixed.
|
|
3693
|
+
if (usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting) {
|
|
3694
|
+
const boundaryAlignedState =
|
|
3695
|
+
yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3696
|
+
...params,
|
|
3697
|
+
denseRoutingStrategy: "boundary-aligned",
|
|
3698
|
+
})
|
|
3699
|
+
if (boundaryAlignedState) return boundaryAlignedState
|
|
3700
|
+
}
|
|
3701
|
+
if (
|
|
3702
|
+
!usePadAlignedDenseRouting ||
|
|
3703
|
+
process.env.FANOUT_DEBUG_DENSE_ONLY === "1"
|
|
3704
|
+
)
|
|
3705
|
+
return null
|
|
3590
3706
|
|
|
3591
3707
|
const maximumStates = 8
|
|
3592
3708
|
const getBoundaryStates = (
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -565,6 +565,67 @@ function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
|
|
|
565
565
|
return Math.max(...lengths) - Math.min(...lengths)
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
function* createSpreadLaneCandidates(
|
|
569
|
+
plan: FanoutRoutePlan,
|
|
570
|
+
clearance: number,
|
|
571
|
+
): Generator<FanoutRoutePlan> {
|
|
572
|
+
for (const { segment, index } of plan.segments
|
|
573
|
+
.map((segment, index) => ({ segment, index }))
|
|
574
|
+
.filter(({ segment }) => segment.layer === plan.targetLayer)
|
|
575
|
+
.toSorted(
|
|
576
|
+
(a, b) =>
|
|
577
|
+
distance(b.segment.start, b.segment.end) -
|
|
578
|
+
distance(a.segment.start, a.segment.end),
|
|
579
|
+
)) {
|
|
580
|
+
const length = distance(segment.start, segment.end)
|
|
581
|
+
if (length <= EPSILON) continue
|
|
582
|
+
const dx = Math.abs(segment.end.x - segment.start.x)
|
|
583
|
+
const dy = Math.abs(segment.end.y - segment.start.y)
|
|
584
|
+
if (dx > EPSILON && dy > EPSILON && Math.abs(dx - dy) > EPSILON) continue
|
|
585
|
+
const tangent = {
|
|
586
|
+
x: (segment.end.x - segment.start.x) / length,
|
|
587
|
+
y: (segment.end.y - segment.start.y) / length,
|
|
588
|
+
}
|
|
589
|
+
const pitch = segment.width + clearance
|
|
590
|
+
for (const multiple of [2, 3, 4, 6]) {
|
|
591
|
+
const offset = pitch * multiple
|
|
592
|
+
if (length < 2 * offset + pitch) continue
|
|
593
|
+
for (const sign of [1, -1]) {
|
|
594
|
+
const normal = { x: -tangent.y * sign, y: tangent.x * sign }
|
|
595
|
+
const points = [
|
|
596
|
+
segment.start,
|
|
597
|
+
addScaled(addScaled(segment.start, tangent, offset), normal, offset),
|
|
598
|
+
addScaled(addScaled(segment.end, tangent, -offset), normal, offset),
|
|
599
|
+
segment.end,
|
|
600
|
+
]
|
|
601
|
+
const replacement = points.slice(1).map((end, i) => ({
|
|
602
|
+
...segment,
|
|
603
|
+
start: points[i]!,
|
|
604
|
+
end,
|
|
605
|
+
}))
|
|
606
|
+
const segments = [
|
|
607
|
+
...plan.segments.slice(0, index),
|
|
608
|
+
...replacement,
|
|
609
|
+
...plan.segments.slice(index + 1),
|
|
610
|
+
]
|
|
611
|
+
if (hasNonAdjacentSelfIntersection(segments)) continue
|
|
612
|
+
if (
|
|
613
|
+
!replacementCopperIsSelfClear({
|
|
614
|
+
plan,
|
|
615
|
+
segments,
|
|
616
|
+
replacementStartIndex: index,
|
|
617
|
+
replacementSegmentCount: replacement.length,
|
|
618
|
+
clearance,
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
continue
|
|
622
|
+
const candidate = createPlanWithSegments(plan, segments)
|
|
623
|
+
if (candidate) yield candidate
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
568
629
|
/**
|
|
569
630
|
* Adds straight/45-degree meanders after the dense component escape. Matching
|
|
570
631
|
* is atomic: a constrained bus either satisfies its declared skew with the
|
|
@@ -584,6 +645,8 @@ export function matchBusPlanLengths(params: {
|
|
|
584
645
|
* revalidated and whose remaining dogbone capacity is checked atomically.
|
|
585
646
|
*/
|
|
586
647
|
allowMatchingInsideDenseBounds?: boolean
|
|
648
|
+
/** Allow a differential pair's longer lane to move aside before tuning its mate. */
|
|
649
|
+
allowPairLaneSpreading?: boolean
|
|
587
650
|
/**
|
|
588
651
|
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
589
652
|
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
@@ -760,6 +823,49 @@ export function matchBusPlanLengths(params: {
|
|
|
760
823
|
}
|
|
761
824
|
if (acceptedPlans) break
|
|
762
825
|
}
|
|
826
|
+
if (
|
|
827
|
+
!acceptedPlans &&
|
|
828
|
+
params.allowPairLaneSpreading &&
|
|
829
|
+
bus.connections.length === 2
|
|
830
|
+
) {
|
|
831
|
+
// A tightly packed pair may leave no space to lengthen the inner lane.
|
|
832
|
+
// Move the outer lane, then retune the complete pair atomically. Only
|
|
833
|
+
// four geometrically clear placements may start another matching pass.
|
|
834
|
+
const longer = busPlans.find((plan) => plan !== shortest)!
|
|
835
|
+
let attempts = 0
|
|
836
|
+
for (const candidate of createSpreadLaneCandidates(longer, clearance)) {
|
|
837
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
838
|
+
plan === longer ? candidate : plan,
|
|
839
|
+
)
|
|
840
|
+
if (
|
|
841
|
+
!fanoutPlansAreClear({
|
|
842
|
+
plans: nextPlans,
|
|
843
|
+
srj: inputSrj,
|
|
844
|
+
sharedBoundary,
|
|
845
|
+
clearance,
|
|
846
|
+
allowBlindAndBuriedVias,
|
|
847
|
+
allowSameNetMerges,
|
|
848
|
+
})
|
|
849
|
+
)
|
|
850
|
+
continue
|
|
851
|
+
if (
|
|
852
|
+
candidatePlansAreFeasible &&
|
|
853
|
+
!candidatePlansAreFeasible(nextPlans)
|
|
854
|
+
)
|
|
855
|
+
continue
|
|
856
|
+
const result = matchBusPlanLengths({
|
|
857
|
+
...params,
|
|
858
|
+
plans: nextPlans,
|
|
859
|
+
preparedBuses: [bus],
|
|
860
|
+
allowPairLaneSpreading: false,
|
|
861
|
+
})
|
|
862
|
+
if (result.plans) {
|
|
863
|
+
acceptedPlans = result.plans
|
|
864
|
+
break
|
|
865
|
+
}
|
|
866
|
+
if (++attempts >= 4) break
|
|
867
|
+
}
|
|
868
|
+
}
|
|
763
869
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
764
870
|
matchedPlans = acceptedPlans
|
|
765
871
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface RouteBusParams {
|
|
|
64
64
|
viaMinimalOnly?: boolean
|
|
65
65
|
/** Permit a singleton to move its provisional via near the boundary. */
|
|
66
66
|
allowBoundarySideViaFallback?: boolean
|
|
67
|
+
/** Reserve corner tuning space when selecting a boundary-side via. */
|
|
68
|
+
preferCornerBoundaryVia?: boolean
|
|
67
69
|
/** Retry blocked winding terminals ahead of already-routed terminals. */
|
|
68
70
|
adaptiveWindingRouteOrder?: boolean
|
|
69
71
|
/** Preserve pad-lattice channels in the automatic dense routing path. */
|
|
@@ -1611,8 +1613,10 @@ function segmentIsClearOfObstacles(params: {
|
|
|
1611
1613
|
) {
|
|
1612
1614
|
continue
|
|
1613
1615
|
}
|
|
1616
|
+
// A winding escape can turn more than once while leaving its own pad.
|
|
1614
1617
|
if (
|
|
1615
|
-
segmentIndex
|
|
1618
|
+
segmentIndex >= 0 &&
|
|
1619
|
+
segmentIndex < (plan.sourceEscapeSegmentCount ?? 1) &&
|
|
1616
1620
|
obstacle === plan.sourceObstacle &&
|
|
1617
1621
|
segment.layer === plan.sourceLayer
|
|
1618
1622
|
) {
|
|
@@ -2003,7 +2007,9 @@ export function fanoutPlansAreClear(params: {
|
|
|
2003
2007
|
}
|
|
2004
2008
|
|
|
2005
2009
|
function routePlaneTerminatedBus(
|
|
2006
|
-
params: RouteBusParams
|
|
2010
|
+
params: RouteBusParams & {
|
|
2011
|
+
collectAlternative?: (plan: FanoutRoutePlan) => boolean
|
|
2012
|
+
},
|
|
2007
2013
|
): FanoutRoutePlan[] | null {
|
|
2008
2014
|
const {
|
|
2009
2015
|
srj,
|
|
@@ -2037,6 +2043,8 @@ function routePlaneTerminatedBus(
|
|
|
2037
2043
|
remainingPlaneCandidatesToSkip--
|
|
2038
2044
|
continue
|
|
2039
2045
|
}
|
|
2046
|
+
if (params.collectAlternative && !params.collectAlternative(plan))
|
|
2047
|
+
continue
|
|
2040
2048
|
return plan
|
|
2041
2049
|
}
|
|
2042
2050
|
return undefined
|
|
@@ -2452,6 +2460,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2452
2460
|
reservedVias = [],
|
|
2453
2461
|
viaMinimalOnly = false,
|
|
2454
2462
|
allowBoundarySideViaFallback = false,
|
|
2463
|
+
preferCornerBoundaryVia = false,
|
|
2455
2464
|
adaptiveWindingRouteOrder = false,
|
|
2456
2465
|
alignWindingGridToPads = false,
|
|
2457
2466
|
fixedViaFallbackRouteOrderAttempts = 24,
|
|
@@ -2473,6 +2482,19 @@ export function* routeBusAlternativesSteps(
|
|
|
2473
2482
|
}
|
|
2474
2483
|
if (bus.termination.type === "plane") {
|
|
2475
2484
|
const alternatives: FanoutRoutePlan[][] = []
|
|
2485
|
+
if (bus.connections.length === 1 && maxAlternatives > 1) {
|
|
2486
|
+
// Enumerate once instead of rebuilding and skipping every earlier route
|
|
2487
|
+
// for each successive alternative. Keep the original candidate order.
|
|
2488
|
+
routePlaneTerminatedBus({
|
|
2489
|
+
...params,
|
|
2490
|
+
planeCandidateSkipCount: 0,
|
|
2491
|
+
collectAlternative: (plan) => {
|
|
2492
|
+
alternatives.push([plan])
|
|
2493
|
+
return alternatives.length >= maxAlternatives
|
|
2494
|
+
},
|
|
2495
|
+
})
|
|
2496
|
+
return alternatives
|
|
2497
|
+
}
|
|
2476
2498
|
for (
|
|
2477
2499
|
let planeCandidateSkipCount = 0;
|
|
2478
2500
|
planeCandidateSkipCount < maxAlternatives;
|
|
@@ -2801,14 +2823,19 @@ export function* routeBusAlternativesSteps(
|
|
|
2801
2823
|
acceptedBoundaryPlansExist && !allowBlindAndBuriedVias
|
|
2802
2824
|
? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
|
|
2803
2825
|
: [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
|
|
2826
|
+
const unreservedTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2827
|
+
canUseViaInPadTerminals
|
|
2828
|
+
? planeTerminationsAlreadyOccupyTheFanout
|
|
2829
|
+
? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
|
|
2830
|
+
: [...dogboneTerminalPatterns, viaInPadTerminalPattern]
|
|
2831
|
+
: dogboneTerminalPatterns
|
|
2832
|
+
// Automatically matched sites are candidates, not caller reservations.
|
|
2833
|
+
// Keep the ordinary dogbone patterns available before adding crossover
|
|
2834
|
+
// vias when that first site assignment cannot route the complete bus.
|
|
2804
2835
|
const terminalPatterns: CoordinatedTerminalPattern[] =
|
|
2805
|
-
|
|
2836
|
+
fixedViaPointsByConnectionIndex
|
|
2806
2837
|
? fixedViaTerminalPatterns
|
|
2807
|
-
:
|
|
2808
|
-
? planeTerminationsAlreadyOccupyTheFanout
|
|
2809
|
-
? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
|
|
2810
|
-
: [...dogboneTerminalPatterns, viaInPadTerminalPattern]
|
|
2811
|
-
: dogboneTerminalPatterns
|
|
2838
|
+
: [...fixedViaTerminalPatterns, ...unreservedTerminalPatterns]
|
|
2812
2839
|
const seenTerminalSignatures = new Set<string>()
|
|
2813
2840
|
for (const terminalPattern of terminalPatterns) {
|
|
2814
2841
|
const terminals = bus.connections.map((preparedConnection) => {
|
|
@@ -2857,12 +2884,24 @@ export function* routeBusAlternativesSteps(
|
|
|
2857
2884
|
),
|
|
2858
2885
|
}
|
|
2859
2886
|
})
|
|
2887
|
+
const alignGridToPads =
|
|
2888
|
+
alignWindingGridToPads ||
|
|
2889
|
+
Boolean(terminalPattern.getViaPoint && !fixedViaPointsByConnectionIndex)
|
|
2890
|
+
const gridStepDivisor =
|
|
2891
|
+
terminalPattern.getViaPoint &&
|
|
2892
|
+
Math.min(bus.pitchX, bus.pitchY) -
|
|
2893
|
+
2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
|
|
2894
|
+
traceWidth + clearance
|
|
2895
|
+
? 2
|
|
2896
|
+
: 1
|
|
2860
2897
|
const terminalSignature = `${terminals
|
|
2861
2898
|
.map(
|
|
2862
2899
|
(terminal) =>
|
|
2863
2900
|
`${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
|
|
2864
2901
|
)
|
|
2865
|
-
.join(
|
|
2902
|
+
.join(
|
|
2903
|
+
"|",
|
|
2904
|
+
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}`
|
|
2866
2905
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
2867
2906
|
seenTerminalSignatures.add(terminalSignature)
|
|
2868
2907
|
const windingSteps = routeViaMinimalWindingAlternativesSteps(
|
|
@@ -2881,17 +2920,9 @@ export function* routeBusAlternativesSteps(
|
|
|
2881
2920
|
allowSameNetMerges,
|
|
2882
2921
|
maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
|
|
2883
2922
|
adaptiveRouteOrder: adaptiveWindingRouteOrder,
|
|
2884
|
-
alignGridToPads
|
|
2885
|
-
alignWindingGridToPads ||
|
|
2886
|
-
Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
|
|
2923
|
+
alignGridToPads,
|
|
2887
2924
|
reservedVias,
|
|
2888
|
-
gridStepDivisor
|
|
2889
|
-
coordinatedViaPoints &&
|
|
2890
|
-
Math.min(bus.pitchX, bus.pitchY) -
|
|
2891
|
-
2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
|
|
2892
|
-
traceWidth + clearance
|
|
2893
|
-
? 2
|
|
2894
|
-
: 1,
|
|
2925
|
+
gridStepDivisor,
|
|
2895
2926
|
preferTargetDirectedLaneBias:
|
|
2896
2927
|
terminalPattern.preferTargetDirectedLaneBias,
|
|
2897
2928
|
},
|
|
@@ -2962,11 +2993,24 @@ export function* routeBusAlternativesSteps(
|
|
|
2962
2993
|
const preparedConnection = bus.connections[0]!
|
|
2963
2994
|
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
2964
2995
|
const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
|
|
2965
|
-
const finalTrack =
|
|
2966
|
-
bus
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2996
|
+
const finalTrack =
|
|
2997
|
+
preferCornerBoundaryVia && getCornerSide(bus)
|
|
2998
|
+
? getCornerTargetTrack({
|
|
2999
|
+
bus,
|
|
3000
|
+
connection: preparedConnection,
|
|
3001
|
+
cornerExitLaneOffset: cornerLaneOffsets.exit,
|
|
3002
|
+
traceWidth,
|
|
3003
|
+
viaDiameter,
|
|
3004
|
+
clearance,
|
|
3005
|
+
layerNames,
|
|
3006
|
+
targetLayer,
|
|
3007
|
+
cornerBandTargetTrackOffset,
|
|
3008
|
+
})
|
|
3009
|
+
: getBoundaryTargetTrack({
|
|
3010
|
+
bus,
|
|
3011
|
+
connection: preparedConnection,
|
|
3012
|
+
boundaryDirection,
|
|
3013
|
+
})
|
|
2970
3014
|
const finalExitPoint = makePoint(
|
|
2971
3015
|
boundaryExitAxis,
|
|
2972
3016
|
finalTrack,
|
|
@@ -2992,13 +3036,27 @@ export function* routeBusAlternativesSteps(
|
|
|
2992
3036
|
viaDiameter / 2 + clearance,
|
|
2993
3037
|
Math.min(bus.pitchX, bus.pitchY) / 2,
|
|
2994
3038
|
)
|
|
2995
|
-
|
|
3039
|
+
const cornerSide = preferCornerBoundaryVia ? getCornerSide(bus) : undefined
|
|
3040
|
+
const boundaryViaCandidates = [1, 2, 3, 4, 5].flatMap((multiple) => {
|
|
2996
3041
|
const inset = multiple * insetStep
|
|
2997
|
-
const
|
|
3042
|
+
const straight = makePoint(
|
|
2998
3043
|
boundaryExitAxis - directionSign(boundaryDirection) * inset,
|
|
2999
3044
|
finalTrack,
|
|
3000
3045
|
boundaryDirection,
|
|
3001
3046
|
)
|
|
3047
|
+
if (!cornerSide) return [straight]
|
|
3048
|
+
// Approach corner exits diagonally to leave the adjacent pair's tuning
|
|
3049
|
+
// lane free of the through-via barrel. Retain the straight fallback.
|
|
3050
|
+
return [
|
|
3051
|
+
makePoint(
|
|
3052
|
+
boundaryExitAxis - directionSign(boundaryDirection) * inset,
|
|
3053
|
+
finalTrack + (cornerSide === "maximum" ? inset : -inset),
|
|
3054
|
+
boundaryDirection,
|
|
3055
|
+
),
|
|
3056
|
+
straight,
|
|
3057
|
+
]
|
|
3058
|
+
})
|
|
3059
|
+
for (const boundaryViaPoint of boundaryViaCandidates) {
|
|
3002
3060
|
const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps(
|
|
3003
3061
|
{
|
|
3004
3062
|
srj: sourceLayerSrj,
|
|
@@ -3065,6 +3123,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3065
3123
|
const plans: FanoutRoutePlan[] = [
|
|
3066
3124
|
{
|
|
3067
3125
|
...sourceLayerPlan,
|
|
3126
|
+
...(preferCornerBoundaryVia
|
|
3127
|
+
? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
|
|
3128
|
+
: {}),
|
|
3068
3129
|
targetLayer,
|
|
3069
3130
|
exitPoint: finalExitPoint,
|
|
3070
3131
|
via,
|
|
@@ -1075,6 +1075,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1075
1075
|
endByNode.set(end.nodeIndex, values)
|
|
1076
1076
|
}
|
|
1077
1077
|
const stateCount = nodeCount * 9
|
|
1078
|
+
// A* visits a grid edge with several incoming directions. Copper does not
|
|
1079
|
+
// change while routing this terminal, so check each edge only once. Keep
|
|
1080
|
+
// this cache local: later terminals and route-order attempts add blockers.
|
|
1081
|
+
const edgeClearance = new Uint8Array(nodeCount * 8)
|
|
1078
1082
|
const distances = new Float64Array(stateCount).fill(
|
|
1079
1083
|
Number.POSITIVE_INFINITY,
|
|
1080
1084
|
)
|
|
@@ -1205,21 +1209,6 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1205
1209
|
}
|
|
1206
1210
|
const nextNode = row * columnCount + column
|
|
1207
1211
|
const nextPoint = nodes[nextNode]!.point
|
|
1208
|
-
const segment: RoutedSegment = {
|
|
1209
|
-
start: node.point,
|
|
1210
|
-
end: nextPoint,
|
|
1211
|
-
width: traceWidth,
|
|
1212
|
-
layer: targetLayer,
|
|
1213
|
-
}
|
|
1214
|
-
if (
|
|
1215
|
-
!segmentIsClear({
|
|
1216
|
-
segment,
|
|
1217
|
-
terminal,
|
|
1218
|
-
acceptedAttemptSegments,
|
|
1219
|
-
})
|
|
1220
|
-
) {
|
|
1221
|
-
continue
|
|
1222
|
-
}
|
|
1223
1212
|
const addsTurn =
|
|
1224
1213
|
current.direction !== 8 && current.direction !== directionIndex
|
|
1225
1214
|
const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection)
|
|
@@ -1242,6 +1231,24 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1242
1231
|
lanePenalty
|
|
1243
1232
|
const nextState = nextNode * 9 + directionIndex
|
|
1244
1233
|
if (nextDistance >= distances[nextState]! - EPSILON) continue
|
|
1234
|
+
const edgeIndex = current.node * 8 + directionIndex
|
|
1235
|
+
if (edgeClearance[edgeIndex] === 0) {
|
|
1236
|
+
const clear = segmentIsClear({
|
|
1237
|
+
segment: {
|
|
1238
|
+
start: node.point,
|
|
1239
|
+
end: nextPoint,
|
|
1240
|
+
width: traceWidth,
|
|
1241
|
+
layer: targetLayer,
|
|
1242
|
+
},
|
|
1243
|
+
terminal,
|
|
1244
|
+
acceptedAttemptSegments,
|
|
1245
|
+
})
|
|
1246
|
+
edgeClearance[edgeIndex] = clear ? 1 : 2
|
|
1247
|
+
edgeClearance[nextNode * 8 + ((directionIndex + 4) % 8)] = clear
|
|
1248
|
+
? 1
|
|
1249
|
+
: 2
|
|
1250
|
+
}
|
|
1251
|
+
if (edgeClearance[edgeIndex] === 2) continue
|
|
1245
1252
|
distances[nextState] = nextDistance
|
|
1246
1253
|
previous[nextState] = state
|
|
1247
1254
|
const remaining = heuristic(nextPoint)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import { fanoutPlansAreClear } from "./route-bus"
|
|
3
|
+
import { routeViaMinimalWinding } from "./route-via-minimal-winding"
|
|
4
|
+
import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
|
|
5
|
+
|
|
6
|
+
/** Revisit excessive winding detours after provisional reservations are gone. */
|
|
7
|
+
export function shortenBusPlans(params: {
|
|
8
|
+
plans: readonly FanoutRoutePlan[]
|
|
9
|
+
bus: PreparedBus
|
|
10
|
+
srj: SimpleRouteJson
|
|
11
|
+
sharedBoundary: Bounds
|
|
12
|
+
layerNames: string[]
|
|
13
|
+
traceWidth: number
|
|
14
|
+
viaDiameter: number
|
|
15
|
+
viaHoleDiameter: number
|
|
16
|
+
clearance: number
|
|
17
|
+
allowSameNetMerges: boolean
|
|
18
|
+
}): FanoutRoutePlan[] {
|
|
19
|
+
const { bus } = params
|
|
20
|
+
let plans = [...params.plans]
|
|
21
|
+
if (bus.maxLengthSkew === undefined) return plans
|
|
22
|
+
const busPlans = plans
|
|
23
|
+
.filter((plan) => plan.busId === bus.busId)
|
|
24
|
+
.toSorted((first, second) => second.length - first.length)
|
|
25
|
+
for (const plan of busPlans) {
|
|
26
|
+
const minimumLength = Math.min(
|
|
27
|
+
...plans.filter((p) => p.busId === bus.busId).map((p) => p.length),
|
|
28
|
+
)
|
|
29
|
+
if (plan.length - minimumLength <= bus.maxLengthSkew) continue
|
|
30
|
+
// Retain the original source dogbone, through-via and boundary endpoint.
|
|
31
|
+
if (
|
|
32
|
+
!plan.via ||
|
|
33
|
+
plan.additionalVias?.length ||
|
|
34
|
+
plan.planeEndpointVia ||
|
|
35
|
+
plan.segments.filter((segment) => segment.layer === plan.sourceLayer)
|
|
36
|
+
.length !== 1
|
|
37
|
+
)
|
|
38
|
+
continue
|
|
39
|
+
const connection = bus.connections.find(
|
|
40
|
+
(c) => c.connectionIndex === plan.connectionIndex,
|
|
41
|
+
)!
|
|
42
|
+
for (const alignGridToPads of [true, false]) {
|
|
43
|
+
const candidate = routeViaMinimalWinding({
|
|
44
|
+
...params,
|
|
45
|
+
bus: {
|
|
46
|
+
...bus,
|
|
47
|
+
connections: [connection],
|
|
48
|
+
routableEscapeLayers: [plan.targetLayer],
|
|
49
|
+
},
|
|
50
|
+
terminals: [
|
|
51
|
+
{ connection, viaPoint: plan.via.center, exitPoint: plan.exitPoint },
|
|
52
|
+
],
|
|
53
|
+
targetLayer: plan.targetLayer,
|
|
54
|
+
acceptedPlans: plans.filter((p) => p !== plan),
|
|
55
|
+
allowBlindAndBuriedVias: false,
|
|
56
|
+
gridStepDivisor: 2,
|
|
57
|
+
alignGridToPads,
|
|
58
|
+
maximumRouteOrderAttempts: 1,
|
|
59
|
+
})?.[0]
|
|
60
|
+
if (!candidate || candidate.length >= plan.length - 1e-6) continue
|
|
61
|
+
const nextPlans = plans.map((p) => (p === plan ? candidate : p))
|
|
62
|
+
if (
|
|
63
|
+
!fanoutPlansAreClear({
|
|
64
|
+
...params,
|
|
65
|
+
plans: nextPlans,
|
|
66
|
+
allowBlindAndBuriedVias: false,
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
continue
|
|
70
|
+
plans = nextPlans
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return plans
|
|
75
|
+
}
|
package/lib/types.ts
CHANGED
|
@@ -388,6 +388,8 @@ export interface FanoutRoutePlan {
|
|
|
388
388
|
exitPoint: Point2D
|
|
389
389
|
trace: FanoutSimplifiedPcbTrace
|
|
390
390
|
segments: RoutedSegment[]
|
|
391
|
+
/** Validated winding segments that leave this route's own source pad. */
|
|
392
|
+
sourceEscapeSegmentCount?: number
|
|
391
393
|
via?: RoutedVia
|
|
392
394
|
/** Additional layer transitions used by an explicit winding channel. */
|
|
393
395
|
additionalVias?: RoutedVia[]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fanout-solver",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.54",
|
|
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
|
-
"
|
|
35
|
+
"generate:dataset31": "bun scripts/generate-repro/generate-dataset31.tsx",
|
|
36
36
|
"render:dataset": "bun scripts/render-dataset-pngs.ts",
|
|
37
37
|
"format": "biome format --write .",
|
|
38
38
|
"format:check": "biome format ."
|