diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index e9f0f08..cb6ced6 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -318,6 +318,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { return { RIP_THRESHOLD_RAMP_ATTEMPTS: 0, STATIC_REACHABILITY_PRECHECK: false, + USE_LAZY_ROUTE_HEURISTIC: true, ...this.options.routeSolveOptions, } } @@ -326,6 +327,12 @@ export class DuplicateCongestedPortSolver extends BaseSolver { const { topology, problem } = loadSerializedHyperGraph( this.serializedHyperGraph, ) + const serializedPortIds = Array.from( + { length: topology.portCount }, + (_, portId) => getSerializedPortId(topology, portId), + ) + topology.portMetadata = undefined + topology.regionMetadata = undefined const portUseCounts = new Map() for (let routeId = 0; routeId < problem.routeCount; routeId++) { @@ -346,7 +353,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { } for (const portId of getUsedPortIdsForSolvedRoute(routeSolver)) { - const serializedPortId = getSerializedPortId(topology, portId) + const serializedPortId = serializedPortIds[portId] ?? `port-${portId}` portUseCounts.set( serializedPortId, (portUseCounts.get(serializedPortId) ?? 0) + 1, @@ -367,27 +374,48 @@ export class DuplicateCongestedPortSolver extends BaseSolver { const { solvedRoutes: _solvedRoutes, ...restHyperGraph } = this.serializedHyperGraph - const regions: SerializedRegion[] = this.serializedHyperGraph.regions.map( - (region) => ({ - ...region, - pointIds: [...region.pointIds], - d: cloneSerializableValue(region.d), - }), - ) - const ports: SerializedPort[] = this.serializedHyperGraph.ports.map( - (port) => ({ - ...port, - d: cloneSerializableValue(port.d), - }), - ) + const regions = [...this.serializedHyperGraph.regions] + const ports = [...this.serializedHyperGraph.ports] const regionById = new Map( - regions.map((region) => [region.regionId, region]), + this.serializedHyperGraph.regions.map((region) => [ + region.regionId, + region, + ]), + ) + const regionIndexById = new Map( + this.serializedHyperGraph.regions.map((region, regionIndex) => [ + region.regionId, + regionIndex, + ]), ) const sourcePortById = new Map( - ports.map((port) => [port.portId, port] as const), + this.serializedHyperGraph.ports.map( + (port) => [port.portId, port] as const, + ), + ) + const usedPortIds = new Set( + this.serializedHyperGraph.ports.map((port) => port.portId), ) - const usedPortIds = new Set(ports.map((port) => port.portId)) const duplicatedPorts: DuplicatedPortSummary[] = [] + const mutableRegionIds = new Set() + + const getMutableRegion = (regionId: string) => { + const regionIndex = regionIndexById.get(regionId) + if (regionIndex === undefined) return undefined + + const region = regions[regionIndex] + if (!region) return undefined + + if (!mutableRegionIds.has(regionId)) { + regions[regionIndex] = { + ...region, + pointIds: [...region.pointIds], + } + mutableRegionIds.add(regionId) + } + + return regions[regionIndex] + } for (const [sourcePortId, useCount] of [...portUseCounts.entries()].sort( ([leftPortId], [rightPortId]) => leftPortId.localeCompare(rightPortId), @@ -442,7 +470,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { } for (const regionId of [sourcePort.region1Id, sourcePort.region2Id]) { - const region = regionById.get(regionId) + const region = getMutableRegion(regionId) if (!region) continue insertDuplicatePortIdsAfterSource( region.pointIds, @@ -470,7 +498,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { connections: this.serializedHyperGraph.connections === undefined ? undefined - : cloneSerializableValue(this.serializedHyperGraph.connections), + : [...this.serializedHyperGraph.connections], } } diff --git a/lib/compat/convertToSerializedHyperGraph.ts b/lib/compat/convertToSerializedHyperGraph.ts index 70e623c..78b70e8 100644 --- a/lib/compat/convertToSerializedHyperGraph.ts +++ b/lib/compat/convertToSerializedHyperGraph.ts @@ -20,8 +20,20 @@ interface RouteSegment { const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null +const copyEnumerableRecord = ( + value: Record, +): Record => { + const clone: Record = {} + + for (const key in value) { + clone[key] = value[key] + } + + return clone +} + const toObjectRecord = (value: unknown): Record => { - if (isRecord(value)) return { ...value } + if (isRecord(value)) return copyEnumerableRecord(value) if (value === undefined) return {} return { value } } diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 6dc4eba..7ff5f58 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -88,7 +88,7 @@ const addSerializedRegionIdToMetadata = ( ) => { const metadata = region.d && typeof region.d === "object" && !Array.isArray(region.d) - ? { ...region.d } + ? Object.create(region.d) : { value: region.d } metadata.layer = layer @@ -109,7 +109,7 @@ const addSerializedPortIdToMetadata = ( ) => { const metadata = port.d && typeof port.d === "object" && !Array.isArray(port.d) - ? { ...port.d } + ? Object.create(port.d) : { value: port.d } metadata.layer = layer diff --git a/lib/core.ts b/lib/core.ts index d334dc5..94c259a 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -344,6 +344,10 @@ interface SegmentGeometryScratch { export class TinyHyperGraphSolver extends BaseSolver { state: TinyHyperGraphWorkingState + /** Slots per port in the compact hop-cost arrays (max incident regions across ports) */ + protected hopSlotStride: number + /** Fallback storage for hops whose region is not incident to the port (encoded as negative hop ids) */ + private hopOverflowBestCost?: Map private _problemSetup?: TinyHyperGraphProblemSetup protected routeAttemptCountByRouteId: Uint32Array protected routeSuccessCountByRouteId: Uint32Array @@ -383,6 +387,14 @@ export class TinyHyperGraphSolver extends BaseSolver { ) { super() applyTinyHyperGraphSolverOptions(this, options) + let hopSlotStride = 1 + for (let portId = 0; portId < topology.portCount; portId++) { + const incidentCount = topology.incidentPortRegion[portId]?.length ?? 0 + if (incidentCount > hopSlotStride) { + hopSlotStride = incidentCount + } + } + this.hopSlotStride = hopSlotStride this.state = { portAssignment: new Int32Array(topology.portCount).fill(-1), regionSegments: Array.from({ length: topology.regionCount }, () => []), @@ -396,10 +408,10 @@ export class TinyHyperGraphSolver extends BaseSolver { candidateQueue: new MinHeap([], compareCandidatesByF), candidateBestCostByHopId: this.USE_SPARSE_CANDIDATE_STORAGE ? new Map() - : new Float64Array(topology.portCount * topology.regionCount), + : new Float64Array(topology.portCount * hopSlotStride), candidateBestCostGenerationByHopId: this.USE_SPARSE_CANDIDATE_STORAGE ? new Map() - : new Uint32Array(topology.portCount * topology.regionCount), + : new Uint32Array(topology.portCount * hopSlotStride), candidateBestCostGeneration: 1, goalPortId: -1, ripCount: 0, @@ -609,6 +621,8 @@ export class TinyHyperGraphSolver extends BaseSolver { resetCandidateBestCosts() { const { state } = this + this.hopOverflowBestCost?.clear() + if (state.candidateBestCostGeneration === 0xffffffff) { if (state.candidateBestCostByHopId instanceof Map) { state.candidateBestCostByHopId.clear() @@ -626,6 +640,10 @@ export class TinyHyperGraphSolver extends BaseSolver { } getCandidateBestCost(hopId: HopId) { + if (hopId < 0) { + return this.hopOverflowBestCost?.get(hopId) ?? Number.POSITIVE_INFINITY + } + const { state } = this const bestCostGeneration = state.candidateBestCostGenerationByHopId @@ -639,6 +657,11 @@ export class TinyHyperGraphSolver extends BaseSolver { } setCandidateBestCost(hopId: HopId, bestCost: number) { + if (hopId < 0) { + ;(this.hopOverflowBestCost ??= new Map()).set(hopId, bestCost) + return + } + const { state } = this if (state.candidateBestCostGenerationByHopId instanceof Map) { @@ -658,8 +681,29 @@ export class TinyHyperGraphSolver extends BaseSolver { } } + /** + * Drop search-only state that is no longer needed once the solver has + * finished. Everything released here is rebuilt on demand: `problemSetup` + * is a lazy getter, and the candidate queue/overflow map are reset at the + * start of every route attempt. + */ + releaseTransientSearchState() { + this._problemSetup = undefined + this.hopOverflowBestCost = undefined + this.bestSolvedStateSnapshot = undefined + this.state.candidateQueue.clear() + } + getHopId(portId: PortId, nextRegionId: RegionId): HopId { - return portId * this.topology.regionCount + nextRegionId + // Hops are keyed by (port, incident-region slot) so the dense cost arrays + // stay O(ports * max-incidence) instead of O(ports * regions). + const incidentRegions = this.topology.incidentPortRegion[portId] + const slot = incidentRegions ? incidentRegions.indexOf(nextRegionId) : -1 + if (slot === -1) { + // Non-incident hop: encode as a negative id routed to the overflow map. + return -(portId * this.topology.regionCount + nextRegionId) - 1 + } + return portId * this.hopSlotStride + slot } getStartingNextRegionId( diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index 9ccd4f9..1d33f4c 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -11,7 +11,11 @@ import type { import { TinyHyperGraphSolver } from "../core" import type { RegionId } from "../types" import type { TinyHyperGraphSectionSolverOptions } from "./index" -import { getActiveSectionRouteIds, TinyHyperGraphSectionSolver } from "./index" +import { + createSolvedSolverFromSolution, + getActiveSectionRouteIds, + TinyHyperGraphSectionSolver, +} from "./index" import { createSectionMaskCandidatesForHotRegions, DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, @@ -72,17 +76,24 @@ const getMaxRegionCost = (solver: TinyHyperGraphSolver) => const getSerializedOutputMaxRegionCost = ( serializedHyperGraph: SerializedHyperGraph, + solveGraphOptions?: TinyHyperGraphSolverOptions, sectionSolverOptions?: TinyHyperGraphSectionSolverOptions, ) => { const replay = loadSerializedHyperGraph(serializedHyperGraph) - const replayedSolver = new TinyHyperGraphSectionSolver( + const replayedSolver = createSolvedSolverFromSolution( replay.topology, replay.problem, replay.solution, - sectionSolverOptions, + { + ...DEFAULT_SOLVE_GRAPH_OPTIONS, + ...solveGraphOptions, + ...(sectionSolverOptions?.minViaPadDiameter === undefined + ? {} + : { minViaPadDiameter: sectionSolverOptions.minViaPadDiameter }), + }, ) - return getMaxRegionCost(replayedSolver.baselineSolver) + return getMaxRegionCost(replayedSolver) } const createPortSectionMaskForRegionIds = ( @@ -257,6 +268,12 @@ const findBestAutomaticSectionMask = ( const candidateReplayScoreStartTime = performance.now() const replayedFinalMaxRegionCost = getSerializedOutputMaxRegionCost( sectionSolver.getOutput(), + { + ...DEFAULT_SOLVE_GRAPH_OPTIONS, + ...(sectionSolverOptions.minViaPadDiameter === undefined + ? {} + : { minViaPadDiameter: sectionSolverOptions.minViaPadDiameter }), + }, sectionSolverOptions, ) candidateReplayScoreMs += @@ -323,6 +340,14 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver }, + onSolved: (instance: TinyHyperGraphSectionPipelineSolver) => { + instance + .getSolver("solveGraph") + ?.releaseTransientSearchState() + }, }, { solverName: "optimizeSection", solverClass: TinyHyperGraphSectionSolver, getConstructorParams: (instance: TinyHyperGraphSectionPipelineSolver) => instance.getSectionStageParams(), + onSolved: (instance: TinyHyperGraphSectionPipelineSolver) => { + instance + .getSolver("optimizeSection") + ?.releaseTransientSearchState() + instance.cachedSectionStageParams = undefined + }, }, ] @@ -387,6 +423,10 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver("solveGraph") @@ -470,9 +510,15 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver { + const graph = createLayerFixture() + graph.regions[0]!.d = { + ...graph.regions[0]!.d, + customRegionFlag: "keep-me", + customRegionNested: { mode: "nested" }, + } + graph.ports[0]!.d = { + ...graph.ports[0]!.d, + customPortFlag: "keep-me-too", + customPortNested: { mode: "nested" }, + } + + const { topology, problem } = loadSerializedHyperGraph(graph) + const solver = new TinyHyperGraphSolver(topology, problem) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + const output = solver.getOutput() + + expect(output.regions[0]?.d?.customRegionFlag).toBe("keep-me") + expect(output.regions[0]?.d?.customRegionNested).toEqual({ mode: "nested" }) + expect(output.ports[0]?.d?.customPortFlag).toBe("keep-me-too") + expect(output.ports[0]?.d?.customPortNested).toEqual({ mode: "nested" }) +})