@tscircuit/fanout-solver 0.0.58 → 0.0.60
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 +5 -0
- package/lib/fanout-solver.ts +313 -14
- package/lib/match-bus-lengths.ts +123 -1
- package/lib/route-bus.ts +208 -13
- package/lib/route-via-minimal-winding.ts +39 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -330,6 +330,11 @@ the solver commit, dataset revision, configuration, solve totals, every sample's
|
|
|
330
330
|
status and timing, and partial routing/validation counts. Reports are saved after
|
|
331
331
|
every completed sample, including the total selected count to identify incomplete runs.
|
|
332
332
|
Timed-out workers do not retain their in-flight routing counts.
|
|
333
|
+
Every solved case also writes `benchmark-results/<sample-id>.svg`. These SVGs
|
|
334
|
+
are committed so route changes can be reviewed in Git. A run replaces the selected
|
|
335
|
+
cases' snapshots and removes their stale SVGs if they no longer solve; filtered
|
|
336
|
+
runs preserve unselected snapshots. JSON reports and captured inputs remain
|
|
337
|
+
ignored. CI includes the SVGs in its benchmark artifacts.
|
|
333
338
|
Compare reports with the same budgets to track progress. Solved means validated
|
|
334
339
|
AM62L fanout, not RAM fanout or downstream inter-chip routing. Partial, error,
|
|
335
340
|
and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { refineAdaptivePlaneReservationCore } from "./refine-adaptive-plane-rese
|
|
|
5
5
|
import { shouldUseAdaptiveDensePlaneRouting } from "./should-use-adaptive-dense-plane-routing"
|
|
6
6
|
import { type GraphicsObject, mergeGraphics } from "graphics-debug"
|
|
7
7
|
import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getCornerBandSide,
|
|
10
|
+
getDirectionForExitEdge,
|
|
11
|
+
getExitEdgeForDirection,
|
|
12
|
+
} from "./boundary-exit"
|
|
9
13
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
10
14
|
import {
|
|
11
15
|
type CompleteOriginalEndpointsResult,
|
|
@@ -1397,9 +1401,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1397
1401
|
denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
|
|
1398
1402
|
lengthMatchingStage?: "before-planes" | "after-planes"
|
|
1399
1403
|
promotedPlaneReservationBusIds?: readonly string[]
|
|
1404
|
+
preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
|
|
1400
1405
|
planeReservationRetryCount?: number
|
|
1401
1406
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1402
1407
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1408
|
+
// An outside-package singleton escape can depend on the completed signal
|
|
1409
|
+
// field. Keep those sites as preferences when plane reservations change;
|
|
1410
|
+
// actual conflicts may still move.
|
|
1411
|
+
let boundaryViaPointsForRetry = params.preferredBoundaryViaPoints
|
|
1403
1412
|
const usePadAlignedDenseRouting =
|
|
1404
1413
|
params.denseRoutingStrategy !== "boundary-aligned"
|
|
1405
1414
|
const debugDense = (...values: unknown[]) => {
|
|
@@ -1444,9 +1453,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1444
1453
|
)
|
|
1445
1454
|
const useConfiguredDensePlaneRouting =
|
|
1446
1455
|
configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
|
|
1456
|
+
// A completed boundary assignment remains worth repairing jointly after
|
|
1457
|
+
// plane reservations change; a greedy refill can discard that assignment.
|
|
1447
1458
|
const useAdaptiveJointPlaneSelection =
|
|
1448
1459
|
useAdaptiveDensePlaneRouting &&
|
|
1449
|
-
(params.planeReservationRetryCount ?? 0) === 0
|
|
1460
|
+
((params.planeReservationRetryCount ?? 0) === 0 ||
|
|
1461
|
+
Boolean(params.preferredBoundaryViaPoints))
|
|
1450
1462
|
const matchLengthsAfterPlanes =
|
|
1451
1463
|
useConfiguredDensePlaneRouting &&
|
|
1452
1464
|
params.lengthMatchingStage !== "before-planes"
|
|
@@ -1871,19 +1883,45 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1871
1883
|
)
|
|
1872
1884
|
})
|
|
1873
1885
|
: []
|
|
1886
|
+
// A corner singleton on the outward source row must leave before its
|
|
1887
|
+
// surrounding wide bus closes the shared target-layer corridor.
|
|
1874
1888
|
const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses
|
|
1875
1889
|
? singletonBoundaryBuses.filter((singletonBus) => {
|
|
1876
1890
|
const containingWideBus = getContainingWideSourceField(singletonBus)
|
|
1877
1891
|
const singletonTargetLayer =
|
|
1878
1892
|
params.busLayerAssignments[singletonBus.busId]
|
|
1893
|
+
if (!containingWideBus || !singletonTargetLayer) return false
|
|
1879
1894
|
const containingWideLayers =
|
|
1880
|
-
containingWideBus
|
|
1881
|
-
containingWideBus
|
|
1895
|
+
containingWideBus.routableEscapeLayers ??
|
|
1896
|
+
containingWideBus.allowedLayers ??
|
|
1882
1897
|
[]
|
|
1898
|
+
const sourceAxis =
|
|
1899
|
+
singletonBus.exitEdge === "left" ||
|
|
1900
|
+
singletonBus.exitEdge === "right"
|
|
1901
|
+
? "x"
|
|
1902
|
+
: "y"
|
|
1903
|
+
const wideCoordinates = containingWideBus.connections.map(
|
|
1904
|
+
(connection) => connection.sourcePoint[sourceAxis],
|
|
1905
|
+
)
|
|
1906
|
+
const outwardSourceCoordinate =
|
|
1907
|
+
singletonBus.exitEdge === "left" ||
|
|
1908
|
+
singletonBus.exitEdge === "bottom"
|
|
1909
|
+
? Math.min(...wideCoordinates)
|
|
1910
|
+
: Math.max(...wideCoordinates)
|
|
1911
|
+
const isOnOutwardSourceEdge = singletonBus.connections.every(
|
|
1912
|
+
(connection) =>
|
|
1913
|
+
Math.abs(
|
|
1914
|
+
connection.sourcePoint[sourceAxis] - outwardSourceCoordinate,
|
|
1915
|
+
) < 1e-9,
|
|
1916
|
+
)
|
|
1883
1917
|
return Boolean(
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1918
|
+
!containingWideLayers.includes(singletonTargetLayer) ||
|
|
1919
|
+
(useAdaptiveDensePlaneRouting &&
|
|
1920
|
+
isOnOutwardSourceEdge &&
|
|
1921
|
+
getCornerBandSide(
|
|
1922
|
+
singletonBus.exitEdge,
|
|
1923
|
+
singletonBus.preferredExit,
|
|
1924
|
+
)),
|
|
1887
1925
|
)
|
|
1888
1926
|
})
|
|
1889
1927
|
: []
|
|
@@ -1948,14 +1986,35 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1948
1986
|
: []),
|
|
1949
1987
|
...singletonDeferralCandidates.filter((bus) => {
|
|
1950
1988
|
const containingBus = getContainingWideSourceField(bus)
|
|
1951
|
-
const
|
|
1989
|
+
const sourcePoint = bus.connections[0]!.sourcePoint
|
|
1990
|
+
const componentCenter = {
|
|
1991
|
+
x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
|
|
1992
|
+
y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
|
|
1993
|
+
}
|
|
1994
|
+
const boundaryDirection = bus.exitEdge
|
|
1995
|
+
? getDirectionForExitEdge(bus.exitEdge)
|
|
1996
|
+
: bus.direction
|
|
1997
|
+
const inwardProjection =
|
|
1998
|
+
boundaryDirection === "right"
|
|
1999
|
+
? componentCenter.x - sourcePoint.x
|
|
2000
|
+
: boundaryDirection === "left"
|
|
2001
|
+
? sourcePoint.x - componentCenter.x
|
|
2002
|
+
: boundaryDirection === "up"
|
|
2003
|
+
? componentCenter.y - sourcePoint.y
|
|
2004
|
+
: sourcePoint.y - componentCenter.y
|
|
2005
|
+
// Crossing the component can consume an embedded singleton's source
|
|
2006
|
+
// dogbone even when the target layers differ. Keep outward boundary
|
|
2007
|
+
// escapes provisional unless they share the wide bus's target layer.
|
|
2008
|
+
const reserveEmbeddedSourceEscape =
|
|
1952
2009
|
usePadAlignedDenseRouting &&
|
|
1953
2010
|
!useConfiguredDensePlaneRouting &&
|
|
1954
2011
|
containingBus &&
|
|
1955
|
-
params.busLayerAssignments[containingBus.busId] ===
|
|
1956
|
-
params.busLayerAssignments[bus.busId]
|
|
2012
|
+
(params.busLayerAssignments[containingBus.busId] ===
|
|
2013
|
+
params.busLayerAssignments[bus.busId] ||
|
|
2014
|
+
inwardProjection > 1e-9)
|
|
1957
2015
|
return (
|
|
1958
|
-
!leadingWideSingletonBuses.includes(bus) &&
|
|
2016
|
+
!leadingWideSingletonBuses.includes(bus) &&
|
|
2017
|
+
!reserveEmbeddedSourceEscape
|
|
1959
2018
|
)
|
|
1960
2019
|
}),
|
|
1961
2020
|
...(hasThreeWideBoundaryBuses
|
|
@@ -1994,6 +2053,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1994
2053
|
traceWidth: this.config.traceWidth,
|
|
1995
2054
|
clearance: this.config.clearance,
|
|
1996
2055
|
maximumSearchStates: 100_000,
|
|
2056
|
+
preferredViaPointsByConnectionIndex:
|
|
2057
|
+
params.preferredBoundaryViaPoints,
|
|
1997
2058
|
preferredBoundaryPerpendicularSideByBusId,
|
|
1998
2059
|
preferBoundaryOutwardByBusId,
|
|
1999
2060
|
additionalObstacles: denseAdditionalObstacles,
|
|
@@ -2012,6 +2073,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2012
2073
|
traceWidth: this.config.traceWidth,
|
|
2013
2074
|
clearance: this.config.clearance,
|
|
2014
2075
|
maximumSearchStates: 20_000,
|
|
2076
|
+
preferredViaPointsByConnectionIndex:
|
|
2077
|
+
params.preferredBoundaryViaPoints,
|
|
2015
2078
|
preferredBoundaryPerpendicularSideByBusId,
|
|
2016
2079
|
preferBoundaryOutwardByBusId,
|
|
2017
2080
|
additionalObstacles: denseAdditionalObstacles,
|
|
@@ -2070,6 +2133,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2070
2133
|
traceWidth: this.config.traceWidth,
|
|
2071
2134
|
clearance: this.config.clearance,
|
|
2072
2135
|
maximumSearchStates: 100_000,
|
|
2136
|
+
preferredViaPointsByConnectionIndex:
|
|
2137
|
+
params.preferredBoundaryViaPoints,
|
|
2073
2138
|
preferredBoundaryPerpendicularSideByBusId,
|
|
2074
2139
|
preferBoundaryOutwardByBusId,
|
|
2075
2140
|
fixedViaPointsByConnectionIndex: seedViaPoints,
|
|
@@ -2124,6 +2189,117 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2124
2189
|
bus,
|
|
2125
2190
|
]),
|
|
2126
2191
|
]
|
|
2192
|
+
const areAdjacentInvertedNarrowBuses = (
|
|
2193
|
+
first: PreparedBus,
|
|
2194
|
+
second: PreparedBus,
|
|
2195
|
+
): boolean => {
|
|
2196
|
+
if (
|
|
2197
|
+
!usePadAlignedDenseRouting ||
|
|
2198
|
+
useConfiguredDensePlaneRouting ||
|
|
2199
|
+
first.componentId !== second.componentId ||
|
|
2200
|
+
first.exitEdge !== second.exitEdge ||
|
|
2201
|
+
first.direction !== second.direction ||
|
|
2202
|
+
getCornerBandSide(first.exitEdge, first.preferredExit) !==
|
|
2203
|
+
getCornerBandSide(second.exitEdge, second.preferredExit) ||
|
|
2204
|
+
params.busLayerAssignments[first.busId] !==
|
|
2205
|
+
params.busLayerAssignments[second.busId] ||
|
|
2206
|
+
getContainingWideSourceField(first) !==
|
|
2207
|
+
getContainingWideSourceField(second) ||
|
|
2208
|
+
!getContainingWideSourceField(first)
|
|
2209
|
+
)
|
|
2210
|
+
return false
|
|
2211
|
+
const firstCenter = {
|
|
2212
|
+
x:
|
|
2213
|
+
first.connections.reduce(
|
|
2214
|
+
(sum, connection) => sum + connection.sourcePoint.x,
|
|
2215
|
+
0,
|
|
2216
|
+
) / first.connections.length,
|
|
2217
|
+
y:
|
|
2218
|
+
first.connections.reduce(
|
|
2219
|
+
(sum, connection) => sum + connection.sourcePoint.y,
|
|
2220
|
+
0,
|
|
2221
|
+
) / first.connections.length,
|
|
2222
|
+
}
|
|
2223
|
+
const secondCenter = {
|
|
2224
|
+
x:
|
|
2225
|
+
second.connections.reduce(
|
|
2226
|
+
(sum, connection) => sum + connection.sourcePoint.x,
|
|
2227
|
+
0,
|
|
2228
|
+
) / second.connections.length,
|
|
2229
|
+
y:
|
|
2230
|
+
second.connections.reduce(
|
|
2231
|
+
(sum, connection) => sum + connection.sourcePoint.y,
|
|
2232
|
+
0,
|
|
2233
|
+
) / second.connections.length,
|
|
2234
|
+
}
|
|
2235
|
+
if (
|
|
2236
|
+
Math.hypot(
|
|
2237
|
+
firstCenter.x - secondCenter.x,
|
|
2238
|
+
firstCenter.y - secondCenter.y,
|
|
2239
|
+
) >
|
|
2240
|
+
1.5 * Math.min(first.pitchX, first.pitchY)
|
|
2241
|
+
)
|
|
2242
|
+
return false
|
|
2243
|
+
const axis =
|
|
2244
|
+
first.exitEdge === "left" || first.exitEdge === "right" ? "y" : "x"
|
|
2245
|
+
const targetTrack = (bus: PreparedBus) =>
|
|
2246
|
+
bus.connections.reduce(
|
|
2247
|
+
(sum, connection) =>
|
|
2248
|
+
sum +
|
|
2249
|
+
(connection.exitTargetPoint ?? connection.targetPoint)[axis],
|
|
2250
|
+
0,
|
|
2251
|
+
) / bus.connections.length
|
|
2252
|
+
return (
|
|
2253
|
+
(firstCenter[axis] - secondCenter[axis]) *
|
|
2254
|
+
(targetTrack(first) - targetTrack(second)) <
|
|
2255
|
+
-1e-9
|
|
2256
|
+
)
|
|
2257
|
+
}
|
|
2258
|
+
// Preserve a centered pair's channel before its leading singleton. A
|
|
2259
|
+
// turning pair instead leaves its adjacent singleton room to escape
|
|
2260
|
+
// before searching for an outside-package via.
|
|
2261
|
+
const centeredPairPromotionGroups = new Set<string>()
|
|
2262
|
+
for (const singleton of multiLayerLeadingSingletonBuses) {
|
|
2263
|
+
if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
|
|
2264
|
+
continue
|
|
2265
|
+
for (const pair of boundaryBuses) {
|
|
2266
|
+
if (
|
|
2267
|
+
pair.connections.length !== 2 ||
|
|
2268
|
+
!areAdjacentInvertedNarrowBuses(pair, singleton)
|
|
2269
|
+
)
|
|
2270
|
+
continue
|
|
2271
|
+
const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
|
|
2272
|
+
const singletonIndex =
|
|
2273
|
+
denseBoundaryBusesInRoutingOrder.indexOf(singleton)
|
|
2274
|
+
if (pairIndex > singletonIndex) {
|
|
2275
|
+
denseBoundaryBusesInRoutingOrder.splice(pairIndex, 1)
|
|
2276
|
+
denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 0, pair)
|
|
2277
|
+
centeredPairPromotionGroups.add(
|
|
2278
|
+
`${pair.componentId}:${pair.exitEdge}`,
|
|
2279
|
+
)
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
for (const pair of boundaryBuses) {
|
|
2284
|
+
if (
|
|
2285
|
+
!centeredPairPromotionGroups.has(
|
|
2286
|
+
`${pair.componentId}:${pair.exitEdge}`,
|
|
2287
|
+
) ||
|
|
2288
|
+
pair.connections.length !== 2 ||
|
|
2289
|
+
!getCornerBandSide(pair.exitEdge, pair.preferredExit)
|
|
2290
|
+
)
|
|
2291
|
+
continue
|
|
2292
|
+
for (const singleton of singletonBoundaryBuses) {
|
|
2293
|
+
if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
|
|
2294
|
+
const singletonIndex =
|
|
2295
|
+
denseBoundaryBusesInRoutingOrder.indexOf(singleton)
|
|
2296
|
+
const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
|
|
2297
|
+
if (singletonIndex > pairIndex) {
|
|
2298
|
+
denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
|
|
2299
|
+
denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2127
2303
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
2128
2304
|
number,
|
|
2129
2305
|
{ x: number; y: number }
|
|
@@ -2221,8 +2397,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2221
2397
|
allowBoundarySideViaFallback: bus.connections.length === 1,
|
|
2222
2398
|
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2223
2399
|
adaptiveWindingRouteOrder,
|
|
2400
|
+
// Retain pad-aligned channels even when plane sites are reserved
|
|
2401
|
+
// adaptively; the boundary grid can fence off a turning wide bus.
|
|
2224
2402
|
alignWindingGridToPads:
|
|
2225
|
-
usePadAlignedDenseRouting && !
|
|
2403
|
+
usePadAlignedDenseRouting && !configuredDensePlaneRouting,
|
|
2226
2404
|
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
|
|
2227
2405
|
? 60
|
|
2228
2406
|
: useConfiguredDensePlaneRouting
|
|
@@ -2269,6 +2447,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2269
2447
|
useConfiguredDensePlaneRouting &&
|
|
2270
2448
|
singleLayerBus !== bus &&
|
|
2271
2449
|
!embeddedNarrowBusAlreadyRouted
|
|
2450
|
+
let usedSoftPlaneRepair = false
|
|
2272
2451
|
let busPlans = (yield* routeAlternatives(
|
|
2273
2452
|
preferSingleLayerWinding
|
|
2274
2453
|
? { ...routeParams, bus: singleLayerBus }
|
|
@@ -2375,6 +2554,86 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2375
2554
|
}
|
|
2376
2555
|
}
|
|
2377
2556
|
}
|
|
2557
|
+
// Once wide-bus copper constrains the field, retry a blocked wide bus
|
|
2558
|
+
// with provisional plane sites as search costs. Future boundary sites
|
|
2559
|
+
// remain fixed, and a complete joint rematch must validate the repair.
|
|
2560
|
+
if (
|
|
2561
|
+
!busPlans &&
|
|
2562
|
+
bus.connections.length >= 8 &&
|
|
2563
|
+
boundaryBuses.some(
|
|
2564
|
+
(candidate) =>
|
|
2565
|
+
candidate.connections.length >= 8 &&
|
|
2566
|
+
matchedPlans.some((plan) => plan.busId === candidate.busId),
|
|
2567
|
+
)
|
|
2568
|
+
) {
|
|
2569
|
+
const committedNames = new Set(
|
|
2570
|
+
matchedPlans.map((plan) => plan.connectionName),
|
|
2571
|
+
)
|
|
2572
|
+
const futurePlaneNames = new Set(
|
|
2573
|
+
this.preparedBuses
|
|
2574
|
+
.filter((candidate) => candidate.termination.type === "plane")
|
|
2575
|
+
.flatMap((candidate) =>
|
|
2576
|
+
candidate.connections.map(
|
|
2577
|
+
(connection) => connection.connection.name,
|
|
2578
|
+
),
|
|
2579
|
+
)
|
|
2580
|
+
.filter((name) => !committedNames.has(name)),
|
|
2581
|
+
)
|
|
2582
|
+
const freePlans = (yield* routeAlternatives(
|
|
2583
|
+
{
|
|
2584
|
+
...routeParams,
|
|
2585
|
+
fixedViaPointsByConnectionIndex: undefined,
|
|
2586
|
+
reservedVias: routeParams.reservedVias.filter(
|
|
2587
|
+
(reserved) => !futurePlaneNames.has(reserved.connectionName),
|
|
2588
|
+
),
|
|
2589
|
+
softReservedVias: routeParams.reservedVias.filter((reserved) =>
|
|
2590
|
+
futurePlaneNames.has(reserved.connectionName),
|
|
2591
|
+
),
|
|
2592
|
+
},
|
|
2593
|
+
1,
|
|
2594
|
+
))[0]
|
|
2595
|
+
if (freePlans) {
|
|
2596
|
+
const allPlans = [...matchedPlans, ...freePlans]
|
|
2597
|
+
const rematchedPoints = matchComponentDogboneViaSites(
|
|
2598
|
+
this.preparedBuses,
|
|
2599
|
+
{
|
|
2600
|
+
viaDiameter: this.config.viaDiameter,
|
|
2601
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
2602
|
+
traceWidth: this.config.traceWidth,
|
|
2603
|
+
clearance: this.config.clearance,
|
|
2604
|
+
maximumSearchStates: 3_000_000,
|
|
2605
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
2606
|
+
preferBoundaryOutwardByBusId,
|
|
2607
|
+
fixedViaPointsByConnectionIndex: new Map(
|
|
2608
|
+
allPlans
|
|
2609
|
+
.filter((plan) => plan.via)
|
|
2610
|
+
.map((plan) => [plan.connectionIndex, plan.via!.center]),
|
|
2611
|
+
),
|
|
2612
|
+
preferredViaPointsByConnectionIndex:
|
|
2613
|
+
fixedViaPointsByConnectionIndex,
|
|
2614
|
+
blockingSegments: allPlans.flatMap((plan) =>
|
|
2615
|
+
plan.segments.map((segment) => ({
|
|
2616
|
+
connectionIndex: plan.connectionIndex,
|
|
2617
|
+
segment,
|
|
2618
|
+
})),
|
|
2619
|
+
),
|
|
2620
|
+
additionalObstacles: this.routingSrj.obstacles,
|
|
2621
|
+
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
2622
|
+
canShareCopper,
|
|
2623
|
+
},
|
|
2624
|
+
)
|
|
2625
|
+
debugDense(
|
|
2626
|
+
"free-sites:rematched",
|
|
2627
|
+
bus.busId,
|
|
2628
|
+
rematchedPoints?.size ?? "failed",
|
|
2629
|
+
)
|
|
2630
|
+
if (rematchedPoints) {
|
|
2631
|
+
busPlans = freePlans
|
|
2632
|
+
fixedViaPointsByConnectionIndex = rematchedPoints
|
|
2633
|
+
usedSoftPlaneRepair = true
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2378
2637
|
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
2379
2638
|
const lengths = busPlans.map((plan) => plan.length)
|
|
2380
2639
|
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
@@ -2388,7 +2647,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2388
2647
|
// Only pay for additional A* variants when the first topology is so
|
|
2389
2648
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2390
2649
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2391
|
-
|
|
2650
|
+
// Keep the jointly rematched repair: routeParams still carries the
|
|
2651
|
+
// earlier provisional sites and cannot safely replace its geometry.
|
|
2652
|
+
if (
|
|
2653
|
+
needsRouteDiversity &&
|
|
2654
|
+
!matchLengthsAfterPlanes &&
|
|
2655
|
+
!usedSoftPlaneRepair
|
|
2656
|
+
) {
|
|
2392
2657
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2393
2658
|
(first, second) => {
|
|
2394
2659
|
const firstLengths = first.map((plan) => plan.length)
|
|
@@ -2407,6 +2672,31 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2407
2672
|
return false
|
|
2408
2673
|
}
|
|
2409
2674
|
matchedPlans.push(...busPlans)
|
|
2675
|
+
if (
|
|
2676
|
+
matchedPlans.length ===
|
|
2677
|
+
boundaryBuses.reduce(
|
|
2678
|
+
(sum, candidate) => sum + candidate.connections.length,
|
|
2679
|
+
0,
|
|
2680
|
+
) &&
|
|
2681
|
+
matchedPlans.some((plan) => {
|
|
2682
|
+
const bus = boundaryBuses.find(
|
|
2683
|
+
(candidate) => candidate.busId === plan.busId,
|
|
2684
|
+
)
|
|
2685
|
+
if (bus?.connections.length !== 1 || !plan.via) return false
|
|
2686
|
+
const { center } = plan.via
|
|
2687
|
+
return (
|
|
2688
|
+
center.x < bus.componentBounds.minX ||
|
|
2689
|
+
center.x > bus.componentBounds.maxX ||
|
|
2690
|
+
center.y < bus.componentBounds.minY ||
|
|
2691
|
+
center.y > bus.componentBounds.maxY
|
|
2692
|
+
)
|
|
2693
|
+
})
|
|
2694
|
+
)
|
|
2695
|
+
boundaryViaPointsForRetry = new Map(
|
|
2696
|
+
matchedPlans
|
|
2697
|
+
.filter((plan) => plan.via)
|
|
2698
|
+
.map((plan) => [plan.connectionIndex, plan.via!.center]),
|
|
2699
|
+
)
|
|
2410
2700
|
debugDense("route:complete", bus.busId, busPlans.length)
|
|
2411
2701
|
return true
|
|
2412
2702
|
}.bind(this)
|
|
@@ -2932,7 +3222,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2932
3222
|
let alternatePlaneSearchStates = 0
|
|
2933
3223
|
const maximumAlternatePlaneSearchStates = Number(
|
|
2934
3224
|
process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ??
|
|
2935
|
-
|
|
3225
|
+
// Keep the initial search short before reserving blocked
|
|
3226
|
+
// plane sites. Later joint repairs retain the full budget.
|
|
3227
|
+
(useAdaptiveDensePlaneRouting &&
|
|
3228
|
+
(params.planeReservationRetryCount ?? 0) === 0
|
|
3229
|
+
? 10_000
|
|
3230
|
+
: useConfiguredDensePlaneRouting
|
|
3231
|
+
? 3_000_000
|
|
3232
|
+
: 1_000),
|
|
2936
3233
|
)
|
|
2937
3234
|
const maximumAlternatePlaneRoutes = Number(
|
|
2938
3235
|
process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
|
|
@@ -3838,6 +4135,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3838
4135
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3839
4136
|
allowMatchingInsideDenseBounds: true,
|
|
3840
4137
|
allowPairLaneSpreading: true,
|
|
4138
|
+
allowUnconstrainedLaneRerouting: true,
|
|
3841
4139
|
}
|
|
3842
4140
|
let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
|
|
3843
4141
|
const shortenedBusIds = new Set<string>()
|
|
@@ -3927,6 +4225,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3927
4225
|
debugDense("promote-reservations", refinedPromotions)
|
|
3928
4226
|
return yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3929
4227
|
...params,
|
|
4228
|
+
preferredBoundaryViaPoints: boundaryViaPointsForRetry,
|
|
3930
4229
|
promotedPlaneReservationBusIds: [
|
|
3931
4230
|
...(params.promotedPlaneReservationBusIds ?? []),
|
|
3932
4231
|
...refinedPromotions,
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
distanceSegmentToSegment,
|
|
9
9
|
segmentsAreClear,
|
|
10
10
|
} from "./geometry"
|
|
11
|
-
import {
|
|
11
|
+
import { getCopperLayerNames } from "./layer-names"
|
|
12
|
+
import { fanoutPlansAreClear, fanoutPlansAreMutuallyClear } from "./route-bus"
|
|
13
|
+
import { routeViaMinimalWinding } from "./route-via-minimal-winding"
|
|
12
14
|
import type {
|
|
13
15
|
Bounds,
|
|
14
16
|
FanoutRoutePlan,
|
|
@@ -647,6 +649,8 @@ export function matchBusPlanLengths(params: {
|
|
|
647
649
|
allowMatchingInsideDenseBounds?: boolean
|
|
648
650
|
/** Allow a differential pair's longer lane to move aside before tuning its mate. */
|
|
649
651
|
allowPairLaneSpreading?: boolean
|
|
652
|
+
/** Allow one unconstrained boundary lane to move around a tuning meander. */
|
|
653
|
+
allowUnconstrainedLaneRerouting?: boolean
|
|
650
654
|
/**
|
|
651
655
|
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
652
656
|
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
@@ -866,6 +870,124 @@ export function matchBusPlanLengths(params: {
|
|
|
866
870
|
if (++attempts >= 4) break
|
|
867
871
|
}
|
|
868
872
|
}
|
|
873
|
+
if (!acceptedPlans && params.allowUnconstrainedLaneRerouting) {
|
|
874
|
+
// A neighboring singleton may occupy the only tuning window. Keep its
|
|
875
|
+
// source dogbone, via and boundary endpoint fixed, and reroute only its
|
|
876
|
+
// target-layer copper around a complete meander. Constrained buses and
|
|
877
|
+
// plane routes are never displaced by this bounded repair.
|
|
878
|
+
let rerouteAttempts = 0
|
|
879
|
+
candidateSearch: for (const targetAddedLength of targetAddedLengths) {
|
|
880
|
+
const candidates = createTunedPlanCandidates({
|
|
881
|
+
plan: shortest,
|
|
882
|
+
bus,
|
|
883
|
+
targetAddedLength,
|
|
884
|
+
clearance,
|
|
885
|
+
sharedBoundary: bus.sharedBoundary,
|
|
886
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
887
|
+
})
|
|
888
|
+
for (const candidate of candidates) {
|
|
889
|
+
if (
|
|
890
|
+
!fanoutPlansAreClear({
|
|
891
|
+
plans: [candidate],
|
|
892
|
+
srj: inputSrj,
|
|
893
|
+
sharedBoundary,
|
|
894
|
+
clearance,
|
|
895
|
+
allowBlindAndBuriedVias,
|
|
896
|
+
allowSameNetMerges,
|
|
897
|
+
})
|
|
898
|
+
)
|
|
899
|
+
continue
|
|
900
|
+
const blockers = matchedPlans.filter(
|
|
901
|
+
(plan) =>
|
|
902
|
+
plan !== shortest &&
|
|
903
|
+
!fanoutPlansAreMutuallyClear({
|
|
904
|
+
plans: [candidate, plan],
|
|
905
|
+
srj: inputSrj,
|
|
906
|
+
clearance,
|
|
907
|
+
allowSameNetMerges,
|
|
908
|
+
}),
|
|
909
|
+
)
|
|
910
|
+
if (blockers.length !== 1) continue
|
|
911
|
+
const blocker = blockers[0]!
|
|
912
|
+
const blockerBus = preparedBuses.find(
|
|
913
|
+
(prepared) => prepared.busId === blocker.busId,
|
|
914
|
+
)
|
|
915
|
+
if (
|
|
916
|
+
!blockerBus ||
|
|
917
|
+
blockerBus.termination.type !== "boundary" ||
|
|
918
|
+
blockerBus.maxLengthSkew !== undefined ||
|
|
919
|
+
blockerBus.connections.length !== 1 ||
|
|
920
|
+
!blocker.via ||
|
|
921
|
+
blocker.additionalVias?.length ||
|
|
922
|
+
blocker.planeEndpointVia ||
|
|
923
|
+
blocker.segments.filter(
|
|
924
|
+
(segment) => segment.layer === blocker.sourceLayer,
|
|
925
|
+
).length !== 1
|
|
926
|
+
)
|
|
927
|
+
continue
|
|
928
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
929
|
+
plan === shortest ? candidate : plan,
|
|
930
|
+
)
|
|
931
|
+
for (const alignGridToPads of [true, false]) {
|
|
932
|
+
if (rerouteAttempts++ >= 4) break candidateSearch
|
|
933
|
+
const rerouted = routeViaMinimalWinding({
|
|
934
|
+
srj: inputSrj,
|
|
935
|
+
bus: blockerBus,
|
|
936
|
+
terminals: [
|
|
937
|
+
{
|
|
938
|
+
connection: blockerBus.connections[0]!,
|
|
939
|
+
viaPoint: blocker.via.center,
|
|
940
|
+
exitPoint: blocker.exitPoint,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
targetLayer: blocker.targetLayer,
|
|
944
|
+
acceptedPlans: nextPlans.filter((plan) => plan !== blocker),
|
|
945
|
+
layerNames: getCopperLayerNames(inputSrj.layerCount),
|
|
946
|
+
traceWidth: blocker.segments[0]!.width,
|
|
947
|
+
viaDiameter: blocker.via.diameter,
|
|
948
|
+
viaHoleDiameter: blocker.via.holeDiameter,
|
|
949
|
+
clearance,
|
|
950
|
+
allowBlindAndBuriedVias,
|
|
951
|
+
allowSameNetMerges,
|
|
952
|
+
gridStepDivisor: 2,
|
|
953
|
+
alignGridToPads,
|
|
954
|
+
maximumRouteOrderAttempts: 1,
|
|
955
|
+
preferTargetDirectedLaneBias: true,
|
|
956
|
+
})?.[0]
|
|
957
|
+
if (!rerouted) continue
|
|
958
|
+
// Rebuild from the original plan to preserve its route identity,
|
|
959
|
+
// endpoint metadata and physical via span exactly.
|
|
960
|
+
const repaired = createPlanWithSegments(blocker, [
|
|
961
|
+
blocker.segments[0]!,
|
|
962
|
+
...rerouted.segments.slice(1),
|
|
963
|
+
])
|
|
964
|
+
if (!repaired) continue
|
|
965
|
+
const repairedPlans = nextPlans.map((plan) =>
|
|
966
|
+
plan === blocker ? repaired : plan,
|
|
967
|
+
)
|
|
968
|
+
if (
|
|
969
|
+
getBusSkew(
|
|
970
|
+
repairedPlans.filter((plan) => plan.busId === bus.busId),
|
|
971
|
+
) >
|
|
972
|
+
maxLengthSkew + EPSILON ||
|
|
973
|
+
!fanoutPlansAreClear({
|
|
974
|
+
plans: repairedPlans,
|
|
975
|
+
srj: inputSrj,
|
|
976
|
+
sharedBoundary,
|
|
977
|
+
clearance,
|
|
978
|
+
allowBlindAndBuriedVias,
|
|
979
|
+
allowSameNetMerges,
|
|
980
|
+
}) ||
|
|
981
|
+
(candidatePlansAreFeasible &&
|
|
982
|
+
!candidatePlansAreFeasible(repairedPlans))
|
|
983
|
+
)
|
|
984
|
+
continue
|
|
985
|
+
acceptedPlans = repairedPlans
|
|
986
|
+
break candidateSearch
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
869
991
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
870
992
|
matchedPlans = acceptedPlans
|
|
871
993
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface RouteBusParams {
|
|
|
62
62
|
stopAfterFirstRejectedViaMinimalCandidate?: boolean
|
|
63
63
|
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
64
64
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
|
+
/** Provisional site preferences; successful callers must rematch future vias. */
|
|
66
|
+
softReservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
67
|
viaMinimalOnly?: boolean
|
|
66
68
|
/** Permit a singleton or pair to move provisional vias near the boundary. */
|
|
67
69
|
allowBoundarySideViaFallback?: boolean
|
|
@@ -311,10 +313,89 @@ function getWindingCrossoverLayer(params: {
|
|
|
311
313
|
)
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
function getDistributedBoundaryTargetTracks(params: {
|
|
317
|
+
bus: PreparedBus
|
|
318
|
+
boundaryDirection: FanoutDirection
|
|
319
|
+
traceWidth: number
|
|
320
|
+
clearance: number
|
|
321
|
+
}): number[] | undefined {
|
|
322
|
+
const { bus, boundaryDirection, traceWidth, clearance } = params
|
|
323
|
+
if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
|
|
324
|
+
return undefined
|
|
325
|
+
const layers = new Set(
|
|
326
|
+
bus.connections.map(
|
|
327
|
+
(connection) =>
|
|
328
|
+
connection.exitTargetPoint?.layer ??
|
|
329
|
+
getPointLayer(connection.targetPoint),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
if (layers.size < 2) return undefined
|
|
333
|
+
const minimum = isHorizontal(boundaryDirection)
|
|
334
|
+
? bus.sharedBoundary.minY
|
|
335
|
+
: bus.sharedBoundary.minX
|
|
336
|
+
const maximum = isHorizontal(boundaryDirection)
|
|
337
|
+
? bus.sharedBoundary.maxY
|
|
338
|
+
: bus.sharedBoundary.maxX
|
|
339
|
+
const tracks = bus.connections
|
|
340
|
+
.map((connection) =>
|
|
341
|
+
Math.max(
|
|
342
|
+
minimum,
|
|
343
|
+
Math.min(
|
|
344
|
+
maximum,
|
|
345
|
+
getPerpendicularAxis(
|
|
346
|
+
connection.exitTargetPoint ?? connection.targetPoint,
|
|
347
|
+
boundaryDirection,
|
|
348
|
+
),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
.toSorted((a, b) => a - b)
|
|
353
|
+
const pitch = traceWidth + clearance
|
|
354
|
+
if (
|
|
355
|
+
tracks.every(
|
|
356
|
+
(track, index) =>
|
|
357
|
+
index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
return undefined
|
|
361
|
+
if (maximum - minimum < (tracks.length - 1) * pitch - 1e-9) return undefined
|
|
362
|
+
// Pool neighboring overlaps while preserving their mean requested location.
|
|
363
|
+
// Subtracting the pitch reduces the clearance constraint to monotonicity.
|
|
364
|
+
const blocks: { start: number; count: number; mean: number }[] = []
|
|
365
|
+
for (const [index, track] of tracks.entries()) {
|
|
366
|
+
blocks.push({ start: index, count: 1, mean: track - index * pitch })
|
|
367
|
+
while (blocks.length > 1 && blocks.at(-2)!.mean > blocks.at(-1)!.mean) {
|
|
368
|
+
const second = blocks.pop()!
|
|
369
|
+
const first = blocks.pop()!
|
|
370
|
+
blocks.push({
|
|
371
|
+
start: first.start,
|
|
372
|
+
count: first.count + second.count,
|
|
373
|
+
mean:
|
|
374
|
+
(first.mean * first.count + second.mean * second.count) /
|
|
375
|
+
(first.count + second.count),
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const block of blocks) {
|
|
380
|
+
const mean = Math.max(
|
|
381
|
+
minimum,
|
|
382
|
+
Math.min(maximum - (tracks.length - 1) * pitch, block.mean),
|
|
383
|
+
)
|
|
384
|
+
for (let index = block.start; index < block.start + block.count; index++)
|
|
385
|
+
tracks[index] = mean + index * pitch
|
|
386
|
+
}
|
|
387
|
+
return tracks
|
|
388
|
+
}
|
|
389
|
+
|
|
314
390
|
export function getBoundaryTargetTrack(params: {
|
|
315
391
|
bus: PreparedBus
|
|
316
392
|
connection: PreparedConnection
|
|
317
393
|
boundaryDirection: FanoutDirection
|
|
394
|
+
traceWidth?: number
|
|
395
|
+
clearance?: number
|
|
396
|
+
layerNames?: readonly string[]
|
|
397
|
+
targetLayer?: string
|
|
398
|
+
windingOrderIndex?: number
|
|
318
399
|
}): number {
|
|
319
400
|
const requestedTrack = getPerpendicularAxis(
|
|
320
401
|
params.connection.exitTargetPoint ?? params.connection.targetPoint,
|
|
@@ -326,6 +407,22 @@ export function getBoundaryTargetTrack(params: {
|
|
|
326
407
|
const boundaryMaximum = isHorizontal(params.boundaryDirection)
|
|
327
408
|
? params.bus.sharedBoundary.maxY
|
|
328
409
|
: params.bus.sharedBoundary.maxX
|
|
410
|
+
const distributedTracks =
|
|
411
|
+
params.traceWidth !== undefined && params.clearance !== undefined
|
|
412
|
+
? getDistributedBoundaryTargetTracks({
|
|
413
|
+
...params,
|
|
414
|
+
traceWidth: params.traceWidth,
|
|
415
|
+
clearance: params.clearance,
|
|
416
|
+
})
|
|
417
|
+
: undefined
|
|
418
|
+
if (distributedTracks && params.layerNames && params.targetLayer) {
|
|
419
|
+
const { rank } = getWindingTargetRank({
|
|
420
|
+
...params,
|
|
421
|
+
layerNames: params.layerNames,
|
|
422
|
+
targetLayer: params.targetLayer,
|
|
423
|
+
})
|
|
424
|
+
return distributedTracks[rank]!
|
|
425
|
+
}
|
|
329
426
|
return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
|
|
330
427
|
}
|
|
331
428
|
|
|
@@ -1102,6 +1199,10 @@ function buildPlan(params: {
|
|
|
1102
1199
|
bus,
|
|
1103
1200
|
connection: preparedConnection,
|
|
1104
1201
|
boundaryDirection,
|
|
1202
|
+
traceWidth,
|
|
1203
|
+
clearance,
|
|
1204
|
+
layerNames,
|
|
1205
|
+
targetLayer,
|
|
1105
1206
|
})
|
|
1106
1207
|
: track
|
|
1107
1208
|
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
@@ -2459,6 +2560,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2459
2560
|
stopAfterFirstRejectedViaMinimalCandidate = false,
|
|
2460
2561
|
fixedViaPointsByConnectionIndex,
|
|
2461
2562
|
reservedVias = [],
|
|
2563
|
+
softReservedVias = [],
|
|
2462
2564
|
viaMinimalOnly = false,
|
|
2463
2565
|
allowBoundarySideViaFallback = false,
|
|
2464
2566
|
preferCornerBoundaryVia = false,
|
|
@@ -2608,14 +2710,21 @@ export function* routeBusAlternativesSteps(
|
|
|
2608
2710
|
localDogboneRepair?: boolean
|
|
2609
2711
|
}
|
|
2610
2712
|
const maximumThroughAllRouteOrderAttempts = 24
|
|
2611
|
-
const windingTargetOrderCount =
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2713
|
+
const windingTargetOrderCount =
|
|
2714
|
+
cornerSide ||
|
|
2715
|
+
getDistributedBoundaryTargetTracks({
|
|
2716
|
+
bus,
|
|
2717
|
+
boundaryDirection,
|
|
2718
|
+
traceWidth,
|
|
2719
|
+
clearance,
|
|
2720
|
+
})
|
|
2721
|
+
? getWindingTargetOrders({
|
|
2722
|
+
bus,
|
|
2723
|
+
boundaryDirection,
|
|
2724
|
+
layerNames,
|
|
2725
|
+
targetLayer,
|
|
2726
|
+
}).orders.length
|
|
2727
|
+
: 1
|
|
2619
2728
|
const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2620
2729
|
viaHandednesses.map((viaHandedness) => ({
|
|
2621
2730
|
label: `uniform-${viaHandedness}`,
|
|
@@ -2760,7 +2869,13 @@ export function* routeBusAlternativesSteps(
|
|
|
2760
2869
|
const coordinatedViaPoints =
|
|
2761
2870
|
fixedViaPointsByConnectionIndex ??
|
|
2762
2871
|
(!allowBlindAndBuriedVias &&
|
|
2763
|
-
bus.connections.length >= 8
|
|
2872
|
+
(bus.connections.length >= 8 ||
|
|
2873
|
+
getDistributedBoundaryTargetTracks({
|
|
2874
|
+
bus,
|
|
2875
|
+
boundaryDirection,
|
|
2876
|
+
traceWidth,
|
|
2877
|
+
clearance,
|
|
2878
|
+
})) &&
|
|
2764
2879
|
reservedVias.length === 0
|
|
2765
2880
|
? matchComponentDogboneViaSites([bus], {
|
|
2766
2881
|
viaDiameter,
|
|
@@ -2902,6 +3017,11 @@ export function* routeBusAlternativesSteps(
|
|
|
2902
3017
|
bus,
|
|
2903
3018
|
connection: preparedConnection,
|
|
2904
3019
|
boundaryDirection,
|
|
3020
|
+
traceWidth,
|
|
3021
|
+
clearance,
|
|
3022
|
+
layerNames,
|
|
3023
|
+
targetLayer,
|
|
3024
|
+
windingOrderIndex: terminalPattern.windingOrderIndex,
|
|
2905
3025
|
})
|
|
2906
3026
|
return {
|
|
2907
3027
|
connection: preparedConnection,
|
|
@@ -3003,6 +3123,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3003
3123
|
alignGridToPads,
|
|
3004
3124
|
includeReverseTargetRotation: terminalPattern.localDogboneRepair,
|
|
3005
3125
|
reservedVias,
|
|
3126
|
+
softReservedVias,
|
|
3006
3127
|
gridStepDivisor,
|
|
3007
3128
|
preferTargetDirectedLaneBias:
|
|
3008
3129
|
terminalPattern.preferTargetDirectedLaneBias,
|
|
@@ -3060,9 +3181,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3060
3181
|
// A through-via does not have to sit next to the source pad. A narrow bus
|
|
3061
3182
|
// embedded in another bus's source field can have every local dogbone site
|
|
3062
3183
|
// occupied while still having a clear source-layer escape. A pair first
|
|
3063
|
-
// relocates its two vias just outside the nearest package edge
|
|
3064
|
-
//
|
|
3065
|
-
// signal without weakening
|
|
3184
|
+
// relocates its two vias just outside the nearest package edge. A singleton
|
|
3185
|
+
// tries the existing boundary-side vias before searching outside the package.
|
|
3186
|
+
// Both forms preserve one via per signal without weakening clearance rules.
|
|
3066
3187
|
if (
|
|
3067
3188
|
alternatives.length === 0 &&
|
|
3068
3189
|
allowBoundarySideViaFallback &&
|
|
@@ -3095,6 +3216,10 @@ export function* routeBusAlternativesSteps(
|
|
|
3095
3216
|
bus,
|
|
3096
3217
|
connection: preparedConnection,
|
|
3097
3218
|
boundaryDirection,
|
|
3219
|
+
traceWidth,
|
|
3220
|
+
clearance,
|
|
3221
|
+
layerNames,
|
|
3222
|
+
targetLayer,
|
|
3098
3223
|
}),
|
|
3099
3224
|
)
|
|
3100
3225
|
const finalExitPoints = finalTracks.map((track) =>
|
|
@@ -3182,7 +3307,64 @@ export function* routeBusAlternativesSteps(
|
|
|
3182
3307
|
),
|
|
3183
3308
|
)
|
|
3184
3309
|
: []
|
|
3310
|
+
const matchedVias = bus.connections.map(
|
|
3311
|
+
(connection) =>
|
|
3312
|
+
fixedViaPointsByConnectionIndex.get(connection.connectionIndex)!,
|
|
3313
|
+
)
|
|
3314
|
+
const packageEdgeViaCandidates = [
|
|
3315
|
+
{
|
|
3316
|
+
distance: sourceCenter.x - bus.componentBounds.minX,
|
|
3317
|
+
axis: "x" as const,
|
|
3318
|
+
value: bus.componentBounds.minX - 2 * insetStep,
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
distance: bus.componentBounds.maxX - sourceCenter.x,
|
|
3322
|
+
axis: "x" as const,
|
|
3323
|
+
value: bus.componentBounds.maxX + 2 * insetStep,
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
distance: sourceCenter.y - bus.componentBounds.minY,
|
|
3327
|
+
axis: "y" as const,
|
|
3328
|
+
value: bus.componentBounds.minY - 2 * insetStep,
|
|
3329
|
+
},
|
|
3330
|
+
{
|
|
3331
|
+
distance: bus.componentBounds.maxY - sourceCenter.y,
|
|
3332
|
+
axis: "y" as const,
|
|
3333
|
+
value: bus.componentBounds.maxY + 2 * insetStep,
|
|
3334
|
+
},
|
|
3335
|
+
]
|
|
3336
|
+
.toSorted((a, b) => a.distance - b.distance)
|
|
3337
|
+
.flatMap(({ axis, value }) => {
|
|
3338
|
+
const otherAxis = axis === "x" ? "y" : "x"
|
|
3339
|
+
const mean =
|
|
3340
|
+
matchedVias.reduce((sum, via) => sum + via[otherAxis], 0) /
|
|
3341
|
+
matchedVias.length
|
|
3342
|
+
const order = matchedVias
|
|
3343
|
+
.map((via, index) => ({ index, track: via[otherAxis] }))
|
|
3344
|
+
.toSorted((a, b) => a.track - b.track || a.index - b.index)
|
|
3345
|
+
const pitch = viaDiameter + clearance
|
|
3346
|
+
const points = matchedVias.map((via, index) => ({
|
|
3347
|
+
...via,
|
|
3348
|
+
[axis]: value,
|
|
3349
|
+
[otherAxis]:
|
|
3350
|
+
order.length === 2 &&
|
|
3351
|
+
Math.abs(order[1]!.track - order[0]!.track) < pitch
|
|
3352
|
+
? mean + (order.findIndex((v) => v.index === index) - 0.5) * pitch
|
|
3353
|
+
: via[otherAxis],
|
|
3354
|
+
}))
|
|
3355
|
+
return points.length === 2
|
|
3356
|
+
? [points, [points[1]!, points[0]!]]
|
|
3357
|
+
: [points]
|
|
3358
|
+
})
|
|
3359
|
+
const preferPackageEdgeVias =
|
|
3360
|
+
bus.connections.length === 2 && Boolean(getCornerSide(bus))
|
|
3185
3361
|
const viaCandidates = [
|
|
3362
|
+
...(preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3363
|
+
(points) => ({
|
|
3364
|
+
points,
|
|
3365
|
+
boundarySide: false,
|
|
3366
|
+
}),
|
|
3367
|
+
),
|
|
3186
3368
|
...displacedViaCandidates.map((points) => ({
|
|
3187
3369
|
points,
|
|
3188
3370
|
boundarySide: false,
|
|
@@ -3191,6 +3373,15 @@ export function* routeBusAlternativesSteps(
|
|
|
3191
3373
|
points,
|
|
3192
3374
|
boundarySide: true,
|
|
3193
3375
|
})),
|
|
3376
|
+
// Preserve existing centered and singleton escapes before trying a short
|
|
3377
|
+
// source-layer route beyond the package. The target layer can then wind to the exit
|
|
3378
|
+
// without a local via being fenced in by an already-routed wide bus.
|
|
3379
|
+
...(!preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3380
|
+
(points) => ({
|
|
3381
|
+
points,
|
|
3382
|
+
boundarySide: false,
|
|
3383
|
+
}),
|
|
3384
|
+
),
|
|
3194
3385
|
]
|
|
3195
3386
|
for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
|
|
3196
3387
|
const boundaryVias = bus.connections.map((connection, index) => ({
|
|
@@ -3254,6 +3445,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3254
3445
|
allowBlindAndBuriedVias,
|
|
3255
3446
|
allowSameNetMerges,
|
|
3256
3447
|
maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
|
|
3448
|
+
softReservedVias,
|
|
3257
3449
|
reservedVias:
|
|
3258
3450
|
bus.connections.length > 1
|
|
3259
3451
|
? [...reservedVias, ...boundaryVias]
|
|
@@ -3303,6 +3495,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3303
3495
|
allowSameNetMerges,
|
|
3304
3496
|
maximumRouteOrderAttempts: 6,
|
|
3305
3497
|
reservedVias,
|
|
3498
|
+
softReservedVias,
|
|
3306
3499
|
gridStepDivisor: 2,
|
|
3307
3500
|
alignGridToPads: true,
|
|
3308
3501
|
},
|
|
@@ -3355,7 +3548,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3355
3548
|
)
|
|
3356
3549
|
return {
|
|
3357
3550
|
...sourceLayerPlan,
|
|
3358
|
-
...(preferCornerBoundaryVia ||
|
|
3551
|
+
...(preferCornerBoundaryVia ||
|
|
3552
|
+
bus.connections.length > 1 ||
|
|
3553
|
+
!boundarySide
|
|
3359
3554
|
? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
|
|
3360
3555
|
: {}),
|
|
3361
3556
|
targetLayer,
|
|
@@ -62,6 +62,8 @@ export interface RouteViaMinimalWindingParams {
|
|
|
62
62
|
allowSameNetMerges?: boolean
|
|
63
63
|
maximumRouteOrderAttempts?: number
|
|
64
64
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
|
+
/** Cost hints for provisional sites that the caller must rematch before commit. */
|
|
66
|
+
softReservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
67
|
/** Use a finer uniform grid for narrow channels between reserved vias. */
|
|
66
68
|
gridStepDivisor?: 1 | 2
|
|
67
69
|
/** Bias bounded fixed-site searches toward the remote target band. */
|
|
@@ -637,6 +639,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
637
639
|
allowSameNetMerges = false,
|
|
638
640
|
maximumRouteOrderAttempts,
|
|
639
641
|
reservedVias = [],
|
|
642
|
+
softReservedVias = [],
|
|
640
643
|
gridStepDivisor = 1,
|
|
641
644
|
preferTargetDirectedLaneBias = false,
|
|
642
645
|
allowSourceLayerRouting = false,
|
|
@@ -702,6 +705,40 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
702
705
|
point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
|
|
703
706
|
}
|
|
704
707
|
})
|
|
708
|
+
// Provisional plane barrels guide search without being fixed obstacles.
|
|
709
|
+
// Touch only the small grid rectangles around each disk, not every node.
|
|
710
|
+
const softViaCosts = softReservedVias.length
|
|
711
|
+
? new Float32Array(nodeCount)
|
|
712
|
+
: undefined
|
|
713
|
+
if (softViaCosts) {
|
|
714
|
+
for (const { via } of softReservedVias) {
|
|
715
|
+
if (!via.spanLayers.includes(targetLayer)) continue
|
|
716
|
+
const radius = via.diameter / 2 + traceWidth / 2 + clearance
|
|
717
|
+
const minimumColumn = Math.max(
|
|
718
|
+
0,
|
|
719
|
+
Math.ceil((via.center.x - radius - gridMinX) / gridStep),
|
|
720
|
+
)
|
|
721
|
+
const maximumColumn = Math.min(
|
|
722
|
+
columnCount - 1,
|
|
723
|
+
Math.floor((via.center.x + radius - gridMinX) / gridStep),
|
|
724
|
+
)
|
|
725
|
+
const minimumRow = Math.max(
|
|
726
|
+
0,
|
|
727
|
+
Math.ceil((via.center.y - radius - gridMinY) / gridStep),
|
|
728
|
+
)
|
|
729
|
+
const maximumRow = Math.min(
|
|
730
|
+
rowCount - 1,
|
|
731
|
+
Math.floor((via.center.y + radius - gridMinY) / gridStep),
|
|
732
|
+
)
|
|
733
|
+
for (let row = minimumRow; row <= maximumRow; row++) {
|
|
734
|
+
for (let column = minimumColumn; column <= maximumColumn; column++) {
|
|
735
|
+
const index = row * columnCount + column
|
|
736
|
+
if (distance(nodes[index]!.point, via.center) < radius)
|
|
737
|
+
softViaCosts[index] = softViaCosts[index]! + 25 * gridStep
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
705
742
|
const sampledGridPoints = includeVisualization
|
|
706
743
|
? nodes
|
|
707
744
|
.filter(
|
|
@@ -1247,7 +1284,8 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1247
1284
|
? gridStep * Math.SQRT2
|
|
1248
1285
|
: gridStep) +
|
|
1249
1286
|
(addsTurn ? gridStep * 0.2 : 0) +
|
|
1250
|
-
lanePenalty
|
|
1287
|
+
lanePenalty +
|
|
1288
|
+
(softViaCosts?.[nextNode] ?? 0)
|
|
1251
1289
|
const nextState = nextNode * 9 + directionIndex
|
|
1252
1290
|
if (nextDistance >= distances[nextState]! - EPSILON) continue
|
|
1253
1291
|
const edgeIndex = current.node * 8 + directionIndex
|