diff --git a/lib/bus-solver/BusTraceInferencePlanner.ts b/lib/bus-solver/BusTraceInferencePlanner.ts new file mode 100644 index 0000000..2e362ab --- /dev/null +++ b/lib/bus-solver/BusTraceInferencePlanner.ts @@ -0,0 +1,995 @@ +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< + string, + AlongsideTraceSearchNode + >() + 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 f69c322..63d2248 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" @@ -32,16 +31,12 @@ import { type BoundaryStep, type BusCenterCandidate, type BusPreview, + type PreviewRoutingStateSnapshot, type TinyHyperGraphBusSolverOptions, type TracePreview, type TraceSegment, } from "./busSolverTypes" -import { - getDistanceFromPortToPolyline, - getPortDistance, - getPortProgressAlongPolyline, - getPortProjection, -} from "./geometry" +import { getPortDistance, getPortProjection } from "./geometry" import { clearPreviewRoutingState as clearPreviewRoutingStateValue, getPreviewIntersectionCounts as getPreviewIntersectionCountsValue, @@ -63,6 +58,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 @@ -76,10 +78,15 @@ 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 private lastPreview?: BusPreview + private bestCompleteFallbackPreview?: BusPreview + private bestCompleteFallbackSnapshot?: PreviewRoutingStateSnapshot + private bestCompleteFallbackIntersectionCount = Number.POSITIVE_INFINITY + private bestCompleteFallbackCost = Number.POSITIVE_INFINITY constructor( topology: TinyHyperGraphTopology, @@ -132,6 +139,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, @@ -166,6 +202,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 +246,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 +279,25 @@ 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 +310,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 +332,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 +427,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 +509,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 +597,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 @@ -578,64 +651,14 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { boundaryPortIdsByStep: Array, usedPortOwners: ReadonlyMap, ) { - const centerPortIds = centerPath.map( - (pathCandidate) => pathCandidate.portId, + return this.traceInferencePlanner.buildBestPrefixTracePreview( + traceIndex, + centerPath, + maxSharedStepCount, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, ) - const targetGuideProgress = getPolylineLength(this.topology, centerPortIds) - const minSharedStepCount = 0 - let bestPreview: 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 ( - !bestPreview || - score < bestScore - BUS_CANDIDATE_EPSILON || - (Math.abs(score - bestScore) <= BUS_CANDIDATE_EPSILON && - sharedStepCount > bestSharedStepCount) - ) { - bestPreview = { - ...prefixPreview, - previewCost: score, - } - bestScore = score - bestSharedStepCount = sharedStepCount - } - } - - return bestPreview } private buildCompleteTracePreviewOptions( @@ -654,64 +677,13 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { return centerPreview ? [centerPreview] : [] } - const routeId = this.busTraceOrder.traces[traceIndex]!.routeId - const previewOptions: TracePreview[] = [] - - 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 remainderSegments = this.inferEndRemainderSegments( - traceIndex, - currentPortId, - currentRegionId, - centerPath, - sharedStepCount, - usedPortOwners, - ) - if (!remainderSegments) { - continue - } - - previewOptions.push({ - traceIndex, - routeId, - segments: [...prefixPreview.segments, ...remainderSegments], - complete: true, - terminalPortId: this.problem.routeEndPort[routeId]!, - previewCost: 0, - }) - } - - return previewOptions + return this.traceInferencePlanner.buildCompleteTracePreviewOptions( + traceIndex, + centerPath, + boundarySteps, + boundaryPortIdsByStep, + usedPortOwners, + ) } private buildBestCompleteBusPreview( @@ -923,165 +895,52 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { } } - private inferEndRemainderSegments( - 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 [] + private maybeStoreBestCompleteFallbackPreview( + preview: BusPreview, + totalIntersectionCount: number, + ) { + if ( + totalIntersectionCount > this.bestCompleteFallbackIntersectionCount || + (totalIntersectionCount === this.bestCompleteFallbackIntersectionCount && + preview.totalCost >= this.bestCompleteFallbackCost) + ) { + 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 + 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 + } - 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++ + private tryAcceptBestCompleteFallbackPreview() { + if ( + !this.bestCompleteFallbackPreview || + !this.bestCompleteFallbackSnapshot ) { - 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 false } - return undefined + 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,11 +1347,26 @@ 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 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 - ] + const portHeuristic = this.getRouteHeuristic(this.centerRouteId, portId) if (nextRegionId === undefined) { return portHeuristic 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/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-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/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-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, + ) +}) 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, + ) }) 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", + ) +})