diff --git a/fixtures/jumper-graph-solver-0603-generated.fixture.tsx b/fixtures/jumper-graph-solver-0603-generated.fixture.tsx
new file mode 100644
index 0000000..0347095
--- /dev/null
+++ b/fixtures/jumper-graph-solver-0603-generated.fixture.tsx
@@ -0,0 +1,252 @@
+import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
+import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
+import type { JPort, JRegion } from "lib/index"
+import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver"
+import { createProblemFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createProblemFromBaseGraph"
+import type { JumperGraph } from "lib/JumperGraphSolver/jumper-types"
+import { useMemo, useState } from "react"
+
+const MIN_ROWS = 1
+const MAX_ROWS = 6
+const MIN_COLS = 1
+const MAX_COLS = 6
+const MIN_CROSSINGS = 1
+const MAX_CROSSINGS = 12
+
+export default () => {
+ const [rows, setRows] = useState(3)
+ const [cols, setCols] = useState(2)
+ const [orientation, setOrientation] = useState<"horizontal" | "vertical">(
+ "horizontal",
+ )
+ const [pattern, setPattern] = useState<"grid" | "staggered">("staggered")
+ const [staggerAxis, setStaggerAxis] = useState<"x" | "y">("x")
+ const [numCrossings, setNumCrossings] = useState(2)
+ const [seed, setSeed] = useState(0)
+ const [resetNonce, setResetNonce] = useState(0)
+
+ const problemState = useMemo(() => {
+ try {
+ const baseGraph = generate0603JumperHyperGraph({
+ rows,
+ cols,
+ orientation,
+ pattern,
+ ...(pattern === "staggered" ? { staggerAxis } : {}),
+ }) as unknown as JumperGraph
+
+ const graphWithConnections = createProblemFromBaseGraph({
+ baseGraph,
+ numCrossings,
+ randomSeed: seed,
+ })
+
+ return {
+ problem: {
+ graph: graphWithConnections,
+ connections: graphWithConnections.connections,
+ },
+ error: null as string | null,
+ }
+ } catch (error) {
+ return {
+ problem: null,
+ error:
+ error instanceof Error
+ ? error.message
+ : "Unknown problem generation error",
+ }
+ }
+ }, [rows, cols, orientation, pattern, staggerAxis, numCrossings, seed])
+
+ const problem = problemState.problem
+ const debuggerKey = [
+ rows,
+ cols,
+ orientation,
+ pattern,
+ staggerAxis,
+ numCrossings,
+ seed,
+ resetNonce,
+ ].join("|")
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {pattern === "staggered" && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {problem
+ ? `Regions: ${problem.graph.regions.length}, Ports: ${problem.graph.ports.length}, Connections: ${problem.connections.length}`
+ : "No problem generated"}
+
+
+
+ {problemState.error && (
+
+ Generation error: {problemState.error}
+
+ )}
+
+
+ {problem ? (
+
+ new JumperGraphSolver({
+ inputGraph: {
+ regions: problem.graph.regions as JRegion[],
+ ports: problem.graph.ports as unknown as JPort[],
+ },
+ inputConnections: problem.connections,
+ })
+ }
+ />
+ ) : (
+
+ Adjust rows/cols/crossings/seed to generate a valid problem.
+
+ )}
+
+
+ )
+}
diff --git a/lib/HyperGraphSolver.ts b/lib/HyperGraphSolver.ts
index 3d5f489..966127a 100644
--- a/lib/HyperGraphSolver.ts
+++ b/lib/HyperGraphSolver.ts
@@ -1,23 +1,23 @@
import { BaseSolver } from "@tscircuit/solver-utils"
-import { convertSerializedHyperGraphToHyperGraph } from "./convertSerializedHyperGraphToHyperGraph"
-import { convertHyperGraphToSerializedHyperGraph } from "./convertHyperGraphToSerializedHyperGraph"
import { convertConnectionsToSerializedConnections } from "./convertConnectionsToSerializedConnections"
+import { convertHyperGraphToSerializedHyperGraph } from "./convertHyperGraphToSerializedHyperGraph"
+import { convertSerializedConnectionsToConnections } from "./convertSerializedConnectionsToConnections"
+import { convertSerializedHyperGraphToHyperGraph } from "./convertSerializedHyperGraphToHyperGraph"
+import { PriorityQueue } from "./PriorityQueue"
import type {
Candidate,
Connection,
- RegionPort,
- PortId,
+ GScore,
HyperGraph,
- SerializedConnection,
- SerializedHyperGraph,
+ PortId,
Region,
RegionId,
- SolvedRoute,
+ RegionPort,
RegionPortAssignment,
- GScore,
+ SerializedConnection,
+ SerializedHyperGraph,
+ SolvedRoute,
} from "./types"
-import { convertSerializedConnectionsToConnections } from "./convertSerializedConnectionsToConnections"
-import { PriorityQueue } from "./PriorityQueue"
export class HyperGraphSolver<
RegionType extends Region = Region,
diff --git a/lib/JumperGraphSolver/JumperGraphSolver.ts b/lib/JumperGraphSolver/JumperGraphSolver.ts
index a358a6d..92eb80f 100644
--- a/lib/JumperGraphSolver/JumperGraphSolver.ts
+++ b/lib/JumperGraphSolver/JumperGraphSolver.ts
@@ -1,3 +1,4 @@
+import { distance } from "@tscircuit/math-utils"
import type { GraphicsObject } from "graphics-debug"
import { HyperGraphSolver } from "../HyperGraphSolver"
import type {
@@ -8,12 +9,11 @@ import type {
SerializedHyperGraph,
SolvedRoute,
} from "../types"
-import type { JPort, JRegion } from "./jumper-types"
-import { visualizeJumperGraphSolver } from "./visualizeJumperGraphSolver"
-import { distance } from "@tscircuit/math-utils"
-import { computeDifferentNetCrossings } from "./computeDifferentNetCrossings"
import { computeCrossingAssignments } from "./computeCrossingAssignments"
+import { computeDifferentNetCrossings } from "./computeDifferentNetCrossings"
import { countInputConnectionCrossings } from "./countInputConnectionCrossings"
+import type { JPort, JRegion } from "./jumper-types"
+import { visualizeJumperGraphSolver } from "./visualizeJumperGraphSolver"
export const JUMPER_GRAPH_SOLVER_DEFAULTS = {
portUsagePenalty: 0.034685181009478865,
@@ -148,6 +148,51 @@ export class JumperGraphSolver extends HyperGraphSolver {
)
}
+ override computeRoutesToRip(newlySolvedRoute: SolvedRoute): Set {
+ const routesToRip = super.computeRoutesToRip(newlySolvedRoute)
+ for (const route of this.computeSingleNetRegionOverlapRoutes(
+ newlySolvedRoute,
+ )) {
+ routesToRip.add(route)
+ }
+ return routesToRip
+ }
+
+ private isSingleNetRegion(region: JRegion): boolean {
+ const regionData = region.d as Record
+ const maxDistinctNets = regionData.maxDistinctNets
+ if (typeof maxDistinctNets === "number") return maxDistinctNets <= 1
+
+ return Boolean(
+ regionData.singleNetOnly ||
+ regionData.singleNet ||
+ regionData.exclusiveToSingleNet ||
+ regionData.isThroughJumper,
+ )
+ }
+
+ private computeSingleNetRegionOverlapRoutes(
+ newlySolvedRoute: SolvedRoute,
+ ): Set {
+ const singleNetRegionRoutesToRip: Set = new Set()
+ const networkId = newlySolvedRoute.connection.mutuallyConnectedNetworkId
+
+ for (const candidate of newlySolvedRoute.path) {
+ const region = candidate.lastRegion
+ if (!region) continue
+ if (!this.isSingleNetRegion(region)) continue
+
+ for (const assignment of region.assignments ?? []) {
+ if (assignment.connection.mutuallyConnectedNetworkId === networkId) {
+ continue
+ }
+ singleNetRegionRoutesToRip.add(assignment.solvedRoute)
+ }
+ }
+
+ return singleNetRegionRoutesToRip
+ }
+
override routeSolvedHook(solvedRoute: SolvedRoute) {}
override routeStartedHook(connection: Connection) {}
diff --git a/lib/JumperGraphSolver/computeCrossingAssignments.ts b/lib/JumperGraphSolver/computeCrossingAssignments.ts
index 3772978..db6d3ff 100644
--- a/lib/JumperGraphSolver/computeCrossingAssignments.ts
+++ b/lib/JumperGraphSolver/computeCrossingAssignments.ts
@@ -1,12 +1,11 @@
import type { RegionPortAssignment } from "../types"
import type { JPort, JRegion } from "./jumper-types"
-import { perimeterT, chordsCross } from "./perimeterChordUtils"
+import { chordsCross, perimeterTForRegion } from "./perimeterChordUtils"
/**
* Compute the assignments that would cross with a new port pair in the region.
*
- * Uses the circle/perimeter mapping approach: two connections MUST cross
- * if their boundary points interleave around the perimeter.
+ * Uses the perimeter-chord method.
*
* Returns the actual RegionPortAssignment objects that would cross with the
* new port pair, allowing callers to determine which routes need to be ripped.
@@ -16,32 +15,21 @@ export function computeCrossingAssignments(
port1: JPort,
port2: JPort,
): RegionPortAssignment[] {
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds
-
- // Map the new port pair to perimeter coordinates
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax)
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax)
+ const newStart = port1.d
+ const newEnd = port2.d
+ const t1 = perimeterTForRegion(newStart, region)
+ const t2 = perimeterTForRegion(newEnd, region)
const newChord: [number, number] = [t1, t2]
- // Find assignments that cross with the new chord
+ // Find assignments that cross with the new port pair.
const crossingAssignments: RegionPortAssignment[] = []
const assignments = region.assignments ?? []
for (const assignment of assignments) {
- const existingT1 = perimeterT(
- (assignment.regionPort1 as JPort).d,
- xmin,
- xmax,
- ymin,
- ymax,
- )
- const existingT2 = perimeterT(
- (assignment.regionPort2 as JPort).d,
- xmin,
- xmax,
- ymin,
- ymax,
- )
+ const existingStart = (assignment.regionPort1 as JPort).d
+ const existingEnd = (assignment.regionPort2 as JPort).d
+ const existingT1 = perimeterTForRegion(existingStart, region)
+ const existingT2 = perimeterTForRegion(existingEnd, region)
const existingChord: [number, number] = [existingT1, existingT2]
if (chordsCross(newChord, existingChord)) {
diff --git a/lib/JumperGraphSolver/computeDifferentNetCrossings.ts b/lib/JumperGraphSolver/computeDifferentNetCrossings.ts
index ef852aa..1c4d60f 100644
--- a/lib/JumperGraphSolver/computeDifferentNetCrossings.ts
+++ b/lib/JumperGraphSolver/computeDifferentNetCrossings.ts
@@ -1,44 +1,32 @@
import type { JPort, JRegion } from "./jumper-types"
-import { perimeterT, chordsCross } from "./perimeterChordUtils"
+import { chordsCross, perimeterTForRegion } from "./perimeterChordUtils"
/**
* Compute the number of crossings between a new port pair and existing
* assignments in the region.
*
- * Uses the circle/perimeter mapping approach: two connections MUST cross
- * if their boundary points interleave around the perimeter.
+ * Uses the perimeter-chord method.
*/
export function computeDifferentNetCrossings(
region: JRegion,
port1: JPort,
port2: JPort,
): number {
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds
-
- // Map the new port pair to perimeter coordinates
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax)
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax)
+ const newStart = port1.d
+ const newEnd = port2.d
+ const t1 = perimeterTForRegion(newStart, region)
+ const t2 = perimeterTForRegion(newEnd, region)
const newChord: [number, number] = [t1, t2]
- // Count crossings with existing assignments
+ // Count crossings with existing assignments.
let crossings = 0
const assignments = region.assignments ?? []
for (const assignment of assignments) {
- const existingT1 = perimeterT(
- (assignment.regionPort1 as JPort).d,
- xmin,
- xmax,
- ymin,
- ymax,
- )
- const existingT2 = perimeterT(
- (assignment.regionPort2 as JPort).d,
- xmin,
- xmax,
- ymin,
- ymax,
- )
+ const existingStart = (assignment.regionPort1 as JPort).d
+ const existingEnd = (assignment.regionPort2 as JPort).d
+ const existingT1 = perimeterTForRegion(existingStart, region)
+ const existingT2 = perimeterTForRegion(existingEnd, region)
const existingChord: [number, number] = [existingT1, existingT2]
if (chordsCross(newChord, existingChord)) {
diff --git a/lib/JumperGraphSolver/perimeterChordUtils.ts b/lib/JumperGraphSolver/perimeterChordUtils.ts
index 20e8215..f3354fa 100644
--- a/lib/JumperGraphSolver/perimeterChordUtils.ts
+++ b/lib/JumperGraphSolver/perimeterChordUtils.ts
@@ -55,10 +55,101 @@ export function perimeterT(
return 2 * W + H + Math.max(0, Math.min(H, p.y - ymin))
}
+type PolygonPoint = { x: number; y: number }
+
+const getDistanceToSegmentAndProjectedLength = (
+ point: PolygonPoint,
+ a: PolygonPoint,
+ b: PolygonPoint,
+): { distance: number; projectedLength: number; segmentLength: number } => {
+ const dx = b.x - a.x
+ const dy = b.y - a.y
+ const segmentLength = Math.hypot(dx, dy)
+ if (segmentLength < 1e-12) {
+ return {
+ distance: Math.hypot(point.x - a.x, point.y - a.y),
+ projectedLength: 0,
+ segmentLength,
+ }
+ }
+
+ const ux = dx / segmentLength
+ const uy = dy / segmentLength
+ const vx = point.x - a.x
+ const vy = point.y - a.y
+ const projected = Math.max(0, Math.min(segmentLength, vx * ux + vy * uy))
+ const closestX = a.x + projected * ux
+ const closestY = a.y + projected * uy
+
+ return {
+ distance: Math.hypot(point.x - closestX, point.y - closestY),
+ projectedLength: projected,
+ segmentLength,
+ }
+}
+
+export function perimeterTOnPolygon(
+ p: PolygonPoint,
+ polygon: PolygonPoint[],
+): number {
+ if (polygon.length < 3) return 0
+
+ let totalPerimeter = 0
+ const segmentData: Array<{
+ a: PolygonPoint
+ b: PolygonPoint
+ cumLength: number
+ }> = []
+
+ for (let i = 0; i < polygon.length; i++) {
+ const a = polygon[i]
+ const b = polygon[(i + 1) % polygon.length]
+ if (!a || !b) continue
+ segmentData.push({ a, b, cumLength: totalPerimeter })
+ totalPerimeter += Math.hypot(b.x - a.x, b.y - a.y)
+ }
+
+ if (totalPerimeter < 1e-12) return 0
+
+ let bestT = 0
+ let bestDistance = Number.POSITIVE_INFINITY
+
+ for (const seg of segmentData) {
+ const { distance, projectedLength, segmentLength } =
+ getDistanceToSegmentAndProjectedLength(p, seg.a, seg.b)
+ if (segmentLength < 1e-12) continue
+
+ const candidateT = seg.cumLength + projectedLength
+ if (distance < bestDistance) {
+ bestDistance = distance
+ bestT = candidateT
+ }
+ }
+
+ return bestT
+}
+
+export function perimeterTForRegion(
+ p: { x: number; y: number },
+ region: {
+ d: {
+ bounds: { minX: number; maxX: number; minY: number; maxY: number }
+ polygon?: PolygonPoint[]
+ }
+ },
+): number {
+ if (Array.isArray(region.d.polygon) && region.d.polygon.length >= 3) {
+ return perimeterTOnPolygon(p, region.d.polygon)
+ }
+
+ const { minX, maxX, minY, maxY } = region.d.bounds
+ return perimeterT(p, minX, maxX, minY, maxY)
+}
+
/**
* Check if two perimeter coordinates are coincident (within epsilon)
*/
-function areCoincident(t1: number, t2: number, eps: number = 1e-6): boolean {
+function areCoincident(t1: number, t2: number, eps = 1e-6): boolean {
return Math.abs(t1 - t2) < eps
}
diff --git a/package.json b/package.json
index ffdd749..a09d0a5 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"typescript": "^5"
},
"dependencies": {
+ "@tscircuit/jumper-topology-generator": "^0.0.1",
"@tscircuit/solver-utils": "^0.0.14"
}
}
diff --git a/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver06.snap.svg b/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver06.snap.svg
new file mode 100644
index 0000000..6dd6a70
--- /dev/null
+++ b/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver06.snap.svg
@@ -0,0 +1,74 @@
+
\ No newline at end of file
diff --git a/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver07.snap.svg b/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver07.snap.svg
new file mode 100644
index 0000000..f8c2cc9
--- /dev/null
+++ b/tests/jumper-graph-solver/__snapshots__/jumper-graph-solver07.snap.svg
@@ -0,0 +1,74 @@
+
\ No newline at end of file
diff --git a/tests/jumper-graph-solver/assertNoTraceIntersectionsOutsideThroughJumpers.ts b/tests/jumper-graph-solver/assertNoTraceIntersectionsOutsideThroughJumpers.ts
new file mode 100644
index 0000000..eab5464
--- /dev/null
+++ b/tests/jumper-graph-solver/assertNoTraceIntersectionsOutsideThroughJumpers.ts
@@ -0,0 +1,121 @@
+import type { JRegion } from "lib/JumperGraphSolver/jumper-types"
+import type { SolvedRoute } from "lib/types"
+
+type Point = { x: number; y: number }
+
+type Segment = {
+ start: Point
+ end: Point
+ connectionId: string
+ regionId: string
+ isThroughJumperRegion: boolean
+}
+
+const EPS = 1e-6
+
+const getProperSegmentIntersection = (
+ aStart: Point,
+ aEnd: Point,
+ bStart: Point,
+ bEnd: Point,
+): Point | null => {
+ const aDx = aEnd.x - aStart.x
+ const aDy = aEnd.y - aStart.y
+ const bDx = bEnd.x - bStart.x
+ const bDy = bEnd.y - bStart.y
+
+ const denom = aDx * bDy - aDy * bDx
+ if (Math.abs(denom) < EPS) return null
+
+ const sx = bStart.x - aStart.x
+ const sy = bStart.y - aStart.y
+
+ const t = (sx * bDy - sy * bDx) / denom
+ const u = (sx * aDy - sy * aDx) / denom
+
+ if (t <= EPS || t >= 1 - EPS || u <= EPS || u >= 1 - EPS) {
+ return null
+ }
+
+ return {
+ x: aStart.x + t * aDx,
+ y: aStart.y + t * aDy,
+ }
+}
+
+const getRouteSegments = (solvedRoutes: SolvedRoute[]): Segment[] => {
+ const segments: Segment[] = []
+
+ for (const solvedRoute of solvedRoutes) {
+ const connectionId = solvedRoute.connection.connectionId
+ for (let i = 1; i < solvedRoute.path.length; i++) {
+ const prev = solvedRoute.path[i - 1]
+ const curr = solvedRoute.path[i]
+ if (!prev || !curr) continue
+
+ const start = { x: prev.port.d.x, y: prev.port.d.y }
+ const end = { x: curr.port.d.x, y: curr.port.d.y }
+
+ if (Math.abs(start.x - end.x) < EPS && Math.abs(start.y - end.y) < EPS) {
+ continue
+ }
+
+ const region = curr.lastRegion
+ if (!region) continue
+
+ segments.push({
+ start,
+ end,
+ connectionId,
+ regionId: region.regionId,
+ isThroughJumperRegion: Boolean(region.d?.isThroughJumper),
+ })
+ }
+ }
+
+ return segments
+}
+
+export const assertNoTraceIntersectionsOutsideThroughJumpers = (
+ solvedRoutes: SolvedRoute[],
+ _regions: JRegion[],
+) => {
+ const segments = getRouteSegments(solvedRoutes)
+ const violations: string[] = []
+
+ for (let i = 0; i < segments.length; i++) {
+ const segA = segments[i]
+ if (!segA) continue
+
+ for (let j = i + 1; j < segments.length; j++) {
+ const segB = segments[j]
+ if (!segB) continue
+ if (segA.connectionId === segB.connectionId) continue
+ if (segA.regionId !== segB.regionId) continue
+
+ const intersection = getProperSegmentIntersection(
+ segA.start,
+ segA.end,
+ segB.start,
+ segB.end,
+ )
+
+ if (!intersection) continue
+
+ if (segA.isThroughJumperRegion || segB.isThroughJumperRegion) continue
+
+ violations.push(
+ `${segA.connectionId}[${segA.regionId}] x ${segB.connectionId}[${segB.regionId}] @ (${intersection.x.toFixed(3)}, ${intersection.y.toFixed(3)})`,
+ )
+ }
+ }
+
+ if (violations.length > 0) {
+ throw new Error(
+ [
+ `Found ${violations.length} trace intersection(s) outside through-jumper regions`,
+ ...violations.slice(0, 10),
+ ].join("\n"),
+ )
+ }
+}
diff --git a/tests/jumper-graph-solver/jumper-graph-solver01.test.ts b/tests/jumper-graph-solver/jumper-graph-solver01.test.ts
index f31d949..457f17d 100644
--- a/tests/jumper-graph-solver/jumper-graph-solver01.test.ts
+++ b/tests/jumper-graph-solver/jumper-graph-solver01.test.ts
@@ -1,8 +1,8 @@
-import { test, expect } from "bun:test"
+import { expect, test } from "bun:test"
import { getSvgFromGraphicsObject } from "graphics-debug"
-import { generateJumperX4Grid } from "lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid"
-import { createGraphWithConnectionsFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver"
+import { createGraphWithConnectionsFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
+import { generateJumperX4Grid } from "lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid"
test(
"jumper-graph-solver01: solve 1x1 X4 grid with external connections",
@@ -50,6 +50,16 @@ test(
solver.solve()
+ for (const region of graphWithConnections.regions) {
+ if (!region.d.isThroughJumper) continue
+ const networkIds = new Set(
+ (region.assignments ?? []).map(
+ (a) => a.connection.mutuallyConnectedNetworkId,
+ ),
+ )
+ expect(networkIds.size).toBeLessThanOrEqual(1)
+ }
+
expect(getSvgFromGraphicsObject(solver.visualize())).toMatchSvgSnapshot(
import.meta.path,
)
diff --git a/tests/jumper-graph-solver/jumper-graph-solver02.test.ts b/tests/jumper-graph-solver/jumper-graph-solver02.test.ts
index 551163e..a6e436e 100644
--- a/tests/jumper-graph-solver/jumper-graph-solver02.test.ts
+++ b/tests/jumper-graph-solver/jumper-graph-solver02.test.ts
@@ -1,8 +1,8 @@
-import { test, expect } from "bun:test"
+import { expect, test } from "bun:test"
import { getSvgFromGraphicsObject } from "graphics-debug"
-import { generateJumperX4Grid } from "lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid"
-import { createGraphWithConnectionsFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver"
+import { createGraphWithConnectionsFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
+import { generateJumperX4Grid } from "lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid"
test(
"jumper-graph-solver02: solve 1x1 X4 grid with 5 external connections",
@@ -60,6 +60,16 @@ test(
solver.solve()
+ for (const region of graphWithConnections.regions) {
+ if (!region.d.isThroughJumper) continue
+ const networkIds = new Set(
+ (region.assignments ?? []).map(
+ (a) => a.connection.mutuallyConnectedNetworkId,
+ ),
+ )
+ expect(networkIds.size).toBeLessThanOrEqual(1)
+ }
+
expect(getSvgFromGraphicsObject(solver.visualize())).toMatchSvgSnapshot(
import.meta.path,
)
diff --git a/tests/jumper-graph-solver/jumper-graph-solver06.test.ts b/tests/jumper-graph-solver/jumper-graph-solver06.test.ts
new file mode 100644
index 0000000..fa7e422
--- /dev/null
+++ b/tests/jumper-graph-solver/jumper-graph-solver06.test.ts
@@ -0,0 +1,60 @@
+import { expect, test } from "bun:test"
+import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
+import { getSvgFromGraphicsObject } from "graphics-debug"
+import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver"
+import { createProblemFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createProblemFromBaseGraph"
+import type { JumperGraph } from "lib/JumperGraphSolver/jumper-types"
+import { assertNoTraceIntersectionsOutsideThroughJumpers } from "./assertNoTraceIntersectionsOutsideThroughJumpers"
+
+test(
+ "jumper-graph-solver06: solve generated 0603 vertical 2x3 grid",
+ // @ts-expect-error bun:test types don't include timeout option
+ { timeout: 30000 },
+ () => {
+ const baseGraph = generate0603JumperHyperGraph({
+ rows: 2,
+ cols: 3,
+ orientation: "vertical",
+ pattern: "grid",
+ }) as unknown as JumperGraph
+
+ const graphWithConnections = createProblemFromBaseGraph({
+ baseGraph,
+ numCrossings: 2,
+ randomSeed: 0,
+ })
+
+ const solver = new JumperGraphSolver({
+ inputGraph: {
+ regions: graphWithConnections.regions,
+ ports: graphWithConnections.ports,
+ },
+ inputConnections: graphWithConnections.connections,
+ })
+
+ solver.solve()
+
+ expect(solver.solved).toBe(true)
+
+ for (const region of graphWithConnections.regions) {
+ if (!region.d.isThroughJumper) continue
+
+ const networkIds = new Set(
+ (region.assignments ?? []).map(
+ (a) => a.connection.mutuallyConnectedNetworkId,
+ ),
+ )
+
+ expect(networkIds.size).toBeLessThanOrEqual(1)
+ }
+
+ assertNoTraceIntersectionsOutsideThroughJumpers(
+ solver.solvedRoutes,
+ graphWithConnections.regions,
+ )
+
+ expect(getSvgFromGraphicsObject(solver.visualize())).toMatchSvgSnapshot(
+ import.meta.path,
+ )
+ },
+)
diff --git a/tests/jumper-graph-solver/jumper-graph-solver07.test.ts b/tests/jumper-graph-solver/jumper-graph-solver07.test.ts
new file mode 100644
index 0000000..e898fda
--- /dev/null
+++ b/tests/jumper-graph-solver/jumper-graph-solver07.test.ts
@@ -0,0 +1,61 @@
+import { expect, test } from "bun:test"
+import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
+import { getSvgFromGraphicsObject } from "graphics-debug"
+import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver"
+import { createProblemFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createProblemFromBaseGraph"
+import type { JumperGraph } from "lib/JumperGraphSolver/jumper-types"
+import { assertNoTraceIntersectionsOutsideThroughJumpers } from "./assertNoTraceIntersectionsOutsideThroughJumpers"
+
+test(
+ "jumper-graph-solver07: solve generated 0603 staggered 3x2 grid",
+ // @ts-expect-error bun:test types don't include timeout option
+ { timeout: 30000 },
+ () => {
+ const baseGraph = generate0603JumperHyperGraph({
+ rows: 3,
+ cols: 2,
+ orientation: "horizontal",
+ pattern: "staggered",
+ staggerAxis: "x",
+ }) as unknown as JumperGraph
+
+ const graphWithConnections = createProblemFromBaseGraph({
+ baseGraph,
+ numCrossings: 2,
+ randomSeed: 0,
+ })
+
+ const solver = new JumperGraphSolver({
+ inputGraph: {
+ regions: graphWithConnections.regions,
+ ports: graphWithConnections.ports,
+ },
+ inputConnections: graphWithConnections.connections,
+ })
+
+ solver.solve()
+
+ expect(solver.solved).toBe(true)
+
+ for (const region of graphWithConnections.regions) {
+ if (!region.d.isThroughJumper) continue
+
+ const networkIds = new Set(
+ (region.assignments ?? []).map(
+ (a) => a.connection.mutuallyConnectedNetworkId,
+ ),
+ )
+
+ expect(networkIds.size).toBeLessThanOrEqual(1)
+ }
+
+ assertNoTraceIntersectionsOutsideThroughJumpers(
+ solver.solvedRoutes,
+ graphWithConnections.regions,
+ )
+
+ expect(getSvgFromGraphicsObject(solver.visualize())).toMatchSvgSnapshot(
+ import.meta.path,
+ )
+ },
+)