@tscircuit/fanout-solver 0.0.16 → 0.0.18
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 +18 -5
- package/lib/fanout-solver.ts +485 -77
- package/lib/layer-names.ts +38 -12
- package/lib/route-bus.ts +199 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -204,7 +204,9 @@ The repository also loads all 200 samples from
|
|
|
204
204
|
[`tscircuit/dataset-srj19`](https://github.com/tscircuit/dataset-srj19) as a
|
|
205
205
|
pinned development dependency. The adapter keeps the complete obstacle field
|
|
206
206
|
but selects only connections that touch the BGA, producing progressively larger
|
|
207
|
-
|
|
207
|
+
fanout problems with opposite-side passive overlays. Every adapted problem uses
|
|
208
|
+
the same six-layer stackup (`top`, `inner1` through `inner4`, and `bottom`) so
|
|
209
|
+
benchmark improvements are directly comparable.
|
|
208
210
|
|
|
209
211
|
Run the full benchmark with:
|
|
210
212
|
|
|
@@ -213,10 +215,21 @@ Run the full benchmark with:
|
|
|
213
215
|
```
|
|
214
216
|
|
|
215
217
|
Use `--sample sample001`, `--limit 10`, or
|
|
216
|
-
`--max-layer-combinations 16` for shorter runs.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
`--max-layer-combinations 16` for shorter runs. Samples run one at a time by
|
|
219
|
+
default and print progress as they finish. `--concurrency 8` runs isolated
|
|
220
|
+
samples in parallel, and `--sample-timeout-seconds 600` prevents a difficult
|
|
221
|
+
sample from blocking the remaining work. Each run writes the full ordered
|
|
222
|
+
results to `benchmark-results/srj19.json` and
|
|
223
|
+
`benchmark-results/srj19.md`. Partial solutions are reported as benchmark
|
|
224
|
+
results instead of failing the command, making current completion rates a
|
|
225
|
+
baseline for solver improvements. `bun run benchmark:srj19` is an alias for the
|
|
226
|
+
same command.
|
|
227
|
+
|
|
228
|
+
The `SRJ19 Benchmark` GitHub Actions workflow runs the complete dataset on a
|
|
229
|
+
Blacksmith 32-vCPU ARM runner with 32 sample processes by default. It can be
|
|
230
|
+
started manually with an optional sample id, or for a pull request by adding
|
|
231
|
+
`[BENCHMARK TEST]` to its title. The workflow publishes the Markdown summary and
|
|
232
|
+
uploads both reports as an artifact.
|
|
220
233
|
|
|
221
234
|
Run `bun run start` and open the `datasets/srj19` Cosmos fixture to step the
|
|
222
235
|
selected sample through `GenericSolverDebugger`. The page has Previous/Next,
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -5,20 +5,24 @@ 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"
|
|
18
21
|
import type {
|
|
19
22
|
AssignmentAttempt,
|
|
20
23
|
FanoutAttemptSummary,
|
|
21
24
|
FanoutBorderDistribution,
|
|
25
|
+
FanoutRoutePlan,
|
|
22
26
|
FanoutSolverOptions,
|
|
23
27
|
FanoutSolverOutput,
|
|
24
28
|
PreparedBus,
|
|
@@ -38,6 +42,17 @@ interface ResolvedFanoutConfig {
|
|
|
38
42
|
maxLayerCombinations: number
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
interface EvaluatedAssignment extends AssignmentAttempt {
|
|
46
|
+
blockingBusIds: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GroupedBeamState {
|
|
50
|
+
assignment: Readonly<Record<string, string>>
|
|
51
|
+
plans: FanoutRoutePlan[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
|
|
55
|
+
|
|
41
56
|
function resolvePositiveNumber(label: string, value: number): number {
|
|
42
57
|
if (!Number.isFinite(value) || value <= 0) {
|
|
43
58
|
throw new Error(
|
|
@@ -199,62 +214,12 @@ function getBusDepthInRows(bus: PreparedBus): number {
|
|
|
199
214
|
)
|
|
200
215
|
}
|
|
201
216
|
|
|
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
217
|
function createPreferredLayerAssignment(params: {
|
|
251
218
|
buses: PreparedBus[]
|
|
252
219
|
escapeLayers: string[]
|
|
253
|
-
|
|
254
|
-
traceWidth: number
|
|
255
|
-
clearance: number
|
|
220
|
+
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
256
221
|
}): Readonly<Record<string, string>> {
|
|
257
|
-
const { buses, escapeLayers,
|
|
222
|
+
const { buses, escapeLayers, escapeLayersByBusId } = params
|
|
258
223
|
const assignment: Record<string, string> = {}
|
|
259
224
|
const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
|
|
260
225
|
let nextViaLayerIndex = 0
|
|
@@ -273,16 +238,13 @@ function createPreferredLayerAssignment(params: {
|
|
|
273
238
|
assignment[bus.busId] = bus.termination.layer
|
|
274
239
|
continue
|
|
275
240
|
}
|
|
276
|
-
const
|
|
241
|
+
const routableEscapeLayers = escapeLayersByBusId[bus.busId] ?? escapeLayers
|
|
242
|
+
const viaLayers = routableEscapeLayers.filter(
|
|
243
|
+
(layer) => layer !== sourceLayer,
|
|
244
|
+
)
|
|
277
245
|
if (
|
|
278
|
-
|
|
279
|
-
busIsOnOutwardComponentEdge(bus)
|
|
280
|
-
!sourceLayerEscapeIsBlocked({
|
|
281
|
-
bus,
|
|
282
|
-
srj,
|
|
283
|
-
traceWidth,
|
|
284
|
-
clearance,
|
|
285
|
-
})
|
|
246
|
+
routableEscapeLayers.includes(sourceLayer) &&
|
|
247
|
+
busIsOnOutwardComponentEdge(bus)
|
|
286
248
|
) {
|
|
287
249
|
assignment[bus.busId] = sourceLayer
|
|
288
250
|
} else if (viaLayers.length > 0) {
|
|
@@ -321,12 +283,66 @@ function prioritizeLayerAssignment(params: {
|
|
|
321
283
|
].slice(0, maxAssignments)
|
|
322
284
|
}
|
|
323
285
|
|
|
286
|
+
function getCandidateEscapeLayersForBus(params: {
|
|
287
|
+
bus: PreparedBus
|
|
288
|
+
srj: SimpleRouteJson
|
|
289
|
+
config: ResolvedFanoutConfig
|
|
290
|
+
staticClearanceCache: RouteBusStaticClearanceCache
|
|
291
|
+
}): string[] {
|
|
292
|
+
const { bus, srj, config, staticClearanceCache } = params
|
|
293
|
+
const individuallyRoutableLayers = config.escapeLayers.filter(
|
|
294
|
+
(targetLayer) =>
|
|
295
|
+
routeBus({
|
|
296
|
+
srj,
|
|
297
|
+
bus,
|
|
298
|
+
targetLayer,
|
|
299
|
+
acceptedPlans: [],
|
|
300
|
+
layerNames: config.layerNames,
|
|
301
|
+
traceWidth: config.traceWidth,
|
|
302
|
+
viaDiameter: config.viaDiameter,
|
|
303
|
+
viaHoleDiameter: config.viaHoleDiameter,
|
|
304
|
+
clearance: config.clearance,
|
|
305
|
+
compactBusTracks: config.compactBusTracks,
|
|
306
|
+
staticClearanceCache,
|
|
307
|
+
}) !== null,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Existing plans only add clearance constraints, so a layer that cannot
|
|
311
|
+
// route this bus by itself cannot become viable later in an assignment.
|
|
312
|
+
// Preserve the original candidates when none route so impossible problems
|
|
313
|
+
// still produce the usual failed-solver result instead of throwing here.
|
|
314
|
+
return individuallyRoutableLayers.length > 0
|
|
315
|
+
? individuallyRoutableLayers
|
|
316
|
+
: config.escapeLayers
|
|
317
|
+
}
|
|
318
|
+
|
|
324
319
|
export class FanoutSolver extends BaseSolver {
|
|
325
320
|
readonly preparedBuses: PreparedBus[]
|
|
326
321
|
readonly attempts: FanoutAttemptSummary[] = []
|
|
327
322
|
readonly layerAssignments: Array<Readonly<Record<string, string>>>
|
|
328
323
|
readonly config: ResolvedFanoutConfig
|
|
324
|
+
private readonly escapeLayersByBusId: Readonly<
|
|
325
|
+
Record<string, readonly string[]>
|
|
326
|
+
>
|
|
327
|
+
private readonly evaluatedAssignmentKeys = new Set<string>()
|
|
328
|
+
private readonly queuedAssignmentKeys = new Set<string>()
|
|
329
|
+
private readonly assignmentRepairDepthByKey = new Map<string, number>()
|
|
330
|
+
private readonly pendingRepairAssignments: Array<
|
|
331
|
+
Readonly<Record<string, string>>
|
|
332
|
+
> = []
|
|
333
|
+
private readonly routeStaticClearanceCache: RouteBusStaticClearanceCache =
|
|
334
|
+
new Map()
|
|
335
|
+
private readonly routingPrefixCache = new Map<
|
|
336
|
+
string,
|
|
337
|
+
{
|
|
338
|
+
plans: AssignmentAttempt["plans"]
|
|
339
|
+
failedBusIds: string[]
|
|
340
|
+
blockingBusCounts: Map<string, number>
|
|
341
|
+
}
|
|
342
|
+
>()
|
|
343
|
+
private groupedBeamEvaluated = false
|
|
329
344
|
private nextAssignmentIndex = 0
|
|
345
|
+
private nextGeneratedAssignmentIndex = 0
|
|
330
346
|
private bestAttempt: AssignmentAttempt | null = null
|
|
331
347
|
|
|
332
348
|
constructor(
|
|
@@ -364,9 +380,27 @@ export class FanoutSolver extends BaseSolver {
|
|
|
364
380
|
: [],
|
|
365
381
|
),
|
|
366
382
|
)
|
|
383
|
+
const escapeLayersByBusId = Object.fromEntries(
|
|
384
|
+
this.preparedBuses.flatMap((bus) => {
|
|
385
|
+
if (bus.termination.type === "plane") return []
|
|
386
|
+
return [
|
|
387
|
+
[
|
|
388
|
+
bus.busId,
|
|
389
|
+
getCandidateEscapeLayersForBus({
|
|
390
|
+
bus,
|
|
391
|
+
srj: inputSrj,
|
|
392
|
+
config: this.config,
|
|
393
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
394
|
+
}),
|
|
395
|
+
] as const,
|
|
396
|
+
]
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
this.escapeLayersByBusId = escapeLayersByBusId
|
|
367
400
|
const generatedAssignments = generateLayerAssignments({
|
|
368
401
|
busIds: boundaryBusIds,
|
|
369
402
|
layers: this.config.escapeLayers,
|
|
403
|
+
layersByBusId: escapeLayersByBusId,
|
|
370
404
|
maxAssignments: this.config.maxLayerCombinations,
|
|
371
405
|
}).map((assignment) => ({
|
|
372
406
|
...assignment,
|
|
@@ -376,26 +410,26 @@ export class FanoutSolver extends BaseSolver {
|
|
|
376
410
|
preferredAssignment: createPreferredLayerAssignment({
|
|
377
411
|
buses: this.preparedBuses,
|
|
378
412
|
escapeLayers: this.config.escapeLayers,
|
|
379
|
-
|
|
380
|
-
traceWidth: this.config.traceWidth,
|
|
381
|
-
clearance: this.config.clearance,
|
|
413
|
+
escapeLayersByBusId,
|
|
382
414
|
}),
|
|
383
415
|
generatedAssignments,
|
|
384
416
|
maxAssignments: this.config.maxLayerCombinations,
|
|
385
417
|
})
|
|
386
|
-
this.MAX_ITERATIONS = this.
|
|
418
|
+
this.MAX_ITERATIONS = this.config.maxLayerCombinations + 2
|
|
387
419
|
}
|
|
388
420
|
|
|
389
421
|
override getSolverName(): string {
|
|
390
422
|
return "FanoutSolver"
|
|
391
423
|
}
|
|
392
424
|
|
|
393
|
-
private
|
|
425
|
+
private evaluateAssignmentWithStrategy(
|
|
394
426
|
assignmentIndex: number,
|
|
395
427
|
busLayerAssignments: Readonly<Record<string, string>>,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
428
|
+
routingStrategy: RoutingStrategy,
|
|
429
|
+
): EvaluatedAssignment {
|
|
430
|
+
let plans: AssignmentAttempt["plans"] = []
|
|
431
|
+
let failedBusIds: string[] = []
|
|
432
|
+
let blockingBusCounts = new Map<string, number>()
|
|
399
433
|
const isSingleLayerFanout = this.config.escapeLayers.length === 1
|
|
400
434
|
if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
|
|
401
435
|
const singleLayerParams = {
|
|
@@ -425,13 +459,21 @@ export class FanoutSolver extends BaseSolver {
|
|
|
425
459
|
(a, b) =>
|
|
426
460
|
Number(a.termination.type === "plane") -
|
|
427
461
|
Number(b.termination.type === "plane") ||
|
|
462
|
+
(routingStrategy === "group-by-layer"
|
|
463
|
+
? (busLayerAssignments[a.busId] ?? "").localeCompare(
|
|
464
|
+
busLayerAssignments[b.busId] ?? "",
|
|
465
|
+
)
|
|
466
|
+
: 0) ||
|
|
428
467
|
b.componentObstacles.length - a.componentObstacles.length ||
|
|
429
468
|
(isSingleLayerFanout
|
|
430
469
|
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
431
470
|
: b.connections.length - a.connections.length ||
|
|
432
|
-
|
|
471
|
+
(routingStrategy === "deep-first"
|
|
472
|
+
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
473
|
+
: getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b))),
|
|
433
474
|
)
|
|
434
475
|
|
|
476
|
+
let routingPrefixKey = `${routingStrategy}|`
|
|
435
477
|
for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
|
|
436
478
|
? []
|
|
437
479
|
: busesInRoutingOrder) {
|
|
@@ -441,6 +483,15 @@ export class FanoutSolver extends BaseSolver {
|
|
|
441
483
|
`FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
|
|
442
484
|
)
|
|
443
485
|
}
|
|
486
|
+
routingPrefixKey += `${targetLayer.length}:${targetLayer};`
|
|
487
|
+
const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey)
|
|
488
|
+
if (cachedPrefix) {
|
|
489
|
+
plans = [...cachedPrefix.plans]
|
|
490
|
+
failedBusIds = [...cachedPrefix.failedBusIds]
|
|
491
|
+
blockingBusCounts = new Map(cachedPrefix.blockingBusCounts)
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
const currentBusBlockingCounts = new Map<string, number>()
|
|
444
495
|
const busPlans = routeBus({
|
|
445
496
|
srj: this.inputSrj,
|
|
446
497
|
bus,
|
|
@@ -452,12 +503,25 @@ export class FanoutSolver extends BaseSolver {
|
|
|
452
503
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
453
504
|
clearance: this.config.clearance,
|
|
454
505
|
compactBusTracks: this.config.compactBusTracks,
|
|
506
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
507
|
+
blockingBusCounts: currentBusBlockingCounts,
|
|
455
508
|
})
|
|
456
509
|
if (!busPlans) {
|
|
457
510
|
failedBusIds.push(bus.busId)
|
|
458
|
-
|
|
511
|
+
for (const [blockingBusId, count] of currentBusBlockingCounts) {
|
|
512
|
+
blockingBusCounts.set(
|
|
513
|
+
blockingBusId,
|
|
514
|
+
(blockingBusCounts.get(blockingBusId) ?? 0) + count,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
plans.push(...busPlans)
|
|
459
519
|
}
|
|
460
|
-
|
|
520
|
+
this.routingPrefixCache.set(routingPrefixKey, {
|
|
521
|
+
plans: [...plans],
|
|
522
|
+
failedBusIds: [...failedBusIds],
|
|
523
|
+
blockingBusCounts: new Map(blockingBusCounts),
|
|
524
|
+
})
|
|
461
525
|
}
|
|
462
526
|
|
|
463
527
|
const routedBusCount = this.preparedBuses.length - failedBusIds.length
|
|
@@ -482,6 +546,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
482
546
|
return {
|
|
483
547
|
summary,
|
|
484
548
|
plans,
|
|
549
|
+
blockingBusIds: [...blockingBusCounts.entries()]
|
|
550
|
+
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
551
|
+
.map(([busId]) => busId),
|
|
485
552
|
outputSrj: buildOutputSimpleRouteJson({
|
|
486
553
|
inputSrj: this.inputSrj,
|
|
487
554
|
plans,
|
|
@@ -490,8 +557,337 @@ export class FanoutSolver extends BaseSolver {
|
|
|
490
557
|
}
|
|
491
558
|
}
|
|
492
559
|
|
|
560
|
+
private evaluateAssignment(
|
|
561
|
+
assignmentIndex: number,
|
|
562
|
+
busLayerAssignments: Readonly<Record<string, string>>,
|
|
563
|
+
): EvaluatedAssignment {
|
|
564
|
+
let bestAttempt = this.evaluateAssignmentWithStrategy(
|
|
565
|
+
assignmentIndex,
|
|
566
|
+
busLayerAssignments,
|
|
567
|
+
"default",
|
|
568
|
+
)
|
|
569
|
+
if (
|
|
570
|
+
bestAttempt.summary.routedConnectionCount ===
|
|
571
|
+
this.inputSrj.connections.length
|
|
572
|
+
) {
|
|
573
|
+
return bestAttempt
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
for (const routingStrategy of ["group-by-layer", "deep-first"] as const) {
|
|
577
|
+
const attempt = this.evaluateAssignmentWithStrategy(
|
|
578
|
+
assignmentIndex,
|
|
579
|
+
busLayerAssignments,
|
|
580
|
+
routingStrategy,
|
|
581
|
+
)
|
|
582
|
+
if (attempt.summary.score < bestAttempt.summary.score) {
|
|
583
|
+
bestAttempt = attempt
|
|
584
|
+
}
|
|
585
|
+
if (
|
|
586
|
+
bestAttempt.summary.routedConnectionCount ===
|
|
587
|
+
this.inputSrj.connections.length
|
|
588
|
+
) {
|
|
589
|
+
return bestAttempt
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return bestAttempt
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Search layer assignments and track alternatives together. The regular
|
|
597
|
+
* assignment loop commits to one route per bus before the next bus is
|
|
598
|
+
* considered, so a locally-valid track can still starve a later bus. A
|
|
599
|
+
* bounded beam keeps several grouped-layer route prefixes alive and is
|
|
600
|
+
* especially useful for the many singleton buses in the SRJ19 samples.
|
|
601
|
+
*/
|
|
602
|
+
private evaluateGroupedBeam(
|
|
603
|
+
assignmentIndex: number,
|
|
604
|
+
groupByDirection = false,
|
|
605
|
+
): EvaluatedAssignment | null {
|
|
606
|
+
if (this.config.escapeLayers.length < 2) return null
|
|
607
|
+
if (this.preparedBuses.length > 56) return null
|
|
608
|
+
if (this.preparedBuses.some((bus) => bus.connections.length !== 1)) {
|
|
609
|
+
return null
|
|
610
|
+
}
|
|
611
|
+
if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
|
|
612
|
+
return null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
|
|
616
|
+
const aLayerCount =
|
|
617
|
+
a.termination.type === "plane"
|
|
618
|
+
? 1
|
|
619
|
+
: (this.escapeLayersByBusId[a.busId]?.length ??
|
|
620
|
+
this.config.escapeLayers.length)
|
|
621
|
+
const bLayerCount =
|
|
622
|
+
b.termination.type === "plane"
|
|
623
|
+
? 1
|
|
624
|
+
: (this.escapeLayersByBusId[b.busId]?.length ??
|
|
625
|
+
this.config.escapeLayers.length)
|
|
626
|
+
return (
|
|
627
|
+
Number(a.termination.type === "plane") -
|
|
628
|
+
Number(b.termination.type === "plane") ||
|
|
629
|
+
(groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
|
|
630
|
+
aLayerCount - bLayerCount ||
|
|
631
|
+
b.componentObstacles.length - a.componentObstacles.length ||
|
|
632
|
+
b.connections.length - a.connections.length ||
|
|
633
|
+
getBusDepthInRows(b) - getBusDepthInRows(a) ||
|
|
634
|
+
a.busId.localeCompare(b.busId)
|
|
635
|
+
)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const beamWidth = 128
|
|
639
|
+
const alternativesPerLayer = 4
|
|
640
|
+
let states: GroupedBeamState[] = [{ assignment: {}, plans: [] }]
|
|
641
|
+
|
|
642
|
+
const getStateScore = (state: GroupedBeamState): number => {
|
|
643
|
+
const routeLength = state.plans.reduce(
|
|
644
|
+
(total, plan) => total + plan.length,
|
|
645
|
+
0,
|
|
646
|
+
)
|
|
647
|
+
const viaCount = state.plans.filter((plan) => plan.via).length
|
|
648
|
+
return (
|
|
649
|
+
routeLength +
|
|
650
|
+
viaCount * 0.1 +
|
|
651
|
+
assignmentLoadPenalty(state.assignment) * 0.01
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for (const bus of busesInSearchOrder) {
|
|
656
|
+
const nextStates: GroupedBeamState[] = []
|
|
657
|
+
for (const state of states) {
|
|
658
|
+
const candidateLayers =
|
|
659
|
+
bus.termination.type === "plane"
|
|
660
|
+
? [bus.termination.layer]
|
|
661
|
+
: (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
|
|
662
|
+
const layerLoads = new Map<string, number>()
|
|
663
|
+
for (const layer of Object.values(state.assignment)) {
|
|
664
|
+
layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + 1)
|
|
665
|
+
}
|
|
666
|
+
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
667
|
+
const orderedLayers = candidateLayers.toSorted(
|
|
668
|
+
(first, second) =>
|
|
669
|
+
(layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
|
|
670
|
+
Number(first === sourceLayer) - Number(second === sourceLayer) ||
|
|
671
|
+
first.localeCompare(second),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
for (const targetLayer of orderedLayers) {
|
|
675
|
+
const busAlternatives = routeBusAlternatives(
|
|
676
|
+
{
|
|
677
|
+
srj: this.inputSrj,
|
|
678
|
+
bus,
|
|
679
|
+
targetLayer,
|
|
680
|
+
acceptedPlans: state.plans,
|
|
681
|
+
layerNames: this.config.layerNames,
|
|
682
|
+
traceWidth: this.config.traceWidth,
|
|
683
|
+
viaDiameter: this.config.viaDiameter,
|
|
684
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
685
|
+
clearance: this.config.clearance,
|
|
686
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
687
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
688
|
+
},
|
|
689
|
+
alternativesPerLayer,
|
|
690
|
+
)
|
|
691
|
+
for (const busPlans of busAlternatives) {
|
|
692
|
+
nextStates.push({
|
|
693
|
+
assignment: {
|
|
694
|
+
...state.assignment,
|
|
695
|
+
[bus.busId]: targetLayer,
|
|
696
|
+
},
|
|
697
|
+
plans: [...state.plans, ...busPlans],
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (nextStates.length === 0) return null
|
|
704
|
+
nextStates.sort((first, second) => {
|
|
705
|
+
const scoreDifference = getStateScore(first) - getStateScore(second)
|
|
706
|
+
if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
|
|
707
|
+
return JSON.stringify(first.assignment).localeCompare(
|
|
708
|
+
JSON.stringify(second.assignment),
|
|
709
|
+
)
|
|
710
|
+
})
|
|
711
|
+
const statesByAssignment = new Map<string, number>()
|
|
712
|
+
states = []
|
|
713
|
+
for (const state of nextStates) {
|
|
714
|
+
const key = JSON.stringify(state.assignment)
|
|
715
|
+
const sameAssignmentCount = statesByAssignment.get(key) ?? 0
|
|
716
|
+
if (sameAssignmentCount >= 2) continue
|
|
717
|
+
statesByAssignment.set(key, sameAssignmentCount + 1)
|
|
718
|
+
states.push(state)
|
|
719
|
+
if (states.length >= beamWidth) break
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const bestState = states[0]
|
|
724
|
+
if (!bestState) return null
|
|
725
|
+
const score =
|
|
726
|
+
bestState.plans.length === this.inputSrj.connections.length
|
|
727
|
+
? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
728
|
+
bestState.plans.filter((plan) => plan.via).length * 0.1 +
|
|
729
|
+
assignmentLoadPenalty(bestState.assignment) * 0.01
|
|
730
|
+
: Number.POSITIVE_INFINITY
|
|
731
|
+
if (!Number.isFinite(score)) return null
|
|
732
|
+
|
|
733
|
+
const summary: FanoutAttemptSummary = {
|
|
734
|
+
assignmentIndex,
|
|
735
|
+
busLayerAssignments: bestState.assignment,
|
|
736
|
+
routedBusCount: this.preparedBuses.length,
|
|
737
|
+
routedConnectionCount: bestState.plans.length,
|
|
738
|
+
failedBusIds: [],
|
|
739
|
+
score,
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
summary,
|
|
743
|
+
plans: bestState.plans,
|
|
744
|
+
blockingBusIds: [],
|
|
745
|
+
outputSrj: buildOutputSimpleRouteJson({
|
|
746
|
+
inputSrj: this.inputSrj,
|
|
747
|
+
plans: bestState.plans,
|
|
748
|
+
layerNames: this.config.layerNames,
|
|
749
|
+
}),
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private prioritizeFailedBusRepairs(
|
|
754
|
+
assignment: Readonly<Record<string, string>>,
|
|
755
|
+
failedBusIds: readonly string[],
|
|
756
|
+
blockingBusIds: readonly string[],
|
|
757
|
+
): void {
|
|
758
|
+
const assignmentKey = JSON.stringify(assignment)
|
|
759
|
+
const repairDepth = this.assignmentRepairDepthByKey.get(assignmentKey) ?? 0
|
|
760
|
+
if (repairDepth >= 2) return
|
|
761
|
+
|
|
762
|
+
const maximumRepairs = 8
|
|
763
|
+
const repairs: Array<Readonly<Record<string, string>>> = []
|
|
764
|
+
const repairKeys = new Set<string>()
|
|
765
|
+
const addRepair = (repair: Readonly<Record<string, string>>): void => {
|
|
766
|
+
const key = JSON.stringify(repair)
|
|
767
|
+
if (
|
|
768
|
+
repairKeys.has(key) ||
|
|
769
|
+
this.evaluatedAssignmentKeys.has(key) ||
|
|
770
|
+
this.queuedAssignmentKeys.has(key)
|
|
771
|
+
) {
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
repairKeys.add(key)
|
|
775
|
+
this.queuedAssignmentKeys.add(key)
|
|
776
|
+
this.assignmentRepairDepthByKey.set(key, repairDepth + 1)
|
|
777
|
+
repairs.push(repair)
|
|
778
|
+
}
|
|
779
|
+
const repairBusIds: string[] = []
|
|
780
|
+
for (
|
|
781
|
+
let index = 0;
|
|
782
|
+
index < Math.max(failedBusIds.length, blockingBusIds.length);
|
|
783
|
+
index++
|
|
784
|
+
) {
|
|
785
|
+
const failedBusId = failedBusIds[index]
|
|
786
|
+
const blockingBusId = blockingBusIds[index]
|
|
787
|
+
if (failedBusId && !repairBusIds.includes(failedBusId)) {
|
|
788
|
+
repairBusIds.push(failedBusId)
|
|
789
|
+
}
|
|
790
|
+
if (blockingBusId && !repairBusIds.includes(blockingBusId)) {
|
|
791
|
+
repairBusIds.push(blockingBusId)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
for (const failedBusId of failedBusIds) {
|
|
796
|
+
const failedLayer = assignment[failedBusId]
|
|
797
|
+
const failedCandidateLayers = this.escapeLayersByBusId[failedBusId]
|
|
798
|
+
if (!failedLayer || !failedCandidateLayers) continue
|
|
799
|
+
for (const blockingBusId of blockingBusIds.slice(0, 4)) {
|
|
800
|
+
const blockingLayer = assignment[blockingBusId]
|
|
801
|
+
const blockingCandidateLayers = this.escapeLayersByBusId[blockingBusId]
|
|
802
|
+
if (
|
|
803
|
+
!blockingLayer ||
|
|
804
|
+
!blockingCandidateLayers ||
|
|
805
|
+
!failedCandidateLayers.includes(blockingLayer) ||
|
|
806
|
+
!blockingCandidateLayers.includes(failedLayer)
|
|
807
|
+
) {
|
|
808
|
+
continue
|
|
809
|
+
}
|
|
810
|
+
addRepair({
|
|
811
|
+
...assignment,
|
|
812
|
+
[failedBusId]: blockingLayer,
|
|
813
|
+
[blockingBusId]: failedLayer,
|
|
814
|
+
})
|
|
815
|
+
if (repairs.length >= maximumRepairs) break
|
|
816
|
+
}
|
|
817
|
+
if (repairs.length >= maximumRepairs) break
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
for (const busId of repairBusIds) {
|
|
821
|
+
const currentLayer = assignment[busId]
|
|
822
|
+
const candidateLayers = this.escapeLayersByBusId[busId]
|
|
823
|
+
if (!currentLayer || !candidateLayers) continue
|
|
824
|
+
const currentLayerIndex = candidateLayers.indexOf(currentLayer)
|
|
825
|
+
for (let shift = 1; shift < candidateLayers.length; shift++) {
|
|
826
|
+
const candidateLayer =
|
|
827
|
+
candidateLayers[
|
|
828
|
+
(Math.max(currentLayerIndex, 0) + shift) % candidateLayers.length
|
|
829
|
+
]!
|
|
830
|
+
if (candidateLayer === currentLayer) continue
|
|
831
|
+
addRepair({ ...assignment, [busId]: candidateLayer })
|
|
832
|
+
if (repairs.length >= maximumRepairs) break
|
|
833
|
+
}
|
|
834
|
+
if (repairs.length >= maximumRepairs) break
|
|
835
|
+
}
|
|
836
|
+
this.pendingRepairAssignments.push(...repairs)
|
|
837
|
+
}
|
|
838
|
+
|
|
493
839
|
override _step(): void {
|
|
494
|
-
|
|
840
|
+
if (!this.groupedBeamEvaluated) {
|
|
841
|
+
this.groupedBeamEvaluated = true
|
|
842
|
+
let beamAttempt = this.evaluateGroupedBeam(-1)
|
|
843
|
+
if (!beamAttempt) {
|
|
844
|
+
beamAttempt = this.evaluateGroupedBeam(-1, true)
|
|
845
|
+
}
|
|
846
|
+
if (beamAttempt) {
|
|
847
|
+
this.attempts.push(beamAttempt.summary)
|
|
848
|
+
this.bestAttempt = beamAttempt
|
|
849
|
+
this.stats = {
|
|
850
|
+
assignment: 0,
|
|
851
|
+
assignmentCount: this.config.maxLayerCombinations,
|
|
852
|
+
routedBuses: `${beamAttempt.summary.routedBusCount}/${this.preparedBuses.length}`,
|
|
853
|
+
routedConnections: `${beamAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
|
|
854
|
+
failedBuses: "none",
|
|
855
|
+
bestScore: beamAttempt.summary.score,
|
|
856
|
+
}
|
|
857
|
+
this.solved = true
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let assignment: Readonly<Record<string, string>> | undefined
|
|
863
|
+
while (
|
|
864
|
+
!assignment &&
|
|
865
|
+
this.nextAssignmentIndex < this.config.maxLayerCombinations
|
|
866
|
+
) {
|
|
867
|
+
const preferGeneratedAssignment = this.nextAssignmentIndex % 3 === 0
|
|
868
|
+
let candidate: Readonly<Record<string, string>> | undefined
|
|
869
|
+
let candidateCameFromRepairQueue = false
|
|
870
|
+
if (preferGeneratedAssignment) {
|
|
871
|
+
candidate = this.layerAssignments[this.nextGeneratedAssignmentIndex++]
|
|
872
|
+
} else {
|
|
873
|
+
candidate = this.pendingRepairAssignments.pop()
|
|
874
|
+
candidateCameFromRepairQueue = candidate !== undefined
|
|
875
|
+
}
|
|
876
|
+
if (!candidate) {
|
|
877
|
+
candidate = preferGeneratedAssignment
|
|
878
|
+
? this.pendingRepairAssignments.pop()
|
|
879
|
+
: this.layerAssignments[this.nextGeneratedAssignmentIndex++]
|
|
880
|
+
candidateCameFromRepairQueue =
|
|
881
|
+
preferGeneratedAssignment && candidate !== undefined
|
|
882
|
+
}
|
|
883
|
+
if (!candidate) break
|
|
884
|
+
const candidateKey = JSON.stringify(candidate)
|
|
885
|
+
if (candidateCameFromRepairQueue) {
|
|
886
|
+
this.queuedAssignmentKeys.delete(candidateKey)
|
|
887
|
+
}
|
|
888
|
+
if (this.evaluatedAssignmentKeys.has(candidateKey)) continue
|
|
889
|
+
assignment = candidate
|
|
890
|
+
}
|
|
495
891
|
if (!assignment) {
|
|
496
892
|
if (
|
|
497
893
|
this.bestAttempt &&
|
|
@@ -513,6 +909,18 @@ export class FanoutSolver extends BaseSolver {
|
|
|
513
909
|
assignment,
|
|
514
910
|
)
|
|
515
911
|
this.nextAssignmentIndex++
|
|
912
|
+
this.evaluatedAssignmentKeys.add(JSON.stringify(assignment))
|
|
913
|
+
if (
|
|
914
|
+
!this.bestAttempt ||
|
|
915
|
+
attempt.summary.routedConnectionCount >=
|
|
916
|
+
this.bestAttempt.summary.routedConnectionCount
|
|
917
|
+
) {
|
|
918
|
+
this.prioritizeFailedBusRepairs(
|
|
919
|
+
assignment,
|
|
920
|
+
attempt.summary.failedBusIds,
|
|
921
|
+
attempt.blockingBusIds,
|
|
922
|
+
)
|
|
923
|
+
}
|
|
516
924
|
this.attempts.push(attempt.summary)
|
|
517
925
|
if (
|
|
518
926
|
!this.bestAttempt ||
|
|
@@ -522,7 +930,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
522
930
|
}
|
|
523
931
|
this.stats = {
|
|
524
932
|
assignment: attempt.summary.assignmentIndex + 1,
|
|
525
|
-
assignmentCount: this.
|
|
933
|
+
assignmentCount: this.config.maxLayerCombinations,
|
|
526
934
|
routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
|
|
527
935
|
routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
|
|
528
936
|
failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
|
|
@@ -537,7 +945,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
537
945
|
|
|
538
946
|
computeProgress(): number {
|
|
539
947
|
if (this.solved || this.failed) return 1
|
|
540
|
-
return this.nextAssignmentIndex / this.
|
|
948
|
+
return this.nextAssignmentIndex / this.config.maxLayerCombinations
|
|
541
949
|
}
|
|
542
950
|
|
|
543
951
|
override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
|
package/lib/layer-names.ts
CHANGED
|
@@ -37,9 +37,10 @@ export function getLayerSpan(
|
|
|
37
37
|
export function generateLayerAssignments(params: {
|
|
38
38
|
busIds: string[]
|
|
39
39
|
layers: string[]
|
|
40
|
+
layersByBusId?: Readonly<Record<string, readonly string[]>>
|
|
40
41
|
maxAssignments: number
|
|
41
42
|
}): Array<Readonly<Record<string, string>>> {
|
|
42
|
-
const { busIds, layers, maxAssignments } = params
|
|
43
|
+
const { busIds, layers, layersByBusId, maxAssignments } = params
|
|
43
44
|
if (layers.length === 0) {
|
|
44
45
|
throw new Error("FanoutSolver: no escape layers are available")
|
|
45
46
|
}
|
|
@@ -49,7 +50,22 @@ export function generateLayerAssignments(params: {
|
|
|
49
50
|
)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const availableLayersByBus = busIds.map(
|
|
54
|
+
(busId) => layersByBusId?.[busId] ?? layers,
|
|
55
|
+
)
|
|
56
|
+
const busWithoutLayersIndex = availableLayersByBus.findIndex(
|
|
57
|
+
(availableLayers) => availableLayers.length === 0,
|
|
58
|
+
)
|
|
59
|
+
if (busWithoutLayersIndex >= 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`FanoutSolver: no escape layers are available for bus "${busIds[busWithoutLayersIndex]}"`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const rawCombinationCount = availableLayersByBus.reduce(
|
|
66
|
+
(count, availableLayers) => count * availableLayers.length,
|
|
67
|
+
1,
|
|
68
|
+
)
|
|
53
69
|
const combinationCount = Math.min(
|
|
54
70
|
maxAssignments,
|
|
55
71
|
Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments,
|
|
@@ -60,7 +76,8 @@ export function generateLayerAssignments(params: {
|
|
|
60
76
|
function addAssignment(layerIndexes: number[]): void {
|
|
61
77
|
const assignment: Record<string, string> = {}
|
|
62
78
|
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
63
|
-
assignment[busIds[busIndex]!] =
|
|
79
|
+
assignment[busIds[busIndex]!] =
|
|
80
|
+
availableLayersByBus[busIndex]![layerIndexes[busIndex]!]!
|
|
64
81
|
}
|
|
65
82
|
const key = JSON.stringify(assignment)
|
|
66
83
|
if (seenAssignments.has(key)) return
|
|
@@ -73,9 +90,10 @@ export function generateLayerAssignments(params: {
|
|
|
73
90
|
const layerIndexes: number[] = []
|
|
74
91
|
let remaining = ordinal
|
|
75
92
|
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
const layerCount = availableLayersByBus[busIndex]!.length
|
|
94
|
+
const digit = remaining % layerCount
|
|
95
|
+
remaining = Math.floor(remaining / layerCount)
|
|
96
|
+
layerIndexes.push((digit + busIndex) % layerCount)
|
|
79
97
|
}
|
|
80
98
|
addAssignment(layerIndexes)
|
|
81
99
|
}
|
|
@@ -90,17 +108,22 @@ export function generateLayerAssignments(params: {
|
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
const balancedLayerIndexes = busIds.map(
|
|
93
|
-
(_, busIndex) => busIndex %
|
|
111
|
+
(_, busIndex) => busIndex % availableLayersByBus[busIndex]!.length,
|
|
94
112
|
)
|
|
95
113
|
addAssignment(balancedLayerIndexes)
|
|
114
|
+
const maximumAvailableLayerCount = Math.max(
|
|
115
|
+
...availableLayersByBus.map((availableLayers) => availableLayers.length),
|
|
116
|
+
)
|
|
96
117
|
for (
|
|
97
118
|
let globalShift = 1;
|
|
98
|
-
globalShift <
|
|
119
|
+
globalShift < maximumAvailableLayerCount &&
|
|
120
|
+
assignments.length < combinationCount;
|
|
99
121
|
globalShift++
|
|
100
122
|
) {
|
|
101
123
|
addAssignment(
|
|
102
124
|
balancedLayerIndexes.map(
|
|
103
|
-
(layerIndex) =>
|
|
125
|
+
(layerIndex, busIndex) =>
|
|
126
|
+
(layerIndex + globalShift) % availableLayersByBus[busIndex]!.length,
|
|
104
127
|
),
|
|
105
128
|
)
|
|
106
129
|
}
|
|
@@ -111,12 +134,14 @@ export function generateLayerAssignments(params: {
|
|
|
111
134
|
) {
|
|
112
135
|
for (
|
|
113
136
|
let shift = 1;
|
|
114
|
-
shift <
|
|
137
|
+
shift < availableLayersByBus[busIndex]!.length &&
|
|
138
|
+
assignments.length < combinationCount;
|
|
115
139
|
shift++
|
|
116
140
|
) {
|
|
117
141
|
const layerIndexes = [...balancedLayerIndexes]
|
|
118
142
|
layerIndexes[busIndex] =
|
|
119
|
-
(balancedLayerIndexes[busIndex]! + shift) %
|
|
143
|
+
(balancedLayerIndexes[busIndex]! + shift) %
|
|
144
|
+
availableLayersByBus[busIndex]!.length
|
|
120
145
|
addAssignment(layerIndexes)
|
|
121
146
|
}
|
|
122
147
|
}
|
|
@@ -128,7 +153,8 @@ export function generateLayerAssignments(params: {
|
|
|
128
153
|
addAssignment(
|
|
129
154
|
busIds.map(
|
|
130
155
|
(_, busIndex) =>
|
|
131
|
-
mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
|
|
156
|
+
mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
|
|
157
|
+
availableLayersByBus[busIndex]!.length,
|
|
132
158
|
),
|
|
133
159
|
)
|
|
134
160
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -21,7 +21,9 @@ import type {
|
|
|
21
21
|
RoutedSegment,
|
|
22
22
|
} from "./types"
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
export type RouteBusStaticClearanceCache = Map<string, boolean>
|
|
25
|
+
|
|
26
|
+
export interface RouteBusParams {
|
|
25
27
|
srj: SimpleRouteJson
|
|
26
28
|
bus: PreparedBus
|
|
27
29
|
targetLayer: string
|
|
@@ -32,6 +34,8 @@ interface RouteBusParams {
|
|
|
32
34
|
viaHoleDiameter: number
|
|
33
35
|
clearance: number
|
|
34
36
|
compactBusTracks: boolean
|
|
37
|
+
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
38
|
+
blockingBusCounts?: Map<string, number>
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
interface TrackCandidate {
|
|
@@ -682,14 +686,13 @@ function segmentIsClearOfObstacles(params: {
|
|
|
682
686
|
return true
|
|
683
687
|
}
|
|
684
688
|
|
|
685
|
-
function
|
|
689
|
+
function planIsStaticallyClear(params: {
|
|
686
690
|
plan: FanoutRoutePlan
|
|
687
|
-
otherPlans: FanoutRoutePlan[]
|
|
688
691
|
srj: SimpleRouteJson
|
|
689
692
|
sharedBoundary: Bounds
|
|
690
693
|
clearance: number
|
|
691
694
|
}): boolean {
|
|
692
|
-
const { plan,
|
|
695
|
+
const { plan, srj, sharedBoundary, clearance } = params
|
|
693
696
|
const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
|
|
694
697
|
if (
|
|
695
698
|
!pointIsInsideBounds(plan.exitPoint, routableBounds) ||
|
|
@@ -730,10 +733,36 @@ function planIsClear(params: {
|
|
|
730
733
|
}
|
|
731
734
|
}
|
|
732
735
|
|
|
736
|
+
return true
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function planIsClearOfPlans(params: {
|
|
740
|
+
plan: FanoutRoutePlan
|
|
741
|
+
otherPlans: FanoutRoutePlan[]
|
|
742
|
+
clearance: number
|
|
743
|
+
blockingBusCounts?: Map<string, number>
|
|
744
|
+
}): boolean {
|
|
745
|
+
const { plan, otherPlans, clearance, blockingBusCounts } = params
|
|
733
746
|
for (const otherPlan of otherPlans) {
|
|
747
|
+
const plansShareSourcePort =
|
|
748
|
+
(plan.sourcePoint.pcb_port_id &&
|
|
749
|
+
plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id) ||
|
|
750
|
+
(plan.sourcePoint.pointId &&
|
|
751
|
+
plan.sourcePoint.pointId === otherPlan.sourcePoint.pointId)
|
|
752
|
+
if (plansShareSourcePort) continue
|
|
753
|
+
const recordBlocker = (): void => {
|
|
754
|
+
if (otherPlan.busId === plan.busId) return
|
|
755
|
+
blockingBusCounts?.set(
|
|
756
|
+
otherPlan.busId,
|
|
757
|
+
(blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
|
|
758
|
+
)
|
|
759
|
+
}
|
|
734
760
|
for (const segment of plan.segments) {
|
|
735
761
|
for (const otherSegment of otherPlan.segments) {
|
|
736
|
-
if (!segmentsAreClear(segment, otherSegment, clearance))
|
|
762
|
+
if (!segmentsAreClear(segment, otherSegment, clearance)) {
|
|
763
|
+
recordBlocker()
|
|
764
|
+
return false
|
|
765
|
+
}
|
|
737
766
|
}
|
|
738
767
|
if (
|
|
739
768
|
otherPlan.via?.spanLayers.includes(segment.layer) &&
|
|
@@ -744,6 +773,7 @@ function planIsClear(params: {
|
|
|
744
773
|
) <
|
|
745
774
|
otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
|
|
746
775
|
) {
|
|
776
|
+
recordBlocker()
|
|
747
777
|
return false
|
|
748
778
|
}
|
|
749
779
|
}
|
|
@@ -758,6 +788,7 @@ function planIsClear(params: {
|
|
|
758
788
|
) <
|
|
759
789
|
plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
|
|
760
790
|
) {
|
|
791
|
+
recordBlocker()
|
|
761
792
|
return false
|
|
762
793
|
}
|
|
763
794
|
}
|
|
@@ -769,6 +800,7 @@ function planIsClear(params: {
|
|
|
769
800
|
distance(plan.via.center, otherPlan.via.center) <
|
|
770
801
|
(plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
|
|
771
802
|
) {
|
|
803
|
+
recordBlocker()
|
|
772
804
|
return false
|
|
773
805
|
}
|
|
774
806
|
}
|
|
@@ -776,6 +808,47 @@ function planIsClear(params: {
|
|
|
776
808
|
return true
|
|
777
809
|
}
|
|
778
810
|
|
|
811
|
+
function planIsClear(params: {
|
|
812
|
+
plan: FanoutRoutePlan
|
|
813
|
+
otherPlans: FanoutRoutePlan[]
|
|
814
|
+
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
815
|
+
blockingBusCounts?: Map<string, number>
|
|
816
|
+
cacheKey: string
|
|
817
|
+
srj: SimpleRouteJson
|
|
818
|
+
sharedBoundary: Bounds
|
|
819
|
+
clearance: number
|
|
820
|
+
}): boolean {
|
|
821
|
+
const {
|
|
822
|
+
plan,
|
|
823
|
+
otherPlans,
|
|
824
|
+
staticClearanceCache,
|
|
825
|
+
blockingBusCounts,
|
|
826
|
+
cacheKey,
|
|
827
|
+
srj,
|
|
828
|
+
sharedBoundary,
|
|
829
|
+
clearance,
|
|
830
|
+
} = params
|
|
831
|
+
let staticallyClear = staticClearanceCache?.get(cacheKey)
|
|
832
|
+
if (staticallyClear === undefined) {
|
|
833
|
+
staticallyClear = planIsStaticallyClear({
|
|
834
|
+
plan,
|
|
835
|
+
srj,
|
|
836
|
+
sharedBoundary,
|
|
837
|
+
clearance,
|
|
838
|
+
})
|
|
839
|
+
staticClearanceCache?.set(cacheKey, staticallyClear)
|
|
840
|
+
}
|
|
841
|
+
return (
|
|
842
|
+
staticallyClear &&
|
|
843
|
+
planIsClearOfPlans({
|
|
844
|
+
plan,
|
|
845
|
+
otherPlans,
|
|
846
|
+
clearance,
|
|
847
|
+
blockingBusCounts,
|
|
848
|
+
})
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
|
|
779
852
|
function routePlaneTerminatedBus(
|
|
780
853
|
params: RouteBusParams,
|
|
781
854
|
): FanoutRoutePlan[] | null {
|
|
@@ -789,6 +862,8 @@ function routePlaneTerminatedBus(
|
|
|
789
862
|
viaDiameter,
|
|
790
863
|
viaHoleDiameter,
|
|
791
864
|
clearance,
|
|
865
|
+
staticClearanceCache,
|
|
866
|
+
blockingBusCounts,
|
|
792
867
|
} = params
|
|
793
868
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
794
869
|
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
@@ -833,6 +908,9 @@ function routePlaneTerminatedBus(
|
|
|
833
908
|
!planIsClear({
|
|
834
909
|
plan,
|
|
835
910
|
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
911
|
+
staticClearanceCache,
|
|
912
|
+
blockingBusCounts,
|
|
913
|
+
cacheKey: `plane:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}`,
|
|
836
914
|
srj,
|
|
837
915
|
sharedBoundary: bus.sharedBoundary,
|
|
838
916
|
clearance,
|
|
@@ -850,7 +928,10 @@ function routePlaneTerminatedBus(
|
|
|
850
928
|
return null
|
|
851
929
|
}
|
|
852
930
|
|
|
853
|
-
export function
|
|
931
|
+
export function routeBusAlternatives(
|
|
932
|
+
params: RouteBusParams,
|
|
933
|
+
maxAlternatives = 1,
|
|
934
|
+
): FanoutRoutePlan[][] {
|
|
854
935
|
const {
|
|
855
936
|
srj,
|
|
856
937
|
bus,
|
|
@@ -862,13 +943,21 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
|
862
943
|
viaHoleDiameter,
|
|
863
944
|
clearance,
|
|
864
945
|
compactBusTracks,
|
|
946
|
+
staticClearanceCache,
|
|
947
|
+
blockingBusCounts,
|
|
865
948
|
} = params
|
|
949
|
+
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`FanoutSolver: maxAlternatives must be a positive integer, received ${maxAlternatives}`,
|
|
952
|
+
)
|
|
953
|
+
}
|
|
866
954
|
if (bus.termination.type === "plane") {
|
|
867
|
-
|
|
955
|
+
const plan = routePlaneTerminatedBus(params)
|
|
956
|
+
return plan ? [plan] : []
|
|
868
957
|
}
|
|
869
958
|
const exitAxis = getExitAxis(bus)
|
|
870
959
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
871
|
-
if (!sourceObstacle) return []
|
|
960
|
+
if (!sourceObstacle) return [[]]
|
|
872
961
|
const directionalPadSize = isHorizontal(bus.direction)
|
|
873
962
|
? sourceObstacle.width
|
|
874
963
|
: sourceObstacle.height
|
|
@@ -886,71 +975,111 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
|
886
975
|
: [1, -1]
|
|
887
976
|
: [0]
|
|
888
977
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
978
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
979
|
+
const seenAlternativeKeys = new Set<string>()
|
|
980
|
+
|
|
981
|
+
const addAlternative = (plans: FanoutRoutePlan[]): void => {
|
|
982
|
+
const key = plans
|
|
983
|
+
.map(
|
|
984
|
+
(plan) =>
|
|
985
|
+
`${plan.connectionIndex}:${plan.targetLayer}:${plan.exitPoint.x}:${plan.exitPoint.y}:${plan.segments.map((segment) => `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`).join(";")}`,
|
|
986
|
+
)
|
|
987
|
+
.join("|")
|
|
988
|
+
if (seenAlternativeKeys.has(key)) return
|
|
989
|
+
seenAlternativeKeys.add(key)
|
|
990
|
+
alternatives.push(plans)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const searchConnectionOrder = (
|
|
994
|
+
connectionOrder: PreparedConnection[],
|
|
995
|
+
viaHandedness: ViaHandedness,
|
|
996
|
+
connectionIndex: number,
|
|
997
|
+
candidatePlans: FanoutRoutePlan[],
|
|
998
|
+
): void => {
|
|
999
|
+
if (alternatives.length >= maxAlternatives) return
|
|
1000
|
+
if (connectionIndex >= connectionOrder.length) {
|
|
1001
|
+
addAlternative(candidatePlans)
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const preparedConnection = connectionOrder[connectionIndex]!
|
|
1006
|
+
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
1007
|
+
const trackCandidates = getTrackCandidates({
|
|
1008
|
+
bus,
|
|
1009
|
+
connection: preparedConnection,
|
|
1010
|
+
preferredTrack: getPreferredTrack({
|
|
1011
|
+
bus,
|
|
1012
|
+
connection: preparedConnection,
|
|
1013
|
+
targetUsesVia,
|
|
1014
|
+
interstitialEscape,
|
|
1015
|
+
compactBusTracks,
|
|
1016
|
+
traceWidth,
|
|
1017
|
+
viaDiameter,
|
|
1018
|
+
clearance,
|
|
1019
|
+
}),
|
|
1020
|
+
traceWidth,
|
|
1021
|
+
clearance,
|
|
1022
|
+
})
|
|
1023
|
+
for (
|
|
1024
|
+
let trackIndex = 0;
|
|
1025
|
+
trackIndex < trackCandidates.length;
|
|
1026
|
+
trackIndex++
|
|
1027
|
+
) {
|
|
1028
|
+
const track = trackCandidates[trackIndex]!
|
|
1029
|
+
const plan = buildPlan({
|
|
1030
|
+
preparedConnection,
|
|
1031
|
+
bus,
|
|
1032
|
+
targetLayer,
|
|
1033
|
+
track: track.value,
|
|
1034
|
+
exitAxis,
|
|
1035
|
+
layerNames,
|
|
1036
|
+
traceWidth,
|
|
1037
|
+
viaDiameter,
|
|
1038
|
+
viaHoleDiameter,
|
|
1039
|
+
viaHandedness,
|
|
1040
|
+
interstitialEscape,
|
|
1041
|
+
spreadLaneIndex: Math.min(
|
|
1042
|
+
connectionRank,
|
|
1043
|
+
bus.connections.length - connectionRank - 1,
|
|
1044
|
+
),
|
|
1045
|
+
clearance,
|
|
1046
|
+
terminateAtVia: false,
|
|
1047
|
+
})
|
|
1048
|
+
if (
|
|
1049
|
+
!planIsClear({
|
|
1050
|
+
plan,
|
|
1051
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
1052
|
+
staticClearanceCache,
|
|
1053
|
+
blockingBusCounts,
|
|
1054
|
+
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}`,
|
|
1055
|
+
srj,
|
|
1056
|
+
sharedBoundary: bus.sharedBoundary,
|
|
909
1057
|
clearance,
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
bus,
|
|
914
|
-
targetLayer,
|
|
915
|
-
track: track.value,
|
|
916
|
-
exitAxis,
|
|
917
|
-
layerNames,
|
|
918
|
-
traceWidth,
|
|
919
|
-
viaDiameter,
|
|
920
|
-
viaHoleDiameter,
|
|
921
|
-
viaHandedness,
|
|
922
|
-
interstitialEscape,
|
|
923
|
-
spreadLaneIndex: Math.min(
|
|
924
|
-
getConnectionRank(bus, preparedConnection),
|
|
925
|
-
bus.connections.length -
|
|
926
|
-
getConnectionRank(bus, preparedConnection) -
|
|
927
|
-
1,
|
|
928
|
-
),
|
|
929
|
-
clearance,
|
|
930
|
-
terminateAtVia: false,
|
|
931
|
-
})
|
|
932
|
-
if (
|
|
933
|
-
planIsClear({
|
|
934
|
-
plan,
|
|
935
|
-
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
936
|
-
srj,
|
|
937
|
-
sharedBoundary: bus.sharedBoundary,
|
|
938
|
-
clearance,
|
|
939
|
-
})
|
|
940
|
-
) {
|
|
941
|
-
acceptedPlan = plan
|
|
942
|
-
break
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (!acceptedPlan) {
|
|
946
|
-
orderIsClear = false
|
|
947
|
-
break
|
|
948
|
-
}
|
|
949
|
-
candidatePlans.push(acceptedPlan)
|
|
1058
|
+
})
|
|
1059
|
+
) {
|
|
1060
|
+
continue
|
|
950
1061
|
}
|
|
951
|
-
|
|
1062
|
+
searchConnectionOrder(
|
|
1063
|
+
connectionOrder,
|
|
1064
|
+
viaHandedness,
|
|
1065
|
+
connectionIndex + 1,
|
|
1066
|
+
[...candidatePlans, plan],
|
|
1067
|
+
)
|
|
1068
|
+
if (alternatives.length >= maxAlternatives) return
|
|
1069
|
+
if (maxAlternatives === 1) return
|
|
952
1070
|
}
|
|
953
1071
|
}
|
|
954
1072
|
|
|
955
|
-
|
|
1073
|
+
for (const viaHandedness of viaHandednesses) {
|
|
1074
|
+
for (const connectionOrder of getConnectionOrders(bus)) {
|
|
1075
|
+
searchConnectionOrder(connectionOrder, viaHandedness, 0, [])
|
|
1076
|
+
if (alternatives.length >= maxAlternatives) return alternatives
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return alternatives
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
1084
|
+
return routeBusAlternatives(params, 1)[0] ?? null
|
|
956
1085
|
}
|