Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
512 changes: 512 additions & 0 deletions lib/DuplicateCongestedPortSolver.ts

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export interface TinyHyperGraphProblem {
routeNet: Int32Array // NetId[]
/** regionNetId[regionId] = reserved net id for the region, -1 means freely traversable */
regionNetId: Int32Array

/** portPenalty[portId] = extra cost paid when a route traverses the port */
portPenalty?: Float64Array
}

export interface TinyHyperGraphProblemSetup {
Expand Down Expand Up @@ -1129,7 +1132,8 @@ export class TinyHyperGraphSolver extends BaseSolver {
return (
currentCandidate.g +
newRegionCost +
state.regionCongestionCost[nextRegionId]
state.regionCongestionCost[nextRegionId] +
(this.problem.portPenalty?.[neighborPortId] ?? 0)
)
}

Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./core"
export * from "./DuplicateCongestedPortSolver"
export * from "./poly"
export * from "./bus-solver"
export * from "./region-graph"
Expand Down
4 changes: 4 additions & 0 deletions lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ const createProblemWithPortSectionMask = (
routeEndPort: new Int32Array(problem.routeEndPort),
routeNet: new Int32Array(problem.routeNet),
regionNetId: new Int32Array(problem.regionNetId),
portPenalty:
problem.portPenalty === undefined
? undefined
: new Float64Array(problem.portPenalty),
})

const getSectionMaskCandidates = (
Expand Down
4 changes: 4 additions & 0 deletions lib/section-solver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,10 @@ const createSectionRoutePlans = (
routeEndPort,
routeNet: new Int32Array(problem.routeNet),
regionNetId: new Int32Array(problem.regionNetId),
portPenalty:
problem.portPenalty === undefined
? undefined
: new Float64Array(problem.portPenalty),
},
routePlans,
activeRouteIds,
Expand Down
18 changes: 12 additions & 6 deletions pages/port-chokepoint.page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import type { SerializedHyperGraph } from "@tscircuit/hypergraph"
import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph"
import { TinyHyperGraphSolver } from "lib/index"
import { DuplicateCongestedPortSolver, TinyHyperGraphSolver } from "lib/index"
import { portChokepointFixture } from "../tests/fixtures/port-chokepoint.fixture"
import { Debugger } from "./components/Debugger"

const createSolver = (serializedHyperGraph: SerializedHyperGraph) => {
const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph)
const duplicateCongestedPortSolver = new DuplicateCongestedPortSolver(
serializedHyperGraph,
)
duplicateCongestedPortSolver.solve()
const { topology, problem } = loadSerializedHyperGraph(
duplicateCongestedPortSolver.getOutput(),
)

return new TinyHyperGraphSolver(topology, problem, {
MAX_ITERATIONS: 20_000,
STATIC_REACHABILITY_PRECHECK: false,
Expand All @@ -18,10 +25,9 @@ export default function PortChokepointPage() {
<div className="rounded border border-slate-300 bg-white p-3 text-sm text-slate-700">
Port chokepoint repro: <code>connection-a</code> routes from the top
left endpoint to the top right endpoint, while <code>connection-b</code>{" "}
routes from the bottom left endpoint to the bottom right endpoint. Both
nets must pass through the single <code>left-center-choke</code> and{" "}
<code>center-right-choke</code> ports, so after one route claims the
corridor the other route has no valid port-disjoint path.
routes from the bottom left endpoint to the bottom right endpoint. The
page first runs <code>DuplicateCongestedPortSolver</code>, then routes
the repaired topology with the strict core solver.
</div>
<div className="min-h-0 flex-1 overflow-hidden rounded border border-slate-300 bg-white">
<Debugger
Expand Down
4 changes: 4 additions & 0 deletions scripts/benchmarking/hg07-section-benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ const createProblemWithPortSectionMask = (
routeEndPort: new Int32Array(problem.routeEndPort),
routeNet: new Int32Array(problem.routeNet),
regionNetId: new Int32Array(problem.regionNetId),
portPenalty:
problem.portPenalty === undefined
? undefined
: new Float64Array(problem.portPenalty),
})

const getSectionMaskCandidates = (
Expand Down
200 changes: 200 additions & 0 deletions tests/solver/duplicate-congested-port-solver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { expect, test } from "bun:test"
import type { SerializedHyperGraph } from "@tscircuit/hypergraph"
import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph"
import { DuplicateCongestedPortSolver, TinyHyperGraphSolver } from "lib/index"

const createRegion = (
regionId: string,
centerX: number,
centerY: number,
width: number,
height: number,
pointIds: string[],
): SerializedHyperGraph["regions"][number] => ({
regionId,
pointIds,
d: {
center: { x: centerX, y: centerY },
width,
height,
},
})

const createPort = (
portId: string,
region1Id: string,
region2Id: string,
x: number,
y: number,
): SerializedHyperGraph["ports"][number] => ({
portId,
region1Id,
region2Id,
d: { x, y, z: 0 },
})

const getNumber = (value: unknown) =>
typeof value === "number" && Number.isFinite(value) ? value : 0

const getPortIndexBySerializedId = (
topology: ReturnType<typeof loadSerializedHyperGraph>["topology"],
serializedPortId: string,
) =>
topology.portMetadata?.findIndex(
(metadata) =>
typeof metadata === "object" &&
metadata !== null &&
"serializedPortId" in metadata &&
metadata.serializedPortId === serializedPortId,
) ?? -1

const createParallelPortFixture = (): SerializedHyperGraph => ({
regions: [
createRegion("start", -4, 0, 2, 2, ["start-port"]),
createRegion("left", -1, 0, 4, 6, ["start-port", "middle-a", "middle-b"]),
createRegion("right", 1, 0, 4, 6, ["middle-a", "middle-b", "end-port"]),
createRegion("end", 4, 0, 2, 2, ["end-port"]),
],
ports: [
createPort("start-port", "start", "left", -3, 0),
createPort("middle-a", "left", "right", 0, 0),
createPort("middle-b", "left", "right", 0, 2),
createPort("end-port", "right", "end", 3, 0),
],
connections: [
{
connectionId: "connection-a",
startRegionId: "start",
endRegionId: "end",
mutuallyConnectedNetworkId: "net-a",
},
],
})

const createDuplicatePortFixture = (): SerializedHyperGraph => ({
regions: [
createRegion("a-start", -4, 0, 2, 2, ["a-start-port"]),
createRegion("b-start", -4, -0.2, 2, 2, ["b-start-port"]),
createRegion("left", -1, 0, 4, 10, [
"a-start-port",
"b-start-port",
"shared-choke",
"shared-neighbor",
]),
createRegion("right", 1, 0, 4, 10, [
"shared-choke",
"shared-neighbor",
"a-end-port",
"b-end-port",
]),
createRegion("a-end", 4, 0, 2, 2, ["a-end-port"]),
createRegion("b-end", 4, -0.2, 2, 2, ["b-end-port"]),
],
ports: [
createPort("a-start-port", "a-start", "left", -3, 0),
createPort("b-start-port", "b-start", "left", -3, -0.2),
createPort("shared-choke", "left", "right", 0, 0),
createPort("shared-neighbor", "left", "right", 0, 4),
createPort("a-end-port", "right", "a-end", 3, 0),
createPort("b-end-port", "right", "b-end", 3, -0.2),
],
connections: [
{
connectionId: "connection-a",
startRegionId: "a-start",
endRegionId: "a-end",
mutuallyConnectedNetworkId: "net-a",
},
{
connectionId: "connection-b",
startRegionId: "b-start",
endRegionId: "b-end",
mutuallyConnectedNetworkId: "net-b",
},
],
})

test("core solver applies port penalties when choosing an intermediate port", () => {
const { topology, problem } = loadSerializedHyperGraph(
createParallelPortFixture(),
)
const penalizedPortIndex = getPortIndexBySerializedId(topology, "middle-a")
expect(penalizedPortIndex).toBeGreaterThanOrEqual(0)

problem.portPenalty = new Float64Array(topology.portCount)
problem.portPenalty[penalizedPortIndex] = 1_000

const solver = new TinyHyperGraphSolver(topology, problem, {
RIP_THRESHOLD_RAMP_ATTEMPTS: 0,
STATIC_REACHABILITY_PRECHECK: false,
})
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)
expect(
solver.getOutput().solvedRoutes?.[0]?.path.map(({ portId }) => portId),
).toEqual(["start-port", "middle-b", "end-port"])
})

test("duplicate congested port solver duplicates independently reused ports in line with the boundary", () => {
const duplicatePortProximity = 0.2
const solver = new DuplicateCongestedPortSolver(
createDuplicatePortFixture(),
{
duplicatePortProximity,
},
)

solver.solve()

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)
expect(solver.report.portUseCounts["shared-choke"]).toBe(2)
expect(solver.report.duplicatedPorts).toContainEqual({
sourcePortId: "shared-choke",
duplicatePortIds: ["shared-choke::dup1"],
useCount: 2,
})

const output = solver.getOutput()
const sourcePort = output.ports.find(
(port) => port.portId === "shared-choke",
)!
const nearestPort = output.ports.find(
(port) => port.portId === "shared-neighbor",
)!
const duplicatePort = output.ports.find(
(port) => port.portId === "shared-choke::dup1",
)!

const sourcePoint = {
x: getNumber(sourcePort.d?.x),
y: getNumber(sourcePort.d?.y),
}
const nearestVector = {
x: getNumber(nearestPort.d?.x) - sourcePoint.x,
y: getNumber(nearestPort.d?.y) - sourcePoint.y,
}
const duplicateVector = {
x: getNumber(duplicatePort.d?.x) - sourcePoint.x,
y: getNumber(duplicatePort.d?.y) - sourcePoint.y,
}
const duplicateDistance = Math.hypot(duplicateVector.x, duplicateVector.y)
const crossProduct =
nearestVector.x * duplicateVector.y - nearestVector.y * duplicateVector.x

expect(duplicatePort.d?.duplicatedFromPortId).toBe("shared-choke")
expect(duplicatePort.d?.duplicateIndex).toBe(1)
expect(duplicateDistance).toBeGreaterThan(0)
expect(duplicateDistance).toBeLessThanOrEqual(duplicatePortProximity)
expect(Math.abs(crossProduct)).toBeLessThan(1e-9)
expect(output.solvedRoutes).toBeUndefined()
expect(
output.regions
.filter(
(region) => region.regionId === "left" || region.regionId === "right",
)
.every((region) => region.pointIds.includes("shared-choke::dup1")),
).toBe(true)
})
Loading