@tscircuit/fanout-solver 0.0.47 → 0.0.49
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 +847 -177
- package/lib/route-bus.ts +35 -4
- package/lib/route-single-layer-adaptive-exits.ts +323 -16
- package/lib/route-via-minimal-winding.ts +253 -8
- package/package.json +15 -2
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
6
7
|
import { getCornerBandSide, getDirectionForExitEdge } from "./boundary-exit"
|
|
7
8
|
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
8
9
|
import {
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
const EPSILON = 1e-7
|
|
30
31
|
const MAX_GRID_NODE_COUNT = 120_000
|
|
31
32
|
const MAX_EXPANDED_STATE_COUNT = 240_000
|
|
33
|
+
const EXPANDED_STATES_PER_STEP = 5_000
|
|
32
34
|
const MAX_CONNECTOR_COUNT = 24
|
|
33
35
|
const CONNECTOR_RADIUS_IN_STEPS = 3.25
|
|
34
36
|
|
|
@@ -64,6 +66,18 @@ export interface RouteViaMinimalWindingParams {
|
|
|
64
66
|
preferTargetDirectedLaneBias?: boolean
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
export interface RouteViaMinimalWindingProgress {
|
|
70
|
+
phase: "route-connection"
|
|
71
|
+
routeOrderAttempt: number
|
|
72
|
+
connectionIndex: number
|
|
73
|
+
connectionCount: number
|
|
74
|
+
connectionName: string
|
|
75
|
+
searchBatch: number
|
|
76
|
+
expandedStateCount: number
|
|
77
|
+
connectionComplete: boolean
|
|
78
|
+
visualization?: GraphicsObject
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
interface GridNode {
|
|
68
82
|
point: Point2D
|
|
69
83
|
column: number
|
|
@@ -251,6 +265,12 @@ class MinHeap {
|
|
|
251
265
|
return this.values.length
|
|
252
266
|
}
|
|
253
267
|
|
|
268
|
+
sampleEntries(maximumCount: number): readonly HeapEntry[] {
|
|
269
|
+
if (this.values.length <= maximumCount) return this.values
|
|
270
|
+
const stride = Math.ceil(this.values.length / maximumCount)
|
|
271
|
+
return this.values.filter((_, index) => index % stride === 0)
|
|
272
|
+
}
|
|
273
|
+
|
|
254
274
|
push(value: HeapEntry): void {
|
|
255
275
|
this.values.push(value)
|
|
256
276
|
let index = this.values.length - 1
|
|
@@ -577,10 +597,11 @@ function buildPlan(params: {
|
|
|
577
597
|
}
|
|
578
598
|
}
|
|
579
599
|
|
|
580
|
-
export function
|
|
600
|
+
export function* routeViaMinimalWindingAlternativesSteps(
|
|
581
601
|
params: RouteViaMinimalWindingParams,
|
|
582
602
|
maximumAlternatives = 1,
|
|
583
|
-
|
|
603
|
+
includeVisualization = false,
|
|
604
|
+
): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[][], void> {
|
|
584
605
|
if (!Number.isInteger(maximumAlternatives) || maximumAlternatives < 1) {
|
|
585
606
|
throw new Error(
|
|
586
607
|
`FanoutSolver: maximum winding alternatives must be a positive integer, received ${maximumAlternatives}`,
|
|
@@ -647,6 +668,14 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
647
668
|
point: { x: minX + column * gridStep, y: minY + row * gridStep },
|
|
648
669
|
}
|
|
649
670
|
})
|
|
671
|
+
const sampledGridPoints = includeVisualization
|
|
672
|
+
? nodes
|
|
673
|
+
.filter(
|
|
674
|
+
(_, index) =>
|
|
675
|
+
index % Math.max(1, Math.ceil(nodes.length / 1_000)) === 0,
|
|
676
|
+
)
|
|
677
|
+
.map((node) => node.point)
|
|
678
|
+
: []
|
|
650
679
|
const targetLayerObstacles = srj.obstacles.filter((obstacle) =>
|
|
651
680
|
obstacle.layers.includes(targetLayer),
|
|
652
681
|
)
|
|
@@ -861,11 +890,129 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
861
890
|
.slice(0, MAX_CONNECTOR_COUNT)
|
|
862
891
|
}
|
|
863
892
|
|
|
864
|
-
const
|
|
893
|
+
const visualizeSearch = (params: {
|
|
894
|
+
terminal: ViaMinimalWindingTerminal
|
|
895
|
+
acceptedAttemptSegments: readonly BlockingSegment[]
|
|
896
|
+
expandedPoints: readonly Point2D[]
|
|
897
|
+
frontierPoints: readonly Point2D[]
|
|
898
|
+
bestPath: readonly Point2D[] | null
|
|
899
|
+
expandedStateCount: number
|
|
900
|
+
searchBatch: number
|
|
901
|
+
connectionComplete: boolean
|
|
902
|
+
}): GraphicsObject => {
|
|
903
|
+
const {
|
|
904
|
+
terminal,
|
|
905
|
+
acceptedAttemptSegments,
|
|
906
|
+
expandedPoints,
|
|
907
|
+
frontierPoints,
|
|
908
|
+
bestPath,
|
|
909
|
+
expandedStateCount,
|
|
910
|
+
searchBatch,
|
|
911
|
+
connectionComplete,
|
|
912
|
+
} = params
|
|
913
|
+
return {
|
|
914
|
+
title: `Winding ${bus.busId}: ${terminal.connection.connection.name}`,
|
|
915
|
+
rects: [
|
|
916
|
+
{
|
|
917
|
+
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
|
|
918
|
+
width: maxX - minX,
|
|
919
|
+
height: maxY - minY,
|
|
920
|
+
fill: "rgba(0, 0, 0, 0)",
|
|
921
|
+
stroke: "rgba(14, 165, 233, 0.9)",
|
|
922
|
+
label: "A* search boundary",
|
|
923
|
+
},
|
|
924
|
+
],
|
|
925
|
+
points: [
|
|
926
|
+
...sampledGridPoints.map((point) => ({
|
|
927
|
+
...point,
|
|
928
|
+
color: "rgba(148, 163, 184, 0.22)",
|
|
929
|
+
})),
|
|
930
|
+
...frontierPoints.map((point) => ({
|
|
931
|
+
...point,
|
|
932
|
+
color: "rgba(6, 182, 212, 0.75)",
|
|
933
|
+
label: "open frontier",
|
|
934
|
+
})),
|
|
935
|
+
...expandedPoints.map((point) => ({
|
|
936
|
+
...point,
|
|
937
|
+
color: "rgba(244, 63, 94, 0.8)",
|
|
938
|
+
label: "expanded in current step",
|
|
939
|
+
})),
|
|
940
|
+
{
|
|
941
|
+
...terminal.connection.sourcePoint,
|
|
942
|
+
color: "#f97316",
|
|
943
|
+
label: `source: ${terminal.connection.connection.name}`,
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
...terminal.exitPoint,
|
|
947
|
+
color: "#a855f7",
|
|
948
|
+
label: "target exit",
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
circles: terminals.map((candidate) => ({
|
|
952
|
+
center: candidate.viaPoint,
|
|
953
|
+
radius: viaDiameter / 2,
|
|
954
|
+
fill:
|
|
955
|
+
candidate === terminal
|
|
956
|
+
? "rgba(250, 204, 21, 0.85)"
|
|
957
|
+
: "rgba(250, 204, 21, 0.25)",
|
|
958
|
+
stroke: candidate === terminal ? "#ca8a04" : "#a16207",
|
|
959
|
+
label:
|
|
960
|
+
candidate === terminal
|
|
961
|
+
? "active via"
|
|
962
|
+
: `reserved via: ${candidate.connection.connection.name}`,
|
|
963
|
+
})),
|
|
964
|
+
lines: [
|
|
965
|
+
...acceptedAttemptSegments.map(({ segment, connectionName }) => ({
|
|
966
|
+
points: [segment.start, segment.end],
|
|
967
|
+
strokeColor: "rgba(34, 197, 94, 0.9)",
|
|
968
|
+
strokeWidth: Math.max(traceWidth, gridStep * 0.35),
|
|
969
|
+
label: `accepted: ${connectionName}`,
|
|
970
|
+
})),
|
|
971
|
+
{
|
|
972
|
+
points: [terminal.viaPoint, terminal.exitPoint],
|
|
973
|
+
strokeColor: "rgba(168, 85, 247, 0.45)",
|
|
974
|
+
strokeWidth: Math.max(traceWidth * 0.5, gridStep * 0.15),
|
|
975
|
+
strokeDash: [gridStep, gridStep],
|
|
976
|
+
label: "active via-to-exit search",
|
|
977
|
+
},
|
|
978
|
+
...(bestPath
|
|
979
|
+
? [
|
|
980
|
+
{
|
|
981
|
+
points: [...bestPath],
|
|
982
|
+
strokeColor: "#facc15",
|
|
983
|
+
strokeWidth: Math.max(traceWidth, gridStep * 0.45),
|
|
984
|
+
label: "best path so far",
|
|
985
|
+
},
|
|
986
|
+
]
|
|
987
|
+
: []),
|
|
988
|
+
],
|
|
989
|
+
texts: [
|
|
990
|
+
{
|
|
991
|
+
x: minX,
|
|
992
|
+
y: maxY + gridStep * 2,
|
|
993
|
+
text: connectionComplete
|
|
994
|
+
? `connection complete · ${expandedStateCount.toLocaleString()} states`
|
|
995
|
+
: `A* batch ${searchBatch} · ${expandedStateCount.toLocaleString()} states`,
|
|
996
|
+
color: "#0f172a",
|
|
997
|
+
fontSize: Math.max(gridStep * 2.5, 0.5),
|
|
998
|
+
anchorSide: "bottom_left",
|
|
999
|
+
},
|
|
1000
|
+
],
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const routeOneSteps = function* (params: {
|
|
865
1005
|
terminal: ViaMinimalWindingTerminal
|
|
866
1006
|
acceptedAttemptSegments: BlockingSegment[]
|
|
867
1007
|
laneBias: -1 | 0 | 1
|
|
868
|
-
}):
|
|
1008
|
+
}): Generator<
|
|
1009
|
+
{
|
|
1010
|
+
expandedStateCount: number
|
|
1011
|
+
visualization?: GraphicsObject
|
|
1012
|
+
},
|
|
1013
|
+
{ points: Point2D[] | null; expandedStateCount: number },
|
|
1014
|
+
void
|
|
1015
|
+
> {
|
|
869
1016
|
const { terminal, acceptedAttemptSegments, laneBias } = params
|
|
870
1017
|
const starts = connectorCandidates({
|
|
871
1018
|
terminal,
|
|
@@ -877,7 +1024,9 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
877
1024
|
endpoint: terminal.exitPoint,
|
|
878
1025
|
acceptedAttemptSegments,
|
|
879
1026
|
})
|
|
880
|
-
if (starts.length === 0 || ends.length === 0)
|
|
1027
|
+
if (starts.length === 0 || ends.length === 0) {
|
|
1028
|
+
return { points: null, expandedStateCount: 0 }
|
|
1029
|
+
}
|
|
881
1030
|
const endByNode = new Map<number, ConnectorCandidate[]>()
|
|
882
1031
|
for (const end of ends) {
|
|
883
1032
|
const values = endByNode.get(end.nodeIndex) ?? []
|
|
@@ -927,6 +1076,9 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
927
1076
|
let bestGoalCost = Number.POSITIVE_INFINITY
|
|
928
1077
|
let bestGoalPoints: Point2D[] | null = null
|
|
929
1078
|
let expandedStateCount = 0
|
|
1079
|
+
let expandedStatesSinceYield = 0
|
|
1080
|
+
let searchBatch = 0
|
|
1081
|
+
let expandedBatchPoints: Point2D[] = []
|
|
930
1082
|
while (heap.size > 0 && expandedStateCount < MAX_EXPANDED_STATE_COUNT) {
|
|
931
1083
|
const current = heap.pop()!
|
|
932
1084
|
if (current.score >= bestGoalCost - EPSILON) break
|
|
@@ -938,6 +1090,10 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
938
1090
|
)
|
|
939
1091
|
continue
|
|
940
1092
|
expandedStateCount++
|
|
1093
|
+
expandedStatesSinceYield++
|
|
1094
|
+
if (includeVisualization && expandedStatesSinceYield % 50 === 0) {
|
|
1095
|
+
expandedBatchPoints.push(nodes[current.node]!.point)
|
|
1096
|
+
}
|
|
941
1097
|
const endConnectors = endByNode.get(current.node)
|
|
942
1098
|
if (endConnectors) {
|
|
943
1099
|
const gridPoints: Point2D[] = []
|
|
@@ -1054,8 +1210,32 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1054
1210
|
score: nextDistance + remaining,
|
|
1055
1211
|
})
|
|
1056
1212
|
}
|
|
1213
|
+
if (expandedStatesSinceYield >= EXPANDED_STATES_PER_STEP) {
|
|
1214
|
+
expandedStatesSinceYield = 0
|
|
1215
|
+
searchBatch++
|
|
1216
|
+
yield {
|
|
1217
|
+
expandedStateCount,
|
|
1218
|
+
...(includeVisualization
|
|
1219
|
+
? {
|
|
1220
|
+
visualization: visualizeSearch({
|
|
1221
|
+
terminal,
|
|
1222
|
+
acceptedAttemptSegments,
|
|
1223
|
+
expandedPoints: expandedBatchPoints,
|
|
1224
|
+
frontierPoints: heap
|
|
1225
|
+
.sampleEntries(120)
|
|
1226
|
+
.map((entry) => nodes[entry.node]!.point),
|
|
1227
|
+
bestPath: bestGoalPoints,
|
|
1228
|
+
expandedStateCount,
|
|
1229
|
+
searchBatch,
|
|
1230
|
+
connectionComplete: false,
|
|
1231
|
+
}),
|
|
1232
|
+
}
|
|
1233
|
+
: {}),
|
|
1234
|
+
}
|
|
1235
|
+
expandedBatchPoints = []
|
|
1236
|
+
}
|
|
1057
1237
|
}
|
|
1058
|
-
return bestGoalPoints
|
|
1238
|
+
return { points: bestGoalPoints, expandedStateCount }
|
|
1059
1239
|
}
|
|
1060
1240
|
|
|
1061
1241
|
const targetOrderedTerminals = terminals.toSorted((first, second) => {
|
|
@@ -1176,12 +1356,64 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1176
1356
|
const acceptedAttemptSegments: BlockingSegment[] = []
|
|
1177
1357
|
const routedPointsByConnectionName = new Map<string, Point2D[]>()
|
|
1178
1358
|
let failed = false
|
|
1179
|
-
for (
|
|
1180
|
-
|
|
1359
|
+
for (
|
|
1360
|
+
let terminalIndex = 0;
|
|
1361
|
+
terminalIndex < routeOrder.length;
|
|
1362
|
+
terminalIndex++
|
|
1363
|
+
) {
|
|
1364
|
+
const terminal = routeOrder[terminalIndex]!
|
|
1365
|
+
const connectionSteps = routeOneSteps({
|
|
1181
1366
|
terminal,
|
|
1182
1367
|
acceptedAttemptSegments,
|
|
1183
1368
|
laneBias,
|
|
1184
1369
|
})
|
|
1370
|
+
let connectionResult = connectionSteps.next()
|
|
1371
|
+
let searchBatch = 0
|
|
1372
|
+
let expandedStateCount = 0
|
|
1373
|
+
while (!connectionResult.done) {
|
|
1374
|
+
expandedStateCount = connectionResult.value.expandedStateCount
|
|
1375
|
+
yield {
|
|
1376
|
+
phase: "route-connection",
|
|
1377
|
+
routeOrderAttempt: routeOrderAttemptCount,
|
|
1378
|
+
connectionIndex: terminalIndex,
|
|
1379
|
+
connectionCount: routeOrder.length,
|
|
1380
|
+
connectionName: terminal.connection.connection.name,
|
|
1381
|
+
searchBatch: ++searchBatch,
|
|
1382
|
+
expandedStateCount,
|
|
1383
|
+
connectionComplete: false,
|
|
1384
|
+
...(connectionResult.value.visualization
|
|
1385
|
+
? { visualization: connectionResult.value.visualization }
|
|
1386
|
+
: {}),
|
|
1387
|
+
}
|
|
1388
|
+
connectionResult = connectionSteps.next()
|
|
1389
|
+
}
|
|
1390
|
+
const { points, expandedStateCount: finalExpandedStateCount } =
|
|
1391
|
+
connectionResult.value
|
|
1392
|
+
expandedStateCount = finalExpandedStateCount
|
|
1393
|
+
yield {
|
|
1394
|
+
phase: "route-connection",
|
|
1395
|
+
routeOrderAttempt: routeOrderAttemptCount,
|
|
1396
|
+
connectionIndex: terminalIndex,
|
|
1397
|
+
connectionCount: routeOrder.length,
|
|
1398
|
+
connectionName: terminal.connection.connection.name,
|
|
1399
|
+
searchBatch,
|
|
1400
|
+
expandedStateCount,
|
|
1401
|
+
connectionComplete: true,
|
|
1402
|
+
...(includeVisualization
|
|
1403
|
+
? {
|
|
1404
|
+
visualization: visualizeSearch({
|
|
1405
|
+
terminal,
|
|
1406
|
+
acceptedAttemptSegments,
|
|
1407
|
+
expandedPoints: [],
|
|
1408
|
+
frontierPoints: [],
|
|
1409
|
+
bestPath: points,
|
|
1410
|
+
expandedStateCount,
|
|
1411
|
+
searchBatch,
|
|
1412
|
+
connectionComplete: true,
|
|
1413
|
+
}),
|
|
1414
|
+
}
|
|
1415
|
+
: {}),
|
|
1416
|
+
}
|
|
1185
1417
|
if (!points) {
|
|
1186
1418
|
failed = true
|
|
1187
1419
|
break
|
|
@@ -1238,6 +1470,19 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1238
1470
|
return alternatives
|
|
1239
1471
|
}
|
|
1240
1472
|
|
|
1473
|
+
export function routeViaMinimalWindingAlternatives(
|
|
1474
|
+
params: RouteViaMinimalWindingParams,
|
|
1475
|
+
maximumAlternatives = 1,
|
|
1476
|
+
): FanoutRoutePlan[][] {
|
|
1477
|
+
const steps = routeViaMinimalWindingAlternativesSteps(
|
|
1478
|
+
params,
|
|
1479
|
+
maximumAlternatives,
|
|
1480
|
+
)
|
|
1481
|
+
let result = steps.next()
|
|
1482
|
+
while (!result.done) result = steps.next()
|
|
1483
|
+
return result.value
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1241
1486
|
export function routeViaMinimalWinding(
|
|
1242
1487
|
params: RouteViaMinimalWindingParams,
|
|
1243
1488
|
): FanoutRoutePlan[] | null {
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fanout-solver",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
|
|
5
5
|
"module": "lib/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
8
|
"lib"
|
|
9
9
|
],
|
|
10
|
+
"workspaces": [
|
|
11
|
+
"scripts/generate-repro"
|
|
12
|
+
],
|
|
10
13
|
"license": "MIT",
|
|
11
14
|
"repository": {
|
|
12
15
|
"type": "git",
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
"typecheck": "bunx tsc --noEmit",
|
|
29
32
|
"test": "bun test",
|
|
30
33
|
"benchmark": "bun benchmarks/run-benchmark.ts",
|
|
34
|
+
"generate:repro04": "bun scripts/generate-repro/generate-repro04.tsx",
|
|
31
35
|
"benchmark:srj29": "./benchmark.sh",
|
|
32
36
|
"render:dataset": "bun scripts/render-dataset-pngs.ts",
|
|
33
37
|
"format": "biome format --write .",
|
|
@@ -44,7 +48,12 @@
|
|
|
44
48
|
"@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#bfdf7bc",
|
|
45
49
|
"@tscircuit/alphabet": "0.0.25",
|
|
46
50
|
"@tscircuit/circuit-json-util": "0.0.97",
|
|
51
|
+
"@tscircuit/copper-pour-solver": "0.0.51",
|
|
47
52
|
"@tscircuit/footprinter": "0.0.394",
|
|
53
|
+
"@tscircuit/infer-cable-insertion-point": "0.0.4",
|
|
54
|
+
"@tscircuit/miniflex": "0.0.4",
|
|
55
|
+
"@tscircuit/schematic-trace-solver": "0.0.175",
|
|
56
|
+
"@tscircuit/soup-util": "0.0.41",
|
|
48
57
|
"@types/bun": "1.3.14",
|
|
49
58
|
"@types/react": "19.2.14",
|
|
50
59
|
"@types/react-dom": "19.2.3",
|
|
@@ -52,12 +61,16 @@
|
|
|
52
61
|
"circuit-json": "0.0.455",
|
|
53
62
|
"circuit-json-to-connectivity-map": "0.0.23",
|
|
54
63
|
"circuit-to-svg": "0.0.364",
|
|
55
|
-
"
|
|
64
|
+
"connectivity-map": "1.0.0",
|
|
65
|
+
"flatbush": "4.5.0",
|
|
66
|
+
"format-si-unit": "0.0.12",
|
|
67
|
+
"minicssgrid": "0.0.9",
|
|
56
68
|
"react": "19.2.8",
|
|
57
69
|
"react-cosmos": "7.3.0",
|
|
58
70
|
"react-cosmos-plugin-vite": "7.3.0",
|
|
59
71
|
"react-dom": "19.2.8",
|
|
60
72
|
"schematic-symbols": "0.0.231",
|
|
73
|
+
"spicey": "0.0.14",
|
|
61
74
|
"vite": "8.1.5"
|
|
62
75
|
},
|
|
63
76
|
"peerDependencies": {
|