@tscircuit/fanout-solver 0.0.59 → 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/lib/fanout-solver.ts +229 -6
- package/lib/route-bus.ts +201 -55
- package/lib/route-via-minimal-winding.ts +39 -1
- package/package.json +1 -1
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,
|
|
@@ -1982,14 +1986,35 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1982
1986
|
: []),
|
|
1983
1987
|
...singletonDeferralCandidates.filter((bus) => {
|
|
1984
1988
|
const containingBus = getContainingWideSourceField(bus)
|
|
1985
|
-
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 =
|
|
1986
2009
|
usePadAlignedDenseRouting &&
|
|
1987
2010
|
!useConfiguredDensePlaneRouting &&
|
|
1988
2011
|
containingBus &&
|
|
1989
|
-
params.busLayerAssignments[containingBus.busId] ===
|
|
1990
|
-
params.busLayerAssignments[bus.busId]
|
|
2012
|
+
(params.busLayerAssignments[containingBus.busId] ===
|
|
2013
|
+
params.busLayerAssignments[bus.busId] ||
|
|
2014
|
+
inwardProjection > 1e-9)
|
|
1991
2015
|
return (
|
|
1992
|
-
!leadingWideSingletonBuses.includes(bus) &&
|
|
2016
|
+
!leadingWideSingletonBuses.includes(bus) &&
|
|
2017
|
+
!reserveEmbeddedSourceEscape
|
|
1993
2018
|
)
|
|
1994
2019
|
}),
|
|
1995
2020
|
...(hasThreeWideBoundaryBuses
|
|
@@ -2164,6 +2189,117 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2164
2189
|
bus,
|
|
2165
2190
|
]),
|
|
2166
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
|
+
}
|
|
2167
2303
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
2168
2304
|
number,
|
|
2169
2305
|
{ x: number; y: number }
|
|
@@ -2311,6 +2447,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2311
2447
|
useConfiguredDensePlaneRouting &&
|
|
2312
2448
|
singleLayerBus !== bus &&
|
|
2313
2449
|
!embeddedNarrowBusAlreadyRouted
|
|
2450
|
+
let usedSoftPlaneRepair = false
|
|
2314
2451
|
let busPlans = (yield* routeAlternatives(
|
|
2315
2452
|
preferSingleLayerWinding
|
|
2316
2453
|
? { ...routeParams, bus: singleLayerBus }
|
|
@@ -2417,6 +2554,86 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2417
2554
|
}
|
|
2418
2555
|
}
|
|
2419
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
|
+
}
|
|
2420
2637
|
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
2421
2638
|
const lengths = busPlans.map((plan) => plan.length)
|
|
2422
2639
|
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
@@ -2430,7 +2647,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2430
2647
|
// Only pay for additional A* variants when the first topology is so
|
|
2431
2648
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2432
2649
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2433
|
-
|
|
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
|
+
) {
|
|
2434
2657
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2435
2658
|
(first, second) => {
|
|
2436
2659
|
const firstLengths = first.map((plan) => plan.length)
|
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,
|
|
@@ -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,47 +3307,64 @@ export function* routeBusAlternativesSteps(
|
|
|
3182
3307
|
),
|
|
3183
3308
|
)
|
|
3184
3309
|
: []
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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))
|
|
3225
3361
|
const viaCandidates = [
|
|
3362
|
+
...(preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3363
|
+
(points) => ({
|
|
3364
|
+
points,
|
|
3365
|
+
boundarySide: false,
|
|
3366
|
+
}),
|
|
3367
|
+
),
|
|
3226
3368
|
...displacedViaCandidates.map((points) => ({
|
|
3227
3369
|
points,
|
|
3228
3370
|
boundarySide: false,
|
|
@@ -3231,13 +3373,15 @@ export function* routeBusAlternativesSteps(
|
|
|
3231
3373
|
points,
|
|
3232
3374
|
boundarySide: true,
|
|
3233
3375
|
})),
|
|
3234
|
-
// Preserve existing singleton escapes before trying a short
|
|
3235
|
-
// route beyond the package. The target layer can then wind to the exit
|
|
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
|
|
3236
3378
|
// without a local via being fenced in by an already-routed wide bus.
|
|
3237
|
-
...packageEdgeViaCandidates.map(
|
|
3238
|
-
points
|
|
3239
|
-
|
|
3240
|
-
|
|
3379
|
+
...(!preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3380
|
+
(points) => ({
|
|
3381
|
+
points,
|
|
3382
|
+
boundarySide: false,
|
|
3383
|
+
}),
|
|
3384
|
+
),
|
|
3241
3385
|
]
|
|
3242
3386
|
for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
|
|
3243
3387
|
const boundaryVias = bus.connections.map((connection, index) => ({
|
|
@@ -3301,6 +3445,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3301
3445
|
allowBlindAndBuriedVias,
|
|
3302
3446
|
allowSameNetMerges,
|
|
3303
3447
|
maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
|
|
3448
|
+
softReservedVias,
|
|
3304
3449
|
reservedVias:
|
|
3305
3450
|
bus.connections.length > 1
|
|
3306
3451
|
? [...reservedVias, ...boundaryVias]
|
|
@@ -3350,6 +3495,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3350
3495
|
allowSameNetMerges,
|
|
3351
3496
|
maximumRouteOrderAttempts: 6,
|
|
3352
3497
|
reservedVias,
|
|
3498
|
+
softReservedVias,
|
|
3353
3499
|
gridStepDivisor: 2,
|
|
3354
3500
|
alignGridToPads: true,
|
|
3355
3501
|
},
|
|
@@ -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
|