diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/apply-fixed-route-replacements-to-preloaded-traces.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/apply-fixed-route-replacements-to-preloaded-traces.ts new file mode 100644 index 000000000..8c96ae8b0 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/apply-fixed-route-replacements-to-preloaded-traces.ts @@ -0,0 +1,168 @@ +import type { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { SimplifiedPcbTrace } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" +import type { Obstacle } from "lib/types/srj-types" +import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute" +import type { PreloadedHighDensityRoute } from "./convert-preloaded-traces-to-hd-routes" + +type ApplyFixedRouteReplacementsParams = { + originalTraces: SimplifiedPcbTrace[] + originalFixedRoutes: PreloadedHighDensityRoute[] + updatedFixedRoutes: HighDensityRoute[] + replacedConnectionNames: ReadonlySet + layerCount: number + defaultViaHoleDiameter: number + obstacles: Obstacle[] + connMap: ConnectivityMap +} + +type ApplyFixedRouteReplacementsResult = { + updatedPreloadedTraces: SimplifiedPcbTrace[] + mutatedPreloadedTraces: SimplifiedPcbTrace[] +} + +const POINT_EPSILON = 1e-9 + +const pointsAreEqual = ( + a: HighDensityRoute["route"][number], + b: HighDensityRoute["route"][number], +) => + Math.abs(a.x - b.x) <= POINT_EPSILON && + Math.abs(a.y - b.y) <= POINT_EPSILON && + a.z === b.z + +const appendConnectedRoute = ( + combinedPoints: HighDensityRoute["route"], + route: HighDensityRoute, +) => { + if (combinedPoints.length === 0) { + combinedPoints.push(...route.route) + return + } + + const previousEnd = combinedPoints.at(-1)! + if (route.route[0] && pointsAreEqual(previousEnd, route.route[0])) { + combinedPoints.push(...route.route.slice(1)) + return + } + if (route.route.at(-1) && pointsAreEqual(previousEnd, route.route.at(-1)!)) { + combinedPoints.push(...route.route.slice(0, -1).reverse()) + return + } + + throw new Error( + `Pipeline9 could not reconnect mutated preloaded segment "${route.connectionName}"`, + ) +} + +/** + * Rebuilds only the preloaded traces touched by a regional fallback. Untouched + * traces retain their original serialized representation and PCB trace ids. + */ +export const applyFixedRouteReplacementsToPreloadedTraces = ({ + originalTraces, + originalFixedRoutes, + updatedFixedRoutes, + replacedConnectionNames, + layerCount, + defaultViaHoleDiameter, + obstacles, + connMap, +}: ApplyFixedRouteReplacementsParams): ApplyFixedRouteReplacementsResult => { + const originalFixedRoutesByName = new Map( + originalFixedRoutes.map((route) => [route.connectionName, route]), + ) + const updatedFixedRoutesByName = new Map( + updatedFixedRoutes.map((route) => [route.connectionName, route]), + ) + const fixedRoutesByTraceIndex = new Map() + + for (const originalFixedRoute of originalFixedRoutes) { + const traceRoutes = + fixedRoutesByTraceIndex.get(originalFixedRoute.preloadedTraceIndex) ?? [] + traceRoutes.push(originalFixedRoute) + fixedRoutesByTraceIndex.set( + originalFixedRoute.preloadedTraceIndex, + traceRoutes, + ) + } + + const mutatedPreloadedTraces: SimplifiedPcbTrace[] = [] + const updatedPreloadedTraces = originalTraces.map((trace, traceIndex) => { + const originalTraceRoutes = ( + fixedRoutesByTraceIndex.get(traceIndex) ?? [] + ).sort((a, b) => a.preloadedRouteIndex - b.preloadedRouteIndex) + const traceWasMutated = originalTraceRoutes.some((route) => + replacedConnectionNames.has(route.connectionName), + ) + if (!traceWasMutated) return trace + if ( + trace.route.some( + (routePoint) => routePoint.route_type === "through_obstacle", + ) + ) { + throw new Error( + `Pipeline9 cannot yet rebuild mutated through-obstacle trace "${trace.pcb_trace_id}"`, + ) + } + + const combinedPoints: HighDensityRoute["route"] = [] + let traceThickness = 0 + let viaDiameter = 0 + let rootConnectionName: string | undefined + + for (const originalTraceRoute of originalTraceRoutes) { + if (!originalFixedRoutesByName.has(originalTraceRoute.connectionName)) { + throw new Error( + `Pipeline9 lost original fixed route "${originalTraceRoute.connectionName}"`, + ) + } + const updatedTraceRoute = updatedFixedRoutesByName.get( + originalTraceRoute.connectionName, + ) + if (!updatedTraceRoute) { + throw new Error( + `Pipeline9 lost updated fixed route "${originalTraceRoute.connectionName}"`, + ) + } + appendConnectedRoute(combinedPoints, updatedTraceRoute) + traceThickness = Math.max( + traceThickness, + updatedTraceRoute.traceThickness, + ) + viaDiameter = Math.max(viaDiameter, updatedTraceRoute.viaDiameter) + rootConnectionName ??= updatedTraceRoute.rootConnectionName + } + + const combinedRoute: HighDensityRoute = { + connectionName: trace.connection_name, + rootConnectionName, + traceThickness, + viaDiameter, + route: combinedPoints, + vias: combinedPoints.slice(0, -1).flatMap((point, pointIndex) => { + const nextPoint = combinedPoints[pointIndex + 1]! + return point.z !== nextPoint.z && + Math.abs(point.x - nextPoint.x) <= POINT_EPSILON && + Math.abs(point.y - nextPoint.y) <= POINT_EPSILON + ? [{ x: nextPoint.x, y: nextPoint.y }] + : [] + }), + } + const mutatedTrace: SimplifiedPcbTrace = { + ...trace, + route: convertHdRouteToSimplifiedRoute(combinedRoute, layerCount, { + defaultViaHoleDiameter, + obstacles, + connMap, + }), + } + mutatedPreloadedTraces.push(mutatedTrace) + return mutatedTrace + }) + + return { + updatedPreloadedTraces, + mutatedPreloadedTraces, + } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index 9f82bebed..f3a059c4a 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -56,7 +56,6 @@ import { CapacityNodeTargetMerger } from "../../solvers/CapacityNodeTargetMerger import { DeadEndSolver } from "../../solvers/DeadEndSolver/DeadEndSolver" import { EscapeViaLocationSolver } from "../../solvers/EscapeViaLocationSolver/EscapeViaLocationSolver" import { Pipeline4HighDensityRepairSolver } from "../../solvers/HighDensityRepairSolver/Pipeline4HighDensityRepairSolver" -import { HighDensitySolver } from "../../solvers/HighDensitySolver/HighDensitySolver" import { MultiSectionPortPointOptimizer } from "../../solvers/MultiSectionPortPointOptimizer" import { NetToPointPairsSolver } from "../../solvers/NetToPointPairsSolver/NetToPointPairsSolver" import { NetToPointPairsSolver2_OffBoardConnection } from "../../solvers/NetToPointPairsSolver2_OffBoardConnection/NetToPointPairsSolver2_OffBoardConnection" @@ -65,7 +64,12 @@ import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver" import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver" import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver" -import { convertPreloadedTraceToHdRoutes } from "./convert-preloaded-traces-to-hd-routes" +import { applyFixedRouteReplacementsToPreloadedTraces } from "./apply-fixed-route-replacements-to-preloaded-traces" +import { + convertPreloadedTraceToHdRoutes, + type PreloadedHighDensityRoute, +} from "./convert-preloaded-traces-to-hd-routes" +import { Pipeline9HighDensitySolver } from "./pipeline9-high-density-solver" import { PreloadedTraceGraphSolver } from "./preloaded-trace-graph-solver" import { PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver } from "./preprocess-simple-route-json-without-trace-obstacles-solver" import { MergedComponentTopologyView } from "../AutoroutingPipeline7_MultiGraph/MergedComponentTopologyView" @@ -222,7 +226,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { nodeTargetMerger?: CapacityNodeTargetMerger edgeSolver?: CapacityMeshEdgeSolver colorMap!: Record - highDensityRouteSolver?: HighDensitySolver + highDensityRouteSolver?: Pipeline9HighDensitySolver highDensityForceImproveSolver?: HighDensityForceImproveSolver highDensityRepairSolver?: Pipeline4HighDensityRepairSolver highDensityStitchSolver?: MultipleHighDensityRouteStitchSolver3 @@ -532,40 +536,46 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { }, ], ), - definePipelineStep("highDensityRouteSolver", HighDensitySolver, (cms) => { - const uniformNodes = cms.uniformPortDistributionSolver?.getOutput() ?? [] - const fallbackNodes = - cms.portPointPathingSolver?.getOutput().nodesWithPortPoints ?? [] - const nodePortPointsSource = - uniformNodes.length > 0 ? uniformNodes : fallbackNodes + definePipelineStep( + "highDensityRouteSolver", + Pipeline9HighDensitySolver, + (cms) => { + const uniformNodes = + cms.uniformPortDistributionSolver?.getOutput() ?? [] + const fallbackNodes = + cms.portPointPathingSolver?.getOutput().nodesWithPortPoints ?? [] + const nodePortPointsSource = + uniformNodes.length > 0 ? uniformNodes : fallbackNodes - cms.highDensityNodePortPoints = structuredClone(nodePortPointsSource) + cms.highDensityNodePortPoints = structuredClone(nodePortPointsSource) + const fixedHdRoutes = (cms.originalSrj.traces ?? []).flatMap( + (trace, traceIndex) => + convertPreloadedTraceToHdRoutes( + trace, + traceIndex, + cms.originalSrj.layerCount, + cms.viaDiameter, + cms.connMap, + ), + ) - return [ - { - nodePortPoints: nodePortPointsSource, - nodePfById: new Map( - ( - cms.portPointPathingSolver?.getOutput().inputNodeWithPortPoints ?? - [] - ).map((node) => [ - node.capacityMeshNodeId, - cms.portPointPathingSolver?.computeNodePf(node) ?? null, - ]), - ), - colorMap: cms.colorMap, - connMap: cms.connMap, - viaDiameter: cms.viaDiameter, - traceWidth: cms.minTraceWidth, - obstacleMargin: cms.srj.defaultObstacleMargin ?? 0.15, - obstacles: cms.srj.obstacles, - layerCount: cms.srj.layerCount, - useGrowShrinkHighDensityIntraNodeSolver: true, - preserveTerminalPcbPortIds: true, - growShrinkFallbackToInvalidGeometryOnFailure: true, - }, - ] - }), + return [ + { + nodePortPoints: nodePortPointsSource, + fixedHdRoutes, + connMap: cms.connMap, + colorMap: cms.colorMap, + obstacles: cms.srj.obstacles, + layerCount: cms.srj.layerCount, + viaDiameter: cms.viaDiameter, + traceWidth: cms.minTraceWidth, + obstacleMargin: cms.srj.defaultObstacleMargin ?? 0.15, + effort: cms.effort, + preserveTerminalPcbPortIds: true, + }, + ] + }, + ), definePipelineStep( "highDensityForceImproveSolver", HighDensityForceImproveSolver, @@ -616,8 +626,9 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { "traceSimplificationSolver", TraceSimplificationSolver, (cms) => { - const preloadedHdRoutes = (cms.originalSrj.traces ?? []).flatMap( - (trace, traceIndex) => + const preloadedHdRoutes = + cms.highDensityRouteSolver?.getUpdatedFixedHdRoutes() ?? + (cms.originalSrj.traces ?? []).flatMap((trace, traceIndex) => convertPreloadedTraceToHdRoutes( trace, traceIndex, @@ -625,7 +636,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { cms.viaDiameter, cms.connMap, ), - ) + ) return [ { @@ -690,7 +701,10 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { defaultViaHoleDiameter: cms.viaHoleDiameter, connMap: cms.connMap, srjWithPointPairs: cms.srjWithPointPairs!, - originalSrj: cms.originalSrj, + originalSrj: { + ...cms.originalSrj, + traces: cms.getUpdatedPreloadedTraces(), + }, }) return [ @@ -784,7 +798,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { this.effort = mutableOpts.effort ?? 1 // scale with effort so the outer cap never decapitates inner solvers this.MAX_ITERATIONS = 100e6 * this.effort - this.maxNodeDimension = mutableOpts.maxNodeDimension ?? 16 + this.maxNodeDimension = mutableOpts.maxNodeDimension ?? 15 this.maxNodeRatio = mutableOpts.maxNodeRatio ?? 6 this.minNodeArea = mutableOpts.minNodeArea ?? 0.1 ** 2 this.visualizationTraceColorMode = @@ -1119,12 +1133,50 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { ) } - getOutputSimplifiedPcbTraces(): SimplifiedPcbTraces { + private getOriginalFixedHdRoutes(): PreloadedHighDensityRoute[] { + return (this.originalSrj.traces ?? []).flatMap((trace, traceIndex) => + convertPreloadedTraceToHdRoutes( + trace, + traceIndex, + this.originalSrj.layerCount, + this.viaDiameter, + this.connMap, + ), + ) + } + + private getPreloadedTraceUpdates() { + const originalFixedRoutes = this.getOriginalFixedHdRoutes() + return applyFixedRouteReplacementsToPreloadedTraces({ + originalTraces: this.originalSrj.traces ?? [], + originalFixedRoutes, + updatedFixedRoutes: + this.highDensityRouteSolver?.getUpdatedFixedHdRoutes() ?? + originalFixedRoutes, + replacedConnectionNames: new Set( + this.highDensityRouteSolver?.fixedRouteReplacements.keys() ?? [], + ), + layerCount: this.originalSrj.layerCount, + defaultViaHoleDiameter: this.viaHoleDiameter, + obstacles: this.originalSrj.obstacles, + connMap: this.connMap, + }) + } + + getUpdatedPreloadedTraces(): SimplifiedPcbTraces { + return this.getPreloadedTraceUpdates().updatedPreloadedTraces + } + + getMutatedPreloadedTraces(): SimplifiedPcbTraces { + return this.getPreloadedTraceUpdates().mutatedPreloadedTraces + } + + private getNewOutputSimplifiedPcbTraces(): SimplifiedPcbTraces { if (!this.solved || !this.highDensityRouteSolver) { throw new Error("Cannot get output before solving is complete") } - return convertPipeline7HdRoutesToSimplifiedPcbTraces({ + const routedTraces = convertPipeline7HdRoutesToSimplifiedPcbTraces({ connections: this.netToPointPairsSolver?.newConnections ?? [], originalConnections: this.originalSrj.connections, hdRoutes: this._getOutputHdRoutes(), @@ -1133,12 +1185,45 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver { defaultViaHoleDiameter: this.viaHoleDiameter, connMap: this.connMap, }) + const usedPcbTraceIds = new Set( + (this.originalSrj.traces ?? []).map((trace) => trace.pcb_trace_id), + ) + + return routedTraces.map((trace) => { + if (!usedPcbTraceIds.has(trace.pcb_trace_id)) { + usedPcbTraceIds.add(trace.pcb_trace_id) + return trace + } + + const basePcbTraceId = `${trace.pcb_trace_id}_routed` + let pcbTraceId = basePcbTraceId + let suffix = 2 + while (usedPcbTraceIds.has(pcbTraceId)) { + pcbTraceId = `${basePcbTraceId}_${suffix}` + suffix += 1 + } + usedPcbTraceIds.add(pcbTraceId) + return { + ...trace, + pcb_trace_id: pcbTraceId, + } + }) + } + + getOutputSimplifiedPcbTraces(): SimplifiedPcbTraces { + return [ + ...this.getMutatedPreloadedTraces(), + ...this.getNewOutputSimplifiedPcbTraces(), + ] } getOutputSimpleRouteJson(): SimpleRouteJson { return { ...this.originalSrj, - traces: this.getOutputSimplifiedPcbTraces(), + traces: [ + ...this.getUpdatedPreloadedTraces(), + ...this.getNewOutputSimplifiedPcbTraces(), + ], } } } diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/convert-preloaded-traces-to-hd-routes.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/convert-preloaded-traces-to-hd-routes.ts index d08e50748..2cd69b135 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/convert-preloaded-traces-to-hd-routes.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/convert-preloaded-traces-to-hd-routes.ts @@ -5,16 +5,21 @@ import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" const MIN_ROUTE_DIMENSION = 1e-9 +export type PreloadedHighDensityRoute = HighDensityRoute & { + preloadedTraceIndex: number + preloadedRouteIndex: number +} + export const convertPreloadedTraceToHdRoutes = ( trace: SimplifiedPcbTrace, traceIndex: number, layerCount: number, defaultViaDiameter: number, connMap: ConnectivityMap, -): HighDensityRoute[] => { +): PreloadedHighDensityRoute[] => { const rootConnectionName = connMap.getNetConnectedToId(trace.connection_name) ?? trace.connection_name - const routes: HighDensityRoute[] = [] + const routes: PreloadedHighDensityRoute[] = [] const addRoute = ( route: HighDensityRoute["route"], traceThickness: number, @@ -25,6 +30,8 @@ export const convertPreloadedTraceToHdRoutes = ( routes.push({ connectionName: `${trace.connection_name}_fixed_${traceIndex}_${routes.length}`, rootConnectionName, + preloadedTraceIndex: traceIndex, + preloadedRouteIndex: routes.length, traceThickness: Math.max(MIN_ROUTE_DIMENSION, traceThickness), viaDiameter: Math.max(MIN_ROUTE_DIMENSION, viaDiameter), route, diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver.ts new file mode 100644 index 000000000..07eef8d40 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver.ts @@ -0,0 +1,440 @@ +import { + defaultB01Params, + HighDensitySolverB01, + type HighDensityRouteObstacle, + type NodeWithPortPoints as B01NodeWithPortPoints, +} from "@tscircuit/high-density-b01" +import type { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { GraphicsObject } from "graphics-debug" +import type { + HighDensityIntraNodeRoute, + HighDensityRoute, + NodeWithPortPoints, +} from "lib/types/high-density-types" +import type { Obstacle } from "lib/types/srj-types" +import { BaseSolver } from "../../solvers/BaseSolver" +import { HighDensitySolver } from "../../solvers/HighDensitySolver/HighDensitySolver" +import { + createRegionalFallbackProblem, + type FixedRouteSlice, + spliceFixedRoute, +} from "./pipeline9-regional-fallback" + +type Pipeline9HighDensitySolverParams = { + nodePortPoints: NodeWithPortPoints[] + fixedHdRoutes: HighDensityRoute[] + connMap: ConnectivityMap + colorMap?: Record + obstacles: Obstacle[] + layerCount: number + viaDiameter: number + traceWidth: number + obstacleMargin: number + effort: number + preserveTerminalPcbPortIds?: boolean +} + +type NodeBounds = { + minX: number + maxX: number + minY: number + maxY: number +} + +const PRELOADED_TRACE_CLEARANCE = 0.095 + +const getNodeBounds = ( + node: NodeWithPortPoints, + margin: number, +): NodeBounds => ({ + minX: node.center.x - node.width / 2 - margin, + maxX: node.center.x + node.width / 2 + margin, + minY: node.center.y - node.height / 2 - margin, + maxY: node.center.y + node.height / 2 + margin, +}) + +const routeOverlapsNode = ( + route: HighDensityRoute, + nodeBounds: NodeBounds, +): boolean => { + const routePoints = [...route.route, ...route.vias.map((via) => ({ ...via }))] + if (routePoints.length === 0) return false + + const minX = Math.min(...routePoints.map((point) => point.x)) + const maxX = Math.max(...routePoints.map((point) => point.x)) + const minY = Math.min(...routePoints.map((point) => point.y)) + const maxY = Math.max(...routePoints.map((point) => point.y)) + const routeMargin = Math.max(route.traceThickness / 2, route.viaDiameter / 2) + + return ( + minX - routeMargin <= nodeBounds.maxX && + maxX + routeMargin >= nodeBounds.minX && + minY - routeMargin <= nodeBounds.maxY && + maxY + routeMargin >= nodeBounds.minY + ) +} + +const convertFixedRouteToB01Obstacle = ( + route: HighDensityRoute, + node: NodeWithPortPoints, +): HighDensityRouteObstacle | undefined => { + const availableZ = new Set( + node.availableZ ?? node.portPoints.map((portPoint) => portPoint.z), + ) + if (availableZ.size === 0) { + throw new Error( + `Pipeline9 B01 node "${node.capacityMeshNodeId}" has no available layers`, + ) + } + + if (route.vias.length > 0) { + const z = + route.route.find((point) => availableZ.has(point.z))?.z ?? + availableZ.values().next().value + if (z === undefined) return undefined + const via = route.vias[0]! + return { + type: "route", + connectionName: route.connectionName, + rootConnectionName: route.rootConnectionName, + traceThickness: route.traceThickness, + viaDiameter: route.viaDiameter, + route: [ + { x: via.x, y: via.y, z }, + { x: via.x, y: via.y, z }, + ], + vias: route.vias, + } + } + + if (!route.route.every((point) => availableZ.has(point.z))) { + return undefined + } + + return { + type: "route", + connectionName: route.connectionName, + rootConnectionName: route.rootConnectionName, + traceThickness: route.traceThickness, + viaDiameter: route.viaDiameter, + route: route.route, + vias: [], + } +} + +const addTerminalPcbPortIds = ( + routes: HighDensityIntraNodeRoute[], + node: NodeWithPortPoints, +): HighDensityIntraNodeRoute[] => { + const terminalPortPoints = node.portPoints.filter( + (portPoint) => portPoint.pcb_port_id !== undefined, + ) + return routes.map((route) => { + const start = route.route[0] + const end = route.route.at(-1) + const startTerminal = terminalPortPoints.find( + (terminal) => + start !== undefined && + terminal.connectionName === route.connectionName && + terminal.x === start.x && + terminal.y === start.y && + terminal.z === start.z, + ) + const endTerminal = terminalPortPoints.find( + (terminal) => + end !== undefined && + terminal.connectionName === route.connectionName && + terminal.x === end.x && + terminal.y === end.y && + terminal.z === end.z, + ) + + return { + ...route, + startPcbPortId: startTerminal?.pcb_port_id, + endPcbPortId: endTerminal?.pcb_port_id, + } + }) +} + +const normalizeNodeRootConnectionNames = ( + node: NodeWithPortPoints, + connMap: ConnectivityMap, +): NodeWithPortPoints => { + const normalizePortPoint = ( + portPoint: NodeWithPortPoints["portPoints"][number], + ): NodeWithPortPoints["portPoints"][number] => ({ + ...portPoint, + rootConnectionName: + connMap.getNetConnectedToId( + portPoint.rootConnectionName ?? portPoint.connectionName, + ) ?? + portPoint.rootConnectionName ?? + portPoint.connectionName, + }) + return { + ...node, + portPoints: node.portPoints.map(normalizePortPoint), + portPointsInPairs: node.portPointsInPairs?.map(([start, end]) => [ + normalizePortPoint(start), + normalizePortPoint(end), + ]), + } +} + +const restoreRootConnectionNames = ( + routes: HighDensityIntraNodeRoute[], + node: NodeWithPortPoints, +): HighDensityIntraNodeRoute[] => + routes.map((route) => ({ + ...route, + rootConnectionName: + node.portPoints.find( + (portPoint) => portPoint.connectionName === route.connectionName, + )?.rootConnectionName ?? route.rootConnectionName, + })) + +/** + * Routes Pipeline9 intra-node connections with B01 while treating preloaded + * copper as immutable, layer-aware route obstacles. + */ +export class Pipeline9HighDensitySolver extends BaseSolver { + readonly fixedHdRoutes: HighDensityRoute[] + readonly connMap: ConnectivityMap + readonly colorMap: Record + readonly obstacles: Obstacle[] + readonly layerCount: number + readonly viaDiameter: number + readonly traceWidth: number + readonly obstacleMargin: number + readonly effort: number + readonly preserveTerminalPcbPortIds: boolean + readonly routes: HighDensityIntraNodeRoute[] = [] + readonly failedSolvers: HighDensitySolverB01[] = [] + readonly unsolvedNodePortPoints: NodeWithPortPoints[] + readonly fixedRouteReplacements = new Map() + + activeB01Solver: HighDensitySolverB01 | null = null + activeFallbackSolver: HighDensitySolver | null = null + activeFallbackFixedRouteSlices = new Map() + activeB01Error: string | null = null + activeNode: NodeWithPortPoints | null = null + + constructor(params: Pipeline9HighDensitySolverParams) { + super() + this.fixedHdRoutes = params.fixedHdRoutes + this.connMap = params.connMap + this.colorMap = params.colorMap ?? {} + this.obstacles = params.obstacles + this.layerCount = params.layerCount + this.viaDiameter = params.viaDiameter + this.traceWidth = params.traceWidth + this.obstacleMargin = params.obstacleMargin + this.effort = params.effort + this.preserveTerminalPcbPortIds = params.preserveTerminalPcbPortIds ?? false + this.unsolvedNodePortPoints = [...params.nodePortPoints] + this.MAX_ITERATIONS = 100e6 * this.effort + this.stats = { + nodeCount: params.nodePortPoints.length, + solvedNodeCount: 0, + fixedObstacleCount: params.fixedHdRoutes.length, + fixedObstacleUses: 0, + fallbackNodeCount: 0, + reroutedFixedRouteCount: 0, + } + } + + override getSolverName(): string { + return "Pipeline9HighDensitySolver" + } + + getUpdatedFixedHdRoutes(): HighDensityRoute[] { + return this.fixedHdRoutes.map( + (route) => this.fixedRouteReplacements.get(route.connectionName) ?? route, + ) + } + + private finishActiveNode(routes: HighDensityIntraNodeRoute[]) { + const solvedRoutes = this.activeNode + ? restoreRootConnectionNames(routes, this.activeNode) + : routes + this.routes.push( + ...(this.preserveTerminalPcbPortIds && this.activeNode + ? addTerminalPcbPortIds(solvedRoutes, this.activeNode) + : solvedRoutes), + ) + this.stats.solvedNodeCount = Number(this.stats.solvedNodeCount ?? 0) + 1 + this.activeB01Solver = null + this.activeFallbackSolver = null + this.activeFallbackFixedRouteSlices.clear() + this.activeB01Error = null + this.activeNode = null + } + + private startRegionalFallback() { + if (!this.activeNode) { + throw new Error( + "Pipeline9 cannot start a regional fallback without an active node", + ) + } + + const normalizedNode = normalizeNodeRootConnectionNames( + this.activeNode, + this.connMap, + ) + const fallbackProblem = createRegionalFallbackProblem( + normalizedNode, + this.getUpdatedFixedHdRoutes(), + ) + this.activeFallbackFixedRouteSlices = + fallbackProblem.fixedRouteSlicesByConnectionName + this.activeFallbackSolver = new HighDensitySolver({ + nodePortPoints: [fallbackProblem.nodeWithPortPoints], + colorMap: this.colorMap, + connMap: this.connMap, + viaDiameter: this.viaDiameter, + traceWidth: this.traceWidth, + obstacleMargin: this.obstacleMargin, + effort: this.effort, + obstacles: this.obstacles, + layerCount: this.layerCount, + useGrowShrinkHighDensityIntraNodeSolver: true, + preserveTerminalPcbPortIds: false, + growShrinkFallbackToInvalidGeometryOnFailure: false, + }) + this.stats.fallbackNodeCount = Number(this.stats.fallbackNodeCount ?? 0) + 1 + } + + private finishRegionalFallback() { + if (!this.activeFallbackSolver) return + + const newRoutes: HighDensityIntraNodeRoute[] = [] + const replacementRoutesByConnectionName = new Map< + string, + HighDensityRoute[] + >() + + for (const route of this.activeFallbackSolver.routes) { + if (!this.activeFallbackFixedRouteSlices.has(route.connectionName)) { + newRoutes.push(route) + continue + } + const replacementRoutes = + replacementRoutesByConnectionName.get(route.connectionName) ?? [] + replacementRoutes.push(route) + replacementRoutesByConnectionName.set( + route.connectionName, + replacementRoutes, + ) + } + + for (const [connectionName, slice] of this.activeFallbackFixedRouteSlices) { + const replacementRoutes = + replacementRoutesByConnectionName.get(connectionName) ?? [] + if (replacementRoutes.length !== 1) { + this.error = `Pipeline9 regional fallback expected one replacement for fixed route "${connectionName}", got ${replacementRoutes.length}` + this.failed = true + return + } + this.fixedRouteReplacements.set( + connectionName, + spliceFixedRoute(slice, replacementRoutes[0]!), + ) + } + + this.stats.reroutedFixedRouteCount = + Number(this.stats.reroutedFixedRouteCount ?? 0) + + this.activeFallbackFixedRouteSlices.size + this.finishActiveNode(newRoutes) + } + + override _step(): void { + if (this.activeFallbackSolver) { + this.activeFallbackSolver.step() + if (this.activeFallbackSolver.failed) { + this.error = [ + `Pipeline9 B01 failed: ${this.activeB01Error ?? "unknown error"}`, + `regional fallback failed: ${this.activeFallbackSolver.error ?? "unknown error"}`, + ].join("; ") + this.failed = true + this.activeFallbackSolver = null + this.activeFallbackFixedRouteSlices.clear() + this.activeNode = null + return + } + if (!this.activeFallbackSolver.solved) return + + this.finishRegionalFallback() + return + } + + if (this.activeB01Solver) { + this.activeB01Solver.step() + if (this.activeB01Solver.failed) { + this.failedSolvers.push(this.activeB01Solver) + this.activeB01Error = this.activeB01Solver.error + this.activeB01Solver = null + this.startRegionalFallback() + return + } + if (!this.activeB01Solver.solved) return + + this.finishActiveNode(this.activeB01Solver.getOutput()) + return + } + + const node = this.unsolvedNodePortPoints.pop() + if (!node) { + this.solved = true + return + } + if (node.width > 15 || node.height > 15) { + this.error = `Pipeline9 B01 node "${node.capacityMeshNodeId}" exceeds the 15x15mm routing limit (${node.width}x${node.height}mm)` + this.failed = true + return + } + + const nodeBounds = getNodeBounds(node, this.obstacleMargin) + const fixedObstacles = this.getUpdatedFixedHdRoutes() + .filter((route) => routeOverlapsNode(route, nodeBounds)) + .map((route) => convertFixedRouteToB01Obstacle(route, node)) + .filter( + (obstacle): obstacle is HighDensityRouteObstacle => + obstacle !== undefined, + ) + this.stats.fixedObstacleUses = + Number(this.stats.fixedObstacleUses ?? 0) + fixedObstacles.length + this.activeNode = node + const normalizedNode = normalizeNodeRootConnectionNames(node, this.connMap) + this.activeB01Solver = new HighDensitySolverB01({ + ...defaultB01Params, + nodeWithPortPoints: normalizedNode as B01NodeWithPortPoints, + obstacles: fixedObstacles, + viaDiameter: this.viaDiameter, + viaMinDistFromBorder: this.viaDiameter / 2, + traceThickness: this.traceWidth, + traceMargin: this.obstacleMargin, + obstacleClearanceMargin: PRELOADED_TRACE_CLEARANCE, + effort: this.effort, + }) + } + + override visualize(): GraphicsObject { + return ( + this.activeFallbackSolver?.visualize() ?? + this.activeB01Solver?.visualize() ?? { + lines: this.routes.flatMap((route) => + route.route.slice(0, -1).map((point, pointIndex) => ({ + points: [point, route.route[pointIndex + 1]!], + strokeWidth: route.traceThickness, + layer: `z${point.z}`, + label: route.connectionName, + })), + ), + points: [], + rects: [], + circles: [], + } + ) + } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-regional-fallback.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-regional-fallback.ts new file mode 100644 index 000000000..0af828651 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-regional-fallback.ts @@ -0,0 +1,290 @@ +import type { + HighDensityRoute, + NodeWithPortPoints, + PortPoint, +} from "lib/types/high-density-types" + +type RoutePoint = HighDensityRoute["route"][number] + +type NodeBounds = { + minX: number + maxX: number + minY: number + maxY: number +} + +type RouteLocation = { + segmentIndex: number + point: RoutePoint +} + +export type FixedRouteSlice = { + sourceRoute: HighDensityRoute + start: RouteLocation + end: RouteLocation +} + +export type RegionalFallbackProblem = { + nodeWithPortPoints: NodeWithPortPoints + fixedRouteSlicesByConnectionName: Map +} + +const POINT_EPSILON = 1e-9 + +const getNodeBounds = (node: NodeWithPortPoints): NodeBounds => ({ + minX: node.center.x - node.width / 2, + maxX: node.center.x + node.width / 2, + minY: node.center.y - node.height / 2, + maxY: node.center.y + node.height / 2, +}) + +const isPointInsideBounds = (point: RoutePoint, bounds: NodeBounds) => + point.x >= bounds.minX - POINT_EPSILON && + point.x <= bounds.maxX + POINT_EPSILON && + point.y >= bounds.minY - POINT_EPSILON && + point.y <= bounds.maxY + POINT_EPSILON + +const interpolateRoutePoint = ( + start: RoutePoint, + end: RoutePoint, + t: number, +): RoutePoint => { + if (t <= POINT_EPSILON) return start + if (t >= 1 - POINT_EPSILON) return end + return { + x: start.x + (end.x - start.x) * t, + y: start.y + (end.y - start.y) * t, + z: start.z, + } +} + +const clipRouteSegmentToBounds = ( + start: RoutePoint, + end: RoutePoint, + bounds: NodeBounds, +): { start: RoutePoint; end: RoutePoint } | null => { + const dx = end.x - start.x + const dy = end.y - start.y + + if (Math.abs(dx) <= POINT_EPSILON && Math.abs(dy) <= POINT_EPSILON) { + return isPointInsideBounds(start, bounds) && + isPointInsideBounds(end, bounds) + ? { start, end } + : null + } + + let entryT = 0 + let exitT = 1 + const constraints: Array<[number, number]> = [ + [-dx, start.x - bounds.minX], + [dx, bounds.maxX - start.x], + [-dy, start.y - bounds.minY], + [dy, bounds.maxY - start.y], + ] + + for (const [direction, distanceToBoundary] of constraints) { + if (Math.abs(direction) <= POINT_EPSILON) { + if (distanceToBoundary < 0) return null + continue + } + + const boundaryT = distanceToBoundary / direction + if (direction < 0) { + entryT = Math.max(entryT, boundaryT) + } else { + exitT = Math.min(exitT, boundaryT) + } + if (entryT > exitT + POINT_EPSILON) return null + } + + return { + start: interpolateRoutePoint(start, end, entryT), + end: interpolateRoutePoint(start, end, exitT), + } +} + +const getFixedRouteSlice = ( + route: HighDensityRoute, + node: NodeWithPortPoints, +): FixedRouteSlice | null => { + const bounds = getNodeBounds(node) + let start: RouteLocation | undefined + let end: RouteLocation | undefined + + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex++ + ) { + const clippedSegment = clipRouteSegmentToBounds( + route.route[segmentIndex]!, + route.route[segmentIndex + 1]!, + bounds, + ) + if (!clippedSegment) continue + + start ??= { + segmentIndex, + point: clippedSegment.start, + } + end = { + segmentIndex, + point: clippedSegment.end, + } + } + + if (!start || !end) return null + if ( + Math.abs(start.point.x - end.point.x) <= POINT_EPSILON && + Math.abs(start.point.y - end.point.y) <= POINT_EPSILON && + start.point.z === end.point.z + ) { + return null + } + + return { + sourceRoute: route, + start, + end, + } +} + +const createFallbackPortPair = ( + slice: FixedRouteSlice, +): [PortPoint, PortPoint] => { + const portPointIdPrefix = `pipeline9_fallback:${slice.sourceRoute.connectionName}` + const startPortPointId = `${portPointIdPrefix}:start` + const endPortPointId = `${portPointIdPrefix}:end` + return [ + { + ...slice.start.point, + portPointId: startPortPointId, + nextPortPointId: endPortPointId, + connectionName: slice.sourceRoute.connectionName, + rootConnectionName: slice.sourceRoute.rootConnectionName, + }, + { + ...slice.end.point, + portPointId: endPortPointId, + prevPortPointId: startPortPointId, + connectionName: slice.sourceRoute.connectionName, + rootConnectionName: slice.sourceRoute.rootConnectionName, + }, + ] +} + +/** + * Builds the regular high-density input used only after B01 fails. Fixed + * pre-routed copper that crosses the node becomes an ordinary port pair, which + * lets the portfolio reroute it together with the new traces in that region. + */ +export const createRegionalFallbackProblem = ( + node: NodeWithPortPoints, + fixedRoutes: HighDensityRoute[], +): RegionalFallbackProblem => { + const fixedRouteSlicesByConnectionName = new Map() + const fallbackPortPairs: Array<[PortPoint, PortPoint]> = [] + + for (const fixedRoute of fixedRoutes) { + const slice = getFixedRouteSlice(fixedRoute, node) + if (!slice) continue + if (fixedRouteSlicesByConnectionName.has(fixedRoute.connectionName)) { + throw new Error( + `Pipeline9 regional fallback found duplicate fixed route identity "${fixedRoute.connectionName}"`, + ) + } + fixedRouteSlicesByConnectionName.set(fixedRoute.connectionName, slice) + fallbackPortPairs.push(createFallbackPortPair(slice)) + } + + return { + nodeWithPortPoints: { + ...node, + portPoints: [ + ...node.portPoints, + ...fallbackPortPairs.flatMap((pair) => pair), + ], + portPointsInPairs: [ + ...(node.portPointsInPairs ?? []), + ...fallbackPortPairs, + ], + }, + fixedRouteSlicesByConnectionName, + } +} + +const pointsAreEqual = (a: RoutePoint, b: RoutePoint) => + Math.abs(a.x - b.x) <= POINT_EPSILON && + Math.abs(a.y - b.y) <= POINT_EPSILON && + a.z === b.z + +const dedupeAdjacentPoints = (points: RoutePoint[]): RoutePoint[] => { + const deduped: RoutePoint[] = [] + for (const point of points) { + if (deduped.at(-1) && pointsAreEqual(deduped.at(-1)!, point)) continue + deduped.push(point) + } + return deduped +} + +const orientReplacementPoints = ( + replacement: HighDensityRoute, + slice: FixedRouteSlice, +): RoutePoint[] => { + const points = replacement.route + const first = points[0] + const last = points.at(-1) + if (!first || !last) { + throw new Error( + `Pipeline9 regional fallback produced an empty replacement for "${slice.sourceRoute.connectionName}"`, + ) + } + + const forwardDistance = + Math.hypot(first.x - slice.start.point.x, first.y - slice.start.point.y) + + Math.hypot(last.x - slice.end.point.x, last.y - slice.end.point.y) + const reverseDistance = + Math.hypot(last.x - slice.start.point.x, last.y - slice.start.point.y) + + Math.hypot(first.x - slice.end.point.x, first.y - slice.end.point.y) + + return reverseDistance < forwardDistance ? [...points].reverse() : points +} + +const getViasFromRoutePoints = ( + points: RoutePoint[], +): Array<{ x: number; y: number }> => { + const vias: Array<{ x: number; y: number }> = [] + for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex++) { + const start = points[pointIndex]! + const end = points[pointIndex + 1]! + if ( + start.z !== end.z && + Math.abs(start.x - end.x) <= POINT_EPSILON && + Math.abs(start.y - end.y) <= POINT_EPSILON + ) { + vias.push({ x: end.x, y: end.y }) + } + } + return vias +} + +/** Splices a regular high-density replacement back into the complete fixed route. */ +export const spliceFixedRoute = ( + slice: FixedRouteSlice, + replacement: HighDensityRoute, +): HighDensityRoute => { + const replacementPoints = orientReplacementPoints(replacement, slice) + const route = dedupeAdjacentPoints([ + ...slice.sourceRoute.route.slice(0, slice.start.segmentIndex + 1), + slice.start.point, + ...replacementPoints.slice(1, -1), + slice.end.point, + ...slice.sourceRoute.route.slice(slice.end.segmentIndex + 1), + ]) + + return { + ...slice.sourceRoute, + route, + vias: getViasFromRoutePoints(route), + } +} diff --git a/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts index addc6757a..c4eee7a6b 100644 --- a/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts +++ b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts @@ -30,6 +30,7 @@ type Via = { net: string routeIndex: number layers: number[] + mutable: boolean } const NEAR_VIA_MERGE_DISTANCE_MULTIPLIER = 2.5 @@ -41,7 +42,10 @@ const tryGetNetForRoute = ( ): string | undefined => connMap.idToNetMap[route.connectionName] ?? (route.rootConnectionName - ? connMap.idToNetMap[route.rootConnectionName] + ? (connMap.idToNetMap[route.rootConnectionName] ?? + (connMap.netMap[route.rootConnectionName] + ? route.rootConnectionName + : undefined)) : undefined) const getNetForRoute = ( @@ -226,8 +230,17 @@ export class SameNetViaMergerSolver extends BaseSolver { this.vias = [] this.viasByNet = new Map() - for (let i = 0; i < this.mergedViaHdRoutes.length; i++) { - const route = this.mergedViaHdRoutes[i] + const addRouteVias = ( + route: HighDensityRoute, + routeIndex: number, + mutable: boolean, + ) => { + if (route.vias.length === 0) return + const net = mutable + ? getNetForRoute(this.connMap, route) + : tryGetNetForRoute(this.connMap, route) + if (!net) return + for (let j = 0; j < route.vias.length; j++) { const viaPoint = route.vias[j] const layers = [...new Set(route.route.map((p) => p.z))] @@ -241,9 +254,10 @@ export class SameNetViaMergerSolver extends BaseSolver { x: viaPoint.x, y: viaPoint.y, diameter: route.viaDiameter, - net: getNetForRoute(this.connMap, route), + net, layers, - routeIndex: i, + routeIndex, + mutable, } this.vias.push(via) const list = this.viasByNet.get(via.net) @@ -251,12 +265,28 @@ export class SameNetViaMergerSolver extends BaseSolver { else this.viasByNet.set(via.net, [via]) } } + + for (let i = 0; i < this.mergedViaHdRoutes.length; i++) { + addRouteVias(this.mergedViaHdRoutes[i]!, i, true) + } + for (let i = 0; i < (this.input.otherHdRoutes?.length ?? 0); i++) { + addRouteVias( + this.input.otherHdRoutes![i]!, + this.mergedViaHdRoutes.length + i, + false, + ) + } } private getViaKey(via: Via): string { - return [via.routeIndex, via.x, via.y, via.layers.join(","), via.net].join( - ":", - ) + return [ + via.mutable ? "mutable" : "immutable", + via.routeIndex, + via.x, + via.y, + via.layers.join(","), + via.net, + ].join(":") } private dedupeRouteVias(route: HighDensityRoute): void { @@ -312,6 +342,7 @@ export class SameNetViaMergerSolver extends BaseSolver { if (candidateIndex === viaIndex) continue const candidate = viasInNet[candidateIndex] + if (!candidate.mutable) continue const pairDx = keep.x - candidate.x const pairDy = keep.y - candidate.y @@ -321,7 +352,10 @@ export class SameNetViaMergerSolver extends BaseSolver { const nearMergeDistance = directOverlapDistance * NEAR_VIA_MERGE_DISTANCE_MULTIPLIER - if (squaredDistance === 0) continue + if (squaredDistance === 0) { + if (!keep.mutable) remove.push(candidate) + continue + } if ( squaredDistance <= @@ -354,6 +388,9 @@ export class SameNetViaMergerSolver extends BaseSolver { if (b.remove.length !== a.remove.length) { return b.remove.length - a.remove.length } + if (a.keep.mutable !== b.keep.mutable) { + return a.keep.mutable ? 1 : -1 + } if (b.keep.layers.length !== a.keep.layers.length) { return b.keep.layers.length - a.keep.layers.length } @@ -381,6 +418,11 @@ export class SameNetViaMergerSolver extends BaseSolver { } private moveViaTo(viaToRemove: Via, viaKeep: Via, rebuildVias = true): void { + if (!viaToRemove.mutable) { + throw new Error( + "SameNetViaMergerSolver cannot mutate an immutable via anchor", + ) + } const routeToUpdate = this.mergedViaHdRoutes[viaToRemove.routeIndex] if (!routeToUpdate) { throw new Error( @@ -433,10 +475,10 @@ export class SameNetViaMergerSolver extends BaseSolver { route[routePointIndex] = { ...point, x: viaKeep.x, y: viaKeep.y } } - routeToUpdate.vias = routeToUpdate.vias.map((vx) => { + routeToUpdate.vias = routeToUpdate.vias.flatMap((vx) => { if (vx.x !== viaToRemove.x || vx.y !== viaToRemove.y) return vx replacedVia = true - return { x: viaKeep.x, y: viaKeep.y } + return viaKeep.mutable ? [{ x: viaKeep.x, y: viaKeep.y }] : [] }) if (!replacedVia) { throw new Error( diff --git a/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index c1a357754..a76c23b77 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -17,6 +17,25 @@ export interface EvaluateRelaxedDrcResult extends GetDrcErrorsResult { circuitJson: AnyCircuitElement[] } +/** + * Combines existing and newly routed copper. A routed trace with an existing + * PCB trace id is an intentional mutation and replaces that preloaded trace. + */ +export const combinePreloadedAndRoutedTraces = ( + preloadedTraces: SimplifiedPcbTrace[], + routedTraces: SimplifiedPcbTrace[], +): SimplifiedPcbTrace[] => { + const replacedTraceIds = new Set( + routedTraces.map((trace) => trace.pcb_trace_id), + ) + return [ + ...preloadedTraces.filter( + (trace) => !replacedTraceIds.has(trace.pcb_trace_id), + ), + ...routedTraces, + ] +} + /** Converts routed traces and evaluates them using the benchmark relaxed DRC. */ export const evaluateRelaxedDrc = ({ inputSrj, @@ -24,7 +43,10 @@ export const evaluateRelaxedDrc = ({ routedTraces, }: EvaluateRelaxedDrcInput): EvaluateRelaxedDrcResult => { const preloadedTraces = inputSrj.traces ?? [] - const jointTraces = [...preloadedTraces, ...routedTraces] + const jointTraces = combinePreloadedAndRoutedTraces( + preloadedTraces, + routedTraces, + ) const circuitJson = convertToCircuitJson(srjWithPointPairs, jointTraces, { minTraceWidth: inputSrj.minTraceWidth, minViaDiameter: inputSrj.minViaDiameter, diff --git a/package.json b/package.json index 9bb57c193..1a725d211 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "high-density-repair01": "git+https://github.com/tscircuit/high-density-repair01.git#cefc7be547ff727bd31573ac096b850da847af46", "high-density-repair02": "https://codeload.github.com/tscircuit/high-density-repair02/tar.gz/2afc0cbba3bf2f7eb6b9cd33615d21e9ad9352d4", "high-density-repair03": "git+https://github.com/tscircuit/high-density-repair03.git#5a963f132a640b38ec75d3ea2f72af2125636d3b", - "@tscircuit/high-density-a01": "git+https://github.com/tscircuit/high-density-a01.git#9a3a3d" + "@tscircuit/high-density-a01": "git+https://github.com/tscircuit/high-density-a01.git#9a3a3d", + "@tscircuit/high-density-b01": "git+https://github.com/tscircuit/high-density-b01.git#1309684" }, "dependencies": { "fast-json-stable-stringify": "^2.1.0", diff --git a/tests/evaluate-relaxed-drc-trace-replacement.test.ts b/tests/evaluate-relaxed-drc-trace-replacement.test.ts new file mode 100644 index 000000000..3231fd6c6 --- /dev/null +++ b/tests/evaluate-relaxed-drc-trace-replacement.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { combinePreloadedAndRoutedTraces } from "lib/testing/evaluate-relaxed-drc" +import type { SimplifiedPcbTrace } from "lib/types" + +const createTrace = (pcbTraceId: string, y: number): SimplifiedPcbTrace => ({ + type: "pcb_trace", + pcb_trace_id: pcbTraceId, + connection_name: pcbTraceId, + route: [ + { + route_type: "wire", + x: 0, + y, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 1, + y, + width: 0.1, + layer: "top", + }, + ], +}) + +test("joint DRC treats matching PCB trace ids as mutations", () => { + const original = createTrace("existing", 0) + const replacement = createTrace("existing", 1) + const newTrace = createTrace("new", 2) + + const jointTraces = combinePreloadedAndRoutedTraces( + [original], + [replacement, newTrace], + ) + + expect(jointTraces).toEqual([replacement, newTrace]) +}) diff --git a/tests/features/__snapshots__/pipeline9-srj23-solved-visual-circuit001.snap.svg b/tests/features/__snapshots__/pipeline9-srj23-solved-visual-circuit001.snap.svg index ad7548ee3..f9663b013 100644 --- a/tests/features/__snapshots__/pipeline9-srj23-solved-visual-circuit001.snap.svg +++ b/tests/features/__snapshots__/pipeline9-srj23-solved-visual-circuit001.snap.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/tests/features/__snapshots__/pipeline9-srj23-solved-visual-linux-circuit023.snap.svg b/tests/features/__snapshots__/pipeline9-srj23-solved-visual-linux-circuit023.snap.svg new file mode 100644 index 000000000..d5c8d9a5e --- /dev/null +++ b/tests/features/__snapshots__/pipeline9-srj23-solved-visual-linux-circuit023.snap.svg @@ -0,0 +1,297 @@ + \ No newline at end of file diff --git a/tests/features/pipeline9-high-density-regional-fallback.test.ts b/tests/features/pipeline9-high-density-regional-fallback.test.ts new file mode 100644 index 000000000..35bfb6675 --- /dev/null +++ b/tests/features/pipeline9-high-density-regional-fallback.test.ts @@ -0,0 +1,203 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { applyFixedRouteReplacementsToPreloadedTraces } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/apply-fixed-route-replacements-to-preloaded-traces" +import type { PreloadedHighDensityRoute } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/convert-preloaded-traces-to-hd-routes" +import { Pipeline9HighDensitySolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver" +import type { SimplifiedPcbTrace } from "lib/types" +import type { NodeWithPortPoints } from "lib/types/high-density-types" + +const node: NodeWithPortPoints = { + capacityMeshNodeId: "cmn_4", + center: { x: 3.4875, y: -5.2375 }, + width: 5.475, + height: 9.525, + availableZ: [0, 1], + portPoints: [ + { + x: 0.75, + y: -6, + z: 0, + connectionName: "source_net_0_mst0", + rootConnectionName: "connectivity_net3", + }, + { + x: 0.9875, + y: -0.475, + z: 0, + connectionName: "source_net_0_mst0", + rootConnectionName: "connectivity_net3", + }, + { + x: 0.75, + y: -1.90875, + z: 0, + connectionName: "source_net_3_mst1", + rootConnectionName: "connectivity_net0", + }, + { + x: 5, + y: -0.475, + z: 0, + connectionName: "source_net_3_mst1", + rootConnectionName: "connectivity_net0", + }, + { + x: 2.5, + y: -0.475, + z: 0, + connectionName: "source_net_0_mst2", + rootConnectionName: "connectivity_net3", + }, + { + x: 3.9625, + y: -0.475, + z: 1, + connectionName: "source_net_0_mst2", + rootConnectionName: "connectivity_net3", + }, + { + x: 0.75, + y: -4.1675, + z: 0, + connectionName: "source_net_2_mst2", + rootConnectionName: "connectivity_net1", + }, + { + x: 6.225, + y: -5.2375, + z: 0, + connectionName: "source_net_2_mst2", + rootConnectionName: "connectivity_net1", + }, + ], +} + +const fixedRoutes: PreloadedHighDensityRoute[] = [ + { + connectionName: "source_net_0_fixed_0_0", + rootConnectionName: "connectivity_net3", + traceThickness: 0.1, + viaDiameter: 0.3, + route: [ + { x: 0, y: -7.27, z: 0 }, + { x: 0.9539681227515293, y: -6.31603187724847, z: 0 }, + ], + vias: [], + preloadedTraceIndex: 0, + preloadedRouteIndex: 0, + }, + { + connectionName: "source_net_0_fixed_0_1", + rootConnectionName: "connectivity_net3", + traceThickness: 0.1, + viaDiameter: 0.3, + route: [ + { x: 0.9539681227515293, y: -6.31603187724847, z: 0 }, + { x: 0.9539681227515293, y: -0.19605561044583464, z: 0 }, + ], + vias: [], + preloadedTraceIndex: 0, + preloadedRouteIndex: 1, + }, + { + connectionName: "source_net_0_fixed_0_2", + rootConnectionName: "connectivity_net3", + traceThickness: 0.1, + viaDiameter: 0.3, + route: [ + { x: 0.9539681227515293, y: -0.19605561044583464, z: 0 }, + { x: 0.9539681227515293, y: -0.12896812275152936, z: 0 }, + ], + vias: [], + preloadedTraceIndex: 0, + preloadedRouteIndex: 2, + }, +] + +const preloadedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "source_net_0_mst0_0", + connection_name: "source_net_0", + route: fixedRoutes.flatMap((fixedRoute, routeIndex) => + fixedRoute.route.slice(routeIndex === 0 ? 0 : 1).map((point) => ({ + route_type: "wire" as const, + x: point.x, + y: point.y, + width: fixedRoute.traceThickness, + layer: "top", + })), + ), +} + +test("Pipeline9 falls back to regional rerouting and splices fixed traces", () => { + const solver = new Pipeline9HighDensitySolver({ + nodePortPoints: [node], + fixedHdRoutes: fixedRoutes, + connMap: new ConnectivityMap({ + connectivity_net0: ["source_net_3_mst1"], + connectivity_net1: ["source_net_2_mst2"], + connectivity_net3: [ + "source_net_0_mst0", + "source_net_0_mst2", + ...fixedRoutes.map((route) => route.connectionName), + ], + }), + obstacles: [], + layerCount: 2, + viaDiameter: 0.3, + traceWidth: 0.1, + obstacleMargin: 0.15, + effort: 1, + }) + + solver.solve() + + expect(solver.solved).toBeTrue() + expect(solver.failed).toBeFalse() + expect(solver.failedSolvers).toHaveLength(1) + expect(solver.stats).toMatchObject({ + fallbackNodeCount: 1, + reroutedFixedRouteCount: 2, + }) + expect(solver.routes).toHaveLength(4) + expect([...solver.fixedRouteReplacements.keys()]).toEqual([ + "source_net_0_fixed_0_0", + "source_net_0_fixed_0_1", + ]) + + const updatedFixedRoutes = solver.getUpdatedFixedHdRoutes() + expect(updatedFixedRoutes[0]!.route[0]).toEqual(fixedRoutes[0]!.route[0]) + expect(updatedFixedRoutes[0]!.route.at(-1)).toEqual( + fixedRoutes[0]!.route.at(-1), + ) + expect(updatedFixedRoutes[1]!.route[0]).toEqual(fixedRoutes[1]!.route[0]) + expect(updatedFixedRoutes[1]!.route.at(-1)).toEqual( + fixedRoutes[1]!.route.at(-1), + ) + expect(updatedFixedRoutes[2]).toBe(fixedRoutes[2]) + + const { updatedPreloadedTraces, mutatedPreloadedTraces } = + applyFixedRouteReplacementsToPreloadedTraces({ + originalTraces: [preloadedTrace], + originalFixedRoutes: fixedRoutes, + updatedFixedRoutes, + replacedConnectionNames: new Set(solver.fixedRouteReplacements.keys()), + layerCount: 2, + defaultViaHoleDiameter: 0.15, + obstacles: [], + connMap: solver.connMap, + }) + expect(mutatedPreloadedTraces).toHaveLength(1) + expect(updatedPreloadedTraces[0]!.pcb_trace_id).toBe( + preloadedTrace.pcb_trace_id, + ) + expect(updatedPreloadedTraces[0]!.route[0]).toMatchObject( + preloadedTrace.route[0]!, + ) + expect(updatedPreloadedTraces[0]!.route.at(-1)).toMatchObject( + preloadedTrace.route.at(-1)!, + ) + expect(updatedPreloadedTraces[0]!.route.length).toBeGreaterThan( + preloadedTrace.route.length, + ) +}) diff --git a/tests/features/pipeline9-minimal-preloaded-trace-graph.test.ts b/tests/features/pipeline9-minimal-preloaded-trace-graph.test.ts index 8ec4f9c0d..34b67d8ce 100644 --- a/tests/features/pipeline9-minimal-preloaded-trace-graph.test.ts +++ b/tests/features/pipeline9-minimal-preloaded-trace-graph.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { AutoroutingPipelineSolver7_MultiGraph } from "lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" +import { Pipeline9HighDensitySolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver" import type { SimpleRouteJson } from "lib/types" import scenario from "./preexisting-connected-traces/srj/preexisting-connected-traces06.srj.json" with { type: "json", @@ -72,7 +73,6 @@ test("Pipeline9 owns copied stages with minimal preloaded-trace changes", () => ) expect(solver.pipelineDef).toHaveLength(pipeline7.pipelineDef.length + 1) for (const stageName of [ - "highDensityRouteSolver", "highDensityRepairSolver", "highDensityStitchSolver", "globalDrcForceImproveSolver", @@ -80,9 +80,14 @@ test("Pipeline9 owns copied stages with minimal preloaded-trace changes", () => ]) { expect( solver.pipelineDef.find((step) => step.solverName === stageName) - ?.solverClass, + ?.solverClass as unknown, ).toBe(pipeline7Stages.get(stageName)) } + expect( + solver.pipelineDef.find( + (step) => step.solverName === "highDensityRouteSolver", + )?.solverClass, + ).toBe(Pipeline9HighDensitySolver) solver.solve() @@ -110,4 +115,8 @@ test("Pipeline9 owns copied stages with minimal preloaded-trace changes", () => ), ), ).toBe(false) + const outputTraceIds = solver + .getOutputSimplifiedPcbTraces() + .map((trace) => trace.pcb_trace_id) + expect(new Set(outputTraceIds).size).toBe(outputTraceIds.length) }) diff --git a/tests/features/pipeline9-srj23-sample10-regional-fallback.test.ts b/tests/features/pipeline9-srj23-sample10-regional-fallback.test.ts new file mode 100644 index 000000000..f8a5b0b78 --- /dev/null +++ b/tests/features/pipeline9-srj23-sample10-regional-fallback.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test("Pipeline9 solves SRJ23 sample 10 by rerouting only failed B01 regions", async () => { + const { scenario } = await loadScenarioBySampleNumber("srj23", 10) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + structuredClone(scenario), + { + cacheProvider: null, + effort: 1, + }, + ) + + solver.solve() + + expect(solver.solved).toBeTrue() + expect(solver.failed).toBeFalse() + expect( + Number(solver.highDensityRouteSolver?.stats.fallbackNodeCount), + ).toBeGreaterThan(0) + expect(solver.getMutatedPreloadedTraces().length).toBeGreaterThan(0) + + const { errors } = evaluateRelaxedDrc({ + inputSrj: scenario, + srjWithPointPairs: solver.srjWithPointPairs!, + routedTraces: solver.getOutputSimplifiedPcbTraces(), + }) + expect(errors).toHaveLength(0) +}) diff --git a/tests/features/pipeline9-srj23-solved-visual.test.ts b/tests/features/pipeline9-srj23-solved-visual.test.ts index ac733792e..d2db8da97 100644 --- a/tests/features/pipeline9-srj23-solved-visual.test.ts +++ b/tests/features/pipeline9-srj23-solved-visual.test.ts @@ -1,12 +1,14 @@ -import { expect, test } from "bun:test" +import { expect, setDefaultTimeout, test } from "bun:test" import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" import { getLastStepSvg } from "../fixtures/getLastStepSvg" const SAMPLE_NUMBERS = [1, 10, 23] -test("Pipeline9 visually solves representative SRJ23 samples", async () => { - for (const sampleNumber of SAMPLE_NUMBERS) { +setDefaultTimeout(180_000) + +for (const sampleNumber of SAMPLE_NUMBERS) { + test(`Pipeline9 visually solves SRJ23 sample ${sampleNumber}`, async () => { const { scenario, scenarioName } = await loadScenarioBySampleNumber( "srj23", sampleNumber, @@ -26,11 +28,16 @@ test("Pipeline9 visually solves representative SRJ23 samples", async () => { expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + const snapshotPath = + process.platform === "linux" && [10, 23].includes(sampleNumber) + ? import.meta.path.replace(/\.test\.ts$/, "-linux.test.ts") + : import.meta.path + await expect(getLastStepSvg(solver.visualize())).toMatchSvgSnapshot( - import.meta.path, + snapshotPath, { svgName: scenarioName, }, ) - } -}) + }) +} diff --git a/tests/solvers/same-net-via-merger-immutable-anchor.test.ts b/tests/solvers/same-net-via-merger-immutable-anchor.test.ts new file mode 100644 index 000000000..f354c721e --- /dev/null +++ b/tests/solvers/same-net-via-merger-immutable-anchor.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { SameNetViaMergerSolver } from "lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver" +import type { HighDensityRoute } from "lib/types/high-density-types" + +const makeViaRoute = ({ + connectionName, + rootConnectionName, + x, +}: { + connectionName: string + rootConnectionName: string + x: number +}): HighDensityRoute => ({ + connectionName, + rootConnectionName, + traceThickness: 0.1, + viaDiameter: 0.3, + route: [ + { x: x - 0.5, y: 0, z: 0 }, + { x, y: 0, z: 0 }, + { x, y: 0, z: 1 }, + { x: x + 0.5, y: 0, z: 1 }, + ], + vias: [{ x, y: 0 }], +}) + +test("same-net via merging reuses an immutable via without mutating it", () => { + const editableRoute = makeViaRoute({ + connectionName: "editable", + rootConnectionName: "net0", + x: 0.02, + }) + const immutableRoute = makeViaRoute({ + connectionName: "preloaded_fixed_0", + rootConnectionName: "net0", + x: 0, + }) + const immutableSnapshot = structuredClone(immutableRoute) + const solver = new SameNetViaMergerSolver({ + inputHdRoutes: [editableRoute], + otherHdRoutes: [immutableRoute], + obstacles: [], + colorMap: {}, + layerCount: 2, + connMap: new ConnectivityMap({ + net0: ["editable"], + }), + }) + + solver.solve() + + expect(solver.failed).toBeFalse() + const [mergedRoute] = solver.getMergedViaHdRoutes()! + expect(mergedRoute!.vias).toHaveLength(0) + expect( + mergedRoute!.route.filter( + (point, pointIndex) => + pointIndex > 0 && point.z !== mergedRoute!.route[pointIndex - 1]!.z, + ), + ).toEqual([{ x: 0, y: 0, z: 1 }]) + expect(immutableRoute).toEqual(immutableSnapshot) +})