@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,167 @@
1
+ import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
+ import { getCornerBandSide } from "./boundary-exit"
3
+ import { matchBusPlanLengths } from "./match-bus-lengths"
4
+ import type { RouteBusParams } from "./route-bus"
5
+ import {
6
+ routeViaMinimalWindingAlternativesSteps,
7
+ type RouteViaMinimalWindingProgress,
8
+ } from "./route-via-minimal-winding"
9
+ import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
10
+
11
+ interface Params
12
+ extends Omit<RouteBusParams, "bus" | "targetLayer" | "acceptedPlans"> {
13
+ inputSrj: SimpleRouteJson
14
+ sharedBoundary: Bounds
15
+ preparedBuses: readonly PreparedBus[]
16
+ plans: readonly FanoutRoutePlan[]
17
+ }
18
+
19
+ /** Move a pair within its boundary band when its first route leaves no tuning room. */
20
+ export function* repairPeripheralBusLengthsSteps(
21
+ params: Params,
22
+ ): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
23
+ const match = (
24
+ plans: readonly FanoutRoutePlan[],
25
+ buses = params.preparedBuses,
26
+ ) =>
27
+ matchBusPlanLengths({
28
+ plans,
29
+ preparedBuses: buses,
30
+ inputSrj: params.inputSrj,
31
+ sharedBoundary: params.sharedBoundary,
32
+ clearance: params.clearance,
33
+ allowBlindAndBuriedVias: params.allowBlindAndBuriedVias,
34
+ allowSameNetMerges: params.allowSameNetMerges,
35
+ })
36
+ const skew = (plans: readonly FanoutRoutePlan[]) =>
37
+ Math.max(...plans.map((plan) => plan.length)) -
38
+ Math.min(...plans.map((plan) => plan.length))
39
+ const repaired = new Set<string>()
40
+ let current = [...params.plans]
41
+ while (repaired.size < 3) {
42
+ const matched = match(current)
43
+ if (matched.plans) return matched.plans
44
+ const bus = matched.failedBus
45
+ if (
46
+ repaired.has(bus.busId) ||
47
+ bus.connections.length !== 2 ||
48
+ bus.termination.type !== "boundary" ||
49
+ !bus.exitEdge
50
+ )
51
+ return null
52
+ repaired.add(bus.busId)
53
+ const own = current.filter((plan) => plan.busId === bus.busId)
54
+ if (
55
+ own.length !== 2 ||
56
+ own.some((plan) => !plan.via || plan.additionalVias?.length) ||
57
+ own[0]!.targetLayer !== own[1]!.targetLayer
58
+ )
59
+ return null
60
+ const accepted = current.filter((plan) => plan.busId !== bus.busId)
61
+ const sourceEscapePaths = new Map(
62
+ own.map((plan) => [
63
+ plan.connectionIndex,
64
+ [
65
+ plan.sourcePoint,
66
+ ...plan.segments
67
+ .slice(0, plan.sourceEscapeSegmentCount ?? 1)
68
+ .map((segment) => segment.end),
69
+ ],
70
+ ]),
71
+ )
72
+ const horizontal = bus.exitEdge === "left" || bus.exitEdge === "right"
73
+ const axis = horizontal ? "y" : "x"
74
+ const minimum = horizontal
75
+ ? params.sharedBoundary.minY
76
+ : params.sharedBoundary.minX
77
+ const maximum = horizontal
78
+ ? params.sharedBoundary.maxY
79
+ : params.sharedBoundary.maxX
80
+ const middle = (minimum + maximum) / 2
81
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
82
+ const pitch = params.traceWidth + params.clearance
83
+ const initialSkew = skew(own)
84
+ const candidates: FanoutRoutePlan[][] = []
85
+ let replacement: FanoutRoutePlan[] | null = null
86
+ search: for (const multiplier of [0, 1, -1, 2, -2, 3, -3, 4, -4]) {
87
+ const offset = multiplier * pitch
88
+ if (
89
+ own.some((plan) => {
90
+ const track = plan.exitPoint[axis] + offset
91
+ return (
92
+ track < minimum + params.traceWidth / 2 ||
93
+ track > maximum - params.traceWidth / 2 ||
94
+ (side === "minimum" && track >= middle) ||
95
+ (side === "maximum" && track <= middle)
96
+ )
97
+ })
98
+ )
99
+ continue
100
+ for (const laneBias of [0, -1, 1] as const) {
101
+ for (const routeOrder of [
102
+ [0, 1],
103
+ [1, 0],
104
+ ]) {
105
+ const alternatives = yield* routeViaMinimalWindingAlternativesSteps(
106
+ {
107
+ ...params,
108
+ bus,
109
+ targetLayer: own[0]!.targetLayer,
110
+ acceptedPlans: accepted,
111
+ terminals: bus.connections.map((connection) => {
112
+ const original = own.find(
113
+ (plan) => plan.connectionIndex === connection.connectionIndex,
114
+ )!
115
+ return {
116
+ connection,
117
+ viaPoint: original.via!.center,
118
+ exitPoint: {
119
+ ...original.exitPoint,
120
+ [axis]: original.exitPoint[axis] + offset,
121
+ },
122
+ }
123
+ }),
124
+ sourceEscapePaths,
125
+ reservedVias: undefined,
126
+ gridStep: pitch / 2,
127
+ gridStepDivisor: 2,
128
+ alignGridToPads: true,
129
+ maximumRouteOrderAttempts: 1,
130
+ routeOrder,
131
+ laneBias,
132
+ },
133
+ 1,
134
+ false,
135
+ )
136
+ if (!alternatives.length || skew(alternatives[0]!) >= initialSkew)
137
+ continue
138
+ const candidate = alternatives[0]!.map((plan) => ({
139
+ ...plan,
140
+ cornerBandSide: side,
141
+ }))
142
+ candidates.push(candidate)
143
+ // A small remaining deficit is inexpensive to tune. Larger deficits
144
+ // are attempted below after collecting the best available geometry.
145
+ if (skew(candidate) > 2 * bus.maxLengthSkew!) continue
146
+ const tuned = match([...accepted, ...candidate], [bus])
147
+ if (!tuned.plans) continue
148
+ replacement = tuned.plans
149
+ break search
150
+ }
151
+ }
152
+ }
153
+ if (!replacement) {
154
+ for (const candidate of candidates
155
+ .toSorted((a, b) => skew(a) - skew(b))
156
+ .slice(0, 3)) {
157
+ const tuned = match([...accepted, ...candidate], [bus])
158
+ if (!tuned.plans) continue
159
+ replacement = tuned.plans
160
+ break
161
+ }
162
+ }
163
+ if (!replacement) return null
164
+ current = replacement
165
+ }
166
+ return match(current).plans
167
+ }
@@ -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
+ }