From 1003eb7f6f687bbf46660c6a48474526e3e42a99 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Wed, 5 Aug 2026 09:34:30 +0200 Subject: [PATCH 1/4] Add fixed-topology portal layer refinement --- lib/DuplicateCongestedPortSolver.ts | 10 + lib/compat/convertToSerializedHyperGraph.ts | 6 +- lib/compat/loadSerializedHyperGraph.ts | 96 ++ lib/computeRegionCost.ts | 18 +- lib/core.ts | 19 + ...topology-portal-layer-refinement-solver.ts | 971 ++++++++++++++++++ lib/index.ts | 3 + .../TinyHyperGraphSectionPipelineSolver.ts | 59 +- lib/section-solver/index.ts | 4 + lib/types.ts | 28 + ...-topology-portal-layer-refinement.snap.svg | 156 +++ ...d-topology-portal-layer-refinement.test.ts | 217 ++++ tests/solver/section-solver.test.ts | 9 +- 13 files changed, 1569 insertions(+), 27 deletions(-) create mode 100644 lib/fixed-topology-portal-layer-refinement-solver.ts create mode 100644 tests/solver/__snapshots__/fixed-topology-portal-layer-refinement.snap.svg create mode 100644 tests/solver/fixed-topology-portal-layer-refinement.test.ts diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index e9f0f08..a607c9a 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -274,6 +274,12 @@ const createSingleRouteProblem = ( problem.portPenalty === undefined ? undefined : new Float64Array(problem.portPenalty), + portalLayerRefinementLockedRouteMask: + problem.portalLayerRefinementLockedRouteMask === undefined + ? undefined + : Int8Array.from([ + problem.portalLayerRefinementLockedRouteMask[routeId] ?? 0, + ]), }) const getUsedPortIdsForSolvedRoute = ( @@ -432,6 +438,10 @@ export class DuplicateCongestedPortSolver extends BaseSolver { duplicatedPortData.duplicatePortUseCount = useCount duplicatedPortData.duplicatePortProximity = duplicatePortProximity duplicatedPortData.repairReason = "congested-port" + if (typeof duplicatedPortData.physicalPortGroupId === "string") { + duplicatedPortData.physicalPortGroupId = + `${duplicatedPortData.physicalPortGroupId}::duplicate-${duplicateIndex}` + } ports.push({ ...sourcePort, diff --git a/lib/compat/convertToSerializedHyperGraph.ts b/lib/compat/convertToSerializedHyperGraph.ts index 26a2fcb..3eaf41b 100644 --- a/lib/compat/convertToSerializedHyperGraph.ts +++ b/lib/compat/convertToSerializedHyperGraph.ts @@ -299,13 +299,17 @@ const getSerializedConnection = ( ? routeMetadata.mutuallyConnectedNetworkId : undefined + const portalLayerRefinementLocked = + solver.problem.portalLayerRefinementLockedRouteMask?.[routeId] === 1 + return { connectionId: metadataConnectionId, startRegionId: metadataStartRegionId ?? startRegionId, endRegionId: metadataEndRegionId ?? endRegionId, mutuallyConnectedNetworkId: metadataNetworkId ?? `net-${solver.problem.routeNet[routeId]}`, - } + ...(portalLayerRefinementLocked && { portalLayerRefinementLocked: true }), + } as SerializedConnection } const getSerializedSolvedRoute = ( diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 9d55265..52d904c 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -213,6 +213,27 @@ const getSerializedPortY = ( port: SerializedHyperGraph["ports"][number], ): number => Number(port.d?.y ?? 0) +const getSerializedPhysicalPortGroupId = ( + port: SerializedHyperGraph["ports"][number], +): string | undefined => { + const physicalPortGroupId = port.d?.physicalPortGroupId + return typeof physicalPortGroupId === "string" && + physicalPortGroupId.length > 0 + ? physicalPortGroupId + : undefined +} + +const getConnectionPortalLayerRefinementLock = ( + connection: NonNullable[number], +): boolean | undefined => { + const value = ( + connection as typeof connection & { + portalLayerRefinementLocked?: unknown + } + ).portalLayerRefinementLocked + return typeof value === "boolean" ? value : undefined +} + const computePortAngle = ( port: SerializedHyperGraph["ports"][number], region: SerializedHyperGraph["regions"][number] | undefined, @@ -390,6 +411,27 @@ export const loadSerializedHyperGraph = ( const portX = new Float64Array(portCount) const portY = new Float64Array(portCount) const portZ = new Int32Array(portCount) + const physicalGroupIds = [ + ...new Set( + filteredHyperGraph.ports + .map(getSerializedPhysicalPortGroupId) + .filter((groupId): groupId is string => groupId !== undefined), + ), + ].sort() + const physicalGroupIdToIndex = new Map( + physicalGroupIds.map((groupId, groupIndex) => [groupId, groupIndex]), + ) + const portPhysicalGroupId = new Int32Array(portCount).fill(-1) + const physicalGroupLayerCount = Math.max( + 1, + ...filteredHyperGraph.ports.map((port) => getSerializedPortZ(port) + 1), + ) + const physicalGroupPortIdByZ = new Int32Array( + physicalGroupIds.length * physicalGroupLayerCount, + ).fill(-1) + const physicalGroupX = new Float64Array(physicalGroupIds.length) + const physicalGroupY = new Float64Array(physicalGroupIds.length) + const physicalGroupHasCoordinates = new Int8Array(physicalGroupIds.length) filteredHyperGraph.ports.forEach((port, portIndex) => { const region1Index = regionIdToIndex.get(port.region1Id) @@ -405,6 +447,40 @@ export const loadSerializedHyperGraph = ( portX[portIndex] = getSerializedPortX(port) portY[portIndex] = getSerializedPortY(port) portZ[portIndex] = getSerializedPortZ(port) + const serializedPhysicalGroupId = getSerializedPhysicalPortGroupId(port) + if (serializedPhysicalGroupId !== undefined) { + const physicalGroupId = physicalGroupIdToIndex.get( + serializedPhysicalGroupId, + ) + if (physicalGroupId === undefined) { + throw new Error( + `Port "${port.portId}" references an unknown physical portal group`, + ) + } + const lookupIndex = + physicalGroupId * physicalGroupLayerCount + portZ[portIndex] + if (physicalGroupPortIdByZ[lookupIndex] !== -1) { + throw new Error( + `Physical portal group "${serializedPhysicalGroupId}" has multiple z${portZ[portIndex]} copies`, + ) + } + if (physicalGroupHasCoordinates[physicalGroupId] === 1) { + if ( + Math.abs(physicalGroupX[physicalGroupId] - portX[portIndex]) > 1e-9 || + Math.abs(physicalGroupY[physicalGroupId] - portY[portIndex]) > 1e-9 + ) { + throw new Error( + `Physical portal group "${serializedPhysicalGroupId}" contains different XY positions`, + ) + } + } else { + physicalGroupX[physicalGroupId] = portX[portIndex] + physicalGroupY[physicalGroupId] = portY[portIndex] + physicalGroupHasCoordinates[physicalGroupId] = 1 + } + portPhysicalGroupId[portIndex] = physicalGroupId + physicalGroupPortIdByZ[lookupIndex] = portIndex + } portAngleForRegion1[portIndex] = computePortAngle( port, filteredHyperGraph.regions[region1Index], @@ -561,6 +637,10 @@ export const loadSerializedHyperGraph = ( portX, portY, portZ, + portPhysicalGroupId, + physicalPortalGroupCount: physicalGroupIds.length, + physicalGroupPortIdByZ, + physicalGroupLayerCount, portMetadata, } @@ -597,6 +677,21 @@ export const loadSerializedHyperGraph = ( } }), ) + const initiallyAssignedRouteIds = new Set( + initialAssignments.map((assignment) => assignment.routeId), + ) + const portalLayerRefinementLockedRouteMask = Int8Array.from( + routableConnections, + ({ connection, solvedRoute }, routeId) => { + const explicitLock = getConnectionPortalLayerRefinementLock(connection) + return explicitLock === true || + (explicitLock === undefined && + solvedRoute === undefined && + initiallyAssignedRouteIds.has(routeId)) + ? 1 + : 0 + }, + ) const problem: TinyHyperGraphProblem = { routeCount, @@ -607,6 +702,7 @@ export const loadSerializedHyperGraph = ( routeNet, regionNetId, ...(initialAssignments.length > 0 && { initialAssignments }), + portalLayerRefinementLockedRouteMask, } const solvedRoutePathSegments: TinyHyperGraphSolution["solvedRoutePathSegments"] = diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 5100584..3f27c55 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -7,6 +7,15 @@ export const isKnownSingleLayerMask = (regionAvailableZMask: number) => regionAvailableZMask > 0 && (regionAvailableZMask & (regionAvailableZMask - 1)) === 0 +export const computeEstimatedViaDemand = ( + numSameLayerIntersections: number, + numCrossLayerIntersections: number, + numEntryExitChanges: number, +) => + numSameLayerIntersections * 2 + + numCrossLayerIntersections + + numEntryExitChanges + export const computeRegionCost = ( regionWidth: number, regionHeight: number, @@ -39,10 +48,11 @@ export const computeRegionCostForArea = ( regionAvailableZMask = 0, minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER, ) => { - const estViasRequired = - numSameLayerIntersections * 2 + - numCrossLayerIntersections * 1 + - numEntryExitChanges * 1 + const estViasRequired = computeEstimatedViaDemand( + numSameLayerIntersections, + numCrossLayerIntersections, + numEntryExitChanges, + ) const viaSizeWithMargin = minViaPadDiameter + TRACE_VIA_MARGIN const viaSizeWithMarginSq = viaSizeWithMargin ** 2 diff --git a/lib/core.ts b/lib/core.ts index c224a31..4c142be 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -119,6 +119,19 @@ export interface TinyHyperGraphTopology { portY: Float64Array portZ: Int32Array + /** + * Numeric physical portal-group metadata. Ports without a replaceable + * physical portal group contain -1. + */ + portPhysicalGroupId?: Int32Array + physicalPortalGroupCount?: number + /** + * Flat lookup indexed by physicalGroupId * physicalGroupLayerCount + z. + * Missing group/layer copies contain -1. + */ + physicalGroupPortIdByZ?: Int32Array + physicalGroupLayerCount?: number + portMetadata?: any[] } @@ -153,6 +166,12 @@ export interface TinyHyperGraphProblem { * state and may be ripped and rerouted by the normal solver machinery. */ initialAssignments?: TinyHyperGraphInitialAssignment[] + + /** + * Routes whose serialized initial assignments or other fixed topology must + * not be changed by fixed-topology portal-layer refinement. + */ + portalLayerRefinementLockedRouteMask?: Int8Array } export interface TinyHyperGraphProblemSetup { diff --git a/lib/fixed-topology-portal-layer-refinement-solver.ts b/lib/fixed-topology-portal-layer-refinement-solver.ts new file mode 100644 index 0000000..da69fc3 --- /dev/null +++ b/lib/fixed-topology-portal-layer-refinement-solver.ts @@ -0,0 +1,971 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" +import { + computeEstimatedViaDemand, +} from "./computeRegionCost" +import { + createEmptyRegionIntersectionCache, + TinyHyperGraphSolver, + type RegionCostSummary, + type TinyHyperGraphProblem, + type TinyHyperGraphSolution, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, +} from "./core" +import { countNewIntersectionsWithValues } from "./countNewIntersections" +import type { + FixedTopologyPortalLayerRefinementStats, + PhysicalPortGroupId, + PortalLayerChange, + PortId, + RegionId, + RegionIntersectionCache, + RouteId, +} from "./types" +import { visualizeTinyGraph } from "./visualizeTinyGraph" + +const REGION_COST_EPSILON = 1e-12 + +interface RoutePlan { + routeId: RouteId + orderedPortIds: PortId[] + orderedRegionIds: RegionId[] + physicalGroupIds: Int32Array + reducibleTransitionCount: number +} + +interface DpCandidate { + portIds: PortId[] + predictedViaDemand: number + entryExitLayerChanges: number + portPenalty: number +} + +interface StateSummary extends RegionCostSummary { + predictedViaDemand: number + entryExitLayerChanges: number +} + +export interface FixedTopologyPortalLayerRefinementSolverOptions + extends TinyHyperGraphSolverOptions { + regionCostEpsilon?: number +} + +const getReplaySolverOptions = ( + options?: TinyHyperGraphSolverOptions, +): TinyHyperGraphSolverOptions => ({ + ...options, + USE_LAZY_ROUTE_HEURISTIC: true, + USE_SPARSE_CANDIDATE_STORAGE: true, + STATIC_REACHABILITY_PRECHECK: false, +}) + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null + +const cloneRegionIntersectionCache = ( + cache: RegionIntersectionCache, +): RegionIntersectionCache => ({ + netIds: new Int32Array(cache.netIds), + lesserAngles: new Int32Array(cache.lesserAngles), + greaterAngles: new Int32Array(cache.greaterAngles), + layerMasks: new Int32Array(cache.layerMasks), + existingCrossingLayerIntersections: + cache.existingCrossingLayerIntersections, + existingSameLayerIntersections: cache.existingSameLayerIntersections, + existingEntryExitLayerChanges: cache.existingEntryExitLayerChanges, + existingRegionCost: cache.existingRegionCost, + existingSegmentCount: cache.existingSegmentCount, +}) + +const getSharedRegionId = ( + topology: TinyHyperGraphTopology, + fromPortId: PortId, + toPortId: PortId, +): RegionId => { + const sharedRegionIds = (topology.incidentPortRegion[fromPortId] ?? []).filter( + (regionId) => + (topology.incidentPortRegion[toPortId] ?? []).includes(regionId), + ) + + if (sharedRegionIds.length !== 1) { + throw new Error( + `Solved segment ${fromPortId}->${toPortId} does not identify exactly one region`, + ) + } + + return sharedRegionIds[0]! +} + +const getOrderedRoutePlan = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, + routeId: RouteId, +): RoutePlan => { + 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) { + throw new Error(`Route ${routeId} has no solved path`) + } + + return { + routeId, + orderedPortIds: [startPortId], + orderedRegionIds: [], + physicalGroupIds: Int32Array.from([ + topology.portPhysicalGroupId?.[startPortId] ?? -1, + ]), + reducibleTransitionCount: 0, + } + } + + const segmentsByPort = new Map< + PortId, + Array<{ + segmentIndex: number + fromPortId: PortId + toPortId: PortId + regionId: RegionId + }> + >() + + routeSegments.forEach(([fromPortId, toPortId], segmentIndex) => { + const regionId = + routeSegmentRegionIds[segmentIndex] ?? + getSharedRegionId(topology, fromPortId, toPortId) + const segment = { + segmentIndex, + fromPortId, + toPortId, + regionId, + } + const fromSegments = segmentsByPort.get(fromPortId) ?? [] + fromSegments.push(segment) + segmentsByPort.set(fromPortId, fromSegments) + const toSegments = segmentsByPort.get(toPortId) ?? [] + toSegments.push(segment) + segmentsByPort.set(toPortId, toSegments) + }) + + const orderedPortIds = [startPortId] + const orderedRegionIds: RegionId[] = [] + const usedSegmentIndices = new Set() + let currentPortId = startPortId + + while (currentPortId !== endPortId) { + const nextSegments = (segmentsByPort.get(currentPortId) ?? []).filter( + ({ segmentIndex }) => !usedSegmentIndices.has(segmentIndex), + ) + + if (nextSegments.length !== 1) { + throw new Error( + `Route ${routeId} is not a single fixed path at port ${currentPortId}`, + ) + } + + const nextSegment = nextSegments[0]! + const nextPortId = + nextSegment.fromPortId === currentPortId + ? nextSegment.toPortId + : nextSegment.fromPortId + usedSegmentIndices.add(nextSegment.segmentIndex) + orderedRegionIds.push(nextSegment.regionId) + orderedPortIds.push(nextPortId) + currentPortId = nextPortId + } + + if (usedSegmentIndices.size !== routeSegments.length) { + throw new Error(`Route ${routeId} contains disconnected solved segments`) + } + + const physicalGroupIds = Int32Array.from(orderedPortIds, (portId) => + Number(topology.portPhysicalGroupId?.[portId] ?? -1), + ) + let reducibleTransitionCount = 0 + for (let portIndex = 1; portIndex < orderedPortIds.length; portIndex++) { + const fromPortId = orderedPortIds[portIndex - 1]! + const toPortId = orderedPortIds[portIndex]! + if ( + topology.portZ[fromPortId] !== topology.portZ[toPortId] && + (physicalGroupIds[portIndex - 1]! >= 0 || + physicalGroupIds[portIndex]! >= 0) + ) { + reducibleTransitionCount += 1 + } + } + + return { + routeId, + orderedPortIds, + orderedRegionIds, + physicalGroupIds, + reducibleTransitionCount, + } +} + +const resetSolvedState = (solver: TinyHyperGraphSolver) => { + 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.regionCongestionCost.fill(0) + 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 +} + +const appendRoutePlanToSolver = ( + solver: TinyHyperGraphSolver, + routePlan: RoutePlan, +) => { + const routeNet = solver.problem.routeNet[routePlan.routeId]! + solver.state.currentRouteNetId = routeNet + + for ( + let segmentIndex = 0; + segmentIndex < routePlan.orderedRegionIds.length; + segmentIndex++ + ) { + const regionId = routePlan.orderedRegionIds[segmentIndex]! + const fromPortId = routePlan.orderedPortIds[segmentIndex]! + const toPortId = routePlan.orderedPortIds[segmentIndex + 1]! + solver.state.regionSegments[regionId]!.push([ + routePlan.routeId, + fromPortId, + toPortId, + ]) + solver.state.portAssignment[fromPortId] = routeNet + solver.state.portAssignment[toPortId] = routeNet + solver.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } +} + +const createSolvedSolver = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + routePlans: RoutePlan[], + options?: TinyHyperGraphSolverOptions, +) => { + const solver = new TinyHyperGraphSolver( + topology, + problem, + getReplaySolverOptions(options), + ) + resetSolvedState(solver) + for (const routePlan of routePlans) { + appendRoutePlanToSolver(solver, routePlan) + } + solver.state.currentRouteNetId = undefined + solver.solved = true + solver.failed = false + solver.error = null + return solver +} + +const summarizeState = (solver: TinyHyperGraphSolver): StateSummary => { + let maxRegionCost = 0 + let totalRegionCost = 0 + let predictedViaDemand = 0 + let entryExitLayerChanges = 0 + + for (const cache of solver.state.regionIntersectionCaches) { + maxRegionCost = Math.max(maxRegionCost, cache.existingRegionCost) + totalRegionCost += cache.existingRegionCost + predictedViaDemand += computeEstimatedViaDemand( + cache.existingSameLayerIntersections, + cache.existingCrossingLayerIntersections, + cache.existingEntryExitLayerChanges, + ) + entryExitLayerChanges += cache.existingEntryExitLayerChanges + } + + return { + maxRegionCost, + totalRegionCost, + predictedViaDemand, + entryExitLayerChanges, + } +} + +const comparePortSequences = ( + topology: TinyHyperGraphTopology, + left: PortId[], + right: PortId[], +) => { + const length = Math.min(left.length, right.length) + for (let index = 0; index < length; index++) { + const leftPortId = left[index]! + const rightPortId = right[index]! + const zDelta = topology.portZ[leftPortId]! - topology.portZ[rightPortId]! + if (zDelta !== 0) return zDelta + if (leftPortId !== rightPortId) return leftPortId - rightPortId + } + return left.length - right.length +} + +const compareDpCandidates = ( + topology: TinyHyperGraphTopology, + left: DpCandidate, + right: DpCandidate, +) => { + if (left.predictedViaDemand !== right.predictedViaDemand) { + return left.predictedViaDemand - right.predictedViaDemand + } + if (left.entryExitLayerChanges !== right.entryExitLayerChanges) { + return left.entryExitLayerChanges - right.entryExitLayerChanges + } + if (left.portPenalty !== right.portPenalty) { + return left.portPenalty - right.portPenalty + } + return comparePortSequences(topology, left.portIds, right.portIds) +} + +const regionSupportsZ = ( + topology: TinyHyperGraphTopology, + regionId: RegionId, + z: number, +) => { + const availableZMask = topology.regionAvailableZMask?.[regionId] ?? 0 + return availableZMask === 0 || (availableZMask & (1 << z)) !== 0 +} + +const isAssignableViaRegion = ( + topology: TinyHyperGraphTopology, + regionId: RegionId, +) => { + const metadata = topology.regionMetadata?.[regionId] + return isRecord(metadata) && metadata._assignableVia === true +} + +export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { + override MAX_ITERATIONS = 1 + + refinedSolver: TinyHyperGraphSolver + scratchSolver: TinyHyperGraphSolver + routePlans: RoutePlan[] + usedRouteIdsByPort: Array> + routeQueue: RouteId[] = [] + queuedRouteIds = new Set() + eligibleRouteIds = new Set() + lastAcceptedRegionIds: RegionId[] = [] + refinementStartTime = 0 + touchedRegionIds = new Set() + changes: PortalLayerChange[] = [] + private scratchRegionIds = new Set() + + private refinementStats: FixedTopologyPortalLayerRefinementStats = { + physicalPortalGroupCount: 0, + eligibleRouteCount: 0, + routesConsidered: 0, + routesImproved: 0, + predictedViaDemandBefore: 0, + predictedViaDemandAfter: 0, + entryExitLayerChangesBefore: 0, + entryExitLayerChangesAfter: 0, + candidateCount: 0, + acceptedCandidateCount: 0, + touchedRegionCount: 0, + rejectedForRegionCostCount: 0, + rejectedForPortConflictCount: 0, + rejectedForLockedAssignmentCount: 0, + portalLayerRefinementMs: 0, + } + + constructor( + public topology: TinyHyperGraphTopology, + public problem: TinyHyperGraphProblem, + public solution: TinyHyperGraphSolution, + public options: FixedTopologyPortalLayerRefinementSolverOptions = {}, + ) { + super() + this.routePlans = Array.from({ length: problem.routeCount }, (_, routeId) => + getOrderedRoutePlan(topology, problem, solution, routeId), + ) + this.usedRouteIdsByPort = Array.from( + { length: topology.portCount }, + () => new Set(), + ) + for (const routePlan of this.routePlans) { + for (const portId of routePlan.orderedPortIds) { + this.usedRouteIdsByPort[portId]!.add(routePlan.routeId) + } + } + this.refinedSolver = createSolvedSolver( + topology, + problem, + this.routePlans, + options, + ) + this.scratchSolver = new TinyHyperGraphSolver( + topology, + problem, + getReplaySolverOptions(options), + ) + resetSolvedState(this.scratchSolver) + } + + override _setup() { + this.refinementStartTime = performance.now() + const initialSummary = summarizeState(this.refinedSolver) + this.refinementStats.physicalPortalGroupCount = + this.topology.physicalPortalGroupCount ?? 0 + this.refinementStats.predictedViaDemandBefore = + initialSummary.predictedViaDemand + this.refinementStats.predictedViaDemandAfter = + initialSummary.predictedViaDemand + this.refinementStats.entryExitLayerChangesBefore = + initialSummary.entryExitLayerChanges + this.refinementStats.entryExitLayerChangesAfter = + initialSummary.entryExitLayerChanges + + const usedRouteIdsByPort = this.getUsedRouteIdsByPort() + this.routeQueue = this.routePlans + .filter((routePlan) => + this.isEligibleRoute(routePlan, usedRouteIdsByPort), + ) + .sort((left, right) => this.compareRoutePlans(left, right)) + .map(({ routeId }) => routeId) + this.queuedRouteIds = new Set(this.routeQueue) + this.eligibleRouteIds = new Set(this.routeQueue) + this.refinementStats.eligibleRouteCount = this.routeQueue.length + this.MAX_ITERATIONS = + this.problem.routeCount * (initialSummary.predictedViaDemand + 1) + + this.problem.routeCount + + 1 + + if (this.routeQueue.length === 0) { + this.finish() + } + } + + private compareRoutePlans(left: RoutePlan, right: RoutePlan) { + return ( + right.reducibleTransitionCount - left.reducibleTransitionCount || + right.orderedPortIds.length - left.orderedPortIds.length || + left.routeId - right.routeId + ) + } + + private isEligibleRoute( + routePlan: RoutePlan, + usedRouteIdsByPort: Array>, + ) { + if ( + this.problem.portalLayerRefinementLockedRouteMask?.[routePlan.routeId] === + 1 + ) { + this.refinementStats.rejectedForLockedAssignmentCount += 1 + return false + } + + if ( + routePlan.orderedRegionIds.some((regionId) => + isAssignableViaRegion(this.topology, regionId), + ) + ) { + this.refinementStats.rejectedForLockedAssignmentCount += 1 + return false + } + + for ( + let portIndex = 1; + portIndex < routePlan.orderedPortIds.length - 1; + portIndex++ + ) { + if ( + routePlan.physicalGroupIds[portIndex]! >= 0 && + this.getAlternativePorts( + routePlan, + portIndex, + usedRouteIdsByPort, + ).length > 1 + ) { + return true + } + } + + return false + } + + private getUsedRouteIdsByPort() { + return this.usedRouteIdsByPort + } + + private getAlternativePorts( + routePlan: RoutePlan, + portIndex: number, + usedRouteIdsByPort: Array>, + ) { + const currentPortId = routePlan.orderedPortIds[portIndex]! + const physicalGroupId = routePlan.physicalGroupIds[portIndex]! + const groupPortIdByZ = this.topology.physicalGroupPortIdByZ + const layerCount = this.topology.physicalGroupLayerCount ?? 0 + + if (physicalGroupId < 0 || !groupPortIdByZ || layerCount === 0) { + return [currentPortId] + } + + const previousRegionId = routePlan.orderedRegionIds[portIndex - 1] + const nextRegionId = routePlan.orderedRegionIds[portIndex] + if (previousRegionId === undefined || nextRegionId === undefined) { + return [currentPortId] + } + + const routeNetId = this.problem.routeNet[routePlan.routeId]! + const alternatives: PortId[] = [] + + for (let z = 0; z < layerCount; z++) { + const portId = groupPortIdByZ[physicalGroupId * layerCount + z] ?? -1 + if (portId < 0) continue + if (portId === currentPortId) { + alternatives.push(portId) + continue + } + + const incidentRegionIds = this.topology.incidentPortRegion[portId] ?? [] + if ( + !incidentRegionIds.includes(previousRegionId) || + !incidentRegionIds.includes(nextRegionId) || + this.problem.portSectionMask[portId] !== 1 || + !regionSupportsZ(this.topology, previousRegionId, z) || + !regionSupportsZ(this.topology, nextRegionId, z) + ) { + continue + } + + const endpointReservationNetId = + this.refinedSolver.problemSetup.portEndpointReservationNetId[portId] ?? + -1 + if ( + endpointReservationNetId !== -1 && + endpointReservationNetId !== routeNetId + ) { + continue + } + + if ( + [...usedRouteIdsByPort[portId]!].some( + (usedRouteId) => usedRouteId !== routePlan.routeId, + ) + ) { + continue + } + + alternatives.push(portId) + } + + if (!alternatives.includes(currentPortId)) { + alternatives.push(currentPortId) + } + alternatives.sort( + (left, right) => + this.topology.portZ[left]! - this.topology.portZ[right]! || + left - right, + ) + return alternatives + } + + private createBackgroundSolver(routeId: RouteId, regionIds: Set) { + const solver = this.scratchSolver + const regionIdsToClear = new Set([...this.scratchRegionIds, ...regionIds]) + for (const regionId of regionIdsToClear) { + solver.state.regionSegments[regionId] = [] + solver.state.regionIntersectionCaches[regionId] = + createEmptyRegionIntersectionCache() + } + this.scratchRegionIds = new Set(regionIds) + + for (const regionId of regionIds) { + for (const [segmentRouteId, fromPortId, toPortId] of this.refinedSolver + .state.regionSegments[regionId] ?? []) { + if (segmentRouteId === routeId) continue + solver.state.currentRouteNetId = + this.problem.routeNet[segmentRouteId]! + solver.state.regionSegments[regionId]!.push([ + segmentRouteId, + fromPortId, + toPortId, + ]) + solver.appendSegmentToRegionCache(regionId, fromPortId, toPortId) + } + } + + solver.state.currentRouteNetId = undefined + return solver + } + + private getBestCandidate(routePlan: RoutePlan): DpCandidate | undefined { + const usedRouteIdsByPort = this.getUsedRouteIdsByPort() + const domains = routePlan.orderedPortIds.map((currentPortId, portIndex) => + portIndex === 0 || portIndex === routePlan.orderedPortIds.length - 1 + ? [currentPortId] + : this.getAlternativePorts( + routePlan, + portIndex, + usedRouteIdsByPort, + ), + ) + const touchedRegionIds = new Set(routePlan.orderedRegionIds) + const backgroundSolver = this.createBackgroundSolver( + routePlan.routeId, + touchedRegionIds, + ) + const routeNetId = this.problem.routeNet[routePlan.routeId]! + let candidatesByPort = new Map() + const startPortId = domains[0]![0]! + candidatesByPort.set(startPortId, { + portIds: [startPortId], + predictedViaDemand: 0, + entryExitLayerChanges: 0, + portPenalty: this.problem.portPenalty?.[startPortId] ?? 0, + }) + + for ( + let segmentIndex = 0; + segmentIndex < routePlan.orderedRegionIds.length; + segmentIndex++ + ) { + const regionId = routePlan.orderedRegionIds[segmentIndex]! + const nextCandidatesByPort = new Map() + + for (const previousCandidate of candidatesByPort.values()) { + const fromPortId = + previousCandidate.portIds[previousCandidate.portIds.length - 1]! + for (const toPortId of domains[segmentIndex + 1]!) { + this.refinementStats.candidateCount += 1 + const geometry = backgroundSolver.populateSegmentGeometryScratch( + regionId, + fromPortId, + toPortId, + ) + const [sameLayer, crossLayer, entryExit] = + countNewIntersectionsWithValues( + backgroundSolver.state.regionIntersectionCaches[regionId]!, + routeNetId, + geometry.lesserAngle, + geometry.greaterAngle, + geometry.layerMask, + geometry.entryExitLayerChanges, + ) + const candidate: DpCandidate = { + portIds: [...previousCandidate.portIds, toPortId], + predictedViaDemand: + previousCandidate.predictedViaDemand + + computeEstimatedViaDemand(sameLayer, crossLayer, entryExit), + entryExitLayerChanges: + previousCandidate.entryExitLayerChanges + entryExit, + portPenalty: + previousCandidate.portPenalty + + (this.problem.portPenalty?.[toPortId] ?? 0), + } + const existingCandidate = nextCandidatesByPort.get(toPortId) + if ( + !existingCandidate || + compareDpCandidates( + this.topology, + candidate, + existingCandidate, + ) < 0 + ) { + nextCandidatesByPort.set(toPortId, candidate) + } + } + } + + candidatesByPort = nextCandidatesByPort + } + + return [...candidatesByPort.values()].sort((left, right) => + compareDpCandidates(this.topology, left, right), + )[0] + } + + private rebuildRegionCache(regionId: RegionId) { + this.refinedSolver.state.regionIntersectionCaches[regionId] = + createEmptyRegionIntersectionCache() + for (const [routeId, fromPortId, toPortId] of this.refinedSolver.state + .regionSegments[regionId] ?? []) { + this.refinedSolver.state.currentRouteNetId = + this.problem.routeNet[routeId]! + this.refinedSolver.appendSegmentToRegionCache( + regionId, + fromPortId, + toPortId, + ) + } + this.refinedSolver.state.currentRouteNetId = undefined + } + + private recomputePortAssignments(): boolean { + const routeIdsByPort = Array.from( + { length: this.topology.portCount }, + () => new Set(), + ) + this.refinedSolver.state.portAssignment.fill(-1) + + for (const regionSegments of this.refinedSolver.state.regionSegments) { + for (const [routeId, fromPortId, toPortId] of regionSegments) { + routeIdsByPort[fromPortId]!.add(routeId) + routeIdsByPort[toPortId]!.add(routeId) + } + } + + for (let portId = 0; portId < routeIdsByPort.length; portId++) { + const routeIds = routeIdsByPort[portId]! + if (routeIds.size === 0) continue + const netIds = new Set( + [...routeIds].map((routeId) => this.problem.routeNet[routeId]!), + ) + if (netIds.size > 1) { + return false + } + this.refinedSolver.state.portAssignment[portId] = [...netIds][0]! + } + + return true + } + + private tryCandidate(routePlan: RoutePlan, candidate: DpCandidate) { + this.lastAcceptedRegionIds = [] + if ( + candidate.portIds.every( + (portId, portIndex) => + portId === routePlan.orderedPortIds[portIndex], + ) + ) { + return false + } + + const baselineSummary = summarizeState(this.refinedSolver) + const touchedRegionIds = [...new Set(routePlan.orderedRegionIds)] + const savedSegments = new Map( + touchedRegionIds.map((regionId) => [ + regionId, + this.refinedSolver.state.regionSegments[regionId]!.map( + ([routeId, fromPortId, toPortId]) => + [routeId, fromPortId, toPortId] as [ + RouteId, + PortId, + PortId, + ], + ), + ]), + ) + const savedCaches = new Map( + touchedRegionIds.map((regionId) => [ + regionId, + cloneRegionIntersectionCache( + this.refinedSolver.state.regionIntersectionCaches[regionId]!, + ), + ]), + ) + const savedPortAssignment = new Int32Array( + this.refinedSolver.state.portAssignment, + ) + + for (const regionId of touchedRegionIds) { + this.refinedSolver.state.regionSegments[regionId] = this.refinedSolver + .state.regionSegments[regionId]!.filter( + ([routeId]) => routeId !== routePlan.routeId, + ) + } + for ( + let segmentIndex = 0; + segmentIndex < routePlan.orderedRegionIds.length; + segmentIndex++ + ) { + const regionId = routePlan.orderedRegionIds[segmentIndex]! + this.refinedSolver.state.regionSegments[regionId]!.push([ + routePlan.routeId, + candidate.portIds[segmentIndex]!, + candidate.portIds[segmentIndex + 1]!, + ]) + } + for (const regionId of touchedRegionIds) { + this.rebuildRegionCache(regionId) + } + + const hasValidPortAssignments = this.recomputePortAssignments() + const candidateSummary = summarizeState(this.refinedSolver) + const epsilon = this.options.regionCostEpsilon ?? REGION_COST_EPSILON + const regionCostDidNotWorsen = + candidateSummary.maxRegionCost <= + baselineSummary.maxRegionCost + epsilon && + candidateSummary.totalRegionCost <= + baselineSummary.totalRegionCost + epsilon + const viaDemandImproved = + candidateSummary.predictedViaDemand < + baselineSummary.predictedViaDemand + + if ( + !hasValidPortAssignments || + !regionCostDidNotWorsen || + !viaDemandImproved + ) { + for (const regionId of touchedRegionIds) { + this.refinedSolver.state.regionSegments[regionId] = + savedSegments.get(regionId)! + this.refinedSolver.state.regionIntersectionCaches[regionId] = + savedCaches.get(regionId)! + } + this.refinedSolver.state.portAssignment = savedPortAssignment + if (!hasValidPortAssignments) { + this.refinementStats.rejectedForPortConflictCount += 1 + } else if (!regionCostDidNotWorsen) { + this.refinementStats.rejectedForRegionCostCount += 1 + } + return false + } + + for ( + let portIndex = 1; + portIndex < routePlan.orderedPortIds.length - 1; + portIndex++ + ) { + const fromPortId = routePlan.orderedPortIds[portIndex]! + const toPortId = candidate.portIds[portIndex]! + if (fromPortId === toPortId) continue + this.changes.push({ + routeId: routePlan.routeId, + physicalPortGroupId: + routePlan.physicalGroupIds[portIndex]! as PhysicalPortGroupId, + fromPortId, + toPortId, + fromZ: this.topology.portZ[fromPortId]!, + toZ: this.topology.portZ[toPortId]!, + }) + } + for (const portId of new Set(routePlan.orderedPortIds)) { + this.usedRouteIdsByPort[portId]!.delete(routePlan.routeId) + } + for (const portId of new Set(candidate.portIds)) { + this.usedRouteIdsByPort[portId]!.add(routePlan.routeId) + } + routePlan.orderedPortIds = candidate.portIds + this.refinementStats.routesImproved += 1 + this.refinementStats.acceptedCandidateCount += 1 + this.refinementStats.predictedViaDemandAfter = + candidateSummary.predictedViaDemand + this.refinementStats.entryExitLayerChangesAfter = + candidateSummary.entryExitLayerChanges + for (const regionId of touchedRegionIds) { + this.touchedRegionIds.add(regionId) + } + this.lastAcceptedRegionIds = touchedRegionIds + return true + } + + override _step() { + const routeId = this.routeQueue.shift() + if (routeId === undefined) { + this.finish() + return + } + this.queuedRouteIds.delete(routeId) + const routePlan = this.routePlans[routeId]! + this.refinementStats.routesConsidered += 1 + const candidate = this.getBestCandidate(routePlan) + if (candidate && this.tryCandidate(routePlan, candidate)) { + const affectedRouteIds = new Set([routeId]) + for (const regionId of this.lastAcceptedRegionIds) { + for (const [affectedRouteId] of this.refinedSolver.state + .regionSegments[regionId] ?? []) { + affectedRouteIds.add(affectedRouteId) + } + } + for (const affectedRouteId of affectedRouteIds) { + if ( + this.eligibleRouteIds.has(affectedRouteId) && + !this.queuedRouteIds.has(affectedRouteId) + ) { + this.routeQueue.push(affectedRouteId) + this.queuedRouteIds.add(affectedRouteId) + } + } + this.routeQueue.sort((leftRouteId, rightRouteId) => + this.compareRoutePlans( + this.routePlans[leftRouteId]!, + this.routePlans[rightRouteId]!, + ), + ) + } + this.updateStats() + } + + private updateStats() { + this.refinementStats.touchedRegionCount = this.touchedRegionIds.size + this.refinementStats.portalLayerRefinementMs = + performance.now() - this.refinementStartTime + this.stats = { ...this.refinementStats } + } + + private finish() { + const finalSummary = summarizeState(this.refinedSolver) + this.refinementStats.predictedViaDemandAfter = + finalSummary.predictedViaDemand + this.refinementStats.entryExitLayerChangesAfter = + finalSummary.entryExitLayerChanges + this.updateStats() + this.refinedSolver.solved = true + this.refinedSolver.failed = false + this.refinedSolver.error = null + this.solved = true + this.failed = false + this.error = null + } + + getRefinedSolver() { + return this.refinedSolver + } + + override getOutput() { + if (!this.solved || this.failed) { + throw new Error( + "FixedTopologyPortalLayerRefinementSolver has no solved output", + ) + } + return convertToSerializedHyperGraph(this.refinedSolver) + } + + override visualize(): GraphicsObject { + const graphics = visualizeTinyGraph(this.refinedSolver) + graphics.circles ??= [] + graphics.texts ??= [] + + for (const change of this.changes) { + const x = this.topology.portX[change.toPortId]! + const y = this.topology.portY[change.toPortId]! + graphics.circles.push({ + center: { x, y }, + radius: 0.11, + fill: "rgba(16, 185, 129, 0.25)", + stroke: "rgba(5, 150, 105, 0.95)", + label: `route ${change.routeId} | group ${change.physicalPortGroupId} | z${change.fromZ}->z${change.toZ}`, + }) + } + + graphics.title = [ + "Fixed-topology portal-layer refinement", + `via demand ${this.refinementStats.predictedViaDemandBefore}->${this.refinementStats.predictedViaDemandAfter}`, + `entry/exit ${this.refinementStats.entryExitLayerChangesBefore}->${this.refinementStats.entryExitLayerChangesAfter}`, + `accepted=${this.refinementStats.acceptedCandidateCount}`, + ].join(" | ") + return graphics + } +} diff --git a/lib/index.ts b/lib/index.ts index 4b90197..0f5d09c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,9 +8,12 @@ export * from "./selective-rerip-tiny-hyper-graph-solver" export * from "./bus-solver" export * from "./region-graph" export { + computeEstimatedViaDemand, DEFAULT_MIN_VIA_PAD_DIAMETER, TRACE_VIA_MARGIN, } from "./computeRegionCost" +export * from "./types" +export * from "./fixed-topology-portal-layer-refinement-solver" export { convertPortPointPathingSolverInputToSerializedHyperGraph } from "./compat/convertPortPointPathingSolverInputToSerializedHyperGraph" export { loadSerializedHyperGraph } from "./compat/loadSerializedHyperGraph" export { diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index 9ccd4f9..ba7463f 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -9,6 +9,10 @@ import type { TinyHyperGraphTopology, } from "../core" import { TinyHyperGraphSolver } from "../core" +import { + FixedTopologyPortalLayerRefinementSolver, + type FixedTopologyPortalLayerRefinementSolverOptions, +} from "../fixed-topology-portal-layer-refinement-solver" import type { RegionId } from "../types" import type { TinyHyperGraphSectionSolverOptions } from "./index" import { getActiveSectionRouteIds, TinyHyperGraphSectionSolver } from "./index" @@ -127,6 +131,10 @@ const createProblemWithPortSectionMask = ( problem.portPenalty === undefined ? undefined : new Float64Array(problem.portPenalty), + portalLayerRefinementLockedRouteMask: + problem.portalLayerRefinementLockedRouteMask === undefined + ? undefined + : new Int8Array(problem.portalLayerRefinementLockedRouteMask), }) const getSectionMaskCandidates = ( @@ -379,6 +387,12 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver instance.getSectionStageParams(), }, + { + solverName: "refinePortalLayers", + solverClass: FixedTopologyPortalLayerRefinementSolver, + getConstructorParams: (instance: TinyHyperGraphSectionPipelineSolver) => + instance.getPortalLayerRefinementStageParams(), + }, ] getSectionStageParams(): [ @@ -473,6 +487,34 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver("optimizeSection") + + if (!optimizedSerializedHyperGraph) { + throw new Error( + "optimizeSection did not produce a solved serialized hypergraph", + ) + } + + const { topology, problem, solution } = this.loadHyperGraph( + optimizedSerializedHyperGraph, + ) + return [ + topology, + problem, + solution, + { + minViaPadDiameter: this.inputProblem.minViaPadDiameter, + }, + ] + } + getInitialVisualizationSolver() { if (!this.initialVisualizationSolver) { const { topology, problem } = this.loadHyperGraph( @@ -502,22 +544,7 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver("optimizeSection") ?? - this.getStageOutput("solveGraph") ?? - null + this.getStageOutput("refinePortalLayers") ?? 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/lib/section-solver/index.ts b/lib/section-solver/index.ts index e66f4a1..ef19bdd 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -555,6 +555,10 @@ const createSectionRoutePlans = ( problem.portPenalty === undefined ? undefined : new Float64Array(problem.portPenalty), + portalLayerRefinementLockedRouteMask: + problem.portalLayerRefinementLockedRouteMask === undefined + ? undefined + : new Int8Array(problem.portalLayerRefinementLockedRouteMask), }, routePlans, activeRouteIds, diff --git a/lib/types.ts b/lib/types.ts index b109000..d127e38 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -4,6 +4,7 @@ export type Integer = number export type RouteId = number export type NetId = number export type HopId = number +export type PhysicalPortGroupId = number /** SegmentIds are computed via port1Id * portCount + port2Id */ export type SegmentId = number @@ -32,6 +33,33 @@ export interface RegionIntersectionCache extends DynamicAnglePairArrays { existingSegmentCount: number } +export interface PortalLayerChange { + routeId: RouteId + physicalPortGroupId: PhysicalPortGroupId + fromPortId: PortId + toPortId: PortId + fromZ: number + toZ: number +} + +export interface FixedTopologyPortalLayerRefinementStats { + physicalPortalGroupCount: number + eligibleRouteCount: number + routesConsidered: number + routesImproved: number + predictedViaDemandBefore: number + predictedViaDemandAfter: number + entryExitLayerChangesBefore: number + entryExitLayerChangesAfter: number + candidateCount: number + acceptedCandidateCount: number + touchedRegionCount: number + rejectedForRegionCostCount: number + rejectedForPortConflictCount: number + rejectedForLockedAssignmentCount: number + portalLayerRefinementMs: number +} + export type SameLayerIntersectionCount = number export type CrossingLayerIntersectionCount = number export type EntryExitLayerChanges = number diff --git a/tests/solver/__snapshots__/fixed-topology-portal-layer-refinement.snap.svg b/tests/solver/__snapshots__/fixed-topology-portal-layer-refinement.snap.svg new file mode 100644 index 0000000..73e6612 --- /dev/null +++ b/tests/solver/__snapshots__/fixed-topology-portal-layer-refinement.snap.svg @@ -0,0 +1,156 @@ +before refinementafter refinement \ No newline at end of file diff --git a/tests/solver/fixed-topology-portal-layer-refinement.test.ts b/tests/solver/fixed-topology-portal-layer-refinement.test.ts new file mode 100644 index 0000000..470a885 --- /dev/null +++ b/tests/solver/fixed-topology-portal-layer-refinement.test.ts @@ -0,0 +1,217 @@ +import "bun-match-svg" +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { + getSvgFromGraphicsObject, + stackGraphicsVertically, +} from "graphics-debug" +import { + FixedTopologyPortalLayerRefinementSolver, + loadSerializedHyperGraph, +} from "lib/index" + +const createRegion = (regionId: string, pointIds: string[]) => ({ + regionId, + pointIds, + d: { + center: { x: 0, y: 0 }, + width: 10, + height: 10, + availableZ: [0, 1], + }, +}) + +const graph = { + regions: [ + createRegion("start", ["start-port"]), + createRegion("r0", ["start-port", "g1-z0", "g1-z1"]), + createRegion("r1", ["g1-z0", "g1-z1", "g2-z0", "g2-z1"]), + createRegion("r2", ["g2-z0", "g2-z1", "end-port"]), + createRegion("end", ["end-port"]), + ], + ports: [ + { + portId: "start-port", + region1Id: "start", + region2Id: "r0", + d: { x: -3, y: 0, z: 0 }, + }, + { + portId: "g1-z0", + region1Id: "r0", + region2Id: "r1", + d: { x: -1, y: 0, z: 0, physicalPortGroupId: "g1" }, + }, + { + portId: "g1-z1", + region1Id: "r0", + region2Id: "r1", + d: { x: -1, y: 0, z: 1, physicalPortGroupId: "g1" }, + }, + { + portId: "g2-z0", + region1Id: "r1", + region2Id: "r2", + d: { x: 1, y: 0, z: 0, physicalPortGroupId: "g2" }, + }, + { + portId: "g2-z1", + region1Id: "r1", + region2Id: "r2", + d: { x: 1, y: 0, z: 1, physicalPortGroupId: "g2" }, + }, + { + portId: "end-port", + region1Id: "r2", + region2Id: "end", + d: { x: 3, y: 0, z: 0 }, + }, + ], + connections: [ + { + connectionId: "route-a", + startRegionId: "start", + endRegionId: "end", + }, + ], + solvedRoutes: [ + { + connection: { + connectionId: "route-a", + startRegionId: "start", + endRegionId: "end", + }, + requiredRip: false, + path: [ + { + portId: "start-port", + nextRegionId: "r0", + g: 0, + h: 0, + f: 0, + hops: 0, + ripRequired: false, + }, + { + portId: "g1-z0", + lastRegionId: "r0", + nextRegionId: "r1", + g: 1, + h: 0, + f: 1, + hops: 1, + ripRequired: false, + }, + { + portId: "g2-z1", + lastRegionId: "r1", + nextRegionId: "r2", + g: 2, + h: 0, + f: 2, + hops: 2, + ripRequired: false, + }, + { + portId: "end-port", + lastRegionId: "r2", + nextRegionId: "end", + g: 3, + h: 0, + f: 3, + hops: 3, + ripRequired: false, + }, + ], + }, + ], +} as SerializedHyperGraph + +test("refines portal layers while preserving the fixed region sequence", () => { + const { topology, problem, solution } = loadSerializedHyperGraph(graph) + expect(topology.physicalPortalGroupCount).toBe(2) + expect( + topology.portPhysicalGroupId?.[1], + ).toBe(topology.portPhysicalGroupId?.[2]) + expect( + topology.portPhysicalGroupId?.[3], + ).toBe(topology.portPhysicalGroupId?.[4]) + const solver = new FixedTopologyPortalLayerRefinementSolver( + topology, + problem, + solution, + ) + const beforeRefinementGraphics = solver.visualize() + + solver.solve() + + const output = solver.getOutput() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.stats.predictedViaDemandBefore).toBe(2) + expect(solver.stats.predictedViaDemandAfter).toBe(0) + expect(solver.stats.acceptedCandidateCount).toBe(1) + expect( + output.solvedRoutes?.[0]?.path.map((candidate) => candidate.nextRegionId), + ).toEqual(["r0", "r1", "r2", "end"]) + expect( + output.solvedRoutes?.[0]?.path.map((candidate) => candidate.portId), + ).toEqual(["start-port", "g1-z0", "g2-z0", "end-port"]) + const stagedSvg = getSvgFromGraphicsObject( + stackGraphicsVertically( + [beforeRefinementGraphics, solver.visualize()], + { titles: ["before refinement", "after refinement"] }, + ), + ) + expect(stagedSvg).toMatchSvgSnapshot(import.meta.path) + + const repeatedLoad = loadSerializedHyperGraph(graph) + const repeatedSolver = new FixedTopologyPortalLayerRefinementSolver( + repeatedLoad.topology, + repeatedLoad.problem, + repeatedLoad.solution, + ) + repeatedSolver.solve() + expect(repeatedSolver.getOutput()).toEqual(output) + + const lockedLoad = loadSerializedHyperGraph(graph) + lockedLoad.problem.portalLayerRefinementLockedRouteMask = + Int8Array.from([1]) + const lockedSolver = new FixedTopologyPortalLayerRefinementSolver( + lockedLoad.topology, + lockedLoad.problem, + lockedLoad.solution, + ) + lockedSolver.solve() + expect(lockedSolver.stats.acceptedCandidateCount).toBe(0) + expect( + lockedSolver + .getOutput() + .solvedRoutes?.[0]?.path.map((candidate) => candidate.portId), + ).toEqual(["start-port", "g1-z0", "g2-z1", "end-port"]) + + const reservedAlternativeLoad = loadSerializedHyperGraph(graph) + reservedAlternativeLoad.problem.routeCount = 2 + reservedAlternativeLoad.problem.routeStartPort = Int32Array.from([0, 3]) + reservedAlternativeLoad.problem.routeEndPort = Int32Array.from([5, 3]) + reservedAlternativeLoad.problem.routeNet = Int32Array.from([0, 1]) + reservedAlternativeLoad.problem.routeMetadata = [ + ...(reservedAlternativeLoad.problem.routeMetadata ?? []), + { connectionId: "fixed-port-owner" }, + ] + reservedAlternativeLoad.problem.portalLayerRefinementLockedRouteMask = + Int8Array.from([0, 1]) + reservedAlternativeLoad.solution.solvedRoutePathSegments.push([]) + reservedAlternativeLoad.solution.solvedRoutePathRegionIds?.push([]) + const reservedAlternativeSolver = + new FixedTopologyPortalLayerRefinementSolver( + reservedAlternativeLoad.topology, + reservedAlternativeLoad.problem, + reservedAlternativeLoad.solution, + ) + reservedAlternativeSolver.solve() + expect(reservedAlternativeSolver.stats.acceptedCandidateCount).toBe(0) + expect( + reservedAlternativeSolver.routePlans[0]?.orderedPortIds, + ).toEqual([0, 1, 4, 5]) +}) diff --git a/tests/solver/section-solver.test.ts b/tests/solver/section-solver.test.ts index cd53248..2df1982 100644 --- a/tests/solver/section-solver.test.ts +++ b/tests/solver/section-solver.test.ts @@ -275,7 +275,7 @@ test("section pipeline uses bounded default iteration limits", () => { expect(pipelineSolver.getSectionSolverOptions().MAX_ITERATIONS).toBe(50_000) }) -test("section pipeline final acceptance falls back to solveGraph output", () => { +test("section pipeline does not skip unfinished optimization and refinement stages", () => { const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ serializedHyperGraph: sectionSolverFixtureGraph, }) @@ -288,12 +288,9 @@ test("section pipeline final acceptance falls back to solveGraph output", () => pipelineSolver.tryFinalAcceptance() - expect(pipelineSolver.solved).toBe(true) + expect(pipelineSolver.solved).toBe(false) expect(pipelineSolver.failed).toBe(false) - expect( - pipelineSolver.stats.acceptedSolveGraphOutputOnSectionPipelineTimeout, - ).toBe(true) - expect(pipelineSolver.getOutput()).toBe(sectionSolverFixtureGraph) + expect(pipelineSolver.getOutput()).toBeNull() }) test("section pipeline searches multiple masks and commits an improving output on hg07 sample029", () => { From 981e8295687115d880d7d9a15304b00833316629 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Wed, 5 Aug 2026 09:49:11 +0200 Subject: [PATCH 2/4] Tighten portal layer refinement acceptance --- ...topology-portal-layer-refinement-solver.ts | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/fixed-topology-portal-layer-refinement-solver.ts b/lib/fixed-topology-portal-layer-refinement-solver.ts index da69fc3..7ba90ef 100644 --- a/lib/fixed-topology-portal-layer-refinement-solver.ts +++ b/lib/fixed-topology-portal-layer-refinement-solver.ts @@ -268,8 +268,36 @@ const createSolvedSolver = ( getReplaySolverOptions(options), ) resetSolvedState(solver) - for (const routePlan of routePlans) { - appendRoutePlanToSolver(solver, routePlan) + const expectedSegmentCount = routePlans.reduce( + (count, routePlan) => count + routePlan.orderedRegionIds.length, + 0, + ) + if ( + problem.initialAssignments && + problem.initialAssignments.length === expectedSegmentCount + ) { + for (const assignment of problem.initialAssignments) { + solver.state.currentRouteNetId = + problem.routeNet[assignment.routeId]! + solver.state.regionSegments[assignment.regionId]!.push([ + assignment.routeId, + assignment.fromPortId, + assignment.toPortId, + ]) + solver.state.portAssignment[assignment.fromPortId] = + solver.state.currentRouteNetId + solver.state.portAssignment[assignment.toPortId] = + solver.state.currentRouteNetId + solver.appendSegmentToRegionCache( + assignment.regionId, + assignment.fromPortId, + assignment.toPortId, + ) + } + } else { + for (const routePlan of routePlans) { + appendRoutePlanToSolver(solver, routePlan) + } } solver.state.currentRouteNetId = undefined solver.solved = true @@ -809,6 +837,21 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { baselineSummary.maxRegionCost + epsilon && candidateSummary.totalRegionCost <= baselineSummary.totalRegionCost + epsilon + const intersectionCountsDidNotWorsen = touchedRegionIds.every( + (regionId) => { + const before = savedCaches.get(regionId)! + const after = + this.refinedSolver.state.regionIntersectionCaches[regionId]! + return ( + after.existingSameLayerIntersections <= + before.existingSameLayerIntersections && + after.existingCrossingLayerIntersections <= + before.existingCrossingLayerIntersections && + after.existingEntryExitLayerChanges <= + before.existingEntryExitLayerChanges + ) + }, + ) const viaDemandImproved = candidateSummary.predictedViaDemand < baselineSummary.predictedViaDemand @@ -816,6 +859,7 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { if ( !hasValidPortAssignments || !regionCostDidNotWorsen || + !intersectionCountsDidNotWorsen || !viaDemandImproved ) { for (const regionId of touchedRegionIds) { @@ -827,7 +871,10 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { this.refinedSolver.state.portAssignment = savedPortAssignment if (!hasValidPortAssignments) { this.refinementStats.rejectedForPortConflictCount += 1 - } else if (!regionCostDidNotWorsen) { + } else if ( + !regionCostDidNotWorsen || + !intersectionCountsDidNotWorsen + ) { this.refinementStats.rejectedForRegionCostCount += 1 } return false From 45f4336d41cb605544a86c1f77e843b104343b88 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Wed, 5 Aug 2026 11:54:45 +0200 Subject: [PATCH 3/4] fix portal refinement transaction boundaries --- lib/DuplicateCongestedPortSolver.ts | 3 +- ...topology-portal-layer-refinement-solver.ts | 138 +++++++++-------- .../duplicate-congested-port-solver.test.ts | 13 +- ...d-topology-portal-layer-refinement.test.ts | 143 ++++++++++++++++-- 4 files changed, 219 insertions(+), 78 deletions(-) diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index a607c9a..f828ad2 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -439,8 +439,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { duplicatedPortData.duplicatePortProximity = duplicatePortProximity duplicatedPortData.repairReason = "congested-port" if (typeof duplicatedPortData.physicalPortGroupId === "string") { - duplicatedPortData.physicalPortGroupId = - `${duplicatedPortData.physicalPortGroupId}::duplicate-${duplicateIndex}` + duplicatedPortData.physicalPortGroupId = `${duplicatedPortData.physicalPortGroupId}::source-${sourcePortId}::duplicate-${duplicateIndex}` } ports.push({ diff --git a/lib/fixed-topology-portal-layer-refinement-solver.ts b/lib/fixed-topology-portal-layer-refinement-solver.ts index 7ba90ef..e4e8910 100644 --- a/lib/fixed-topology-portal-layer-refinement-solver.ts +++ b/lib/fixed-topology-portal-layer-refinement-solver.ts @@ -1,9 +1,7 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" -import { - computeEstimatedViaDemand, -} from "./computeRegionCost" +import { computeEstimatedViaDemand } from "./computeRegionCost" import { createEmptyRegionIntersectionCache, TinyHyperGraphSolver, @@ -71,8 +69,7 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), - existingCrossingLayerIntersections: - cache.existingCrossingLayerIntersections, + existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, existingEntryExitLayerChanges: cache.existingEntryExitLayerChanges, existingRegionCost: cache.existingRegionCost, @@ -84,9 +81,10 @@ const getSharedRegionId = ( fromPortId: PortId, toPortId: PortId, ): RegionId => { - const sharedRegionIds = (topology.incidentPortRegion[fromPortId] ?? []).filter( - (regionId) => - (topology.incidentPortRegion[toPortId] ?? []).includes(regionId), + const sharedRegionIds = ( + topology.incidentPortRegion[fromPortId] ?? [] + ).filter((regionId) => + (topology.incidentPortRegion[toPortId] ?? []).includes(regionId), ) if (sharedRegionIds.length !== 1) { @@ -277,8 +275,7 @@ const createSolvedSolver = ( problem.initialAssignments.length === expectedSegmentCount ) { for (const assignment of problem.initialAssignments) { - solver.state.currentRouteNetId = - problem.routeNet[assignment.routeId]! + solver.state.currentRouteNetId = problem.routeNet[assignment.routeId]! solver.state.regionSegments[assignment.regionId]!.push([ assignment.routeId, assignment.fromPortId, @@ -518,11 +515,8 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { ) { if ( routePlan.physicalGroupIds[portIndex]! >= 0 && - this.getAlternativePorts( - routePlan, - portIndex, - usedRouteIdsByPort, - ).length > 1 + this.getAlternativePorts(routePlan, portIndex, usedRouteIdsByPort) + .length > 1 ) { return true } @@ -623,8 +617,7 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { for (const [segmentRouteId, fromPortId, toPortId] of this.refinedSolver .state.regionSegments[regionId] ?? []) { if (segmentRouteId === routeId) continue - solver.state.currentRouteNetId = - this.problem.routeNet[segmentRouteId]! + solver.state.currentRouteNetId = this.problem.routeNet[segmentRouteId]! solver.state.regionSegments[regionId]!.push([ segmentRouteId, fromPortId, @@ -643,11 +636,7 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { const domains = routePlan.orderedPortIds.map((currentPortId, portIndex) => portIndex === 0 || portIndex === routePlan.orderedPortIds.length - 1 ? [currentPortId] - : this.getAlternativePorts( - routePlan, - portIndex, - usedRouteIdsByPort, - ), + : this.getAlternativePorts(routePlan, portIndex, usedRouteIdsByPort), ) const touchedRegionIds = new Set(routePlan.orderedRegionIds) const backgroundSolver = this.createBackgroundSolver( @@ -705,11 +694,7 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { const existingCandidate = nextCandidatesByPort.get(toPortId) if ( !existingCandidate || - compareDpCandidates( - this.topology, - candidate, - existingCandidate, - ) < 0 + compareDpCandidates(this.topology, candidate, existingCandidate) < 0 ) { nextCandidatesByPort.set(toPortId, candidate) } @@ -773,25 +758,41 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { this.lastAcceptedRegionIds = [] if ( candidate.portIds.every( - (portId, portIndex) => - portId === routePlan.orderedPortIds[portIndex], + (portId, portIndex) => portId === routePlan.orderedPortIds[portIndex], ) ) { return false } const baselineSummary = summarizeState(this.refinedSolver) - const touchedRegionIds = [...new Set(routePlan.orderedRegionIds)] + const changedSegmentIndices: number[] = [] + for ( + let segmentIndex = 0; + segmentIndex < routePlan.orderedRegionIds.length; + segmentIndex++ + ) { + if ( + routePlan.orderedPortIds[segmentIndex] !== + candidate.portIds[segmentIndex] || + routePlan.orderedPortIds[segmentIndex + 1] !== + candidate.portIds[segmentIndex + 1] + ) { + changedSegmentIndices.push(segmentIndex) + } + } + const touchedRegionIds = [ + ...new Set( + changedSegmentIndices.map( + (segmentIndex) => routePlan.orderedRegionIds[segmentIndex]!, + ), + ), + ] const savedSegments = new Map( touchedRegionIds.map((regionId) => [ regionId, this.refinedSolver.state.regionSegments[regionId]!.map( ([routeId, fromPortId, toPortId]) => - [routeId, fromPortId, toPortId] as [ - RouteId, - PortId, - PortId, - ], + [routeId, fromPortId, toPortId] as [RouteId, PortId, PortId], ), ]), ) @@ -808,22 +809,35 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { ) for (const regionId of touchedRegionIds) { - this.refinedSolver.state.regionSegments[regionId] = this.refinedSolver - .state.regionSegments[regionId]!.filter( - ([routeId]) => routeId !== routePlan.routeId, + const candidateSegments = routePlan.orderedRegionIds + .flatMap((candidateRegionId, segmentIndex) => + candidateRegionId === regionId ? [segmentIndex] : [], ) - } - for ( - let segmentIndex = 0; - segmentIndex < routePlan.orderedRegionIds.length; - segmentIndex++ - ) { - const regionId = routePlan.orderedRegionIds[segmentIndex]! - this.refinedSolver.state.regionSegments[regionId]!.push([ - routePlan.routeId, - candidate.portIds[segmentIndex]!, - candidate.portIds[segmentIndex + 1]!, - ]) + .map( + (segmentIndex) => + [ + routePlan.routeId, + candidate.portIds[segmentIndex]!, + candidate.portIds[segmentIndex + 1]!, + ] as [RouteId, PortId, PortId], + ) + let candidateSegmentIndex = 0 + this.refinedSolver.state.regionSegments[regionId] = + this.refinedSolver.state.regionSegments[regionId]!.map((segment) => { + if ( + segment[0] !== routePlan.routeId || + candidateSegmentIndex >= candidateSegments.length + ) { + return segment + } + return candidateSegments[candidateSegmentIndex++]! + }) + + if (candidateSegmentIndex !== candidateSegments.length) { + throw new Error( + `Route ${routePlan.routeId} changed segment count in region ${regionId}`, + ) + } } for (const regionId of touchedRegionIds) { this.rebuildRegionCache(regionId) @@ -853,14 +867,17 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { }, ) const viaDemandImproved = - candidateSummary.predictedViaDemand < - baselineSummary.predictedViaDemand + candidateSummary.predictedViaDemand < baselineSummary.predictedViaDemand + const entryExitLayerChangesImproved = + candidateSummary.entryExitLayerChanges < + baselineSummary.entryExitLayerChanges if ( !hasValidPortAssignments || !regionCostDidNotWorsen || !intersectionCountsDidNotWorsen || - !viaDemandImproved + !viaDemandImproved || + !entryExitLayerChangesImproved ) { for (const regionId of touchedRegionIds) { this.refinedSolver.state.regionSegments[regionId] = @@ -871,10 +888,7 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { this.refinedSolver.state.portAssignment = savedPortAssignment if (!hasValidPortAssignments) { this.refinementStats.rejectedForPortConflictCount += 1 - } else if ( - !regionCostDidNotWorsen || - !intersectionCountsDidNotWorsen - ) { + } else if (!regionCostDidNotWorsen || !intersectionCountsDidNotWorsen) { this.refinementStats.rejectedForRegionCostCount += 1 } return false @@ -890,8 +904,9 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { if (fromPortId === toPortId) continue this.changes.push({ routeId: routePlan.routeId, - physicalPortGroupId: - routePlan.physicalGroupIds[portIndex]! as PhysicalPortGroupId, + physicalPortGroupId: routePlan.physicalGroupIds[ + portIndex + ]! as PhysicalPortGroupId, fromPortId, toPortId, fromZ: this.topology.portZ[fromPortId]!, @@ -931,8 +946,9 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { if (candidate && this.tryCandidate(routePlan, candidate)) { const affectedRouteIds = new Set([routeId]) for (const regionId of this.lastAcceptedRegionIds) { - for (const [affectedRouteId] of this.refinedSolver.state - .regionSegments[regionId] ?? []) { + for (const [affectedRouteId] of this.refinedSolver.state.regionSegments[ + regionId + ] ?? []) { affectedRouteIds.add(affectedRouteId) } } diff --git a/tests/solver/duplicate-congested-port-solver.test.ts b/tests/solver/duplicate-congested-port-solver.test.ts index ec2c1e5..4a4472d 100644 --- a/tests/solver/duplicate-congested-port-solver.test.ts +++ b/tests/solver/duplicate-congested-port-solver.test.ts @@ -93,7 +93,15 @@ const createDuplicatePortFixture = (): SerializedHyperGraph => ({ ports: [ createPort("a-start-port", "a-start", "left", -3, 0), createPort("b-start-port", "b-start", "left", -3, -0.2), - createPort("shared-choke", "left", "right", 0, 0), + { + ...createPort("shared-choke", "left", "right", 0, 0), + d: { + x: 0, + y: 0, + z: 0, + physicalPortGroupId: "shared-choke-physical", + }, + }, createPort("shared-neighbor", "left", "right", 0, 4), createPort("a-end-port", "right", "a-end", 3, 0), createPort("b-end-port", "right", "b-end", 3, -0.2), @@ -186,6 +194,9 @@ test("duplicate congested port solver duplicates independently reused ports in l expect(duplicatePort.d?.duplicatedFromPortId).toBe("shared-choke") expect(duplicatePort.d?.duplicateIndex).toBe(1) + expect(duplicatePort.d?.physicalPortGroupId).toBe( + "shared-choke-physical::source-shared-choke::duplicate-1", + ) expect(duplicateDistance).toBeGreaterThan(0) expect(duplicateDistance).toBeLessThanOrEqual(duplicatePortProximity) expect(Math.abs(crossProduct)).toBeLessThan(1e-9) diff --git a/tests/solver/fixed-topology-portal-layer-refinement.test.ts b/tests/solver/fixed-topology-portal-layer-refinement.test.ts index 470a885..d031b2f 100644 --- a/tests/solver/fixed-topology-portal-layer-refinement.test.ts +++ b/tests/solver/fixed-topology-portal-layer-refinement.test.ts @@ -130,12 +130,12 @@ const graph = { test("refines portal layers while preserving the fixed region sequence", () => { const { topology, problem, solution } = loadSerializedHyperGraph(graph) expect(topology.physicalPortalGroupCount).toBe(2) - expect( - topology.portPhysicalGroupId?.[1], - ).toBe(topology.portPhysicalGroupId?.[2]) - expect( - topology.portPhysicalGroupId?.[3], - ).toBe(topology.portPhysicalGroupId?.[4]) + expect(topology.portPhysicalGroupId?.[1]).toBe( + topology.portPhysicalGroupId?.[2], + ) + expect(topology.portPhysicalGroupId?.[3]).toBe( + topology.portPhysicalGroupId?.[4], + ) const solver = new FixedTopologyPortalLayerRefinementSolver( topology, problem, @@ -158,10 +158,9 @@ test("refines portal layers while preserving the fixed region sequence", () => { output.solvedRoutes?.[0]?.path.map((candidate) => candidate.portId), ).toEqual(["start-port", "g1-z0", "g2-z0", "end-port"]) const stagedSvg = getSvgFromGraphicsObject( - stackGraphicsVertically( - [beforeRefinementGraphics, solver.visualize()], - { titles: ["before refinement", "after refinement"] }, - ), + stackGraphicsVertically([beforeRefinementGraphics, solver.visualize()], { + titles: ["before refinement", "after refinement"], + }), ) expect(stagedSvg).toMatchSvgSnapshot(import.meta.path) @@ -175,8 +174,7 @@ test("refines portal layers while preserving the fixed region sequence", () => { expect(repeatedSolver.getOutput()).toEqual(output) const lockedLoad = loadSerializedHyperGraph(graph) - lockedLoad.problem.portalLayerRefinementLockedRouteMask = - Int8Array.from([1]) + lockedLoad.problem.portalLayerRefinementLockedRouteMask = Int8Array.from([1]) const lockedSolver = new FixedTopologyPortalLayerRefinementSolver( lockedLoad.topology, lockedLoad.problem, @@ -211,7 +209,124 @@ test("refines portal layers while preserving the fixed region sequence", () => { ) reservedAlternativeSolver.solve() expect(reservedAlternativeSolver.stats.acceptedCandidateCount).toBe(0) + expect(reservedAlternativeSolver.routePlans[0]?.orderedPortIds).toEqual([ + 0, 1, 4, 5, + ]) +}) + +test("replaces changed segments in place without reordering other routes", () => { + const multiRouteGraph = structuredClone(graph) as SerializedHyperGraph + multiRouteGraph.regions.push( + createRegion("b-start", ["b-start-port"]), + createRegion("b-end", ["b-end-port"]), + ) + for (const [regionId, pointIds] of [ + ["r0", ["b-start-port", "b-g1"]], + ["r1", ["b-g1", "b-g2"]], + ["r2", ["b-g2", "b-end-port"]], + ] as const) { + multiRouteGraph.regions + .find((region) => region.regionId === regionId)! + .pointIds.push(...pointIds) + } + multiRouteGraph.ports.push( + { + portId: "b-start-port", + region1Id: "b-start", + region2Id: "r0", + d: { x: -3, y: 4, z: 0 }, + }, + { + portId: "b-g1", + region1Id: "r0", + region2Id: "r1", + d: { x: -1, y: 4, z: 0 }, + }, + { + portId: "b-g2", + region1Id: "r1", + region2Id: "r2", + d: { x: 1, y: 4, z: 0 }, + }, + { + portId: "b-end-port", + region1Id: "r2", + region2Id: "b-end", + d: { x: 3, y: 4, z: 0 }, + }, + ) + multiRouteGraph.connections?.push({ + connectionId: "route-b", + startRegionId: "b-start", + endRegionId: "b-end", + }) + multiRouteGraph.solvedRoutes?.push({ + connection: { + connectionId: "route-b", + startRegionId: "b-start", + endRegionId: "b-end", + }, + requiredRip: false, + path: [ + { + portId: "b-start-port", + nextRegionId: "r0", + g: 0, + h: 0, + f: 0, + hops: 0, + ripRequired: false, + }, + { + portId: "b-g1", + lastRegionId: "r0", + nextRegionId: "r1", + g: 1, + h: 0, + f: 1, + hops: 1, + ripRequired: false, + }, + { + portId: "b-g2", + lastRegionId: "r1", + nextRegionId: "r2", + g: 2, + h: 0, + f: 2, + hops: 2, + ripRequired: false, + }, + { + portId: "b-end-port", + lastRegionId: "r2", + nextRegionId: "b-end", + g: 3, + h: 0, + f: 3, + hops: 3, + ripRequired: false, + }, + ], + }) + + const { topology, problem, solution } = + loadSerializedHyperGraph(multiRouteGraph) + const solver = new FixedTopologyPortalLayerRefinementSolver( + topology, + problem, + solution, + ) + const routeOrderBefore = solver.refinedSolver.state.regionSegments.map( + (segments) => segments.map(([routeId]) => routeId), + ) + + solver.solve() + + expect(solver.stats.acceptedCandidateCount).toBe(1) expect( - reservedAlternativeSolver.routePlans[0]?.orderedPortIds, - ).toEqual([0, 1, 4, 5]) + solver.refinedSolver.state.regionSegments.map((segments) => + segments.map(([routeId]) => routeId), + ), + ).toEqual(routeOrderBefore) }) From 9cc3a682a8abf8ca6fd20144140716273f2f11ae Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Thu, 6 Aug 2026 02:00:07 +0200 Subject: [PATCH 4/4] Add portal refinement rejection diagnostics --- ...d-topology-portal-layer-refinement-solver.ts | 17 ++++++++++++++++- lib/types.ts | 3 +++ ...xed-topology-portal-layer-refinement.test.ts | 7 +++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/fixed-topology-portal-layer-refinement-solver.ts b/lib/fixed-topology-portal-layer-refinement-solver.ts index e4e8910..7f065cd 100644 --- a/lib/fixed-topology-portal-layer-refinement-solver.ts +++ b/lib/fixed-topology-portal-layer-refinement-solver.ts @@ -407,8 +407,11 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { acceptedCandidateCount: 0, touchedRegionCount: 0, rejectedForRegionCostCount: 0, + rejectedForIntersectionRegressionCount: 0, rejectedForPortConflictCount: 0, rejectedForLockedAssignmentCount: 0, + rejectedForNoViaDemandImprovementCount: 0, + rejectedForNoEntryExitImprovementCount: 0, portalLayerRefinementMs: 0, } @@ -761,6 +764,8 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { (portId, portIndex) => portId === routePlan.orderedPortIds[portIndex], ) ) { + this.refinementStats.rejectedForNoViaDemandImprovementCount += 1 + this.refinementStats.rejectedForNoEntryExitImprovementCount += 1 return false } @@ -888,9 +893,19 @@ export class FixedTopologyPortalLayerRefinementSolver extends BaseSolver { this.refinedSolver.state.portAssignment = savedPortAssignment if (!hasValidPortAssignments) { this.refinementStats.rejectedForPortConflictCount += 1 - } else if (!regionCostDidNotWorsen || !intersectionCountsDidNotWorsen) { + } + if (!regionCostDidNotWorsen) { this.refinementStats.rejectedForRegionCostCount += 1 } + if (!intersectionCountsDidNotWorsen) { + this.refinementStats.rejectedForIntersectionRegressionCount += 1 + } + if (!viaDemandImproved) { + this.refinementStats.rejectedForNoViaDemandImprovementCount += 1 + } + if (!entryExitLayerChangesImproved) { + this.refinementStats.rejectedForNoEntryExitImprovementCount += 1 + } return false } diff --git a/lib/types.ts b/lib/types.ts index d127e38..b254078 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -55,8 +55,11 @@ export interface FixedTopologyPortalLayerRefinementStats { acceptedCandidateCount: number touchedRegionCount: number rejectedForRegionCostCount: number + rejectedForIntersectionRegressionCount: number rejectedForPortConflictCount: number rejectedForLockedAssignmentCount: number + rejectedForNoViaDemandImprovementCount: number + rejectedForNoEntryExitImprovementCount: number portalLayerRefinementMs: number } diff --git a/tests/solver/fixed-topology-portal-layer-refinement.test.ts b/tests/solver/fixed-topology-portal-layer-refinement.test.ts index d031b2f..bd0dd95 100644 --- a/tests/solver/fixed-topology-portal-layer-refinement.test.ts +++ b/tests/solver/fixed-topology-portal-layer-refinement.test.ts @@ -182,6 +182,7 @@ test("refines portal layers while preserving the fixed region sequence", () => { ) lockedSolver.solve() expect(lockedSolver.stats.acceptedCandidateCount).toBe(0) + expect(lockedSolver.stats.rejectedForLockedAssignmentCount).toBe(1) expect( lockedSolver .getOutput() @@ -209,6 +210,12 @@ test("refines portal layers while preserving the fixed region sequence", () => { ) reservedAlternativeSolver.solve() expect(reservedAlternativeSolver.stats.acceptedCandidateCount).toBe(0) + expect( + reservedAlternativeSolver.stats.rejectedForNoViaDemandImprovementCount, + ).toBeGreaterThan(0) + expect( + reservedAlternativeSolver.stats.rejectedForNoEntryExitImprovementCount, + ).toBeGreaterThan(0) expect(reservedAlternativeSolver.routePlans[0]?.orderedPortIds).toEqual([ 0, 1, 4, 5, ])