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, + ) + }, +)