@tscircuit/fanout-solver 0.0.63 → 0.0.65
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/lib/fanout-solver.ts +269 -18
- 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 +44 -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/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
|
|
@@ -1551,6 +1719,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1551
1719
|
promotedPlaneReservationBusIds?: readonly string[]
|
|
1552
1720
|
preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
|
|
1553
1721
|
planeReservationRetryCount?: number
|
|
1722
|
+
boundaryRecovery?: {
|
|
1723
|
+
busId: string
|
|
1724
|
+
preferOutward: boolean
|
|
1725
|
+
perpendicularSide: -1 | 1
|
|
1726
|
+
}
|
|
1554
1727
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1555
1728
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1556
1729
|
// An outside-package singleton escape can depend on the completed signal
|
|
@@ -1601,12 +1774,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1601
1774
|
)
|
|
1602
1775
|
const useConfiguredDensePlaneRouting =
|
|
1603
1776
|
configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
|
|
1777
|
+
const useBoundaryRecovery = params.boundaryRecovery !== undefined
|
|
1778
|
+
const useJointPlaneRepair =
|
|
1779
|
+
useConfiguredDensePlaneRouting || useBoundaryRecovery
|
|
1604
1780
|
// A completed boundary assignment remains worth repairing jointly after
|
|
1605
1781
|
// plane reservations change; a greedy refill can discard that assignment.
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
(
|
|
1609
|
-
|
|
1782
|
+
const useJointPlaneSelection =
|
|
1783
|
+
useBoundaryRecovery ||
|
|
1784
|
+
(useAdaptiveDensePlaneRouting &&
|
|
1785
|
+
((params.planeReservationRetryCount ?? 0) === 0 ||
|
|
1786
|
+
Boolean(params.preferredBoundaryViaPoints)))
|
|
1610
1787
|
const matchLengthsAfterPlanes =
|
|
1611
1788
|
useConfiguredDensePlaneRouting &&
|
|
1612
1789
|
params.lengthMatchingStage !== "before-planes"
|
|
@@ -1852,7 +2029,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1852
2029
|
process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ??
|
|
1853
2030
|
(process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS
|
|
1854
2031
|
? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS]
|
|
1855
|
-
:
|
|
2032
|
+
: params.boundaryRecovery
|
|
2033
|
+
? [params.boundaryRecovery.busId]
|
|
2034
|
+
: [])
|
|
1856
2035
|
const boundaryBuses =
|
|
1857
2036
|
debugBoundaryOrder.length > 0
|
|
1858
2037
|
? initiallySortedBoundaryBuses.toSorted((first, second) => {
|
|
@@ -1914,6 +2093,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1914
2093
|
]
|
|
1915
2094
|
}
|
|
1916
2095
|
const unroutablePlaneBusIds = new Set<string>()
|
|
2096
|
+
let failedWideBoundaryBus: PreparedBus | undefined
|
|
1917
2097
|
debugDense(
|
|
1918
2098
|
"start",
|
|
1919
2099
|
boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
|
|
@@ -2015,6 +2195,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2015
2195
|
if (entersNeighboringSourceField)
|
|
2016
2196
|
preferBoundaryOutwardByBusId.set(bus.busId, false)
|
|
2017
2197
|
}
|
|
2198
|
+
if (params.boundaryRecovery) {
|
|
2199
|
+
preferredBoundaryPerpendicularSideByBusId.set(
|
|
2200
|
+
params.boundaryRecovery.busId,
|
|
2201
|
+
params.boundaryRecovery.perpendicularSide,
|
|
2202
|
+
)
|
|
2203
|
+
preferBoundaryOutwardByBusId.set(
|
|
2204
|
+
params.boundaryRecovery.busId,
|
|
2205
|
+
params.boundaryRecovery.preferOutward,
|
|
2206
|
+
)
|
|
2207
|
+
}
|
|
2018
2208
|
const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
|
|
2019
2209
|
if (debugFlippedBoundaryBus) {
|
|
2020
2210
|
preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
|
|
@@ -2653,6 +2843,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2653
2843
|
allowBoundarySideViaFallback: bus.connections.length === 1,
|
|
2654
2844
|
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2655
2845
|
adaptiveWindingRouteOrder,
|
|
2846
|
+
allowFixedViaReservedExitFallback: useBoundaryRecovery,
|
|
2656
2847
|
// Retain pad-aligned channels even when plane sites are reserved
|
|
2657
2848
|
// adaptively; the boundary grid can fence off a turning wide bus.
|
|
2658
2849
|
alignWindingGridToPads:
|
|
@@ -3035,6 +3226,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3035
3226
|
}
|
|
3036
3227
|
}
|
|
3037
3228
|
if (!busPlans) {
|
|
3229
|
+
if (bus.connections.length >= 8) failedWideBoundaryBus ??= bus
|
|
3038
3230
|
debugDense("route:failed", bus.busId)
|
|
3039
3231
|
return false
|
|
3040
3232
|
}
|
|
@@ -3365,6 +3557,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3365
3557
|
promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
|
|
3366
3558
|
): Map<number, { x: number; y: number }> | null => {
|
|
3367
3559
|
feasibleAlternatePlanePlans = []
|
|
3560
|
+
matchedPlaneBusesInRoutingOrder = null
|
|
3368
3561
|
const fixedBoundaryViaPoints = new Map(
|
|
3369
3562
|
candidatePlans.flatMap((plan) =>
|
|
3370
3563
|
plan.via
|
|
@@ -3431,11 +3624,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3431
3624
|
if (retainedViaPoints)
|
|
3432
3625
|
return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints])
|
|
3433
3626
|
if (
|
|
3434
|
-
|
|
3627
|
+
useJointPlaneRepair ||
|
|
3435
3628
|
process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
|
|
3436
3629
|
) {
|
|
3437
3630
|
let incrementalViaPoints = new Map(fixedBoundaryViaPoints)
|
|
3438
|
-
|
|
3631
|
+
// Reservations guided the boundary search, but their copper is
|
|
3632
|
+
// still uncommitted. Recovery must choose local and longer plane
|
|
3633
|
+
// escapes together instead of locking every provisional site.
|
|
3634
|
+
const matchedPlaneBuses = useBoundaryRecovery
|
|
3635
|
+
? []
|
|
3636
|
+
: [...activeBoundaryReservationPlaneBuses]
|
|
3439
3637
|
for (const planeBus of matchedPlaneBuses) {
|
|
3440
3638
|
for (const connection of planeBus.connections) {
|
|
3441
3639
|
const reservedPoint = fixedViaPointsByConnectionIndex.get(
|
|
@@ -3581,7 +3779,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3581
3779
|
independentlyUnmatchablePlaneBuses.map((bus) => bus.busId),
|
|
3582
3780
|
)
|
|
3583
3781
|
if (
|
|
3584
|
-
!
|
|
3782
|
+
!useJointPlaneRepair &&
|
|
3585
3783
|
process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1"
|
|
3586
3784
|
) {
|
|
3587
3785
|
return null
|
|
@@ -3596,7 +3794,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3596
3794
|
? 10_000
|
|
3597
3795
|
: useConfiguredDensePlaneRouting
|
|
3598
3796
|
? 3_000_000
|
|
3599
|
-
:
|
|
3797
|
+
: useBoundaryRecovery
|
|
3798
|
+
? 10_000
|
|
3799
|
+
: 1_000),
|
|
3600
3800
|
)
|
|
3601
3801
|
const maximumAlternatePlaneRoutes = Number(
|
|
3602
3802
|
process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
|
|
@@ -3774,7 +3974,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3774
3974
|
}
|
|
3775
3975
|
let alternatePlanePlans: FanoutRoutePlan[] | null
|
|
3776
3976
|
if (
|
|
3777
|
-
|
|
3977
|
+
useJointPlaneRepair ||
|
|
3778
3978
|
process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1"
|
|
3779
3979
|
) {
|
|
3780
3980
|
type IndependentPlaneRouteCandidate = {
|
|
@@ -3795,7 +3995,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3795
3995
|
})),
|
|
3796
3996
|
}),
|
|
3797
3997
|
)
|
|
3798
|
-
if (
|
|
3998
|
+
if (useJointPlaneSelection) {
|
|
3799
3999
|
const acceptedPlans = [
|
|
3800
4000
|
...candidatePlans,
|
|
3801
4001
|
...feasibleAlternatePlanePlans,
|
|
@@ -4106,7 +4306,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
4106
4306
|
)
|
|
4107
4307
|
if (!alternatePlanePlans) return null
|
|
4108
4308
|
feasibleAlternatePlanePlans = alternatePlanePlans
|
|
4109
|
-
if (
|
|
4309
|
+
if (useJointPlaneSelection) {
|
|
4110
4310
|
matchedPlaneBusesInRoutingOrder = []
|
|
4111
4311
|
return new Map([
|
|
4112
4312
|
...fixedBoundaryViaPoints,
|
|
@@ -4620,6 +4820,33 @@ export class FanoutSolver extends BaseSolver {
|
|
|
4620
4820
|
denseRoutingStrategy: "boundary-aligned",
|
|
4621
4821
|
})
|
|
4622
4822
|
if (boundaryAlignedState) return boundaryAlignedState
|
|
4823
|
+
// A later wide bus can be fenced by provisional through-vias even
|
|
4824
|
+
// though its requested exit order is routable. After both existing
|
|
4825
|
+
// grids fail, give that bus first choice of the field and retry the
|
|
4826
|
+
// four dogbone orientations. The marker bounds recursion and enables
|
|
4827
|
+
// joint selection of the remaining uncommitted plane escapes.
|
|
4828
|
+
if (!useBoundaryRecovery && failedWideBoundaryBus) {
|
|
4829
|
+
const busId = failedWideBoundaryBus.busId
|
|
4830
|
+
const outward = preferBoundaryOutwardByBusId.get(busId) ?? true
|
|
4831
|
+
const side = preferredBoundaryPerpendicularSideByBusId.get(busId) ?? 1
|
|
4832
|
+
for (const [preferOutward, perpendicularSide] of [
|
|
4833
|
+
[!outward, -side],
|
|
4834
|
+
[!outward, side],
|
|
4835
|
+
[outward, -side],
|
|
4836
|
+
[outward, side],
|
|
4837
|
+
] as const) {
|
|
4838
|
+
const recoveredState =
|
|
4839
|
+
yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
4840
|
+
...params,
|
|
4841
|
+
boundaryRecovery: {
|
|
4842
|
+
busId,
|
|
4843
|
+
preferOutward,
|
|
4844
|
+
perpendicularSide: perpendicularSide as -1 | 1,
|
|
4845
|
+
},
|
|
4846
|
+
})
|
|
4847
|
+
if (recoveredState) return recoveredState
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4623
4850
|
}
|
|
4624
4851
|
if (
|
|
4625
4852
|
!usePadAlignedDenseRouting ||
|
|
@@ -5083,6 +5310,23 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5083
5310
|
|
|
5084
5311
|
let mixedTerminationState: MixedTerminationState | null = null
|
|
5085
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
|
+
) {
|
|
5086
5330
|
const denseSolver = this.createWorkSolver(
|
|
5087
5331
|
"DenseMixedTerminationSolver",
|
|
5088
5332
|
this.routeDenseThroughAllMixedTerminationSteps({
|
|
@@ -5282,6 +5526,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5282
5526
|
return {
|
|
5283
5527
|
summary,
|
|
5284
5528
|
plans,
|
|
5529
|
+
...(mixedTerminationState?.stopAfterCompleteValidation &&
|
|
5530
|
+
validation?.valid
|
|
5531
|
+
? { stopAfterCompleteValidation: true as const }
|
|
5532
|
+
: {}),
|
|
5285
5533
|
blockingBusIds: [...blockingBusCounts.entries()]
|
|
5286
5534
|
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
5287
5535
|
.map(([busId]) => busId),
|
|
@@ -5302,7 +5550,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5302
5550
|
if (
|
|
5303
5551
|
bestAttempt.summary.routedConnectionCount ===
|
|
5304
5552
|
this.inputSrj.connections.length &&
|
|
5305
|
-
|
|
5553
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5554
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5306
5555
|
) {
|
|
5307
5556
|
return bestAttempt
|
|
5308
5557
|
}
|
|
@@ -5319,7 +5568,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5319
5568
|
if (
|
|
5320
5569
|
bestAttempt.summary.routedConnectionCount ===
|
|
5321
5570
|
this.inputSrj.connections.length &&
|
|
5322
|
-
|
|
5571
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5572
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5323
5573
|
) {
|
|
5324
5574
|
return bestAttempt
|
|
5325
5575
|
}
|
|
@@ -5873,10 +6123,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5873
6123
|
bestScore: this.bestAttempt.summary.score,
|
|
5874
6124
|
}
|
|
5875
6125
|
if (
|
|
5876
|
-
this.
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
6126
|
+
(attempt.stopAfterCompleteValidation && this.hasCompleteBestAttempt()) ||
|
|
6127
|
+
(this.groupedBeamEvaluated &&
|
|
6128
|
+
attempt.summary.routedConnectionCount ===
|
|
6129
|
+
this.inputSrj.connections.length &&
|
|
6130
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0)
|
|
5880
6131
|
) {
|
|
5881
6132
|
this.completeBestAttemptEndpoints()
|
|
5882
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
|
+
}
|