diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts new file mode 100644 index 0000000..0101424 --- /dev/null +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -0,0 +1,1249 @@ +import type { GraphicsObject } from "graphics-debug" +import { MinHeap } from "../MinHeap" +import { + type Candidate, + createEmptyRegionIntersectionCache, + TinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphSolverOptions, + type TinyHyperGraphTopology, +} from "../core" +import type { NetId, PortId, RegionId, RouteId } from "../types" +import { visualizeTinyGraph } from "../visualizeTinyGraph" +import { deriveBusTraceOrder, type BusTraceOrder } from "./deriveBusTraceOrder" +import { + doSegmentsConflict, + getPortDistance, + getPortProjection, + getWeightedDistanceFromPortToPolyline, +} from "./geometry" + +export interface TinyHyperGraphBusSolverOptions + extends TinyHyperGraphSolverOptions { + BUS_TRACE_SEPARATION?: number +} + +interface BusTraceState { + routeId: RouteId + portId: PortId + nextRegionId?: RegionId + atGoal: boolean + prevState?: BusTraceState +} + +interface TraceSearchCandidate { + state: BusTraceState + g: number + h: number + f: number +} + +interface ActiveTraceSearch { + traceIndex: number + routeId: RouteId + candidateQueue: MinHeap + bestCostByTraceState: Map +} + +interface BusSolveState { + phase: "center" | "outer" | "done" + currentOuterTraceCursor: number + centerlinePortIds?: PortId[] + centerlineHasLayerChanges: boolean + reservedPortIds: Set + solvedTraceStates: Array + solvedTraceCosts: Float64Array + activeTraceSearch?: ActiveTraceSearch + lastExpandedCandidate?: TraceSearchCandidate +} + +const BUS_CANDIDATE_EPSILON = 1e-9 + +const compareTraceCandidates = ( + left: TraceSearchCandidate, + right: TraceSearchCandidate, +) => left.f - right.f || left.h - right.h || left.g - right.g + +export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { + BUS_TRACE_SEPARATION = 0.1 + BUS_ALIGNMENT_COST_FACTOR = 0.2 + BUS_HEURISTIC_WEIGHT = 1.5 + BUS_LAYER_DISTANCE_COST = 100 + + readonly busTraceOrder: BusTraceOrder + + private readonly centerTraceIndex: number + private readonly outerTraceIndices: number[] + + private busState: BusSolveState + + constructor( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + options?: TinyHyperGraphBusSolverOptions, + ) { + super(topology, problem, options) + + if (options?.BUS_TRACE_SEPARATION !== undefined) { + this.BUS_TRACE_SEPARATION = options.BUS_TRACE_SEPARATION + } + + this.busTraceOrder = deriveBusTraceOrder(topology, problem) + this.centerTraceIndex = this.busTraceOrder.centerTraceIndex + this.outerTraceIndices = this.busTraceOrder.traces + .map((trace) => trace.orderIndex) + .filter((traceIndex) => traceIndex !== this.centerTraceIndex) + .sort((leftIndex, rightIndex) => { + const leftTrace = this.busTraceOrder.traces[leftIndex]! + const rightTrace = this.busTraceOrder.traces[rightIndex]! + return ( + leftTrace.distanceFromCenter - rightTrace.distanceFromCenter || + leftTrace.signedIndexFromCenter - rightTrace.signedIndexFromCenter + ) + }) + + this.busState = this.createInitialBusState() + this.updateBusStats() + } + + override _setup() { + this.resetCommittedSolution() + void this.problemSetup + this.busState = this.createInitialBusState() + this.updateBusStats() + } + + override _step() { + if (!this.busState.activeTraceSearch) { + this.startNextTraceSearch() + + if (this.solved || this.failed) { + this.updateBusStats() + return + } + } + + const activeTraceSearch = this.busState.activeTraceSearch + if (!activeTraceSearch) { + this.failed = true + this.error = "Failed to start the next bus-trace search" + this.updateBusStats() + return + } + + const currentCandidate = activeTraceSearch.candidateQueue.dequeue() + this.busState.lastExpandedCandidate = currentCandidate + + if (!currentCandidate) { + this.failed = true + this.error = `No path found for bus trace ${this.getTraceConnectionId(activeTraceSearch.traceIndex)}` + this.updateBusStats() + return + } + + const currentBestCost = activeTraceSearch.bestCostByTraceState.get( + this.getTraceStateKey(currentCandidate.state), + ) + if ( + currentBestCost !== undefined && + currentCandidate.g > currentBestCost + BUS_CANDIDATE_EPSILON + ) { + this.updateBusStats(currentCandidate) + return + } + + if (currentCandidate.state.atGoal) { + this.finalizeSolvedTrace( + activeTraceSearch.traceIndex, + currentCandidate.state, + currentCandidate.g, + ) + this.updateBusStats(currentCandidate) + return + } + + for (const move of this.getAvailableTraceMoves(currentCandidate.state)) { + if ( + this.isMoveBlockedByBusConstraints(activeTraceSearch.traceIndex, move) + ) { + continue + } + + let nextG = + currentCandidate.g + move.segmentLength * this.DISTANCE_TO_COST + if (activeTraceSearch.traceIndex !== this.centerTraceIndex) { + nextG += + this.computeTraceAlignmentCost( + activeTraceSearch.traceIndex, + this.busState.centerlinePortIds!, + move.nextState.portId, + ) * this.BUS_ALIGNMENT_COST_FACTOR + } + + const nextH = this.computeTraceHeuristic(move.nextState) + const nextF = + nextG + + nextH * + (activeTraceSearch.traceIndex === this.centerTraceIndex + ? 1 + : this.BUS_HEURISTIC_WEIGHT) + + const nextStateKey = this.getTraceStateKey(move.nextState) + const existingBestCost = + activeTraceSearch.bestCostByTraceState.get(nextStateKey) + + if ( + existingBestCost !== undefined && + nextG >= existingBestCost - BUS_CANDIDATE_EPSILON + ) { + continue + } + + activeTraceSearch.bestCostByTraceState.set(nextStateKey, nextG) + activeTraceSearch.candidateQueue.queue({ + state: move.nextState, + g: nextG, + h: nextH, + f: nextF, + }) + } + + this.updateBusStats(currentCandidate) + } + + override visualize(): GraphicsObject { + const activeRouteId = + this.busState.activeTraceSearch?.routeId ?? + this.busState.lastExpandedCandidate?.state.routeId + + if (this.iterations === 0 || activeRouteId === undefined || this.solved) { + return visualizeTinyGraph(this) + } + + const previousCurrentRouteId = this.state.currentRouteId + const previousCurrentRouteNetId = this.state.currentRouteNetId + const previousGoalPortId = this.state.goalPortId + const previousCandidateQueue = this.state.candidateQueue + const previousUnroutedRoutes = this.state.unroutedRoutes + + try { + this.state.currentRouteId = activeRouteId + this.state.currentRouteNetId = this.problem.routeNet[activeRouteId] + this.state.goalPortId = this.problem.routeEndPort[activeRouteId]! + this.state.candidateQueue = new MinHeap( + this.getVisualizationCandidates(activeRouteId), + (left, right) => + left.f - right.f || left.h - right.h || left.g - right.g, + ) + this.state.unroutedRoutes = + this.getVisualizationUnroutedRouteIds(activeRouteId) + + const graphics = visualizeTinyGraph(this) + this.removeActiveRouteHint(graphics, activeRouteId) + this.pushBusTraceCandidatePaths( + graphics, + this.buildVisualizationBusTraceStates(activeRouteId), + activeRouteId, + ) + this.pushActiveCandidateOverlay(graphics) + return graphics + } finally { + this.state.currentRouteId = previousCurrentRouteId + this.state.currentRouteNetId = previousCurrentRouteNetId + this.state.goalPortId = previousGoalPortId + this.state.candidateQueue = previousCandidateQueue + this.state.unroutedRoutes = previousUnroutedRoutes + } + } + + private createInitialBusState(): BusSolveState { + return { + phase: "center", + currentOuterTraceCursor: 0, + centerlinePortIds: undefined, + centerlineHasLayerChanges: false, + reservedPortIds: new Set(), + solvedTraceStates: Array.from( + { length: this.problem.routeCount }, + () => undefined as BusTraceState | undefined, + ), + solvedTraceCosts: new Float64Array(this.problem.routeCount), + activeTraceSearch: undefined, + lastExpandedCandidate: undefined, + } + } + + private getVisualizationCandidates(activeRouteId: RouteId) { + const candidates: Candidate[] = [] + + if ( + this.busState.lastExpandedCandidate && + this.busState.lastExpandedCandidate.state.routeId === activeRouteId + ) { + candidates.push( + this.convertTraceSearchCandidateToVisualizationCandidate( + this.busState.lastExpandedCandidate, + ), + ) + } + + for (const candidate of this.busState.activeTraceSearch?.candidateQueue.toArray() ?? + []) { + candidates.push( + this.convertTraceSearchCandidateToVisualizationCandidate(candidate), + ) + } + + return candidates + } + + private getVisualizationUnroutedRouteIds(activeRouteId: RouteId) { + const remainingRouteIds: RouteId[] = [] + + for ( + let traceIndex = 0; + traceIndex < this.busTraceOrder.traces.length; + traceIndex++ + ) { + if (this.busState.solvedTraceStates[traceIndex]) { + continue + } + + const routeId = this.busTraceOrder.traces[traceIndex]!.routeId + if (routeId === activeRouteId) { + continue + } + + remainingRouteIds.push(routeId) + } + + return remainingRouteIds + } + + private convertTraceSearchCandidateToVisualizationCandidate( + candidate: TraceSearchCandidate, + ): Candidate { + const visualizationCandidate = + this.convertTraceStateToVisualizationCandidate(candidate.state) + + visualizationCandidate.g = candidate.g + visualizationCandidate.h = candidate.h + visualizationCandidate.f = candidate.f + + return visualizationCandidate + } + + private convertTraceStateToVisualizationCandidate( + traceState: BusTraceState, + ): Candidate { + return { + portId: traceState.portId, + nextRegionId: + traceState.nextRegionId ?? traceState.prevState?.nextRegionId ?? -1, + prevRegionId: traceState.prevState?.nextRegionId, + prevCandidate: traceState.prevState + ? this.convertTraceStateToVisualizationCandidate(traceState.prevState) + : undefined, + g: 0, + h: 0, + f: 0, + } + } + + private getCurrentVisualizationTraceState(routeId: RouteId) { + if (this.busState.lastExpandedCandidate?.state.routeId === routeId) { + return this.busState.lastExpandedCandidate.state + } + + const activeTraceSearch = this.busState.activeTraceSearch + if (!activeTraceSearch || activeTraceSearch.routeId !== routeId) { + return undefined + } + + let bestCandidate: TraceSearchCandidate | undefined + for (const candidate of activeTraceSearch.candidateQueue.toArray()) { + if ( + !bestCandidate || + compareTraceCandidates(candidate, bestCandidate) < 0 + ) { + bestCandidate = candidate + } + } + + return bestCandidate?.state + } + + private buildVisualizationBusTraceStates(activeRouteId: RouteId) { + const traceStates = [...this.busState.solvedTraceStates] + const reservedPortIds = new Set(this.busState.reservedPortIds) + let centerlinePortIds = this.busState.centerlinePortIds + let centerlineHasLayerChanges = this.busState.centerlineHasLayerChanges + + const activeTraceIndex = this.busTraceOrder.traces.findIndex( + (trace) => trace.routeId === activeRouteId, + ) + const activeTraceState = + this.getCurrentVisualizationTraceState(activeRouteId) + + if (activeTraceIndex !== -1 && activeTraceState) { + traceStates[activeTraceIndex] = activeTraceState + + const activePathPortIds = this.getTracePathPortIds(activeTraceState) + for (const portId of activePathPortIds) { + reservedPortIds.add(portId) + } + + if (activeTraceIndex === this.centerTraceIndex) { + centerlinePortIds = activePathPortIds + centerlineHasLayerChanges = this.doesPathChangeLayers(activePathPortIds) + } + } + + for (const traceIndex of [ + this.centerTraceIndex, + ...this.outerTraceIndices, + ]) { + if (traceStates[traceIndex]) { + continue + } + + if (traceIndex !== this.centerTraceIndex && !centerlinePortIds) { + continue + } + + const previewTraceState = this.solveVisualizationTracePath(traceIndex, { + reservedPortIds, + centerlinePortIds, + centerlineHasLayerChanges, + }) + + if (!previewTraceState) { + continue + } + + traceStates[traceIndex] = previewTraceState + + const previewPathPortIds = this.getTracePathPortIds(previewTraceState) + for (const portId of previewPathPortIds) { + reservedPortIds.add(portId) + } + + if (traceIndex === this.centerTraceIndex) { + centerlinePortIds = previewPathPortIds + centerlineHasLayerChanges = + this.doesPathChangeLayers(previewPathPortIds) + } + } + + return traceStates + } + + private solveVisualizationTracePath( + traceIndex: number, + options: { + reservedPortIds: ReadonlySet + centerlinePortIds?: readonly PortId[] + centerlineHasLayerChanges: boolean + }, + ) { + const routeId = this.busTraceOrder.traces[traceIndex]!.routeId + const startState = this.createStartingTraceState(routeId) + const startPortId = this.problem.routeStartPort[routeId]! + const goalPortId = this.problem.routeEndPort[routeId]! + + if ( + traceIndex !== this.centerTraceIndex && + (options.reservedPortIds.has(startPortId) || + options.reservedPortIds.has(goalPortId)) + ) { + return undefined + } + + if (startState.atGoal) { + return startState + } + + const candidateQueue = new MinHeap( + [], + compareTraceCandidates, + ) + const bestCostByTraceState = new Map() + const startH = this.computeTraceHeuristic(startState) + + candidateQueue.queue({ + state: startState, + g: 0, + h: startH, + f: + startH * + (traceIndex === this.centerTraceIndex ? 1 : this.BUS_HEURISTIC_WEIGHT), + }) + bestCostByTraceState.set(this.getTraceStateKey(startState), 0) + + while (candidateQueue.length > 0) { + const currentCandidate = candidateQueue.dequeue() + if (!currentCandidate) { + break + } + + const currentBestCost = bestCostByTraceState.get( + this.getTraceStateKey(currentCandidate.state), + ) + if ( + currentBestCost !== undefined && + currentCandidate.g > currentBestCost + BUS_CANDIDATE_EPSILON + ) { + continue + } + + if (currentCandidate.state.atGoal) { + return currentCandidate.state + } + + for (const move of this.getAvailableTraceMoves(currentCandidate.state)) { + if ( + this.isMoveBlockedByBusConstraints(traceIndex, move, { + reservedPortIds: options.reservedPortIds, + centerlinePortIds: options.centerlinePortIds, + centerlineHasLayerChanges: options.centerlineHasLayerChanges, + }) + ) { + continue + } + + let nextG = + currentCandidate.g + move.segmentLength * this.DISTANCE_TO_COST + if ( + traceIndex !== this.centerTraceIndex && + options.centerlinePortIds && + options.centerlinePortIds.length > 0 + ) { + nextG += + this.computeTraceAlignmentCost( + traceIndex, + options.centerlinePortIds, + move.nextState.portId, + ) * this.BUS_ALIGNMENT_COST_FACTOR + } + + const nextH = this.computeTraceHeuristic(move.nextState) + const nextF = + nextG + + nextH * + (traceIndex === this.centerTraceIndex + ? 1 + : this.BUS_HEURISTIC_WEIGHT) + + const nextStateKey = this.getTraceStateKey(move.nextState) + const existingBestCost = bestCostByTraceState.get(nextStateKey) + + if ( + existingBestCost !== undefined && + nextG >= existingBestCost - BUS_CANDIDATE_EPSILON + ) { + continue + } + + bestCostByTraceState.set(nextStateKey, nextG) + candidateQueue.queue({ + state: move.nextState, + g: nextG, + h: nextH, + f: nextF, + }) + } + } + + return undefined + } + + private startNextTraceSearch() { + while (!this.busState.activeTraceSearch && !this.solved && !this.failed) { + if (this.busState.phase === "center") { + this.initializeTraceSearch(this.centerTraceIndex) + return + } + + if (this.busState.phase === "outer") { + if ( + this.busState.currentOuterTraceCursor >= this.outerTraceIndices.length + ) { + this.busState.phase = "done" + this.solved = true + return + } + + const traceIndex = + this.outerTraceIndices[this.busState.currentOuterTraceCursor]! + this.initializeTraceSearch(traceIndex) + return + } + + if (this.busState.phase === "done") { + this.solved = true + return + } + } + } + + private initializeTraceSearch(traceIndex: number) { + const routeId = this.busTraceOrder.traces[traceIndex]!.routeId + const startState = this.createStartingTraceState(routeId) + const startPortId = this.problem.routeStartPort[routeId]! + const goalPortId = this.problem.routeEndPort[routeId]! + + if ( + traceIndex !== this.centerTraceIndex && + (this.busState.reservedPortIds.has(startPortId) || + this.busState.reservedPortIds.has(goalPortId)) + ) { + this.failed = true + this.error = `Bus trace ${this.getTraceConnectionId(traceIndex)} starts or ends on a reserved port` + return + } + + if (startState.atGoal) { + this.finalizeSolvedTrace(traceIndex, startState, 0) + return + } + + const candidateQueue = new MinHeap( + [], + compareTraceCandidates, + ) + const bestCostByTraceState = new Map() + const startH = this.computeTraceHeuristic(startState) + + candidateQueue.queue({ + state: startState, + g: 0, + h: startH, + f: + startH * + (traceIndex === this.centerTraceIndex ? 1 : this.BUS_HEURISTIC_WEIGHT), + }) + bestCostByTraceState.set(this.getTraceStateKey(startState), 0) + + this.busState.activeTraceSearch = { + traceIndex, + routeId, + candidateQueue, + bestCostByTraceState, + } + } + + private resetCommittedSolution() { + const { topology, state } = this + + state.portAssignment.fill(-1) + state.regionSegments = Array.from( + { length: topology.regionCount }, + () => [], + ) + state.regionIntersectionCaches = Array.from( + { length: topology.regionCount }, + () => createEmptyRegionIntersectionCache(), + ) + state.currentRouteId = undefined + state.currentRouteNetId = undefined + state.unroutedRoutes = [] + state.candidateQueue.clear() + state.goalPortId = -1 + state.ripCount = 0 + state.regionCongestionCost.fill(0) + } + + private createStartingTraceState(routeId: RouteId): BusTraceState { + const startPortId = this.problem.routeStartPort[routeId]! + const goalPortId = this.problem.routeEndPort[routeId]! + + if (startPortId === goalPortId) { + return { + routeId, + portId: startPortId, + atGoal: true, + } + } + + const nextRegionId = this.getStartingNextRegionId(routeId, startPortId) + if (nextRegionId === undefined) { + throw new Error(`Bus route ${routeId} has no incident start region`) + } + + return { + routeId, + portId: startPortId, + nextRegionId, + atGoal: false, + } + } + + private getAvailableTraceMoves(traceState: BusTraceState) { + if (traceState.atGoal || traceState.nextRegionId === undefined) { + return [] as Array<{ + nextState: BusTraceState + segmentLength: number + }> + } + + const routeId = traceState.routeId + const goalPortId = this.problem.routeEndPort[routeId]! + const currentNetId = this.problem.routeNet[routeId]! + const currentRegionId = traceState.nextRegionId + const moves: Array<{ + nextState: BusTraceState + segmentLength: number + }> = [] + + for (const neighborPortId of this.topology.regionIncidentPorts[ + currentRegionId + ] ?? []) { + if (neighborPortId === traceState.portId) { + continue + } + + if (this.problem.portSectionMask[neighborPortId] === 0) { + continue + } + + if (this.isPortReservedForDifferentBusNet(currentNetId, neighborPortId)) { + continue + } + + const segmentLength = getPortDistance( + this.topology, + traceState.portId, + neighborPortId, + ) + + if (neighborPortId === goalPortId) { + moves.push({ + nextState: { + routeId, + portId: goalPortId, + atGoal: true, + prevState: traceState, + }, + segmentLength, + }) + continue + } + + const nextRegionId = + this.topology.incidentPortRegion[neighborPortId]?.[0] === + currentRegionId + ? this.topology.incidentPortRegion[neighborPortId]?.[1] + : this.topology.incidentPortRegion[neighborPortId]?.[0] + + if ( + nextRegionId === undefined || + this.isRegionReservedForDifferentBusNet(currentNetId, nextRegionId) + ) { + continue + } + + moves.push({ + nextState: { + routeId, + portId: neighborPortId, + nextRegionId, + atGoal: false, + prevState: traceState, + }, + segmentLength, + }) + } + + return moves + } + + private isMoveBlockedByBusConstraints( + traceIndex: number, + move: { nextState: BusTraceState; segmentLength: number }, + options?: { + reservedPortIds?: ReadonlySet + centerlinePortIds?: readonly PortId[] + centerlineHasLayerChanges?: boolean + }, + ) { + const reservedPortIds = + options?.reservedPortIds ?? this.busState.reservedPortIds + if (reservedPortIds.has(move.nextState.portId)) { + return true + } + + if (traceIndex === this.centerTraceIndex) { + return false + } + + if ( + !( + options?.centerlineHasLayerChanges ?? + this.busState.centerlineHasLayerChanges + ) && + this.doesMoveChangeLayers(move) + ) { + return true + } + + const centerlinePortIds = + options?.centerlinePortIds ?? this.busState.centerlinePortIds + if (!centerlinePortIds || centerlinePortIds.length === 0) { + return true + } + + const centerPortId = centerlinePortIds[centerlinePortIds.length - 1] + const traceMetadata = this.busTraceOrder.traces[traceIndex]! + const fromPortId = move.nextState.prevState?.portId + + if (centerPortId === undefined || fromPortId === undefined) { + return true + } + + if ( + !this.isPortOnExpectedSideOfCenterline( + traceMetadata.signedIndexFromCenter, + centerPortId, + move.nextState.portId, + ) + ) { + return true + } + + return this.doesTraceMoveIntersectCenterline( + fromPortId, + move.nextState.portId, + centerlinePortIds, + ) + } + + private isPortOnExpectedSideOfCenterline( + signedIndexFromCenter: number, + centerPortId: PortId, + candidatePortId: PortId, + ) { + if (signedIndexFromCenter === 0) { + return true + } + + const centerProjection = getPortProjection( + this.topology, + centerPortId, + this.busTraceOrder.normalX, + this.busTraceOrder.normalY, + ) + const candidateProjection = getPortProjection( + this.topology, + candidatePortId, + this.busTraceOrder.normalX, + this.busTraceOrder.normalY, + ) + + return signedIndexFromCenter < 0 + ? candidateProjection <= centerProjection + BUS_CANDIDATE_EPSILON + : candidateProjection >= centerProjection - BUS_CANDIDATE_EPSILON + } + + private doesTraceMoveIntersectCenterline( + fromPortId: PortId, + toPortId: PortId, + centerlinePortIds: readonly PortId[], + ) { + for (let index = 1; index < centerlinePortIds.length; index++) { + if ( + doSegmentsConflict( + this.topology, + fromPortId, + toPortId, + centerlinePortIds[index - 1]!, + centerlinePortIds[index]!, + ) + ) { + return true + } + } + + return false + } + + private computeTraceAlignmentCost( + traceIndex: number, + centerlinePortIds: readonly PortId[], + candidatePortId: PortId, + ) { + const traceMetadata = this.busTraceOrder.traces[traceIndex]! + const targetDistance = + this.BUS_TRACE_SEPARATION * traceMetadata.distanceFromCenter + const actualDistance = getWeightedDistanceFromPortToPolyline( + this.topology, + candidatePortId, + centerlinePortIds, + this.BUS_LAYER_DISTANCE_COST, + ) + + return Math.abs(actualDistance - targetDistance) + } + + private doesMoveChangeLayers(move: { + nextState: BusTraceState + segmentLength: number + }) { + const fromPortId = move.nextState.prevState?.portId + if (fromPortId === undefined) { + return false + } + + return ( + this.topology.portZ[fromPortId] !== + this.topology.portZ[move.nextState.portId] + ) + } + + private doesPathChangeLayers(portIds: readonly PortId[]) { + for (let index = 1; index < portIds.length; index++) { + if ( + this.topology.portZ[portIds[index - 1]!] !== + this.topology.portZ[portIds[index]!] + ) { + return true + } + } + + return false + } + + private computeTraceHeuristic(traceState: BusTraceState) { + if (traceState.atGoal) { + return 0 + } + + return this.problemSetup.portHCostToEndOfRoute[ + traceState.portId * this.problem.routeCount + traceState.routeId + ] + } + + private isPortReservedForDifferentBusNet( + currentNetId: NetId, + portId: PortId, + ) { + const reservedNetIds = this.problemSetup.portEndpointNetIds[portId] + if (!reservedNetIds) { + return false + } + + for (const reservedNetId of reservedNetIds) { + if (reservedNetId !== currentNetId) { + return true + } + } + + return false + } + + private isRegionReservedForDifferentBusNet( + currentNetId: NetId, + regionId: RegionId, + ) { + const reservedNetId = this.problem.regionNetId[regionId] + return reservedNetId !== -1 && reservedNetId !== currentNetId + } + + private getTracePathPortIds(traceState: BusTraceState) { + const portIds: PortId[] = [] + let cursor: BusTraceState | undefined = traceState + + while (cursor) { + portIds.unshift(cursor.portId) + cursor = cursor.prevState + } + + return portIds + } + + private getTraceSegments(traceState: BusTraceState) { + const pathStates: BusTraceState[] = [] + let cursor: BusTraceState | undefined = traceState + + while (cursor) { + pathStates.unshift(cursor) + cursor = cursor.prevState + } + + const segments: Array<{ + regionId: RegionId + fromPortId: PortId + toPortId: PortId + }> = [] + + for (let index = 1; index < pathStates.length; index++) { + const previousState = pathStates[index - 1]! + const currentState = pathStates[index]! + + if (previousState.nextRegionId === undefined) { + throw new Error( + `Bus route ${traceState.routeId} is missing a region before port ${currentState.portId}`, + ) + } + + segments.push({ + regionId: previousState.nextRegionId, + fromPortId: previousState.portId, + toPortId: currentState.portId, + }) + } + + return segments + } + + private commitSolvedTrace(traceState: BusTraceState) { + const routeId = traceState.routeId + const routeNetId = this.problem.routeNet[routeId]! + const previousRouteId = this.state.currentRouteId + const previousRouteNetId = this.state.currentRouteNetId + + this.state.currentRouteId = routeId + this.state.currentRouteNetId = routeNetId + + for (const segment of this.getTraceSegments(traceState)) { + this.state.regionSegments[segment.regionId]!.push([ + routeId, + segment.fromPortId, + segment.toPortId, + ]) + this.state.portAssignment[segment.fromPortId] = routeNetId + this.state.portAssignment[segment.toPortId] = routeNetId + this.appendSegmentToRegionCache( + segment.regionId, + segment.fromPortId, + segment.toPortId, + ) + } + + this.state.currentRouteId = previousRouteId + this.state.currentRouteNetId = previousRouteNetId + } + + private getPortRenderPoint(portId: PortId) { + const layerOffset = this.topology.portZ[portId] * 0.002 + + return { + x: this.topology.portX[portId] + layerOffset, + y: this.topology.portY[portId] + layerOffset, + } + } + + private getVisualizationRouteColor(routeId: RouteId, alpha = 0.8) { + const routeNet = this.problem.routeNet[routeId] + const routeLabel = + this.problem.routeMetadata?.[routeId]?.connectionId ?? `route-${routeId}` + const hashSource = `${routeNet}:${routeLabel}` + + let hash = 0 + for (let index = 0; index < hashSource.length; index++) { + hash = hashSource.charCodeAt(index) * 17777 + ((hash << 5) - hash) + } + + const hue = Math.abs(hash) % 360 + return `hsla(${hue}, 70%, 50%, ${alpha})` + } + + private removeActiveRouteHint( + graphics: GraphicsObject, + activeRouteId: RouteId, + ) { + const activeRouteLabel = + this.problem.routeMetadata?.[activeRouteId]?.connectionId ?? + `route-${activeRouteId}` + + graphics.lines = (graphics.lines ?? []).filter( + (line) => + !(line.strokeDash === "10 5" && line.label === activeRouteLabel), + ) + } + + private pushBusTraceCandidatePaths( + graphics: GraphicsObject, + traceStates: Array, + activeRouteId: RouteId, + ) { + graphics.lines ??= [] + graphics.points ??= [] + + for ( + let traceIndex = 0; + traceIndex < this.busTraceOrder.traces.length; + traceIndex++ + ) { + const trace = this.busTraceOrder.traces[traceIndex]! + const traceState = traceStates[traceIndex] + + if (trace.routeId === activeRouteId) { + continue + } + + if (this.busState.solvedTraceStates[traceIndex] || !traceState) { + continue + } + + const pathPortIds = this.getTracePathPortIds(traceState) + if (pathPortIds.length < 2) { + continue + } + + const pathPoints = pathPortIds.map((portId) => + this.getPortRenderPoint(portId), + ) + const finalPortId = pathPortIds[pathPortIds.length - 1]! + const routeColor = this.getVisualizationRouteColor(trace.routeId, 0.7) + + graphics.lines.push({ + points: pathPoints, + strokeColor: routeColor, + strokeDash: "4 2", + label: ["candidate path", `route: ${trace.connectionId}`].join("\n"), + }) + + graphics.points.push({ + ...this.getPortRenderPoint(finalPortId), + color: this.getVisualizationRouteColor(trace.routeId, 1), + label: [ + "candidate path", + `route: ${trace.connectionId}`, + `port: ${finalPortId}`, + ].join("\n"), + }) + } + } + + private pushActiveCandidateOverlay(graphics: GraphicsObject) { + const activeCandidate = this.busState.lastExpandedCandidate + if (!activeCandidate) { + return + } + + const activeRouteConnectionId = + this.problem.routeMetadata?.[activeCandidate.state.routeId] + ?.connectionId ?? `route-${activeCandidate.state.routeId}` + + const activePathPoints: Array<{ x: number; y: number }> = [] + let cursor: BusTraceState | undefined = activeCandidate.state + + while (cursor) { + activePathPoints.unshift(this.getPortRenderPoint(cursor.portId)) + cursor = cursor.prevState + } + + if (activePathPoints.length > 1) { + graphics.lines ??= [] + graphics.lines.push({ + points: activePathPoints, + strokeColor: "rgba(16, 185, 129, 0.95)", + label: [ + "active candidate", + `route: ${activeRouteConnectionId}`, + `g: ${activeCandidate.g.toFixed(2)}`, + `h: ${activeCandidate.h.toFixed(2)}`, + `f: ${activeCandidate.f.toFixed(2)}`, + ].join("\n"), + }) + } + + graphics.points ??= [] + graphics.points.push({ + ...this.getPortRenderPoint(activeCandidate.state.portId), + color: "rgba(16, 185, 129, 1)", + label: [ + "active candidate", + `route: ${activeRouteConnectionId}`, + `port: ${activeCandidate.state.portId}`, + `g: ${activeCandidate.g.toFixed(2)}`, + `h: ${activeCandidate.h.toFixed(2)}`, + `f: ${activeCandidate.f.toFixed(2)}`, + ].join("\n"), + }) + } + + private finalizeSolvedTrace( + traceIndex: number, + traceState: BusTraceState, + traceCost: number, + ) { + this.busState.solvedTraceStates[traceIndex] = traceState + this.busState.solvedTraceCosts[traceIndex] = traceCost + this.busState.activeTraceSearch = undefined + this.commitSolvedTrace(traceState) + + const pathPortIds = this.getTracePathPortIds(traceState) + for (const portId of pathPortIds) { + this.busState.reservedPortIds.add(portId) + } + + if (traceIndex === this.centerTraceIndex) { + this.busState.centerlinePortIds = pathPortIds + this.busState.centerlineHasLayerChanges = + this.doesPathChangeLayers(pathPortIds) + if (this.outerTraceIndices.length === 0) { + this.busState.phase = "done" + this.solved = true + return + } + + this.busState.phase = "outer" + return + } + + this.busState.currentOuterTraceCursor += 1 + if ( + this.busState.currentOuterTraceCursor >= this.outerTraceIndices.length + ) { + this.busState.phase = "done" + this.solved = true + } + } + + private getTraceStateKey(traceState: BusTraceState) { + return [ + traceState.routeId, + traceState.portId, + traceState.nextRegionId ?? -1, + traceState.atGoal ? 1 : 0, + ].join(":") + } + + private getTraceConnectionId(traceIndex: number) { + return ( + this.busTraceOrder.traces[traceIndex]?.connectionId ?? + `trace-${traceIndex}` + ) + } + + private getSolvedBusCost() { + let totalCost = 0 + for (const traceCost of this.busState.solvedTraceCosts) { + totalCost += traceCost + } + return totalCost + } + + private updateBusStats(currentCandidate?: TraceSearchCandidate) { + const activeTraceSearch = this.busState.activeTraceSearch + this.stats = { + ...this.stats, + routeCount: this.problem.routeCount, + busCenterConnectionId: + this.busTraceOrder.traces[this.centerTraceIndex]?.connectionId, + busPhase: this.busState.phase, + currentTraceIndex: activeTraceSearch?.traceIndex, + currentTraceConnectionId: + activeTraceSearch !== undefined + ? this.getTraceConnectionId(activeTraceSearch.traceIndex) + : undefined, + busCandidateCount: activeTraceSearch?.bestCostByTraceState.size ?? 0, + openCandidateCount: activeTraceSearch?.candidateQueue.length ?? 0, + solvedTraceCount: this.busState.solvedTraceStates.filter(Boolean).length, + currentBusCost: this.getSolvedBusCost(), + currentTraceCost: currentCandidate?.g, + currentTraceHeuristic: currentCandidate?.h, + } + } +} diff --git a/lib/bus-solver/deriveBusTraceOrder.ts b/lib/bus-solver/deriveBusTraceOrder.ts new file mode 100644 index 0000000..8c4c326 --- /dev/null +++ b/lib/bus-solver/deriveBusTraceOrder.ts @@ -0,0 +1,134 @@ +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "../core" +import type { RouteId } from "../types" + +export interface OrderedBusTrace { + routeId: RouteId + orderIndex: number + signedIndexFromCenter: number + distanceFromCenter: number + score: number + connectionId: string +} + +export interface BusTraceOrder { + traces: OrderedBusTrace[] + centerTraceIndex: number + centerTraceRouteId: RouteId + normalX: number + normalY: number +} + +const EPSILON = 1e-9 + +const getConnectionId = ( + problem: TinyHyperGraphProblem, + routeId: RouteId, +): string => { + const metadata = problem.routeMetadata?.[routeId] + return typeof metadata?.connectionId === "string" + ? metadata.connectionId + : `route-${routeId}` +} + +export const deriveBusTraceOrder = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, +): BusTraceOrder => { + if (problem.routeCount === 0) { + throw new Error("Bus solver requires at least one route") + } + + let startCenterX = 0 + let startCenterY = 0 + let endCenterX = 0 + let endCenterY = 0 + + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + const startPortId = problem.routeStartPort[routeId]! + const endPortId = problem.routeEndPort[routeId]! + startCenterX += topology.portX[startPortId] + startCenterY += topology.portY[startPortId] + endCenterX += topology.portX[endPortId] + endCenterY += topology.portY[endPortId] + } + + startCenterX /= problem.routeCount + startCenterY /= problem.routeCount + endCenterX /= problem.routeCount + endCenterY /= problem.routeCount + + let directionX = endCenterX - startCenterX + let directionY = endCenterY - startCenterY + const directionLength = Math.hypot(directionX, directionY) + + if (directionLength <= EPSILON) { + directionX = 1 + directionY = 0 + } else { + directionX /= directionLength + directionY /= directionLength + } + + const normalX = -directionY + const normalY = directionX + + const rawTraceScores = Array.from( + { length: problem.routeCount }, + (_, routeId) => { + const startPortId = problem.routeStartPort[routeId]! + const endPortId = problem.routeEndPort[routeId]! + const startProjection = + (topology.portX[startPortId] - startCenterX) * normalX + + (topology.portY[startPortId] - startCenterY) * normalY + const endProjection = + (topology.portX[endPortId] - endCenterX) * normalX + + (topology.portY[endPortId] - endCenterY) * normalY + + return { + routeId, + connectionId: getConnectionId(problem, routeId), + startProjection, + endProjection, + } + }, + ) + + const endpointCorrelation = rawTraceScores.reduce( + (sum, trace) => sum + trace.startProjection * trace.endProjection, + 0, + ) + const endProjectionMultiplier = endpointCorrelation < 0 ? -1 : 1 + + const orderedScores = rawTraceScores + .map((trace) => ({ + routeId: trace.routeId, + connectionId: trace.connectionId, + score: + (trace.startProjection + + trace.endProjection * endProjectionMultiplier) / + 2, + })) + .sort( + (left, right) => + left.score - right.score || + left.connectionId.localeCompare(right.connectionId), + ) + + const centerTraceIndex = Math.floor((orderedScores.length - 1) / 2) + const traces: OrderedBusTrace[] = orderedScores.map((trace, orderIndex) => ({ + routeId: trace.routeId, + orderIndex, + signedIndexFromCenter: orderIndex - centerTraceIndex, + distanceFromCenter: Math.abs(orderIndex - centerTraceIndex), + score: trace.score, + connectionId: trace.connectionId, + })) + + return { + traces, + centerTraceIndex, + centerTraceRouteId: traces[centerTraceIndex]!.routeId, + normalX, + normalY, + } +} diff --git a/lib/bus-solver/geometry.ts b/lib/bus-solver/geometry.ts new file mode 100644 index 0000000..4e28f0a --- /dev/null +++ b/lib/bus-solver/geometry.ts @@ -0,0 +1,253 @@ +import type { TinyHyperGraphTopology } from "../core" +import type { PortId } from "../types" + +const EPSILON = 1e-9 + +const getPortPoint = (topology: TinyHyperGraphTopology, portId: PortId) => ({ + x: topology.portX[portId], + y: topology.portY[portId], + z: topology.portZ[portId], +}) + +const orientation = ( + ax: number, + ay: number, + bx: number, + by: number, + cx: number, + cy: number, +) => (bx - ax) * (cy - ay) - (by - ay) * (cx - ax) + +const hasOppositeSigns = (left: number, right: number) => + (left > EPSILON && right < -EPSILON) || (left < -EPSILON && right > EPSILON) + +export const doSegmentsIntersectInXY = ( + topology: TinyHyperGraphTopology, + portAId: PortId, + portBId: PortId, + portCId: PortId, + portDId: PortId, +) => { + const portA = getPortPoint(topology, portAId) + const portB = getPortPoint(topology, portBId) + const portC = getPortPoint(topology, portCId) + const portD = getPortPoint(topology, portDId) + + const abC = orientation(portA.x, portA.y, portB.x, portB.y, portC.x, portC.y) + const abD = orientation(portA.x, portA.y, portB.x, portB.y, portD.x, portD.y) + const cdA = orientation(portC.x, portC.y, portD.x, portD.y, portA.x, portA.y) + const cdB = orientation(portC.x, portC.y, portD.x, portD.y, portB.x, portB.y) + + return hasOppositeSigns(abC, abD) && hasOppositeSigns(cdA, cdB) +} + +export const doSegmentsOverlapInZ = ( + topology: TinyHyperGraphTopology, + portAId: PortId, + portBId: PortId, + portCId: PortId, + portDId: PortId, +) => { + const zA1 = topology.portZ[portAId] + const zA2 = topology.portZ[portBId] + const zB1 = topology.portZ[portCId] + const zB2 = topology.portZ[portDId] + + return zA1 === zB1 || zA1 === zB2 || zA2 === zB1 || zA2 === zB2 +} + +export const doSegmentsConflict = ( + topology: TinyHyperGraphTopology, + portAId: PortId, + portBId: PortId, + portCId: PortId, + portDId: PortId, +) => + doSegmentsOverlapInZ(topology, portAId, portBId, portCId, portDId) && + doSegmentsIntersectInXY(topology, portAId, portBId, portCId, portDId) + +export const getPortProjection = ( + topology: TinyHyperGraphTopology, + portId: PortId, + normalX: number, + normalY: number, +) => topology.portX[portId] * normalX + topology.portY[portId] * normalY + +export const getPortDistance = ( + topology: TinyHyperGraphTopology, + fromPortId: PortId, + toPortId: PortId, +) => + Math.hypot( + topology.portX[fromPortId] - topology.portX[toPortId], + topology.portY[fromPortId] - topology.portY[toPortId], + ) + +const getPointToPointDistance = ( + x1: number, + y1: number, + x2: number, + y2: number, +) => Math.hypot(x1 - x2, y1 - y2) + +const getPointToPointDistance3d = ( + x1: number, + y1: number, + z1: number, + x2: number, + y2: number, + z2: number, +) => Math.hypot(x1 - x2, y1 - y2, z1 - z2) + +const getPointToSegmentDistance = ( + px: number, + py: number, + ax: number, + ay: number, + bx: number, + by: number, +) => { + const abX = bx - ax + const abY = by - ay + const abLengthSquared = abX * abX + abY * abY + + if (abLengthSquared <= EPSILON) { + return getPointToPointDistance(px, py, ax, ay) + } + + const t = Math.max( + 0, + Math.min(1, ((px - ax) * abX + (py - ay) * abY) / abLengthSquared), + ) + const projectionX = ax + abX * t + const projectionY = ay + abY * t + + return getPointToPointDistance(px, py, projectionX, projectionY) +} + +const getPointToSegmentDistance3d = ( + px: number, + py: number, + pz: number, + ax: number, + ay: number, + az: number, + bx: number, + by: number, + bz: number, +) => { + const abX = bx - ax + const abY = by - ay + const abZ = bz - az + const abLengthSquared = abX * abX + abY * abY + abZ * abZ + + if (abLengthSquared <= EPSILON) { + return getPointToPointDistance3d(px, py, pz, ax, ay, az) + } + + const t = Math.max( + 0, + Math.min( + 1, + ((px - ax) * abX + (py - ay) * abY + (pz - az) * abZ) / abLengthSquared, + ), + ) + const projectionX = ax + abX * t + const projectionY = ay + abY * t + const projectionZ = az + abZ * t + + return getPointToPointDistance3d( + px, + py, + pz, + projectionX, + projectionY, + projectionZ, + ) +} + +export const getDistanceFromPortToPolyline = ( + topology: TinyHyperGraphTopology, + portId: PortId, + polylinePortIds: readonly PortId[], +) => { + if (polylinePortIds.length === 0) { + return Number.POSITIVE_INFINITY + } + + const point = getPortPoint(topology, portId) + + if (polylinePortIds.length === 1) { + const anchor = getPortPoint(topology, polylinePortIds[0]!) + return getPointToPointDistance(point.x, point.y, anchor.x, anchor.y) + } + + let bestDistance = Number.POSITIVE_INFINITY + + for (let index = 1; index < polylinePortIds.length; index++) { + const start = getPortPoint(topology, polylinePortIds[index - 1]!) + const end = getPortPoint(topology, polylinePortIds[index]!) + bestDistance = Math.min( + bestDistance, + getPointToSegmentDistance( + point.x, + point.y, + start.x, + start.y, + end.x, + end.y, + ), + ) + } + + return bestDistance +} + +export const getWeightedDistanceFromPortToPolyline = ( + topology: TinyHyperGraphTopology, + portId: PortId, + polylinePortIds: readonly PortId[], + zDistanceScale: number, +) => { + if (polylinePortIds.length === 0) { + return Number.POSITIVE_INFINITY + } + + const point = getPortPoint(topology, portId) + const pointZ = point.z * zDistanceScale + + if (polylinePortIds.length === 1) { + const anchor = getPortPoint(topology, polylinePortIds[0]!) + return getPointToPointDistance3d( + point.x, + point.y, + pointZ, + anchor.x, + anchor.y, + anchor.z * zDistanceScale, + ) + } + + let bestDistance = Number.POSITIVE_INFINITY + + for (let index = 1; index < polylinePortIds.length; index++) { + const start = getPortPoint(topology, polylinePortIds[index - 1]!) + const end = getPortPoint(topology, polylinePortIds[index]!) + bestDistance = Math.min( + bestDistance, + getPointToSegmentDistance3d( + point.x, + point.y, + pointZ, + start.x, + start.y, + start.z * zDistanceScale, + end.x, + end.y, + end.z * zDistanceScale, + ), + ) + } + + return bestDistance +} diff --git a/lib/bus-solver/index.ts b/lib/bus-solver/index.ts new file mode 100644 index 0000000..3f5b13a --- /dev/null +++ b/lib/bus-solver/index.ts @@ -0,0 +1,5 @@ +export { + TinyHyperGraphBusSolver, + type TinyHyperGraphBusSolverOptions, +} from "./TinyHyperGraphBusSolver" +export { deriveBusTraceOrder, type BusTraceOrder } from "./deriveBusTraceOrder" diff --git a/lib/index.ts b/lib/index.ts index 8eafe66..e637d68 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,5 @@ export * from "./core" +export * from "./bus-solver" export * from "./region-graph" export { convertPortPointPathingSolverInputToSerializedHyperGraph } from "./compat/convertPortPointPathingSolverInputToSerializedHyperGraph" export { diff --git a/pages/cm5io/cm5io-bus-router-bus1.page.tsx b/pages/cm5io/cm5io-bus-router-bus1.page.tsx new file mode 100644 index 0000000..9b5c174 --- /dev/null +++ b/pages/cm5io/cm5io-bus-router-bus1.page.tsx @@ -0,0 +1,131 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { + filterPortPointPathingSolverInputByConnectionPatches, + TinyHyperGraphBusSolver, + type ConnectionPatchSelection, +} from "lib/index" +import { + convertPortPointPathingSolverInputToSerializedHyperGraph, + type SerializedHyperGraphPortPointPathingSolverInput, +} from "lib/compat/convertPortPointPathingSolverInputToSerializedHyperGraph" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { useEffect, useState } from "react" +import { Debugger } from "../components/Debugger" + +const cm5ioHyperGraphFixtureUrl = new URL( + "../../tests/fixtures/CM5IO_HyperGraph.json", + import.meta.url, +).href + +const cm5ioBusSelectionFixtureUrl = new URL( + "../../tests/fixtures/CM5IO_bus1.json", + import.meta.url, +).href + +const createBusSubsetSerializedHyperGraph = ( + fullInput: SerializedHyperGraphPortPointPathingSolverInput, + busSelection: ConnectionPatchSelection, +): SerializedHyperGraph => + convertPortPointPathingSolverInputToSerializedHyperGraph( + filterPortPointPathingSolverInputByConnectionPatches( + fullInput, + busSelection, + ), + ) + +export default function Cm5ioBusRouterBus1Page() { + const [serializedHyperGraph, setSerializedHyperGraph] = + useState() + const [errorMessage, setErrorMessage] = useState() + + useEffect(() => { + let isCancelled = false + + const loadFixture = async () => { + try { + const [fullResponse, busSelectionResponse] = await Promise.all([ + fetch(cm5ioHyperGraphFixtureUrl), + fetch(cm5ioBusSelectionFixtureUrl), + ]) + + if (!fullResponse.ok) { + throw new Error( + `Failed to load CM5IO hypergraph fixture (${fullResponse.status})`, + ) + } + + if (!busSelectionResponse.ok) { + throw new Error( + `Failed to load CM5IO bus selection fixture (${busSelectionResponse.status})`, + ) + } + + const fullInput = + (await fullResponse.json()) as SerializedHyperGraphPortPointPathingSolverInput + const busSelection = + (await busSelectionResponse.json()) as ConnectionPatchSelection + const subsetSerializedHyperGraph = createBusSubsetSerializedHyperGraph( + fullInput, + busSelection, + ) + + if (!isCancelled) { + setSerializedHyperGraph(subsetSerializedHyperGraph) + setErrorMessage(undefined) + } + } catch (error) { + if (!isCancelled) { + setErrorMessage( + error instanceof Error + ? error.message + : "Failed to load CM5IO bus subset fixture", + ) + } + } + } + + void loadFixture() + + return () => { + isCancelled = true + } + }, []) + + if (errorMessage) { + return ( +
+
+ {errorMessage} +
+
+ ) + } + + if (!serializedHyperGraph) { + return ( +
+ Loading CM5IO bus subset fixture... +
+ ) + } + + return ( +
+
+ CM5IO `bus1` routed with the fixed-centerline bus solver. The solver + solves the middle trace first, then routes the remaining traces against + that centerline using the bus spacing penalty and a centerline + intersection veto. +
+
+ { + const { topology, problem } = loadSerializedHyperGraph(graph) + return new TinyHyperGraphBusSolver(topology, problem) + }} + /> +
+
+ ) +} diff --git a/tests/solver/bus-solver-layering.test.ts b/tests/solver/bus-solver-layering.test.ts new file mode 100644 index 0000000..1cd9bdd --- /dev/null +++ b/tests/solver/bus-solver-layering.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from "bun:test" +import type { TinyHyperGraphProblem, TinyHyperGraphTopology } from "lib/index" +import { TinyHyperGraphBusSolver } from "lib/index" + +const createBusLayeringTopology = (): TinyHyperGraphTopology => ({ + portCount: 9, + regionCount: 1, + regionIncidentPorts: [[0, 1, 2, 3, 4, 5, 6, 7, 8]], + incidentPortRegion: Array.from({ length: 9 }, () => [0, 0]), + regionWidth: new Float64Array([10]), + regionHeight: new Float64Array([10]), + regionCenterX: new Float64Array([2]), + regionCenterY: new Float64Array([0]), + portAngleForRegion1: new Int32Array(9), + portAngleForRegion2: new Int32Array(9), + portX: new Float64Array([0, 0, 0, 4, 4, 4, 1, 2, 2]), + portY: new Float64Array([-1, 0, 1, -1, 0, 1, -1, -1, -1]), + portZ: new Int32Array([0, 0, 0, 0, 0, 0, 0, 1, 0]), +}) + +const createBusLayeringProblem = (): TinyHyperGraphProblem => ({ + routeCount: 3, + portSectionMask: new Int8Array(9).fill(1), + routeMetadata: [ + { connectionId: "outer-low" }, + { connectionId: "center" }, + { connectionId: "outer-high" }, + ], + routeStartPort: new Int32Array([0, 1, 2]), + routeEndPort: new Int32Array([3, 4, 5]), + routeNet: new Int32Array([0, 1, 2]), + regionNetId: new Int32Array([-1]), +}) + +const createTestSolver = () => + new TinyHyperGraphBusSolver( + createBusLayeringTopology(), + createBusLayeringProblem(), + ) + +test("outer bus traces cannot introduce layer changes when the centerline stays on one layer", () => { + const solver = createTestSolver() + const solverInternal = solver as any + + solverInternal.busState.centerlinePortIds = [1, 4] + solverInternal.busState.centerlineHasLayerChanges = false + + const outerTraceIndex = solver.busTraceOrder.traces.findIndex( + (trace) => trace.connectionId === "outer-low", + ) + + expect( + solverInternal.isMoveBlockedByBusConstraints(outerTraceIndex, { + nextState: { + routeId: 0, + portId: 7, + nextRegionId: 0, + atGoal: false, + prevState: { + routeId: 0, + portId: 6, + nextRegionId: 0, + atGoal: false, + }, + }, + segmentLength: 1, + }), + ).toBe(true) + + expect( + solverInternal.isMoveBlockedByBusConstraints(outerTraceIndex, { + nextState: { + routeId: 0, + portId: 8, + nextRegionId: 0, + atGoal: false, + prevState: { + routeId: 0, + portId: 6, + nextRegionId: 0, + atGoal: false, + }, + }, + segmentLength: 1, + }), + ).toBe(false) +}) + +test("bus alignment cost treats layer mismatch as a very large distance", () => { + const solver = createTestSolver() + const solverInternal = solver as any + + const outerTraceIndex = solver.busTraceOrder.traces.findIndex( + (trace) => trace.connectionId === "outer-low", + ) + + const sameLayerCost = solverInternal.computeTraceAlignmentCost( + outerTraceIndex, + [1, 4], + 8, + ) + const crossLayerCost = solverInternal.computeTraceAlignmentCost( + outerTraceIndex, + [1, 4], + 7, + ) + + expect(crossLayerCost).toBeGreaterThan(sameLayerCost + 50) +}) diff --git a/tests/solver/cm5io-bus-routing.test.ts b/tests/solver/cm5io-bus-routing.test.ts index 943cc3f..e21eec9 100644 --- a/tests/solver/cm5io-bus-routing.test.ts +++ b/tests/solver/cm5io-bus-routing.test.ts @@ -1,23 +1,111 @@ import { expect, test } from "bun:test" import { convertPortPointPathingSolverInputToSerializedHyperGraph } from "lib/compat/convertPortPointPathingSolverInputToSerializedHyperGraph" import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" -import { TinyHyperGraphSolver } from "lib/index" +import { + filterPortPointPathingSolverInputByConnectionPatches, + TinyHyperGraphBusSolver, + type ConnectionPatchSelection, +} from "lib/index" -// Kept as a copied regression case from `buses`; it requires the bus-routing -// implementation that has intentionally not been ported onto this branch. -test.skip("CM5IO solves with fixed bus routing", async () => { - const input = await Bun.file( +test("CM5IO bus1 bus solver expands one search candidate per step", async () => { + const fullInput = await Bun.file( new URL("../fixtures/CM5IO_HyperGraph.json", import.meta.url), ).json() + const busSelection = (await Bun.file( + new URL("../fixtures/CM5IO_bus1.json", import.meta.url), + ).json()) as ConnectionPatchSelection const serializedHyperGraph = - convertPortPointPathingSolverInputToSerializedHyperGraph(input) + convertPortPointPathingSolverInputToSerializedHyperGraph( + filterPortPointPathingSolverInputByConnectionPatches( + fullInput, + busSelection, + ), + ) const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - const solver = new TinyHyperGraphSolver(topology, problem, { - MAX_ITERATIONS: 50_000, - }) + const solver = new TinyHyperGraphBusSolver(topology, problem) + + solver.step() + + expect(solver.iterations).toBe(1) + expect(solver.solved).toBe(false) + expect(solver.failed).toBe(false) + expect(solver.stats.busPhase).toBe("center") + expect(solver.stats.currentTraceConnectionId).toBe("source_trace_108") + expect(solver.stats.openCandidateCount).toBeGreaterThan(0) + + const graphics = solver.visualize() + const activeCandidatePoint = (graphics.points ?? []).find((point) => + point.label?.includes("active candidate"), + ) + const previewCandidateLines = (graphics.lines ?? []).filter((line) => + line.label?.includes("candidate path"), + ) + const renderedTraceIds = new Set( + solver.busTraceOrder.traces + .map((trace) => trace.connectionId) + .filter((connectionId) => + (graphics.lines ?? []).some((line) => + line.label?.includes(connectionId), + ), + ), + ) + + expect(activeCandidatePoint?.label).toContain("active candidate") + expect(activeCandidatePoint?.label).toContain("route: source_trace_108") + expect(previewCandidateLines.length).toBeGreaterThan(0) + expect(renderedTraceIds.size).toBeGreaterThan(1) + expect((graphics.lines ?? []).some((line) => line.strokeDash === "3 3")).toBe( + false, + ) + expect( + (graphics.lines ?? []).some((line) => line.strokeDash === "10 5"), + ).toBe(false) +}) + +test("CM5IO bus1 solves with fixed-centerline bus routing", async () => { + const fullInput = await Bun.file( + new URL("../fixtures/CM5IO_HyperGraph.json", import.meta.url), + ).json() + const busSelection = (await Bun.file( + new URL("../fixtures/CM5IO_bus1.json", import.meta.url), + ).json()) as ConnectionPatchSelection + const serializedHyperGraph = + convertPortPointPathingSolverInputToSerializedHyperGraph( + filterPortPointPathingSolverInputByConnectionPatches( + fullInput, + busSelection, + ), + ) + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + const solver = new TinyHyperGraphBusSolver(topology, problem) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(solver.stats.busCenterConnectionId).toBe("source_trace_108") + expect( + solver.state.regionSegments.reduce((segmentCount, segments) => { + return segmentCount + segments.length + }, 0), + ).toBeGreaterThan(0) + expect(solver.getOutput().solvedRoutes).toHaveLength(9) + + const routeIdsByPortId = new Map>() + for (const regionSegments of solver.state.regionSegments) { + for (const [routeId, fromPortId, toPortId] of regionSegments) { + const fromPortRoutes = + routeIdsByPortId.get(fromPortId) ?? new Set() + fromPortRoutes.add(routeId) + routeIdsByPortId.set(fromPortId, fromPortRoutes) + + const toPortRoutes = routeIdsByPortId.get(toPortId) ?? new Set() + toPortRoutes.add(routeId) + routeIdsByPortId.set(toPortId, toPortRoutes) + } + } + + for (const routeIds of routeIdsByPortId.values()) { + expect(routeIds.size).toBe(1) + } })