diff --git a/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts new file mode 100644 index 0000000..22fd2b3 --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts @@ -0,0 +1,113 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import { + TinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, +} from "../core" +import { splitOverloadedRouteEndpointPorts } from "./splitOverloadedRouteEndpointPorts" +import { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" + +export class TinyHyperGraphAutoRepairFallbackSolver extends BaseSolver { + primarySolver: TinyHyperGraphSolver + fallbackSolver?: TinyHyperGraphVirtualFanoutSolver + fallbackTriggered = false + + constructor( + public topology: TinyHyperGraphTopology, + public problem: TinyHyperGraphProblem, + public options?: TinyHyperGraphSolverOptions, + ) { + super() + this.primarySolver = new TinyHyperGraphSolver(topology, problem, options) + this.activeSubSolver = this.primarySolver + this.MAX_ITERATIONS = this.primarySolver.MAX_ITERATIONS * 2 + 2 + } + + override _step() { + const activeSolver = this.activeSubSolver + + if (!activeSolver) { + this.failed = true + this.error = "Auto-repair fallback solver has no active solver" + return + } + + activeSolver.step() + this.updateStats() + + if (activeSolver.solved) { + this.solved = true + return + } + + if (!activeSolver.failed) { + return + } + + if ( + activeSolver === this.primarySolver && + this.didSolverRunOutOfIterations(this.primarySolver) + ) { + this.startFallbackSolver() + return + } + + this.failed = true + this.error = activeSolver.error + this.activeSubSolver = activeSolver + } + + private didSolverRunOutOfIterations(solver: TinyHyperGraphSolver) { + return ( + solver.failed && + solver.iterations >= solver.MAX_ITERATIONS && + solver.error === `${solver.getSolverName()} ran out of iterations` + ) + } + + private startFallbackSolver() { + const split = splitOverloadedRouteEndpointPorts(this.topology, this.problem) + this.fallbackSolver = new TinyHyperGraphVirtualFanoutSolver( + split.topology, + split.problem, + this.options, + ) + this.fallbackTriggered = true + this.failedSubSolvers = [this.primarySolver] + this.activeSubSolver = this.fallbackSolver + this.updateStats() + } + + private updateStats() { + const activeSolver = this.activeSubSolver + + this.stats = { + ...activeSolver?.stats, + autoRepairFallback: true, + autoRepairFallbackTriggered: this.fallbackTriggered, + autoRepairStage: + activeSolver === this.fallbackSolver ? "fallback" : "primary", + primaryIterations: this.primarySolver.iterations, + fallbackIterations: this.fallbackSolver?.iterations ?? 0, + primaryError: this.primarySolver.error, + } + } + + override visualize(): GraphicsObject { + return this.activeSubSolver?.visualize() ?? super.visualize() + } + + override preview(): GraphicsObject { + return this.activeSubSolver?.preview() ?? super.preview() + } + + override getOutput() { + const solvedSolver = this.fallbackSolver?.solved + ? this.fallbackSolver + : this.primarySolver + + return solvedSolver.getOutput() + } +} diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts new file mode 100644 index 0000000..64a75fa --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts @@ -0,0 +1 @@ +export { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver/TinyHyperGraphVirtualFanoutSolver" diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/TinyHyperGraphVirtualFanoutSolver.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/TinyHyperGraphVirtualFanoutSolver.ts new file mode 100644 index 0000000..dfbff1b --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/TinyHyperGraphVirtualFanoutSolver.ts @@ -0,0 +1,207 @@ +import { + createEmptyRegionIntersectionCache, + TinyHyperGraphSolver, + type Candidate, + type TinyHyperGraphProblem, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, +} from "../../core" +import type { PortId, RegionId, RouteId } from "../../types" +import { appendInt32, cloneVirtualFanoutPort } from "./cloneVirtualFanoutPort" + +type SolvedRouteSegment = { + regionId: RegionId + fromPortId: PortId + toPortId: PortId +} + +/** + * Routes every connection as if intermediate ports have unbounded virtual + * fanout capacity, then duplicates and spaces any different-net reused ports in + * the assembled TinyHyperGraphSolver output. + * + * Real route endpoints are still reserved by net. Use + * splitOverloadedRouteEndpointPorts first when multiple different-net route + * endpoints share the same physical port. + */ +export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { + readonly solvedRouteSegmentsByRouteId: Array + readonly virtualSharedPortSpacing: number + + constructor( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options?: TinyHyperGraphSolverOptions, + ) { + super(topology, problem, options) + this.solvedRouteSegmentsByRouteId = Array.from( + { length: problem.routeCount }, + () => undefined, + ) + this.virtualSharedPortSpacing = 0.2 + } + + override onPathFound(finalCandidate: Candidate) { + const currentRouteId = this.state.currentRouteId + + if (currentRouteId === undefined) return + + this.routeSuccessCountByRouteId[currentRouteId] += 1 + this.solvedRouteSegmentsByRouteId[currentRouteId] = + this.getSolvedPathSegments(finalCandidate) + + this.state.candidateQueue.clear() + this.state.currentRouteNetId = undefined + this.state.currentRouteId = undefined + } + + override onAllRoutesRouted() { + if ( + this.solvedRouteSegmentsByRouteId.some( + (routeSegments) => routeSegments === undefined, + ) + ) { + super.onAllRoutesRouted() + return + } + + this.assembleFanoutSolution() + this.solved = true + } + + protected assembleFanoutSolution() { + const { topology, problem, state } = this + const clonedPortCountByOriginalPortId = new Map() + const clonedPortIdByOriginalPortIdAndNetId = new Map() + let virtualFanoutClonedPortCount = 0 + + state.portAssignment.fill(-1) + state.regionSegments = Array.from( + { length: topology.regionCount }, + () => [], + ) + state.regionIntersectionCaches = Array.from( + { length: topology.regionCount }, + () => createEmptyRegionIntersectionCache(), + ) + state.currentRouteId = undefined + state.currentRouteNetId = undefined + state.unroutedRoutes = [] + state.candidateQueue.clear() + this.resetCandidateBestCosts() + state.goalPortId = -1 + + const getPortIdForNet = ( + originalPortId: PortId, + adjacentPortId: PortId, + routeNetId: number, + ) => { + const assignedNetId = state.portAssignment[originalPortId] + + if (assignedNetId === -1 || assignedNetId === routeNetId) { + state.portAssignment[originalPortId] = routeNetId + return originalPortId + } + + const cloneKey = `${originalPortId}:${routeNetId}` + const existingClonePortId = + clonedPortIdByOriginalPortIdAndNetId.get(cloneKey) + if (existingClonePortId !== undefined) { + state.portAssignment[existingClonePortId] = routeNetId + return existingClonePortId + } + + const cloneIndex = + clonedPortCountByOriginalPortId.get(originalPortId) ?? 0 + const { clonedPortId } = cloneVirtualFanoutPort({ + topology, + problem, + originalPortId, + adjacentPortId, + routeNetId, + cloneIndex, + spacing: this.virtualSharedPortSpacing, + }) + + state.portAssignment = appendInt32(state.portAssignment, routeNetId)! + clonedPortCountByOriginalPortId.set(originalPortId, cloneIndex + 1) + clonedPortIdByOriginalPortIdAndNetId.set(cloneKey, clonedPortId) + virtualFanoutClonedPortCount += 1 + + return clonedPortId + } + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + const routeNetId = problem.routeNet[routeId] + const originalRouteStartPortId = problem.routeStartPort[routeId] + const originalRouteEndPortId = problem.routeEndPort[routeId] + state.currentRouteNetId = routeNetId + + for (const { regionId, fromPortId, toPortId } of this + .solvedRouteSegmentsByRouteId[routeId] ?? []) { + const routedFromPortId = getPortIdForNet( + fromPortId, + toPortId, + routeNetId, + ) + const routedToPortId = getPortIdForNet(toPortId, fromPortId, routeNetId) + + if (originalRouteStartPortId === fromPortId) { + problem.routeStartPort[routeId] = routedFromPortId + } + if (originalRouteEndPortId === fromPortId) { + problem.routeEndPort[routeId] = routedFromPortId + } + if (originalRouteStartPortId === toPortId) { + problem.routeStartPort[routeId] = routedToPortId + } + if (originalRouteEndPortId === toPortId) { + problem.routeEndPort[routeId] = routedToPortId + } + + state.regionSegments[regionId].push([ + routeId, + routedFromPortId, + routedToPortId, + ]) + this.appendSegmentToRegionCache( + regionId, + routedFromPortId, + routedToPortId, + ) + } + } + + state.currentRouteNetId = undefined + const differentNetSharedPortCount = + this.getDifferentNetSharedPortCountFromSegments() + + this.stats = { + ...this.stats, + virtualFanout: true, + capturedPathCount: problem.routeCount, + virtualFanoutClonedPortCount, + differentNetSharedPortCount, + maxRegionCost: this.getMaxRegionCost(), + } + } + + private getDifferentNetSharedPortCountFromSegments() { + const netIdsByPortId = new Map>() + + for (const regionSegments of this.state.regionSegments) { + for (const [routeId, fromPortId, toPortId] of regionSegments) { + const routeNetId = this.problem.routeNet[routeId] + for (const portId of [fromPortId, toPortId]) { + const netIds = netIdsByPortId.get(portId) ?? new Set() + netIds.add(routeNetId) + netIdsByPortId.set(portId, netIds) + } + } + } + + return Array.from(netIdsByPortId.values()).filter( + (netIds) => netIds.size > 1, + ).length + } +} diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/cloneVirtualFanoutPort.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/cloneVirtualFanoutPort.ts new file mode 100644 index 0000000..c13ffb5 --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/cloneVirtualFanoutPort.ts @@ -0,0 +1,169 @@ +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "../../core" +import type { PortId } from "../../types" + +export const appendInt8 = (source: Int8Array, value: number) => { + const next = new Int8Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +export const appendInt32 = (source: Int32Array | undefined, value: number) => { + if (!source) return undefined + const next = new Int32Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +const appendFloat64 = (source: Float64Array, value: number) => { + const next = new Float64Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +const getCloneOffsetDirection = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + adjacentPortId: PortId, +) => { + const dx = topology.portX[adjacentPortId] - topology.portX[originalPortId] + const dy = topology.portY[adjacentPortId] - topology.portY[originalPortId] + const length = Math.hypot(dx, dy) + + if (length > 0) { + return { + x: dx / length, + y: dy / length, + } + } + + const angleRadians = + ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI + + return { + x: Math.cos(angleRadians), + y: Math.sin(angleRadians), + } +} + +const getCloneOffset = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + adjacentPortId: PortId, + cloneIndex: number, + spacing: number, +) => { + const direction = getCloneOffsetDirection( + topology, + originalPortId, + adjacentPortId, + ) + const side = cloneIndex % 2 === 0 ? 1 : -1 + const step = Math.floor(cloneIndex / 2) + 1 + const distance = spacing * step * side + + return { + x: direction.x * distance, + y: direction.y * distance, + } +} + +const clonePortMetadata = ( + metadata: unknown, + originalPortId: PortId, + cloneIndex: number, + offsetX: number, + offsetY: number, +) => { + const base = + metadata && typeof metadata === "object" && !Array.isArray(metadata) + ? { ...metadata } + : metadata === undefined + ? {} + : { value: metadata } + + return { + ...base, + virtualPort: true, + virtualFanoutPort: true, + originalPortIndex: originalPortId, + virtualPortCloneIndex: cloneIndex, + virtualPortOffsetX: offsetX, + virtualPortOffsetY: offsetY, + } +} + +export const cloneVirtualFanoutPort = ({ + topology, + problem, + originalPortId, + adjacentPortId, + routeNetId, + cloneIndex, + spacing, +}: { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + originalPortId: PortId + adjacentPortId: PortId + routeNetId: number + cloneIndex: number + spacing: number +}) => { + const cloneOffset = getCloneOffset( + topology, + originalPortId, + adjacentPortId, + cloneIndex, + spacing, + ) + const clonedPortId = topology.portCount + const originalIncidentRegions = + topology.incidentPortRegion[originalPortId] ?? [] + + topology.portCount += 1 + topology.incidentPortRegion.push([...originalIncidentRegions]) + for (const regionId of originalIncidentRegions) { + topology.regionIncidentPorts[regionId]?.push(clonedPortId) + } + topology.portAngleForRegion1 = appendInt32( + topology.portAngleForRegion1, + topology.portAngleForRegion1[originalPortId], + )! + topology.portAngleForRegion2 = topology.portAngleForRegion2 + ? appendInt32( + topology.portAngleForRegion2, + topology.portAngleForRegion2[originalPortId] ?? + topology.portAngleForRegion1[originalPortId], + ) + : undefined + topology.portX = appendFloat64( + topology.portX, + topology.portX[originalPortId] + cloneOffset.x, + ) + topology.portY = appendFloat64( + topology.portY, + topology.portY[originalPortId] + cloneOffset.y, + ) + topology.portZ = appendInt32(topology.portZ, topology.portZ[originalPortId])! + topology.portMetadata?.push( + clonePortMetadata( + topology.portMetadata?.[originalPortId], + originalPortId, + cloneIndex, + cloneOffset.x, + cloneOffset.y, + ), + ) + problem.portSectionMask = appendInt8( + problem.portSectionMask, + problem.portSectionMask[originalPortId] ?? 1, + ) + + return { + clonedPortId, + routeNetId, + } +} diff --git a/lib/auto-repair/index.ts b/lib/auto-repair/index.ts new file mode 100644 index 0000000..f5e6431 --- /dev/null +++ b/lib/auto-repair/index.ts @@ -0,0 +1,7 @@ +export { + splitOverloadedRouteEndpointPorts, + type SplitOverloadedRouteEndpointPortsOptions, + type SplitOverloadedRouteEndpointPortsResult, +} from "./splitOverloadedRouteEndpointPorts" +export { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" +export { TinyHyperGraphAutoRepairFallbackSolver } from "./TinyHyperGraphAutoRepairFallbackSolver" diff --git a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts new file mode 100644 index 0000000..550108d --- /dev/null +++ b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts @@ -0,0 +1,260 @@ +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "../core" +import type { PortId, RouteId } from "../types" + +export interface SplitOverloadedRouteEndpointPortsOptions { + maxRouteEndpointsPerPort?: number + clonedPortSpacing?: number +} + +export interface SplitOverloadedRouteEndpointPortsResult { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + clonedPortCount: number + clonedPortIdsByOriginalPortId: Map +} + +type RouteEndpointUsage = { + routeId: RouteId + side: "start" | "end" +} + +const cloneMetadataWithVirtualPortInfo = ( + metadata: unknown, + originalPortId: PortId, + cloneIndex: number, + offsetX: number, + offsetY: number, +) => { + const base = + metadata && typeof metadata === "object" && !Array.isArray(metadata) + ? { ...metadata } + : metadata === undefined + ? {} + : { value: metadata } + + return { + ...base, + virtualPort: true, + originalPortIndex: originalPortId, + virtualPortCloneIndex: cloneIndex, + virtualPortOffsetX: offsetX, + virtualPortOffsetY: offsetY, + } +} + +const getCloneOffsetDirection = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + adjacentPortId: PortId, +) => { + const dx = topology.portX[adjacentPortId] - topology.portX[originalPortId] + const dy = topology.portY[adjacentPortId] - topology.portY[originalPortId] + const length = Math.hypot(dx, dy) + + if (length > 0) { + return { + x: dx / length, + y: dy / length, + } + } + + const angleRadians = + ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI + + return { + x: Math.cos(angleRadians), + y: Math.sin(angleRadians), + } +} + +const getCloneOffset = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + adjacentPortId: PortId, + cloneIndex: number, + spacing: number, +) => { + if (spacing <= 0) { + return { x: 0, y: 0 } + } + + const direction = getCloneOffsetDirection( + topology, + originalPortId, + adjacentPortId, + ) + const side = cloneIndex % 2 === 0 ? 1 : -1 + const step = Math.floor(cloneIndex / 2) + 1 + const distance = spacing * step * side + + return { + x: direction.x * distance, + y: direction.y * distance, + } +} + +const appendInt32 = (source: Int32Array | undefined, value: number) => { + if (!source) return undefined + const next = new Int32Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +const appendFloat64 = (source: Float64Array, value: number) => { + const next = new Float64Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +const getRouteEndpointUsagesByPortId = (problem: TinyHyperGraphProblem) => { + const usagesByPortId = new Map() + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + for (const side of ["start", "end"] as const) { + const portId = + side === "start" + ? problem.routeStartPort[routeId] + : problem.routeEndPort[routeId] + const usages = usagesByPortId.get(portId) ?? [] + usages.push({ routeId, side }) + usagesByPortId.set(portId, usages) + } + } + + return usagesByPortId +} + +export const splitOverloadedRouteEndpointPorts = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options: SplitOverloadedRouteEndpointPortsOptions = {}, +): SplitOverloadedRouteEndpointPortsResult => { + const maxRouteEndpointsPerPort = Math.max( + 1, + Math.floor(options.maxRouteEndpointsPerPort ?? 1), + ) + const clonedPortSpacing = Math.max(0, options.clonedPortSpacing ?? 0.2) + const routeStartPort = new Int32Array(problem.routeStartPort) + const routeEndPort = new Int32Array(problem.routeEndPort) + const regionIncidentPorts = topology.regionIncidentPorts.map((ports) => [ + ...ports, + ]) + const incidentPortRegion = topology.incidentPortRegion.map((regions) => [ + ...regions, + ]) + const portAngleForRegion1 = [...topology.portAngleForRegion1] + const portAngleForRegion2 = topology.portAngleForRegion2 + ? [...topology.portAngleForRegion2] + : undefined + const portSectionMask = [...problem.portSectionMask] + let portX = new Float64Array(topology.portX) + let portY = new Float64Array(topology.portY) + let portZ = new Int32Array(topology.portZ) + const portMetadata = topology.portMetadata + ? [...topology.portMetadata] + : undefined + const clonedPortIdsByOriginalPortId = new Map() + + for (const [originalPortId, usages] of getRouteEndpointUsagesByPortId( + problem, + )) { + if (usages.length <= maxRouteEndpointsPerPort) { + continue + } + + for ( + let usageIndex = maxRouteEndpointsPerPort; + usageIndex < usages.length; + usageIndex++ + ) { + const usage = usages[usageIndex]! + const cloneIndex = usageIndex - maxRouteEndpointsPerPort + const clonedPortId = incidentPortRegion.length + const originalIncidentRegions = + topology.incidentPortRegion[originalPortId] ?? [] + const adjacentPortId = + usage.side === "start" + ? problem.routeEndPort[usage.routeId] + : problem.routeStartPort[usage.routeId] + const cloneOffset = getCloneOffset( + topology, + originalPortId, + adjacentPortId, + cloneIndex, + clonedPortSpacing, + ) + + incidentPortRegion.push([...originalIncidentRegions]) + for (const regionId of originalIncidentRegions) { + regionIncidentPorts[regionId]?.push(clonedPortId) + } + + portAngleForRegion1.push(topology.portAngleForRegion1[originalPortId]) + portAngleForRegion2?.push( + topology.portAngleForRegion2?.[originalPortId] ?? + topology.portAngleForRegion1[originalPortId], + ) + portX = appendFloat64( + portX, + topology.portX[originalPortId] + cloneOffset.x, + ) + portY = appendFloat64( + portY, + topology.portY[originalPortId] + cloneOffset.y, + ) + portZ = appendInt32(portZ, topology.portZ[originalPortId])! + portSectionMask.push(problem.portSectionMask[originalPortId] ?? 1) + portMetadata?.push( + cloneMetadataWithVirtualPortInfo( + topology.portMetadata?.[originalPortId], + originalPortId, + cloneIndex, + cloneOffset.x, + cloneOffset.y, + ), + ) + + const clonedPortIds = + clonedPortIdsByOriginalPortId.get(originalPortId) ?? [] + clonedPortIds.push(clonedPortId) + clonedPortIdsByOriginalPortId.set(originalPortId, clonedPortIds) + + if (usage.side === "start") { + routeStartPort[usage.routeId] = clonedPortId + } else { + routeEndPort[usage.routeId] = clonedPortId + } + } + } + + const topologyWithSplitPorts: TinyHyperGraphTopology = { + ...topology, + portCount: incidentPortRegion.length, + regionIncidentPorts, + incidentPortRegion, + portAngleForRegion1: Int32Array.from(portAngleForRegion1), + portAngleForRegion2: portAngleForRegion2 + ? Int32Array.from(portAngleForRegion2) + : undefined, + portX, + portY, + portZ, + portMetadata, + } + + return { + topology: topologyWithSplitPorts, + problem: { + ...problem, + portSectionMask: Int8Array.from(portSectionMask), + routeStartPort, + routeEndPort, + routeNet: new Int32Array(problem.routeNet), + regionNetId: new Int32Array(problem.regionNetId), + }, + clonedPortCount: topologyWithSplitPorts.portCount - topology.portCount, + clonedPortIdsByOriginalPortId, + } +} diff --git a/lib/index.ts b/lib/index.ts index b3f5c40..3752994 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,6 +2,7 @@ export * from "./core" export * from "./poly" export * from "./bus-solver" export * from "./region-graph" +export * from "./auto-repair" export { DEFAULT_MIN_VIA_PAD_DIAMETER, TRACE_VIA_MARGIN, diff --git a/pages/cm5io/cm5io-hypergraph.page.tsx b/pages/cm5io/cm5io-hypergraph.page.tsx index bc606d6..5e74895 100644 --- a/pages/cm5io/cm5io-hypergraph.page.tsx +++ b/pages/cm5io/cm5io-hypergraph.page.tsx @@ -2,7 +2,7 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" import { convertPortPointPathingSolverInputToSerializedHyperGraph, - TinyHyperGraphSolver, + TinyHyperGraphAutoRepairFallbackSolver, } from "lib/index" import { useEffect, useState } from "react" import { Debugger } from "../components/Debugger" @@ -74,17 +74,21 @@ export default function Cm5ioHyperGraphPage() { return (
- CM5IO hypergraph fixture. The debugger uses the hypergraph solver with a - `MAX_ITERATIONS` budget of `50_000`. + CM5IO hypergraph fixture. The debugger uses the explicit auto-repair + fallback with a `MAX_ITERATIONS` budget of `50_000`.
{ const { topology, problem } = loadSerializedHyperGraph(graph) - return new TinyHyperGraphSolver(topology, problem, { - MAX_ITERATIONS: 50_000, - }) + return new TinyHyperGraphAutoRepairFallbackSolver( + topology, + problem, + { + MAX_ITERATIONS: 50_000, + }, + ) }} />
diff --git a/pages/dataset-srj13.page.tsx b/pages/dataset-srj13.page.tsx index e24379c..9e7f943 100644 --- a/pages/dataset-srj13.page.tsx +++ b/pages/dataset-srj13.page.tsx @@ -3,7 +3,7 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { type ChangeEvent, useEffect, useMemo, useState } from "react" import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" import { - TinyHyperGraphSolver, + TinyHyperGraphAutoRepairFallbackSolver, type TinyHyperGraphSolverOptions, } from "lib/index" import { Debugger } from "./components/Debugger" @@ -279,7 +279,7 @@ export default function DatasetSrj13Page() { createSolver={() => { const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - return new TinyHyperGraphSolver( + return new TinyHyperGraphAutoRepairFallbackSolver( topology, problem, getSolverOptions(benchmarkCase), diff --git a/pages/port-chokepoint.page.tsx b/pages/port-chokepoint.page.tsx index 2baee30..a0c3534 100644 --- a/pages/port-chokepoint.page.tsx +++ b/pages/port-chokepoint.page.tsx @@ -1,12 +1,12 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" -import { TinyHyperGraphSolver } from "lib/index" +import { TinyHyperGraphAutoRepairFallbackSolver } 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) - return new TinyHyperGraphSolver(topology, problem, { + return new TinyHyperGraphAutoRepairFallbackSolver(topology, problem, { MAX_ITERATIONS: 20_000, STATIC_REACHABILITY_PRECHECK: false, }) @@ -20,8 +20,9 @@ export default function PortChokepointPage() { left endpoint to the top right endpoint, while connection-b{" "} routes from the bottom left endpoint to the bottom right endpoint. Both nets must pass through the single left-center-choke and{" "} - center-right-choke ports, so after one route claims the - corridor the other route has no valid port-disjoint path. + center-right-choke ports. The debugger uses the explicit + auto-repair fallback so reused choke ports are duplicated and spaced in + the final solution.
{ return parsedValue } +const parseSolverMode = (rawValue: string | undefined): SolverMode => { + if ( + rawValue === "core" || + rawValue === "duplicate-space" || + rawValue === "auto-repair" + ) { + return rawValue + } + + return usageError(`Invalid --solver value: ${rawValue ?? ""}`) +} + const parseArgs = () => { let limit: number | null = null let sampleName: string | null = null + let solverMode: SolverMode = "auto-repair" let maxIterations = 1_000_000 let strict = false @@ -113,6 +134,12 @@ const parseArgs = () => { continue } + if (arg === "--solver") { + solverMode = parseSolverMode(process.argv[index + 1]) + index += 1 + continue + } + if (arg === "--max-iterations") { maxIterations = parsePositiveInteger(arg, process.argv[index + 1]) index += 1 @@ -128,7 +155,7 @@ const parseArgs = () => { usageError("Use either --limit or --sample, not both") } - return { limit, sampleName, maxIterations, strict } + return { limit, sampleName, solverMode, maxIterations, strict } } const formatSeconds = (durationMs: number) => @@ -221,16 +248,30 @@ const getSolverOptions = ( const runSample = ( sample: Srj13BenchmarkSample, maxIterations: number, + solverMode: SolverMode, ): BenchmarkResult => { const startTime = performance.now() const benchmarkCase = sample.tinyHypergraphBenchmark const serializedHyperGraph = normalizeSerializedHyperGraph(benchmarkCase) const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - const solver = new TinyHyperGraphSolver( - topology, - problem, - getSolverOptions(benchmarkCase, maxIterations), - ) + const solverOptions = getSolverOptions(benchmarkCase, maxIterations) + const solver = + solverMode === "duplicate-space" + ? (() => { + const split = splitOverloadedRouteEndpointPorts(topology, problem) + return new TinyHyperGraphVirtualFanoutSolver( + split.topology, + split.problem, + solverOptions, + ) + })() + : solverMode === "auto-repair" + ? new TinyHyperGraphAutoRepairFallbackSolver( + topology, + problem, + solverOptions, + ) + : new TinyHyperGraphSolver(topology, problem, solverOptions) try { solver.solve() @@ -276,17 +317,17 @@ const runSample = ( } const main = () => { - const { limit, sampleName, maxIterations, strict } = parseArgs() + const { limit, sampleName, solverMode, maxIterations, strict } = parseArgs() const allSamples = srj13Samples as Srj13BenchmarkSample[] const selectedSamples = getSelectedSamples(allSamples, limit, sampleName) const results: BenchmarkResult[] = [] console.log( - `dataset=srj13 solver=core samples=${selectedSamples.length}/${allSamples.length} maxIterations=${maxIterations}`, + `dataset=srj13 solver=${solverMode} samples=${selectedSamples.length}/${allSamples.length} maxIterations=${maxIterations}`, ) for (const sample of selectedSamples) { - const result = runSample(sample, maxIterations) + const result = runSample(sample, maxIterations, solverMode) results.push(result) console.log( diff --git a/tests/solver/split-overloaded-route-endpoint-ports.test.ts b/tests/solver/split-overloaded-route-endpoint-ports.test.ts new file mode 100644 index 0000000..ac5fa59 --- /dev/null +++ b/tests/solver/split-overloaded-route-endpoint-ports.test.ts @@ -0,0 +1,85 @@ +import { expect, test } from "bun:test" +import { + splitOverloadedRouteEndpointPorts, + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib/index" + +const createSharedEndpointProblem = (): { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem +} => { + const topology: TinyHyperGraphTopology = { + portCount: 2, + regionCount: 3, + regionIncidentPorts: [[0], [0, 1], [1]], + incidentPortRegion: [ + [1, 0], + [1, 2], + ], + regionWidth: new Float64Array([1, 4, 1]), + regionHeight: new Float64Array([1, 1, 1]), + regionCenterX: new Float64Array([-2, 0, 2]), + regionCenterY: new Float64Array([0, 0, 0]), + portAngleForRegion1: new Int32Array([0, 18000]), + portAngleForRegion2: new Int32Array([18000, 0]), + portX: new Float64Array([-1, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + portMetadata: [{ portId: "start" }, { portId: "end" }], + } + + const problem: TinyHyperGraphProblem = { + routeCount: 2, + portSectionMask: new Int8Array([1, 1]), + routeMetadata: [ + { + connectionId: "route-a", + startRegionId: "source", + endRegionId: "sink", + }, + { + connectionId: "route-b", + startRegionId: "source", + endRegionId: "sink", + }, + ], + routeStartPort: new Int32Array([0, 0]), + routeEndPort: new Int32Array([1, 1]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1, -1, -1]), + } + + return { topology, problem } +} + +test("splitting overloaded route endpoint ports lets different-net routes share a physical endpoint", () => { + const { topology, problem } = createSharedEndpointProblem() + const rawSolver = new TinyHyperGraphSolver(topology, problem) + + rawSolver.solve() + + expect(rawSolver.solved).toBe(false) + expect(rawSolver.failed).toBe(true) + + const repaired = splitOverloadedRouteEndpointPorts(topology, problem) + const repairedSolver = new TinyHyperGraphSolver( + repaired.topology, + repaired.problem, + ) + + repairedSolver.solve() + + expect(repaired.clonedPortCount).toBe(2) + expect(repaired.topology.portCount).toBe(4) + expect(Array.from(repaired.problem.routeStartPort)).toEqual([0, 2]) + expect(Array.from(repaired.problem.routeEndPort)).toEqual([1, 3]) + expect(repaired.topology.portX[2]).toBeGreaterThan(topology.portX[0]) + expect(repaired.topology.portY[2]).toBe(topology.portY[0]) + expect(repaired.topology.portX[3]).toBeLessThan(topology.portX[1]) + expect(repaired.topology.portY[3]).toBe(topology.portY[1]) + expect(repairedSolver.solved).toBe(true) + expect(repairedSolver.failed).toBe(false) + expect(repairedSolver.getOutput().solvedRoutes).toHaveLength(2) +}) diff --git a/tests/solver/virtual-fanout-solver.test.ts b/tests/solver/virtual-fanout-solver.test.ts new file mode 100644 index 0000000..a0ab0f0 --- /dev/null +++ b/tests/solver/virtual-fanout-solver.test.ts @@ -0,0 +1,140 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphAutoRepairFallbackSolver, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, + TinyHyperGraphVirtualFanoutSolver, +} from "lib/index" + +const getDifferentNetSharedPortCount = (solver: TinyHyperGraphSolver) => { + const netIdsByPortId = new Map>() + + for (const regionSegments of solver.state.regionSegments) { + for (const [routeId, fromPortId, toPortId] of regionSegments) { + const routeNetId = solver.problem.routeNet[routeId] + for (const portId of [fromPortId, toPortId]) { + const netIds = netIdsByPortId.get(portId) ?? new Set() + netIds.add(routeNetId) + netIdsByPortId.set(portId, netIds) + } + } + } + + return Array.from(netIdsByPortId.values()).filter((netIds) => netIds.size > 1) + .length +} + +const createSharedInternalPortProblem = (): { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem +} => { + const topology: TinyHyperGraphTopology = { + portCount: 5, + regionCount: 6, + regionIncidentPorts: [[0], [0, 1, 2], [2, 3, 4], [3], [1], [4]], + incidentPortRegion: [ + [1, 0], + [1, 4], + [1, 2], + [2, 3], + [2, 5], + ], + regionWidth: new Float64Array([1, 2, 2, 1, 1, 1]), + regionHeight: new Float64Array([1, 1, 1, 1, 1, 1]), + regionCenterX: new Float64Array([-2, -1, 1, 2, -2, 2]), + regionCenterY: new Float64Array([0, 0, 0, 0.5, -0.5, -0.5]), + portAngleForRegion1: new Int32Array([0, 0, 0, 18000, 18000]), + portAngleForRegion2: new Int32Array([18000, 18000, 0, 0, 0]), + portX: new Float64Array([-1.5, -0.25, 0, 1.5, 1.5]), + portY: new Float64Array([0, 0.25, 0, 0.5, -0.5]), + portZ: new Int32Array([0, 0, 0, 0, 0]), + portMetadata: [ + { portId: "a-start" }, + { portId: "b-start" }, + { portId: "shared-choke" }, + { portId: "a-end" }, + { portId: "b-end" }, + ], + } + + const problem: TinyHyperGraphProblem = { + routeCount: 2, + portSectionMask: new Int8Array(topology.portCount).fill(1), + routeMetadata: [ + { + connectionId: "route-a", + startRegionId: "a-source", + endRegionId: "a-sink", + }, + { + connectionId: "route-b", + startRegionId: "b-source", + endRegionId: "b-sink", + }, + ], + routeStartPort: new Int32Array([0, 1]), + routeEndPort: new Int32Array([3, 4]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1, -1, -1, -1, -1, -1]), + } + + return { topology, problem } +} + +test("virtual fanout solves different-net routes through a shared internal port", () => { + const { topology, problem } = createSharedInternalPortProblem() + const originalPortCount = topology.portCount + const sharedPortX = topology.portX[2] + const sharedPortY = topology.portY[2] + const rawSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100, + }) + + rawSolver.solve() + + expect(rawSolver.solved).toBe(false) + expect(rawSolver.failed).toBe(true) + + const fanoutSolver = new TinyHyperGraphVirtualFanoutSolver( + topology, + problem, + { + MAX_ITERATIONS: 100, + }, + ) + + fanoutSolver.solve() + + expect(fanoutSolver.solved).toBe(true) + expect(fanoutSolver.failed).toBe(false) + expect(fanoutSolver.stats.virtualFanout).toBe(true) + expect(fanoutSolver.stats.virtualFanoutClonedPortCount).toBeGreaterThan(0) + expect(fanoutSolver.stats.differentNetSharedPortCount).toBe(0) + expect(getDifferentNetSharedPortCount(fanoutSolver)).toBe(0) + expect(fanoutSolver.topology.portCount).toBeGreaterThan(originalPortCount) + expect(fanoutSolver.topology.portX[5]).not.toBe(sharedPortX) + expect(fanoutSolver.topology.portY[5]).not.toBe(sharedPortY) + expect(fanoutSolver.getOutput().solvedRoutes).toHaveLength(2) +}) + +test("explicit auto-repair solver falls back to virtual fanout after primary timeout", () => { + const { topology, problem } = createSharedInternalPortProblem() + const solver = new TinyHyperGraphAutoRepairFallbackSolver(topology, problem, { + MAX_ITERATIONS: 100, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.stats.autoRepairFallbackTriggered).toBe(true) + expect(solver.stats.virtualFanout).toBe(true) + expect(solver.stats.differentNetSharedPortCount).toBe(0) + expect( + getDifferentNetSharedPortCount( + solver.fallbackSolver ?? solver.primarySolver, + ), + ).toBe(0) + expect(solver.getOutput().solvedRoutes).toHaveLength(2) +})