@tscircuit/fanout-solver 0.0.17 → 0.0.19
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 +41 -22
- package/lib/fanout-solver.ts +583 -82
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/layer-names.ts +38 -12
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +305 -71
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
package/lib/fanout-solver.ts
CHANGED
|
@@ -5,20 +5,26 @@ import {
|
|
|
5
5
|
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
6
6
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
7
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
8
|
-
import { distanceSegmentToObstacle } from "./geometry"
|
|
9
8
|
import { getCopperLayerColor } from "./layer-colors"
|
|
10
9
|
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
11
10
|
import {
|
|
12
11
|
prepareFanoutBuses,
|
|
13
12
|
resolveAvailableBoundaryRegions,
|
|
14
13
|
} from "./prepare-buses"
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
routeBus,
|
|
16
|
+
routeBusAlternatives,
|
|
17
|
+
type RouteBusStaticClearanceCache,
|
|
18
|
+
} from "./route-bus"
|
|
16
19
|
import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
|
|
17
20
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
21
|
+
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
18
22
|
import type {
|
|
19
23
|
AssignmentAttempt,
|
|
24
|
+
Bounds,
|
|
20
25
|
FanoutAttemptSummary,
|
|
21
26
|
FanoutBorderDistribution,
|
|
27
|
+
FanoutRoutePlan,
|
|
22
28
|
FanoutSolverOptions,
|
|
23
29
|
FanoutSolverOutput,
|
|
24
30
|
PreparedBus,
|
|
@@ -30,6 +36,7 @@ interface ResolvedFanoutConfig {
|
|
|
30
36
|
viaHoleDiameter: number
|
|
31
37
|
clearance: number
|
|
32
38
|
compactBusTracks: boolean
|
|
39
|
+
allowSameNetMerges: boolean
|
|
33
40
|
singleLayerPushAndShove: boolean
|
|
34
41
|
singleLayerAdaptiveExits: boolean
|
|
35
42
|
borderDistribution: FanoutBorderDistribution
|
|
@@ -38,6 +45,17 @@ interface ResolvedFanoutConfig {
|
|
|
38
45
|
maxLayerCombinations: number
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
interface EvaluatedAssignment extends AssignmentAttempt {
|
|
49
|
+
blockingBusIds: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface GroupedBeamState {
|
|
53
|
+
assignment: Readonly<Record<string, string>>
|
|
54
|
+
plans: FanoutRoutePlan[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
|
|
58
|
+
|
|
41
59
|
function resolvePositiveNumber(label: string, value: number): number {
|
|
42
60
|
if (!Number.isFinite(value) || value <= 0) {
|
|
43
61
|
throw new Error(
|
|
@@ -108,6 +126,7 @@ function resolveConfig(
|
|
|
108
126
|
viaHoleDiameter,
|
|
109
127
|
clearance,
|
|
110
128
|
compactBusTracks: options.compactBusTracks ?? false,
|
|
129
|
+
allowSameNetMerges: options.allowSameNetMerges ?? false,
|
|
111
130
|
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
|
|
112
131
|
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
|
|
113
132
|
borderDistribution,
|
|
@@ -199,62 +218,12 @@ function getBusDepthInRows(bus: PreparedBus): number {
|
|
|
199
218
|
)
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
function sourceLayerEscapeIsBlocked(params: {
|
|
203
|
-
bus: PreparedBus
|
|
204
|
-
srj: SimpleRouteJson
|
|
205
|
-
traceWidth: number
|
|
206
|
-
clearance: number
|
|
207
|
-
}): boolean {
|
|
208
|
-
const { bus, srj, traceWidth, clearance } = params
|
|
209
|
-
for (const connection of bus.connections) {
|
|
210
|
-
const source = {
|
|
211
|
-
x: connection.sourcePoint.x,
|
|
212
|
-
y: connection.sourcePoint.y,
|
|
213
|
-
}
|
|
214
|
-
const boundaryPoint = (() => {
|
|
215
|
-
switch (bus.direction) {
|
|
216
|
-
case "left":
|
|
217
|
-
return { x: bus.sharedBoundary.minX, y: source.y }
|
|
218
|
-
case "right":
|
|
219
|
-
return { x: bus.sharedBoundary.maxX, y: source.y }
|
|
220
|
-
case "up":
|
|
221
|
-
return { x: source.x, y: bus.sharedBoundary.maxY }
|
|
222
|
-
case "down":
|
|
223
|
-
return { x: source.x, y: bus.sharedBoundary.minY }
|
|
224
|
-
}
|
|
225
|
-
})()
|
|
226
|
-
const directEscapeSegment = {
|
|
227
|
-
start: source,
|
|
228
|
-
end: boundaryPoint,
|
|
229
|
-
width: traceWidth,
|
|
230
|
-
layer: connection.sourceLayer,
|
|
231
|
-
}
|
|
232
|
-
for (const obstacle of srj.obstacles) {
|
|
233
|
-
if (
|
|
234
|
-
obstacle === connection.sourceObstacle ||
|
|
235
|
-
!obstacle.layers.includes(connection.sourceLayer)
|
|
236
|
-
) {
|
|
237
|
-
continue
|
|
238
|
-
}
|
|
239
|
-
if (
|
|
240
|
-
distanceSegmentToObstacle(directEscapeSegment, obstacle) <
|
|
241
|
-
traceWidth / 2 + clearance - 1e-9
|
|
242
|
-
) {
|
|
243
|
-
return true
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return false
|
|
248
|
-
}
|
|
249
|
-
|
|
250
221
|
function createPreferredLayerAssignment(params: {
|
|
251
222
|
buses: PreparedBus[]
|
|
252
223
|
escapeLayers: string[]
|
|
253
|
-
|
|
254
|
-
traceWidth: number
|
|
255
|
-
clearance: number
|
|
224
|
+
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
256
225
|
}): Readonly<Record<string, string>> {
|
|
257
|
-
const { buses, escapeLayers,
|
|
226
|
+
const { buses, escapeLayers, escapeLayersByBusId } = params
|
|
258
227
|
const assignment: Record<string, string> = {}
|
|
259
228
|
const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
|
|
260
229
|
let nextViaLayerIndex = 0
|
|
@@ -273,16 +242,13 @@ function createPreferredLayerAssignment(params: {
|
|
|
273
242
|
assignment[bus.busId] = bus.termination.layer
|
|
274
243
|
continue
|
|
275
244
|
}
|
|
276
|
-
const
|
|
245
|
+
const routableEscapeLayers = escapeLayersByBusId[bus.busId] ?? escapeLayers
|
|
246
|
+
const viaLayers = routableEscapeLayers.filter(
|
|
247
|
+
(layer) => layer !== sourceLayer,
|
|
248
|
+
)
|
|
277
249
|
if (
|
|
278
|
-
|
|
279
|
-
busIsOnOutwardComponentEdge(bus)
|
|
280
|
-
!sourceLayerEscapeIsBlocked({
|
|
281
|
-
bus,
|
|
282
|
-
srj,
|
|
283
|
-
traceWidth,
|
|
284
|
-
clearance,
|
|
285
|
-
})
|
|
250
|
+
routableEscapeLayers.includes(sourceLayer) &&
|
|
251
|
+
busIsOnOutwardComponentEdge(bus)
|
|
286
252
|
) {
|
|
287
253
|
assignment[bus.busId] = sourceLayer
|
|
288
254
|
} else if (viaLayers.length > 0) {
|
|
@@ -321,12 +287,67 @@ function prioritizeLayerAssignment(params: {
|
|
|
321
287
|
].slice(0, maxAssignments)
|
|
322
288
|
}
|
|
323
289
|
|
|
290
|
+
function getCandidateEscapeLayersForBus(params: {
|
|
291
|
+
bus: PreparedBus
|
|
292
|
+
srj: SimpleRouteJson
|
|
293
|
+
config: ResolvedFanoutConfig
|
|
294
|
+
staticClearanceCache: RouteBusStaticClearanceCache
|
|
295
|
+
}): string[] {
|
|
296
|
+
const { bus, srj, config, staticClearanceCache } = params
|
|
297
|
+
const individuallyRoutableLayers = config.escapeLayers.filter(
|
|
298
|
+
(targetLayer) =>
|
|
299
|
+
routeBus({
|
|
300
|
+
srj,
|
|
301
|
+
bus,
|
|
302
|
+
targetLayer,
|
|
303
|
+
acceptedPlans: [],
|
|
304
|
+
layerNames: config.layerNames,
|
|
305
|
+
traceWidth: config.traceWidth,
|
|
306
|
+
viaDiameter: config.viaDiameter,
|
|
307
|
+
viaHoleDiameter: config.viaHoleDiameter,
|
|
308
|
+
clearance: config.clearance,
|
|
309
|
+
compactBusTracks: config.compactBusTracks,
|
|
310
|
+
allowSameNetMerges: config.allowSameNetMerges,
|
|
311
|
+
staticClearanceCache,
|
|
312
|
+
}) !== null,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// Existing plans only add clearance constraints, so a layer that cannot
|
|
316
|
+
// route this bus by itself cannot become viable later in an assignment.
|
|
317
|
+
// Preserve the original candidates when none route so impossible problems
|
|
318
|
+
// still produce the usual failed-solver result instead of throwing here.
|
|
319
|
+
return individuallyRoutableLayers.length > 0
|
|
320
|
+
? individuallyRoutableLayers
|
|
321
|
+
: config.escapeLayers
|
|
322
|
+
}
|
|
323
|
+
|
|
324
324
|
export class FanoutSolver extends BaseSolver {
|
|
325
325
|
readonly preparedBuses: PreparedBus[]
|
|
326
326
|
readonly attempts: FanoutAttemptSummary[] = []
|
|
327
327
|
readonly layerAssignments: Array<Readonly<Record<string, string>>>
|
|
328
328
|
readonly config: ResolvedFanoutConfig
|
|
329
|
+
private readonly escapeLayersByBusId: Readonly<
|
|
330
|
+
Record<string, readonly string[]>
|
|
331
|
+
>
|
|
332
|
+
private readonly evaluatedAssignmentKeys = new Set<string>()
|
|
333
|
+
private readonly queuedAssignmentKeys = new Set<string>()
|
|
334
|
+
private readonly assignmentRepairDepthByKey = new Map<string, number>()
|
|
335
|
+
private readonly pendingRepairAssignments: Array<
|
|
336
|
+
Readonly<Record<string, string>>
|
|
337
|
+
> = []
|
|
338
|
+
private readonly routeStaticClearanceCache: RouteBusStaticClearanceCache =
|
|
339
|
+
new Map()
|
|
340
|
+
private readonly routingPrefixCache = new Map<
|
|
341
|
+
string,
|
|
342
|
+
{
|
|
343
|
+
plans: AssignmentAttempt["plans"]
|
|
344
|
+
failedBusIds: string[]
|
|
345
|
+
blockingBusCounts: Map<string, number>
|
|
346
|
+
}
|
|
347
|
+
>()
|
|
348
|
+
private groupedBeamEvaluated = false
|
|
329
349
|
private nextAssignmentIndex = 0
|
|
350
|
+
private nextGeneratedAssignmentIndex = 0
|
|
330
351
|
private bestAttempt: AssignmentAttempt | null = null
|
|
331
352
|
|
|
332
353
|
constructor(
|
|
@@ -364,9 +385,27 @@ export class FanoutSolver extends BaseSolver {
|
|
|
364
385
|
: [],
|
|
365
386
|
),
|
|
366
387
|
)
|
|
388
|
+
const escapeLayersByBusId = Object.fromEntries(
|
|
389
|
+
this.preparedBuses.flatMap((bus) => {
|
|
390
|
+
if (bus.termination.type === "plane") return []
|
|
391
|
+
return [
|
|
392
|
+
[
|
|
393
|
+
bus.busId,
|
|
394
|
+
getCandidateEscapeLayersForBus({
|
|
395
|
+
bus,
|
|
396
|
+
srj: inputSrj,
|
|
397
|
+
config: this.config,
|
|
398
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
399
|
+
}),
|
|
400
|
+
] as const,
|
|
401
|
+
]
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
this.escapeLayersByBusId = escapeLayersByBusId
|
|
367
405
|
const generatedAssignments = generateLayerAssignments({
|
|
368
406
|
busIds: boundaryBusIds,
|
|
369
407
|
layers: this.config.escapeLayers,
|
|
408
|
+
layersByBusId: escapeLayersByBusId,
|
|
370
409
|
maxAssignments: this.config.maxLayerCombinations,
|
|
371
410
|
}).map((assignment) => ({
|
|
372
411
|
...assignment,
|
|
@@ -376,26 +415,55 @@ export class FanoutSolver extends BaseSolver {
|
|
|
376
415
|
preferredAssignment: createPreferredLayerAssignment({
|
|
377
416
|
buses: this.preparedBuses,
|
|
378
417
|
escapeLayers: this.config.escapeLayers,
|
|
379
|
-
|
|
380
|
-
traceWidth: this.config.traceWidth,
|
|
381
|
-
clearance: this.config.clearance,
|
|
418
|
+
escapeLayersByBusId,
|
|
382
419
|
}),
|
|
383
420
|
generatedAssignments,
|
|
384
421
|
maxAssignments: this.config.maxLayerCombinations,
|
|
385
422
|
})
|
|
386
|
-
this.MAX_ITERATIONS = this.
|
|
423
|
+
this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
|
|
387
424
|
}
|
|
388
425
|
|
|
389
426
|
override getSolverName(): string {
|
|
390
427
|
return "FanoutSolver"
|
|
391
428
|
}
|
|
392
429
|
|
|
393
|
-
private
|
|
430
|
+
private getValidationBoundary(): Bounds {
|
|
431
|
+
if (this.options.sharedBoundary) return this.options.sharedBoundary
|
|
432
|
+
const firstBoundary = this.preparedBuses[0]?.sharedBoundary
|
|
433
|
+
if (!firstBoundary) return this.inputSrj.bounds
|
|
434
|
+
return this.preparedBuses.slice(1).reduce<Bounds>(
|
|
435
|
+
(boundary, bus) => ({
|
|
436
|
+
minX: Math.min(boundary.minX, bus.sharedBoundary.minX),
|
|
437
|
+
maxX: Math.max(boundary.maxX, bus.sharedBoundary.maxX),
|
|
438
|
+
minY: Math.min(boundary.minY, bus.sharedBoundary.minY),
|
|
439
|
+
maxY: Math.max(boundary.maxY, bus.sharedBoundary.maxY),
|
|
440
|
+
}),
|
|
441
|
+
{ ...firstBoundary },
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private validateCompletePlans(
|
|
446
|
+
plans: readonly FanoutRoutePlan[],
|
|
447
|
+
outputSrj: SimpleRouteJson,
|
|
448
|
+
) {
|
|
449
|
+
return validateFanoutSolution({
|
|
450
|
+
inputSrj: this.inputSrj,
|
|
451
|
+
outputSrj,
|
|
452
|
+
plans,
|
|
453
|
+
preparedBuses: this.preparedBuses,
|
|
454
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
455
|
+
clearance: this.config.clearance,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private evaluateAssignmentWithStrategy(
|
|
394
460
|
assignmentIndex: number,
|
|
395
461
|
busLayerAssignments: Readonly<Record<string, string>>,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
462
|
+
routingStrategy: RoutingStrategy,
|
|
463
|
+
): EvaluatedAssignment {
|
|
464
|
+
let plans: AssignmentAttempt["plans"] = []
|
|
465
|
+
let failedBusIds: string[] = []
|
|
466
|
+
let blockingBusCounts = new Map<string, number>()
|
|
399
467
|
const isSingleLayerFanout = this.config.escapeLayers.length === 1
|
|
400
468
|
if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
|
|
401
469
|
const singleLayerParams = {
|
|
@@ -425,13 +493,21 @@ export class FanoutSolver extends BaseSolver {
|
|
|
425
493
|
(a, b) =>
|
|
426
494
|
Number(a.termination.type === "plane") -
|
|
427
495
|
Number(b.termination.type === "plane") ||
|
|
496
|
+
(routingStrategy === "group-by-layer"
|
|
497
|
+
? (busLayerAssignments[a.busId] ?? "").localeCompare(
|
|
498
|
+
busLayerAssignments[b.busId] ?? "",
|
|
499
|
+
)
|
|
500
|
+
: 0) ||
|
|
428
501
|
b.componentObstacles.length - a.componentObstacles.length ||
|
|
429
502
|
(isSingleLayerFanout
|
|
430
503
|
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
431
504
|
: b.connections.length - a.connections.length ||
|
|
432
|
-
|
|
505
|
+
(routingStrategy === "deep-first"
|
|
506
|
+
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
507
|
+
: getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b))),
|
|
433
508
|
)
|
|
434
509
|
|
|
510
|
+
let routingPrefixKey = `${routingStrategy}|`
|
|
435
511
|
for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
|
|
436
512
|
? []
|
|
437
513
|
: busesInRoutingOrder) {
|
|
@@ -441,6 +517,20 @@ export class FanoutSolver extends BaseSolver {
|
|
|
441
517
|
`FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
|
|
442
518
|
)
|
|
443
519
|
}
|
|
520
|
+
// The routing order can change between assignments (notably for
|
|
521
|
+
// group-by-layer search). A layer-only key can therefore replay a
|
|
522
|
+
// prefix belonging to a different bus and duplicate or drop plans when
|
|
523
|
+
// buses contain multiple connections. Include the bus identity so the
|
|
524
|
+
// cache remains valid for grouped power/signal lanes.
|
|
525
|
+
routingPrefixKey += `${bus.busId.length}:${bus.busId};${targetLayer.length}:${targetLayer};`
|
|
526
|
+
const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey)
|
|
527
|
+
if (cachedPrefix) {
|
|
528
|
+
plans = [...cachedPrefix.plans]
|
|
529
|
+
failedBusIds = [...cachedPrefix.failedBusIds]
|
|
530
|
+
blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
const currentBusBlockingCounts = new Map<string, number>()
|
|
444
534
|
const busPlans = routeBus({
|
|
445
535
|
srj: this.inputSrj,
|
|
446
536
|
bus,
|
|
@@ -452,12 +542,50 @@ export class FanoutSolver extends BaseSolver {
|
|
|
452
542
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
453
543
|
clearance: this.config.clearance,
|
|
454
544
|
compactBusTracks: this.config.compactBusTracks,
|
|
545
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
546
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
547
|
+
blockingBusCounts: currentBusBlockingCounts,
|
|
455
548
|
})
|
|
456
549
|
if (!busPlans) {
|
|
457
550
|
failedBusIds.push(bus.busId)
|
|
458
|
-
|
|
551
|
+
for (const [blockingBusId, count] of currentBusBlockingCounts) {
|
|
552
|
+
blockingBusCounts.set(
|
|
553
|
+
blockingBusId,
|
|
554
|
+
(blockingBusCounts.get(blockingBusId) ?? 0) + count,
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
plans.push(...busPlans)
|
|
459
559
|
}
|
|
460
|
-
|
|
560
|
+
this.routingPrefixCache.set(routingPrefixKey, {
|
|
561
|
+
plans: [...plans],
|
|
562
|
+
failedBusIds: [...failedBusIds],
|
|
563
|
+
blockingBusCounts: new Map(blockingBusCounts),
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let validationIssues: FanoutAttemptSummary["validationIssues"]
|
|
568
|
+
let outputSrj = buildOutputSimpleRouteJson({
|
|
569
|
+
inputSrj: this.inputSrj,
|
|
570
|
+
plans,
|
|
571
|
+
layerNames: this.config.layerNames,
|
|
572
|
+
})
|
|
573
|
+
const validation =
|
|
574
|
+
plans.length === this.inputSrj.connections.length
|
|
575
|
+
? this.validateCompletePlans(plans, outputSrj)
|
|
576
|
+
: null
|
|
577
|
+
if (validation && !validation.valid) {
|
|
578
|
+
// Every route-producing strategy must pass the same final layer-aware,
|
|
579
|
+
// same-net-aware copper validation before it can be scored as complete.
|
|
580
|
+
validationIssues = validation.issues
|
|
581
|
+
plans = []
|
|
582
|
+
failedBusIds = this.preparedBuses.map((bus) => bus.busId)
|
|
583
|
+
blockingBusCounts.clear()
|
|
584
|
+
outputSrj = buildOutputSimpleRouteJson({
|
|
585
|
+
inputSrj: this.inputSrj,
|
|
586
|
+
plans,
|
|
587
|
+
layerNames: this.config.layerNames,
|
|
588
|
+
})
|
|
461
589
|
}
|
|
462
590
|
|
|
463
591
|
const routedBusCount = this.preparedBuses.length - failedBusIds.length
|
|
@@ -477,21 +605,372 @@ export class FanoutSolver extends BaseSolver {
|
|
|
477
605
|
routedConnectionCount: plans.length,
|
|
478
606
|
failedBusIds,
|
|
479
607
|
score,
|
|
608
|
+
...(validationIssues ? { validationIssues } : {}),
|
|
480
609
|
}
|
|
481
610
|
|
|
482
611
|
return {
|
|
483
612
|
summary,
|
|
484
613
|
plans,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}),
|
|
614
|
+
blockingBusIds: [...blockingBusCounts.entries()]
|
|
615
|
+
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
616
|
+
.map(([busId]) => busId),
|
|
617
|
+
outputSrj,
|
|
490
618
|
}
|
|
491
619
|
}
|
|
492
620
|
|
|
621
|
+
private evaluateAssignment(
|
|
622
|
+
assignmentIndex: number,
|
|
623
|
+
busLayerAssignments: Readonly<Record<string, string>>,
|
|
624
|
+
): EvaluatedAssignment {
|
|
625
|
+
let bestAttempt = this.evaluateAssignmentWithStrategy(
|
|
626
|
+
assignmentIndex,
|
|
627
|
+
busLayerAssignments,
|
|
628
|
+
"default",
|
|
629
|
+
)
|
|
630
|
+
if (
|
|
631
|
+
bestAttempt.summary.routedConnectionCount ===
|
|
632
|
+
this.inputSrj.connections.length
|
|
633
|
+
) {
|
|
634
|
+
return bestAttempt
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
|
|
638
|
+
const attempt = this.evaluateAssignmentWithStrategy(
|
|
639
|
+
assignmentIndex,
|
|
640
|
+
busLayerAssignments,
|
|
641
|
+
routingStrategy,
|
|
642
|
+
)
|
|
643
|
+
if (attempt.summary.score < bestAttempt.summary.score) {
|
|
644
|
+
bestAttempt = attempt
|
|
645
|
+
}
|
|
646
|
+
if (
|
|
647
|
+
bestAttempt.summary.routedConnectionCount ===
|
|
648
|
+
this.inputSrj.connections.length
|
|
649
|
+
) {
|
|
650
|
+
return bestAttempt
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return bestAttempt
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Search layer assignments and track alternatives together. The regular
|
|
658
|
+
* assignment loop commits to one route per bus before the next bus is
|
|
659
|
+
* considered, so a locally-valid track can still starve a later bus. A
|
|
660
|
+
* bounded beam keeps several grouped-layer route prefixes alive. It also
|
|
661
|
+
* evaluates multi-connection buses atomically, so one promising route for a
|
|
662
|
+
* power or signal lane cannot starve a later bus before the solver explores
|
|
663
|
+
* an alternate layer/track combination.
|
|
664
|
+
*/
|
|
665
|
+
private evaluateGroupedBeam(
|
|
666
|
+
assignmentIndex: number,
|
|
667
|
+
groupByDirection = false,
|
|
668
|
+
): EvaluatedAssignment | null {
|
|
669
|
+
if (this.config.escapeLayers.length < 2) return null
|
|
670
|
+
if (this.preparedBuses.length > 56) return null
|
|
671
|
+
const totalConnections = this.inputSrj.connections.length
|
|
672
|
+
// Multi-pin alternatives grow with both the bus width and the number of
|
|
673
|
+
// layer prefixes. Keep the new search bounded on the small/medium grouped
|
|
674
|
+
// problems it can improve, then let the regular assignment/repair search
|
|
675
|
+
// handle the very large benchmark samples without starving them.
|
|
676
|
+
if (totalConnections > 64) return null
|
|
677
|
+
if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
|
|
678
|
+
return null
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
|
|
682
|
+
const aLayerCount =
|
|
683
|
+
a.termination.type === "plane"
|
|
684
|
+
? 1
|
|
685
|
+
: (this.escapeLayersByBusId[a.busId]?.length ??
|
|
686
|
+
this.config.escapeLayers.length)
|
|
687
|
+
const bLayerCount =
|
|
688
|
+
b.termination.type === "plane"
|
|
689
|
+
? 1
|
|
690
|
+
: (this.escapeLayersByBusId[b.busId]?.length ??
|
|
691
|
+
this.config.escapeLayers.length)
|
|
692
|
+
return (
|
|
693
|
+
Number(a.termination.type === "plane") -
|
|
694
|
+
Number(b.termination.type === "plane") ||
|
|
695
|
+
(groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
|
|
696
|
+
aLayerCount - bLayerCount ||
|
|
697
|
+
b.componentObstacles.length - a.componentObstacles.length ||
|
|
698
|
+
b.connections.length - a.connections.length ||
|
|
699
|
+
getBusDepthInRows(b) - getBusDepthInRows(a) ||
|
|
700
|
+
a.busId.localeCompare(b.busId)
|
|
701
|
+
)
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
const isSmallProblem = totalConnections <= 24
|
|
705
|
+
const hasMultiConnectionBus = this.preparedBuses.some(
|
|
706
|
+
(bus) => bus.connections.length > 1,
|
|
707
|
+
)
|
|
708
|
+
// Preserve the broad track search for small singleton problems. Grouped
|
|
709
|
+
// power buses already branch across every candidate layer and make each
|
|
710
|
+
// route-alternative expansion combinatorial, so retain layer diversity but
|
|
711
|
+
// only one atomic route per layer for those buses.
|
|
712
|
+
const beamWidth = isSmallProblem ? 48 : totalConnections <= 32 ? 24 : 12
|
|
713
|
+
const alternativesPerLayer =
|
|
714
|
+
isSmallProblem && !hasMultiConnectionBus ? 4 : 1
|
|
715
|
+
let states: GroupedBeamState[] = [{ assignment: {}, plans: [] }]
|
|
716
|
+
|
|
717
|
+
const getStateScore = (state: GroupedBeamState): number => {
|
|
718
|
+
const routeLength = state.plans.reduce(
|
|
719
|
+
(total, plan) => total + plan.length,
|
|
720
|
+
0,
|
|
721
|
+
)
|
|
722
|
+
const viaCount = state.plans.filter((plan) => plan.via).length
|
|
723
|
+
return (
|
|
724
|
+
routeLength +
|
|
725
|
+
viaCount * 0.1 +
|
|
726
|
+
assignmentLoadPenalty(state.assignment) * 0.01
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
for (const bus of busesInSearchOrder) {
|
|
731
|
+
const nextStates: GroupedBeamState[] = []
|
|
732
|
+
for (const state of states) {
|
|
733
|
+
const candidateLayers =
|
|
734
|
+
bus.termination.type === "plane"
|
|
735
|
+
? [bus.termination.layer]
|
|
736
|
+
: (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
|
|
737
|
+
const layerLoads = new Map<string, number>()
|
|
738
|
+
for (const layer of Object.values(state.assignment)) {
|
|
739
|
+
layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + 1)
|
|
740
|
+
}
|
|
741
|
+
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
742
|
+
const orderedLayers = candidateLayers.toSorted(
|
|
743
|
+
(first, second) =>
|
|
744
|
+
(layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
|
|
745
|
+
Number(first === sourceLayer) - Number(second === sourceLayer) ||
|
|
746
|
+
first.localeCompare(second),
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
for (const targetLayer of orderedLayers) {
|
|
750
|
+
const busAlternatives = routeBusAlternatives(
|
|
751
|
+
{
|
|
752
|
+
srj: this.inputSrj,
|
|
753
|
+
bus,
|
|
754
|
+
targetLayer,
|
|
755
|
+
acceptedPlans: state.plans,
|
|
756
|
+
layerNames: this.config.layerNames,
|
|
757
|
+
traceWidth: this.config.traceWidth,
|
|
758
|
+
viaDiameter: this.config.viaDiameter,
|
|
759
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
760
|
+
clearance: this.config.clearance,
|
|
761
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
762
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
763
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
764
|
+
},
|
|
765
|
+
alternativesPerLayer,
|
|
766
|
+
)
|
|
767
|
+
for (const busPlans of busAlternatives) {
|
|
768
|
+
nextStates.push({
|
|
769
|
+
assignment: {
|
|
770
|
+
...state.assignment,
|
|
771
|
+
[bus.busId]: targetLayer,
|
|
772
|
+
},
|
|
773
|
+
plans: [...state.plans, ...busPlans],
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (nextStates.length === 0) return null
|
|
780
|
+
nextStates.sort((first, second) => {
|
|
781
|
+
const scoreDifference = getStateScore(first) - getStateScore(second)
|
|
782
|
+
if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
|
|
783
|
+
return JSON.stringify(first.assignment).localeCompare(
|
|
784
|
+
JSON.stringify(second.assignment),
|
|
785
|
+
)
|
|
786
|
+
})
|
|
787
|
+
const statesByAssignment = new Map<string, number>()
|
|
788
|
+
states = []
|
|
789
|
+
for (const state of nextStates) {
|
|
790
|
+
const key = JSON.stringify(state.assignment)
|
|
791
|
+
const sameAssignmentCount = statesByAssignment.get(key) ?? 0
|
|
792
|
+
if (sameAssignmentCount >= 2) continue
|
|
793
|
+
statesByAssignment.set(key, sameAssignmentCount + 1)
|
|
794
|
+
states.push(state)
|
|
795
|
+
if (states.length >= beamWidth) break
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const bestState = states[0]
|
|
800
|
+
if (!bestState) return null
|
|
801
|
+
const outputSrj = buildOutputSimpleRouteJson({
|
|
802
|
+
inputSrj: this.inputSrj,
|
|
803
|
+
plans: bestState.plans,
|
|
804
|
+
layerNames: this.config.layerNames,
|
|
805
|
+
})
|
|
806
|
+
if (
|
|
807
|
+
bestState.plans.length === this.inputSrj.connections.length &&
|
|
808
|
+
!this.validateCompletePlans(bestState.plans, outputSrj).valid
|
|
809
|
+
) {
|
|
810
|
+
return null
|
|
811
|
+
}
|
|
812
|
+
const score =
|
|
813
|
+
bestState.plans.length === this.inputSrj.connections.length
|
|
814
|
+
? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
815
|
+
bestState.plans.filter((plan) => plan.via).length * 0.1 +
|
|
816
|
+
assignmentLoadPenalty(bestState.assignment) * 0.01
|
|
817
|
+
: Number.POSITIVE_INFINITY
|
|
818
|
+
if (!Number.isFinite(score)) return null
|
|
819
|
+
|
|
820
|
+
const summary: FanoutAttemptSummary = {
|
|
821
|
+
assignmentIndex,
|
|
822
|
+
busLayerAssignments: bestState.assignment,
|
|
823
|
+
routedBusCount: this.preparedBuses.length,
|
|
824
|
+
routedConnectionCount: bestState.plans.length,
|
|
825
|
+
failedBusIds: [],
|
|
826
|
+
score,
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
summary,
|
|
830
|
+
plans: bestState.plans,
|
|
831
|
+
blockingBusIds: [],
|
|
832
|
+
outputSrj,
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private prioritizeFailedBusRepairs(
|
|
837
|
+
assignment: Readonly<Record<string, string>>,
|
|
838
|
+
failedBusIds: readonly string[],
|
|
839
|
+
blockingBusIds: readonly string[],
|
|
840
|
+
): void {
|
|
841
|
+
const assignmentKey = JSON.stringify(assignment)
|
|
842
|
+
const repairDepth = this.assignmentRepairDepthByKey.get(assignmentKey) ?? 0
|
|
843
|
+
if (repairDepth >= 2) return
|
|
844
|
+
|
|
845
|
+
const maximumRepairs = 8
|
|
846
|
+
const repairs: Array<Readonly<Record<string, string>>> = []
|
|
847
|
+
const repairKeys = new Set<string>()
|
|
848
|
+
const addRepair = (repair: Readonly<Record<string, string>>): void => {
|
|
849
|
+
const key = JSON.stringify(repair)
|
|
850
|
+
if (
|
|
851
|
+
repairKeys.has(key) ||
|
|
852
|
+
this.evaluatedAssignmentKeys.has(key) ||
|
|
853
|
+
this.queuedAssignmentKeys.has(key)
|
|
854
|
+
) {
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
repairKeys.add(key)
|
|
858
|
+
this.queuedAssignmentKeys.add(key)
|
|
859
|
+
this.assignmentRepairDepthByKey.set(key, repairDepth + 1)
|
|
860
|
+
repairs.push(repair)
|
|
861
|
+
}
|
|
862
|
+
const repairBusIds: string[] = []
|
|
863
|
+
for (
|
|
864
|
+
let index = 0;
|
|
865
|
+
index < Math.max(failedBusIds.length, blockingBusIds.length);
|
|
866
|
+
index++
|
|
867
|
+
) {
|
|
868
|
+
const failedBusId = failedBusIds[index]
|
|
869
|
+
const blockingBusId = blockingBusIds[index]
|
|
870
|
+
if (failedBusId && !repairBusIds.includes(failedBusId)) {
|
|
871
|
+
repairBusIds.push(failedBusId)
|
|
872
|
+
}
|
|
873
|
+
if (blockingBusId && !repairBusIds.includes(blockingBusId)) {
|
|
874
|
+
repairBusIds.push(blockingBusId)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
for (const failedBusId of failedBusIds) {
|
|
879
|
+
const failedLayer = assignment[failedBusId]
|
|
880
|
+
const failedCandidateLayers = this.escapeLayersByBusId[failedBusId]
|
|
881
|
+
if (!failedLayer || !failedCandidateLayers) continue
|
|
882
|
+
for (const blockingBusId of blockingBusIds.slice(0, 4)) {
|
|
883
|
+
const blockingLayer = assignment[blockingBusId]
|
|
884
|
+
const blockingCandidateLayers = this.escapeLayersByBusId[blockingBusId]
|
|
885
|
+
if (
|
|
886
|
+
!blockingLayer ||
|
|
887
|
+
!blockingCandidateLayers ||
|
|
888
|
+
!failedCandidateLayers.includes(blockingLayer) ||
|
|
889
|
+
!blockingCandidateLayers.includes(failedLayer)
|
|
890
|
+
) {
|
|
891
|
+
continue
|
|
892
|
+
}
|
|
893
|
+
addRepair({
|
|
894
|
+
...assignment,
|
|
895
|
+
[failedBusId]: blockingLayer,
|
|
896
|
+
[blockingBusId]: failedLayer,
|
|
897
|
+
})
|
|
898
|
+
if (repairs.length >= maximumRepairs) break
|
|
899
|
+
}
|
|
900
|
+
if (repairs.length >= maximumRepairs) break
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
for (const busId of repairBusIds) {
|
|
904
|
+
const currentLayer = assignment[busId]
|
|
905
|
+
const candidateLayers = this.escapeLayersByBusId[busId]
|
|
906
|
+
if (!currentLayer || !candidateLayers) continue
|
|
907
|
+
const currentLayerIndex = candidateLayers.indexOf(currentLayer)
|
|
908
|
+
for (let shift = 1; shift < candidateLayers.length; shift++) {
|
|
909
|
+
const candidateLayer =
|
|
910
|
+
candidateLayers[
|
|
911
|
+
(Math.max(currentLayerIndex, 0) + shift) % candidateLayers.length
|
|
912
|
+
]!
|
|
913
|
+
if (candidateLayer === currentLayer) continue
|
|
914
|
+
addRepair({ ...assignment, [busId]: candidateLayer })
|
|
915
|
+
if (repairs.length >= maximumRepairs) break
|
|
916
|
+
}
|
|
917
|
+
if (repairs.length >= maximumRepairs) break
|
|
918
|
+
}
|
|
919
|
+
this.pendingRepairAssignments.push(...repairs)
|
|
920
|
+
}
|
|
921
|
+
|
|
493
922
|
override _step(): void {
|
|
494
|
-
|
|
923
|
+
if (!this.groupedBeamEvaluated) {
|
|
924
|
+
this.groupedBeamEvaluated = true
|
|
925
|
+
let beamAttempt = this.evaluateGroupedBeam(-1)
|
|
926
|
+
if (!beamAttempt) {
|
|
927
|
+
beamAttempt = this.evaluateGroupedBeam(-1, true)
|
|
928
|
+
}
|
|
929
|
+
if (beamAttempt) {
|
|
930
|
+
this.attempts.push(beamAttempt.summary)
|
|
931
|
+
this.bestAttempt = beamAttempt
|
|
932
|
+
this.stats = {
|
|
933
|
+
assignment: 0,
|
|
934
|
+
assignmentCount: this.config.maxLayerCombinations,
|
|
935
|
+
routedBuses: `${beamAttempt.summary.routedBusCount}/${this.preparedBuses.length}`,
|
|
936
|
+
routedConnections: `${beamAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
|
|
937
|
+
failedBuses: "none",
|
|
938
|
+
bestScore: beamAttempt.summary.score,
|
|
939
|
+
}
|
|
940
|
+
this.solved = true
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
let assignment: Readonly<Record<string, string>> | undefined
|
|
946
|
+
while (
|
|
947
|
+
!assignment &&
|
|
948
|
+
this.nextAssignmentIndex < this.config.maxLayerCombinations
|
|
949
|
+
) {
|
|
950
|
+
const preferGeneratedAssignment = this.nextAssignmentIndex % 3 === 0
|
|
951
|
+
let candidate: Readonly<Record<string, string>> | undefined
|
|
952
|
+
let candidateCameFromRepairQueue = false
|
|
953
|
+
if (preferGeneratedAssignment) {
|
|
954
|
+
candidate = this.layerAssignments[this.nextGeneratedAssignmentIndex++]
|
|
955
|
+
} else {
|
|
956
|
+
candidate = this.pendingRepairAssignments.pop()
|
|
957
|
+
candidateCameFromRepairQueue = candidate !== undefined
|
|
958
|
+
}
|
|
959
|
+
if (!candidate) {
|
|
960
|
+
candidate = preferGeneratedAssignment
|
|
961
|
+
? this.pendingRepairAssignments.pop()
|
|
962
|
+
: this.layerAssignments[this.nextGeneratedAssignmentIndex++]
|
|
963
|
+
candidateCameFromRepairQueue =
|
|
964
|
+
preferGeneratedAssignment && candidate !== undefined
|
|
965
|
+
}
|
|
966
|
+
if (!candidate) break
|
|
967
|
+
const candidateKey = JSON.stringify(candidate)
|
|
968
|
+
if (candidateCameFromRepairQueue) {
|
|
969
|
+
this.queuedAssignmentKeys.delete(candidateKey)
|
|
970
|
+
}
|
|
971
|
+
if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
|
|
972
|
+
assignment = candidate
|
|
973
|
+
}
|
|
495
974
|
if (!assignment) {
|
|
496
975
|
if (
|
|
497
976
|
this.bestAttempt &&
|
|
@@ -513,6 +992,18 @@ export class FanoutSolver extends BaseSolver {
|
|
|
513
992
|
assignment,
|
|
514
993
|
)
|
|
515
994
|
this.nextAssignmentIndex++
|
|
995
|
+
this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
|
|
996
|
+
if (
|
|
997
|
+
!this.bestAttempt ||
|
|
998
|
+
attempt.summary.routedConnectionCount >=
|
|
999
|
+
this.bestAttempt.summary.routedConnectionCount
|
|
1000
|
+
) {
|
|
1001
|
+
this.prioritizeFailedBusRepairs(
|
|
1002
|
+
assignment,
|
|
1003
|
+
attempt.summary.failedBusIds,
|
|
1004
|
+
attempt.blockingBusIds,
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
516
1007
|
this.attempts.push(attempt.summary)
|
|
517
1008
|
if (
|
|
518
1009
|
!this.bestAttempt ||
|
|
@@ -522,7 +1013,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
522
1013
|
}
|
|
523
1014
|
this.stats = {
|
|
524
1015
|
assignment: attempt.summary.assignmentIndex + 1,
|
|
525
|
-
assignmentCount: this.
|
|
1016
|
+
assignmentCount: this.config.maxLayerCombinations,
|
|
526
1017
|
routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
|
|
527
1018
|
routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
|
|
528
1019
|
failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
|
|
@@ -537,7 +1028,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
537
1028
|
|
|
538
1029
|
computeProgress(): number {
|
|
539
1030
|
if (this.solved || this.failed) return 1
|
|
540
|
-
return this.nextAssignmentIndex / this.
|
|
1031
|
+
return this.nextAssignmentIndex / this.config.maxLayerCombinations
|
|
541
1032
|
}
|
|
542
1033
|
|
|
543
1034
|
override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
|
|
@@ -550,6 +1041,15 @@ export class FanoutSolver extends BaseSolver {
|
|
|
550
1041
|
"FanoutSolver: getOutput() called before a complete fanout was solved",
|
|
551
1042
|
)
|
|
552
1043
|
}
|
|
1044
|
+
const validation = this.validateCompletePlans(
|
|
1045
|
+
this.bestAttempt.plans,
|
|
1046
|
+
this.bestAttempt.outputSrj,
|
|
1047
|
+
)
|
|
1048
|
+
if (!validation.valid) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
`FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
553
1053
|
return {
|
|
554
1054
|
simpleRouteJson: this.bestAttempt.outputSrj,
|
|
555
1055
|
fanoutTraces: this.bestAttempt.plans.map((plan) => plan.trace),
|
|
@@ -570,6 +1070,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
570
1070
|
this.preparedBuses.map((bus) => [bus.busId, bus.direction]),
|
|
571
1071
|
),
|
|
572
1072
|
attempts: [...this.attempts],
|
|
1073
|
+
validation,
|
|
573
1074
|
}
|
|
574
1075
|
}
|
|
575
1076
|
|