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