diff --git a/algos/common/generalized-astar/FixedMemoryGeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/FixedMemoryGeneralizedAstarAutorouter.ts new file mode 100644 index 0000000..736847b --- /dev/null +++ b/algos/common/generalized-astar/FixedMemoryGeneralizedAstarAutorouter.ts @@ -0,0 +1,20 @@ +import type { IGeneralizedAstarAutorouter } from "./IGeneralizedAstarAutorouter" +import type { Node, Point, PointWithObstacleHit } from "./types" + +/** + * This is a version of GeneralizedAstarAutorouter that has completely fixed + * memory. Conceptually it is a precursor to a C implementation. + */ +export class FixedMemoryGeneralizedAstar + implements IGeneralizedAstarAutorouter +{ + computeG(current: Node, neighbor: Point): number { + throw new Error("computeG needs override") + } + computeH(node: Point): number { + throw new Error("computeH needs override") + } + getNeighbors(node: Node): Array { + throw new Error("getNeighbors needs override") + } +} diff --git a/algos/common/generalized-astar/GENERALIZED_ASTAR_README.md b/algos/common/generalized-astar/GENERALIZED_ASTAR_README.md new file mode 100644 index 0000000..1b615bf --- /dev/null +++ b/algos/common/generalized-astar/GENERALIZED_ASTAR_README.md @@ -0,0 +1,97 @@ +# Generalized Astar README + +This is a short introduction to our GeneralizedAstar class. This is a class you +can extend to create lots of different algorithms. Let's review how A\* works +and why and how you might want to extend it. + +## Autorouting and A\*, what paths are we finding? + +We have a list of squares (pads) that we want to connect with wires (traces), +we know the location of the pads but we don't know what the traces should look +like. The GeneralizedAstar class has an opinionated way of finding all the traces, +it says: + +1. let's just go through each pair of connected pads +2. find the approximate shortest path between the pads, make that path a trace, +3. repeat until we've routed the entire board + +You can extend the GeneralizedAstar class to change how step 2 works. You'd +think this would be a pretty straightforward process and for many pathfinding +solvers it is, but we want to make it _crazy fast_ and that's where things get +tricky. + +## A brief introduction to A\* + +A\* is a really good pathfinding algorithm, here's how it works: + +- store a queue of points, at the beginning it's just the starting point +- go to the starting point and find all of it's neighbors, score each neighbor + with TWO costs + - the `g` cost is the distance it's taken to get the that neighbor so far + - the `h` cost which is a guess for how far that neighbor is from the goal +- insert each new neighbor into the queue with it's total cost `g + h` +- select the point with the lowest cost from the queue and repeat! + +This is a much faster than other approaches of exploring paths. We even employ +some optimizations like the "greedy multipler" to make A\* run even faster (but +sometimes return less-than-optimal results) + +## Customizing `GeneralizedAstar` + +There are a couple ways that you can customize `GeneralizedAstar` to try out +new algorithms: + +- Override `getNeighbors` to change how you find the neighbors of a point +- Override `computeH` to change how you guess at the distance to the goal +- Override `computeG` to change how you compute the distance of a point so far + +Here are examples of reasons you might change or override each of these: + +- Override `computeH` and `computeG` to add penalties for vias +- Override `getNeighbors` to calculate the next neighbors by finding line + intersections using the [intersection jumping technique](https://blog.autorouting.com/p/the-intersection-jump-autorouter) +- Override `getNeighbors` to consider "bus lanes" or diagonal movements + +## What you can't do with `GeneralizedAstar` + +- Customize which traces are selected to go first (for this, we recommend + a higher-level algorithm that _orchestrates_ a `GeneralizedAstar` autorouter) + +## Show me an example of extending `GeneralizedAstar` + +Sure! Here's a version of `GeneralizedAstar` that does a fairly standard +grid-based search (however, unlike many grid-searches, this one can operate +on an infinitely sized grid!) + +```tsx +export class InfiniteGridAutorouter extends GeneralizedAstarAutorouter { + getNeighbors(node: Node): Array { + const dirs = [ + { x: 0, y: this.GRID_STEP }, + { x: this.GRID_STEP, y: 0 }, + { x: 0, y: -this.GRID_STEP }, + { x: -this.GRID_STEP, y: 0 }, + ] + + return dirs + .filter( + (dir) => !this.obstacles!.isObstacleAt(node.x + dir.x, node.y + dir.y) + ) + .map((dir) => ({ + x: node.x + dir.x, + y: node.y + dir.y, + })) + } +} +``` + +## Glossary + +If you're trying to understand `GeneralizedAstar`, it can help to know these +terms: + +- `closedSet` - The set of explored points +- `openSet` - The set of unexplored points +- `GREEDY_MULTIPLIER` - By default set to `1.1`, this makes the algorithm find + suboptimal paths because it will act more greedily, it can dramatically + increase the speed of the algorithm diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts new file mode 100644 index 0000000..1d50777 --- /dev/null +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -0,0 +1,495 @@ +import type { AnySoupElement, LayerRef, PCBSMTPad } from "@tscircuit/soup" +// import { QuadtreeObstacleList } from "./QuadtreeObstacleList" +import type { Node, Point, PointWithObstacleHit } from "./types" +import { manDist, nodeName } from "./util" + +import Debug from "debug" +import type { + Obstacle, + SimpleRouteConnection, + SimpleRouteJson, + SimplifiedPcbTrace, +} from "solver-utils" +import { getObstaclesFromRoute } from "solver-utils/getObstaclesFromRoute" +import { ObstacleList } from "./ObstacleList" +import { removePathLoops } from "solver-postprocessing/remove-path-loops" +import { addViasWhenLayerChanges } from "solver-postprocessing/add-vias-when-layer-changes" +import type { AnyCircuitElement } from "circuit-json" + +const debug = Debug("autorouting-dataset:astar") + +export interface PointWithLayer extends Point { + layer: string +} + +export type ConnectionSolveResult = + | { solved: false; connectionName: string } + | { solved: true; connectionName: string; route: PointWithLayer[] } + +export class GeneralizedAstarAutorouter { + openSet: Node[] = [] + closedSet: Set = new Set() + debug = false + + debugSolutions?: Record + debugMessage: string | null = null + debugTraceCount: number = 0 + + input: SimpleRouteJson + obstacles?: ObstacleList + allObstacles: Obstacle[] + startNode?: Node + goalPoint?: Point & { l: number } + GRID_STEP: number + OBSTACLE_MARGIN: number + MAX_ITERATIONS: number + isRemovePathLoopsEnabled: boolean + /** + * Setting this greater than 1 makes the algorithm find suboptimal paths and + * act more greedy, but at greatly improves performance. + * + * Recommended value is between 1.1 and 1.5 + */ + GREEDY_MULTIPLIER = 1.1 + + iterations: number = -1 + + constructor(opts: { + input: SimpleRouteJson + startNode?: Node + goalPoint?: Point + GRID_STEP?: number + OBSTACLE_MARGIN?: number + MAX_ITERATIONS?: number + isRemovePathLoopsEnabled?: boolean + debug?: boolean + }) { + this.input = opts.input + this.allObstacles = opts.input.obstacles + this.startNode = opts.startNode + this.goalPoint = opts.goalPoint + ? ({ l: 0, ...opts.goalPoint } as any) + : undefined + this.GRID_STEP = opts.GRID_STEP ?? 0.1 + this.OBSTACLE_MARGIN = opts.OBSTACLE_MARGIN ?? 0.15 + this.MAX_ITERATIONS = opts.MAX_ITERATIONS ?? 100 + this.debug = opts.debug ?? debug.enabled + this.isRemovePathLoopsEnabled = opts.isRemovePathLoopsEnabled ?? false + if (this.debug) { + debug.enabled = true + } + + if (debug.enabled) { + this.debugSolutions = {} + this.debugMessage = "" + } + } + + /** + * Return points of interest for this node. Don't worry about checking if + * points are already visited. You must check that these neighbors are valid + * (not inside an obstacle) + * + * In a simple grid, this is just the 4 neighbors surrounding the node. + * + * In ijump-astar, this is the 2-4 surrounding intersections + */ + getNeighbors(node: Node): Array { + return [] + } + + isSameNode(a: Point, b: Point): boolean { + return manDist(a, b) < this.GRID_STEP + } + + /** + * Compute the cost of this path. In normal astar, this is just the length of + * the path, but you can override this term to penalize paths that are more + * complex. + */ + computeG(current: Node, neighbor: Point): number { + return current.g + manDist(current, neighbor) + } + + computeH(node: Point): number { + return manDist(node, this.goalPoint!) + } + + getNodeName(node: Point): string { + return nodeName(node, this.GRID_STEP) + } + + solveOneStep(): { + solved: boolean + current: Node + newNeighbors: Node[] + } { + this.iterations += 1 + const { openSet, closedSet, GRID_STEP, goalPoint } = this + openSet.sort((a, b) => a.f - b.f) + + const current = openSet.shift()! + const goalDist = this.computeH(current) + if (goalDist <= GRID_STEP * 2) { + return { + solved: true, + current, + newNeighbors: [], + } + } + + this.closedSet.add(this.getNodeName(current)) + + let newNeighbors: Node[] = [] + for (const neighbor of this.getNeighbors(current)) { + if (closedSet.has(this.getNodeName(neighbor))) continue + + const tentativeG = this.computeG(current, neighbor) + + const existingNeighbor = this.openSet.find((n) => + this.isSameNode(n, neighbor), + ) + + if (!existingNeighbor || tentativeG < existingNeighbor.g) { + const h = this.computeH(neighbor) + + const f = tentativeG + h * this.GREEDY_MULTIPLIER + + const neighborNode: Node = { + ...neighbor, + g: tentativeG, + h, + f, + obstacleHit: neighbor.obstacleHit ?? undefined, + manDistFromParent: manDist(current, neighbor), // redundant compute... + nodesInPath: current.nodesInPath + 1, + parent: current, + enterMarginCost: neighbor.enterMarginCost, + travelMarginCostFactor: neighbor.travelMarginCostFactor, + } + + openSet.push(neighborNode) + newNeighbors.push(neighborNode) + } + } + + if (debug.enabled) { + openSet.sort((a, b) => a.f - b.f) + this.drawDebugSolution({ current, newNeighbors }) + } + + return { + solved: false, + current, + newNeighbors, + } + } + + getStartNode(connection: SimpleRouteConnection): Node { + return { + x: connection.pointsToConnect[0].x, + y: connection.pointsToConnect[0].y, + manDistFromParent: 0, + f: 0, + g: 0, + h: 0, + nodesInPath: 0, + parent: null, + } + } + + layerToIndex(layer: string): number { + return 0 + } + indexToLayer(index: number): string { + return "top" + } + + /** + * Add a preprocessing step before solving a connection to do adjust points + * based on previous iterations. For example, if a previous connection solved + * for a trace on the same net, you may want to preprocess the connection to + * solve for an easier start and end point + * + * The simplest way to do this is to run getConnectionWithAlternativeGoalBoxes + * with any pcb_traces created by previous iterations + */ + preprocessConnectionBeforeSolving( + connection: SimpleRouteConnection, + ): SimpleRouteConnection { + return connection + } + + solveConnection(connection: SimpleRouteConnection): ConnectionSolveResult { + if (connection.pointsToConnect.length > 2) { + throw new Error( + "GeneralizedAstarAutorouter doesn't currently support 2+ points in a connection", + ) + } + connection = this.preprocessConnectionBeforeSolving(connection) + + const { pointsToConnect } = connection + + this.iterations = 0 + this.closedSet = new Set() + this.startNode = this.getStartNode(connection) + this.goalPoint = { + ...pointsToConnect[pointsToConnect.length - 1], + l: this.layerToIndex(pointsToConnect[pointsToConnect.length - 1].layer), + } + this.openSet = [this.startNode] + + while (this.iterations < this.MAX_ITERATIONS) { + const { solved, current } = this.solveOneStep() + + if (solved) { + let route: PointWithLayer[] = [] + let node: Node | null = current + while (node) { + const l: number | undefined = (node as any).l + route.unshift({ + x: node.x, + y: node.y, + // TODO: this layer should be included as part of the node + layer: + l !== undefined ? this.indexToLayer(l) : pointsToConnect[0].layer, + }) + node = node.parent + } + + if (debug.enabled) { + this.debugMessage += `t${this.debugTraceCount}: ${this.iterations} iterations\n` + } + + if (this.isRemovePathLoopsEnabled) { + route = removePathLoops(route) + } + + return { solved: true, route, connectionName: connection.name } + } + + if (this.openSet.length === 0) { + break + } + } + + if (debug.enabled) { + this.debugMessage += `t${this.debugTraceCount}: ${this.iterations} iterations (failed)\n` + } + + return { solved: false, connectionName: connection.name } + } + + createObstacleList({ + dominantLayer, + connection, + obstaclesFromTraces, + }: { + dominantLayer?: string + connection: SimpleRouteConnection + obstaclesFromTraces: Obstacle[] + }): ObstacleList { + return new ObstacleList( + this.allObstacles + .filter((obstacle) => !obstacle.connectedTo.includes(connection.name)) + // TODO obstacles on different layers should be filtered inside + // the algorithm, not for the entire connection, this is a hack in + // relation to https://github.com/tscircuit/tscircuit/issues/432 + .filter((obstacle) => obstacle.layers.includes(dominantLayer as any)) + .concat(obstaclesFromTraces ?? []), + ) + } + + /** + * Override this to implement smoothing strategies or incorporate new traces + * into a connectivity map + */ + postprocessConnectionSolveResult( + connection: SimpleRouteConnection, + result: ConnectionSolveResult, + ): ConnectionSolveResult { + return result + } + + /** + * By default, this will solve the connections in the order they are given, + * and add obstacles for each successfully solved connection. Override this + * to implement "rip and replace" rerouting strategies. + */ + solve(): ConnectionSolveResult[] { + const solutions: ConnectionSolveResult[] = [] + const obstaclesFromTraces: Obstacle[] = [] + this.debugTraceCount = 0 + for (const connection of this.input.connections) { + const dominantLayer = connection.pointsToConnect[0].layer ?? "top" + this.debugTraceCount += 1 + this.obstacles = this.createObstacleList({ + dominantLayer, + connection, + obstaclesFromTraces, + }) + let result = this.solveConnection(connection) + result = this.postprocessConnectionSolveResult(connection, result) + solutions.push(result) + + if (debug.enabled) { + this.drawDebugTraceObstacles(obstaclesFromTraces) + } + + if (result.solved) { + obstaclesFromTraces.push( + ...getObstaclesFromRoute( + result.route.map((p) => ({ + x: p.x, + y: p.y, + layer: p.layer ?? dominantLayer, + })), + connection.name, + ), + ) + } + } + + return solutions + } + + solveAndMapToTraces(): SimplifiedPcbTrace[] { + const solutions = this.solve() + + return solutions.flatMap((solution): SimplifiedPcbTrace[] => { + if (!solution.solved) return [] + return [ + { + type: "pcb_trace" as const, + pcb_trace_id: `pcb_trace_for_${solution.connectionName}`, + route: addViasWhenLayerChanges( + solution.route.map((point) => ({ + route_type: "wire" as const, + x: point.x, + y: point.y, + width: this.input.minTraceWidth, + layer: point.layer as LayerRef, + })), + ), + }, + ] + }) + } + + getDebugGroup(): string | null { + const dgn = `t${this.debugTraceCount}_iter[${this.iterations - 1}]` + if (this.iterations < 30) return dgn + if (this.iterations < 100 && this.iterations % 10 === 0) return dgn + if (this.iterations < 1000 && this.iterations % 100 === 0) return dgn + if (!this.debugSolutions) return dgn + return null + } + + drawDebugTraceObstacles(obstacles: Obstacle[]) { + const { debugTraceCount, debugSolutions } = this + for (const key in debugSolutions) { + if (key.startsWith(`t${debugTraceCount}_`)) { + debugSolutions[key].push( + ...obstacles.map( + (obstacle, i) => + ({ + type: "pcb_smtpad", + pcb_component_id: "", + layer: obstacle.layers[0], + width: obstacle.width, + shape: "rect", + x: obstacle.center.x, + y: obstacle.center.y, + pcb_smtpad_id: `trace_obstacle_${i}`, + height: obstacle.height, + }) as PCBSMTPad, + ), + ) + } + } + } + + drawDebugSolution({ + current, + newNeighbors, + }: { + current: Node + newNeighbors: Node[] + }) { + const debugGroup = this.getDebugGroup() + if (!debugGroup) return + + const { openSet, debugTraceCount, debugSolutions } = this + + debugSolutions![debugGroup] ??= [] + const debugSolution = debugSolutions![debugGroup]! + + debugSolution.push({ + type: "pcb_fabrication_note_text", + pcb_fabrication_note_text_id: `debug_note_${current.x}_${current.y}`, + font: "tscircuit2024", + font_size: 0.25, + text: "X" + (current.l !== undefined ? current.l : ""), + pcb_component_id: "", + layer: "top", + anchor_position: { + x: current.x, + y: current.y, + }, + anchor_alignment: "center", + }) + // Add all the openSet as small diamonds + for (let i = 0; i < openSet.length; i++) { + const node = openSet[i] + debugSolution.push({ + type: "pcb_fabrication_note_path", + pcb_component_id: "", + pcb_fabrication_note_path_id: `note_path_${node.x}_${node.y}`, + layer: "top", + route: [ + [0, 0.05], + [0.05, 0], + [0, -0.05], + [-0.05, 0], + [0, 0.05], + ].map(([dx, dy]) => ({ + x: node.x + dx, + y: node.y + dy, + })), + stroke_width: 0.01, + }) + // Add text that indicates the order of this point + debugSolution.push({ + type: "pcb_fabrication_note_text", + pcb_fabrication_note_text_id: `debug_note_${node.x}_${node.y}`, + font: "tscircuit2024", + font_size: 0.03, + text: i.toString(), + pcb_component_id: "", + layer: "top", + anchor_position: { + x: node.x, + y: node.y, + }, + anchor_alignment: "center", + }) + } + + if (current.parent) { + const path: Node[] = [] + let p: Node | null = current + while (p) { + path.unshift(p) + p = p.parent + } + debugSolution!.push({ + type: "pcb_fabrication_note_path", + pcb_component_id: "", + pcb_fabrication_note_path_id: `note_path_${current.x}_${current.y}`, + layer: "top", + route: path, + stroke_width: 0.01, + }) + } + } +} diff --git a/algos/common/generalized-astar/IGeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/IGeneralizedAstarAutorouter.ts new file mode 100644 index 0000000..6a76ff0 --- /dev/null +++ b/algos/common/generalized-astar/IGeneralizedAstarAutorouter.ts @@ -0,0 +1,7 @@ +import type { Node, Point, PointWithObstacleHit } from "./types" + +export interface IGeneralizedAstarAutorouter { + computeG(current: Node, neighbor: Point): number + computeH(node: Point): number + getNeighbors(node: Node): Array +} diff --git a/algos/common/generalized-astar/ObstacleList.ts b/algos/common/generalized-astar/ObstacleList.ts new file mode 100644 index 0000000..22c0864 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList.ts @@ -0,0 +1,168 @@ +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import type { + Direction, + DirectionDistances, + DirectionWithCollisionInfo, + Point, +} from "./types" + +/** + * A list of obstacles with functions for fast lookups, this default implementation + * has no optimizations, you should override this class to implement faster lookups + */ +export class ObstacleList { + protected obstacles: ObstacleWithEdges[] + protected GRID_STEP = 0.1 + + constructor(obstacles: Array) { + this.obstacles = obstacles.map((obstacle) => ({ + ...obstacle, + left: obstacle.center.x - obstacle.width / 2, + right: obstacle.center.x + obstacle.width / 2, + top: obstacle.center.y + obstacle.height / 2, + bottom: obstacle.center.y - obstacle.height / 2, + })) + } + + getObstacleAt(x: number, y: number, m?: number): Obstacle | null { + m ??= this.GRID_STEP + for (const obstacle of this.obstacles) { + const halfWidth = obstacle.width / 2 + m + const halfHeight = obstacle.height / 2 + m + if ( + x >= obstacle.center.x - halfWidth && + x <= obstacle.center.x + halfWidth && + y >= obstacle.center.y - halfHeight && + y <= obstacle.center.y + halfHeight + ) { + return obstacle + } + } + return null + } + + isObstacleAt(x: number, y: number, m?: number): boolean { + return this.getObstacleAt(x, y, m) !== null + } + + getDirectionDistancesToNearestObstacle( + x: number, + y: number, + ): DirectionDistances { + const { GRID_STEP } = this + const result: DirectionDistances = { + left: Infinity, + top: Infinity, + bottom: Infinity, + right: Infinity, + } + + for (const obstacle of this.obstacles) { + if (obstacle.type === "rect") { + const left = obstacle.center.x - obstacle.width / 2 - GRID_STEP + const right = obstacle.center.x + obstacle.width / 2 + GRID_STEP + const top = obstacle.center.y + obstacle.height / 2 + GRID_STEP + const bottom = obstacle.center.y - obstacle.height / 2 - GRID_STEP + + // Check left + if (y >= bottom && y <= top && x > left) { + result.left = Math.min(result.left, x - right) + } + + // Check right + if (y >= bottom && y <= top && x < right) { + result.right = Math.min(result.right, left - x) + } + + // Check top + if (x >= left && x <= right && y < top) { + result.top = Math.min(result.top, bottom - y) + } + + // Check bottom + if (x >= left && x <= right && y > bottom) { + result.bottom = Math.min(result.bottom, y - top) + } + } + } + + return result + } + + getOrthoDirectionCollisionInfo( + point: Point, + dir: Direction, + { margin = 0 }: { margin?: number } = {}, + ): DirectionWithCollisionInfo { + const { x, y } = point + const { dx, dy } = dir + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null + + for (const obstacle of this.obstacles) { + const leftMargin = obstacle.left - margin + const rightMargin = obstacle.right + margin + const topMargin = obstacle.top + margin + const bottomMargin = obstacle.bottom - margin + + let distance: number | null = null + + if (dx === 1 && dy === 0) { + // Right + if (y > bottomMargin && y < topMargin && x < obstacle.left) { + distance = obstacle.left - x + } + } else if (dx === -1 && dy === 0) { + // Left + if (y > bottomMargin && y < topMargin && x > obstacle.right) { + distance = x - obstacle.right + } + } else if (dx === 0 && dy === 1) { + // Up + if (x > leftMargin && x < rightMargin && y < obstacle.bottom) { + distance = obstacle.bottom - y + } + } else if (dx === 0 && dy === -1) { + // Down + if (x > leftMargin && x < rightMargin && y > obstacle.top) { + distance = y - obstacle.top + } + } + + if (distance !== null && distance < minDistance) { + minDistance = distance + collisionObstacle = obstacle + } + } + + return { + dx, + dy, + wallDistance: minDistance, + obstacle: collisionObstacle as ObstacleWithEdges, + } + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + }): ObstacleWithEdges[] { + const obstacles: ObstacleWithEdges[] = [] + for (const obstacle of this.obstacles) { + const { left, right, top, bottom } = obstacle + + if ( + left >= region.minX && + right <= region.maxX && + top >= region.minY && + bottom <= region.maxY + ) { + obstacles.push(obstacle) + } + } + + return obstacles + } +} diff --git a/algos/common/generalized-astar/types.ts b/algos/common/generalized-astar/types.ts new file mode 100644 index 0000000..0ce70ff --- /dev/null +++ b/algos/common/generalized-astar/types.ts @@ -0,0 +1,66 @@ +import type { Obstacle } from "autorouting-dataset/lib/types" + +export interface DirectionDistances { + left: number + top: number + bottom: number + right: number +} + +export interface Direction { + dx: number + dy: number +} + +export interface DirectionWithWallDistance extends Direction { + wallDistance: number +} + +export interface DirectionWithCollisionInfo extends Direction { + wallDistance: number + obstacle: Obstacle | null +} + +export interface Point { + x: number + y: number +} + +export interface PointWithObstacleHit extends Point { + obstacleHit?: Obstacle | null + + /** + * Used in multi-margin autorouter to penalize traveling close to the wall + */ + travelMarginCostFactor?: number + enterMarginCost?: number +} + +export interface PointWithWallDistance extends Point { + wallDistance: number +} + +export interface Node extends Point { + /** Distance from the parent node (along path) */ + g: number + /** Heuristic distance from the goal */ + h: number + /** Distance score for this node (g + h) */ + f: number + /** Manhattan Distance from the parent node */ + manDistFromParent: number + nodesInPath: number + obstacleHit?: Obstacle + parent: Node | null + + /** + * Used in multi-margin autorouter to penalize traveling close to the wall + */ + travelMarginCostFactor?: number + enterMarginCost?: number + + /** + * Layer index, not needed for single-layer autorouters + */ + l?: number +} diff --git a/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts b/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts index 1d50777..7f2a54d 100644 --- a/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts +++ b/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts @@ -1,495 +1 @@ -import type { AnySoupElement, LayerRef, PCBSMTPad } from "@tscircuit/soup" -// import { QuadtreeObstacleList } from "./QuadtreeObstacleList" -import type { Node, Point, PointWithObstacleHit } from "./types" -import { manDist, nodeName } from "./util" - -import Debug from "debug" -import type { - Obstacle, - SimpleRouteConnection, - SimpleRouteJson, - SimplifiedPcbTrace, -} from "solver-utils" -import { getObstaclesFromRoute } from "solver-utils/getObstaclesFromRoute" -import { ObstacleList } from "./ObstacleList" -import { removePathLoops } from "solver-postprocessing/remove-path-loops" -import { addViasWhenLayerChanges } from "solver-postprocessing/add-vias-when-layer-changes" -import type { AnyCircuitElement } from "circuit-json" - -const debug = Debug("autorouting-dataset:astar") - -export interface PointWithLayer extends Point { - layer: string -} - -export type ConnectionSolveResult = - | { solved: false; connectionName: string } - | { solved: true; connectionName: string; route: PointWithLayer[] } - -export class GeneralizedAstarAutorouter { - openSet: Node[] = [] - closedSet: Set = new Set() - debug = false - - debugSolutions?: Record - debugMessage: string | null = null - debugTraceCount: number = 0 - - input: SimpleRouteJson - obstacles?: ObstacleList - allObstacles: Obstacle[] - startNode?: Node - goalPoint?: Point & { l: number } - GRID_STEP: number - OBSTACLE_MARGIN: number - MAX_ITERATIONS: number - isRemovePathLoopsEnabled: boolean - /** - * Setting this greater than 1 makes the algorithm find suboptimal paths and - * act more greedy, but at greatly improves performance. - * - * Recommended value is between 1.1 and 1.5 - */ - GREEDY_MULTIPLIER = 1.1 - - iterations: number = -1 - - constructor(opts: { - input: SimpleRouteJson - startNode?: Node - goalPoint?: Point - GRID_STEP?: number - OBSTACLE_MARGIN?: number - MAX_ITERATIONS?: number - isRemovePathLoopsEnabled?: boolean - debug?: boolean - }) { - this.input = opts.input - this.allObstacles = opts.input.obstacles - this.startNode = opts.startNode - this.goalPoint = opts.goalPoint - ? ({ l: 0, ...opts.goalPoint } as any) - : undefined - this.GRID_STEP = opts.GRID_STEP ?? 0.1 - this.OBSTACLE_MARGIN = opts.OBSTACLE_MARGIN ?? 0.15 - this.MAX_ITERATIONS = opts.MAX_ITERATIONS ?? 100 - this.debug = opts.debug ?? debug.enabled - this.isRemovePathLoopsEnabled = opts.isRemovePathLoopsEnabled ?? false - if (this.debug) { - debug.enabled = true - } - - if (debug.enabled) { - this.debugSolutions = {} - this.debugMessage = "" - } - } - - /** - * Return points of interest for this node. Don't worry about checking if - * points are already visited. You must check that these neighbors are valid - * (not inside an obstacle) - * - * In a simple grid, this is just the 4 neighbors surrounding the node. - * - * In ijump-astar, this is the 2-4 surrounding intersections - */ - getNeighbors(node: Node): Array { - return [] - } - - isSameNode(a: Point, b: Point): boolean { - return manDist(a, b) < this.GRID_STEP - } - - /** - * Compute the cost of this path. In normal astar, this is just the length of - * the path, but you can override this term to penalize paths that are more - * complex. - */ - computeG(current: Node, neighbor: Point): number { - return current.g + manDist(current, neighbor) - } - - computeH(node: Point): number { - return manDist(node, this.goalPoint!) - } - - getNodeName(node: Point): string { - return nodeName(node, this.GRID_STEP) - } - - solveOneStep(): { - solved: boolean - current: Node - newNeighbors: Node[] - } { - this.iterations += 1 - const { openSet, closedSet, GRID_STEP, goalPoint } = this - openSet.sort((a, b) => a.f - b.f) - - const current = openSet.shift()! - const goalDist = this.computeH(current) - if (goalDist <= GRID_STEP * 2) { - return { - solved: true, - current, - newNeighbors: [], - } - } - - this.closedSet.add(this.getNodeName(current)) - - let newNeighbors: Node[] = [] - for (const neighbor of this.getNeighbors(current)) { - if (closedSet.has(this.getNodeName(neighbor))) continue - - const tentativeG = this.computeG(current, neighbor) - - const existingNeighbor = this.openSet.find((n) => - this.isSameNode(n, neighbor), - ) - - if (!existingNeighbor || tentativeG < existingNeighbor.g) { - const h = this.computeH(neighbor) - - const f = tentativeG + h * this.GREEDY_MULTIPLIER - - const neighborNode: Node = { - ...neighbor, - g: tentativeG, - h, - f, - obstacleHit: neighbor.obstacleHit ?? undefined, - manDistFromParent: manDist(current, neighbor), // redundant compute... - nodesInPath: current.nodesInPath + 1, - parent: current, - enterMarginCost: neighbor.enterMarginCost, - travelMarginCostFactor: neighbor.travelMarginCostFactor, - } - - openSet.push(neighborNode) - newNeighbors.push(neighborNode) - } - } - - if (debug.enabled) { - openSet.sort((a, b) => a.f - b.f) - this.drawDebugSolution({ current, newNeighbors }) - } - - return { - solved: false, - current, - newNeighbors, - } - } - - getStartNode(connection: SimpleRouteConnection): Node { - return { - x: connection.pointsToConnect[0].x, - y: connection.pointsToConnect[0].y, - manDistFromParent: 0, - f: 0, - g: 0, - h: 0, - nodesInPath: 0, - parent: null, - } - } - - layerToIndex(layer: string): number { - return 0 - } - indexToLayer(index: number): string { - return "top" - } - - /** - * Add a preprocessing step before solving a connection to do adjust points - * based on previous iterations. For example, if a previous connection solved - * for a trace on the same net, you may want to preprocess the connection to - * solve for an easier start and end point - * - * The simplest way to do this is to run getConnectionWithAlternativeGoalBoxes - * with any pcb_traces created by previous iterations - */ - preprocessConnectionBeforeSolving( - connection: SimpleRouteConnection, - ): SimpleRouteConnection { - return connection - } - - solveConnection(connection: SimpleRouteConnection): ConnectionSolveResult { - if (connection.pointsToConnect.length > 2) { - throw new Error( - "GeneralizedAstarAutorouter doesn't currently support 2+ points in a connection", - ) - } - connection = this.preprocessConnectionBeforeSolving(connection) - - const { pointsToConnect } = connection - - this.iterations = 0 - this.closedSet = new Set() - this.startNode = this.getStartNode(connection) - this.goalPoint = { - ...pointsToConnect[pointsToConnect.length - 1], - l: this.layerToIndex(pointsToConnect[pointsToConnect.length - 1].layer), - } - this.openSet = [this.startNode] - - while (this.iterations < this.MAX_ITERATIONS) { - const { solved, current } = this.solveOneStep() - - if (solved) { - let route: PointWithLayer[] = [] - let node: Node | null = current - while (node) { - const l: number | undefined = (node as any).l - route.unshift({ - x: node.x, - y: node.y, - // TODO: this layer should be included as part of the node - layer: - l !== undefined ? this.indexToLayer(l) : pointsToConnect[0].layer, - }) - node = node.parent - } - - if (debug.enabled) { - this.debugMessage += `t${this.debugTraceCount}: ${this.iterations} iterations\n` - } - - if (this.isRemovePathLoopsEnabled) { - route = removePathLoops(route) - } - - return { solved: true, route, connectionName: connection.name } - } - - if (this.openSet.length === 0) { - break - } - } - - if (debug.enabled) { - this.debugMessage += `t${this.debugTraceCount}: ${this.iterations} iterations (failed)\n` - } - - return { solved: false, connectionName: connection.name } - } - - createObstacleList({ - dominantLayer, - connection, - obstaclesFromTraces, - }: { - dominantLayer?: string - connection: SimpleRouteConnection - obstaclesFromTraces: Obstacle[] - }): ObstacleList { - return new ObstacleList( - this.allObstacles - .filter((obstacle) => !obstacle.connectedTo.includes(connection.name)) - // TODO obstacles on different layers should be filtered inside - // the algorithm, not for the entire connection, this is a hack in - // relation to https://github.com/tscircuit/tscircuit/issues/432 - .filter((obstacle) => obstacle.layers.includes(dominantLayer as any)) - .concat(obstaclesFromTraces ?? []), - ) - } - - /** - * Override this to implement smoothing strategies or incorporate new traces - * into a connectivity map - */ - postprocessConnectionSolveResult( - connection: SimpleRouteConnection, - result: ConnectionSolveResult, - ): ConnectionSolveResult { - return result - } - - /** - * By default, this will solve the connections in the order they are given, - * and add obstacles for each successfully solved connection. Override this - * to implement "rip and replace" rerouting strategies. - */ - solve(): ConnectionSolveResult[] { - const solutions: ConnectionSolveResult[] = [] - const obstaclesFromTraces: Obstacle[] = [] - this.debugTraceCount = 0 - for (const connection of this.input.connections) { - const dominantLayer = connection.pointsToConnect[0].layer ?? "top" - this.debugTraceCount += 1 - this.obstacles = this.createObstacleList({ - dominantLayer, - connection, - obstaclesFromTraces, - }) - let result = this.solveConnection(connection) - result = this.postprocessConnectionSolveResult(connection, result) - solutions.push(result) - - if (debug.enabled) { - this.drawDebugTraceObstacles(obstaclesFromTraces) - } - - if (result.solved) { - obstaclesFromTraces.push( - ...getObstaclesFromRoute( - result.route.map((p) => ({ - x: p.x, - y: p.y, - layer: p.layer ?? dominantLayer, - })), - connection.name, - ), - ) - } - } - - return solutions - } - - solveAndMapToTraces(): SimplifiedPcbTrace[] { - const solutions = this.solve() - - return solutions.flatMap((solution): SimplifiedPcbTrace[] => { - if (!solution.solved) return [] - return [ - { - type: "pcb_trace" as const, - pcb_trace_id: `pcb_trace_for_${solution.connectionName}`, - route: addViasWhenLayerChanges( - solution.route.map((point) => ({ - route_type: "wire" as const, - x: point.x, - y: point.y, - width: this.input.minTraceWidth, - layer: point.layer as LayerRef, - })), - ), - }, - ] - }) - } - - getDebugGroup(): string | null { - const dgn = `t${this.debugTraceCount}_iter[${this.iterations - 1}]` - if (this.iterations < 30) return dgn - if (this.iterations < 100 && this.iterations % 10 === 0) return dgn - if (this.iterations < 1000 && this.iterations % 100 === 0) return dgn - if (!this.debugSolutions) return dgn - return null - } - - drawDebugTraceObstacles(obstacles: Obstacle[]) { - const { debugTraceCount, debugSolutions } = this - for (const key in debugSolutions) { - if (key.startsWith(`t${debugTraceCount}_`)) { - debugSolutions[key].push( - ...obstacles.map( - (obstacle, i) => - ({ - type: "pcb_smtpad", - pcb_component_id: "", - layer: obstacle.layers[0], - width: obstacle.width, - shape: "rect", - x: obstacle.center.x, - y: obstacle.center.y, - pcb_smtpad_id: `trace_obstacle_${i}`, - height: obstacle.height, - }) as PCBSMTPad, - ), - ) - } - } - } - - drawDebugSolution({ - current, - newNeighbors, - }: { - current: Node - newNeighbors: Node[] - }) { - const debugGroup = this.getDebugGroup() - if (!debugGroup) return - - const { openSet, debugTraceCount, debugSolutions } = this - - debugSolutions![debugGroup] ??= [] - const debugSolution = debugSolutions![debugGroup]! - - debugSolution.push({ - type: "pcb_fabrication_note_text", - pcb_fabrication_note_text_id: `debug_note_${current.x}_${current.y}`, - font: "tscircuit2024", - font_size: 0.25, - text: "X" + (current.l !== undefined ? current.l : ""), - pcb_component_id: "", - layer: "top", - anchor_position: { - x: current.x, - y: current.y, - }, - anchor_alignment: "center", - }) - // Add all the openSet as small diamonds - for (let i = 0; i < openSet.length; i++) { - const node = openSet[i] - debugSolution.push({ - type: "pcb_fabrication_note_path", - pcb_component_id: "", - pcb_fabrication_note_path_id: `note_path_${node.x}_${node.y}`, - layer: "top", - route: [ - [0, 0.05], - [0.05, 0], - [0, -0.05], - [-0.05, 0], - [0, 0.05], - ].map(([dx, dy]) => ({ - x: node.x + dx, - y: node.y + dy, - })), - stroke_width: 0.01, - }) - // Add text that indicates the order of this point - debugSolution.push({ - type: "pcb_fabrication_note_text", - pcb_fabrication_note_text_id: `debug_note_${node.x}_${node.y}`, - font: "tscircuit2024", - font_size: 0.03, - text: i.toString(), - pcb_component_id: "", - layer: "top", - anchor_position: { - x: node.x, - y: node.y, - }, - anchor_alignment: "center", - }) - } - - if (current.parent) { - const path: Node[] = [] - let p: Node | null = current - while (p) { - path.unshift(p) - p = p.parent - } - debugSolution!.push({ - type: "pcb_fabrication_note_path", - pcb_component_id: "", - pcb_fabrication_note_path_id: `note_path_${current.x}_${current.y}`, - layer: "top", - route: path, - stroke_width: 0.01, - }) - } - } -} +export * from "algos/common/generalized-astar/GeneralizedAstarAutorouter" diff --git a/algos/infinite-grid-ijump-astar/v2/lib/types.ts b/algos/infinite-grid-ijump-astar/v2/lib/types.ts index 0ce70ff..6c3fbaf 100644 --- a/algos/infinite-grid-ijump-astar/v2/lib/types.ts +++ b/algos/infinite-grid-ijump-astar/v2/lib/types.ts @@ -1,66 +1 @@ -import type { Obstacle } from "autorouting-dataset/lib/types" - -export interface DirectionDistances { - left: number - top: number - bottom: number - right: number -} - -export interface Direction { - dx: number - dy: number -} - -export interface DirectionWithWallDistance extends Direction { - wallDistance: number -} - -export interface DirectionWithCollisionInfo extends Direction { - wallDistance: number - obstacle: Obstacle | null -} - -export interface Point { - x: number - y: number -} - -export interface PointWithObstacleHit extends Point { - obstacleHit?: Obstacle | null - - /** - * Used in multi-margin autorouter to penalize traveling close to the wall - */ - travelMarginCostFactor?: number - enterMarginCost?: number -} - -export interface PointWithWallDistance extends Point { - wallDistance: number -} - -export interface Node extends Point { - /** Distance from the parent node (along path) */ - g: number - /** Heuristic distance from the goal */ - h: number - /** Distance score for this node (g + h) */ - f: number - /** Manhattan Distance from the parent node */ - manDistFromParent: number - nodesInPath: number - obstacleHit?: Obstacle - parent: Node | null - - /** - * Used in multi-margin autorouter to penalize traveling close to the wall - */ - travelMarginCostFactor?: number - enterMarginCost?: number - - /** - * Layer index, not needed for single-layer autorouters - */ - l?: number -} +export * from "algos/common/generalized-astar/types" diff --git a/module/lib/solver-utils/getAlternativeGoalBoxes.ts b/module/lib/solver-utils/getAlternativeGoalBoxes.ts index b0fe34b..6f479d5 100644 --- a/module/lib/solver-utils/getAlternativeGoalBoxes.ts +++ b/module/lib/solver-utils/getAlternativeGoalBoxes.ts @@ -40,6 +40,13 @@ export function getAlternativeGoalBoxes(params: { })) } +/** + * Takes a connection and a connectivity map, then swaps the pointsToConnect + * with more optimal points. + * + * For example, we may see there is an easier or closer way to connect two + * points because of a trace that has already been routed. + */ export const getConnectionWithAlternativeGoalBoxes = (params: { connection: SimpleRouteConnection pcbConnMap: PcbConnectivityMap