From 913baa18ecddb6c909296858100605577fcc7696 Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Wed, 20 May 2026 17:06:59 +0200 Subject: [PATCH 1/6] test --- .../TinyHyperGraphAutoRepairFallbackSolver.ts | 119 ++++++++++++ .../TinyHyperGraphVirtualFanoutSolver.ts | 112 +++++++++++ lib/auto-repair/index.ts | 26 +++ .../splitOverloadedRouteEndpointPorts.ts | 183 ++++++++++++++++++ lib/core.ts | 67 +++++++ lib/index.ts | 1 + scripts/benchmarking/srj13-core.ts | 62 +++++- tests/solver/port-chokepoint-repro.test.ts | 1 + ...it-overloaded-route-endpoint-ports.test.ts | 81 ++++++++ tests/solver/virtual-fanout-solver.test.ts | 107 ++++++++++ 10 files changed, 749 insertions(+), 10 deletions(-) create mode 100644 lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts create mode 100644 lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts create mode 100644 lib/auto-repair/index.ts create mode 100644 lib/auto-repair/splitOverloadedRouteEndpointPorts.ts create mode 100644 tests/solver/split-overloaded-route-endpoint-ports.test.ts create mode 100644 tests/solver/virtual-fanout-solver.test.ts diff --git a/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts new file mode 100644 index 0000000..9c878a1 --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts @@ -0,0 +1,119 @@ +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, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }) + 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, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }, + ) + 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..0d1491d --- /dev/null +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts @@ -0,0 +1,112 @@ +import { + createEmptyRegionIntersectionCache, + TinyHyperGraphSolver, + type Candidate, + type TinyHyperGraphProblem, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, +} from "../core" +import type { PortId, RegionId, RouteId } from "../types" + +type SolvedRouteSegment = { + regionId: RegionId + fromPortId: PortId + toPortId: PortId +} + +/** + * Routes every connection as if intermediate ports have unbounded virtual + * fanout capacity, then assembles the discovered paths into a normal + * 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 + + constructor( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options?: TinyHyperGraphSolverOptions, + ) { + super(topology, problem, { + ...options, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }) + this.solvedRouteSegmentsByRouteId = Array.from( + { length: problem.routeCount }, + () => undefined, + ) + } + + 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 + + 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 + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + state.currentRouteNetId = problem.routeNet[routeId] + + for (const { regionId, fromPortId, toPortId } of this + .solvedRouteSegmentsByRouteId[routeId] ?? []) { + state.regionSegments[regionId].push([routeId, fromPortId, toPortId]) + state.portAssignment[fromPortId] = state.currentRouteNetId + state.portAssignment[toPortId] = state.currentRouteNetId + this.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } + } + + state.currentRouteNetId = undefined + + this.stats = { + ...this.stats, + virtualFanout: true, + capturedPathCount: problem.routeCount, + maxRegionCost: this.getMaxRegionCost(), + } + } +} diff --git a/lib/auto-repair/index.ts b/lib/auto-repair/index.ts new file mode 100644 index 0000000..21cbb9a --- /dev/null +++ b/lib/auto-repair/index.ts @@ -0,0 +1,26 @@ +import { setTinyHyperGraphIterationTimeoutFallbackFactory } from "../core" +import { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" +import { splitOverloadedRouteEndpointPorts } from "./splitOverloadedRouteEndpointPorts" + +export { + splitOverloadedRouteEndpointPorts, + type SplitOverloadedRouteEndpointPortsOptions, + type SplitOverloadedRouteEndpointPortsResult, +} from "./splitOverloadedRouteEndpointPorts" +export { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" +export { TinyHyperGraphAutoRepairFallbackSolver } from "./TinyHyperGraphAutoRepairFallbackSolver" + +setTinyHyperGraphIterationTimeoutFallbackFactory( + (topology, problem, options) => { + const split = splitOverloadedRouteEndpointPorts(topology, problem) + + return new TinyHyperGraphVirtualFanoutSolver( + split.topology, + split.problem, + { + ...options, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }, + ) + }, +) diff --git a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts new file mode 100644 index 0000000..e909526 --- /dev/null +++ b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts @@ -0,0 +1,183 @@ +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "../core" +import type { PortId, RouteId } from "../types" + +export interface SplitOverloadedRouteEndpointPortsOptions { + maxRouteEndpointsPerPort?: 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, +) => { + const base = + metadata && typeof metadata === "object" && !Array.isArray(metadata) + ? { ...metadata } + : metadata === undefined + ? {} + : { value: metadata } + + return { + ...base, + virtualPort: true, + originalPortIndex: originalPortId, + virtualPortCloneIndex: cloneIndex, + } +} + +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 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 clonedPortId = incidentPortRegion.length + const originalIncidentRegions = + topology.incidentPortRegion[originalPortId] ?? [] + + 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]) + portY = appendFloat64(portY, topology.portY[originalPortId]) + portZ = appendInt32(portZ, topology.portZ[originalPortId])! + portSectionMask.push(problem.portSectionMask[originalPortId] ?? 1) + portMetadata?.push( + cloneMetadataWithVirtualPortInfo( + topology.portMetadata?.[originalPortId], + originalPortId, + usageIndex - maxRouteEndpointsPerPort, + ), + ) + + 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/core.ts b/lib/core.ts index 78627e2..607fe99 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -185,6 +185,7 @@ export interface TinyHyperGraphSolverOptions { VERBOSE?: boolean STATIC_REACHABILITY_PRECHECK?: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number + AUTO_REPAIR_ON_ITERATION_TIMEOUT?: boolean } export interface TinyHyperGraphSolverOptionTarget { @@ -200,6 +201,23 @@ export interface TinyHyperGraphSolverOptionTarget { VERBOSE: boolean STATIC_REACHABILITY_PRECHECK: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number + AUTO_REPAIR_ON_ITERATION_TIMEOUT?: boolean +} + +export type TinyHyperGraphIterationTimeoutFallbackFactory = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options: TinyHyperGraphSolverOptions, +) => TinyHyperGraphSolver | undefined + +let iterationTimeoutFallbackFactory: + | TinyHyperGraphIterationTimeoutFallbackFactory + | undefined + +export const setTinyHyperGraphIterationTimeoutFallbackFactory = ( + factory: TinyHyperGraphIterationTimeoutFallbackFactory | undefined, +) => { + iterationTimeoutFallbackFactory = factory } export const applyTinyHyperGraphSolverOptions = ( @@ -248,6 +266,10 @@ export const applyTinyHyperGraphSolverOptions = ( solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS = options.STATIC_REACHABILITY_PRECHECK_MAX_HOPS } + if (options.AUTO_REPAIR_ON_ITERATION_TIMEOUT !== undefined) { + solver.AUTO_REPAIR_ON_ITERATION_TIMEOUT = + options.AUTO_REPAIR_ON_ITERATION_TIMEOUT + } } export const getTinyHyperGraphSolverOptions = ( @@ -266,6 +288,7 @@ export const getTinyHyperGraphSolverOptions = ( STATIC_REACHABILITY_PRECHECK: solver.STATIC_REACHABILITY_PRECHECK, STATIC_REACHABILITY_PRECHECK_MAX_HOPS: solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: solver.AUTO_REPAIR_ON_ITERATION_TIMEOUT, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -306,6 +329,7 @@ export class TinyHyperGraphSolver extends BaseSolver { VERBOSE = false STATIC_REACHABILITY_PRECHECK = true STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 + AUTO_REPAIR_ON_ITERATION_TIMEOUT = true constructor( public topology: TinyHyperGraphTopology, @@ -1142,6 +1166,49 @@ export class TinyHyperGraphSolver extends BaseSolver { neverSuccessfullyRoutedRouteCount: neverSuccessfullyRoutedRoutes.length, } this.logNeverSuccessfullyRoutedRoutes() + + if ( + !this.AUTO_REPAIR_ON_ITERATION_TIMEOUT || + !iterationTimeoutFallbackFactory + ) { + return + } + + const fallbackSolver = iterationTimeoutFallbackFactory( + this.topology, + this.problem, + { + ...getTinyHyperGraphSolverOptions(this), + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }, + ) + + if (!fallbackSolver) { + return + } + + fallbackSolver.solve() + + this.stats = { + ...this.stats, + autoRepairFallbackTriggered: true, + autoRepairFallbackSolver: fallbackSolver.getSolverName(), + autoRepairFallbackIterations: fallbackSolver.iterations, + autoRepairFallbackError: fallbackSolver.error, + ...fallbackSolver.stats, + } + + if (!fallbackSolver.solved || fallbackSolver.failed) { + return + } + + this.topology = fallbackSolver.topology + this.problem = fallbackSolver.problem + this._problemSetup = undefined + this.state = fallbackSolver.state + this.solved = true + this.failed = false + this.error = null } computeH(neighborPortId: PortId): number { 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/scripts/benchmarking/srj13-core.ts b/scripts/benchmarking/srj13-core.ts index 960d02f..977ccac 100644 --- a/scripts/benchmarking/srj13-core.ts +++ b/scripts/benchmarking/srj13-core.ts @@ -4,11 +4,15 @@ import { } from "@tsci/seveibar.dataset-srj13" import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { + splitOverloadedRouteEndpointPorts, + TinyHyperGraphVirtualFanoutSolver, TinyHyperGraphSolver, type TinyHyperGraphSolverOptions, } from "../../lib/index" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" +type SolverMode = "core" | "fanout-split" | "auto-repair" + type Srj13BenchmarkSample = { sampleName: string tinyHypergraphBenchmark: TinyHypergraphBenchmarkCase @@ -30,12 +34,13 @@ type BenchmarkResult = { const HELP_TEXT = `Usage: ./benchmark-srj13.sh [options] -Run the SRJ13 tiny-hypergraph benchmark against TinyHyperGraphSolver directly. +Run the SRJ13 tiny-hypergraph benchmark directly. This intentionally skips the section pipeline. Many samples are expected to fail. Options: --limit N Run the first N samples from the dataset. --sample NUM Run a specific sample by number or name (e.g. 2, 02, example-02). + --solver MODE Solver mode: core, fanout-split, or auto-repair. Defaults to auto-repair. --max-iterations N Override the core solver iteration cap. Defaults to 1000000. --strict Exit non-zero if any sample fails. --help Show this help text. @@ -44,6 +49,8 @@ Examples: ./benchmark-srj13.sh ./benchmark-srj13.sh --limit 3 ./benchmark-srj13.sh --sample example-02 + ./benchmark-srj13.sh --solver fanout-split --limit 3 + ./benchmark-srj13.sh --solver auto-repair --sample example-02 ./benchmark-srj13.sh --max-iterations 250000 ` @@ -77,9 +84,22 @@ const parsePositiveInteger = (flag: string, rawValue: string | undefined) => { return parsedValue } +const parseSolverMode = (rawValue: string | undefined): SolverMode => { + if ( + rawValue === "core" || + rawValue === "fanout-split" || + 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 +133,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 +154,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 +247,32 @@ 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 === "fanout-split" + ? (() => { + const split = splitOverloadedRouteEndpointPorts(topology, problem) + return new TinyHyperGraphVirtualFanoutSolver( + split.topology, + split.problem, + { + ...solverOptions, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }, + ) + })() + : solverMode === "auto-repair" + ? new TinyHyperGraphSolver(topology, problem, solverOptions) + : new TinyHyperGraphSolver(topology, problem, { + ...solverOptions, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }) try { solver.solve() @@ -276,17 +318,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/port-chokepoint-repro.test.ts b/tests/solver/port-chokepoint-repro.test.ts index 372c5e0..a600ff4 100644 --- a/tests/solver/port-chokepoint-repro.test.ts +++ b/tests/solver/port-chokepoint-repro.test.ts @@ -13,6 +13,7 @@ test("repro: two different nets cannot share the one-port chokepoint", () => { const solver = new TinyHyperGraphSolver(topology, problem, { MAX_ITERATIONS: 20_000, STATIC_REACHABILITY_PRECHECK: false, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, }) const beforeSolveGraphics = solver.visualize() 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..4e55ce7 --- /dev/null +++ b/tests/solver/split-overloaded-route-endpoint-ports.test.ts @@ -0,0 +1,81 @@ +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(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..92b9b3e --- /dev/null +++ b/tests/solver/virtual-fanout-solver.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, + TinyHyperGraphVirtualFanoutSolver, +} from "lib/index" + +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 rawSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100, + AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, + }) + + 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.getOutput().solvedRoutes).toHaveLength(2) +}) + +test("core solver falls back to virtual fanout after running out of iterations", () => { + const { topology, problem } = createSharedInternalPortProblem() + const solver = new TinyHyperGraphSolver(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.getOutput().solvedRoutes).toHaveLength(2) +}) From 45041b118c19b6f7cd27d6d2534c019f6c7c9b62 Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Thu, 21 May 2026 18:53:32 +0200 Subject: [PATCH 2/6] patch --- .../splitOverloadedRouteEndpointPorts.ts | 82 ++++++++++++++++++- ...it-overloaded-route-endpoint-ports.test.ts | 4 + 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts index e909526..352ecd5 100644 --- a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts +++ b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts @@ -3,6 +3,7 @@ import type { PortId, RouteId } from "../types" export interface SplitOverloadedRouteEndpointPortsOptions { maxRouteEndpointsPerPort?: number + clonedPortSpacing?: number } export interface SplitOverloadedRouteEndpointPortsResult { @@ -21,6 +22,8 @@ const cloneMetadataWithVirtualPortInfo = ( metadata: unknown, originalPortId: PortId, cloneIndex: number, + offsetX: number, + offsetY: number, ) => { const base = metadata && typeof metadata === "object" && !Array.isArray(metadata) @@ -34,6 +37,63 @@ const cloneMetadataWithVirtualPortInfo = ( virtualPort: true, originalPortIndex: originalPortId, virtualPortCloneIndex: cloneIndex, + virtualPortOffsetX: offsetX, + virtualPortOffsetY: offsetY, + } +} + +const getCloneOffsetDirection = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, +) => { + const incidentRegions = topology.incidentPortRegion[originalPortId] ?? [] + const firstRegionId = incidentRegions[0] + const secondRegionId = incidentRegions[1] + + if (firstRegionId !== undefined && secondRegionId !== undefined) { + const dx = + topology.regionCenterX[secondRegionId] - + topology.regionCenterX[firstRegionId] + const dy = + topology.regionCenterY[secondRegionId] - + topology.regionCenterY[firstRegionId] + const length = Math.hypot(dx, dy) + + if (length > 0) { + return { + x: -dy / length, + y: dx / length, + } + } + } + + const angleRadians = + ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI + + return { + x: -Math.sin(angleRadians), + y: Math.cos(angleRadians), + } +} + +const getCloneOffset = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + cloneIndex: number, + spacing: number, +) => { + if (spacing <= 0) { + return { x: 0, y: 0 } + } + + const direction = getCloneOffsetDirection(topology, originalPortId) + 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, } } @@ -79,6 +139,7 @@ export const splitOverloadedRouteEndpointPorts = ( 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) => [ @@ -113,9 +174,16 @@ export const splitOverloadedRouteEndpointPorts = ( usageIndex++ ) { const usage = usages[usageIndex]! + const cloneIndex = usageIndex - maxRouteEndpointsPerPort const clonedPortId = incidentPortRegion.length const originalIncidentRegions = topology.incidentPortRegion[originalPortId] ?? [] + const cloneOffset = getCloneOffset( + topology, + originalPortId, + cloneIndex, + clonedPortSpacing, + ) incidentPortRegion.push([...originalIncidentRegions]) for (const regionId of originalIncidentRegions) { @@ -127,15 +195,23 @@ export const splitOverloadedRouteEndpointPorts = ( topology.portAngleForRegion2?.[originalPortId] ?? topology.portAngleForRegion1[originalPortId], ) - portX = appendFloat64(portX, topology.portX[originalPortId]) - portY = appendFloat64(portY, topology.portY[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, - usageIndex - maxRouteEndpointsPerPort, + cloneIndex, + cloneOffset.x, + cloneOffset.y, ), ) diff --git a/tests/solver/split-overloaded-route-endpoint-ports.test.ts b/tests/solver/split-overloaded-route-endpoint-ports.test.ts index 4e55ce7..f9ddf7c 100644 --- a/tests/solver/split-overloaded-route-endpoint-ports.test.ts +++ b/tests/solver/split-overloaded-route-endpoint-ports.test.ts @@ -75,6 +75,10 @@ test("splitting overloaded route endpoint ports lets different-net routes share 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]).toBe(topology.portX[0]) + expect(repaired.topology.portY[2]).not.toBe(topology.portY[0]) + expect(repaired.topology.portX[3]).toBe(topology.portX[1]) + expect(repaired.topology.portY[3]).not.toBe(topology.portY[1]) expect(repairedSolver.solved).toBe(true) expect(repairedSolver.failed).toBe(false) expect(repairedSolver.getOutput().solvedRoutes).toHaveLength(2) From 46c018668f78f5130ed8e5c10b896a04dbc9f490 Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Thu, 21 May 2026 21:35:26 +0200 Subject: [PATCH 3/6] patch --- .../TinyHyperGraphAutoRepairFallbackSolver.ts | 10 +- .../TinyHyperGraphVirtualFanoutSolver.ts | 243 +++++++++++++++++- lib/auto-repair/index.ts | 19 -- lib/core.ts | 67 ----- pages/cm5io/cm5io-hypergraph.page.tsx | 16 +- pages/dataset-srj13.page.tsx | 4 +- pages/port-chokepoint.page.tsx | 9 +- scripts/benchmarking/srj13-core.ts | 17 +- tests/solver/port-chokepoint-repro.test.ts | 1 - tests/solver/virtual-fanout-solver.test.ts | 39 ++- 10 files changed, 297 insertions(+), 128 deletions(-) diff --git a/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts index 9c878a1..22fd2b3 100644 --- a/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts +++ b/lib/auto-repair/TinyHyperGraphAutoRepairFallbackSolver.ts @@ -20,10 +20,7 @@ export class TinyHyperGraphAutoRepairFallbackSolver extends BaseSolver { public options?: TinyHyperGraphSolverOptions, ) { super() - this.primarySolver = new TinyHyperGraphSolver(topology, problem, { - ...options, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, - }) + this.primarySolver = new TinyHyperGraphSolver(topology, problem, options) this.activeSubSolver = this.primarySolver this.MAX_ITERATIONS = this.primarySolver.MAX_ITERATIONS * 2 + 2 } @@ -75,10 +72,7 @@ export class TinyHyperGraphAutoRepairFallbackSolver extends BaseSolver { this.fallbackSolver = new TinyHyperGraphVirtualFanoutSolver( split.topology, split.problem, - { - ...this.options, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, - }, + this.options, ) this.fallbackTriggered = true this.failedSubSolvers = [this.primarySolver] diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts index 0d1491d..8daa0f1 100644 --- a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts @@ -14,6 +14,104 @@ type SolvedRouteSegment = { toPortId: PortId } +const appendInt8 = (source: Int8Array, value: number) => { + const next = new Int8Array(source.length + 1) + next.set(source) + next[source.length] = value + return next +} + +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, +) => { + const incidentRegions = topology.incidentPortRegion[originalPortId] ?? [] + const firstRegionId = incidentRegions[0] + const secondRegionId = incidentRegions[1] + + if (firstRegionId !== undefined && secondRegionId !== undefined) { + const dx = + topology.regionCenterX[secondRegionId] - + topology.regionCenterX[firstRegionId] + const dy = + topology.regionCenterY[secondRegionId] - + topology.regionCenterY[firstRegionId] + const length = Math.hypot(dx, dy) + + if (length > 0) { + return { + x: -dy / length, + y: dx / length, + } + } + } + + const angleRadians = + ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI + + return { + x: -Math.sin(angleRadians), + y: Math.cos(angleRadians), + } +} + +const getCloneOffset = ( + topology: TinyHyperGraphTopology, + originalPortId: PortId, + cloneIndex: number, + spacing: number, +) => { + const direction = getCloneOffsetDirection(topology, originalPortId) + 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, + } +} + /** * Routes every connection as if intermediate ports have unbounded virtual * fanout capacity, then assembles the discovered paths into a normal @@ -25,20 +123,19 @@ type SolvedRouteSegment = { */ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { readonly solvedRouteSegmentsByRouteId: Array + readonly virtualSharedPortSpacing: number constructor( topology: TinyHyperGraphTopology, problem: TinyHyperGraphProblem, options?: TinyHyperGraphSolverOptions, ) { - super(topology, problem, { - ...options, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, - }) + super(topology, problem, options) this.solvedRouteSegmentsByRouteId = Array.from( { length: problem.routeCount }, () => undefined, ) + this.virtualSharedPortSpacing = 0.2 } override onPathFound(finalCandidate: Candidate) { @@ -71,6 +168,9 @@ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { 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( @@ -88,25 +188,150 @@ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { this.resetCandidateBestCosts() state.goalPortId = -1 + const getPortIdForNet = (originalPortId: 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 cloneOffset = getCloneOffset( + topology, + originalPortId, + cloneIndex, + this.virtualSharedPortSpacing, + ) + 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, + ) + 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++) { - state.currentRouteNetId = problem.routeNet[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] ?? []) { - state.regionSegments[regionId].push([routeId, fromPortId, toPortId]) - state.portAssignment[fromPortId] = state.currentRouteNetId - state.portAssignment[toPortId] = state.currentRouteNetId - this.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + const routedFromPortId = getPortIdForNet(fromPortId, routeNetId) + const routedToPortId = getPortIdForNet(toPortId, 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/index.ts b/lib/auto-repair/index.ts index 21cbb9a..f5e6431 100644 --- a/lib/auto-repair/index.ts +++ b/lib/auto-repair/index.ts @@ -1,7 +1,3 @@ -import { setTinyHyperGraphIterationTimeoutFallbackFactory } from "../core" -import { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" -import { splitOverloadedRouteEndpointPorts } from "./splitOverloadedRouteEndpointPorts" - export { splitOverloadedRouteEndpointPorts, type SplitOverloadedRouteEndpointPortsOptions, @@ -9,18 +5,3 @@ export { } from "./splitOverloadedRouteEndpointPorts" export { TinyHyperGraphVirtualFanoutSolver } from "./TinyHyperGraphVirtualFanoutSolver" export { TinyHyperGraphAutoRepairFallbackSolver } from "./TinyHyperGraphAutoRepairFallbackSolver" - -setTinyHyperGraphIterationTimeoutFallbackFactory( - (topology, problem, options) => { - const split = splitOverloadedRouteEndpointPorts(topology, problem) - - return new TinyHyperGraphVirtualFanoutSolver( - split.topology, - split.problem, - { - ...options, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, - }, - ) - }, -) diff --git a/lib/core.ts b/lib/core.ts index 607fe99..78627e2 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -185,7 +185,6 @@ export interface TinyHyperGraphSolverOptions { VERBOSE?: boolean STATIC_REACHABILITY_PRECHECK?: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number - AUTO_REPAIR_ON_ITERATION_TIMEOUT?: boolean } export interface TinyHyperGraphSolverOptionTarget { @@ -201,23 +200,6 @@ export interface TinyHyperGraphSolverOptionTarget { VERBOSE: boolean STATIC_REACHABILITY_PRECHECK: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number - AUTO_REPAIR_ON_ITERATION_TIMEOUT?: boolean -} - -export type TinyHyperGraphIterationTimeoutFallbackFactory = ( - topology: TinyHyperGraphTopology, - problem: TinyHyperGraphProblem, - options: TinyHyperGraphSolverOptions, -) => TinyHyperGraphSolver | undefined - -let iterationTimeoutFallbackFactory: - | TinyHyperGraphIterationTimeoutFallbackFactory - | undefined - -export const setTinyHyperGraphIterationTimeoutFallbackFactory = ( - factory: TinyHyperGraphIterationTimeoutFallbackFactory | undefined, -) => { - iterationTimeoutFallbackFactory = factory } export const applyTinyHyperGraphSolverOptions = ( @@ -266,10 +248,6 @@ export const applyTinyHyperGraphSolverOptions = ( solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS = options.STATIC_REACHABILITY_PRECHECK_MAX_HOPS } - if (options.AUTO_REPAIR_ON_ITERATION_TIMEOUT !== undefined) { - solver.AUTO_REPAIR_ON_ITERATION_TIMEOUT = - options.AUTO_REPAIR_ON_ITERATION_TIMEOUT - } } export const getTinyHyperGraphSolverOptions = ( @@ -288,7 +266,6 @@ export const getTinyHyperGraphSolverOptions = ( STATIC_REACHABILITY_PRECHECK: solver.STATIC_REACHABILITY_PRECHECK, STATIC_REACHABILITY_PRECHECK_MAX_HOPS: solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: solver.AUTO_REPAIR_ON_ITERATION_TIMEOUT, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -329,7 +306,6 @@ export class TinyHyperGraphSolver extends BaseSolver { VERBOSE = false STATIC_REACHABILITY_PRECHECK = true STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 - AUTO_REPAIR_ON_ITERATION_TIMEOUT = true constructor( public topology: TinyHyperGraphTopology, @@ -1166,49 +1142,6 @@ export class TinyHyperGraphSolver extends BaseSolver { neverSuccessfullyRoutedRouteCount: neverSuccessfullyRoutedRoutes.length, } this.logNeverSuccessfullyRoutedRoutes() - - if ( - !this.AUTO_REPAIR_ON_ITERATION_TIMEOUT || - !iterationTimeoutFallbackFactory - ) { - return - } - - const fallbackSolver = iterationTimeoutFallbackFactory( - this.topology, - this.problem, - { - ...getTinyHyperGraphSolverOptions(this), - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, - }, - ) - - if (!fallbackSolver) { - return - } - - fallbackSolver.solve() - - this.stats = { - ...this.stats, - autoRepairFallbackTriggered: true, - autoRepairFallbackSolver: fallbackSolver.getSolverName(), - autoRepairFallbackIterations: fallbackSolver.iterations, - autoRepairFallbackError: fallbackSolver.error, - ...fallbackSolver.stats, - } - - if (!fallbackSolver.solved || fallbackSolver.failed) { - return - } - - this.topology = fallbackSolver.topology - this.problem = fallbackSolver.problem - this._problemSetup = undefined - this.state = fallbackSolver.state - this.solved = true - this.failed = false - this.error = null } computeH(neighborPortId: PortId): number { 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.
{ const solver = new TinyHyperGraphSolver(topology, problem, { MAX_ITERATIONS: 20_000, STATIC_REACHABILITY_PRECHECK: false, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, }) const beforeSolveGraphics = solver.visualize() diff --git a/tests/solver/virtual-fanout-solver.test.ts b/tests/solver/virtual-fanout-solver.test.ts index 92b9b3e..023a20c 100644 --- a/tests/solver/virtual-fanout-solver.test.ts +++ b/tests/solver/virtual-fanout-solver.test.ts @@ -1,11 +1,30 @@ 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 @@ -65,9 +84,11 @@ const createSharedInternalPortProblem = (): { 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, - AUTO_REPAIR_ON_ITERATION_TIMEOUT: false, }) rawSolver.solve() @@ -88,12 +109,18 @@ test("virtual fanout solves different-net routes through a shared internal port" 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]).toBe(sharedPortX) + expect(fanoutSolver.topology.portY[5]).not.toBe(sharedPortY) expect(fanoutSolver.getOutput().solvedRoutes).toHaveLength(2) }) -test("core solver falls back to virtual fanout after running out of iterations", () => { +test("explicit auto-repair solver falls back to virtual fanout after primary timeout", () => { const { topology, problem } = createSharedInternalPortProblem() - const solver = new TinyHyperGraphSolver(topology, problem, { + const solver = new TinyHyperGraphAutoRepairFallbackSolver(topology, problem, { MAX_ITERATIONS: 100, }) @@ -103,5 +130,11 @@ test("core solver falls back to virtual fanout after running out of iterations", 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) }) From d6d0808ec3ef52dd95acadb11b147da94b581285 Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Thu, 21 May 2026 23:01:19 +0200 Subject: [PATCH 4/6] patch --- scripts/benchmarking/srj13-core.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/benchmarking/srj13-core.ts b/scripts/benchmarking/srj13-core.ts index 32965be..f537954 100644 --- a/scripts/benchmarking/srj13-core.ts +++ b/scripts/benchmarking/srj13-core.ts @@ -12,7 +12,7 @@ import { } from "../../lib/index" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" -type SolverMode = "core" | "fanout-split" | "auto-repair" +type SolverMode = "core" | "duplicate-space" | "auto-repair" type Srj13BenchmarkSample = { sampleName: string @@ -41,7 +41,7 @@ This intentionally skips the section pipeline. Many samples are expected to fail Options: --limit N Run the first N samples from the dataset. --sample NUM Run a specific sample by number or name (e.g. 2, 02, example-02). - --solver MODE Solver mode: core, fanout-split, or auto-repair. Defaults to auto-repair. + --solver MODE Solver mode: core, duplicate-space, or auto-repair. Defaults to auto-repair. --max-iterations N Override the core solver iteration cap. Defaults to 1000000. --strict Exit non-zero if any sample fails. --help Show this help text. @@ -50,7 +50,7 @@ Examples: ./benchmark-srj13.sh ./benchmark-srj13.sh --limit 3 ./benchmark-srj13.sh --sample example-02 - ./benchmark-srj13.sh --solver fanout-split --limit 3 + ./benchmark-srj13.sh --solver duplicate-space --limit 3 ./benchmark-srj13.sh --solver auto-repair --sample example-02 ./benchmark-srj13.sh --max-iterations 250000 ` @@ -88,7 +88,7 @@ const parsePositiveInteger = (flag: string, rawValue: string | undefined) => { const parseSolverMode = (rawValue: string | undefined): SolverMode => { if ( rawValue === "core" || - rawValue === "fanout-split" || + rawValue === "duplicate-space" || rawValue === "auto-repair" ) { return rawValue @@ -256,7 +256,7 @@ const runSample = ( const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) const solverOptions = getSolverOptions(benchmarkCase, maxIterations) const solver = - solverMode === "fanout-split" + solverMode === "duplicate-space" ? (() => { const split = splitOverloadedRouteEndpointPorts(topology, problem) return new TinyHyperGraphVirtualFanoutSolver( From 4bd0df9ca11723122d21ebc88a03c8df3dec38ec Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Thu, 21 May 2026 23:06:50 +0200 Subject: [PATCH 5/6] patch --- .../TinyHyperGraphVirtualFanoutSolver.ts | 53 ++++++++++--------- .../splitOverloadedRouteEndpointPorts.ts | 41 +++++++------- ...it-overloaded-route-endpoint-ports.test.ts | 8 +-- tests/solver/virtual-fanout-solver.test.ts | 2 +- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts index 8daa0f1..5af54ec 100644 --- a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts @@ -39,25 +39,16 @@ const appendFloat64 = (source: Float64Array, value: number) => { const getCloneOffsetDirection = ( topology: TinyHyperGraphTopology, originalPortId: PortId, + adjacentPortId: PortId, ) => { - const incidentRegions = topology.incidentPortRegion[originalPortId] ?? [] - const firstRegionId = incidentRegions[0] - const secondRegionId = incidentRegions[1] - - if (firstRegionId !== undefined && secondRegionId !== undefined) { - const dx = - topology.regionCenterX[secondRegionId] - - topology.regionCenterX[firstRegionId] - const dy = - topology.regionCenterY[secondRegionId] - - topology.regionCenterY[firstRegionId] - const length = Math.hypot(dx, dy) - - if (length > 0) { - return { - x: -dy / length, - y: dx / length, - } + 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, } } @@ -65,18 +56,23 @@ const getCloneOffsetDirection = ( ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI return { - x: -Math.sin(angleRadians), - y: Math.cos(angleRadians), + 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) + 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 @@ -188,7 +184,11 @@ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { this.resetCandidateBestCosts() state.goalPortId = -1 - const getPortIdForNet = (originalPortId: PortId, routeNetId: number) => { + const getPortIdForNet = ( + originalPortId: PortId, + adjacentPortId: PortId, + routeNetId: number, + ) => { const assignedNetId = state.portAssignment[originalPortId] if (assignedNetId === -1 || assignedNetId === routeNetId) { @@ -209,6 +209,7 @@ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { const cloneOffset = getCloneOffset( topology, originalPortId, + adjacentPortId, cloneIndex, this.virtualSharedPortSpacing, ) @@ -273,8 +274,12 @@ export class TinyHyperGraphVirtualFanoutSolver extends TinyHyperGraphSolver { for (const { regionId, fromPortId, toPortId } of this .solvedRouteSegmentsByRouteId[routeId] ?? []) { - const routedFromPortId = getPortIdForNet(fromPortId, routeNetId) - const routedToPortId = getPortIdForNet(toPortId, routeNetId) + const routedFromPortId = getPortIdForNet( + fromPortId, + toPortId, + routeNetId, + ) + const routedToPortId = getPortIdForNet(toPortId, fromPortId, routeNetId) if (originalRouteStartPortId === fromPortId) { problem.routeStartPort[routeId] = routedFromPortId diff --git a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts index 352ecd5..550108d 100644 --- a/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts +++ b/lib/auto-repair/splitOverloadedRouteEndpointPorts.ts @@ -45,25 +45,16 @@ const cloneMetadataWithVirtualPortInfo = ( const getCloneOffsetDirection = ( topology: TinyHyperGraphTopology, originalPortId: PortId, + adjacentPortId: PortId, ) => { - const incidentRegions = topology.incidentPortRegion[originalPortId] ?? [] - const firstRegionId = incidentRegions[0] - const secondRegionId = incidentRegions[1] + const dx = topology.portX[adjacentPortId] - topology.portX[originalPortId] + const dy = topology.portY[adjacentPortId] - topology.portY[originalPortId] + const length = Math.hypot(dx, dy) - if (firstRegionId !== undefined && secondRegionId !== undefined) { - const dx = - topology.regionCenterX[secondRegionId] - - topology.regionCenterX[firstRegionId] - const dy = - topology.regionCenterY[secondRegionId] - - topology.regionCenterY[firstRegionId] - const length = Math.hypot(dx, dy) - - if (length > 0) { - return { - x: -dy / length, - y: dx / length, - } + if (length > 0) { + return { + x: dx / length, + y: dy / length, } } @@ -71,14 +62,15 @@ const getCloneOffsetDirection = ( ((topology.portAngleForRegion1[originalPortId] ?? 0) / 100 / 180) * Math.PI return { - x: -Math.sin(angleRadians), - y: Math.cos(angleRadians), + x: Math.cos(angleRadians), + y: Math.sin(angleRadians), } } const getCloneOffset = ( topology: TinyHyperGraphTopology, originalPortId: PortId, + adjacentPortId: PortId, cloneIndex: number, spacing: number, ) => { @@ -86,7 +78,11 @@ const getCloneOffset = ( return { x: 0, y: 0 } } - const direction = getCloneOffsetDirection(topology, originalPortId) + 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 @@ -178,9 +174,14 @@ export const splitOverloadedRouteEndpointPorts = ( 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, ) diff --git a/tests/solver/split-overloaded-route-endpoint-ports.test.ts b/tests/solver/split-overloaded-route-endpoint-ports.test.ts index f9ddf7c..ac5fa59 100644 --- a/tests/solver/split-overloaded-route-endpoint-ports.test.ts +++ b/tests/solver/split-overloaded-route-endpoint-ports.test.ts @@ -75,10 +75,10 @@ test("splitting overloaded route endpoint ports lets different-net routes share 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]).toBe(topology.portX[0]) - expect(repaired.topology.portY[2]).not.toBe(topology.portY[0]) - expect(repaired.topology.portX[3]).toBe(topology.portX[1]) - expect(repaired.topology.portY[3]).not.toBe(topology.portY[1]) + 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 index 023a20c..a0ab0f0 100644 --- a/tests/solver/virtual-fanout-solver.test.ts +++ b/tests/solver/virtual-fanout-solver.test.ts @@ -113,7 +113,7 @@ test("virtual fanout solves different-net routes through a shared internal port" expect(fanoutSolver.stats.differentNetSharedPortCount).toBe(0) expect(getDifferentNetSharedPortCount(fanoutSolver)).toBe(0) expect(fanoutSolver.topology.portCount).toBeGreaterThan(originalPortCount) - expect(fanoutSolver.topology.portX[5]).toBe(sharedPortX) + expect(fanoutSolver.topology.portX[5]).not.toBe(sharedPortX) expect(fanoutSolver.topology.portY[5]).not.toBe(sharedPortY) expect(fanoutSolver.getOutput().solvedRoutes).toHaveLength(2) }) From b92fb681cc6a3a7ba0cb7d471bf2417dfbab09c0 Mon Sep 17 00:00:00 2001 From: Abse2001 Date: Thu, 21 May 2026 23:17:52 +0200 Subject: [PATCH 6/6] patch --- .../TinyHyperGraphVirtualFanoutSolver.ts | 343 +----------------- .../TinyHyperGraphVirtualFanoutSolver.ts | 207 +++++++++++ .../cloneVirtualFanoutPort.ts | 169 +++++++++ 3 files changed, 377 insertions(+), 342 deletions(-) create mode 100644 lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/TinyHyperGraphVirtualFanoutSolver.ts create mode 100644 lib/auto-repair/TinyHyperGraphVirtualFanoutSolver/cloneVirtualFanoutPort.ts diff --git a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts index 5af54ec..64a75fa 100644 --- a/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts +++ b/lib/auto-repair/TinyHyperGraphVirtualFanoutSolver.ts @@ -1,342 +1 @@ -import { - createEmptyRegionIntersectionCache, - TinyHyperGraphSolver, - type Candidate, - type TinyHyperGraphProblem, - type TinyHyperGraphSolverOptions, - type TinyHyperGraphTopology, -} from "../core" -import type { PortId, RegionId, RouteId } from "../types" - -type SolvedRouteSegment = { - regionId: RegionId - fromPortId: PortId - toPortId: PortId -} - -const appendInt8 = (source: Int8Array, value: number) => { - const next = new Int8Array(source.length + 1) - next.set(source) - next[source.length] = value - return next -} - -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, - } -} - -/** - * Routes every connection as if intermediate ports have unbounded virtual - * fanout capacity, then assembles the discovered paths into a normal - * 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 cloneOffset = getCloneOffset( - topology, - originalPortId, - adjacentPortId, - cloneIndex, - this.virtualSharedPortSpacing, - ) - 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, - ) - 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 - } -} +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, + } +}