From 1f6944ff927ea026621dfa793ab344708c8b856f Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 10 Apr 2026 20:00:13 -0700 Subject: [PATCH] untangle-on-path-found implementation --- lib/core.ts | 855 ++++++++++++++++++++++- lib/section-solver/index.ts | 23 +- tests/solver/route-untangle-swap.test.ts | 188 +++++ 3 files changed, 1026 insertions(+), 40 deletions(-) create mode 100644 tests/solver/route-untangle-swap.test.ts diff --git a/lib/core.ts b/lib/core.ts index 650cae5..eb34c19 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -210,6 +210,8 @@ export const getTinyHyperGraphSolverOptions = ( const compareCandidatesByF = (left: Candidate, right: Candidate) => left.f - right.f +const IMPROVEMENT_EPSILON = 1e-9 + interface SegmentGeometryScratch { lesserAngle: number greaterAngle: number @@ -217,6 +219,92 @@ interface SegmentGeometryScratch { entryExitLayerChanges: number } +interface SolvedRouteSegment { + regionId: RegionId + fromPortId: PortId + toPortId: PortId +} + +interface OrderedRoutePath { + orderedPortIds: PortId[] + orderedRegionIds: RegionId[] +} + +interface RoutePortTraversal { + routeId: RouteId + portId: PortId + regionId1: RegionId + regionId2: RegionId + otherPortIdInRegion1: PortId + otherPortIdInRegion2: PortId + edgeKey: string + solvedSegmentIndex?: number +} + +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 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 compareRegionCostSummaries = ( + left: RegionCostSummary, + right: RegionCostSummary, +) => { + if ( + Math.abs(left.maxRegionCost - right.maxRegionCost) > IMPROVEMENT_EPSILON + ) { + return left.maxRegionCost - right.maxRegionCost + } + + return left.totalRegionCost - right.totalRegionCost +} + +const summarizeRegionCostsForRegionIds = ( + regionIds: RegionId[], + getRegionCache: (regionId: RegionId) => RegionIntersectionCache | undefined, +): RegionCostSummary => { + let maxRegionCost = 0 + let totalRegionCost = 0 + + for (const regionId of regionIds) { + const regionCost = getRegionCache(regionId)?.existingRegionCost ?? 0 + maxRegionCost = Math.max(maxRegionCost, regionCost) + totalRegionCost += regionCost + } + + return { + maxRegionCost, + totalRegionCost, + } +} + +const getRegionPairKey = (regionId1: RegionId, regionId2: RegionId) => + regionId1 < regionId2 + ? `${regionId1}:${regionId2}` + : `${regionId2}:${regionId1}` + export class TinyHyperGraphSolver extends BaseSolver { state: TinyHyperGraphWorkingState private _problemSetup?: TinyHyperGraphProblemSetup @@ -511,10 +599,15 @@ export class TinyHyperGraphSolver extends BaseSolver { } isKnownSingleLayerRegion(regionId: RegionId): boolean { - const regionAvailableZMask = this.topology.regionAvailableZMask?.[regionId] ?? 0 + const regionAvailableZMask = + this.topology.regionAvailableZMask?.[regionId] ?? 0 return isKnownSingleLayerMask(regionAvailableZMask) } + protected canRewriteSolvedRoute(_routeId: RouteId): boolean { + return true + } + populateSegmentGeometryScratch( regionId: RegionId, port1Id: PortId, @@ -525,15 +618,17 @@ export class TinyHyperGraphSolver extends BaseSolver { const port1IncidentRegions = topology.incidentPortRegion[port1Id] const port2IncidentRegions = topology.incidentPortRegion[port2Id] const angle1 = - port1IncidentRegions[0] === regionId || port1IncidentRegions[1] !== regionId + port1IncidentRegions[0] === regionId || + port1IncidentRegions[1] !== regionId ? topology.portAngleForRegion1[port1Id] - : topology.portAngleForRegion2?.[port1Id] ?? - topology.portAngleForRegion1[port1Id] + : (topology.portAngleForRegion2?.[port1Id] ?? + topology.portAngleForRegion1[port1Id]) const angle2 = - port2IncidentRegions[0] === regionId || port2IncidentRegions[1] !== regionId + port2IncidentRegions[0] === regionId || + port2IncidentRegions[1] !== regionId ? topology.portAngleForRegion1[port2Id] - : topology.portAngleForRegion2?.[port2Id] ?? - topology.portAngleForRegion1[port2Id] + : (topology.portAngleForRegion2?.[port2Id] ?? + topology.portAngleForRegion1[port2Id]) const z1 = topology.portZ[port1Id] const z2 = topology.portZ[port2Id] scratch.lesserAngle = angle1 < angle2 ? angle1 : angle2 @@ -544,13 +639,14 @@ export class TinyHyperGraphSolver extends BaseSolver { return scratch } - appendSegmentToRegionCache( + buildNextRegionIntersectionCache( + regionCache: RegionIntersectionCache, regionId: RegionId, + routeNetId: NetId, port1Id: PortId, port2Id: PortId, - ) { - const { topology, state } = this - const regionCache = state.regionIntersectionCaches[regionId] + ): RegionIntersectionCache { + const { topology } = this const segmentGeometry = this.populateSegmentGeometryScratch( regionId, port1Id, @@ -562,7 +658,7 @@ export class TinyHyperGraphSolver extends BaseSolver { newEntryExitLayerChanges, ] = countNewIntersectionsWithValues( regionCache, - state.currentRouteNetId!, + routeNetId, segmentGeometry.lesserAngle, segmentGeometry.greaterAngle, segmentGeometry.layerMask, @@ -572,7 +668,7 @@ export class TinyHyperGraphSolver extends BaseSolver { const netIds = new Int32Array(nextLength) netIds.set(regionCache.netIds) - netIds[nextLength - 1] = state.currentRouteNetId! + netIds[nextLength - 1] = routeNetId const lesserAngles = new Int32Array(nextLength) lesserAngles.set(regionCache.lesserAngles) @@ -595,7 +691,7 @@ export class TinyHyperGraphSolver extends BaseSolver { regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges const existingSegmentCount = lesserAngles.length - state.regionIntersectionCaches[regionId] = { + return { netIds, lesserAngles, greaterAngles, @@ -616,11 +712,42 @@ export class TinyHyperGraphSolver extends BaseSolver { } } - getSolvedPathSegments(finalCandidate: Candidate): Array<{ - regionId: RegionId - fromPortId: PortId - toPortId: PortId - }> { + appendSegmentToRegionCache( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + ) { + const { state } = this + state.regionIntersectionCaches[regionId] = + this.buildNextRegionIntersectionCache( + state.regionIntersectionCaches[regionId]!, + regionId, + state.currentRouteNetId!, + port1Id, + port2Id, + ) + } + + computeRegionIntersectionCacheFromSegments( + regionId: RegionId, + regionSegments: [RouteId, PortId, PortId][], + ): RegionIntersectionCache { + let regionCache = createEmptyRegionIntersectionCache() + + for (const [routeId, fromPortId, toPortId] of regionSegments) { + regionCache = this.buildNextRegionIntersectionCache( + regionCache, + regionId, + this.problem.routeNet[routeId]!, + fromPortId, + toPortId, + ) + } + + return regionCache + } + + getSolvedPathSegments(finalCandidate: Candidate): SolvedRouteSegment[] { const { state } = this const candidatePath: Candidate[] = [] let cursor: Candidate | undefined = finalCandidate @@ -630,11 +757,7 @@ export class TinyHyperGraphSolver extends BaseSolver { cursor = cursor.prevCandidate } - const solvedSegments: Array<{ - regionId: RegionId - fromPortId: PortId - toPortId: PortId - }> = [] + const solvedSegments: SolvedRouteSegment[] = [] for (let i = 1; i < candidatePath.length; i++) { solvedSegments.push({ @@ -656,6 +779,665 @@ export class TinyHyperGraphSolver extends BaseSolver { return solvedSegments } + getOrderedRoutePathFromRegionSegments( + routeId: RouteId, + regionSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, + ): OrderedRoutePath | null { + const routeSegments: SolvedRouteSegment[] = [] + + for ( + let regionId = 0; + regionId < regionSegmentsByRegion.length; + regionId++ + ) { + for (const [ + segmentRouteId, + fromPortId, + toPortId, + ] of regionSegmentsByRegion[regionId] ?? []) { + if (segmentRouteId !== routeId) { + continue + } + + routeSegments.push({ + regionId, + fromPortId, + toPortId, + }) + } + } + + const startPortId = this.problem.routeStartPort[routeId] + const endPortId = this.problem.routeEndPort[routeId] + + if (routeSegments.length === 0) { + return startPortId === endPortId + ? { + orderedPortIds: [startPortId], + orderedRegionIds: [], + } + : null + } + + const segmentsByPort = new Map< + PortId, + Array<{ + segmentIndex: number + regionId: RegionId + fromPortId: PortId + toPortId: PortId + }> + >() + + routeSegments.forEach((routeSegment, segmentIndex) => { + const indexedSegment = { + segmentIndex, + ...routeSegment, + } + + const fromPortSegments = segmentsByPort.get(routeSegment.fromPortId) ?? [] + fromPortSegments.push(indexedSegment) + segmentsByPort.set(routeSegment.fromPortId, fromPortSegments) + + const toPortSegments = segmentsByPort.get(routeSegment.toPortId) ?? [] + toPortSegments.push(indexedSegment) + segmentsByPort.set(routeSegment.toPortId, toPortSegments) + }) + + 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) { + return null + } + + const nextSegment = nextSegments[0]! + const nextPortId = + nextSegment.fromPortId === currentPortId + ? nextSegment.toPortId + : nextSegment.fromPortId + + usedSegmentIndices.add(nextSegment.segmentIndex) + orderedRegionIds.push(nextSegment.regionId) + orderedPortIds.push(nextPortId) + previousPortId = currentPortId + currentPortId = nextPortId + } + + return usedSegmentIndices.size === routeSegments.length + ? { + orderedPortIds, + orderedRegionIds, + } + : null + } + + getRoutePortTraversalsFromOrderedPath( + routeId: RouteId, + orderedRoutePath: OrderedRoutePath, + ): RoutePortTraversal[] { + const traversals: RoutePortTraversal[] = [] + + for ( + let portIndex = 1; + portIndex < orderedRoutePath.orderedPortIds.length - 1; + portIndex++ + ) { + const portId = orderedRoutePath.orderedPortIds[portIndex]! + const previousPortId = orderedRoutePath.orderedPortIds[portIndex - 1]! + const nextPortId = orderedRoutePath.orderedPortIds[portIndex + 1]! + const previousRegionId = orderedRoutePath.orderedRegionIds[portIndex - 1]! + const nextRegionId = orderedRoutePath.orderedRegionIds[portIndex]! + const incidentRegionIds = this.topology.incidentPortRegion[portId] ?? [] + const regionId1 = incidentRegionIds[0] + const regionId2 = incidentRegionIds[1] + + if (regionId1 === undefined || regionId2 === undefined) { + continue + } + + if (regionId1 === previousRegionId && regionId2 === nextRegionId) { + traversals.push({ + routeId, + portId, + regionId1, + regionId2, + otherPortIdInRegion1: previousPortId, + otherPortIdInRegion2: nextPortId, + edgeKey: getRegionPairKey(regionId1, regionId2), + }) + continue + } + + if (regionId1 === nextRegionId && regionId2 === previousRegionId) { + traversals.push({ + routeId, + portId, + regionId1, + regionId2, + otherPortIdInRegion1: nextPortId, + otherPortIdInRegion2: previousPortId, + edgeKey: getRegionPairKey(regionId1, regionId2), + }) + } + } + + return traversals + } + + getRoutePortTraversalsFromSolvedSegments( + routeId: RouteId, + solvedSegments: SolvedRouteSegment[], + ): RoutePortTraversal[] { + const traversals: RoutePortTraversal[] = [] + + for ( + let segmentIndex = 0; + segmentIndex < solvedSegments.length - 1; + segmentIndex++ + ) { + const leftSegment = solvedSegments[segmentIndex]! + const rightSegment = solvedSegments[segmentIndex + 1]! + + if (leftSegment.toPortId !== rightSegment.fromPortId) { + continue + } + + const portId = leftSegment.toPortId + const incidentRegionIds = this.topology.incidentPortRegion[portId] ?? [] + const regionId1 = incidentRegionIds[0] + const regionId2 = incidentRegionIds[1] + + if (regionId1 === undefined || regionId2 === undefined) { + continue + } + + if ( + regionId1 === leftSegment.regionId && + regionId2 === rightSegment.regionId + ) { + traversals.push({ + routeId, + portId, + regionId1, + regionId2, + otherPortIdInRegion1: leftSegment.fromPortId, + otherPortIdInRegion2: rightSegment.toPortId, + edgeKey: getRegionPairKey(regionId1, regionId2), + solvedSegmentIndex: segmentIndex, + }) + continue + } + + if ( + regionId1 === rightSegment.regionId && + regionId2 === leftSegment.regionId + ) { + traversals.push({ + routeId, + portId, + regionId1, + regionId2, + otherPortIdInRegion1: rightSegment.toPortId, + otherPortIdInRegion2: leftSegment.fromPortId, + edgeKey: getRegionPairKey(regionId1, regionId2), + solvedSegmentIndex: segmentIndex, + }) + } + } + + return traversals + } + + getOtherPortIdForTraversalRegion( + traversal: RoutePortTraversal, + regionId: RegionId, + ): PortId | undefined { + if (traversal.regionId1 === regionId) { + return traversal.otherPortIdInRegion1 + } + + if (traversal.regionId2 === regionId) { + return traversal.otherPortIdInRegion2 + } + + return undefined + } + + replacePortInRouteSegment( + regionSegments: [RouteId, PortId, PortId][], + routeId: RouteId, + otherPortId: PortId, + oldPortId: PortId, + newPortId: PortId, + ): boolean { + for ( + let segmentIndex = 0; + segmentIndex < regionSegments.length; + segmentIndex++ + ) { + const [segmentRouteId, port1Id, port2Id] = regionSegments[segmentIndex]! + + if (segmentRouteId !== routeId) { + continue + } + + const matchesSegment = + (port1Id === otherPortId && port2Id === oldPortId) || + (port1Id === oldPortId && port2Id === otherPortId) + + if (!matchesSegment) { + continue + } + + regionSegments[segmentIndex] = [ + routeId, + port1Id === oldPortId ? newPortId : port1Id, + port2Id === oldPortId ? newPortId : port2Id, + ] + return true + } + + return false + } + + isPortCompatibleWithRouteNet(routeId: RouteId, portId: PortId): boolean { + const routeNetId = this.problem.routeNet[routeId] + const reservedNetIds = this.problemSetup.portEndpointNetIds[portId] + + if (!reservedNetIds) { + return true + } + + for (const netId of reservedNetIds) { + if (netId !== routeNetId) { + return false + } + } + + return true + } + + isPortUsableByRouteAfterSwap( + routeId: RouteId, + portId: PortId, + regionSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, + ignoredRouteIds: RouteId[], + ): boolean { + if (!this.isPortCompatibleWithRouteNet(routeId, portId)) { + return false + } + + const ignoredRouteIdSet = new Set(ignoredRouteIds) + const routeNetId = this.problem.routeNet[routeId] + + for (const regionSegments of regionSegmentsByRegion) { + for (const [segmentRouteId, port1Id, port2Id] of regionSegments) { + if (ignoredRouteIdSet.has(segmentRouteId)) { + continue + } + + if (port1Id !== portId && port2Id !== portId) { + continue + } + + if (this.problem.routeNet[segmentRouteId] !== routeNetId) { + return false + } + } + } + + return true + } + + rebuildPortAssignmentsFromRegionSegments() { + const { state } = this + state.portAssignment.fill(-1) + + for (const regionSegments of state.regionSegments) { + for (const [routeId, port1Id, port2Id] of regionSegments) { + const routeNetId = this.problem.routeNet[routeId] + state.portAssignment[port1Id] = routeNetId + state.portAssignment[port2Id] = routeNetId + } + } + } + + tryCreateImprovingPortSwap( + currentTraversal: RoutePortTraversal, + otherTraversal: RoutePortTraversal, + currentRoutePortIds: Set, + otherRoutePortIds: Set, + regionSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, + regionIntersectionCaches: RegionIntersectionCache[], + ): + | { + affectedRegionIds: RegionId[] + candidateRegionSegmentsById: Map + candidateRegionCachesById: Map + } + | undefined { + if (currentTraversal.portId === otherTraversal.portId) { + return + } + + if ( + currentRoutePortIds.has(otherTraversal.portId) || + otherRoutePortIds.has(currentTraversal.portId) + ) { + return + } + + if ( + !this.isPortUsableByRouteAfterSwap( + currentTraversal.routeId, + otherTraversal.portId, + regionSegmentsByRegion, + [otherTraversal.routeId], + ) || + !this.isPortUsableByRouteAfterSwap( + otherTraversal.routeId, + currentTraversal.portId, + regionSegmentsByRegion, + [currentTraversal.routeId], + ) + ) { + return + } + + const affectedRegionIds = [ + ...new Set([ + currentTraversal.regionId1, + currentTraversal.regionId2, + otherTraversal.regionId1, + otherTraversal.regionId2, + ]), + ] + const candidateRegionSegmentsById = new Map< + RegionId, + [RouteId, PortId, PortId][] + >( + affectedRegionIds.map((regionId) => [ + regionId, + regionSegmentsByRegion[regionId]!.map( + ([routeId, fromPortId, toPortId]) => + [routeId, fromPortId, toPortId] as [RouteId, PortId, PortId], + ), + ]), + ) + + for (const regionId of affectedRegionIds) { + const currentOtherPortId = this.getOtherPortIdForTraversalRegion( + currentTraversal, + regionId, + ) + const otherOtherPortId = this.getOtherPortIdForTraversalRegion( + otherTraversal, + regionId, + ) + + if ( + currentOtherPortId === undefined || + otherOtherPortId === undefined || + currentOtherPortId === otherTraversal.portId || + otherOtherPortId === currentTraversal.portId + ) { + return + } + + const candidateRegionSegments = candidateRegionSegmentsById.get(regionId) + if ( + !candidateRegionSegments || + !this.replacePortInRouteSegment( + candidateRegionSegments, + currentTraversal.routeId, + currentOtherPortId, + currentTraversal.portId, + otherTraversal.portId, + ) || + !this.replacePortInRouteSegment( + candidateRegionSegments, + otherTraversal.routeId, + otherOtherPortId, + otherTraversal.portId, + currentTraversal.portId, + ) + ) { + return + } + } + + const currentSummary = summarizeRegionCostsForRegionIds( + affectedRegionIds, + (regionId) => regionIntersectionCaches[regionId], + ) + const candidateRegionCachesById = new Map< + RegionId, + RegionIntersectionCache + >( + affectedRegionIds.map((regionId) => [ + regionId, + this.computeRegionIntersectionCacheFromSegments( + regionId, + candidateRegionSegmentsById.get(regionId)!, + ), + ]), + ) + const candidateSummary = summarizeRegionCostsForRegionIds( + affectedRegionIds, + (regionId) => candidateRegionCachesById.get(regionId), + ) + + return compareRegionCostSummaries(candidateSummary, currentSummary) < 0 + ? { + affectedRegionIds, + candidateRegionSegmentsById, + candidateRegionCachesById, + } + : undefined + } + + untangleRecentlySolvedRoute( + currentRouteId: RouteId, + solvedSegments: SolvedRouteSegment[], + ): { + regionSegmentsByRegion: Array<[RouteId, PortId, PortId][]> + regionIntersectionCaches: RegionIntersectionCache[] + dirtyRegionIds: Set + acceptedSwapCount: number + } { + const { state } = this + const workingRegionSegments = cloneRegionSegments(state.regionSegments) + const workingRegionIntersectionCaches = state.regionIntersectionCaches.map( + cloneRegionIntersectionCache, + ) + const dirtyRegionIds = new Set() + const mutableSolvedSegments = solvedSegments.map((segment) => ({ + ...segment, + })) + + for (const { regionId, fromPortId, toPortId } of mutableSolvedSegments) { + workingRegionSegments[regionId]!.push([ + currentRouteId, + fromPortId, + toPortId, + ]) + workingRegionIntersectionCaches[regionId] = + this.buildNextRegionIntersectionCache( + workingRegionIntersectionCaches[regionId]!, + regionId, + state.currentRouteNetId!, + fromPortId, + toPortId, + ) + dirtyRegionIds.add(regionId) + } + + let acceptedSwapCount = 0 + + while (true) { + const currentRoutePath = this.getOrderedRoutePathFromRegionSegments( + currentRouteId, + workingRegionSegments, + ) + if (!currentRoutePath) { + break + } + + const currentRoutePortIds = new Set(currentRoutePath.orderedPortIds) + const currentTraversals = this.getRoutePortTraversalsFromSolvedSegments( + currentRouteId, + mutableSolvedSegments, + ) + const otherRouteContextByRouteId = new Map< + RouteId, + { + portIds: Set + traversals: RoutePortTraversal[] + } + >() + let appliedSwap = false + + for (const currentTraversal of currentTraversals) { + const currentRegionCost1 = + workingRegionIntersectionCaches[currentTraversal.regionId1] + ?.existingRegionCost ?? 0 + const currentRegionCost2 = + workingRegionIntersectionCaches[currentTraversal.regionId2] + ?.existingRegionCost ?? 0 + + if ( + currentRegionCost1 <= IMPROVEMENT_EPSILON && + currentRegionCost2 <= IMPROVEMENT_EPSILON + ) { + continue + } + + const routeIdsInRegion1 = new Set( + workingRegionSegments[currentTraversal.regionId1]!.map( + ([routeId]) => routeId, + ), + ) + const candidateRouteIds = new Set() + + for (const [routeId] of workingRegionSegments[ + currentTraversal.regionId2 + ]!) { + if ( + routeId === currentRouteId || + !routeIdsInRegion1.has(routeId) || + !this.canRewriteSolvedRoute(routeId) + ) { + continue + } + + candidateRouteIds.add(routeId) + } + + for (const otherRouteId of candidateRouteIds) { + let otherRouteContext = otherRouteContextByRouteId.get(otherRouteId) + + if (!otherRouteContext) { + const otherRoutePath = this.getOrderedRoutePathFromRegionSegments( + otherRouteId, + workingRegionSegments, + ) + if (!otherRoutePath) { + continue + } + + otherRouteContext = { + portIds: new Set(otherRoutePath.orderedPortIds), + traversals: this.getRoutePortTraversalsFromOrderedPath( + otherRouteId, + otherRoutePath, + ), + } + otherRouteContextByRouteId.set(otherRouteId, otherRouteContext) + } + + for (const otherTraversal of otherRouteContext.traversals) { + if (otherTraversal.edgeKey !== currentTraversal.edgeKey) { + continue + } + + const swapResult = this.tryCreateImprovingPortSwap( + currentTraversal, + otherTraversal, + currentRoutePortIds, + otherRouteContext.portIds, + workingRegionSegments, + workingRegionIntersectionCaches, + ) + + if (!swapResult) { + continue + } + + for (const regionId of swapResult.affectedRegionIds) { + workingRegionSegments[regionId] = + swapResult.candidateRegionSegmentsById.get(regionId)! + workingRegionIntersectionCaches[regionId] = + swapResult.candidateRegionCachesById.get(regionId)! + dirtyRegionIds.add(regionId) + } + + if (currentTraversal.solvedSegmentIndex !== undefined) { + const leftSolvedSegment = + mutableSolvedSegments[currentTraversal.solvedSegmentIndex] + const rightSolvedSegment = + mutableSolvedSegments[currentTraversal.solvedSegmentIndex + 1] + + if (leftSolvedSegment && rightSolvedSegment) { + leftSolvedSegment.toPortId = otherTraversal.portId + rightSolvedSegment.fromPortId = otherTraversal.portId + } + } + + acceptedSwapCount += 1 + appliedSwap = true + break + } + + if (appliedSwap) { + break + } + } + + if (appliedSwap) { + break + } + } + + if (!appliedSwap) { + break + } + } + + return { + regionSegmentsByRegion: workingRegionSegments, + regionIntersectionCaches: workingRegionIntersectionCaches, + dirtyRegionIds, + acceptedSwapCount, + } + } + resetRoutingStateForRerip() { const { topology, problem, state } = this @@ -757,16 +1539,23 @@ export class TinyHyperGraphSolver extends BaseSolver { if (currentRouteId === undefined) return const solvedSegments = this.getSolvedPathSegments(finalCandidate) + const untangledRoute = this.untangleRecentlySolvedRoute( + currentRouteId, + solvedSegments, + ) - 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) + for (const regionId of untangledRoute.dirtyRegionIds) { + state.regionSegments[regionId] = + untangledRoute.regionSegmentsByRegion[regionId]! + state.regionIntersectionCaches[regionId] = + untangledRoute.regionIntersectionCaches[regionId]! + } + this.rebuildPortAssignmentsFromRegionSegments() + this.stats = { + ...this.stats, + untangleAcceptedSwapCount: + (this.stats.untangleAcceptedSwapCount ?? 0) + + untangledRoute.acceptedSwapCount, } state.candidateQueue.clear() diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index cc8d4c6..14b5524 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -52,9 +52,7 @@ export interface TinyHyperGraphSectionSolverOptions } const applyTinyHyperGraphSectionSolverOptions = ( - solver: - | TinyHyperGraphSectionSearchSolver - | TinyHyperGraphSectionSolver, + solver: TinyHyperGraphSectionSearchSolver | TinyHyperGraphSectionSolver, options?: TinyHyperGraphSectionSolverOptions, ) => { applyTinyHyperGraphSolverOptions(solver, options) @@ -131,7 +129,8 @@ const restoreSolvedStateSnapshot = ( const clonedSnapshot = cloneSolvedStateSnapshot(snapshot) solver.state.portAssignment = clonedSnapshot.portAssignment solver.state.regionSegments = clonedSnapshot.regionSegments - solver.state.regionIntersectionCaches = clonedSnapshot.regionIntersectionCaches + solver.state.regionIntersectionCaches = + clonedSnapshot.regionIntersectionCaches } const summarizeRegionIntersectionCaches = ( @@ -245,7 +244,8 @@ const getOrderedRoutePath = ( orderedRegionIds: RegionId[] } => { const routeSegments = solution.solvedRoutePathSegments[routeId] ?? [] - const routeSegmentRegionIds = solution.solvedRoutePathRegionIds?.[routeId] ?? [] + const routeSegmentRegionIds = + solution.solvedRoutePathRegionIds?.[routeId] ?? [] const startPortId = problem.routeStartPort[routeId] const endPortId = problem.routeEndPort[routeId] @@ -616,7 +616,11 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { applyFixedSegments() { for (const routePlan of this.routePlans) { - for (const { regionId, fromPortId, toPortId } of routePlan.fixedSegments) { + for (const { + regionId, + fromPortId, + toPortId, + } of routePlan.fixedSegments) { this.state.currentRouteNetId = this.problem.routeNet[routePlan.routeId] this.state.regionSegments[regionId]!.push([ routePlan.routeId, @@ -675,6 +679,10 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { return super.getStartingNextRegionId(routeId, startingPortId) } + protected override canRewriteSolvedRoute(routeId: RouteId): boolean { + return this.activeRouteIds.includes(routeId) + } + override resetRoutingStateForRerip() { if (!this.fixedSnapshot) { super.resetRoutingStateForRerip() @@ -944,7 +952,8 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { this.stats = { ...this.stats, sectionBaselineMaxRegionCost: this.sectionBaselineSummary.maxRegionCost, - sectionBaselineTotalRegionCost: this.sectionBaselineSummary.totalRegionCost, + sectionBaselineTotalRegionCost: + this.sectionBaselineSummary.totalRegionCost, effectiveRipThresholdStart: this.RIP_THRESHOLD_START, effectiveRipThresholdEnd: this.RIP_THRESHOLD_END, effectiveMaxRips: this.MAX_RIPS, diff --git a/tests/solver/route-untangle-swap.test.ts b/tests/solver/route-untangle-swap.test.ts new file mode 100644 index 0000000..fa2c379 --- /dev/null +++ b/tests/solver/route-untangle-swap.test.ts @@ -0,0 +1,188 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { expect, test } from "bun:test" +import { type Candidate, TinyHyperGraphSolver } from "lib/index" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" + +const crossingSwapFixture: SerializedHyperGraph = { + regions: [ + { + regionId: "start-0", + pointIds: ["s0"], + d: { center: { x: -4, y: 2 }, width: 1, height: 1 }, + }, + { + regionId: "start-1", + pointIds: ["s1"], + d: { center: { x: -4, y: -2 }, width: 1, height: 1 }, + }, + { + regionId: "left", + pointIds: ["s0", "s1", "edge-top", "edge-bottom"], + d: { center: { x: -1.5, y: 0 }, width: 3, height: 6 }, + }, + { + regionId: "right", + pointIds: ["edge-top", "edge-bottom", "e0", "e1"], + d: { center: { x: 1.5, y: 0 }, width: 3, height: 6 }, + }, + { + regionId: "end-0", + pointIds: ["e0"], + d: { center: { x: 4, y: 2 }, width: 1, height: 1 }, + }, + { + regionId: "end-1", + pointIds: ["e1"], + d: { center: { x: 4, y: -2 }, width: 1, height: 1 }, + }, + ], + ports: [ + { + portId: "s0", + region1Id: "start-0", + region2Id: "left", + d: { x: -3, y: 2, z: 0 }, + }, + { + portId: "s1", + region1Id: "start-1", + region2Id: "left", + d: { x: -3, y: -2, z: 0 }, + }, + { + portId: "edge-top", + region1Id: "left", + region2Id: "right", + d: { x: 0, y: 2, z: 0 }, + }, + { + portId: "edge-bottom", + region1Id: "left", + region2Id: "right", + d: { x: 0, y: -2, z: 0 }, + }, + { + portId: "e0", + region1Id: "right", + region2Id: "end-0", + d: { x: 3, y: 2, z: 0 }, + }, + { + portId: "e1", + region1Id: "right", + region2Id: "end-1", + d: { x: 3, y: -2, z: 0 }, + }, + ], + connections: [ + { + connectionId: "route-0", + startRegionId: "start-0", + endRegionId: "end-0", + mutuallyConnectedNetworkId: "net-0", + }, + { + connectionId: "route-1", + startRegionId: "start-1", + endRegionId: "end-1", + mutuallyConnectedNetworkId: "net-1", + }, + ], +} + +const createTwoSegmentCandidate = ( + startPortId: number, + middlePortId: number, + leftRegionId: number, + rightRegionId: number, +): Candidate => ({ + nextRegionId: rightRegionId, + portId: middlePortId, + f: 0, + g: 0, + h: 0, + prevCandidate: { + nextRegionId: leftRegionId, + portId: startPortId, + f: 0, + g: 0, + h: 0, + }, +}) + +test("onPathFound swaps an earlier route's edge port when that untangles two regions", () => { + const { topology, problem } = loadSerializedHyperGraph(crossingSwapFixture) + const solver = new TinyHyperGraphSolver(topology, problem) + const portIndexById = new Map() + const regionIndexById = new Map() + + topology.portMetadata?.forEach((metadata, portId) => { + if (typeof metadata?.serializedPortId === "string") { + portIndexById.set(metadata.serializedPortId, portId) + } + }) + topology.regionMetadata?.forEach((metadata, regionId) => { + if (typeof metadata?.serializedRegionId === "string") { + regionIndexById.set(metadata.serializedRegionId, regionId) + } + }) + + const leftRegionId = regionIndexById.get("left") + const rightRegionId = regionIndexById.get("right") + const s0 = portIndexById.get("s0") + const s1 = portIndexById.get("s1") + const edgeTop = portIndexById.get("edge-top") + const edgeBottom = portIndexById.get("edge-bottom") + const e0 = portIndexById.get("e0") + const e1 = portIndexById.get("e1") + + if ( + leftRegionId === undefined || + rightRegionId === undefined || + s0 === undefined || + s1 === undefined || + edgeTop === undefined || + edgeBottom === undefined || + e0 === undefined || + e1 === undefined + ) { + throw new Error("Fixture ids did not map to topology indexes") + } + + solver.state.currentRouteId = 0 + solver.state.currentRouteNetId = problem.routeNet[0] + solver.state.goalPortId = problem.routeEndPort[0] + solver.onPathFound( + createTwoSegmentCandidate(s0, edgeBottom, leftRegionId, rightRegionId), + ) + + solver.state.currentRouteId = 1 + solver.state.currentRouteNetId = problem.routeNet[1] + solver.state.goalPortId = problem.routeEndPort[1] + solver.onPathFound( + createTwoSegmentCandidate(s1, edgeTop, leftRegionId, rightRegionId), + ) + + expect( + solver.state.regionIntersectionCaches[leftRegionId]?.existingRegionCost, + ).toBe(0) + expect( + solver.state.regionIntersectionCaches[rightRegionId]?.existingRegionCost, + ).toBe(0) + + expect(solver.state.regionSegments[leftRegionId]).toEqual( + expect.arrayContaining([ + [0, s0, edgeTop], + [1, s1, edgeBottom], + ]), + ) + expect(solver.state.regionSegments[rightRegionId]).toEqual( + expect.arrayContaining([ + [0, edgeTop, e0], + [1, edgeBottom, e1], + ]), + ) + expect(solver.state.portAssignment[edgeTop]).toBe(problem.routeNet[0]) + expect(solver.state.portAssignment[edgeBottom]).toBe(problem.routeNet[1]) + expect(solver.stats.untangleAcceptedSwapCount).toBe(1) +})