@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.
@@ -0,0 +1,425 @@
1
+ import type { Obstacle } from "@tscircuit/capacity-autorouter"
2
+ import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
3
+ import { buildViaMinimalWindingPlan } from "./route-via-minimal-winding"
4
+ import { getCornerBandSide } from "./boundary-exit"
5
+ import {
6
+ distance,
7
+ distancePointToSegment,
8
+ distanceSegmentToSegment,
9
+ } from "./geometry"
10
+ import { getViaSpanLayers } from "./layer-names"
11
+ import {
12
+ fanoutPlansAreClear,
13
+ getCornerTargetTrack,
14
+ type RouteBusParams,
15
+ } from "./route-bus"
16
+ import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
17
+ import {
18
+ routeViaMinimalWindingAlternativesSteps,
19
+ type RouteViaMinimalWindingProgress,
20
+ } from "./route-via-minimal-winding"
21
+ import type {
22
+ Bounds,
23
+ FanoutRoutePlan,
24
+ Point2D,
25
+ RoutedSegment,
26
+ RoutedVia,
27
+ } from "./types"
28
+
29
+ /** Choose unordered left source exits, then reconnect their original target order through a two-layer crossbar. */
30
+ export function* routeAdaptiveLeftCrossbarBusSteps(
31
+ params: RouteBusParams & {
32
+ sourceEscapes: readonly PeripheralSourceEscape[]
33
+ sourceBoundary: Bounds
34
+ },
35
+ ): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
36
+ const {
37
+ bus,
38
+ sourceBoundary,
39
+ sourceEscapes,
40
+ traceWidth: w,
41
+ clearance: c,
42
+ targetLayer,
43
+ } = params
44
+ const cornerSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
45
+ if (
46
+ params.allowBlindAndBuriedVias ||
47
+ bus.exitEdge !== "right" ||
48
+ (cornerSide !== undefined && cornerSide !== "minimum") ||
49
+ bus.connections.length < 3
50
+ )
51
+ return null
52
+ const allowedLayers = bus.routableEscapeLayers ?? bus.allowedLayers ?? []
53
+ const crossoverLayer = allowedLayers.find((layer) => layer !== targetLayer)
54
+ if (
55
+ !crossoverLayer ||
56
+ !allowedLayers.includes(targetLayer) ||
57
+ bus.connections.some((connection) => connection.sourceLayer === targetLayer)
58
+ )
59
+ return null
60
+ const byIndex = new Map(
61
+ sourceEscapes.map((source) => [source.connectionIndex, source]),
62
+ )
63
+ const pitch = w + c,
64
+ padPitch = Math.min(bus.pitchX, bus.pitchY),
65
+ viaPitch = params.viaDiameter + c
66
+ const firstColumn =
67
+ bus.sharedBoundary.minX + Math.max(padPitch / 2, params.viaDiameter / 2 + c)
68
+ if (
69
+ firstColumn + (bus.connections.length - 1) * viaPitch >=
70
+ sourceBoundary.minX
71
+ )
72
+ return null
73
+ const sameCornerCount = params.acceptedPlans.filter(
74
+ (plan) =>
75
+ plan.exitEdge === bus.exitEdge && plan.cornerBandSide === "minimum",
76
+ ).length
77
+ const terminals = bus.connections.map((connection) => {
78
+ const source = byIndex.get(connection.connectionIndex)
79
+ if (!source)
80
+ throw new Error(
81
+ `FanoutSolver: missing crossbar source escape for ${connection.connection.name}`,
82
+ )
83
+ return {
84
+ connection,
85
+ viaPoint: source.via.center,
86
+ exitPoint: {
87
+ x: bus.sharedBoundary.maxX,
88
+ y: cornerSide
89
+ ? getCornerTargetTrack({
90
+ ...params,
91
+ connection,
92
+ cornerExitLaneOffset: sameCornerCount,
93
+ })
94
+ : getCornerTargetTrack({
95
+ ...params,
96
+ bus: { ...bus, preferredExit: "bottom-right" },
97
+ connection,
98
+ cornerExitLaneOffset: sameCornerCount,
99
+ windingOrderIndex: 0,
100
+ }),
101
+ },
102
+ }
103
+ })
104
+ const targetOrdered = terminals.toSorted(
105
+ (a, b) => a.exitPoint.y - b.exitPoint.y,
106
+ )
107
+ if (!cornerSide)
108
+ for (const [rank, terminal] of targetOrdered.entries())
109
+ terminal.exitPoint.y = sourceBoundary.minY + padPitch + rank * viaPitch
110
+ const crossoverTrackByIndex = new Map(
111
+ targetOrdered.map((terminal) => [
112
+ terminal.connection.connectionIndex,
113
+ terminal.exitPoint.y,
114
+ ]),
115
+ )
116
+ const maxPort = Math.min(
117
+ sourceBoundary.maxY - pitch,
118
+ Math.max(...terminals.map((t) => t.viaPoint.y)) + 2 * padPitch,
119
+ )
120
+ const minPort = Math.max(
121
+ sourceBoundary.minY + pitch,
122
+ Math.min(...terminals.map((t) => t.viaPoint.y)) - 2 * padPitch,
123
+ ...(cornerSide
124
+ ? []
125
+ : [
126
+ Math.max(...crossoverTrackByIndex.values()) +
127
+ params.viaDiameter / 2 +
128
+ w / 2 +
129
+ c +
130
+ pitch,
131
+ ]),
132
+ )
133
+ if (
134
+ maxPort - minPort < (terminals.length - 1) * viaPitch ||
135
+ minPort <=
136
+ Math.max(...crossoverTrackByIndex.values()) +
137
+ params.viaDiameter / 2 +
138
+ w / 2 +
139
+ c
140
+ )
141
+ return null
142
+ const sourcePaths = new Map(
143
+ sourceEscapes.map((source) => [
144
+ source.connectionIndex,
145
+ [source.segments[0]!.start, ...source.segments.map((s) => s.end)],
146
+ ]),
147
+ )
148
+ const obstacles: Obstacle[] = [
149
+ ...params.srj.obstacles
150
+ .filter((o) => o.layers.includes(targetLayer))
151
+ .map((o) => ({ ...o, layers: ["top"] })),
152
+ ...sourceEscapes.map((source) => ({
153
+ type: "rect" as const,
154
+ shape: "circle",
155
+ center: source.via.center,
156
+ width: source.via.diameter,
157
+ height: source.via.diameter,
158
+ layers: ["top"],
159
+ connectedTo: [source.connectionName],
160
+ })),
161
+ ]
162
+ for (let y = sourceBoundary.minY; y <= sourceBoundary.maxY; y += pitch)
163
+ if (y < minPort || y > maxPort)
164
+ obstacles.push({
165
+ type: "rect",
166
+ shape: "circle",
167
+ center: { x: sourceBoundary.minX, y },
168
+ width: w / 1000,
169
+ height: w / 1000,
170
+ layers: ["top"],
171
+ connectedTo: ["blocked-port"],
172
+ } as Obstacle)
173
+ const flowBuses = terminals.map((terminal, i) => ({
174
+ ...bus,
175
+ busId: bus.busId + ":" + i,
176
+ sharedBoundary: sourceBoundary,
177
+ preferredExit: undefined,
178
+ exitEdge: undefined,
179
+ connections: [
180
+ {
181
+ ...terminal.connection,
182
+ sourcePoint: { ...terminal.viaPoint, layer: "top" },
183
+ sourceLayer: "top",
184
+ sourceObstacle: obstacles.find(
185
+ (o) =>
186
+ o.connectedTo.includes(terminal.connection.connection.name) &&
187
+ distance(o.center, terminal.viaPoint) < 1e-7,
188
+ )!,
189
+ exitTargetPoint: undefined,
190
+ hasExplicitLayeredExitTarget: false,
191
+ hasExplicitExitTarget: false,
192
+ },
193
+ ],
194
+ }))
195
+ const flow = routeSingleLayerWithAdaptiveExitsSteps({
196
+ srj: { ...params.srj, obstacles },
197
+ buses: flowBuses,
198
+ traceWidth: w,
199
+ clearance: c,
200
+ availableBoundaryRegions: [
201
+ { exitEdge: "left", direction: "left", preferredExit: "left" },
202
+ ],
203
+ })
204
+ let flowStep = flow.next()
205
+ while (!flowStep.done) {
206
+ yield {
207
+ phase: "route-connection",
208
+ routeOrderAttempt: 0,
209
+ connectionIndex: 0,
210
+ connectionCount: terminals.length,
211
+ connectionName: terminals[0]!.connection.connection.name,
212
+ searchBatch: 0,
213
+ expandedStateCount: 0,
214
+ connectionComplete: false,
215
+ }
216
+ flowStep = flow.next()
217
+ }
218
+ const stage: FanoutRoutePlan[] | undefined =
219
+ flowStep.value?.length === terminals.length
220
+ ? flowStep.value.map((plan) => {
221
+ const terminal = terminals.find(
222
+ (t) => t.connection.connectionIndex === plan.connectionIndex,
223
+ )!
224
+ return buildViaMinimalWindingPlan({
225
+ ...params,
226
+ terminal: { ...terminal, exitPoint: plan.exitPoint },
227
+ sourceEscapePoints: sourcePaths.get(plan.connectionIndex),
228
+ targetLayerPoints: [
229
+ plan.segments[0]!.start,
230
+ ...plan.segments.map((s) => s.end),
231
+ ],
232
+ allowBlindAndBuriedVias: false,
233
+ })
234
+ })
235
+ : undefined
236
+ if (!stage) return null
237
+ const sorted = stage.toSorted((a, b) =>
238
+ cornerSide
239
+ ? a.exitPoint.y - b.exitPoint.y
240
+ : terminals.find(
241
+ (t) => t.connection.connectionIndex === a.connectionIndex,
242
+ )!.exitPoint.y -
243
+ terminals.find(
244
+ (t) => t.connection.connectionIndex === b.connectionIndex,
245
+ )!.exitPoint.y,
246
+ )
247
+ const via = (
248
+ point: Point2D,
249
+ fromLayer: string,
250
+ toLayer: string,
251
+ ): RoutedVia => ({
252
+ center: point,
253
+ diameter: params.viaDiameter,
254
+ holeDiameter: params.viaHoleDiameter,
255
+ fromLayer,
256
+ toLayer,
257
+ spanLayers: getViaSpanLayers({
258
+ fromLayer,
259
+ toLayer,
260
+ layerNames: params.layerNames,
261
+ allowBlindAndBuriedVias: false,
262
+ }),
263
+ })
264
+ const prefixByIndex = new Map<number, FanoutRoutePlan>()
265
+ for (const [rank, plan] of sorted.entries()) {
266
+ const last = plan.segments.at(-1)!,
267
+ dx = last.end.x - last.start.x,
268
+ dy = last.end.y - last.start.y
269
+ if (dx >= 0 || Math.abs(dy) > -dx + 1e-7) return null
270
+ const original = terminals.find(
271
+ (t) => t.connection.connectionIndex === plan.connectionIndex,
272
+ )!
273
+ const column = firstColumn + rank * viaPitch,
274
+ first = { x: column, y: plan.exitPoint.y },
275
+ second = {
276
+ x: column,
277
+ y: crossoverTrackByIndex.get(plan.connectionIndex)!,
278
+ }
279
+ const additions: RoutedSegment[] = [
280
+ { start: plan.exitPoint, end: first, width: w, layer: targetLayer },
281
+ { start: first, end: second, width: w, layer: crossoverLayer },
282
+ ]
283
+ prefixByIndex.set(plan.connectionIndex, {
284
+ ...plan,
285
+ segments: [...plan.segments, ...additions],
286
+ additionalVias: [
287
+ via(first, targetLayer, crossoverLayer),
288
+ via(second, crossoverLayer, targetLayer),
289
+ ],
290
+ direction: bus.direction,
291
+ exitEdge: bus.exitEdge,
292
+ cornerBandSide: getCornerBandSide(bus.exitEdge, bus.preferredExit),
293
+ exitPoint: original.exitPoint,
294
+ })
295
+ }
296
+ const prefixes = [...prefixByIndex.values()]
297
+ const continuationConnections = terminals.map((t) => ({
298
+ ...t.connection,
299
+ sourcePoint: {
300
+ ...prefixByIndex.get(t.connection.connectionIndex)!.additionalVias![1]!
301
+ .center,
302
+ layer: targetLayer,
303
+ },
304
+ sourceLayer: targetLayer,
305
+ }))
306
+ const tailParams = {
307
+ ...params,
308
+ bus: { ...bus, connections: continuationConnections },
309
+ terminals: continuationConnections.map((connection, i) => ({
310
+ connection,
311
+ viaPoint: connection.sourcePoint,
312
+ exitPoint: terminals[i]!.exitPoint,
313
+ })),
314
+ acceptedPlans: [...params.acceptedPlans, ...prefixes],
315
+ reservedVias: [
316
+ ...sourceEscapes.map((source) => ({
317
+ connectionName: source.connectionName,
318
+ via: source.via,
319
+ })),
320
+ ...prefixes.flatMap((plan) =>
321
+ plan.additionalVias!.map((via) => ({
322
+ connectionName: plan.connectionName,
323
+ via,
324
+ })),
325
+ ),
326
+ ],
327
+ allowSourceLayerRouting: true,
328
+ sourceEscapePaths: undefined,
329
+ gridStep: (pitch * 3) / 8,
330
+ gridStepDivisor: 2 as const,
331
+ alignGridToPads: true,
332
+ maximumRouteOrderAttempts: 6,
333
+ routeOrder: terminals
334
+ .map((_, i) => i)
335
+ .sort((a, b) => terminals[a]!.exitPoint.y - terminals[b]!.exitPoint.y),
336
+ }
337
+ const continuations = yield* routeViaMinimalWindingAlternativesSteps(
338
+ tailParams,
339
+ 1,
340
+ false,
341
+ )
342
+ if (!continuations.length) return null
343
+ const plans = stage.map((original) => {
344
+ const prefix = prefixByIndex.get(original.connectionIndex)!,
345
+ tail = continuations[0]!.find(
346
+ (plan) => plan.connectionIndex === original.connectionIndex,
347
+ )!,
348
+ [first, second] = prefix.additionalVias!
349
+ const wire = (point: Point2D, layer: string) => ({
350
+ route_type: "wire" as const,
351
+ ...point,
352
+ width: w,
353
+ layer,
354
+ })
355
+ const viaPoint = (via: RoutedVia) => ({
356
+ route_type: "via" as const,
357
+ ...via.center,
358
+ from_layer: via.fromLayer,
359
+ to_layer: via.toLayer,
360
+ via_diameter: via.diameter,
361
+ via_hole_diameter: via.holeDiameter,
362
+ })
363
+ const segments = [...prefix.segments, ...tail.segments]
364
+ return {
365
+ ...prefix,
366
+ segments,
367
+ trace: {
368
+ ...original.trace,
369
+ route: [
370
+ ...original.trace.route,
371
+ wire(first!.center, targetLayer),
372
+ viaPoint(first!),
373
+ wire(first!.center, crossoverLayer),
374
+ wire(second!.center, crossoverLayer),
375
+ viaPoint(second!),
376
+ ...tail.trace.route,
377
+ ],
378
+ },
379
+ length: segments.reduce((sum, s) => sum + distance(s.start, s.end), 0),
380
+ }
381
+ })
382
+ if (
383
+ !fanoutPlansAreClear({
384
+ ...params,
385
+ plans: [...params.acceptedPlans, ...plans],
386
+ sharedBoundary: bus.sharedBoundary,
387
+ })
388
+ )
389
+ return null
390
+ for (const plan of plans)
391
+ for (const source of sourceEscapes) {
392
+ if (plan.connectionIndex === source.connectionIndex) continue
393
+ for (const s of plan.segments) {
394
+ if (
395
+ source.via.spanLayers.includes(s.layer) &&
396
+ distancePointToSegment(source.via.center, s.start, s.end) <
397
+ source.via.diameter / 2 + w / 2 + c - 1e-7
398
+ )
399
+ return null
400
+ for (const other of source.segments)
401
+ if (
402
+ other.layer === s.layer &&
403
+ distanceSegmentToSegment(s.start, s.end, other.start, other.end) <
404
+ w + c - 1e-7
405
+ )
406
+ return null
407
+ }
408
+ for (const added of plan.additionalVias!) {
409
+ if (
410
+ distance(added.center, source.via.center) <
411
+ params.viaDiameter + c - 1e-7
412
+ )
413
+ return null
414
+ for (const s of source.segments)
415
+ if (
416
+ added.spanLayers.includes(s.layer) &&
417
+ distancePointToSegment(added.center, s.start, s.end) <
418
+ params.viaDiameter / 2 + w / 2 + c - 1e-7
419
+ )
420
+ return null
421
+ }
422
+ }
423
+ // The caller length-matches all complete buses before final validation.
424
+ return plans
425
+ }