@tscircuit/fanout-solver 0.0.63 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fanout-solver.ts +269 -18
- package/lib/get-free-boundary-tracks.ts +102 -0
- package/lib/match-angularly-ordered-local-vias.ts +239 -0
- package/lib/reflect-fanout-x.ts +67 -0
- package/lib/repair-peripheral-bus-lengths.ts +167 -0
- package/lib/route-adaptive-left-crossbar-bus.ts +425 -0
- package/lib/route-bottom-crossbar-bus.ts +559 -0
- package/lib/route-bus.ts +44 -2
- package/lib/route-left-crossbar-bus.ts +399 -0
- package/lib/route-opposite-bottom-crossbar-bus.ts +70 -0
- package/lib/route-peripheral-source-escapes.ts +488 -0
- package/lib/route-reserved-narrow-buses.ts +430 -0
- package/lib/route-reserved-source-buses.ts +243 -0
- package/lib/route-shallow-split-perimeter-bus.ts +268 -0
- package/lib/route-split-perimeter-bus.ts +397 -0
- package/lib/route-split-perimeter-source-escapes.ts +579 -0
- package/lib/route-staged-perimeter-bus.ts +257 -0
- package/lib/route-via-minimal-winding.ts +78 -30
- package/package.json +1 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
2
|
+
import { getFreeBoundaryTracks } from "./get-free-boundary-tracks"
|
|
3
|
+
import { routeViaMinimalWindingAlternativesSteps } from "./route-via-minimal-winding"
|
|
4
|
+
import {
|
|
5
|
+
getCornerTargetTrack,
|
|
6
|
+
routeBusAlternativesSteps,
|
|
7
|
+
type RouteBusAlternativesProgress,
|
|
8
|
+
type RouteBusParams,
|
|
9
|
+
} from "./route-bus"
|
|
10
|
+
import type { FanoutBorderTarget, FanoutRoutePlan, PreparedBus } from "./types"
|
|
11
|
+
|
|
12
|
+
interface Params extends Omit<RouteBusParams, "bus"> {
|
|
13
|
+
buses: readonly PreparedBus[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Route fixed-via small buses together so an early lane cannot trap a later bus. */
|
|
17
|
+
export function* routeReservedNarrowBusesSteps(
|
|
18
|
+
params: Params,
|
|
19
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
|
|
20
|
+
if (
|
|
21
|
+
params.buses.some(
|
|
22
|
+
(bus) =>
|
|
23
|
+
bus.termination.type !== "boundary" || bus.connections.length > 2,
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
return null
|
|
27
|
+
let attempts = 0
|
|
28
|
+
const maximumAttempts = 192
|
|
29
|
+
const pitch = params.traceWidth + params.clearance
|
|
30
|
+
function* route(
|
|
31
|
+
bus: PreparedBus,
|
|
32
|
+
accepted: FanoutRoutePlan[],
|
|
33
|
+
preferredExit = bus.preferredExit,
|
|
34
|
+
offset = 0,
|
|
35
|
+
) {
|
|
36
|
+
if (attempts++ >= maximumAttempts) return []
|
|
37
|
+
return yield* routeBusAlternativesSteps(
|
|
38
|
+
{
|
|
39
|
+
...params,
|
|
40
|
+
bus: { ...bus, preferredExit },
|
|
41
|
+
acceptedPlans: accepted,
|
|
42
|
+
reservedVias: params.reservedVias?.filter(
|
|
43
|
+
(reserved) =>
|
|
44
|
+
!bus.connections.some(
|
|
45
|
+
(connection) =>
|
|
46
|
+
connection.connection.name === reserved.connectionName,
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
viaMinimalOnly: true,
|
|
50
|
+
adaptiveWindingRouteOrder: true,
|
|
51
|
+
alignWindingGridToPads: true,
|
|
52
|
+
windingGridStep: pitch / 2,
|
|
53
|
+
fixedViaFallbackRouteOrderAttempts: 32,
|
|
54
|
+
cornerBandTargetTrackOffset: offset,
|
|
55
|
+
},
|
|
56
|
+
1,
|
|
57
|
+
false,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
const restore = (plans: FanoutRoutePlan[]) =>
|
|
61
|
+
plans.map((plan) => {
|
|
62
|
+
const bus = params.buses.find((bus) => bus.busId === plan.busId)
|
|
63
|
+
return bus
|
|
64
|
+
? {
|
|
65
|
+
...plan,
|
|
66
|
+
cornerBandSide: getCornerBandSide(bus.exitEdge, bus.preferredExit),
|
|
67
|
+
}
|
|
68
|
+
: plan
|
|
69
|
+
})
|
|
70
|
+
// Retain successful ordinary routing before searching alternate bands/orders.
|
|
71
|
+
let ordinary = [...params.acceptedPlans]
|
|
72
|
+
for (const bus of params.buses) {
|
|
73
|
+
const plans = yield* route(bus, ordinary)
|
|
74
|
+
if (!plans.length) {
|
|
75
|
+
ordinary = []
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
ordinary.push(...plans[0]!)
|
|
79
|
+
}
|
|
80
|
+
if (ordinary.length)
|
|
81
|
+
return restore(ordinary.slice(params.acceptedPlans.length))
|
|
82
|
+
|
|
83
|
+
const sourceMean = (bus: PreparedBus, axis: "x" | "y") =>
|
|
84
|
+
bus.connections.reduce(
|
|
85
|
+
(sum, c) =>
|
|
86
|
+
sum +
|
|
87
|
+
(params.fixedViaPointsByConnectionIndex?.get(c.connectionIndex) ??
|
|
88
|
+
c.sourcePoint)[axis],
|
|
89
|
+
0,
|
|
90
|
+
) / bus.connections.length
|
|
91
|
+
// Commit the most constrained corner exits first, then sweep across the
|
|
92
|
+
// source field. Two complete greedy orders cheaply resolve cases where the
|
|
93
|
+
// recursive search otherwise spends its budget revisiting center lanes.
|
|
94
|
+
if (params.buses.length > 1) {
|
|
95
|
+
for (const sign of [1, -1]) {
|
|
96
|
+
const sweep = params.buses.toSorted((a, b) => {
|
|
97
|
+
const aCorner =
|
|
98
|
+
getCornerBandSide(a.exitEdge, a.preferredExit) !== undefined
|
|
99
|
+
const bCorner =
|
|
100
|
+
getCornerBandSide(b.exitEdge, b.preferredExit) !== undefined
|
|
101
|
+
if (aCorner !== bCorner) return Number(bCorner) - Number(aCorner)
|
|
102
|
+
const axis = a.exitEdge === "left" || a.exitEdge === "right" ? "y" : "x"
|
|
103
|
+
return sign * (sourceMean(a, axis) - sourceMean(b, axis))
|
|
104
|
+
})
|
|
105
|
+
let accepted = [...params.acceptedPlans]
|
|
106
|
+
for (const bus of sweep) {
|
|
107
|
+
const alternatives = yield* route(bus, accepted)
|
|
108
|
+
if (!alternatives.length) {
|
|
109
|
+
accepted = []
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
accepted.push(...alternatives[0]!)
|
|
113
|
+
}
|
|
114
|
+
if (accepted.length)
|
|
115
|
+
return restore(accepted.slice(params.acceptedPlans.length))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const ordered = params.buses.toSorted((a, b) => {
|
|
119
|
+
const aSide = getCornerBandSide(a.exitEdge, a.preferredExit)
|
|
120
|
+
const bSide = getCornerBandSide(b.exitEdge, b.preferredExit)
|
|
121
|
+
const aLower = aSide === "minimum",
|
|
122
|
+
bLower = bSide === "minimum"
|
|
123
|
+
if (aLower !== bLower) return Number(bLower) - Number(aLower)
|
|
124
|
+
if (aLower) return sourceMean(a, "y") - sourceMean(b, "y")
|
|
125
|
+
if (a.connections.length !== b.connections.length)
|
|
126
|
+
return a.connections.length - b.connections.length
|
|
127
|
+
if (a.connections.length === 1)
|
|
128
|
+
return sourceMean(a, "x") - sourceMean(b, "x")
|
|
129
|
+
if (aSide !== bSide)
|
|
130
|
+
return Number(aSide === "maximum") - Number(bSide === "maximum")
|
|
131
|
+
return (
|
|
132
|
+
(aSide === "maximum" ? 1 : -1) * (sourceMean(a, "y") - sourceMean(b, "y"))
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
// An edge-only singleton can reserve a separating channel before the
|
|
136
|
+
// corner-guided pairs. Keep this ordered retry bounded; each bus may also
|
|
137
|
+
// move its complete, ordered track envelope into a free edge interval.
|
|
138
|
+
if (params.buses.length > 1) {
|
|
139
|
+
const intervalOrder = ordered.toSorted((a, b) => {
|
|
140
|
+
const singleton = (bus: PreparedBus) =>
|
|
141
|
+
bus.connections.length === 1 &&
|
|
142
|
+
!getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
143
|
+
return Number(singleton(b)) - Number(singleton(a))
|
|
144
|
+
})
|
|
145
|
+
const intervalAttemptLimit = Math.min(maximumAttempts, attempts + 48)
|
|
146
|
+
function* orderedIntervals(
|
|
147
|
+
index: number,
|
|
148
|
+
accepted: FanoutRoutePlan[],
|
|
149
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
|
|
150
|
+
if (index === intervalOrder.length) return accepted
|
|
151
|
+
if (attempts >= intervalAttemptLimit) return null
|
|
152
|
+
const bus = intervalOrder[index]!
|
|
153
|
+
const ordinary = yield* route(bus, accepted)
|
|
154
|
+
for (const plans of ordinary) {
|
|
155
|
+
const result = yield* orderedIntervals(index + 1, [
|
|
156
|
+
...accepted,
|
|
157
|
+
...plans,
|
|
158
|
+
])
|
|
159
|
+
if (result) return result
|
|
160
|
+
}
|
|
161
|
+
const alternatives = yield* freeIntervals(
|
|
162
|
+
bus,
|
|
163
|
+
accepted,
|
|
164
|
+
intervalAttemptLimit,
|
|
165
|
+
)
|
|
166
|
+
for (const plans of alternatives) {
|
|
167
|
+
const result = yield* orderedIntervals(index + 1, [
|
|
168
|
+
...accepted,
|
|
169
|
+
...plans,
|
|
170
|
+
])
|
|
171
|
+
if (result) return result
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
const intervalPlans = yield* orderedIntervals(0, [...params.acceptedPlans])
|
|
176
|
+
if (intervalPlans)
|
|
177
|
+
return restore(intervalPlans.slice(params.acceptedPlans.length))
|
|
178
|
+
}
|
|
179
|
+
function* freeIntervals(
|
|
180
|
+
bus: PreparedBus,
|
|
181
|
+
accepted: FanoutRoutePlan[],
|
|
182
|
+
attemptLimit: number,
|
|
183
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[][], void> {
|
|
184
|
+
if (!bus.exitEdge) return []
|
|
185
|
+
const reservedVias = params.reservedVias?.filter(
|
|
186
|
+
(reserved) =>
|
|
187
|
+
!bus.connections.some(
|
|
188
|
+
(connection) =>
|
|
189
|
+
connection.connection.name === reserved.connectionName,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
const common = { ...params, bus, acceptedPlans: accepted, reservedVias }
|
|
193
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
194
|
+
const vertical = bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
195
|
+
const boundary = bus.sharedBoundary
|
|
196
|
+
const lower = vertical ? boundary.minY : boundary.minX
|
|
197
|
+
const upper = vertical ? boundary.maxY : boundary.maxX
|
|
198
|
+
const middle = (lower + upper) / 2
|
|
199
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
200
|
+
for (const track of getFreeBoundaryTracks(common)) {
|
|
201
|
+
if (attempts >= attemptLimit) break
|
|
202
|
+
const preferredExit = side
|
|
203
|
+
? bus.preferredExit
|
|
204
|
+
: guidance(bus).find(
|
|
205
|
+
(candidate) =>
|
|
206
|
+
getCornerBandSide(bus.exitEdge, candidate) ===
|
|
207
|
+
(track > middle ? "maximum" : "minimum"),
|
|
208
|
+
)
|
|
209
|
+
const reference = bus.connections.map((connection) =>
|
|
210
|
+
getCornerTargetTrack({
|
|
211
|
+
...common,
|
|
212
|
+
bus: { ...bus, preferredExit },
|
|
213
|
+
connection,
|
|
214
|
+
cornerExitLaneOffset: 0,
|
|
215
|
+
windingOrderIndex: 0,
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
const mean =
|
|
219
|
+
reference.reduce((sum, value) => sum + value, 0) / reference.length
|
|
220
|
+
const tracks = reference.map((value) => value - mean + track)
|
|
221
|
+
if (
|
|
222
|
+
tracks.some(
|
|
223
|
+
(value) =>
|
|
224
|
+
value <= lower + params.traceWidth / 2 ||
|
|
225
|
+
value >= upper - params.traceWidth / 2 ||
|
|
226
|
+
(side === "minimum" && value >= middle) ||
|
|
227
|
+
(side === "maximum" && value <= middle),
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
continue
|
|
231
|
+
const terminals = bus.connections.map((connection, index) => {
|
|
232
|
+
const viaPoint = params.fixedViaPointsByConnectionIndex?.get(
|
|
233
|
+
connection.connectionIndex,
|
|
234
|
+
)
|
|
235
|
+
const along = tracks[index]!
|
|
236
|
+
const exitPoint =
|
|
237
|
+
bus.exitEdge === "left"
|
|
238
|
+
? { x: boundary.minX, y: along }
|
|
239
|
+
: bus.exitEdge === "right"
|
|
240
|
+
? { x: boundary.maxX, y: along }
|
|
241
|
+
: bus.exitEdge === "bottom"
|
|
242
|
+
? { x: along, y: boundary.minY }
|
|
243
|
+
: { x: along, y: boundary.maxY }
|
|
244
|
+
return { connection, viaPoint: viaPoint!, exitPoint }
|
|
245
|
+
})
|
|
246
|
+
if (terminals.some((terminal) => !terminal.viaPoint)) return []
|
|
247
|
+
attempts++
|
|
248
|
+
const steps = routeViaMinimalWindingAlternativesSteps(
|
|
249
|
+
{
|
|
250
|
+
...common,
|
|
251
|
+
terminals,
|
|
252
|
+
gridStep: pitch / 2,
|
|
253
|
+
alignGridToPads: true,
|
|
254
|
+
maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 16,
|
|
255
|
+
adaptiveRouteOrder: true,
|
|
256
|
+
},
|
|
257
|
+
1,
|
|
258
|
+
false,
|
|
259
|
+
)
|
|
260
|
+
let result = steps.next()
|
|
261
|
+
while (!result.done) {
|
|
262
|
+
yield {
|
|
263
|
+
phase: "via-minimal-winding",
|
|
264
|
+
busId: bus.busId,
|
|
265
|
+
targetLayer: params.targetLayer,
|
|
266
|
+
winding: result.value,
|
|
267
|
+
}
|
|
268
|
+
result = steps.next()
|
|
269
|
+
}
|
|
270
|
+
alternatives.push(...result.value)
|
|
271
|
+
}
|
|
272
|
+
return alternatives
|
|
273
|
+
}
|
|
274
|
+
function guidance(bus: PreparedBus): (FanoutBorderTarget | undefined)[] {
|
|
275
|
+
if (getCornerBandSide(bus.exitEdge, bus.preferredExit))
|
|
276
|
+
return [bus.preferredExit]
|
|
277
|
+
switch (bus.exitEdge) {
|
|
278
|
+
case "right":
|
|
279
|
+
return [bus.preferredExit, "top-right", "bottom-right"]
|
|
280
|
+
case "left":
|
|
281
|
+
return [bus.preferredExit, "top-left", "bottom-left"]
|
|
282
|
+
case "top":
|
|
283
|
+
return [bus.preferredExit, "top-left", "top-right"]
|
|
284
|
+
case "bottom":
|
|
285
|
+
return [bus.preferredExit, "bottom-left", "bottom-right"]
|
|
286
|
+
default:
|
|
287
|
+
return [bus.preferredExit]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function offsets(
|
|
291
|
+
bus: PreparedBus,
|
|
292
|
+
accepted: FanoutRoutePlan[],
|
|
293
|
+
preferredExit: FanoutBorderTarget | undefined,
|
|
294
|
+
) {
|
|
295
|
+
const side = getCornerBandSide(bus.exitEdge, preferredExit)
|
|
296
|
+
if (!side || params.buses.length > 1) return [0]
|
|
297
|
+
const horizontal = bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
298
|
+
const lower = horizontal ? bus.sharedBoundary.minY : bus.sharedBoundary.minX
|
|
299
|
+
const upper = horizontal ? bus.sharedBoundary.maxY : bus.sharedBoundary.maxX
|
|
300
|
+
const middle = (lower + upper) / 2
|
|
301
|
+
const cornerExitLaneOffset = accepted.filter(
|
|
302
|
+
(plan) => plan.exitEdge === bus.exitEdge && plan.cornerBandSide === side,
|
|
303
|
+
).length
|
|
304
|
+
const tracks = bus.connections.map((connection) =>
|
|
305
|
+
getCornerTargetTrack({
|
|
306
|
+
...params,
|
|
307
|
+
bus: { ...bus, preferredExit },
|
|
308
|
+
connection,
|
|
309
|
+
cornerExitLaneOffset,
|
|
310
|
+
windingOrderIndex: 0,
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
const sign = side === "minimum" ? -1 : 1
|
|
314
|
+
const padPitch = Math.min(bus.pitchX, bus.pitchY)
|
|
315
|
+
const mean = tracks.reduce((sum, value) => sum + value, 0) / tracks.length
|
|
316
|
+
const edgeTrack = side === "minimum" ? lower + pitch : upper - pitch
|
|
317
|
+
const candidates = [
|
|
318
|
+
0,
|
|
319
|
+
sign * 2 * pitch,
|
|
320
|
+
-sign * 2 * pitch,
|
|
321
|
+
sign * padPitch,
|
|
322
|
+
sign * 2 * padPitch,
|
|
323
|
+
sign * 3 * padPitch,
|
|
324
|
+
sign * 4 * padPitch,
|
|
325
|
+
edgeTrack - mean,
|
|
326
|
+
]
|
|
327
|
+
return candidates.filter(
|
|
328
|
+
(offset, i) =>
|
|
329
|
+
candidates.indexOf(offset) === i &&
|
|
330
|
+
tracks.every(
|
|
331
|
+
(track) =>
|
|
332
|
+
track + offset > lower + params.traceWidth / 2 &&
|
|
333
|
+
track + offset < upper - params.traceWidth / 2 &&
|
|
334
|
+
(side === "minimum"
|
|
335
|
+
? track + offset < middle
|
|
336
|
+
: track + offset > middle),
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
function* search(
|
|
341
|
+
remaining: readonly PreparedBus[],
|
|
342
|
+
accepted: FanoutRoutePlan[],
|
|
343
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
|
|
344
|
+
if (!remaining.length) return accepted
|
|
345
|
+
if (attempts >= maximumAttempts) return null
|
|
346
|
+
for (const bus of remaining) {
|
|
347
|
+
for (const preferredExit of guidance(bus)) {
|
|
348
|
+
for (const offset of offsets(bus, accepted, preferredExit)) {
|
|
349
|
+
const alternatives = yield* route(
|
|
350
|
+
bus,
|
|
351
|
+
accepted,
|
|
352
|
+
preferredExit,
|
|
353
|
+
offset,
|
|
354
|
+
)
|
|
355
|
+
if (!alternatives.length) continue
|
|
356
|
+
const result = yield* search(
|
|
357
|
+
remaining.filter((candidate) => candidate !== bus),
|
|
358
|
+
[...accepted, ...alternatives[0]!],
|
|
359
|
+
)
|
|
360
|
+
if (result) return result
|
|
361
|
+
if (attempts >= maximumAttempts) return null
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// A singleton has no intra-bus track ordering to preserve. If the ordinary
|
|
366
|
+
// bands fail, use free intervals on its original edge and declared half-band.
|
|
367
|
+
for (const bus of remaining) {
|
|
368
|
+
if (bus.connections.length !== 1 || !bus.exitEdge) continue
|
|
369
|
+
const connection = bus.connections[0]!
|
|
370
|
+
const viaPoint = params.fixedViaPointsByConnectionIndex?.get(
|
|
371
|
+
connection.connectionIndex,
|
|
372
|
+
)
|
|
373
|
+
if (!viaPoint) continue
|
|
374
|
+
const reservedVias = params.reservedVias?.filter(
|
|
375
|
+
(reserved) => reserved.connectionName !== connection.connection.name,
|
|
376
|
+
)
|
|
377
|
+
const tracks = getFreeBoundaryTracks({
|
|
378
|
+
...params,
|
|
379
|
+
bus,
|
|
380
|
+
acceptedPlans: accepted,
|
|
381
|
+
reservedVias,
|
|
382
|
+
})
|
|
383
|
+
for (const track of tracks) {
|
|
384
|
+
if (attempts++ >= maximumAttempts) return null
|
|
385
|
+
const boundary = bus.sharedBoundary
|
|
386
|
+
const exitPoint =
|
|
387
|
+
bus.exitEdge === "left"
|
|
388
|
+
? { x: boundary.minX, y: track }
|
|
389
|
+
: bus.exitEdge === "right"
|
|
390
|
+
? { x: boundary.maxX, y: track }
|
|
391
|
+
: bus.exitEdge === "bottom"
|
|
392
|
+
? { x: track, y: boundary.minY }
|
|
393
|
+
: { x: track, y: boundary.maxY }
|
|
394
|
+
const steps = routeViaMinimalWindingAlternativesSteps(
|
|
395
|
+
{
|
|
396
|
+
...params,
|
|
397
|
+
bus,
|
|
398
|
+
acceptedPlans: accepted,
|
|
399
|
+
reservedVias,
|
|
400
|
+
terminals: [{ connection, viaPoint, exitPoint }],
|
|
401
|
+
gridStep: pitch / 2,
|
|
402
|
+
alignGridToPads: true,
|
|
403
|
+
maximumRouteOrderAttempts: 3,
|
|
404
|
+
},
|
|
405
|
+
1,
|
|
406
|
+
false,
|
|
407
|
+
)
|
|
408
|
+
let result = steps.next()
|
|
409
|
+
while (!result.done) {
|
|
410
|
+
yield {
|
|
411
|
+
phase: "via-minimal-winding",
|
|
412
|
+
busId: bus.busId,
|
|
413
|
+
targetLayer: params.targetLayer,
|
|
414
|
+
winding: result.value,
|
|
415
|
+
}
|
|
416
|
+
result = steps.next()
|
|
417
|
+
}
|
|
418
|
+
if (!result.value.length) continue
|
|
419
|
+
const complete = yield* search(
|
|
420
|
+
remaining.filter((candidate) => candidate !== bus),
|
|
421
|
+
[...accepted, ...result.value[0]!],
|
|
422
|
+
)
|
|
423
|
+
if (complete) return complete
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return null
|
|
427
|
+
}
|
|
428
|
+
const result = yield* search(ordered, [...params.acceptedPlans])
|
|
429
|
+
return result && restore(result.slice(params.acceptedPlans.length))
|
|
430
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { routeAdaptiveLeftCrossbarBusSteps } from "./route-adaptive-left-crossbar-bus"
|
|
2
|
+
import { routeOppositeBottomCrossbarBusSteps } from "./route-opposite-bottom-crossbar-bus"
|
|
3
|
+
import { reflectFanoutX } from "./reflect-fanout-x"
|
|
4
|
+
import { routeReservedNarrowBusesSteps } from "./route-reserved-narrow-buses"
|
|
5
|
+
import { routeBottomCrossbarBusSteps } from "./route-bottom-crossbar-bus"
|
|
6
|
+
import { routeLeftCrossbarBusSteps } from "./route-left-crossbar-bus"
|
|
7
|
+
import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
|
|
8
|
+
import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
|
|
9
|
+
import {
|
|
10
|
+
fanoutPlansAreClear,
|
|
11
|
+
routeBusAlternativesSteps,
|
|
12
|
+
type RouteBusAlternativesProgress,
|
|
13
|
+
type RouteBusParams,
|
|
14
|
+
} from "./route-bus"
|
|
15
|
+
|
|
16
|
+
export interface RouteReservedSourceBusesParams
|
|
17
|
+
extends Omit<RouteBusParams, "bus" | "targetLayer" | "acceptedPlans"> {
|
|
18
|
+
buses: readonly PreparedBus[]
|
|
19
|
+
sourceEscapes: readonly PeripheralSourceEscape[]
|
|
20
|
+
initialPlans: readonly FanoutRoutePlan[]
|
|
21
|
+
targetLayerByBusId?: ReadonlyMap<string, string>
|
|
22
|
+
sourceBoundary?: Bounds
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Finish buses while preserving every already matched source escape and via. */
|
|
26
|
+
export function* routeReservedSourceBusesSteps(
|
|
27
|
+
params: RouteReservedSourceBusesParams,
|
|
28
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
|
|
29
|
+
if (
|
|
30
|
+
params.buses.some((bus) => bus.termination.type === "boundary") &&
|
|
31
|
+
params.buses.every(
|
|
32
|
+
(bus) => bus.termination.type === "plane" || bus.exitEdge === "left",
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
const mirrored = reflectFanoutX(params)
|
|
36
|
+
const plans = yield* routeReservedSourceBusesSteps(mirrored)
|
|
37
|
+
if (!plans) return null
|
|
38
|
+
const restored = reflectFanoutX(plans)
|
|
39
|
+
const originalConnections = new Map(
|
|
40
|
+
params.buses.flatMap((bus) =>
|
|
41
|
+
bus.connections.map(
|
|
42
|
+
(connection) => [connection.connectionIndex, connection] as const,
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
return restored.map((plan) => ({
|
|
47
|
+
...plan,
|
|
48
|
+
sourceObstacle: originalConnections.get(plan.connectionIndex)!
|
|
49
|
+
.sourceObstacle,
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
const { buses, sourceEscapes, initialPlans } = params
|
|
53
|
+
const byIndex = new Map(
|
|
54
|
+
sourceEscapes.map((source) => [source.connectionIndex, source]),
|
|
55
|
+
)
|
|
56
|
+
const committedIndices = new Set(
|
|
57
|
+
initialPlans.map((plan) => plan.connectionIndex),
|
|
58
|
+
)
|
|
59
|
+
const pending = buses.filter((bus) =>
|
|
60
|
+
bus.connections.some(
|
|
61
|
+
(connection) => !committedIndices.has(connection.connectionIndex),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
if (
|
|
65
|
+
pending.some((bus) =>
|
|
66
|
+
bus.connections.some((connection) =>
|
|
67
|
+
committedIndices.has(connection.connectionIndex),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"FanoutSolver: a reserved-source continuation cannot start with a partially committed bus",
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
for (const bus of pending)
|
|
76
|
+
for (const connection of bus.connections) {
|
|
77
|
+
if (!byIndex.has(connection.connectionIndex))
|
|
78
|
+
throw new Error(
|
|
79
|
+
`FanoutSolver: missing reserved source escape for ${connection.connection.name}`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
const fixedViaPointsByConnectionIndex = new Map(
|
|
83
|
+
sourceEscapes.map((source) => [source.connectionIndex, source.via.center]),
|
|
84
|
+
)
|
|
85
|
+
const sourceEscapePaths = new Map(
|
|
86
|
+
sourceEscapes.map((source) => [
|
|
87
|
+
source.connectionIndex,
|
|
88
|
+
[
|
|
89
|
+
source.segments[0]!.start,
|
|
90
|
+
...source.segments.map((segment) => segment.end),
|
|
91
|
+
],
|
|
92
|
+
]),
|
|
93
|
+
)
|
|
94
|
+
const accepted = [...initialPlans]
|
|
95
|
+
const occupiedLayerCount = (bus: PreparedBus) => {
|
|
96
|
+
const layer =
|
|
97
|
+
params.targetLayerByBusId?.get(bus.busId) ??
|
|
98
|
+
(bus.allowedLayers ?? params.layerNames).find(
|
|
99
|
+
(layer) => layer !== bus.connections[0]!.sourceLayer,
|
|
100
|
+
)
|
|
101
|
+
return initialPlans.filter((plan) => plan.targetLayer === layer).length
|
|
102
|
+
}
|
|
103
|
+
const boundaryBuses = pending
|
|
104
|
+
.filter((bus) => bus.termination.type === "boundary")
|
|
105
|
+
.sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
b.connections.length - a.connections.length ||
|
|
108
|
+
occupiedLayerCount(a) - occupiedLayerCount(b),
|
|
109
|
+
)
|
|
110
|
+
const groupedNarrow = new Set<string>()
|
|
111
|
+
for (const bus of [
|
|
112
|
+
...boundaryBuses,
|
|
113
|
+
...pending.filter((bus) => bus.termination.type === "plane"),
|
|
114
|
+
]) {
|
|
115
|
+
if (groupedNarrow.has(bus.busId)) continue
|
|
116
|
+
if (
|
|
117
|
+
bus.termination.type === "boundary" &&
|
|
118
|
+
bus.connections.length <= 2 &&
|
|
119
|
+
params.targetLayerByBusId
|
|
120
|
+
) {
|
|
121
|
+
const targetLayer = params.targetLayerByBusId.get(bus.busId)
|
|
122
|
+
if (!targetLayer) return null
|
|
123
|
+
const group = boundaryBuses.filter(
|
|
124
|
+
(candidate) =>
|
|
125
|
+
candidate.connections.length <= 2 &&
|
|
126
|
+
params.targetLayerByBusId!.get(candidate.busId) === targetLayer,
|
|
127
|
+
)
|
|
128
|
+
const plans = yield* routeReservedNarrowBusesSteps({
|
|
129
|
+
...params,
|
|
130
|
+
buses: group,
|
|
131
|
+
targetLayer,
|
|
132
|
+
acceptedPlans: accepted,
|
|
133
|
+
fixedViaPointsByConnectionIndex,
|
|
134
|
+
sourceEscapePaths,
|
|
135
|
+
reservedVias: sourceEscapes.map((source) => ({
|
|
136
|
+
connectionName: source.connectionName,
|
|
137
|
+
via: source.via,
|
|
138
|
+
})),
|
|
139
|
+
})
|
|
140
|
+
if (!plans) return null
|
|
141
|
+
accepted.push(...plans)
|
|
142
|
+
for (const candidate of group) groupedNarrow.add(candidate.busId)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
const ownIndices = new Set(
|
|
146
|
+
bus.connections.map((connection) => connection.connectionIndex),
|
|
147
|
+
)
|
|
148
|
+
const reservedVias = sourceEscapes
|
|
149
|
+
.filter((source) => !ownIndices.has(source.connectionIndex))
|
|
150
|
+
.map((source) => ({
|
|
151
|
+
connectionName: source.connectionName,
|
|
152
|
+
via: source.via,
|
|
153
|
+
...(source.segments.length === 1
|
|
154
|
+
? { sourceEscapeSegment: source.segments[0] }
|
|
155
|
+
: {}),
|
|
156
|
+
}))
|
|
157
|
+
const assignedLayer = params.targetLayerByBusId?.get(bus.busId)
|
|
158
|
+
const layers = assignedLayer
|
|
159
|
+
? [assignedLayer]
|
|
160
|
+
: bus.termination.type === "plane"
|
|
161
|
+
? [bus.termination.layer]
|
|
162
|
+
: (
|
|
163
|
+
bus.routableEscapeLayers ??
|
|
164
|
+
bus.allowedLayers ??
|
|
165
|
+
params.layerNames
|
|
166
|
+
).filter((layer) => layer !== bus.connections[0]!.sourceLayer)
|
|
167
|
+
let routed: FanoutRoutePlan[] | undefined
|
|
168
|
+
for (const targetLayer of layers) {
|
|
169
|
+
const routeParams = {
|
|
170
|
+
...params,
|
|
171
|
+
bus,
|
|
172
|
+
targetLayer,
|
|
173
|
+
acceptedPlans: accepted,
|
|
174
|
+
fixedViaPointsByConnectionIndex,
|
|
175
|
+
sourceEscapePaths,
|
|
176
|
+
reservedVias,
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
params.sourceBoundary &&
|
|
180
|
+
bus.termination.type === "boundary" &&
|
|
181
|
+
bus.connections.length > 2
|
|
182
|
+
) {
|
|
183
|
+
for (const routeCrossbar of [
|
|
184
|
+
routeBottomCrossbarBusSteps,
|
|
185
|
+
routeLeftCrossbarBusSteps,
|
|
186
|
+
routeAdaptiveLeftCrossbarBusSteps,
|
|
187
|
+
routeOppositeBottomCrossbarBusSteps,
|
|
188
|
+
]) {
|
|
189
|
+
const steps = routeCrossbar({
|
|
190
|
+
...routeParams,
|
|
191
|
+
sourceBoundary: params.sourceBoundary,
|
|
192
|
+
})
|
|
193
|
+
let step = steps.next()
|
|
194
|
+
while (!step.done) {
|
|
195
|
+
yield {
|
|
196
|
+
phase: "via-minimal-winding",
|
|
197
|
+
busId: bus.busId,
|
|
198
|
+
targetLayer,
|
|
199
|
+
winding: step.value,
|
|
200
|
+
}
|
|
201
|
+
step = steps.next()
|
|
202
|
+
}
|
|
203
|
+
if (step.value) {
|
|
204
|
+
routed = step.value
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (routed) break
|
|
209
|
+
}
|
|
210
|
+
const alternatives = yield* routeBusAlternativesSteps(
|
|
211
|
+
{
|
|
212
|
+
...params,
|
|
213
|
+
bus,
|
|
214
|
+
targetLayer,
|
|
215
|
+
acceptedPlans: accepted,
|
|
216
|
+
fixedViaPointsByConnectionIndex,
|
|
217
|
+
sourceEscapePaths,
|
|
218
|
+
reservedVias,
|
|
219
|
+
viaMinimalOnly: true,
|
|
220
|
+
adaptiveWindingRouteOrder: true,
|
|
221
|
+
alignWindingGridToPads: true,
|
|
222
|
+
windingGridStep: (params.traceWidth + params.clearance) / 2,
|
|
223
|
+
fixedViaFallbackRouteOrderAttempts: 32,
|
|
224
|
+
},
|
|
225
|
+
1,
|
|
226
|
+
false,
|
|
227
|
+
)
|
|
228
|
+
if (alternatives.length) {
|
|
229
|
+
routed = alternatives[0]
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!routed) return null
|
|
234
|
+
accepted.push(...routed)
|
|
235
|
+
}
|
|
236
|
+
const sharedBoundary = buses[0]?.sharedBoundary
|
|
237
|
+
if (
|
|
238
|
+
!sharedBoundary ||
|
|
239
|
+
!fanoutPlansAreClear({ ...params, plans: accepted, sharedBoundary })
|
|
240
|
+
)
|
|
241
|
+
return null
|
|
242
|
+
return accepted
|
|
243
|
+
}
|