diff --git a/pages/bus-routing/region-span.page.tsx b/pages/bus-routing/region-span.page.tsx new file mode 100644 index 0000000..a4d56d0 --- /dev/null +++ b/pages/bus-routing/region-span.page.tsx @@ -0,0 +1,66 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { + BUS_REGION_SPAN_ROUTE_COUNT, + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + busRegionSpanFixture, +} from "../../tests/fixtures/bus-region-span.fixture" +import { Debugger } from "../components/Debugger" + +const createBusSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 50_000, + }) +} + +const createPlainSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 50_000, + }) +} + +export default function BusRoutingRegionSpanPage() { + return ( +
+
+ This synthetic repro builds a{" "} + {BUS_REGION_SPAN_ROUTE_COUNT}-trace bus with four main + routing regions: a large top-main, a large{" "} + bottom-main, and a slit middle split into{" "} + mid-left and mid-right. Each middle half only + exposes {BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE}{" "} + shared ports on its top and bottom edges, so no single half can carry + the full bus. Two extra bottom-side regions keep the middle halves more + than two hops from the goal transit region, so the bus solver cannot + escape through the manual two-hop finish rule. +
+
+
+
+ Current Bus Solver +
+
+ +
+
+
+
+ Reference Plain Solver +
+
+ +
+
+
+
+ ) +} diff --git a/tests/fixtures/bus-region-span.fixture.ts b/tests/fixtures/bus-region-span.fixture.ts new file mode 100644 index 0000000..ceb2b39 --- /dev/null +++ b/tests/fixtures/bus-region-span.fixture.ts @@ -0,0 +1,152 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" + +export const BUS_REGION_SPAN_ROUTE_COUNT = 6 +export const BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE = 4 + +const START_END_XS = [-7.5, -4.5, -1.5, 1.5, 4.5, 7.5] as const +const MID_LEFT_XS = [-6.5, -5.0, -3.5, -2.0] as const +const MID_RIGHT_XS = [2.0, 3.5, 5.0, 6.5] as const + +const createRegion = ( + regionId: string, + centerX: number, + centerY: number, + width: number, + height: number, + pointIds: string[], +): NonNullable[number] => ({ + regionId, + pointIds, + d: { + center: { x: centerX, y: centerY }, + width, + height, + }, +}) + +const createPort = ( + portId: string, + region1Id: string, + region2Id: string, + x: number, + y: number, +): NonNullable[number] => ({ + portId, + region1Id, + region2Id, + d: { + x, + y, + z: 0, + }, +}) + +const createConnection = ( + routeIndex: number, +): NonNullable[number] => ({ + connectionId: `route-${routeIndex}`, + startRegionId: `start-${routeIndex}`, + endRegionId: `end-${routeIndex}`, + mutuallyConnectedNetworkId: `net-${routeIndex}`, +}) + +const topStartPortIds = START_END_XS.map( + (_, routeIndex) => `start-port-${routeIndex}`, +) +const bottomChainPortIds = START_END_XS.map( + (_, routeIndex) => `bottom-chain-port-${routeIndex}`, +) +const bottomExitPortIds = START_END_XS.map( + (_, routeIndex) => `bottom-exit-port-${routeIndex}`, +) +const endPortIds = START_END_XS.map((_, routeIndex) => `end-port-${routeIndex}`) +const topLeftMidPortIds = MID_LEFT_XS.map((_, portIndex) => `tl-${portIndex}`) +const bottomLeftMidPortIds = MID_LEFT_XS.map( + (_, portIndex) => `bl-${portIndex}`, +) +const topRightMidPortIds = MID_RIGHT_XS.map((_, portIndex) => `tr-${portIndex}`) +const bottomRightMidPortIds = MID_RIGHT_XS.map( + (_, portIndex) => `br-${portIndex}`, +) + +export const busRegionSpanFixture: SerializedHyperGraph = { + regions: [ + ...START_END_XS.flatMap((x, routeIndex) => [ + createRegion(`start-${routeIndex}`, x, 12, 1.2, 1.2, [ + `start-port-${routeIndex}`, + ]), + createRegion(`end-${routeIndex}`, x, -16, 1.2, 1.2, [ + `end-port-${routeIndex}`, + ]), + ]), + createRegion("top-main", 0, 7.5, 18, 3, [ + ...topStartPortIds, + ...topLeftMidPortIds, + ...topRightMidPortIds, + ]), + createRegion("mid-left", -4, 2.5, 6, 7, [ + ...topLeftMidPortIds, + ...bottomLeftMidPortIds, + ]), + createRegion("mid-right", 4, 2.5, 6, 7, [ + ...topRightMidPortIds, + ...bottomRightMidPortIds, + ]), + createRegion("bottom-main", 0, -3.5, 18, 3, [ + ...bottomLeftMidPortIds, + ...bottomRightMidPortIds, + ...bottomChainPortIds, + ]), + createRegion("bottom-buffer", 0, -9.5, 18, 3, [ + ...bottomChainPortIds, + ...bottomExitPortIds, + ]), + createRegion("bottom-exit", 0, -13, 18, 2.5, [ + ...bottomExitPortIds, + ...endPortIds, + ]), + ], + ports: [ + ...START_END_XS.flatMap((x, routeIndex) => [ + createPort( + `start-port-${routeIndex}`, + `start-${routeIndex}`, + "top-main", + x, + 10.5, + ), + createPort( + `bottom-chain-port-${routeIndex}`, + "bottom-main", + "bottom-buffer", + x, + -6.5, + ), + createPort( + `bottom-exit-port-${routeIndex}`, + "bottom-buffer", + "bottom-exit", + x, + -11.25, + ), + createPort( + `end-port-${routeIndex}`, + "bottom-exit", + `end-${routeIndex}`, + x, + -14.5, + ), + ]), + ...MID_LEFT_XS.flatMap((x, portIndex) => [ + createPort(`tl-${portIndex}`, "top-main", "mid-left", x, 6.0), + createPort(`bl-${portIndex}`, "mid-left", "bottom-main", x, -0.5), + ]), + ...MID_RIGHT_XS.flatMap((x, portIndex) => [ + createPort(`tr-${portIndex}`, "top-main", "mid-right", x, 6.0), + createPort(`br-${portIndex}`, "mid-right", "bottom-main", x, -0.5), + ]), + ], + connections: START_END_XS.map((_, routeIndex) => + createConnection(routeIndex), + ), +} diff --git a/tests/solver/bus-region-span-repro.test.ts b/tests/solver/bus-region-span-repro.test.ts new file mode 100644 index 0000000..5690869 --- /dev/null +++ b/tests/solver/bus-region-span-repro.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from "bun:test" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { + BUS_REGION_SPAN_ROUTE_COUNT, + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + busRegionSpanFixture, +} from "tests/fixtures/bus-region-span.fixture" + +const countSharedPorts = (regionAId: string, regionBId: string) => + busRegionSpanFixture.ports.filter( + (port) => + (port.region1Id === regionAId && port.region2Id === regionBId) || + (port.region1Id === regionBId && port.region2Id === regionAId), + ).length + +const getRegionIndexBySerializedId = ( + topology: ReturnType["topology"], +) => { + const regionIndexBySerializedId = new Map() + + topology.regionMetadata?.forEach((metadata, regionIndex) => { + const serializedRegionId = (metadata as { serializedRegionId?: string }) + .serializedRegionId + if (typeof serializedRegionId === "string") { + regionIndexBySerializedId.set(serializedRegionId, regionIndex) + } + }) + + return regionIndexBySerializedId +} + +test("repro: six-trace slit middle requires spanning both middle regions", () => { + const { topology, problem } = loadSerializedHyperGraph(busRegionSpanFixture) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 50_000, + }) + + expect(problem.routeCount).toBe(BUS_REGION_SPAN_ROUTE_COUNT) + expect(countSharedPorts("top-main", "mid-left")).toBe( + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + ) + expect(countSharedPorts("top-main", "mid-right")).toBe( + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + ) + expect(countSharedPorts("mid-left", "bottom-main")).toBe( + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + ) + expect(countSharedPorts("mid-right", "bottom-main")).toBe( + BUS_REGION_SPAN_SHARED_PORTS_PER_MIDDLE_EDGE, + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("top-main", "mid-left"), + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("top-main", "mid-right"), + ) + + plainSolver.solve() + + expect(plainSolver.solved).toBe(true) + expect(plainSolver.failed).toBe(false) + + const solvedRoutes = plainSolver.getOutput().solvedRoutes ?? [] + const routesUsingMidLeft = solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === "mid-left"), + ) + const routesUsingMidRight = solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === "mid-right"), + ) + + expect(solvedRoutes).toHaveLength(BUS_REGION_SPAN_ROUTE_COUNT) + expect(routesUsingMidLeft.length).toBeGreaterThan(0) + expect(routesUsingMidRight.length).toBeGreaterThan(0) +}) + +test("repro: current bus solver fails on the split-middle span fixture", () => { + const { topology, problem } = loadSerializedHyperGraph(busRegionSpanFixture) + const busSolver = new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 50_000, + }) + const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) + + const midLeftRegionIndex = regionIndexBySerializedId.get("mid-left") + const midRightRegionIndex = regionIndexBySerializedId.get("mid-right") + + expect(midLeftRegionIndex).toBeDefined() + expect(midRightRegionIndex).toBeDefined() + expect( + busSolver.centerGoalHopDistanceByRegion[midLeftRegionIndex!], + ).toBeGreaterThan(2) + expect( + busSolver.centerGoalHopDistanceByRegion[midRightRegionIndex!], + ).toBeGreaterThan(2) + + busSolver.solve() + + expect(busSolver.solved).toBe(false) + expect(busSolver.failed).toBe(true) + expect(busSolver.error).toBeTruthy() +})