diff --git a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/AssignableAutoroutingPipeline2.ts b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/AssignableAutoroutingPipeline2.ts index 5daeadd4e..e5f37ad96 100644 --- a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/AssignableAutoroutingPipeline2.ts +++ b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/AssignableAutoroutingPipeline2.ts @@ -34,6 +34,7 @@ import { MultipleHighDensityRouteStitchSolver } from "../../solvers/RouteStitchi import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger/SingleLayerNodeMergerSolver" import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver" import { TraceKeepoutSolver } from "../../solvers/TraceKeepoutSolver/TraceKeepoutSolver" +import { TraceMarginDrcRepairSolver } from "../../solvers/TraceMarginDrcRepairSolver/TraceMarginDrcRepairSolver" import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver" import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver" import { getColorMap } from "../../solvers/colors" @@ -117,6 +118,7 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver { traceSimplificationSolver?: TraceSimplificationSolver traceKeepoutSolver?: TraceKeepoutSolver traceWidthSolver?: TraceWidthSolver + traceMarginDrcRepairSolver?: TraceMarginDrcRepairSolver availableSegmentPointSolver?: AvailableSegmentPointSolver portPointPathingSolver?: PortPointPathingSolver multiSectionPortPointOptimizer?: MultiSectionPortPointOptimizer @@ -391,6 +393,22 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver { layerCount: cms.srj.layerCount, }, ]), + definePipelineStep( + "traceMarginDrcRepairSolver", + TraceMarginDrcRepairSolver, + (cms) => [ + { + hdRoutes: + cms.traceWidthSolver?.hdRoutesWithWidths ?? + cms.traceKeepoutSolver?.redrawnHdRoutes ?? + [], + srj: cms.srjWithPointPairs ?? cms.srj, + minTraceWidth: cms.minTraceWidth, + obstacleMargin: cms.srj.defaultObstacleMargin ?? 0.15, + colorMap: cms.colorMap, + }, + ], + ), ] constructor( @@ -504,6 +522,7 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver { const traceSimplificationViz = this.traceSimplificationSolver?.visualize() const traceKeepoutViz = this.traceKeepoutSolver?.visualize() const traceWidthViz = this.traceWidthSolver?.visualize() + const traceMarginDrcRepairViz = this.traceMarginDrcRepairSolver?.visualize() const problemOutline = this.srj.outline const problemLines: Line[] = [] @@ -595,6 +614,7 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver { traceSimplificationViz, traceKeepoutViz, traceWidthViz, + traceMarginDrcRepairViz, this.solved ? combineVisualizations( problemViz, @@ -662,6 +682,7 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver { _getOutputHdRoutes(): HighDensityRoute[] { return ( + this.traceMarginDrcRepairSolver?.repairedHdRoutes ?? this.traceWidthSolver?.hdRoutesWithWidths ?? this.traceKeepoutSolver?.redrawnHdRoutes ?? this.traceSimplificationSolver?.simplifiedHdRoutes ?? diff --git a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/JumperHighDensitySolver.ts b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/JumperHighDensitySolver.ts index b724a2438..4218dcb01 100644 --- a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/JumperHighDensitySolver.ts +++ b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/JumperHighDensitySolver.ts @@ -40,10 +40,12 @@ export type UnifiedHighDensityRoute = */ function convertJumperRouteToStandard( route: HighDensityIntraNodeRouteWithJumpers, + capacityMeshNodeId: string, ): HighDensityIntraNodeRoute & { jumpers?: HighDensityIntraNodeRouteWithJumpers["jumpers"] } { return { + capacityMeshNodeId, connectionName: route.connectionName, rootConnectionName: route.rootConnectionName, traceThickness: route.traceThickness, @@ -473,7 +475,12 @@ export class JumperHighDensitySolver extends BaseSolver { if (currentSolver.solved) { // Convert jumper routes to unified format and collect for (const jumperRoute of currentSolver.solvedRoutes) { - this.routes.push(convertJumperRouteToStandard(jumperRoute)) + this.routes.push( + convertJumperRouteToStandard( + jumperRoute, + currentSolver.nodeWithPortPoints.capacityMeshNodeId, + ), + ) } // Collect all jumpers from the solver (SRJ format with connectedTo populated) diff --git a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/SimpleHighDensitySolver.ts b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/SimpleHighDensitySolver.ts index f05229477..d338779ba 100644 --- a/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/SimpleHighDensitySolver.ts +++ b/lib/autorouter-pipelines/AssignableAutoroutingPipeline2/SimpleHighDensitySolver.ts @@ -611,6 +611,8 @@ export class SimpleHighDensitySolver extends BaseSolver { } _finalizeRoutesForCurrentNode() { + const node = this.currentNode! + for (const routeInProgress of this.routesInProgress) { const { connectionName, @@ -667,6 +669,7 @@ export class SimpleHighDensitySolver extends BaseSolver { routePointList.push({ x: endPoint.x, y: endPoint.y, z: endPoint.z }) const route: HighDensityIntraNodeRoute = { + capacityMeshNodeId: node.capacityMeshNodeId, connectionName, rootConnectionName, traceThickness: this.traceWidth, diff --git a/lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver2_PortPointPathing.ts b/lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver2_PortPointPathing.ts index ec87d7842..74070e657 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver2_PortPointPathing.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver2_PortPointPathing.ts @@ -21,6 +21,7 @@ import { CapacityMeshNodeSolver2_NodeUnderObstacle } from "../../solvers/Capacit import { CapacityNodeTargetMerger } from "../../solvers/CapacityNodeTargetMerger/CapacityNodeTargetMerger" import { DeadEndSolver } from "../../solvers/DeadEndSolver/DeadEndSolver" import { HighDensitySolver } from "../../solvers/HighDensitySolver/HighDensitySolver" +import { HighDensityRepairSolver } from "../../solvers/HighDensityRepairSolver/HighDensityRepairSolver" import { MultiSectionPortPointOptimizer } from "../../solvers/MultiSectionPortPointOptimizer" import { NetToPointPairsSolver } from "../../solvers/NetToPointPairsSolver/NetToPointPairsSolver" import { NetToPointPairsSolver2_OffBoardConnection } from "../../solvers/NetToPointPairsSolver2_OffBoardConnection/NetToPointPairsSolver2_OffBoardConnection" @@ -37,6 +38,7 @@ import { MultipleHighDensityRouteStitchSolver } from "../../solvers/RouteStitchi import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger/SingleLayerNodeMergerSolver" import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver" import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver" +import { TraceMarginDrcRepairSolver } from "../../solvers/TraceMarginDrcRepairSolver/TraceMarginDrcRepairSolver" import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver" import { getColorMap } from "../../solvers/colors" import type { @@ -102,6 +104,7 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { edgeSolver?: CapacityMeshEdgeSolver colorMap: Record highDensityRouteSolver?: HighDensitySolver + highDensityRepairSolver?: HighDensityRepairSolver highDensityStitchSolver?: MultipleHighDensityRouteStitchSolver singleLayerNodeMerger?: SingleLayerNodeMergerSolver strawSolver?: StrawSolver @@ -112,6 +115,7 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { multiSectionPortPointOptimizer?: MultiSectionPortPointOptimizer uniformPortDistributionSolver?: UniformPortDistributionSolver traceWidthSolver?: TraceWidthSolver + traceMarginDrcRepairSolver?: TraceMarginDrcRepairSolver viaDiameter: number minTraceWidth: number effort: number @@ -349,13 +353,31 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { effort: cms.effort, }, ]), + definePipelineStep( + "highDensityRepairSolver", + HighDensityRepairSolver, + (cms) => [ + { + nodePortPoints: + cms.uniformPortDistributionSolver?.getOutput() ?? + cms.multiSectionPortPointOptimizer?.getNodesWithPortPoints() ?? + cms.portPointPathingSolver?.getNodesWithPortPoints() ?? + [], + obstacles: cms.srj.obstacles, + hdRoutes: cms.highDensityRouteSolver!.routes, + connMap: cms.connMap, + }, + ], + ), definePipelineStep( "highDensityStitchSolver", MultipleHighDensityRouteStitchSolver, (cms) => [ { connections: cms.srjWithPointPairs!.connections, - hdRoutes: cms.highDensityRouteSolver!.routes, + hdRoutes: + cms.highDensityRepairSolver?.repairedHdRoutes ?? + cms.highDensityRouteSolver!.routes, colorMap: cms.colorMap, layerCount: cms.srj.layerCount, defaultViaDiameter: cms.viaDiameter, @@ -391,6 +413,22 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { }, ] }), + definePipelineStep( + "traceMarginDrcRepairSolver", + TraceMarginDrcRepairSolver, + (cms) => [ + { + hdRoutes: + cms.traceWidthSolver?.getHdRoutesWithWidths() ?? + cms.traceSimplificationSolver?.simplifiedHdRoutes ?? + cms.highDensityStitchSolver!.mergedHdRoutes, + srj: cms.srjWithPointPairs ?? cms.srj, + minTraceWidth: cms.minTraceWidth, + obstacleMargin: cms.srj.defaultObstacleMargin ?? 0.15, + colorMap: cms.colorMap, + }, + ], + ), ] constructor( @@ -499,8 +537,10 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { const uniformPortDistributionViz = this.uniformPortDistributionSolver?.visualize() const highDensityViz = this.highDensityRouteSolver?.visualize() + const highDensityRepairViz = this.highDensityRepairSolver?.visualize() const highDensityStitchViz = this.highDensityStitchSolver?.visualize() const traceSimplificationViz = this.traceSimplificationSolver?.visualize() + const traceMarginDrcRepairViz = this.traceMarginDrcRepairSolver?.visualize() const problemOutline = this.srj.outline const problemLines: Line[] = [] @@ -574,8 +614,10 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { multiSectionOptViz, uniformPortDistributionViz, highDensityViz ? combineVisualizations(problemViz, highDensityViz) : null, + highDensityRepairViz, highDensityStitchViz, traceSimplificationViz, + traceMarginDrcRepairViz, this.solved ? combineVisualizations( problemViz, @@ -597,6 +639,26 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { * 3. High Density Route Solver Output, max 200 lines */ preview(): GraphicsObject { + if (this.highDensityRepairSolver) { + const lines: Line[] = [] + for ( + let i = this.highDensityRepairSolver.repairedHdRoutes.length - 1; + i >= 0; + i-- + ) { + const route = this.highDensityRepairSolver.repairedHdRoutes[i] + lines.push({ + points: route.route.map((n) => ({ + x: n.x, + y: n.y, + })), + strokeColor: this.colorMap[route.connectionName], + }) + if (lines.length > 200) break + } + return { lines } + } + if (this.highDensityRouteSolver) { const lines: Line[] = [] for (let i = this.highDensityRouteSolver.routes.length - 1; i >= 0; i--) { @@ -639,6 +701,7 @@ export class AutoroutingPipelineSolver2_PortPointPathing extends BaseSolver { _getOutputHdRoutes(): HighDensityRoute[] { return ( + this.traceMarginDrcRepairSolver?.repairedHdRoutes ?? this.traceWidthSolver?.getHdRoutesWithWidths() ?? this.traceSimplificationSolver?.simplifiedHdRoutes ?? this.highDensityStitchSolver!.mergedHdRoutes diff --git a/lib/data-structures/HighDensityRouteSpatialIndex.ts b/lib/data-structures/HighDensityRouteSpatialIndex.ts index 9e9576cb4..0c56d8ca1 100644 --- a/lib/data-structures/HighDensityRouteSpatialIndex.ts +++ b/lib/data-structures/HighDensityRouteSpatialIndex.ts @@ -1,5 +1,13 @@ import { doSegmentsIntersect, Point3 } from "@tscircuit/math-utils" // Assuming this is available and correct -import type { Jumper } from "lib/types/high-density-types" +import type { + HighDensityIntraNodeRoute, + HighDensityRoute, + Jumper, +} from "lib/types/high-density-types" +export type { + HighDensityIntraNodeRoute, + HighDensityRoute, +} from "lib/types/high-density-types" // --- Interfaces and Types (Unchanged) --- @@ -13,17 +21,6 @@ type Point2D = { x: number; y: number } // Use Point2D for clarity in calculatio type Segment = [Point, Point] -export type HighDensityIntraNodeRoute = { - connectionName: string // Assuming this is unique per route - rootConnectionName?: string // Parent connection for merged routes - traceThickness: number - viaDiameter: number // Now used in conflict calculation - route: Array<{ x: number; y: number; z: number; insideJumperPad?: boolean }> - vias: Array<{ x: number; y: number }> // Will be indexed - jumpers?: Jumper[] -} -export type HighDensityRoute = HighDensityIntraNodeRoute - // --- Utility Functions (Unchanged) --- const getSegmentBounds = (segment: Segment) => { diff --git a/lib/solvers/CurvyIntraNodeSolver/CurvyIntraNodeSolver.ts b/lib/solvers/CurvyIntraNodeSolver/CurvyIntraNodeSolver.ts index 2972c67c3..bcb245705 100644 --- a/lib/solvers/CurvyIntraNodeSolver/CurvyIntraNodeSolver.ts +++ b/lib/solvers/CurvyIntraNodeSolver/CurvyIntraNodeSolver.ts @@ -188,6 +188,7 @@ export class CurvyIntraNodeSolver extends BaseSolver { if (!info) continue const route: HighDensityIntraNodeRoute = { + capacityMeshNodeId: node.capacityMeshNodeId, connectionName: info.connectionName, rootConnectionName: info.rootConnectionName, traceThickness: this.traceWidth, diff --git a/lib/solvers/FixedTopologyHighDensityIntraNodeSolver/FixedTopologyHighDensityIntraNodeSolver.ts b/lib/solvers/FixedTopologyHighDensityIntraNodeSolver/FixedTopologyHighDensityIntraNodeSolver.ts index fb4668a7f..0bdb814ef 100644 --- a/lib/solvers/FixedTopologyHighDensityIntraNodeSolver/FixedTopologyHighDensityIntraNodeSolver.ts +++ b/lib/solvers/FixedTopologyHighDensityIntraNodeSolver/FixedTopologyHighDensityIntraNodeSolver.ts @@ -546,6 +546,7 @@ export class FixedTopologyHighDensityIntraNodeSolver extends BaseSolver { })) this.solvedRoutes.push({ + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName, rootConnectionName, traceThickness: this.traceWidth, diff --git a/lib/solvers/HighDensityRepairSolver/HighDensityRepairSolver.ts b/lib/solvers/HighDensityRepairSolver/HighDensityRepairSolver.ts new file mode 100644 index 000000000..094b45112 --- /dev/null +++ b/lib/solvers/HighDensityRepairSolver/HighDensityRepairSolver.ts @@ -0,0 +1,212 @@ +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { GraphicsObject } from "graphics-debug" +import { ObstacleSpatialHashIndex } from "lib/data-structures/ObstacleTree" +import { BaseSolver } from "lib/solvers/BaseSolver" +import type { + HighDensityIntraNodeRoute, + NodeWithPortPoints, +} from "lib/types/high-density-types" +import type { Obstacle } from "lib/types/srj-types" +import { getBoundsFromNodeWithPortPoints } from "lib/utils/getBoundsFromNodeWithPortPoints" +import { + SingleNodeHighDensityRepair, + SingleNodeHighDensityRepairParams, +} from "./SingleNodeHighDensityRepair" + +export interface HighDensityRepairSolverParams { + nodePortPoints: NodeWithPortPoints[] + obstacles: Obstacle[] + hdRoutes: HighDensityIntraNodeRoute[] + connMap: ConnectivityMap +} + +export const SINGLE_NODE_REPAIR_ADJACENT_OBSTACLE_MARGIN_MM = 0.1 + +export const createNodeHdRoutesByNodeId = ( + hdRoutes: HighDensityIntraNodeRoute[], +): Map => { + const nodeHdRoutesByNodeId = new Map() + + for (const hdRoute of hdRoutes) { + const nodeRoutes = + nodeHdRoutesByNodeId.get(hdRoute.capacityMeshNodeId) ?? [] + nodeRoutes.push(hdRoute) + nodeHdRoutesByNodeId.set(hdRoute.capacityMeshNodeId, nodeRoutes) + } + + return nodeHdRoutesByNodeId +} + +const getObstacleBounds = (obstacle: Obstacle) => ({ + minX: obstacle.center.x - obstacle.width / 2, + minY: obstacle.center.y - obstacle.height / 2, + maxX: obstacle.center.x + obstacle.width / 2, + maxY: obstacle.center.y + obstacle.height / 2, +}) + +const getRectDistance = ( + a: { minX: number; minY: number; maxX: number; maxY: number }, + b: { minX: number; minY: number; maxX: number; maxY: number }, +) => { + const dx = Math.max(a.minX - b.maxX, b.minX - a.maxX, 0) + const dy = Math.max(a.minY - b.maxY, b.minY - a.maxY, 0) + return Math.hypot(dx, dy) +} + +export const getAdjacentObstaclesForNode = ( + nodeWithPortPoints: NodeWithPortPoints, + obstacleIndex: ObstacleSpatialHashIndex, + adjacentObstacleMarginMm: number = SINGLE_NODE_REPAIR_ADJACENT_OBSTACLE_MARGIN_MM, +): Obstacle[] => { + const bounds = getBoundsFromNodeWithPortPoints(nodeWithPortPoints) + const candidates = obstacleIndex.search({ + minX: bounds.minX - adjacentObstacleMarginMm, + minY: bounds.minY - adjacentObstacleMarginMm, + maxX: bounds.maxX + adjacentObstacleMarginMm, + maxY: bounds.maxY + adjacentObstacleMarginMm, + }) + + return candidates.filter((obstacle) => { + const obstacleBounds = getObstacleBounds(obstacle) + return getRectDistance(bounds, obstacleBounds) <= adjacentObstacleMarginMm + }) +} + +export const createSingleNodeHighDensityRepairParams = ({ + nodeWithPortPoints, + obstacleIndex, + nodeHdRoutesByNodeId, + connMap, + adjacentObstacleMarginMm = SINGLE_NODE_REPAIR_ADJACENT_OBSTACLE_MARGIN_MM, +}: { + nodeWithPortPoints: NodeWithPortPoints + obstacleIndex: ObstacleSpatialHashIndex + nodeHdRoutesByNodeId: Map + connMap: ConnectivityMap + adjacentObstacleMarginMm?: number +}): SingleNodeHighDensityRepairParams => { + return { + nodeWithPortPoints, + adjacentObstacles: getAdjacentObstaclesForNode( + nodeWithPortPoints, + obstacleIndex, + adjacentObstacleMarginMm, + ), + nodeHdRoutes: + nodeHdRoutesByNodeId.get(nodeWithPortPoints.capacityMeshNodeId) ?? [], + connMap, + } +} + +export const createSingleNodeHighDensityRepairParamsList = ({ + nodePortPoints, + obstacles, + hdRoutes, + connMap, + adjacentObstacleMarginMm = SINGLE_NODE_REPAIR_ADJACENT_OBSTACLE_MARGIN_MM, +}: HighDensityRepairSolverParams & { + adjacentObstacleMarginMm?: number +}): SingleNodeHighDensityRepairParams[] => { + const obstacleIndex = new ObstacleSpatialHashIndex("flatbush", obstacles) + const nodeHdRoutesByNodeId = createNodeHdRoutesByNodeId(hdRoutes) + + return nodePortPoints.map((nodeWithPortPoints) => + createSingleNodeHighDensityRepairParams({ + nodeWithPortPoints, + obstacleIndex, + nodeHdRoutesByNodeId, + connMap, + adjacentObstacleMarginMm, + }), + ) +} + +export class HighDensityRepairSolver extends BaseSolver { + override getSolverName(): string { + return "HighDensityRepairSolver" + } + + unprocessedNodes: NodeWithPortPoints[] + repairedHdRoutes: HighDensityIntraNodeRoute[] + activeSubSolver: SingleNodeHighDensityRepair | null = null + obstacleIndex: ObstacleSpatialHashIndex + nodeHdRoutesByNodeId: Map + + constructor(public readonly params: HighDensityRepairSolverParams) { + super() + this.unprocessedNodes = [...params.nodePortPoints] + this.repairedHdRoutes = [] + this.obstacleIndex = new ObstacleSpatialHashIndex( + "flatbush", + params.obstacles, + ) + this.nodeHdRoutesByNodeId = createNodeHdRoutesByNodeId(params.hdRoutes) + this.MAX_ITERATIONS = Math.max(1000, this.unprocessedNodes.length * 4) + } + + getConstructorParams(): [HighDensityRepairSolverParams] { + return [this.params] + } + + private getSingleNodeParams( + nodeWithPortPoints: NodeWithPortPoints, + ): SingleNodeHighDensityRepairParams { + return createSingleNodeHighDensityRepairParams({ + nodeWithPortPoints, + obstacleIndex: this.obstacleIndex, + nodeHdRoutesByNodeId: this.nodeHdRoutesByNodeId, + connMap: this.params.connMap, + }) + } + + _step() { + if (this.activeSubSolver) { + this.activeSubSolver.step() + + if (this.activeSubSolver.solved) { + this.repairedHdRoutes.push(...this.activeSubSolver.repairedNodeHdRoutes) + this.activeSubSolver = null + } else if (this.activeSubSolver.failed) { + this.error = this.activeSubSolver.error + this.failed = true + } + return + } + + const nodeToRepair = this.unprocessedNodes.shift() + if (!nodeToRepair) { + this.solved = true + return + } + + this.activeSubSolver = new SingleNodeHighDensityRepair( + this.getSingleNodeParams(nodeToRepair), + ) + } + + visualize(): GraphicsObject { + const repairedLines = this.repairedHdRoutes.map((route) => ({ + points: route.route.map((point) => ({ x: point.x, y: point.y })), + strokeColor: "rgba(0, 180, 120, 0.65)", + strokeWidth: route.traceThickness, + label: `${route.capacityMeshNodeId}:${route.connectionName}`, + })) + + if (this.activeSubSolver) { + const activeViz = this.activeSubSolver.visualize() + return { + lines: [...repairedLines, ...(activeViz.lines ?? [])], + points: activeViz.points ?? [], + rects: activeViz.rects ?? [], + circles: activeViz.circles ?? [], + } + } + + return { + lines: repairedLines, + points: [], + rects: [], + circles: [], + } + } +} diff --git a/lib/solvers/HighDensityRepairSolver/SingleNodeHighDensityRepair.ts b/lib/solvers/HighDensityRepairSolver/SingleNodeHighDensityRepair.ts new file mode 100644 index 000000000..6309fe972 --- /dev/null +++ b/lib/solvers/HighDensityRepairSolver/SingleNodeHighDensityRepair.ts @@ -0,0 +1,96 @@ +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver" +import type { + HighDensityIntraNodeRoute, + NodeWithPortPoints, +} from "lib/types/high-density-types" +import type { Obstacle } from "lib/types/srj-types" + +export interface SingleNodeHighDensityRepairParams { + nodeWithPortPoints: NodeWithPortPoints + adjacentObstacles: Obstacle[] + nodeHdRoutes: HighDensityIntraNodeRoute[] + connMap: ConnectivityMap +} + +export class SingleNodeHighDensityRepair extends BaseSolver { + override getSolverName(): string { + return "SingleNodeHighDensityRepair" + } + + nodeWithPortPoints: NodeWithPortPoints + adjacentObstacles: Obstacle[] + nodeHdRoutes: HighDensityIntraNodeRoute[] + connMap: ConnectivityMap + repairedNodeHdRoutes: HighDensityIntraNodeRoute[] + + constructor(params: SingleNodeHighDensityRepairParams) { + super() + this.nodeWithPortPoints = params.nodeWithPortPoints + this.adjacentObstacles = params.adjacentObstacles + this.nodeHdRoutes = params.nodeHdRoutes + this.connMap = params.connMap + this.repairedNodeHdRoutes = [] + this.MAX_ITERATIONS = 10 + } + + getConstructorParams(): [SingleNodeHighDensityRepairParams] { + return [ + { + nodeWithPortPoints: this.nodeWithPortPoints, + adjacentObstacles: this.adjacentObstacles, + nodeHdRoutes: this.nodeHdRoutes, + connMap: this.connMap, + }, + ] + } + + _step() { + this.repairedNodeHdRoutes = this.nodeHdRoutes.map((route) => ({ + ...route, + route: route.route.map((point) => ({ ...point })), + vias: route.vias.map((via) => ({ ...via })), + jumpers: route.jumpers?.map((jumper) => ({ + ...jumper, + start: { ...jumper.start }, + end: { ...jumper.end }, + })), + })) + this.solved = true + } + + visualize(): GraphicsObject { + const node = this.nodeWithPortPoints + const graphics: GraphicsObject = { + rects: [ + { + center: node.center, + width: node.width, + height: node.height, + fill: "rgba(0, 160, 255, 0.08)", + stroke: "rgba(0, 160, 255, 0.6)", + label: node.capacityMeshNodeId, + }, + ...this.adjacentObstacles.map((obstacle) => ({ + center: obstacle.center, + width: obstacle.width, + height: obstacle.height, + fill: "rgba(255, 0, 0, 0.08)", + stroke: "rgba(255, 0, 0, 0.3)", + label: obstacle.obstacleId, + })), + ], + lines: this.repairedNodeHdRoutes.map((route) => ({ + points: route.route.map((point) => ({ x: point.x, y: point.y })), + strokeColor: "rgba(0, 160, 255, 0.85)", + strokeWidth: route.traceThickness, + label: route.connectionName, + })), + points: [], + circles: [], + } + + return graphics + } +} diff --git a/lib/solvers/HighDensitySolver/HighDensitySolver.ts b/lib/solvers/HighDensitySolver/HighDensitySolver.ts index 9e951892b..00990176f 100644 --- a/lib/solvers/HighDensitySolver/HighDensitySolver.ts +++ b/lib/solvers/HighDensitySolver/HighDensitySolver.ts @@ -68,7 +68,7 @@ export class HighDensitySolver extends BaseSolver { | Record }) { super() - this.unsolvedNodePortPoints = nodePortPoints + this.unsolvedNodePortPoints = [...nodePortPoints] this.colorMap = colorMap ?? {} this.connMap = connMap this.routes = [] diff --git a/lib/solvers/HighDensitySolver/IntraNodeSolver.ts b/lib/solvers/HighDensitySolver/IntraNodeSolver.ts index 72fd95aa3..cd4974de6 100644 --- a/lib/solvers/HighDensitySolver/IntraNodeSolver.ts +++ b/lib/solvers/HighDensitySolver/IntraNodeSolver.ts @@ -206,6 +206,7 @@ export class IntraNodeRouteSolver extends BaseSolver { ) this.solvedRoutes.push({ + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: unsolvedConnection.connectionName, traceThickness: this.traceWidth, viaDiameter: this.viaDiameter, @@ -218,6 +219,7 @@ export class IntraNodeRouteSolver extends BaseSolver { const { connectionName, points } = unsolvedConnection this.activeSubSolver = new SingleHighDensityRouteSolver6_VertHorzLayer_FutureCost({ + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName, minDistBetweenEnteringPoints: this.minDistBetweenEnteringPoints, bounds: getBoundsFromNodeWithPortPoints(this.nodeWithPortPoints), diff --git a/lib/solvers/HighDensitySolver/MultiHeadPolyLineIntraNodeSolver/MultiHeadPolyLineIntraNodeSolver.ts b/lib/solvers/HighDensitySolver/MultiHeadPolyLineIntraNodeSolver/MultiHeadPolyLineIntraNodeSolver.ts index 3691b73fc..f1a96a04e 100644 --- a/lib/solvers/HighDensitySolver/MultiHeadPolyLineIntraNodeSolver/MultiHeadPolyLineIntraNodeSolver.ts +++ b/lib/solvers/HighDensitySolver/MultiHeadPolyLineIntraNodeSolver/MultiHeadPolyLineIntraNodeSolver.ts @@ -1357,6 +1357,7 @@ export class MultiHeadPolyLineIntraNodeSolver extends BaseSolver { // TODO: Optimize the route points (remove collinear points on the same layer) solvedRoutes.push({ + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: polyLine.connectionName, traceThickness: this.traceWidth, viaDiameter: this.viaDiameter, diff --git a/lib/solvers/HighDensitySolver/SingleHighDensityRouteSolver.ts b/lib/solvers/HighDensitySolver/SingleHighDensityRouteSolver.ts index 76273e103..91d1e4ef7 100644 --- a/lib/solvers/HighDensitySolver/SingleHighDensityRouteSolver.ts +++ b/lib/solvers/HighDensitySolver/SingleHighDensityRouteSolver.ts @@ -49,6 +49,7 @@ export class SingleHighDensityRouteSolver extends BaseSolver { candidates: SingleRouteCandidatePriorityQueue connectionName: string + capacityMeshNodeId: string solvedPath: HighDensityIntraNodeRoute | null = null futureConnections: FutureConnection[] @@ -72,6 +73,7 @@ export class SingleHighDensityRouteSolver extends BaseSolver { constructor(opts: { connectionName: string + capacityMeshNodeId?: string obstacleRoutes: HighDensityIntraNodeRoute[] minDistBetweenEnteringPoints: number bounds: { minX: number; maxX: number; minY: number; maxY: number } @@ -99,6 +101,7 @@ export class SingleHighDensityRouteSolver extends BaseSolver { y: (this.bounds.minY + this.bounds.maxY) / 2, } this.connectionName = opts.connectionName + this.capacityMeshNodeId = opts.capacityMeshNodeId ?? opts.connectionName this.obstacleRoutes = opts.obstacleRoutes this.A = opts.A this.B = opts.B @@ -198,6 +201,7 @@ export class SingleHighDensityRouteSolver extends BaseSolver { B, ] this.solvedPath = { + capacityMeshNodeId: this.capacityMeshNodeId, connectionName: this.connectionName, route, traceThickness: this.traceThickness, @@ -503,6 +507,7 @@ export class SingleHighDensityRouteSolver extends BaseSolver { } this.solvedPath = { + capacityMeshNodeId: this.capacityMeshNodeId, connectionName: this.connectionName, traceThickness: this.traceThickness, viaDiameter: this.viaDiameter, diff --git a/lib/solvers/HighDensitySolver/SingleTransitionIntraNodeSolver.ts b/lib/solvers/HighDensitySolver/SingleTransitionIntraNodeSolver.ts index c51b2321e..2343a1e99 100644 --- a/lib/solvers/HighDensitySolver/SingleTransitionIntraNodeSolver.ts +++ b/lib/solvers/HighDensitySolver/SingleTransitionIntraNodeSolver.ts @@ -137,6 +137,7 @@ export class SingleTransitionIntraNodeSolver extends BaseSolver { ] return { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName, route, traceThickness: this.traceThickness, diff --git a/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/SingleTransitionCrossingRouteSolver.ts b/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/SingleTransitionCrossingRouteSolver.ts index 2df141154..1cfed09c6 100644 --- a/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/SingleTransitionCrossingRouteSolver.ts +++ b/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/SingleTransitionCrossingRouteSolver.ts @@ -290,6 +290,7 @@ export class SingleTransitionCrossingRouteSolver extends BaseSolver { ] return { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName, route, traceThickness: this.traceThickness, @@ -373,6 +374,7 @@ export class SingleTransitionCrossingRouteSolver extends BaseSolver { // We need to navigate around the via return { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: flatRouteConnectionName, route: [ { x: flatStart.x, y: flatStart.y, z: flatStart.z ?? 0 }, diff --git a/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/TwoCrossingRoutesHighDensitySolver.ts b/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/TwoCrossingRoutesHighDensitySolver.ts index 73c568649..b437ac691 100644 --- a/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/TwoCrossingRoutesHighDensitySolver.ts +++ b/lib/solvers/HighDensitySolver/TwoRouteHighDensitySolver/TwoCrossingRoutesHighDensitySolver.ts @@ -385,6 +385,7 @@ export class TwoCrossingRoutesHighDensitySolver extends BaseSolver { if (!jPair) return false const routeASolution: HighDensityIntraNodeRoute = { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: routeA.connectionName, route: optimalPath.points.map((p) => ({ x: p.x, @@ -397,6 +398,7 @@ export class TwoCrossingRoutesHighDensitySolver extends BaseSolver { } jPair.line2.points.reverse() const routeBSolution: HighDensityIntraNodeRoute = { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: routeB.connectionName, route: [ ...jPair.line1.points.map((p) => ({ @@ -605,6 +607,7 @@ export class TwoCrossingRoutesHighDensitySolver extends BaseSolver { const [routeA, routeB] = this.routes // Routes don't cross, create simple direct connections const routeASolution: HighDensityIntraNodeRoute = { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: routeA.connectionName, route: [ { @@ -624,6 +627,7 @@ export class TwoCrossingRoutesHighDensitySolver extends BaseSolver { } const routeBSolution: HighDensityIntraNodeRoute = { + capacityMeshNodeId: this.nodeWithPortPoints.capacityMeshNodeId, connectionName: routeB.connectionName, route: [ { diff --git a/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver2.ts b/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver2.ts index a89a352c5..6b7ccfa07 100644 --- a/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver2.ts +++ b/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver2.ts @@ -144,6 +144,7 @@ export class MultipleHighDensityRouteStitchSolver2 extends BaseSolver { if (hdRoutes.length === 0) { // No routes, just create a direct connection return { + capacityMeshNodeId: connectionName, connectionName, traceThickness: this.defaultTraceThickness, viaDiameter: this.defaultViaDiameter, @@ -170,6 +171,8 @@ export class MultipleHighDensityRouteStitchSolver2 extends BaseSolver { const mergedRoute: Array<{ x: number; y: number; z: number }> = [] const mergedVias: Array<{ x: number; y: number }> = [] const mergedJumpers: HighDensityIntraNodeRoute["jumpers"] = [] + const capacityMeshNodeId = + orderedRoutes[0]?.capacityMeshNodeId ?? connectionName // Always add start point - it's where the connection begins mergedRoute.push({ x: start.x, y: start.y, z: start.z }) @@ -234,6 +237,7 @@ export class MultipleHighDensityRouteStitchSolver2 extends BaseSolver { } return { + capacityMeshNodeId, connectionName, rootConnectionName: hdRoutes[0]?.rootConnectionName, traceThickness: hdRoutes[0]?.traceThickness ?? this.defaultTraceThickness, diff --git a/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver.ts b/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver.ts index 2137abd91..1cdef4279 100644 --- a/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver.ts +++ b/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver.ts @@ -48,6 +48,7 @@ export class SingleHighDensityRouteStitchSolver extends BaseSolver { routePoints.push({ x: opts.end.x, y: opts.end.y, z: opts.end.z }) this.mergedHdRoute = { + capacityMeshNodeId: opts.connectionName, connectionName: opts.connectionName, rootConnectionName: opts.hdRoutes[0]?.rootConnectionName, route: routePoints, @@ -116,6 +117,7 @@ export class SingleHighDensityRouteStitchSolver extends BaseSolver { distToFirst <= distToLast ? firstRouteFirstPoint : firstRouteLastPoint this.mergedHdRoute = { + capacityMeshNodeId: firstRoute.capacityMeshNodeId, connectionName: opts.connectionName, // Use mandatory connectionName rootConnectionName: firstRoute.rootConnectionName, route: [ diff --git a/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver.ts b/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver.ts index f2daf852f..0b811f8e7 100644 --- a/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver.ts +++ b/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver.ts @@ -63,6 +63,7 @@ export class SingleSimplifiedPathSolver extends BaseSolver { get simplifiedRoute(): HighDensityIntraNodeRoute { return { + capacityMeshNodeId: this.inputRoute.capacityMeshNodeId, connectionName: this.inputRoute.connectionName, rootConnectionName: this.inputRoute.rootConnectionName, traceThickness: this.inputRoute.traceThickness, diff --git a/lib/solvers/TraceKeepoutSolver/TraceKeepoutSolver.ts b/lib/solvers/TraceKeepoutSolver/TraceKeepoutSolver.ts index 793a26b23..a90e0338d 100644 --- a/lib/solvers/TraceKeepoutSolver/TraceKeepoutSolver.ts +++ b/lib/solvers/TraceKeepoutSolver/TraceKeepoutSolver.ts @@ -774,6 +774,7 @@ export class TraceKeepoutSolver extends BaseSolver { // Create the redrawn trace const redrawnTrace: HighDensityRoute = { + capacityMeshNodeId: this.currentTrace.capacityMeshNodeId, connectionName: this.currentTrace.connectionName, rootConnectionName: this.currentTrace.rootConnectionName, traceThickness: this.currentTrace.traceThickness, @@ -902,8 +903,10 @@ export class TraceKeepoutSolver extends BaseSolver { const end = outlinePoints[(i + 1) % outlinePoints.length]! for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) { + const connectionName = `${BOARD_OUTLINE_CONNECTION_NAME}_${i}_z${layerIndex}` routes.push({ - connectionName: `${BOARD_OUTLINE_CONNECTION_NAME}_${i}_z${layerIndex}`, + capacityMeshNodeId: connectionName, + connectionName, traceThickness: 0.01, // Thin trace for outline viaDiameter: 0, route: [ diff --git a/lib/solvers/TraceMarginDrcRepairSolver/TraceMarginDrcRepairSolver.ts b/lib/solvers/TraceMarginDrcRepairSolver/TraceMarginDrcRepairSolver.ts new file mode 100644 index 000000000..29c4b2ddb --- /dev/null +++ b/lib/solvers/TraceMarginDrcRepairSolver/TraceMarginDrcRepairSolver.ts @@ -0,0 +1,918 @@ +import { checkEachPcbTraceNonOverlapping } from "@tscircuit/checks" +import { + distance, + doesSegmentIntersectRect, + pointToBoxDistance, + pointToSegmentDistance, +} from "@tscircuit/math-utils" +import type { Point, Point3 } from "@tscircuit/math-utils" +import type { PcbTraceError } from "circuit-json" +import { GraphicsObject } from "graphics-debug" +import { ObstacleSpatialHashIndex } from "lib/data-structures/ObstacleTree" +import { BaseSolver } from "lib/solvers/BaseSolver" +import { convertToCircuitJson } from "lib/testing/utils/convertToCircuitJson" +import { Obstacle, SimpleRouteJson } from "lib/types" +import { HighDensityRoute } from "lib/types/high-density-types" +import { mapZToLayerName } from "lib/utils/mapZToLayerName" + +type Point3D = Point3 & { insideJumperPad?: boolean } +type RoutePoint = HighDensityRoute["route"][number] +type RoutePointWithOptionalPort = RoutePoint & { pcb_port_id?: string } + +type TraceSpacingIssue = { + pcb_trace_id: string + pcb_trace_error_id?: string + message?: string + center: Point + pcb_port_ids?: string[] +} + +type IssueContext = { + issue: TraceSpacingIssue + routeIndex: number + routeLayer: string + obstacle: Obstacle | null + segmentIndex: number + runStartSegIndex: number + runEndSegIndex: number + axis: "horizontal" | "vertical" + directionOrder: [number, number] + directionIndex: number + shiftStep: number + previousIssueCount: number + previousIssueKeys: Set +} + +const SHIFT_INCREMENT_MM = 0.05 +const MAX_SHIFT_STEPS = 40 +const LOCAL_ROUTE_WINDOW_MARGIN_MM = 1.0 + +const cloneRoute = (route: HighDensityRoute): HighDensityRoute => + structuredClone(route) + +function normalizeRoutesForTwoLayerCheck( + routes: HighDensityRoute[], +): HighDensityRoute[] { + return routes.map((route) => ({ + ...cloneRoute(route), + route: route.route.map((point) => ({ + ...point, + z: Math.max(0, Math.min(1, point.z)), + })), + })) +} + +function rectBounds(rect: Obstacle) { + return { + minX: rect.center.x - rect.width / 2, + maxX: rect.center.x + rect.width / 2, + minY: rect.center.y - rect.height / 2, + maxY: rect.center.y + rect.height / 2, + } +} + +function pointToRectDistance(point: Point, rect: Obstacle): number { + return pointToBoxDistance(point, rect) +} + +function expandObstacle(obstacle: Obstacle, margin: number): Obstacle { + return { + ...obstacle, + width: obstacle.width + margin * 2, + height: obstacle.height + margin * 2, + } +} + +function segmentIntersectsRect(a: Point, b: Point, rect: Obstacle): boolean { + const rb = rectBounds(rect) + return doesSegmentIntersectRect(a, b, rb) +} + +function pointInsideRect(point: Point, rect: Obstacle): boolean { + const rb = rectBounds(rect) + return ( + point.x >= rb.minX && + point.x <= rb.maxX && + point.y >= rb.minY && + point.y <= rb.maxY + ) +} + +function boundsIntersect( + a: { minX: number; minY: number; maxX: number; maxY: number }, + b: { minX: number; minY: number; maxX: number; maxY: number }, +): boolean { + return ( + a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY + ) +} + +function normalizeTraceIssues( + traceErrors: PcbTraceError[], +): TraceSpacingIssue[] { + const issues: TraceSpacingIssue[] = [] + for (const error of traceErrors) { + const pcbTraceId = + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : null + const center = + error.center && + typeof error.center.x === "number" && + typeof error.center.y === "number" + ? ({ x: error.center.x, y: error.center.y } as Point) + : null + if (!pcbTraceId || !center) continue + issues.push({ + pcb_trace_id: pcbTraceId, + center, + message: error.message, + pcb_trace_error_id: + typeof error.pcb_trace_error_id === "string" + ? error.pcb_trace_error_id + : undefined, + pcb_port_ids: Array.isArray(error.pcb_port_ids) + ? error.pcb_port_ids.filter( + (id): id is string => typeof id === "string", + ) + : undefined, + }) + } + return issues +} + +function hasPcbPortId(point: RoutePoint): point is RoutePointWithOptionalPort { + return ( + "pcb_port_id" in point && + typeof (point as RoutePointWithOptionalPort).pcb_port_id === "string" + ) +} + +function parseRouteIndexFromTraceId(pcbTraceId: string): number | null { + const m = /^trace_(\d+)/.exec(pcbTraceId) + if (!m) return null + const idx = Number(m[1]) + return Number.isFinite(idx) ? idx : null +} + +function routeLayerFromIssue( + route: HighDensityRoute, + issueCenter: Point, + layerCount: number, +): string { + let bestZ = route.route[0]?.z ?? 0 + let bestDist = Number.POSITIVE_INFINITY + for (let i = 0; i < route.route.length - 1; i++) { + const a = route.route[i]! + const b = route.route[i + 1]! + if (a.z !== b.z) continue + const d = pointToSegmentDistance(issueCenter, a, b) + if (d < bestDist) { + bestDist = d + bestZ = a.z + } + } + return mapZToLayerName(bestZ, layerCount) +} + +function recomputeViasFromRoute( + route: HighDensityRoute, +): Array<{ x: number; y: number }> { + const vias: Array<{ x: number; y: number }> = [] + const seen = new Set() + for (let i = 1; i < route.route.length; i++) { + const prev = route.route[i - 1]! + const curr = route.route[i]! + if ( + prev.z !== curr.z && + Math.abs(prev.x - curr.x) < 0.01 && + Math.abs(prev.y - curr.y) < 0.01 + ) { + const key = `${curr.x.toFixed(4)},${curr.y.toFixed(4)}` + if (seen.has(key)) continue + seen.add(key) + vias.push({ x: curr.x, y: curr.y }) + } + } + return vias +} + +function sameIssue(a: TraceSpacingIssue, b: TraceSpacingIssue): boolean { + if (a.pcb_trace_id !== b.pcb_trace_id) return false + if (a.pcb_trace_error_id && b.pcb_trace_error_id) { + return a.pcb_trace_error_id === b.pcb_trace_error_id + } + if (distance(a.center, b.center) > 0.25) return false + if (!a.pcb_port_ids || !b.pcb_port_ids) return true + return a.pcb_port_ids.some((id) => b.pcb_port_ids!.includes(id)) +} + +function routeToGraphicsLines( + route: HighDensityRoute, + strokeColor: string, + strokeWidth = route.traceThickness, +): NonNullable { + const lines: NonNullable = [] + for (let i = 0; i < route.route.length - 1; i++) { + const a = route.route[i]! + const b = route.route[i + 1]! + if (a.z !== b.z) continue + lines.push({ + points: [ + { x: a.x, y: a.y }, + { x: b.x, y: b.y }, + ], + strokeColor, + strokeWidth, + }) + } + return lines +} + +export class TraceMarginDrcRepairSolver extends BaseSolver { + override getSolverName(): string { + return "TraceMarginDrcRepairSolver" + } + + private originalHdRoutes: HighDensityRoute[] + private hdRoutes: HighDensityRoute[] + private unresolvedIssueKeys = new Set() + private currentIssueContext: IssueContext | null = null + private currentCandidateRoute: HighDensityRoute | null = null + private currentObstacleMarginRect: Obstacle | null = null + private obstaclesByLayer = new Map() + private obstacleIndexByLayer = new Map() + private cachedIssues: TraceSpacingIssue[] = [] + private latestIssueCount = 0 + + constructor( + private readonly input: { + hdRoutes: HighDensityRoute[] + srj: SimpleRouteJson + minTraceWidth: number + obstacleMargin: number + colorMap?: Record + }, + ) { + super() + this.MAX_ITERATIONS = 2e6 + this.originalHdRoutes = input.hdRoutes.map(cloneRoute) + this.hdRoutes = input.hdRoutes.map(cloneRoute) + + for (const obstacle of this.input.srj.obstacles) { + for (const layer of obstacle.layers) { + const existing = this.obstaclesByLayer.get(layer) + if (existing) { + existing.push(obstacle) + } else { + this.obstaclesByLayer.set(layer, [obstacle]) + } + } + } + + for (const [layer, obstacles] of this.obstaclesByLayer.entries()) { + this.obstacleIndexByLayer.set( + layer, + new ObstacleSpatialHashIndex("flatbush", obstacles), + ) + } + } + + get repairedHdRoutes(): HighDensityRoute[] { + return this.hdRoutes + } + + private buildIssueKey(issue: TraceSpacingIssue): string { + return `${issue.pcb_trace_error_id ?? "issue"}:${issue.pcb_trace_id}:${issue.center.x.toFixed(3)}:${issue.center.y.toFixed(3)}` + } + + private runTraceSpacingCheck( + hdRoutes: HighDensityRoute[], + ): TraceSpacingIssue[] { + const normalizedRoutes = normalizeRoutesForTwoLayerCheck(hdRoutes) + const circuitJson = convertToCircuitJson( + this.input.srj, + normalizedRoutes, + this.input.minTraceWidth, + this.input.srj.minViaDiameter ?? 0.3, + ) + const rawErrors = checkEachPcbTraceNonOverlapping(circuitJson, { + minSpacing: this.input.obstacleMargin, + }) + return normalizeTraceIssues(rawErrors) + } + + private getCurrentIssues(): TraceSpacingIssue[] { + const issues = this.runTraceSpacingCheck(this.hdRoutes).filter( + (issue) => !this.unresolvedIssueKeys.has(this.buildIssueKey(issue)), + ) + this.cachedIssues = issues + this.latestIssueCount = issues.length + return issues + } + + private getRunBounds( + route: HighDensityRoute, + runStartSegIndex: number, + runEndSegIndex: number, + ): { minX: number; minY: number; maxX: number; maxY: number } { + let minX = Number.POSITIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY + + for ( + let pointIndex = runStartSegIndex; + pointIndex <= runEndSegIndex + 1; + pointIndex++ + ) { + const p = route.route[pointIndex]! + if (p.x < minX) minX = p.x + if (p.y < minY) minY = p.y + if (p.x > maxX) maxX = p.x + if (p.y > maxY) maxY = p.y + } + + return { minX, minY, maxX, maxY } + } + + private isRouteNearBounds( + route: HighDensityRoute, + bounds: { minX: number; minY: number; maxX: number; maxY: number }, + ): boolean { + for (let i = 0; i < route.route.length - 1; i++) { + const a = route.route[i]! + const b = route.route[i + 1]! + if (a.z !== b.z) continue + const segmentBounds = { + minX: Math.min(a.x, b.x), + minY: Math.min(a.y, b.y), + maxX: Math.max(a.x, b.x), + maxY: Math.max(a.y, b.y), + } + if (boundsIntersect(segmentBounds, bounds)) return true + } + return false + } + + private runLocalTraceSpacingCheck( + hdRoutes: HighDensityRoute[], + context: IssueContext, + candidateRoute: HighDensityRoute, + ): TraceSpacingIssue[] { + const runBounds = this.getRunBounds( + candidateRoute, + context.runStartSegIndex, + context.runEndSegIndex, + ) + const routeWindow = { + minX: + runBounds.minX - + candidateRoute.traceThickness - + this.input.obstacleMargin - + LOCAL_ROUTE_WINDOW_MARGIN_MM, + minY: + runBounds.minY - + candidateRoute.traceThickness - + this.input.obstacleMargin - + LOCAL_ROUTE_WINDOW_MARGIN_MM, + maxX: + runBounds.maxX + + candidateRoute.traceThickness + + this.input.obstacleMargin + + LOCAL_ROUTE_WINDOW_MARGIN_MM, + maxY: + runBounds.maxY + + candidateRoute.traceThickness + + this.input.obstacleMargin + + LOCAL_ROUTE_WINDOW_MARGIN_MM, + } + + const keepRouteIndices = new Set([context.routeIndex]) + for (let i = 0; i < hdRoutes.length; i++) { + if (i === context.routeIndex) continue + if (this.isRouteNearBounds(hdRoutes[i]!, routeWindow)) { + keepRouteIndices.add(i) + } + } + + const maskedRoutes = hdRoutes.map((route, index) => { + if (keepRouteIndices.has(index)) return route + const anchorPoint = route.route[0] + return { + ...route, + route: anchorPoint ? [anchorPoint] : [], + vias: [], + jumpers: [], + } + }) + + return this.runTraceSpacingCheck(maskedRoutes) + } + + private getNearbyObstaclesForIssue( + routeLayer: string, + center: Point, + ): Obstacle[] { + const layerObstacles = this.obstaclesByLayer.get(routeLayer) ?? [] + if (layerObstacles.length === 0) return [] + + const index = this.obstacleIndexByLayer.get(routeLayer) + if (!index) return layerObstacles + + const searchRadii = [0.5, 1, 2, 4, 8, 16] + for (const radius of searchRadii) { + const nearby = index.search({ + minX: center.x - radius, + minY: center.y - radius, + maxX: center.x + radius, + maxY: center.y + radius, + }) + if (nearby.length > 0) return nearby + } + + return layerObstacles + } + + private findObstacleForIssue( + issue: TraceSpacingIssue, + routeLayer: string, + ): Obstacle | null { + const obstaclesOnLayer = this.obstaclesByLayer.get(routeLayer) ?? [] + if (obstaclesOnLayer.length === 0) return null + const nearbyObstacles = this.getNearbyObstaclesForIssue( + routeLayer, + issue.center, + ) + + const matchingByPortIds = + issue.pcb_port_ids && issue.pcb_port_ids.length > 0 + ? nearbyObstacles.filter((obstacle) => + obstacle.connectedTo.some((id) => issue.pcb_port_ids!.includes(id)), + ) + : [] + + const anyMatchingByPortIds = + issue.pcb_port_ids && issue.pcb_port_ids.length > 0 + ? obstaclesOnLayer.filter((obstacle) => + obstacle.connectedTo.some((id) => issue.pcb_port_ids!.includes(id)), + ) + : [] + + const candidates = + matchingByPortIds.length > 0 + ? matchingByPortIds + : anyMatchingByPortIds.length > 0 + ? anyMatchingByPortIds + : nearbyObstacles + + let best: Obstacle | null = null + let bestDist = Number.POSITIVE_INFINITY + for (const obstacle of candidates) { + const d = pointToRectDistance(issue.center, obstacle) + if (d < bestDist) { + best = obstacle + bestDist = d + } + } + return best + } + + private findOffendingSegment( + route: HighDensityRoute, + issue: TraceSpacingIssue, + obstacle: Obstacle | null, + ): number { + let bestIndex = -1 + let bestScore = Number.POSITIVE_INFINITY + for (let i = 0; i < route.route.length - 1; i++) { + const a = route.route[i]! + const b = route.route[i + 1]! + if (a.z !== b.z) continue + const dCenter = pointToSegmentDistance(issue.center, a, b) + const expanded = + obstacle !== null + ? expandObstacle( + obstacle, + route.traceThickness / 2 + this.input.obstacleMargin, + ) + : null + const dObstacle = + expanded === null + ? 0 + : segmentIntersectsRect(a, b, expanded) + ? 0 + : Math.min( + pointToRectDistance(a, expanded), + pointToRectDistance(b, expanded), + ) + const score = dCenter * 0.7 + dObstacle * 0.3 + if (score < bestScore) { + bestScore = score + bestIndex = i + } + } + return bestIndex + } + + private computeSegmentRun( + route: HighDensityRoute, + segmentIndex: number, + ): { + runStartSegIndex: number + runEndSegIndex: number + axis: "horizontal" | "vertical" + } { + const a = route.route[segmentIndex]! + const b = route.route[segmentIndex + 1]! + const axis = + Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? "horizontal" : "vertical" + let runStart = segmentIndex + let runEnd = segmentIndex + + const isSegmentAligned = (p1: Point3D, p2: Point3D) => { + if (p1.z !== p2.z) return false + if (axis === "horizontal") { + return Math.abs(p1.y - p2.y) < 0.01 + } + return Math.abs(p1.x - p2.x) < 0.01 + } + + for (let i = segmentIndex - 1; i >= 0; i--) { + const p1 = route.route[i]! + const p2 = route.route[i + 1]! + if (!isSegmentAligned(p1, p2)) break + runStart = i + } + for (let i = segmentIndex + 1; i < route.route.length - 1; i++) { + const p1 = route.route[i]! + const p2 = route.route[i + 1]! + if (!isSegmentAligned(p1, p2)) break + runEnd = i + } + + return { runStartSegIndex: runStart, runEndSegIndex: runEnd, axis } + } + + private startNextIssue(): boolean { + const issues = this.getCurrentIssues() + if (issues.length === 0) return false + + const issue = issues[0]! + const routeIndex = parseRouteIndexFromTraceId(issue.pcb_trace_id) + if (routeIndex === null || !this.hdRoutes[routeIndex]) { + this.unresolvedIssueKeys.add(this.buildIssueKey(issue)) + return true + } + + const route = this.hdRoutes[routeIndex]! + const routeLayer = routeLayerFromIssue( + route, + issue.center, + this.input.srj.layerCount, + ) + const obstacle = this.findObstacleForIssue(issue, routeLayer) + const segmentIndex = this.findOffendingSegment(route, issue, obstacle) + if (segmentIndex < 0) { + this.unresolvedIssueKeys.add(this.buildIssueKey(issue)) + return true + } + + const { runStartSegIndex, runEndSegIndex, axis } = this.computeSegmentRun( + route, + segmentIndex, + ) + const segmentA = route.route[segmentIndex]! + const segmentB = route.route[segmentIndex + 1]! + const segmentCenter = { + x: (segmentA.x + segmentB.x) / 2, + y: (segmentA.y + segmentB.y) / 2, + } + const obstacleCenter = obstacle?.center ?? issue.center + const preferredSign = + axis === "horizontal" + ? segmentCenter.y >= obstacleCenter.y + ? 1 + : -1 + : segmentCenter.x >= obstacleCenter.x + ? 1 + : -1 + + this.currentIssueContext = { + issue, + routeIndex, + routeLayer, + obstacle, + segmentIndex, + runStartSegIndex, + runEndSegIndex, + axis, + directionOrder: [preferredSign, -preferredSign], + directionIndex: 0, + shiftStep: 1, + previousIssueCount: this.latestIssueCount, + previousIssueKeys: new Set(issues.map((i) => this.buildIssueKey(i))), + } + this.currentCandidateRoute = null + + this.currentObstacleMarginRect = + obstacle !== null + ? expandObstacle( + obstacle, + route.traceThickness / 2 + this.input.obstacleMargin, + ) + : null + return true + } + + private createShiftedCandidate( + context: IssueContext, + ): HighDensityRoute | null { + const baseRoute = this.hdRoutes[context.routeIndex]! + let startPointIndex = context.runStartSegIndex + let endPointIndex = context.runEndSegIndex + 1 + const pointCount = baseRoute.route.length + if (pointCount < 2) return null + + const firstPoint = baseRoute.route[0]! + const secondPoint = baseRoute.route[1]! + const lastPoint = baseRoute.route[pointCount - 1]! + const beforeLastPoint = baseRoute.route[pointCount - 2]! + + const firstEndpointIsSensitive = + hasPcbPortId(firstPoint) || + this.endpointInsideLayerObstacle(context.routeLayer, firstPoint) || + (firstPoint.z !== secondPoint.z && + Math.abs(firstPoint.x - secondPoint.x) < 0.01 && + Math.abs(firstPoint.y - secondPoint.y) < 0.01) + const lastEndpointIsSensitive = + hasPcbPortId(lastPoint) || + this.endpointInsideLayerObstacle(context.routeLayer, lastPoint) || + (lastPoint.z !== beforeLastPoint.z && + Math.abs(lastPoint.x - beforeLastPoint.x) < 0.01 && + Math.abs(lastPoint.y - beforeLastPoint.y) < 0.01) + + if (firstEndpointIsSensitive && startPointIndex === 0) startPointIndex = 1 + if (lastEndpointIsSensitive && endPointIndex === pointCount - 1) { + endPointIndex = pointCount - 2 + } + if (startPointIndex > endPointIndex) return null + + // Preserve existing via-style transitions: never move only one side. + for (let i = 1; i < pointCount; i++) { + const prev = baseRoute.route[i - 1]! + const curr = baseRoute.route[i]! + const baseViaStyle = + prev.z !== curr.z && + Math.abs(prev.x - curr.x) < 0.01 && + Math.abs(prev.y - curr.y) < 0.01 + if (!baseViaStyle) continue + const prevMoved = i - 1 >= startPointIndex && i - 1 <= endPointIndex + const currMoved = i >= startPointIndex && i <= endPointIndex + if (prevMoved !== currMoved) return null + } + + const candidate = cloneRoute(baseRoute) + const shiftDistance = context.shiftStep * SHIFT_INCREMENT_MM + const signedShift = + context.directionOrder[context.directionIndex] * shiftDistance + + for (let i = startPointIndex; i <= endPointIndex; i++) { + const point = candidate.route[i]! + if (hasPcbPortId(point)) { + return null + } + if (context.axis === "horizontal") { + point.y += signedShift + } else { + point.x += signedShift + } + } + + candidate.vias = recomputeViasFromRoute(candidate) + return candidate + } + + private endpointInsideLayerObstacle( + routeLayer: string, + point: Point3, + ): boolean { + const layerObstacles = this.obstaclesByLayer.get(routeLayer) ?? [] + for (const obstacle of layerObstacles) { + if (pointToRectDistance(point, obstacle) <= 0.001) return true + } + return false + } + + private advanceAttemptCursor(context: IssueContext) { + context.shiftStep++ + if (context.shiftStep <= MAX_SHIFT_STEPS) return + context.shiftStep = 1 + context.directionIndex++ + } + + private finishIssueAsUnresolved(context: IssueContext) { + this.unresolvedIssueKeys.add(this.buildIssueKey(context.issue)) + this.currentIssueContext = null + this.currentCandidateRoute = null + this.currentObstacleMarginRect = null + } + + private candidateStillViolatesCurrentObstacleMargin( + context: IssueContext, + candidateRoute: HighDensityRoute, + ): boolean { + const obstacleRect = this.currentObstacleMarginRect + if (!obstacleRect) return false + + for ( + let segIndex = context.runStartSegIndex; + segIndex <= context.runEndSegIndex; + segIndex++ + ) { + const a = candidateRoute.route[segIndex]! + const b = candidateRoute.route[segIndex + 1]! + if (a.z !== b.z) continue + + if (segmentIntersectsRect(a, b, obstacleRect)) return true + if ( + pointInsideRect(a, obstacleRect) || + pointInsideRect(b, obstacleRect) + ) { + return true + } + } + + return false + } + + private processCurrentIssue() { + const context = this.currentIssueContext + if (!context) return + if (context.directionIndex > 1) { + this.finishIssueAsUnresolved(context) + return + } + + const candidateRoute = this.createShiftedCandidate(context) + if (!candidateRoute) { + this.advanceAttemptCursor(context) + return + } + this.currentCandidateRoute = candidateRoute + if ( + this.candidateStillViolatesCurrentObstacleMargin(context, candidateRoute) + ) { + this.advanceAttemptCursor(context) + if (context.directionIndex > 1) { + this.finishIssueAsUnresolved(context) + } + return + } + + const candidateHdRoutes = this.hdRoutes.map(cloneRoute) + candidateHdRoutes[context.routeIndex] = candidateRoute + const localCandidateIssues = this.runLocalTraceSpacingCheck( + candidateHdRoutes, + context, + candidateRoute, + ) + const localIssueStillPresent = localCandidateIssues.some((issue) => + sameIssue(issue, context.issue), + ) + if (localIssueStillPresent) { + this.advanceAttemptCursor(context) + if (context.directionIndex > 1) { + this.finishIssueAsUnresolved(context) + } + return + } + const localCandidateIssueKeys = new Set( + localCandidateIssues.map((issue) => this.buildIssueKey(issue)), + ) + const localIntroducedNewIssue = [...localCandidateIssueKeys].some( + (key) => !context.previousIssueKeys.has(key), + ) + if (localIntroducedNewIssue) { + this.advanceAttemptCursor(context) + if (context.directionIndex > 1) { + this.finishIssueAsUnresolved(context) + } + return + } + const candidateIssues = this.runTraceSpacingCheck(candidateHdRoutes) + + const issueStillPresent = candidateIssues.some((issue) => + sameIssue(issue, context.issue), + ) + const candidateIssueKeys = new Set( + candidateIssues.map((issue) => this.buildIssueKey(issue)), + ) + const introducedNewIssue = [...candidateIssueKeys].some( + (key) => !context.previousIssueKeys.has(key), + ) + + if (!issueStillPresent && !introducedNewIssue) { + this.hdRoutes = candidateHdRoutes + this.currentIssueContext = null + this.currentCandidateRoute = null + this.currentObstacleMarginRect = null + this.cachedIssues = candidateIssues + this.latestIssueCount = candidateIssues.length + return + } + + this.advanceAttemptCursor(context) + if (context.directionIndex > 1) { + this.finishIssueAsUnresolved(context) + } + } + + _step() { + if (!this.currentIssueContext) { + const hasIssue = this.startNextIssue() + if (!hasIssue) { + this.solved = true + } + return + } + + this.processCurrentIssue() + } + + visualize(): GraphicsObject { + const graphics: GraphicsObject & { + lines: NonNullable + rects: NonNullable + points: NonNullable + circles: NonNullable + } = { + lines: [], + rects: [], + points: [], + circles: [], + coordinateSystem: "cartesian", + title: "Trace Margin DRC Repair Solver", + } + + if (this.currentIssueContext) { + const context = this.currentIssueContext + const originalRoute = this.hdRoutes[context.routeIndex] + if (context.obstacle) { + graphics.rects.push({ + center: context.obstacle.center, + width: context.obstacle.width, + height: context.obstacle.height, + fill: "rgba(255, 0, 0, 0.3)", + label: "DRC obstacle", + }) + } + if (this.currentObstacleMarginRect) { + graphics.rects.push({ + center: this.currentObstacleMarginRect.center, + width: this.currentObstacleMarginRect.width, + height: this.currentObstacleMarginRect.height, + fill: "rgba(255, 128, 0, 0.12)", + label: "required margin", + }) + } + + if (originalRoute) { + graphics.lines.push( + ...routeToGraphicsLines(originalRoute, "rgba(220, 20, 20, 0.9)"), + ) + } + if (this.currentCandidateRoute) { + graphics.lines.push( + ...routeToGraphicsLines( + this.currentCandidateRoute, + "rgba(32, 32, 32, 0.95)", + ), + ) + } + + graphics.points.push({ + x: context.issue.center.x, + y: context.issue.center.y, + color: "red", + label: context.issue.message ?? "Trace spacing issue", + }) + return graphics + } + + // Final visualization: old traces + repaired traces, with repaired routes emphasized + for (const route of this.originalHdRoutes) { + graphics.lines.push( + ...routeToGraphicsLines(route, "rgba(220, 20, 20, 0.35)", 0.08), + ) + } + for (let i = 0; i < this.hdRoutes.length; i++) { + const route = this.hdRoutes[i]! + const original = this.originalHdRoutes[i] + const changed = + JSON.stringify(route.route) !== JSON.stringify(original?.route ?? []) + const color = changed ? "rgba(20, 20, 20, 0.95)" : "rgba(0, 0, 0, 0.5)" + graphics.lines.push(...routeToGraphicsLines(route, color)) + } + + return graphics + } +} diff --git a/lib/solvers/TraceWidthSolver/TraceWidthSolver.ts b/lib/solvers/TraceWidthSolver/TraceWidthSolver.ts index fbd3c4dac..1369eff3c 100644 --- a/lib/solvers/TraceWidthSolver/TraceWidthSolver.ts +++ b/lib/solvers/TraceWidthSolver/TraceWidthSolver.ts @@ -446,6 +446,7 @@ export class TraceWidthSolver extends BaseSolver { if (!this.currentTrace) return const routeWithWidth: HighDensityRoute = { + capacityMeshNodeId: this.currentTrace.capacityMeshNodeId, connectionName: this.currentTrace.connectionName, rootConnectionName: this.currentTrace.rootConnectionName, traceThickness: traceWidth, diff --git a/lib/solvers/UselessViaRemovalSolver/SingleRouteUselessViaRemovalSolver.ts b/lib/solvers/UselessViaRemovalSolver/SingleRouteUselessViaRemovalSolver.ts index c5b8c3925..a0eb9f247 100644 --- a/lib/solvers/UselessViaRemovalSolver/SingleRouteUselessViaRemovalSolver.ts +++ b/lib/solvers/UselessViaRemovalSolver/SingleRouteUselessViaRemovalSolver.ts @@ -323,6 +323,7 @@ export class SingleRouteUselessViaRemovalSolver extends BaseSolver { } } return { + capacityMeshNodeId: this.unsimplifiedRoute.capacityMeshNodeId, connectionName: this.unsimplifiedRoute.connectionName, rootConnectionName: this.unsimplifiedRoute.rootConnectionName, route, diff --git a/lib/types/high-density-types.ts b/lib/types/high-density-types.ts index c331df815..6ca055565 100644 --- a/lib/types/high-density-types.ts +++ b/lib/types/high-density-types.ts @@ -31,6 +31,7 @@ export type NodeWithPortPoints = { * z must be an integer */ export type HighDensityIntraNodeRoute = { + capacityMeshNodeId: string connectionName: string rootConnectionName?: string traceThickness: number diff --git a/scripts/export-high-density-repair-dataset.ts b/scripts/export-high-density-repair-dataset.ts new file mode 100644 index 000000000..5081a82d2 --- /dev/null +++ b/scripts/export-high-density-repair-dataset.ts @@ -0,0 +1,317 @@ +#!/usr/bin/env bun +// @ts-nocheck + +import { spawn } from "node:child_process" +import { mkdir, writeFile } from "node:fs/promises" +import path from "node:path" +import stringify from "fast-json-stable-stringify" +import * as dataset01 from "@tscircuit/autorouting-dataset-01" +import { AutoroutingPipelineSolver2_PortPointPathing } from "lib/autorouter-pipelines/AutoroutingPipeline2_PortPointPathing/AutoroutingPipelineSolver2_PortPointPathing" +import { createSingleNodeHighDensityRepairParamsList } from "lib/solvers/HighDensityRepairSolver/HighDensityRepairSolver" + +type CircuitRunResult = { + circuitId: string + status: "ok" | "timed_out" | "failed" + exportedSampleCount: number + detail?: string +} + +type WorkerSummary = { + circuitId: string + exportedSampleCount: number + startIndex: number +} + +const OUTPUT_DIR = path.resolve(process.cwd(), "dataset-hd08") +const CIRCUIT_KEY_REGEX = /^circuit(\d{3})$/ +const DEFAULT_TIMEOUT_MS = 30_000 + +const parseArgs = () => { + const args = process.argv.slice(2) + let circuitId: string | null = null + let limit: number | null = null + let worker = false + let timeoutMs = DEFAULT_TIMEOUT_MS + let startIndex = 1 + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i] + if (arg === "--worker") { + worker = true + continue + } + if (arg === "--sample" || arg === "--circuit") { + const value = (args[i + 1] ?? "").replace(/[^0-9]/g, "") + if (!value) { + throw new Error(`${arg} requires a numeric value`) + } + circuitId = value.padStart(3, "0").slice(-3) + i += 1 + continue + } + if (arg === "--limit") { + const value = Number.parseInt(args[i + 1] ?? "", 10) + if (!Number.isFinite(value) || value < 1) { + throw new Error("--limit must be a positive integer") + } + limit = value + i += 1 + continue + } + if (arg === "--timeout-ms") { + const value = Number.parseInt(args[i + 1] ?? "", 10) + if (!Number.isFinite(value) || value < 1) { + throw new Error("--timeout-ms must be a positive integer") + } + timeoutMs = value + i += 1 + continue + } + if (arg === "--start-index") { + const value = Number.parseInt(args[i + 1] ?? "", 10) + if (!Number.isFinite(value) || value < 1) { + throw new Error("--start-index must be a positive integer") + } + startIndex = value + i += 1 + continue + } + throw new Error(`Unknown argument: ${arg}`) + } + + return { circuitId, limit, worker, timeoutMs, startIndex } +} + +const getDatasetCircuitIds = (): string[] => { + return Object.keys(dataset01) + .map((key) => key.match(CIRCUIT_KEY_REGEX)?.[1] ?? null) + .filter(Boolean) + .sort((a, b) => Number(a) - Number(b)) +} + +const getCircuitSrj = (circuitId: string) => dataset01[`circuit${circuitId}`] + +const getNodePortPointsForRepairStage = ( + solver: AutoroutingPipelineSolver2_PortPointPathing, +) => { + return ( + solver.uniformPortDistributionSolver?.getOutput() ?? + solver.multiSectionPortPointOptimizer?.getNodesWithPortPoints() ?? + solver.portPointPathingSolver?.getNodesWithPortPoints() ?? + [] + ) +} + +const toSampleFileName = (sampleIndex: number) => + `sample${String(sampleIndex).padStart(3, "0")}.json` + +const writeCircuitSamples = async (circuitId: string, startIndex: number) => { + const srj = getCircuitSrj(circuitId) + if (!srj) { + throw new Error(`Dataset sample circuit${circuitId} was not found`) + } + + const solver = new AutoroutingPipelineSolver2_PortPointPathing(srj as any) + solver.solveUntilPhase("highDensityRepairSolver") + + const singleNodeParamsList = createSingleNodeHighDensityRepairParamsList({ + nodePortPoints: getNodePortPointsForRepairStage(solver), + obstacles: solver.srj.obstacles, + hdRoutes: solver.highDensityRouteSolver?.routes ?? [], + connMap: solver.connMap, + }) + + await mkdir(OUTPUT_DIR, { recursive: true }) + + for (const [index, singleNodeParams] of singleNodeParamsList.entries()) { + const sampleIndex = startIndex + index + const outputPath = path.join(OUTPUT_DIR, toSampleFileName(sampleIndex)) + await writeFile( + outputPath, + `${stringify(singleNodeParams, { space: 2 })}\n`, + ) + } + + return { + circuitId, + exportedSampleCount: singleNodeParamsList.length, + startIndex, + } satisfies WorkerSummary +} + +const runCircuitWithTimeout = async ( + circuitId: string, + startIndex: number, + timeoutMs: number, +): Promise => { + return await new Promise((resolve) => { + let settled = false + let timedOut = false + let stdout = "" + let stderr = "" + + const child = spawn( + process.execPath, + [ + import.meta.path, + "--worker", + "--circuit", + circuitId, + "--start-index", + String(startIndex), + ], + { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + }, + ) + + child.stdout?.on("data", (chunk) => { + stdout += String(chunk) + }) + child.stderr?.on("data", (chunk) => { + stderr += String(chunk) + }) + + const settle = (result: CircuitRunResult) => { + if (settled) return + settled = true + clearTimeout(timeout) + resolve(result) + } + + const timeout = setTimeout(() => { + timedOut = true + child.kill("SIGKILL") + settle({ + circuitId, + status: "timed_out", + exportedSampleCount: 0, + detail: `timed out after ${timeoutMs}ms`, + }) + }, timeoutMs) + + child.once("error", (error) => { + settle({ + circuitId, + status: "failed", + exportedSampleCount: 0, + detail: String(error), + }) + }) + + child.once("exit", (code, signal) => { + if (timedOut) return + if (code !== 0) { + settle({ + circuitId, + status: "failed", + exportedSampleCount: 0, + detail: + stderr.trim() || `child exited with code=${code} signal=${signal}`, + }) + return + } + + try { + const summary = JSON.parse(stdout.trim()) as WorkerSummary + settle({ + circuitId, + status: "ok", + exportedSampleCount: summary.exportedSampleCount, + }) + } catch (error) { + settle({ + circuitId, + status: "failed", + exportedSampleCount: 0, + detail: `invalid worker summary: ${error}`, + }) + } + }) + }) +} + +const runParent = async () => { + const { circuitId, limit, timeoutMs } = parseArgs() + let circuitIds = getDatasetCircuitIds() + if (circuitId) { + circuitIds = circuitIds.filter((id) => id === circuitId) + } + if (limit !== null) { + circuitIds = circuitIds.slice(0, limit) + } + if (circuitIds.length === 0) { + throw new Error("No dataset circuits matched the provided filters") + } + + const timedOutCircuits: string[] = [] + const failedCircuits: string[] = [] + let nextSampleIndex = 1 + let exportedSampleCount = 0 + + for (const currentCircuitId of circuitIds) { + const result = await runCircuitWithTimeout( + currentCircuitId, + nextSampleIndex, + timeoutMs, + ) + + if (result.status === "ok") { + const startSampleIndex = nextSampleIndex + const endSampleIndex = nextSampleIndex + result.exportedSampleCount - 1 + exportedSampleCount += result.exportedSampleCount + nextSampleIndex += result.exportedSampleCount + + if (result.exportedSampleCount === 0) { + console.log(`circuit${currentCircuitId}: exported 0 node samples`) + } else { + console.log( + `circuit${currentCircuitId}: wrote ${result.exportedSampleCount} node samples (${toSampleFileName(startSampleIndex)}-${toSampleFileName(endSampleIndex)})`, + ) + } + continue + } + + if (result.status === "timed_out") { + timedOutCircuits.push(currentCircuitId) + console.error(`timed out circuit${currentCircuitId} after ${timeoutMs}ms`) + continue + } + + failedCircuits.push(currentCircuitId) + console.error(`failed circuit${currentCircuitId}: ${result.detail}`) + } + + console.log( + `exported ${exportedSampleCount} node-level SingleNodeHighDensityRepair inputs from ${circuitIds.length} circuits to ${OUTPUT_DIR}`, + ) + + if (timedOutCircuits.length > 0) { + console.error(`timed out circuits: ${timedOutCircuits.join(", ")}`) + } + if (failedCircuits.length > 0) { + console.error(`failed circuits: ${failedCircuits.join(", ")}`) + } + if (timedOutCircuits.length > 0 || failedCircuits.length > 0) { + process.exit(1) + } +} + +const runWorker = async () => { + const { circuitId, startIndex } = parseArgs() + if (!circuitId) { + throw new Error("--worker requires --circuit") + } + + const summary = await writeCircuitSamples(circuitId, startIndex) + process.stdout.write(JSON.stringify(summary)) +} + +const { worker } = parseArgs() +const main = worker ? runWorker : runParent + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/tests/__snapshots__/dataset01-sample3.snapshot.snap.svg b/tests/__snapshots__/dataset01-sample3.snapshot.snap.svg new file mode 100644 index 000000000..20dcc2f7c --- /dev/null +++ b/tests/__snapshots__/dataset01-sample3.snapshot.snap.svg @@ -0,0 +1,108 @@ + \ No newline at end of file diff --git a/tests/__snapshots__/e2e3-jumpers.snap.svg b/tests/__snapshots__/e2e3-jumpers.snap.svg index 62bd2e362..c7b2e3836 100644 --- a/tests/__snapshots__/e2e3-jumpers.snap.svg +++ b/tests/__snapshots__/e2e3-jumpers.snap.svg @@ -1,4 +1,36 @@ -