@tscircuit/fanout-solver 0.0.10
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/LICENSE +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import {
|
|
2
|
+
convertSrjToGraphicsObject,
|
|
3
|
+
type SimpleRouteJson,
|
|
4
|
+
} from "@tscircuit/capacity-autorouter"
|
|
5
|
+
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
6
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
7
|
+
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
8
|
+
import { distanceSegmentToObstacle } from "./geometry"
|
|
9
|
+
import { getCopperLayerColor } from "./layer-colors"
|
|
10
|
+
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
11
|
+
import { prepareFanoutBuses } from "./prepare-buses"
|
|
12
|
+
import { routeBus } from "./route-bus"
|
|
13
|
+
import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
|
|
14
|
+
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
15
|
+
import type {
|
|
16
|
+
AssignmentAttempt,
|
|
17
|
+
FanoutAttemptSummary,
|
|
18
|
+
FanoutBorderDistribution,
|
|
19
|
+
FanoutSolverOptions,
|
|
20
|
+
FanoutSolverOutput,
|
|
21
|
+
PreparedBus,
|
|
22
|
+
} from "./types"
|
|
23
|
+
|
|
24
|
+
interface ResolvedFanoutConfig {
|
|
25
|
+
traceWidth: number
|
|
26
|
+
viaDiameter: number
|
|
27
|
+
viaHoleDiameter: number
|
|
28
|
+
clearance: number
|
|
29
|
+
compactBusTracks: boolean
|
|
30
|
+
singleLayerPushAndShove: boolean
|
|
31
|
+
singleLayerAdaptiveExits: boolean
|
|
32
|
+
borderDistribution: FanoutBorderDistribution
|
|
33
|
+
layerNames: string[]
|
|
34
|
+
escapeLayers: string[]
|
|
35
|
+
maxLayerCombinations: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolvePositiveNumber(label: string, value: number): number {
|
|
39
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`FanoutSolver: ${label} must be a positive number, received ${value}`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveConfig(
|
|
48
|
+
srj: SimpleRouteJson,
|
|
49
|
+
options: FanoutSolverOptions,
|
|
50
|
+
): ResolvedFanoutConfig {
|
|
51
|
+
const traceWidth = resolvePositiveNumber(
|
|
52
|
+
"traceWidth",
|
|
53
|
+
options.traceWidth ?? srj.nominalTraceWidth ?? srj.minTraceWidth,
|
|
54
|
+
)
|
|
55
|
+
const viaDiameter = resolvePositiveNumber(
|
|
56
|
+
"viaDiameter",
|
|
57
|
+
options.viaDiameter ??
|
|
58
|
+
srj.minViaPadDiameter ??
|
|
59
|
+
srj.min_via_pad_diameter ??
|
|
60
|
+
srj.minViaDiameter ??
|
|
61
|
+
Math.max(traceWidth * 2, 0.3),
|
|
62
|
+
)
|
|
63
|
+
const viaHoleDiameter = resolvePositiveNumber(
|
|
64
|
+
"viaHoleDiameter",
|
|
65
|
+
options.viaHoleDiameter ??
|
|
66
|
+
srj.minViaHoleDiameter ??
|
|
67
|
+
srj.min_via_hole_diameter ??
|
|
68
|
+
viaDiameter * 0.5,
|
|
69
|
+
)
|
|
70
|
+
if (viaHoleDiameter >= viaDiameter) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`FanoutSolver: viaHoleDiameter ${viaHoleDiameter} must be smaller than viaDiameter ${viaDiameter}`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
const clearance = resolvePositiveNumber(
|
|
76
|
+
"clearance",
|
|
77
|
+
options.clearance ??
|
|
78
|
+
srj.minViaEdgeToPadEdgeClearance ??
|
|
79
|
+
srj.minTraceToPadEdgeClearance ??
|
|
80
|
+
srj.defaultObstacleMargin ??
|
|
81
|
+
srj.minTraceWidth,
|
|
82
|
+
)
|
|
83
|
+
const layerNames = getCopperLayerNames(srj.layerCount)
|
|
84
|
+
const escapeLayers = options.escapeLayers ?? layerNames
|
|
85
|
+
for (const layer of escapeLayers) {
|
|
86
|
+
if (!layerNames.includes(layer)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`FanoutSolver: escape layer "${layer}" is not available in a ${srj.layerCount}-layer SimpleRouteJson`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (new Set(escapeLayers).size !== escapeLayers.length) {
|
|
93
|
+
throw new Error("FanoutSolver: escapeLayers contains duplicates")
|
|
94
|
+
}
|
|
95
|
+
const borderDistribution = options.borderDistribution ?? "preserve"
|
|
96
|
+
if (borderDistribution !== "preserve" && borderDistribution !== "even") {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`FanoutSolver: borderDistribution must be "preserve" or "even", received "${borderDistribution}"`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
traceWidth,
|
|
104
|
+
viaDiameter,
|
|
105
|
+
viaHoleDiameter,
|
|
106
|
+
clearance,
|
|
107
|
+
compactBusTracks: options.compactBusTracks ?? false,
|
|
108
|
+
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
|
|
109
|
+
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
|
|
110
|
+
borderDistribution,
|
|
111
|
+
layerNames,
|
|
112
|
+
escapeLayers,
|
|
113
|
+
maxLayerCombinations:
|
|
114
|
+
options.maxLayerCombinations === undefined
|
|
115
|
+
? 256
|
|
116
|
+
: resolvePositiveNumber(
|
|
117
|
+
"maxLayerCombinations",
|
|
118
|
+
options.maxLayerCombinations,
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assignmentLoadPenalty(
|
|
124
|
+
assignment: Readonly<Record<string, string>>,
|
|
125
|
+
): number {
|
|
126
|
+
const loadByLayer = new Map<string, number>()
|
|
127
|
+
for (const layer of Object.values(assignment)) {
|
|
128
|
+
loadByLayer.set(layer, (loadByLayer.get(layer) ?? 0) + 1)
|
|
129
|
+
}
|
|
130
|
+
return [...loadByLayer.values()].reduce(
|
|
131
|
+
(penalty, load) => penalty + load * load,
|
|
132
|
+
0,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getBusDistanceToBoundary(bus: PreparedBus): number {
|
|
137
|
+
const averageSource =
|
|
138
|
+
bus.connections.reduce((sum, connection) => {
|
|
139
|
+
const sourceAxis =
|
|
140
|
+
bus.direction === "left" || bus.direction === "right"
|
|
141
|
+
? connection.sourcePoint.x
|
|
142
|
+
: connection.sourcePoint.y
|
|
143
|
+
return sum + sourceAxis
|
|
144
|
+
}, 0) / bus.connections.length
|
|
145
|
+
switch (bus.direction) {
|
|
146
|
+
case "right":
|
|
147
|
+
return bus.sharedBoundary.maxX - averageSource
|
|
148
|
+
case "left":
|
|
149
|
+
return averageSource - bus.sharedBoundary.minX
|
|
150
|
+
case "up":
|
|
151
|
+
return bus.sharedBoundary.maxY - averageSource
|
|
152
|
+
case "down":
|
|
153
|
+
return averageSource - bus.sharedBoundary.minY
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
|
|
158
|
+
const isHorizontal = bus.direction === "left" || bus.direction === "right"
|
|
159
|
+
const directionalCoordinates = isHorizontal
|
|
160
|
+
? bus.xCoordinates
|
|
161
|
+
: bus.yCoordinates
|
|
162
|
+
const averageSource =
|
|
163
|
+
bus.connections.reduce(
|
|
164
|
+
(sum, connection) =>
|
|
165
|
+
sum +
|
|
166
|
+
(isHorizontal ? connection.sourcePoint.x : connection.sourcePoint.y),
|
|
167
|
+
0,
|
|
168
|
+
) / bus.connections.length
|
|
169
|
+
const outwardCoordinate =
|
|
170
|
+
bus.direction === "right" || bus.direction === "up"
|
|
171
|
+
? Math.max(...directionalCoordinates)
|
|
172
|
+
: Math.min(...directionalCoordinates)
|
|
173
|
+
return Math.abs(averageSource - outwardCoordinate) < 1e-6
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getBusDepthInRows(bus: PreparedBus): number {
|
|
177
|
+
const isHorizontal = bus.direction === "left" || bus.direction === "right"
|
|
178
|
+
const directionalCoordinates = isHorizontal
|
|
179
|
+
? bus.xCoordinates
|
|
180
|
+
: bus.yCoordinates
|
|
181
|
+
const averageSource =
|
|
182
|
+
bus.connections.reduce(
|
|
183
|
+
(sum, connection) =>
|
|
184
|
+
sum +
|
|
185
|
+
(isHorizontal ? connection.sourcePoint.x : connection.sourcePoint.y),
|
|
186
|
+
0,
|
|
187
|
+
) / bus.connections.length
|
|
188
|
+
const outwardCoordinate =
|
|
189
|
+
bus.direction === "right" || bus.direction === "up"
|
|
190
|
+
? Math.max(...directionalCoordinates)
|
|
191
|
+
: Math.min(...directionalCoordinates)
|
|
192
|
+
const directionalPitch = isHorizontal ? bus.pitchX : bus.pitchY
|
|
193
|
+
|
|
194
|
+
return Math.round(
|
|
195
|
+
Math.abs(averageSource - outwardCoordinate) / directionalPitch,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sourceLayerEscapeIsBlocked(params: {
|
|
200
|
+
bus: PreparedBus
|
|
201
|
+
srj: SimpleRouteJson
|
|
202
|
+
traceWidth: number
|
|
203
|
+
clearance: number
|
|
204
|
+
}): boolean {
|
|
205
|
+
const { bus, srj, traceWidth, clearance } = params
|
|
206
|
+
for (const connection of bus.connections) {
|
|
207
|
+
const source = {
|
|
208
|
+
x: connection.sourcePoint.x,
|
|
209
|
+
y: connection.sourcePoint.y,
|
|
210
|
+
}
|
|
211
|
+
const boundaryPoint = (() => {
|
|
212
|
+
switch (bus.direction) {
|
|
213
|
+
case "left":
|
|
214
|
+
return { x: bus.sharedBoundary.minX, y: source.y }
|
|
215
|
+
case "right":
|
|
216
|
+
return { x: bus.sharedBoundary.maxX, y: source.y }
|
|
217
|
+
case "up":
|
|
218
|
+
return { x: source.x, y: bus.sharedBoundary.maxY }
|
|
219
|
+
case "down":
|
|
220
|
+
return { x: source.x, y: bus.sharedBoundary.minY }
|
|
221
|
+
}
|
|
222
|
+
})()
|
|
223
|
+
const directEscapeSegment = {
|
|
224
|
+
start: source,
|
|
225
|
+
end: boundaryPoint,
|
|
226
|
+
width: traceWidth,
|
|
227
|
+
layer: connection.sourceLayer,
|
|
228
|
+
}
|
|
229
|
+
for (const obstacle of srj.obstacles) {
|
|
230
|
+
if (
|
|
231
|
+
obstacle === connection.sourceObstacle ||
|
|
232
|
+
!obstacle.layers.includes(connection.sourceLayer)
|
|
233
|
+
) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
distanceSegmentToObstacle(directEscapeSegment, obstacle) <
|
|
238
|
+
traceWidth / 2 + clearance - 1e-9
|
|
239
|
+
) {
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createPreferredLayerAssignment(params: {
|
|
248
|
+
buses: PreparedBus[]
|
|
249
|
+
escapeLayers: string[]
|
|
250
|
+
srj: SimpleRouteJson
|
|
251
|
+
traceWidth: number
|
|
252
|
+
clearance: number
|
|
253
|
+
}): Readonly<Record<string, string>> {
|
|
254
|
+
const { buses, escapeLayers, srj, traceWidth, clearance } = params
|
|
255
|
+
const assignment: Record<string, string> = {}
|
|
256
|
+
const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
|
|
257
|
+
let nextViaLayerIndex = 0
|
|
258
|
+
for (const bus of buses) {
|
|
259
|
+
const directions = directionsByComponent.get(bus.componentId) ?? new Set()
|
|
260
|
+
directions.add(bus.direction)
|
|
261
|
+
directionsByComponent.set(bus.componentId, directions)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const bus of buses) {
|
|
265
|
+
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
266
|
+
if (!sourceLayer) {
|
|
267
|
+
throw new Error(`FanoutSolver: bus "${bus.busId}" has no connections`)
|
|
268
|
+
}
|
|
269
|
+
if (bus.termination.type === "plane") {
|
|
270
|
+
assignment[bus.busId] = bus.termination.layer
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
const viaLayers = escapeLayers.filter((layer) => layer !== sourceLayer)
|
|
274
|
+
if (
|
|
275
|
+
escapeLayers.includes(sourceLayer) &&
|
|
276
|
+
busIsOnOutwardComponentEdge(bus) &&
|
|
277
|
+
!sourceLayerEscapeIsBlocked({
|
|
278
|
+
bus,
|
|
279
|
+
srj,
|
|
280
|
+
traceWidth,
|
|
281
|
+
clearance,
|
|
282
|
+
})
|
|
283
|
+
) {
|
|
284
|
+
assignment[bus.busId] = sourceLayer
|
|
285
|
+
} else if (viaLayers.length > 0) {
|
|
286
|
+
const componentDirections = directionsByComponent.get(bus.componentId)!
|
|
287
|
+
const hasOpposingDirection =
|
|
288
|
+
(componentDirections.has("left") && componentDirections.has("right")) ||
|
|
289
|
+
(componentDirections.has("up") && componentDirections.has("down"))
|
|
290
|
+
if (hasOpposingDirection) {
|
|
291
|
+
const depthInRows = getBusDepthInRows(bus)
|
|
292
|
+
assignment[bus.busId] =
|
|
293
|
+
viaLayers[Math.max(depthInRows - 1, 0) % viaLayers.length]!
|
|
294
|
+
} else {
|
|
295
|
+
assignment[bus.busId] = viaLayers[nextViaLayerIndex % viaLayers.length]!
|
|
296
|
+
nextViaLayerIndex++
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
assignment[bus.busId] = sourceLayer
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return assignment
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function prioritizeLayerAssignment(params: {
|
|
307
|
+
preferredAssignment: Readonly<Record<string, string>>
|
|
308
|
+
generatedAssignments: Array<Readonly<Record<string, string>>>
|
|
309
|
+
maxAssignments: number
|
|
310
|
+
}): Array<Readonly<Record<string, string>>> {
|
|
311
|
+
const { preferredAssignment, generatedAssignments, maxAssignments } = params
|
|
312
|
+
const preferredKey = JSON.stringify(preferredAssignment)
|
|
313
|
+
return [
|
|
314
|
+
preferredAssignment,
|
|
315
|
+
...generatedAssignments.filter(
|
|
316
|
+
(assignment) => JSON.stringify(assignment) !== preferredKey,
|
|
317
|
+
),
|
|
318
|
+
].slice(0, maxAssignments)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export class FanoutSolver extends BaseSolver {
|
|
322
|
+
readonly preparedBuses: PreparedBus[]
|
|
323
|
+
readonly attempts: FanoutAttemptSummary[] = []
|
|
324
|
+
readonly layerAssignments: Array<Readonly<Record<string, string>>>
|
|
325
|
+
readonly config: ResolvedFanoutConfig
|
|
326
|
+
private nextAssignmentIndex = 0
|
|
327
|
+
private bestAttempt: AssignmentAttempt | null = null
|
|
328
|
+
|
|
329
|
+
constructor(
|
|
330
|
+
public readonly inputSrj: SimpleRouteJson,
|
|
331
|
+
public readonly options: FanoutSolverOptions = {},
|
|
332
|
+
) {
|
|
333
|
+
super()
|
|
334
|
+
this.config = resolveConfig(inputSrj, options)
|
|
335
|
+
this.preparedBuses = prepareFanoutBuses(inputSrj, options)
|
|
336
|
+
for (const bus of this.preparedBuses) {
|
|
337
|
+
if (bus.termination.type !== "plane") continue
|
|
338
|
+
const planeLayer = bus.termination.layer
|
|
339
|
+
if (!this.config.layerNames.includes(planeLayer)) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`FanoutSolver: plane-terminated bus "${bus.busId}" targets unavailable layer "${planeLayer}"`,
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
if (
|
|
345
|
+
bus.connections.some(
|
|
346
|
+
(connection) => connection.sourceLayer === planeLayer,
|
|
347
|
+
)
|
|
348
|
+
) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`FanoutSolver: plane-terminated bus "${bus.busId}" must target a layer below its source pad`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const boundaryBusIds = this.preparedBuses
|
|
355
|
+
.filter((bus) => bus.termination.type === "boundary")
|
|
356
|
+
.map((bus) => bus.busId)
|
|
357
|
+
const fixedPlaneAssignments = Object.fromEntries(
|
|
358
|
+
this.preparedBuses.flatMap((bus) =>
|
|
359
|
+
bus.termination.type === "plane"
|
|
360
|
+
? [[bus.busId, bus.termination.layer] as const]
|
|
361
|
+
: [],
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
const generatedAssignments = generateLayerAssignments({
|
|
365
|
+
busIds: boundaryBusIds,
|
|
366
|
+
layers: this.config.escapeLayers,
|
|
367
|
+
maxAssignments: this.config.maxLayerCombinations,
|
|
368
|
+
}).map((assignment) => ({
|
|
369
|
+
...assignment,
|
|
370
|
+
...fixedPlaneAssignments,
|
|
371
|
+
}))
|
|
372
|
+
this.layerAssignments = prioritizeLayerAssignment({
|
|
373
|
+
preferredAssignment: createPreferredLayerAssignment({
|
|
374
|
+
buses: this.preparedBuses,
|
|
375
|
+
escapeLayers: this.config.escapeLayers,
|
|
376
|
+
srj: inputSrj,
|
|
377
|
+
traceWidth: this.config.traceWidth,
|
|
378
|
+
clearance: this.config.clearance,
|
|
379
|
+
}),
|
|
380
|
+
generatedAssignments,
|
|
381
|
+
maxAssignments: this.config.maxLayerCombinations,
|
|
382
|
+
})
|
|
383
|
+
this.MAX_ITERATIONS = this.layerAssignments.length + 2
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
override getSolverName(): string {
|
|
387
|
+
return "FanoutSolver"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private evaluateAssignment(
|
|
391
|
+
assignmentIndex: number,
|
|
392
|
+
busLayerAssignments: Readonly<Record<string, string>>,
|
|
393
|
+
): AssignmentAttempt {
|
|
394
|
+
const plans: AssignmentAttempt["plans"] = []
|
|
395
|
+
const failedBusIds: string[] = []
|
|
396
|
+
const isSingleLayerFanout = this.config.escapeLayers.length === 1
|
|
397
|
+
if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
|
|
398
|
+
const singleLayerParams = {
|
|
399
|
+
srj: this.inputSrj,
|
|
400
|
+
buses: this.preparedBuses,
|
|
401
|
+
traceWidth: this.config.traceWidth,
|
|
402
|
+
clearance: this.config.clearance,
|
|
403
|
+
borderDistribution: this.config.borderDistribution,
|
|
404
|
+
}
|
|
405
|
+
const singleLayerPlans =
|
|
406
|
+
routeSingleLayerWithPushAndShove(singleLayerParams) ??
|
|
407
|
+
(this.config.singleLayerAdaptiveExits &&
|
|
408
|
+
this.options.availableCornersAndSides === undefined
|
|
409
|
+
? routeSingleLayerWithAdaptiveExits(singleLayerParams)
|
|
410
|
+
: null)
|
|
411
|
+
if (singleLayerPlans) {
|
|
412
|
+
plans.push(...singleLayerPlans)
|
|
413
|
+
} else {
|
|
414
|
+
failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const busesInRoutingOrder = [...this.preparedBuses].sort(
|
|
418
|
+
(a, b) =>
|
|
419
|
+
Number(a.termination.type === "plane") -
|
|
420
|
+
Number(b.termination.type === "plane") ||
|
|
421
|
+
b.componentObstacles.length - a.componentObstacles.length ||
|
|
422
|
+
(isSingleLayerFanout
|
|
423
|
+
? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
|
|
424
|
+
: b.connections.length - a.connections.length ||
|
|
425
|
+
getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
|
|
429
|
+
? []
|
|
430
|
+
: busesInRoutingOrder) {
|
|
431
|
+
const targetLayer = busLayerAssignments[bus.busId]
|
|
432
|
+
if (!targetLayer) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
const busPlans = routeBus({
|
|
438
|
+
srj: this.inputSrj,
|
|
439
|
+
bus,
|
|
440
|
+
targetLayer,
|
|
441
|
+
acceptedPlans: plans,
|
|
442
|
+
layerNames: this.config.layerNames,
|
|
443
|
+
traceWidth: this.config.traceWidth,
|
|
444
|
+
viaDiameter: this.config.viaDiameter,
|
|
445
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
446
|
+
clearance: this.config.clearance,
|
|
447
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
448
|
+
})
|
|
449
|
+
if (!busPlans) {
|
|
450
|
+
failedBusIds.push(bus.busId)
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
plans.push(...busPlans)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const routedBusCount = this.preparedBuses.length - failedBusIds.length
|
|
457
|
+
const routeLength = plans.reduce((total, plan) => total + plan.length, 0)
|
|
458
|
+
const unroutedConnectionCount =
|
|
459
|
+
this.inputSrj.connections.length - plans.length
|
|
460
|
+
const score =
|
|
461
|
+
unroutedConnectionCount * 1_000_000 +
|
|
462
|
+
failedBusIds.length * 100_000 +
|
|
463
|
+
routeLength +
|
|
464
|
+
plans.filter((plan) => plan.via).length * 0.1 +
|
|
465
|
+
assignmentLoadPenalty(busLayerAssignments) * 0.01
|
|
466
|
+
const summary: FanoutAttemptSummary = {
|
|
467
|
+
assignmentIndex,
|
|
468
|
+
busLayerAssignments,
|
|
469
|
+
routedBusCount,
|
|
470
|
+
routedConnectionCount: plans.length,
|
|
471
|
+
failedBusIds,
|
|
472
|
+
score,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
summary,
|
|
477
|
+
plans,
|
|
478
|
+
outputSrj: buildOutputSimpleRouteJson({
|
|
479
|
+
inputSrj: this.inputSrj,
|
|
480
|
+
plans,
|
|
481
|
+
layerNames: this.config.layerNames,
|
|
482
|
+
}),
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
override _step(): void {
|
|
487
|
+
const assignment = this.layerAssignments[this.nextAssignmentIndex]
|
|
488
|
+
if (!assignment) {
|
|
489
|
+
if (
|
|
490
|
+
this.bestAttempt &&
|
|
491
|
+
this.bestAttempt.summary.routedConnectionCount ===
|
|
492
|
+
this.inputSrj.connections.length
|
|
493
|
+
) {
|
|
494
|
+
this.solved = true
|
|
495
|
+
} else {
|
|
496
|
+
this.failed = true
|
|
497
|
+
this.error = this.bestAttempt
|
|
498
|
+
? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections`
|
|
499
|
+
: "FanoutSolver: no layer assignment could be evaluated"
|
|
500
|
+
}
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const attempt = this.evaluateAssignment(
|
|
505
|
+
this.nextAssignmentIndex,
|
|
506
|
+
assignment,
|
|
507
|
+
)
|
|
508
|
+
this.nextAssignmentIndex++
|
|
509
|
+
this.attempts.push(attempt.summary)
|
|
510
|
+
if (
|
|
511
|
+
!this.bestAttempt ||
|
|
512
|
+
attempt.summary.score < this.bestAttempt.summary.score
|
|
513
|
+
) {
|
|
514
|
+
this.bestAttempt = attempt
|
|
515
|
+
}
|
|
516
|
+
this.stats = {
|
|
517
|
+
assignment: attempt.summary.assignmentIndex + 1,
|
|
518
|
+
assignmentCount: this.layerAssignments.length,
|
|
519
|
+
routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
|
|
520
|
+
routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
|
|
521
|
+
failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
|
|
522
|
+
bestScore: this.bestAttempt.summary.score,
|
|
523
|
+
}
|
|
524
|
+
if (
|
|
525
|
+
attempt.summary.routedConnectionCount === this.inputSrj.connections.length
|
|
526
|
+
) {
|
|
527
|
+
this.solved = true
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
computeProgress(): number {
|
|
532
|
+
if (this.solved || this.failed) return 1
|
|
533
|
+
return this.nextAssignmentIndex / this.layerAssignments.length
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
override getConstructorParams(): [SimpleRouteJson, FanoutSolverOptions] {
|
|
537
|
+
return [this.inputSrj, this.options]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
override getOutput(): FanoutSolverOutput {
|
|
541
|
+
if (!this.solved || !this.bestAttempt) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"FanoutSolver: getOutput() called before a complete fanout was solved",
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
simpleRouteJson: this.bestAttempt.outputSrj,
|
|
548
|
+
fanoutTraces: this.bestAttempt.plans.map((plan) => plan.trace),
|
|
549
|
+
planeTerminations: this.bestAttempt.plans.flatMap((plan) =>
|
|
550
|
+
plan.termination.type === "plane" && plan.via
|
|
551
|
+
? [
|
|
552
|
+
{
|
|
553
|
+
busId: plan.busId,
|
|
554
|
+
connectionName: plan.connectionName,
|
|
555
|
+
layer: plan.termination.layer,
|
|
556
|
+
via: plan.via,
|
|
557
|
+
},
|
|
558
|
+
]
|
|
559
|
+
: [],
|
|
560
|
+
),
|
|
561
|
+
busLayerAssignments: this.bestAttempt.summary.busLayerAssignments,
|
|
562
|
+
busDirections: Object.fromEntries(
|
|
563
|
+
this.preparedBuses.map((bus) => [bus.busId, bus.direction]),
|
|
564
|
+
),
|
|
565
|
+
attempts: [...this.attempts],
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getOutputSimpleRouteJson(): SimpleRouteJson {
|
|
570
|
+
return this.getOutput().simpleRouteJson
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
override visualize(): GraphicsObject {
|
|
574
|
+
const visualizedSrj = this.bestAttempt?.outputSrj ?? this.inputSrj
|
|
575
|
+
const graphics = convertSrjToGraphicsObject(visualizedSrj)
|
|
576
|
+
const circularPadKeys = new Set(
|
|
577
|
+
visualizedSrj.obstacles
|
|
578
|
+
.filter(
|
|
579
|
+
(obstacle) =>
|
|
580
|
+
(obstacle as typeof obstacle & { shape?: string }).shape ===
|
|
581
|
+
"circle",
|
|
582
|
+
)
|
|
583
|
+
.map(
|
|
584
|
+
(obstacle) =>
|
|
585
|
+
`${obstacle.center.x}:${obstacle.center.y}:${obstacle.width}:${obstacle.height}`,
|
|
586
|
+
),
|
|
587
|
+
)
|
|
588
|
+
const circularPadGraphics: NonNullable<GraphicsObject["circles"]> = []
|
|
589
|
+
const rects = graphics.rects?.filter((rect) => {
|
|
590
|
+
const key = `${rect.center.x}:${rect.center.y}:${rect.width}:${rect.height}`
|
|
591
|
+
if (!circularPadKeys.has(key)) return true
|
|
592
|
+
circularPadGraphics.push({
|
|
593
|
+
center: rect.center,
|
|
594
|
+
radius: Math.min(rect.width, rect.height) / 2,
|
|
595
|
+
fill: rect.fill,
|
|
596
|
+
stroke: rect.stroke,
|
|
597
|
+
layer: rect.layer,
|
|
598
|
+
label: rect.label,
|
|
599
|
+
})
|
|
600
|
+
return false
|
|
601
|
+
})
|
|
602
|
+
return {
|
|
603
|
+
...graphics,
|
|
604
|
+
rects,
|
|
605
|
+
circles: [...(graphics.circles ?? []), ...circularPadGraphics],
|
|
606
|
+
lines: graphics.lines?.map((line) => {
|
|
607
|
+
const layerMatch = /^z(\d+)$/.exec(line.layer ?? "")
|
|
608
|
+
if (!layerMatch) return line
|
|
609
|
+
const { strokeDash: _strokeDash, ...solidLine } = line
|
|
610
|
+
return {
|
|
611
|
+
...solidLine,
|
|
612
|
+
strokeColor: getCopperLayerColor(Number(layerMatch[1])),
|
|
613
|
+
}
|
|
614
|
+
}),
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|