From b1a66deb708f430aad885b06c95bd3c0c1ca3da2 Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 21 Nov 2024 19:43:15 -0800 Subject: [PATCH 01/16] astar documentation and explanations --- .../FixedMemoryGeneralizedAstarAutorouter.ts | 20 + .../GENERALIZED_ASTAR_README.md | 97 ++++ .../GeneralizedAstarAutorouter.ts | 495 +++++++++++++++++ .../IGeneralizedAstarAutorouter.ts | 7 + .../common/generalized-astar/ObstacleList.ts | 168 ++++++ algos/common/generalized-astar/types.ts | 66 +++ .../v2/lib/GeneralizedAstar.ts | 496 +----------------- .../infinite-grid-ijump-astar/v2/lib/types.ts | 67 +-- .../solver-utils/getAlternativeGoalBoxes.ts | 7 + 9 files changed, 862 insertions(+), 561 deletions(-) create mode 100644 algos/common/generalized-astar/FixedMemoryGeneralizedAstarAutorouter.ts create mode 100644 algos/common/generalized-astar/GENERALIZED_ASTAR_README.md create mode 100644 algos/common/generalized-astar/GeneralizedAstarAutorouter.ts create mode 100644 algos/common/generalized-astar/IGeneralizedAstarAutorouter.ts create mode 100644 algos/common/generalized-astar/ObstacleList.ts create mode 100644 algos/common/generalized-astar/types.ts 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 From 7673890fa9fa6ff4ca7fdc115fad6f0173c5448f Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 18:34:13 +0800 Subject: [PATCH 02/16] profiling and moving obstacle list around --- .../common/generalized-astar/ObstacleList.ts | 34 +++ .../generalized-astar/ObstacleList3d.ts | 216 ++++++++++++++++++ algos/common/generalized-astar/Profiler.ts | 91 ++++++++ algos/common/generalized-astar/types.ts | 48 ++++ algos/common/generalized-astar/util.ts | 48 ++++ algos/multi-layer-ijump/MultilayerIjump.ts | 2 +- algos/seveibar/seveibar1/index.ts | 25 ++ .../keyboard-route-test-profile.snap.svg | 13 ++ .../keyboard-route-test-profile.test.tsx | 45 ++++ 9 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 algos/common/generalized-astar/ObstacleList3d.ts create mode 100644 algos/common/generalized-astar/Profiler.ts create mode 100644 algos/common/generalized-astar/util.ts create mode 100644 algos/seveibar/seveibar1/index.ts create mode 100644 algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg create mode 100644 algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx diff --git a/algos/common/generalized-astar/ObstacleList.ts b/algos/common/generalized-astar/ObstacleList.ts index 22c0864..68c61d5 100644 --- a/algos/common/generalized-astar/ObstacleList.ts +++ b/algos/common/generalized-astar/ObstacleList.ts @@ -5,16 +5,31 @@ import type { DirectionWithCollisionInfo, Point, } from "./types" +import { Profiler } from "./Profiler" + +declare global { + namespace NodeJS { + interface ProcessEnv { + TSCIRCUIT_AUTOROUTER_PROFILING_ENABLED?: string + } + } +} + +const globalObstacleListProfiler = new Profiler() /** * 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 { + profiler?: Profiler protected obstacles: ObstacleWithEdges[] protected GRID_STEP = 0.1 constructor(obstacles: Array) { + if (process.env.TSCIRCUIT_AUTOROUTER_PROFILING_ENABLED) { + this.profiler = globalObstacleListProfiler + } this.obstacles = obstacles.map((obstacle) => ({ ...obstacle, left: obstacle.center.x - obstacle.width / 2, @@ -22,6 +37,25 @@ export class ObstacleList { top: obstacle.center.y + obstacle.height / 2, bottom: obstacle.center.y - obstacle.height / 2, })) + + if (this.profiler) { + for (const methodName of [ + "getObstacleAt", + "isObstacleAt", + "getDirectionDistancesToNearestObstacle", + "getOrthoDirectionCollisionInfo", + "getObstaclesOverlappingRegion", + ]) { + const originalMethod = this[ + methodName as keyof ObstacleList + ] as Function + // @ts-ignore + this[methodName as keyof ObstacleList] = this.profiler!.wrapMethod( + `ObstacleList.${methodName}`, + originalMethod.bind(this), + ) + } + } } getObstacleAt(x: number, y: number, m?: number): Obstacle | null { diff --git a/algos/common/generalized-astar/ObstacleList3d.ts b/algos/common/generalized-astar/ObstacleList3d.ts new file mode 100644 index 0000000..5b23951 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3d.ts @@ -0,0 +1,216 @@ +// ObstacleList3d.ts + +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import type { + Direction3d, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { getLayerIndex, getLayerNamesForLayerCount } from "./util" +import { ObstacleList } from "./ObstacleList" + +/** + * 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 ObstacleList3d extends ObstacleList { + obstacles: ObstacleWithEdges3d[] + GRID_STEP = 0.1 + layerCount: number + + constructor(layerCount: number, obstacles: Array) { + super([]) + this.layerCount = layerCount + const availableLayers = getLayerNamesForLayerCount(layerCount) + this.obstacles = obstacles.flatMap((obstacle) => + obstacle.layers + .filter((layer) => availableLayers.includes(layer)) + .map((layer) => ({ + ...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, + l: getLayerIndex(layerCount, layer), + })), + ) + } + + getObstacleAt(x: number, y: number, l: number, m?: number): Obstacle | null { + m ??= this.GRID_STEP + for (const obstacle of this.obstacles) { + if (obstacle.l !== l) continue // Only consider obstacles on the same layer + 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, l: number, m?: number): boolean { + return this.getObstacleAt(x, y, l, m) !== null + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + const { GRID_STEP } = this + const result: DirectionDistances3d = { + left: Infinity, + top: Infinity, + bottom: Infinity, + right: Infinity, + } + + for (const obstacle of this.obstacles) { + if (obstacle.l !== l) continue // Only consider obstacles on the same layer + 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: Point3d, + dir: Direction3d, + { margin = 0 }: { margin?: number } = {}, + ): DirectionWithCollisionInfo3d { + const { x, y, l } = point + const { dx, dy, dl } = dir + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null + + if (dl !== 0) { + // Moving between layers + const newLayer = l + dl + // Check if there's an obstacle at the same (x, y) on the new layer + if (this.isObstacleAt(x, y, newLayer, margin)) { + minDistance = 1 // Distance to obstacle is 1 (layer change) + collisionObstacle = this.getObstacleAt( + x, + y, + newLayer, + margin, + ) as ObstacleWithEdges + } else { + minDistance = 1 // Distance to move to the next layer + } + + return { + dx, + dy, + dl, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } else { + // Moving within the same layer + for (const obstacle of this.obstacles) { + if (obstacle.l !== l) continue // Only consider obstacles on the same layer + + 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, + dl: 0, + wallDistance: minDistance, + obstacle: collisionObstacle as ObstacleWithEdges, + } + } + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number // Layer to check + }): ObstacleWithEdges[] { + const obstacles: ObstacleWithEdges[] = [] + for (const obstacle of this.obstacles) { + if (obstacle.l !== region.l) continue // Only consider obstacles on the specified layer + const { left, right, top, bottom } = obstacle + + if ( + left <= region.maxX && + right >= region.minX && + top >= region.minY && + bottom <= region.maxY + ) { + obstacles.push(obstacle) + } + } + + return obstacles + } +} diff --git a/algos/common/generalized-astar/Profiler.ts b/algos/common/generalized-astar/Profiler.ts new file mode 100644 index 0000000..f23e30a --- /dev/null +++ b/algos/common/generalized-astar/Profiler.ts @@ -0,0 +1,91 @@ +export class Profiler { + private measurements: Record< + string, + { + totalTime: number + calls: number + lastStart?: number + } + > = {} + + startMeasurement(name: string) { + if (!this.measurements[name]) { + this.measurements[name] = { totalTime: 0, calls: 0 } + } + this.measurements[name].lastStart = performance.now() + } + + endMeasurement(name: string) { + const measurement = this.measurements[name] + if (!measurement || measurement.lastStart === undefined) return + + const duration = performance.now() - measurement.lastStart + measurement.totalTime += duration + measurement.calls += 1 + measurement.lastStart = undefined + } + + wrapMethod( + name: string, + fn: (...args: T) => R, + ): (...args: T) => R { + return (...args: T) => { + this.startMeasurement(name) + const result = fn(...args) + this.endMeasurement(name) + return result + } + } + + getResults() { + const results: Record< + string, + { + totalTime: number + calls: number + averageTime: number + } + > = {} + + for (const [name, data] of Object.entries(this.measurements)) { + results[name] = { + totalTime: data.totalTime, + calls: data.calls, + averageTime: data.totalTime / (data.calls || 1), + } + } + + return results + } + + getResultsPretty(): Record< + string, + { + totalTime: string + calls: number + averageTime: string + } + > { + const results = this.getResults() + const prettyResults: Record< + string, + { + totalTime: string + calls: number + averageTime: string + } + > = {} + for (const key in results) { + prettyResults[key] = { + totalTime: `${results[key].totalTime.toFixed(2)}ms`, + calls: results[key].calls, + averageTime: `${(results[key].averageTime * 1000).toFixed(1)}us`, + } + } + return prettyResults + } + + reset() { + this.measurements = {} + } +} diff --git a/algos/common/generalized-astar/types.ts b/algos/common/generalized-astar/types.ts index 0ce70ff..ac11bb7 100644 --- a/algos/common/generalized-astar/types.ts +++ b/algos/common/generalized-astar/types.ts @@ -1,5 +1,53 @@ import type { Obstacle } from "autorouting-dataset/lib/types" +export type Direction3d = { + dx: number + dy: number + /** + * Always integer, usually -1 or 1, -1 indicating to go towards the top layer, + * 1 indicating to go down a layer towards the bottom layer + */ + dl: number +} + +export interface Point3d { + x: number + y: number + l: number // Layer +} + +export interface Point3dWithObstacleHit extends Point3d { + obstacleHit?: Obstacle | null +} + +export interface Node3d extends Node { + l: number + parent: Node3d | null +} + +export interface Obstacle3d extends Obstacle { + l: number +} + +export interface ObstacleWithEdges3d extends Obstacle3d { + top: number + bottom: number + left: number + right: number +} + +export interface DirectionWithCollisionInfo3d extends Direction3d { + wallDistance: number + obstacle: Obstacle | null +} + +export interface DirectionDistances3d { + left: number + top: number + bottom: number + right: number +} + export interface DirectionDistances { left: number top: number diff --git a/algos/common/generalized-astar/util.ts b/algos/common/generalized-astar/util.ts new file mode 100644 index 0000000..d2c19bf --- /dev/null +++ b/algos/common/generalized-astar/util.ts @@ -0,0 +1,48 @@ +import type { LayerRef } from "@tscircuit/soup" +import type { Point3d, Direction3d } from "./types" + +export function dirFromAToB(nodeA: Point3d, nodeB: Point3d): Direction3d { + const dx = nodeB.x > nodeA.x ? 1 : nodeB.x < nodeA.x ? -1 : 0 + const dy = nodeB.y > nodeA.y ? 1 : nodeB.y < nodeA.y ? -1 : 0 + const dl = nodeB.l > nodeA.l ? 1 : nodeB.l < nodeA.l ? -1 : 0 + return { dx, dy, dl } +} + +export function manDist(a: Point3d, b: Point3d): number { + return Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.l - b.l) +} + +export const LAYER_COUNT_INDEX_MAP: Record = { + 1: ["top"], + 2: ["top", "bottom"], + 4: ["top", "inner1", "inner2", "bottom"], +} + +export const getLayerNamesForLayerCount = (layerCount: number): string[] => { + return LAYER_COUNT_INDEX_MAP[layerCount] +} + +export function getLayerIndex(layerCount: number, layer: string): number { + const layerArray = LAYER_COUNT_INDEX_MAP[layerCount] + const index = layerArray.indexOf(layer) + if (index === -1) { + throw new Error( + `Invalid layer for getLayerIndex (for layerCount === ${layerCount}): "${layer}"`, + ) + } + return index +} + +export function indexToLayer(layerCount: number, index: number): string { + const layerArray = LAYER_COUNT_INDEX_MAP[layerCount] + const layer = layerArray[index] + if (!layer) { + throw new Error( + `Invalid index for indexToLayer (for layerCount === ${layerCount}): "${index}"`, + ) + } + return layer +} + +export const nodeName = (node: Point3d, GRID_STEP: number = 0.1): string => + `${Math.round(node.x / GRID_STEP)},${Math.round(node.y / GRID_STEP)}` diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index 60a4350..7931111 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -26,7 +26,7 @@ import type { SimpleRouteConnection, SimpleRouteJson, } from "autorouting-dataset/lib/solver-utils/SimpleRouteJson" -import { ObstacleList3d } from "./ObstacleList3d" +import { ObstacleList3d } from "algos/common/generalized-astar/ObstacleList3d" import type { Obstacle } from "autorouting-dataset/lib/types" import { PcbConnectivityMap, diff --git a/algos/seveibar/seveibar1/index.ts b/algos/seveibar/seveibar1/index.ts new file mode 100644 index 0000000..9f2de65 --- /dev/null +++ b/algos/seveibar/seveibar1/index.ts @@ -0,0 +1,25 @@ +import { GeneralizedAstarAutorouter } from "algos/common/generalized-astar/GeneralizedAstarAutorouter" +import { ObstacleList } from "algos/common/generalized-astar/ObstacleList" +import type { + PointWithObstacleHit, + Node, +} from "algos/common/generalized-astar/types" +import type { Obstacle } from "solver-utils" +import type { SimpleRouteConnection } from "solver-utils/SimpleRouteJson" + +/** + * seveibar1 is a variation of the multi-layer ijump autorouter that uses more + * efficient memory access and tries to avoid some slow paths in the original + * multi-layer autorouter. + * + * This is created by profiling the operations of each of the major components + * of the original MultilayerIjump autorouter and optimizing each component + * independently. + * + * There are three main time-consuming components of the autorouter: + * - GeneralizedAstarAutorouter (the base class that determines what point + * is highest in the priority queue) + * - ObstacleList + * - getNeighbors + */ +export class Seveibar1 extends GeneralizedAstarAutorouter {} diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg new file mode 100644 index 0000000..48431ba --- /dev/null +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx new file mode 100644 index 0000000..e665350 --- /dev/null +++ b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx @@ -0,0 +1,45 @@ +import { getKeyboardGenerator } from "autorouting-dataset/lib/generators/keyboards" +import { expect, test } from "bun:test" +import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { getSimpleRouteJson } from "solver-utils" +import { MultilayerIjump } from "algos/multi-layer-ijump/MultilayerIjump" + +test("multi-layer ijump keyboard", async () => { + const durations: Array<[number, number]> = [] + let soup: any + let autorouter: MultilayerIjump = null as any + let result: any + for (let i = 0; i < 20; i++) { + soup = await getKeyboardGenerator().getExample({ seed: 7 + i }) + const connMap = getFullConnectivityMapFromCircuitJson(soup) + const input = getSimpleRouteJson(soup, { layerCount: 2, connMap }) + + autorouter = new MultilayerIjump({ + input, + connMap, + optimizeWithGoalBoxes: true, + }) + + const start = process.hrtime() + result = autorouter.solveAndMapToTraces() + const duration = process.hrtime(start) + durations.push(duration) + } + + const totalDuration = durations.reduce( + (acc, duration) => { + return [acc[0] + duration[0], acc[1] + duration[1]] + }, + [0, 0], + ) + console.log("\n\n-----------------------------\n\n") + console.log( + `TIME TO ROUTE: ${totalDuration[0] * 1000 + totalDuration[1] / 1e6}ms`, + ) + console.table(autorouter.obstacles.profiler!.getResultsPretty()) + + expect( + convertCircuitJsonToPcbSvg(soup.concat(result as any) as any), + ).toMatchSvgSnapshot(import.meta.path) +}) From 94d12d99972bd82bf6111ac4bca00b8a9f74bdb0 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 18:45:17 +0800 Subject: [PATCH 03/16] lots of measuring --- .../GeneralizedAstarAutorouter.ts | 29 ++++++++++++++++++- .../keyboard-route-test-profile.test.tsx | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index 1d50777..fede672 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -15,6 +15,7 @@ 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" +import { Profiler } from "./Profiler" const debug = Debug("autorouting-dataset:astar") @@ -26,7 +27,10 @@ export type ConnectionSolveResult = | { solved: false; connectionName: string } | { solved: true; connectionName: string; route: PointWithLayer[] } +const globalGeneralizedAstarAutorouterProfiler = new Profiler() + export class GeneralizedAstarAutorouter { + profiler?: Profiler openSet: Node[] = [] closedSet: Set = new Set() debug = false @@ -64,6 +68,9 @@ export class GeneralizedAstarAutorouter { isRemovePathLoopsEnabled?: boolean debug?: boolean }) { + if (process.env.TSCIRCUIT_AUTOROUTER_PROFILING_ENABLED) { + this.profiler = globalGeneralizedAstarAutorouterProfiler + } this.input = opts.input this.allObstacles = opts.input.obstacles this.startNode = opts.startNode @@ -83,6 +90,22 @@ export class GeneralizedAstarAutorouter { this.debugSolutions = {} this.debugMessage = "" } + + if (this.profiler) { + for (const methodName of [ + "getNeighbors", + "solveOneStep", + "_sortOpenSet", + ]) { + // @ts-ignore + const originalMethod = this[methodName] as Function + // @ts-ignore + this[methodName as keyof ObstacleList] = this.profiler!.wrapMethod( + `GenerlizedAstarAutorouter.${methodName}`, + originalMethod.bind(this), + ) + } + } } /** @@ -119,6 +142,10 @@ export class GeneralizedAstarAutorouter { return nodeName(node, this.GRID_STEP) } + _sortOpenSet() { + this.openSet.sort((a, b) => a.f - b.f) + } + solveOneStep(): { solved: boolean current: Node @@ -126,7 +153,7 @@ export class GeneralizedAstarAutorouter { } { this.iterations += 1 const { openSet, closedSet, GRID_STEP, goalPoint } = this - openSet.sort((a, b) => a.f - b.f) + this._sortOpenSet() const current = openSet.shift()! const goalDist = this.computeH(current) diff --git a/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx index e665350..509f1ea 100644 --- a/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx +++ b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx @@ -38,6 +38,7 @@ test("multi-layer ijump keyboard", async () => { `TIME TO ROUTE: ${totalDuration[0] * 1000 + totalDuration[1] / 1e6}ms`, ) console.table(autorouter.obstacles.profiler!.getResultsPretty()) + console.table(autorouter.profiler!.getResultsPretty()) expect( convertCircuitJsonToPcbSvg(soup.concat(result as any) as any), From c402271ce05f77034a1b503d42dd5db278140b4c Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 18:50:36 +0800 Subject: [PATCH 04/16] insert sorted optimization --- .../GeneralizedAstarAutorouter.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index fede672..7fdc1c9 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -143,7 +143,12 @@ export class GeneralizedAstarAutorouter { } _sortOpenSet() { - this.openSet.sort((a, b) => a.f - b.f) + // Not needed because we do an insert sort + // this.openSet.sort((a, b) => a.f - b.f) + // Limit the size of the openset + if (this.openSet.length > this.MAX_ITERATIONS) { + this.openSet.splice(this.MAX_ITERATIONS) + } } solveOneStep(): { @@ -195,7 +200,14 @@ export class GeneralizedAstarAutorouter { travelMarginCostFactor: neighbor.travelMarginCostFactor, } - openSet.push(neighborNode) + // Insert into openSet in sorted order by f value + const insertIndex = openSet.findIndex((node) => node.f > neighborNode.f) + if (insertIndex === -1) { + openSet.push(neighborNode) + } else { + openSet.splice(insertIndex, 0, neighborNode) + } + newNeighbors.push(neighborNode) } } From 92bd4207a0d212c82bbdef533f9ebbcdb4965971 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 20:23:53 +0800 Subject: [PATCH 05/16] introduce F64V1 obstacle list --- .../GeneralizedAstarAutorouter.ts | 1 + .../generalized-astar/ObstacleList3dF64V1.ts | 279 +++++++++++++++++ .../ObstacleList3dFloatArrays.ts | 30 ++ .../ObstacleList3dSectional.ts | 291 ++++++++++++++++++ algos/multi-layer-ijump/MultilayerIjump.ts | 5 +- .../keyboard-route-test-profile.snap.svg | 2 +- 6 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 algos/common/generalized-astar/ObstacleList3dF64V1.ts create mode 100644 algos/common/generalized-astar/ObstacleList3dFloatArrays.ts create mode 100644 algos/common/generalized-astar/ObstacleList3dSectional.ts diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index 7fdc1c9..aafa675 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -95,6 +95,7 @@ export class GeneralizedAstarAutorouter { for (const methodName of [ "getNeighbors", "solveOneStep", + "createObstacleList", "_sortOpenSet", ]) { // @ts-ignore diff --git a/algos/common/generalized-astar/ObstacleList3dF64V1.ts b/algos/common/generalized-astar/ObstacleList3dF64V1.ts new file mode 100644 index 0000000..e28574f --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dF64V1.ts @@ -0,0 +1,279 @@ +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import type { + Direction3d, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { getLayerIndex, getLayerNamesForLayerCount } from "./util" +import { ObstacleList } from "./ObstacleList" +import { ObstacleList3d } from "./ObstacleList3d" + +/** + * Indices for each obstacle property in the data array. + * Each obstacle is stored in this order: + * [centerX, centerY, width, height, l, top, bottom, left, right, typeCode] + */ +const CENTER_X = 0 +const CENTER_Y = 1 +const WIDTH = 2 +const HEIGHT = 3 +const LAYER = 4 +const TOP = 5 +const BOTTOM = 6 +const LEFT = 7 +const RIGHT = 8 +const TYPE = 9 + +// Number of numeric fields per obstacle +const STRIDE = 10 + +// We can define numeric codes for obstacle types. +// For simplicity, let's assume all obstacles are rect for now. +const TYPE_RECT = 0 + +export class ObstacleList3dF64V1 extends ObstacleList3d { + data: Float64Array + originalObstacles: ObstacleWithEdges3d[] + GRID_STEP = 0.1 + layerCount: number + + constructor(layerCount: number, obstacles: Array) { + super(layerCount, []) + this.layerCount = layerCount + const availableLayers = getLayerNamesForLayerCount(layerCount) + + // Filter and expand obstacles based on layers + const filtered: ObstacleWithEdges3d[] = obstacles.flatMap((obstacle) => + obstacle.layers + .filter((layer) => availableLayers.includes(layer)) + .map((layer) => ({ + ...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, + l: getLayerIndex(layerCount, layer), + })), + ) + + this.originalObstacles = filtered + const count = filtered.length + this.data = new Float64Array(count * STRIDE) + + for (let i = 0; i < count; i++) { + const obs = filtered[i] + const base = i * STRIDE + this.data[base + CENTER_X] = obs.center.x + this.data[base + CENTER_Y] = obs.center.y + this.data[base + WIDTH] = obs.width + this.data[base + HEIGHT] = obs.height + this.data[base + LAYER] = obs.l + this.data[base + TOP] = obs.top + this.data[base + BOTTOM] = obs.bottom + this.data[base + LEFT] = obs.left + this.data[base + RIGHT] = obs.right + // Assuming type is always 'rect' for these obstacles. If needed, map other types. + this.data[base + TYPE] = TYPE_RECT + } + } + + getObstacleAt( + x: number, + y: number, + l: number, + m?: number, + ): ObstacleWithEdges | null { + m ??= this.GRID_STEP + const { data } = this + const count = data.length / STRIDE + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + const halfWidth = data[base + WIDTH] / 2 + m + const halfHeight = data[base + HEIGHT] / 2 + m + const centerX = data[base + CENTER_X] + const centerY = data[base + CENTER_Y] + + if ( + x >= centerX - halfWidth && + x <= centerX + halfWidth && + y >= centerY - halfHeight && + y <= centerY + halfHeight + ) { + return this.originalObstacles[i] + } + } + return null + } + + isObstacleAt(x: number, y: number, l: number, m?: number): boolean { + return this.getObstacleAt(x, y, l, m) !== null + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + const { GRID_STEP, data } = this + const count = data.length / STRIDE + + const result: DirectionDistances3d = { + left: Infinity, + top: Infinity, + bottom: Infinity, + right: Infinity, + } + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + // Assume rect + const left = data[base + LEFT] - GRID_STEP + const right = data[base + RIGHT] + GRID_STEP + const top = data[base + TOP] + GRID_STEP + const bottom = data[base + BOTTOM] - 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: Point3d, + dir: Direction3d, + { margin = 0 }: { margin?: number } = {}, + ): DirectionWithCollisionInfo3d { + const { x, y, l } = point + const { dx, dy, dl } = dir + + if (dl !== 0) { + // Moving between layers + const newLayer = l + dl + let collisionObstacle: ObstacleWithEdges | null = null + let minDistance = 1 + if (this.isObstacleAt(x, y, newLayer, margin)) { + collisionObstacle = this.getObstacleAt( + x, + y, + newLayer, + margin, + ) as ObstacleWithEdges + } + + return { + dx, + dy, + dl, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } else { + // Moving within the same layer + const { data } = this + const count = data.length / STRIDE + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + + const leftMargin = data[base + LEFT] - margin + const rightMargin = data[base + RIGHT] + margin + const topMargin = data[base + TOP] + margin + const bottomMargin = data[base + BOTTOM] - margin + + let distance: number | null = null + + if (dx === 1 && dy === 0) { + // Right + if (y > bottomMargin && y < topMargin && x < leftMargin) { + distance = leftMargin - x + } + } else if (dx === -1 && dy === 0) { + // Left + if (y > bottomMargin && y < topMargin && x > rightMargin) { + distance = x - rightMargin + } + } else if (dx === 0 && dy === 1) { + // Up + if (x > leftMargin && x < rightMargin && y < bottomMargin) { + distance = bottomMargin - y + } + } else if (dx === 0 && dy === -1) { + // Down + if (x > leftMargin && x < rightMargin && y > topMargin) { + distance = y - topMargin + } + } + + if (distance !== null && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[i] + } + } + + return { + dx, + dy, + dl: 0, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number // Layer to check + }): ObstacleWithEdges[] { + const { data, originalObstacles } = this + const count = data.length / STRIDE + const obstacles: ObstacleWithEdges[] = [] + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== region.l) continue + const left = data[base + LEFT] + const right = data[base + RIGHT] + const top = data[base + TOP] + const bottom = data[base + BOTTOM] + + if ( + left <= region.maxX && + right >= region.minX && + top >= region.minY && + bottom <= region.maxY + ) { + obstacles.push(originalObstacles[i]) + } + } + + return obstacles + } +} diff --git a/algos/common/generalized-astar/ObstacleList3dFloatArrays.ts b/algos/common/generalized-astar/ObstacleList3dFloatArrays.ts new file mode 100644 index 0000000..e6af310 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dFloatArrays.ts @@ -0,0 +1,30 @@ +import type { Obstacle } from "solver-utils" +import { ObstacleList3d } from "./ObstacleList3d" + +export class ObstacleList3dFloatArrays extends ObstacleList3d { + /** + * This is a flattened representation of obstacles, each obstacle is + * represented like so: + * [ left, top, right, bottom ] + */ + obstaclesF64: Float64Array + + constructor(layerCount: number, obstacles: Array) { + super(layerCount, obstacles) + + // Create float64 representation of obstacles + const internalObstacles = this.obstacles + + this.obstaclesF64 = new Float64Array(internalObstacles.length * 4) + + for (let i = 0; i < internalObstacles.length; i++) { + const obstacle = internalObstacles[i] + this.obstaclesF64[i * 5] = obstacle.left + this.obstaclesF64[i * 5 + 1] = obstacle.top + this.obstaclesF64[i * 5 + 2] = obstacle.right + this.obstaclesF64[i * 5 + 3] = obstacle.bottom + } + } + + // getObstacleAt(x: number, y: number, l: number, m?: number): Obstacle | null {} +} diff --git a/algos/common/generalized-astar/ObstacleList3dSectional.ts b/algos/common/generalized-astar/ObstacleList3dSectional.ts new file mode 100644 index 0000000..0b31832 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dSectional.ts @@ -0,0 +1,291 @@ +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import { ObstacleList3d } from "./ObstacleList3d" +import type { + Direction3d, + DirectionDistances, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + Obstacle3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { Profiler } from "./Profiler" + +type SectionId = `${number},${number}` + +const globalObstacleList3dSectionalProfiler = new Profiler() + +export class ObstacleList3dSectional implements ObstacleList3d { + /** + * Contains obstacles that are touching or in any way interacting with a small + * section + */ + sections: Record = {} + + /** + * Contains obstacles that are aligned to a section, i.e. they lie directly + * above, below, to the left, or to the right of a section (including the + * section). This is used to do ray intersections + */ + alignedSections: Record = {} + profiler?: Profiler | undefined + obstacles = [] + bounds: { minX: number; minY: number; maxX: number; maxY: number } + GRID_STEP: number = 0.1 + + sectionSizeX: number + sectionSizeY: number + numSectionsX: number + numSectionsY: number + + layerCount: number + + constructor(layerCount: number, obstacles: Array) { + if (process.env.TSCIRCUIT_AUTOROUTER_PROFILING_ENABLED) { + this.profiler = globalObstacleList3dSectionalProfiler + } + this.layerCount = layerCount + + this.bounds = { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + } + + for (const obstacle of obstacles) { + this.bounds.minX = Math.min( + this.bounds.minX, + obstacle.center.x - obstacle.width / 2, + ) + this.bounds.minY = Math.min( + this.bounds.minY, + obstacle.center.y - obstacle.height / 2, + ) + this.bounds.maxX = Math.max( + this.bounds.maxX, + obstacle.center.x + obstacle.width / 2, + ) + this.bounds.maxY = Math.max( + this.bounds.maxY, + obstacle.center.y + obstacle.height / 2, + ) + } + + this.numSectionsX = 10 + this.numSectionsY = 10 + + this.sectionSizeX = + (this.bounds.maxX - this.bounds.minX) / this.numSectionsX + this.sectionSizeY = + (this.bounds.maxY - this.bounds.minY) / this.numSectionsY + + const obstaclesInSection: Record> = {} + const obstaclesInAlignedSection: Record> = {} + for (let sectionX = 0; sectionX < this.numSectionsX; sectionX++) { + for (let sectionY = 0; sectionY < this.numSectionsY; sectionY++) { + obstaclesInSection[`${sectionX},${sectionY}`] = [] + obstaclesInAlignedSection[`${sectionX},${sectionY}`] = [] + } + } + + for (const obstacle of obstacles) { + const sections = this._getSectionsOfObstacle(obstacle) + for (const section of sections) { + obstaclesInSection[section].push(obstacle) + } + + const alignedSections = this._getAlignedSectionsForObstacle(obstacle) + for (const section of alignedSections) { + obstaclesInAlignedSection[section].push(obstacle) + } + } + + this.sections = {} + for (const sectionId of Object.keys(obstaclesInSection)) { + const typedSectionId = sectionId as `${number},${number}` + this.sections[typedSectionId] = new ObstacleList3d( + layerCount, + obstaclesInSection[typedSectionId], + ) + } + + this.alignedSections = {} + for (const sectionId of Object.keys(obstaclesInAlignedSection)) { + const typedSectionId = sectionId as `${number},${number}` + this.alignedSections[typedSectionId] = new ObstacleList3d( + layerCount, + obstaclesInAlignedSection[typedSectionId], + ) + } + + if (this.profiler) { + for (const methodName of [ + "getObstacleAt", + "isObstacleAt", + "getDirectionDistancesToNearestObstacle", + "getOrthoDirectionCollisionInfo", + "getObstaclesOverlappingRegion", + ]) { + // @ts-ignore + const originalMethod = this[methodName] as Function + // @ts-ignore + this[methodName as keyof ObstacleList] = this.profiler!.wrapMethod( + `ObstacleList3dSectional.${methodName}`, + originalMethod.bind(this), + ) + } + } + } + + _getSectionXYForPoint(x: number, y: number): [number, number] { + let sectionX = Math.floor((x - this.bounds.minX) / this.sectionSizeX) + let sectionY = Math.floor((y - this.bounds.minY) / this.sectionSizeY) + // clamp + sectionX = Math.max(0, Math.min(this.numSectionsX - 1, sectionX)) + sectionY = Math.max(0, Math.min(this.numSectionsY - 1, sectionY)) + + return [sectionX, sectionY] + } + + _getSectionForPoint(x: number, y: number): SectionId { + const [sectionX, sectionY] = this._getSectionXYForPoint(x, y) + return `${sectionX},${sectionY}` + } + + _getSectionsOfRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + }) { + const [minSectionX, minSectionY] = this._getSectionXYForPoint( + region.minX, + region.minY, + ) + const [maxSectionX, maxSectionY] = this._getSectionXYForPoint( + region.maxX, + region.maxY, + ) + + const sections: SectionId[] = [] + for (let sectionX = minSectionX; sectionX <= maxSectionX; sectionX++) { + for (let sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + sections.push(`${sectionX},${sectionY}`) + } + } + + return sections + } + + _getSectionsOfObstacle(obstacle: Obstacle): SectionId[] { + const { + center: { x, y }, + height, + width, + } = obstacle + return this._getSectionsOfRegion({ + minX: x - width / 2, + minY: y - height / 2, + maxX: x + width / 2, + maxY: y + height / 2, + }) + } + + _getAlignedSectionsForObstacle(obstacle: Obstacle): SectionId[] { + const { + center: { x, y }, + height, + width, + } = obstacle + const alignedSections: Set = new Set() + + const [minSectionX, minSectionY] = this._getSectionXYForPoint( + x - width / 2, + y - height / 2, + ) + const [maxSectionX, maxSectionY] = this._getSectionXYForPoint( + x + width / 2, + y + height / 2, + ) + + for (let sectionX = minSectionX; sectionX <= maxSectionX; sectionX++) { + // Add all sections above and below + for (let sectionY = 0; sectionY < this.numSectionsY; sectionY++) { + alignedSections.add(`${sectionX},${sectionY}`) + } + } + + for (let sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + // Add all sections to the left and right + for (let sectionX = 0; sectionX < this.numSectionsX; sectionX++) { + alignedSections.add(`${sectionX},${sectionY}`) + } + } + + return Array.from(alignedSections) + } + + getObstacleAt(x: number, y: number, l: number, m?: number): Obstacle | null { + const sectionId = this._getSectionForPoint(x, y) + return this.sections[sectionId]?.getObstacleAt(x, y, l, m) ?? null + } + + isObstacleAt(x: number, y: number, l: number, m?: number): boolean { + const sectionId = this._getSectionForPoint(x, y) + return this.sections[sectionId]?.isObstacleAt(x, y, l, m) ?? false + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + const sectionId = this._getSectionForPoint(x, y) + return this.alignedSections[ + sectionId + ].getDirectionDistancesToNearestObstacle3d(x, y, l) + } + getOrthoDirectionCollisionInfo( + point: Point3d, + dir: Direction3d, + opts?: { margin?: number }, + ): DirectionWithCollisionInfo3d { + const sectionId = this._getSectionForPoint(point.x, point.y) + return this.alignedSections[sectionId].getOrthoDirectionCollisionInfo( + point, + dir, + opts, + ) + } + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number + }): ObstacleWithEdges[] { + const sections = this._getSectionsOfRegion(region) + + const obstacles: ObstacleWithEdges[] = [] + for (const section of sections) { + obstacles.push( + ...this.sections[section].getObstaclesOverlappingRegion(region), + ) + } + + // TODO dedupe obstacles + + return obstacles + } + + getDirectionDistancesToNearestObstacle( + x: number, + y: number, + ): DirectionDistances { + const sectionId = this._getSectionForPoint(x, y) + return this.alignedSections[ + sectionId + ].getDirectionDistancesToNearestObstacle(x, y) + } +} diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index 7931111..3875f46 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -39,6 +39,8 @@ import { getAlternativeGoalBoxes, getConnectionWithAlternativeGoalBoxes, } from "autorouting-dataset/lib/solver-utils/getAlternativeGoalBoxes" +import { ObstacleList3dSectional } from "algos/common/generalized-astar/ObstacleList3dSectional" +import { ObstacleList3dF64V1 } from "algos/common/generalized-astar/ObstacleList3dF64V1" export class MultilayerIjump extends GeneralizedAstarAutorouter { MAX_ITERATIONS: number = 500 @@ -189,7 +191,8 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { ) } - return new ObstacleList3d( + // return new ObstacleList3dSectional( + return new ObstacleList3dF64V1( this.layerCount, this.allObstacles .filter((obstacle) => !obstacle.connectedTo.includes(bestConnectionId)) diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index 48431ba..f49883d 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 95f739c76ce9129ee70653920f7ca2e89d097de3 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 20:28:51 +0800 Subject: [PATCH 06/16] obstacle sort attempt 1 --- .../generalized-astar/ObstacleList3dF64V2.ts | 412 ++++++++++++++++++ algos/multi-layer-ijump/MultilayerIjump.ts | 3 +- .../keyboard-route-test-profile.snap.svg | 2 +- 3 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 algos/common/generalized-astar/ObstacleList3dF64V2.ts diff --git a/algos/common/generalized-astar/ObstacleList3dF64V2.ts b/algos/common/generalized-astar/ObstacleList3dF64V2.ts new file mode 100644 index 0000000..cbf122f --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dF64V2.ts @@ -0,0 +1,412 @@ +// ObstacleList3dOptimized.ts + +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import type { + Direction3d, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { getLayerIndex, getLayerNamesForLayerCount } from "./util" +import { ObstacleList } from "./ObstacleList" +import { ObstacleList3d } from "./ObstacleList3d" + +const CENTER_X = 0 +const CENTER_Y = 1 +const WIDTH = 2 +const HEIGHT = 3 +const LAYER = 4 +const TOP = 5 +const BOTTOM = 6 +const LEFT = 7 +const RIGHT = 8 +const TYPE = 9 + +const STRIDE = 10 +const TYPE_RECT = 0 + +export class ObstacleList3dF64V2 extends ObstacleList3d { + data: Float64Array + originalObstacles: ObstacleWithEdges3d[] + GRID_STEP = 0.1 + layerCount: number + + // Arrays for faster direction-specific queries + // Each entry: obstaclesByLeft[layer] = array of indices into `data` sorted by LEFT + obstaclesByLeft: number[][] + obstaclesByRight: number[][] + obstaclesByTop: number[][] + obstaclesByBottom: number[][] + + constructor(layerCount: number, obstacles: Array) { + super(layerCount, []) + this.layerCount = layerCount + const availableLayers = getLayerNamesForLayerCount(layerCount) + + const filtered: ObstacleWithEdges3d[] = obstacles.flatMap((obstacle) => + obstacle.layers + .filter((layer) => availableLayers.includes(layer)) + .map((layer) => ({ + ...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, + l: getLayerIndex(layerCount, layer), + })), + ) + + this.originalObstacles = filtered + const count = filtered.length + this.data = new Float64Array(count * STRIDE) + + for (let i = 0; i < count; i++) { + const obs = filtered[i] + const base = i * STRIDE + this.data[base + CENTER_X] = obs.center.x + this.data[base + CENTER_Y] = obs.center.y + this.data[base + WIDTH] = obs.width + this.data[base + HEIGHT] = obs.height + this.data[base + LAYER] = obs.l + this.data[base + TOP] = obs.top + this.data[base + BOTTOM] = obs.bottom + this.data[base + LEFT] = obs.left + this.data[base + RIGHT] = obs.right + this.data[base + TYPE] = TYPE_RECT + } + + // Build direction-specific indexes + this.obstaclesByLeft = [] + this.obstaclesByRight = [] + this.obstaclesByTop = [] + this.obstaclesByBottom = [] + + for (let layer = 0; layer < layerCount; layer++) { + const indicesForLayer: number[] = [] + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (this.data[base + LAYER] === layer) { + indicesForLayer.push(i) + } + } + + // Sort by LEFT + const byLeft = indicesForLayer + .slice() + .sort( + (a, b) => this.data[a * STRIDE + LEFT] - this.data[b * STRIDE + LEFT], + ) + const byRight = indicesForLayer + .slice() + .sort( + (a, b) => + this.data[a * STRIDE + RIGHT] - this.data[b * STRIDE + RIGHT], + ) + const byTop = indicesForLayer.slice().sort( + (a, b) => this.data[b * STRIDE + TOP] - this.data[a * STRIDE + TOP], // descending top + ) + const byBottom = indicesForLayer + .slice() + .sort( + (a, b) => + this.data[a * STRIDE + BOTTOM] - this.data[b * STRIDE + BOTTOM], + ) + + this.obstaclesByLeft[layer] = byLeft + this.obstaclesByRight[layer] = byRight + this.obstaclesByTop[layer] = byTop + this.obstaclesByBottom[layer] = byBottom + } + } + + getObstacleAt( + x: number, + y: number, + l: number, + m?: number, + ): ObstacleWithEdges | null { + m ??= this.GRID_STEP + const { data } = this + const count = data.length / STRIDE + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + const halfWidth = data[base + WIDTH] / 2 + m + const halfHeight = data[base + HEIGHT] / 2 + m + const cx = data[base + CENTER_X] + const cy = data[base + CENTER_Y] + + if ( + x >= cx - halfWidth && + x <= cx + halfWidth && + y >= cy - halfHeight && + y <= cy + halfHeight + ) { + return this.originalObstacles[i] + } + } + return null + } + + isObstacleAt(x: number, y: number, l: number, m?: number): boolean { + return this.getObstacleAt(x, y, l, m) !== null + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + // This method can remain unchanged or also benefit from the indexes if needed. + // For brevity, we keep it as is: + const { GRID_STEP, data } = this + const count = data.length / STRIDE + const result: DirectionDistances3d = { + left: Infinity, + top: Infinity, + bottom: Infinity, + right: Infinity, + } + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + const left = data[base + LEFT] - GRID_STEP + const right = data[base + RIGHT] + GRID_STEP + const top = data[base + TOP] + GRID_STEP + const bottom = data[base + BOTTOM] - 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: Point3d, + dir: Direction3d, + { margin = 0 }: { margin?: number } = {}, + ): DirectionWithCollisionInfo3d { + const { x, y, l } = point + const { dx, dy, dl } = dir + + if (dl !== 0) { + // Layer change remains O(1), just check the next layer + const newLayer = l + dl + let collisionObstacle: ObstacleWithEdges | null = null + let minDistance = 1 + if (this.isObstacleAt(x, y, newLayer, margin)) { + collisionObstacle = this.getObstacleAt( + x, + y, + newLayer, + margin, + ) as ObstacleWithEdges + } + return { + dx, + dy, + dl, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } + + // Horizontal or vertical movement within the same layer + let candidateIndices: number[] | null = null + let searchValue: number + let verticalCheck: (base: number) => boolean + let horizontalCheck: (base: number) => boolean + + const { data } = this + + if (dx === 1 && dy === 0) { + // Moving right: find obstacles with left > x + candidateIndices = this.obstaclesByLeft[l] + searchValue = x + verticalCheck = (base: number) => + y > data[base + BOTTOM] - margin && y < data[base + TOP] + margin + horizontalCheck = (base: number) => data[base + LEFT] > x + } else if (dx === -1 && dy === 0) { + // Moving left: find obstacles with right < x + candidateIndices = this.obstaclesByRight[l] + searchValue = x + verticalCheck = (base: number) => + y > data[base + BOTTOM] - margin && y < data[base + TOP] + margin + horizontalCheck = (base: number) => data[base + RIGHT] < x + } else if (dx === 0 && dy === 1) { + // Moving up: find obstacles with bottom > y + candidateIndices = this.obstaclesByBottom[l] + searchValue = y + verticalCheck = (base: number) => + x > data[base + LEFT] - margin && x < data[base + RIGHT] + margin + horizontalCheck = (base: number) => data[base + BOTTOM] > y + } else if (dx === 0 && dy === -1) { + // Moving down: find obstacles with top < y + candidateIndices = this.obstaclesByTop[l] + searchValue = y + verticalCheck = (base: number) => + x > data[base + LEFT] - margin && x < data[base + RIGHT] + margin + horizontalCheck = (base: number) => data[base + TOP] < y + } else { + // No movement + return { + dx, + dy, + dl: 0, + wallDistance: Infinity, + obstacle: null, + } + } + + // Binary search in the chosen array for the first obstacle boundary + // that meets the horizontal/vertical condition (e.g., left > x) + let low = 0 + let high = candidateIndices.length - 1 + let idx = candidateIndices.length // Default: no candidate found + + // Depending on direction, we adjust comparison + const compareFn = () => { + // For right movement: we want the first obstacle where LEFT > x + // For left movement: we want the last obstacle where RIGHT < x (so first from right array that is strictly less than x) + // For up: first obstacle where BOTTOM > y + // For down: last obstacle where TOP < y + } + + while (low <= high) { + const mid = (low + high) >>> 1 + const obstIndex = candidateIndices[mid] + const base = obstIndex * STRIDE + let val: number + if (dx === 1) val = data[base + LEFT] + else if (dx === -1) val = data[base + RIGHT] + else if (dy === 1) val = data[base + BOTTOM] + else val = data[base + TOP] + + if ( + (dx === 1 && val > searchValue) || // moving right + (dx === -1 && val < searchValue) || // moving left + (dy === 1 && val > searchValue) || // moving up + (dy === -1 && val < searchValue) + ) { + // moving down + idx = mid + // We can still try to find a closer obstacle in that direction + if (dx === 1 || dy === 1) { + // Move left in array for first larger boundary + high = mid - 1 + } else { + // Move right in array for last smaller boundary + low = mid + 1 + } + } else { + // Not meeting condition, we move in opposite direction + if (dx === 1 || dy === 1) { + low = mid + 1 + } else { + high = mid - 1 + } + } + } + + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null + + // If idx is within array bounds, check candidates around idx for vertical/horizontal overlap + // Because binary search gives us a position, we might want to check the found index and neighbors + // to ensure we find the closest suitable obstacle. Usually, checking a few neighbors is enough. + for (let checkOffset = 0; checkOffset < 5; checkOffset++) { + const candidateIdx = + dx === 1 || dy === 1 ? idx + checkOffset : idx - checkOffset + if (candidateIdx < 0 || candidateIdx >= candidateIndices.length) break + + const obstIndex = candidateIndices[candidateIdx] + const base = obstIndex * STRIDE + + // Check direction conditions again + if (!horizontalCheck(base) && !verticalCheck(base)) continue + + // Now ensure vertical/horizontal overlap: + if ( + (dx !== 0 && verticalCheck(base)) || + (dy !== 0 && verticalCheck(base)) + ) { + let distance: number | null = null + + if (dx === 1) { + distance = data[base + LEFT] - margin - x + } else if (dx === -1) { + distance = x - (data[base + RIGHT] + margin) + } else if (dy === 1) { + distance = data[base + BOTTOM] - margin - y + } else if (dy === -1) { + distance = y - (data[base + TOP] + margin) + } + + if (distance !== null && distance > 0 && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[obstIndex] + } + } + } + + return { + dx, + dy, + dl: 0, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number + }): ObstacleWithEdges[] { + const { data, originalObstacles } = this + const count = data.length / STRIDE + const obstacles: ObstacleWithEdges[] = [] + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== region.l) continue + const left = data[base + LEFT] + const right = data[base + RIGHT] + const top = data[base + TOP] + const bottom = data[base + BOTTOM] + + if ( + left <= region.maxX && + right >= region.minX && + top >= region.minY && + bottom <= region.maxY + ) { + obstacles.push(originalObstacles[i]) + } + } + + return obstacles + } +} diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index 3875f46..747e475 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -41,6 +41,7 @@ import { } from "autorouting-dataset/lib/solver-utils/getAlternativeGoalBoxes" import { ObstacleList3dSectional } from "algos/common/generalized-astar/ObstacleList3dSectional" import { ObstacleList3dF64V1 } from "algos/common/generalized-astar/ObstacleList3dF64V1" +import { ObstacleList3dF64V2 } from "algos/common/generalized-astar/ObstacleList3dF64V2" export class MultilayerIjump extends GeneralizedAstarAutorouter { MAX_ITERATIONS: number = 500 @@ -192,7 +193,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { } // return new ObstacleList3dSectional( - return new ObstacleList3dF64V1( + return new ObstacleList3dF64V2( this.layerCount, this.allObstacles .filter((obstacle) => !obstacle.connectedTo.includes(bestConnectionId)) diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index f49883d..56d49dc 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 6692336795671ceddb36a35b0fb93c3d311009ed Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 20:40:21 +0800 Subject: [PATCH 07/16] add proper v2 with obstacle side sorting --- .../generalized-astar/ObstacleList3dF64V2.ts | 420 +++++++++++------- .../keyboard-route-test-profile.snap.svg | 2 +- 2 files changed, 250 insertions(+), 172 deletions(-) diff --git a/algos/common/generalized-astar/ObstacleList3dF64V2.ts b/algos/common/generalized-astar/ObstacleList3dF64V2.ts index cbf122f..653e8e7 100644 --- a/algos/common/generalized-astar/ObstacleList3dF64V2.ts +++ b/algos/common/generalized-astar/ObstacleList3dF64V2.ts @@ -1,4 +1,4 @@ -// ObstacleList3dOptimized.ts +// ObstacleList3dFastWithSorting.ts import type { Obstacle, ObstacleWithEdges } from "solver-utils" import type { @@ -21,10 +21,9 @@ const TOP = 5 const BOTTOM = 6 const LEFT = 7 const RIGHT = 8 -const TYPE = 9 - +// We can store a type if needed, currently unused in sorting. +// const TYPE = 9 const STRIDE = 10 -const TYPE_RECT = 0 export class ObstacleList3dF64V2 extends ObstacleList3d { data: Float64Array @@ -32,8 +31,7 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { GRID_STEP = 0.1 layerCount: number - // Arrays for faster direction-specific queries - // Each entry: obstaclesByLeft[layer] = array of indices into `data` sorted by LEFT + // Arrays of obstacle indices per layer sorted by their boundaries obstaclesByLeft: number[][] obstaclesByRight: number[][] obstaclesByTop: number[][] @@ -73,50 +71,41 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { this.data[base + BOTTOM] = obs.bottom this.data[base + LEFT] = obs.left this.data[base + RIGHT] = obs.right - this.data[base + TYPE] = TYPE_RECT } - // Build direction-specific indexes - this.obstaclesByLeft = [] - this.obstaclesByRight = [] - this.obstaclesByTop = [] - this.obstaclesByBottom = [] - - for (let layer = 0; layer < layerCount; layer++) { - const indicesForLayer: number[] = [] - for (let i = 0; i < count; i++) { - const base = i * STRIDE - if (this.data[base + LAYER] === layer) { - indicesForLayer.push(i) - } - } + // Initialize arrays + this.obstaclesByLeft = Array.from({ length: layerCount }, () => []) + this.obstaclesByRight = Array.from({ length: layerCount }, () => []) + this.obstaclesByTop = Array.from({ length: layerCount }, () => []) + this.obstaclesByBottom = Array.from({ length: layerCount }, () => []) + + // Distribute obstacle indices into these arrays + for (let i = 0; i < count; i++) { + const base = i * STRIDE + const l = this.data[base + LAYER] + const li = l | 0 // layer as integer - // Sort by LEFT - const byLeft = indicesForLayer - .slice() - .sort( - (a, b) => this.data[a * STRIDE + LEFT] - this.data[b * STRIDE + LEFT], - ) - const byRight = indicesForLayer - .slice() - .sort( - (a, b) => - this.data[a * STRIDE + RIGHT] - this.data[b * STRIDE + RIGHT], - ) - const byTop = indicesForLayer.slice().sort( - (a, b) => this.data[b * STRIDE + TOP] - this.data[a * STRIDE + TOP], // descending top + this.obstaclesByLeft[li].push(i) + this.obstaclesByRight[li].push(i) + this.obstaclesByTop[li].push(i) + this.obstaclesByBottom[li].push(i) + } + + // Sort each array by their respective edges + for (let l = 0; l < layerCount; l++) { + this.obstaclesByLeft[l].sort( + (a, b) => this.data[a * STRIDE + LEFT] - this.data[b * STRIDE + LEFT], + ) + this.obstaclesByRight[l].sort( + (a, b) => this.data[a * STRIDE + RIGHT] - this.data[b * STRIDE + RIGHT], + ) + this.obstaclesByTop[l].sort( + (a, b) => this.data[a * STRIDE + TOP] - this.data[b * STRIDE + TOP], + ) + this.obstaclesByBottom[l].sort( + (a, b) => + this.data[a * STRIDE + BOTTOM] - this.data[b * STRIDE + BOTTOM], ) - const byBottom = indicesForLayer - .slice() - .sort( - (a, b) => - this.data[a * STRIDE + BOTTOM] - this.data[b * STRIDE + BOTTOM], - ) - - this.obstaclesByLeft[layer] = byLeft - this.obstaclesByRight[layer] = byRight - this.obstaclesByTop[layer] = byTop - this.obstaclesByBottom[layer] = byBottom } } @@ -134,14 +123,14 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { if (data[base + LAYER] !== l) continue const halfWidth = data[base + WIDTH] / 2 + m const halfHeight = data[base + HEIGHT] / 2 + m - const cx = data[base + CENTER_X] - const cy = data[base + CENTER_Y] + const centerX = data[base + CENTER_X] + const centerY = data[base + CENTER_Y] if ( - x >= cx - halfWidth && - x <= cx + halfWidth && - y >= cy - halfHeight && - y <= cy + halfHeight + x >= centerX - halfWidth && + x <= centerX + halfWidth && + y >= centerY - halfHeight && + y <= centerY + halfHeight ) { return this.originalObstacles[i] } @@ -158,10 +147,10 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { y: number, l: number, ): DirectionDistances3d { - // This method can remain unchanged or also benefit from the indexes if needed. - // For brevity, we keep it as is: + // This method remains O(n). If needed, we could also optimize it similarly. const { GRID_STEP, data } = this const count = data.length / STRIDE + const result: DirectionDistances3d = { left: Infinity, top: Infinity, @@ -172,6 +161,7 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { for (let i = 0; i < count; i++) { const base = i * STRIDE if (data[base + LAYER] !== l) continue + const left = data[base + LEFT] - GRID_STEP const right = data[base + RIGHT] + GRID_STEP const top = data[base + TOP] + GRID_STEP @@ -201,6 +191,45 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { return result } + /** + * Binary search utility: finds the insertion index for value in a sorted array, + * similar to Python's bisect_left. Returns index where value could be inserted + * to maintain sorted order. If exact match is found, returns that index. + */ + private bisectLeft( + array: number[], + getVal: (i: number) => number, + value: number, + ): number { + let low = 0 + let high = array.length + while (low < high) { + const mid = (low + high) >>> 1 + if (getVal(array[mid]) < value) low = mid + 1 + else high = mid + } + return low + } + + /** + * Binary search utility: finds the insertion index for value similar to bisect_right, + * returns the insertion point to the right of existing entries of value. + */ + private bisectRight( + array: number[], + getVal: (i: number) => number, + value: number, + ): number { + let low = 0 + let high = array.length + while (low < high) { + const mid = (low + high) >>> 1 + if (getVal(array[mid]) <= value) low = mid + 1 + else high = mid + } + return low + } + getOrthoDirectionCollisionInfo( point: Point3d, dir: Direction3d, @@ -210,7 +239,7 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { const { dx, dy, dl } = dir if (dl !== 0) { - // Layer change remains O(1), just check the next layer + // Layer change is unchanged - no spatial optimization for layer transitions. const newLayer = l + dl let collisionObstacle: ObstacleWithEdges | null = null let minDistance = 1 @@ -222,6 +251,7 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { margin, ) as ObstacleWithEdges } + return { dx, dy, @@ -231,140 +261,187 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { } } - // Horizontal or vertical movement within the same layer - let candidateIndices: number[] | null = null - let searchValue: number - let verticalCheck: (base: number) => boolean - let horizontalCheck: (base: number) => boolean - - const { data } = this + // Moving within the same layer + // Determine which sorted array to use based on direction + const layerIndex = l | 0 + let candidates: number[] = [] + let edgeGetter: (i: number) => number + let forwardSearch = true if (dx === 1 && dy === 0) { - // Moving right: find obstacles with left > x - candidateIndices = this.obstaclesByLeft[l] - searchValue = x - verticalCheck = (base: number) => - y > data[base + BOTTOM] - margin && y < data[base + TOP] + margin - horizontalCheck = (base: number) => data[base + LEFT] > x + // Moving right, check obstaclesByLeft: + // We want the smallest left that is > x + candidates = this.obstaclesByLeft[layerIndex] + edgeGetter = (idx) => this.data[idx * STRIDE + LEFT] + // We'll binary search for the first obstacle with LEFT > x + const startIndex = this.bisectLeft(candidates, edgeGetter, x) + return this.findCollisionCandidateHorizontal( + candidates, + startIndex, + x, + y, + margin, + dx, + dy, + /*movingRight=*/ true, + ) } else if (dx === -1 && dy === 0) { - // Moving left: find obstacles with right < x - candidateIndices = this.obstaclesByRight[l] - searchValue = x - verticalCheck = (base: number) => - y > data[base + BOTTOM] - margin && y < data[base + TOP] + margin - horizontalCheck = (base: number) => data[base + RIGHT] < x + // Moving left, check obstaclesByRight: + // We want the largest right that is < x. We'll use bisectLeft for x and then go one left. + candidates = this.obstaclesByRight[layerIndex] + edgeGetter = (idx) => this.data[idx * STRIDE + RIGHT] + // Find insertion for x in RIGHT array + const startIndex = this.bisectLeft(candidates, edgeGetter, x) - 1 + return this.findCollisionCandidateHorizontal( + candidates, + startIndex, + x, + y, + margin, + dx, + dy, + /*movingRight=*/ false, + ) } else if (dx === 0 && dy === 1) { - // Moving up: find obstacles with bottom > y - candidateIndices = this.obstaclesByBottom[l] - searchValue = y - verticalCheck = (base: number) => - x > data[base + LEFT] - margin && x < data[base + RIGHT] + margin - horizontalCheck = (base: number) => data[base + BOTTOM] > y + // Moving up, check obstaclesByBottom: + // We want the smallest bottom that is > y + candidates = this.obstaclesByBottom[layerIndex] + edgeGetter = (idx) => this.data[idx * STRIDE + BOTTOM] + const startIndex = this.bisectLeft(candidates, edgeGetter, y) + return this.findCollisionCandidateVertical( + candidates, + startIndex, + x, + y, + margin, + dx, + dy, + /*movingUp=*/ true, + ) } else if (dx === 0 && dy === -1) { - // Moving down: find obstacles with top < y - candidateIndices = this.obstaclesByTop[l] - searchValue = y - verticalCheck = (base: number) => - x > data[base + LEFT] - margin && x < data[base + RIGHT] + margin - horizontalCheck = (base: number) => data[base + TOP] < y - } else { - // No movement - return { + // Moving down, check obstaclesByTop: + // We want the largest top that is < y + candidates = this.obstaclesByTop[layerIndex] + edgeGetter = (idx) => this.data[idx * STRIDE + TOP] + const startIndex = this.bisectLeft(candidates, edgeGetter, y) - 1 + return this.findCollisionCandidateVertical( + candidates, + startIndex, + x, + y, + margin, dx, dy, - dl: 0, - wallDistance: Infinity, - obstacle: null, - } + /*movingUp=*/ false, + ) + } else { + // No movement + return { dx, dy, dl: 0, wallDistance: Infinity, obstacle: null } } + } - // Binary search in the chosen array for the first obstacle boundary - // that meets the horizontal/vertical condition (e.g., left > x) - let low = 0 - let high = candidateIndices.length - 1 - let idx = candidateIndices.length // Default: no candidate found - - // Depending on direction, we adjust comparison - const compareFn = () => { - // For right movement: we want the first obstacle where LEFT > x - // For left movement: we want the last obstacle where RIGHT < x (so first from right array that is strictly less than x) - // For up: first obstacle where BOTTOM > y - // For down: last obstacle where TOP < y - } + private findCollisionCandidateHorizontal( + candidates: number[], + startIndex: number, + x: number, + y: number, + margin: number, + dx: number, + dy: number, + movingRight: boolean, + ): DirectionWithCollisionInfo3d { + // If movingRight = true, we start from startIndex and move forward + // If movingRight = false, we start from startIndex and move backward + const step = movingRight ? 1 : -1 + const { data } = this + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null - while (low <= high) { - const mid = (low + high) >>> 1 - const obstIndex = candidateIndices[mid] - const base = obstIndex * STRIDE - let val: number - if (dx === 1) val = data[base + LEFT] - else if (dx === -1) val = data[base + RIGHT] - else if (dy === 1) val = data[base + BOTTOM] - else val = data[base + TOP] + for (let i = startIndex; i >= 0 && i < candidates.length; i += step) { + const idx = candidates[i] + const base = idx * STRIDE - if ( - (dx === 1 && val > searchValue) || // moving right - (dx === -1 && val < searchValue) || // moving left - (dy === 1 && val > searchValue) || // moving up - (dy === -1 && val < searchValue) - ) { - // moving down - idx = mid - // We can still try to find a closer obstacle in that direction - if (dx === 1 || dy === 1) { - // Move left in array for first larger boundary - high = mid - 1 - } else { - // Move right in array for last smaller boundary - low = mid + 1 - } - } else { - // Not meeting condition, we move in opposite direction - if (dx === 1 || dy === 1) { - low = mid + 1 + // Vertical overlap check: + const topMargin = data[base + TOP] + margin + const bottomMargin = data[base + BOTTOM] - margin + const leftMargin = data[base + LEFT] - margin + const rightMargin = data[base + RIGHT] + margin + + if (y > bottomMargin && y < topMargin) { + // Compute distance + let distance: number + if (dx === 1) { + // moving right: obstacle's left edge is relevant + distance = leftMargin - x + if (distance >= 0 && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[idx] + break + } } else { - high = mid - 1 + // moving left: obstacle's right edge is relevant + distance = x - rightMargin + if (distance >= 0 && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[idx] + break + } } } } + return { + dx, + dy, + dl: 0, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } + + private findCollisionCandidateVertical( + candidates: number[], + startIndex: number, + x: number, + y: number, + margin: number, + dx: number, + dy: number, + movingUp: boolean, + ): DirectionWithCollisionInfo3d { + // Similar logic for vertical movement + const step = movingUp ? 1 : -1 + const { data } = this let minDistance = Infinity let collisionObstacle: ObstacleWithEdges | null = null - // If idx is within array bounds, check candidates around idx for vertical/horizontal overlap - // Because binary search gives us a position, we might want to check the found index and neighbors - // to ensure we find the closest suitable obstacle. Usually, checking a few neighbors is enough. - for (let checkOffset = 0; checkOffset < 5; checkOffset++) { - const candidateIdx = - dx === 1 || dy === 1 ? idx + checkOffset : idx - checkOffset - if (candidateIdx < 0 || candidateIdx >= candidateIndices.length) break - - const obstIndex = candidateIndices[candidateIdx] - const base = obstIndex * STRIDE - - // Check direction conditions again - if (!horizontalCheck(base) && !verticalCheck(base)) continue - - // Now ensure vertical/horizontal overlap: - if ( - (dx !== 0 && verticalCheck(base)) || - (dy !== 0 && verticalCheck(base)) - ) { - let distance: number | null = null - - if (dx === 1) { - distance = data[base + LEFT] - margin - x - } else if (dx === -1) { - distance = x - (data[base + RIGHT] + margin) - } else if (dy === 1) { - distance = data[base + BOTTOM] - margin - y - } else if (dy === -1) { - distance = y - (data[base + TOP] + margin) - } - - if (distance !== null && distance > 0 && distance < minDistance) { - minDistance = distance - collisionObstacle = this.originalObstacles[obstIndex] + for (let i = startIndex; i >= 0 && i < candidates.length; i += step) { + const idx = candidates[i] + const base = idx * STRIDE + + const leftMargin = data[base + LEFT] - margin + const rightMargin = data[base + RIGHT] + margin + const topMargin = data[base + TOP] + margin + const bottomMargin = data[base + BOTTOM] - margin + + if (x > leftMargin && x < rightMargin) { + let distance: number + if (dy === 1) { + // moving up: obstacle's bottom edge is relevant + distance = bottomMargin - y + if (distance >= 0 && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[idx] + break + } + } else { + // moving down: obstacle's top edge is relevant + distance = y - topMargin + if (distance >= 0 && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[idx] + break + } } } } @@ -383,8 +460,9 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { minY: number maxX: number maxY: number - l: number + l: number // Layer to check }): ObstacleWithEdges[] { + // This remains the same O(n) approach. It can also be improved using spatial indexing if needed. const { data, originalObstacles } = this const count = data.length / STRIDE const obstacles: ObstacleWithEdges[] = [] diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index 56d49dc..d81b6f3 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 08ed9d693ddd8e637eda988a4d11ec75a3fbde1f Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 20:56:40 +0800 Subject: [PATCH 08/16] version 3 with dynamic and fast obstacle cell computation for getObstacleAt --- .../generalized-astar/ObstacleList3dF64V3.ts | 377 ++++++++++++++++++ algos/multi-layer-ijump/MultilayerIjump.ts | 3 +- .../keyboard-route-test-profile.snap.svg | 2 +- 3 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 algos/common/generalized-astar/ObstacleList3dF64V3.ts diff --git a/algos/common/generalized-astar/ObstacleList3dF64V3.ts b/algos/common/generalized-astar/ObstacleList3dF64V3.ts new file mode 100644 index 0000000..0f52e45 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dF64V3.ts @@ -0,0 +1,377 @@ +// ObstacleList3dCellBased.ts + +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import type { + Direction3d, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { getLayerIndex, getLayerNamesForLayerCount } from "./util" +import { ObstacleList } from "./ObstacleList" +import { ObstacleList3d } from "./ObstacleList3d" + +const CELL_COLS = 16 +const CELL_ROWS = 16 + +const CENTER_X = 0 +const CENTER_Y = 1 +const WIDTH = 2 +const HEIGHT = 3 +const LAYER = 4 +const TOP = 5 +const BOTTOM = 6 +const LEFT = 7 +const RIGHT = 8 +const STRIDE = 10 + +export class ObstacleList3dF64V3 extends ObstacleList3d { + data: Float64Array + originalObstacles: ObstacleWithEdges3d[] + GRID_STEP = 0.1 + layerCount: number + + // Cells: each cell is lazily filled with obstacle indices + // Key: row * CELL_COLS + col + cells: (number[] | null)[] + + // Bounding box for all obstacles + minX: number + maxX: number + minY: number + maxY: number + cellWidth: number + cellHeight: number + + constructor(layerCount: number, obstacles: Array) { + super(layerCount, []) + this.layerCount = layerCount + const availableLayers = getLayerNamesForLayerCount(layerCount) + + const filtered: ObstacleWithEdges3d[] = obstacles.flatMap((obstacle) => + obstacle.layers + .filter((layer) => availableLayers.includes(layer)) + .map((layer) => ({ + ...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, + l: getLayerIndex(layerCount, layer), + })), + ) + + this.originalObstacles = filtered + const count = filtered.length + this.data = new Float64Array(count * STRIDE) + + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + + for (let i = 0; i < count; i++) { + const obs = filtered[i] + const base = i * STRIDE + this.data[base + CENTER_X] = obs.center.x + this.data[base + CENTER_Y] = obs.center.y + this.data[base + WIDTH] = obs.width + this.data[base + HEIGHT] = obs.height + this.data[base + LAYER] = obs.l + this.data[base + TOP] = obs.top + this.data[base + BOTTOM] = obs.bottom + this.data[base + LEFT] = obs.left + this.data[base + RIGHT] = obs.right + + if (obs.left < minX) minX = obs.left + if (obs.right > maxX) maxX = obs.right + if (obs.bottom < minY) minY = obs.bottom + if (obs.top > maxY) maxY = obs.top + } + + // Compute cell sizes based on bounding box + // If no obstacles, handle gracefully + if (count === 0) { + minX = 0 + maxX = 1 + minY = 0 + maxY = 1 + } + + this.minX = minX + this.maxX = maxX + this.minY = minY + this.maxY = maxY + + const totalWidth = this.maxX - this.minX || 1 + const totalHeight = this.maxY - this.minY || 1 + this.cellWidth = totalWidth / CELL_COLS + this.cellHeight = totalHeight / CELL_ROWS + + // Initialize cells to null for lazy filling + this.cells = Array(CELL_COLS * CELL_ROWS).fill(null) + } + + private getCellIndexForPoint(x: number, y: number): number | null { + // Compute which cell (x,y) falls into + const col = Math.floor( + ((x - this.minX) / (this.maxX - this.minX)) * CELL_COLS, + ) + const row = Math.floor( + ((y - this.minY) / (this.maxY - this.minY)) * CELL_ROWS, + ) + + if (col < 0 || col >= CELL_COLS || row < 0 || row >= CELL_ROWS) { + return null // Point outside the bounding box range + } + return row * CELL_COLS + col + } + + private fillCell(cellIndex: number) { + // If the cell is not filled, we scan all obstacles to find which belong here + if (this.cells[cellIndex] !== null) return // Already filled + + const row = Math.floor(cellIndex / CELL_COLS) + const col = cellIndex % CELL_COLS + + const cellMinX = this.minX + col * this.cellWidth + const cellMaxX = cellMinX + this.cellWidth + const cellMinY = this.minY + row * this.cellHeight + const cellMaxY = cellMinY + this.cellHeight + + const { data } = this + const count = data.length / STRIDE + const cellObstacles: number[] = [] + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + const left = data[base + LEFT] + const right = data[base + RIGHT] + const top = data[base + TOP] + const bottom = data[base + BOTTOM] + + // Check if obstacle overlaps this cell at all + if ( + right >= cellMinX && + left <= cellMaxX && + top >= cellMinY && + bottom <= cellMaxY + ) { + cellObstacles.push(i) + } + } + + this.cells[cellIndex] = cellObstacles + } + + getObstacleAt( + x: number, + y: number, + l: number, + m?: number, + ): ObstacleWithEdges | null { + m ??= this.GRID_STEP + + const cellIndex = this.getCellIndexForPoint(x, y) + if (cellIndex === null) { + // Out of range, just scan all or return null + // For simplicity, return null. If needed, handle differently. + return null + } + + this.fillCell(cellIndex) + const cellObstacles = this.cells[cellIndex]! + const { data } = this + + for (let idx of cellObstacles) { + const base = idx * STRIDE + // Check layer + if (data[base + LAYER] !== l) continue + + const halfWidth = data[base + WIDTH] / 2 + m + const halfHeight = data[base + HEIGHT] / 2 + m + const centerX = data[base + CENTER_X] + const centerY = data[base + CENTER_Y] + + if ( + x >= centerX - halfWidth && + x <= centerX + halfWidth && + y >= centerY - halfHeight && + y <= centerY + halfHeight + ) { + return this.originalObstacles[idx] + } + } + + return null + } + + isObstacleAt(x: number, y: number, l: number, m?: number): boolean { + return this.getObstacleAt(x, y, l, m) !== null + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + // This method is unchanged, still O(n), but can be similarly optimized if needed. + const { GRID_STEP, data } = this + const count = data.length / STRIDE + const result: DirectionDistances3d = { + left: Infinity, + top: Infinity, + bottom: Infinity, + right: Infinity, + } + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + const left = data[base + LEFT] - GRID_STEP + const right = data[base + RIGHT] + GRID_STEP + const top = data[base + TOP] + GRID_STEP + const bottom = data[base + BOTTOM] - 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: Point3d, + dir: Direction3d, + { margin = 0 }: { margin?: number } = {}, + ): DirectionWithCollisionInfo3d { + // This method is unchanged. Could also be optimized using spatial partitioning if needed. + const { x, y, l } = point + const { dx, dy, dl } = dir + let minDistance = Infinity + let collisionObstacle: ObstacleWithEdges | null = null + + if (dl !== 0) { + // Moving between layers + const newLayer = l + dl + if (this.isObstacleAt(x, y, newLayer, margin)) { + minDistance = 1 // Distance to obstacle is 1 (layer change) + collisionObstacle = this.getObstacleAt( + x, + y, + newLayer, + margin, + ) as ObstacleWithEdges + } else { + minDistance = 1 // No obstacle, just distance to next layer + } + + return { + dx, + dy, + dl, + wallDistance: minDistance, + obstacle: collisionObstacle, + } + } else { + const { data } = this + const count = data.length / STRIDE + + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== l) continue + + const leftMargin = data[base + LEFT] - margin + const rightMargin = data[base + RIGHT] + margin + const topMargin = data[base + TOP] + margin + const bottomMargin = data[base + BOTTOM] - margin + + let distance: number | null = null + + if (dx === 1 && dy === 0) { + // Right + if (y > bottomMargin && y < topMargin && x < leftMargin) { + distance = leftMargin - x + } + } else if (dx === -1 && dy === 0) { + // Left + if (y > bottomMargin && y < topMargin && x > rightMargin) { + distance = x - rightMargin + } + } else if (dx === 0 && dy === 1) { + // Up + if (x > leftMargin && x < rightMargin && y < bottomMargin) { + distance = bottomMargin - y + } + } else if (dx === 0 && dy === -1) { + // Down + if (x > leftMargin && x < rightMargin && y > topMargin) { + distance = y - topMargin + } + } + + if (distance !== null && distance < minDistance) { + minDistance = distance + collisionObstacle = this.originalObstacles[i] + } + } + + return { + dx, + dy, + dl: 0, + wallDistance: minDistance, + obstacle: collisionObstacle as ObstacleWithEdges, + } + } + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number // Layer to check + }): ObstacleWithEdges[] { + const { data, originalObstacles } = this + const count = data.length / STRIDE + const obstacles: ObstacleWithEdges[] = [] + for (let i = 0; i < count; i++) { + const base = i * STRIDE + if (data[base + LAYER] !== region.l) continue + const left = data[base + LEFT] + const right = data[base + RIGHT] + const top = data[base + TOP] + const bottom = data[base + BOTTOM] + + if ( + left <= region.maxX && + right >= region.minX && + top >= region.minY && + bottom <= region.maxY + ) { + obstacles.push(originalObstacles[i]) + } + } + + return obstacles + } +} diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index 747e475..3b3165a 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -42,6 +42,7 @@ import { import { ObstacleList3dSectional } from "algos/common/generalized-astar/ObstacleList3dSectional" import { ObstacleList3dF64V1 } from "algos/common/generalized-astar/ObstacleList3dF64V1" import { ObstacleList3dF64V2 } from "algos/common/generalized-astar/ObstacleList3dF64V2" +import { ObstacleList3dF64V3 } from "algos/common/generalized-astar/ObstacleList3dF64V3" export class MultilayerIjump extends GeneralizedAstarAutorouter { MAX_ITERATIONS: number = 500 @@ -193,7 +194,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { } // return new ObstacleList3dSectional( - return new ObstacleList3dF64V2( + return new ObstacleList3dF64V3( this.layerCount, this.allObstacles .filter((obstacle) => !obstacle.connectedTo.includes(bestConnectionId)) diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index d81b6f3..f49883d 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 5700690d957ea7d7111eaac97030d5ec2c6e26c1 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 21:09:44 +0800 Subject: [PATCH 09/16] more profiling, discoveries --- .../GeneralizedAstarAutorouter.ts | 4 + .../generalized-astar/ObstacleList3dF64V2.ts | 29 +++ .../generalized-astar/ObstacleList3dF64V3.ts | 231 +----------------- .../keyboard-route-test-profile.snap.svg | 2 +- 4 files changed, 37 insertions(+), 229 deletions(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index aafa675..f25c904 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -179,9 +179,11 @@ export class GeneralizedAstarAutorouter { const tentativeG = this.computeG(current, neighbor) + this.profiler?.startMeasurement("openSetFind") const existingNeighbor = this.openSet.find((n) => this.isSameNode(n, neighbor), ) + this.profiler?.endMeasurement("openSetFind") if (!existingNeighbor || tentativeG < existingNeighbor.g) { const h = this.computeH(neighbor) @@ -202,12 +204,14 @@ export class GeneralizedAstarAutorouter { } // Insert into openSet in sorted order by f value + this.profiler?.startMeasurement("openSetInsert") const insertIndex = openSet.findIndex((node) => node.f > neighborNode.f) if (insertIndex === -1) { openSet.push(neighborNode) } else { openSet.splice(insertIndex, 0, neighborNode) } + this.profiler?.endMeasurement("openSetInsert") newNeighbors.push(neighborNode) } diff --git a/algos/common/generalized-astar/ObstacleList3dF64V2.ts b/algos/common/generalized-astar/ObstacleList3dF64V2.ts index 653e8e7..d1f3a30 100644 --- a/algos/common/generalized-astar/ObstacleList3dF64V2.ts +++ b/algos/common/generalized-astar/ObstacleList3dF64V2.ts @@ -37,6 +37,11 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { obstaclesByTop: number[][] obstaclesByBottom: number[][] + minX: number + maxX: number + minY: number + maxY: number + constructor(layerCount: number, obstacles: Array) { super(layerCount, []) this.layerCount = layerCount @@ -59,6 +64,11 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { const count = filtered.length this.data = new Float64Array(count * STRIDE) + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + for (let i = 0; i < count; i++) { const obs = filtered[i] const base = i * STRIDE @@ -71,8 +81,27 @@ export class ObstacleList3dF64V2 extends ObstacleList3d { this.data[base + BOTTOM] = obs.bottom this.data[base + LEFT] = obs.left this.data[base + RIGHT] = obs.right + + if (obs.left < minX) minX = obs.left + if (obs.right > maxX) maxX = obs.right + if (obs.bottom < minY) minY = obs.bottom + if (obs.top > maxY) maxY = obs.top } + // Compute cell sizes based on bounding box + // If no obstacles, handle gracefully + if (count === 0) { + minX = 0 + maxX = 1 + minY = 0 + maxY = 1 + } + + this.minX = minX + this.maxX = maxX + this.minY = minY + this.maxY = maxY + // Initialize arrays this.obstaclesByLeft = Array.from({ length: layerCount }, () => []) this.obstaclesByRight = Array.from({ length: layerCount }, () => []) diff --git a/algos/common/generalized-astar/ObstacleList3dF64V3.ts b/algos/common/generalized-astar/ObstacleList3dF64V3.ts index 0f52e45..c06bdb0 100644 --- a/algos/common/generalized-astar/ObstacleList3dF64V3.ts +++ b/algos/common/generalized-astar/ObstacleList3dF64V3.ts @@ -11,6 +11,7 @@ import type { import { getLayerIndex, getLayerNamesForLayerCount } from "./util" import { ObstacleList } from "./ObstacleList" import { ObstacleList3d } from "./ObstacleList3d" +import { ObstacleList3dF64V2 } from "./ObstacleList3dF64V2" const CELL_COLS = 16 const CELL_ROWS = 16 @@ -26,9 +27,7 @@ const LEFT = 7 const RIGHT = 8 const STRIDE = 10 -export class ObstacleList3dF64V3 extends ObstacleList3d { - data: Float64Array - originalObstacles: ObstacleWithEdges3d[] +export class ObstacleList3dF64V3 extends ObstacleList3dF64V2 { GRID_STEP = 0.1 layerCount: number @@ -36,74 +35,14 @@ export class ObstacleList3dF64V3 extends ObstacleList3d { // Key: row * CELL_COLS + col cells: (number[] | null)[] - // Bounding box for all obstacles - minX: number - maxX: number - minY: number - maxY: number cellWidth: number cellHeight: number constructor(layerCount: number, obstacles: Array) { - super(layerCount, []) + super(layerCount, obstacles) this.layerCount = layerCount const availableLayers = getLayerNamesForLayerCount(layerCount) - const filtered: ObstacleWithEdges3d[] = obstacles.flatMap((obstacle) => - obstacle.layers - .filter((layer) => availableLayers.includes(layer)) - .map((layer) => ({ - ...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, - l: getLayerIndex(layerCount, layer), - })), - ) - - this.originalObstacles = filtered - const count = filtered.length - this.data = new Float64Array(count * STRIDE) - - let minX = Infinity - let maxX = -Infinity - let minY = Infinity - let maxY = -Infinity - - for (let i = 0; i < count; i++) { - const obs = filtered[i] - const base = i * STRIDE - this.data[base + CENTER_X] = obs.center.x - this.data[base + CENTER_Y] = obs.center.y - this.data[base + WIDTH] = obs.width - this.data[base + HEIGHT] = obs.height - this.data[base + LAYER] = obs.l - this.data[base + TOP] = obs.top - this.data[base + BOTTOM] = obs.bottom - this.data[base + LEFT] = obs.left - this.data[base + RIGHT] = obs.right - - if (obs.left < minX) minX = obs.left - if (obs.right > maxX) maxX = obs.right - if (obs.bottom < minY) minY = obs.bottom - if (obs.top > maxY) maxY = obs.top - } - - // Compute cell sizes based on bounding box - // If no obstacles, handle gracefully - if (count === 0) { - minX = 0 - maxX = 1 - minY = 0 - maxY = 1 - } - - this.minX = minX - this.maxX = maxX - this.minY = minY - this.maxY = maxY - const totalWidth = this.maxX - this.minX || 1 const totalHeight = this.maxY - this.minY || 1 this.cellWidth = totalWidth / CELL_COLS @@ -210,168 +149,4 @@ export class ObstacleList3dF64V3 extends ObstacleList3d { isObstacleAt(x: number, y: number, l: number, m?: number): boolean { return this.getObstacleAt(x, y, l, m) !== null } - - getDirectionDistancesToNearestObstacle3d( - x: number, - y: number, - l: number, - ): DirectionDistances3d { - // This method is unchanged, still O(n), but can be similarly optimized if needed. - const { GRID_STEP, data } = this - const count = data.length / STRIDE - const result: DirectionDistances3d = { - left: Infinity, - top: Infinity, - bottom: Infinity, - right: Infinity, - } - - for (let i = 0; i < count; i++) { - const base = i * STRIDE - if (data[base + LAYER] !== l) continue - const left = data[base + LEFT] - GRID_STEP - const right = data[base + RIGHT] + GRID_STEP - const top = data[base + TOP] + GRID_STEP - const bottom = data[base + BOTTOM] - 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: Point3d, - dir: Direction3d, - { margin = 0 }: { margin?: number } = {}, - ): DirectionWithCollisionInfo3d { - // This method is unchanged. Could also be optimized using spatial partitioning if needed. - const { x, y, l } = point - const { dx, dy, dl } = dir - let minDistance = Infinity - let collisionObstacle: ObstacleWithEdges | null = null - - if (dl !== 0) { - // Moving between layers - const newLayer = l + dl - if (this.isObstacleAt(x, y, newLayer, margin)) { - minDistance = 1 // Distance to obstacle is 1 (layer change) - collisionObstacle = this.getObstacleAt( - x, - y, - newLayer, - margin, - ) as ObstacleWithEdges - } else { - minDistance = 1 // No obstacle, just distance to next layer - } - - return { - dx, - dy, - dl, - wallDistance: minDistance, - obstacle: collisionObstacle, - } - } else { - const { data } = this - const count = data.length / STRIDE - - for (let i = 0; i < count; i++) { - const base = i * STRIDE - if (data[base + LAYER] !== l) continue - - const leftMargin = data[base + LEFT] - margin - const rightMargin = data[base + RIGHT] + margin - const topMargin = data[base + TOP] + margin - const bottomMargin = data[base + BOTTOM] - margin - - let distance: number | null = null - - if (dx === 1 && dy === 0) { - // Right - if (y > bottomMargin && y < topMargin && x < leftMargin) { - distance = leftMargin - x - } - } else if (dx === -1 && dy === 0) { - // Left - if (y > bottomMargin && y < topMargin && x > rightMargin) { - distance = x - rightMargin - } - } else if (dx === 0 && dy === 1) { - // Up - if (x > leftMargin && x < rightMargin && y < bottomMargin) { - distance = bottomMargin - y - } - } else if (dx === 0 && dy === -1) { - // Down - if (x > leftMargin && x < rightMargin && y > topMargin) { - distance = y - topMargin - } - } - - if (distance !== null && distance < minDistance) { - minDistance = distance - collisionObstacle = this.originalObstacles[i] - } - } - - return { - dx, - dy, - dl: 0, - wallDistance: minDistance, - obstacle: collisionObstacle as ObstacleWithEdges, - } - } - } - - getObstaclesOverlappingRegion(region: { - minX: number - minY: number - maxX: number - maxY: number - l: number // Layer to check - }): ObstacleWithEdges[] { - const { data, originalObstacles } = this - const count = data.length / STRIDE - const obstacles: ObstacleWithEdges[] = [] - for (let i = 0; i < count; i++) { - const base = i * STRIDE - if (data[base + LAYER] !== region.l) continue - const left = data[base + LEFT] - const right = data[base + RIGHT] - const top = data[base + TOP] - const bottom = data[base + BOTTOM] - - if ( - left <= region.maxX && - right >= region.minX && - top >= region.minY && - bottom <= region.maxY - ) { - obstacles.push(originalObstacles[i]) - } - } - - return obstacles - } } diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index f49883d..d81b6f3 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 7b301bd192037fb978ef491528101c2c6f2ff0e3 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 21:24:17 +0800 Subject: [PATCH 10/16] optimize openSetFind timing --- .../generalized-astar/GeneralizedAstarAutorouter.ts | 12 +++++++++--- .../keyboard-route-test-profile.snap.svg | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index f25c904..345646e 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -32,6 +32,7 @@ const globalGeneralizedAstarAutorouterProfiler = new Profiler() export class GeneralizedAstarAutorouter { profiler?: Profiler openSet: Node[] = [] + openSetMap: Map = new Map() closedSet: Set = new Set() debug = false @@ -162,6 +163,9 @@ export class GeneralizedAstarAutorouter { this._sortOpenSet() const current = openSet.shift()! + const currentKey = this.getNodeName(current) + this.openSetMap.delete(currentKey) + const goalDist = this.computeH(current) if (goalDist <= GRID_STEP * 2) { return { @@ -180,9 +184,9 @@ export class GeneralizedAstarAutorouter { const tentativeG = this.computeG(current, neighbor) this.profiler?.startMeasurement("openSetFind") - const existingNeighbor = this.openSet.find((n) => - this.isSameNode(n, neighbor), - ) + const neighborKey = this.getNodeName(neighbor) + const existingNeighbor = this.openSetMap.get(neighborKey) + this.profiler?.endMeasurement("openSetFind") if (!existingNeighbor || tentativeG < existingNeighbor.g) { @@ -282,6 +286,8 @@ export class GeneralizedAstarAutorouter { l: this.layerToIndex(pointsToConnect[pointsToConnect.length - 1].layer), } this.openSet = [this.startNode] + this.openSetMap.clear() + this.openSetMap.set(this.getNodeName(this.startNode), this.startNode) while (this.iterations < this.MAX_ITERATIONS) { const { solved, current } = this.solveOneStep() diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index d81b6f3..f49883d 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file From 898749c1e40bf5f2b7a7dae5a0c3c22e9aab83f2 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 22:06:18 +0800 Subject: [PATCH 11/16] sectional v2 (fail), openset binary insert for 10ms improvement --- .../GeneralizedAstarAutorouter.ts | 30 +- .../ObstacleList3dSectionalV2.ts | 342 ++++++++++++++++++ algos/multi-layer-ijump/MultilayerIjump.ts | 3 +- 3 files changed, 366 insertions(+), 9 deletions(-) create mode 100644 algos/common/generalized-astar/ObstacleList3dSectionalV2.ts diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index 345646e..23b3693 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -48,6 +48,9 @@ export class GeneralizedAstarAutorouter { GRID_STEP: number OBSTACLE_MARGIN: number MAX_ITERATIONS: number + + // Set this to MAX_ITERATIONS for best quality- but at the cost of some speed + MAX_OPEN_SET_SIZE: number = 200 isRemovePathLoopsEnabled: boolean /** * Setting this greater than 1 makes the algorithm find suboptimal paths and @@ -148,8 +151,8 @@ export class GeneralizedAstarAutorouter { // Not needed because we do an insert sort // this.openSet.sort((a, b) => a.f - b.f) // Limit the size of the openset - if (this.openSet.length > this.MAX_ITERATIONS) { - this.openSet.splice(this.MAX_ITERATIONS) + if (this.openSet.length > this.MAX_OPEN_SET_SIZE) { + this.openSet.splice(this.MAX_OPEN_SET_SIZE) } } @@ -209,12 +212,7 @@ export class GeneralizedAstarAutorouter { // Insert into openSet in sorted order by f value this.profiler?.startMeasurement("openSetInsert") - const insertIndex = openSet.findIndex((node) => node.f > neighborNode.f) - if (insertIndex === -1) { - openSet.push(neighborNode) - } else { - openSet.splice(insertIndex, 0, neighborNode) - } + this._binarySearchOpenSetInsert(neighborNode) this.profiler?.endMeasurement("openSetInsert") newNeighbors.push(neighborNode) @@ -233,6 +231,22 @@ export class GeneralizedAstarAutorouter { } } + _binarySearchOpenSetInsert(neighborNode: Node) { + let left = 0 + let right = this.openSet.length - 1 + + while (left <= right) { + const mid = Math.floor((left + right) / 2) + if (this.openSet[mid].f > neighborNode.f) { + right = mid - 1 + } else { + left = mid + 1 + } + } + + this.openSet.splice(left, 0, neighborNode) + } + getStartNode(connection: SimpleRouteConnection): Node { return { x: connection.pointsToConnect[0].x, diff --git a/algos/common/generalized-astar/ObstacleList3dSectionalV2.ts b/algos/common/generalized-astar/ObstacleList3dSectionalV2.ts new file mode 100644 index 0000000..30d9f26 --- /dev/null +++ b/algos/common/generalized-astar/ObstacleList3dSectionalV2.ts @@ -0,0 +1,342 @@ +import type { Obstacle, ObstacleWithEdges } from "solver-utils" +import { ObstacleList3d } from "./ObstacleList3d" +import type { + Direction3d, + DirectionDistances, + DirectionDistances3d, + DirectionWithCollisionInfo3d, + Obstacle3d, + ObstacleWithEdges3d, + Point3d, +} from "./types" +import { Profiler } from "./Profiler" +import { ObstacleList3dF64V2 } from "./ObstacleList3dF64V2" + +type SectionId = `${number},${number}` + +const globalObstacleList3dSectionalV2Profiler = new Profiler() + +export class ObstacleList3dSectionalV2 implements ObstacleList3d { + /** + * Cached on-demand computed sections. + * Each section caches an ObstacleList3d containing obstacles intersecting that section. + */ + private sectionsCache: Record = {} + + /** + * Cached on-demand computed aligned sections. + * Each aligned section caches an ObstacleList3d containing obstacles aligned with that section. + */ + private alignedSectionsCache: Record = {} + + profiler?: Profiler | undefined + obstacles: Obstacle[] + bounds: { minX: number; minY: number; maxX: number; maxY: number } + GRID_STEP: number = 0.1 + + sectionSizeX: number + sectionSizeY: number + numSectionsX: number + numSectionsY: number + + layerCount: number + + constructor(layerCount: number, obstacles: Array) { + if (process.env.TSCIRCUIT_AUTOROUTER_PROFILING_ENABLED) { + this.profiler = globalObstacleList3dSectionalV2Profiler + } + this.layerCount = layerCount + this.obstacles = obstacles + + this.bounds = { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + } + + for (const obstacle of obstacles) { + this.bounds.minX = Math.min( + this.bounds.minX, + obstacle.center.x - obstacle.width / 2, + ) + this.bounds.minY = Math.min( + this.bounds.minY, + obstacle.center.y - obstacle.height / 2, + ) + this.bounds.maxX = Math.max( + this.bounds.maxX, + obstacle.center.x + obstacle.width / 2, + ) + this.bounds.maxY = Math.max( + this.bounds.maxY, + obstacle.center.y + obstacle.height / 2, + ) + } + + // We choose a fixed number of sections for demonstration. + // This can be dynamic or configurable if needed. + this.numSectionsX = 10 + this.numSectionsY = 10 + + this.sectionSizeX = + (this.bounds.maxX - this.bounds.minX) / this.numSectionsX + this.sectionSizeY = + (this.bounds.maxY - this.bounds.minY) / this.numSectionsY + + if (this.profiler) { + for (const methodName of [ + "getObstacleAt", + "isObstacleAt", + "getDirectionDistancesToNearestObstacle", + "getOrthoDirectionCollisionInfo", + "getObstaclesOverlappingRegion", + ]) { + // @ts-ignore + const originalMethod = this[methodName] as Function + // @ts-ignore + this[methodName as keyof ObstacleList3d] = this.profiler!.wrapMethod( + `ObstacleList3dSectional.${methodName}`, + originalMethod.bind(this), + ) + } + } + } + + /** + * Compute sections that intersect with a given region. + */ + _getSectionsOfRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + }) { + const [minSectionX, minSectionY] = this._getSectionXYForPoint( + region.minX, + region.minY, + ) + const [maxSectionX, maxSectionY] = this._getSectionXYForPoint( + region.maxX, + region.maxY, + ) + + const sections: SectionId[] = [] + for (let sectionX = minSectionX; sectionX <= maxSectionX; sectionX++) { + for (let sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + sections.push(`${sectionX},${sectionY}`) + } + } + + return sections + } + + _getSectionsOfObstacle(obstacle: Obstacle): SectionId[] { + const { + center: { x, y }, + height, + width, + } = obstacle + return this._getSectionsOfRegion({ + minX: x - width / 2, + minY: y - height / 2, + maxX: x + width / 2, + maxY: y + height / 2, + }) + } + + _getAlignedSectionsForObstacle(obstacle: Obstacle): SectionId[] { + const { + center: { x, y }, + height, + width, + } = obstacle + const alignedSections: Set = new Set() + + const [minSectionX, minSectionY] = this._getSectionXYForPoint( + x - width / 2, + y - height / 2, + ) + const [maxSectionX, maxSectionY] = this._getSectionXYForPoint( + x + width / 2, + y + height / 2, + ) + + // Vertical alignment (above/below): all sections with x between minSectionX and maxSectionX. + for (let sectionX = minSectionX; sectionX <= maxSectionX; sectionX++) { + for (let sectionY = 0; sectionY < this.numSectionsY; sectionY++) { + alignedSections.add(`${sectionX},${sectionY}`) + } + } + + // Horizontal alignment (left/right): all sections with y between minSectionY and maxSectionY. + for (let sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + for (let sectionX = 0; sectionX < this.numSectionsX; sectionX++) { + alignedSections.add(`${sectionX},${sectionY}`) + } + } + + return Array.from(alignedSections) + } + + _getSectionXYForPoint(x: number, y: number): [number, number] { + let sectionX = Math.floor((x - this.bounds.minX) / this.sectionSizeX) + let sectionY = Math.floor((y - this.bounds.minY) / this.sectionSizeY) + // clamp + sectionX = Math.max(0, Math.min(this.numSectionsX - 1, sectionX)) + sectionY = Math.max(0, Math.min(this.numSectionsY - 1, sectionY)) + + return [sectionX, sectionY] + } + + _getSectionForPoint(x: number, y: number): SectionId { + const [sectionX, sectionY] = this._getSectionXYForPoint(x, y) + return `${sectionX},${sectionY}` + } + + /** + * Lazily compute and cache the ObstacleList3d for a given section. + */ + private _getSectionObstacleList(sectionId: SectionId): ObstacleList3d { + if (!this.sectionsCache[sectionId]) { + // Compute obstacles in this section + const [sectionXStr, sectionYStr] = sectionId.split(",") + const sectionX = parseInt(sectionXStr, 10) + const sectionY = parseInt(sectionYStr, 10) + + const sectionMinX = this.bounds.minX + sectionX * this.sectionSizeX + const sectionMinY = this.bounds.minY + sectionY * this.sectionSizeY + const sectionMaxX = sectionMinX + this.sectionSizeX + const sectionMaxY = sectionMinY + this.sectionSizeY + + const obstaclesInSection = this.obstacles.filter((obstacle) => { + const oxMin = obstacle.center.x - obstacle.width / 2 + const oxMax = obstacle.center.x + obstacle.width / 2 + const oyMin = obstacle.center.y - obstacle.height / 2 + const oyMax = obstacle.center.y + obstacle.height / 2 + + // Check if obstacle intersects with the section region + return ( + oxMax >= sectionMinX && + oxMin <= sectionMaxX && + oyMax >= sectionMinY && + oyMin <= sectionMaxY + ) + }) + + this.sectionsCache[sectionId] = new ObstacleList3dF64V2( + this.layerCount, + obstaclesInSection, + ) + } + return this.sectionsCache[sectionId] + } + + /** + * Lazily compute and cache the ObstacleList3d for a given aligned section. + */ + private _getAlignedSectionObstacleList(sectionId: SectionId): ObstacleList3d { + if (!this.alignedSectionsCache[sectionId]) { + // Compute obstacles aligned with this section + const [sectionXStr, sectionYStr] = sectionId.split(",") + const sectionX = parseInt(sectionXStr, 10) + const sectionY = parseInt(sectionYStr, 10) + + // Find all obstacles that would appear in the alignedSections of this section. + // Aligned sections contain all obstacles that share x or y alignment. + // In other words, if the obstacle spans horizontally above, below, + // or vertically to the left/right of this section, it's included. + + // To find aligned obstacles, we basically consider: + // - All obstacles that would intersect at least one section with the same sectionX (vertically aligned). + // - All obstacles that would intersect at least one section with the same sectionY (horizontally aligned). + + const obstaclesInAlignedSection = this.obstacles.filter((obstacle) => { + const obstacleSections = this._getSectionsOfObstacle(obstacle) + + // If the obstacle intersects any section with the same X index or + // any section with the same Y index, it's aligned. + return obstacleSections.some((sid) => { + const [oxStr, oyStr] = sid.split(",") + const ox = parseInt(oxStr, 10) + const oy = parseInt(oyStr, 10) + return ox === sectionX || oy === sectionY + }) + }) + + this.alignedSectionsCache[sectionId] = new ObstacleList3dF64V2( + this.layerCount, + obstaclesInAlignedSection, + ) + } + return this.alignedSectionsCache[sectionId] + } + + getObstacleAt(x: number, y: number, l: number, m?: number): Obstacle | null { + const sectionId = this._getSectionForPoint(x, y) + return ( + this._getSectionObstacleList(sectionId)?.getObstacleAt(x, y, l, m) ?? null + ) + } + + isObstacleAt(x: number, y: number, l: number, m?: number): boolean { + const sectionId = this._getSectionForPoint(x, y) + return ( + this._getSectionObstacleList(sectionId)?.isObstacleAt(x, y, l, m) ?? false + ) + } + + getDirectionDistancesToNearestObstacle3d( + x: number, + y: number, + l: number, + ): DirectionDistances3d { + const sectionId = this._getSectionForPoint(x, y) + return this._getAlignedSectionObstacleList( + sectionId, + ).getDirectionDistancesToNearestObstacle3d(x, y, l) + } + + getOrthoDirectionCollisionInfo( + point: Point3d, + dir: Direction3d, + opts?: { margin?: number }, + ): DirectionWithCollisionInfo3d { + const sectionId = this._getSectionForPoint(point.x, point.y) + return this._getAlignedSectionObstacleList( + sectionId, + ).getOrthoDirectionCollisionInfo(point, dir, opts) + } + + getObstaclesOverlappingRegion(region: { + minX: number + minY: number + maxX: number + maxY: number + l: number + }): ObstacleWithEdges[] { + const sections = this._getSectionsOfRegion(region) + + const obstacles: ObstacleWithEdges[] = [] + for (const section of sections) { + obstacles.push( + ...this._getSectionObstacleList(section).getObstaclesOverlappingRegion( + region, + ), + ) + } + + // TODO: Deduplicate obstacles if necessary. + return obstacles + } + + getDirectionDistancesToNearestObstacle( + x: number, + y: number, + ): DirectionDistances { + const sectionId = this._getSectionForPoint(x, y) + return this._getAlignedSectionObstacleList( + sectionId, + ).getDirectionDistancesToNearestObstacle(x, y) + } +} diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index 3b3165a..ce05025 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -43,6 +43,7 @@ import { ObstacleList3dSectional } from "algos/common/generalized-astar/Obstacle import { ObstacleList3dF64V1 } from "algos/common/generalized-astar/ObstacleList3dF64V1" import { ObstacleList3dF64V2 } from "algos/common/generalized-astar/ObstacleList3dF64V2" import { ObstacleList3dF64V3 } from "algos/common/generalized-astar/ObstacleList3dF64V3" +import { ObstacleList3dSectionalV2 } from "algos/common/generalized-astar/ObstacleList3dSectionalV2" export class MultilayerIjump extends GeneralizedAstarAutorouter { MAX_ITERATIONS: number = 500 @@ -193,7 +194,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { ) } - // return new ObstacleList3dSectional( + // return new ObstacleList3dSectionalV2( return new ObstacleList3dF64V3( this.layerCount, this.allObstacles From 9ebaaa1d23e0a111adcce3920559a623a12f2870 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 22:16:07 +0800 Subject: [PATCH 12/16] minor enhancements to prevent recompute, focus on profiling neighbor eval --- .../GeneralizedAstarAutorouter.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index 23b3693..a91142c 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -166,8 +166,8 @@ export class GeneralizedAstarAutorouter { this._sortOpenSet() const current = openSet.shift()! - const currentKey = this.getNodeName(current) - this.openSetMap.delete(currentKey) + const currentNodeName = this.getNodeName(current) + this.openSetMap.delete(currentNodeName) const goalDist = this.computeH(current) if (goalDist <= GRID_STEP * 2) { @@ -178,21 +178,19 @@ export class GeneralizedAstarAutorouter { } } - this.closedSet.add(this.getNodeName(current)) + this.closedSet.add(currentNodeName) let newNeighbors: Node[] = [] for (const neighbor of this.getNeighbors(current)) { - if (closedSet.has(this.getNodeName(neighbor))) continue + const neighborName = this.getNodeName(neighbor) + if (closedSet.has(neighborName)) continue const tentativeG = this.computeG(current, neighbor) - this.profiler?.startMeasurement("openSetFind") - const neighborKey = this.getNodeName(neighbor) - const existingNeighbor = this.openSetMap.get(neighborKey) - - this.profiler?.endMeasurement("openSetFind") + const existingNeighbor = this.openSetMap.get(neighborName) if (!existingNeighbor || tentativeG < existingNeighbor.g) { + this.profiler?.startMeasurement("createNeighborNode") const h = this.computeH(neighbor) const f = tentativeG + h * this.GREEDY_MULTIPLIER @@ -209,6 +207,7 @@ export class GeneralizedAstarAutorouter { enterMarginCost: neighbor.enterMarginCost, travelMarginCostFactor: neighbor.travelMarginCostFactor, } + this.profiler?.endMeasurement("createNeighborNode") // Insert into openSet in sorted order by f value this.profiler?.startMeasurement("openSetInsert") From fafb7fd829cd09ad5db7b3d03e7781fe10f7b03d Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 22:29:38 +0800 Subject: [PATCH 13/16] remove openSet sort --- algos/common/generalized-astar/GeneralizedAstarAutorouter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index a91142c..171f144 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -219,7 +219,6 @@ export class GeneralizedAstarAutorouter { } if (debug.enabled) { - openSet.sort((a, b) => a.f - b.f) this.drawDebugSolution({ current, newNeighbors }) } From e906261e138e8947a6a6462c06b86d02f2bc5341 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 22:55:52 +0800 Subject: [PATCH 14/16] cache the margin option for a 5ms gain --- algos/common/generalized-astar/Profiler.ts | 8 ++++++- algos/multi-layer-ijump/MultilayerIjump.ts | 25 +++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/algos/common/generalized-astar/Profiler.ts b/algos/common/generalized-astar/Profiler.ts index f23e30a..f180bee 100644 --- a/algos/common/generalized-astar/Profiler.ts +++ b/algos/common/generalized-astar/Profiler.ts @@ -75,7 +75,13 @@ export class Profiler { averageTime: string } > = {} - for (const key in results) { + + // Sort keys by totalTime in descending order + const sortedKeys = Object.keys(results).sort( + (a, b) => results[b].totalTime - results[a].totalTime, + ) + + for (const key of sortedKeys) { prettyResults[key] = { totalTime: `${results[key].totalTime.toFixed(2)}ms`, calls: results[key].calls, diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index ce05025..b5162d6 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -67,6 +67,8 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { GOAL_RUSH_FACTOR: number = 1.1 + cachedMarginOption: { margin: number } + // TODO we need to travel far enough away from the goal so that we're not // hitting a pad, which means we need to know the bounds of the goal // The simplest way to do this is to change SimpleJsonInput to include a @@ -127,6 +129,25 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { travelCostFactor: 2, }, ] + this.cachedMarginOption = { + margin: this.OBSTACLE_MARGIN, + } + + if (this.profiler) { + for (const methodName of [ + "preprocessConnectionBeforeSolving", + "hasSpaceForVia", + "getNeighborsSurroundingGoal", + ]) { + // @ts-ignore + const originalMethod = this[methodName] as Function + // @ts-ignore + this[methodName as keyof ObstacleList] = this.profiler!.wrapMethod( + `GenerlizedAstarAutorouter.${methodName}`, + originalMethod.bind(this), + ) + } + } } preprocessConnectionBeforeSolving( @@ -360,9 +381,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { const collisionInfo = obstacles.getOrthoDirectionCollisionInfo( node, dir, - { - margin: this.OBSTACLE_MARGIN, - }, + this.cachedMarginOption, ) return collisionInfo From 0741d53ddc25e578024f7228e71077b83205551c Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 23:33:16 +0800 Subject: [PATCH 15/16] fix for duplicate node exploration --- .../generalized-astar/GeneralizedAstarAutorouter.ts | 13 +++++++++++-- algos/common/generalized-astar/ObstacleList3d.ts | 11 +++++++++++ algos/multi-layer-ijump/MultilayerIjump.ts | 7 +++++++ .../keyboard-route-test-profile.snap.svg | 2 +- .../tests/keyboard-route-test-profile.test.tsx | 2 ++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts index 171f144..4fcbf37 100644 --- a/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts +++ b/algos/common/generalized-astar/GeneralizedAstarAutorouter.ts @@ -165,8 +165,14 @@ export class GeneralizedAstarAutorouter { const { openSet, closedSet, GRID_STEP, goalPoint } = this this._sortOpenSet() - const current = openSet.shift()! - const currentNodeName = this.getNodeName(current) + let current = openSet.shift()! + let currentNodeName = this.getNodeName(current) + + while (closedSet.has(currentNodeName)) { + current = openSet.shift()! + currentNodeName = this.getNodeName(current) + } + this.openSetMap.delete(currentNodeName) const goalDist = this.computeH(current) @@ -211,6 +217,9 @@ export class GeneralizedAstarAutorouter { // Insert into openSet in sorted order by f value this.profiler?.startMeasurement("openSetInsert") + if (this.openSetMap.has(neighborName)) { + throw new Error("Open set already has neighbor node") + } this._binarySearchOpenSetInsert(neighborNode) this.profiler?.endMeasurement("openSetInsert") diff --git a/algos/common/generalized-astar/ObstacleList3d.ts b/algos/common/generalized-astar/ObstacleList3d.ts index 5b23951..71d8a4c 100644 --- a/algos/common/generalized-astar/ObstacleList3d.ts +++ b/algos/common/generalized-astar/ObstacleList3d.ts @@ -106,6 +106,7 @@ export class ObstacleList3d extends ObstacleList { return result } + // cases: any = {} getOrthoDirectionCollisionInfo( point: Point3d, dir: Direction3d, @@ -113,6 +114,16 @@ export class ObstacleList3d extends ObstacleList { ): DirectionWithCollisionInfo3d { const { x, y, l } = point const { dx, dy, dl } = dir + // const xHash = [l, x.toFixed(3), dy, dl].join(",") + // const yHash = [l, y.toFixed(3), dx, dl].join(",") + // if (dy !== 0) { + // this.cases[xHash] ??= [] + // this.cases[xHash].push(y) + // } + // if (dx !== 0) { + // this.cases[yHash] ??= [] + // this.cases[yHash].push(x) + // } let minDistance = Infinity let collisionObstacle: ObstacleWithEdges | null = null diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index b5162d6..b9d2186 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -217,6 +217,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { // return new ObstacleList3dSectionalV2( return new ObstacleList3dF64V3( + // return new ObstacleList3d( this.layerCount, this.allObstacles .filter((obstacle) => !obstacle.connectedTo.includes(bestConnectionId)) @@ -329,6 +330,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { forwardDir = dirFromAToB(node.parent, node) } + this.profiler?.startMeasurement("getNeighbors.travelDirs1") /** * Get the possible next directions (excluding backwards direction), and * excluding the forward direction if we just ran into a wall @@ -357,7 +359,9 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { travelDirs1.push({ dx: 0, dy: 0, dl: -1 }) } } + this.profiler?.endMeasurement("getNeighbors.travelDirs1") + this.profiler?.startMeasurement("getNeighbors.travelDirs2") const travelDirs2 = travelDirs1 .filter((dir) => { // If we have a parent, don't go backwards towards the parent @@ -388,7 +392,9 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { }) // Filter out directions that are too close to the wall .filter((dir) => !(dir.wallDistance < this.OBSTACLE_MARGIN)) + this.profiler?.endMeasurement("getNeighbors.travelDirs2") + this.profiler?.startMeasurement("getNeighbors.travelDirs3") /** * Figure out how far to travel. There are a couple reasons we would stop * traveling: @@ -541,6 +547,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { } } } + this.profiler?.endMeasurement("getNeighbors.travelDirs3") return travelDirs3.map((dir) => ({ x: node.x + dir.dx * dir.travelDistance, diff --git a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg index f49883d..d81b6f3 100644 --- a/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg +++ b/algos/seveibar/seveibar1/tests/__snapshots__/keyboard-route-test-profile.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file diff --git a/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx index 509f1ea..f181f65 100644 --- a/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx +++ b/algos/seveibar/seveibar1/tests/keyboard-route-test-profile.test.tsx @@ -40,6 +40,8 @@ test("multi-layer ijump keyboard", async () => { console.table(autorouter.obstacles.profiler!.getResultsPretty()) console.table(autorouter.profiler!.getResultsPretty()) + // console.log(autorouter.obstacles.cases) + expect( convertCircuitJsonToPcbSvg(soup.concat(result as any) as any), ).toMatchSvgSnapshot(import.meta.path) From 022d6eb1b8e06bf73d548adaace94aa8de67ddd8 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 8 Dec 2024 23:39:24 +0800 Subject: [PATCH 16/16] distAlongDir optimization --- algos/infinite-grid-ijump-astar/v2/lib/util.ts | 5 +---- algos/multi-layer-ijump/MultilayerIjump.ts | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/algos/infinite-grid-ijump-astar/v2/lib/util.ts b/algos/infinite-grid-ijump-astar/v2/lib/util.ts index 766eb26..43e454f 100644 --- a/algos/infinite-grid-ijump-astar/v2/lib/util.ts +++ b/algos/infinite-grid-ijump-astar/v2/lib/util.ts @@ -24,10 +24,7 @@ export const dirFromAToB = (a: Point, b: Point): { dx: number; dy: number } => { } export const distAlongDir = (A: Point, B: Point, dir: Direction): number => { - return ( - Math.abs(A.x - B.x) * Math.abs(dir.dx) + - Math.abs(A.y - B.y) * Math.abs(dir.dy) - ) + return Math.abs((A.x - B.x) * dir.dx) + Math.abs((A.y - B.y) * dir.dy) } export const nodeName = (node: Point, GRID_STEP: number = 0.1): string => diff --git a/algos/multi-layer-ijump/MultilayerIjump.ts b/algos/multi-layer-ijump/MultilayerIjump.ts index b9d2186..c3efb7b 100644 --- a/algos/multi-layer-ijump/MultilayerIjump.ts +++ b/algos/multi-layer-ijump/MultilayerIjump.ts @@ -455,6 +455,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { let overcomeDistance: number | null = null if (node?.obstacleHit) { + this.profiler?.startMeasurement("getNeighbors.overcomeDistance") overcomeDistance = getDistanceToOvercomeObstacle({ node, travelDir, @@ -464,6 +465,7 @@ export class MultilayerIjump extends GeneralizedAstarAutorouter { OBSTACLE_MARGIN: this.OBSTACLE_MARGIN, SHOULD_DETECT_CONJOINED_OBSTACLES: true, }) + this.profiler?.endMeasurement("getNeighbors.overcomeDistance") } const goalDistAlongTravelDir = distAlongDir(node, goalPoint, travelDir)