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