From 2541ca72fe8cec9ed9746182fade10096878284a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Sun, 28 Jun 2026 02:33:23 +0530 Subject: [PATCH 1/3] v1 --- dictionary.md | 30 + idea.md | 28 + lib/compat/convertToSerializedHyperGraph.ts | 22 +- lib/solver-view.ts | 94 ++ lib/visualizeStaticReachabilityFailure.ts | 27 +- lib/visualizeTinyGraph.ts | 96 +- lib2/compute-region-cost.ts | 60 + lib2/domain.ts | 131 ++ lib2/graph-input.ts | 97 ++ lib2/graph-load.ts | 630 ++++++++ lib2/graph-output.ts | 429 ++++++ lib2/index.ts | 29 + lib2/min-heap.ts | 92 ++ lib2/prelude.ts | 56 + lib2/region-cache.ts | 263 ++++ lib2/route-cost.ts | 41 + lib2/route-search.ts | 202 +++ lib2/section-candidate-families.ts | 125 ++ lib2/section-pipeline.ts | 533 +++++++ lib2/section-solver.ts | 1084 ++++++++++++++ lib2/segment-geometry.ts | 72 + lib2/shuffle.ts | 23 + lib2/solver-view.ts | 18 + lib2/solver.ts | 1449 +++++++++++++++++++ lib2/types.ts | 37 + lib2/utils.ts | 7 + progress.md | 137 ++ scripts/benchmarking/benchmark.ts | 48 +- scripts/benchmarking/cm5io.ts | 55 +- scripts/benchmarking/srj13-core.ts | 37 +- tests/lib2/min-heap.test.ts | 28 + tests/lib2/region-cache.test.ts | 73 + tests/lib2/route-cost.test.ts | 67 + tests/lib2/route-search.test.ts | 88 ++ tests/lib2/section-solver2.test.ts | 105 ++ tests/lib2/solver2.test.ts | 62 + tsconfig.json | 4 +- 37 files changed, 6288 insertions(+), 91 deletions(-) create mode 100644 dictionary.md create mode 100644 idea.md create mode 100644 lib/solver-view.ts create mode 100644 lib2/compute-region-cost.ts create mode 100644 lib2/domain.ts create mode 100644 lib2/graph-input.ts create mode 100644 lib2/graph-load.ts create mode 100644 lib2/graph-output.ts create mode 100644 lib2/index.ts create mode 100644 lib2/min-heap.ts create mode 100644 lib2/prelude.ts create mode 100644 lib2/region-cache.ts create mode 100644 lib2/route-cost.ts create mode 100644 lib2/route-search.ts create mode 100644 lib2/section-candidate-families.ts create mode 100644 lib2/section-pipeline.ts create mode 100644 lib2/section-solver.ts create mode 100644 lib2/segment-geometry.ts create mode 100644 lib2/shuffle.ts create mode 100644 lib2/solver-view.ts create mode 100644 lib2/solver.ts create mode 100644 lib2/types.ts create mode 100644 lib2/utils.ts create mode 100644 progress.md create mode 100644 tests/lib2/min-heap.test.ts create mode 100644 tests/lib2/region-cache.test.ts create mode 100644 tests/lib2/route-cost.test.ts create mode 100644 tests/lib2/route-search.test.ts create mode 100644 tests/lib2/section-solver2.test.ts create mode 100644 tests/lib2/solver2.test.ts diff --git a/dictionary.md b/dictionary.md new file mode 100644 index 0000000..3e15a2c --- /dev/null +++ b/dictionary.md @@ -0,0 +1,30 @@ +# Tiny Hypergraph Vocabulary + +The `lib2` code uses a small vocabulary so names stay predictable. New names +should be built from these roots unless the domain clearly needs a new word. + +1. `graph` - the full hypergraph input, loaded model, or output. +2. `region` - a traversable area in the graph. +3. `port` - a boundary point connecting regions. +4. `route` - one connection to solve from start port to end port. +5. `net` - electrical ownership for routes, ports, and reserved regions. +6. `hop` - one searchable state of port plus next region. +7. `path` - the ordered ports and regions chosen for a route. +8. `segment` - one routed edge inside a region. +9. `cost` - routing penalty or quality score. +10. `state` - mutable solver working data. +11. `queue` - frontier used by route search. +12. `cache` - stored derived data reused during solving. +13. `solve` - run routing work to completion or failure. +14. `parse` - convert boundary input into a trusted shape. +15. `load` - adapt serialized graph data to solver topology/problem data. +16. `result` - explicit success or failure value. +17. `error` - typed expected failure information. +18. `section` - a selected subgraph span optimized after full-graph routing. +19. `candidate` - one option considered during route or section search. +20. `srj` - simple route JSON style dataset/input family. + +Example: `srjGraphResult` is acceptable because it combines `srj`, `graph`, +and `result`. Avoid inventing synonyms like `node`, `vertex`, `task`, +`payload`, or `manager` when one of these roots fits. `lib2` is allowed as the +version boundary name; it is not a domain root. diff --git a/idea.md b/idea.md new file mode 100644 index 0000000..4d8fa4e --- /dev/null +++ b/idea.md @@ -0,0 +1,28 @@ +# Lib2 Idea + +Goal: create `lib2`, a clearer TypeScript-standard entrypoint for tiny +hypergraph solving while preserving current benchmark behavior. + +The first version keeps the current hot solver as the execution engine. That is +intentional: the existing A* loop, intersection counting, typed-array state, and +rerip policy are performance-sensitive and already covered by regression tests. +`lib2` starts by making the boundary cleaner: + +- typed `Result` values for expected parse/solve failures +- explicit serialized-graph parsing at the edge +- a named `TinyHyperGraphSolver2` facade that can be benchmarked separately +- benchmark flags for raw core comparisons +- a small vocabulary documented in `dictionary.md` + +Next improvement target after this slice: move one cohesive hot module at a time +behind lib2 names, starting with region cache operations. That module has a good +chance to improve both readability and performance because the current append +path reallocates several typed arrays per committed segment. + +Success rule for this slice: + +- original test suite stays green +- new lib2 tests pass +- SRJ13 and CM5IO raw benchmarks can run with `--solver lib2` +- representative lib2 benchmark stats are the same as core within normal local + noise because the engine is still shared diff --git a/lib/compat/convertToSerializedHyperGraph.ts b/lib/compat/convertToSerializedHyperGraph.ts index 70e623c..62ad7ea 100644 --- a/lib/compat/convertToSerializedHyperGraph.ts +++ b/lib/compat/convertToSerializedHyperGraph.ts @@ -1,5 +1,5 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" -import type { TinyHyperGraphSolver } from "../index" +import type { TinyHyperGraphSolverView } from "../solver-view" import { getAvailableZFromMask, getZLayerLabel } from "../layerLabels" import type { PortId, RegionId } from "../types" @@ -30,7 +30,7 @@ const normalizePortIdFallback = (value: string) => value.includes("::") ? value.slice(0, value.indexOf("::")) : value const getSerializedRegionId = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): string => { const metadata = solver.topology.regionMetadata?.[regionId] @@ -52,7 +52,7 @@ const getSerializedRegionId = ( } const getSerializedPortId = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => { const metadata = solver.topology.portMetadata?.[portId] @@ -70,7 +70,7 @@ const getSerializedPortId = ( } const getSerializedRegionData = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): Record => { const data = toObjectRecord(solver.topology.regionMetadata?.[regionId]) @@ -110,7 +110,7 @@ const getSerializedRegionData = ( } const getSerializedPortData = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): Record => { const data = toObjectRecord(solver.topology.portMetadata?.[portId]) @@ -133,7 +133,7 @@ const getSerializedPortData = ( } const getRouteSegmentsByRoute = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, ): Array => { const routeSegmentsByRoute = Array.from( { length: solver.problem.routeCount }, @@ -154,7 +154,7 @@ const getRouteSegmentsByRoute = ( } const getOppositeRegionIdForPort = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, traversedRegionId: RegionId, ): RegionId => { @@ -173,7 +173,7 @@ const getOppositeRegionIdForPort = ( } const getOrderedRoutePath = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: number, routeSegments: RouteSegment[], ): { @@ -267,7 +267,7 @@ const getOrderedRoutePath = ( } const getSerializedConnection = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: number, startRegionId: string, endRegionId: string, @@ -301,7 +301,7 @@ const getSerializedConnection = ( } const getSerializedSolvedRoute = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: number, routeSegments: RouteSegment[], ): { @@ -379,7 +379,7 @@ const getSerializedSolvedRoute = ( } export const convertToSerializedHyperGraph = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, ): SerializedHyperGraph => { if (!solver.solved || solver.failed) { throw new Error( diff --git a/lib/solver-view.ts b/lib/solver-view.ts new file mode 100644 index 0000000..b734812 --- /dev/null +++ b/lib/solver-view.ts @@ -0,0 +1,94 @@ +import type { + HopId, + NetId, + PortId, + RegionId, + RegionIntersectionCache, + RouteId, +} from "./types" + +export type SolverViewCandidate = { + prevRegionId?: RegionId + portId: PortId + nextRegionId: RegionId + prevCandidate?: SolverViewCandidate + f: number + g: number + h: number +} + +export type SolverViewTopology = { + portCount: number + regionCount: number + regionIncidentPorts: PortId[][] + incidentPortRegion: RegionId[][] + regionWidth: Float64Array + regionHeight: Float64Array + regionCenterX: Float64Array + regionCenterY: Float64Array + regionAvailableZMask?: Int32Array + regionMetadata?: any[] + portAngleForRegion1: Int32Array + portAngleForRegion2?: Int32Array + portX: Float64Array + portY: Float64Array + portZ: Int32Array + portMetadata?: any[] +} + +export type SolverViewProblem = { + routeCount: number + portSectionMask: Int8Array + routeMetadata?: any[] + routeStartPort: Int32Array + routeEndPort: Int32Array + routeNet: Int32Array + regionNetId: Int32Array + portPenalty?: Float64Array +} + +export type SolverViewWorkingState = { + portAssignment: Int32Array + regionSegments: Array<[RouteId, PortId, PortId][]> + regionIntersectionCaches: RegionIntersectionCache[] + currentRouteNetId: NetId | undefined + currentRouteId: RouteId | undefined + unroutedRoutes: RouteId[] + candidateQueue: { + toArray(): SolverViewCandidate[] + } + candidateBestCostByHopId: Float64Array | Map + candidateBestCostGenerationByHopId: Uint32Array | Map + candidateBestCostGeneration: number + goalPortId: PortId + ripCount: number + regionCongestionCost: Float64Array +} + +export type SolverViewRouteSummary = { + routeId: RouteId + connectionId: string + startPortId: PortId + endPortId: PortId + startRegionId?: string + endRegionId?: string + pointIds: string[] +} + +export type NeverSuccessfullyRoutedSolverViewRouteSummary = + SolverViewRouteSummary & { + attempts: number + } + +/** Public read shape required by solver serializers and visualizers. */ +export type TinyHyperGraphSolverView = { + readonly topology: SolverViewTopology + readonly problem: SolverViewProblem + readonly state: SolverViewWorkingState + readonly solved: boolean + readonly failed: boolean + readonly iterations: number + getAdditionalRegionLabel(regionId: RegionId): string | undefined + getNeverSuccessfullyRoutedRoutes(): NeverSuccessfullyRoutedSolverViewRouteSummary[] + getStaticallyUnroutableRoutes(): SolverViewRouteSummary[] +} diff --git a/lib/visualizeStaticReachabilityFailure.ts b/lib/visualizeStaticReachabilityFailure.ts index 612c5cf..2021b3f 100644 --- a/lib/visualizeStaticReachabilityFailure.ts +++ b/lib/visualizeStaticReachabilityFailure.ts @@ -1,5 +1,5 @@ import type { GraphicsObject } from "graphics-debug" -import type { TinyHyperGraphSolver } from "./core" +import type { TinyHyperGraphSolverView } from "./solver-view" import { getZLayerLabel } from "./layerLabels" import type { PortId, RouteId } from "./types" @@ -13,7 +13,7 @@ const formatLabel = (...lines: Array) => lines.filter((line): line is string => Boolean(line)).join("\n") export const getStaticallyUnroutableRouteIds = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, ): Set | undefined => { const staticallyUnroutableRoutes = solver.getStaticallyUnroutableRoutes() if (staticallyUnroutableRoutes.length === 0) { @@ -27,7 +27,10 @@ export const getStaticallyUnroutableRouteIds = ( ) } -const getPortRenderPoint = (solver: TinyHyperGraphSolver, portId: PortId) => { +const getPortRenderPoint = ( + solver: TinyHyperGraphSolverView, + portId: PortId, +) => { const layerOffset = solver.topology.portZ[portId] * PORT_LAYER_COORDINATE_OFFSET @@ -38,12 +41,12 @@ const getPortRenderPoint = (solver: TinyHyperGraphSolver, portId: PortId) => { } const getPortVisualizationLayer = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => getZLayerLabel([solver.topology.portZ[portId]]) ?? "z0" const getPortIdentifierLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => { const metadata = solver.topology.portMetadata?.[portId] @@ -52,11 +55,13 @@ const getPortIdentifierLabel = ( return `port: ${rawPortId ?? `port-${portId}`}` } -const getPortZLabel = (solver: TinyHyperGraphSolver, portId: PortId): string => - `z: ${solver.topology.portZ[portId]}` +const getPortZLabel = ( + solver: TinyHyperGraphSolverView, + portId: PortId, +): string => `z: ${solver.topology.portZ[portId]}` const getPortPairZLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, port1Id: PortId, port2Id: PortId, ): string => { @@ -67,7 +72,7 @@ const getPortPairZLabel = ( } const getRouteEndpointZLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): string => getPortPairZLabel( @@ -77,12 +82,12 @@ const getRouteEndpointZLabel = ( ) const getRouteNetLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): string => `net: ${solver.problem.routeNet[routeId]}` export const visualizeStaticReachabilityFailure = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { for (const staticallyUnroutableRoute of solver.getStaticallyUnroutableRoutes()) { diff --git a/lib/visualizeTinyGraph.ts b/lib/visualizeTinyGraph.ts index e242be3..a1c5fd2 100644 --- a/lib/visualizeTinyGraph.ts +++ b/lib/visualizeTinyGraph.ts @@ -1,5 +1,5 @@ import type { GraphicsObject } from "graphics-debug" -import type { TinyHyperGraphSolver } from "./index" +import type { TinyHyperGraphSolverView } from "./solver-view" import { getAvailableZFromMask, getZLayerLabel } from "./layerLabels" import type { PortId, RegionId, RouteId } from "./types" import { @@ -53,7 +53,7 @@ const toRgbaString = ({ r, g, b, a }: RgbaColor) => `rgba(${r}, ${g}, ${b}, ${a})` const getRouteLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): string => { const routeMetadata = solver.problem.routeMetadata?.[routeId] @@ -65,7 +65,7 @@ const getRouteLabel = ( } const getRouteColor = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, alpha = 0.8, ): string => { @@ -83,13 +83,13 @@ const getRouteColor = ( } const isBusVisualizationSolver = ( - solver: TinyHyperGraphSolver, -): solver is TinyHyperGraphSolver & { centerRouteId: RouteId } => + solver: TinyHyperGraphSolverView, +): solver is TinyHyperGraphSolverView & { centerRouteId: RouteId } => typeof (solver as { centerRouteId?: RouteId }).centerRouteId === "number" const shouldShowBusUnassignedPorts = ( - solver: TinyHyperGraphSolver, -): solver is TinyHyperGraphSolver & { + solver: TinyHyperGraphSolverView, +): solver is TinyHyperGraphSolverView & { centerRouteId: RouteId showUnassignedPortsInVisualization: boolean } => @@ -98,7 +98,7 @@ const shouldShowBusUnassignedPorts = ( .showUnassignedPortsInVisualization === true const getRouteOpacity = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): number => { if (!isBusVisualizationSolver(solver)) { @@ -120,17 +120,20 @@ const scaleColorAlpha = (color: string, opacity: number): string => { } const getRenderedRouteColor = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, alpha = 0.8, ) => getRouteColor(solver, routeId, alpha * getRouteOpacity(solver, routeId)) const getRouteNetLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): string => `net: ${solver.problem.routeNet[routeId]}` -const getRegionBounds = (solver: TinyHyperGraphSolver, regionId: RegionId) => { +const getRegionBounds = ( + solver: TinyHyperGraphSolverView, + regionId: RegionId, +) => { const regionMetadata = solver.topology.regionMetadata?.[regionId] const polygon = regionMetadata?.polygon if (Array.isArray(polygon) && polygon.length >= 3) { @@ -168,13 +171,16 @@ const getRegionBounds = (solver: TinyHyperGraphSolver, regionId: RegionId) => { } } -const getRegionCenter = (solver: TinyHyperGraphSolver, regionId: RegionId) => ({ +const getRegionCenter = ( + solver: TinyHyperGraphSolverView, + regionId: RegionId, +) => ({ x: solver.topology.regionCenterX[regionId], y: solver.topology.regionCenterY[regionId], }) const getRegionVisualizationLayer = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): string => { const regionMetadata = solver.topology.regionMetadata?.[regionId] @@ -209,7 +215,7 @@ const getRegionVisualizationLayer = ( } const getRegionCostLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): string => { const regionCache = solver.state.regionIntersectionCaches[regionId] @@ -231,7 +237,7 @@ const getRegionCostLabel = ( } const getBaseRegionFillColor = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): RgbaColor => { const regionMetadata = solver.topology.regionMetadata?.[regionId] @@ -256,7 +262,7 @@ const getBaseRegionFillColor = ( } const getRegionRectFill = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, regionId: RegionId, ): string => { const baseFill = getBaseRegionFillColor(solver, regionId) @@ -268,12 +274,15 @@ const getRegionRectFill = ( return toRgbaString(mixColor(baseFill, HOT_REGION_FILL, redness)) } -const getPortPoint = (solver: TinyHyperGraphSolver, portId: PortId) => ({ +const getPortPoint = (solver: TinyHyperGraphSolverView, portId: PortId) => ({ x: solver.topology.portX[portId], y: solver.topology.portY[portId], }) -const getPortRenderPoint = (solver: TinyHyperGraphSolver, portId: PortId) => { +const getPortRenderPoint = ( + solver: TinyHyperGraphSolverView, + portId: PortId, +) => { const portPoint = getPortPoint(solver, portId) const layerOffset = solver.topology.portZ[portId] * PORT_LAYER_COORDINATE_OFFSET @@ -287,12 +296,12 @@ const getPortRenderPoint = (solver: TinyHyperGraphSolver, portId: PortId) => { const getPortCircleCenter = getPortRenderPoint const getPortVisualizationLayer = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => getZLayerLabel([solver.topology.portZ[portId]]) ?? "z0" const getPortIdentifierLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => { const metadata = solver.topology.portMetadata?.[portId] @@ -302,7 +311,7 @@ const getPortIdentifierLabel = ( } const getPortConnectionLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, ): string => { const r1 = solver.topology.incidentPortRegion[portId]?.[0] @@ -312,7 +321,7 @@ const getPortConnectionLabel = ( } const getPortNetLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, routeId?: RouteId, ): string | undefined => { @@ -348,11 +357,13 @@ const getPortNetLabel = ( .join(", ")}` } -const getPortZLabel = (solver: TinyHyperGraphSolver, portId: PortId): string => - `z: ${solver.topology.portZ[portId]}` +const getPortZLabel = ( + solver: TinyHyperGraphSolverView, + portId: PortId, +): string => `z: ${solver.topology.portZ[portId]}` const getPortPairZLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, port1Id: PortId, port2Id: PortId, ): string => { @@ -363,7 +374,7 @@ const getPortPairZLabel = ( } const getRouteEndpointZLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, ): string => getPortPairZLabel( @@ -373,7 +384,7 @@ const getRouteEndpointZLabel = ( ) const getPortLabel = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, portId: PortId, routeId?: RouteId, ): string => @@ -384,7 +395,7 @@ const getPortLabel = ( ) const getHighlightedSectionPortMask = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, options?: TinyHyperGraphVisualizationOptions, ) => { if (!options?.highlightSectionMask) { @@ -416,7 +427,7 @@ const getHighlightedSectionPortMask = ( } const getSectionRegionIds = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, sectionPortMask: Int8Array, ) => { const sectionRegionIds = new Set() @@ -433,7 +444,7 @@ const getSectionRegionIds = ( } const getSegmentStyle = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, routeId: RouteId, port1Id: PortId, port2Id: PortId, @@ -467,7 +478,7 @@ const getSegmentStyle = ( } const pushSolvedRegionSegments = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { for ( @@ -495,7 +506,7 @@ const pushSolvedRegionSegments = ( } const pushRoutePortZPoints = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { const seenRoutePorts = new Set() @@ -531,7 +542,10 @@ const pushRoutePortZPoints = ( } } -const isRouteEndpointPort = (solver: TinyHyperGraphSolver, portId: PortId) => { +const isRouteEndpointPort = ( + solver: TinyHyperGraphSolverView, + portId: PortId, +) => { for (let routeId = 0; routeId < solver.problem.routeCount; routeId++) { if ( solver.problem.routeStartPort[routeId] === portId || @@ -545,7 +559,7 @@ const isRouteEndpointPort = (solver: TinyHyperGraphSolver, portId: PortId) => { } const pushUnassignedPortCircles = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { for (let portId = 0; portId < solver.topology.portCount; portId++) { @@ -578,7 +592,7 @@ const pushUnassignedPortCircles = ( } const pushInitialRouteHints = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { for (let routeId = 0; routeId < solver.problem.routeCount; routeId++) { @@ -614,7 +628,7 @@ const pushInitialRouteHints = ( } const pushRouteEndpoints = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, routeIds?: Set, ) => { @@ -660,7 +674,7 @@ const pushRouteEndpoints = ( } const pushActiveRoute = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { const routeId = solver.state.currentRouteId @@ -682,7 +696,7 @@ const pushActiveRoute = ( } const pushCandidates = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { if (solver.solved) return @@ -740,7 +754,7 @@ const pushCandidates = ( } const pushSectionMaskOverlay = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, sectionPortMask: Int8Array, ) => { @@ -802,7 +816,7 @@ const pushSectionMaskOverlay = ( } const pushNeverSuccessfullyRoutedEndpoints = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, graphics: Required, ) => { if (!solver.failed) { @@ -880,7 +894,7 @@ const pushNeverSuccessfullyRoutedEndpoints = ( } export const visualizeTinyHyperGraph = ( - solver: TinyHyperGraphSolver, + solver: TinyHyperGraphSolverView, options: TinyHyperGraphVisualizationOptions = {}, ): GraphicsObject => { const graphics: Required = { diff --git a/lib2/compute-region-cost.ts b/lib2/compute-region-cost.ts new file mode 100644 index 0000000..5100584 --- /dev/null +++ b/lib2/compute-region-cost.ts @@ -0,0 +1,60 @@ +export const DEFAULT_MIN_VIA_PAD_DIAMETER = 0.3 +export const TRACE_VIA_MARGIN = 0.15 +const traceWidth = 0.1 +const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10 + +export const isKnownSingleLayerMask = (regionAvailableZMask: number) => + regionAvailableZMask > 0 && + (regionAvailableZMask & (regionAvailableZMask - 1)) === 0 + +export const computeRegionCost = ( + regionWidth: number, + regionHeight: number, + numSameLayerIntersections: number, + numCrossLayerIntersections: number, + numEntryExitChanges: number, + traceCount: number, + regionAvailableZMask = 0, + minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER, +) => { + const area = regionWidth * regionHeight + + return computeRegionCostForArea( + area, + numSameLayerIntersections, + numCrossLayerIntersections, + numEntryExitChanges, + traceCount, + regionAvailableZMask, + minViaPadDiameter, + ) +} + +export const computeRegionCostForArea = ( + area: number, + numSameLayerIntersections: number, + numCrossLayerIntersections: number, + numEntryExitChanges: number, + traceCount: number, + regionAvailableZMask = 0, + minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER, +) => { + const estViasRequired = + numSameLayerIntersections * 2 + + numCrossLayerIntersections * 1 + + numEntryExitChanges * 1 + const viaSizeWithMargin = minViaPadDiameter + TRACE_VIA_MARGIN + const viaSizeWithMarginSq = viaSizeWithMargin ** 2 + + const traceCountMult = 1 + traceCount / 5 + const impossibleSingleLayerIntersectionCost = isKnownSingleLayerMask( + regionAvailableZMask, + ) + ? numSameLayerIntersections * IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST + : 0 + + return ( + (estViasRequired * viaSizeWithMarginSq * traceCountMult) / area + + impossibleSingleLayerIntersectionCost + ) +} diff --git a/lib2/domain.ts b/lib2/domain.ts new file mode 100644 index 0000000..61c7d45 --- /dev/null +++ b/lib2/domain.ts @@ -0,0 +1,131 @@ +import type { + HopId, + NetId, + PortId, + RegionId, + RegionIntersectionCache, + RouteId, +} from "./types" +import type { MinHeap } from "./min-heap" + +export interface TinyHyperGraphTopology { + portCount: number + regionCount: number + + /** regionIncidentPorts[regionId] = list of port ids incident to the region */ + regionIncidentPorts: PortId[][] + + /** incidentPortRegion[portId] = list of region ids incident to the port */ + incidentPortRegion: RegionId[][] + + regionWidth: Float64Array + regionHeight: Float64Array + regionCenterX: Float64Array + regionCenterY: Float64Array + /** + * regionAvailableZMask[regionId] is a bitmask of the routed layers available + * within the region. A zero mask means "unknown", which preserves legacy cost + * behavior for manually-constructed topologies that do not provide this data. + */ + regionAvailableZMask?: Int32Array + + /** regionMetadata[regionId] = metadata for the region */ + regionMetadata?: any[] + + /** portAngleForRegion1[portId] = CCW angle of the port on incidentPortRegion[portId][0], where 0 is the right side and 9000 is the top */ + portAngleForRegion1: Int32Array + /** portAngleForRegion2[portId] = CCW angle of the port on incidentPortRegion[portId][1] */ + portAngleForRegion2?: Int32Array + portX: Float64Array + portY: Float64Array + portZ: Int32Array + + portMetadata?: any[] +} + +export interface TinyHyperGraphProblem { + routeCount: number + + /** + * portSectionMask[portId] = true if port in section. + * Only ports within a section can be explored to solve the problem. + */ + portSectionMask: Int8Array + + /** routeMetadata[routeId] = metadata for the route */ + routeMetadata?: any[] + + /** routeStartPort[routeId] = port id at the start of the route */ + routeStartPort: Int32Array + routeEndPort: Int32Array + + /** routeNet[routeId] = net id of the route */ + routeNet: Int32Array + /** regionNetId[regionId] = reserved net id for the region, -1 means freely traversable */ + regionNetId: Int32Array + + /** portPenalty[portId] = extra cost paid when a route traverses the port */ + portPenalty?: Float64Array +} + +export interface TinyHyperGraphProblemSetup { + /** portHCostToEndOfRoute[portId * routeCount + routeId] = distance from port to end of route */ + portHCostToEndOfRoute: Float64Array + portEndpointNetIds: Array> +} + +export interface TinyHyperGraphSolution { + /** solvedRoutePathSegments[routeId] = ordered segments for the route */ + solvedRoutePathSegments: Array<[PortId, PortId][]> + /** + * solvedRoutePathRegionIds[routeId][segmentIndex] = explicit region id for + * solvedRoutePathSegments[routeId][segmentIndex], when known from serialized + * route data. This preserves exact routed regions for replay instead of + * inferring from the port pair. + */ + solvedRoutePathRegionIds?: Array> +} + +export interface RegionCostSummary { + maxRegionCost: number + totalRegionCost: number +} + +export interface Candidate { + prevRegionId?: RegionId + portId: PortId + nextRegionId: RegionId + + prevCandidate?: Candidate + + f: number + g: number + h: number +} + +export interface TinyHyperGraphWorkingState { + /** portAssignment[portId] = NetId, -1 means unassigned */ + portAssignment: Int32Array + + /** regionSegments[regionId] = route assignment and two ports */ + regionSegments: Array<[RouteId, PortId, PortId][]> + + /** regionIntersectionCaches[regionId] = dynamic segment cost cache */ + regionIntersectionCaches: RegionIntersectionCache[] + + currentRouteNetId: NetId | undefined + currentRouteId: RouteId | undefined + + unroutedRoutes: RouteId[] + + candidateQueue: MinHeap + candidateBestCostByHopId: Float64Array | Map + candidateBestCostGenerationByHopId: Uint32Array | Map + candidateBestCostGeneration: number + + goalPortId: PortId + ripCount: number + + /** regionCongestionCost[regionId] = congestion cost */ + regionCongestionCost: Float64Array +} diff --git a/lib2/graph-input.ts b/lib2/graph-input.ts new file mode 100644 index 0000000..dcba707 --- /dev/null +++ b/lib2/graph-input.ts @@ -0,0 +1,97 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { loadSerializedHyperGraph } from "./graph-load" +import type { + TinyHyperGraphProblem, + TinyHyperGraphSolution, + TinyHyperGraphTopology, +} from "./domain" +import { err, ok, type Result } from "./prelude" + +/** A serialized graph that has passed lib2 boundary parsing. */ +export type ParsedSerializedGraph = SerializedHyperGraph + +/** Loaded solver input produced from a parsed serialized graph. */ +export type LoadedGraph = { + readonly topology: TinyHyperGraphTopology + readonly problem: TinyHyperGraphProblem + readonly solution: TinyHyperGraphSolution +} + +/** Expected failure while parsing serialized graph input. */ +export class ParseGraphError extends Error { + readonly _tag = "ParseGraphError" + readonly graphCause: unknown | undefined + + constructor( + readonly reason: string, + graphCause?: unknown, + ) { + super(`Invalid serialized graph: ${reason}`) + this.graphCause = graphCause + } +} + +/** Expected failure while loading parsed graph data into solver structures. */ +export class LoadGraphError extends Error { + readonly _tag = "LoadGraphError" + readonly graphCause: unknown + + constructor(graphCause: unknown) { + super( + graphCause instanceof Error + ? `Unable to load serialized graph: ${graphCause.message}` + : "Unable to load serialized graph", + ) + this.graphCause = graphCause + } +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null + +/** + * Parse unknown input into a serialized hypergraph boundary type. + * + * @param graph - Unknown boundary input. + * @returns A parsed serialized graph or a typed parse error. + */ +export function parseGraph( + graph: unknown, +): Result { + if (!isRecord(graph)) { + return err(new ParseGraphError("expected an object")) + } + + if (!Array.isArray(graph.regions)) { + return err(new ParseGraphError("expected regions array")) + } + + if (!Array.isArray(graph.ports)) { + return err(new ParseGraphError("expected ports array")) + } + + if (graph.connections !== undefined && !Array.isArray(graph.connections)) { + return err(new ParseGraphError("expected connections array when present")) + } + + // SAFETY: The compat loader owns detailed SerializedHyperGraph semantics. + // This parser establishes the top-level shape before handing the graph to + // that boundary adapter. + return ok(graph as ParsedSerializedGraph) +} + +/** + * Load a parsed serialized graph into topology, problem, and solution values. + * + * @param graph - Parsed serialized graph. + * @returns Loaded graph values or a typed load error. + */ +export function loadGraph( + graph: ParsedSerializedGraph, +): Result { + try { + return ok(loadSerializedHyperGraph(graph)) + } catch (cause) { + return err(new LoadGraphError(cause)) + } +} diff --git a/lib2/graph-load.ts b/lib2/graph-load.ts new file mode 100644 index 0000000..8c8d21b --- /dev/null +++ b/lib2/graph-load.ts @@ -0,0 +1,630 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import type { + TinyHyperGraphProblem, + TinyHyperGraphSolution, + TinyHyperGraphTopology, +} from "./domain" +import { getAvailableZFromMask, getZLayerLabel } from "../lib/layerLabels" + +const getSerializedRegionNetId = ( + region: SerializedHyperGraph["regions"][number], +) => { + const netId = + typeof region.d?.netId === "number" + ? region.d.netId + : typeof region.d?.NetId === "number" + ? region.d.NetId + : undefined + + return Number.isFinite(netId) ? netId : undefined +} + +const isFullObstacleRegion = ( + region: SerializedHyperGraph["regions"][number], +) => { + if (region.d?._containsObstacle !== true) { + return false + } + + if (region.d?._containsTarget !== true) { + return true + } + + const netId = getSerializedRegionNetId(region) + return netId === undefined || netId === -1 +} + +const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { + const connectedRegionIds = new Set() + for (const connection of serializedHyperGraph.connections ?? []) { + connectedRegionIds.add(connection.startRegionId) + connectedRegionIds.add(connection.endRegionId) + } + + const removedRegionIds = new Set( + serializedHyperGraph.regions + .filter( + (region) => + isFullObstacleRegion(region) && + !connectedRegionIds.has(region.regionId), + ) + .map((region) => region.regionId), + ) + + if (removedRegionIds.size === 0) { + return serializedHyperGraph + } + + const filteredPorts = serializedHyperGraph.ports.filter( + (port) => + !removedRegionIds.has(port.region1Id) && + !removedRegionIds.has(port.region2Id), + ) + + const invalidConnection = (serializedHyperGraph.connections ?? []).find( + (connection) => + removedRegionIds.has(connection.startRegionId) || + removedRegionIds.has(connection.endRegionId), + ) + + if (invalidConnection) { + throw new Error( + `Connection "${invalidConnection.connectionId}" references full-obstacle region`, + ) + } + + return { + ...serializedHyperGraph, + regions: serializedHyperGraph.regions.filter( + (region) => !removedRegionIds.has(region.regionId), + ), + ports: filteredPorts, + } +} + +const addSerializedRegionIdToMetadata = ( + region: SerializedHyperGraph["regions"][number], + layer: string, +) => { + const metadata = + region.d && typeof region.d === "object" && !Array.isArray(region.d) + ? { ...region.d } + : { value: region.d } + + metadata.layer = layer + + Object.defineProperty(metadata, "serializedRegionId", { + value: region.regionId, + enumerable: false, + configurable: true, + writable: true, + }) + + return metadata +} + +const addSerializedPortIdToMetadata = ( + port: SerializedHyperGraph["ports"][number], + layer: string, +) => { + const metadata = + port.d && typeof port.d === "object" && !Array.isArray(port.d) + ? { ...port.d } + : { value: port.d } + + metadata.layer = layer + + Object.defineProperty(metadata, "serializedPortId", { + value: port.portId, + enumerable: false, + configurable: true, + writable: true, + }) + + return metadata +} + +const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { + const bounds = region.d?.bounds + if (bounds) { + return bounds + } + + const center = region.d?.center + const width = region.d?.width + const height = region.d?.height + if ( + center && + typeof center.x === "number" && + typeof center.y === "number" && + typeof width === "number" && + typeof height === "number" + ) { + return { + minX: center.x - width / 2, + maxX: center.x + width / 2, + minY: center.y - height / 2, + maxY: center.y + height / 2, + } + } + + return { + minX: 0, + maxX: 0, + minY: 0, + maxY: 0, + } +} + +const getRegionGeometry = ( + region: SerializedHyperGraph["regions"][number], +): { centerX: number; centerY: number; width: number; height: number } => { + const bounds = getRegionBounds(region) + const width = + typeof region.d?.width === "number" + ? region.d.width + : bounds.maxX - bounds.minX + const height = + typeof region.d?.height === "number" + ? region.d.height + : bounds.maxY - bounds.minY + + return { + centerX: + typeof region.d?.center?.x === "number" + ? region.d.center.x + : (bounds.minX + bounds.maxX) / 2, + centerY: + typeof region.d?.center?.y === "number" + ? region.d.center.y + : (bounds.minY + bounds.maxY) / 2, + width, + height, + } +} + +const getRegionAvailableZMask = ( + region: SerializedHyperGraph["regions"][number], +): number => { + const availableZ = region.d?.availableZ + if (!Array.isArray(availableZ)) { + return 0 + } + + let mask = 0 + for (const z of availableZ) { + if (!Number.isInteger(z) || z < 0 || z >= 31) { + continue + } + mask |= 1 << z + } + + return mask +} + +const getSerializedPortZ = ( + port: SerializedHyperGraph["ports"][number], +): number => { + const z = Number(port.d?.z ?? 0) + return Number.isFinite(z) ? z : 0 +} + +const getSerializedPortX = ( + port: SerializedHyperGraph["ports"][number], +): number => Number(port.d?.x ?? 0) + +const getSerializedPortY = ( + port: SerializedHyperGraph["ports"][number], +): number => Number(port.d?.y ?? 0) + +const computePortAngle = ( + port: SerializedHyperGraph["ports"][number], + region: SerializedHyperGraph["regions"][number] | undefined, +): number => { + if (!region) return 0 + + const bounds = getRegionBounds(region) + const x = getSerializedPortX(port) + const y = getSerializedPortY(port) + const withinXBounds = bounds.minX <= x && x <= bounds.maxX + const withinYBounds = bounds.minY <= y && y <= bounds.maxY + + // Prefer the side whose outward half-plane actually contains the port. + // This keeps ports above/below a region from being misclassified onto a + // nearby vertical edge when they sit close to a corner. + if (withinYBounds && x >= bounds.maxX) { + const safeHeight = Math.max(bounds.maxY - bounds.minY, 1e-9) + const t = (y - bounds.minY) / safeHeight + return Math.round(t * 9000) + } + + if (withinXBounds && y >= bounds.maxY) { + const safeWidth = Math.max(bounds.maxX - bounds.minX, 1e-9) + const t = (bounds.maxX - x) / safeWidth + return 9000 + Math.round(t * 9000) + } + + if (withinYBounds && x <= bounds.minX) { + const safeHeight = Math.max(bounds.maxY - bounds.minY, 1e-9) + const t = (bounds.maxY - y) / safeHeight + return 18000 + Math.round(t * 9000) + } + + if (withinXBounds && y <= bounds.minY) { + const safeWidth = Math.max(bounds.maxX - bounds.minX, 1e-9) + const t = (x - bounds.minX) / safeWidth + return 27000 + Math.round(t * 9000) + } + + const distLeft = Math.abs(x - bounds.minX) + const distRight = Math.abs(x - bounds.maxX) + const distBottom = Math.abs(y - bounds.minY) + const distTop = Math.abs(y - bounds.maxY) + const minDist = Math.min(distLeft, distRight, distBottom, distTop) + + const safeWidth = Math.max(bounds.maxX - bounds.minX, 1e-9) + const safeHeight = Math.max(bounds.maxY - bounds.minY, 1e-9) + + if (minDist === distRight) { + const t = (y - bounds.minY) / safeHeight + return Math.round(t * 9000) + } + + if (minDist === distTop) { + const t = (bounds.maxX - x) / safeWidth + return 9000 + Math.round(t * 9000) + } + + if (minDist === distLeft) { + const t = (bounds.maxY - y) / safeHeight + return 18000 + Math.round(t * 9000) + } + + const t = (x - bounds.minX) / safeWidth + return 27000 + Math.round(t * 9000) +} + +const getCentermostPortIdForRegion = ( + region: SerializedHyperGraph["regions"][number] | undefined, + portById: Map, +): string | undefined => { + if (!region) return undefined + + const sortedPortIds = [...region.pointIds].sort((a, b) => { + const portA = portById.get(a) + const portB = portById.get(b) + const distA = Number( + portA?.d?.distToCentermostPortOnZ ?? Number.POSITIVE_INFINITY, + ) + const distB = Number( + portB?.d?.distToCentermostPortOnZ ?? Number.POSITIVE_INFINITY, + ) + + if (distA !== distB) return distA - distB + + const zA = Number(portA?.d?.z ?? 0) + const zB = Number(portB?.d?.z ?? 0) + if (zA !== zB) return zA - zB + + return a.localeCompare(b) + }) + + return sortedPortIds[0] +} + +const getSharedPortIdsForConnection = ( + serializedHyperGraph: SerializedHyperGraph, + connection: NonNullable[number], +): string[] => + serializedHyperGraph.ports + .filter( + (port) => + (port.region1Id === connection.startRegionId && + port.region2Id === connection.endRegionId) || + (port.region2Id === connection.startRegionId && + port.region1Id === connection.endRegionId), + ) + .map((port) => port.portId) + +export const loadSerializedHyperGraph = ( + serializedHyperGraph: SerializedHyperGraph, +): { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + solution: TinyHyperGraphSolution +} => { + const filteredHyperGraph = filterObstacleRegions(serializedHyperGraph) + const regionIdToIndex = new Map() + const portIdToIndex = new Map() + const portById = new Map() + const solvedRouteByConnectionId = new Map( + (filteredHyperGraph.solvedRoutes ?? []).map((route) => [ + route.connection.connectionId, + route, + ]), + ) + + filteredHyperGraph.regions.forEach((region, regionIndex) => { + regionIdToIndex.set(region.regionId, regionIndex) + }) + + filteredHyperGraph.ports.forEach((port, portIndex) => { + portIdToIndex.set(port.portId, portIndex) + portById.set(port.portId, port) + }) + + const regionCount = filteredHyperGraph.regions.length + const portCount = filteredHyperGraph.ports.length + + const regionIncidentPorts = filteredHyperGraph.regions.map((region) => + region.pointIds + .map((portId) => portIdToIndex.get(portId)) + .filter((portIndex): portIndex is number => portIndex !== undefined), + ) + + const incidentPortRegion = Array.from( + { length: portCount }, + () => [] as number[], + ) + const regionWidth = new Float64Array(regionCount) + const regionHeight = new Float64Array(regionCount) + const regionCenterX = new Float64Array(regionCount) + const regionCenterY = new Float64Array(regionCount) + const regionAvailableZMask = new Int32Array(regionCount) + const regionNetId = new Int32Array(regionCount).fill(-1) + const hasSerializedRegionNetId = new Int8Array(regionCount) + + filteredHyperGraph.regions.forEach((region, regionIndex) => { + const geometry = getRegionGeometry(region) + regionWidth[regionIndex] = geometry.width + regionHeight[regionIndex] = geometry.height + regionCenterX[regionIndex] = geometry.centerX + regionCenterY[regionIndex] = geometry.centerY + regionAvailableZMask[regionIndex] = getRegionAvailableZMask(region) + + const serializedRegionNetId = getSerializedRegionNetId(region) + if (serializedRegionNetId !== undefined) { + regionNetId[regionIndex] = serializedRegionNetId + hasSerializedRegionNetId[regionIndex] = 1 + } + }) + + const portAngleForRegion1 = new Int32Array(portCount) + const portAngleForRegion2 = new Int32Array(portCount) + const portX = new Float64Array(portCount) + const portY = new Float64Array(portCount) + const portZ = new Int32Array(portCount) + + filteredHyperGraph.ports.forEach((port, portIndex) => { + const region1Index = regionIdToIndex.get(port.region1Id) + const region2Index = regionIdToIndex.get(port.region2Id) + + if (region1Index === undefined || region2Index === undefined) { + throw new Error( + `Port "${port.portId}" references missing regions "${port.region1Id}" or "${port.region2Id}"`, + ) + } + + incidentPortRegion[portIndex] = [region1Index, region2Index] + portX[portIndex] = getSerializedPortX(port) + portY[portIndex] = getSerializedPortY(port) + portZ[portIndex] = getSerializedPortZ(port) + portAngleForRegion1[portIndex] = computePortAngle( + port, + filteredHyperGraph.regions[region1Index], + ) + portAngleForRegion2[portIndex] = computePortAngle( + port, + filteredHyperGraph.regions[region2Index], + ) + }) + + const getRegionLayer = (regionIndex: number): string => + getZLayerLabel(getAvailableZFromMask(regionAvailableZMask[regionIndex])) ?? + getZLayerLabel( + (regionIncidentPorts[regionIndex] ?? []).map( + (portIndex) => portZ[portIndex], + ), + ) ?? + "z0" + + const regionMetadata = filteredHyperGraph.regions.map((region, regionIndex) => + addSerializedRegionIdToMetadata(region, getRegionLayer(regionIndex)), + ) + const portMetadata = filteredHyperGraph.ports.map((port, portIndex) => + addSerializedPortIdToMetadata( + port, + getZLayerLabel([portZ[portIndex]]) ?? "z0", + ), + ) + + const connections = filteredHyperGraph.connections ?? [] + const netIdToIndex = new Map() + let nextNetIndex = 0 + const getNetIndex = (connection: (typeof connections)[number]) => { + const netId = + connection.mutuallyConnectedNetworkId ?? connection.connectionId + let netIndex = netIdToIndex.get(netId) + if (netIndex === undefined) { + netIndex = nextNetIndex++ + netIdToIndex.set(netId, netIndex) + } + return netIndex + } + + const regionNetCandidates = Array.from( + { length: regionCount }, + () => new Set(), + ) + + const assignRegionNet = (regionId: string, netIndex: number) => { + const regionIndex = regionIdToIndex.get(regionId) + if (regionIndex === undefined) { + throw new Error(`Connection references missing region "${regionId}"`) + } + + regionNetCandidates[regionIndex]!.add(netIndex) + } + + connections.forEach((connection) => { + const netIndex = getNetIndex(connection) + assignRegionNet(connection.startRegionId, netIndex) + assignRegionNet(connection.endRegionId, netIndex) + }) + + regionNetCandidates.forEach((candidateNetIndexes, regionIndex) => { + if (hasSerializedRegionNetId[regionIndex] === 1) { + return + } + + if (candidateNetIndexes.size === 1) { + regionNetId[regionIndex] = [...candidateNetIndexes][0]! + } + }) + + const routableConnections = connections + .map((connection) => { + const solvedRoute = solvedRouteByConnectionId.get(connection.connectionId) + const sharedPortIds = getSharedPortIdsForConnection( + filteredHyperGraph, + connection, + ) + + return { + connection, + solvedRoute, + sharedPortIds, + } + }) + .filter( + ({ solvedRoute, sharedPortIds }) => + sharedPortIds.length === 0 || (solvedRoute?.path.length ?? 0) > 1, + ) + + const routeCount = routableConnections.length + const portSectionMask = new Int8Array(portCount).fill(1) + const routeStartPort = new Int32Array(routeCount) + const routeEndPort = new Int32Array(routeCount) + const routeNet = new Int32Array(routeCount) + + routableConnections.forEach(({ connection, solvedRoute }, routeIndex) => { + const fallbackStartPortId = getCentermostPortIdForRegion( + filteredHyperGraph.regions.find( + (region) => region.regionId === connection.startRegionId, + ), + portById, + ) + const fallbackEndPortId = getCentermostPortIdForRegion( + filteredHyperGraph.regions.find( + (region) => region.regionId === connection.endRegionId, + ), + portById, + ) + + const startPortId = solvedRoute?.path[0]?.portId ?? fallbackStartPortId + const endPortId = + solvedRoute?.path[solvedRoute.path.length - 1]?.portId ?? + fallbackEndPortId + + const startPortIndex = + startPortId !== undefined ? portIdToIndex.get(startPortId) : undefined + const endPortIndex = + endPortId !== undefined ? portIdToIndex.get(endPortId) : undefined + + if (startPortIndex === undefined || endPortIndex === undefined) { + throw new Error( + `Connection "${connection.connectionId}" could not be mapped to route endpoints`, + ) + } + + routeStartPort[routeIndex] = startPortIndex + routeEndPort[routeIndex] = endPortIndex + + routeNet[routeIndex] = getNetIndex(connection) + }) + + const topology: TinyHyperGraphTopology = { + portCount, + regionCount, + regionIncidentPorts, + incidentPortRegion, + regionWidth, + regionHeight, + regionCenterX, + regionCenterY, + regionAvailableZMask, + regionMetadata, + portAngleForRegion1, + portAngleForRegion2, + portX, + portY, + portZ, + portMetadata, + } + + const problem: TinyHyperGraphProblem = { + routeCount, + portSectionMask, + routeMetadata: routableConnections.map(({ connection }) => connection), + routeStartPort, + routeEndPort, + routeNet, + regionNetId, + } + + const solvedRoutePathSegments: TinyHyperGraphSolution["solvedRoutePathSegments"] = + [] + const solvedRoutePathRegionIds: NonNullable< + TinyHyperGraphSolution["solvedRoutePathRegionIds"] + > = [] + + for (const { solvedRoute: route } of routableConnections) { + if (!route) { + solvedRoutePathSegments.push([]) + solvedRoutePathRegionIds.push([]) + continue + } + + const segments: Array<[number, number]> = [] + const segmentRegionIds: Array = [] + + for (let i = 1; i < route.path.length; i++) { + const fromCandidate = route.path[i - 1] + const toCandidate = route.path[i] + const fromPortId = fromCandidate?.portId + const toPortId = toCandidate?.portId + const fromPortIndex = + fromPortId !== undefined ? portIdToIndex.get(fromPortId) : undefined + const toPortIndex = + toPortId !== undefined ? portIdToIndex.get(toPortId) : undefined + + if (fromPortIndex === undefined || toPortIndex === undefined) { + continue + } + + const serializedRegionId = + typeof fromCandidate?.nextRegionId === "string" + ? fromCandidate.nextRegionId + : typeof toCandidate?.lastRegionId === "string" + ? toCandidate.lastRegionId + : undefined + + segments.push([fromPortIndex, toPortIndex]) + segmentRegionIds.push( + serializedRegionId !== undefined + ? regionIdToIndex.get(serializedRegionId) + : undefined, + ) + } + + solvedRoutePathSegments.push(segments) + solvedRoutePathRegionIds.push(segmentRegionIds) + } + + const solution: TinyHyperGraphSolution = { + solvedRoutePathSegments, + solvedRoutePathRegionIds, + } + + return { topology, problem, solution } +} diff --git a/lib2/graph-output.ts b/lib2/graph-output.ts new file mode 100644 index 0000000..4f51622 --- /dev/null +++ b/lib2/graph-output.ts @@ -0,0 +1,429 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import type { TinyHyperGraphSolver2View } from "./solver-view" +import { getAvailableZFromMask, getZLayerLabel } from "../lib/layerLabels" +import type { PortId, RegionId } from "./types" + +type SerializedConnection = NonNullable< + SerializedHyperGraph["connections"] +>[number] +type SerializedSolvedRoute = NonNullable< + SerializedHyperGraph["solvedRoutes"] +>[number] +type SerializedCandidate = SerializedSolvedRoute["path"][number] + +interface RouteSegment { + regionId: RegionId + fromPortId: PortId + toPortId: PortId +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null + +const toObjectRecord = (value: unknown): Record => { + if (isRecord(value)) return { ...value } + if (value === undefined) return {} + return { value } +} + +const normalizePortIdFallback = (value: string) => + value.includes("::") ? value.slice(0, value.indexOf("::")) : value + +const getSerializedRegionId = ( + solver: TinyHyperGraphSolver2View, + regionId: RegionId, +): string => { + const metadata = solver.topology.regionMetadata?.[regionId] + if (isRecord(metadata)) { + if (typeof metadata.serializedRegionId === "string") { + return metadata.serializedRegionId + } + + if (typeof metadata.regionId === "string") { + return metadata.regionId + } + + if (typeof metadata.capacityMeshNodeId === "string") { + return metadata.capacityMeshNodeId + } + } + + return `region-${regionId}` +} + +const getSerializedPortId = ( + solver: TinyHyperGraphSolver2View, + portId: PortId, +): string => { + const metadata = solver.topology.portMetadata?.[portId] + if (isRecord(metadata)) { + if (typeof metadata.serializedPortId === "string") { + return metadata.serializedPortId + } + + if (typeof metadata.portId === "string") { + return normalizePortIdFallback(metadata.portId) + } + } + + return `port-${portId}` +} + +const getSerializedRegionData = ( + solver: TinyHyperGraphSolver2View, + regionId: RegionId, +): Record => { + const data = toObjectRecord(solver.topology.regionMetadata?.[regionId]) + + if (!isRecord(data.center)) { + data.center = { + x: solver.topology.regionCenterX[regionId], + y: solver.topology.regionCenterY[regionId], + } + } + + if (typeof data.width !== "number") { + data.width = solver.topology.regionWidth[regionId] + } + + if (typeof data.height !== "number") { + data.height = solver.topology.regionHeight[regionId] + } + + if (!Array.isArray(data.availableZ)) { + const availableZMask = solver.topology.regionAvailableZMask?.[regionId] ?? 0 + if (availableZMask !== 0) { + data.availableZ = getAvailableZFromMask(availableZMask) + } + } + + data.layer = + getZLayerLabel(Array.isArray(data.availableZ) ? data.availableZ : []) ?? + getZLayerLabel( + (solver.topology.regionIncidentPorts[regionId] ?? []).map( + (portId) => solver.topology.portZ[portId], + ), + ) ?? + "z0" + + return data +} + +const getSerializedPortData = ( + solver: TinyHyperGraphSolver2View, + portId: PortId, +): Record => { + const data = toObjectRecord(solver.topology.portMetadata?.[portId]) + + if (typeof data.x !== "number") { + data.x = solver.topology.portX[portId] + } + + if (typeof data.y !== "number") { + data.y = solver.topology.portY[portId] + } + + if (typeof data.z !== "number") { + data.z = solver.topology.portZ[portId] + } + + data.layer = getZLayerLabel([solver.topology.portZ[portId]]) ?? "z0" + + return data +} + +const getRouteSegmentsByRoute = ( + solver: TinyHyperGraphSolver2View, +): Array => { + const routeSegmentsByRoute = Array.from( + { length: solver.problem.routeCount }, + () => [] as RouteSegment[], + ) + + solver.state.regionSegments.forEach((regionSegments, regionId) => { + for (const [routeId, fromPortId, toPortId] of regionSegments) { + routeSegmentsByRoute[routeId]!.push({ + regionId, + fromPortId, + toPortId, + }) + } + }) + + return routeSegmentsByRoute +} + +const getOppositeRegionIdForPort = ( + solver: TinyHyperGraphSolver2View, + portId: PortId, + traversedRegionId: RegionId, +): RegionId => { + const incidentRegionIds = solver.topology.incidentPortRegion[portId] ?? [] + const oppositeRegionId = incidentRegionIds.find( + (regionId) => regionId !== traversedRegionId, + ) + + if (oppositeRegionId === undefined) { + throw new Error( + `Port ${portId} is not incident to a region outside route region ${traversedRegionId}`, + ) + } + + return oppositeRegionId +} + +const getOrderedRoutePath = ( + solver: TinyHyperGraphSolver2View, + routeId: number, + routeSegments: RouteSegment[], +): { + orderedPortIds: PortId[] + orderedRegionIds: RegionId[] +} => { + if (routeSegments.length === 0) { + throw new Error(`Route ${routeId} has no solved segments`) + } + + const startPortId = solver.problem.routeStartPort[routeId] + const endPortId = solver.problem.routeEndPort[routeId] + const segmentsByPort = new Map< + PortId, + Array + >() + + routeSegments.forEach((routeSegment, segmentIndex) => { + const indexedRouteSegment = { + ...routeSegment, + segmentIndex, + } + + const fromPortSegments = segmentsByPort.get(routeSegment.fromPortId) ?? [] + fromPortSegments.push(indexedRouteSegment) + segmentsByPort.set(routeSegment.fromPortId, fromPortSegments) + + const toPortSegments = segmentsByPort.get(routeSegment.toPortId) ?? [] + toPortSegments.push(indexedRouteSegment) + segmentsByPort.set(routeSegment.toPortId, toPortSegments) + }) + + const orderedPortIds = [startPortId] + const orderedRegionIds: RegionId[] = [] + + const appendSimplePathToEnd = ( + currentPortId: PortId, + usedSegmentIndices: Set, + visitedPortIds: Set, + ): boolean => { + if (currentPortId === endPortId) { + return true + } + + for (const routeSegment of segmentsByPort.get(currentPortId) ?? []) { + if (usedSegmentIndices.has(routeSegment.segmentIndex)) continue + + const nextPortId = + routeSegment.fromPortId === currentPortId + ? routeSegment.toPortId + : routeSegment.fromPortId + + if (visitedPortIds.has(nextPortId)) continue + + usedSegmentIndices.add(routeSegment.segmentIndex) + visitedPortIds.add(nextPortId) + orderedRegionIds.push(routeSegment.regionId) + orderedPortIds.push(nextPortId) + + if ( + appendSimplePathToEnd(nextPortId, usedSegmentIndices, visitedPortIds) + ) { + return true + } + + orderedPortIds.pop() + orderedRegionIds.pop() + visitedPortIds.delete(nextPortId) + usedSegmentIndices.delete(routeSegment.segmentIndex) + } + + return false + } + + if ( + !appendSimplePathToEnd( + startPortId, + new Set(), + new Set([startPortId]), + ) + ) { + throw new Error( + `Route ${routeId} is not a single ordered path from ${startPortId} to ${endPortId}`, + ) + } + + return { + orderedPortIds, + orderedRegionIds, + } +} + +const getSerializedConnection = ( + solver: TinyHyperGraphSolver2View, + routeId: number, + startRegionId: string, + endRegionId: string, +): SerializedConnection => { + const routeMetadata = solver.problem.routeMetadata?.[routeId] + const metadataConnectionId = + isRecord(routeMetadata) && typeof routeMetadata.connectionId === "string" + ? routeMetadata.connectionId + : undefined + const metadataStartRegionId = + isRecord(routeMetadata) && typeof routeMetadata.startRegionId === "string" + ? routeMetadata.startRegionId + : undefined + const metadataEndRegionId = + isRecord(routeMetadata) && typeof routeMetadata.endRegionId === "string" + ? routeMetadata.endRegionId + : undefined + const metadataNetworkId = + isRecord(routeMetadata) && + typeof routeMetadata.mutuallyConnectedNetworkId === "string" + ? routeMetadata.mutuallyConnectedNetworkId + : undefined + + return { + connectionId: metadataConnectionId ?? `route-${routeId}`, + startRegionId: metadataStartRegionId ?? startRegionId, + endRegionId: metadataEndRegionId ?? endRegionId, + mutuallyConnectedNetworkId: + metadataNetworkId ?? `net-${solver.problem.routeNet[routeId]}`, + } +} + +const getSerializedSolvedRoute = ( + solver: TinyHyperGraphSolver2View, + routeId: number, + routeSegments: RouteSegment[], +): { + connection: SerializedConnection + solvedRoute: SerializedSolvedRoute +} => { + const { orderedPortIds, orderedRegionIds } = getOrderedRoutePath( + solver, + routeId, + routeSegments, + ) + + const firstRegionId = orderedRegionIds[0] + const lastRegionId = orderedRegionIds[orderedRegionIds.length - 1] + + if (firstRegionId === undefined || lastRegionId === undefined) { + throw new Error(`Route ${routeId} could not determine endpoint regions`) + } + + const fallbackStartRegionId = getSerializedRegionId( + solver, + getOppositeRegionIdForPort(solver, orderedPortIds[0]!, firstRegionId), + ) + const fallbackEndRegionId = getSerializedRegionId( + solver, + getOppositeRegionIdForPort( + solver, + orderedPortIds[orderedPortIds.length - 1]!, + lastRegionId, + ), + ) + const connection = getSerializedConnection( + solver, + routeId, + fallbackStartRegionId, + fallbackEndRegionId, + ) + + const path = orderedPortIds.map((portId, pathIndex) => { + const serializedCandidate: SerializedCandidate = { + portId: getSerializedPortId(solver, portId), + g: pathIndex, + h: 0, + f: pathIndex, + hops: pathIndex, + ripRequired: false, + nextRegionId: + pathIndex < orderedRegionIds.length + ? getSerializedRegionId(solver, orderedRegionIds[pathIndex]!) + : connection.endRegionId, + } + + if (pathIndex > 0) { + serializedCandidate.lastPortId = getSerializedPortId( + solver, + orderedPortIds[pathIndex - 1]!, + ) + serializedCandidate.lastRegionId = getSerializedRegionId( + solver, + orderedRegionIds[pathIndex - 1]!, + ) + } + + return serializedCandidate + }) + + return { + connection, + solvedRoute: { + connection, + path, + requiredRip: false, + }, + } +} + +export const convertToSerializedHyperGraph = ( + solver: TinyHyperGraphSolver2View, +): SerializedHyperGraph => { + if (!solver.solved || solver.failed) { + throw new Error( + "convertToSerializedHyperGraph requires a solved, non-failed solver", + ) + } + + const { topology } = solver + const routeSegmentsByRoute = getRouteSegmentsByRoute(solver) + + const regions = Array.from( + { length: topology.regionCount }, + (_, regionId) => ({ + regionId: getSerializedRegionId(solver, regionId), + pointIds: topology.regionIncidentPorts[regionId]!.map((portId) => + getSerializedPortId(solver, portId), + ), + d: getSerializedRegionData(solver, regionId), + }), + ) + + const ports = Array.from({ length: topology.portCount }, (_, portId) => { + const [region1Id, region2Id] = topology.incidentPortRegion[portId] ?? [] + + if (region1Id === undefined || region2Id === undefined) { + throw new Error(`Port ${portId} is missing incident regions`) + } + + return { + portId: getSerializedPortId(solver, portId), + region1Id: getSerializedRegionId(solver, region1Id), + region2Id: getSerializedRegionId(solver, region2Id), + d: getSerializedPortData(solver, portId), + } + }) + + const serializedRoutes = routeSegmentsByRoute.map((routeSegments, routeId) => + getSerializedSolvedRoute(solver, routeId, routeSegments), + ) + + return { + regions, + ports, + connections: serializedRoutes.map(({ connection }) => connection), + solvedRoutes: serializedRoutes.map(({ solvedRoute }) => solvedRoute), + } +} diff --git a/lib2/index.ts b/lib2/index.ts new file mode 100644 index 0000000..5995a0e --- /dev/null +++ b/lib2/index.ts @@ -0,0 +1,29 @@ +export type { + LoadedGraph, + ParsedSerializedGraph, +} from "./graph-input" +export { + LoadGraphError, + ParseGraphError, + loadGraph, + parseGraph, +} from "./graph-input" +export { loadSerializedHyperGraph } from "./graph-load" +export { convertToSerializedHyperGraph } from "./graph-output" +export type * from "./domain" +export type * from "./types" +export type { Result } from "./prelude" +export { capture, err, getErrorMessage, ok } from "./prelude" +export { TinyHyperGraphSectionPipelineSolver2 } from "./section-pipeline" +export { + TinyHyperGraphSectionSolver2, + getActiveSectionRouteIds, + type TinyHyperGraphSectionSolver2Options, +} from "./section-solver" +export type { SolvedGraph } from "./solver" +export { + SolveGraphError, + TinyHyperGraphSolver2, + createSolver, + solveGraph, +} from "./solver" diff --git a/lib2/min-heap.ts b/lib2/min-heap.ts new file mode 100644 index 0000000..cbedb41 --- /dev/null +++ b/lib2/min-heap.ts @@ -0,0 +1,92 @@ +export class MinHeap { + constructor( + private readonly items: T[], + private readonly compare: (left: T, right: T) => number, + ) {} + + get length(): number { + return this.items.length + } + + toArray(): T[] { + return [...this.items] + } + + clear() { + this.items.length = 0 + } + + queue(item: T) { + this.items.push(item) + this.siftUp(this.items.length - 1) + } + + dequeue(): T | undefined { + const bestItem = this.items[0] + if (bestItem === undefined) { + return undefined + } + + const lastItem = this.items.pop()! + if (this.items.length > 0) { + this.items[0] = lastItem + this.siftDown(0) + } + + return bestItem + } + + private siftUp(startIndex: number) { + let index = startIndex + + while (index > 0) { + const parentIndex = (index - 1) >> 1 + const parent = this.items[parentIndex]! + const item = this.items[index]! + + if (this.compare(parent, item) <= 0) { + return + } + + this.items[parentIndex] = item + this.items[index] = parent + index = parentIndex + } + } + + private siftDown(startIndex: number) { + let index = startIndex + const length = this.items.length + + while (true) { + const leftChildIndex = index * 2 + 1 + if (leftChildIndex >= length) { + return + } + + const rightChildIndex = leftChildIndex + 1 + let smallestChildIndex = leftChildIndex + + if ( + rightChildIndex < length && + this.compare( + this.items[rightChildIndex]!, + this.items[leftChildIndex]!, + ) < 0 + ) { + smallestChildIndex = rightChildIndex + } + + const item = this.items[index]! + const smallestChild = this.items[smallestChildIndex]! + + if (this.compare(item, smallestChild) <= 0) { + return + } + + this.items[index] = smallestChild + this.items[smallestChildIndex] = item + index = smallestChildIndex + } + } +} diff --git a/lib2/prelude.ts b/lib2/prelude.ts new file mode 100644 index 0000000..3376a4a --- /dev/null +++ b/lib2/prelude.ts @@ -0,0 +1,56 @@ +/** A typed success or expected-failure result. */ +export type Result = + | { readonly _tag: "ok"; readonly value: T } + | { readonly _tag: "err"; readonly error: E } + +/** + * Create a successful result. + * + * @template T - Success value type. + * @param value - The successful value. + * @returns A tagged success result. + */ +export function ok(value: T): Result { + return { _tag: "ok", value } +} + +/** + * Create a failed result. + * + * @template E - Expected error type. + * @param error - The expected error value. + * @returns A tagged error result. + */ +export function err(error: E): Result { + return { _tag: "err", error } +} + +/** + * Run a function and classify thrown values as an expected error. + * + * @template T - Success value type. + * @template E - Expected error type. + * @param run - Work that may throw at a legacy boundary. + * @param mapError - Converts the thrown value into a typed error. + * @returns A success result or a typed error result. + */ +export function capture( + run: () => T, + mapError: (cause: unknown) => E, +): Result { + try { + return ok(run()) + } catch (cause) { + return err(mapError(cause)) + } +} + +/** + * Convert an unknown thrown value to a readable message. + * + * @param cause - Unknown thrown value. + * @returns A message safe to expose in test and benchmark output. + */ +export function getErrorMessage(cause: unknown): string { + return cause instanceof Error ? cause.message : String(cause) +} diff --git a/lib2/region-cache.ts b/lib2/region-cache.ts new file mode 100644 index 0000000..f067995 --- /dev/null +++ b/lib2/region-cache.ts @@ -0,0 +1,263 @@ +import type { RegionIntersectionCache } from "./types" +import type { NetId } from "./types" +import type { SegmentGeometry } from "./segment-geometry" + +/** Counts added by one candidate segment. */ +export type RegionSegmentDelta = { + readonly sameLayerIntersections: number + readonly crossingLayerIntersections: number + readonly entryExitLayerChanges: number +} + +/** Computes region cost from accumulated intersection counters. */ +export type RegionCostFn = ( + sameLayerIntersections: number, + crossingLayerIntersections: number, + entryExitLayerChanges: number, + segmentCount: number, +) => number + +const EMPTY_INT32 = new Int32Array(0) + +/** + * Create the public empty region cache shape. + * + * @returns An empty region intersection cache. + */ +export function createEmptyCache(): RegionIntersectionCache { + return { + netIds: EMPTY_INT32, + lesserAngles: EMPTY_INT32, + greaterAngles: EMPTY_INT32, + layerMasks: EMPTY_INT32, + existingCrossingLayerIntersections: 0, + existingSameLayerIntersections: 0, + existingEntryExitLayerChanges: 0, + existingRegionCost: 0, + existingSegmentCount: 0, + } +} + +/** + * Mutable backing store for one region's segment cache. + * + * The public solver state still receives exact-length typed-array views. The + * backing store keeps capacity between appends, so lib2 avoids copying all + * previous segment arrays on every committed segment. + */ +export class MutableRegionCache { + private netIds: Int32Array + private lesserAngles: Int32Array + private greaterAngles: Int32Array + private layerMasks: Int32Array + private segmentCount: number + private visibleCache: RegionIntersectionCache + + private constructor(cache: RegionIntersectionCache) { + this.segmentCount = cache.existingSegmentCount + const capacity = Math.max(1, this.segmentCount) + this.netIds = new Int32Array(capacity) + this.lesserAngles = new Int32Array(capacity) + this.greaterAngles = new Int32Array(capacity) + this.layerMasks = new Int32Array(capacity) + this.netIds.set(cache.netIds) + this.lesserAngles.set(cache.lesserAngles) + this.greaterAngles.set(cache.greaterAngles) + this.layerMasks.set(cache.layerMasks) + this.visibleCache = cache + } + + /** + * Create mutable backing storage from a public cache. + * + * @param cache - Public exact-length cache. + * @returns Mutable cache storage. + */ + static from(cache: RegionIntersectionCache): MutableRegionCache { + return new MutableRegionCache(cache) + } + + /** + * Check whether this store still owns the public cache in solver state. + * + * @param cache - Public cache currently stored on the solver state. + * @returns True when the store and public state are aligned. + */ + owns(cache: RegionIntersectionCache): boolean { + return this.visibleCache === cache + } + + /** Current same-layer intersection count. */ + get sameLayerIntersections(): number { + return this.visibleCache.existingSameLayerIntersections + } + + /** Current crossing-layer intersection count. */ + get crossingLayerIntersections(): number { + return this.visibleCache.existingCrossingLayerIntersections + } + + /** Current entry/exit layer-change count. */ + get entryExitLayerChanges(): number { + return this.visibleCache.existingEntryExitLayerChanges + } + + /** Current committed segment count. */ + get committedSegmentCount(): number { + return this.segmentCount + } + + /** Current computed region cost. */ + get regionCost(): number { + return this.visibleCache.existingRegionCost + } + + /** + * Count the intersections added by a candidate segment. + * + * @param netId - Net id for the candidate route. + * @param geometry - Segment geometry inside this region. + * @returns Delta counters for the candidate segment. + */ + countDelta(netId: NetId, geometry: SegmentGeometry): RegionSegmentDelta { + let sameLayerIntersections = 0 + let crossingLayerIntersections = 0 + + for (let index = 0; index < this.segmentCount; index += 1) { + if (netId === this.netIds[index]) { + continue + } + + const lesserAngleIsInsideInterval = + geometry.lesserAngle < this.lesserAngles[index] && + this.lesserAngles[index] < geometry.greaterAngle + const greaterAngleIsInsideInterval = + geometry.lesserAngle < this.greaterAngles[index] && + this.greaterAngles[index] < geometry.greaterAngle + + if (lesserAngleIsInsideInterval === greaterAngleIsInsideInterval) { + continue + } + + if ((geometry.layerMask & this.layerMasks[index]) !== 0) { + sameLayerIntersections += 1 + } else { + crossingLayerIntersections += 1 + } + } + + return { + sameLayerIntersections, + crossingLayerIntersections, + entryExitLayerChanges: geometry.entryExitLayerChanges, + } + } + + /** + * Compute the incremental region cost for a candidate segment. + * + * @param delta - Candidate segment delta counters. + * @param computeRegionCost - Region cost function for this region. + * @returns Additional cost compared with the current region cost. + */ + getAddedCost( + delta: RegionSegmentDelta, + computeRegionCost: RegionCostFn, + ): number { + const nextSameLayerIntersections = + this.visibleCache.existingSameLayerIntersections + + delta.sameLayerIntersections + const nextCrossingLayerIntersections = + this.visibleCache.existingCrossingLayerIntersections + + delta.crossingLayerIntersections + const nextEntryExitLayerChanges = + this.visibleCache.existingEntryExitLayerChanges + + delta.entryExitLayerChanges + const nextSegmentCount = this.segmentCount + 1 + + return ( + computeRegionCost( + nextSameLayerIntersections, + nextCrossingLayerIntersections, + nextEntryExitLayerChanges, + nextSegmentCount, + ) - this.visibleCache.existingRegionCost + ) + } + + /** + * Append a committed segment and expose an updated public cache view. + * + * @param netId - Net id for the committed route. + * @param geometry - Segment geometry inside this region. + * @param delta - Intersection counters added by the segment. + * @param computeRegionCost - Region cost function for this region. + * @returns Updated exact-length public cache view. + */ + append( + netId: NetId, + geometry: SegmentGeometry, + delta: RegionSegmentDelta, + computeRegionCost: RegionCostFn, + ): RegionIntersectionCache { + this.ensureCapacity(this.segmentCount + 1) + + const writeIndex = this.segmentCount + this.netIds[writeIndex] = netId + this.lesserAngles[writeIndex] = geometry.lesserAngle + this.greaterAngles[writeIndex] = geometry.greaterAngle + this.layerMasks[writeIndex] = geometry.layerMask + this.segmentCount += 1 + + const existingSameLayerIntersections = + this.visibleCache.existingSameLayerIntersections + + delta.sameLayerIntersections + const existingCrossingLayerIntersections = + this.visibleCache.existingCrossingLayerIntersections + + delta.crossingLayerIntersections + const existingEntryExitLayerChanges = + this.visibleCache.existingEntryExitLayerChanges + + delta.entryExitLayerChanges + + this.visibleCache = { + netIds: this.netIds.subarray(0, this.segmentCount), + lesserAngles: this.lesserAngles.subarray(0, this.segmentCount), + greaterAngles: this.greaterAngles.subarray(0, this.segmentCount), + layerMasks: this.layerMasks.subarray(0, this.segmentCount), + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount: this.segmentCount, + existingRegionCost: computeRegionCost( + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + this.segmentCount, + ), + } + + return this.visibleCache + } + + private ensureCapacity(requiredCapacity: number) { + if (requiredCapacity <= this.netIds.length) { + return + } + + const nextCapacity = Math.max(requiredCapacity, this.netIds.length * 2) + const nextNetIds = new Int32Array(nextCapacity) + const nextLesserAngles = new Int32Array(nextCapacity) + const nextGreaterAngles = new Int32Array(nextCapacity) + const nextLayerMasks = new Int32Array(nextCapacity) + + nextNetIds.set(this.netIds.subarray(0, this.segmentCount)) + nextLesserAngles.set(this.lesserAngles.subarray(0, this.segmentCount)) + nextGreaterAngles.set(this.greaterAngles.subarray(0, this.segmentCount)) + nextLayerMasks.set(this.layerMasks.subarray(0, this.segmentCount)) + + this.netIds = nextNetIds + this.lesserAngles = nextLesserAngles + this.greaterAngles = nextGreaterAngles + this.layerMasks = nextLayerMasks + } +} diff --git a/lib2/route-cost.ts b/lib2/route-cost.ts new file mode 100644 index 0000000..4c531c8 --- /dev/null +++ b/lib2/route-cost.ts @@ -0,0 +1,41 @@ +import type { Candidate } from "./domain" +import type { NetId, PortId } from "./types" +import type { MutableRegionCache, RegionCostFn } from "./region-cache" +import type { SegmentGeometry } from "./segment-geometry" + +/** Input needed to score one possible route step. */ +export type RouteCostInput = { + readonly currentCandidate: Candidate + readonly neighborPortId: PortId + readonly routeNetId: NetId + readonly regionCache: MutableRegionCache + readonly regionCongestionCost: number + readonly portPenalty: number + readonly segmentGeometry: SegmentGeometry + readonly isKnownSingleLayerRegion: boolean + readonly computeRegionCost: RegionCostFn +} + +/** + * Compute the A* `g` score for moving from the current candidate to a port. + * + * @param input - Route step cost input. + * @returns The next `g` score, or infinity when the step is impossible. + */ +export function computeRouteG(input: RouteCostInput): number { + const delta = input.regionCache.countDelta( + input.routeNetId, + input.segmentGeometry, + ) + + if (delta.sameLayerIntersections > 0 && input.isKnownSingleLayerRegion) { + return Number.POSITIVE_INFINITY + } + + return ( + input.currentCandidate.g + + input.regionCache.getAddedCost(delta, input.computeRegionCost) + + input.regionCongestionCost + + input.portPenalty + ) +} diff --git a/lib2/route-search.ts b/lib2/route-search.ts new file mode 100644 index 0000000..20f4a01 --- /dev/null +++ b/lib2/route-search.ts @@ -0,0 +1,202 @@ +import type { + Candidate, + TinyHyperGraphProblem, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "./domain" +import type { HopId, PortId, RegionId, RouteId } from "./types" + +/** Route search status when every route has been routed. */ +export const ROUTE_SEARCH_ALL_ROUTES_ROUTED = "allRoutesRouted" + +/** Route search status when work advanced but no lifecycle hook is needed. */ +export const ROUTE_SEARCH_ADVANCED = "advanced" + +/** Route search status when the current route has no candidates left. */ +export const ROUTE_SEARCH_OUT_OF_CANDIDATES = "outOfCandidates" + +/** Route search failure result. */ +export type RouteSearchFailure = { + readonly _tag: "failed" + readonly error: string +} + +/** Result of one lib2 route-search step. Hot statuses are interned strings. */ +export type RouteSearchStepResult = + | typeof ROUTE_SEARCH_ALL_ROUTES_ROUTED + | typeof ROUTE_SEARCH_ADVANCED + | typeof ROUTE_SEARCH_OUT_OF_CANDIDATES + | Candidate + | RouteSearchFailure + +/** Runtime methods needed by route search. */ +export type RouteSearchRuntime = { + readonly topology: TinyHyperGraphTopology + readonly problem: TinyHyperGraphProblem + readonly state: TinyHyperGraphWorkingState + readonly getHopId: (portId: PortId, nextRegionId: RegionId) => HopId + readonly getCandidateBestCost: (hopId: HopId) => number + readonly setCandidateBestCost: (hopId: HopId, cost: number) => void + readonly resetCandidateBestCosts: () => void + readonly getStartingNextRegionId: ( + routeId: RouteId, + startingPortId: PortId, + ) => RegionId | undefined + readonly isPortReservedForDifferentNet: (portId: PortId) => boolean + readonly isRegionReservedForDifferentNet: (regionId: RegionId) => boolean + readonly computeG: ( + currentCandidate: Candidate, + neighborPortId: PortId, + ) => number + readonly computeH: (portId: PortId) => number + readonly onRouteAttempt: (routeId: RouteId) => void +} + +/** + * Run one A* route-search step against mutable solver state. + * + * @param runtime - Solver state and route-search callbacks. + * @returns The lifecycle result for the solver shell to handle. + */ +export function runRouteSearchStep( + runtime: RouteSearchRuntime, +): RouteSearchStepResult { + const { problem, state, topology } = runtime + + if (state.currentRouteId === undefined) { + const nextRouteId = state.unroutedRoutes.shift() + if (nextRouteId === undefined) { + return ROUTE_SEARCH_ALL_ROUTES_ROUTED + } + + state.currentRouteId = nextRouteId + state.currentRouteNetId = problem.routeNet[nextRouteId] + runtime.onRouteAttempt(nextRouteId) + + runtime.resetCandidateBestCosts() + const startingPortId = problem.routeStartPort[nextRouteId] + state.candidateQueue.clear() + const startingNextRegionId = runtime.getStartingNextRegionId( + nextRouteId, + startingPortId, + ) + + if (startingNextRegionId === undefined) { + return failed(`Start port ${startingPortId} has no incident regions`) + } + + runtime.setCandidateBestCost( + runtime.getHopId(startingPortId, startingNextRegionId), + 0, + ) + state.candidateQueue.queue({ + nextRegionId: startingNextRegionId, + portId: startingPortId, + f: 0, + g: 0, + h: 0, + }) + state.goalPortId = problem.routeEndPort[nextRouteId] + } + + const currentRouteNetId = state.currentRouteNetId + if (currentRouteNetId === undefined) { + return failed("Current route net is missing during route search") + } + + const currentCandidate = state.candidateQueue.dequeue() + if (!currentCandidate) { + return ROUTE_SEARCH_OUT_OF_CANDIDATES + } + + const currentCandidateHopId = runtime.getHopId( + currentCandidate.portId, + currentCandidate.nextRegionId, + ) + if ( + currentCandidate.g > runtime.getCandidateBestCost(currentCandidateHopId) + ) { + return ROUTE_SEARCH_ADVANCED + } + + if (runtime.isRegionReservedForDifferentNet(currentCandidate.nextRegionId)) { + return ROUTE_SEARCH_ADVANCED + } + + const neighbors = topology.regionIncidentPorts[currentCandidate.nextRegionId] + + for (const neighborPortId of neighbors) { + const assignedNetId = state.portAssignment[neighborPortId] + if (runtime.isPortReservedForDifferentNet(neighborPortId)) { + continue + } + + if (neighborPortId === state.goalPortId) { + if (assignedNetId !== -1 && assignedNetId !== currentRouteNetId) { + continue + } + + return currentCandidate + } + + if (assignedNetId !== -1 && assignedNetId !== currentRouteNetId) { + continue + } + + if (neighborPortId === currentCandidate.portId) { + continue + } + + if (problem.portSectionMask[neighborPortId] === 0) { + continue + } + + const g = runtime.computeG(currentCandidate, neighborPortId) + if (!Number.isFinite(g)) { + continue + } + + const h = runtime.computeH(neighborPortId) + const incidentRegions = topology.incidentPortRegion[neighborPortId] + const nextRegionId = + incidentRegions[0] === currentCandidate.nextRegionId + ? incidentRegions[1] + : incidentRegions[0] + + if ( + nextRegionId === undefined || + runtime.isRegionReservedForDifferentNet(nextRegionId) + ) { + continue + } + + const nextCandidate: Candidate = { + prevRegionId: currentCandidate.nextRegionId, + nextRegionId, + portId: neighborPortId, + g, + h, + f: g + h, + prevCandidate: currentCandidate, + } + + if (neighborPortId === state.goalPortId) { + return nextCandidate + } + + const nextHopId = runtime.getHopId(neighborPortId, nextRegionId) + if (g >= runtime.getCandidateBestCost(nextHopId)) { + continue + } + + runtime.setCandidateBestCost(nextHopId, g) + state.candidateQueue.queue(nextCandidate) + } + + return ROUTE_SEARCH_ADVANCED +} + +const failed = (error: string): RouteSearchFailure => ({ + _tag: "failed", + error, +}) diff --git a/lib2/section-candidate-families.ts b/lib2/section-candidate-families.ts new file mode 100644 index 0000000..424b045 --- /dev/null +++ b/lib2/section-candidate-families.ts @@ -0,0 +1,125 @@ +import type { TinyHyperGraphTopology } from "./solver" +import type { RegionId } from "./types" + +export type TinyHyperGraphSectionCandidateFamily = + | "self-all" + | "self-touch" + | "onehop-all" + | "onehop-touch" + | "twohop-all" + | "twohop-touch" + | "threehop-all" + | "threehop-touch" + | "fourhop-all" + | "fourhop-touch" + +export type TinyHyperGraphSectionPortSelectionRule = + | "touches-selected-region" + | "all-incident-regions-selected" + +export type TinyHyperGraphSectionMaskCandidate = { + label: string + family: TinyHyperGraphSectionCandidateFamily + regionIds: RegionId[] + portSelectionRule: TinyHyperGraphSectionPortSelectionRule +} + +/** + * Default automatic search families. Deliberately excludes the deeper + * three-hop and four-hop families so callers must opt into them explicitly. + */ +export const DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES = [ + "self-touch", + "onehop-all", + "onehop-touch", + "twohop-all", + "twohop-touch", +] as const satisfies readonly TinyHyperGraphSectionCandidateFamily[] + +export const OPT_IN_DEEP_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES = [ + "threehop-all", + "threehop-touch", + "fourhop-all", + "fourhop-touch", +] as const satisfies readonly TinyHyperGraphSectionCandidateFamily[] + +export const ALL_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES = [ + "self-all", + ...DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + ...OPT_IN_DEEP_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, +] as const satisfies readonly TinyHyperGraphSectionCandidateFamily[] + +const CANDIDATE_FAMILY_HOP_COUNT: Record< + TinyHyperGraphSectionCandidateFamily, + number +> = { + "self-all": 0, + "self-touch": 0, + "onehop-all": 1, + "onehop-touch": 1, + "twohop-all": 2, + "twohop-touch": 2, + "threehop-all": 3, + "threehop-touch": 3, + "fourhop-all": 4, + "fourhop-touch": 4, +} + +const getAdjacentRegionIds = ( + topology: TinyHyperGraphTopology, + seedRegionIds: RegionId[], +) => { + const adjacentRegionIds = new Set(seedRegionIds) + + for (const seedRegionId of seedRegionIds) { + for (const portId of topology.regionIncidentPorts[seedRegionId] ?? []) { + for (const regionId of topology.incidentPortRegion[portId] ?? []) { + adjacentRegionIds.add(regionId) + } + } + } + + return [...adjacentRegionIds] +} + +const getRegionIdsWithinHopCount = ( + topology: TinyHyperGraphTopology, + seedRegionIds: RegionId[], + hopCount: number, +) => { + let expandedRegionIds = [...new Set(seedRegionIds)] + + for (let hopIndex = 0; hopIndex < hopCount; hopIndex += 1) { + expandedRegionIds = getAdjacentRegionIds(topology, expandedRegionIds) + } + + return expandedRegionIds +} + +export const createSectionMaskCandidate = ( + topology: TinyHyperGraphTopology, + hotRegionId: RegionId, + family: TinyHyperGraphSectionCandidateFamily, +): TinyHyperGraphSectionMaskCandidate => ({ + label: `hot-${hotRegionId}-${family}`, + family, + regionIds: getRegionIdsWithinHopCount( + topology, + [hotRegionId], + CANDIDATE_FAMILY_HOP_COUNT[family], + ), + portSelectionRule: family.endsWith("-all") + ? "all-incident-regions-selected" + : "touches-selected-region", +}) + +export const createSectionMaskCandidatesForHotRegions = ( + topology: TinyHyperGraphTopology, + hotRegionIds: RegionId[], + candidateFamilies: TinyHyperGraphSectionCandidateFamily[], +): TinyHyperGraphSectionMaskCandidate[] => + hotRegionIds.flatMap((hotRegionId) => + candidateFamilies.map((family) => + createSectionMaskCandidate(topology, hotRegionId, family), + ), + ) diff --git a/lib2/section-pipeline.ts b/lib2/section-pipeline.ts new file mode 100644 index 0000000..9b9e8b5 --- /dev/null +++ b/lib2/section-pipeline.ts @@ -0,0 +1,533 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { BasePipelineSolver, type PipelineStep } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import { loadSerializedHyperGraph } from "./graph-load" +import { + TinyHyperGraphSolver2, + type TinyHyperGraphProblem, + type TinyHyperGraphSolution, + type TinyHyperGraphTopology, + type TinyHyperGraphSolver2Options, + type TinyHyperGraphWorkingState, +} from "./solver" +import type { RegionId } from "./types" +import { + getActiveSectionRouteIds, + TinyHyperGraphSectionSolver2, + type TinyHyperGraphSectionSolver2Options, +} from "./section-solver" +import { + createSectionMaskCandidatesForHotRegions, + DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + type TinyHyperGraphSectionCandidateFamily, + type TinyHyperGraphSectionMaskCandidate, +} from "./section-candidate-families" + +export { + ALL_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + OPT_IN_DEEP_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + type TinyHyperGraphSectionCandidateFamily, +} from "./section-candidate-families" + +type AutomaticSectionSearchResult = { + portSectionMask: Int8Array + baselineMaxRegionCost: number + finalMaxRegionCost: number + generatedCandidateCount: number + candidateCount: number + duplicateCandidateCount: number + totalMs: number + baselineEvaluationMs: number + candidateEligibilityMs: number + candidateInitMs: number + candidateSolveMs: number + candidateReplayScoreMs: number + winningCandidateLabel?: string + winningCandidateFamily?: TinyHyperGraphSectionCandidateFamily +} + +const DEFAULT_SOLVE_GRAPH_OPTIONS: TinyHyperGraphSolver2Options = { + 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: TinyHyperGraphSectionSolver2Options = { + DISTANCE_TO_COST: 0.05, + RIP_THRESHOLD_RAMP_ATTEMPTS: 16, + RIP_CONGESTION_REGION_COST_FACTOR: 0.1, + MAX_ITERATIONS: DEFAULT_SECTION_SOLVER_MAX_ITERATIONS, + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT: 6, + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST: Number.POSITIVE_INFINITY, +} + +const DEFAULT_MAX_HOT_REGIONS = 2 + +const IMPROVEMENT_EPSILON = 1e-9 + +type RegionCostSolverView = { + readonly state: Pick +} + +const getMaxRegionCost = (solver: RegionCostSolverView) => + solver.state.regionIntersectionCaches.reduce( + (maxRegionCost, regionIntersectionCache) => + Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), + 0, + ) + +const getSerializedOutputMaxRegionCost = ( + serializedHyperGraph: SerializedHyperGraph, + sectionSolverOptions?: TinyHyperGraphSectionSolver2Options, +) => { + const replay = loadSerializedHyperGraph(serializedHyperGraph) + const replayedSolver = new TinyHyperGraphSectionSolver2( + replay.topology, + replay.problem, + replay.solution, + sectionSolverOptions, + ) + + return getMaxRegionCost(replayedSolver.baselineSolver) +} + +const createPortSectionMaskForRegionIds = ( + topology: TinyHyperGraphTopology, + regionIds: RegionId[], + portSelectionRule: + | "touches-selected-region" + | "all-incident-regions-selected", +) => { + const selectedRegionIds = new Set(regionIds) + + return Int8Array.from({ length: topology.portCount }, (_, portId) => { + const incidentRegionIds = topology.incidentPortRegion[portId] ?? [] + + if (portSelectionRule === "touches-selected-region") { + return incidentRegionIds.some((regionId) => + selectedRegionIds.has(regionId), + ) + ? 1 + : 0 + } + + return incidentRegionIds.length > 0 && + incidentRegionIds.every((regionId) => selectedRegionIds.has(regionId)) + ? 1 + : 0 + }) +} + +const createProblemWithPortSectionMask = ( + problem: TinyHyperGraphProblem, + portSectionMask: Int8Array, +): TinyHyperGraphProblem => ({ + routeCount: problem.routeCount, + portSectionMask, + routeMetadata: problem.routeMetadata, + routeStartPort: new Int32Array(problem.routeStartPort), + routeEndPort: new Int32Array(problem.routeEndPort), + routeNet: new Int32Array(problem.routeNet), + regionNetId: new Int32Array(problem.regionNetId), + portPenalty: + problem.portPenalty === undefined + ? undefined + : new Float64Array(problem.portPenalty), +}) + +const getSectionMaskCandidates = ( + solvedSolver: TinyHyperGraphSolver2, + topology: TinyHyperGraphTopology, + maxHotRegions: number, + candidateFamilies: TinyHyperGraphSectionCandidateFamily[], +): TinyHyperGraphSectionMaskCandidate[] => { + const hotRegionIds = solvedSolver.state.regionIntersectionCaches + .map((regionIntersectionCache, regionId) => ({ + regionId, + regionCost: regionIntersectionCache.existingRegionCost, + })) + .filter(({ regionCost }) => regionCost > 0) + .sort((left, right) => right.regionCost - left.regionCost) + .slice(0, maxHotRegions) + .map(({ regionId }) => regionId) + + return createSectionMaskCandidatesForHotRegions( + topology, + hotRegionIds, + candidateFamilies, + ) +} + +const findBestAutomaticSectionMask = ( + solvedSolver: TinyHyperGraphSolver2, + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, + searchConfig: TinyHyperGraphSectionPipelineSearchConfig | undefined, + sectionSolverOptions: TinyHyperGraphSectionSolver2Options, +): AutomaticSectionSearchResult => { + const searchStartTime = performance.now() + const baselineEvaluationStartTime = performance.now() + const baselineSectionSolver = new TinyHyperGraphSectionSolver2( + topology, + problem, + solution, + sectionSolverOptions, + ) + const baselineMaxRegionCost = getMaxRegionCost( + baselineSectionSolver.baselineSolver, + ) + const baselineEvaluationMs = performance.now() - baselineEvaluationStartTime + + let bestFinalMaxRegionCost = baselineMaxRegionCost + let bestPortSectionMask = new Int8Array(topology.portCount) + let winningCandidateLabel: string | undefined + let winningCandidateFamily: TinyHyperGraphSectionCandidateFamily | undefined + let generatedCandidateCount = 0 + let candidateCount = 0 + let duplicateCandidateCount = 0 + let candidateEligibilityMs = 0 + let candidateInitMs = 0 + let candidateSolveMs = 0 + let candidateReplayScoreMs = 0 + const seenPortSectionMasks = new Set() + const maxHotRegions = + searchConfig?.maxHotRegions ?? + sectionSolverOptions.MAX_HOT_REGIONS ?? + DEFAULT_MAX_HOT_REGIONS + + for (const candidate of getSectionMaskCandidates( + solvedSolver, + topology, + maxHotRegions, + searchConfig?.candidateFamilies ?? [ + ...DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, + ], + )) { + const candidateProblem = createProblemWithPortSectionMask( + problem, + createPortSectionMaskForRegionIds( + topology, + candidate.regionIds, + candidate.portSelectionRule, + ), + ) + generatedCandidateCount += 1 + const portSectionMaskKey = candidateProblem.portSectionMask.join(",") + + if (seenPortSectionMasks.has(portSectionMaskKey)) { + duplicateCandidateCount += 1 + continue + } + + seenPortSectionMasks.add(portSectionMaskKey) + + try { + const eligibilityStartTime = performance.now() + const activeRouteIds = getActiveSectionRouteIds( + topology, + candidateProblem, + solution, + ) + candidateEligibilityMs += performance.now() - eligibilityStartTime + + if (activeRouteIds.length === 0) { + continue + } + + candidateCount += 1 + + const candidateInitStartTime = performance.now() + const sectionSolver = new TinyHyperGraphSectionSolver2( + topology, + candidateProblem, + solution, + sectionSolverOptions, + ) + candidateInitMs += performance.now() - candidateInitStartTime + + const candidateSolveStartTime = performance.now() + sectionSolver.solve() + candidateSolveMs += performance.now() - candidateSolveStartTime + + if (sectionSolver.failed || !sectionSolver.solved) { + continue + } + + const finalMaxRegionCost = Number( + sectionSolver.stats.finalMaxRegionCost ?? + getMaxRegionCost(sectionSolver.getSolvedSolver()), + ) + + if (finalMaxRegionCost < bestFinalMaxRegionCost - IMPROVEMENT_EPSILON) { + const candidateReplayScoreStartTime = performance.now() + const replayedFinalMaxRegionCost = getSerializedOutputMaxRegionCost( + sectionSolver.getOutput(), + sectionSolverOptions, + ) + candidateReplayScoreMs += + performance.now() - candidateReplayScoreStartTime + + if ( + replayedFinalMaxRegionCost < + bestFinalMaxRegionCost - IMPROVEMENT_EPSILON + ) { + bestFinalMaxRegionCost = replayedFinalMaxRegionCost + bestPortSectionMask = new Int8Array(candidateProblem.portSectionMask) + winningCandidateLabel = candidate.label + winningCandidateFamily = candidate.family + } + } + } catch { + // Skip invalid section masks that split a route into multiple spans. + } + } + + return { + portSectionMask: bestPortSectionMask, + baselineMaxRegionCost, + finalMaxRegionCost: bestFinalMaxRegionCost, + generatedCandidateCount, + candidateCount, + duplicateCandidateCount, + totalMs: performance.now() - searchStartTime, + baselineEvaluationMs, + candidateEligibilityMs, + candidateInitMs, + candidateSolveMs, + candidateReplayScoreMs, + winningCandidateLabel, + winningCandidateFamily, + } +} + +export interface TinyHyperGraphSectionMaskContext { + serializedHyperGraph: SerializedHyperGraph + solvedSerializedHyperGraph: SerializedHyperGraph + solvedSolver: TinyHyperGraphSolver2 + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + solution: TinyHyperGraphSolution +} + +export interface TinyHyperGraphSectionPipelineSearchConfig { + maxHotRegions?: number + candidateFamilies?: TinyHyperGraphSectionCandidateFamily[] +} + +export interface TinyHyperGraphSectionPipelineInput { + serializedHyperGraph: SerializedHyperGraph + minViaPadDiameter?: number + createSectionMask?: (context: TinyHyperGraphSectionMaskContext) => Int8Array + solveGraphOptions?: TinyHyperGraphSolver2Options + sectionSolverOptions?: TinyHyperGraphSectionSolver2Options + sectionSearchConfig?: TinyHyperGraphSectionPipelineSearchConfig +} + +export class TinyHyperGraphSectionPipelineSolver2 extends BasePipelineSolver { + initialVisualizationSolver?: TinyHyperGraphSolver2 + selectedSectionMask?: Int8Array + selectedSectionCandidateLabel?: string + selectedSectionCandidateFamily?: TinyHyperGraphSectionCandidateFamily + + constructor(inputProblem: TinyHyperGraphSectionPipelineInput) { + super(inputProblem) + this.MAX_ITERATIONS = DEFAULT_SECTION_PIPELINE_MAX_ITERATIONS + } + + loadHyperGraph(serializedHyperGraph: SerializedHyperGraph): { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + solution: TinyHyperGraphSolution + } { + return loadSerializedHyperGraph(serializedHyperGraph) + } + + getSolveGraphOptions(): TinyHyperGraphSolver2Options { + return { + ...DEFAULT_SOLVE_GRAPH_OPTIONS, + ...(this.inputProblem.minViaPadDiameter === undefined + ? {} + : { minViaPadDiameter: this.inputProblem.minViaPadDiameter }), + ...this.inputProblem.solveGraphOptions, + } + } + + getSectionSolverOptions(): TinyHyperGraphSectionSolver2Options { + return { + ...DEFAULT_SECTION_SOLVER_OPTIONS, + ...(this.inputProblem.minViaPadDiameter === undefined + ? {} + : { minViaPadDiameter: this.inputProblem.minViaPadDiameter }), + ...this.inputProblem.sectionSolverOptions, + } + } + + override pipelineDef: PipelineStep[] = [ + { + solverName: "solveGraph", + solverClass: TinyHyperGraphSolver2, + getConstructorParams: ( + instance: TinyHyperGraphSectionPipelineSolver2, + ) => { + const { topology, problem } = instance.loadHyperGraph( + instance.inputProblem.serializedHyperGraph, + ) + + return [ + topology, + problem, + instance.getSolveGraphOptions(), + ] as ConstructorParameters + }, + }, + { + solverName: "optimizeSection", + solverClass: TinyHyperGraphSectionSolver2, + getConstructorParams: (instance: TinyHyperGraphSectionPipelineSolver2) => + instance.getSectionStageParams(), + }, + ] + + getSectionStageParams(): [ + TinyHyperGraphTopology, + TinyHyperGraphProblem, + TinyHyperGraphSolution, + TinyHyperGraphSectionSolver2Options, + ] { + const solvedSerializedHyperGraph = + this.getStageOutput("solveGraph") + + if (!solvedSerializedHyperGraph) { + throw new Error( + "solveGraph did not produce a solved serialized hypergraph", + ) + } + + const solvedSolver = this.getSolver("solveGraph") + + if (!solvedSolver) { + throw new Error("solveGraph solver is unavailable") + } + + const sectionSolverOptions = this.getSectionSolverOptions() + const { topology, problem, solution } = this.loadHyperGraph( + solvedSerializedHyperGraph, + ) + + const portSectionMask = this.inputProblem.createSectionMask + ? this.inputProblem.createSectionMask({ + serializedHyperGraph: this.inputProblem.serializedHyperGraph, + solvedSerializedHyperGraph, + solvedSolver, + topology, + problem, + solution, + }) + : (() => { + 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) + + this.stats = { + ...this.stats, + sectionMaskPortCount: [...portSectionMask].filter((value) => value === 1) + .length, + } + + return [topology, problem, solution, sectionSolverOptions] + } + + getInitialVisualizationSolver() { + if (!this.initialVisualizationSolver) { + const { topology, problem } = this.loadHyperGraph( + this.inputProblem.serializedHyperGraph, + ) + this.initialVisualizationSolver = new TinyHyperGraphSolver2( + topology, + problem, + this.getSolveGraphOptions(), + ) + } + + return this.initialVisualizationSolver + } + + override initialVisualize() { + return this.getInitialVisualizationSolver().visualize() + } + + override visualize(): GraphicsObject { + if (this.iterations === 0) { + return this.initialVisualize() ?? super.visualize() + } + + return super.visualize() + } + + override getOutput() { + return ( + this.getStageOutput("optimizeSection") ?? + this.getStageOutput("solveGraph") ?? + null + ) + } + + override tryFinalAcceptance() { + if (this.getStageOutput("solveGraph")) { + this.stats = { + ...this.stats, + acceptedSolveGraphOutputOnSectionPipelineTimeout: true, + } + this.activeSubSolver = undefined + this.solved = true + this.failed = false + this.error = null + } + } +} diff --git a/lib2/section-solver.ts b/lib2/section-solver.ts new file mode 100644 index 0000000..a5deb84 --- /dev/null +++ b/lib2/section-solver.ts @@ -0,0 +1,1084 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import { + applyTinyHyperGraphSolver2Options, + createEmptyRegionIntersectionCache, + getTinyHyperGraphSolver2Options, + type RegionCostSummary, + TinyHyperGraphSolver2, + type TinyHyperGraphProblem, + type TinyHyperGraphSolution, + type TinyHyperGraphTopology, + type TinyHyperGraphSolver2Options, +} from "./solver" +import { DEFAULT_MIN_VIA_PAD_DIAMETER } from "./compute-region-cost" +import { shuffle } from "./shuffle" +import type { + PortId, + RegionId, + RegionIntersectionCache, + RouteId, +} from "./types" +import { visualizeTinyGraph } from "../lib/visualizeTinyGraph" + +interface SolvedStateSnapshot { + portAssignment: Int32Array + regionSegments: Array<[RouteId, PortId, PortId][]> + regionIntersectionCaches: RegionIntersectionCache[] +} + +interface SectionRoutePlan { + routeId: RouteId + fixedSegments: Array<{ + regionId: RegionId + fromPortId: PortId + toPortId: PortId + }> + activeStartPortId?: PortId + activeEndPortId?: PortId + forcedStartRegionId?: RegionId +} + +export interface TinyHyperGraphSectionSolver2Options + extends TinyHyperGraphSolver2Options { + MAX_RIPS?: number + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT?: number + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST?: number + /** + * Pipeline convenience option for automatic section-mask search. + * When `sectionSearchConfig.maxHotRegions` is omitted, the section pipeline + * falls back to this value before using its built-in default. + */ + MAX_HOT_REGIONS?: number +} + +const applyTinyHyperGraphSectionSolver2Options = ( + solver: TinyHyperGraphSectionSearchSolver2 | TinyHyperGraphSectionSolver2, + options?: TinyHyperGraphSectionSolver2Options, +) => { + applyTinyHyperGraphSolver2Options(solver, options) + + if (!options) { + return + } + + if (options.MAX_RIPS !== undefined) { + solver.MAX_RIPS = options.MAX_RIPS + } + if (options.MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT !== undefined) { + solver.MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT = + options.MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT + } + if (options.EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST !== undefined) { + solver.EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST = + options.EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST + } +} + +const getTinyHyperGraphSectionSolver2Options = ( + solver: TinyHyperGraphSectionSolver2, +): TinyHyperGraphSectionSolver2Options => ({ + ...getTinyHyperGraphSolver2Options(solver), + MAX_RIPS: solver.MAX_RIPS, + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT: + solver.MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT, + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST: + solver.EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST, +}) + +const cloneRegionSegments = ( + regionSegments: Array<[RouteId, PortId, PortId][]>, +): Array<[RouteId, PortId, PortId][]> => + regionSegments.map((segments) => + segments.map( + ([routeId, fromPortId, toPortId]) => + [routeId, fromPortId, toPortId] as [RouteId, PortId, PortId], + ), + ) + +const cloneRegionIntersectionCache = ( + regionIntersectionCache: RegionIntersectionCache, +): RegionIntersectionCache => ({ + netIds: new Int32Array(regionIntersectionCache.netIds), + lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), + greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), + layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + existingCrossingLayerIntersections: + regionIntersectionCache.existingCrossingLayerIntersections, + existingSameLayerIntersections: + regionIntersectionCache.existingSameLayerIntersections, + existingEntryExitLayerChanges: + regionIntersectionCache.existingEntryExitLayerChanges, + existingRegionCost: regionIntersectionCache.existingRegionCost, + existingSegmentCount: regionIntersectionCache.existingSegmentCount, +}) + +const cloneSolvedStateSnapshot = ( + snapshot: SolvedStateSnapshot, +): SolvedStateSnapshot => ({ + portAssignment: new Int32Array(snapshot.portAssignment), + regionSegments: cloneRegionSegments(snapshot.regionSegments), + regionIntersectionCaches: snapshot.regionIntersectionCaches.map( + cloneRegionIntersectionCache, + ), +}) + +const restoreSolvedStateSnapshot = ( + solver: TinyHyperGraphSolver2, + snapshot: SolvedStateSnapshot, +) => { + const clonedSnapshot = cloneSolvedStateSnapshot(snapshot) + solver.state.portAssignment = clonedSnapshot.portAssignment + solver.state.regionSegments = clonedSnapshot.regionSegments + solver.state.regionIntersectionCaches = + clonedSnapshot.regionIntersectionCaches +} + +const summarizeRegionIntersectionCaches = ( + regionIntersectionCaches: ArrayLike, +): RegionCostSummary => { + let maxRegionCost = 0 + let totalRegionCost = 0 + + for ( + let regionId = 0; + regionId < regionIntersectionCaches.length; + regionId++ + ) { + const regionCost = + regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + } + + return { + maxRegionCost, + totalRegionCost, + } +} + +const summarizeRegionIntersectionCachesForRegionIds = ( + regionIntersectionCaches: ArrayLike, + regionIds: RegionId[], +): RegionCostSummary => { + let maxRegionCost = 0 + let totalRegionCost = 0 + + for (const regionId of regionIds) { + const regionCost = + regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + } + + return { + maxRegionCost, + totalRegionCost, + } +} + +const summarizeRegionIntersectionCachesExcludingRegionIds = ( + regionIntersectionCaches: ArrayLike, + excludedRegionIds: RegionId[], +): RegionCostSummary => { + const excludedRegionIdSet = new Set(excludedRegionIds) + let maxRegionCost = 0 + let totalRegionCost = 0 + + for ( + let regionId = 0; + regionId < regionIntersectionCaches.length; + regionId++ + ) { + if (excludedRegionIdSet.has(regionId)) { + continue + } + + const regionCost = + regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + } + + return { + maxRegionCost, + totalRegionCost, + } +} + +const compareRegionCostSummaries = ( + left: RegionCostSummary, + right: RegionCostSummary, +) => { + if (left.maxRegionCost !== right.maxRegionCost) { + return left.maxRegionCost - right.maxRegionCost + } + + return left.totalRegionCost - right.totalRegionCost +} + +const getSharedRegionIdForPorts = ( + topology: TinyHyperGraphTopology, + fromPortId: PortId, + toPortId: PortId, +): RegionId => { + const fromIncidentRegions = topology.incidentPortRegion[fromPortId] ?? [] + const toIncidentRegions = topology.incidentPortRegion[toPortId] ?? [] + const sharedRegionId = fromIncidentRegions.find((regionId) => + toIncidentRegions.includes(regionId), + ) + + if (sharedRegionId === undefined) { + throw new Error(`Ports ${fromPortId} and ${toPortId} do not share a region`) + } + + return sharedRegionId +} + +const getOrderedRoutePath = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, + routeId: RouteId, +): { + orderedPortIds: PortId[] + orderedRegionIds: RegionId[] +} => { + const routeSegments = solution.solvedRoutePathSegments[routeId] ?? [] + const routeSegmentRegionIds = + solution.solvedRoutePathRegionIds?.[routeId] ?? [] + const startPortId = problem.routeStartPort[routeId] + const endPortId = problem.routeEndPort[routeId] + + if (routeSegments.length === 0) { + if (startPortId === endPortId) { + return { + orderedPortIds: [startPortId], + orderedRegionIds: [], + } + } + + throw new Error(`Route ${routeId} does not have an existing solved path`) + } + + const segmentsByPort = new Map< + PortId, + Array<{ + segmentIndex: number + fromPortId: PortId + toPortId: PortId + regionId?: RegionId + }> + >() + + routeSegments.forEach(([fromPortId, toPortId], segmentIndex) => { + const indexedSegment = { + segmentIndex, + fromPortId, + toPortId, + regionId: routeSegmentRegionIds[segmentIndex], + } + + const fromSegments = segmentsByPort.get(fromPortId) ?? [] + fromSegments.push(indexedSegment) + segmentsByPort.set(fromPortId, fromSegments) + + const toSegments = segmentsByPort.get(toPortId) ?? [] + toSegments.push(indexedSegment) + segmentsByPort.set(toPortId, toSegments) + }) + + const orderedPortIds = [startPortId] + const orderedRegionIds: RegionId[] = [] + const usedSegmentIndices = new Set() + let currentPortId = startPortId + let previousPortId: PortId | undefined + + while (currentPortId !== endPortId) { + const nextSegments = (segmentsByPort.get(currentPortId) ?? []).filter( + ({ segmentIndex, fromPortId, toPortId }) => { + if (usedSegmentIndices.has(segmentIndex)) { + return false + } + + const nextPortId = fromPortId === currentPortId ? toPortId : fromPortId + + return nextPortId !== previousPortId + }, + ) + + if (nextSegments.length !== 1) { + throw new Error( + `Route ${routeId} is not a single ordered path from ${startPortId} to ${endPortId}`, + ) + } + + const nextSegment = nextSegments[0]! + const nextPortId = + nextSegment.fromPortId === currentPortId + ? nextSegment.toPortId + : nextSegment.fromPortId + + usedSegmentIndices.add(nextSegment.segmentIndex) + orderedRegionIds.push( + nextSegment.regionId ?? + getSharedRegionIdForPorts( + topology, + nextSegment.fromPortId, + nextSegment.toPortId, + ), + ) + orderedPortIds.push(nextPortId) + previousPortId = currentPortId + currentPortId = nextPortId + } + + if (usedSegmentIndices.size !== routeSegments.length) { + throw new Error(`Route ${routeId} contains disconnected solved segments`) + } + + return { + orderedPortIds, + orderedRegionIds, + } +} + +const applyRouteSegmentsToSolver = ( + solver: TinyHyperGraphSolver2, + routeSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, +) => { + solver.state.portAssignment.fill(-1) + solver.state.regionSegments = Array.from( + { length: solver.topology.regionCount }, + () => [], + ) + solver.state.regionIntersectionCaches = Array.from( + { length: solver.topology.regionCount }, + () => createEmptyRegionIntersectionCache(), + ) + solver.state.currentRouteId = undefined + solver.state.currentRouteNetId = undefined + solver.state.unroutedRoutes = [] + solver.state.candidateQueue.clear() + solver.resetCandidateBestCosts() + solver.state.goalPortId = -1 + solver.state.ripCount = 0 + solver.state.regionCongestionCost.fill(0) + + for (let regionId = 0; regionId < routeSegmentsByRegion.length; regionId++) { + for (const [routeId, fromPortId, toPortId] of routeSegmentsByRegion[ + regionId + ] ?? []) { + solver.state.currentRouteNetId = solver.problem.routeNet[routeId] + solver.state.regionSegments[regionId]!.push([ + routeId, + fromPortId, + toPortId, + ]) + solver.state.portAssignment[fromPortId] = solver.state.currentRouteNetId + solver.state.portAssignment[toPortId] = solver.state.currentRouteNetId + solver.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } + } + + solver.state.currentRouteId = undefined + solver.state.currentRouteNetId = undefined + solver.solved = true + solver.failed = false + solver.error = null +} + +const createSolvedSolverFromRegionSegments = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + routeSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, + options?: TinyHyperGraphSolver2Options, +) => { + const solver = new TinyHyperGraphSolver2(topology, problem, options) + applyRouteSegmentsToSolver(solver, routeSegmentsByRegion) + return solver +} + +const createSolvedSolverFromSolution = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, + options?: TinyHyperGraphSolver2Options, +) => { + const routeSegmentsByRegion = Array.from( + { length: topology.regionCount }, + () => [] as [RouteId, PortId, PortId][], + ) + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + const { orderedPortIds, orderedRegionIds } = getOrderedRoutePath( + topology, + problem, + solution, + routeId, + ) + for (let portIndex = 1; portIndex < orderedPortIds.length; portIndex++) { + const fromPortId = orderedPortIds[portIndex - 1]! + const toPortId = orderedPortIds[portIndex]! + const regionId = orderedRegionIds[portIndex - 1]! + routeSegmentsByRegion[regionId]!.push([routeId, fromPortId, toPortId]) + } + } + + return createSolvedSolverFromRegionSegments( + topology, + problem, + routeSegmentsByRegion, + options, + ) +} + +const createSectionRoutePlans = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, +): { + sectionProblem: TinyHyperGraphProblem + routePlans: SectionRoutePlan[] + activeRouteIds: RouteId[] +} => { + const routeStartPort = new Int32Array(problem.routeStartPort) + const routeEndPort = new Int32Array(problem.routeEndPort) + const routePlans: SectionRoutePlan[] = Array.from( + { length: problem.routeCount }, + (_, routeId) => ({ + routeId, + fixedSegments: [], + }), + ) + const activeRouteIds: RouteId[] = [] + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + const routePlan = routePlans[routeId]! + const { orderedPortIds, orderedRegionIds } = getOrderedRoutePath( + topology, + problem, + solution, + routeId, + ) + const maskedRuns: Array<{ startIndex: number; endIndex: number }> = [] + let currentRunStartIndex: number | undefined + + for (let portIndex = 0; portIndex < orderedPortIds.length; portIndex++) { + const portId = orderedPortIds[portIndex]! + const isMasked = problem.portSectionMask[portId] === 1 + + if (isMasked && currentRunStartIndex === undefined) { + currentRunStartIndex = portIndex + } else if (!isMasked && currentRunStartIndex !== undefined) { + maskedRuns.push({ + startIndex: currentRunStartIndex, + endIndex: portIndex - 1, + }) + currentRunStartIndex = undefined + } + } + + if (currentRunStartIndex !== undefined) { + maskedRuns.push({ + startIndex: currentRunStartIndex, + endIndex: orderedPortIds.length - 1, + }) + } + + if (maskedRuns.length === 0) { + for (let portIndex = 1; portIndex < orderedPortIds.length; portIndex++) { + routePlan.fixedSegments.push({ + regionId: orderedRegionIds[portIndex - 1]!, + fromPortId: orderedPortIds[portIndex - 1]!, + toPortId: orderedPortIds[portIndex]!, + }) + } + continue + } + + if (maskedRuns.length > 1) { + throw new Error( + `Route ${routeId} enters the section multiple times; only one contiguous section span is currently supported`, + ) + } + + const maskedRun = maskedRuns[0]! + const activeStartIndex = Math.max(0, maskedRun.startIndex - 1) + const activeEndIndex = Math.min( + orderedPortIds.length - 1, + maskedRun.endIndex + 1, + ) + + if (activeEndIndex <= activeStartIndex) { + throw new Error(`Route ${routeId} does not have a valid section span`) + } + + for (let portIndex = 1; portIndex <= activeStartIndex; portIndex++) { + routePlan.fixedSegments.push({ + regionId: orderedRegionIds[portIndex - 1]!, + fromPortId: orderedPortIds[portIndex - 1]!, + toPortId: orderedPortIds[portIndex]!, + }) + } + + for ( + let portIndex = activeEndIndex + 1; + portIndex < orderedPortIds.length; + portIndex++ + ) { + routePlan.fixedSegments.push({ + regionId: orderedRegionIds[portIndex - 1]!, + fromPortId: orderedPortIds[portIndex - 1]!, + toPortId: orderedPortIds[portIndex]!, + }) + } + + routePlan.activeStartPortId = orderedPortIds[activeStartIndex] + routePlan.activeEndPortId = orderedPortIds[activeEndIndex] + routePlan.forcedStartRegionId = orderedRegionIds[activeStartIndex] + routeStartPort[routeId] = routePlan.activeStartPortId + routeEndPort[routeId] = routePlan.activeEndPortId + activeRouteIds.push(routeId) + } + + return { + sectionProblem: { + routeCount: problem.routeCount, + portSectionMask: new Int8Array(problem.portSectionMask), + routeMetadata: problem.routeMetadata, + routeStartPort, + routeEndPort, + routeNet: new Int32Array(problem.routeNet), + regionNetId: new Int32Array(problem.regionNetId), + portPenalty: + problem.portPenalty === undefined + ? undefined + : new Float64Array(problem.portPenalty), + }, + routePlans, + activeRouteIds, + } +} + +export const getActiveSectionRouteIds = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, +) => createSectionRoutePlans(topology, problem, solution).activeRouteIds + +const getSectionRegionIds = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, +): RegionId[] => { + const sectionRegionIds = new Set() + + for (let portId = 0; portId < problem.portSectionMask.length; portId++) { + if (problem.portSectionMask[portId] !== 1) { + continue + } + + for (const regionId of topology.incidentPortRegion[portId] ?? []) { + sectionRegionIds.add(regionId) + } + } + + return [...sectionRegionIds] +} + +class TinyHyperGraphSectionSearchSolver2 extends TinyHyperGraphSolver2 { + bestSnapshot?: SolvedStateSnapshot + fixedSnapshot?: SolvedStateSnapshot + bestSummary?: RegionCostSummary + baselineBeatRipCount?: number + previousBestMaxRegionCost = Number.POSITIVE_INFINITY + ripsSinceBestMaxRegionCostImprovement = 0 + + MAX_RIPS = Number.POSITIVE_INFINITY + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT = Number.POSITIVE_INFINITY + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST = Number.POSITIVE_INFINITY + + constructor( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + private routePlans: SectionRoutePlan[], + private activeRouteIds: RouteId[], + private mutableRegionIds: RegionId[], + private immutableRegionSummary: RegionCostSummary, + private baselineSummary: RegionCostSummary, + options?: TinyHyperGraphSectionSolver2Options, + ) { + super(topology, problem, options) + applyTinyHyperGraphSectionSolver2Options(this, options) + this.state.unroutedRoutes = [...activeRouteIds] + this.applyFixedSegments() + this.fixedSnapshot = cloneSolvedStateSnapshot({ + portAssignment: this.state.portAssignment, + regionSegments: this.state.regionSegments, + regionIntersectionCaches: this.state.regionIntersectionCaches, + }) + } + + applyFixedSegments() { + for (const routePlan of this.routePlans) { + for (const { + regionId, + fromPortId, + toPortId, + } of routePlan.fixedSegments) { + this.state.currentRouteNetId = this.problem.routeNet[routePlan.routeId] + this.state.regionSegments[regionId]!.push([ + routePlan.routeId, + fromPortId, + toPortId, + ]) + this.state.portAssignment[fromPortId] = this.state.currentRouteNetId + this.state.portAssignment[toPortId] = this.state.currentRouteNetId + this.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } + } + + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + } + + captureBestState(summary: RegionCostSummary) { + if ( + this.bestSummary && + compareRegionCostSummaries(summary, this.bestSummary) >= 0 + ) { + return + } + + this.bestSummary = summary + this.bestSnapshot = cloneSolvedStateSnapshot({ + portAssignment: this.state.portAssignment, + regionSegments: this.state.regionSegments, + regionIntersectionCaches: this.state.regionIntersectionCaches, + }) + } + + restoreBestState() { + if (!this.bestSnapshot) { + return + } + + restoreSolvedStateSnapshot(this, this.bestSnapshot) + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = [] + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + } + + override getStartingNextRegionId( + routeId: RouteId, + startingPortId: PortId, + ): RegionId | undefined { + const forcedStartRegionId = this.routePlans[routeId]?.forcedStartRegionId + if (forcedStartRegionId !== undefined) { + return forcedStartRegionId + } + + return super.getStartingNextRegionId(routeId, startingPortId) + } + + override resetRoutingStateForRerip() { + if (!this.fixedSnapshot) { + super.resetRoutingStateForRerip() + this.state.unroutedRoutes = shuffle( + [...this.activeRouteIds], + this.state.ripCount, + ) + this.applyFixedSegments() + return + } + + restoreSolvedStateSnapshot(this, this.fixedSnapshot) + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = shuffle( + [...this.activeRouteIds], + this.state.ripCount, + ) + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + } + + override onAllRoutesRouted() { + const { state } = this + const maxRips = Math.min(this.MAX_RIPS, this.RIP_THRESHOLD_RAMP_ATTEMPTS) + const ripThresholdProgress = + maxRips <= 0 ? 1 : Math.min(1, state.ripCount / maxRips) + const currentRipThreshold = + this.RIP_THRESHOLD_START + + (this.RIP_THRESHOLD_END - this.RIP_THRESHOLD_START) * ripThresholdProgress + + const regionIdsOverCostThreshold: RegionId[] = [] + const mutableRegionCosts = new Float64Array(this.mutableRegionIds.length) + let mutableMaxRegionCost = 0 + let mutableTotalRegionCost = 0 + + for ( + let mutableRegionIndex = 0; + mutableRegionIndex < this.mutableRegionIds.length; + mutableRegionIndex++ + ) { + const regionId = this.mutableRegionIds[mutableRegionIndex]! + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + mutableRegionCosts[mutableRegionIndex] = regionCost + mutableMaxRegionCost = Math.max(mutableMaxRegionCost, regionCost) + mutableTotalRegionCost += regionCost + + if (regionCost > currentRipThreshold) { + regionIdsOverCostThreshold.push(regionId) + } + } + + const maxRegionCost = Math.max( + this.immutableRegionSummary.maxRegionCost, + mutableMaxRegionCost, + ) + const totalRegionCost = + this.immutableRegionSummary.totalRegionCost + mutableTotalRegionCost + + this.captureBestState({ + maxRegionCost, + totalRegionCost, + }) + const bestSummary = this.bestSummary ?? { + maxRegionCost, + totalRegionCost, + } + + if ( + bestSummary.maxRegionCost < + this.previousBestMaxRegionCost - Number.EPSILON + ) { + this.previousBestMaxRegionCost = bestSummary.maxRegionCost + this.ripsSinceBestMaxRegionCostImprovement = 0 + } else { + this.ripsSinceBestMaxRegionCostImprovement += 1 + } + + if ( + this.baselineBeatRipCount === undefined && + bestSummary.maxRegionCost < + this.baselineSummary.maxRegionCost - Number.EPSILON + ) { + this.baselineBeatRipCount = state.ripCount + } + + this.stats = { + ...this.stats, + activeRouteCount: this.activeRouteIds.length, + currentRipThreshold, + hotRegionCount: regionIdsOverCostThreshold.length, + maxRegionCost, + totalRegionCost, + bestMaxRegionCost: bestSummary.maxRegionCost, + bestTotalRegionCost: bestSummary.totalRegionCost, + ripCount: state.ripCount, + } + + if ( + regionIdsOverCostThreshold.length === 0 || + state.ripCount >= maxRips || + this.ripsSinceBestMaxRegionCostImprovement >= + this.MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT || + (this.baselineBeatRipCount !== undefined && + state.ripCount - this.baselineBeatRipCount >= + this.EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST) + ) { + this.restoreBestState() + this.solved = true + return + } + + for ( + let mutableRegionIndex = 0; + mutableRegionIndex < this.mutableRegionIds.length; + mutableRegionIndex++ + ) { + const regionId = this.mutableRegionIds[mutableRegionIndex]! + state.regionCongestionCost[regionId] += + mutableRegionCosts[mutableRegionIndex]! * + this.RIP_CONGESTION_REGION_COST_FACTOR + } + + state.ripCount += 1 + this.resetRoutingStateForRerip() + this.stats = { + ...this.stats, + ripCount: state.ripCount, + reripRegionCount: regionIdsOverCostThreshold.length, + } + } + + override onOutOfCandidates() { + const { state } = this + + for (const regionId of this.mutableRegionIds) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + state.regionCongestionCost[regionId] += + regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + } + + state.ripCount += 1 + this.resetRoutingStateForRerip() + this.stats = { + ...this.stats, + ripCount: state.ripCount, + reripReason: "out_of_candidates", + } + } + + override tryFinalAcceptance() { + if (this.bestSnapshot) { + this.restoreBestState() + this.solved = true + return + } + + this.failed = true + this.error = + "Section search timed out before completing any active route solution" + this.stats = { + ...this.stats, + rejectedIncompleteSectionStateOnTimeout: true, + } + } + + override visualize(): GraphicsObject { + return visualizeTinyGraph(this, { + highlightSectionMask: true, + showInitialRouteHints: false, + showOnlySectionPortsOnIdle: true, + }) + } +} + +export class TinyHyperGraphSectionSolver2 extends BaseSolver { + baselineSolver: TinyHyperGraphSolver2 + baselineSummary: RegionCostSummary + sectionBaselineSummary: RegionCostSummary + outsideSectionBaselineSummary: RegionCostSummary + sectionRegionIds: RegionId[] + optimizedSolver?: TinyHyperGraphSolver2 + sectionSolver?: TinyHyperGraphSectionSearchSolver2 + activeRouteIds: RouteId[] = [] + + DISTANCE_TO_COST = 0.05 + minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER + VERBOSE = false + + RIP_THRESHOLD_START = 0.05 + RIP_THRESHOLD_END = 0.8 + RIP_THRESHOLD_RAMP_ATTEMPTS = 50 + MAX_RIPS = Number.POSITIVE_INFINITY + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT = 10 + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST = 10 + + RIP_CONGESTION_REGION_COST_FACTOR = 0.1 + + override MAX_ITERATIONS = 1e6 + STATIC_REACHABILITY_PRECHECK = false + STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 + ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true + GREEDY_FINAL_ROUTE_ITERS = 4 + + constructor( + public topology: TinyHyperGraphTopology, + public problem: TinyHyperGraphProblem, + public initialSolution: TinyHyperGraphSolution, + options?: TinyHyperGraphSectionSolver2Options, + ) { + super() + applyTinyHyperGraphSectionSolver2Options(this, options) + this.baselineSolver = createSolvedSolverFromSolution( + topology, + problem, + initialSolution, + getTinyHyperGraphSolver2Options(this), + ) + this.baselineSummary = summarizeRegionIntersectionCaches( + this.baselineSolver.state.regionIntersectionCaches, + ) + this.sectionRegionIds = getSectionRegionIds(topology, problem) + this.sectionBaselineSummary = summarizeRegionIntersectionCachesForRegionIds( + this.baselineSolver.state.regionIntersectionCaches, + this.sectionRegionIds, + ) + this.outsideSectionBaselineSummary = + summarizeRegionIntersectionCachesExcludingRegionIds( + this.baselineSolver.state.regionIntersectionCaches, + this.sectionRegionIds, + ) + this.applySectionRipPolicy() + } + + applySectionRipPolicy() { + this.RIP_THRESHOLD_START = 0.05 + this.RIP_THRESHOLD_END = Math.max( + this.RIP_THRESHOLD_START, + this.sectionBaselineSummary.maxRegionCost, + ) + this.MAX_RIPS = Math.min(this.MAX_RIPS, 20) + } + + override _setup() { + this.applySectionRipPolicy() + + const { sectionProblem, routePlans, activeRouteIds } = + createSectionRoutePlans(this.topology, this.problem, this.initialSolution) + + this.activeRouteIds = activeRouteIds + + if (activeRouteIds.length === 0) { + this.optimizedSolver = this.baselineSolver + this.stats = { + ...this.stats, + activeRouteCount: 0, + initialMaxRegionCost: this.baselineSummary.maxRegionCost, + finalMaxRegionCost: this.baselineSummary.maxRegionCost, + optimized: false, + } + this.solved = true + return + } + + this.sectionSolver = new TinyHyperGraphSectionSearchSolver2( + this.topology, + sectionProblem, + routePlans, + activeRouteIds, + this.sectionRegionIds, + this.outsideSectionBaselineSummary, + this.baselineSummary, + getTinyHyperGraphSectionSolver2Options(this), + ) + this.activeSubSolver = this.sectionSolver + this.stats = { + ...this.stats, + sectionBaselineMaxRegionCost: this.sectionBaselineSummary.maxRegionCost, + sectionBaselineTotalRegionCost: + this.sectionBaselineSummary.totalRegionCost, + effectiveRipThresholdStart: this.RIP_THRESHOLD_START, + effectiveRipThresholdEnd: this.RIP_THRESHOLD_END, + effectiveMaxRips: this.MAX_RIPS, + } + } + + override _step() { + if (!this.sectionSolver) { + this.solved = true + return + } + + this.sectionSolver.step() + this.stats = { + ...this.stats, + ...this.sectionSolver.stats, + activeRouteCount: this.activeRouteIds.length, + } + + if (this.sectionSolver.failed) { + this.optimizedSolver = this.baselineSolver + this.stats = { + ...this.stats, + initialMaxRegionCost: this.baselineSummary.maxRegionCost, + initialTotalRegionCost: this.baselineSummary.totalRegionCost, + finalMaxRegionCost: this.baselineSummary.maxRegionCost, + finalTotalRegionCost: this.baselineSummary.totalRegionCost, + optimized: false, + sectionSearchFailedFallbackToBaseline: true, + sectionSearchError: this.sectionSolver.error, + } + this.solved = true + return + } + + if (!this.sectionSolver.solved) { + return + } + + const candidateSolver = createSolvedSolverFromRegionSegments( + this.topology, + this.problem, + cloneRegionSegments(this.sectionSolver.state.regionSegments), + getTinyHyperGraphSolver2Options(this), + ) + const candidateSummary = summarizeRegionIntersectionCaches( + candidateSolver.state.regionIntersectionCaches, + ) + const optimized = + compareRegionCostSummaries(candidateSummary, this.baselineSummary) < 0 + + this.optimizedSolver = optimized ? candidateSolver : this.baselineSolver + + const finalSummary = optimized ? candidateSummary : this.baselineSummary + this.stats = { + ...this.stats, + initialMaxRegionCost: this.baselineSummary.maxRegionCost, + initialTotalRegionCost: this.baselineSummary.totalRegionCost, + candidateMaxRegionCost: candidateSummary.maxRegionCost, + candidateTotalRegionCost: candidateSummary.totalRegionCost, + finalMaxRegionCost: finalSummary.maxRegionCost, + finalTotalRegionCost: finalSummary.totalRegionCost, + optimized, + } + this.solved = true + } + + override tryFinalAcceptance() { + this.optimizedSolver = this.baselineSolver + this.stats = { + ...this.stats, + initialMaxRegionCost: this.baselineSummary.maxRegionCost, + initialTotalRegionCost: this.baselineSummary.totalRegionCost, + finalMaxRegionCost: this.baselineSummary.maxRegionCost, + finalTotalRegionCost: this.baselineSummary.totalRegionCost, + optimized: false, + sectionSolverTimeoutFallbackToBaseline: true, + } + this.solved = true + this.failed = false + this.error = null + } + + getSolvedSolver(): TinyHyperGraphSolver2 { + if (!this.solved || this.failed || !this.optimizedSolver) { + throw new Error( + "TinyHyperGraphSectionSolver2 does not have a solved output yet", + ) + } + + return this.optimizedSolver + } + + override visualize(): GraphicsObject { + if (this.optimizedSolver) { + return visualizeTinyGraph(this.optimizedSolver, { + highlightSectionMask: true, + }) + } + + if (this.sectionSolver) { + return this.sectionSolver.visualize() + } + + return visualizeTinyGraph(this.baselineSolver, { + highlightSectionMask: true, + showInitialRouteHints: false, + showOnlySectionPortsOnIdle: true, + }) + } + + override getOutput() { + return this.getSolvedSolver().getOutput() + } +} diff --git a/lib2/segment-geometry.ts b/lib2/segment-geometry.ts new file mode 100644 index 0000000..2351039 --- /dev/null +++ b/lib2/segment-geometry.ts @@ -0,0 +1,72 @@ +import type { TinyHyperGraphTopology } from "./domain" +import type { PortId, RegionId } from "./types" + +/** Geometry for one routed segment inside one region. */ +export type SegmentGeometry = { + readonly lesserAngle: number + readonly greaterAngle: number + readonly layerMask: number + readonly entryExitLayerChanges: number +} + +/** Mutable scratch object used by hot route-cost code to avoid allocations. */ +export type SegmentGeometryScratch = { + lesserAngle: number + greaterAngle: number + layerMask: number + entryExitLayerChanges: number +} + +/** + * Create reusable segment geometry scratch storage. + * + * @returns A mutable scratch object initialized to zero values. + */ +export function createSegmentGeometryScratch(): SegmentGeometryScratch { + return { + lesserAngle: 0, + greaterAngle: 0, + layerMask: 0, + entryExitLayerChanges: 0, + } +} + +/** + * Read segment geometry for a pair of ports in a region. + * + * @param topology - The loaded graph topology. + * @param regionId - Region containing the segment. + * @param fromPortId - First port of the segment. + * @param toPortId - Second port of the segment. + * @param scratch - Mutable output storage. + * @returns The populated scratch value. + */ +export function readSegmentGeometry( + topology: TinyHyperGraphTopology, + regionId: RegionId, + fromPortId: PortId, + toPortId: PortId, + scratch: SegmentGeometryScratch, +): SegmentGeometryScratch { + const fromPortRegions = topology.incidentPortRegion[fromPortId] + const toPortRegions = topology.incidentPortRegion[toPortId] + const fromAngle = + fromPortRegions[0] === regionId || fromPortRegions[1] !== regionId + ? topology.portAngleForRegion1[fromPortId] + : (topology.portAngleForRegion2?.[fromPortId] ?? + topology.portAngleForRegion1[fromPortId]) + const toAngle = + toPortRegions[0] === regionId || toPortRegions[1] !== regionId + ? topology.portAngleForRegion1[toPortId] + : (topology.portAngleForRegion2?.[toPortId] ?? + topology.portAngleForRegion1[toPortId]) + const fromZ = topology.portZ[fromPortId] + const toZ = topology.portZ[toPortId] + + scratch.lesserAngle = fromAngle < toAngle ? fromAngle : toAngle + scratch.greaterAngle = fromAngle < toAngle ? toAngle : fromAngle + scratch.layerMask = (1 << fromZ) | (1 << toZ) + scratch.entryExitLayerChanges = fromZ !== toZ ? 1 : 0 + + return scratch +} diff --git a/lib2/shuffle.ts b/lib2/shuffle.ts new file mode 100644 index 0000000..9b7bdfe --- /dev/null +++ b/lib2/shuffle.ts @@ -0,0 +1,23 @@ +const createMulberry32 = (seed: number) => { + let state = seed >>> 0 + + return () => { + state += 0x6d2b79f5 + let t = state + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +export const shuffle = (items: readonly T[], seed: number): T[] => { + const shuffled = [...items] + const random = createMulberry32(seed) + + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(random() * (i + 1)) + ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]] + } + + return shuffled +} diff --git a/lib2/solver-view.ts b/lib2/solver-view.ts new file mode 100644 index 0000000..8dc485e --- /dev/null +++ b/lib2/solver-view.ts @@ -0,0 +1,18 @@ +import type { + TinyHyperGraphProblem, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "./domain" +import type { RegionId } from "./types" + +export type TinyHyperGraphSolver2View = { + readonly topology: TinyHyperGraphTopology + readonly problem: TinyHyperGraphProblem + readonly state: TinyHyperGraphWorkingState + readonly solved: boolean + readonly failed: boolean + readonly iterations: number + getAdditionalRegionLabel(regionId: RegionId): string | undefined + getNeverSuccessfullyRoutedRoutes(): unknown[] + getStaticallyUnroutableRoutes(): unknown[] +} diff --git a/lib2/solver.ts b/lib2/solver.ts new file mode 100644 index 0000000..208dac3 --- /dev/null +++ b/lib2/solver.ts @@ -0,0 +1,1449 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import type { GraphicsObject } from "graphics-debug" +import { convertToSerializedHyperGraph } from "./graph-output" +import { + computeRegionCost, + DEFAULT_MIN_VIA_PAD_DIAMETER, + isKnownSingleLayerMask, +} from "./compute-region-cost" +import { MinHeap } from "./min-heap" +import { shuffle } from "./shuffle" +import type { StaticallyUnroutableRouteSummary } from "../lib/static-reachability" +import { + createStaticallyUnroutableRouteSummary, + getStaticallyUnroutableRoutes, + getStaticReachabilityError, +} from "../lib/static-reachability" +import type { + HopId, + NetId, + PortId, + RegionId, + RegionIntersectionCache, + RouteId, +} from "./types" +import { range } from "./utils" +import { visualizeTinyGraph } from "../lib/visualizeTinyGraph" +import type { + Candidate, + RegionCostSummary, + TinyHyperGraphProblem, + TinyHyperGraphProblemSetup, + TinyHyperGraphSolution, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "./domain" +import { + loadGraph, + type LoadGraphError, + parseGraph, + type ParseGraphError, +} from "./graph-input" +import { err, ok, type Result } from "./prelude" +import { MutableRegionCache } from "./region-cache" +import { + createSegmentGeometryScratch, + readSegmentGeometry, + type SegmentGeometryScratch, +} from "./segment-geometry" + +export type { StaticallyUnroutableRouteSummary } from "../lib/static-reachability" +export type { + Candidate, + RegionCostSummary, + TinyHyperGraphProblem, + TinyHyperGraphProblemSetup, + TinyHyperGraphSolution, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "./domain" + +const GREEDY_FINAL_ROUTE_MAX_ITERATIONS = 50e3 + +export const createEmptyRegionIntersectionCache = + (): RegionIntersectionCache => ({ + netIds: new Int32Array(0), + lesserAngles: new Int32Array(0), + greaterAngles: new Int32Array(0), + layerMasks: new Int32Array(0), + existingCrossingLayerIntersections: 0, + existingSameLayerIntersections: 0, + existingEntryExitLayerChanges: 0, + existingRegionCost: 0, + existingSegmentCount: 0, + }) + +const cloneRegionSegments = ( + regionSegments: Array<[RouteId, PortId, PortId][]>, +): Array<[RouteId, PortId, PortId][]> => + regionSegments.map((segments) => + segments.map( + ([routeId, fromPortId, toPortId]) => + [routeId, fromPortId, toPortId] as [RouteId, PortId, PortId], + ), + ) + +const cloneRegionIntersectionCache = ( + regionIntersectionCache: RegionIntersectionCache, +): RegionIntersectionCache => ({ + netIds: new Int32Array(regionIntersectionCache.netIds), + lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), + greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), + layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + existingCrossingLayerIntersections: + regionIntersectionCache.existingCrossingLayerIntersections, + existingSameLayerIntersections: + regionIntersectionCache.existingSameLayerIntersections, + existingEntryExitLayerChanges: + regionIntersectionCache.existingEntryExitLayerChanges, + existingRegionCost: regionIntersectionCache.existingRegionCost, + existingSegmentCount: regionIntersectionCache.existingSegmentCount, +}) + +const cloneSolvedStateSnapshot = ( + snapshot: SolvedStateSnapshot, +): SolvedStateSnapshot => ({ + portAssignment: new Int32Array(snapshot.portAssignment), + regionSegments: cloneRegionSegments(snapshot.regionSegments), + regionIntersectionCaches: snapshot.regionIntersectionCaches.map( + cloneRegionIntersectionCache, + ), + regionCongestionCost: new Float64Array(snapshot.regionCongestionCost), + ripCount: snapshot.ripCount, +}) + +interface SolvedStateSnapshot { + portAssignment: Int32Array + regionSegments: Array<[RouteId, PortId, PortId][]> + regionIntersectionCaches: RegionIntersectionCache[] + regionCongestionCost: Float64Array + ripCount: number +} + +export interface NeverSuccessfullyRoutedRouteSummary { + routeId: RouteId + connectionId: string + attempts: number + startPortId: PortId + endPortId: PortId + startRegionId?: string + endRegionId?: string + pointIds: string[] +} + +export interface TinyHyperGraphSolver2Options { + minViaPadDiameter?: number + DISTANCE_TO_COST?: number + RIP_THRESHOLD_START?: number + RIP_THRESHOLD_END?: number + RIP_THRESHOLD_RAMP_ATTEMPTS?: number + RIP_CONGESTION_REGION_COST_FACTOR?: number + USE_LAZY_ROUTE_HEURISTIC?: boolean + USE_SPARSE_CANDIDATE_STORAGE?: boolean + MAX_ITERATIONS?: number + VERBOSE?: boolean + STATIC_REACHABILITY_PRECHECK?: boolean + STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number + ACCEPT_BEST_SOLUTION_ON_TIMEOUT?: boolean + GREEDY_FINAL_ROUTE_ITERS?: number +} + +export interface TinyHyperGraphSolver2OptionTarget { + minViaPadDiameter: number + DISTANCE_TO_COST: number + RIP_THRESHOLD_START: number + RIP_THRESHOLD_END: number + RIP_THRESHOLD_RAMP_ATTEMPTS: number + RIP_CONGESTION_REGION_COST_FACTOR: number + USE_LAZY_ROUTE_HEURISTIC?: boolean + USE_SPARSE_CANDIDATE_STORAGE?: boolean + MAX_ITERATIONS: number + VERBOSE: boolean + STATIC_REACHABILITY_PRECHECK: boolean + STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: boolean + GREEDY_FINAL_ROUTE_ITERS: number +} + +export const applyTinyHyperGraphSolver2Options = ( + solver: TinyHyperGraphSolver2OptionTarget, + options?: TinyHyperGraphSolver2Options, +) => { + if (!options) { + return + } + + if (options.minViaPadDiameter !== undefined) { + solver.minViaPadDiameter = options.minViaPadDiameter + } + if (options.DISTANCE_TO_COST !== undefined) { + solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST + } + if (options.RIP_THRESHOLD_START !== undefined) { + solver.RIP_THRESHOLD_START = options.RIP_THRESHOLD_START + } + if (options.RIP_THRESHOLD_END !== undefined) { + solver.RIP_THRESHOLD_END = options.RIP_THRESHOLD_END + } + if (options.RIP_THRESHOLD_RAMP_ATTEMPTS !== undefined) { + solver.RIP_THRESHOLD_RAMP_ATTEMPTS = options.RIP_THRESHOLD_RAMP_ATTEMPTS + } + if (options.RIP_CONGESTION_REGION_COST_FACTOR !== undefined) { + solver.RIP_CONGESTION_REGION_COST_FACTOR = + options.RIP_CONGESTION_REGION_COST_FACTOR + } + if (options.USE_LAZY_ROUTE_HEURISTIC !== undefined) { + solver.USE_LAZY_ROUTE_HEURISTIC = options.USE_LAZY_ROUTE_HEURISTIC + } + if (options.USE_SPARSE_CANDIDATE_STORAGE !== undefined) { + solver.USE_SPARSE_CANDIDATE_STORAGE = options.USE_SPARSE_CANDIDATE_STORAGE + } + if (options.MAX_ITERATIONS !== undefined) { + solver.MAX_ITERATIONS = options.MAX_ITERATIONS + } + if (options.VERBOSE !== undefined) { + solver.VERBOSE = options.VERBOSE + } + if (options.STATIC_REACHABILITY_PRECHECK !== undefined) { + solver.STATIC_REACHABILITY_PRECHECK = options.STATIC_REACHABILITY_PRECHECK + } + if (options.STATIC_REACHABILITY_PRECHECK_MAX_HOPS !== undefined) { + solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS = + options.STATIC_REACHABILITY_PRECHECK_MAX_HOPS + } + if (options.ACCEPT_BEST_SOLUTION_ON_TIMEOUT !== undefined) { + 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 + } +} + +export const getTinyHyperGraphSolver2Options = ( + solver: TinyHyperGraphSolver2OptionTarget, +): TinyHyperGraphSolver2Options => ({ + minViaPadDiameter: solver.minViaPadDiameter, + DISTANCE_TO_COST: solver.DISTANCE_TO_COST, + RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, + RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, + RIP_THRESHOLD_RAMP_ATTEMPTS: solver.RIP_THRESHOLD_RAMP_ATTEMPTS, + RIP_CONGESTION_REGION_COST_FACTOR: solver.RIP_CONGESTION_REGION_COST_FACTOR, + USE_LAZY_ROUTE_HEURISTIC: solver.USE_LAZY_ROUTE_HEURISTIC, + USE_SPARSE_CANDIDATE_STORAGE: solver.USE_SPARSE_CANDIDATE_STORAGE, + MAX_ITERATIONS: solver.MAX_ITERATIONS, + VERBOSE: solver.VERBOSE, + STATIC_REACHABILITY_PRECHECK: solver.STATIC_REACHABILITY_PRECHECK, + 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, +}) + +const compareCandidatesByF = (left: Candidate, right: Candidate) => + left.f - right.f + +export class TinyHyperGraphSolver2 extends BaseSolver { + state: TinyHyperGraphWorkingState + private _problemSetup?: TinyHyperGraphProblemSetup + protected routeAttemptCountByRouteId: Uint32Array + protected routeSuccessCountByRouteId: Uint32Array + protected bestSolvedStateSnapshot?: SolvedStateSnapshot + protected bestSolvedStateSummary?: RegionCostSummary + private hasLoggedNeverSuccessfullyRoutedRoutes = false + private staticallyUnroutableRoutes: StaticallyUnroutableRouteSummary[] = [] + private readonly segmentGeometryScratch: SegmentGeometryScratch = + createSegmentGeometryScratch() + private regionCaches: MutableRegionCache[] = [] + + DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) + minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER + + RIP_THRESHOLD_START = 0.05 + RIP_THRESHOLD_END = 0.8 + RIP_THRESHOLD_RAMP_ATTEMPTS = 50 + + RIP_CONGESTION_REGION_COST_FACTOR = 0.1 + USE_LAZY_ROUTE_HEURISTIC = false + USE_SPARSE_CANDIDATE_STORAGE = false + + override MAX_ITERATIONS = 1e6 + VERBOSE = false + STATIC_REACHABILITY_PRECHECK = true + STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 + ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true + GREEDY_FINAL_ROUTE_ITERS = 4 + + constructor( + public topology: TinyHyperGraphTopology, + public problem: TinyHyperGraphProblem, + options?: TinyHyperGraphSolver2Options, + ) { + super() + applyTinyHyperGraphSolver2Options(this, options) + this.state = { + portAssignment: new Int32Array(topology.portCount).fill(-1), + regionSegments: Array.from({ length: topology.regionCount }, () => []), + regionIntersectionCaches: Array.from( + { length: topology.regionCount }, + () => createEmptyRegionIntersectionCache(), + ), + currentRouteId: undefined, + currentRouteNetId: undefined, + unroutedRoutes: range(problem.routeCount), + candidateQueue: new MinHeap([], compareCandidatesByF), + candidateBestCostByHopId: this.USE_SPARSE_CANDIDATE_STORAGE + ? new Map() + : new Float64Array(topology.portCount * topology.regionCount), + candidateBestCostGenerationByHopId: this.USE_SPARSE_CANDIDATE_STORAGE + ? new Map() + : new Uint32Array(topology.portCount * topology.regionCount), + candidateBestCostGeneration: 1, + goalPortId: -1, + ripCount: 0, + regionCongestionCost: new Float64Array(topology.regionCount).fill(0), + } + this.routeAttemptCountByRouteId = new Uint32Array(problem.routeCount) + this.routeSuccessCountByRouteId = new Uint32Array(problem.routeCount) + this.regionCaches = this.createRegionCachesFromState() + } + + get problemSetup(): TinyHyperGraphProblemSetup { + if (!this._problemSetup) { + this._problemSetup = this.computeProblemSetup() + } + + return this._problemSetup + } + + computeProblemSetup(): TinyHyperGraphProblemSetup { + const { topology, problem } = this + const portHCostToEndOfRoute = this.USE_LAZY_ROUTE_HEURISTIC + ? undefined + : new Float64Array(topology.portCount * problem.routeCount) + const portX = topology.portX as unknown as ArrayLike + const portY = topology.portY as unknown as ArrayLike + const portEndpointNetIds = Array.from( + { length: topology.portCount }, + () => new Set(), + ) + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + portEndpointNetIds[problem.routeStartPort[routeId]]!.add( + problem.routeNet[routeId], + ) + portEndpointNetIds[problem.routeEndPort[routeId]]!.add( + problem.routeNet[routeId], + ) + + if (portHCostToEndOfRoute) { + const endPortId = problem.routeEndPort[routeId] + const endX = portX[endPortId] + const endY = portY[endPortId] + + for (let portId = 0; portId < topology.portCount; portId++) { + const dx = portX[portId] - endX + const dy = portY[portId] - endY + portHCostToEndOfRoute[portId * problem.routeCount + routeId] = + Math.hypot(dx, dy) * this.DISTANCE_TO_COST + } + } + } + + return { + portHCostToEndOfRoute: portHCostToEndOfRoute as Float64Array, + portEndpointNetIds, + } + } + + override _setup() { + void this.problemSetup + + if (this.STATIC_REACHABILITY_PRECHECK) { + const staticallyUnroutableRoutes = getStaticallyUnroutableRoutes({ + topology: this.topology, + problem: this.problem, + problemSetup: this.problemSetup, + portAssignment: this.state.portAssignment, + routeIds: this.state.unroutedRoutes, + maxPrecheckHops: Math.max( + 0, + this.STATIC_REACHABILITY_PRECHECK_MAX_HOPS, + ), + getStartingNextRegionId: (routeId, startingPortId) => + this.getStartingNextRegionId(routeId, startingPortId), + getRouteSummary: (routeId) => this.getRouteSummary(routeId), + }) + this.staticallyUnroutableRoutes = staticallyUnroutableRoutes + if (staticallyUnroutableRoutes.length > 0) { + this.failed = true + this.error = getStaticReachabilityError(staticallyUnroutableRoutes) + this.stats = { + ...this.stats, + staticallyUnroutableRouteCount: staticallyUnroutableRoutes.length, + } + } + } + } + + override _step() { + const { problem, topology, state } = this + + if (state.currentRouteId === undefined) { + if (state.unroutedRoutes.length === 0) { + this.onAllRoutesRouted() + return + } + + state.currentRouteId = state.unroutedRoutes.shift() + state.currentRouteNetId = problem.routeNet[state.currentRouteId!] + this.routeAttemptCountByRouteId[state.currentRouteId!] += 1 + + this.resetCandidateBestCosts() + const startingPortId = problem.routeStartPort[state.currentRouteId!] + state.candidateQueue.clear() + const startingNextRegionId = this.getStartingNextRegionId( + state.currentRouteId!, + startingPortId, + ) + + if (startingNextRegionId === undefined) { + this.failed = true + this.error = `Start port ${startingPortId} has no incident regions` + return + } + + this.setCandidateBestCost( + this.getHopId(startingPortId, startingNextRegionId), + 0, + ) + state.candidateQueue.queue({ + nextRegionId: startingNextRegionId, + portId: startingPortId, + f: 0, + g: 0, + h: 0, + }) + state.goalPortId = problem.routeEndPort[state.currentRouteId!] + } + + const currentCandidate = state.candidateQueue.dequeue() + + if (!currentCandidate) { + this.onOutOfCandidates() + return + } + + const currentCandidateHopId = this.getHopId( + currentCandidate.portId, + currentCandidate.nextRegionId, + ) + if (currentCandidate.g > this.getCandidateBestCost(currentCandidateHopId)) { + return + } + + if (this.isRegionReservedForDifferentNet(currentCandidate.nextRegionId)) { + return + } + + const neighbors = + topology.regionIncidentPorts[currentCandidate.nextRegionId] + + for (const neighborPortId of neighbors) { + const assignedNetId = state.portAssignment[neighborPortId] + if (this.isPortReservedForDifferentNet(neighborPortId)) continue + if (neighborPortId === state.goalPortId) { + if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { + continue + } + this.onPathFound(currentCandidate) + return + } + if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { + continue + } + if (neighborPortId === currentCandidate.portId) continue + if (problem.portSectionMask[neighborPortId] === 0) continue + + const g = this.computeG(currentCandidate, neighborPortId) + if (!Number.isFinite(g)) continue + const h = this.computeH(neighborPortId) + + const nextRegionId = + topology.incidentPortRegion[neighborPortId][0] === + currentCandidate.nextRegionId + ? topology.incidentPortRegion[neighborPortId][1] + : topology.incidentPortRegion[neighborPortId][0] + + if ( + nextRegionId === undefined || + this.isRegionReservedForDifferentNet(nextRegionId) + ) { + continue + } + + const newCandidate = { + prevRegionId: currentCandidate.nextRegionId, + nextRegionId, + portId: neighborPortId, + g, + h, + f: g + h, + prevCandidate: currentCandidate, + } + + if (neighborPortId === state.goalPortId) { + this.onPathFound(newCandidate) + return + } + + const candidateHopId = this.getHopId(neighborPortId, nextRegionId) + if (g >= this.getCandidateBestCost(candidateHopId)) continue + + this.setCandidateBestCost(candidateHopId, g) + state.candidateQueue.queue(newCandidate) + } + } + + resetCandidateBestCosts() { + const { state } = this + + if (state.candidateBestCostGeneration === 0xffffffff) { + if (state.candidateBestCostByHopId instanceof Map) { + state.candidateBestCostByHopId.clear() + } + if (state.candidateBestCostGenerationByHopId instanceof Map) { + state.candidateBestCostGenerationByHopId.clear() + } else { + state.candidateBestCostGenerationByHopId.fill(0) + } + state.candidateBestCostGeneration = 1 + return + } + + state.candidateBestCostGeneration += 1 + } + + getCandidateBestCost(hopId: HopId) { + const { state } = this + const bestCostGeneration = state.candidateBestCostGenerationByHopId + + return (bestCostGeneration instanceof Map + ? bestCostGeneration.get(hopId) + : bestCostGeneration[hopId]) === state.candidateBestCostGeneration + ? state.candidateBestCostByHopId instanceof Map + ? state.candidateBestCostByHopId.get(hopId)! + : state.candidateBestCostByHopId[hopId]! + : Number.POSITIVE_INFINITY + } + + setCandidateBestCost(hopId: HopId, bestCost: number) { + const { state } = this + + if (state.candidateBestCostGenerationByHopId instanceof Map) { + state.candidateBestCostGenerationByHopId.set( + hopId, + state.candidateBestCostGeneration, + ) + } else { + state.candidateBestCostGenerationByHopId[hopId] = + state.candidateBestCostGeneration + } + + if (state.candidateBestCostByHopId instanceof Map) { + state.candidateBestCostByHopId.set(hopId, bestCost) + } else { + state.candidateBestCostByHopId[hopId] = bestCost + } + } + + getHopId(portId: PortId, nextRegionId: RegionId): HopId { + return portId * this.topology.regionCount + nextRegionId + } + + getStartingNextRegionId( + routeId: RouteId, + startingPortId: PortId, + ): RegionId | undefined { + const startingIncidentRegions = + this.topology.incidentPortRegion[startingPortId] ?? [] + const currentRouteNetId = this.problem.routeNet[routeId] + + return ( + startingIncidentRegions.find( + (regionId) => this.problem.regionNetId[regionId] === -1, + ) ?? + startingIncidentRegions.find( + (regionId) => this.problem.regionNetId[regionId] === currentRouteNetId, + ) ?? + startingIncidentRegions[0] + ) + } + + isPortReservedForDifferentNet(portId: PortId): boolean { + const reservedNetIds = this.problemSetup.portEndpointNetIds[portId] + if (!reservedNetIds) { + return false + } + + for (const netId of reservedNetIds) { + if (netId !== this.state.currentRouteNetId) { + return true + } + } + + return false + } + + isRegionReservedForDifferentNet(regionId: RegionId): boolean { + const reservedNetId = this.problem.regionNetId[regionId] + return ( + reservedNetId !== -1 && reservedNetId !== this.state.currentRouteNetId + ) + } + + isKnownSingleLayerRegion(regionId: RegionId): boolean { + const regionAvailableZMask = + this.topology.regionAvailableZMask?.[regionId] ?? 0 + return isKnownSingleLayerMask(regionAvailableZMask) + } + + protected computeRegionCostForRegion( + regionId: RegionId, + numSameLayerIntersections: number, + numCrossLayerIntersections: number, + numEntryExitChanges: number, + traceCount: number, + ): number { + return computeRegionCost( + this.topology.regionWidth[regionId], + this.topology.regionHeight[regionId], + numSameLayerIntersections, + numCrossLayerIntersections, + numEntryExitChanges, + traceCount, + this.topology.regionAvailableZMask?.[regionId] ?? 0, + this.minViaPadDiameter, + ) + } + + populateSegmentGeometryScratch( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + ): SegmentGeometryScratch { + return readSegmentGeometry( + this.topology, + regionId, + port1Id, + port2Id, + this.segmentGeometryScratch, + ) + } + + appendSegmentToRegionCache( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + ) { + const netId = this.getCurrentRouteNetId() + const segmentGeometry = this.populateSegmentGeometryScratch( + regionId, + port1Id, + port2Id, + ) + const regionCache = this.getMutableRegionCache(regionId) + const delta = regionCache.countDelta(netId, segmentGeometry) + + this.state.regionIntersectionCaches[regionId] = regionCache.append( + netId, + segmentGeometry, + delta, + ( + sameLayerIntersections, + crossingLayerIntersections, + entryExitLayerChanges, + segmentCount, + ) => + this.computeRegionCostForRegion( + regionId, + sameLayerIntersections, + crossingLayerIntersections, + entryExitLayerChanges, + segmentCount, + ), + ) + } + + private createRegionCachesFromState(): MutableRegionCache[] { + return this.state.regionIntersectionCaches.map((cache) => + MutableRegionCache.from(cache), + ) + } + + private getMutableRegionCache(regionId: RegionId): MutableRegionCache { + const visibleCache = this.state.regionIntersectionCaches[regionId] + if (!visibleCache) { + throw new Error(`Region cache ${regionId} is missing`) + } + + const existingCache = this.regionCaches[regionId] + if (existingCache?.owns(visibleCache)) { + return existingCache + } + + const nextCache = MutableRegionCache.from(visibleCache) + this.regionCaches[regionId] = nextCache + return nextCache + } + + private getCurrentRouteNetId(): NetId { + const netId = this.state.currentRouteNetId + if (netId === undefined) { + throw new Error("Cannot read current route net before a route is active") + } + + return netId + } + getSolvedPathSegments(finalCandidate: Candidate): Array<{ + regionId: RegionId + fromPortId: PortId + toPortId: PortId + }> { + const { state } = this + const candidatePath: Candidate[] = [] + let cursor: Candidate | undefined = finalCandidate + + while (cursor) { + candidatePath.unshift(cursor) + cursor = cursor.prevCandidate + } + + const solvedSegments: Array<{ + regionId: RegionId + fromPortId: PortId + toPortId: PortId + }> = [] + + for (let i = 1; i < candidatePath.length; i++) { + solvedSegments.push({ + regionId: candidatePath[i - 1].nextRegionId, + fromPortId: candidatePath[i - 1].portId, + toPortId: candidatePath[i].portId, + }) + } + + const lastCandidate = candidatePath[candidatePath.length - 1] + if (lastCandidate && lastCandidate.portId !== state.goalPortId) { + solvedSegments.push({ + regionId: lastCandidate.nextRegionId, + fromPortId: lastCandidate.portId, + toPortId: state.goalPortId, + }) + } + + return solvedSegments + } + + resetRoutingStateForRerip() { + 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.currentRouteNetId = undefined + state.currentRouteId = undefined + state.unroutedRoutes = shuffle(range(problem.routeCount), state.ripCount) + state.candidateQueue.clear() + this.resetCandidateBestCosts() + state.goalPortId = -1 + this.regionCaches = this.createRegionCachesFromState() + } + + protected getMaxRegionCost() { + const { topology, state } = this + let maxRegionCost = 0 + + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + maxRegionCost = Math.max(maxRegionCost, regionCost) + } + + return maxRegionCost + } + + protected getRouteMetadata(routeId: RouteId): + | { + connectionId?: unknown + startRegionId?: unknown + endRegionId?: unknown + simpleRouteConnection?: { + pointsToConnect?: Array<{ + pointId?: unknown + }> + } + } + | undefined { + return this.problem.routeMetadata?.[routeId] as + | { + connectionId?: unknown + startRegionId?: unknown + endRegionId?: unknown + simpleRouteConnection?: { + pointsToConnect?: Array<{ + pointId?: unknown + }> + } + } + | undefined + } + + protected getRouteConnectionId(routeId: RouteId) { + const connectionId = this.getRouteMetadata(routeId)?.connectionId + return typeof connectionId === "string" ? connectionId : `route-${routeId}` + } + + protected getRouteSummary( + routeId: RouteId, + ): StaticallyUnroutableRouteSummary { + return createStaticallyUnroutableRouteSummary({ + problem: this.problem, + routeId, + getRouteMetadata: (currentRouteId) => + this.getRouteMetadata(currentRouteId), + getRouteConnectionId: (currentRouteId) => + this.getRouteConnectionId(currentRouteId), + }) + } + + getAdditionalRegionLabel(_regionId: RegionId): string | undefined { + return undefined + } + + getNeverSuccessfullyRoutedRoutes(): NeverSuccessfullyRoutedRouteSummary[] { + const neverSuccessfullyRoutedRoutes: NeverSuccessfullyRoutedRouteSummary[] = + [] + + for (let routeId = 0; routeId < this.problem.routeCount; routeId++) { + const attempts = this.routeAttemptCountByRouteId[routeId]! + if (attempts === 0 || this.routeSuccessCountByRouteId[routeId]! > 0) { + continue + } + + neverSuccessfullyRoutedRoutes.push({ + ...this.getRouteSummary(routeId), + attempts, + }) + } + + return neverSuccessfullyRoutedRoutes + } + + getStaticallyUnroutableRoutes(): StaticallyUnroutableRouteSummary[] { + return this.staticallyUnroutableRoutes + } + + protected logNeverSuccessfullyRoutedRoutes() { + if (!this.VERBOSE || this.hasLoggedNeverSuccessfullyRoutedRoutes) { + return + } + + const neverSuccessfullyRoutedRoutes = + this.getNeverSuccessfullyRoutedRoutes() + this.hasLoggedNeverSuccessfullyRoutedRoutes = true + + if (neverSuccessfullyRoutedRoutes.length === 0) { + return + } + + console.log( + [ + "[TinyHyperGraphSolver2:never-routed-summary]", + `count=${neverSuccessfullyRoutedRoutes.length}`, + ].join(" "), + ) + + for (const neverSuccessfullyRoutedRoute of neverSuccessfullyRoutedRoutes) { + const pointPath = + neverSuccessfullyRoutedRoute.pointIds.length >= 2 + ? `${neverSuccessfullyRoutedRoute.pointIds[0]}->${neverSuccessfullyRoutedRoute.pointIds[1]}` + : "unknown" + + console.log( + [ + "[TinyHyperGraphSolver2:never-routed]", + `routeId=${neverSuccessfullyRoutedRoute.routeId}`, + `connectionId=${neverSuccessfullyRoutedRoute.connectionId}`, + `attempts=${neverSuccessfullyRoutedRoute.attempts}`, + `pointPath=${pointPath}`, + `startRegionId=${neverSuccessfullyRoutedRoute.startRegionId ?? "unknown"}`, + `endRegionId=${neverSuccessfullyRoutedRoute.endRegionId ?? "unknown"}`, + ].join(" "), + ) + } + } + + protected logRipEvent( + reason: "hot_regions" | "out_of_candidates", + maxRegionCostBeforeRip: number, + extraFields: Record = {}, + ) { + if (!this.VERBOSE) { + return + } + + console.log( + [ + "[TinyHyperGraphSolver2:rip]", + `ripCount=${this.state.ripCount}`, + `maxRegionCostBeforeRip=${maxRegionCostBeforeRip.toFixed(3)}`, + `reason=${reason}`, + ...Object.entries(extraFields).map( + ([key, value]) => + `${key}=${ + typeof value === "number" + ? Number.isInteger(value) + ? String(value) + : value.toFixed(3) + : value + }`, + ), + ].join(" "), + ) + } + + protected compareRegionCostSummaries( + left: RegionCostSummary, + right: RegionCostSummary, + ) { + if (left.maxRegionCost !== right.maxRegionCost) { + return left.maxRegionCost - right.maxRegionCost + } + + return left.totalRegionCost - right.totalRegionCost + } + + protected captureBestSolvedState(summary: RegionCostSummary) { + if ( + this.bestSolvedStateSummary && + this.compareRegionCostSummaries(summary, this.bestSolvedStateSummary) >= 0 + ) { + return + } + + this.bestSolvedStateSummary = summary + this.bestSolvedStateSnapshot = cloneSolvedStateSnapshot({ + portAssignment: this.state.portAssignment, + regionSegments: this.state.regionSegments, + regionIntersectionCaches: this.state.regionIntersectionCaches, + regionCongestionCost: this.state.regionCongestionCost, + ripCount: this.state.ripCount, + }) + } + + protected restoreBestSolvedState() { + if (!this.bestSolvedStateSnapshot) { + return + } + + const snapshot = cloneSolvedStateSnapshot(this.bestSolvedStateSnapshot) + this.state.portAssignment = snapshot.portAssignment + this.state.regionSegments = snapshot.regionSegments + this.state.regionIntersectionCaches = snapshot.regionIntersectionCaches + this.state.regionCongestionCost = snapshot.regionCongestionCost + this.state.ripCount = snapshot.ripCount + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = [] + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + this.regionCaches = this.createRegionCachesFromState() + } + + 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: TinyHyperGraphSolver2, + 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: TinyHyperGraphSolver2, + ): RegionCostSummary { + let maxRegionCost = 0 + let totalRegionCost = 0 + + for (const regionIntersectionCache of solver.state + .regionIntersectionCaches) { + const regionCost = regionIntersectionCache.existingRegionCost + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + } + + return { + maxRegionCost, + totalRegionCost, + } + } + + protected tryGreedyFinalRouteAcceptance(): boolean { + const greedyFinalRouteIters = Math.max( + 0, + Math.floor(this.GREEDY_FINAL_ROUTE_ITERS), + ) + if (greedyFinalRouteIters === 0) { + return false + } + + const remainingRouteIds = this.getRemainingRouteIdsForGreedyFinalRoute() + if (remainingRouteIds.length === 0) { + return false + } + + const startingSnapshot = cloneSolvedStateSnapshot({ + portAssignment: this.state.portAssignment, + regionSegments: this.state.regionSegments, + regionIntersectionCaches: this.state.regionIntersectionCaches, + regionCongestionCost: this.state.regionCongestionCost, + 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, + { + ...getTinyHyperGraphSolver2Options(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.stats = { + ...this.stats, + greedyFinalRouteAttemptCount: greedyFinalRouteIters, + greedyFinalRouteRemainingRouteCount: remainingRouteIds.length, + greedyFinalRouteMaxIterations: GREEDY_FINAL_ROUTE_MAX_ITERATIONS, + } + + return false + } + + onAllRoutesRouted() { + const { topology, state } = this + const ripThresholdProgress = + this.RIP_THRESHOLD_RAMP_ATTEMPTS <= 0 + ? 1 + : Math.min(1, state.ripCount / this.RIP_THRESHOLD_RAMP_ATTEMPTS) + const currentRipThreshold = + this.RIP_THRESHOLD_START + + (this.RIP_THRESHOLD_END - this.RIP_THRESHOLD_START) * ripThresholdProgress + + const regionIdsOverCostThreshold: RegionId[] = [] + const regionCosts = new Float64Array(topology.regionCount) + let maxRegionCost = 0 + let totalRegionCost = 0 + + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + regionCosts[regionId] = regionCost + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + + if (regionCost > currentRipThreshold) { + regionIdsOverCostThreshold.push(regionId) + } + } + + this.captureBestSolvedState({ + maxRegionCost, + totalRegionCost, + }) + + this.stats = { + ...this.stats, + currentRipThreshold, + hotRegionCount: regionIdsOverCostThreshold.length, + maxRegionCost, + totalRegionCost, + bestMaxRegionCost: this.bestSolvedStateSummary?.maxRegionCost, + bestTotalRegionCost: this.bestSolvedStateSummary?.totalRegionCost, + ripCount: state.ripCount, + } + + if ( + regionIdsOverCostThreshold.length === 0 || + state.ripCount >= this.RIP_THRESHOLD_RAMP_ATTEMPTS + ) { + this.solved = true + return + } + + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + state.regionCongestionCost[regionId] += + regionCosts[regionId] * this.RIP_CONGESTION_REGION_COST_FACTOR + } + + state.ripCount += 1 + this.resetRoutingStateForRerip() + this.stats = { + ...this.stats, + ripCount: state.ripCount, + maxRegionCostBeforeRip: maxRegionCost, + reripRegionCount: regionIdsOverCostThreshold.length, + } + this.logRipEvent("hot_regions", maxRegionCost, { + hotRegionCount: regionIdsOverCostThreshold.length, + currentRipThreshold, + }) + } + + onOutOfCandidates() { + const { topology, state } = this + const currentRouteId = state.currentRouteId + const maxRegionCostBeforeRip = this.getMaxRegionCost() + + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + state.regionCongestionCost[regionId] += + regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + } + + state.ripCount += 1 + this.resetRoutingStateForRerip() + this.stats = { + ...this.stats, + ripCount: state.ripCount, + maxRegionCost: maxRegionCostBeforeRip, + maxRegionCostBeforeRip, + reripReason: "out_of_candidates", + } + this.logRipEvent("out_of_candidates", maxRegionCostBeforeRip, { + ...(currentRouteId === undefined + ? {} + : { + routeId: currentRouteId, + connectionId: this.getRouteConnectionId(currentRouteId), + }), + }) + } + + onPathFound(finalCandidate: Candidate) { + const { state } = this + const currentRouteId = state.currentRouteId + + if (currentRouteId === undefined) return + this.routeSuccessCountByRouteId[currentRouteId] += 1 + + const solvedSegments = this.getSolvedPathSegments(finalCandidate) + + for (const { regionId, fromPortId, toPortId } of solvedSegments) { + state.regionSegments[regionId].push([ + currentRouteId, + fromPortId, + toPortId, + ]) + state.portAssignment[fromPortId] = state.currentRouteNetId! + state.portAssignment[toPortId] = state.currentRouteNetId! + this.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } + + state.candidateQueue.clear() + state.currentRouteNetId = undefined + state.currentRouteId = undefined + } + + computeG(currentCandidate: Candidate, neighborPortId: PortId): number { + const nextRegionId = currentCandidate.nextRegionId + const segmentGeometry = this.populateSegmentGeometryScratch( + nextRegionId, + currentCandidate.portId, + neighborPortId, + ) + const regionCache = this.getMutableRegionCache(nextRegionId) + const delta = regionCache.countDelta( + this.getCurrentRouteNetId(), + segmentGeometry, + ) + + if ( + delta.sameLayerIntersections > 0 && + this.isKnownSingleLayerRegion(nextRegionId) + ) { + return Number.POSITIVE_INFINITY + } + + const newRegionCost = + this.computeRegionCostForRegion( + nextRegionId, + regionCache.sameLayerIntersections + delta.sameLayerIntersections, + regionCache.crossingLayerIntersections + + delta.crossingLayerIntersections, + regionCache.entryExitLayerChanges + delta.entryExitLayerChanges, + regionCache.committedSegmentCount + 1, + ) - regionCache.regionCost + + return ( + currentCandidate.g + + newRegionCost + + this.state.regionCongestionCost[nextRegionId] + + (this.problem.portPenalty?.[neighborPortId] ?? 0) + ) + } + + override tryFinalAcceptance() { + const neverSuccessfullyRoutedRoutes = + this.getNeverSuccessfullyRoutedRoutes() + + this.stats = { + ...this.stats, + neverSuccessfullyRoutedRouteCount: neverSuccessfullyRoutedRoutes.length, + } + + if ( + this.ACCEPT_BEST_SOLUTION_ON_TIMEOUT && + this.bestSolvedStateSnapshot && + this.bestSolvedStateSummary + ) { + this.restoreBestSolvedState() + this.stats = { + ...this.stats, + acceptedBestSolutionOnTimeout: true, + 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 + } + + if ( + this.ACCEPT_BEST_SOLUTION_ON_TIMEOUT && + this.tryGreedyFinalRouteAcceptance() + ) { + return + } + + this.logNeverSuccessfullyRoutedRoutes() + } + + computeH(neighborPortId: PortId): number { + const precomputedHCost = this.problemSetup.portHCostToEndOfRoute + if (precomputedHCost) { + return precomputedHCost[ + neighborPortId * this.problem.routeCount + this.state.currentRouteId! + ] + } + + const endPortId = this.problem.routeEndPort[this.state.currentRouteId!] + const dx = + this.topology.portX[neighborPortId] - this.topology.portX[endPortId] + const dy = + this.topology.portY[neighborPortId] - this.topology.portY[endPortId] + return Math.hypot(dx, dy) * this.DISTANCE_TO_COST + } + + /** + * Run the solver and return expected solve failures as values. + * + * @returns The current solver on success, or a typed solve error. + */ + solveResult(): Result { + try { + this.solve() + } catch (cause) { + return err( + new SolveGraphError( + cause instanceof Error ? cause.message : String(cause), + this.stats, + ), + ) + } + + if (!this.solved || this.failed) { + return err( + new SolveGraphError(this.error ?? "solver did not finish", this.stats), + ) + } + + return ok(this) + } + + override visualize(): GraphicsObject { + return visualizeTinyGraph(this) + } + + override getOutput() { + return convertToSerializedHyperGraph(this) + } +} + +class GreedyFinalRouteSolver extends TinyHyperGraphSolver2 { + override computeG( + currentCandidate: Candidate, + _neighborPortId: PortId, + ): number { + return currentCandidate.g + } +} + +/** Expected failure produced while running a lib2 solve. */ +export class SolveGraphError extends Error { + readonly _tag = "SolveGraphError" + + constructor( + readonly reason: string, + readonly stats: Record, + ) { + super(`Unable to solve graph: ${reason}`) + } +} + +/** Successful serialized solve output. */ +export type SolvedGraph = { + readonly solver: TinyHyperGraphSolver2 + readonly graph: SerializedHyperGraph +} + +/** + * Create a lib2 solver from loaded topology and problem data. + * + * @param topology - Loaded graph topology. + * @param problem - Loaded routing problem. + * @param options - Optional solver parameters. + * @returns A lib2 solver instance. + */ +export function createSolver( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options?: TinyHyperGraphSolver2Options, +): TinyHyperGraphSolver2 { + return new TinyHyperGraphSolver2(topology, problem, options) +} + +/** + * Parse, load, solve, and serialize a graph through the lib2 boundary. + * + * @param graph - Unknown serialized graph input. + * @param options - Optional solver parameters. + * @returns A solved serialized graph or a typed expected failure. + */ +export function solveGraph( + graph: unknown, + options?: TinyHyperGraphSolver2Options, +): Result { + const parsedGraphResult = parseGraph(graph) + if (parsedGraphResult._tag === "err") { + return parsedGraphResult + } + + const loadedGraphResult = loadGraph(parsedGraphResult.value) + if (loadedGraphResult._tag === "err") { + return loadedGraphResult + } + + const solver = createSolver( + loadedGraphResult.value.topology, + loadedGraphResult.value.problem, + options, + ) + const solveResult = solver.solveResult() + if (solveResult._tag === "err") { + return solveResult + } + + return ok({ + solver, + graph: solver.getOutput(), + }) +} diff --git a/lib2/types.ts b/lib2/types.ts new file mode 100644 index 0000000..b109000 --- /dev/null +++ b/lib2/types.ts @@ -0,0 +1,37 @@ +export type PortId = number +export type RegionId = number +export type Integer = number +export type RouteId = number +export type NetId = number +export type HopId = number + +/** SegmentIds are computed via port1Id * portCount + port2Id */ +export type SegmentId = number + +/** A lossy hash on a segment id, can be used for congestion where an erroneous collision is not too bad */ +export type LossySegmentIdHash = number + +export type LesserAngle = number +export type Z1 = number +export type GreaterAngle = number +export type Z2 = number + +export type DynamicAnglePair = [NetId, LesserAngle, Z1, GreaterAngle, Z2] +export interface DynamicAnglePairArrays { + netIds: Int32Array + lesserAngles: Int32Array + greaterAngles: Int32Array + layerMasks: Int32Array +} + +export interface RegionIntersectionCache extends DynamicAnglePairArrays { + existingSameLayerIntersections: Integer + existingCrossingLayerIntersections: Integer + existingEntryExitLayerChanges: Integer + existingRegionCost: number + existingSegmentCount: number +} + +export type SameLayerIntersectionCount = number +export type CrossingLayerIntersectionCount = number +export type EntryExitLayerChanges = number diff --git a/lib2/utils.ts b/lib2/utils.ts new file mode 100644 index 0000000..a9e83e0 --- /dev/null +++ b/lib2/utils.ts @@ -0,0 +1,7 @@ +export const range = (len: number) => { + const ar: number[] = new Array(len) + for (let i = 0; i < len; i++) { + ar[i] = i + } + return ar +} diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..642be0e --- /dev/null +++ b/progress.md @@ -0,0 +1,137 @@ +# Progress + +## Baseline Stats + +Environment: local Bun run in `/home/ohmx/Documents/tiny-hypergraph`. + +- Repo stats before edits: 94 files under `lib/tests/scripts`; 38 TypeScript + files in `lib`; 39 TypeScript test files; 13,332 total lines in `lib/*.ts`. +- `bun test`: 90 pass, 0 fail, 11.23s. +- `bun run typecheck`: passed. +- `./benchmark.sh --limit 10 --concurrency 6`: 10/10 success, avg duration + 0.090s, p50 0.052s, p95 0.347s, avg final max region cost 0.170. +- `./benchmark-srj13.sh --limit 5`: 40.0% success, avg duration 8.459s, + avg iterations 1,000,000.0, avg solved max region cost 519.897. +- `./benchmark2.sh`: solved CM5IO in 49.892s, routeCount 158, regionCount + 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. + +## Work System + +1. Capture stats before changing behavior. +2. Keep vocabulary changes in `dictionary.md`. +3. Build `lib2` as a measurable compatibility slice first. +4. Add tests through public seams. +5. Run typecheck, tests, and representative benchmarks. +6. Only move performance-sensitive internals after a benchmark comparison exists. + +## Current Checklist + +- [x] Load TypeScript standards. +- [x] Capture original tests and benchmarks. +- [x] Use sub-agent architecture review. +- [x] Start Claude CLI architecture review without killing early. +- [x] Add `lib2` API and docs. +- [x] Add benchmark flags for `lib2`. +- [x] Verify and compare. + +## Lib2 Verification + +- `bun run typecheck`: passed. +- `bun test`: 93 pass, 0 fail, 11.92s. +- `./benchmark.sh --limit 10 --concurrency 6 --solver lib2`: 10/10 success, + avg duration 0.043s, p50 0.025s, p95 0.150s, avg final max region cost + 0.170. +- `./benchmark-srj13.sh --limit 5 --solver lib2`: 40.0% success, avg + duration 7.693s, avg iterations 1,000,000.0, avg solved max region cost + 519.897. +- `./benchmark2.sh --solver lib2`: solved CM5IO in 47.531s, routeCount 158, + regionCount 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. + +## Owned Lib2 Rewrite + +- `TinyHyperGraphSolver2` now extends `BaseSolver` directly instead of + `TinyHyperGraphSolver`. +- `TinyHyperGraphSectionPipelineSolver2` now extends `BasePipelineSolver` + directly instead of `TinyHyperGraphSectionPipelineSolver`. +- `TinyHyperGraphSolver2` owns its solver state, route search lifecycle, rerip + policy, final acceptance, output, and visualization hooks. +- Lib2 uses `MutableRegionCache` and `readSegmentGeometry` for route cost and + committed segment cache updates. +- `convertToSerializedHyperGraph`, `visualizeTinyGraph`, and static + reachability visualization now depend on a structural + `TinyHyperGraphSolverView` instead of the nominal legacy solver class. + +Verification after ownership rewrite: + +- `bun run typecheck`: passed. +- `bun test`: 97 pass, 0 fail, 13.63s. +- `./benchmark-srj13.sh --limit 5 --solver lib2`: 40.0% success, avg duration + 7.935s, avg iterations 1,000,000.0, avg solved max region cost 519.897. +- `./benchmark.sh --limit 10 --concurrency 6 --solver lib2`: 10/10 success, + avg duration 0.053s, p50 0.028s, p95 0.180s, avg final max region cost + 0.170. +- `./benchmark2.sh --solver lib2`: solved CM5IO in 47.168s, routeCount 158, + regionCount 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. + +## Owned Lib2 Section Optimization + +- `TinyHyperGraphSectionSolver2` now lives in `lib2/section-solver.ts` and uses + `TinyHyperGraphSolver2` for baseline replay, section search, candidate + scoring, visualization, and output. +- `TinyHyperGraphSectionPipelineSolver2` now instantiates + `TinyHyperGraphSectionSolver2`; it no longer imports or creates the legacy + section solver. +- Section candidate-family helpers now live under `lib2` and use lib2 topology + types. +- Fixed a lib2 section-search timeout bug: incomplete fixed-only section state + is rejected and the public section solver falls back to the baseline solution + instead of treating missing active routes as an optimized candidate. +- This bug appears to be inherited from legacy `lib/section-solver/index.ts`. + The current fix is only in `lib2/section-solver.ts`; legacy `lib` still + needs the same fallback behavior if it remains supported. + +Verification after section ownership: + +- `bun run typecheck`: passed. +- `bun test tests/lib2`: 10 pass, 0 fail. +- `bun test`: 100 pass, 0 fail, 11.16s. +- `./benchmark.sh --limit 10 --concurrency 6 --solver lib2`: 10/10 + success, avg duration 0.053s, p50 0.025s, p95 0.171s, avg final max + region cost 0.170. +- `./benchmark-srj13.sh --limit 5 --solver lib2`: 40.0% success, avg + duration 8.091s, avg iterations 1,000,000.0, avg solved max region cost + 519.897. +- `./benchmark2.sh --solver lib2`: solved CM5IO in 47.470s, routeCount 158, + regionCount 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. + +## Lib2 Independence Cut + +- Moved lib2-owned domain types into `lib2/domain.ts` and primitive graph/cache + types into `lib2/types.ts`. +- Moved `computeRegionCost`, `MinHeap`, `shuffle`, and `range` into lib2-owned + modules. +- Moved serialized graph load/output adapters into `lib2/graph-load.ts` and + `lib2/graph-output.ts`. +- Removed lib2 runtime and test imports from `lib/core`, `lib/types`, + `lib/computeRegionCost`, `lib/MinHeap`, `lib/shuffle`, `lib/utils`, + `lib/compat/loadSerializedHyperGraph`, and + `lib/compat/convertToSerializedHyperGraph`. +- Updated lib2-capable benchmarks so `--solver lib2` uses the lib2 graph loader + while legacy/core branches still use legacy adapters. +- Made `lib/solver-view.ts` structural around the fields visualizers and + serializers actually read, avoiding nominal coupling through private heap + fields. + +Verification after independence cut: + +- `bun run typecheck`: passed. +- `bun test tests/lib2`: 12 pass, 0 fail. +- `bun test`: 102 pass, 0 fail, 13.31s. +- `./benchmark.sh --limit 10 --concurrency 6 --solver lib2`: 10/10 success, + avg duration 0.054s, p50 0.028s, p95 0.174s, avg final max region cost + 0.170. +- `./benchmark-srj13.sh --limit 5 --solver lib2`: 40.0% success, avg + duration 8.302s, avg iterations 1,000,000.0, avg solved max region cost + 519.897. +- `./benchmark2.sh --solver lib2`: solved CM5IO in 47.780s, routeCount 158, + regionCount 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. diff --git a/scripts/benchmarking/benchmark.ts b/scripts/benchmarking/benchmark.ts index 0afbead..f52f069 100644 --- a/scripts/benchmarking/benchmark.ts +++ b/scripts/benchmarking/benchmark.ts @@ -9,6 +9,7 @@ import { availableParallelism } from "node:os" import path from "node:path" import { fileURLToPath } from "node:url" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" +import { loadSerializedHyperGraph as loadSerializedHyperGraph2 } from "../../lib2/graph-load" import { ALL_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, DEFAULT_TINY_HYPERGRAPH_SECTION_CANDIDATE_FAMILIES, @@ -20,6 +21,10 @@ import { type TinyHyperGraphSolver, loadSerializedHyperGraphAsPoly, } from "../../lib/index" +import { + TinyHyperGraphSectionPipelineSolver2, + TinyHyperGraphSectionSolver2, +} from "../../lib2/index" type DatasetModule = Record & { manifest: { @@ -81,7 +86,7 @@ type BenchmarkReport = { samples: BenchmarkSampleResult[] } -type SolverVariant = "core" | "poly" +type SolverVariant = "core" | "lib2" | "poly" type DatasetKey = "hg07" | "srj18" const IMPROVEMENT_EPSILON = 1e-9 @@ -94,7 +99,7 @@ Options: --dataset NAME Dataset to run: hg07 or 18/srj18. Defaults to hg07. --limit N Run the first N samples from the dataset. --sample NUM Run a specific sample by number or name (e.g. 2, 002, sample002). - --solver NAME Solver variant: core or poly. Defaults to core. + --solver NAME Solver variant: core, lib2, or poly. Defaults to core. --concurrency N Benchmark concurrency value, or "auto". Defaults to BENCHMARK_CONCURRENCY or CPU count. --families LIST Override candidate families. Use a preset (default, default+deep, all) or a comma-separated list such as self-touch,onehop-all,twohop-touch. @@ -276,11 +281,12 @@ const parseArgs = () => { if (arg === "--solver") { const rawValue = process.argv[index + 1] - if (rawValue !== "core" && rawValue !== "poly") { + if (rawValue !== "core" && rawValue !== "lib2" && rawValue !== "poly") { usageError(`Invalid --solver value: ${rawValue ?? ""}`) } - solverVariant = rawValue as SolverVariant + solverVariant = + rawValue === "core" ? "core" : rawValue === "lib2" ? "lib2" : "poly" index += 1 continue } @@ -464,8 +470,14 @@ const formatBenchmarkReportText = (report: BenchmarkReport) => { ].join("\n")}\n` } -const getMaxRegionCost = (solver: TinyHyperGraphSolver) => - solver.state.regionIntersectionCaches.reduce( +const getMaxRegionCost = (solver: { + state: { + regionIntersectionCaches: ArrayLike<{ + existingRegionCost: number + }> + } +}) => + Array.from(solver.state.regionIntersectionCaches).reduce( (maxRegionCost, regionIntersectionCache) => Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), 0, @@ -478,12 +490,13 @@ const getSerializedOutputMaxRegionCost = ( const { topology, problem, solution } = solverVariant === "poly" ? loadSerializedHyperGraphAsPoly(serializedHyperGraph) + : solverVariant === "lib2" + ? loadSerializedHyperGraph2(serializedHyperGraph) : loadSerializedHyperGraph(serializedHyperGraph) - const replaySolver = new TinyHyperGraphSectionSolver( - topology, - problem, - solution, - ) + const replaySolver = + solverVariant === "lib2" + ? new TinyHyperGraphSectionSolver2(topology, problem, solution) + : new TinyHyperGraphSectionSolver(topology, problem, solution) return getMaxRegionCost(replaySolver.baselineSolver) } @@ -641,10 +654,13 @@ const getSelectedSamples = ( const toAbsoluteResultPath = (cwd: string, targetPath: string) => path.resolve(cwd, targetPath) +type SnapshotPipelineSolver = { + initialVisualize(): GraphicsObject | null + visualize(): GraphicsObject +} + const getSnapshotPng = async ( - pipelineSolver: - | TinyHyperGraphSectionPipelineSolver - | PolyHyperGraphSectionPipelineSolver, + pipelineSolver: SnapshotPipelineSolver, ): Promise => { const graphics = stackGraphicsHorizontally( [ @@ -688,7 +704,9 @@ const main = async () => { const PipelineSolver = solverVariant === "poly" ? PolyHyperGraphSectionPipelineSolver - : TinyHyperGraphSectionPipelineSolver + : solverVariant === "lib2" + ? TinyHyperGraphSectionPipelineSolver2 + : TinyHyperGraphSectionPipelineSolver console.log( `dataset=${datasetKey} samples=${sampleMetas.length}/${datasetModule.manifest.sampleCount} run=${runName} solver=${solverVariant} families=${candidateFamilies?.join(",") ?? "default"} concurrency=${concurrency}`, diff --git a/scripts/benchmarking/cm5io.ts b/scripts/benchmarking/cm5io.ts index e2b14b2..6015887 100644 --- a/scripts/benchmarking/cm5io.ts +++ b/scripts/benchmarking/cm5io.ts @@ -2,6 +2,8 @@ import type { SerializedHyperGraphPortPointPathingSolverInput } from "../../lib/ import { convertPortPointPathingSolverInputToSerializedHyperGraph } from "../../lib/compat/convertPortPointPathingSolverInputToSerializedHyperGraph" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" import { TinyHyperGraphSolver } from "../../lib/core" +import { loadSerializedHyperGraph as loadSerializedHyperGraph2 } from "../../lib2/graph-load" +import { TinyHyperGraphSolver2 } from "../../lib2/index" const CM5IO_FIXTURE_URL = new URL( "../../tests/fixtures/CM5IO_HyperGraph.json", @@ -16,8 +18,14 @@ const formatMetric = (value: number) => value.toFixed(3) const formatOptionalMetric = (value: number | null) => value === null ? "n/a" : formatMetric(value) -const getMaxRegionCost = (solver: TinyHyperGraphSolver) => - solver.state.regionIntersectionCaches.reduce( +const getMaxRegionCost = (solver: { + state: { + regionIntersectionCaches: ArrayLike<{ + existingRegionCost: number + }> + } +}) => + Array.from(solver.state.regionIntersectionCaches).reduce( (maxRegionCost, regionIntersectionCache) => Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), 0, @@ -30,6 +38,9 @@ const average = (values: number[]) => const parseArgs = () => ({ verbose: process.argv.includes("--verbose") || process.argv.includes("-v"), + solverVariant: process.argv.includes("--solver") + ? process.argv[process.argv.indexOf("--solver") + 1] + : "core", }) class BenchmarkTinyHyperGraphSolver extends TinyHyperGraphSolver { @@ -57,14 +68,46 @@ class BenchmarkTinyHyperGraphSolver extends TinyHyperGraphSolver { } const main = async () => { - const { verbose } = parseArgs() + const { verbose, solverVariant } = parseArgs() + if (solverVariant !== "core" && solverVariant !== "lib2") { + throw new Error(`Invalid --solver value: ${solverVariant ?? ""}`) + } const input = (await Bun.file( CM5IO_FIXTURE_URL, ).json()) as SerializedHyperGraphPortPointPathingSolverInput const serializedHyperGraph = convertPortPointPathingSolverInputToSerializedHyperGraph(input) - const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - const solver = new BenchmarkTinyHyperGraphSolver(topology, problem, { + const { topology, problem } = + solverVariant === "lib2" + ? loadSerializedHyperGraph2(serializedHyperGraph) + : loadSerializedHyperGraph(serializedHyperGraph) + const Solver = + solverVariant === "lib2" + ? class BenchmarkTinyHyperGraphSolver2 extends TinyHyperGraphSolver2 { + maxRegionCostsBeforeRip: number[] = [] + + override onAllRoutesRouted() { + const previousRipCount = this.state.ripCount + const maxRegionCostBeforeRip = getMaxRegionCost(this) + super.onAllRoutesRouted() + + if (this.state.ripCount > previousRipCount) { + this.maxRegionCostsBeforeRip.push(maxRegionCostBeforeRip) + } + } + + override onOutOfCandidates() { + const previousRipCount = this.state.ripCount + const maxRegionCostBeforeRip = getMaxRegionCost(this) + super.onOutOfCandidates() + + if (this.state.ripCount > previousRipCount) { + this.maxRegionCostsBeforeRip.push(maxRegionCostBeforeRip) + } + } + } + : BenchmarkTinyHyperGraphSolver + const solver = new Solver(topology, problem, { MAX_ITERATIONS, VERBOSE: verbose, }) @@ -79,7 +122,7 @@ const main = async () => { "TinyHyperGraphSolver", ) ?? null - console.log("benchmark=cm5io solver=TinyHyperGraphSolver") + console.log(`benchmark=cm5io solver=${solverVariant}`) console.log( [ `maxIterations=${MAX_ITERATIONS}`, diff --git a/scripts/benchmarking/srj13-core.ts b/scripts/benchmarking/srj13-core.ts index 960d02f..a794a21 100644 --- a/scripts/benchmarking/srj13-core.ts +++ b/scripts/benchmarking/srj13-core.ts @@ -8,6 +8,8 @@ import { type TinyHyperGraphSolverOptions, } from "../../lib/index" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" +import { loadSerializedHyperGraph as loadSerializedHyperGraph2 } from "../../lib2/graph-load" +import { TinyHyperGraphSolver2 } from "../../lib2/index" type Srj13BenchmarkSample = { sampleName: string @@ -28,6 +30,8 @@ type BenchmarkResult = { error: string | null } +type SolverVariant = "core" | "lib2" + const HELP_TEXT = `Usage: ./benchmark-srj13.sh [options] Run the SRJ13 tiny-hypergraph benchmark against TinyHyperGraphSolver directly. @@ -37,12 +41,14 @@ 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). --max-iterations N Override the core solver iteration cap. Defaults to 1000000. + --solver NAME Solver variant: core or lib2. Defaults to core. --strict Exit non-zero if any sample fails. --help Show this help text. Examples: ./benchmark-srj13.sh ./benchmark-srj13.sh --limit 3 + ./benchmark-srj13.sh --limit 3 --solver lib2 ./benchmark-srj13.sh --sample example-02 ./benchmark-srj13.sh --max-iterations 250000 ` @@ -81,6 +87,7 @@ const parseArgs = () => { let limit: number | null = null let sampleName: string | null = null let maxIterations = 1_000_000 + let solverVariant: SolverVariant = "core" let strict = false for (let index = 0; index < process.argv.length; index += 1) { @@ -119,6 +126,17 @@ const parseArgs = () => { continue } + if (arg === "--solver") { + const rawValue = process.argv[index + 1] + if (rawValue !== "core" && rawValue !== "lib2") { + usageError(`Invalid --solver value: ${rawValue ?? ""}`) + } + + solverVariant = rawValue === "core" ? "core" : "lib2" + index += 1 + continue + } + if (index >= 2 && arg.startsWith("-")) { usageError(`Unknown option: ${arg}`) } @@ -128,7 +146,7 @@ const parseArgs = () => { usageError("Use either --limit or --sample, not both") } - return { limit, sampleName, maxIterations, strict } + return { limit, sampleName, maxIterations, solverVariant, strict } } const formatSeconds = (durationMs: number) => @@ -221,12 +239,18 @@ const getSolverOptions = ( const runSample = ( sample: Srj13BenchmarkSample, maxIterations: number, + solverVariant: SolverVariant, ): BenchmarkResult => { const startTime = performance.now() const benchmarkCase = sample.tinyHypergraphBenchmark const serializedHyperGraph = normalizeSerializedHyperGraph(benchmarkCase) - const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - const solver = new TinyHyperGraphSolver( + const { topology, problem } = + solverVariant === "lib2" + ? loadSerializedHyperGraph2(serializedHyperGraph) + : loadSerializedHyperGraph(serializedHyperGraph) + const Solver = + solverVariant === "lib2" ? TinyHyperGraphSolver2 : TinyHyperGraphSolver + const solver = new Solver( topology, problem, getSolverOptions(benchmarkCase, maxIterations), @@ -276,17 +300,18 @@ const runSample = ( } const main = () => { - const { limit, sampleName, maxIterations, strict } = parseArgs() + const { limit, sampleName, maxIterations, solverVariant, 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=${solverVariant} samples=${selectedSamples.length}/${allSamples.length} maxIterations=${maxIterations}`, ) for (const sample of selectedSamples) { - const result = runSample(sample, maxIterations) + const result = runSample(sample, maxIterations, solverVariant) results.push(result) console.log( diff --git a/tests/lib2/min-heap.test.ts b/tests/lib2/min-heap.test.ts new file mode 100644 index 0000000..25e7849 --- /dev/null +++ b/tests/lib2/min-heap.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "bun:test" +import { MinHeap } from "lib2/min-heap" + +test("lib2 heap pops items in ascending order", () => { + const heap = new MinHeap([], (left, right) => left - right) + + heap.queue(4) + heap.queue(1) + heap.queue(3) + heap.queue(2) + + expect(heap.dequeue()).toBe(1) + expect(heap.dequeue()).toBe(2) + expect(heap.dequeue()).toBe(3) + expect(heap.dequeue()).toBe(4) + expect(heap.dequeue()).toBeUndefined() +}) + +test("lib2 heap clear empties the backing array", () => { + const heap = new MinHeap([], (left, right) => left - right) + + heap.queue(2) + heap.queue(1) + heap.clear() + + expect(heap.length).toBe(0) + expect(heap.toArray()).toEqual([]) +}) diff --git a/tests/lib2/region-cache.test.ts b/tests/lib2/region-cache.test.ts new file mode 100644 index 0000000..f3530bc --- /dev/null +++ b/tests/lib2/region-cache.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test" +import { + countNewIntersectionsWithValues, + createDynamicAnglePairArrays, +} from "lib/countNewIntersections" +import type { DynamicAnglePair } from "lib2/types" +import { createEmptyCache, MutableRegionCache } from "lib2/region-cache" +import type { SegmentGeometry } from "lib2/segment-geometry" + +const toGeometry = (pair: DynamicAnglePair): SegmentGeometry => ({ + lesserAngle: pair[1], + greaterAngle: pair[3], + layerMask: (1 << pair[2]) | (1 << pair[4]), + entryExitLayerChanges: pair[2] !== pair[4] ? 1 : 0, +}) + +const computeTestCost = ( + sameLayerIntersections: number, + crossingLayerIntersections: number, + entryExitLayerChanges: number, + segmentCount: number, +) => + sameLayerIntersections * 100 + + crossingLayerIntersections * 10 + + entryExitLayerChanges + + segmentCount / 100 + +test("mutable region cache matches legacy incremental intersection counting", () => { + const pairs: DynamicAnglePair[] = [ + [1, 1000, 0, 7000, 0], + [2, 3000, 0, 9000, 1], + [3, 2000, 2, 8000, 2], + [1, 4000, 1, 6000, 1], + ] + const cache = MutableRegionCache.from(createEmptyCache()) + const existingPairs: DynamicAnglePair[] = [] + + for (const pair of pairs) { + const geometry = toGeometry(pair) + const delta = cache.countDelta(pair[0], geometry) + const legacyDelta = countNewIntersectionsWithValues( + createDynamicAnglePairArrays(existingPairs), + pair[0], + geometry.lesserAngle, + geometry.greaterAngle, + geometry.layerMask, + geometry.entryExitLayerChanges, + ) + + expect(delta).toEqual({ + sameLayerIntersections: legacyDelta[0], + crossingLayerIntersections: legacyDelta[1], + entryExitLayerChanges: legacyDelta[2], + }) + + const publicCache = cache.append(pair[0], geometry, delta, computeTestCost) + existingPairs.push(pair) + + expect(publicCache.netIds).toHaveLength(existingPairs.length) + expect(publicCache.lesserAngles).toHaveLength(existingPairs.length) + expect(publicCache.greaterAngles).toHaveLength(existingPairs.length) + expect(publicCache.layerMasks).toHaveLength(existingPairs.length) + expect(publicCache.existingSegmentCount).toBe(existingPairs.length) + expect(publicCache.existingRegionCost).toBe( + computeTestCost( + publicCache.existingSameLayerIntersections, + publicCache.existingCrossingLayerIntersections, + publicCache.existingEntryExitLayerChanges, + publicCache.existingSegmentCount, + ), + ) + } +}) diff --git a/tests/lib2/route-cost.test.ts b/tests/lib2/route-cost.test.ts new file mode 100644 index 0000000..a85b4ff --- /dev/null +++ b/tests/lib2/route-cost.test.ts @@ -0,0 +1,67 @@ +import { expect, test } from "bun:test" +import type { Candidate } from "lib2/domain" +import { createEmptyCache, MutableRegionCache } from "lib2/region-cache" +import { computeRouteG } from "lib2/route-cost" +import type { SegmentGeometry } from "lib2/segment-geometry" + +const currentCandidate: Candidate = { + nextRegionId: 0, + portId: 0, + g: 7, + h: 0, + f: 7, +} + +const baseGeometry: SegmentGeometry = { + lesserAngle: 1000, + greaterAngle: 7000, + layerMask: 1, + entryExitLayerChanges: 0, +} + +const crossingGeometry: SegmentGeometry = { + lesserAngle: 3000, + greaterAngle: 9000, + layerMask: 1, + entryExitLayerChanges: 0, +} + +test("computeRouteG rejects same-layer crossings in known single-layer regions", () => { + const cache = MutableRegionCache.from(createEmptyCache()) + const delta = cache.countDelta(1, baseGeometry) + cache.append(1, baseGeometry, delta, () => 0) + + const routeCost = computeRouteG({ + currentCandidate, + neighborPortId: 1, + routeNetId: 2, + regionCache: cache, + regionCongestionCost: 3, + portPenalty: 5, + segmentGeometry: crossingGeometry, + isKnownSingleLayerRegion: true, + computeRegionCost: () => 0, + }) + + expect(routeCost).toBe(Number.POSITIVE_INFINITY) +}) + +test("computeRouteG adds region cost delta, congestion, and port penalty", () => { + const cache = MutableRegionCache.from(createEmptyCache()) + const delta = cache.countDelta(1, baseGeometry) + cache.append(1, baseGeometry, delta, () => 0) + + const routeCost = computeRouteG({ + currentCandidate, + neighborPortId: 1, + routeNetId: 2, + regionCache: cache, + regionCongestionCost: 3, + portPenalty: 5, + segmentGeometry: crossingGeometry, + isKnownSingleLayerRegion: false, + computeRegionCost: (sameLayerIntersections) => sameLayerIntersections * 11, + }) + + expect(routeCost).toBe(26) +}) diff --git a/tests/lib2/route-search.test.ts b/tests/lib2/route-search.test.ts new file mode 100644 index 0000000..f032052 --- /dev/null +++ b/tests/lib2/route-search.test.ts @@ -0,0 +1,88 @@ +import { expect, test } from "bun:test" +import type { + Candidate, + TinyHyperGraphProblem, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "lib2/domain" +import { MinHeap } from "lib2/min-heap" +import { runRouteSearchStep } from "lib2/route-search" + +const compareCandidate = (left: Candidate, right: Candidate) => + left.f - right.f + +test("route search accepts a goal neighbor before route-cost checks", () => { + const topology: TinyHyperGraphTopology = { + portCount: 2, + regionCount: 1, + regionIncidentPorts: [[0, 1]], + incidentPortRegion: [[0], [0]], + regionWidth: new Float64Array([1]), + regionHeight: new Float64Array([1]), + regionCenterX: new Float64Array([0]), + regionCenterY: new Float64Array([0]), + portAngleForRegion1: new Int32Array([0, 9000]), + portX: new Float64Array([0, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + } + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array([1, 0]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([2]), + regionNetId: new Int32Array([-1]), + } + const state: TinyHyperGraphWorkingState = { + portAssignment: new Int32Array([-1, -1]), + regionSegments: [[]], + regionIntersectionCaches: [], + currentRouteNetId: undefined, + currentRouteId: undefined, + unroutedRoutes: [0], + candidateQueue: new MinHeap([], compareCandidate), + candidateBestCostByHopId: new Float64Array(2), + candidateBestCostGenerationByHopId: new Uint32Array(2), + candidateBestCostGeneration: 1, + goalPortId: -1, + ripCount: 0, + regionCongestionCost: new Float64Array([0]), + } + const bestCostByHopId = new Map() + let routeAttemptCount = 0 + let routeCostWasRead = false + + const result = runRouteSearchStep({ + topology, + problem, + state, + getHopId: (portId, nextRegionId) => portId * 10 + nextRegionId, + getCandidateBestCost: (hopId) => + bestCostByHopId.get(hopId) ?? Number.POSITIVE_INFINITY, + setCandidateBestCost: (hopId, cost) => { + bestCostByHopId.set(hopId, cost) + }, + resetCandidateBestCosts: () => { + bestCostByHopId.clear() + }, + getStartingNextRegionId: () => 0, + isPortReservedForDifferentNet: () => false, + isRegionReservedForDifferentNet: () => false, + computeG: () => { + routeCostWasRead = true + return Number.POSITIVE_INFINITY + }, + computeH: () => 0, + onRouteAttempt: () => { + routeAttemptCount += 1 + }, + }) + + expect(typeof result).toBe("object") + if (typeof result === "object" && !("_tag" in result)) { + expect(result.portId).toBe(0) + } + expect(routeAttemptCount).toBe(1) + expect(routeCostWasRead).toBe(false) +}) diff --git a/tests/lib2/section-solver2.test.ts b/tests/lib2/section-solver2.test.ts new file mode 100644 index 0000000..67c928b --- /dev/null +++ b/tests/lib2/section-solver2.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from "bun:test" +import * as datasetHg07 from "dataset-hg07" +import { loadSerializedHyperGraph } from "lib2/graph-load" +import { + TinyHyperGraphSectionSolver2, + type TinyHyperGraphSolver2, +} from "lib2/index" +import { createSample002SectionPortMask } from "tests/fixtures/sample002-section.fixture" +import { + createSectionSolverFixturePortMask, + sectionSolverFixtureGraph, +} from "tests/fixtures/section-solver.fixture" + +const getMaxRegionCost = (solver: TinyHyperGraphSolver2) => + solver.state.regionIntersectionCaches.reduce( + (maxRegionCost, regionIntersectionCache) => + Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), + 0, + ) + +test("section solver 2 improves the max region cost on hg07 sample002", () => { + const { topology, problem, solution } = loadSerializedHyperGraph( + datasetHg07.sample002, + ) + problem.portSectionMask = createSample002SectionPortMask(topology) + + const sectionSolver = new TinyHyperGraphSectionSolver2( + topology, + problem, + solution, + ) + + sectionSolver.solve() + + expect(sectionSolver.solved).toBe(true) + expect(sectionSolver.failed).toBe(false) + expect(sectionSolver.activeRouteIds.length).toBeGreaterThan(0) + expect(sectionSolver.stats.optimized).toBe(true) + + const initialMaxRegionCost = getMaxRegionCost(sectionSolver.baselineSolver) + const finalMaxRegionCost = getMaxRegionCost(sectionSolver.getSolvedSolver()) + + expect(finalMaxRegionCost).toBeLessThan(initialMaxRegionCost) + expect(initialMaxRegionCost - finalMaxRegionCost).toBeGreaterThan(0.5) +}) + +test("section solver 2 output preserves optimized max region cost", () => { + const { topology, problem, solution } = loadSerializedHyperGraph( + datasetHg07.sample002, + ) + problem.portSectionMask = createSample002SectionPortMask(topology) + + const sectionSolver = new TinyHyperGraphSectionSolver2( + topology, + problem, + solution, + ) + + sectionSolver.solve() + + expect(sectionSolver.solved).toBe(true) + expect(sectionSolver.failed).toBe(false) + expect(sectionSolver.stats.optimized).toBe(true) + + const optimizedMaxRegionCost = getMaxRegionCost( + sectionSolver.getSolvedSolver(), + ) + const replay = loadSerializedHyperGraph(sectionSolver.getOutput()) + const replayedSolver = new TinyHyperGraphSectionSolver2( + replay.topology, + replay.problem, + replay.solution, + ) + + expect(getMaxRegionCost(replayedSolver.baselineSolver)).toBeCloseTo( + optimizedMaxRegionCost, + 10, + ) +}) + +test("section solver 2 rejects incomplete timeout candidates", () => { + const { topology, problem, solution } = loadSerializedHyperGraph( + sectionSolverFixtureGraph, + ) + problem.portSectionMask = createSectionSolverFixturePortMask(topology) + + const sectionSolver = new TinyHyperGraphSectionSolver2( + topology, + problem, + solution, + { + MAX_ITERATIONS: 1, + }, + ) + + sectionSolver.solve() + + expect(sectionSolver.solved).toBe(true) + expect(sectionSolver.failed).toBe(false) + expect(sectionSolver.getSolvedSolver()).toBe(sectionSolver.baselineSolver) + expect(sectionSolver.stats.sectionSearchFailedFallbackToBaseline).toBe(true) + expect( + sectionSolver.stats.rejectedIncompleteSectionStateOnTimeout, + ).toBe(true) +}) diff --git a/tests/lib2/solver2.test.ts b/tests/lib2/solver2.test.ts new file mode 100644 index 0000000..d119d74 --- /dev/null +++ b/tests/lib2/solver2.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import * as datasetHg07 from "dataset-hg07" +import { loadSerializedHyperGraph } from "lib2/graph-load" +import { TinyHyperGraphSolver } from "lib/index" +import { ParseGraphError, TinyHyperGraphSolver2, solveGraph } from "lib2/index" + +const getMaxRegionCost = ( + solver: TinyHyperGraphSolver | TinyHyperGraphSolver2, +) => + solver.state.regionIntersectionCaches.reduce( + (maxRegionCost, regionIntersectionCache) => + Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), + 0, + ) + +test("lib2 solveGraph returns a typed parse error for invalid input", () => { + const result = solveGraph({ regions: [] }) + + expect(result._tag).toBe("err") + if (result._tag === "err") { + expect(result.error).toBeInstanceOf(ParseGraphError) + expect(result.error.message).toBe( + "Invalid serialized graph: expected ports array", + ) + } +}) + +test("lib2 solveGraph solves and serializes hg07 sample002", () => { + const serializedGraph = datasetHg07.sample002 as SerializedHyperGraph + const { topology, problem } = loadSerializedHyperGraph(serializedGraph) + const result = solveGraph(serializedGraph) + + expect(result._tag).toBe("ok") + if (result._tag === "ok") { + expect(result.value.solver.solved).toBe(true) + expect(result.value.solver.failed).toBe(false) + expect(result.value.graph.regions).toHaveLength(topology.regionCount) + expect(result.value.graph.ports).toHaveLength(topology.portCount) + expect(result.value.graph.solvedRoutes).toHaveLength(problem.routeCount) + } +}) + +test("lib2 solver facade matches core solver on hg07 sample002", () => { + const serializedGraph = datasetHg07.sample002 as SerializedHyperGraph + const { topology, problem } = loadSerializedHyperGraph(serializedGraph) + const coreSolver = new TinyHyperGraphSolver(topology, problem) + const lib2Solver = new TinyHyperGraphSolver2(topology, problem) + + coreSolver.solve() + const lib2Result = lib2Solver.solveResult() + + expect(coreSolver.solved).toBe(true) + expect(coreSolver.failed).toBe(false) + expect(lib2Result._tag).toBe("ok") + expect(lib2Solver.solved).toBe(true) + expect(lib2Solver.failed).toBe(false) + expect(getMaxRegionCost(lib2Solver)).toBeCloseTo( + getMaxRegionCost(coreSolver), + 10, + ) +}) diff --git a/tsconfig.json b/tsconfig.json index ad93792..a7e2888 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "types": ["bun-types"], "paths": { - "lib/*": ["./lib/*"] + "lib/*": ["./lib/*"], + "lib2/*": ["./lib2/*"] }, // Bundler mode @@ -35,6 +36,7 @@ "include": [ "cosmos.decorator.tsx", "lib/**/*.ts", + "lib2/**/*.ts", "pages/**/*.tsx", "scripts/**/*.ts", "tests/**/*.ts", From a3f7bb85bd6a33649c4238e90b7df35646c953e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Sun, 28 Jun 2026 02:57:18 +0530 Subject: [PATCH 2/3] added error for anyfallback case --- lib2/graph-load.ts | 92 +++++++++++++++----- lib2/graph-output.ts | 72 +++++++++++----- lib2/index.ts | 8 +- lib2/route-search.ts | 17 ++++ lib2/section-pipeline.ts | 53 ++++++++++-- lib2/section-solver.ts | 90 +++++++++++++++----- lib2/segment-geometry.ts | 49 ++++++++--- lib2/solver.ts | 108 ++++++++++++++++++++---- progress.md | 36 ++++++++ tests/lib2/route-cost.test.ts | 38 ++++++++- tests/lib2/route-search.test.ts | 61 ++++++++++++++ tests/lib2/section-solver2.test.ts | 50 +++++++++++ tests/lib2/solver2.test.ts | 129 ++++++++++++++++++++++++++++- 13 files changed, 704 insertions(+), 99 deletions(-) diff --git a/lib2/graph-load.ts b/lib2/graph-load.ts index 8c8d21b..51a004d 100644 --- a/lib2/graph-load.ts +++ b/lib2/graph-load.ts @@ -6,6 +6,14 @@ import type { } from "./domain" import { getAvailableZFromMask, getZLayerLabel } from "../lib/layerLabels" +export class SerializedGraphLoadInvariantError extends Error { + readonly _tag = "SerializedGraphLoadInvariantError" + + constructor(readonly reason: string) { + super(`Invalid serialized graph load invariant: ${reason}`) + } +} + const getSerializedRegionNetId = ( region: SerializedHyperGraph["regions"][number], ) => { @@ -68,7 +76,7 @@ const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { ) if (invalidConnection) { - throw new Error( + throw new SerializedGraphLoadInvariantError( `Connection "${invalidConnection.connectionId}" references full-obstacle region`, ) } @@ -126,7 +134,13 @@ const addSerializedPortIdToMetadata = ( const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { const bounds = region.d?.bounds - if (bounds) { + if ( + bounds && + Number.isFinite(bounds.minX) && + Number.isFinite(bounds.maxX) && + Number.isFinite(bounds.minY) && + Number.isFinite(bounds.maxY) + ) { return bounds } @@ -148,12 +162,9 @@ const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { } } - return { - minX: 0, - maxX: 0, - minY: 0, - maxY: 0, - } + throw new SerializedGraphLoadInvariantError( + `region "${region.regionId}" is missing finite bounds or center/width/height geometry`, + ) } const getRegionGeometry = ( @@ -194,7 +205,9 @@ const getRegionAvailableZMask = ( let mask = 0 for (const z of availableZ) { if (!Number.isInteger(z) || z < 0 || z >= 31) { - continue + throw new SerializedGraphLoadInvariantError( + `region "${region.regionId}" has invalid availableZ value ${String(z)}`, + ) } mask |= 1 << z } @@ -205,23 +218,51 @@ const getRegionAvailableZMask = ( const getSerializedPortZ = ( port: SerializedHyperGraph["ports"][number], ): number => { - const z = Number(port.d?.z ?? 0) - return Number.isFinite(z) ? z : 0 + const z = Number(port.d?.z) + if (!Number.isFinite(z)) { + throw new SerializedGraphLoadInvariantError( + `port "${port.portId}" is missing a finite z coordinate`, + ) + } + + return z } const getSerializedPortX = ( port: SerializedHyperGraph["ports"][number], -): number => Number(port.d?.x ?? 0) +): number => { + const x = Number(port.d?.x) + if (!Number.isFinite(x)) { + throw new SerializedGraphLoadInvariantError( + `port "${port.portId}" is missing a finite x coordinate`, + ) + } + + return x +} const getSerializedPortY = ( port: SerializedHyperGraph["ports"][number], -): number => Number(port.d?.y ?? 0) +): number => { + const y = Number(port.d?.y) + if (!Number.isFinite(y)) { + throw new SerializedGraphLoadInvariantError( + `port "${port.portId}" is missing a finite y coordinate`, + ) + } + + return y +} const computePortAngle = ( port: SerializedHyperGraph["ports"][number], region: SerializedHyperGraph["regions"][number] | undefined, ): number => { - if (!region) return 0 + if (!region) { + throw new SerializedGraphLoadInvariantError( + `port "${port.portId}" cannot compute angle for missing region`, + ) + } const bounds = getRegionBounds(region) const x = getSerializedPortX(port) @@ -400,7 +441,7 @@ export const loadSerializedHyperGraph = ( const region2Index = regionIdToIndex.get(port.region2Id) if (region1Index === undefined || region2Index === undefined) { - throw new Error( + throw new SerializedGraphLoadInvariantError( `Port "${port.portId}" references missing regions "${port.region1Id}" or "${port.region2Id}"`, ) } @@ -460,7 +501,9 @@ export const loadSerializedHyperGraph = ( const assignRegionNet = (regionId: string, netIndex: number) => { const regionIndex = regionIdToIndex.get(regionId) if (regionIndex === undefined) { - throw new Error(`Connection references missing region "${regionId}"`) + throw new SerializedGraphLoadInvariantError( + `Connection references missing region "${regionId}"`, + ) } regionNetCandidates[regionIndex]!.add(netIndex) @@ -532,7 +575,7 @@ export const loadSerializedHyperGraph = ( endPortId !== undefined ? portIdToIndex.get(endPortId) : undefined if (startPortIndex === undefined || endPortIndex === undefined) { - throw new Error( + throw new SerializedGraphLoadInvariantError( `Connection "${connection.connectionId}" could not be mapped to route endpoints`, ) } @@ -599,7 +642,9 @@ export const loadSerializedHyperGraph = ( toPortId !== undefined ? portIdToIndex.get(toPortId) : undefined if (fromPortIndex === undefined || toPortIndex === undefined) { - continue + throw new SerializedGraphLoadInvariantError( + `solved route "${route.connection.connectionId}" references missing segment ports "${String(fromPortId)}" and "${String(toPortId)}"`, + ) } const serializedRegionId = @@ -612,7 +657,16 @@ export const loadSerializedHyperGraph = ( segments.push([fromPortIndex, toPortIndex]) segmentRegionIds.push( serializedRegionId !== undefined - ? regionIdToIndex.get(serializedRegionId) + ? (() => { + const regionIndex = regionIdToIndex.get(serializedRegionId) + if (regionIndex === undefined) { + throw new SerializedGraphLoadInvariantError( + `solved route "${route.connection.connectionId}" references missing region "${serializedRegionId}"`, + ) + } + + return regionIndex + })() : undefined, ) } diff --git a/lib2/graph-output.ts b/lib2/graph-output.ts index 4f51622..5c8d26b 100644 --- a/lib2/graph-output.ts +++ b/lib2/graph-output.ts @@ -17,6 +17,14 @@ interface RouteSegment { toPortId: PortId } +export class SerializedGraphOutputInvariantError extends Error { + readonly _tag = "SerializedGraphOutputInvariantError" + + constructor(readonly reason: string) { + super(`Invalid serialized graph output invariant: ${reason}`) + } +} + const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null @@ -48,7 +56,9 @@ const getSerializedRegionId = ( } } - return `region-${regionId}` + throw new SerializedGraphOutputInvariantError( + `region ${regionId} is missing serializedRegionId, regionId, or capacityMeshNodeId metadata`, + ) } const getSerializedPortId = ( @@ -66,7 +76,9 @@ const getSerializedPortId = ( } } - return `port-${portId}` + throw new SerializedGraphOutputInvariantError( + `port ${portId} is missing serializedPortId or portId metadata`, + ) } const getSerializedRegionData = ( @@ -159,17 +171,17 @@ const getOppositeRegionIdForPort = ( traversedRegionId: RegionId, ): RegionId => { const incidentRegionIds = solver.topology.incidentPortRegion[portId] ?? [] - const oppositeRegionId = incidentRegionIds.find( + const oppositeRegionIds = incidentRegionIds.filter( (regionId) => regionId !== traversedRegionId, ) - if (oppositeRegionId === undefined) { - throw new Error( - `Port ${portId} is not incident to a region outside route region ${traversedRegionId}`, + if (oppositeRegionIds.length !== 1) { + throw new SerializedGraphOutputInvariantError( + `port ${portId} has ${oppositeRegionIds.length} opposite regions for route region ${traversedRegionId}`, ) } - return oppositeRegionId + return oppositeRegionIds[0]! } const getOrderedRoutePath = ( @@ -181,7 +193,9 @@ const getOrderedRoutePath = ( orderedRegionIds: RegionId[] } => { if (routeSegments.length === 0) { - throw new Error(`Route ${routeId} has no solved segments`) + throw new SerializedGraphOutputInvariantError( + `route ${routeId} has no solved segments`, + ) } const startPortId = solver.problem.routeStartPort[routeId] @@ -255,7 +269,7 @@ const getOrderedRoutePath = ( new Set([startPortId]), ) ) { - throw new Error( + throw new SerializedGraphOutputInvariantError( `Route ${routeId} is not a single ordered path from ${startPortId} to ${endPortId}`, ) } @@ -273,30 +287,46 @@ const getSerializedConnection = ( endRegionId: string, ): SerializedConnection => { const routeMetadata = solver.problem.routeMetadata?.[routeId] + if (!isRecord(routeMetadata)) { + throw new SerializedGraphOutputInvariantError( + `route ${routeId} is missing route metadata`, + ) + } + const metadataConnectionId = - isRecord(routeMetadata) && typeof routeMetadata.connectionId === "string" + typeof routeMetadata.connectionId === "string" ? routeMetadata.connectionId : undefined const metadataStartRegionId = - isRecord(routeMetadata) && typeof routeMetadata.startRegionId === "string" + typeof routeMetadata.startRegionId === "string" ? routeMetadata.startRegionId : undefined const metadataEndRegionId = - isRecord(routeMetadata) && typeof routeMetadata.endRegionId === "string" + typeof routeMetadata.endRegionId === "string" ? routeMetadata.endRegionId : undefined const metadataNetworkId = - isRecord(routeMetadata) && typeof routeMetadata.mutuallyConnectedNetworkId === "string" ? routeMetadata.mutuallyConnectedNetworkId : undefined + if (!metadataConnectionId) { + throw new SerializedGraphOutputInvariantError( + `route ${routeId} is missing connectionId metadata`, + ) + } + + if (!metadataNetworkId) { + throw new SerializedGraphOutputInvariantError( + `route ${routeId} is missing mutuallyConnectedNetworkId metadata`, + ) + } + return { - connectionId: metadataConnectionId ?? `route-${routeId}`, + connectionId: metadataConnectionId, startRegionId: metadataStartRegionId ?? startRegionId, endRegionId: metadataEndRegionId ?? endRegionId, - mutuallyConnectedNetworkId: - metadataNetworkId ?? `net-${solver.problem.routeNet[routeId]}`, + mutuallyConnectedNetworkId: metadataNetworkId, } } @@ -318,7 +348,9 @@ const getSerializedSolvedRoute = ( const lastRegionId = orderedRegionIds[orderedRegionIds.length - 1] if (firstRegionId === undefined || lastRegionId === undefined) { - throw new Error(`Route ${routeId} could not determine endpoint regions`) + throw new SerializedGraphOutputInvariantError( + `route ${routeId} could not determine endpoint regions`, + ) } const fallbackStartRegionId = getSerializedRegionId( @@ -382,7 +414,7 @@ export const convertToSerializedHyperGraph = ( solver: TinyHyperGraphSolver2View, ): SerializedHyperGraph => { if (!solver.solved || solver.failed) { - throw new Error( + throw new SerializedGraphOutputInvariantError( "convertToSerializedHyperGraph requires a solved, non-failed solver", ) } @@ -405,7 +437,9 @@ export const convertToSerializedHyperGraph = ( const [region1Id, region2Id] = topology.incidentPortRegion[portId] ?? [] if (region1Id === undefined || region2Id === undefined) { - throw new Error(`Port ${portId} is missing incident regions`) + throw new SerializedGraphOutputInvariantError( + `port ${portId} is missing incident regions`, + ) } return { diff --git a/lib2/index.ts b/lib2/index.ts index 5995a0e..f9cef00 100644 --- a/lib2/index.ts +++ b/lib2/index.ts @@ -9,13 +9,18 @@ export { parseGraph, } from "./graph-input" export { loadSerializedHyperGraph } from "./graph-load" -export { convertToSerializedHyperGraph } from "./graph-output" +export { + SerializedGraphOutputInvariantError, + convertToSerializedHyperGraph, +} from "./graph-output" export type * from "./domain" export type * from "./types" export type { Result } from "./prelude" export { capture, err, getErrorMessage, ok } from "./prelude" export { TinyHyperGraphSectionPipelineSolver2 } from "./section-pipeline" export { + InvalidSectionMaskError, + SectionRoutePathError, TinyHyperGraphSectionSolver2, getActiveSectionRouteIds, type TinyHyperGraphSectionSolver2Options, @@ -23,6 +28,7 @@ export { export type { SolvedGraph } from "./solver" export { SolveGraphError, + SolverInvariantError, TinyHyperGraphSolver2, createSolver, solveGraph, diff --git a/lib2/route-search.ts b/lib2/route-search.ts index 20f4a01..b5883d2 100644 --- a/lib2/route-search.ts +++ b/lib2/route-search.ts @@ -124,6 +124,11 @@ export function runRouteSearchStep( } const neighbors = topology.regionIncidentPorts[currentCandidate.nextRegionId] + if (neighbors === undefined) { + return failed( + `Region ${currentCandidate.nextRegionId} is missing incident ports during route search`, + ) + } for (const neighborPortId of neighbors) { const assignedNetId = state.portAssignment[neighborPortId] @@ -158,6 +163,18 @@ export function runRouteSearchStep( const h = runtime.computeH(neighborPortId) const incidentRegions = topology.incidentPortRegion[neighborPortId] + if (incidentRegions === undefined) { + return failed( + `Port ${neighborPortId} is missing incident regions during route search`, + ) + } + + if (!incidentRegions.includes(currentCandidate.nextRegionId)) { + return failed( + `Port ${neighborPortId} is not incident to current region ${currentCandidate.nextRegionId}`, + ) + } + const nextRegionId = incidentRegions[0] === currentCandidate.nextRegionId ? incidentRegions[1] diff --git a/lib2/section-pipeline.ts b/lib2/section-pipeline.ts index 9b9e8b5..ac23990 100644 --- a/lib2/section-pipeline.ts +++ b/lib2/section-pipeline.ts @@ -13,6 +13,7 @@ import { import type { RegionId } from "./types" import { getActiveSectionRouteIds, + InvalidSectionMaskError, TinyHyperGraphSectionSolver2, type TinyHyperGraphSectionSolver2Options, } from "./section-solver" @@ -67,6 +68,14 @@ const DEFAULT_MAX_HOT_REGIONS = 2 const IMPROVEMENT_EPSILON = 1e-9 +class InvalidPortSectionMaskError extends Error { + readonly _tag = "InvalidPortSectionMaskError" + + constructor(reason: string) { + super(`Invalid port section mask: ${reason}`) + } +} + type RegionCostSolverView = { readonly state: Pick } @@ -137,6 +146,32 @@ const createProblemWithPortSectionMask = ( : new Float64Array(problem.portPenalty), }) +const parsePortSectionMask = ( + portSectionMask: Int8Array, + topology: TinyHyperGraphTopology, +): Int8Array => { + if (!(portSectionMask instanceof Int8Array)) { + throw new InvalidPortSectionMaskError("expected Int8Array") + } + + if (portSectionMask.length !== topology.portCount) { + throw new InvalidPortSectionMaskError( + `expected length ${topology.portCount}, got ${portSectionMask.length}`, + ) + } + + for (let portId = 0; portId < portSectionMask.length; portId++) { + const value = portSectionMask[portId] + if (value !== 0 && value !== 1) { + throw new InvalidPortSectionMaskError( + `port ${portId} has value ${value}; expected 0 or 1`, + ) + } + } + + return new Int8Array(portSectionMask) +} + const getSectionMaskCandidates = ( solvedSolver: TinyHyperGraphSolver2, topology: TinyHyperGraphTopology, @@ -280,8 +315,12 @@ const findBestAutomaticSectionMask = ( winningCandidateFamily = candidate.family } } - } catch { - // Skip invalid section masks that split a route into multiple spans. + } catch (error) { + if (!(error instanceof InvalidSectionMaskError)) { + throw error + } + + // Skip candidate masks that split a route into multiple section spans. } } @@ -471,13 +510,15 @@ export class TinyHyperGraphSectionPipelineSolver2 extends BasePipelineSolver value === 1) - .length, + sectionMaskPortCount: [...parsedPortSectionMask].filter( + (value) => value === 1, + ).length, } return [topology, problem, solution, sectionSolverOptions] diff --git a/lib2/section-solver.ts b/lib2/section-solver.ts index a5deb84..678cd96 100644 --- a/lib2/section-solver.ts +++ b/lib2/section-solver.ts @@ -3,6 +3,7 @@ import type { GraphicsObject } from "graphics-debug" import { applyTinyHyperGraphSolver2Options, createEmptyRegionIntersectionCache, + getExistingRegionCost, getTinyHyperGraphSolver2Options, type RegionCostSummary, TinyHyperGraphSolver2, @@ -39,6 +40,28 @@ interface SectionRoutePlan { forcedStartRegionId?: RegionId } +export class SectionRoutePathError extends Error { + readonly _tag = "SectionRoutePathError" + + constructor( + readonly routeId: RouteId, + reason: string, + ) { + super(`Route ${routeId} has an invalid solved path: ${reason}`) + } +} + +export class InvalidSectionMaskError extends Error { + readonly _tag = "InvalidSectionMaskError" + + constructor( + readonly routeId: RouteId, + reason: string, + ) { + super(`Route ${routeId} has an invalid section mask: ${reason}`) + } +} + export interface TinyHyperGraphSectionSolver2Options extends TinyHyperGraphSolver2Options { MAX_RIPS?: number @@ -145,8 +168,7 @@ const summarizeRegionIntersectionCaches = ( regionId < regionIntersectionCaches.length; regionId++ ) { - const regionCost = - regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost(regionIntersectionCaches, regionId) maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost } @@ -165,8 +187,7 @@ const summarizeRegionIntersectionCachesForRegionIds = ( let totalRegionCost = 0 for (const regionId of regionIds) { - const regionCost = - regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost(regionIntersectionCaches, regionId) maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost } @@ -194,8 +215,7 @@ const summarizeRegionIntersectionCachesExcludingRegionIds = ( continue } - const regionCost = - regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost(regionIntersectionCaches, regionId) maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost } @@ -221,18 +241,22 @@ const getSharedRegionIdForPorts = ( topology: TinyHyperGraphTopology, fromPortId: PortId, toPortId: PortId, + routeId: RouteId, ): RegionId => { const fromIncidentRegions = topology.incidentPortRegion[fromPortId] ?? [] const toIncidentRegions = topology.incidentPortRegion[toPortId] ?? [] - const sharedRegionId = fromIncidentRegions.find((regionId) => + const sharedRegionIds = fromIncidentRegions.filter((regionId) => toIncidentRegions.includes(regionId), ) - if (sharedRegionId === undefined) { - throw new Error(`Ports ${fromPortId} and ${toPortId} do not share a region`) + if (sharedRegionIds.length !== 1) { + throw new SectionRoutePathError( + routeId, + `ports ${fromPortId} and ${toPortId} share ${sharedRegionIds.length} regions; explicit solvedRoutePathRegionIds are required`, + ) } - return sharedRegionId + return sharedRegionIds[0]! } const getOrderedRoutePath = ( @@ -258,7 +282,7 @@ const getOrderedRoutePath = ( } } - throw new Error(`Route ${routeId} does not have an existing solved path`) + throw new SectionRoutePathError(routeId, "missing solved path") } const segmentsByPort = new Map< @@ -308,8 +332,9 @@ const getOrderedRoutePath = ( ) if (nextSegments.length !== 1) { - throw new Error( - `Route ${routeId} is not a single ordered path from ${startPortId} to ${endPortId}`, + throw new SectionRoutePathError( + routeId, + `not a single ordered path from ${startPortId} to ${endPortId}`, ) } @@ -326,6 +351,7 @@ const getOrderedRoutePath = ( topology, nextSegment.fromPortId, nextSegment.toPortId, + routeId, ), ) orderedPortIds.push(nextPortId) @@ -334,7 +360,10 @@ const getOrderedRoutePath = ( } if (usedSegmentIndices.size !== routeSegments.length) { - throw new Error(`Route ${routeId} contains disconnected solved segments`) + throw new SectionRoutePathError( + routeId, + "contains disconnected solved segments", + ) } return { @@ -498,8 +527,9 @@ const createSectionRoutePlans = ( } if (maskedRuns.length > 1) { - throw new Error( - `Route ${routeId} enters the section multiple times; only one contiguous section span is currently supported`, + throw new InvalidSectionMaskError( + routeId, + "enters the section multiple times; only one contiguous section span is currently supported", ) } @@ -511,7 +541,10 @@ const createSectionRoutePlans = ( ) if (activeEndIndex <= activeStartIndex) { - throw new Error(`Route ${routeId} does not have a valid section span`) + throw new InvalidSectionMaskError( + routeId, + "does not have a valid section span", + ) } for (let portIndex = 1; portIndex <= activeStartIndex; portIndex++) { @@ -727,8 +760,10 @@ class TinyHyperGraphSectionSearchSolver2 extends TinyHyperGraphSolver2 { mutableRegionIndex++ ) { const regionId = this.mutableRegionIds[mutableRegionIndex]! - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost( + state.regionIntersectionCaches, + regionId, + ) mutableRegionCosts[mutableRegionIndex] = regionCost mutableMaxRegionCost = Math.max(mutableMaxRegionCost, regionCost) mutableTotalRegionCost += regionCost @@ -822,8 +857,10 @@ class TinyHyperGraphSectionSearchSolver2 extends TinyHyperGraphSolver2 { const { state } = this for (const regionId of this.mutableRegionIds) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost( + state.regionIntersectionCaches, + regionId, + ) state.regionCongestionCost[regionId] += regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR } @@ -987,6 +1024,17 @@ export class TinyHyperGraphSectionSolver2 extends BaseSolver { } if (this.sectionSolver.failed) { + if (!this.sectionSolver.stats.rejectedIncompleteSectionStateOnTimeout) { + this.failed = true + this.error = this.sectionSolver.error + this.stats = { + ...this.stats, + unexpectedSectionSearchFailure: true, + sectionSearchError: this.sectionSolver.error, + } + return + } + this.optimizedSolver = this.baselineSolver this.stats = { ...this.stats, diff --git a/lib2/segment-geometry.ts b/lib2/segment-geometry.ts index 2351039..3707252 100644 --- a/lib2/segment-geometry.ts +++ b/lib2/segment-geometry.ts @@ -17,6 +17,14 @@ export type SegmentGeometryScratch = { entryExitLayerChanges: number } +export class SegmentGeometryTopologyError extends Error { + readonly _tag = "SegmentGeometryTopologyError" + + constructor(reason: string) { + super(`Invalid segment geometry topology: ${reason}`) + } +} + /** * Create reusable segment geometry scratch storage. * @@ -31,6 +39,33 @@ export function createSegmentGeometryScratch(): SegmentGeometryScratch { } } +const getPortAngleForRegion = ( + topology: TinyHyperGraphTopology, + regionId: RegionId, + portId: PortId, +): number => { + const portRegions = topology.incidentPortRegion[portId] + + if (portRegions[0] === regionId) { + return topology.portAngleForRegion1[portId] + } + + if (portRegions[1] === regionId) { + const angle = topology.portAngleForRegion2?.[portId] + if (angle === undefined) { + throw new SegmentGeometryTopologyError( + `port ${portId} is missing region-2 angle for region ${regionId}`, + ) + } + + return angle + } + + throw new SegmentGeometryTopologyError( + `port ${portId} is not incident to region ${regionId}`, + ) +} + /** * Read segment geometry for a pair of ports in a region. * @@ -48,18 +83,8 @@ export function readSegmentGeometry( toPortId: PortId, scratch: SegmentGeometryScratch, ): SegmentGeometryScratch { - const fromPortRegions = topology.incidentPortRegion[fromPortId] - const toPortRegions = topology.incidentPortRegion[toPortId] - const fromAngle = - fromPortRegions[0] === regionId || fromPortRegions[1] !== regionId - ? topology.portAngleForRegion1[fromPortId] - : (topology.portAngleForRegion2?.[fromPortId] ?? - topology.portAngleForRegion1[fromPortId]) - const toAngle = - toPortRegions[0] === regionId || toPortRegions[1] !== regionId - ? topology.portAngleForRegion1[toPortId] - : (topology.portAngleForRegion2?.[toPortId] ?? - topology.portAngleForRegion1[toPortId]) + const fromAngle = getPortAngleForRegion(topology, regionId, fromPortId) + const toAngle = getPortAngleForRegion(topology, regionId, toPortId) const fromZ = topology.portZ[fromPortId] const toZ = topology.portZ[toPortId] diff --git a/lib2/solver.ts b/lib2/solver.ts index 208dac3..54d6659 100644 --- a/lib2/solver.ts +++ b/lib2/solver.ts @@ -61,6 +61,37 @@ export type { const GREEDY_FINAL_ROUTE_MAX_ITERATIONS = 50e3 +/** Internal lib2 invariant failure that should not be hidden by display fallbacks. */ +export class SolverInvariantError extends Error { + readonly _tag = "SolverInvariantError" + + constructor( + readonly routeId: RouteId | undefined, + readonly reason: string, + ) { + super( + routeId === undefined + ? `Solver invariant failed: ${reason}` + : `Solver invariant failed for route ${routeId}: ${reason}`, + ) + } +} + +export const getExistingRegionCost = ( + regionIntersectionCaches: ArrayLike, + regionId: RegionId, +): number => { + const cache = regionIntersectionCaches[regionId] + if (cache === undefined) { + throw new SolverInvariantError( + undefined, + `missing region intersection cache for region ${regionId}`, + ) + } + + return cache.existingRegionCost +} + export const createEmptyRegionIntersectionCache = (): RegionIntersectionCache => ({ netIds: new Int32Array(0), @@ -410,7 +441,7 @@ export class TinyHyperGraphSolver2 extends BaseSolver { if (startingNextRegionId === undefined) { this.failed = true - this.error = `Start port ${startingPortId} has no incident regions` + this.error = `Start port ${startingPortId} has no legal starting region` return } @@ -449,6 +480,12 @@ export class TinyHyperGraphSolver2 extends BaseSolver { const neighbors = topology.regionIncidentPorts[currentCandidate.nextRegionId] + if (neighbors === undefined) { + throw new SolverInvariantError( + state.currentRouteId, + `region ${currentCandidate.nextRegionId} is missing incident ports`, + ) + } for (const neighborPortId of neighbors) { const assignedNetId = state.portAssignment[neighborPortId] @@ -469,12 +506,26 @@ export class TinyHyperGraphSolver2 extends BaseSolver { const g = this.computeG(currentCandidate, neighborPortId) if (!Number.isFinite(g)) continue const h = this.computeH(neighborPortId) + const incidentRegions = topology.incidentPortRegion[neighborPortId] + + if (incidentRegions === undefined) { + throw new SolverInvariantError( + state.currentRouteId, + `port ${neighborPortId} is missing incident regions`, + ) + } + + if (!incidentRegions.includes(currentCandidate.nextRegionId)) { + throw new SolverInvariantError( + state.currentRouteId, + `Port ${neighborPortId} is not incident to current region ${currentCandidate.nextRegionId}`, + ) + } const nextRegionId = - topology.incidentPortRegion[neighborPortId][0] === - currentCandidate.nextRegionId - ? topology.incidentPortRegion[neighborPortId][1] - : topology.incidentPortRegion[neighborPortId][0] + incidentRegions[0] === currentCandidate.nextRegionId + ? incidentRegions[1] + : incidentRegions[0] if ( nextRegionId === undefined || @@ -576,8 +627,7 @@ export class TinyHyperGraphSolver2 extends BaseSolver { ) ?? startingIncidentRegions.find( (regionId) => this.problem.regionNetId[regionId] === currentRouteNetId, - ) ?? - startingIncidentRegions[0] + ) ) } @@ -772,8 +822,10 @@ export class TinyHyperGraphSolver2 extends BaseSolver { let maxRegionCost = 0 for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost( + state.regionIntersectionCaches, + regionId, + ) maxRegionCost = Math.max(maxRegionCost, regionCost) } @@ -808,7 +860,9 @@ export class TinyHyperGraphSolver2 extends BaseSolver { protected getRouteConnectionId(routeId: RouteId) { const connectionId = this.getRouteMetadata(routeId)?.connectionId - return typeof connectionId === "string" ? connectionId : `route-${routeId}` + if (typeof connectionId === "string") return connectionId + + throw new SolverInvariantError(routeId, "missing route connectionId metadata") } protected getRouteSummary( @@ -1130,8 +1184,10 @@ export class TinyHyperGraphSolver2 extends BaseSolver { let totalRegionCost = 0 for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost( + state.regionIntersectionCaches, + regionId, + ) regionCosts[regionId] = regionCost maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost @@ -1190,8 +1246,10 @@ export class TinyHyperGraphSolver2 extends BaseSolver { const maxRegionCostBeforeRip = this.getMaxRegionCost() for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCost = getExistingRegionCost( + state.regionIntersectionCaches, + regionId, + ) state.regionCongestionCost[regionId] += regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR } @@ -1346,6 +1404,7 @@ export class TinyHyperGraphSolver2 extends BaseSolver { new SolveGraphError( cause instanceof Error ? cause.message : String(cause), this.stats, + cause, ), ) } @@ -1380,12 +1439,15 @@ class GreedyFinalRouteSolver extends TinyHyperGraphSolver2 { /** Expected failure produced while running a lib2 solve. */ export class SolveGraphError extends Error { readonly _tag = "SolveGraphError" + readonly errorCause: unknown | undefined constructor( readonly reason: string, readonly stats: Record, + errorCause?: unknown, ) { super(`Unable to solve graph: ${reason}`) + this.errorCause = errorCause } } @@ -1442,8 +1504,18 @@ export function solveGraph( return solveResult } - return ok({ - solver, - graph: solver.getOutput(), - }) + try { + return ok({ + solver, + graph: solver.getOutput(), + }) + } catch (cause) { + return err( + new SolveGraphError( + cause instanceof Error ? cause.message : String(cause), + solver.stats, + cause, + ), + ) + } } diff --git a/progress.md b/progress.md index 642be0e..5755c51 100644 --- a/progress.md +++ b/progress.md @@ -135,3 +135,39 @@ Verification after independence cut: 519.897. - `./benchmark2.sh --solver lib2`: solved CM5IO in 47.780s, routeCount 158, regionCount 2538, portCount 17816, ripCount 9, avgMaxRegionBeforeRip 3.633. + +## Lib2 Strict Fallback Pass + +- Added typed lib2 invariant errors for serialized graph loading, serialized graph + output, section route replay, section masks, segment geometry, and solver + route metadata. +- Replaced malformed-input fallbacks with explicit throws for missing finite + region geometry, invalid `availableZ` values, missing finite port coordinates, + missing solved-route ports, missing replay regions, ambiguous replayed section + regions, invalid section masks, non-incident segment geometry, and missing + serialized output identities. +- Replaced hidden internal fallbacks with explicit invariants for missing route + connection IDs, missing route network IDs, missing region cost caches, and + missing topology incident-port arrays. +- Kept expected solve failures as values where the public lib2 boundary already + returns `Result`, including parse/load errors and solve/output errors from + `solveGraph`; wrapped solve errors retain their original `errorCause`. +- Narrowed section pipeline fallback behavior: automatic section masks skip only + typed invalid-mask candidates, and section-search fallback to baseline is only + allowed for the known incomplete-timeout path. +- Kept compatibility paths where absence means "unknown" rather than malformed: + missing region `availableZ`, unresolved serialized region `pointIds`, inferred + solved-route segment regions when no explicit region metadata exists, and + final pipeline fallback to a complete solve output. + +Verification after strict fallback pass: + +- `bun run typecheck`: passed. +- `bun test tests/lib2`: 20 pass, 0 fail. +- `bun test`: 110 pass, 0 fail, 12.33s. +- `./benchmark.sh --limit 10 --concurrency 6 --solver lib2`: 10/10 success, + avg duration 0.051s, p50 0.029s, p95 0.173s, avg final max region cost + 0.170. +- `./benchmark-srj13.sh --limit 5 --solver lib2`: 40.0% success, avg + duration 8.432s, avg iterations 1,000,000.0, avg solved max region cost + 519.897. diff --git a/tests/lib2/route-cost.test.ts b/tests/lib2/route-cost.test.ts index a85b4ff..0e2c802 100644 --- a/tests/lib2/route-cost.test.ts +++ b/tests/lib2/route-cost.test.ts @@ -2,7 +2,11 @@ import { expect, test } from "bun:test" import type { Candidate } from "lib2/domain" import { createEmptyCache, MutableRegionCache } from "lib2/region-cache" import { computeRouteG } from "lib2/route-cost" -import type { SegmentGeometry } from "lib2/segment-geometry" +import { + readSegmentGeometry, + SegmentGeometryTopologyError, + type SegmentGeometry, +} from "lib2/segment-geometry" const currentCandidate: Candidate = { nextRegionId: 0, @@ -65,3 +69,35 @@ test("computeRouteG adds region cost delta, congestion, and port penalty", () => expect(routeCost).toBe(26) }) + +test("readSegmentGeometry rejects ports outside the requested region", () => { + const topology = { + portCount: 2, + regionCount: 2, + regionIncidentPorts: [[0], [1]], + incidentPortRegion: [[0], [1]], + regionWidth: new Float64Array([1, 1]), + regionHeight: new Float64Array([1, 1]), + regionCenterX: new Float64Array([0, 1]), + regionCenterY: new Float64Array([0, 0]), + portAngleForRegion1: new Int32Array([0, 9000]), + portX: new Float64Array([0, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + } + + expect(() => + readSegmentGeometry( + topology, + 0, + 0, + 1, + { + lesserAngle: 0, + greaterAngle: 0, + layerMask: 0, + entryExitLayerChanges: 0, + }, + ), + ).toThrow(SegmentGeometryTopologyError) +}) diff --git a/tests/lib2/route-search.test.ts b/tests/lib2/route-search.test.ts index f032052..9afd160 100644 --- a/tests/lib2/route-search.test.ts +++ b/tests/lib2/route-search.test.ts @@ -86,3 +86,64 @@ test("route search accepts a goal neighbor before route-cost checks", () => { expect(routeAttemptCount).toBe(1) expect(routeCostWasRead).toBe(false) }) + +test("route search reports missing region incident ports as failure", () => { + const topology: TinyHyperGraphTopology = { + portCount: 2, + regionCount: 2, + regionIncidentPorts: [[0, 1]], + incidentPortRegion: [[0], [0]], + regionWidth: new Float64Array([1, 1]), + regionHeight: new Float64Array([1, 1]), + regionCenterX: new Float64Array([0, 1]), + regionCenterY: new Float64Array([0, 0]), + portAngleForRegion1: new Int32Array([0, 9000]), + portX: new Float64Array([0, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + } + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array([1, 1]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([2]), + regionNetId: new Int32Array([-1, -1]), + } + const state: TinyHyperGraphWorkingState = { + portAssignment: new Int32Array([-1, -1]), + regionSegments: [[], []], + regionIntersectionCaches: [], + currentRouteNetId: undefined, + currentRouteId: undefined, + unroutedRoutes: [0], + candidateQueue: new MinHeap([], compareCandidate), + candidateBestCostByHopId: new Float64Array(4), + candidateBestCostGenerationByHopId: new Uint32Array(4), + candidateBestCostGeneration: 1, + goalPortId: -1, + ripCount: 0, + regionCongestionCost: new Float64Array([0, 0]), + } + + const result = runRouteSearchStep({ + topology, + problem, + state, + getHopId: (portId, nextRegionId) => portId * 10 + nextRegionId, + getCandidateBestCost: () => Number.POSITIVE_INFINITY, + setCandidateBestCost: () => {}, + resetCandidateBestCosts: () => {}, + getStartingNextRegionId: () => 1, + isPortReservedForDifferentNet: () => false, + isRegionReservedForDifferentNet: () => false, + computeG: () => 0, + computeH: () => 0, + onRouteAttempt: () => {}, + }) + + expect(result).toEqual({ + _tag: "failed", + error: "Region 1 is missing incident ports during route search", + }) +}) diff --git a/tests/lib2/section-solver2.test.ts b/tests/lib2/section-solver2.test.ts index 67c928b..d834e49 100644 --- a/tests/lib2/section-solver2.test.ts +++ b/tests/lib2/section-solver2.test.ts @@ -2,6 +2,8 @@ import { expect, test } from "bun:test" import * as datasetHg07 from "dataset-hg07" import { loadSerializedHyperGraph } from "lib2/graph-load" import { + SectionRoutePathError, + TinyHyperGraphSectionPipelineSolver2, TinyHyperGraphSectionSolver2, type TinyHyperGraphSolver2, } from "lib2/index" @@ -103,3 +105,51 @@ test("section solver 2 rejects incomplete timeout candidates", () => { sectionSolver.stats.rejectedIncompleteSectionStateOnTimeout, ).toBe(true) }) + +test("section solver 2 rejects ambiguous replayed regions without explicit region ids", () => { + const topology = { + portCount: 2, + regionCount: 2, + regionIncidentPorts: [ + [0, 1], + [0, 1], + ], + incidentPortRegion: [ + [0, 1], + [0, 1], + ], + regionWidth: new Float64Array([1, 1]), + regionHeight: new Float64Array([1, 1]), + regionCenterX: new Float64Array([0, 0]), + regionCenterY: new Float64Array([0, 0]), + portAngleForRegion1: new Int32Array([0, 9000]), + portAngleForRegion2: new Int32Array([18000, 27000]), + portX: new Float64Array([0, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + } + const problem = { + routeCount: 1, + portSectionMask: new Int8Array([1, 1]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array([-1, -1]), + } + const solution = { + solvedRoutePathSegments: [[[0, 1] as [number, number]]], + } + + expect( + () => new TinyHyperGraphSectionSolver2(topology, problem, solution), + ).toThrow(SectionRoutePathError) +}) + +test("section pipeline 2 rejects invalid custom section masks", () => { + const pipelineSolver = new TinyHyperGraphSectionPipelineSolver2({ + serializedHyperGraph: sectionSolverFixtureGraph, + createSectionMask: () => new Int8Array([2]), + }) + + expect(() => pipelineSolver.solve()).toThrow("Invalid port section mask") +}) diff --git a/tests/lib2/solver2.test.ts b/tests/lib2/solver2.test.ts index d119d74..36ac17e 100644 --- a/tests/lib2/solver2.test.ts +++ b/tests/lib2/solver2.test.ts @@ -1,9 +1,19 @@ import { expect, test } from "bun:test" import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import * as datasetHg07 from "dataset-hg07" -import { loadSerializedHyperGraph } from "lib2/graph-load" +import { + loadSerializedHyperGraph, + SerializedGraphLoadInvariantError, +} from "lib2/graph-load" import { TinyHyperGraphSolver } from "lib/index" -import { ParseGraphError, TinyHyperGraphSolver2, solveGraph } from "lib2/index" +import { + ParseGraphError, + SerializedGraphOutputInvariantError, + SolveGraphError, + SolverInvariantError, + TinyHyperGraphSolver2, + solveGraph, +} from "lib2/index" const getMaxRegionCost = ( solver: TinyHyperGraphSolver | TinyHyperGraphSolver2, @@ -60,3 +70,118 @@ test("lib2 solver facade matches core solver on hg07 sample002", () => { 10, ) }) + +test("lib2 solveResult preserves thrown invariant causes", () => { + const serializedGraph = datasetHg07.sample002 as SerializedHyperGraph + const { topology, problem } = loadSerializedHyperGraph(serializedGraph) + const invariantError = new SolverInvariantError(undefined, "forced test") + + class ThrowingSolver extends TinyHyperGraphSolver2 { + override _step() { + throw invariantError + } + } + + const solver = new ThrowingSolver(topology, problem) + const result = solver.solveResult() + + expect(result._tag).toBe("err") + if (result._tag === "err") { + expect(result.error).toBeInstanceOf(SolveGraphError) + if (result.error instanceof SolveGraphError) { + expect(result.error.errorCause).toBe(invariantError) + } + } +}) + +test("lib2 output rejects missing network metadata", () => { + const serializedGraph = datasetHg07.sample002 as SerializedHyperGraph + const { topology, problem } = loadSerializedHyperGraph(serializedGraph) + const solver = new TinyHyperGraphSolver2(topology, problem) + const result = solver.solveResult() + + expect(result._tag).toBe("ok") + delete (solver.problem.routeMetadata?.[0] as { mutuallyConnectedNetworkId?: string }) + .mutuallyConnectedNetworkId + + expect(() => solver.getOutput()).toThrow(SerializedGraphOutputInvariantError) +}) + +test("lib2 graph loader rejects solved route ports that do not exist", () => { + const graph: SerializedHyperGraph = { + regions: [ + { + regionId: "r0", + pointIds: ["p0", "p1"], + d: { center: { x: 0, y: 0 }, width: 1, height: 1 }, + }, + { + regionId: "r1", + pointIds: ["p0", "p1"], + d: { center: { x: 1, y: 0 }, width: 1, height: 1 }, + }, + ], + ports: [ + { portId: "p0", region1Id: "r0", region2Id: "r1", d: { x: 0, y: 0, z: 0 } }, + { portId: "p1", region1Id: "r0", region2Id: "r1", d: { x: 1, y: 0, z: 0 } }, + ], + connections: [ + { + connectionId: "c0", + startRegionId: "r0", + endRegionId: "r1", + }, + ], + solvedRoutes: [ + { + connection: { + connectionId: "c0", + startRegionId: "r0", + endRegionId: "r1", + }, + path: [ + { portId: "p0", nextRegionId: "r0", g: 0, h: 0, f: 0, hops: 0, ripRequired: false }, + { portId: "missing-port", nextRegionId: "r1", g: 1, h: 0, f: 1, hops: 1, ripRequired: false }, + ], + requiredRip: false, + }, + ], + } + + expect(() => loadSerializedHyperGraph(graph)).toThrow( + SerializedGraphLoadInvariantError, + ) +}) + +test("lib2 solver fails before seeding a route with no legal starting region", () => { + const topology = { + portCount: 2, + regionCount: 1, + regionIncidentPorts: [[0, 1]], + incidentPortRegion: [[0], [0]], + regionWidth: new Float64Array([1]), + regionHeight: new Float64Array([1]), + regionCenterX: new Float64Array([0]), + regionCenterY: new Float64Array([0]), + portAngleForRegion1: new Int32Array([0, 9000]), + portX: new Float64Array([0, 1]), + portY: new Float64Array([0, 0]), + portZ: new Int32Array([0, 0]), + } + const problem = { + routeCount: 1, + portSectionMask: new Int8Array([1, 1]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([1]), + regionNetId: new Int32Array([2]), + } + const solver = new TinyHyperGraphSolver2(topology, problem, { + STATIC_REACHABILITY_PRECHECK: false, + }) + + solver.solve() + + expect(solver.failed).toBe(true) + expect(solver.error).toBe("Start port 0 has no legal starting region") +}) From dc115c3dc4b1b0a137f2fc3fbabf2d5f8aab7254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 1 Jul 2026 15:58:17 +0530 Subject: [PATCH 3/3] add layered route search option --- lib/core.ts | 100 ++++++ lib2/layered-search-map.ts | 433 ++++++++++++++++++++++++++ lib2/route-search.ts | 53 +++- lib2/solver.ts | 254 ++++++++------- tests/lib2/layered-search-map.test.ts | 135 ++++++++ tests/lib2/route-search.test.ts | 1 + 6 files changed, 857 insertions(+), 119 deletions(-) create mode 100644 lib2/layered-search-map.ts create mode 100644 tests/lib2/layered-search-map.test.ts diff --git a/lib/core.ts b/lib/core.ts index d334dc5..3df2a6c 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -15,6 +15,11 @@ import { getStaticallyUnroutableRoutes, getStaticReachabilityError, } from "./static-reachability" +import { + buildLayeredSearchMap, + findLayeredRouteCorridor, + type LayeredSearchMap, +} from "../lib2/layered-search-map" import type { HopId, NetId, @@ -238,6 +243,9 @@ export interface TinyHyperGraphSolverOptions { STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT?: boolean GREEDY_FINAL_ROUTE_ITERS?: number + USE_LAYERED_ROUTE_SEARCH?: boolean + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS?: boolean } export interface TinyHyperGraphSolverOptionTarget { @@ -255,6 +263,9 @@ export interface TinyHyperGraphSolverOptionTarget { STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT: boolean GREEDY_FINAL_ROUTE_ITERS: number + USE_LAYERED_ROUTE_SEARCH?: boolean + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS?: boolean } export const applyTinyHyperGraphSolverOptions = ( @@ -310,6 +321,16 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.GREEDY_FINAL_ROUTE_ITERS !== undefined) { solver.GREEDY_FINAL_ROUTE_ITERS = options.GREEDY_FINAL_ROUTE_ITERS } + if (options.USE_LAYERED_ROUTE_SEARCH !== undefined) { + solver.USE_LAYERED_ROUTE_SEARCH = options.USE_LAYERED_ROUTE_SEARCH + } + if (options.LAYERED_SEARCH_BUCKET_SIZE !== undefined) { + solver.LAYERED_SEARCH_BUCKET_SIZE = options.LAYERED_SEARCH_BUCKET_SIZE + } + if (options.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS !== undefined) { + solver.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS = + options.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS + } } export const getTinyHyperGraphSolverOptions = ( @@ -330,6 +351,10 @@ export const getTinyHyperGraphSolverOptions = ( 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, + USE_LAYERED_ROUTE_SEARCH: solver.USE_LAYERED_ROUTE_SEARCH, + LAYERED_SEARCH_BUCKET_SIZE: solver.LAYERED_SEARCH_BUCKET_SIZE, + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS: + solver.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -357,6 +382,8 @@ export class TinyHyperGraphSolver extends BaseSolver { layerMask: 0, entryExitLayerChanges: 0, } + private layeredSearchMap?: LayeredSearchMap + private layeredSearchAllowedFineRegionMask?: Uint8Array DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER @@ -375,6 +402,9 @@ export class TinyHyperGraphSolver extends BaseSolver { STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true GREEDY_FINAL_ROUTE_ITERS = 4 + USE_LAYERED_ROUTE_SEARCH = false + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS = true constructor( public topology: TinyHyperGraphTopology, @@ -485,6 +515,12 @@ export class TinyHyperGraphSolver extends BaseSolver { } } } + + if (this.USE_LAYERED_ROUTE_SEARCH) { + this.layeredSearchMap = buildLayeredSearchMap(this.topology, { + bucketSize: this.LAYERED_SEARCH_BUCKET_SIZE, + }) + } } override _step() { @@ -499,6 +535,7 @@ export class TinyHyperGraphSolver extends BaseSolver { state.currentRouteId = state.unroutedRoutes.shift() state.currentRouteNetId = problem.routeNet[state.currentRouteId!] this.routeAttemptCountByRouteId[state.currentRouteId!] += 1 + this.layeredSearchAllowedFineRegionMask = undefined this.resetCandidateBestCosts() const startingPortId = problem.routeStartPort[state.currentRouteId!] @@ -526,6 +563,15 @@ export class TinyHyperGraphSolver extends BaseSolver { h: 0, }) state.goalPortId = problem.routeEndPort[state.currentRouteId!] + + if ( + !this.prepareLayeredRouteSearch( + startingNextRegionId, + state.goalPortId, + ) + ) { + return + } } const currentCandidate = state.candidateQueue.dequeue() @@ -583,6 +629,10 @@ export class TinyHyperGraphSolver extends BaseSolver { continue } + if (!this.isRegionAllowedForRouteSearch(nextRegionId)) { + continue + } + const newCandidate = { prevRegionId: currentCandidate.nextRegionId, nextRegionId, @@ -606,6 +656,54 @@ export class TinyHyperGraphSolver extends BaseSolver { } } + protected prepareLayeredRouteSearch( + startRegionId: RegionId, + goalPortId: PortId, + ): boolean { + if (!this.USE_LAYERED_ROUTE_SEARCH) { + return true + } + + const currentRouteNetId = this.state.currentRouteNetId + if (currentRouteNetId === undefined) { + throw new Error("Current route net is missing during layered route search") + } + + const layeredSearchMap = + this.layeredSearchMap ?? + buildLayeredSearchMap(this.topology, { + bucketSize: this.LAYERED_SEARCH_BUCKET_SIZE, + }) + this.layeredSearchMap = layeredSearchMap + + const corridor = findLayeredRouteCorridor({ + layeredMap: layeredSearchMap, + topology: this.topology, + problem: this.problem, + regionCongestionCost: this.state.regionCongestionCost, + currentRouteNetId, + startRegionId, + goalPortId, + distanceToCost: this.DISTANCE_TO_COST, + includeAdjacentCoarseRegions: + this.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS, + }) + + if (corridor._tag === "notFound") { + this.failed = true + this.error = corridor.error + return false + } + + this.layeredSearchAllowedFineRegionMask = corridor.allowedFineRegionMask + return true + } + + protected isRegionAllowedForRouteSearch(regionId: RegionId): boolean { + const allowedFineRegionMask = this.layeredSearchAllowedFineRegionMask + return !allowedFineRegionMask || allowedFineRegionMask[regionId] === 1 + } + resetCandidateBestCosts() { const { state } = this @@ -887,6 +985,7 @@ export class TinyHyperGraphSolver extends BaseSolver { state.candidateQueue.clear() this.resetCandidateBestCosts() state.goalPortId = -1 + this.layeredSearchAllowedFineRegionMask = undefined } protected getMaxRegionCost() { @@ -1359,6 +1458,7 @@ export class TinyHyperGraphSolver extends BaseSolver { state.candidateQueue.clear() state.currentRouteNetId = undefined state.currentRouteId = undefined + this.layeredSearchAllowedFineRegionMask = undefined } computeG(currentCandidate: Candidate, neighborPortId: PortId): number { diff --git a/lib2/layered-search-map.ts b/lib2/layered-search-map.ts new file mode 100644 index 0000000..38627fe --- /dev/null +++ b/lib2/layered-search-map.ts @@ -0,0 +1,433 @@ +import type { + TinyHyperGraphProblem, + TinyHyperGraphTopology, +} from "./domain" +import { MinHeap } from "./min-heap" +import type { NetId, PortId, RegionId } from "./types" + +export type CoarseRegionId = number + +export interface LayeredSearchMap { + readonly fineToCoarseRegionId: Int32Array + readonly coarseRegionIds: RegionId[][] + readonly coarseCenterX: Float64Array + readonly coarseCenterY: Float64Array + readonly coarseAvailableZMask: Int32Array + readonly coarseAdjacency: CoarseRegionId[][] +} + +export interface BuildLayeredSearchMapOptions { + readonly bucketSize?: number +} + +export interface FindLayeredRouteCorridorInput { + readonly layeredMap: LayeredSearchMap + readonly topology: TinyHyperGraphTopology + readonly problem: TinyHyperGraphProblem + readonly regionCongestionCost: ArrayLike + readonly currentRouteNetId: NetId + readonly startRegionId: RegionId + readonly goalPortId: PortId + readonly distanceToCost: number + readonly includeAdjacentCoarseRegions: boolean +} + +export type LayeredRouteCorridorResult = + | { + readonly _tag: "found" + readonly coarsePath: CoarseRegionId[] + readonly allowedFineRegionMask: Uint8Array + } + | { + readonly _tag: "notFound" + readonly error: string + } + +interface CoarseCandidate { + readonly coarseRegionId: CoarseRegionId + readonly g: number + readonly f: number +} + +const compareCoarseCandidates = ( + left: CoarseCandidate, + right: CoarseCandidate, +): number => left.f - right.f + +export function buildLayeredSearchMap( + topology: TinyHyperGraphTopology, + options: BuildLayeredSearchMapOptions = {}, +): LayeredSearchMap { + const bucketSize = options.bucketSize ?? deriveBucketSize(topology) + if (!Number.isFinite(bucketSize) || bucketSize <= 0) { + throw new Error(`Invalid layered-search bucket size: ${bucketSize}`) + } + + const fineToCoarseRegionId = new Int32Array(topology.regionCount).fill(-1) + const coarseRegionIds: RegionId[][] = [] + const coarseIdByBucketKey = new Map() + + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const centerX = topology.regionCenterX[regionId] + const centerY = topology.regionCenterY[regionId] + if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) { + throw new Error(`Region ${regionId} has invalid center coordinates`) + } + + const bucketX = Math.floor(centerX / bucketSize) + const bucketY = Math.floor(centerY / bucketSize) + const bucketKey = `${bucketX}:${bucketY}` + let coarseRegionId = coarseIdByBucketKey.get(bucketKey) + + if (coarseRegionId === undefined) { + coarseRegionId = coarseRegionIds.length + coarseIdByBucketKey.set(bucketKey, coarseRegionId) + coarseRegionIds.push([]) + } + + fineToCoarseRegionId[regionId] = coarseRegionId + const fineRegionIds = coarseRegionIds[coarseRegionId] + if (!fineRegionIds) { + throw new Error(`Coarse region ${coarseRegionId} was not initialized`) + } + fineRegionIds.push(regionId) + } + + const coarseCenterX = new Float64Array(coarseRegionIds.length) + const coarseCenterY = new Float64Array(coarseRegionIds.length) + const coarseAvailableZMask = new Int32Array(coarseRegionIds.length) + for (let coarseRegionId = 0; coarseRegionId < coarseRegionIds.length; coarseRegionId++) { + const regionIds = coarseRegionIds[coarseRegionId] + if (!regionIds || regionIds.length === 0) { + throw new Error(`Coarse region ${coarseRegionId} has no fine regions`) + } + + let centerXSum = 0 + let centerYSum = 0 + let availableZMask = 0 + for (const regionId of regionIds) { + centerXSum += topology.regionCenterX[regionId] + centerYSum += topology.regionCenterY[regionId] + availableZMask |= topology.regionAvailableZMask?.[regionId] ?? 0 + } + coarseCenterX[coarseRegionId] = centerXSum / regionIds.length + coarseCenterY[coarseRegionId] = centerYSum / regionIds.length + coarseAvailableZMask[coarseRegionId] = availableZMask + } + + const adjacencySets = Array.from( + { length: coarseRegionIds.length }, + () => new Set(), + ) + for (let portId = 0; portId < topology.portCount; portId++) { + const incidentRegions = topology.incidentPortRegion[portId] + if (incidentRegions === undefined) { + throw new Error(`Port ${portId} is missing incident regions`) + } + if (incidentRegions.length < 2) { + continue + } + + const firstRegionId = incidentRegions[0] + const secondRegionId = incidentRegions[1] + if (firstRegionId === undefined || secondRegionId === undefined) { + throw new Error(`Port ${portId} has invalid incident regions`) + } + + const firstCoarseRegionId = fineToCoarseRegionId[firstRegionId] + const secondCoarseRegionId = fineToCoarseRegionId[secondRegionId] + if (firstCoarseRegionId === undefined || secondCoarseRegionId === undefined) { + throw new Error(`Port ${portId} references an unmapped region`) + } + if (firstCoarseRegionId < 0 || secondCoarseRegionId < 0) { + throw new Error(`Port ${portId} references an unmapped region`) + } + if (firstCoarseRegionId === secondCoarseRegionId) { + continue + } + + const firstAdjacency = adjacencySets[firstCoarseRegionId] + const secondAdjacency = adjacencySets[secondCoarseRegionId] + if (!firstAdjacency || !secondAdjacency) { + throw new Error(`Port ${portId} references unknown coarse adjacency`) + } + firstAdjacency.add(secondCoarseRegionId) + secondAdjacency.add(firstCoarseRegionId) + } + + return { + fineToCoarseRegionId, + coarseRegionIds, + coarseCenterX, + coarseCenterY, + coarseAvailableZMask, + coarseAdjacency: adjacencySets.map((adjacentIds) => + Array.from(adjacentIds).sort((left, right) => left - right), + ), + } +} + +export function findLayeredRouteCorridor( + input: FindLayeredRouteCorridorInput, +): LayeredRouteCorridorResult { + const startCoarseRegionId = + input.layeredMap.fineToCoarseRegionId[input.startRegionId] + if (startCoarseRegionId === undefined || startCoarseRegionId < 0) { + throw new Error(`Start region ${input.startRegionId} is not in coarse map`) + } + + const goalCoarseRegionIds = getLegalGoalCoarseRegionIds(input) + if (goalCoarseRegionIds.size === 0) { + return { + _tag: "notFound", + error: `Goal port ${input.goalPortId} has no legal coarse regions`, + } + } + + const coarsePath = runCoarseAStar( + input, + startCoarseRegionId, + goalCoarseRegionIds, + ) + if (coarsePath.length === 0) { + return { + _tag: "notFound", + error: `No coarse path from region ${input.startRegionId} to goal port ${input.goalPortId}`, + } + } + + return { + _tag: "found", + coarsePath, + allowedFineRegionMask: createAllowedFineRegionMask(input, coarsePath), + } +} + +function deriveBucketSize(topology: TinyHyperGraphTopology): number { + const diameters: number[] = [] + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const width = topology.regionWidth[regionId] + const height = topology.regionHeight[regionId] + if (Number.isFinite(width) && Number.isFinite(height)) { + diameters.push(Math.max(width, height)) + } + } + + diameters.sort((left, right) => left - right) + const medianDiameter = diameters[Math.floor(diameters.length / 2)] ?? 1 + return Math.max(medianDiameter * 4, 1) +} + +function getLegalGoalCoarseRegionIds( + input: FindLayeredRouteCorridorInput, +): Set { + const goalCoarseRegionIds = new Set() + const incidentRegions = input.topology.incidentPortRegion[input.goalPortId] + if (incidentRegions === undefined) { + throw new Error(`Goal port ${input.goalPortId} is missing incident regions`) + } + + for (const regionId of incidentRegions) { + if (isRegionReservedForDifferentNet(input, regionId)) { + continue + } + const coarseRegionId = input.layeredMap.fineToCoarseRegionId[regionId] + if (coarseRegionId === undefined || coarseRegionId < 0) { + throw new Error(`Goal region ${regionId} is not in coarse map`) + } + goalCoarseRegionIds.add(coarseRegionId) + } + + return goalCoarseRegionIds +} + +function runCoarseAStar( + input: FindLayeredRouteCorridorInput, + startCoarseRegionId: CoarseRegionId, + goalCoarseRegionIds: Set, +): CoarseRegionId[] { + const queue = new MinHeap([], compareCoarseCandidates) + const bestCostByCoarseRegionId = new Float64Array( + input.layeredMap.coarseRegionIds.length, + ).fill(Number.POSITIVE_INFINITY) + const previousCoarseRegionId = new Int32Array( + input.layeredMap.coarseRegionIds.length, + ).fill(-1) + + bestCostByCoarseRegionId[startCoarseRegionId] = 0 + queue.queue({ + coarseRegionId: startCoarseRegionId, + g: 0, + f: getCoarseHeuristic(input, startCoarseRegionId, goalCoarseRegionIds), + }) + + while (queue.length > 0) { + const current = queue.dequeue() + if (!current) { + break + } + if (current.g > bestCostByCoarseRegionId[current.coarseRegionId]) { + continue + } + if (goalCoarseRegionIds.has(current.coarseRegionId)) { + return reconstructCoarsePath( + previousCoarseRegionId, + current.coarseRegionId, + ) + } + + const adjacentCoarseRegionIds = + input.layeredMap.coarseAdjacency[current.coarseRegionId] + if (!adjacentCoarseRegionIds) { + throw new Error(`Coarse region ${current.coarseRegionId} is missing adjacency`) + } + + for (const nextCoarseRegionId of adjacentCoarseRegionIds) { + if (isCoarseRegionReservedForDifferentNet(input, nextCoarseRegionId)) { + continue + } + const g = + current.g + + getCoarseEdgeCost(input, current.coarseRegionId, nextCoarseRegionId) + if (g >= bestCostByCoarseRegionId[nextCoarseRegionId]) { + continue + } + + bestCostByCoarseRegionId[nextCoarseRegionId] = g + previousCoarseRegionId[nextCoarseRegionId] = current.coarseRegionId + queue.queue({ + coarseRegionId: nextCoarseRegionId, + g, + f: g + getCoarseHeuristic(input, nextCoarseRegionId, goalCoarseRegionIds), + }) + } + } + + return [] +} + +function reconstructCoarsePath( + previousCoarseRegionId: Int32Array, + endCoarseRegionId: CoarseRegionId, +): CoarseRegionId[] { + const coarsePath: CoarseRegionId[] = [] + let cursor = endCoarseRegionId + + while (cursor >= 0) { + coarsePath.unshift(cursor) + cursor = previousCoarseRegionId[cursor] ?? -1 + } + + return coarsePath +} + +function createAllowedFineRegionMask( + input: FindLayeredRouteCorridorInput, + coarsePath: CoarseRegionId[], +): Uint8Array { + const allowedCoarseRegionIds = new Set() + for (const coarseRegionId of coarsePath) { + allowedCoarseRegionIds.add(coarseRegionId) + if (!input.includeAdjacentCoarseRegions) { + continue + } + const adjacentCoarseRegionIds = + input.layeredMap.coarseAdjacency[coarseRegionId] + if (!adjacentCoarseRegionIds) { + throw new Error(`Coarse region ${coarseRegionId} is missing adjacency`) + } + for (const adjacentCoarseRegionId of adjacentCoarseRegionIds) { + allowedCoarseRegionIds.add(adjacentCoarseRegionId) + } + } + + const allowedFineRegionMask = new Uint8Array(input.topology.regionCount) + for (const coarseRegionId of allowedCoarseRegionIds) { + const fineRegionIds = input.layeredMap.coarseRegionIds[coarseRegionId] + if (!fineRegionIds) { + throw new Error(`Coarse region ${coarseRegionId} has no fine regions`) + } + for (const regionId of fineRegionIds) { + allowedFineRegionMask[regionId] = 1 + } + } + + return allowedFineRegionMask +} + +function isCoarseRegionReservedForDifferentNet( + input: FindLayeredRouteCorridorInput, + coarseRegionId: CoarseRegionId, +): boolean { + const fineRegionIds = input.layeredMap.coarseRegionIds[coarseRegionId] + if (!fineRegionIds) { + throw new Error(`Coarse region ${coarseRegionId} has no fine regions`) + } + + for (const regionId of fineRegionIds) { + if (!isRegionReservedForDifferentNet(input, regionId)) { + return false + } + } + + return true +} + +function isRegionReservedForDifferentNet( + input: FindLayeredRouteCorridorInput, + regionId: RegionId, +): boolean { + const reservedNetId = input.problem.regionNetId[regionId] + return reservedNetId !== -1 && reservedNetId !== input.currentRouteNetId +} + +function getCoarseEdgeCost( + input: FindLayeredRouteCorridorInput, + fromCoarseRegionId: CoarseRegionId, + toCoarseRegionId: CoarseRegionId, +): number { + const dx = + input.layeredMap.coarseCenterX[fromCoarseRegionId] - + input.layeredMap.coarseCenterX[toCoarseRegionId] + const dy = + input.layeredMap.coarseCenterY[fromCoarseRegionId] - + input.layeredMap.coarseCenterY[toCoarseRegionId] + const congestionCost = getAverageCoarseCongestionCost(input, toCoarseRegionId) + return Math.hypot(dx, dy) * input.distanceToCost + congestionCost +} + +function getCoarseHeuristic( + input: FindLayeredRouteCorridorInput, + coarseRegionId: CoarseRegionId, + goalCoarseRegionIds: Set, +): number { + let bestDistance = Number.POSITIVE_INFINITY + for (const goalCoarseRegionId of goalCoarseRegionIds) { + const dx = + input.layeredMap.coarseCenterX[coarseRegionId] - + input.layeredMap.coarseCenterX[goalCoarseRegionId] + const dy = + input.layeredMap.coarseCenterY[coarseRegionId] - + input.layeredMap.coarseCenterY[goalCoarseRegionId] + bestDistance = Math.min(bestDistance, Math.hypot(dx, dy)) + } + + return bestDistance * input.distanceToCost +} + +function getAverageCoarseCongestionCost( + input: FindLayeredRouteCorridorInput, + coarseRegionId: CoarseRegionId, +): number { + const fineRegionIds = input.layeredMap.coarseRegionIds[coarseRegionId] + if (!fineRegionIds || fineRegionIds.length === 0) { + throw new Error(`Coarse region ${coarseRegionId} has no fine regions`) + } + + let totalCongestionCost = 0 + for (const regionId of fineRegionIds) { + totalCongestionCost += input.regionCongestionCost[regionId] ?? 0 + } + + return totalCongestionCost / fineRegionIds.length +} diff --git a/lib2/route-search.ts b/lib2/route-search.ts index b5883d2..7b93ba8 100644 --- a/lib2/route-search.ts +++ b/lib2/route-search.ts @@ -15,9 +15,19 @@ export const ROUTE_SEARCH_ADVANCED = "advanced" /** Route search status when the current route has no candidates left. */ export const ROUTE_SEARCH_OUT_OF_CANDIDATES = "outOfCandidates" +/** Route search failure reason. */ +export type RouteSearchFailureReason = + | "noLegalStartingRegion" + | "coarsePathNotFound" + | "missingCurrentRouteNet" + | "missingRegionIncidentPorts" + | "missingPortIncidentRegions" + | "portNotIncidentToCurrentRegion" + /** Route search failure result. */ export type RouteSearchFailure = { readonly _tag: "failed" + readonly reason: RouteSearchFailureReason readonly error: string } @@ -50,6 +60,13 @@ export type RouteSearchRuntime = { ) => number readonly computeH: (portId: PortId) => number readonly onRouteAttempt: (routeId: RouteId) => void + readonly prepareRouteSearch?: (input: { + readonly routeId: RouteId + readonly startPortId: PortId + readonly startRegionId: RegionId + readonly goalPortId: PortId + }) => RouteSearchFailure | undefined + readonly isRegionAllowedForRouteSearch?: (regionId: RegionId) => boolean } /** @@ -82,7 +99,10 @@ export function runRouteSearchStep( ) if (startingNextRegionId === undefined) { - return failed(`Start port ${startingPortId} has no incident regions`) + return failed( + "noLegalStartingRegion", + `Start port ${startingPortId} has no legal starting region`, + ) } runtime.setCandidateBestCost( @@ -97,11 +117,24 @@ export function runRouteSearchStep( h: 0, }) state.goalPortId = problem.routeEndPort[nextRouteId] + + const routeSearchFailure = runtime.prepareRouteSearch?.({ + routeId: nextRouteId, + startPortId: startingPortId, + startRegionId: startingNextRegionId, + goalPortId: state.goalPortId, + }) + if (routeSearchFailure) { + return routeSearchFailure + } } const currentRouteNetId = state.currentRouteNetId if (currentRouteNetId === undefined) { - return failed("Current route net is missing during route search") + return failed( + "missingCurrentRouteNet", + "Current route net is missing during route search", + ) } const currentCandidate = state.candidateQueue.dequeue() @@ -126,6 +159,7 @@ export function runRouteSearchStep( const neighbors = topology.regionIncidentPorts[currentCandidate.nextRegionId] if (neighbors === undefined) { return failed( + "missingRegionIncidentPorts", `Region ${currentCandidate.nextRegionId} is missing incident ports during route search`, ) } @@ -165,12 +199,14 @@ export function runRouteSearchStep( const incidentRegions = topology.incidentPortRegion[neighborPortId] if (incidentRegions === undefined) { return failed( + "missingPortIncidentRegions", `Port ${neighborPortId} is missing incident regions during route search`, ) } if (!incidentRegions.includes(currentCandidate.nextRegionId)) { return failed( + "portNotIncidentToCurrentRegion", `Port ${neighborPortId} is not incident to current region ${currentCandidate.nextRegionId}`, ) } @@ -187,6 +223,13 @@ export function runRouteSearchStep( continue } + if ( + runtime.isRegionAllowedForRouteSearch && + !runtime.isRegionAllowedForRouteSearch(nextRegionId) + ) { + continue + } + const nextCandidate: Candidate = { prevRegionId: currentCandidate.nextRegionId, nextRegionId, @@ -213,7 +256,11 @@ export function runRouteSearchStep( return ROUTE_SEARCH_ADVANCED } -const failed = (error: string): RouteSearchFailure => ({ +const failed = ( + reason: RouteSearchFailureReason, + error: string, +): RouteSearchFailure => ({ _tag: "failed", + reason, error, }) diff --git a/lib2/solver.ts b/lib2/solver.ts index 54d6659..1bf6a2f 100644 --- a/lib2/solver.ts +++ b/lib2/solver.ts @@ -40,8 +40,20 @@ import { parseGraph, type ParseGraphError, } from "./graph-input" +import { + buildLayeredSearchMap, + findLayeredRouteCorridor, + type LayeredSearchMap, +} from "./layered-search-map" import { err, ok, type Result } from "./prelude" import { MutableRegionCache } from "./region-cache" +import { + ROUTE_SEARCH_ADVANCED, + ROUTE_SEARCH_ALL_ROUTES_ROUTED, + ROUTE_SEARCH_OUT_OF_CANDIDATES, + runRouteSearchStep, + type RouteSearchFailure, +} from "./route-search" import { createSegmentGeometryScratch, readSegmentGeometry, @@ -178,6 +190,9 @@ export interface TinyHyperGraphSolver2Options { STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT?: boolean GREEDY_FINAL_ROUTE_ITERS?: number + USE_LAYERED_ROUTE_SEARCH?: boolean + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS?: boolean } export interface TinyHyperGraphSolver2OptionTarget { @@ -195,6 +210,9 @@ export interface TinyHyperGraphSolver2OptionTarget { STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT: boolean GREEDY_FINAL_ROUTE_ITERS: number + USE_LAYERED_ROUTE_SEARCH?: boolean + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS?: boolean } export const applyTinyHyperGraphSolver2Options = ( @@ -250,6 +268,16 @@ export const applyTinyHyperGraphSolver2Options = ( if (options.GREEDY_FINAL_ROUTE_ITERS !== undefined) { solver.GREEDY_FINAL_ROUTE_ITERS = options.GREEDY_FINAL_ROUTE_ITERS } + if (options.USE_LAYERED_ROUTE_SEARCH !== undefined) { + solver.USE_LAYERED_ROUTE_SEARCH = options.USE_LAYERED_ROUTE_SEARCH + } + if (options.LAYERED_SEARCH_BUCKET_SIZE !== undefined) { + solver.LAYERED_SEARCH_BUCKET_SIZE = options.LAYERED_SEARCH_BUCKET_SIZE + } + if (options.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS !== undefined) { + solver.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS = + options.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS + } } export const getTinyHyperGraphSolver2Options = ( @@ -270,6 +298,10 @@ export const getTinyHyperGraphSolver2Options = ( 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, + USE_LAYERED_ROUTE_SEARCH: solver.USE_LAYERED_ROUTE_SEARCH, + LAYERED_SEARCH_BUCKET_SIZE: solver.LAYERED_SEARCH_BUCKET_SIZE, + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS: + solver.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -287,6 +319,8 @@ export class TinyHyperGraphSolver2 extends BaseSolver { private readonly segmentGeometryScratch: SegmentGeometryScratch = createSegmentGeometryScratch() private regionCaches: MutableRegionCache[] = [] + private layeredSearchMap?: LayeredSearchMap + private layeredSearchAllowedFineRegionMask?: Uint8Array DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER @@ -305,6 +339,9 @@ export class TinyHyperGraphSolver2 extends BaseSolver { STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true GREEDY_FINAL_ROUTE_ITERS = 4 + USE_LAYERED_ROUTE_SEARCH = false + LAYERED_SEARCH_BUCKET_SIZE?: number + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS = true constructor( public topology: TinyHyperGraphTopology, @@ -416,145 +453,128 @@ export class TinyHyperGraphSolver2 extends BaseSolver { } } } - } - - override _step() { - const { problem, topology, state } = this - - if (state.currentRouteId === undefined) { - if (state.unroutedRoutes.length === 0) { - this.onAllRoutesRouted() - return - } - - state.currentRouteId = state.unroutedRoutes.shift() - state.currentRouteNetId = problem.routeNet[state.currentRouteId!] - this.routeAttemptCountByRouteId[state.currentRouteId!] += 1 - - this.resetCandidateBestCosts() - const startingPortId = problem.routeStartPort[state.currentRouteId!] - state.candidateQueue.clear() - const startingNextRegionId = this.getStartingNextRegionId( - state.currentRouteId!, - startingPortId, - ) - - if (startingNextRegionId === undefined) { - this.failed = true - this.error = `Start port ${startingPortId} has no legal starting region` - return - } - this.setCandidateBestCost( - this.getHopId(startingPortId, startingNextRegionId), - 0, - ) - state.candidateQueue.queue({ - nextRegionId: startingNextRegionId, - portId: startingPortId, - f: 0, - g: 0, - h: 0, + if (this.USE_LAYERED_ROUTE_SEARCH) { + this.layeredSearchMap = buildLayeredSearchMap(this.topology, { + bucketSize: this.LAYERED_SEARCH_BUCKET_SIZE, }) - state.goalPortId = problem.routeEndPort[state.currentRouteId!] } + } - const currentCandidate = state.candidateQueue.dequeue() + override _step() { + const result = runRouteSearchStep({ + topology: this.topology, + problem: this.problem, + state: this.state, + getHopId: (portId, nextRegionId) => this.getHopId(portId, nextRegionId), + getCandidateBestCost: (hopId) => this.getCandidateBestCost(hopId), + setCandidateBestCost: (hopId, cost) => + this.setCandidateBestCost(hopId, cost), + resetCandidateBestCosts: () => this.resetCandidateBestCosts(), + getStartingNextRegionId: (routeId, startingPortId) => + this.getStartingNextRegionId(routeId, startingPortId), + isPortReservedForDifferentNet: (portId) => + this.isPortReservedForDifferentNet(portId), + isRegionReservedForDifferentNet: (regionId) => + this.isRegionReservedForDifferentNet(regionId), + computeG: (currentCandidate, neighborPortId) => + this.computeG(currentCandidate, neighborPortId), + computeH: (portId) => this.computeH(portId), + onRouteAttempt: (routeId) => { + this.routeAttemptCountByRouteId[routeId] += 1 + this.layeredSearchAllowedFineRegionMask = undefined + }, + prepareRouteSearch: (input) => this.prepareLayeredRouteSearch(input), + isRegionAllowedForRouteSearch: (regionId) => + this.isRegionAllowedForRouteSearch(regionId), + }) - if (!currentCandidate) { + if (result === ROUTE_SEARCH_ALL_ROUTES_ROUTED) { + this.onAllRoutesRouted() + return + } + if (result === ROUTE_SEARCH_OUT_OF_CANDIDATES) { this.onOutOfCandidates() return } - - const currentCandidateHopId = this.getHopId( - currentCandidate.portId, - currentCandidate.nextRegionId, - ) - if (currentCandidate.g > this.getCandidateBestCost(currentCandidateHopId)) { + if (result === ROUTE_SEARCH_ADVANCED) { return } - - if (this.isRegionReservedForDifferentNet(currentCandidate.nextRegionId)) { + if ("_tag" in result) { + this.onRouteSearchFailure(result) return } - const neighbors = - topology.regionIncidentPorts[currentCandidate.nextRegionId] - if (neighbors === undefined) { - throw new SolverInvariantError( - state.currentRouteId, - `region ${currentCandidate.nextRegionId} is missing incident ports`, - ) - } + this.onPathFound(result) + } - for (const neighborPortId of neighbors) { - const assignedNetId = state.portAssignment[neighborPortId] - if (this.isPortReservedForDifferentNet(neighborPortId)) continue - if (neighborPortId === state.goalPortId) { - if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { - continue - } - this.onPathFound(currentCandidate) - return - } - if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { - continue - } - if (neighborPortId === currentCandidate.portId) continue - if (problem.portSectionMask[neighborPortId] === 0) continue - - const g = this.computeG(currentCandidate, neighborPortId) - if (!Number.isFinite(g)) continue - const h = this.computeH(neighborPortId) - const incidentRegions = topology.incidentPortRegion[neighborPortId] - - if (incidentRegions === undefined) { - throw new SolverInvariantError( - state.currentRouteId, - `port ${neighborPortId} is missing incident regions`, - ) - } + protected onRouteSearchFailure(failure: RouteSearchFailure): void { + if ( + failure.reason === "noLegalStartingRegion" || + failure.reason === "coarsePathNotFound" + ) { + this.failed = true + this.error = failure.error + return + } - if (!incidentRegions.includes(currentCandidate.nextRegionId)) { - throw new SolverInvariantError( - state.currentRouteId, - `Port ${neighborPortId} is not incident to current region ${currentCandidate.nextRegionId}`, - ) - } + throw new SolverInvariantError(this.state.currentRouteId, failure.error) + } - const nextRegionId = - incidentRegions[0] === currentCandidate.nextRegionId - ? incidentRegions[1] - : incidentRegions[0] + protected prepareLayeredRouteSearch(input: { + readonly routeId: RouteId + readonly startPortId: PortId + readonly startRegionId: RegionId + readonly goalPortId: PortId + }): RouteSearchFailure | undefined { + if (!this.USE_LAYERED_ROUTE_SEARCH) { + return undefined + } - if ( - nextRegionId === undefined || - this.isRegionReservedForDifferentNet(nextRegionId) - ) { - continue + const currentRouteNetId = this.state.currentRouteNetId + if (currentRouteNetId === undefined) { + return { + _tag: "failed", + reason: "missingCurrentRouteNet", + error: "Current route net is missing during layered route search", } + } - const newCandidate = { - prevRegionId: currentCandidate.nextRegionId, - nextRegionId, - portId: neighborPortId, - g, - h, - f: g + h, - prevCandidate: currentCandidate, - } + const layeredSearchMap = + this.layeredSearchMap ?? + buildLayeredSearchMap(this.topology, { + bucketSize: this.LAYERED_SEARCH_BUCKET_SIZE, + }) + this.layeredSearchMap = layeredSearchMap - if (neighborPortId === state.goalPortId) { - this.onPathFound(newCandidate) - return + const corridor = findLayeredRouteCorridor({ + layeredMap: layeredSearchMap, + topology: this.topology, + problem: this.problem, + regionCongestionCost: this.state.regionCongestionCost, + currentRouteNetId, + startRegionId: input.startRegionId, + goalPortId: input.goalPortId, + distanceToCost: this.DISTANCE_TO_COST, + includeAdjacentCoarseRegions: + this.LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS, + }) + + if (corridor._tag === "notFound") { + return { + _tag: "failed", + reason: "coarsePathNotFound", + error: corridor.error, } + } - const candidateHopId = this.getHopId(neighborPortId, nextRegionId) - if (g >= this.getCandidateBestCost(candidateHopId)) continue + this.layeredSearchAllowedFineRegionMask = corridor.allowedFineRegionMask + return undefined + } - this.setCandidateBestCost(candidateHopId, g) - state.candidateQueue.queue(newCandidate) - } + protected isRegionAllowedForRouteSearch(regionId: RegionId): boolean { + const allowedFineRegionMask = this.layeredSearchAllowedFineRegionMask + return !allowedFineRegionMask || allowedFineRegionMask[regionId] === 1 } resetCandidateBestCosts() { @@ -815,6 +835,7 @@ export class TinyHyperGraphSolver2 extends BaseSolver { this.resetCandidateBestCosts() state.goalPortId = -1 this.regionCaches = this.createRegionCachesFromState() + this.layeredSearchAllowedFineRegionMask = undefined } protected getMaxRegionCost() { @@ -1296,6 +1317,7 @@ export class TinyHyperGraphSolver2 extends BaseSolver { state.candidateQueue.clear() state.currentRouteNetId = undefined state.currentRouteId = undefined + this.layeredSearchAllowedFineRegionMask = undefined } computeG(currentCandidate: Candidate, neighborPortId: PortId): number { diff --git a/tests/lib2/layered-search-map.test.ts b/tests/lib2/layered-search-map.test.ts new file mode 100644 index 0000000..27633c5 --- /dev/null +++ b/tests/lib2/layered-search-map.test.ts @@ -0,0 +1,135 @@ +import { expect, test } from "bun:test" +import type { + TinyHyperGraphProblem, + TinyHyperGraphTopology, + TinyHyperGraphWorkingState, +} from "lib2/domain" +import { + buildLayeredSearchMap, + findLayeredRouteCorridor, +} from "lib2/layered-search-map" +import { MinHeap } from "lib2/min-heap" +import { TinyHyperGraphSolver2 } from "lib2/solver" +import type { Candidate } from "lib2/domain" + +const compareCandidate = (left: Candidate, right: Candidate) => + left.f - right.f + +const createLineTopology = (): TinyHyperGraphTopology => ({ + portCount: 4, + regionCount: 3, + regionIncidentPorts: [ + [0, 1], + [1, 2], + [2, 3], + ], + incidentPortRegion: [[0], [0, 1], [1, 2], [2]], + regionWidth: new Float64Array([1, 1, 1]), + regionHeight: new Float64Array([1, 1, 1]), + regionCenterX: new Float64Array([0, 10, 20]), + regionCenterY: new Float64Array([0, 0, 0]), + regionAvailableZMask: new Int32Array([1, 1, 1]), + portAngleForRegion1: new Int32Array([0, 0, 0, 0]), + portAngleForRegion2: new Int32Array([0, 0, 0, 0]), + portX: new Float64Array([0, 5, 15, 20]), + portY: new Float64Array([0, 0, 0, 0]), + portZ: new Int32Array([0, 0, 0, 0]), +}) + +const createProblem = (regionNetId: Int32Array): TinyHyperGraphProblem => ({ + routeCount: 1, + portSectionMask: new Int8Array([1, 1, 1, 1]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([3]), + routeNet: new Int32Array([1]), + regionNetId, +}) + +const createWorkingState = (): TinyHyperGraphWorkingState => ({ + portAssignment: new Int32Array([-1, -1, -1, -1]), + regionSegments: [[], [], []], + regionIntersectionCaches: [], + currentRouteNetId: 1, + currentRouteId: 0, + unroutedRoutes: [], + candidateQueue: new MinHeap([], compareCandidate), + candidateBestCostByHopId: new Float64Array(12), + candidateBestCostGenerationByHopId: new Uint32Array(12), + candidateBestCostGeneration: 1, + goalPortId: 3, + ripCount: 0, + regionCongestionCost: new Float64Array([0, 0, 0]), +}) + +test("layered search map groups regions and builds coarse adjacency", () => { + const topology = createLineTopology() + const layeredMap = buildLayeredSearchMap(topology, { bucketSize: 5 }) + + expect(Array.from(layeredMap.fineToCoarseRegionId)).toEqual([0, 1, 2]) + expect(layeredMap.coarseAdjacency).toEqual([[1], [0, 2], [1]]) + expect(Array.from(layeredMap.coarseAvailableZMask)).toEqual([1, 1, 1]) +}) + +test("layered route corridor finds a coarse path and fine mask", () => { + const topology = createLineTopology() + const problem = createProblem(new Int32Array([-1, -1, -1])) + const layeredMap = buildLayeredSearchMap(topology, { bucketSize: 5 }) + const result = findLayeredRouteCorridor({ + layeredMap, + topology, + problem, + regionCongestionCost: createWorkingState().regionCongestionCost, + currentRouteNetId: 1, + startRegionId: 0, + goalPortId: 3, + distanceToCost: 1, + includeAdjacentCoarseRegions: false, + }) + + expect(result._tag).toBe("found") + if (result._tag === "found") { + expect(result.coarsePath).toEqual([0, 1, 2]) + expect(Array.from(result.allowedFineRegionMask)).toEqual([1, 1, 1]) + } +}) + +test("layered route corridor rejects a foreign-net coarse bottleneck", () => { + const topology = createLineTopology() + const problem = createProblem(new Int32Array([-1, 2, -1])) + const layeredMap = buildLayeredSearchMap(topology, { bucketSize: 5 }) + const result = findLayeredRouteCorridor({ + layeredMap, + topology, + problem, + regionCongestionCost: createWorkingState().regionCongestionCost, + currentRouteNetId: 1, + startRegionId: 0, + goalPortId: 3, + distanceToCost: 1, + includeAdjacentCoarseRegions: false, + }) + + expect(result).toEqual({ + _tag: "notFound", + error: "No coarse path from region 0 to goal port 3", + }) +}) + +test("solver can route through layered search when enabled", () => { + const topology = createLineTopology() + const problem = createProblem(new Int32Array([-1, -1, -1])) + const solver = new TinyHyperGraphSolver2(topology, problem, { + STATIC_REACHABILITY_PRECHECK: false, + USE_LAYERED_ROUTE_SEARCH: true, + LAYERED_SEARCH_BUCKET_SIZE: 5, + LAYERED_SEARCH_INCLUDE_ADJACENT_COARSE_REGIONS: false, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.state.regionSegments[0]).toHaveLength(1) + expect(solver.state.regionSegments[1]).toHaveLength(1) + expect(solver.state.regionSegments[2]).toHaveLength(1) +}) diff --git a/tests/lib2/route-search.test.ts b/tests/lib2/route-search.test.ts index 9afd160..469ec5c 100644 --- a/tests/lib2/route-search.test.ts +++ b/tests/lib2/route-search.test.ts @@ -144,6 +144,7 @@ test("route search reports missing region incident ports as failure", () => { expect(result).toEqual({ _tag: "failed", + reason: "missingRegionIncidentPorts", error: "Region 1 is missing incident ports during route search", }) })