@tscircuit/fanout-solver 0.0.64 → 0.0.66
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 +22 -15
- package/lib/fanout-solver.ts +198 -6
- package/lib/get-free-boundary-tracks.ts +102 -0
- package/lib/match-angularly-ordered-local-vias.ts +239 -0
- package/lib/reflect-fanout-x.ts +67 -0
- package/lib/repair-peripheral-bus-lengths.ts +167 -0
- package/lib/route-adaptive-left-crossbar-bus.ts +425 -0
- package/lib/route-bottom-crossbar-bus.ts +559 -0
- package/lib/route-bus.ts +18 -2
- package/lib/route-left-crossbar-bus.ts +399 -0
- package/lib/route-opposite-bottom-crossbar-bus.ts +70 -0
- package/lib/route-peripheral-source-escapes.ts +488 -0
- package/lib/route-reserved-narrow-buses.ts +430 -0
- package/lib/route-reserved-source-buses.ts +243 -0
- package/lib/route-shallow-split-perimeter-bus.ts +268 -0
- package/lib/route-split-perimeter-bus.ts +397 -0
- package/lib/route-split-perimeter-source-escapes.ts +579 -0
- package/lib/route-staged-perimeter-bus.ts +257 -0
- package/lib/route-via-minimal-winding.ts +78 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,8 +282,8 @@ extents.
|
|
|
282
282
|
|
|
283
283
|
## Dataset 31 benchmark
|
|
284
284
|
|
|
285
|
-
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark **all
|
|
286
|
-
directional cases: 12 AM62L and 12
|
|
285
|
+
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark **all 36 dataset 31
|
|
286
|
+
directional cases: 12 AM62L, 12 RK3308, and 12 K230** from
|
|
287
287
|
[`tscircuit/dataset-fanout31-am62l`](https://github.com/tscircuit/dataset-fanout31-am62l).
|
|
288
288
|
The upstream revision is pinned in `scripts/generate-repro/package.json` and
|
|
289
289
|
recorded in every report. Other datasets remain available for regression tests
|
|
@@ -294,6 +294,7 @@ and the debugger, but have no benchmark commands or workflows.
|
|
|
294
294
|
./benchmark.sh --list
|
|
295
295
|
./benchmark.sh --sample 11-left-center
|
|
296
296
|
./benchmark.sh --sample 13-rk3308-top-left-offset
|
|
297
|
+
./benchmark.sh --sample 25-k230-top-left-offset
|
|
297
298
|
./benchmark.sh --concurrency 8 --sample-timeout-seconds 300
|
|
298
299
|
```
|
|
299
300
|
|
|
@@ -307,11 +308,16 @@ constraints:
|
|
|
307
308
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
308
309
|
| AM62L | 12 | 33 | 102 | 135 | 573 |
|
|
309
310
|
| RK3308 | 12 | 49 | 113 | 162 | 451 |
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
and
|
|
313
|
-
The
|
|
314
|
-
|
|
311
|
+
| K230 | 12 | 65 | 106 | 171 | 790 |
|
|
312
|
+
|
|
313
|
+
AM62L and RK3308 have nine DDR signal buses each. The RK3308 samples use a
|
|
314
|
+
355-ball SoC and 96-ball DDR3L RAM. The K230 samples use a 390-ball SoC and two
|
|
315
|
+
200-ball x16 LPDDR4 RAM packages, with 17 signal buses and six differential pairs.
|
|
316
|
+
Each family places its RAM on all four sides at three offsets per side. K230
|
|
317
|
+
retains both physical RAM endpoints of its shared RESET net; the two RAM packages
|
|
318
|
+
have separate breakout regions. All 790 pads in each K230 case remain obstacles,
|
|
319
|
+
including unused signal and supply pads. The timed workers run **this checkout's
|
|
320
|
+
solver**, not the upstream package's released solver.
|
|
315
321
|
To capture the inputs without solving, use `bun run generate:dataset31`.
|
|
316
322
|
The optional `--dataset dataset31` flag is accepted for explicit CI invocation;
|
|
317
323
|
other dataset selections are rejected.
|
|
@@ -343,20 +349,21 @@ Timed-out workers do not retain their in-flight routing counts.
|
|
|
343
349
|
Every solved case also writes `benchmark-results/<sample-id>.svg`. These SVGs
|
|
344
350
|
are committed so route changes can be reviewed in Git. A run replaces the selected
|
|
345
351
|
cases' snapshots and removes their stale SVGs if they no longer solve; filtered
|
|
346
|
-
runs preserve unselected snapshots. JSON
|
|
347
|
-
ignored. CI includes the
|
|
352
|
+
runs preserve unselected snapshots. The latest JSON and Markdown reports are
|
|
353
|
+
also committed; captured inputs remain ignored. CI includes the reports, inputs,
|
|
354
|
+
and SVGs in its benchmark artifacts.
|
|
348
355
|
Compare reports with the same dataset revision and budgets to track progress.
|
|
349
356
|
A case is solved only when every SoC connection has validated fanout: all 135
|
|
350
|
-
connections for AM62L or all
|
|
351
|
-
fanout and downstream inter-chip routing are separate phases.
|
|
352
|
-
and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
|
357
|
+
connections for AM62L, all 162 for RK3308, or all 171 for K230. This covers the SoC
|
|
358
|
+
fanout phase; RAM fanout and downstream inter-chip routing are separate phases.
|
|
359
|
+
Partial, error, and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
|
353
360
|
I/O failures are command failures (nonzero exit).
|
|
354
361
|
|
|
355
362
|
### PR comment trigger
|
|
356
363
|
|
|
357
364
|
Once `.github/workflows/benchmark.yml` is on the default branch, a repository
|
|
358
365
|
writer can comment **`/benchmark`** on an open PR. The workflow captures that
|
|
359
|
-
PR's exact head SHA, runs all
|
|
366
|
+
PR's exact head SHA, runs all 36 dataset 31 samples on a **32-vCPU Blacksmith ARM**
|
|
360
367
|
runner, then updates a status comment with solve totals, per-sample results,
|
|
361
368
|
and a link to the complete JSON/Markdown reports and captured inputs. The Actions
|
|
362
369
|
UI also supports a manual run, optionally supplying an open PR number. No custom
|
|
@@ -522,8 +529,8 @@ bun run render:dataset
|
|
|
522
529
|
bun run start
|
|
523
530
|
```
|
|
524
531
|
|
|
525
|
-
The benchmark runs all
|
|
526
|
-
counts, validation, and timing. `bun run start` opens all regression
|
|
532
|
+
The benchmark runs all 36 dataset 31 AM62L, RK3308, and K230 cases and reports
|
|
533
|
+
solve counts, validation, and timing. `bun run start` opens all regression
|
|
527
534
|
datasets in the standard tscircuit solver debugger. `bun run
|
|
528
535
|
render:dataset` writes `graphics-debug` PNGs under one subdirectory per dataset,
|
|
529
536
|
with a red shared boundary, gray component courtyards, and green fanout-exit
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -39,6 +39,13 @@ import {
|
|
|
39
39
|
routeBusAlternatives,
|
|
40
40
|
routeBusAlternativesSteps,
|
|
41
41
|
} from "./route-bus"
|
|
42
|
+
import { routePeripheralSourceEscapesSteps } from "./route-peripheral-source-escapes"
|
|
43
|
+
import { routeStagedPerimeterBusSteps } from "./route-staged-perimeter-bus"
|
|
44
|
+
import { routeReservedSourceBusesSteps } from "./route-reserved-source-buses"
|
|
45
|
+
import { repairPeripheralBusLengthsSteps } from "./repair-peripheral-bus-lengths"
|
|
46
|
+
import { routeSplitPerimeterSourceEscapesSteps } from "./route-split-perimeter-source-escapes"
|
|
47
|
+
import { routeSplitPerimeterBusSteps } from "./route-split-perimeter-bus"
|
|
48
|
+
import { routeShallowSplitPerimeterBusSteps } from "./route-shallow-split-perimeter-bus"
|
|
42
49
|
import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
|
|
43
50
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
44
51
|
import { getRuntimeProcess } from "./runtime-process"
|
|
@@ -84,6 +91,8 @@ interface ResolvedFanoutConfig {
|
|
|
84
91
|
|
|
85
92
|
interface EvaluatedAssignment extends AssignmentAttempt {
|
|
86
93
|
blockingBusIds: string[]
|
|
94
|
+
/** The bounded peripheral strategy may finish after complete validation. */
|
|
95
|
+
stopAfterCompleteValidation?: true
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
interface GroupedBeamState {
|
|
@@ -94,6 +103,7 @@ interface GroupedBeamState {
|
|
|
94
103
|
interface MixedTerminationState {
|
|
95
104
|
plans: FanoutRoutePlan[]
|
|
96
105
|
failedBusIds: string[]
|
|
106
|
+
stopAfterCompleteValidation?: true
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
|
|
@@ -513,6 +523,43 @@ function createInitialLayerAssignment(params: {
|
|
|
513
523
|
) {
|
|
514
524
|
assignment[bus.busId] = sourceLayer
|
|
515
525
|
} else if (viaLayers.length > 0) {
|
|
526
|
+
// A source strictly inside a wide field can be enclosed by its routes.
|
|
527
|
+
// Boundary sources still have an outward channel, so preserve the
|
|
528
|
+
// ordinary layer preference for those buses.
|
|
529
|
+
const sourceXs = bus.connections.map(
|
|
530
|
+
(connection) => connection.sourcePoint.x,
|
|
531
|
+
)
|
|
532
|
+
const sourceYs = bus.connections.map(
|
|
533
|
+
(connection) => connection.sourcePoint.y,
|
|
534
|
+
)
|
|
535
|
+
const embeddedSingletonCount = (layer: string) =>
|
|
536
|
+
buses.filter((other) => {
|
|
537
|
+
const point = other.connections[0]?.sourcePoint
|
|
538
|
+
return (
|
|
539
|
+
other.termination.type === "boundary" &&
|
|
540
|
+
point !== undefined &&
|
|
541
|
+
point.x > Math.min(...sourceXs) + 1e-9 &&
|
|
542
|
+
point.x < Math.max(...sourceXs) - 1e-9 &&
|
|
543
|
+
point.y > Math.min(...sourceYs) + 1e-9 &&
|
|
544
|
+
point.y < Math.max(...sourceYs) - 1e-9 &&
|
|
545
|
+
getCommonExplicitExitTargetLayer(other) === layer &&
|
|
546
|
+
isDenseSingletonEmbeddedInMultiLayerWideBus({
|
|
547
|
+
singletonBus: other,
|
|
548
|
+
singletonTargetLayer: layer,
|
|
549
|
+
wideBuses: [bus],
|
|
550
|
+
})
|
|
551
|
+
)
|
|
552
|
+
}).length
|
|
553
|
+
const isolatedLayers = viaLayers.toSorted(
|
|
554
|
+
(a, b) => embeddedSingletonCount(a) - embeddedSingletonCount(b),
|
|
555
|
+
)
|
|
556
|
+
if (
|
|
557
|
+
embeddedSingletonCount(isolatedLayers[0]!) <
|
|
558
|
+
embeddedSingletonCount(viaLayers[0]!)
|
|
559
|
+
) {
|
|
560
|
+
assignment[bus.busId] = isolatedLayers[0]!
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
516
563
|
if (
|
|
517
564
|
preferOrderedCoordinatedWindingLayers &&
|
|
518
565
|
busUsesCoordinatedWinding(bus)
|
|
@@ -1535,6 +1582,127 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1535
1582
|
})
|
|
1536
1583
|
}
|
|
1537
1584
|
|
|
1585
|
+
/** Reserve source escapes before joining complete buses around the package. */
|
|
1586
|
+
private *routePeripheralMixedTerminationSteps(params: {
|
|
1587
|
+
busLayerAssignments: Readonly<Record<string, string>>
|
|
1588
|
+
busesInRoutingOrder: readonly PreparedBus[]
|
|
1589
|
+
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1590
|
+
if (this.config.allowBlindAndBuriedVias) return null
|
|
1591
|
+
const bus = params.busesInRoutingOrder.find(
|
|
1592
|
+
(candidate) =>
|
|
1593
|
+
candidate.termination.type === "boundary" &&
|
|
1594
|
+
(candidate.exitEdge === "right" || candidate.exitEdge === "left") &&
|
|
1595
|
+
candidate.connections.length >= 16 &&
|
|
1596
|
+
candidate.componentObstacles.length >= 200,
|
|
1597
|
+
)
|
|
1598
|
+
if (!bus) return null
|
|
1599
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
1600
|
+
if (!targetLayer) return null
|
|
1601
|
+
const targetLayerByBusId = new Map(
|
|
1602
|
+
Object.entries(params.busLayerAssignments),
|
|
1603
|
+
)
|
|
1604
|
+
this.setInProgressPlans({
|
|
1605
|
+
phase: "route-peripheral-source-escapes",
|
|
1606
|
+
plans: [],
|
|
1607
|
+
busId: bus.busId,
|
|
1608
|
+
})
|
|
1609
|
+
const sourceSteps = (
|
|
1610
|
+
bus.exitEdge === "left"
|
|
1611
|
+
? routeSplitPerimeterSourceEscapesSteps
|
|
1612
|
+
: routePeripheralSourceEscapesSteps
|
|
1613
|
+
)({
|
|
1614
|
+
...this.config,
|
|
1615
|
+
srj: this.routingSrj,
|
|
1616
|
+
buses: this.preparedBuses,
|
|
1617
|
+
bus,
|
|
1618
|
+
targetLayer,
|
|
1619
|
+
targetLayerByBusId,
|
|
1620
|
+
})
|
|
1621
|
+
let sourceResult = sourceSteps.next()
|
|
1622
|
+
while (!sourceResult.done) {
|
|
1623
|
+
yield
|
|
1624
|
+
sourceResult = sourceSteps.next()
|
|
1625
|
+
}
|
|
1626
|
+
if (!sourceResult.value) return null
|
|
1627
|
+
let source = sourceResult.value
|
|
1628
|
+
const stageParams = {
|
|
1629
|
+
...this.config,
|
|
1630
|
+
...source,
|
|
1631
|
+
srj: this.routingSrj,
|
|
1632
|
+
bus,
|
|
1633
|
+
targetLayer,
|
|
1634
|
+
}
|
|
1635
|
+
const stageSteps =
|
|
1636
|
+
"lowerConnectionIndices" in source
|
|
1637
|
+
? routeSplitPerimeterBusSteps({ ...stageParams, ...source })
|
|
1638
|
+
: routeStagedPerimeterBusSteps(stageParams)
|
|
1639
|
+
let stageResult = stageSteps.next()
|
|
1640
|
+
while (!stageResult.done) {
|
|
1641
|
+
yield
|
|
1642
|
+
stageResult = stageSteps.next()
|
|
1643
|
+
}
|
|
1644
|
+
let stagePlans = stageResult.value
|
|
1645
|
+
if (!stagePlans && "lowerConnectionIndices" in source) {
|
|
1646
|
+
const shallowSteps = routeShallowSplitPerimeterBusSteps({
|
|
1647
|
+
...stageParams,
|
|
1648
|
+
...source,
|
|
1649
|
+
buses: this.preparedBuses,
|
|
1650
|
+
})
|
|
1651
|
+
let shallowResult = shallowSteps.next()
|
|
1652
|
+
while (!shallowResult.done) {
|
|
1653
|
+
yield
|
|
1654
|
+
shallowResult = shallowSteps.next()
|
|
1655
|
+
}
|
|
1656
|
+
if (shallowResult.value) {
|
|
1657
|
+
source = shallowResult.value
|
|
1658
|
+
stagePlans = shallowResult.value.plans
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (!stagePlans) return null
|
|
1662
|
+
this.setInProgressPlans({
|
|
1663
|
+
phase: "route-peripheral-bus-continuations",
|
|
1664
|
+
plans: stagePlans,
|
|
1665
|
+
busId: bus.busId,
|
|
1666
|
+
})
|
|
1667
|
+
const remainingSteps = routeReservedSourceBusesSteps({
|
|
1668
|
+
...this.config,
|
|
1669
|
+
srj: this.routingSrj,
|
|
1670
|
+
buses: this.preparedBuses,
|
|
1671
|
+
sourceEscapes: source.sourceEscapes,
|
|
1672
|
+
sourceBoundary: source.sourceBoundary,
|
|
1673
|
+
initialPlans: stagePlans,
|
|
1674
|
+
targetLayerByBusId,
|
|
1675
|
+
})
|
|
1676
|
+
let remainingResult = remainingSteps.next()
|
|
1677
|
+
while (!remainingResult.done) {
|
|
1678
|
+
yield
|
|
1679
|
+
remainingResult = remainingSteps.next()
|
|
1680
|
+
}
|
|
1681
|
+
if (!remainingResult.value) return null
|
|
1682
|
+
const repairSteps = repairPeripheralBusLengthsSteps({
|
|
1683
|
+
...this.config,
|
|
1684
|
+
srj: this.routingSrj,
|
|
1685
|
+
inputSrj: this.inputSrj,
|
|
1686
|
+
plans: remainingResult.value,
|
|
1687
|
+
preparedBuses: this.preparedBuses,
|
|
1688
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
1689
|
+
})
|
|
1690
|
+
let repairResult = repairSteps.next()
|
|
1691
|
+
while (!repairResult.done) {
|
|
1692
|
+
yield
|
|
1693
|
+
repairResult = repairSteps.next()
|
|
1694
|
+
}
|
|
1695
|
+
if (!repairResult.value) return null
|
|
1696
|
+
const plans = repairResult.value
|
|
1697
|
+
const output = buildOutputSimpleRouteJson({
|
|
1698
|
+
inputSrj: this.inputSrj,
|
|
1699
|
+
plans,
|
|
1700
|
+
layerNames: this.config.layerNames,
|
|
1701
|
+
})
|
|
1702
|
+
if (!this.validateCompletePlans(plans, output).valid) return null
|
|
1703
|
+
return { plans, failedBusIds: [], stopAfterCompleteValidation: true }
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1538
1706
|
/**
|
|
1539
1707
|
* Through-all source vias from a wide boundary bus can consume the only
|
|
1540
1708
|
* legal dogbone channel for nearby plane pads. Conversely, routing hundreds
|
|
@@ -5142,6 +5310,23 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5142
5310
|
|
|
5143
5311
|
let mixedTerminationState: MixedTerminationState | null = null
|
|
5144
5312
|
if (!useSingleLayerPushAndShove && routingStrategy === "default") {
|
|
5313
|
+
const peripheralSolver = this.createWorkSolver(
|
|
5314
|
+
"PeripheralMixedTerminationSolver",
|
|
5315
|
+
this.routePeripheralMixedTerminationSteps({
|
|
5316
|
+
busLayerAssignments,
|
|
5317
|
+
busesInRoutingOrder,
|
|
5318
|
+
}),
|
|
5319
|
+
)
|
|
5320
|
+
mixedTerminationState = (yield {
|
|
5321
|
+
type: "subsolver",
|
|
5322
|
+
solver: peripheralSolver,
|
|
5323
|
+
}) as MixedTerminationState | null
|
|
5324
|
+
}
|
|
5325
|
+
if (
|
|
5326
|
+
!mixedTerminationState &&
|
|
5327
|
+
!useSingleLayerPushAndShove &&
|
|
5328
|
+
routingStrategy === "default"
|
|
5329
|
+
) {
|
|
5145
5330
|
const denseSolver = this.createWorkSolver(
|
|
5146
5331
|
"DenseMixedTerminationSolver",
|
|
5147
5332
|
this.routeDenseThroughAllMixedTerminationSteps({
|
|
@@ -5341,6 +5526,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5341
5526
|
return {
|
|
5342
5527
|
summary,
|
|
5343
5528
|
plans,
|
|
5529
|
+
...(mixedTerminationState?.stopAfterCompleteValidation &&
|
|
5530
|
+
validation?.valid
|
|
5531
|
+
? { stopAfterCompleteValidation: true as const }
|
|
5532
|
+
: {}),
|
|
5344
5533
|
blockingBusIds: [...blockingBusCounts.entries()]
|
|
5345
5534
|
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
5346
5535
|
.map(([busId]) => busId),
|
|
@@ -5361,7 +5550,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5361
5550
|
if (
|
|
5362
5551
|
bestAttempt.summary.routedConnectionCount ===
|
|
5363
5552
|
this.inputSrj.connections.length &&
|
|
5364
|
-
|
|
5553
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5554
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5365
5555
|
) {
|
|
5366
5556
|
return bestAttempt
|
|
5367
5557
|
}
|
|
@@ -5378,7 +5568,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5378
5568
|
if (
|
|
5379
5569
|
bestAttempt.summary.routedConnectionCount ===
|
|
5380
5570
|
this.inputSrj.connections.length &&
|
|
5381
|
-
|
|
5571
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5572
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5382
5573
|
) {
|
|
5383
5574
|
return bestAttempt
|
|
5384
5575
|
}
|
|
@@ -5932,10 +6123,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5932
6123
|
bestScore: this.bestAttempt.summary.score,
|
|
5933
6124
|
}
|
|
5934
6125
|
if (
|
|
5935
|
-
this.
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
6126
|
+
(attempt.stopAfterCompleteValidation && this.hasCompleteBestAttempt()) ||
|
|
6127
|
+
(this.groupedBeamEvaluated &&
|
|
6128
|
+
attempt.summary.routedConnectionCount ===
|
|
6129
|
+
this.inputSrj.connections.length &&
|
|
6130
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0)
|
|
5939
6131
|
) {
|
|
5940
6132
|
this.completeBestAttemptEndpoints()
|
|
5941
6133
|
this.solved = true
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
2
|
+
import type { RouteBusParams } from "./route-bus"
|
|
3
|
+
|
|
4
|
+
/** Midpoints of unoccupied edge intervals, clipped to the bus's declared band. */
|
|
5
|
+
export function getFreeBoundaryTracks(
|
|
6
|
+
params: Pick<
|
|
7
|
+
RouteBusParams,
|
|
8
|
+
| "bus"
|
|
9
|
+
| "targetLayer"
|
|
10
|
+
| "acceptedPlans"
|
|
11
|
+
| "reservedVias"
|
|
12
|
+
| "traceWidth"
|
|
13
|
+
| "clearance"
|
|
14
|
+
>,
|
|
15
|
+
): number[] {
|
|
16
|
+
const { bus, targetLayer, traceWidth, clearance } = params
|
|
17
|
+
if (!bus.exitEdge) return []
|
|
18
|
+
const vertical = bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
19
|
+
const along = vertical ? "y" : "x"
|
|
20
|
+
const across = vertical ? "x" : "y"
|
|
21
|
+
const boundary = bus.sharedBoundary
|
|
22
|
+
const edge =
|
|
23
|
+
bus.exitEdge === "left"
|
|
24
|
+
? boundary.minX
|
|
25
|
+
: bus.exitEdge === "right"
|
|
26
|
+
? boundary.maxX
|
|
27
|
+
: bus.exitEdge === "bottom"
|
|
28
|
+
? boundary.minY
|
|
29
|
+
: boundary.maxY
|
|
30
|
+
const lower = vertical ? boundary.minY : boundary.minX
|
|
31
|
+
const upper = vertical ? boundary.maxY : boundary.maxX
|
|
32
|
+
const middle = (lower + upper) / 2
|
|
33
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
34
|
+
const minimum = (side === "maximum" ? middle : lower) + traceWidth / 2
|
|
35
|
+
const maximum = (side === "minimum" ? middle : upper) - traceWidth / 2
|
|
36
|
+
const blocked: [number, number][] = []
|
|
37
|
+
for (const plan of params.acceptedPlans) {
|
|
38
|
+
for (const segment of [
|
|
39
|
+
...plan.segments,
|
|
40
|
+
...(plan.planeEndpointSegments ?? []),
|
|
41
|
+
]) {
|
|
42
|
+
if (segment.layer !== targetLayer) continue
|
|
43
|
+
const radius = (segment.width + traceWidth) / 2 + clearance
|
|
44
|
+
const start = segment.start[across]
|
|
45
|
+
const delta = segment.end[across] - start
|
|
46
|
+
let from = 0
|
|
47
|
+
let to = 1
|
|
48
|
+
if (Math.abs(delta) < 1e-9) {
|
|
49
|
+
if (Math.abs(start - edge) > radius) continue
|
|
50
|
+
} else {
|
|
51
|
+
const first = (edge - radius - start) / delta
|
|
52
|
+
const last = (edge + radius - start) / delta
|
|
53
|
+
from = Math.max(0, Math.min(first, last))
|
|
54
|
+
to = Math.min(1, Math.max(first, last))
|
|
55
|
+
if (from > to) continue
|
|
56
|
+
}
|
|
57
|
+
const alongStart = segment.start[along]
|
|
58
|
+
const alongDelta = segment.end[along] - alongStart
|
|
59
|
+
const first = alongStart + from * alongDelta
|
|
60
|
+
const last = alongStart + to * alongDelta
|
|
61
|
+
blocked.push([
|
|
62
|
+
Math.min(first, last) - radius,
|
|
63
|
+
Math.max(first, last) + radius,
|
|
64
|
+
])
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const vias = [
|
|
68
|
+
...params.acceptedPlans.flatMap((plan) => [
|
|
69
|
+
...(plan.via ? [plan.via] : []),
|
|
70
|
+
...(plan.additionalVias ?? []),
|
|
71
|
+
...(plan.planeEndpointVia ? [plan.planeEndpointVia] : []),
|
|
72
|
+
]),
|
|
73
|
+
...(params.reservedVias ?? []).map((reserved) => reserved.via),
|
|
74
|
+
]
|
|
75
|
+
for (const via of vias) {
|
|
76
|
+
if (!via.spanLayers.includes(targetLayer)) continue
|
|
77
|
+
const radius = via.diameter / 2 + traceWidth / 2 + clearance
|
|
78
|
+
const distance = Math.abs(via.center[across] - edge)
|
|
79
|
+
if (distance >= radius) continue
|
|
80
|
+
const extent = Math.sqrt(radius * radius - distance * distance)
|
|
81
|
+
blocked.push([via.center[along] - extent, via.center[along] + extent])
|
|
82
|
+
}
|
|
83
|
+
const gaps: [number, number][] = []
|
|
84
|
+
let cursor = minimum
|
|
85
|
+
for (const [from, to] of blocked.sort((a, b) => a[0] - b[0])) {
|
|
86
|
+
if (to <= cursor || from >= maximum) continue
|
|
87
|
+
if (from > cursor) gaps.push([cursor, Math.min(from, maximum)])
|
|
88
|
+
cursor = Math.max(cursor, to)
|
|
89
|
+
if (cursor >= maximum) break
|
|
90
|
+
}
|
|
91
|
+
if (cursor < maximum) gaps.push([cursor, maximum])
|
|
92
|
+
// Edge order gives deterministic, symmetric coverage without hard-coded tracks.
|
|
93
|
+
const tracks = gaps
|
|
94
|
+
.filter(([from, to]) => to - from > 1e-6)
|
|
95
|
+
.map(([from, to]) => (from + to) / 2)
|
|
96
|
+
return tracks.length <= 32
|
|
97
|
+
? tracks
|
|
98
|
+
: Array.from(
|
|
99
|
+
{ length: 32 },
|
|
100
|
+
(_, index) => tracks[Math.round((index * (tracks.length - 1)) / 31)]!,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { distance, distancePointToSegment, segmentsAreClear } from "./geometry"
|
|
2
|
+
import {
|
|
3
|
+
getComponentDogboneViaSiteCandidates,
|
|
4
|
+
matchComponentDogboneViaSites,
|
|
5
|
+
type DogboneViaSiteGeometryRules,
|
|
6
|
+
} from "./match-component-dogbone-via-sites"
|
|
7
|
+
import type {
|
|
8
|
+
Point2D,
|
|
9
|
+
PreparedBus,
|
|
10
|
+
PreparedConnection,
|
|
11
|
+
RoutedSegment,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
const EPSILON = 1e-9
|
|
15
|
+
const TAU = Math.PI * 2
|
|
16
|
+
interface Candidate {
|
|
17
|
+
connectionIndex: number
|
|
18
|
+
point: Point2D
|
|
19
|
+
angle: number
|
|
20
|
+
sourceSegment: RoutedSegment
|
|
21
|
+
siteIndex: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Reserve ordered source vias, then assign every remaining source via together. */
|
|
25
|
+
export function matchAngularlyOrderedLocalVias(params: {
|
|
26
|
+
buses: readonly PreparedBus[]
|
|
27
|
+
busId: string
|
|
28
|
+
rules: DogboneViaSiteGeometryRules
|
|
29
|
+
maximumOrderingStates?: number
|
|
30
|
+
maximumCompleteAssignments?: number
|
|
31
|
+
}): Map<number, Point2D> | null {
|
|
32
|
+
const { buses, rules } = params
|
|
33
|
+
const bus = buses.find((b) => b.busId === params.busId)
|
|
34
|
+
if (
|
|
35
|
+
!bus ||
|
|
36
|
+
bus.termination.type !== "boundary" ||
|
|
37
|
+
!bus.exitEdge ||
|
|
38
|
+
bus.connections.length < 3
|
|
39
|
+
)
|
|
40
|
+
return null
|
|
41
|
+
const maximumOrderingStates = params.maximumOrderingStates ?? 10_000
|
|
42
|
+
const maximumCompleteAssignments = params.maximumCompleteAssignments ?? 16
|
|
43
|
+
for (const [name, value] of Object.entries({
|
|
44
|
+
maximumOrderingStates,
|
|
45
|
+
maximumCompleteAssignments,
|
|
46
|
+
})) {
|
|
47
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
48
|
+
throw new Error(`FanoutSolver: ${name} must be a positive safe integer`)
|
|
49
|
+
}
|
|
50
|
+
const center = {
|
|
51
|
+
x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
|
|
52
|
+
y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
|
|
53
|
+
}
|
|
54
|
+
const angle = (p: Point2D) => Math.atan2(p.y - center.y, p.x - center.x)
|
|
55
|
+
const targetCoordinate = (c: PreparedConnection) => {
|
|
56
|
+
const p = c.exitTargetPoint ?? c.targetPoint
|
|
57
|
+
return bus.exitEdge === "right"
|
|
58
|
+
? p.y
|
|
59
|
+
: bus.exitEdge === "top"
|
|
60
|
+
? -p.x
|
|
61
|
+
: bus.exitEdge === "left"
|
|
62
|
+
? -p.y
|
|
63
|
+
: p.x
|
|
64
|
+
}
|
|
65
|
+
const ordered = bus.connections.toSorted(
|
|
66
|
+
(a, b) =>
|
|
67
|
+
targetCoordinate(a) - targetCoordinate(b) ||
|
|
68
|
+
a.connectionIndex - b.connectionIndex,
|
|
69
|
+
)
|
|
70
|
+
const sourceAngles: number[] = []
|
|
71
|
+
for (const c of ordered) {
|
|
72
|
+
if (distance(c.sourcePoint, center) < EPSILON) return null
|
|
73
|
+
let a = angle(c.sourcePoint)
|
|
74
|
+
while (a < (sourceAngles.at(-1) ?? a) - EPSILON) a += TAU
|
|
75
|
+
sourceAngles.push(a)
|
|
76
|
+
}
|
|
77
|
+
if (sourceAngles.at(-1)! - sourceAngles[0]! >= TAU - EPSILON) return null
|
|
78
|
+
const connections = new Map(
|
|
79
|
+
buses.flatMap((b) =>
|
|
80
|
+
b.connections.map((c) => [c.connectionIndex, c] as const),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
const orderedIndex = new Map(ordered.map((c, i) => [c.connectionIndex, i]))
|
|
84
|
+
const sites = new Map<string, number>()
|
|
85
|
+
const candidateGroups = new Map<number, Candidate[]>()
|
|
86
|
+
for (const c of connections.values())
|
|
87
|
+
candidateGroups.set(c.connectionIndex, [])
|
|
88
|
+
for (const raw of getComponentDogboneViaSiteCandidates(buses, rules)) {
|
|
89
|
+
const c = connections.get(raw.connectionIndex)!
|
|
90
|
+
const key = `${raw.point.x.toFixed(9)},${raw.point.y.toFixed(9)}`
|
|
91
|
+
if (!sites.has(key)) sites.set(key, sites.size)
|
|
92
|
+
const index = orderedIndex.get(raw.connectionIndex)
|
|
93
|
+
const sourceAngle = angle(c.sourcePoint)
|
|
94
|
+
const a =
|
|
95
|
+
index === undefined
|
|
96
|
+
? angle(raw.point)
|
|
97
|
+
: sourceAngles[index]! +
|
|
98
|
+
Math.atan2(
|
|
99
|
+
Math.sin(angle(raw.point) - sourceAngle),
|
|
100
|
+
Math.cos(angle(raw.point) - sourceAngle),
|
|
101
|
+
)
|
|
102
|
+
candidateGroups.get(raw.connectionIndex)!.push({
|
|
103
|
+
...raw,
|
|
104
|
+
angle: a,
|
|
105
|
+
siteIndex: sites.get(key)!,
|
|
106
|
+
sourceSegment: {
|
|
107
|
+
start: c.sourcePoint,
|
|
108
|
+
end: raw.point,
|
|
109
|
+
layer: c.sourceLayer,
|
|
110
|
+
width: rules.traceWidth,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
const groups = [...candidateGroups.entries()].map(
|
|
115
|
+
([connectionIndex, candidates]) => ({ connectionIndex, candidates }),
|
|
116
|
+
)
|
|
117
|
+
if (groups.some((g) => g.candidates.length === 0)) return null
|
|
118
|
+
const domains = ordered.map((c, i) =>
|
|
119
|
+
candidateGroups
|
|
120
|
+
.get(c.connectionIndex)!
|
|
121
|
+
.toSorted(
|
|
122
|
+
(a, b) =>
|
|
123
|
+
Math.abs(a.angle - sourceAngles[i]!) -
|
|
124
|
+
Math.abs(b.angle - sourceAngles[i]!) ||
|
|
125
|
+
distance(b.point, center) - distance(a.point, center),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
const requiredHoleSeparation = rules.viaHoleDiameter
|
|
129
|
+
? rules.viaHoleDiameter + (rules.holeToHoleClearance ?? rules.clearance)
|
|
130
|
+
: 0
|
|
131
|
+
const requiredViaToTraceSeparation =
|
|
132
|
+
rules.viaDiameter / 2 + rules.traceWidth / 2 + rules.clearance
|
|
133
|
+
const compatible = (a: Candidate, b: Candidate) => {
|
|
134
|
+
const share =
|
|
135
|
+
rules.canShareCopper?.(a.connectionIndex, b.connectionIndex) ?? false
|
|
136
|
+
const minimumViaDistance = share
|
|
137
|
+
? requiredHoleSeparation
|
|
138
|
+
: Math.max(requiredHoleSeparation, rules.viaDiameter + rules.clearance)
|
|
139
|
+
if (distance(a.point, b.point) < minimumViaDistance - EPSILON) return false
|
|
140
|
+
if (share) return true
|
|
141
|
+
return (
|
|
142
|
+
distancePointToSegment(
|
|
143
|
+
a.point,
|
|
144
|
+
b.sourceSegment.start,
|
|
145
|
+
b.sourceSegment.end,
|
|
146
|
+
) >=
|
|
147
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
148
|
+
distancePointToSegment(
|
|
149
|
+
b.point,
|
|
150
|
+
a.sourceSegment.start,
|
|
151
|
+
a.sourceSegment.end,
|
|
152
|
+
) >=
|
|
153
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
154
|
+
segmentsAreClear(a.sourceSegment, b.sourceSegment, rules.clearance)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const forbidden = new Map<Candidate, Set<Candidate>>()
|
|
158
|
+
for (const domain of domains)
|
|
159
|
+
for (const a of domain) {
|
|
160
|
+
const conflicts = new Set<Candidate>()
|
|
161
|
+
for (const group of groups)
|
|
162
|
+
for (const b of group.candidates) {
|
|
163
|
+
if (a.connectionIndex !== b.connectionIndex && !compatible(a, b))
|
|
164
|
+
conflicts.add(b)
|
|
165
|
+
}
|
|
166
|
+
forbidden.set(a, conflicts)
|
|
167
|
+
}
|
|
168
|
+
const chosen: Candidate[] = []
|
|
169
|
+
// A perfect matching is necessary because distinct drilled holes cannot use
|
|
170
|
+
// the same site. It is a pruning test; native matching still validates copper.
|
|
171
|
+
const canUseDistinctSiteCheck =
|
|
172
|
+
requiredHoleSeparation > EPSILON || !rules.canShareCopper
|
|
173
|
+
const remainingSitesCanMatch = () => {
|
|
174
|
+
if (!canUseDistinctSiteCheck) return true
|
|
175
|
+
const selected = new Map(
|
|
176
|
+
chosen.map((c) => [c.connectionIndex, c.siteIndex]),
|
|
177
|
+
)
|
|
178
|
+
const allowed = groups
|
|
179
|
+
.map((g) =>
|
|
180
|
+
g.candidates
|
|
181
|
+
.filter(
|
|
182
|
+
(c) =>
|
|
183
|
+
(!selected.has(c.connectionIndex) ||
|
|
184
|
+
selected.get(c.connectionIndex) === c.siteIndex) &&
|
|
185
|
+
chosen.every((a) => !forbidden.get(a)!.has(c)),
|
|
186
|
+
)
|
|
187
|
+
.map((c) => c.siteIndex),
|
|
188
|
+
)
|
|
189
|
+
.sort((a, b) => a.length - b.length)
|
|
190
|
+
if (allowed.some((d) => d.length === 0)) return false
|
|
191
|
+
const owner = new Int32Array(sites.size).fill(-1)
|
|
192
|
+
const augment = (index: number, visited: Uint8Array): boolean => {
|
|
193
|
+
for (const site of allowed[index]!) {
|
|
194
|
+
if (visited[site]) continue
|
|
195
|
+
visited[site] = 1
|
|
196
|
+
if (owner[site] === -1 || augment(owner[site]!, visited)) {
|
|
197
|
+
owner[site] = index
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
return allowed.every((_, i) => augment(i, new Uint8Array(sites.size)))
|
|
204
|
+
}
|
|
205
|
+
let states = 0
|
|
206
|
+
let completeAssignments = 0
|
|
207
|
+
let result: Map<number, Point2D> | null = null
|
|
208
|
+
const search = (index: number): boolean => {
|
|
209
|
+
if (++states > maximumOrderingStates) return false
|
|
210
|
+
if (index === domains.length) {
|
|
211
|
+
if (++completeAssignments > maximumCompleteAssignments) return false
|
|
212
|
+
const fixed = new Map(rules.fixedViaPointsByConnectionIndex)
|
|
213
|
+
for (const c of chosen) fixed.set(c.connectionIndex, c.point)
|
|
214
|
+
result = matchComponentDogboneViaSites(buses, {
|
|
215
|
+
...rules,
|
|
216
|
+
fixedViaPointsByConnectionIndex: fixed,
|
|
217
|
+
})
|
|
218
|
+
return result !== null
|
|
219
|
+
}
|
|
220
|
+
for (const candidate of domains[index]!) {
|
|
221
|
+
if (
|
|
222
|
+
candidate.angle < (chosen.at(-1)?.angle ?? -Infinity) - EPSILON ||
|
|
223
|
+
chosen.some((c) => forbidden.get(c)!.has(candidate))
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
chosen.push(candidate)
|
|
227
|
+
if (remainingSitesCanMatch() && search(index + 1)) return true
|
|
228
|
+
chosen.pop()
|
|
229
|
+
if (
|
|
230
|
+
states > maximumOrderingStates ||
|
|
231
|
+
completeAssignments >= maximumCompleteAssignments
|
|
232
|
+
)
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
search(0)
|
|
238
|
+
return result
|
|
239
|
+
}
|