@tscircuit/hypergraph 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tscircuit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @tscircuit/hypergraph
2
+
3
+ A generic A* pathfinding solver for routing connections through hypergraphs. Designed for circuit routing problems but extensible to any graph-based pathfinding scenario.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @tscircuit/hypergraph
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ HyperGraphSolver implements an A* algorithm that routes connections through a hypergraph data structure where:
14
+
15
+ - **Regions** are nodes representing spaces in your problem domain
16
+ - **Ports** are edges connecting two regions (the boundary between them)
17
+ - **Connections** define start and end regions that need to be linked
18
+
19
+ The solver finds optimal paths through the graph while handling conflicts via "ripping" - the ability to reroute existing paths when they block new connections.
20
+
21
+ ## Core Types
22
+
23
+ ```typescript
24
+ interface Region {
25
+ regionId: string
26
+ ports: RegionPort[]
27
+ d: any // Domain-specific data (e.g., bounds, coordinates)
28
+ assignments?: RegionPortAssignment[]
29
+ }
30
+
31
+ interface RegionPort {
32
+ portId: string
33
+ region1: Region
34
+ region2: Region
35
+ d: any // Domain-specific data (e.g., x, y position)
36
+ assignment?: PortAssignment
37
+ ripCount?: number
38
+ }
39
+
40
+ interface Connection {
41
+ connectionId: string
42
+ mutuallyConnectedNetworkId: string
43
+ startRegion: Region
44
+ endRegion: Region
45
+ }
46
+
47
+ interface HyperGraph {
48
+ regions: Region[]
49
+ ports: RegionPort[]
50
+ }
51
+ ```
52
+
53
+ ## Basic Usage
54
+
55
+ ```typescript
56
+ import { HyperGraphSolver } from "@tscircuit/hypergraph"
57
+
58
+ const solver = new HyperGraphSolver({
59
+ inputGraph: {
60
+ regions: [...],
61
+ ports: [...],
62
+ },
63
+ inputConnections: [
64
+ { connectionId: "c1", mutuallyConnectedNetworkId: "net1", startRegion: regionA, endRegion: regionB },
65
+ { connectionId: "c2", mutuallyConnectedNetworkId: "net2", startRegion: regionC, endRegion: regionD },
66
+ ],
67
+ })
68
+
69
+ solver.solve()
70
+
71
+ if (solver.solved) {
72
+ console.log(solver.solvedRoutes)
73
+ }
74
+ ```
75
+
76
+ ## Configuration Options
77
+
78
+ | Option | Type | Default | Description |
79
+ |--------|------|---------|-------------|
80
+ | `inputGraph` | `HyperGraph \| SerializedHyperGraph` | required | The graph with regions and ports |
81
+ | `inputConnections` | `Connection[]` | required | Connections to route |
82
+ | `greedyMultiplier` | `number` | `1.0` | Weight for heuristic score (higher = greedier search) |
83
+ | `rippingEnabled` | `boolean` | `false` | Allow rerouting existing paths to resolve conflicts |
84
+ | `ripCost` | `number` | `0` | Additional cost penalty when ripping is required |
85
+
86
+ ## Creating a Custom Solver
87
+
88
+ HyperGraphSolver is designed to be extended. Override these methods to customize behavior for your domain:
89
+
90
+ ```typescript
91
+ import { HyperGraphSolver, Region, RegionPort, Candidate } from "@tscircuit/hypergraph"
92
+
93
+ class MyCustomSolver extends HyperGraphSolver<MyRegion, MyPort> {
94
+ /**
95
+ * Estimate cost from a port to the destination region.
96
+ * Used for the A* heuristic.
97
+ */
98
+ estimateCostToEnd(port: MyPort): number {
99
+ // Return estimated distance/cost to currentEndRegion
100
+ }
101
+
102
+ /**
103
+ * Compute heuristic score for a candidate.
104
+ * Default uses estimateCostToEnd().
105
+ */
106
+ computeH(candidate: Candidate<MyRegion, MyPort>): number {
107
+ return this.estimateCostToEnd(candidate.port)
108
+ }
109
+
110
+ /**
111
+ * Return penalty for using a port (e.g., if previously ripped).
112
+ */
113
+ getPortUsagePenalty(port: MyPort): number {
114
+ return port.ripCount ?? 0
115
+ }
116
+
117
+ /**
118
+ * Cost increase when routing through a region using two specific ports.
119
+ * Useful for penalizing crossings or congestion.
120
+ */
121
+ computeIncreasedRegionCostIfPortsAreUsed(
122
+ region: MyRegion,
123
+ port1: MyPort,
124
+ port2: MyPort
125
+ ): number {
126
+ return 0
127
+ }
128
+
129
+ /**
130
+ * Detect assignments that conflict with using port1 and port2 together.
131
+ * Return assignments that must be ripped.
132
+ */
133
+ getRipsRequiredForPortUsage(
134
+ region: MyRegion,
135
+ port1: MyPort,
136
+ port2: MyPort
137
+ ): RegionPortAssignment[] {
138
+ return []
139
+ }
140
+
141
+ /**
142
+ * Filter candidates entering a region to reduce redundant exploration.
143
+ */
144
+ selectCandidatesForEnteringRegion(
145
+ candidates: Candidate<MyRegion, MyPort>[]
146
+ ): Candidate<MyRegion, MyPort>[] {
147
+ return candidates
148
+ }
149
+
150
+ /**
151
+ * Hook called after each route is solved.
152
+ */
153
+ routeSolvedHook(solvedRoute: SolvedRoute): void {
154
+ // Custom logic after route completion
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## Solver Output
160
+
161
+ After calling `solve()`, access results via:
162
+
163
+ ```typescript
164
+ solver.solved // boolean - true if all connections routed
165
+ solver.solvedRoutes // SolvedRoute[] - array of solved paths
166
+ solver.iterations // number - iterations used
167
+ solver.failed // boolean - true if max iterations exceeded
168
+ ```
169
+
170
+ Each `SolvedRoute` contains:
171
+
172
+ ```typescript
173
+ interface SolvedRoute {
174
+ connection: Connection // The connection that was routed
175
+ path: Candidate[] // Sequence of candidates forming the path
176
+ requiredRip: boolean // Whether ripping was needed
177
+ }
178
+ ```
179
+
180
+ ## Serialized Input Format
181
+
182
+ For JSON serialization, use ID references instead of object references:
183
+
184
+ ```typescript
185
+ interface SerializedHyperGraph {
186
+ regions: Array<{
187
+ regionId: string
188
+ d: any
189
+ }>
190
+ ports: Array<{
191
+ portId: string
192
+ region1Id: string
193
+ region2Id: string
194
+ d: any
195
+ }>
196
+ }
197
+
198
+ interface SerializedConnection {
199
+ connectionId: string
200
+ mutuallyConnectedNetworkId: string
201
+ startRegionId: string
202
+ endRegionId: string
203
+ }
204
+ ```
205
+
206
+ The solver automatically converts serialized inputs to hydrated object references.
207
+
208
+ ## Algorithm
209
+
210
+ HyperGraphSolver uses A* pathfinding:
211
+
212
+ 1. Initialize candidate queue with all ports of the start region
213
+ 2. Process candidates in priority order (lowest `f = g + h * greedyMultiplier`)
214
+ 3. When reaching the destination, trace back through parent pointers
215
+ 4. Handle conflicts by ripping (removing conflicting routes and re-queuing them)
216
+ 5. Continue until all connections are routed or max iterations exceeded
217
+
218
+ ## Example Implementation
219
+
220
+ For a complete real-world implementation, see [JumperGraphSolver](./lib/JumperGraphSolver/JumperGraphSolver.ts) which extends HyperGraphSolver for PCB resistor network routing.
221
+
222
+ ## License
223
+
224
+ MIT
@@ -0,0 +1,360 @@
1
+ import { GraphicsObject } from 'graphics-debug';
2
+ import { BaseSolver } from '@tscircuit/solver-utils';
3
+ import { Matrix } from 'transformation-matrix';
4
+
5
+ type PortId = string;
6
+ type RegionId = string;
7
+ type ConnectionId = string;
8
+ type NetworkId = string;
9
+ type GScore = number;
10
+ type RegionPort = {
11
+ portId: PortId;
12
+ region1: Region;
13
+ region2: Region;
14
+ d: any;
15
+ assignment?: PortAssignment;
16
+ /**
17
+ * The number of times this port has been ripped. Can be used to penalize
18
+ * ports that are likely to block off connections
19
+ */
20
+ ripCount?: number;
21
+ };
22
+ type Region = {
23
+ regionId: RegionId;
24
+ ports: RegionPort[];
25
+ d: any;
26
+ assignments?: RegionPortAssignment[];
27
+ };
28
+ type PortAssignment = {
29
+ solvedRoute: SolvedRoute;
30
+ connection: Connection;
31
+ };
32
+ type RegionPortAssignment = {
33
+ regionPort1: RegionPort;
34
+ regionPort2: RegionPort;
35
+ region: Region;
36
+ connection: Connection;
37
+ solvedRoute: SolvedRoute;
38
+ };
39
+ type SolvedRoute = {
40
+ path: Candidate[];
41
+ connection: Connection;
42
+ requiredRip: boolean;
43
+ };
44
+ type Candidate<RegionType extends Region = Region, RegionPortType extends RegionPort = RegionPort> = {
45
+ port: RegionPortType;
46
+ g: number;
47
+ h: number;
48
+ f: number;
49
+ hops: number;
50
+ parent?: Candidate;
51
+ lastPort?: RegionPortType;
52
+ lastRegion?: RegionType;
53
+ nextRegion?: RegionType;
54
+ ripRequired: boolean;
55
+ };
56
+ type HyperGraph = {
57
+ ports: RegionPort[];
58
+ regions: Region[];
59
+ };
60
+ type SerializedGraphPort = Omit<RegionPort, "edges"> & {
61
+ portId: PortId;
62
+ region1Id: RegionId;
63
+ region2Id: RegionId;
64
+ };
65
+ type SerializedGraphRegion = Omit<Region, "points" | "assignments"> & {
66
+ pointIds: PortId[];
67
+ assignments?: SerializedRegionPortAssignment[];
68
+ };
69
+ type SerializedRegionPortAssignment = {
70
+ regionPort1Id: PortId;
71
+ regionPort2Id: PortId;
72
+ connectionId: ConnectionId;
73
+ };
74
+ type SerializedHyperGraph = {
75
+ ports: SerializedGraphPort[];
76
+ regions: SerializedGraphRegion[];
77
+ };
78
+ type Connection = {
79
+ connectionId: ConnectionId;
80
+ mutuallyConnectedNetworkId: NetworkId;
81
+ startRegion: Region;
82
+ endRegion: Region;
83
+ };
84
+ type SerializedConnection = {
85
+ connectionId: ConnectionId;
86
+ startRegionId: RegionId;
87
+ endRegionId: RegionId;
88
+ };
89
+
90
+ type Bounds = {
91
+ minX: number;
92
+ minY: number;
93
+ maxX: number;
94
+ maxY: number;
95
+ };
96
+
97
+ interface JRegion extends Region {
98
+ d: {
99
+ bounds: Bounds;
100
+ center: {
101
+ x: number;
102
+ y: number;
103
+ };
104
+ isPad: boolean;
105
+ isThroughJumper?: boolean;
106
+ isConnectionRegion?: boolean;
107
+ };
108
+ }
109
+ interface JPort extends RegionPort {
110
+ d: {
111
+ x: number;
112
+ y: number;
113
+ };
114
+ }
115
+ type JumperGraph = {
116
+ regions: JRegion[];
117
+ ports: JPort[];
118
+ };
119
+
120
+ declare const generateJumperX4Grid: ({ cols, rows, marginX, marginY, innerColChannelPointCount, innerRowChannelPointCount, regionsBetweenPads, outerPaddingX, outerPaddingY, outerChannelXPointCount, outerChannelYPointCount, orientation, center, }: {
121
+ cols: number;
122
+ rows: number;
123
+ marginX: number;
124
+ marginY: number;
125
+ innerColChannelPointCount?: number;
126
+ innerRowChannelPointCount?: number;
127
+ regionsBetweenPads?: boolean;
128
+ outerPaddingX?: number;
129
+ outerPaddingY?: number;
130
+ outerChannelXPointCount?: number;
131
+ outerChannelYPointCount?: number;
132
+ orientation?: "vertical" | "horizontal";
133
+ center?: {
134
+ x: number;
135
+ y: number;
136
+ };
137
+ }) => JumperGraph;
138
+
139
+ declare const generateJumperGrid: ({ cols, rows, marginX, marginY, innerColChannelPointCount, innerRowChannelPointCount, outerPaddingX, outerPaddingY, outerChannelXPoints, outerChannelYPoints, }: {
140
+ cols: number;
141
+ rows: number;
142
+ marginX: number;
143
+ marginY: number;
144
+ innerColChannelPointCount?: number;
145
+ innerRowChannelPointCount?: number;
146
+ outerPaddingX?: number;
147
+ outerPaddingY?: number;
148
+ outerChannelXPoints?: number;
149
+ outerChannelYPoints?: number;
150
+ }) => {
151
+ regions: JRegion[];
152
+ ports: JPort[];
153
+ };
154
+
155
+ type XYConnection = {
156
+ start: {
157
+ x: number;
158
+ y: number;
159
+ };
160
+ end: {
161
+ x: number;
162
+ y: number;
163
+ };
164
+ connectionId: string;
165
+ };
166
+ type JumperGraphWithConnections = JumperGraph & {
167
+ connections: Connection[];
168
+ };
169
+ /**
170
+ * Creates a new graph from a base graph with additional connection regions at
171
+ * specified positions on the boundary. Connection regions are 0.4x0.4 pseudo-regions
172
+ * that contain one port point connecting them to the grid boundary.
173
+ */
174
+ declare const createGraphWithConnectionsFromBaseGraph: (baseGraph: JumperGraph, xyConnections: XYConnection[]) => JumperGraphWithConnections;
175
+
176
+ type Node = {
177
+ f: number;
178
+ [key: string]: any;
179
+ };
180
+ declare class PriorityQueue<T extends Node = Node> {
181
+ private heap;
182
+ private maxSize;
183
+ /**
184
+ * Creates a new Priority Queue.
185
+ * @param nodes An optional initial array of nodes to populate the queue.
186
+ * @param maxSize The maximum number of elements the queue can hold. Defaults to 10,000.
187
+ */
188
+ constructor(nodes?: T[], maxSize?: number);
189
+ /**
190
+ * Returns the number of elements currently in the queue.
191
+ */
192
+ get size(): number;
193
+ /**
194
+ * Checks if the queue is empty.
195
+ */
196
+ isEmpty(): boolean;
197
+ /**
198
+ * Returns the node with the highest priority (smallest 'f') without removing it.
199
+ * Returns null if the queue is empty.
200
+ * @returns The highest priority node or null.
201
+ */
202
+ peek(): T | null;
203
+ /**
204
+ * Returns up to N nodes with the highest priority (smallest 'f') without removing them.
205
+ * Nodes are returned sorted by priority (smallest 'f' first).
206
+ * @param count Maximum number of nodes to return.
207
+ * @returns Array of nodes sorted by priority.
208
+ */
209
+ peekMany(count: number): T[];
210
+ /**
211
+ * Removes and returns the node with the highest priority (smallest 'f').
212
+ * Returns null if the queue is empty.
213
+ * Maintains the heap property.
214
+ * @returns The highest priority node or null.
215
+ */
216
+ dequeue(): T | null;
217
+ /**
218
+ * Adds a new node to the queue.
219
+ * Maintains the heap property.
220
+ * If the queue is full (at maxSize), the node is not added.
221
+ * @param node The node to add.
222
+ */
223
+ enqueue(node: T): void;
224
+ /**
225
+ * Moves the node at the given index up the heap to maintain the heap property.
226
+ * @param index The index of the node to sift up.
227
+ */
228
+ private _siftUp;
229
+ /**
230
+ * Moves the node at the given index down the heap to maintain the heap property.
231
+ * @param index The index of the node to sift down.
232
+ */
233
+ private _siftDown;
234
+ /**
235
+ * Swaps two elements in the heap array.
236
+ * @param i Index of the first element.
237
+ * @param j Index of the second element.
238
+ */
239
+ private _swap;
240
+ /** Calculates the parent index of a node. */
241
+ private _parentIndex;
242
+ /** Calculates the left child index of a node. */
243
+ private _leftChildIndex;
244
+ /** Calculates the right child index of a node. */
245
+ private _rightChildIndex;
246
+ }
247
+
248
+ declare class HyperGraphSolver<RegionType extends Region = Region, RegionPortType extends RegionPort = RegionPort, CandidateType extends Candidate<RegionType, RegionPortType> = Candidate<RegionType, RegionPortType>> extends BaseSolver {
249
+ input: {
250
+ inputGraph: HyperGraph | SerializedHyperGraph;
251
+ inputConnections: (Connection | SerializedConnection)[];
252
+ greedyMultiplier?: number;
253
+ rippingEnabled?: boolean;
254
+ ripCost?: number;
255
+ };
256
+ graph: HyperGraph;
257
+ connections: Connection[];
258
+ candidateQueue: PriorityQueue<Candidate>;
259
+ unprocessedConnections: Connection[];
260
+ solvedRoutes: SolvedRoute[];
261
+ currentConnection: Connection | null;
262
+ currentEndRegion: Region | null;
263
+ greedyMultiplier: number;
264
+ rippingEnabled: boolean;
265
+ ripCost: number;
266
+ lastCandidate: Candidate | null;
267
+ visitedPointsForCurrentConnection: Map<PortId, GScore>;
268
+ constructor(input: {
269
+ inputGraph: HyperGraph | SerializedHyperGraph;
270
+ inputConnections: (Connection | SerializedConnection)[];
271
+ greedyMultiplier?: number;
272
+ rippingEnabled?: boolean;
273
+ ripCost?: number;
274
+ });
275
+ computeH(candidate: CandidateType): number;
276
+ /**
277
+ * OVERRIDE THIS
278
+ *
279
+ * Return the estimated remaining cost to the end of the route. You must
280
+ * first understand the UNIT of your costs. If it's distance, then this could
281
+ * be something like distance(port, this.currentEndRegion.d.center)
282
+ */
283
+ estimateCostToEnd(port: RegionPortType): number;
284
+ /**
285
+ * OPTIONALLY OVERRIDE THIS
286
+ *
287
+ * This is a penalty for using a port that is not relative to a connection,
288
+ * e.g. maybe this port is in a special area of congestion. Use this to
289
+ * penalize ports that are e.g. likely to block off connections, you may want
290
+ * to use port.ripCount to help determine this penalty, or you can use port
291
+ * position, region volume etc.
292
+ */
293
+ getPortUsagePenalty(port: RegionPortType): number;
294
+ /**
295
+ * OVERRIDE THIS
296
+ *
297
+ * Return the cost of using two ports in the region, make sure to consider
298
+ * existing assignments. You may use this to penalize intersections
299
+ */
300
+ computeIncreasedRegionCostIfPortsAreUsed(region: RegionType, port1: RegionPortType, port2: RegionPortType): number;
301
+ /**
302
+ * OPTIONALLY OVERRIDE THIS
303
+ *
304
+ * Return the assignments that would need to be ripped if the given ports
305
+ * are used together in the region. This is used to determine if adopting
306
+ * a route would require ripping other routes due to problematic crossings.
307
+ */
308
+ getRipsRequiredForPortUsage(_region: RegionType, _port1: RegionPortType, _port2: RegionPortType): RegionPortAssignment[];
309
+ computeG(candidate: CandidateType): number;
310
+ /**
311
+ * Return a subset of the candidates for entering a region. These candidates
312
+ * are all possible ways to enter the region- you can e.g. return the middle
313
+ * port to make it so that you're not queueing candidates that are likely
314
+ * redundant.
315
+ */
316
+ selectCandidatesForEnteringRegion(candidates: Candidate[]): Candidate[];
317
+ getNextCandidates(currentCandidate: CandidateType): CandidateType[];
318
+ processSolvedRoute(finalCandidate: CandidateType): void;
319
+ /**
320
+ * OPTIONALLY OVERRIDE THIS
321
+ *
322
+ * You can override this to perform actions after a route is solved, e.g.
323
+ * you may want to detect if a solvedRoute.requiredRip is true, in which
324
+ * case you might want to execute a "random rip" to avoid loops or check
325
+ * if we've exceeded a maximum number of rips.
326
+ *
327
+ * You can also use this to shuffle unprocessed routes if a rip occurred, this
328
+ * can also help avoid loops
329
+ */
330
+ routeSolvedHook(solvedRoute: SolvedRoute): void;
331
+ ripSolvedRoute(solvedRoute: SolvedRoute): void;
332
+ beginNewConnection(): void;
333
+ _step(): void;
334
+ }
335
+
336
+ declare class JumperGraphSolver extends HyperGraphSolver<JRegion, JPort> {
337
+ UNIT_OF_COST: string;
338
+ constructor(input: {
339
+ inputGraph: HyperGraph | SerializedHyperGraph;
340
+ inputConnections: (Connection | SerializedConnection)[];
341
+ });
342
+ estimateCostToEnd(port: JPort): number;
343
+ getPortUsagePenalty(port: JPort): number;
344
+ computeIncreasedRegionCostIfPortsAreUsed(region: JRegion, port1: JPort, port2: JPort): number;
345
+ getRipsRequiredForPortUsage(region: JRegion, port1: JPort, port2: JPort): RegionPortAssignment[];
346
+ routeSolvedHook(solvedRoute: SolvedRoute): void;
347
+ visualize(): GraphicsObject;
348
+ }
349
+
350
+ /**
351
+ * Applies a transformation matrix to all points in a graph.
352
+ * Transforms region bounds, region centers, and port positions.
353
+ */
354
+ declare const applyTransformToGraph: (graph: JumperGraph, matrix: Matrix) => JumperGraph;
355
+ /**
356
+ * Rotates a graph 90 degrees clockwise around its center.
357
+ */
358
+ declare const rotateGraph90Degrees: (graph: JumperGraph) => JumperGraph;
359
+
360
+ export { HyperGraphSolver, JumperGraphSolver, type JumperGraphWithConnections, type XYConnection, applyTransformToGraph, createGraphWithConnectionsFromBaseGraph, generateJumperGrid, generateJumperX4Grid, rotateGraph90Degrees };