@tscircuit/fanout-solver 0.0.17 → 0.0.19
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/README.md +41 -22
- package/lib/fanout-solver.ts +583 -82
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/layer-names.ts +38 -12
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +305 -71
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
package/lib/geometry.ts
CHANGED
|
@@ -3,12 +3,27 @@ import type { Point2D, RoutedSegment } from "./types"
|
|
|
3
3
|
|
|
4
4
|
const EPSILON = 1e-9
|
|
5
5
|
|
|
6
|
-
type ShapeAwareObstacle = Obstacle & {
|
|
6
|
+
type ShapeAwareObstacle = Obstacle & {
|
|
7
|
+
shape?: "circle"
|
|
8
|
+
ccwRotationDegrees?: number
|
|
9
|
+
}
|
|
7
10
|
|
|
8
11
|
function obstacleIsCircular(obstacle: Obstacle): boolean {
|
|
9
12
|
return (obstacle as ShapeAwareObstacle).shape === "circle"
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
function toObstacleLocalPoint(point: Point2D, obstacle: Obstacle): Point2D {
|
|
16
|
+
const rotationRadians =
|
|
17
|
+
(-((obstacle as ShapeAwareObstacle).ccwRotationDegrees ?? 0) * Math.PI) /
|
|
18
|
+
180
|
|
19
|
+
const dx = point.x - obstacle.center.x
|
|
20
|
+
const dy = point.y - obstacle.center.y
|
|
21
|
+
return {
|
|
22
|
+
x: dx * Math.cos(rotationRadians) - dy * Math.sin(rotationRadians),
|
|
23
|
+
y: dx * Math.sin(rotationRadians) + dy * Math.cos(rotationRadians),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
export function distance(a: Point2D, b: Point2D): number {
|
|
13
28
|
return Math.hypot(a.x - b.x, a.y - b.y)
|
|
14
29
|
}
|
|
@@ -76,9 +91,10 @@ export function pointIsInsideObstacle(
|
|
|
76
91
|
if (obstacleIsCircular(obstacle)) {
|
|
77
92
|
return distance(point, obstacle.center) <= obstacle.width / 2 + tolerance
|
|
78
93
|
}
|
|
94
|
+
const localPoint = toObstacleLocalPoint(point, obstacle)
|
|
79
95
|
return (
|
|
80
|
-
Math.abs(
|
|
81
|
-
Math.abs(
|
|
96
|
+
Math.abs(localPoint.x) <= obstacle.width / 2 + tolerance &&
|
|
97
|
+
Math.abs(localPoint.y) <= obstacle.height / 2 + tolerance
|
|
82
98
|
)
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -89,14 +105,9 @@ export function distancePointToObstacle(
|
|
|
89
105
|
if (obstacleIsCircular(obstacle)) {
|
|
90
106
|
return Math.max(0, distance(point, obstacle.center) - obstacle.width / 2)
|
|
91
107
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
const dy = Math.max(
|
|
97
|
-
Math.abs(point.y - obstacle.center.y) - obstacle.height / 2,
|
|
98
|
-
0,
|
|
99
|
-
)
|
|
108
|
+
const localPoint = toObstacleLocalPoint(point, obstacle)
|
|
109
|
+
const dx = Math.max(Math.abs(localPoint.x) - obstacle.width / 2, 0)
|
|
110
|
+
const dy = Math.max(Math.abs(localPoint.y) - obstacle.height / 2, 0)
|
|
100
111
|
return Math.hypot(dx, dy)
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -111,16 +122,24 @@ export function distanceSegmentToObstacle(
|
|
|
111
122
|
obstacle.width / 2,
|
|
112
123
|
)
|
|
113
124
|
}
|
|
125
|
+
const localStart = toObstacleLocalPoint(segment.start, obstacle)
|
|
126
|
+
const localEnd = toObstacleLocalPoint(segment.end, obstacle)
|
|
127
|
+
if (
|
|
128
|
+
Math.abs(localStart.x) <= obstacle.width / 2 + EPSILON &&
|
|
129
|
+
Math.abs(localStart.y) <= obstacle.height / 2 + EPSILON
|
|
130
|
+
) {
|
|
131
|
+
return 0
|
|
132
|
+
}
|
|
114
133
|
if (
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
Math.abs(localEnd.x) <= obstacle.width / 2 + EPSILON &&
|
|
135
|
+
Math.abs(localEnd.y) <= obstacle.height / 2 + EPSILON
|
|
117
136
|
) {
|
|
118
137
|
return 0
|
|
119
138
|
}
|
|
120
|
-
const minX =
|
|
121
|
-
const maxX = obstacle.
|
|
122
|
-
const minY =
|
|
123
|
-
const maxY = obstacle.
|
|
139
|
+
const minX = -obstacle.width / 2
|
|
140
|
+
const maxX = obstacle.width / 2
|
|
141
|
+
const minY = -obstacle.height / 2
|
|
142
|
+
const maxY = obstacle.height / 2
|
|
124
143
|
const corners = [
|
|
125
144
|
{ x: minX, y: minY },
|
|
126
145
|
{ x: maxX, y: minY },
|
|
@@ -133,8 +152,8 @@ export function distanceSegmentToObstacle(
|
|
|
133
152
|
minimumDistance = Math.min(
|
|
134
153
|
minimumDistance,
|
|
135
154
|
distanceSegmentToSegment(
|
|
136
|
-
|
|
137
|
-
|
|
155
|
+
localStart,
|
|
156
|
+
localEnd,
|
|
138
157
|
corners[index]!,
|
|
139
158
|
corners[(index + 1) % corners.length]!,
|
|
140
159
|
),
|
package/lib/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { FanoutSolver } from "./fanout-solver"
|
|
2
2
|
export { getCopperLayerColor } from "./layer-colors"
|
|
3
3
|
export { getCopperLayerNames } from "./layer-names"
|
|
4
|
+
export { validateFanoutSolution } from "./validate-fanout-solution"
|
|
4
5
|
export type {
|
|
5
6
|
Bounds,
|
|
6
7
|
FanoutAttemptSummary,
|
|
@@ -15,6 +16,10 @@ export type {
|
|
|
15
16
|
FanoutDirection,
|
|
16
17
|
FanoutEdge,
|
|
17
18
|
FanoutPlaneTermination,
|
|
19
|
+
FanoutRoutePlan,
|
|
18
20
|
FanoutSolverOptions,
|
|
19
21
|
FanoutSolverOutput,
|
|
22
|
+
FanoutValidationIssue,
|
|
23
|
+
FanoutValidationReport,
|
|
24
|
+
PreparedBus,
|
|
20
25
|
} from "./types"
|
package/lib/layer-names.ts
CHANGED
|
@@ -37,9 +37,10 @@ export function getLayerSpan(
|
|
|
37
37
|
export function generateLayerAssignments(params: {
|
|
38
38
|
busIds: string[]
|
|
39
39
|
layers: string[]
|
|
40
|
+
layersByBusId?: Readonly<Record<string, readonly string[]>>
|
|
40
41
|
maxAssignments: number
|
|
41
42
|
}): Array<Readonly<Record<string, string>>> {
|
|
42
|
-
const { busIds, layers, maxAssignments } = params
|
|
43
|
+
const { busIds, layers, layersByBusId, maxAssignments } = params
|
|
43
44
|
if (layers.length === 0) {
|
|
44
45
|
throw new Error("FanoutSolver: no escape layers are available")
|
|
45
46
|
}
|
|
@@ -49,7 +50,22 @@ export function generateLayerAssignments(params: {
|
|
|
49
50
|
)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const availableLayersByBus = busIds.map(
|
|
54
|
+
(busId) => layersByBusId?.[busId] ?? layers,
|
|
55
|
+
)
|
|
56
|
+
const busWithoutLayersIndex = availableLayersByBus.findIndex(
|
|
57
|
+
(availableLayers) => availableLayers.length === 0,
|
|
58
|
+
)
|
|
59
|
+
if (busWithoutLayersIndex >= 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`FanoutSolver: no escape layers are available for bus "${busIds[busWithoutLayersIndex]}"`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const rawCombinationCount = availableLayersByBus.reduce(
|
|
66
|
+
(count, availableLayers) => count * availableLayers.length,
|
|
67
|
+
1,
|
|
68
|
+
)
|
|
53
69
|
const combinationCount = Math.min(
|
|
54
70
|
maxAssignments,
|
|
55
71
|
Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments,
|
|
@@ -60,7 +76,8 @@ export function generateLayerAssignments(params: {
|
|
|
60
76
|
function addAssignment(layerIndexes: number[]): void {
|
|
61
77
|
const assignment: Record<string, string> = {}
|
|
62
78
|
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
63
|
-
assignment[busIds[busIndex]!] =
|
|
79
|
+
assignment[busIds[busIndex]!] =
|
|
80
|
+
availableLayersByBus[busIndex]![layerIndexes[busIndex]!]!
|
|
64
81
|
}
|
|
65
82
|
const key = JSON.stringify(assignment)
|
|
66
83
|
if (seenAssignments.has(key)) return
|
|
@@ -73,9 +90,10 @@ export function generateLayerAssignments(params: {
|
|
|
73
90
|
const layerIndexes: number[] = []
|
|
74
91
|
let remaining = ordinal
|
|
75
92
|
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
const layerCount = availableLayersByBus[busIndex]!.length
|
|
94
|
+
const digit = remaining % layerCount
|
|
95
|
+
remaining = Math.floor(remaining / layerCount)
|
|
96
|
+
layerIndexes.push((digit + busIndex) % layerCount)
|
|
79
97
|
}
|
|
80
98
|
addAssignment(layerIndexes)
|
|
81
99
|
}
|
|
@@ -90,17 +108,22 @@ export function generateLayerAssignments(params: {
|
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
const balancedLayerIndexes = busIds.map(
|
|
93
|
-
(_, busIndex) => busIndex %
|
|
111
|
+
(_, busIndex) => busIndex % availableLayersByBus[busIndex]!.length,
|
|
94
112
|
)
|
|
95
113
|
addAssignment(balancedLayerIndexes)
|
|
114
|
+
const maximumAvailableLayerCount = Math.max(
|
|
115
|
+
...availableLayersByBus.map((availableLayers) => availableLayers.length),
|
|
116
|
+
)
|
|
96
117
|
for (
|
|
97
118
|
let globalShift = 1;
|
|
98
|
-
globalShift <
|
|
119
|
+
globalShift < maximumAvailableLayerCount &&
|
|
120
|
+
assignments.length < combinationCount;
|
|
99
121
|
globalShift++
|
|
100
122
|
) {
|
|
101
123
|
addAssignment(
|
|
102
124
|
balancedLayerIndexes.map(
|
|
103
|
-
(layerIndex) =>
|
|
125
|
+
(layerIndex, busIndex) =>
|
|
126
|
+
(layerIndex + globalShift) % availableLayersByBus[busIndex]!.length,
|
|
104
127
|
),
|
|
105
128
|
)
|
|
106
129
|
}
|
|
@@ -111,12 +134,14 @@ export function generateLayerAssignments(params: {
|
|
|
111
134
|
) {
|
|
112
135
|
for (
|
|
113
136
|
let shift = 1;
|
|
114
|
-
shift <
|
|
137
|
+
shift < availableLayersByBus[busIndex]!.length &&
|
|
138
|
+
assignments.length < combinationCount;
|
|
115
139
|
shift++
|
|
116
140
|
) {
|
|
117
141
|
const layerIndexes = [...balancedLayerIndexes]
|
|
118
142
|
layerIndexes[busIndex] =
|
|
119
|
-
(balancedLayerIndexes[busIndex]! + shift) %
|
|
143
|
+
(balancedLayerIndexes[busIndex]! + shift) %
|
|
144
|
+
availableLayersByBus[busIndex]!.length
|
|
120
145
|
addAssignment(layerIndexes)
|
|
121
146
|
}
|
|
122
147
|
}
|
|
@@ -128,7 +153,8 @@ export function generateLayerAssignments(params: {
|
|
|
128
153
|
addAssignment(
|
|
129
154
|
busIds.map(
|
|
130
155
|
(_, busIndex) =>
|
|
131
|
-
mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
|
|
156
|
+
mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
|
|
157
|
+
availableLayersByBus[busIndex]!.length,
|
|
132
158
|
),
|
|
133
159
|
)
|
|
134
160
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Obstacle,
|
|
3
|
+
SimpleRouteConnection,
|
|
4
|
+
SimpleRouteJson,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
|
|
7
|
+
interface ElectricalNetIdentity {
|
|
8
|
+
connectionNetKeys: Map<string, string>
|
|
9
|
+
tokenNetKeys: Map<string, Set<string>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const identityCache = new WeakMap<SimpleRouteJson, ElectricalNetIdentity>()
|
|
13
|
+
|
|
14
|
+
export function getConnectionNetKey(connection: SimpleRouteConnection): string {
|
|
15
|
+
return (
|
|
16
|
+
connection.netConnectionName ??
|
|
17
|
+
connection.rootConnectionName ??
|
|
18
|
+
connection.name
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addTokenNet(
|
|
23
|
+
tokenNetKeys: Map<string, Set<string>>,
|
|
24
|
+
token: string | undefined,
|
|
25
|
+
netKey: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (!token) return false
|
|
28
|
+
const keys = tokenNetKeys.get(token) ?? new Set<string>()
|
|
29
|
+
const sizeBefore = keys.size
|
|
30
|
+
keys.add(netKey)
|
|
31
|
+
tokenNetKeys.set(token, keys)
|
|
32
|
+
return keys.size !== sizeBefore
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getKnownNetKeys(
|
|
36
|
+
tokenNetKeys: Map<string, Set<string>>,
|
|
37
|
+
tokens: readonly string[],
|
|
38
|
+
): Set<string> {
|
|
39
|
+
const keys = new Set<string>()
|
|
40
|
+
for (const token of tokens) {
|
|
41
|
+
for (const key of tokenNetKeys.get(token) ?? []) keys.add(key)
|
|
42
|
+
}
|
|
43
|
+
return keys
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createElectricalNetIdentity(
|
|
47
|
+
srj: SimpleRouteJson,
|
|
48
|
+
): ElectricalNetIdentity {
|
|
49
|
+
const connectionNetKeys = new Map<string, string>()
|
|
50
|
+
const tokenNetKeys = new Map<string, Set<string>>()
|
|
51
|
+
|
|
52
|
+
for (const connection of srj.connections) {
|
|
53
|
+
const netKey = getConnectionNetKey(connection)
|
|
54
|
+
connectionNetKeys.set(connection.name, netKey)
|
|
55
|
+
addTokenNet(tokenNetKeys, connection.name, netKey)
|
|
56
|
+
addTokenNet(tokenNetKeys, connection.rootConnectionName, netKey)
|
|
57
|
+
addTokenNet(tokenNetKeys, connection.netConnectionName, netKey)
|
|
58
|
+
for (const point of connection.pointsToConnect) {
|
|
59
|
+
addTokenNet(tokenNetKeys, point.pointId, netKey)
|
|
60
|
+
addTokenNet(tokenNetKeys, point.pcb_port_id, netKey)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const trace of srj.traces ?? []) {
|
|
65
|
+
const netKey = trace.connection_name
|
|
66
|
+
? connectionNetKeys.get(trace.connection_name)
|
|
67
|
+
: undefined
|
|
68
|
+
if (!netKey) continue
|
|
69
|
+
addTokenNet(tokenNetKeys, trace.pcb_trace_id, netKey)
|
|
70
|
+
for (const token of trace.connectsTo ?? []) {
|
|
71
|
+
addTokenNet(tokenNetKeys, token, netKey)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Obstacle metadata often contains both a connection id and a lower-level
|
|
76
|
+
// connectivity id. Propagate the known net across that metadata so another
|
|
77
|
+
// pad that only names the connectivity id is still recognized as same-net.
|
|
78
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
79
|
+
let changed = false
|
|
80
|
+
for (const obstacle of srj.obstacles) {
|
|
81
|
+
const netKeys = getKnownNetKeys(tokenNetKeys, obstacle.connectedTo)
|
|
82
|
+
if (netKeys.size !== 1) continue
|
|
83
|
+
const netKey = [...netKeys][0]!
|
|
84
|
+
for (const token of obstacle.connectedTo) {
|
|
85
|
+
changed = addTokenNet(tokenNetKeys, token, netKey) || changed
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!changed) break
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parentByNetKey = new Map<string, string>()
|
|
92
|
+
const findRoot = (netKey: string): string => {
|
|
93
|
+
const parent = parentByNetKey.get(netKey) ?? netKey
|
|
94
|
+
parentByNetKey.set(netKey, parent)
|
|
95
|
+
if (parent === netKey) return netKey
|
|
96
|
+
const root = findRoot(parent)
|
|
97
|
+
parentByNetKey.set(netKey, root)
|
|
98
|
+
return root
|
|
99
|
+
}
|
|
100
|
+
const union = (first: string, second: string): void => {
|
|
101
|
+
const firstRoot = findRoot(first)
|
|
102
|
+
const secondRoot = findRoot(second)
|
|
103
|
+
if (firstRoot !== secondRoot) parentByNetKey.set(secondRoot, firstRoot)
|
|
104
|
+
}
|
|
105
|
+
for (const netKeys of tokenNetKeys.values()) {
|
|
106
|
+
const [firstNetKey, ...otherNetKeys] = [...netKeys]
|
|
107
|
+
if (!firstNetKey) continue
|
|
108
|
+
for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
|
|
109
|
+
}
|
|
110
|
+
for (const connectedTokens of [
|
|
111
|
+
...srj.obstacles.map((obstacle) => obstacle.connectedTo),
|
|
112
|
+
...(srj.traces ?? []).map((trace) => trace.connectsTo ?? []),
|
|
113
|
+
]) {
|
|
114
|
+
const [firstNetKey, ...otherNetKeys] = [
|
|
115
|
+
...getKnownNetKeys(tokenNetKeys, connectedTokens),
|
|
116
|
+
]
|
|
117
|
+
if (!firstNetKey) continue
|
|
118
|
+
for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
|
|
119
|
+
}
|
|
120
|
+
for (const [connectionName, netKey] of connectionNetKeys) {
|
|
121
|
+
connectionNetKeys.set(connectionName, findRoot(netKey))
|
|
122
|
+
}
|
|
123
|
+
for (const [token, netKeys] of tokenNetKeys) {
|
|
124
|
+
tokenNetKeys.set(
|
|
125
|
+
token,
|
|
126
|
+
new Set([...netKeys].map((netKey) => findRoot(netKey))),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { connectionNetKeys, tokenNetKeys }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getElectricalNetIdentity(srj: SimpleRouteJson): ElectricalNetIdentity {
|
|
134
|
+
const cached = identityCache.get(srj)
|
|
135
|
+
if (cached) return cached
|
|
136
|
+
const identity = createElectricalNetIdentity(srj)
|
|
137
|
+
identityCache.set(srj, identity)
|
|
138
|
+
return identity
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function connectionsShareElectricalNet(
|
|
142
|
+
srj: SimpleRouteJson,
|
|
143
|
+
firstConnectionName: string,
|
|
144
|
+
secondConnectionName: string,
|
|
145
|
+
): boolean {
|
|
146
|
+
const identity = getElectricalNetIdentity(srj)
|
|
147
|
+
const firstNet = identity.connectionNetKeys.get(firstConnectionName)
|
|
148
|
+
const secondNet = identity.connectionNetKeys.get(secondConnectionName)
|
|
149
|
+
return firstNet !== undefined && firstNet === secondNet
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function obstacleSharesElectricalNet(
|
|
153
|
+
srj: SimpleRouteJson,
|
|
154
|
+
obstacle: Obstacle,
|
|
155
|
+
connectionName: string,
|
|
156
|
+
): boolean {
|
|
157
|
+
const identity = getElectricalNetIdentity(srj)
|
|
158
|
+
const connectionNet = identity.connectionNetKeys.get(connectionName)
|
|
159
|
+
if (!connectionNet) return false
|
|
160
|
+
return obstacle.connectedTo.some((token) =>
|
|
161
|
+
identity.tokenNetKeys.get(token)?.has(connectionNet),
|
|
162
|
+
)
|
|
163
|
+
}
|