diff --git a/lib/core.ts b/lib/core.ts index d334dc5..9be94d2 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -28,7 +28,9 @@ import { visualizeTinyGraph } from "./visualizeTinyGraph" export type { StaticallyUnroutableRouteSummary } from "./static-reachability" -const GREEDY_FINAL_ROUTE_MAX_ITERATIONS = 50e3 +export const DEFAULT_PANIC_GREEDY_ITERATION_BUDGET = 50_000 +export const DEFAULT_PANIC_GREEDY_START_COST_FACTOR = 0 +export const DEFAULT_PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR = 2 export const createEmptyRegionIntersectionCache = (): RegionIntersectionCache => ({ @@ -162,6 +164,8 @@ export interface TinyHyperGraphSolution { export interface RegionCostSummary { maxRegionCost: number totalRegionCost: number + totalSegmentCount: number + maxRouteSegmentCount: number } interface SolvedStateSnapshot { @@ -237,7 +241,11 @@ export interface TinyHyperGraphSolverOptions { STATIC_REACHABILITY_PRECHECK?: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT?: boolean - GREEDY_FINAL_ROUTE_ITERS?: number + GREEDY_INITIALIZATION?: boolean + PANIC_GREEDY?: boolean + PANIC_GREEDY_ITERATION_BUDGET?: number + PANIC_GREEDY_START_COST_FACTOR?: number + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR?: number } export interface TinyHyperGraphSolverOptionTarget { @@ -254,7 +262,11 @@ export interface TinyHyperGraphSolverOptionTarget { STATIC_REACHABILITY_PRECHECK: boolean STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT: boolean - GREEDY_FINAL_ROUTE_ITERS: number + GREEDY_INITIALIZATION?: boolean + PANIC_GREEDY?: boolean + PANIC_GREEDY_ITERATION_BUDGET?: number + PANIC_GREEDY_START_COST_FACTOR?: number + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR?: number } export const applyTinyHyperGraphSolverOptions = ( @@ -307,8 +319,22 @@ export const applyTinyHyperGraphSolverOptions = ( solver.ACCEPT_BEST_SOLUTION_ON_TIMEOUT = options.ACCEPT_BEST_SOLUTION_ON_TIMEOUT } - if (options.GREEDY_FINAL_ROUTE_ITERS !== undefined) { - solver.GREEDY_FINAL_ROUTE_ITERS = options.GREEDY_FINAL_ROUTE_ITERS + if (options.GREEDY_INITIALIZATION !== undefined) { + solver.GREEDY_INITIALIZATION = options.GREEDY_INITIALIZATION + } + if (options.PANIC_GREEDY !== undefined) { + solver.PANIC_GREEDY = options.PANIC_GREEDY + } + if (options.PANIC_GREEDY_ITERATION_BUDGET !== undefined) { + solver.PANIC_GREEDY_ITERATION_BUDGET = options.PANIC_GREEDY_ITERATION_BUDGET + } + if (options.PANIC_GREEDY_START_COST_FACTOR !== undefined) { + solver.PANIC_GREEDY_START_COST_FACTOR = + options.PANIC_GREEDY_START_COST_FACTOR + } + if (options.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR !== undefined) { + solver.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR = + options.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR } } @@ -329,7 +355,12 @@ export const getTinyHyperGraphSolverOptions = ( STATIC_REACHABILITY_PRECHECK_MAX_HOPS: solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS, ACCEPT_BEST_SOLUTION_ON_TIMEOUT: solver.ACCEPT_BEST_SOLUTION_ON_TIMEOUT, - GREEDY_FINAL_ROUTE_ITERS: solver.GREEDY_FINAL_ROUTE_ITERS, + GREEDY_INITIALIZATION: solver.GREEDY_INITIALIZATION, + PANIC_GREEDY: solver.PANIC_GREEDY, + PANIC_GREEDY_ITERATION_BUDGET: solver.PANIC_GREEDY_ITERATION_BUDGET, + PANIC_GREEDY_START_COST_FACTOR: solver.PANIC_GREEDY_START_COST_FACTOR, + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR: + solver.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -374,7 +405,19 @@ export class TinyHyperGraphSolver extends BaseSolver { STATIC_REACHABILITY_PRECHECK = true STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true - GREEDY_FINAL_ROUTE_ITERS = 4 + GREEDY_INITIALIZATION = false + greedyInitializationActive = false + greedyInitializationCompleted = false + PANIC_GREEDY = false + PANIC_GREEDY_ITERATION_BUDGET = DEFAULT_PANIC_GREEDY_ITERATION_BUDGET + PANIC_GREEDY_START_COST_FACTOR = DEFAULT_PANIC_GREEDY_START_COST_FACTOR + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR = + DEFAULT_PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR + panicGreedyActive = false + panicGreedyStarted = false + panicGreedyCompleted = false + panicGreedyStartIteration = 0 + panicGreedyEndIteration = 0 constructor( public topology: TinyHyperGraphTopology, @@ -383,6 +426,7 @@ export class TinyHyperGraphSolver extends BaseSolver { ) { super() applyTinyHyperGraphSolverOptions(this, options) + this.greedyInitializationActive = this.GREEDY_INITIALIZATION this.state = { portAssignment: new Int32Array(topology.portCount).fill(-1), regionSegments: Array.from({ length: topology.regionCount }, () => []), @@ -1050,6 +1094,14 @@ export class TinyHyperGraphSolver extends BaseSolver { return left.maxRegionCost - right.maxRegionCost } + if (left.maxRouteSegmentCount !== right.maxRouteSegmentCount) { + return left.maxRouteSegmentCount - right.maxRouteSegmentCount + } + + if (left.totalSegmentCount !== right.totalSegmentCount) { + return left.totalSegmentCount - right.totalSegmentCount + } + return left.totalRegionCost - right.totalRegionCost } @@ -1090,71 +1142,65 @@ export class TinyHyperGraphSolver extends BaseSolver { this.state.goalPortId = -1 } - protected getRemainingRouteIdsForGreedyFinalRoute(): RouteId[] { - const routeIds = new Set(this.state.unroutedRoutes) - - if (this.state.currentRouteId !== undefined) { - routeIds.add(this.state.currentRouteId) - } - - return [...routeIds] - } - - protected applySnapshotToGreedyFinalRouteSolver( - solver: TinyHyperGraphSolver, - snapshot: SolvedStateSnapshot, - routeIds: RouteId[], - ) { - const clonedSnapshot = cloneSolvedStateSnapshot(snapshot) - - solver.state.portAssignment = clonedSnapshot.portAssignment - solver.state.regionSegments = clonedSnapshot.regionSegments - solver.state.regionIntersectionCaches = - clonedSnapshot.regionIntersectionCaches - solver.state.regionCongestionCost = clonedSnapshot.regionCongestionCost - solver.state.ripCount = 0 - solver.state.currentRouteId = undefined - solver.state.currentRouteNetId = undefined - solver.state.unroutedRoutes = [...routeIds] - solver.state.candidateQueue.clear() - solver.resetCandidateBestCosts() - solver.state.goalPortId = -1 - } - protected summarizeSolvedState( solver: TinyHyperGraphSolver, ): RegionCostSummary { let maxRegionCost = 0 let totalRegionCost = 0 + let totalSegmentCount = 0 + const routeSegmentCounts = new Uint32Array(solver.problem.routeCount) for (const regionIntersectionCache of solver.state .regionIntersectionCaches) { const regionCost = regionIntersectionCache.existingRegionCost maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost + totalSegmentCount += regionIntersectionCache.existingSegmentCount + } + + for (const regionSegments of solver.state.regionSegments) { + for (const [routeId] of regionSegments) { + routeSegmentCounts[routeId] += 1 + } } return { maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: Math.max(0, ...routeSegmentCounts), } } - protected tryGreedyFinalRouteAcceptance(): boolean { - const greedyFinalRouteIters = Math.max( + protected getPanicGreedyRouteIds(): RouteId[] { + const routeIds = new Set(this.state.unroutedRoutes) + + if (this.state.currentRouteId !== undefined) { + routeIds.add(this.state.currentRouteId) + } + + return [...routeIds] + } + + protected startPanicGreedy(): boolean { + if (!this.PANIC_GREEDY || this.panicGreedyStarted) { + return false + } + + const iterationBudget = Math.max( 0, - Math.floor(this.GREEDY_FINAL_ROUTE_ITERS), + Math.floor(this.PANIC_GREEDY_ITERATION_BUDGET), ) - if (greedyFinalRouteIters === 0) { + if (iterationBudget === 0) { return false } - const remainingRouteIds = this.getRemainingRouteIdsForGreedyFinalRoute() + const remainingRouteIds = this.getPanicGreedyRouteIds() if (remainingRouteIds.length === 0) { return false } - const startingSnapshot = cloneSolvedStateSnapshot({ + const snapshot = cloneSolvedStateSnapshot({ portAssignment: this.state.portAssignment, regionSegments: this.state.regionSegments, regionIntersectionCaches: this.state.regionIntersectionCaches, @@ -1162,77 +1208,41 @@ export class TinyHyperGraphSolver extends BaseSolver { ripCount: this.state.ripCount, }) - for ( - let greedyFinalRouteIter = 0; - greedyFinalRouteIter < greedyFinalRouteIters; - greedyFinalRouteIter++ - ) { - const routeIds = - greedyFinalRouteIter === 0 - ? remainingRouteIds - : shuffle( - remainingRouteIds, - this.state.ripCount + greedyFinalRouteIter, - ) - const greedySolver = new GreedyFinalRouteSolver( - this.topology, - this.problem, - { - ...getTinyHyperGraphSolverOptions(this), - ACCEPT_BEST_SOLUTION_ON_TIMEOUT: false, - GREEDY_FINAL_ROUTE_ITERS: 0, - MAX_ITERATIONS: GREEDY_FINAL_ROUTE_MAX_ITERATIONS, - RIP_THRESHOLD_RAMP_ATTEMPTS: 0, - STATIC_REACHABILITY_PRECHECK: false, - }, - ) - - this.applySnapshotToGreedyFinalRouteSolver( - greedySolver, - startingSnapshot, - routeIds, - ) - greedySolver.solve() - - if (!greedySolver.solved || greedySolver.failed) { - continue - } - - this.bestSolvedStateSnapshot = cloneSolvedStateSnapshot({ - portAssignment: greedySolver.state.portAssignment, - regionSegments: greedySolver.state.regionSegments, - regionIntersectionCaches: greedySolver.state.regionIntersectionCaches, - regionCongestionCost: greedySolver.state.regionCongestionCost, - ripCount: greedySolver.state.ripCount, - }) - this.bestSolvedStateSummary = this.summarizeSolvedState(greedySolver) - this.restoreBestSolvedState() - this.stats = { - ...this.stats, - acceptedGreedyFinalRouteOnTimeout: true, - greedyFinalRouteIter, - greedyFinalRouteRemainingRouteCount: remainingRouteIds.length, - greedyFinalRouteMaxIterations: GREEDY_FINAL_ROUTE_MAX_ITERATIONS, - neverSuccessfullyRoutedRouteCount: 0, - maxRegionCost: this.bestSolvedStateSummary.maxRegionCost, - totalRegionCost: this.bestSolvedStateSummary.totalRegionCost, - bestMaxRegionCost: this.bestSolvedStateSummary.maxRegionCost, - bestTotalRegionCost: this.bestSolvedStateSummary.totalRegionCost, - } - this.solved = true - this.failed = false - this.error = null - return true - } + this.state.portAssignment = snapshot.portAssignment + this.state.regionSegments = snapshot.regionSegments + this.state.regionIntersectionCaches = snapshot.regionIntersectionCaches + this.state.regionCongestionCost = snapshot.regionCongestionCost + this.state.ripCount = 0 + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = remainingRouteIds + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + this.panicGreedyStarted = true + this.panicGreedyActive = true + this.panicGreedyStartIteration = this.iterations + this.panicGreedyEndIteration = this.iterations + iterationBudget + this.MAX_ITERATIONS += iterationBudget this.stats = { ...this.stats, - greedyFinalRouteAttemptCount: greedyFinalRouteIters, - greedyFinalRouteRemainingRouteCount: remainingRouteIds.length, - greedyFinalRouteMaxIterations: GREEDY_FINAL_ROUTE_MAX_ITERATIONS, + panicGreedyStarted: true, + panicGreedyStartIteration: this.iterations, + panicGreedyIterationBudget: iterationBudget, + panicGreedyStartCostFactor: this.PANIC_GREEDY_START_COST_FACTOR, + panicGreedyRemainingRouteCount: remainingRouteIds.length, } - return false + return true + } + + protected getPanicGreedyCostFactor() { + if (!this.panicGreedyActive) { + return 1 + } + + return Math.max(0, this.PANIC_GREEDY_START_COST_FACTOR) } onAllRoutesRouted() { @@ -1249,23 +1259,43 @@ export class TinyHyperGraphSolver extends BaseSolver { const regionCosts = new Float64Array(topology.regionCount) let maxRegionCost = 0 let totalRegionCost = 0 + let totalSegmentCount = 0 + const routeSegmentCounts = new Uint32Array(this.problem.routeCount) for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionIntersectionCache = state.regionIntersectionCaches[regionId] + const regionCost = regionIntersectionCache?.existingRegionCost ?? 0 regionCosts[regionId] = regionCost maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost + totalSegmentCount += regionIntersectionCache?.existingSegmentCount ?? 0 if (regionCost > currentRipThreshold) { regionIdsOverCostThreshold.push(regionId) } } - this.captureBestSolvedState({ + for (const regionSegments of state.regionSegments) { + for (const [routeId] of regionSegments) { + routeSegmentCounts[routeId] += 1 + } + } + + const maxRouteSegmentCount = Math.max(0, ...routeSegmentCounts) + + const currentSolvedStateSummary = { maxRegionCost, totalRegionCost, - }) + totalSegmentCount, + maxRouteSegmentCount, + } + + const previousBestSolvedStateSummary = this.bestSolvedStateSummary + const previousBestSolvedStateSnapshot = this.bestSolvedStateSnapshot + ? cloneSolvedStateSnapshot(this.bestSolvedStateSnapshot) + : undefined + + this.captureBestSolvedState(currentSolvedStateSummary) this.stats = { ...this.stats, @@ -1273,15 +1303,111 @@ export class TinyHyperGraphSolver extends BaseSolver { hotRegionCount: regionIdsOverCostThreshold.length, maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount, bestMaxRegionCost: this.bestSolvedStateSummary?.maxRegionCost, bestTotalRegionCost: this.bestSolvedStateSummary?.totalRegionCost, + bestTotalSegmentCount: this.bestSolvedStateSummary?.totalSegmentCount, + bestMaxRouteSegmentCount: + this.bestSolvedStateSummary?.maxRouteSegmentCount, ripCount: state.ripCount, } - if ( - regionIdsOverCostThreshold.length === 0 || - state.ripCount >= this.RIP_THRESHOLD_RAMP_ATTEMPTS - ) { + if (this.greedyInitializationActive) { + this.greedyInitializationActive = false + this.greedyInitializationCompleted = true + this.stats = { + ...this.stats, + greedyInitializationCompleted: true, + greedyInitializationMaxRegionCost: maxRegionCost, + greedyInitializationTotalRegionCost: totalRegionCost, + } + } + + if (this.panicGreedyActive) { + this.panicGreedyActive = false + this.panicGreedyCompleted = true + const maxRouteSegmentGrowthFactor = Math.max( + 1, + this.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR, + ) + const panicExceededRouteComplexity = + previousBestSolvedStateSnapshot && + previousBestSolvedStateSummary && + currentSolvedStateSummary.maxRouteSegmentCount > + previousBestSolvedStateSummary.maxRouteSegmentCount * + maxRouteSegmentGrowthFactor + + if (panicExceededRouteComplexity) { + const bestSolvedStateSummary = previousBestSolvedStateSummary! + const bestSolvedStateSnapshot = cloneSolvedStateSnapshot( + previousBestSolvedStateSnapshot!, + ) + this.bestSolvedStateSummary = bestSolvedStateSummary + this.bestSolvedStateSnapshot = bestSolvedStateSnapshot + this.restoreBestSolvedState() + this.stats = { + ...this.stats, + panicGreedyCompleted: true, + panicGreedyRejectedForRouteComplexity: true, + panicGreedyMaxRegionCost: maxRegionCost, + panicGreedyTotalRegionCost: totalRegionCost, + panicGreedyTotalSegmentCount: totalSegmentCount, + panicGreedyMaxRouteSegmentCount: maxRouteSegmentCount, + maxRegionCost: bestSolvedStateSummary.maxRegionCost, + totalRegionCost: bestSolvedStateSummary.totalRegionCost, + totalSegmentCount: bestSolvedStateSummary.totalSegmentCount, + maxRouteSegmentCount: bestSolvedStateSummary.maxRouteSegmentCount, + } + this.solved = true + return + } + + this.stats = { + ...this.stats, + panicGreedyCompleted: true, + acceptedPanicGreedyOnTimeout: true, + panicGreedyMaxRegionCost: maxRegionCost, + panicGreedyTotalRegionCost: totalRegionCost, + panicGreedyTotalSegmentCount: totalSegmentCount, + panicGreedyMaxRouteSegmentCount: maxRouteSegmentCount, + } + this.solved = true + return + } + + if (regionIdsOverCostThreshold.length === 0) { + this.solved = true + return + } + + if (state.ripCount >= this.RIP_THRESHOLD_RAMP_ATTEMPTS) { + if ( + this.bestSolvedStateSnapshot && + this.bestSolvedStateSummary && + this.compareRegionCostSummaries( + this.bestSolvedStateSummary, + currentSolvedStateSummary, + ) < 0 + ) { + this.restoreBestSolvedState() + this.stats = { + ...this.stats, + restoredBestSolutionOnRipLimit: true, + maxRegionCost: this.bestSolvedStateSummary.maxRegionCost, + totalRegionCost: this.bestSolvedStateSummary.totalRegionCost, + totalSegmentCount: this.bestSolvedStateSummary.totalSegmentCount, + maxRouteSegmentCount: + this.bestSolvedStateSummary.maxRouteSegmentCount, + bestMaxRegionCost: this.bestSolvedStateSummary.maxRegionCost, + bestTotalRegionCost: this.bestSolvedStateSummary.totalRegionCost, + bestTotalSegmentCount: this.bestSolvedStateSummary.totalSegmentCount, + bestMaxRouteSegmentCount: + this.bestSolvedStateSummary.maxRouteSegmentCount, + ripCount: this.state.ripCount, + } + } + this.solved = true return } @@ -1309,6 +1435,11 @@ export class TinyHyperGraphSolver extends BaseSolver { const { topology, state } = this const currentRouteId = state.currentRouteId const maxRegionCostBeforeRip = this.getMaxRegionCost() + const wasGreedyInitializationActive = this.greedyInitializationActive + + if (wasGreedyInitializationActive) { + this.greedyInitializationActive = false + } for (let regionId = 0; regionId < topology.regionCount; regionId++) { const regionCost = @@ -1325,6 +1456,9 @@ export class TinyHyperGraphSolver extends BaseSolver { maxRegionCost: maxRegionCostBeforeRip, maxRegionCostBeforeRip, reripReason: "out_of_candidates", + ...(wasGreedyInitializationActive + ? { greedyInitializationFailed: true } + : {}), } this.logRipEvent("out_of_candidates", maxRegionCostBeforeRip, { ...(currentRouteId === undefined @@ -1364,6 +1498,14 @@ export class TinyHyperGraphSolver extends BaseSolver { computeG(currentCandidate: Candidate, neighborPortId: PortId): number { const { state } = this + if (this.greedyInitializationActive) { + return currentCandidate.g + } + + if (this.panicGreedyActive && this.getPanicGreedyCostFactor() <= 0) { + return currentCandidate.g + } + const nextRegionId = currentCandidate.nextRegionId const regionCache = state.regionIntersectionCaches[nextRegionId] @@ -1403,11 +1545,15 @@ export class TinyHyperGraphSolver extends BaseSolver { regionCache.existingSegmentCount + 1, ) - regionCache.existingRegionCost - return ( - currentCandidate.g + + const incrementalCost = newRegionCost + state.regionCongestionCost[nextRegionId] + (this.problem.portPenalty?.[neighborPortId] ?? 0) + + return ( + currentCandidate.g + + incrementalCost * + (this.panicGreedyActive ? this.getPanicGreedyCostFactor() : 1) ) } @@ -1420,6 +1566,10 @@ export class TinyHyperGraphSolver extends BaseSolver { neverSuccessfullyRoutedRouteCount: neverSuccessfullyRoutedRoutes.length, } + if (this.ACCEPT_BEST_SOLUTION_ON_TIMEOUT && this.startPanicGreedy()) { + return + } + if ( this.ACCEPT_BEST_SOLUTION_ON_TIMEOUT && this.bestSolvedStateSnapshot && @@ -1431,8 +1581,16 @@ export class TinyHyperGraphSolver extends BaseSolver { acceptedBestSolutionOnTimeout: true, maxRegionCost: this.bestSolvedStateSummary.maxRegionCost, totalRegionCost: this.bestSolvedStateSummary.totalRegionCost, + totalSegmentCount: this.bestSolvedStateSummary.totalSegmentCount, + maxRouteSegmentCount: this.bestSolvedStateSummary.maxRouteSegmentCount, bestMaxRegionCost: this.bestSolvedStateSummary.maxRegionCost, bestTotalRegionCost: this.bestSolvedStateSummary.totalRegionCost, + bestTotalSegmentCount: this.bestSolvedStateSummary.totalSegmentCount, + bestMaxRouteSegmentCount: + this.bestSolvedStateSummary.maxRouteSegmentCount, + ...(this.panicGreedyStarted + ? { panicGreedyFallbackAcceptedBestSnapshot: true } + : {}), } this.solved = true this.failed = false @@ -1440,13 +1598,6 @@ export class TinyHyperGraphSolver extends BaseSolver { return } - if ( - this.ACCEPT_BEST_SOLUTION_ON_TIMEOUT && - this.tryGreedyFinalRouteAcceptance() - ) { - return - } - this.logNeverSuccessfullyRoutedRoutes() } @@ -1474,12 +1625,3 @@ export class TinyHyperGraphSolver extends BaseSolver { return convertToSerializedHyperGraph(this) } } - -class GreedyFinalRouteSolver extends TinyHyperGraphSolver { - override computeG( - currentCandidate: Candidate, - _neighborPortId: PortId, - ): number { - return currentCandidate.g - } -} diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index 9ccd4f9..1984ec9 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -2,13 +2,14 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { BasePipelineSolver, type PipelineStep } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { loadSerializedHyperGraph } from "../compat/loadSerializedHyperGraph" -import type { - TinyHyperGraphProblem, - TinyHyperGraphSolution, - TinyHyperGraphSolverOptions, - TinyHyperGraphTopology, +import { + DEFAULT_PANIC_GREEDY_ITERATION_BUDGET, + TinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphSolution, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, } from "../core" -import { TinyHyperGraphSolver } from "../core" import type { RegionId } from "../types" import type { TinyHyperGraphSectionSolverOptions } from "./index" import { getActiveSectionRouteIds, TinyHyperGraphSectionSolver } from "./index" @@ -44,11 +45,12 @@ type AutomaticSectionSearchResult = { } const DEFAULT_SOLVE_GRAPH_OPTIONS: TinyHyperGraphSolverOptions = { + GREEDY_INITIALIZATION: true, + PANIC_GREEDY: true, RIP_THRESHOLD_RAMP_ATTEMPTS: 5, } const DEFAULT_SECTION_SOLVER_MAX_ITERATIONS = 50_000 -const DEFAULT_SECTION_PIPELINE_MAX_ITERATIONS = 200_000 const DEFAULT_SECTION_SOLVER_OPTIONS: TinyHyperGraphSectionSolverOptions = { DISTANCE_TO_COST: 0.05, @@ -61,6 +63,14 @@ const DEFAULT_SECTION_SOLVER_OPTIONS: TinyHyperGraphSectionSolverOptions = { const DEFAULT_MAX_HOT_REGIONS = 2 +const getPanicGreedyIterationBudget = ( + solveGraphOptions: TinyHyperGraphSolverOptions, +) => + solveGraphOptions.PANIC_GREEDY + ? (solveGraphOptions.PANIC_GREEDY_ITERATION_BUDGET ?? + DEFAULT_PANIC_GREEDY_ITERATION_BUDGET) + : 0 + const IMPROVEMENT_EPSILON = 1e-9 const getMaxRegionCost = (solver: TinyHyperGraphSolver) => @@ -326,7 +336,12 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver { - const searchResult = findBestAutomaticSectionMask( - solvedSolver, - topology, - problem, - solution, - this.inputProblem.sectionSearchConfig, - sectionSolverOptions, - ) - - this.selectedSectionCandidateLabel = - searchResult.winningCandidateLabel - this.selectedSectionCandidateFamily = - searchResult.winningCandidateFamily - this.stats = { - ...this.stats, - sectionSearchGeneratedCandidateCount: - searchResult.generatedCandidateCount, - sectionSearchCandidateCount: searchResult.candidateCount, - sectionSearchDuplicateCandidateCount: - searchResult.duplicateCandidateCount, - sectionSearchBaselineMaxRegionCost: - searchResult.baselineMaxRegionCost, - sectionSearchFinalMaxRegionCost: searchResult.finalMaxRegionCost, - sectionSearchDelta: - searchResult.baselineMaxRegionCost - - searchResult.finalMaxRegionCost, - selectedSectionCandidateLabel: - searchResult.winningCandidateLabel ?? null, - selectedSectionCandidateFamily: - searchResult.winningCandidateFamily ?? null, - sectionSearchMs: searchResult.totalMs, - sectionSearchBaselineEvaluationMs: - searchResult.baselineEvaluationMs, - sectionSearchCandidateEligibilityMs: - searchResult.candidateEligibilityMs, - sectionSearchCandidateInitMs: searchResult.candidateInitMs, - sectionSearchCandidateSolveMs: searchResult.candidateSolveMs, - sectionSearchCandidateReplayScoreMs: - searchResult.candidateReplayScoreMs, - } - - return searchResult.portSectionMask - })() + : solvedSolver.stats.acceptedBestSolutionOnTimeout === true && + solvedSolver.stats.greedyInitializationCompleted === true + ? (() => { + this.stats = { + ...this.stats, + skippedSectionSearchAfterTimeoutSolveGraph: true, + sectionSearchGeneratedCandidateCount: 0, + sectionSearchCandidateCount: 0, + sectionSearchDuplicateCandidateCount: 0, + selectedSectionCandidateLabel: null, + selectedSectionCandidateFamily: null, + } + + return new Int8Array(topology.portCount) + })() + : (() => { + const searchResult = findBestAutomaticSectionMask( + solvedSolver, + topology, + problem, + solution, + this.inputProblem.sectionSearchConfig, + sectionSolverOptions, + ) + + this.selectedSectionCandidateLabel = + searchResult.winningCandidateLabel + this.selectedSectionCandidateFamily = + searchResult.winningCandidateFamily + this.stats = { + ...this.stats, + sectionSearchGeneratedCandidateCount: + searchResult.generatedCandidateCount, + sectionSearchCandidateCount: searchResult.candidateCount, + sectionSearchDuplicateCandidateCount: + searchResult.duplicateCandidateCount, + sectionSearchBaselineMaxRegionCost: + searchResult.baselineMaxRegionCost, + sectionSearchFinalMaxRegionCost: searchResult.finalMaxRegionCost, + sectionSearchDelta: + searchResult.baselineMaxRegionCost - + searchResult.finalMaxRegionCost, + selectedSectionCandidateLabel: + searchResult.winningCandidateLabel ?? null, + selectedSectionCandidateFamily: + searchResult.winningCandidateFamily ?? null, + sectionSearchMs: searchResult.totalMs, + sectionSearchBaselineEvaluationMs: + searchResult.baselineEvaluationMs, + sectionSearchCandidateEligibilityMs: + searchResult.candidateEligibilityMs, + sectionSearchCandidateInitMs: searchResult.candidateInitMs, + sectionSearchCandidateSolveMs: searchResult.candidateSolveMs, + sectionSearchCandidateReplayScoreMs: + searchResult.candidateReplayScoreMs, + } + + return searchResult.portSectionMask + })() this.selectedSectionMask = new Int8Array(portSectionMask) problem.portSectionMask = new Int8Array(portSectionMask) diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e66f4a1..9342360 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -139,6 +139,7 @@ const summarizeRegionIntersectionCaches = ( ): RegionCostSummary => { let maxRegionCost = 0 let totalRegionCost = 0 + let totalSegmentCount = 0 for ( let regionId = 0; @@ -149,11 +150,15 @@ const summarizeRegionIntersectionCaches = ( regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost + totalSegmentCount += + regionIntersectionCaches[regionId]?.existingSegmentCount ?? 0 } return { maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: totalSegmentCount, } } @@ -163,17 +168,22 @@ const summarizeRegionIntersectionCachesForRegionIds = ( ): RegionCostSummary => { let maxRegionCost = 0 let totalRegionCost = 0 + let totalSegmentCount = 0 for (const regionId of regionIds) { const regionCost = regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost + totalSegmentCount += + regionIntersectionCaches[regionId]?.existingSegmentCount ?? 0 } return { maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: totalSegmentCount, } } @@ -184,6 +194,7 @@ const summarizeRegionIntersectionCachesExcludingRegionIds = ( const excludedRegionIdSet = new Set(excludedRegionIds) let maxRegionCost = 0 let totalRegionCost = 0 + let totalSegmentCount = 0 for ( let regionId = 0; @@ -198,11 +209,15 @@ const summarizeRegionIntersectionCachesExcludingRegionIds = ( regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost + totalSegmentCount += + regionIntersectionCaches[regionId]?.existingSegmentCount ?? 0 } return { maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: totalSegmentCount, } } @@ -214,6 +229,14 @@ const compareRegionCostSummaries = ( return left.maxRegionCost - right.maxRegionCost } + if (left.maxRouteSegmentCount !== right.maxRouteSegmentCount) { + return left.maxRouteSegmentCount - right.maxRouteSegmentCount + } + + if (left.totalSegmentCount !== right.totalSegmentCount) { + return left.totalSegmentCount - right.totalSegmentCount + } + return left.totalRegionCost - right.totalRegionCost } @@ -720,6 +743,7 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { const mutableRegionCosts = new Float64Array(this.mutableRegionIds.length) let mutableMaxRegionCost = 0 let mutableTotalRegionCost = 0 + let mutableTotalSegmentCount = 0 for ( let mutableRegionIndex = 0; @@ -732,6 +756,8 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { mutableRegionCosts[mutableRegionIndex] = regionCost mutableMaxRegionCost = Math.max(mutableMaxRegionCost, regionCost) mutableTotalRegionCost += regionCost + mutableTotalSegmentCount += + state.regionIntersectionCaches[regionId]?.existingSegmentCount ?? 0 if (regionCost > currentRipThreshold) { regionIdsOverCostThreshold.push(regionId) @@ -744,14 +770,20 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { ) const totalRegionCost = this.immutableRegionSummary.totalRegionCost + mutableTotalRegionCost + const totalSegmentCount = + this.immutableRegionSummary.totalSegmentCount + mutableTotalSegmentCount this.captureBestState({ maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: totalSegmentCount, }) const bestSummary = this.bestSummary ?? { maxRegionCost, totalRegionCost, + totalSegmentCount, + maxRouteSegmentCount: totalSegmentCount, } if ( diff --git a/package.json b/package.json index 649cb1c..dda77b7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@types/bun": "latest", "cosmos": "^0.1.2", "dataset-hg07": "https://github.com/tscircuit/dataset-hg07#6adb2ed998a16675b11cf59c25789ae95f0e1a6f", - "dataset-srj18": "github:tscircuit/dataset-srj18#cafe0b46c28e6101d0e8d1b8ea184fdeebc5cc1a", + "dataset-srj18": "github:tscircuit/dataset-srj18#28d47419660b55f36c8cf503dfa24381fd159b03", "graphics-debug": "^0.0.93", "react-cosmos-plugin-vite": "^7.2.0", "stack-svgs": "^0.0.1", diff --git a/scripts/benchmarking/benchmark.ts b/scripts/benchmarking/benchmark.ts index 0afbead..47ff37c 100644 --- a/scripts/benchmarking/benchmark.ts +++ b/scripts/benchmarking/benchmark.ts @@ -1,4 +1,5 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter" import { getPngBufferFromGraphicsObject, stackGraphicsHorizontally, @@ -35,6 +36,18 @@ type DatasetSampleMeta = { stepsToPortPointSolve: number } +type Srj18DatasetSource = + | { + kind: "serialized-hypergraph" + dir: string + fileExtension: ".hg.json" + } + | { + kind: "simple-route-json" + dir: string + fileExtension: ".json" + } + type BenchmarkSampleResult = { sampleName: string circuitId: string @@ -516,45 +529,143 @@ const loadHg07DatasetModule = async (): Promise => { return datasetModule } -const getSrj18DatasetDir = async (cwd: string) => { - const candidateDirs = [ - path.join(cwd, "generated-datasets", "srj18"), - path.join( - path.dirname(fileURLToPath(import.meta.resolve("dataset-srj18"))), - "generated-datasets", - "srj18", - ), +const getSrj18DatasetSource = async ( + cwd: string, +): Promise => { + const packageDir = path.dirname( + fileURLToPath(import.meta.resolve("dataset-srj18")), + ) + const candidateSources: Srj18DatasetSource[] = [ + { + kind: "serialized-hypergraph", + dir: path.join(cwd, "generated-datasets", "srj18"), + fileExtension: ".hg.json", + }, + { + kind: "serialized-hypergraph", + dir: path.join(packageDir, "generated-datasets", "srj18"), + fileExtension: ".hg.json", + }, + { + kind: "simple-route-json", + dir: path.join(packageDir, "samples"), + fileExtension: ".json", + }, ] - for (const candidateDir of candidateDirs) { + for (const candidateSource of candidateSources) { try { - await access(candidateDir) - return candidateDir + await access(candidateSource.dir) + return candidateSource } catch { // Try the next known dataset layout. } } return usageError( - `Could not find srj18 generated dataset directory. Tried: ${candidateDirs.join(", ")}`, + `Could not find srj18 dataset directory. Tried: ${candidateSources.map((source) => source.dir).join(", ")}`, ) } -const getSrj18SampleNames = async (datasetDir: string) => - (await readdir(datasetDir)) - .map((entryName) => /^(sample\d+)\.hg\.json$/.exec(entryName)?.[1] ?? null) +const getSrj18SampleNames = async (datasetSource: Srj18DatasetSource) => { + const samplePattern = new RegExp( + `^(sample\\d+)${datasetSource.fileExtension.replace(".", "\\.")}$`, + ) + + return (await readdir(datasetSource.dir)) + .map((entryName) => samplePattern.exec(entryName)?.[1] ?? null) .filter((sampleName): sampleName is string => sampleName !== null) .sort((leftSampleName, rightSampleName) => leftSampleName.localeCompare(rightSampleName), ) +} + +const cloneSerializableRecord = (value: unknown) => + value && typeof value === "object" && !Array.isArray(value) + ? JSON.parse(JSON.stringify(value)) + : value + +const generateSrj18SerializedHyperGraph = async ( + simpleRouteJson: SimpleRouteJson, +): Promise => { + const autorouterModule = await import("@tscircuit/capacity-autorouter") + const AutoroutingPipelineSolver4 = + autorouterModule.AutoroutingPipelineSolver4_TinyHypergraph ?? + autorouterModule.AutoroutingPipelineSolver4 ?? + autorouterModule.AutoroutingPipelineSolver + + const autorouter = new AutoroutingPipelineSolver4(simpleRouteJson, { + effort: 1, + }) + autorouter.solveUntilPhase("portPointPathingSolver") + autorouter.step() + + if (autorouter.failed) { + throw new Error( + autorouter.error ?? + "srj18 hypergraph generation failed before port-point pathing", + ) + } + + const portPointPathingSolver = autorouter.portPointPathingSolver + if (!portPointPathingSolver) { + throw new Error( + "srj18 hypergraph generation did not create a port-point pathing solver", + ) + } + + const [params] = portPointPathingSolver.getConstructorParams() + + return { + regions: params.graph.regions.map((region: any) => ({ + regionId: region.regionId, + pointIds: region.ports.map((port: any) => port.portId), + d: cloneSerializableRecord(region.d), + })), + ports: params.graph.ports.map((port: any) => { + const { regions: _regions, ...portData } = port.d ?? {} + + return { + portId: port.portId, + region1Id: port.region1.regionId, + region2Id: port.region2.regionId, + d: cloneSerializableRecord(portData), + } + }), + connections: params.connections.map((connection: any) => ({ + connectionId: connection.connectionId, + mutuallyConnectedNetworkId: connection.mutuallyConnectedNetworkId, + startRegionId: connection.startRegion.regionId, + endRegionId: connection.endRegion.regionId, + d: cloneSerializableRecord(connection.simpleRouteConnection), + })), + } +} + +const loadSrj18SerializedHyperGraph = async ( + datasetSource: Srj18DatasetSource, + sampleName: string, +): Promise => { + const samplePath = path.join( + datasetSource.dir, + `${sampleName}${datasetSource.fileExtension}`, + ) + const sampleJson = JSON.parse(await readFile(samplePath, "utf8")) + + if (datasetSource.kind === "serialized-hypergraph") { + return sampleJson as SerializedHyperGraph + } + + return generateSrj18SerializedHyperGraph(sampleJson as SimpleRouteJson) +} const loadSrj18DatasetModule = async ( cwd: string, limit: number | null, sampleName: string | null, ): Promise => { - const datasetDir = await getSrj18DatasetDir(cwd) - const allSampleNames = await getSrj18SampleNames(datasetDir) + const datasetSource = await getSrj18DatasetSource(cwd) + const allSampleNames = await getSrj18SampleNames(datasetSource) if (sampleName && !allSampleNames.includes(sampleName)) { usageError(`Unknown sample: ${sampleName}`) } @@ -568,7 +679,9 @@ const loadSrj18DatasetModule = async ( : Math.min(limit, allSampleNames.length), ) - console.log(`loading dataset=srj18 dir=${datasetDir}`) + console.log( + `loading dataset=srj18 kind=${datasetSource.kind} dir=${datasetSource.dir}`, + ) const datasetModule: DatasetModule = { manifest: { @@ -583,12 +696,10 @@ const loadSrj18DatasetModule = async ( } for (const srj18SampleName of requestedSampleNames) { - const serializedHyperGraph = JSON.parse( - await readFile( - path.join(datasetDir, `${srj18SampleName}.hg.json`), - "utf8", - ), - ) as SerializedHyperGraph + const serializedHyperGraph = await loadSrj18SerializedHyperGraph( + datasetSource, + srj18SampleName, + ) datasetModule[srj18SampleName] = serializedHyperGraph } diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 16431f3..07fa28b 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -71,7 +71,7 @@ const createTestSolver = ( return new TinyHyperGraphSolver(topology, problem, options) } -const createGreedyFinalRouteTestSolver = ( +const createGreedyInitializationTestSolver = ( options?: ConstructorParameters[2], ) => { const portCount = 2 @@ -206,32 +206,145 @@ test("best solution timeout acceptance can be disabled", () => { expect(Array.from(solver.state.portAssignment)).toEqual([-1, -1, -1, -1]) }) -test("final acceptance greedily routes remaining routes when no complete snapshot exists", () => { - const solver = createGreedyFinalRouteTestSolver({ - GREEDY_FINAL_ROUTE_ITERS: 2, +test("greedy initialization routes through normal solver steps", () => { + const solver = createGreedyInitializationTestSolver({ + GREEDY_INITIALIZATION: true, }) - solver.tryFinalAcceptance() + solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(solver.stats.acceptedGreedyFinalRouteOnTimeout).toBe(true) - expect(solver.stats.greedyFinalRouteRemainingRouteCount).toBe(1) + expect(solver.stats.greedyInitializationCompleted).toBe(true) + expect(solver.stats.greedyInitializationMaxRegionCost).toBe(0) expect(solver.state.unroutedRoutes).toEqual([]) expect(Array.from(solver.state.portAssignment)).toEqual([0, 0]) expect(solver.state.regionSegments[1]).toEqual([[0, 0, 1]]) }) -test("greedy final routing can be disabled independently", () => { - const solver = createGreedyFinalRouteTestSolver({ - GREEDY_FINAL_ROUTE_ITERS: 0, +test("timeout acceptance does not start a hidden greedy solver", () => { + const solver = createGreedyInitializationTestSolver({ + MAX_ITERATIONS: 1, + GREEDY_INITIALIZATION: false, + PANIC_GREEDY: false, }) + solver.step() + + expect(solver.solved).toBe(false) + expect(solver.failed).toBe(true) + expect(solver.error).toBe("TinyHyperGraphSolver ran out of iterations") + expect(solver.stats.greedyInitializationCompleted).toBeUndefined() + expect(solver.stats.acceptedBestSolutionOnTimeout).toBeUndefined() +}) + +test("timeout can start panic greedy through normal solver steps", () => { + const solver = createTestSolver({ + MAX_ITERATIONS: 10, + PANIC_GREEDY: true, + PANIC_GREEDY_ITERATION_BUDGET: 5, + }) + + solver.state.currentRouteId = 1 + solver.state.currentRouteNetId = 1 + solver.state.unroutedRoutes = [2] + solver.state.goalPortId = 2 + solver.iterations = 10 solver.tryFinalAcceptance() expect(solver.solved).toBe(false) expect(solver.failed).toBe(false) - expect(solver.stats.acceptedGreedyFinalRouteOnTimeout).toBeUndefined() + expect(solver.panicGreedyActive).toBe(true) + expect(solver.stats.panicGreedyStarted).toBe(true) + expect(solver.stats.panicGreedyIterationBudget).toBe(5) + expect(solver.stats.panicGreedyRemainingRouteCount).toBe(2) + expect(solver.MAX_ITERATIONS).toBe(15) + expect(solver.state.currentRouteId).toBeUndefined() + expect(solver.state.currentRouteNetId).toBeUndefined() + expect([...solver.state.unroutedRoutes].sort((a, b) => a - b)).toEqual([1, 2]) +}) + +test("timeout starts gradual panic greedy before accepting best solved snapshot", () => { + const solver = createTestSolver({ + MAX_ITERATIONS: 10, + PANIC_GREEDY: true, + PANIC_GREEDY_ITERATION_BUDGET: 5, + }) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, 1, 1]) + solver.state.regionSegments[0] = [[0, 0, 1]] + solver.state.regionSegments[1] = [[1, 2, 3]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.5) + solver.state.regionIntersectionCaches[1] = createRegionCache(0.1) + solver.step() + solver.solved = false + + solver.state.currentRouteId = 2 + solver.state.currentRouteNetId = 2 + solver.state.unroutedRoutes = [1] + solver.iterations = 10 + solver.tryFinalAcceptance() + + expect(solver.solved).toBe(false) + expect(solver.failed).toBe(false) + expect(solver.panicGreedyActive).toBe(true) + expect(solver.stats.acceptedBestSolutionOnTimeout).toBeUndefined() + expect(solver.stats.panicGreedyStarted).toBe(true) + expect(Array.from(solver.state.portAssignment)).toEqual([-1, -1, -1, -1]) +}) + +test("panic greedy accepts completed routing without another rerip", () => { + const solver = createTestSolver() + + solver.panicGreedyActive = true + solver.state.unroutedRoutes = [] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.5) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.panicGreedyActive).toBe(false) + expect(solver.panicGreedyCompleted).toBe(true) + expect(solver.stats.acceptedPanicGreedyOnTimeout).toBe(true) + expect(solver.stats.panicGreedyMaxRegionCost).toBe(0.5) +}) + +test("panic greedy falls back when route complexity grows too much", () => { + const solver = createTestSolver({ + RIP_THRESHOLD_START: 0.1, + RIP_THRESHOLD_RAMP_ATTEMPTS: 2, + PANIC_GREEDY: true, + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR: 2, + }) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, -1, -1]) + solver.state.regionSegments[0] = [[0, 0, 1]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.2) + solver.step() + + expect(solver.solved).toBe(false) + expect(solver.stats.bestMaxRouteSegmentCount).toBe(1) + + solver.panicGreedyActive = true + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, 0, 0]) + solver.state.regionSegments[0] = [ + [0, 0, 1], + [0, 1, 2], + [0, 2, 3], + ] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.05) + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.stats.panicGreedyRejectedForRouteComplexity).toBe(true) + expect(solver.stats.panicGreedyMaxRouteSegmentCount).toBe(3) + expect(solver.stats.maxRouteSegmentCount).toBe(1) + expect(Array.from(solver.state.portAssignment)).toEqual([0, 0, -1, -1]) + expect(solver.state.regionSegments[0]).toEqual([[0, 0, 1]]) }) test("completed routing is accepted once all region costs are under the threshold", () => { @@ -249,6 +362,33 @@ test("completed routing is accepted once all region costs are under the threshol expect(Array.from(solver.state.regionCongestionCost)).toEqual([0, 0]) }) +test("rip limit restores the best solved snapshot instead of the last worse attempt", () => { + const solver = createTestSolver({ + RIP_THRESHOLD_RAMP_ATTEMPTS: 1, + }) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, 1, 1]) + solver.state.regionSegments[0] = [[0, 0, 1]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.2) + solver.step() + + expect(solver.solved).toBe(false) + expect(solver.state.ripCount).toBe(1) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([2, 2, 3, 3]) + solver.state.regionSegments[0] = [[2, 0, 1]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.9) + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.stats.restoredBestSolutionOnRipLimit).toBe(true) + expect(solver.stats.maxRegionCost).toBe(0.2) + expect(Array.from(solver.state.portAssignment)).toEqual([0, 0, 1, 1]) + expect(solver.state.regionSegments[0]).toEqual([[0, 0, 1]]) +}) + test("constructor options override snake-case hyperparameters before setup", () => { const solver = createTestSolver({ DISTANCE_TO_COST: 0.25, @@ -258,7 +398,11 @@ test("constructor options override snake-case hyperparameters before setup", () RIP_CONGESTION_REGION_COST_FACTOR: 0.45, MAX_ITERATIONS: 1234, ACCEPT_BEST_SOLUTION_ON_TIMEOUT: false, - GREEDY_FINAL_ROUTE_ITERS: 6, + GREEDY_INITIALIZATION: true, + PANIC_GREEDY: true, + PANIC_GREEDY_ITERATION_BUDGET: 4321, + PANIC_GREEDY_START_COST_FACTOR: 0.5, + PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR: 3, }) expect(solver.DISTANCE_TO_COST).toBe(0.25) @@ -268,6 +412,10 @@ test("constructor options override snake-case hyperparameters before setup", () expect(solver.RIP_CONGESTION_REGION_COST_FACTOR).toBe(0.45) expect(solver.MAX_ITERATIONS).toBe(1234) expect(solver.ACCEPT_BEST_SOLUTION_ON_TIMEOUT).toBe(false) - expect(solver.GREEDY_FINAL_ROUTE_ITERS).toBe(6) + expect(solver.GREEDY_INITIALIZATION).toBe(true) + expect(solver.PANIC_GREEDY).toBe(true) + expect(solver.PANIC_GREEDY_ITERATION_BUDGET).toBe(4321) + expect(solver.PANIC_GREEDY_START_COST_FACTOR).toBe(0.5) + expect(solver.PANIC_GREEDY_MAX_ROUTE_SEGMENT_GROWTH_FACTOR).toBe(3) expect(solver.problemSetup.portHCostToEndOfRoute[0]).toBe(0.25) }) diff --git a/tests/solver/section-solver.test.ts b/tests/solver/section-solver.test.ts index cd53248..dcc0b6d 100644 --- a/tests/solver/section-solver.test.ts +++ b/tests/solver/section-solver.test.ts @@ -270,9 +270,22 @@ test("section pipeline uses bounded default iteration limits", () => { const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ serializedHyperGraph: datasetHg07.sample029, }) - - expect(pipelineSolver.MAX_ITERATIONS).toBe(200_000) - expect(pipelineSolver.getSectionSolverOptions().MAX_ITERATIONS).toBe(50_000) + const solveGraphMaxIterations = + pipelineSolver.getSolveGraphOptions().MAX_ITERATIONS ?? 1_000_000 + const panicGreedyMaxIterations = pipelineSolver.getSolveGraphOptions() + .PANIC_GREEDY + ? (pipelineSolver.getSolveGraphOptions().PANIC_GREEDY_ITERATION_BUDGET ?? + 50_000) + : 0 + const sectionSolverMaxIterations = + pipelineSolver.getSectionSolverOptions().MAX_ITERATIONS ?? 50_000 + + expect(sectionSolverMaxIterations).toBe(50_000) + expect(pipelineSolver.MAX_ITERATIONS).toBe( + solveGraphMaxIterations + + panicGreedyMaxIterations + + sectionSolverMaxIterations, + ) }) test("section pipeline final acceptance falls back to solveGraph output", () => {