From 4dc3b217a3c381adc49b975e05157cc4ab6dff7d Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 14 Apr 2026 13:19:33 -0700 Subject: [PATCH 1/5] wip --- lib/bus-solver/TinyHyperGraphBusSolver.ts | 990 ++++++++++++++++++--- tests/solver/bus-region-span-repro.test.ts | 53 +- 2 files changed, 896 insertions(+), 147 deletions(-) diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index f69c322..98adac7 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -32,6 +32,7 @@ import { type BoundaryStep, type BusCenterCandidate, type BusPreview, + type PreviewRoutingStateSnapshot, type TinyHyperGraphBusSolverOptions, type TracePreview, type TraceSegment, @@ -50,6 +51,24 @@ import { snapshotPreviewRoutingState as snapshotPreviewRoutingStateValue, } from "./previewRoutingState" +interface AlongsideTraceSearchNode { + portId: PortId + regionId: RegionId + segments: TraceSegment[] + guideProgress: number + travelCost: number + priority: number + visitedPortIds: Set + visitedStateKeys: Set +} + +interface AlongsideTraceSearchOption { + segments: TraceSegment[] + terminalPortId: PortId + terminalRegionId: RegionId + searchScore: number +} + export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { BUS_END_MARGIN_STEPS = 3 BUS_MAX_REMAINDER_STEPS = 8 @@ -63,6 +82,13 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { MANUAL_CENTER_FINISH_MAX_HOPS = 2 MANUAL_CENTER_FINISH_PORT_OPTIONS_PER_BOUNDARY = 6 MANUAL_CENTER_FINISH_CANDIDATE_LIMIT = 24 + TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT = 6 + TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH = 24 + TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT = 8 + TRACE_ALONGSIDE_LANE_WEIGHT = 1 + TRACE_ALONGSIDE_REGRESSION_WEIGHT = 2 + BUS_INTERSECTION_PREVIEW_PENALTY = 20 + PARTIAL_INTERSECTION_EXPANSION_THRESHOLD = 2 readonly busTraceOrder: BusTraceOrder readonly centerTraceIndex: number @@ -80,6 +106,10 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { private readonly regionIndexBySerializedId = new Map() private lastExpandedCandidate?: BusCenterCandidate private lastPreview?: BusPreview + private bestCompleteFallbackPreview?: BusPreview + private bestCompleteFallbackSnapshot?: PreviewRoutingStateSnapshot + private bestCompleteFallbackIntersectionCount = Number.POSITIVE_INFINITY + private bestCompleteFallbackCost = Number.POSITIVE_INFINITY constructor( topology: TinyHyperGraphTopology, @@ -166,6 +196,10 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { ) this.lastExpandedCandidate = undefined this.lastPreview = undefined + this.bestCompleteFallbackPreview = undefined + this.bestCompleteFallbackSnapshot = undefined + this.bestCompleteFallbackIntersectionCount = Number.POSITIVE_INFINITY + this.bestCompleteFallbackCost = Number.POSITIVE_INFINITY clearPreviewRoutingStateValue(this.state, this.topology.regionCount) this.resetCandidateBestCosts() this.state.candidateQueue = new MinHeap([], compareBusCandidatesByF) @@ -206,6 +240,10 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { | BusCenterCandidate | undefined if (!currentCandidate) { + if (this.tryAcceptBestCompleteFallbackPreview()) { + return + } + this.failed = true this.error = "Centerline candidates are exhausted without a non-intersecting bus solution" @@ -235,8 +273,21 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { const hasIntersections = preview.sameLayerIntersectionCount > 0 || preview.crossingLayerIntersectionCount > 0 + const totalIntersectionCount = + preview.sameLayerIntersectionCount + preview.crossingLayerIntersectionCount const hasInferenceFailure = preview.reason !== undefined && !hasIntersections + const allowIntersectingPartialExpansion = + !currentCandidate.atGoal && + totalIntersectionCount <= this.PARTIAL_INTERSECTION_EXPANSION_THRESHOLD + + if ( + currentCandidate.atGoal && + preview.completeTraceCount === this.problem.routeCount && + totalIntersectionCount > 0 + ) { + this.maybeStoreBestCompleteFallbackPreview(preview, totalIntersectionCount) + } if ( currentCandidate.atGoal && @@ -249,7 +300,11 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { return } - if (!hasIntersections && !hasInferenceFailure && !currentCandidate.atGoal) { + if ( + (!hasIntersections || allowIntersectingPartialExpansion) && + !hasInferenceFailure && + !currentCandidate.atGoal + ) { for (const nextCandidate of this.getAvailableCenterMoves( currentCandidate, )) { @@ -267,6 +322,10 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } if (this.state.candidateQueue.length === 0) { + if (this.tryAcceptBestCompleteFallbackPreview()) { + return + } + this.failed = true this.error = preview.reason ?? @@ -358,7 +417,8 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { totalLength: completeBusPreview.totalLength, totalCost: completeBusPreview.totalLength * this.DISTANCE_TO_COST + - totalRegionCost, + totalRegionCost + + totalIntersectionCount * this.BUS_INTERSECTION_PREVIEW_PENALTY, completeTraceCount: completeBusPreview.tracePreviews.length, sameLayerIntersectionCount, crossingLayerIntersectionCount, @@ -439,7 +499,8 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { totalCost: totalLength * this.DISTANCE_TO_COST + totalRegionCost + - totalPreviewCost, + totalPreviewCost + + totalIntersectionCount * this.BUS_INTERSECTION_PREVIEW_PENALTY, completeTraceCount: tracePreviews.filter((preview) => preview.complete) .length, sameLayerIntersectionCount, @@ -526,7 +587,9 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } const localOwners = new Map(usedPortOwners) - ensurePortOwnership(routeId, startPortId, localOwners) + if (!ensurePortOwnership(routeId, startPortId, localOwners)) { + return undefined + } const segments: TraceSegment[] = [] let currentPortId = startPortId @@ -583,7 +646,7 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { ) const targetGuideProgress = getPolylineLength(this.topology, centerPortIds) const minSharedStepCount = 0 - let bestPreview: TracePreview | undefined + let bestExactPreview: TracePreview | undefined let bestScore = Number.POSITIVE_INFINITY let bestSharedStepCount = -1 @@ -621,12 +684,12 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { const score = shortfallPenalty * 2 + overshootPenalty * 4 + lagPenalty if ( - !bestPreview || + !bestExactPreview || score < bestScore - BUS_CANDIDATE_EPSILON || (Math.abs(score - bestScore) <= BUS_CANDIDATE_EPSILON && sharedStepCount > bestSharedStepCount) ) { - bestPreview = { + bestExactPreview = { ...prefixPreview, previewCost: score, } @@ -635,7 +698,63 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } } - return bestPreview + if (!bestExactPreview) { + return undefined + } + + if (bestSharedStepCount > 0) { + return bestExactPreview + } + + const searchPrefixPreview = this.buildPrefixTracePreview( + traceIndex, + bestSharedStepCount, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) + if (!searchPrefixPreview || searchPrefixPreview.terminalRegionId === undefined) { + return bestExactPreview + } + + const guidePortIds = getGuidePortIds(centerPath, bestSharedStepCount) + const alongsideOptions = this.searchTraceAlongsideOptions({ + traceIndex, + startPortId: searchPrefixPreview.terminalPortId, + startRegionId: searchPrefixPreview.terminalRegionId, + guidePortIds, + usedPortOwners, + targetGuideProgress: getPolylineLength(this.topology, guidePortIds), + maxSteps: this.getPartialTraceSearchMaxSteps(maxSharedStepCount), + maxOptions: 1, + initialVisitedPortIds: this.getTracePreviewVisitedPortIds( + searchPrefixPreview, + ), + initialVisitedStateKeys: + this.getTracePreviewSearchStartStateKeys(searchPrefixPreview), + }) + const bestAlongsideOption = alongsideOptions[0] + + if (!bestAlongsideOption) { + return bestExactPreview + } + + const searchScore = + bestAlongsideOption.searchScore + maxSharedStepCount * this.tracePitch + if ((bestExactPreview.previewCost ?? Number.POSITIVE_INFINITY) <= searchScore) { + return bestExactPreview + } + + return { + ...searchPrefixPreview, + segments: [ + ...searchPrefixPreview.segments, + ...bestAlongsideOption.segments, + ], + terminalPortId: bestAlongsideOption.terminalPortId, + terminalRegionId: bestAlongsideOption.terminalRegionId, + previewCost: searchScore, + } } private buildCompleteTracePreviewOptions( @@ -656,6 +775,7 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { const routeId = this.busTraceOrder.traces[traceIndex]!.routeId const previewOptions: TracePreview[] = [] + const previewOptionKeys = new Set() for ( let sharedStepCount = boundarySteps.length; @@ -689,7 +809,7 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { continue } - const remainderSegments = this.inferEndRemainderSegments( + const greedyRemainderSegments = this.inferEndRemainderSegmentsGreedy( traceIndex, currentPortId, currentRegionId, @@ -697,21 +817,252 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { sharedStepCount, usedPortOwners, ) - if (!remainderSegments) { - continue + + if (greedyRemainderSegments) { + const greedySegments = [ + ...prefixPreview.segments, + ...greedyRemainderSegments, + ] + const greedyPreviewKey = this.getTracePreviewPathKey( + greedySegments, + this.problem.routeEndPort[routeId]!, + undefined, + ) + + if (!previewOptionKeys.has(greedyPreviewKey)) { + previewOptionKeys.add(greedyPreviewKey) + previewOptions.push({ + traceIndex, + routeId, + segments: greedySegments, + complete: true, + terminalPortId: this.problem.routeEndPort[routeId]!, + previewCost: 0, + }) + } } - previewOptions.push({ - traceIndex, - routeId, - segments: [...prefixPreview.segments, ...remainderSegments], - complete: true, - terminalPortId: this.problem.routeEndPort[routeId]!, - previewCost: 0, - }) + if (sharedStepCount === 0 || !greedyRemainderSegments) { + const remainingBoundaryStepCount = Math.max( + boundarySteps.length - sharedStepCount, + 0, + ) + const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) + const completionOptions = this.searchTraceAlongsideOptions({ + traceIndex, + startPortId: prefixPreview.terminalPortId, + startRegionId: prefixPreview.terminalRegionId!, + guidePortIds, + usedPortOwners, + goalPortId: this.problem.routeEndPort[routeId]!, + maxSteps: this.getCompleteTraceSearchMaxSteps( + remainingBoundaryStepCount, + ), + maxOptions: this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, + initialVisitedPortIds: this.getTracePreviewVisitedPortIds( + prefixPreview, + ), + initialVisitedStateKeys: + this.getTracePreviewSearchStartStateKeys(prefixPreview), + }) + + for (const completionOption of completionOptions) { + const combinedSegments = [ + ...prefixPreview.segments, + ...completionOption.segments, + ] + const previewKey = this.getTracePreviewPathKey( + combinedSegments, + this.problem.routeEndPort[routeId]!, + undefined, + ) + + if (previewOptionKeys.has(previewKey)) { + continue + } + + previewOptionKeys.add(previewKey) + previewOptions.push({ + traceIndex, + routeId, + segments: combinedSegments, + complete: true, + terminalPortId: this.problem.routeEndPort[routeId]!, + previewCost: completionOption.searchScore, + }) + } + } } return previewOptions + .sort( + (left, right) => + (left.previewCost ?? 0) - (right.previewCost ?? 0) || + getTracePreviewLength(this.topology, left) - + getTracePreviewLength(this.topology, right), + ) + .slice(0, this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT) + } + + private inferEndRemainderSegmentsGreedy( + traceIndex: number, + startPortId: PortId, + startRegionId: RegionId, + centerPath: BusCenterCandidate[], + sharedStepCount: number, + usedPortOwners: ReadonlyMap, + ): TraceSegment[] | undefined { + const routeId = this.busTraceOrder.traces[traceIndex]!.routeId + const endPortId = this.problem.routeEndPort[routeId]! + + if (this.topology.portZ[endPortId] !== 0) { + return undefined + } + + if (isPortIncidentToRegion(this.topology, endPortId, startRegionId)) { + if ( + ensurePortOwnership(routeId, endPortId, new Map(usedPortOwners)) && + startPortId !== endPortId + ) { + return [ + { + regionId: startRegionId, + fromPortId: startPortId, + toPortId: endPortId, + }, + ] + } + + return [] + } + + const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) + const goalTransitRegionIds = + this.topology.incidentPortRegion[endPortId]?.filter( + (regionId) => regionId !== undefined, + ) ?? [] + const currentNetId = this.problem.routeNet[routeId]! + const localOwners = new Map(usedPortOwners) + if (!ensurePortOwnership(routeId, startPortId, localOwners)) { + return undefined + } + + const visitedStates = new Set([`${startPortId}:${startRegionId}`]) + const segments: TraceSegment[] = [] + let currentPortId = startPortId + let currentRegionId = startRegionId + + for ( + let stepIndex = 0; + stepIndex < this.BUS_MAX_REMAINDER_STEPS; + stepIndex++ + ) { + if (isPortIncidentToRegion(this.topology, endPortId, currentRegionId)) { + if (!ensurePortOwnership(routeId, endPortId, localOwners)) { + return undefined + } + + if (currentPortId !== endPortId) { + segments.push({ + regionId: currentRegionId, + fromPortId: currentPortId, + toPortId: endPortId, + }) + } + + return segments + } + + let bestMove: + | { + boundaryPortId: PortId + nextRegionId: RegionId + score: number + } + | undefined + + for (const boundaryPortId of this.topology.regionIncidentPorts[ + currentRegionId + ] ?? []) { + if ( + boundaryPortId === currentPortId || + this.topology.portZ[boundaryPortId] !== 0 + ) { + continue + } + + const nextRegionId = + this.topology.incidentPortRegion[boundaryPortId]?.[0] === + currentRegionId + ? this.topology.incidentPortRegion[boundaryPortId]?.[1] + : this.topology.incidentPortRegion[boundaryPortId]?.[0] + + if ( + nextRegionId === undefined || + this.isRegionReservedForDifferentBusNet(currentNetId, nextRegionId) || + visitedStates.has(`${boundaryPortId}:${nextRegionId}`) + ) { + continue + } + + const owner = localOwners.get(boundaryPortId) + if (owner !== undefined && owner !== routeId) { + continue + } + + const goalDistance = getPortDistance( + this.topology, + boundaryPortId, + endPortId, + ) + const guideDistance = getDistanceFromPortToPolyline( + this.topology, + boundaryPortId, + guidePortIds, + ) + const sidePenalty = this.getTraceSidePenalty(traceIndex, boundaryPortId) + const goalRegionBonus = goalTransitRegionIds.includes(nextRegionId) + ? -5 + : 0 + const score = + guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + + goalDistance * this.BUS_REMAINDER_GOAL_WEIGHT + + sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + + goalRegionBonus + + if ( + !bestMove || + score < bestMove.score - BUS_CANDIDATE_EPSILON || + (Math.abs(score - bestMove.score) <= BUS_CANDIDATE_EPSILON && + boundaryPortId < bestMove.boundaryPortId) + ) { + bestMove = { + boundaryPortId, + nextRegionId, + score, + } + } + } + + if (!bestMove) { + return undefined + } + + if (!ensurePortOwnership(routeId, bestMove.boundaryPortId, localOwners)) { + return undefined + } + + segments.push({ + regionId: currentRegionId, + fromPortId: currentPortId, + toPortId: bestMove.boundaryPortId, + }) + currentPortId = bestMove.boundaryPortId + currentRegionId = bestMove.nextRegionId + visitedStates.add(`${currentPortId}:${currentRegionId}`) + } + + return undefined } private buildBestCompleteBusPreview( @@ -923,165 +1274,506 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } } - private inferEndRemainderSegments( - traceIndex: number, - startPortId: PortId, - startRegionId: RegionId, - centerPath: BusCenterCandidate[], - sharedStepCount: number, - usedPortOwners: ReadonlyMap, - ): TraceSegment[] | undefined { + private searchTraceAlongsideOptions({ + traceIndex, + startPortId, + startRegionId, + guidePortIds, + usedPortOwners, + maxSteps, + maxOptions, + targetGuideProgress, + goalPortId, + initialVisitedPortIds = [], + initialVisitedStateKeys = [], + }: { + traceIndex: number + startPortId: PortId + startRegionId: RegionId + guidePortIds: readonly PortId[] + usedPortOwners: ReadonlyMap + maxSteps: number + maxOptions: number + targetGuideProgress?: number + goalPortId?: PortId + initialVisitedPortIds?: readonly PortId[] + initialVisitedStateKeys?: readonly string[] + }): AlongsideTraceSearchOption[] { const routeId = this.busTraceOrder.traces[traceIndex]!.routeId - const endPortId = this.problem.routeEndPort[routeId]! + const effectiveGuidePortIds = + guidePortIds.length > 0 ? guidePortIds : [startPortId] + const initialGuideProgress = getPortProgressAlongPolyline( + this.topology, + startPortId, + effectiveGuidePortIds, + ) + const visitedPortIds = new Set(initialVisitedPortIds) + const visitedStateKeys = new Set(initialVisitedStateKeys) + visitedPortIds.add(startPortId) + visitedStateKeys.add( + this.getTraceSearchStateKey(startPortId, startRegionId), + ) - if (this.topology.portZ[endPortId] !== 0) { - return undefined + const initialNode: AlongsideTraceSearchNode = { + portId: startPortId, + regionId: startRegionId, + segments: [], + guideProgress: initialGuideProgress, + travelCost: 0, + priority: + goalPortId === undefined + ? this.getPartialTraceSearchPriority( + traceIndex, + startPortId, + effectiveGuidePortIds, + initialGuideProgress, + 0, + targetGuideProgress ?? 0, + ) + : this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + startPortId, + effectiveGuidePortIds, + 0, + ), + visitedPortIds, + visitedStateKeys, } - if (isPortIncidentToRegion(this.topology, endPortId, startRegionId)) { + const searchOptions: AlongsideTraceSearchOption[] = [] + const searchOptionKeys = new Set() + const pushOption = (option: AlongsideTraceSearchOption) => { + const optionKey = this.getTracePreviewPathKey( + option.segments, + option.terminalPortId, + option.terminalRegionId, + ) + + if (searchOptionKeys.has(optionKey)) { + return + } + + searchOptionKeys.add(optionKey) + searchOptions.push(option) + } + const tryCompleteFromNode = (node: AlongsideTraceSearchNode) => { if ( - ensurePortOwnership(routeId, endPortId, new Map(usedPortOwners)) && - startPortId !== endPortId + goalPortId === undefined || + !isPortIncidentToRegion(this.topology, goalPortId, node.regionId) ) { - return [ - { - regionId: startRegionId, - fromPortId: startPortId, - toPortId: endPortId, - }, - ] + return } - return [] + const owner = usedPortOwners.get(goalPortId) + if (owner !== undefined && owner !== routeId) { + return + } + + const completionSegments = + node.portId === goalPortId + ? node.segments + : [ + ...node.segments, + { + regionId: node.regionId, + fromPortId: node.portId, + toPortId: goalPortId, + }, + ] + const completionTravelCost = + node.travelCost + + (node.portId === goalPortId + ? 0 + : getPortDistance(this.topology, node.portId, goalPortId) * + this.DISTANCE_TO_COST) + + pushOption({ + segments: completionSegments, + terminalPortId: goalPortId, + terminalRegionId: node.regionId, + searchScore: this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + goalPortId, + effectiveGuidePortIds, + completionTravelCost, + ), + }) } + const tryPartialFromNode = (node: AlongsideTraceSearchNode) => { + if (targetGuideProgress === undefined) { + return + } - const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) - const goalTransitRegionIds = - this.topology.incidentPortRegion[endPortId]?.filter( - (regionId) => regionId !== undefined, - ) ?? [] - const currentNetId = this.problem.routeNet[routeId]! - const localOwners = new Map(usedPortOwners) - if (!ensurePortOwnership(routeId, startPortId, localOwners)) { - return undefined + pushOption({ + segments: node.segments, + terminalPortId: node.portId, + terminalRegionId: node.regionId, + searchScore: this.getPartialTraceSearchPriority( + traceIndex, + node.portId, + effectiveGuidePortIds, + node.guideProgress, + node.travelCost, + targetGuideProgress, + ), + }) } - const visitedStates = new Set([`${startPortId}:${startRegionId}`]) - const segments: TraceSegment[] = [] - let currentPortId = startPortId - let currentRegionId = startRegionId + let beam = [initialNode] + tryCompleteFromNode(initialNode) + tryPartialFromNode(initialNode) - for ( - let stepIndex = 0; - stepIndex < this.BUS_MAX_REMAINDER_STEPS; - stepIndex++ - ) { - if (isPortIncidentToRegion(this.topology, endPortId, currentRegionId)) { - if (!ensurePortOwnership(routeId, endPortId, localOwners)) { - return undefined - } + for (let stepIndex = 0; stepIndex < maxSteps && beam.length > 0; stepIndex++) { + const nextBeamCandidates: AlongsideTraceSearchNode[] = [] - if (currentPortId !== endPortId) { - segments.push({ - regionId: currentRegionId, - fromPortId: currentPortId, - toPortId: endPortId, - }) - } + for (const node of beam) { + const moveCandidates: AlongsideTraceSearchNode[] = [] - return segments - } + for (const boundaryPortId of this.topology.regionIncidentPorts[ + node.regionId + ] ?? []) { + if ( + boundaryPortId === node.portId || + this.topology.portZ[boundaryPortId] !== 0 || + node.visitedPortIds.has(boundaryPortId) + ) { + continue + } - let bestMove: - | { - boundaryPortId: PortId - nextRegionId: RegionId - score: number + const nextRegionId = this.getOppositeRegionId( + boundaryPortId, + node.regionId, + ) + + if ( + nextRegionId === undefined || + this.isRegionReservedForDifferentBusNet( + this.problem.routeNet[routeId]!, + nextRegionId, + ) + ) { + continue } - | undefined - for (const boundaryPortId of this.topology.regionIncidentPorts[ - currentRegionId - ] ?? []) { - if ( - boundaryPortId === currentPortId || - this.topology.portZ[boundaryPortId] !== 0 - ) { - continue - } + const owner = usedPortOwners.get(boundaryPortId) + if (owner !== undefined && owner !== routeId) { + continue + } - const nextRegionId = - this.topology.incidentPortRegion[boundaryPortId]?.[0] === - currentRegionId - ? this.topology.incidentPortRegion[boundaryPortId]?.[1] - : this.topology.incidentPortRegion[boundaryPortId]?.[0] + const nextStateKey = this.getTraceSearchStateKey( + boundaryPortId, + nextRegionId, + ) + if (node.visitedStateKeys.has(nextStateKey)) { + continue + } - if ( - nextRegionId === undefined || - this.isRegionReservedForDifferentBusNet(currentNetId, nextRegionId) || - visitedStates.has(`${boundaryPortId}:${nextRegionId}`) - ) { - continue + const nextGuideProgress = getPortProgressAlongPolyline( + this.topology, + boundaryPortId, + effectiveGuidePortIds, + ) + const regressionPenalty = + Math.max(0, node.guideProgress - nextGuideProgress) * + this.TRACE_ALONGSIDE_REGRESSION_WEIGHT + const nextTravelCost = + node.travelCost + + getPortDistance(this.topology, node.portId, boundaryPortId) * + this.DISTANCE_TO_COST + + regressionPenalty + const nextVisitedPortIds = new Set(node.visitedPortIds) + const nextVisitedStateKeys = new Set(node.visitedStateKeys) + nextVisitedPortIds.add(boundaryPortId) + nextVisitedStateKeys.add(nextStateKey) + + moveCandidates.push({ + portId: boundaryPortId, + regionId: nextRegionId, + segments: [ + ...node.segments, + { + regionId: node.regionId, + fromPortId: node.portId, + toPortId: boundaryPortId, + }, + ], + guideProgress: nextGuideProgress, + travelCost: nextTravelCost, + priority: + goalPortId === undefined + ? this.getPartialTraceSearchPriority( + traceIndex, + boundaryPortId, + effectiveGuidePortIds, + nextGuideProgress, + nextTravelCost, + targetGuideProgress ?? 0, + ) + : this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + boundaryPortId, + effectiveGuidePortIds, + nextTravelCost, + ), + visitedPortIds: nextVisitedPortIds, + visitedStateKeys: nextVisitedStateKeys, + }) } - const owner = localOwners.get(boundaryPortId) - if (owner !== undefined && owner !== routeId) { - continue - } + moveCandidates + .sort( + (left, right) => + left.priority - right.priority || + left.portId - right.portId || + left.regionId - right.regionId, + ) + .slice(0, this.TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT) + .forEach((candidate) => { + nextBeamCandidates.push(candidate) + tryCompleteFromNode(candidate) + tryPartialFromNode(candidate) + }) + } - const goalDistance = getPortDistance( - this.topology, - boundaryPortId, - endPortId, - ) - const guideDistance = getDistanceFromPortToPolyline( - this.topology, - boundaryPortId, - guidePortIds, + const bestCandidateByStateKey = new Map() + for (const candidate of nextBeamCandidates) { + const candidateStateKey = this.getTraceSearchStateKey( + candidate.portId, + candidate.regionId, ) - const sidePenalty = this.getTraceSidePenalty(traceIndex, boundaryPortId) - const goalRegionBonus = goalTransitRegionIds.includes(nextRegionId) - ? -5 - : 0 - const score = - guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + - goalDistance * this.BUS_REMAINDER_GOAL_WEIGHT + - sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + - goalRegionBonus + const existingCandidate = + bestCandidateByStateKey.get(candidateStateKey) if ( - !bestMove || - score < bestMove.score - BUS_CANDIDATE_EPSILON || - (Math.abs(score - bestMove.score) <= BUS_CANDIDATE_EPSILON && - boundaryPortId < bestMove.boundaryPortId) + !existingCandidate || + candidate.priority < + existingCandidate.priority - BUS_CANDIDATE_EPSILON || + (Math.abs(candidate.priority - existingCandidate.priority) <= + BUS_CANDIDATE_EPSILON && + candidate.segments.length < existingCandidate.segments.length) ) { - bestMove = { - boundaryPortId, - nextRegionId, - score, - } + bestCandidateByStateKey.set(candidateStateKey, candidate) } } - if (!bestMove) { - return undefined - } + beam = [...bestCandidateByStateKey.values()] + .sort( + (left, right) => + left.priority - right.priority || + left.portId - right.portId || + left.regionId - right.regionId, + ) + .slice(0, this.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH) + } - if (!ensurePortOwnership(routeId, bestMove.boundaryPortId, localOwners)) { - return undefined + return searchOptions + .sort( + (left, right) => + left.searchScore - right.searchScore || + left.segments.length - right.segments.length, + ) + .slice(0, maxOptions) + } + + private getPartialTraceSearchPriority( + traceIndex: number, + portId: PortId, + guidePortIds: readonly PortId[], + guideProgress: number, + travelCost: number, + targetGuideProgress: number, + ) { + const guideDistance = getDistanceFromPortToPolyline( + this.topology, + portId, + guidePortIds, + ) + const lanePenalty = this.getTraceLanePenalty(traceIndex, portId) + const sidePenalty = this.getTraceSidePenalty(traceIndex, portId) + const shortfallPenalty = Math.max(0, targetGuideProgress - guideProgress) + const overshootPenalty = Math.max(0, guideProgress - targetGuideProgress) + + return ( + travelCost + + guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + + lanePenalty * this.TRACE_ALONGSIDE_LANE_WEIGHT + + sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + + shortfallPenalty * 2 + + overshootPenalty * 4 + ) + } + + private getCompleteTraceSearchPriority( + traceIndex: number, + routeId: RouteId, + portId: PortId, + guidePortIds: readonly PortId[], + travelCost: number, + ) { + const guideDistance = getDistanceFromPortToPolyline( + this.topology, + portId, + guidePortIds, + ) + const lanePenalty = this.getTraceLanePenalty(traceIndex, portId) + const sidePenalty = this.getTraceSidePenalty(traceIndex, portId) + const goalHeuristic = + this.problemSetup.portHCostToEndOfRoute[ + portId * this.problem.routeCount + routeId + ] + + return ( + travelCost + + guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + + lanePenalty * this.TRACE_ALONGSIDE_LANE_WEIGHT + + sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + + goalHeuristic * this.BUS_REMAINDER_GOAL_WEIGHT + ) + } + + private getTracePreviewSearchStartStateKeys(tracePreview: TracePreview) { + const visitedStateKeys: string[] = [] + let currentPortId = this.problem.routeStartPort[tracePreview.routeId]! + let currentRegionId = this.getStartingNextRegionId( + tracePreview.routeId, + currentPortId, + ) + + if (currentRegionId === undefined) { + return visitedStateKeys + } + + visitedStateKeys.push( + this.getTraceSearchStateKey(currentPortId, currentRegionId), + ) + + for (const segment of tracePreview.segments) { + currentPortId = segment.toPortId + const nextRegionId = this.getOppositeRegionId(currentPortId, segment.regionId) + + if (nextRegionId === undefined) { + break } - segments.push({ - regionId: currentRegionId, - fromPortId: currentPortId, - toPortId: bestMove.boundaryPortId, - }) - currentPortId = bestMove.boundaryPortId - currentRegionId = bestMove.nextRegionId - visitedStates.add(`${currentPortId}:${currentRegionId}`) + currentRegionId = nextRegionId + visitedStateKeys.push( + this.getTraceSearchStateKey(currentPortId, currentRegionId), + ) } - return undefined + return visitedStateKeys + } + + private getTracePreviewVisitedPortIds(tracePreview: TracePreview) { + const visitedPortIds = new Set([ + this.problem.routeStartPort[tracePreview.routeId]!, + ]) + + for (const segment of tracePreview.segments) { + visitedPortIds.add(segment.fromPortId) + visitedPortIds.add(segment.toPortId) + } + + return [...visitedPortIds] + } + + private getTraceSearchStateKey(portId: PortId, regionId: RegionId) { + return `${portId}:${regionId}` + } + + private getTracePreviewPathKey( + segments: readonly TraceSegment[], + terminalPortId: PortId, + terminalRegionId?: RegionId, + ) { + return [ + ...segments.map( + (segment) => + `${segment.regionId}:${segment.fromPortId}->${segment.toPortId}`, + ), + `end:${terminalPortId}:${terminalRegionId ?? -1}`, + ].join("|") + } + + private getOppositeRegionId(portId: PortId, regionId: RegionId) { + const incidentRegionIds = this.topology.incidentPortRegion[portId] ?? [] + return incidentRegionIds[0] === regionId + ? incidentRegionIds[1] + : incidentRegionIds[0] + } + + private getPartialTraceSearchMaxSteps(remainingBoundaryStepCount: number) { + return Math.max( + 0, + Math.min( + Math.max(1, remainingBoundaryStepCount + 1), + 4, + ), + ) + } + + private getCompleteTraceSearchMaxSteps(remainingBoundaryStepCount: number) { + return Math.max( + 0, + Math.min( + Math.max( + 2, + remainingBoundaryStepCount * 2 + this.BUS_MAX_REMAINDER_STEPS, + ), + this.BUS_MAX_REMAINDER_STEPS * 3, + ), + ) + } + + private maybeStoreBestCompleteFallbackPreview( + preview: BusPreview, + totalIntersectionCount: number, + ) { + if ( + totalIntersectionCount > this.bestCompleteFallbackIntersectionCount || + (totalIntersectionCount === this.bestCompleteFallbackIntersectionCount && + preview.totalCost >= this.bestCompleteFallbackCost) + ) { + return + } + + this.bestCompleteFallbackPreview = { + ...preview, + reason: undefined, + tracePreviews: preview.tracePreviews.map((tracePreview) => ({ + ...tracePreview, + segments: tracePreview.segments.map((segment) => ({ ...segment })), + })), + } + this.bestCompleteFallbackSnapshot = snapshotPreviewRoutingStateValue( + this.state, + ) + this.bestCompleteFallbackIntersectionCount = totalIntersectionCount + this.bestCompleteFallbackCost = preview.totalCost + } + + private tryAcceptBestCompleteFallbackPreview() { + if ( + !this.bestCompleteFallbackPreview || + !this.bestCompleteFallbackSnapshot + ) { + return false + } + + restorePreviewRoutingStateValue( + this.state, + this.bestCompleteFallbackSnapshot, + ) + this.lastPreview = this.bestCompleteFallbackPreview + this.solved = true + this.failed = false + this.error = null + this.state.unroutedRoutes = [] + this.updateBusStats() + return true } private isUsableCenterlineBoundaryPort(portId: PortId) { @@ -1488,6 +2180,18 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { return Math.abs(offset) } + private getTraceLanePenalty(traceIndex: number, portId: PortId) { + const trace = this.busTraceOrder.traces[traceIndex]! + const projection = getPortProjection( + this.topology, + portId, + this.busTraceOrder.normalX, + this.busTraceOrder.normalY, + ) + + return Math.abs(projection - trace.score) + } + private computeCenterHeuristic(portId: PortId, nextRegionId?: RegionId) { const portHeuristic = this.problemSetup.portHCostToEndOfRoute[ diff --git a/tests/solver/bus-region-span-repro.test.ts b/tests/solver/bus-region-span-repro.test.ts index 5690869..aedea58 100644 --- a/tests/solver/bus-region-span-repro.test.ts +++ b/tests/solver/bus-region-span-repro.test.ts @@ -74,11 +74,14 @@ test("repro: six-trace slit middle requires spanning both middle regions", () => expect(routesUsingMidRight.length).toBeGreaterThan(0) }) -test("repro: current bus solver fails on the split-middle span fixture", () => { +test("repro: bus solver spans both middle regions on the split-middle fixture", () => { const { topology, problem } = loadSerializedHyperGraph(busRegionSpanFixture) const busSolver = new TinyHyperGraphBusSolver(topology, problem, { MAX_ITERATIONS: 50_000, }) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 50_000, + }) const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) const midLeftRegionIndex = regionIndexBySerializedId.get("mid-left") @@ -94,8 +97,50 @@ test("repro: current bus solver fails on the split-middle span fixture", () => { ).toBeGreaterThan(2) busSolver.solve() + plainSolver.solve() + + expect(busSolver.solved).toBe(true) + expect(busSolver.failed).toBe(false) - expect(busSolver.solved).toBe(false) - expect(busSolver.failed).toBe(true) - expect(busSolver.error).toBeTruthy() + const solvedRoutes = busSolver.getOutput().solvedRoutes ?? [] + const routesUsingMidLeft = solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === "mid-left"), + ) + const routesUsingMidRight = solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === "mid-right"), + ) + const sameLayerIntersectionCount = + busSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingSameLayerIntersections, + 0, + ) + const crossingLayerIntersectionCount = + busSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingCrossingLayerIntersections, + 0, + ) + const plainSameLayerIntersectionCount = + plainSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingSameLayerIntersections, + 0, + ) + const plainCrossingLayerIntersectionCount = + plainSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingCrossingLayerIntersections, + 0, + ) + + expect(solvedRoutes).toHaveLength(BUS_REGION_SPAN_ROUTE_COUNT) + expect(routesUsingMidLeft.length).toBeGreaterThan(0) + expect(routesUsingMidRight.length).toBeGreaterThan(0) + expect(sameLayerIntersectionCount).toBeLessThanOrEqual( + plainSameLayerIntersectionCount, + ) + expect(crossingLayerIntersectionCount).toBeLessThanOrEqual( + plainCrossingLayerIntersectionCount, + ) }) From c67aaac15a56fccdf15e654de633d8743092880f Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 14 Apr 2026 20:52:23 -0700 Subject: [PATCH 2/5] wip --- lib/bus-solver/BusTraceInferencePlanner.ts | 964 +++++++++++++++++++++ lib/bus-solver/TinyHyperGraphBusSolver.ts | 926 +------------------- 2 files changed, 1012 insertions(+), 878 deletions(-) create mode 100644 lib/bus-solver/BusTraceInferencePlanner.ts diff --git a/lib/bus-solver/BusTraceInferencePlanner.ts b/lib/bus-solver/BusTraceInferencePlanner.ts new file mode 100644 index 0000000..a32eaae --- /dev/null +++ b/lib/bus-solver/BusTraceInferencePlanner.ts @@ -0,0 +1,964 @@ +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "../core" +import type { PortId, RegionId, RouteId } from "../types" +import { + ensurePortOwnership, + getGuidePortIds, + getPolylineLength, + getTracePreviewLength, + isPortIncidentToRegion, +} from "./busPathHelpers" +import type { BusTraceOrder } from "./deriveBusTraceOrder" +import { + getDistanceFromPortToPolyline, + getPortDistance, + getPortProgressAlongPolyline, +} from "./geometry" +import { + BUS_CANDIDATE_EPSILON, + type BoundaryStep, + type BusCenterCandidate, + type TracePreview, + type TraceSegment, +} from "./busSolverTypes" + +interface AlongsideTraceSearchNode { + portId: PortId + regionId: RegionId + segments: TraceSegment[] + guideProgress: number + travelCost: number + priority: number + visitedPortIds: Set + visitedStateKeys: Set +} + +interface AlongsideTraceSearchOption { + segments: TraceSegment[] + terminalPortId: PortId + terminalRegionId: RegionId + searchScore: number +} + +interface BuildPrefixTracePreviewFn { + ( + traceIndex: number, + sharedStepCount: number, + boundarySteps: BoundaryStep[], + boundaryPortIdsByStep: Array, + usedPortOwners: ReadonlyMap, + ): TracePreview | undefined +} + +interface BusTraceInferencePlannerOptions { + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + busTraceOrder: BusTraceOrder + centerTraceIndex: number + tracePitch: number + DISTANCE_TO_COST: number + BUS_MAX_REMAINDER_STEPS: number + BUS_REMAINDER_GUIDE_WEIGHT: number + BUS_REMAINDER_GOAL_WEIGHT: number + BUS_REMAINDER_SIDE_WEIGHT: number + TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT: number + TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH: number + TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT: number + TRACE_ALONGSIDE_LANE_WEIGHT: number + TRACE_ALONGSIDE_REGRESSION_WEIGHT: number + buildPrefixTracePreview: BuildPrefixTracePreviewFn + getStartingNextRegionId: (routeId: RouteId, startPortId: PortId) => RegionId | undefined + isRegionReservedForDifferentBusNet: ( + currentNetId: number, + regionId: RegionId, + ) => boolean + getTraceSidePenalty: (traceIndex: number, portId: PortId) => number + getTraceLanePenalty: (traceIndex: number, portId: PortId) => number + getRouteHeuristic: (routeId: RouteId, portId: PortId) => number +} + +export class BusTraceInferencePlanner { + constructor(private readonly options: BusTraceInferencePlannerOptions) {} + + buildBestPrefixTracePreview( + traceIndex: number, + centerPath: BusCenterCandidate[], + maxSharedStepCount: number, + boundarySteps: BoundaryStep[], + boundaryPortIdsByStep: Array, + usedPortOwners: ReadonlyMap, + ) { + const centerPortIds = centerPath.map( + (pathCandidate) => pathCandidate.portId, + ) + const targetGuideProgress = getPolylineLength( + this.options.topology, + centerPortIds, + ) + const minSharedStepCount = 0 + let bestExactPreview: TracePreview | undefined + let bestScore = Number.POSITIVE_INFINITY + let bestSharedStepCount = -1 + + for ( + let sharedStepCount = maxSharedStepCount; + sharedStepCount >= minSharedStepCount; + sharedStepCount-- + ) { + const prefixPreview = this.options.buildPrefixTracePreview( + traceIndex, + sharedStepCount, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) + if (!prefixPreview) { + continue + } + + const terminalGuideProgress = getPortProgressAlongPolyline( + this.options.topology, + prefixPreview.terminalPortId, + centerPortIds, + ) + const shortfallPenalty = Math.max( + 0, + targetGuideProgress - terminalGuideProgress, + ) + const overshootPenalty = Math.max( + 0, + terminalGuideProgress - targetGuideProgress, + ) + const lagPenalty = + (maxSharedStepCount - sharedStepCount) * this.options.tracePitch + const score = shortfallPenalty * 2 + overshootPenalty * 4 + lagPenalty + + if ( + !bestExactPreview || + score < bestScore - BUS_CANDIDATE_EPSILON || + (Math.abs(score - bestScore) <= BUS_CANDIDATE_EPSILON && + sharedStepCount > bestSharedStepCount) + ) { + bestExactPreview = { + ...prefixPreview, + previewCost: score, + } + bestScore = score + bestSharedStepCount = sharedStepCount + } + } + + if (!bestExactPreview) { + return undefined + } + + if (bestSharedStepCount > 0) { + return bestExactPreview + } + + const searchPrefixPreview = this.options.buildPrefixTracePreview( + traceIndex, + bestSharedStepCount, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) + if ( + !searchPrefixPreview || + searchPrefixPreview.terminalRegionId === undefined + ) { + return bestExactPreview + } + + const guidePortIds = getGuidePortIds(centerPath, bestSharedStepCount) + const alongsideOptions = this.searchTraceAlongsideOptions({ + traceIndex, + startPortId: searchPrefixPreview.terminalPortId, + startRegionId: searchPrefixPreview.terminalRegionId, + guidePortIds, + usedPortOwners, + targetGuideProgress: getPolylineLength(this.options.topology, guidePortIds), + maxSteps: this.getPartialTraceSearchMaxSteps(maxSharedStepCount), + maxOptions: 1, + initialVisitedPortIds: this.getTracePreviewVisitedPortIds( + searchPrefixPreview, + ), + initialVisitedStateKeys: + this.getTracePreviewSearchStartStateKeys(searchPrefixPreview), + }) + const bestAlongsideOption = alongsideOptions[0] + + if (!bestAlongsideOption) { + return bestExactPreview + } + + const searchScore = + bestAlongsideOption.searchScore + + maxSharedStepCount * this.options.tracePitch + if ( + (bestExactPreview.previewCost ?? Number.POSITIVE_INFINITY) <= searchScore + ) { + return bestExactPreview + } + + return { + ...searchPrefixPreview, + segments: [ + ...searchPrefixPreview.segments, + ...bestAlongsideOption.segments, + ], + terminalPortId: bestAlongsideOption.terminalPortId, + terminalRegionId: bestAlongsideOption.terminalRegionId, + previewCost: searchScore, + } + } + + buildCompleteTracePreviewOptions( + traceIndex: number, + centerPath: BusCenterCandidate[], + boundarySteps: BoundaryStep[], + boundaryPortIdsByStep: Array, + usedPortOwners: ReadonlyMap, + ) { + const routeId = this.options.busTraceOrder.traces[traceIndex]!.routeId + const previewOptions: TracePreview[] = [] + const previewOptionKeys = new Set() + + for ( + let sharedStepCount = boundarySteps.length; + sharedStepCount >= 0; + sharedStepCount-- + ) { + const prefixPreview = this.options.buildPrefixTracePreview( + traceIndex, + sharedStepCount, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) + if (!prefixPreview) { + continue + } + + const currentRegionId = + sharedStepCount === 0 + ? this.options.getStartingNextRegionId( + routeId, + this.options.problem.routeStartPort[routeId]!, + ) + : boundarySteps[sharedStepCount - 1]!.toRegionId + const currentPortId = + sharedStepCount === 0 + ? this.options.problem.routeStartPort[routeId]! + : boundaryPortIdsByStep[sharedStepCount - 1]?.[traceIndex] + + if (currentRegionId === undefined || currentPortId === undefined) { + continue + } + + const greedyRemainderSegments = this.inferEndRemainderSegmentsGreedy( + traceIndex, + currentPortId, + currentRegionId, + centerPath, + sharedStepCount, + usedPortOwners, + ) + + if (greedyRemainderSegments) { + const greedySegments = [ + ...prefixPreview.segments, + ...greedyRemainderSegments, + ] + const greedyPreviewKey = this.getTracePreviewPathKey( + greedySegments, + this.options.problem.routeEndPort[routeId]!, + undefined, + ) + + if (!previewOptionKeys.has(greedyPreviewKey)) { + previewOptionKeys.add(greedyPreviewKey) + previewOptions.push({ + traceIndex, + routeId, + segments: greedySegments, + complete: true, + terminalPortId: this.options.problem.routeEndPort[routeId]!, + previewCost: 0, + }) + } + } + + if (sharedStepCount === 0 || !greedyRemainderSegments) { + const remainingBoundaryStepCount = Math.max( + boundarySteps.length - sharedStepCount, + 0, + ) + const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) + const completionOptions = this.searchTraceAlongsideOptions({ + traceIndex, + startPortId: prefixPreview.terminalPortId, + startRegionId: prefixPreview.terminalRegionId!, + guidePortIds, + usedPortOwners, + goalPortId: this.options.problem.routeEndPort[routeId]!, + maxSteps: this.getCompleteTraceSearchMaxSteps( + remainingBoundaryStepCount, + ), + maxOptions: this.options.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, + initialVisitedPortIds: this.getTracePreviewVisitedPortIds( + prefixPreview, + ), + initialVisitedStateKeys: + this.getTracePreviewSearchStartStateKeys(prefixPreview), + }) + + for (const completionOption of completionOptions) { + const combinedSegments = [ + ...prefixPreview.segments, + ...completionOption.segments, + ] + const previewKey = this.getTracePreviewPathKey( + combinedSegments, + this.options.problem.routeEndPort[routeId]!, + undefined, + ) + + if (previewOptionKeys.has(previewKey)) { + continue + } + + previewOptionKeys.add(previewKey) + previewOptions.push({ + traceIndex, + routeId, + segments: combinedSegments, + complete: true, + terminalPortId: this.options.problem.routeEndPort[routeId]!, + previewCost: completionOption.searchScore, + }) + } + } + } + + return previewOptions + .sort( + (left, right) => + (left.previewCost ?? 0) - (right.previewCost ?? 0) || + getTracePreviewLength(this.options.topology, left) - + getTracePreviewLength(this.options.topology, right), + ) + .slice(0, this.options.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT) + } + + private inferEndRemainderSegmentsGreedy( + traceIndex: number, + startPortId: PortId, + startRegionId: RegionId, + centerPath: BusCenterCandidate[], + sharedStepCount: number, + usedPortOwners: ReadonlyMap, + ): TraceSegment[] | undefined { + const routeId = this.options.busTraceOrder.traces[traceIndex]!.routeId + const endPortId = this.options.problem.routeEndPort[routeId]! + + if (this.options.topology.portZ[endPortId] !== 0) { + return undefined + } + + if (isPortIncidentToRegion(this.options.topology, endPortId, startRegionId)) { + if ( + ensurePortOwnership(routeId, endPortId, new Map(usedPortOwners)) && + startPortId !== endPortId + ) { + return [ + { + regionId: startRegionId, + fromPortId: startPortId, + toPortId: endPortId, + }, + ] + } + + return [] + } + + const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) + const goalTransitRegionIds = + this.options.topology.incidentPortRegion[endPortId]?.filter( + (regionId) => regionId !== undefined, + ) ?? [] + const currentNetId = this.options.problem.routeNet[routeId]! + const localOwners = new Map(usedPortOwners) + if (!ensurePortOwnership(routeId, startPortId, localOwners)) { + return undefined + } + + const visitedStates = new Set([`${startPortId}:${startRegionId}`]) + const segments: TraceSegment[] = [] + let currentPortId = startPortId + let currentRegionId = startRegionId + + for ( + let stepIndex = 0; + stepIndex < this.options.BUS_MAX_REMAINDER_STEPS; + stepIndex++ + ) { + if (isPortIncidentToRegion(this.options.topology, endPortId, currentRegionId)) { + if (!ensurePortOwnership(routeId, endPortId, localOwners)) { + return undefined + } + + if (currentPortId !== endPortId) { + segments.push({ + regionId: currentRegionId, + fromPortId: currentPortId, + toPortId: endPortId, + }) + } + + return segments + } + + let bestMove: + | { + boundaryPortId: PortId + nextRegionId: RegionId + score: number + } + | undefined + + for (const boundaryPortId of this.options.topology.regionIncidentPorts[ + currentRegionId + ] ?? []) { + if ( + boundaryPortId === currentPortId || + this.options.topology.portZ[boundaryPortId] !== 0 + ) { + continue + } + + const nextRegionId = + this.options.topology.incidentPortRegion[boundaryPortId]?.[0] === + currentRegionId + ? this.options.topology.incidentPortRegion[boundaryPortId]?.[1] + : this.options.topology.incidentPortRegion[boundaryPortId]?.[0] + + if ( + nextRegionId === undefined || + this.options.isRegionReservedForDifferentBusNet( + currentNetId, + nextRegionId, + ) || + visitedStates.has(`${boundaryPortId}:${nextRegionId}`) + ) { + continue + } + + const owner = localOwners.get(boundaryPortId) + if (owner !== undefined && owner !== routeId) { + continue + } + + const goalDistance = getPortDistance( + this.options.topology, + boundaryPortId, + endPortId, + ) + const guideDistance = getDistanceFromPortToPolyline( + this.options.topology, + boundaryPortId, + guidePortIds, + ) + const sidePenalty = this.options.getTraceSidePenalty( + traceIndex, + boundaryPortId, + ) + const goalRegionBonus = goalTransitRegionIds.includes(nextRegionId) + ? -5 + : 0 + const score = + guideDistance * this.options.BUS_REMAINDER_GUIDE_WEIGHT + + goalDistance * this.options.BUS_REMAINDER_GOAL_WEIGHT + + sidePenalty * this.options.BUS_REMAINDER_SIDE_WEIGHT + + goalRegionBonus + + if ( + !bestMove || + score < bestMove.score - BUS_CANDIDATE_EPSILON || + (Math.abs(score - bestMove.score) <= BUS_CANDIDATE_EPSILON && + boundaryPortId < bestMove.boundaryPortId) + ) { + bestMove = { + boundaryPortId, + nextRegionId, + score, + } + } + } + + if (!bestMove) { + return undefined + } + + if (!ensurePortOwnership(routeId, bestMove.boundaryPortId, localOwners)) { + return undefined + } + + segments.push({ + regionId: currentRegionId, + fromPortId: currentPortId, + toPortId: bestMove.boundaryPortId, + }) + currentPortId = bestMove.boundaryPortId + currentRegionId = bestMove.nextRegionId + visitedStates.add(`${currentPortId}:${currentRegionId}`) + } + + return undefined + } + + private searchTraceAlongsideOptions({ + traceIndex, + startPortId, + startRegionId, + guidePortIds, + usedPortOwners, + maxSteps, + maxOptions, + targetGuideProgress, + goalPortId, + initialVisitedPortIds = [], + initialVisitedStateKeys = [], + }: { + traceIndex: number + startPortId: PortId + startRegionId: RegionId + guidePortIds: readonly PortId[] + usedPortOwners: ReadonlyMap + maxSteps: number + maxOptions: number + targetGuideProgress?: number + goalPortId?: PortId + initialVisitedPortIds?: readonly PortId[] + initialVisitedStateKeys?: readonly string[] + }): AlongsideTraceSearchOption[] { + const routeId = this.options.busTraceOrder.traces[traceIndex]!.routeId + const effectiveGuidePortIds = + guidePortIds.length > 0 ? guidePortIds : [startPortId] + const initialGuideProgress = getPortProgressAlongPolyline( + this.options.topology, + startPortId, + effectiveGuidePortIds, + ) + const visitedPortIds = new Set(initialVisitedPortIds) + const visitedStateKeys = new Set(initialVisitedStateKeys) + visitedPortIds.add(startPortId) + visitedStateKeys.add( + this.getTraceSearchStateKey(startPortId, startRegionId), + ) + + const initialNode: AlongsideTraceSearchNode = { + portId: startPortId, + regionId: startRegionId, + segments: [], + guideProgress: initialGuideProgress, + travelCost: 0, + priority: + goalPortId === undefined + ? this.getPartialTraceSearchPriority( + traceIndex, + startPortId, + effectiveGuidePortIds, + initialGuideProgress, + 0, + targetGuideProgress ?? 0, + ) + : this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + startPortId, + effectiveGuidePortIds, + 0, + ), + visitedPortIds, + visitedStateKeys, + } + + const searchOptions: AlongsideTraceSearchOption[] = [] + const searchOptionKeys = new Set() + const pushOption = (option: AlongsideTraceSearchOption) => { + const optionKey = this.getTracePreviewPathKey( + option.segments, + option.terminalPortId, + option.terminalRegionId, + ) + + if (searchOptionKeys.has(optionKey)) { + return + } + + searchOptionKeys.add(optionKey) + searchOptions.push(option) + } + const tryCompleteFromNode = (node: AlongsideTraceSearchNode) => { + if ( + goalPortId === undefined || + !isPortIncidentToRegion(this.options.topology, goalPortId, node.regionId) + ) { + return + } + + const owner = usedPortOwners.get(goalPortId) + if (owner !== undefined && owner !== routeId) { + return + } + + const completionSegments = + node.portId === goalPortId + ? node.segments + : [ + ...node.segments, + { + regionId: node.regionId, + fromPortId: node.portId, + toPortId: goalPortId, + }, + ] + const completionTravelCost = + node.travelCost + + (node.portId === goalPortId + ? 0 + : getPortDistance(this.options.topology, node.portId, goalPortId) * + this.options.DISTANCE_TO_COST) + + pushOption({ + segments: completionSegments, + terminalPortId: goalPortId, + terminalRegionId: node.regionId, + searchScore: this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + goalPortId, + effectiveGuidePortIds, + completionTravelCost, + ), + }) + } + const tryPartialFromNode = (node: AlongsideTraceSearchNode) => { + if (targetGuideProgress === undefined) { + return + } + + pushOption({ + segments: node.segments, + terminalPortId: node.portId, + terminalRegionId: node.regionId, + searchScore: this.getPartialTraceSearchPriority( + traceIndex, + node.portId, + effectiveGuidePortIds, + node.guideProgress, + node.travelCost, + targetGuideProgress, + ), + }) + } + + let beam = [initialNode] + tryCompleteFromNode(initialNode) + tryPartialFromNode(initialNode) + + for (let stepIndex = 0; stepIndex < maxSteps && beam.length > 0; stepIndex++) { + const nextBeamCandidates: AlongsideTraceSearchNode[] = [] + + for (const node of beam) { + const moveCandidates: AlongsideTraceSearchNode[] = [] + + for (const boundaryPortId of this.options.topology.regionIncidentPorts[ + node.regionId + ] ?? []) { + if ( + boundaryPortId === node.portId || + this.options.topology.portZ[boundaryPortId] !== 0 || + node.visitedPortIds.has(boundaryPortId) + ) { + continue + } + + const nextRegionId = this.getOppositeRegionId( + boundaryPortId, + node.regionId, + ) + + if ( + nextRegionId === undefined || + this.options.isRegionReservedForDifferentBusNet( + this.options.problem.routeNet[routeId]!, + nextRegionId, + ) + ) { + continue + } + + const owner = usedPortOwners.get(boundaryPortId) + if (owner !== undefined && owner !== routeId) { + continue + } + + const nextStateKey = this.getTraceSearchStateKey( + boundaryPortId, + nextRegionId, + ) + if (node.visitedStateKeys.has(nextStateKey)) { + continue + } + + const nextGuideProgress = getPortProgressAlongPolyline( + this.options.topology, + boundaryPortId, + effectiveGuidePortIds, + ) + const regressionPenalty = + Math.max(0, node.guideProgress - nextGuideProgress) * + this.options.TRACE_ALONGSIDE_REGRESSION_WEIGHT + const nextTravelCost = + node.travelCost + + getPortDistance(this.options.topology, node.portId, boundaryPortId) * + this.options.DISTANCE_TO_COST + + regressionPenalty + const nextVisitedPortIds = new Set(node.visitedPortIds) + const nextVisitedStateKeys = new Set(node.visitedStateKeys) + nextVisitedPortIds.add(boundaryPortId) + nextVisitedStateKeys.add(nextStateKey) + + moveCandidates.push({ + portId: boundaryPortId, + regionId: nextRegionId, + segments: [ + ...node.segments, + { + regionId: node.regionId, + fromPortId: node.portId, + toPortId: boundaryPortId, + }, + ], + guideProgress: nextGuideProgress, + travelCost: nextTravelCost, + priority: + goalPortId === undefined + ? this.getPartialTraceSearchPriority( + traceIndex, + boundaryPortId, + effectiveGuidePortIds, + nextGuideProgress, + nextTravelCost, + targetGuideProgress ?? 0, + ) + : this.getCompleteTraceSearchPriority( + traceIndex, + routeId, + boundaryPortId, + effectiveGuidePortIds, + nextTravelCost, + ), + visitedPortIds: nextVisitedPortIds, + visitedStateKeys: nextVisitedStateKeys, + }) + } + + moveCandidates + .sort( + (left, right) => + left.priority - right.priority || + left.portId - right.portId || + left.regionId - right.regionId, + ) + .slice(0, this.options.TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT) + .forEach((candidate) => { + nextBeamCandidates.push(candidate) + tryCompleteFromNode(candidate) + tryPartialFromNode(candidate) + }) + } + + const bestCandidateByStateKey = new Map() + for (const candidate of nextBeamCandidates) { + const candidateStateKey = this.getTraceSearchStateKey( + candidate.portId, + candidate.regionId, + ) + const existingCandidate = + bestCandidateByStateKey.get(candidateStateKey) + + if ( + !existingCandidate || + candidate.priority < + existingCandidate.priority - BUS_CANDIDATE_EPSILON || + (Math.abs(candidate.priority - existingCandidate.priority) <= + BUS_CANDIDATE_EPSILON && + candidate.segments.length < existingCandidate.segments.length) + ) { + bestCandidateByStateKey.set(candidateStateKey, candidate) + } + } + + beam = [...bestCandidateByStateKey.values()] + .sort( + (left, right) => + left.priority - right.priority || + left.portId - right.portId || + left.regionId - right.regionId, + ) + .slice(0, this.options.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH) + } + + return searchOptions + .sort( + (left, right) => + left.searchScore - right.searchScore || + left.segments.length - right.segments.length, + ) + .slice(0, maxOptions) + } + + private getPartialTraceSearchPriority( + traceIndex: number, + portId: PortId, + guidePortIds: readonly PortId[], + guideProgress: number, + travelCost: number, + targetGuideProgress: number, + ) { + const guideDistance = getDistanceFromPortToPolyline( + this.options.topology, + portId, + guidePortIds, + ) + const lanePenalty = this.options.getTraceLanePenalty(traceIndex, portId) + const sidePenalty = this.options.getTraceSidePenalty(traceIndex, portId) + const shortfallPenalty = Math.max(0, targetGuideProgress - guideProgress) + const overshootPenalty = Math.max(0, guideProgress - targetGuideProgress) + + return ( + travelCost + + guideDistance * this.options.BUS_REMAINDER_GUIDE_WEIGHT + + lanePenalty * this.options.TRACE_ALONGSIDE_LANE_WEIGHT + + sidePenalty * this.options.BUS_REMAINDER_SIDE_WEIGHT + + shortfallPenalty * 2 + + overshootPenalty * 4 + ) + } + + private getCompleteTraceSearchPriority( + traceIndex: number, + routeId: RouteId, + portId: PortId, + guidePortIds: readonly PortId[], + travelCost: number, + ) { + const guideDistance = getDistanceFromPortToPolyline( + this.options.topology, + portId, + guidePortIds, + ) + const lanePenalty = this.options.getTraceLanePenalty(traceIndex, portId) + const sidePenalty = this.options.getTraceSidePenalty(traceIndex, portId) + const goalHeuristic = this.options.getRouteHeuristic(routeId, portId) + + return ( + travelCost + + guideDistance * this.options.BUS_REMAINDER_GUIDE_WEIGHT + + lanePenalty * this.options.TRACE_ALONGSIDE_LANE_WEIGHT + + sidePenalty * this.options.BUS_REMAINDER_SIDE_WEIGHT + + goalHeuristic * this.options.BUS_REMAINDER_GOAL_WEIGHT + ) + } + + private getTracePreviewSearchStartStateKeys(tracePreview: TracePreview) { + const visitedStateKeys: string[] = [] + let currentPortId = this.options.problem.routeStartPort[tracePreview.routeId]! + let currentRegionId = this.options.getStartingNextRegionId( + tracePreview.routeId, + currentPortId, + ) + + if (currentRegionId === undefined) { + return visitedStateKeys + } + + visitedStateKeys.push( + this.getTraceSearchStateKey(currentPortId, currentRegionId), + ) + + for (const segment of tracePreview.segments) { + currentPortId = segment.toPortId + const nextRegionId = this.getOppositeRegionId(currentPortId, segment.regionId) + + if (nextRegionId === undefined) { + break + } + + currentRegionId = nextRegionId + visitedStateKeys.push( + this.getTraceSearchStateKey(currentPortId, currentRegionId), + ) + } + + return visitedStateKeys + } + + private getTracePreviewVisitedPortIds(tracePreview: TracePreview) { + const visitedPortIds = new Set([ + this.options.problem.routeStartPort[tracePreview.routeId]!, + ]) + + for (const segment of tracePreview.segments) { + visitedPortIds.add(segment.fromPortId) + visitedPortIds.add(segment.toPortId) + } + + return [...visitedPortIds] + } + + private getTraceSearchStateKey(portId: PortId, regionId: RegionId) { + return `${portId}:${regionId}` + } + + private getTracePreviewPathKey( + segments: readonly TraceSegment[], + terminalPortId: PortId, + terminalRegionId?: RegionId, + ) { + return [ + ...segments.map( + (segment) => + `${segment.regionId}:${segment.fromPortId}->${segment.toPortId}`, + ), + `end:${terminalPortId}:${terminalRegionId ?? -1}`, + ].join("|") + } + + private getOppositeRegionId(portId: PortId, regionId: RegionId) { + const incidentRegionIds = this.options.topology.incidentPortRegion[portId] ?? [] + return incidentRegionIds[0] === regionId + ? incidentRegionIds[1] + : incidentRegionIds[0] + } + + private getPartialTraceSearchMaxSteps(remainingBoundaryStepCount: number) { + return Math.max(0, Math.min(Math.max(1, remainingBoundaryStepCount + 1), 4)) + } + + private getCompleteTraceSearchMaxSteps(remainingBoundaryStepCount: number) { + return Math.max( + 0, + Math.min( + Math.max( + 2, + remainingBoundaryStepCount * 2 + this.options.BUS_MAX_REMAINDER_STEPS, + ), + this.options.BUS_MAX_REMAINDER_STEPS * 3, + ), + ) + } +} diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 98adac7..8f1cecc 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -7,6 +7,7 @@ import { } from "../core" import type { NetId, PortId, RegionId, RouteId } from "../types" import { visualizeTinyGraph } from "../visualizeTinyGraph" +import { BusTraceInferencePlanner } from "./BusTraceInferencePlanner" import { deriveBusTraceOrder, type BusTraceOrder } from "./deriveBusTraceOrder" import { BusBoundaryPlanner } from "./BusBoundaryPlanner" import { @@ -20,8 +21,6 @@ import { getCandidateBoundaryNormal, getCenterCandidatePath, getCenterCandidatePathKey, - getGuidePortIds, - getPolylineLength, getTracePreviewLength, isPortIncidentToRegion, } from "./busPathHelpers" @@ -38,9 +37,7 @@ import { type TraceSegment, } from "./busSolverTypes" import { - getDistanceFromPortToPolyline, getPortDistance, - getPortProgressAlongPolyline, getPortProjection, } from "./geometry" import { @@ -51,24 +48,6 @@ import { snapshotPreviewRoutingState as snapshotPreviewRoutingStateValue, } from "./previewRoutingState" -interface AlongsideTraceSearchNode { - portId: PortId - regionId: RegionId - segments: TraceSegment[] - guideProgress: number - travelCost: number - priority: number - visitedPortIds: Set - visitedStateKeys: Set -} - -interface AlongsideTraceSearchOption { - segments: TraceSegment[] - terminalPortId: PortId - terminalRegionId: RegionId - searchScore: number -} - export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { BUS_END_MARGIN_STEPS = 3 BUS_MAX_REMAINDER_STEPS = 8 @@ -102,6 +81,7 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { readonly regionDistanceToGoalByRegion: Float64Array private readonly boundaryPlanner: BusBoundaryPlanner + private readonly traceInferencePlanner: BusTraceInferencePlanner private readonly centerlineNeighborRegionIdsByRegion: RegionId[][] private readonly regionIndexBySerializedId = new Map() private lastExpandedCandidate?: BusCenterCandidate @@ -162,6 +142,35 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { this.BUS_REMAINDER_SIDE_WEIGHT = options.BUS_REMAINDER_SIDE_WEIGHT } + this.traceInferencePlanner = new BusTraceInferencePlanner({ + topology: this.topology, + problem: this.problem, + busTraceOrder: this.busTraceOrder, + centerTraceIndex: this.centerTraceIndex, + tracePitch: this.tracePitch, + DISTANCE_TO_COST: this.DISTANCE_TO_COST, + BUS_MAX_REMAINDER_STEPS: this.BUS_MAX_REMAINDER_STEPS, + BUS_REMAINDER_GUIDE_WEIGHT: this.BUS_REMAINDER_GUIDE_WEIGHT, + BUS_REMAINDER_GOAL_WEIGHT: this.BUS_REMAINDER_GOAL_WEIGHT, + BUS_REMAINDER_SIDE_WEIGHT: this.BUS_REMAINDER_SIDE_WEIGHT, + TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT: + this.TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT, + TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH: + this.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH, + TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT: + this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, + TRACE_ALONGSIDE_LANE_WEIGHT: this.TRACE_ALONGSIDE_LANE_WEIGHT, + TRACE_ALONGSIDE_REGRESSION_WEIGHT: + this.TRACE_ALONGSIDE_REGRESSION_WEIGHT, + buildPrefixTracePreview: (...args) => this.buildPrefixTracePreview(...args), + getStartingNextRegionId: (...args) => this.getStartingNextRegionId(...args), + isRegionReservedForDifferentBusNet: (...args) => + this.isRegionReservedForDifferentBusNet(...args), + getTraceSidePenalty: (...args) => this.getTraceSidePenalty(...args), + getTraceLanePenalty: (...args) => this.getTraceLanePenalty(...args), + getRouteHeuristic: (...args) => this.getRouteHeuristic(...args), + }) + this.boundaryPlanner = new BusBoundaryPlanner({ topology: this.topology, problem: this.problem, @@ -641,120 +650,14 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { boundaryPortIdsByStep: Array, usedPortOwners: ReadonlyMap, ) { - const centerPortIds = centerPath.map( - (pathCandidate) => pathCandidate.portId, - ) - const targetGuideProgress = getPolylineLength(this.topology, centerPortIds) - const minSharedStepCount = 0 - let bestExactPreview: TracePreview | undefined - let bestScore = Number.POSITIVE_INFINITY - let bestSharedStepCount = -1 - - for ( - let sharedStepCount = maxSharedStepCount; - sharedStepCount >= minSharedStepCount; - sharedStepCount-- - ) { - const prefixPreview = this.buildPrefixTracePreview( - traceIndex, - sharedStepCount, - boundarySteps, - boundaryPortIdsByStep, - usedPortOwners, - ) - if (!prefixPreview) { - continue - } - - const terminalGuideProgress = getPortProgressAlongPolyline( - this.topology, - prefixPreview.terminalPortId, - centerPortIds, - ) - const shortfallPenalty = Math.max( - 0, - targetGuideProgress - terminalGuideProgress, - ) - const overshootPenalty = Math.max( - 0, - terminalGuideProgress - targetGuideProgress, - ) - const lagPenalty = - (maxSharedStepCount - sharedStepCount) * this.tracePitch - const score = shortfallPenalty * 2 + overshootPenalty * 4 + lagPenalty - - if ( - !bestExactPreview || - score < bestScore - BUS_CANDIDATE_EPSILON || - (Math.abs(score - bestScore) <= BUS_CANDIDATE_EPSILON && - sharedStepCount > bestSharedStepCount) - ) { - bestExactPreview = { - ...prefixPreview, - previewCost: score, - } - bestScore = score - bestSharedStepCount = sharedStepCount - } - } - - if (!bestExactPreview) { - return undefined - } - - if (bestSharedStepCount > 0) { - return bestExactPreview - } - - const searchPrefixPreview = this.buildPrefixTracePreview( + return this.traceInferencePlanner.buildBestPrefixTracePreview( traceIndex, - bestSharedStepCount, + centerPath, + maxSharedStepCount, boundarySteps, boundaryPortIdsByStep, usedPortOwners, ) - if (!searchPrefixPreview || searchPrefixPreview.terminalRegionId === undefined) { - return bestExactPreview - } - - const guidePortIds = getGuidePortIds(centerPath, bestSharedStepCount) - const alongsideOptions = this.searchTraceAlongsideOptions({ - traceIndex, - startPortId: searchPrefixPreview.terminalPortId, - startRegionId: searchPrefixPreview.terminalRegionId, - guidePortIds, - usedPortOwners, - targetGuideProgress: getPolylineLength(this.topology, guidePortIds), - maxSteps: this.getPartialTraceSearchMaxSteps(maxSharedStepCount), - maxOptions: 1, - initialVisitedPortIds: this.getTracePreviewVisitedPortIds( - searchPrefixPreview, - ), - initialVisitedStateKeys: - this.getTracePreviewSearchStartStateKeys(searchPrefixPreview), - }) - const bestAlongsideOption = alongsideOptions[0] - - if (!bestAlongsideOption) { - return bestExactPreview - } - - const searchScore = - bestAlongsideOption.searchScore + maxSharedStepCount * this.tracePitch - if ((bestExactPreview.previewCost ?? Number.POSITIVE_INFINITY) <= searchScore) { - return bestExactPreview - } - - return { - ...searchPrefixPreview, - segments: [ - ...searchPrefixPreview.segments, - ...bestAlongsideOption.segments, - ], - terminalPortId: bestAlongsideOption.terminalPortId, - terminalRegionId: bestAlongsideOption.terminalRegionId, - previewCost: searchScore, - } } private buildCompleteTracePreviewOptions( @@ -773,296 +676,13 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { return centerPreview ? [centerPreview] : [] } - const routeId = this.busTraceOrder.traces[traceIndex]!.routeId - const previewOptions: TracePreview[] = [] - const previewOptionKeys = new Set() - - for ( - let sharedStepCount = boundarySteps.length; - sharedStepCount >= 0; - sharedStepCount-- - ) { - const prefixPreview = this.buildPrefixTracePreview( - traceIndex, - sharedStepCount, - boundarySteps, - boundaryPortIdsByStep, - usedPortOwners, - ) - if (!prefixPreview) { - continue - } - - const currentRegionId = - sharedStepCount === 0 - ? this.getStartingNextRegionId( - routeId, - this.problem.routeStartPort[routeId]!, - ) - : boundarySteps[sharedStepCount - 1]!.toRegionId - const currentPortId = - sharedStepCount === 0 - ? this.problem.routeStartPort[routeId]! - : boundaryPortIdsByStep[sharedStepCount - 1]?.[traceIndex] - - if (currentRegionId === undefined || currentPortId === undefined) { - continue - } - - const greedyRemainderSegments = this.inferEndRemainderSegmentsGreedy( - traceIndex, - currentPortId, - currentRegionId, - centerPath, - sharedStepCount, - usedPortOwners, - ) - - if (greedyRemainderSegments) { - const greedySegments = [ - ...prefixPreview.segments, - ...greedyRemainderSegments, - ] - const greedyPreviewKey = this.getTracePreviewPathKey( - greedySegments, - this.problem.routeEndPort[routeId]!, - undefined, - ) - - if (!previewOptionKeys.has(greedyPreviewKey)) { - previewOptionKeys.add(greedyPreviewKey) - previewOptions.push({ - traceIndex, - routeId, - segments: greedySegments, - complete: true, - terminalPortId: this.problem.routeEndPort[routeId]!, - previewCost: 0, - }) - } - } - - if (sharedStepCount === 0 || !greedyRemainderSegments) { - const remainingBoundaryStepCount = Math.max( - boundarySteps.length - sharedStepCount, - 0, - ) - const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) - const completionOptions = this.searchTraceAlongsideOptions({ - traceIndex, - startPortId: prefixPreview.terminalPortId, - startRegionId: prefixPreview.terminalRegionId!, - guidePortIds, - usedPortOwners, - goalPortId: this.problem.routeEndPort[routeId]!, - maxSteps: this.getCompleteTraceSearchMaxSteps( - remainingBoundaryStepCount, - ), - maxOptions: this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, - initialVisitedPortIds: this.getTracePreviewVisitedPortIds( - prefixPreview, - ), - initialVisitedStateKeys: - this.getTracePreviewSearchStartStateKeys(prefixPreview), - }) - - for (const completionOption of completionOptions) { - const combinedSegments = [ - ...prefixPreview.segments, - ...completionOption.segments, - ] - const previewKey = this.getTracePreviewPathKey( - combinedSegments, - this.problem.routeEndPort[routeId]!, - undefined, - ) - - if (previewOptionKeys.has(previewKey)) { - continue - } - - previewOptionKeys.add(previewKey) - previewOptions.push({ - traceIndex, - routeId, - segments: combinedSegments, - complete: true, - terminalPortId: this.problem.routeEndPort[routeId]!, - previewCost: completionOption.searchScore, - }) - } - } - } - - return previewOptions - .sort( - (left, right) => - (left.previewCost ?? 0) - (right.previewCost ?? 0) || - getTracePreviewLength(this.topology, left) - - getTracePreviewLength(this.topology, right), - ) - .slice(0, this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT) - } - - private inferEndRemainderSegmentsGreedy( - traceIndex: number, - startPortId: PortId, - startRegionId: RegionId, - centerPath: BusCenterCandidate[], - sharedStepCount: number, - usedPortOwners: ReadonlyMap, - ): TraceSegment[] | undefined { - const routeId = this.busTraceOrder.traces[traceIndex]!.routeId - const endPortId = this.problem.routeEndPort[routeId]! - - if (this.topology.portZ[endPortId] !== 0) { - return undefined - } - - if (isPortIncidentToRegion(this.topology, endPortId, startRegionId)) { - if ( - ensurePortOwnership(routeId, endPortId, new Map(usedPortOwners)) && - startPortId !== endPortId - ) { - return [ - { - regionId: startRegionId, - fromPortId: startPortId, - toPortId: endPortId, - }, - ] - } - - return [] - } - - const guidePortIds = getGuidePortIds(centerPath, sharedStepCount) - const goalTransitRegionIds = - this.topology.incidentPortRegion[endPortId]?.filter( - (regionId) => regionId !== undefined, - ) ?? [] - const currentNetId = this.problem.routeNet[routeId]! - const localOwners = new Map(usedPortOwners) - if (!ensurePortOwnership(routeId, startPortId, localOwners)) { - return undefined - } - - const visitedStates = new Set([`${startPortId}:${startRegionId}`]) - const segments: TraceSegment[] = [] - let currentPortId = startPortId - let currentRegionId = startRegionId - - for ( - let stepIndex = 0; - stepIndex < this.BUS_MAX_REMAINDER_STEPS; - stepIndex++ - ) { - if (isPortIncidentToRegion(this.topology, endPortId, currentRegionId)) { - if (!ensurePortOwnership(routeId, endPortId, localOwners)) { - return undefined - } - - if (currentPortId !== endPortId) { - segments.push({ - regionId: currentRegionId, - fromPortId: currentPortId, - toPortId: endPortId, - }) - } - - return segments - } - - let bestMove: - | { - boundaryPortId: PortId - nextRegionId: RegionId - score: number - } - | undefined - - for (const boundaryPortId of this.topology.regionIncidentPorts[ - currentRegionId - ] ?? []) { - if ( - boundaryPortId === currentPortId || - this.topology.portZ[boundaryPortId] !== 0 - ) { - continue - } - - const nextRegionId = - this.topology.incidentPortRegion[boundaryPortId]?.[0] === - currentRegionId - ? this.topology.incidentPortRegion[boundaryPortId]?.[1] - : this.topology.incidentPortRegion[boundaryPortId]?.[0] - - if ( - nextRegionId === undefined || - this.isRegionReservedForDifferentBusNet(currentNetId, nextRegionId) || - visitedStates.has(`${boundaryPortId}:${nextRegionId}`) - ) { - continue - } - - const owner = localOwners.get(boundaryPortId) - if (owner !== undefined && owner !== routeId) { - continue - } - - const goalDistance = getPortDistance( - this.topology, - boundaryPortId, - endPortId, - ) - const guideDistance = getDistanceFromPortToPolyline( - this.topology, - boundaryPortId, - guidePortIds, - ) - const sidePenalty = this.getTraceSidePenalty(traceIndex, boundaryPortId) - const goalRegionBonus = goalTransitRegionIds.includes(nextRegionId) - ? -5 - : 0 - const score = - guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + - goalDistance * this.BUS_REMAINDER_GOAL_WEIGHT + - sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + - goalRegionBonus - - if ( - !bestMove || - score < bestMove.score - BUS_CANDIDATE_EPSILON || - (Math.abs(score - bestMove.score) <= BUS_CANDIDATE_EPSILON && - boundaryPortId < bestMove.boundaryPortId) - ) { - bestMove = { - boundaryPortId, - nextRegionId, - score, - } - } - } - - if (!bestMove) { - return undefined - } - - if (!ensurePortOwnership(routeId, bestMove.boundaryPortId, localOwners)) { - return undefined - } - - segments.push({ - regionId: currentRegionId, - fromPortId: currentPortId, - toPortId: bestMove.boundaryPortId, - }) - currentPortId = bestMove.boundaryPortId - currentRegionId = bestMove.nextRegionId - visitedStates.add(`${currentPortId}:${currentRegionId}`) - } - - return undefined + return this.traceInferencePlanner.buildCompleteTracePreviewOptions( + traceIndex, + centerPath, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) } private buildBestCompleteBusPreview( @@ -1274,460 +894,6 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } } - private searchTraceAlongsideOptions({ - traceIndex, - startPortId, - startRegionId, - guidePortIds, - usedPortOwners, - maxSteps, - maxOptions, - targetGuideProgress, - goalPortId, - initialVisitedPortIds = [], - initialVisitedStateKeys = [], - }: { - traceIndex: number - startPortId: PortId - startRegionId: RegionId - guidePortIds: readonly PortId[] - usedPortOwners: ReadonlyMap - maxSteps: number - maxOptions: number - targetGuideProgress?: number - goalPortId?: PortId - initialVisitedPortIds?: readonly PortId[] - initialVisitedStateKeys?: readonly string[] - }): AlongsideTraceSearchOption[] { - const routeId = this.busTraceOrder.traces[traceIndex]!.routeId - const effectiveGuidePortIds = - guidePortIds.length > 0 ? guidePortIds : [startPortId] - const initialGuideProgress = getPortProgressAlongPolyline( - this.topology, - startPortId, - effectiveGuidePortIds, - ) - const visitedPortIds = new Set(initialVisitedPortIds) - const visitedStateKeys = new Set(initialVisitedStateKeys) - visitedPortIds.add(startPortId) - visitedStateKeys.add( - this.getTraceSearchStateKey(startPortId, startRegionId), - ) - - const initialNode: AlongsideTraceSearchNode = { - portId: startPortId, - regionId: startRegionId, - segments: [], - guideProgress: initialGuideProgress, - travelCost: 0, - priority: - goalPortId === undefined - ? this.getPartialTraceSearchPriority( - traceIndex, - startPortId, - effectiveGuidePortIds, - initialGuideProgress, - 0, - targetGuideProgress ?? 0, - ) - : this.getCompleteTraceSearchPriority( - traceIndex, - routeId, - startPortId, - effectiveGuidePortIds, - 0, - ), - visitedPortIds, - visitedStateKeys, - } - - const searchOptions: AlongsideTraceSearchOption[] = [] - const searchOptionKeys = new Set() - const pushOption = (option: AlongsideTraceSearchOption) => { - const optionKey = this.getTracePreviewPathKey( - option.segments, - option.terminalPortId, - option.terminalRegionId, - ) - - if (searchOptionKeys.has(optionKey)) { - return - } - - searchOptionKeys.add(optionKey) - searchOptions.push(option) - } - const tryCompleteFromNode = (node: AlongsideTraceSearchNode) => { - if ( - goalPortId === undefined || - !isPortIncidentToRegion(this.topology, goalPortId, node.regionId) - ) { - return - } - - const owner = usedPortOwners.get(goalPortId) - if (owner !== undefined && owner !== routeId) { - return - } - - const completionSegments = - node.portId === goalPortId - ? node.segments - : [ - ...node.segments, - { - regionId: node.regionId, - fromPortId: node.portId, - toPortId: goalPortId, - }, - ] - const completionTravelCost = - node.travelCost + - (node.portId === goalPortId - ? 0 - : getPortDistance(this.topology, node.portId, goalPortId) * - this.DISTANCE_TO_COST) - - pushOption({ - segments: completionSegments, - terminalPortId: goalPortId, - terminalRegionId: node.regionId, - searchScore: this.getCompleteTraceSearchPriority( - traceIndex, - routeId, - goalPortId, - effectiveGuidePortIds, - completionTravelCost, - ), - }) - } - const tryPartialFromNode = (node: AlongsideTraceSearchNode) => { - if (targetGuideProgress === undefined) { - return - } - - pushOption({ - segments: node.segments, - terminalPortId: node.portId, - terminalRegionId: node.regionId, - searchScore: this.getPartialTraceSearchPriority( - traceIndex, - node.portId, - effectiveGuidePortIds, - node.guideProgress, - node.travelCost, - targetGuideProgress, - ), - }) - } - - let beam = [initialNode] - tryCompleteFromNode(initialNode) - tryPartialFromNode(initialNode) - - for (let stepIndex = 0; stepIndex < maxSteps && beam.length > 0; stepIndex++) { - const nextBeamCandidates: AlongsideTraceSearchNode[] = [] - - for (const node of beam) { - const moveCandidates: AlongsideTraceSearchNode[] = [] - - for (const boundaryPortId of this.topology.regionIncidentPorts[ - node.regionId - ] ?? []) { - if ( - boundaryPortId === node.portId || - this.topology.portZ[boundaryPortId] !== 0 || - node.visitedPortIds.has(boundaryPortId) - ) { - continue - } - - const nextRegionId = this.getOppositeRegionId( - boundaryPortId, - node.regionId, - ) - - if ( - nextRegionId === undefined || - this.isRegionReservedForDifferentBusNet( - this.problem.routeNet[routeId]!, - nextRegionId, - ) - ) { - continue - } - - const owner = usedPortOwners.get(boundaryPortId) - if (owner !== undefined && owner !== routeId) { - continue - } - - const nextStateKey = this.getTraceSearchStateKey( - boundaryPortId, - nextRegionId, - ) - if (node.visitedStateKeys.has(nextStateKey)) { - continue - } - - const nextGuideProgress = getPortProgressAlongPolyline( - this.topology, - boundaryPortId, - effectiveGuidePortIds, - ) - const regressionPenalty = - Math.max(0, node.guideProgress - nextGuideProgress) * - this.TRACE_ALONGSIDE_REGRESSION_WEIGHT - const nextTravelCost = - node.travelCost + - getPortDistance(this.topology, node.portId, boundaryPortId) * - this.DISTANCE_TO_COST + - regressionPenalty - const nextVisitedPortIds = new Set(node.visitedPortIds) - const nextVisitedStateKeys = new Set(node.visitedStateKeys) - nextVisitedPortIds.add(boundaryPortId) - nextVisitedStateKeys.add(nextStateKey) - - moveCandidates.push({ - portId: boundaryPortId, - regionId: nextRegionId, - segments: [ - ...node.segments, - { - regionId: node.regionId, - fromPortId: node.portId, - toPortId: boundaryPortId, - }, - ], - guideProgress: nextGuideProgress, - travelCost: nextTravelCost, - priority: - goalPortId === undefined - ? this.getPartialTraceSearchPriority( - traceIndex, - boundaryPortId, - effectiveGuidePortIds, - nextGuideProgress, - nextTravelCost, - targetGuideProgress ?? 0, - ) - : this.getCompleteTraceSearchPriority( - traceIndex, - routeId, - boundaryPortId, - effectiveGuidePortIds, - nextTravelCost, - ), - visitedPortIds: nextVisitedPortIds, - visitedStateKeys: nextVisitedStateKeys, - }) - } - - moveCandidates - .sort( - (left, right) => - left.priority - right.priority || - left.portId - right.portId || - left.regionId - right.regionId, - ) - .slice(0, this.TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT) - .forEach((candidate) => { - nextBeamCandidates.push(candidate) - tryCompleteFromNode(candidate) - tryPartialFromNode(candidate) - }) - } - - const bestCandidateByStateKey = new Map() - for (const candidate of nextBeamCandidates) { - const candidateStateKey = this.getTraceSearchStateKey( - candidate.portId, - candidate.regionId, - ) - const existingCandidate = - bestCandidateByStateKey.get(candidateStateKey) - - if ( - !existingCandidate || - candidate.priority < - existingCandidate.priority - BUS_CANDIDATE_EPSILON || - (Math.abs(candidate.priority - existingCandidate.priority) <= - BUS_CANDIDATE_EPSILON && - candidate.segments.length < existingCandidate.segments.length) - ) { - bestCandidateByStateKey.set(candidateStateKey, candidate) - } - } - - beam = [...bestCandidateByStateKey.values()] - .sort( - (left, right) => - left.priority - right.priority || - left.portId - right.portId || - left.regionId - right.regionId, - ) - .slice(0, this.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH) - } - - return searchOptions - .sort( - (left, right) => - left.searchScore - right.searchScore || - left.segments.length - right.segments.length, - ) - .slice(0, maxOptions) - } - - private getPartialTraceSearchPriority( - traceIndex: number, - portId: PortId, - guidePortIds: readonly PortId[], - guideProgress: number, - travelCost: number, - targetGuideProgress: number, - ) { - const guideDistance = getDistanceFromPortToPolyline( - this.topology, - portId, - guidePortIds, - ) - const lanePenalty = this.getTraceLanePenalty(traceIndex, portId) - const sidePenalty = this.getTraceSidePenalty(traceIndex, portId) - const shortfallPenalty = Math.max(0, targetGuideProgress - guideProgress) - const overshootPenalty = Math.max(0, guideProgress - targetGuideProgress) - - return ( - travelCost + - guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + - lanePenalty * this.TRACE_ALONGSIDE_LANE_WEIGHT + - sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + - shortfallPenalty * 2 + - overshootPenalty * 4 - ) - } - - private getCompleteTraceSearchPriority( - traceIndex: number, - routeId: RouteId, - portId: PortId, - guidePortIds: readonly PortId[], - travelCost: number, - ) { - const guideDistance = getDistanceFromPortToPolyline( - this.topology, - portId, - guidePortIds, - ) - const lanePenalty = this.getTraceLanePenalty(traceIndex, portId) - const sidePenalty = this.getTraceSidePenalty(traceIndex, portId) - const goalHeuristic = - this.problemSetup.portHCostToEndOfRoute[ - portId * this.problem.routeCount + routeId - ] - - return ( - travelCost + - guideDistance * this.BUS_REMAINDER_GUIDE_WEIGHT + - lanePenalty * this.TRACE_ALONGSIDE_LANE_WEIGHT + - sidePenalty * this.BUS_REMAINDER_SIDE_WEIGHT + - goalHeuristic * this.BUS_REMAINDER_GOAL_WEIGHT - ) - } - - private getTracePreviewSearchStartStateKeys(tracePreview: TracePreview) { - const visitedStateKeys: string[] = [] - let currentPortId = this.problem.routeStartPort[tracePreview.routeId]! - let currentRegionId = this.getStartingNextRegionId( - tracePreview.routeId, - currentPortId, - ) - - if (currentRegionId === undefined) { - return visitedStateKeys - } - - visitedStateKeys.push( - this.getTraceSearchStateKey(currentPortId, currentRegionId), - ) - - for (const segment of tracePreview.segments) { - currentPortId = segment.toPortId - const nextRegionId = this.getOppositeRegionId(currentPortId, segment.regionId) - - if (nextRegionId === undefined) { - break - } - - currentRegionId = nextRegionId - visitedStateKeys.push( - this.getTraceSearchStateKey(currentPortId, currentRegionId), - ) - } - - return visitedStateKeys - } - - private getTracePreviewVisitedPortIds(tracePreview: TracePreview) { - const visitedPortIds = new Set([ - this.problem.routeStartPort[tracePreview.routeId]!, - ]) - - for (const segment of tracePreview.segments) { - visitedPortIds.add(segment.fromPortId) - visitedPortIds.add(segment.toPortId) - } - - return [...visitedPortIds] - } - - private getTraceSearchStateKey(portId: PortId, regionId: RegionId) { - return `${portId}:${regionId}` - } - - private getTracePreviewPathKey( - segments: readonly TraceSegment[], - terminalPortId: PortId, - terminalRegionId?: RegionId, - ) { - return [ - ...segments.map( - (segment) => - `${segment.regionId}:${segment.fromPortId}->${segment.toPortId}`, - ), - `end:${terminalPortId}:${terminalRegionId ?? -1}`, - ].join("|") - } - - private getOppositeRegionId(portId: PortId, regionId: RegionId) { - const incidentRegionIds = this.topology.incidentPortRegion[portId] ?? [] - return incidentRegionIds[0] === regionId - ? incidentRegionIds[1] - : incidentRegionIds[0] - } - - private getPartialTraceSearchMaxSteps(remainingBoundaryStepCount: number) { - return Math.max( - 0, - Math.min( - Math.max(1, remainingBoundaryStepCount + 1), - 4, - ), - ) - } - - private getCompleteTraceSearchMaxSteps(remainingBoundaryStepCount: number) { - return Math.max( - 0, - Math.min( - Math.max( - 2, - remainingBoundaryStepCount * 2 + this.BUS_MAX_REMAINDER_STEPS, - ), - this.BUS_MAX_REMAINDER_STEPS * 3, - ), - ) - } - private maybeStoreBestCompleteFallbackPreview( preview: BusPreview, totalIntersectionCount: number, @@ -2192,11 +1358,15 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { return Math.abs(projection - trace.score) } + private getRouteHeuristic(routeId: RouteId, portId: PortId) { + return this.problemSetup.portHCostToEndOfRoute[ + portId * this.problem.routeCount + routeId + ] + } + private computeCenterHeuristic(portId: PortId, nextRegionId?: RegionId) { const portHeuristic = - this.problemSetup.portHCostToEndOfRoute[ - portId * this.problem.routeCount + this.centerRouteId - ] + this.getRouteHeuristic(this.centerRouteId, portId) if (nextRegionId === undefined) { return portHeuristic From cb12a419e466cbfdb7b238f8085c5f754a46fe7d Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 14 Apr 2026 20:56:34 -0700 Subject: [PATCH 3/5] format --- lib/bus-solver/BusTraceInferencePlanner.ts | 69 ++++++++++++++++------ lib/bus-solver/TinyHyperGraphBusSolver.ts | 28 ++++----- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/lib/bus-solver/BusTraceInferencePlanner.ts b/lib/bus-solver/BusTraceInferencePlanner.ts index a32eaae..2e362ab 100644 --- a/lib/bus-solver/BusTraceInferencePlanner.ts +++ b/lib/bus-solver/BusTraceInferencePlanner.ts @@ -66,7 +66,10 @@ interface BusTraceInferencePlannerOptions { TRACE_ALONGSIDE_LANE_WEIGHT: number TRACE_ALONGSIDE_REGRESSION_WEIGHT: number buildPrefixTracePreview: BuildPrefixTracePreviewFn - getStartingNextRegionId: (routeId: RouteId, startPortId: PortId) => RegionId | undefined + getStartingNextRegionId: ( + routeId: RouteId, + startPortId: PortId, + ) => RegionId | undefined isRegionReservedForDifferentBusNet: ( currentNetId: number, regionId: RegionId, @@ -176,12 +179,14 @@ export class BusTraceInferencePlanner { startRegionId: searchPrefixPreview.terminalRegionId, guidePortIds, usedPortOwners, - targetGuideProgress: getPolylineLength(this.options.topology, guidePortIds), + targetGuideProgress: getPolylineLength( + this.options.topology, + guidePortIds, + ), maxSteps: this.getPartialTraceSearchMaxSteps(maxSharedStepCount), maxOptions: 1, - initialVisitedPortIds: this.getTracePreviewVisitedPortIds( - searchPrefixPreview, - ), + initialVisitedPortIds: + this.getTracePreviewVisitedPortIds(searchPrefixPreview), initialVisitedStateKeys: this.getTracePreviewSearchStartStateKeys(searchPrefixPreview), }) @@ -305,9 +310,8 @@ export class BusTraceInferencePlanner { remainingBoundaryStepCount, ), maxOptions: this.options.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, - initialVisitedPortIds: this.getTracePreviewVisitedPortIds( - prefixPreview, - ), + initialVisitedPortIds: + this.getTracePreviewVisitedPortIds(prefixPreview), initialVisitedStateKeys: this.getTracePreviewSearchStartStateKeys(prefixPreview), }) @@ -365,7 +369,9 @@ export class BusTraceInferencePlanner { return undefined } - if (isPortIncidentToRegion(this.options.topology, endPortId, startRegionId)) { + if ( + isPortIncidentToRegion(this.options.topology, endPortId, startRegionId) + ) { if ( ensurePortOwnership(routeId, endPortId, new Map(usedPortOwners)) && startPortId !== endPortId @@ -403,7 +409,13 @@ export class BusTraceInferencePlanner { stepIndex < this.options.BUS_MAX_REMAINDER_STEPS; stepIndex++ ) { - if (isPortIncidentToRegion(this.options.topology, endPortId, currentRegionId)) { + if ( + isPortIncidentToRegion( + this.options.topology, + endPortId, + currentRegionId, + ) + ) { if (!ensurePortOwnership(routeId, endPortId, localOwners)) { return undefined } @@ -603,7 +615,11 @@ export class BusTraceInferencePlanner { const tryCompleteFromNode = (node: AlongsideTraceSearchNode) => { if ( goalPortId === undefined || - !isPortIncidentToRegion(this.options.topology, goalPortId, node.regionId) + !isPortIncidentToRegion( + this.options.topology, + goalPortId, + node.regionId, + ) ) { return } @@ -668,7 +684,11 @@ export class BusTraceInferencePlanner { tryCompleteFromNode(initialNode) tryPartialFromNode(initialNode) - for (let stepIndex = 0; stepIndex < maxSteps && beam.length > 0; stepIndex++) { + for ( + let stepIndex = 0; + stepIndex < maxSteps && beam.length > 0; + stepIndex++ + ) { const nextBeamCandidates: AlongsideTraceSearchNode[] = [] for (const node of beam) { @@ -723,7 +743,11 @@ export class BusTraceInferencePlanner { this.options.TRACE_ALONGSIDE_REGRESSION_WEIGHT const nextTravelCost = node.travelCost + - getPortDistance(this.options.topology, node.portId, boundaryPortId) * + getPortDistance( + this.options.topology, + node.portId, + boundaryPortId, + ) * this.options.DISTANCE_TO_COST + regressionPenalty const nextVisitedPortIds = new Set(node.visitedPortIds) @@ -781,14 +805,16 @@ export class BusTraceInferencePlanner { }) } - const bestCandidateByStateKey = new Map() + const bestCandidateByStateKey = new Map< + string, + AlongsideTraceSearchNode + >() for (const candidate of nextBeamCandidates) { const candidateStateKey = this.getTraceSearchStateKey( candidate.portId, candidate.regionId, ) - const existingCandidate = - bestCandidateByStateKey.get(candidateStateKey) + const existingCandidate = bestCandidateByStateKey.get(candidateStateKey) if ( !existingCandidate || @@ -876,7 +902,8 @@ export class BusTraceInferencePlanner { private getTracePreviewSearchStartStateKeys(tracePreview: TracePreview) { const visitedStateKeys: string[] = [] - let currentPortId = this.options.problem.routeStartPort[tracePreview.routeId]! + let currentPortId = + this.options.problem.routeStartPort[tracePreview.routeId]! let currentRegionId = this.options.getStartingNextRegionId( tracePreview.routeId, currentPortId, @@ -892,7 +919,10 @@ export class BusTraceInferencePlanner { for (const segment of tracePreview.segments) { currentPortId = segment.toPortId - const nextRegionId = this.getOppositeRegionId(currentPortId, segment.regionId) + const nextRegionId = this.getOppositeRegionId( + currentPortId, + segment.regionId, + ) if (nextRegionId === undefined) { break @@ -939,7 +969,8 @@ export class BusTraceInferencePlanner { } private getOppositeRegionId(portId: PortId, regionId: RegionId) { - const incidentRegionIds = this.options.topology.incidentPortRegion[portId] ?? [] + const incidentRegionIds = + this.options.topology.incidentPortRegion[portId] ?? [] return incidentRegionIds[0] === regionId ? incidentRegionIds[1] : incidentRegionIds[0] diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 8f1cecc..63d2248 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -36,10 +36,7 @@ import { type TracePreview, type TraceSegment, } from "./busSolverTypes" -import { - getPortDistance, - getPortProjection, -} from "./geometry" +import { getPortDistance, getPortProjection } from "./geometry" import { clearPreviewRoutingState as clearPreviewRoutingStateValue, getPreviewIntersectionCounts as getPreviewIntersectionCountsValue, @@ -155,15 +152,15 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { BUS_REMAINDER_SIDE_WEIGHT: this.BUS_REMAINDER_SIDE_WEIGHT, TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT: this.TRACE_ALONGSIDE_SEARCH_BRANCH_LIMIT, - TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH: - this.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH, + TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH: this.TRACE_ALONGSIDE_SEARCH_BEAM_WIDTH, TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT: this.TRACE_ALONGSIDE_SEARCH_OPTION_LIMIT, TRACE_ALONGSIDE_LANE_WEIGHT: this.TRACE_ALONGSIDE_LANE_WEIGHT, - TRACE_ALONGSIDE_REGRESSION_WEIGHT: - this.TRACE_ALONGSIDE_REGRESSION_WEIGHT, - buildPrefixTracePreview: (...args) => this.buildPrefixTracePreview(...args), - getStartingNextRegionId: (...args) => this.getStartingNextRegionId(...args), + TRACE_ALONGSIDE_REGRESSION_WEIGHT: this.TRACE_ALONGSIDE_REGRESSION_WEIGHT, + buildPrefixTracePreview: (...args) => + this.buildPrefixTracePreview(...args), + getStartingNextRegionId: (...args) => + this.getStartingNextRegionId(...args), isRegionReservedForDifferentBusNet: (...args) => this.isRegionReservedForDifferentBusNet(...args), getTraceSidePenalty: (...args) => this.getTraceSidePenalty(...args), @@ -283,7 +280,8 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { preview.sameLayerIntersectionCount > 0 || preview.crossingLayerIntersectionCount > 0 const totalIntersectionCount = - preview.sameLayerIntersectionCount + preview.crossingLayerIntersectionCount + preview.sameLayerIntersectionCount + + preview.crossingLayerIntersectionCount const hasInferenceFailure = preview.reason !== undefined && !hasIntersections const allowIntersectingPartialExpansion = @@ -295,7 +293,10 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { preview.completeTraceCount === this.problem.routeCount && totalIntersectionCount > 0 ) { - this.maybeStoreBestCompleteFallbackPreview(preview, totalIntersectionCount) + this.maybeStoreBestCompleteFallbackPreview( + preview, + totalIntersectionCount, + ) } if ( @@ -1365,8 +1366,7 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } private computeCenterHeuristic(portId: PortId, nextRegionId?: RegionId) { - const portHeuristic = - this.getRouteHeuristic(this.centerRouteId, portId) + const portHeuristic = this.getRouteHeuristic(this.centerRouteId, portId) if (nextRegionId === undefined) { return portHeuristic From 1e2c4e9d6a9808e51e2543befffed4209bb4cbc1 Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 14 Apr 2026 21:45:58 -0700 Subject: [PATCH 4/5] testing bus splitting --- .../double-split-long-span.page.tsx | 52 ++++ .../bus-double-split-long-span.fixture.ts | 225 ++++++++++++++++++ .../bus-double-split-long-span-repro.test.ts | 205 ++++++++++++++++ 3 files changed, 482 insertions(+) create mode 100644 pages/bus-routing/double-split-long-span.page.tsx create mode 100644 tests/fixtures/bus-double-split-long-span.fixture.ts create mode 100644 tests/solver/bus-double-split-long-span-repro.test.ts diff --git a/pages/bus-routing/double-split-long-span.page.tsx b/pages/bus-routing/double-split-long-span.page.tsx new file mode 100644 index 0000000..0433c77 --- /dev/null +++ b/pages/bus-routing/double-split-long-span.page.tsx @@ -0,0 +1,52 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { + busDoubleSplitLongSpanFixture, +} from "../../tests/fixtures/bus-double-split-long-span.fixture" +import { Debugger } from "../components/Debugger" + +const createBusSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) +} + +const createPlainSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) +} + +export default function BusRoutingDoubleSplitLongSpanPage() { + return ( +
+
+
+
+ Current Bus Solver +
+
+ +
+
+
+
+ Reference Plain Solver +
+
+ +
+
+
+
+ ) +} diff --git a/tests/fixtures/bus-double-split-long-span.fixture.ts b/tests/fixtures/bus-double-split-long-span.fixture.ts new file mode 100644 index 0000000..58fb74c --- /dev/null +++ b/tests/fixtures/bus-double-split-long-span.fixture.ts @@ -0,0 +1,225 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" + +export const BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT = 6 +export const BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE = 4 +export const BUS_DOUBLE_SPLIT_LONG_SPAN_BRIDGE_REGION_COUNT = 3 +export const BUS_DOUBLE_SPLIT_LONG_SPAN_SPLIT_STAGE_COUNT = 3 + +const START_END_XS = [-10, -6, -2, 2, 6, 10] as const +const SPLIT_LEFT_XS = [-9, -6, -3, -0.5] as const +const SPLIT_RIGHT_XS = [0.5, 3, 6, 9] as const + +const createRegion = ( + regionId: string, + centerX: number, + centerY: number, + width: number, + height: number, + pointIds: string[], +): NonNullable[number] => ({ + regionId, + pointIds, + d: { + center: { x: centerX, y: centerY }, + width, + height, + }, +}) + +const createPort = ( + portId: string, + region1Id: string, + region2Id: string, + x: number, + y: number, +): NonNullable[number] => ({ + portId, + region1Id, + region2Id, + d: { + x, + y, + z: 0, + }, +}) + +const createConnection = ( + routeIndex: number, +): NonNullable[number] => ({ + connectionId: `route-${routeIndex}`, + startRegionId: `start-${routeIndex}`, + endRegionId: `end-${routeIndex}`, + mutuallyConnectedNetworkId: `net-${routeIndex}`, +}) + +const startPortIds = START_END_XS.map((_, routeIndex) => `start-port-${routeIndex}`) +const bridgeChainPortIds = START_END_XS.map( + (_, routeIndex) => `bridge-chain-port-${routeIndex}`, +) +const bottomChainPortIds = START_END_XS.map( + (_, routeIndex) => `bottom-chain-port-${routeIndex}`, +) +const bottomExitPortIds = START_END_XS.map( + (_, routeIndex) => `bottom-exit-port-${routeIndex}`, +) +const endPortIds = START_END_XS.map((_, routeIndex) => `end-port-${routeIndex}`) + +const splitATopLeftPortIds = SPLIT_LEFT_XS.map((_, portIndex) => `a-tl-${portIndex}`) +const splitABottomLeftPortIds = SPLIT_LEFT_XS.map( + (_, portIndex) => `a-bl-${portIndex}`, +) +const splitATopRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `a-tr-${portIndex}`, +) +const splitABottomRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `a-br-${portIndex}`, +) +const splitBTopLeftPortIds = SPLIT_LEFT_XS.map((_, portIndex) => `b-tl-${portIndex}`) +const splitBBottomLeftPortIds = SPLIT_LEFT_XS.map( + (_, portIndex) => `b-bl-${portIndex}`, +) +const splitBTopRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `b-tr-${portIndex}`, +) +const splitBBottomRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `b-br-${portIndex}`, +) +const splitCTopLeftPortIds = SPLIT_LEFT_XS.map((_, portIndex) => `c-tl-${portIndex}`) +const splitCBottomLeftPortIds = SPLIT_LEFT_XS.map( + (_, portIndex) => `c-bl-${portIndex}`, +) +const splitCTopRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `c-tr-${portIndex}`, +) +const splitCBottomRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `c-br-${portIndex}`, +) + +export const busDoubleSplitLongSpanFixture: SerializedHyperGraph = { + regions: [ + ...START_END_XS.flatMap((x, routeIndex) => [ + createRegion(`start-${routeIndex}`, x, 31.5, 1.2, 1.2, [ + `start-port-${routeIndex}`, + ]), + createRegion(`end-${routeIndex}`, x, -43.5, 1.2, 1.2, [ + `end-port-${routeIndex}`, + ]), + ]), + createRegion("top-main", 0, 25.5, 24, 3, [ + ...startPortIds, + ...splitATopLeftPortIds, + ...splitATopRightPortIds, + ]), + createRegion("split-a-left", -5.5, 19.5, 8.5, 9, [ + ...splitATopLeftPortIds, + ...splitABottomLeftPortIds, + ]), + createRegion("split-a-right", 5.5, 19.5, 8.5, 9, [ + ...splitATopRightPortIds, + ...splitABottomRightPortIds, + ]), + createRegion("bridge-upper", 0, 12, 28, 6, [ + ...splitABottomLeftPortIds, + ...splitABottomRightPortIds, + ...bridgeChainPortIds, + ]), + createRegion("bridge-lower", 0, 6, 28, 6, [ + ...bridgeChainPortIds, + ...splitBTopLeftPortIds, + ...splitBTopRightPortIds, + ]), + createRegion("split-b-left", -5.5, -1.5, 8.5, 9, [ + ...splitBTopLeftPortIds, + ...splitBBottomLeftPortIds, + ]), + createRegion("split-b-right", 5.5, -1.5, 8.5, 9, [ + ...splitBTopRightPortIds, + ...splitBBottomRightPortIds, + ]), + createRegion("bridge-final", 0, -10, 28, 7, [ + ...splitBBottomLeftPortIds, + ...splitBBottomRightPortIds, + ...splitCTopLeftPortIds, + ...splitCTopRightPortIds, + ]), + createRegion("split-c-left", -5.5, -18, 8.5, 9, [ + ...splitCTopLeftPortIds, + ...splitCBottomLeftPortIds, + ]), + createRegion("split-c-right", 5.5, -18, 8.5, 9, [ + ...splitCTopRightPortIds, + ...splitCBottomRightPortIds, + ]), + createRegion("bottom-main", 0, -25.5, 24, 6, [ + ...splitCBottomLeftPortIds, + ...splitCBottomRightPortIds, + ...bottomChainPortIds, + ]), + createRegion("bottom-buffer", 0, -32, 24, 6, [ + ...bottomChainPortIds, + ...bottomExitPortIds, + ]), + createRegion("bottom-exit", 0, -38, 24, 4, [ + ...bottomExitPortIds, + ...endPortIds, + ]), + ], + ports: [ + ...START_END_XS.flatMap((x, routeIndex) => [ + createPort( + `start-port-${routeIndex}`, + `start-${routeIndex}`, + "top-main", + x, + 28.5, + ), + createPort( + `bridge-chain-port-${routeIndex}`, + "bridge-upper", + "bridge-lower", + x, + 8.5, + ), + createPort( + `bottom-chain-port-${routeIndex}`, + "bottom-main", + "bottom-buffer", + x, + -28.5, + ), + createPort( + `bottom-exit-port-${routeIndex}`, + "bottom-buffer", + "bottom-exit", + x, + -35, + ), + createPort( + `end-port-${routeIndex}`, + "bottom-exit", + `end-${routeIndex}`, + x, + -41, + ), + ]), + ...SPLIT_LEFT_XS.flatMap((x, portIndex) => [ + createPort(`a-tl-${portIndex}`, "top-main", "split-a-left", x, 24), + createPort(`a-bl-${portIndex}`, "split-a-left", "bridge-upper", x, 15), + createPort(`b-tl-${portIndex}`, "bridge-lower", "split-b-left", x, 3), + createPort(`b-bl-${portIndex}`, "split-b-left", "bridge-final", x, -6), + createPort(`c-tl-${portIndex}`, "bridge-final", "split-c-left", x, -13), + createPort(`c-bl-${portIndex}`, "split-c-left", "bottom-main", x, -22), + ]), + ...SPLIT_RIGHT_XS.flatMap((x, portIndex) => [ + createPort(`a-tr-${portIndex}`, "top-main", "split-a-right", x, 24), + createPort(`a-br-${portIndex}`, "split-a-right", "bridge-upper", x, 15), + createPort(`b-tr-${portIndex}`, "bridge-lower", "split-b-right", x, 3), + createPort(`b-br-${portIndex}`, "split-b-right", "bridge-final", x, -6), + createPort(`c-tr-${portIndex}`, "bridge-final", "split-c-right", x, -13), + createPort(`c-br-${portIndex}`, "split-c-right", "bottom-main", x, -22), + ]), + ], + connections: START_END_XS.map((_, routeIndex) => + createConnection(routeIndex), + ), +} diff --git a/tests/solver/bus-double-split-long-span-repro.test.ts b/tests/solver/bus-double-split-long-span-repro.test.ts new file mode 100644 index 0000000..c50c444 --- /dev/null +++ b/tests/solver/bus-double-split-long-span-repro.test.ts @@ -0,0 +1,205 @@ +import { expect, test } from "bun:test" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { + BUS_DOUBLE_SPLIT_LONG_SPAN_BRIDGE_REGION_COUNT, + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + BUS_DOUBLE_SPLIT_LONG_SPAN_SPLIT_STAGE_COUNT, + busDoubleSplitLongSpanFixture, +} from "tests/fixtures/bus-double-split-long-span.fixture" + +const countSharedPorts = (regionAId: string, regionBId: string) => + busDoubleSplitLongSpanFixture.ports.filter( + (port) => + (port.region1Id === regionAId && port.region2Id === regionBId) || + (port.region1Id === regionBId && port.region2Id === regionAId), + ).length + +const countRoutesUsingRegion = ( + solvedRoutes: NonNullable["solvedRoutes"]>, + regionId: string, +) => + solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === regionId), + ).length + +const getRegionIndexBySerializedId = ( + topology: ReturnType["topology"], +) => { + const regionIndexBySerializedId = new Map() + + topology.regionMetadata?.forEach((metadata, regionIndex) => { + const serializedRegionId = (metadata as { serializedRegionId?: string }) + .serializedRegionId + if (typeof serializedRegionId === "string") { + regionIndexBySerializedId.set(serializedRegionId, regionIndex) + } + }) + + return regionIndexBySerializedId +} + +test("repro: six-trace triple split requires both split halves at all stages", () => { + const { topology, problem } = loadSerializedHyperGraph( + busDoubleSplitLongSpanFixture, + ) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + + expect(problem.routeCount).toBe(BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT) + expect(BUS_DOUBLE_SPLIT_LONG_SPAN_SPLIT_STAGE_COUNT).toBe(3) + expect(BUS_DOUBLE_SPLIT_LONG_SPAN_BRIDGE_REGION_COUNT).toBe(3) + expect(countSharedPorts("top-main", "split-a-left")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("top-main", "split-a-right")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-lower", "split-b-left")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-lower", "split-b-right")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-final", "split-c-left")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-final", "split-c-right")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("top-main", "split-a-left"), + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("bridge-lower", "split-b-left"), + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("bridge-final", "split-c-left"), + ) + + plainSolver.solve() + + expect(plainSolver.solved).toBe(true) + expect(plainSolver.failed).toBe(false) + + const solvedRoutes = plainSolver.getOutput().solvedRoutes ?? [] + + expect(solvedRoutes).toHaveLength(BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-upper")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-lower")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-final")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-right")).toBeGreaterThan(0) +}) + +test("repro: bus solver spans all three split stages across the long bridge", () => { + const { topology, problem } = loadSerializedHyperGraph( + busDoubleSplitLongSpanFixture, + ) + const busSolver = new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) + + const splitALeftRegionIndex = regionIndexBySerializedId.get("split-a-left") + const splitARightRegionIndex = regionIndexBySerializedId.get("split-a-right") + const splitBLeftRegionIndex = regionIndexBySerializedId.get("split-b-left") + const splitBRightRegionIndex = regionIndexBySerializedId.get("split-b-right") + const splitCLeftRegionIndex = regionIndexBySerializedId.get("split-c-left") + const splitCRightRegionIndex = regionIndexBySerializedId.get("split-c-right") + + expect(splitALeftRegionIndex).toBeDefined() + expect(splitARightRegionIndex).toBeDefined() + expect(splitBLeftRegionIndex).toBeDefined() + expect(splitBRightRegionIndex).toBeDefined() + expect(splitCLeftRegionIndex).toBeDefined() + expect(splitCRightRegionIndex).toBeDefined() + expect( + busSolver.centerGoalHopDistanceByRegion[splitALeftRegionIndex!], + ).toBeGreaterThan(6) + expect( + busSolver.centerGoalHopDistanceByRegion[splitARightRegionIndex!], + ).toBeGreaterThan(6) + expect( + busSolver.centerGoalHopDistanceByRegion[splitBLeftRegionIndex!], + ).toBeGreaterThan(4) + expect( + busSolver.centerGoalHopDistanceByRegion[splitBRightRegionIndex!], + ).toBeGreaterThan(4) + expect( + busSolver.centerGoalHopDistanceByRegion[splitCLeftRegionIndex!], + ).toBeGreaterThan(2) + expect( + busSolver.centerGoalHopDistanceByRegion[splitCRightRegionIndex!], + ).toBeGreaterThan(2) + + busSolver.solve() + plainSolver.solve() + + expect(busSolver.solved).toBe(true) + expect(busSolver.failed).toBe(false) + + const solvedRoutes = busSolver.getOutput().solvedRoutes ?? [] + const sameLayerIntersectionCount = + busSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingSameLayerIntersections, + 0, + ) + const crossingLayerIntersectionCount = + busSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingCrossingLayerIntersections, + 0, + ) + const plainSameLayerIntersectionCount = + plainSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingSameLayerIntersections, + 0, + ) + const plainCrossingLayerIntersectionCount = + plainSolver.state.regionIntersectionCaches.reduce( + (total, regionCache) => + total + regionCache.existingCrossingLayerIntersections, + 0, + ) + + expect(solvedRoutes).toHaveLength(BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-upper")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-lower")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-final")).toBe( + BUS_DOUBLE_SPLIT_LONG_SPAN_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-right")).toBeGreaterThan(0) + expect(sameLayerIntersectionCount).toBeLessThanOrEqual( + plainSameLayerIntersectionCount, + ) + expect(crossingLayerIntersectionCount).toBeLessThanOrEqual( + plainCrossingLayerIntersectionCount, + ) +}) From 9ef7a7fd4226bbf36df02f764f48a9e59d584590 Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 14 Apr 2026 21:51:44 -0700 Subject: [PATCH 5/5] regionspan countercase --- pages/bus-routing/right-turn-l-shape.page.tsx | 50 ++++ .../bus-right-turn-l-shape.fixture.ts | 234 ++++++++++++++++++ .../bus-right-turn-l-shape-repro.test.ts | 161 ++++++++++++ 3 files changed, 445 insertions(+) create mode 100644 pages/bus-routing/right-turn-l-shape.page.tsx create mode 100644 tests/fixtures/bus-right-turn-l-shape.fixture.ts create mode 100644 tests/solver/bus-right-turn-l-shape-repro.test.ts diff --git a/pages/bus-routing/right-turn-l-shape.page.tsx b/pages/bus-routing/right-turn-l-shape.page.tsx new file mode 100644 index 0000000..a438743 --- /dev/null +++ b/pages/bus-routing/right-turn-l-shape.page.tsx @@ -0,0 +1,50 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { busRightTurnLShapeFixture } from "../../tests/fixtures/bus-right-turn-l-shape.fixture" +import { Debugger } from "../components/Debugger" + +const createBusSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) +} + +const createPlainSolver = (serializedHyperGraph: SerializedHyperGraph) => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + return new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) +} + +export default function BusRoutingRightTurnLShapePage() { + return ( +
+
+
+
+ Current Bus Solver +
+
+ +
+
+
+
+ Reference Plain Solver +
+
+ +
+
+
+
+ ) +} diff --git a/tests/fixtures/bus-right-turn-l-shape.fixture.ts b/tests/fixtures/bus-right-turn-l-shape.fixture.ts new file mode 100644 index 0000000..bf89040 --- /dev/null +++ b/tests/fixtures/bus-right-turn-l-shape.fixture.ts @@ -0,0 +1,234 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" + +export const BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT = 6 +export const BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE = 4 +export const BUS_RIGHT_TURN_L_SHAPE_BRIDGE_REGION_COUNT = 3 +export const BUS_RIGHT_TURN_L_SHAPE_SPLIT_STAGE_COUNT = 3 + +const START_XS = [-10, -6, -2, 2, 6, 10] as const +const END_XS = [24, 26, 28, 30, 32, 34] as const +const SPLIT_LEFT_XS = [-9, -6, -3, -0.5] as const +const SPLIT_RIGHT_XS = [0.5, 3, 6, 9] as const +const SPLIT_TOP_YS = [-7, -8, -9, -10] as const +const SPLIT_BOTTOM_YS = [-10.5, -11.5, -12.5, -13.5] as const + +const createRegion = ( + regionId: string, + centerX: number, + centerY: number, + width: number, + height: number, + pointIds: string[], +): NonNullable[number] => ({ + regionId, + pointIds, + d: { + center: { x: centerX, y: centerY }, + width, + height, + }, +}) + +const createPort = ( + portId: string, + region1Id: string, + region2Id: string, + x: number, + y: number, +): NonNullable[number] => ({ + portId, + region1Id, + region2Id, + d: { + x, + y, + z: 0, + }, +}) + +const createConnection = ( + routeIndex: number, +): NonNullable[number] => ({ + connectionId: `route-${routeIndex}`, + startRegionId: `start-${routeIndex}`, + endRegionId: `end-${routeIndex}`, + mutuallyConnectedNetworkId: `net-${routeIndex}`, +}) + +const startPortIds = START_XS.map((_, routeIndex) => `start-port-${routeIndex}`) +const bridgeChainPortIds = START_XS.map( + (_, routeIndex) => `bridge-chain-port-${routeIndex}`, +) +const rightChainPortIds = END_XS.map( + (_, routeIndex) => `right-chain-port-${routeIndex}`, +) +const rightExitPortIds = END_XS.map( + (_, routeIndex) => `right-exit-port-${routeIndex}`, +) +const endPortIds = END_XS.map((_, routeIndex) => `end-port-${routeIndex}`) + +const splitATopLeftPortIds = SPLIT_LEFT_XS.map((_, portIndex) => `a-tl-${portIndex}`) +const splitABottomLeftPortIds = SPLIT_LEFT_XS.map( + (_, portIndex) => `a-bl-${portIndex}`, +) +const splitATopRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `a-tr-${portIndex}`, +) +const splitABottomRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `a-br-${portIndex}`, +) +const splitBTopLeftPortIds = SPLIT_LEFT_XS.map((_, portIndex) => `b-tl-${portIndex}`) +const splitBBottomLeftPortIds = SPLIT_LEFT_XS.map( + (_, portIndex) => `b-bl-${portIndex}`, +) +const splitBTopRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `b-tr-${portIndex}`, +) +const splitBBottomRightPortIds = SPLIT_RIGHT_XS.map( + (_, portIndex) => `b-br-${portIndex}`, +) +const splitCTopLeftPortIds = SPLIT_TOP_YS.map((_, portIndex) => `c-tl-${portIndex}`) +const splitCTopRightPortIds = SPLIT_TOP_YS.map( + (_, portIndex) => `c-tr-${portIndex}`, +) +const splitCBottomLeftPortIds = SPLIT_BOTTOM_YS.map( + (_, portIndex) => `c-bl-${portIndex}`, +) +const splitCBottomRightPortIds = SPLIT_BOTTOM_YS.map( + (_, portIndex) => `c-br-${portIndex}`, +) + +export const busRightTurnLShapeFixture: SerializedHyperGraph = { + regions: [ + ...START_XS.flatMap((x, routeIndex) => [ + createRegion(`start-${routeIndex}`, x, 31.5, 1.2, 1.2, [ + `start-port-${routeIndex}`, + ]), + createRegion(`end-${routeIndex}`, END_XS[routeIndex]!, -46.5, 1.2, 1.2, [ + `end-port-${routeIndex}`, + ]), + ]), + createRegion("top-main", 0, 25.5, 24, 3, [ + ...startPortIds, + ...splitATopLeftPortIds, + ...splitATopRightPortIds, + ]), + createRegion("split-a-left", -5.5, 19.5, 8.5, 9, [ + ...splitATopLeftPortIds, + ...splitABottomLeftPortIds, + ]), + createRegion("split-a-right", 5.5, 19.5, 8.5, 9, [ + ...splitATopRightPortIds, + ...splitABottomRightPortIds, + ]), + createRegion("bridge-upper", 0, 12, 28, 6, [ + ...splitABottomLeftPortIds, + ...splitABottomRightPortIds, + ...bridgeChainPortIds, + ]), + createRegion("bridge-lower", 0, 6, 28, 6, [ + ...bridgeChainPortIds, + ...splitBTopLeftPortIds, + ...splitBTopRightPortIds, + ]), + createRegion("split-b-left", -5.5, -1.5, 8.5, 9, [ + ...splitBTopLeftPortIds, + ...splitBBottomLeftPortIds, + ]), + createRegion("split-b-right", 5.5, -1.5, 8.5, 9, [ + ...splitBTopRightPortIds, + ...splitBBottomRightPortIds, + ]), + createRegion("bridge-final", 0, -10, 28, 7, [ + ...splitBBottomLeftPortIds, + ...splitBBottomRightPortIds, + ...splitCTopLeftPortIds, + ...splitCBottomLeftPortIds, + ]), + createRegion("split-c-top", 18.5, -8, 9, 5, [ + ...splitCTopLeftPortIds, + ...splitCTopRightPortIds, + ]), + createRegion("split-c-bottom", 18.5, -12.5, 9, 5, [ + ...splitCBottomLeftPortIds, + ...splitCBottomRightPortIds, + ]), + createRegion("right-main", 29, -18, 12, 22, [ + ...splitCTopRightPortIds, + ...splitCBottomRightPortIds, + ...rightChainPortIds, + ]), + createRegion("right-buffer", 29, -32.5, 12, 7, [ + ...rightChainPortIds, + ...rightExitPortIds, + ]), + createRegion("right-exit", 29, -39.5, 12, 7, [ + ...rightExitPortIds, + ...endPortIds, + ]), + ], + ports: [ + ...START_XS.flatMap((x, routeIndex) => [ + createPort( + `start-port-${routeIndex}`, + `start-${routeIndex}`, + "top-main", + x, + 28.5, + ), + ]), + ...END_XS.flatMap((x, routeIndex) => [ + createPort( + `right-chain-port-${routeIndex}`, + "right-main", + "right-buffer", + x, + -29, + ), + createPort( + `right-exit-port-${routeIndex}`, + "right-buffer", + "right-exit", + x, + -36, + ), + createPort( + `end-port-${routeIndex}`, + "right-exit", + `end-${routeIndex}`, + x, + -43, + ), + ]), + ...START_XS.flatMap((x, routeIndex) => [ + createPort( + `bridge-chain-port-${routeIndex}`, + "bridge-upper", + "bridge-lower", + x, + 8.5, + ), + ]), + ...SPLIT_LEFT_XS.flatMap((x, portIndex) => [ + createPort(`a-tl-${portIndex}`, "top-main", "split-a-left", x, 24), + createPort(`a-bl-${portIndex}`, "split-a-left", "bridge-upper", x, 15), + createPort(`b-tl-${portIndex}`, "bridge-lower", "split-b-left", x, 3), + createPort(`b-bl-${portIndex}`, "split-b-left", "bridge-final", x, -6), + ]), + ...SPLIT_RIGHT_XS.flatMap((x, portIndex) => [ + createPort(`a-tr-${portIndex}`, "top-main", "split-a-right", x, 24), + createPort(`a-br-${portIndex}`, "split-a-right", "bridge-upper", x, 15), + createPort(`b-tr-${portIndex}`, "bridge-lower", "split-b-right", x, 3), + createPort(`b-br-${portIndex}`, "split-b-right", "bridge-final", x, -6), + ]), + ...SPLIT_TOP_YS.flatMap((y, portIndex) => [ + createPort(`c-tl-${portIndex}`, "bridge-final", "split-c-top", 14, y), + createPort(`c-tr-${portIndex}`, "split-c-top", "right-main", 23, y), + ]), + ...SPLIT_BOTTOM_YS.flatMap((y, portIndex) => [ + createPort(`c-bl-${portIndex}`, "bridge-final", "split-c-bottom", 14, y), + createPort(`c-br-${portIndex}`, "split-c-bottom", "right-main", 23, y), + ]), + ], + connections: START_XS.map((_, routeIndex) => createConnection(routeIndex)), +} diff --git a/tests/solver/bus-right-turn-l-shape-repro.test.ts b/tests/solver/bus-right-turn-l-shape-repro.test.ts new file mode 100644 index 0000000..b84fef9 --- /dev/null +++ b/tests/solver/bus-right-turn-l-shape-repro.test.ts @@ -0,0 +1,161 @@ +import { expect, test } from "bun:test" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { TinyHyperGraphBusSolver, TinyHyperGraphSolver } from "lib/index" +import { + BUS_RIGHT_TURN_L_SHAPE_BRIDGE_REGION_COUNT, + BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT, + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + BUS_RIGHT_TURN_L_SHAPE_SPLIT_STAGE_COUNT, + busRightTurnLShapeFixture, +} from "tests/fixtures/bus-right-turn-l-shape.fixture" + +const countSharedPorts = (regionAId: string, regionBId: string) => + busRightTurnLShapeFixture.ports.filter( + (port) => + (port.region1Id === regionAId && port.region2Id === regionBId) || + (port.region1Id === regionBId && port.region2Id === regionAId), + ).length + +const countRoutesUsingRegion = ( + solvedRoutes: NonNullable["solvedRoutes"]>, + regionId: string, +) => + solvedRoutes.filter((route) => + route.path.some((node) => node.nextRegionId === regionId), + ).length + +const getRegionIndexBySerializedId = ( + topology: ReturnType["topology"], +) => { + const regionIndexBySerializedId = new Map() + + topology.regionMetadata?.forEach((metadata, regionIndex) => { + const serializedRegionId = (metadata as { serializedRegionId?: string }) + .serializedRegionId + if (typeof serializedRegionId === "string") { + regionIndexBySerializedId.set(serializedRegionId, regionIndex) + } + }) + + return regionIndexBySerializedId +} + +test("repro: region-19 turns right into an L-shaped top-bottom split", () => { + const { topology, problem } = loadSerializedHyperGraph( + busRightTurnLShapeFixture, + ) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) + + expect(problem.routeCount).toBe(BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT) + expect(BUS_RIGHT_TURN_L_SHAPE_SPLIT_STAGE_COUNT).toBe(3) + expect(BUS_RIGHT_TURN_L_SHAPE_BRIDGE_REGION_COUNT).toBe(3) + expect(regionIndexBySerializedId.get("bridge-final")).toBe(19) + expect(countSharedPorts("top-main", "split-a-left")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("top-main", "split-a-right")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-lower", "split-b-left")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-lower", "split-b-right")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-final", "split-c-top")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("bridge-final", "split-c-bottom")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("split-c-top", "right-main")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(countSharedPorts("split-c-bottom", "right-main")).toBe( + BUS_RIGHT_TURN_L_SHAPE_SHARED_PORTS_PER_SPLIT_EDGE, + ) + expect(problem.routeCount).toBeGreaterThan( + countSharedPorts("bridge-final", "split-c-top"), + ) + + plainSolver.solve() + + expect(plainSolver.solved).toBe(true) + expect(plainSolver.failed).toBe(false) + + const solvedRoutes = plainSolver.getOutput().solvedRoutes ?? [] + + expect(solvedRoutes).toHaveLength(BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT) + expect(countRoutesUsingRegion(solvedRoutes, "bridge-final")).toBe( + BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT, + ) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-a-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-left")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-b-right")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-top")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "split-c-bottom")).toBeGreaterThan(0) + expect(countRoutesUsingRegion(solvedRoutes, "right-main")).toBe( + BUS_RIGHT_TURN_L_SHAPE_ROUTE_COUNT, + ) +}) + +test("repro: bus solver currently fails the region-19 right turn and L-shaped split", () => { + const { topology, problem } = loadSerializedHyperGraph( + busRightTurnLShapeFixture, + ) + const busSolver = new TinyHyperGraphBusSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + const plainSolver = new TinyHyperGraphSolver(topology, problem, { + MAX_ITERATIONS: 100_000, + }) + const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) + + const splitALeftRegionIndex = regionIndexBySerializedId.get("split-a-left") + const splitARightRegionIndex = regionIndexBySerializedId.get("split-a-right") + const splitBLeftRegionIndex = regionIndexBySerializedId.get("split-b-left") + const splitBRightRegionIndex = regionIndexBySerializedId.get("split-b-right") + const splitCTopRegionIndex = regionIndexBySerializedId.get("split-c-top") + const splitCBottomRegionIndex = regionIndexBySerializedId.get("split-c-bottom") + + expect(splitALeftRegionIndex).toBeDefined() + expect(splitARightRegionIndex).toBeDefined() + expect(splitBLeftRegionIndex).toBeDefined() + expect(splitBRightRegionIndex).toBeDefined() + expect(splitCTopRegionIndex).toBeDefined() + expect(splitCBottomRegionIndex).toBeDefined() + expect(regionIndexBySerializedId.get("bridge-final")).toBe(19) + expect( + busSolver.centerGoalHopDistanceByRegion[splitALeftRegionIndex!], + ).toBeGreaterThan(6) + expect( + busSolver.centerGoalHopDistanceByRegion[splitARightRegionIndex!], + ).toBeGreaterThan(6) + expect( + busSolver.centerGoalHopDistanceByRegion[splitBLeftRegionIndex!], + ).toBeGreaterThan(4) + expect( + busSolver.centerGoalHopDistanceByRegion[splitBRightRegionIndex!], + ).toBeGreaterThan(4) + expect( + busSolver.centerGoalHopDistanceByRegion[splitCTopRegionIndex!], + ).toBeGreaterThan(2) + expect( + busSolver.centerGoalHopDistanceByRegion[splitCBottomRegionIndex!], + ).toBeGreaterThan(2) + + busSolver.solve() + plainSolver.solve() + + expect(plainSolver.solved).toBe(true) + expect(plainSolver.failed).toBe(false) + expect(busSolver.solved).toBe(false) + expect(busSolver.failed).toBe(true) + expect(busSolver.error).toBe( + "Failed to infer a complete bus preview from the centerline", + ) +})