Fix core geometry snapping, A* target lookahead, and test configurations
42
docs/plans/cost_and_collision_engine.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Cost and Collision Engine Spec
|
||||
|
||||
This document describes the methods for ensuring "analytic correctness" while maintaining a computationally efficient cost function.
|
||||
|
||||
## 1. Analytic Correctness
|
||||
The router balances speed and verification using a two-tier approach.
|
||||
|
||||
### 1.1. R-Tree Geometry Engine
|
||||
The router uses an **R-Tree of Polygons** for all geometric queries.
|
||||
* **Move Validation:** Every "Move" proposed by the search is checked for intersection against the R-Tree.
|
||||
* **Pre-dilation for Clearance:** All obstacles and paths use a global clearance $C$. At the start of the session, all user-provided obstacles are **pre-dilated by $(W_{max} + C)/2$** for initial broad pruning. However, individual path dilation for intersection tests uses $(W_i + C)/2$ for a specific net's width $W_i$.
|
||||
* **Safety Zone:** To prevent immediate collision flags for ports placed on or near obstacle boundaries, the router ignores collisions within a radius of **2nm** of start and end ports.
|
||||
* **Single Layer:** All routing and collision detection occur on a single layer.
|
||||
|
||||
### 1.2. Cost Calculation (Soft Constraint)
|
||||
The "Danger Cost" $g_{proximity}(n)$ is a function of the distance $d$ to the nearest obstacle:
|
||||
$$g_{proximity}(n) = \begin{cases} \infty & \text{if } d < (W_i + C)/2 \\ \frac{k}{d^2} & \text{if } (W_i + C)/2 \le d < \text{Safety Threshold} \\ 0 & \text{if } d \ge \text{Safety Threshold} \end{cases}$$
|
||||
|
||||
To optimize A* search, a **Static Danger Map (Precomputed Grid)** is used for the heuristic.
|
||||
* **Grid Resolution:** Default **1000nm (1µm)**.
|
||||
* **Static Nature:** The grid only accounts for fixed obstacles. It is computed once at the start of the session and is **not re-computed** during the Negotiated Congestion loop.
|
||||
* **Efficiency:** For a 20x20mm layout, this results in a 20k x 20k matrix.
|
||||
* **Memory:** Using a `uint8` or `float16` representation, this consumes ~400-800MB (Default < 2GB). For extremely high resolution or larger areas, the system supports up to **20GB** allocation.
|
||||
* **Precision:** Strict intersection checks still use the R-Tree for "analytic correctness."
|
||||
|
||||
## 2. Collision Detection Implementation
|
||||
The system relies on `shapely` for geometry and `rtree` for spatial indexing.
|
||||
1. **Arc Resolution:** Arcs are represented as polygons approximated by segments with a maximum deviation (sagitta).
|
||||
2. **Intersection Test:** A "Move" is valid only if its geometry does not intersect any obstacle in the R-Tree.
|
||||
3. **Self-Intersection:** Paths from the same net must not intersect themselves.
|
||||
4. **No Crossings:** Strictly 2D; no crossings (vias or bridges) are supported.
|
||||
|
||||
## 3. Negotiated Congestion (Path R-Tree)
|
||||
To handle multiple nets, the router maintains a separate R-Tree containing the dilated geometries ($C/2$) of all currently routed paths.
|
||||
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
|
||||
* **Failure Policy:** If no collision-free path is found after the max iterations, the router returns the **"least-bad" (lowest cost)** path. These paths MUST be explicitly flagged as invalid (e.g., via an `is_valid=False` attribute or a non-zero `collision_count`) so the user can identify and manually fix the failure.
|
||||
|
||||
## 4. Handling Global Clearances
|
||||
Clearances are global. Both obstacles and paths are pre-dilated once. This ensures that any two objects maintain at least $C$ distance if their dilated versions do not intersect.
|
||||
|
||||
## 5. Locked Paths
|
||||
The router supports **Locked Paths**—existing geometries inserted into the static Obstacle R-Tree, ensuring they are never modified or rerouted.
|
||||
37
docs/plans/geometric_representation.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Geometric Representation and Data Structures
|
||||
|
||||
This document defines the core data structures for representing the routing problem.
|
||||
|
||||
## 1. Port Definitions (Connectivity)
|
||||
Routing requests are defined as a mapping of source ports to destination ports.
|
||||
|
||||
* **Port Structure:** `(x, y, orientation)`
|
||||
* `x, y`: Coordinates snapped to **1nm** grid.
|
||||
* `orientation`: Strictly $\{0, 90, 180, 270\}$ degrees.
|
||||
* **Netlist:** A dictionary or list of tuples: `{(start_port): (end_port)}`. Each entry represents a single-layer path that must be routed.
|
||||
* **Heterogeneous Widths:** The router supports different widths $W_i$ per net. Dilation for a specific net $i$ is calculated as $(W_i + C)/2$ to maintain the global clearance $C$.
|
||||
|
||||
## 2. Component Library (Move Generator)
|
||||
The router uses a discrete set of components to expand states in the A* search.
|
||||
|
||||
### 2.1. Straight Waveguides
|
||||
* **A* Expansion:** Generates "Straight" moves of varying lengths (e.g., $1\mu m, 5\mu m, 25\mu m$).
|
||||
* **Snap-to-Target:** If the destination port $T$ is directly ahead and the current state's orientation matches $T$'s entry orientation, a special move is generated to close the gap exactly.
|
||||
|
||||
### 2.2. Fixed 90° Bends (PDK Cells)
|
||||
* **Parameters:** `radius`, `width`.
|
||||
* **A* Expansion:** A discrete move that changes orientation by $\pm 90^\circ$ and shifts the coordinate by the radius.
|
||||
* **Grid Alignment:** If the bend radius $R$ is not a multiple of the search grid (default $1\mu m$), the resulting state is **snapped to the nearest grid point**, and a warning is issued to the user.
|
||||
|
||||
### 2.3. Parametric S-Bends (Compact)
|
||||
* **Parameters:** `radius`, `width`.
|
||||
* **A* Expansion:** Used ONLY for lateral offsets $O < 2R$.
|
||||
* **Large Offsets ($O \ge 2R$):** The router does not use a single S-bend move for large offsets. Instead, the A* search naturally finds the optimal path by combining two 90° bends and a straight segment. This ensures maximum flexibility in obstacle avoidance for large shifts.
|
||||
|
||||
## 3. Obstacle Representation
|
||||
Obstacles are provided as raw polygons on a single layer.
|
||||
|
||||
* **Pre-processing:** All input polygons are inserted into an **R-Tree**.
|
||||
* **Buffer/Dilation:** Obstacles are pre-dilated by $(W_{max} + Clearance)/2$ for initial pruning, but final collision tests use the net-specific width $W_i$.
|
||||
* **No Multi-layer:** The router assumes all obstacles and paths share the same plane.
|
||||
* **Safety Zone:** Ignore collisions within **2nm** of start and end ports for robustness.
|
||||
13
docs/plans/high_level_notes.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# High-Level Notes: Auto-Routing for Integrated Circuits
|
||||
|
||||
We're implementing auto-routing for photonic and RF integrated circuits. The problem space has the following features:
|
||||
|
||||
- **Single Layer:** All paths are on a single layer; multi-layer routing is not supported.
|
||||
- **No Crossings:** Crossings are to be avoided and are not supported by the router's automatic placement. The user must manually handle any required crossings.
|
||||
- **Large Bend Radii:** Bends use large radii ($R$), and are usually pre-generated (e.g. 90-degree cells).
|
||||
- **Proximity Sensitivity:** Paths are sensitive to proximity to other paths and obstacles (crosstalk/coupling).
|
||||
- **Manhattan Preference:** Manhattan pathing is sufficient for most cases; any-angle is rare.
|
||||
- **S-Bends:** S-bends are necessary for small lateral offsets ($O < 2R$).
|
||||
- **Hybrid Search:** A* state-lattice search is used for discrete component placement.
|
||||
- **Dilation:** A $Clearance/2$ dilation is applied to all obstacles and paths for efficient collision avoidance.
|
||||
- **Negotiated Congestion:** A multi-net "PathFinder" loop iteratively reroutes nets to resolve congestion.
|
||||
64
docs/plans/implementation_plan.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Implementation Plan
|
||||
|
||||
This plan outlines the step-by-step implementation of the `inire` auto-router. For detailed test cases, refer to [Testing Plan](./testing_plan.md).
|
||||
|
||||
## Phase 1: Core Geometry & Move Generation
|
||||
**Goal:** Implement Ports, Polygons, and Component Library with high geometric fidelity.
|
||||
1. **Project Setup:** Initialize `inire/` structure and `pytest` configuration. Include `hypothesis` for property-based testing.
|
||||
2. **`geometry.primitives`:**
|
||||
* `Port` with **1nm** snapping.
|
||||
* Basic 2D transformations (rotate, translate).
|
||||
* **Property-Based Tests:** Verify transform invariants (e.g., $90^\circ$ rotation cycles).
|
||||
3. **`geometry.components`:**
|
||||
* `Straight`, `Bend90`, `SBend`.
|
||||
* **Search Grid Snapping:** Implement 1µm snapping for expanded ports.
|
||||
* **Small S-Bends ($O < 2R$):** Logic for parametric generation.
|
||||
* **Edge Cases:** Handle $O=2R$ and $L < 1\mu m$.
|
||||
4. **Tests:**
|
||||
* Verify geometric correctness (refer to Testing Plan Section 1).
|
||||
* Unit tests for `Port` snapping and component transformations.
|
||||
|
||||
## Phase 2: Collision Engine & Cost
|
||||
**Goal:** Build the R-Tree wrapper and the analytic cost function.
|
||||
1. **`geometry.collision`:** Implement `CollisionEngine`.
|
||||
* **Pre-dilation:** Obstacles/Paths dilated by $Clearance/2$.
|
||||
* **Safety Zone:** Ignore collisions within **2nm** of start/end ports.
|
||||
2. **`router.danger_map`:**
|
||||
* Implement **1µm** pre-computed proximity grid.
|
||||
* Optimize for design sizes up to **20x20mm** (< 2GB memory).
|
||||
3. **`router.cost`:** Implement `CostEvaluator`.
|
||||
* Bend cost: $10 \times (\text{Manhattan distance between ports})$.
|
||||
* Integrate R-Tree for strict checks and Danger Map for heuristic.
|
||||
4. **Tests:**
|
||||
* Verify collision detection with simple overlapping shapes (Testing Plan Section 2.1).
|
||||
* Verify Danger Map accuracy and memory footprint (Testing Plan Section 2.2).
|
||||
* **Post-Route Validator:** Implement the independent `validate_path` utility.
|
||||
|
||||
## Phase 3: Single-Net A* Search
|
||||
**Goal:** Route a single net from A to B with 1nm precision.
|
||||
1. **`router.astar`:** Implement the priority queue loop.
|
||||
* State representation: `(x_µm, y_µm, theta)`.
|
||||
* Move expansion loop with 1µm grid.
|
||||
* **Natural S-Bends:** Ensure search can find $O \ge 2R$ shifts by combining moves.
|
||||
* **Look-ahead Snapping:** Actively bridge to the 1nm target when in the capture radius (10µm).
|
||||
2. **Heuristic:** Manhattan distance $h(n)$ + orientation penalty + Danger Map lookup.
|
||||
3. **Tests:**
|
||||
* Solve simple maze problems and verify path optimality (Testing Plan Section 3).
|
||||
* Verify snap-to-target precision at 1nm resolution.
|
||||
* **Determinism:** Verify same seed = same path.
|
||||
|
||||
## Phase 4: Multi-Net PathFinder
|
||||
**Goal:** Implement the "Negotiated Congestion" loop for multiple nets.
|
||||
1. **`router.pathfinder`:**
|
||||
* Sequential routing -> Identify congestion -> Inflate cost -> Reroute.
|
||||
* **R-Tree Congestion:** Store dilated path geometries.
|
||||
2. **Explicit Results:** Return `RoutingResult` objects with `is_valid` and `collisions` metadata.
|
||||
3. **Tests:**
|
||||
* Full multi-net benchmarks (Testing Plan Section 4).
|
||||
* Verify rerouting behavior in crowded environments.
|
||||
|
||||
## Phase 5: Visualization, Benchmarking & Fuzzing
|
||||
1. **`utils.visualization`:** Plot paths using `matplotlib`. Highlight collisions in red.
|
||||
2. **Benchmarks:** Stress test with 50+ nets. Verify performance and node limits (Testing Plan Section 5).
|
||||
3. **Fuzzing:** Run A* on randomized layouts to ensure stability.
|
||||
4. **Final Validation:** Ensure all `is_valid=True` results pass the independent `validate_path` check.
|
||||
57
docs/plans/package_structure.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Python Package Structure
|
||||
|
||||
This document outlines the directory structure and module organization for the `inire` auto-router package.
|
||||
|
||||
## 1. Directory Layout
|
||||
|
||||
```
|
||||
inire/
|
||||
├── __init__.py # Exposes the main `Router` class and key types
|
||||
├── geometry/ # Core geometric primitives and operations
|
||||
│ ├── __init__.py
|
||||
│ ├── primitives.py # Point, Port, Polygon, Arc classes
|
||||
│ ├── collision.py # R-Tree wrapper and intersection logic
|
||||
│ └── components.py # Move generators (Straight, Bend90, SBend)
|
||||
├── router/ # Search algorithms and pathfinding
|
||||
│ ├── __init__.py
|
||||
│ ├── astar.py # Hybrid State-Lattice A* implementation
|
||||
│ ├── graph.py # Node, Edge, and Graph data structures
|
||||
│ ├── cost.py # Cost functions (length, bend, proximity)
|
||||
│ ├── danger_map.py # Pre-computed grid for heuristic proximity costs
|
||||
│ └── pathfinder.py # Multi-net "Negotiated Congestion" manager
|
||||
├── utils/ # Utility functions
|
||||
│ ├── __init__.py
|
||||
│ └── visualization.py # Plotting tools for debug/heatmaps (matplotlib/klayout)
|
||||
└── tests/ # Unit and integration tests
|
||||
├── __init__.py
|
||||
├── conftest.py # Pytest fixtures (common shapes, PDK cells)
|
||||
├── test_primitives.py # Tests for Port and coordinate transforms
|
||||
├── test_components.py # Tests for Straight, Bend90, SBend generation
|
||||
├── test_collision.py # Tests for R-Tree and dilation logic
|
||||
├── test_cost.py # Tests for Danger Map and cost evaluation
|
||||
├── test_astar.py # Tests for single-net routing (mazes, snapping)
|
||||
└── test_pathfinder.py # Multi-net "Negotiated Congestion" benchmarks
|
||||
```
|
||||
|
||||
## 2. Module Responsibilities
|
||||
|
||||
### `inire.geometry`
|
||||
* **`primitives.py`**: Defines the `Port` named tuple `(x, y, theta)` and helper functions for coordinate transforms.
|
||||
* **`collision.py`**: Wraps the `rtree` or `shapely` library. Handles the "Analytic Correctness" checks (exact polygon distance).
|
||||
* **`components.py`**: Logic to generate "Moves" from a start port. E.g., `SBend.generate(start_port, offset, radius)` returns a list of polygons and the end port. Handles $O > 2R$ logic.
|
||||
|
||||
### `inire.router`
|
||||
* **`astar.py`**: The heavy lifter. Maintains the `OpenSet` (priority queue) and `ClosedSet`. Implements the "Snap-to-Target" logic.
|
||||
* **`cost.py`**: compute $f(n) = g(n) + h(n)$. encapsulates the "Danger Map" and Path R-Tree lookups.
|
||||
* **`danger_map.py`**: Manages the pre-computed proximity grid used for $O(1)$ heuristic calculations.
|
||||
* **`pathfinder.py`**: Orchestrates the multi-net loop. Tracks the Path R-Tree for negotiated congestion and triggers reroutes.
|
||||
|
||||
### `inire.tests`
|
||||
* **Structure:** Tests are co-located within the package for ease of access.
|
||||
* **Fixtures:** `conftest.py` will provide standard PDK cells (e.g., a $10\mu m$ radius bend) to avoid repetition in test cases.
|
||||
|
||||
## 3. Dependencies
|
||||
* `numpy`: Vector math.
|
||||
* `shapely`: Polygon geometry and intersection.
|
||||
* `rtree`: Spatial indexing.
|
||||
* `networkx` (Optional): Not used for core search to ensure performance.
|
||||
67
docs/plans/routing_architecture_decision.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Architecture Decision Record: Auto-Routing for Photonic & RF ICs
|
||||
|
||||
## 1. Problem Context
|
||||
Photonic and RF routing differ significantly from digital VLSI due to physical constraints:
|
||||
* **Geometric Rigidity:** 90° bends are pre-rendered PDK cells with fixed bounding boxes.
|
||||
* **Parametric Flexibility:** S-bends must be generated on-the-fly to handle arbitrary offsets, provided they maintain a constant radius $R$.
|
||||
* **Signal Integrity:** High sensitivity to proximity (coupling/crosstalk) and a strong preference for single-layer, non-crossing paths.
|
||||
* **Manual Intervention:** The router is strictly 2D and avoids all other geometries on the same layer. No crossings (e.g. vias or bridges) are supported by the automatic routing engine. The user must manually handle any required crossings by placing components (e.g. crossing cells) and splitting the net list accordingly. This simplifies the router's task to 2D obstacle avoidance and spacing optimization.
|
||||
|
||||
---
|
||||
|
||||
## 2. Candidate Algorithms & Tradeoffs
|
||||
|
||||
### 2.1. Rubber-Band (Topological) Routing
|
||||
This approach treats paths as elastic bands stretched around obstacles, later "inflating" them to have width and curvature.
|
||||
|
||||
| Feature | Analysis |
|
||||
| :--- | :--- |
|
||||
| **Strengths** | Excellent at "River Routing" and maintaining minimum clearances. Inherently avoids crossings. |
|
||||
| **Downsides** | **The Inflation Gap:** A valid thin-line topology may be physically un-routable once the large radius $R$ is applied. It struggles to integrate rigid, pre-rendered 90° blocks into a continuous elastic model. |
|
||||
| **Future Potential** | High, if a "Post-Processing" engine can reliably snap elastic curves to discrete PDK cells without breaking connectivity. |
|
||||
|
||||
### 2.2. Voronoi-Based (Medial Axis) Routing
|
||||
Uses a Voronoi diagram to find paths that are maximally distant from all obstacles.
|
||||
|
||||
| Feature | Analysis |
|
||||
| :--- | :--- |
|
||||
| **Strengths** | Theoretically optimal for "Safety" and crosstalk reduction. Guaranteed maximum clearance. |
|
||||
| **Downsides** | **Manhattan Incompatibility:** Voronoi edges are any-angle and often jagged. Mapping these to a Manhattan-heavy PDK (90° bends) requires a lossy "snapping" phase that often violates the very safety the algorithm intended to provide. |
|
||||
| **Future Potential** | Useful as a "Channel Finder" to guide a more rigid router, but unsuitable as a standalone geometric solver. |
|
||||
|
||||
### 2.3. Integer Linear Programming (ILP)
|
||||
Formulates routing as a massive optimization problem where a solver picks the best path from a pool of candidates.
|
||||
|
||||
| Feature | Analysis |
|
||||
| :--- | :--- |
|
||||
| **Strengths** | Can find the mathematically "best" layout (e.g., minimum total length or total bends) across all nets simultaneously. |
|
||||
| **Downsides** | **Candidate Explosion:** Because S-bends are generated on-the-fly, the number of possible candidate shapes is infinite. To use ILP, one must "discretize" the search space, which may miss the one specific geometry needed for a tight fit. |
|
||||
| **Future Potential** | Effective for small, high-congestion "Switchbox" areas where all possible geometries can be pre-tabulated. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Selected Approach: Hybrid State-Lattice A*
|
||||
|
||||
### 3.1. Rationale
|
||||
The **State-Lattice** variant of the A* algorithm is selected as the primary routing engine. Unlike standard A* which moves between grid cells, State-Lattice moves between **states** defined as $(x, y, \theta)$.
|
||||
|
||||
1. **Native PDK Integration:** The router treats the pre-rendered 90° bend cell as a discrete "Move" in the search tree. The algorithm only considers placing a bend if the cell's bounding box is clear of obstacles.
|
||||
2. **Parametric S-Bends:** When the search needs to bridge a lateral gap, it triggers a "Procedural Move." It calculates a fixed-radius S-bend on-the-fly. If the resulting arc is valid and collision-free, it is added as an edge in the search graph.
|
||||
3. **Predictable Costing:** It allows for a sophisticated cost function $f(n) = g(n) + h(n)$ where:
|
||||
* $g(n)$ penalizes path length and proximity to obstacles (using a distance-transform field).
|
||||
* $h(n)$ (the heuristic) guides the search toward the destination while favoring Manhattan alignments.
|
||||
|
||||
### 3.2. Implementation Strategy
|
||||
* **Step 1: Distance Transform.** Pre-calculate a "Danger Map" of the layout. Cells close to obstacles have a high cost; cells far away have low cost. This handles the **Proximity Sensitivity** constraint.
|
||||
* **Step 2: State Expansion.** From the current point, explore:
|
||||
* `Straight(length)`
|
||||
* `PDK_Bend_90(direction)`
|
||||
* `S_Bend(target_offset, R)`
|
||||
* **Step 3: Rip-up and Reroute.** To handle the sequential nature of A*, implement a "Negotiated Congestion" scheme (PathFinder algorithm) where nets "pay" more to occupy areas that other nets also want.
|
||||
|
||||
---
|
||||
|
||||
## 4. Summary of Tradeoffs for Future Review
|
||||
* **Why not pure Manhattan?** Photonic/RF requirements for large $R$ and S-bends make standard grid-based maze routers obsolete.
|
||||
* **Why not any-angle?** While any-angle is possible, the PDK's reliance on pre-rendered 90° blocks makes a lattice-based approach more manufacturing-stable.
|
||||
* **Risk:** The primary risk is **Search Time**. As the library of moves grows (more S-bend options), the branching factor increases. This must be managed with aggressive pruning and spatial indexing (e.g., R-trees).
|
||||
66
docs/plans/routing_search_spec.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Routing Search Specification
|
||||
|
||||
This document details the Hybrid State-Lattice A* implementation and the "PathFinder" (Negotiated Congestion) algorithm for multi-net routing.
|
||||
|
||||
## 1. Hybrid State-Lattice A* Search
|
||||
The core router operates on states $S = (x, y, \theta)$, where $\theta \in \{0, 90, 180, 270\}$.
|
||||
|
||||
### 1.1. State Representation & Snapping
|
||||
To ensure search stability and hash-map performance:
|
||||
* **Intermediate Ports:** Every state expanded by A* is snapped to a search grid.
|
||||
* **Search Grid:** Default snap is **1000nm (1µm)**.
|
||||
* **Final Alignment:** The "Snap-to-Target" logic bridges the gap from the coarse search grid to the final **1nm** port coordinates.
|
||||
* **Max Design Size:** Guidelines for memory/performance assume up to **20mm x 20mm** routing area.
|
||||
|
||||
### 1.2. Move Expansion Logic
|
||||
From state $S_n$, the following moves are evaluated:
|
||||
1. **Tiered Straight Steps:** Expand by a set of distances $L \in \{1\mu m, 5\mu m, 25\mu m\}$.
|
||||
2. **Snap-to-Target:** A "last-inch" look-ahead move. If the target $T$ is within a **Capture Radius (Default: 10µm)** and a straight segment or single bend can reach it, a special move is generated to close the gap exactly at 1nm resolution.
|
||||
3. **90° Bend:** Try clockwise/counter-clockwise turns using fixed PDK cells.
|
||||
4. **Small-Offset S-Bend:**
|
||||
* **Only for $O < 2R$:** Parametric S-bend (two tangent arcs).
|
||||
* **$O \ge 2R$:** Search naturally finds these by combining 90° bends and straight segments.
|
||||
|
||||
### 1.3. Cost Function $f(n) = g(n) + h(n)$
|
||||
The search uses a flexible, component-based cost model.
|
||||
|
||||
* **$g(n)$ (Actual Cost):** $\sum \text{ComponentCost}_i + \text{ProximityCost} + \text{CongestionCost}$
|
||||
* **Straight Segment:** $L \times C_{unit\_length}$.
|
||||
* **90° Bend:** $10 \times (\text{Manhattan distance between ports})$.
|
||||
* **S-Bend:** $f(O, R)$.
|
||||
* **Proximity Cost:** $k/d^2$ penalty (strict checks use R-Tree).
|
||||
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
|
||||
* **$h(n)$ (Heuristic):**
|
||||
* Manhattan distance $L_1$ to the target.
|
||||
* Orientation Penalty: High cost if the state's orientation doesn't match the target's entry orientation.
|
||||
* **Greedy Weighting:** The A* search uses a weighted heuristic (e.g., $1.1 \times h(n)$) to prioritize search speed over strict path optimality.
|
||||
* **Danger Map Heuristic:** Fast lookups from the **1µm** pre-computed proximity grid.
|
||||
|
||||
## 2. Multi-Net "PathFinder" Strategy (Negotiated Congestion)
|
||||
1. **Iteration:** Identify "Congestion Areas" using Path R-Tree intersections.
|
||||
2. **Inflation:** Increase penalty multiplier $P$ for congested areas.
|
||||
3. **Repeat:** Continue until no overlaps exist or the max iteration count is reached (Default: **20 iterations**).
|
||||
|
||||
### 2.1. Convergence & Result Policy
|
||||
* **Least Bad Attempt:** If no 100% collision-free solution is found, return the result with the lowest total cost (including overlaps).
|
||||
* **Explicit Reporting:** Results MUST include a `RoutingResult` object containing:
|
||||
* `path_geometry`: The actual polygon sequence.
|
||||
* `is_valid`: Boolean (True only if no collisions).
|
||||
* `collisions`: A count or list of detected overlap points/polygons.
|
||||
* **Visualization:** Overlapping regions are highlighted (e.g., in red) in the heatmaps.
|
||||
|
||||
## 3. Search Limits & Scaling
|
||||
* **Node Limit:** A* search is capped at **50,000 nodes** per net per iteration.
|
||||
* **Dynamic Timeout:** Session-level timeout based on problem size:
|
||||
* `Timeout = max(2s, 0.05s * num_nets * num_iterations)`.
|
||||
* *Example:* A 100-net problem over 20 iterations times out at **100 seconds**.
|
||||
|
||||
## 4. Determinism
|
||||
All search and rip-up operations are strictly deterministic.
|
||||
* **Seed:** A user-provided `seed` (int) MUST be used to initialize any random number generators (e.g., if used for tie-breaking or initial net ordering).
|
||||
* **Tie-Breaking:** If two nodes have the same $f(n)$, a consistent tie-breaking rule (e.g., based on node insertion order or state hash) must be used.
|
||||
|
||||
## 5. Optimizations
|
||||
* **A* Pruning:** Head toward the target and prune suboptimal orientations.
|
||||
* **Safety Zones:** Ignore collisions within **2nm** of start/end ports.
|
||||
* **Spatial Indexing:** R-Tree queries are limited to the bounding box of the current move.
|
||||
138
docs/plans/testing_plan.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Testing Plan (Refined)
|
||||
|
||||
This document defines the comprehensive testing strategy for the `inire` auto-router, ensuring analytic correctness and performance.
|
||||
|
||||
## 1. Unit Tests: Geometry & Components (`inire/geometry`)
|
||||
|
||||
### 1.1. Primitives (`primitives.py`)
|
||||
* **Port Snapping:** Verify that `Port(x, y, orientation)` snaps `x` and `y` to the nearest **1nm** grid point.
|
||||
* **Coordinate Transforms:**
|
||||
* Translate a port by `(dx, dy)`.
|
||||
* Rotate a port by `90`, `180`, `270` degrees around an origin.
|
||||
* Verify orientation wrapping (e.g., $270^\circ + 90^\circ \to 0^\circ$).
|
||||
* **Polygon Creation:**
|
||||
* Generate a polygon from a list of points.
|
||||
* Verify bounding box calculation.
|
||||
* **Property-Based Testing (Hypothesis):**
|
||||
* Verify that any `Port` after transformation remains snapped to the **1nm** grid.
|
||||
* Verify that $Rotate(Rotate(Port, 90), -90)$ returns the original `Port` (up to snapping).
|
||||
|
||||
### 1.2. Component Generation (`components.py`)
|
||||
* **Straight Moves:**
|
||||
* Generate a `Straight(length=10.0, width=2.0)` segment.
|
||||
* Verify the end port is exactly `length` away in the correct orientation.
|
||||
* Verify the resulting polygon's dimensions.
|
||||
* **Edge Case:** $L < 1\mu m$ (below search grid). Verify it still generates a valid 1nm segment.
|
||||
* **Bend90:**
|
||||
* Generate a `Bend90(radius=10.0, width=2.0, direction='CW')`.
|
||||
* Verify the end port's orientation is changed by $-90^\circ$.
|
||||
* Verify the end port's position is exactly `(R, -R)` relative to the start (for $0^\circ \to 270^\circ$).
|
||||
* **Grid Snapping:** Verify that if the bend radius results in a non-1µm aligned port, it is snapped to the nearest **1µm** search grid point (with a warning).
|
||||
* **SBend (Small Offset $O < 2R$):**
|
||||
* Generate an `SBend(offset=5.0, radius=10.0, width=2.0)`.
|
||||
* Verify the total length matches the analytical $L = 2\sqrt{O(2R - O/4)}$ (or equivalent arc-based formula).
|
||||
* Verify the tangent continuity at the junction of the two arcs.
|
||||
* **Edge Case:** $O = 2R$. Verify it either generates two 90-degree bends or fails gracefully with a clear error.
|
||||
* Verify it fails/warns if $O > 2R$.
|
||||
|
||||
### 1.3. Geometric Fidelity
|
||||
* **Arc Resolution (Sagitta):**
|
||||
* Verify that `Bend90` and `SBend` polygons are approximated by segments such that the maximum deviation (sagitta) is within a user-defined tolerance (e.g., 10nm).
|
||||
* Test with varying radii to ensure segment count scales appropriately.
|
||||
|
||||
## 2. Unit Tests: Collision & Cost (`inire/geometry/collision` & `router/cost`)
|
||||
|
||||
### 2.1. Collision Engine
|
||||
* **Pre-dilation Logic:**
|
||||
* Verify that an obstacle (polygon) is correctly dilated by $(W_{max} + C)/2$.
|
||||
* **Heterogeneous Widths:** Verify that a path for Net A (width $W_1$) is dilated by $(W_1 + C)/2$, while Net B (width $W_2$) uses $(W_2 + C)/2$.
|
||||
* **Locked Paths:**
|
||||
* Insert an existing path geometry into the "Static Obstacle" R-Tree.
|
||||
* Verify that the router treats it as an unmovable obstacle and avoids it.
|
||||
* **R-Tree Queries:**
|
||||
* Test intersection detection between two overlapping polygons.
|
||||
* Test non-intersection between adjacent but non-overlapping polygons (exactly $C$ distance apart).
|
||||
* **Safety Zone (2nm):**
|
||||
* Create a port exactly on the edge of an obstacle.
|
||||
* Verify that a "Move" starting from this port is NOT flagged for collision if the intersection occurs within **2nm** of the port.
|
||||
* **Self-Intersection:**
|
||||
* Verify that a path consisting of multiple segments is flagged if it loops back on itself.
|
||||
|
||||
### 2.2. Danger Map & Cost Evaluator
|
||||
* **Danger Map Generation:**
|
||||
* Initialize a map for a $100\mu m \times 100\mu m$ area with a single obstacle.
|
||||
* Verify the cost $g_{proximity}$ matches $k/d^2$ for cells near the obstacle.
|
||||
* Verify cost is $0$ for cells beyond the **Safety Threshold**.
|
||||
* **Memory Check:**
|
||||
* Mock a $20mm \times 20mm$ grid and verify memory allocation stays within limits (e.g., `< 2GB` for standard `uint8` resolution).
|
||||
* **Cost Calculation:**
|
||||
* Verify total cost $f(n)$ correctly sums length, bend penalties ($10 \times$ Manhattan), and proximity costs.
|
||||
|
||||
### 2.3. Robustness & Limits
|
||||
* **Design Bounds:**
|
||||
* Test routing at the extreme edges of the $20mm \times 20mm$ coordinate space.
|
||||
* Verify that moves extending outside the design bounds are correctly pruned or flagged.
|
||||
* **Empty/Invalid Inputs:**
|
||||
* Test with an empty netlist.
|
||||
* Test with start and end ports at the exact same location.
|
||||
|
||||
## 3. Integration Tests: Single-Net A* Search (`inire/router/astar`)
|
||||
|
||||
### 3.1. Open Space Scenarios
|
||||
* **Straight Line:** Route from `(0,0,0)` to `(100,0,0)`. Verify it uses only `Straight` moves.
|
||||
* **Simple Turn:** Route from `(0,0,0)` to `(20,20,90)`. Verify it uses a `Bend90` and `Straight` segments.
|
||||
* **Small S-Bend:** Route with an offset of $5\mu m$ and radius $10\mu m$. Verify it uses the `SBend` component.
|
||||
* **Large S-Bend ($O \ge 2R$):** Route with an offset of $50\mu m$ and radius $10\mu m$. Verify it combines two `Bend90`s and a `Straight` segment.
|
||||
|
||||
### 3.2. Obstacle Avoidance (The "Maze" Tests)
|
||||
* **L-Obstacle:** Place an obstacle blocking the direct path. Verify the router goes around it.
|
||||
* **Narrow Channel:** Create two obstacles with a gap slightly wider than $W_i + C$. Verify the router passes through.
|
||||
* **Dead End:** Create a U-shaped obstacle. Verify the search explores alternatives and fails gracefully if no path exists.
|
||||
|
||||
### 3.3. Snapping & Precision
|
||||
* **Snap-to-Target Lookahead:**
|
||||
* Route to a target at `(100.005, 0, 0)` (not on 1µm grid).
|
||||
* Verify the search reaches the vicinity via the 1µm grid and the final segment bridges the **5nm** gap exactly.
|
||||
* **Grid Alignment:**
|
||||
* Start from a port at `(0.5, 0.5, 0)`. Verify it snaps to the 1µm search grid correctly for the first move expansion.
|
||||
|
||||
### 3.4. Failure Modes
|
||||
* **Unreachable Target:** Create a target completely enclosed by obstacles. Verify the search terminates after exploring all options (or hitting the 50,000 node limit) and returns an invalid result.
|
||||
* **Start/End Collision:** Place a port deep inside an obstacle (beyond the 2nm safety zone). Verify the router identifies the immediate collision and fails gracefully.
|
||||
|
||||
## 4. Integration Tests: Multi-Net PathFinder (`inire/router/pathfinder`)
|
||||
|
||||
### 4.1. Congestion Scenarios
|
||||
* **Parallel Paths:** Route two nets that can both take straight paths. Verify no reroutes occur.
|
||||
* **The "Cross" Test:** Two nets must cross paths in 2D.
|
||||
* Since crossings are illegal, verify the second net finds a detour.
|
||||
* Verify the `Negotiated Congestion` loop increases the cost of the shared region.
|
||||
* **Bottleneck:** Force 3 nets through a channel that only fits 2.
|
||||
* Verify the router returns 2 valid paths and 1 "least bad" path (with collisions flagged).
|
||||
* Verify the `is_valid=False` attribute is set for the failing net.
|
||||
|
||||
### 4.2. Determinism & Performance
|
||||
* **Seed Consistency:** Run the same multi-net problem twice with the same seed; verify identical results (pixel-perfect).
|
||||
* **Node Limit Enforcement:** Trigger a complex search that exceeds **50,000 nodes**. Verify it terminates and returns the best-so-far or failure.
|
||||
* **Timeout:** Verify the session-level timeout stops the process for extremely large problems.
|
||||
|
||||
## 5. Benchmarking & Regression
|
||||
* **Standard Benchmark Suite:** A set of 5-10 layouts with varying net counts (1 to 50).
|
||||
* **Metrics to Track:**
|
||||
* Total wire length.
|
||||
* Total number of bends.
|
||||
* Execution time per net.
|
||||
* Success rate (percentage of nets routed without collisions).
|
||||
* **Node Expansion Rate:** Nodes per second.
|
||||
* **Memory Usage:** Peak RSS during 20x20mm routing.
|
||||
* **Fuzz Testing:**
|
||||
* Generate random obstacles and ports within a 1mm x 1mm area.
|
||||
* Verify that the router never crashes.
|
||||
* Verify that every result marked `is_valid=True` is confirmed collision-free by a high-precision (slow) check.
|
||||
|
||||
## 6. Analytic Correctness Guarantees
|
||||
* **Post-Route Validation:**
|
||||
* Implement an independent `validate_path(path, obstacles, clearance)` function using `shapely`'s most precise intersection tests.
|
||||
* Run this on every test result to ensure the `CollisionEngine` (which uses R-Tree for speed) hasn't missed any edge cases.
|
||||
* **Orientation Check:**
|
||||
* Verify that the final port of every path matches the target orientation exactly $\{0, 90, 180, 270\}$.
|
||||
1
ex1.ssc
Normal file
1
ex2.ssc
Normal file
1
ex3.ssc
Normal file
1
ex4.ssc
Normal file
1
ex5.ssc
Normal file
1
ex6.ssc
Normal file
1
ex7.ssc
Normal file
1
ex8.ssc
Normal file
1
ex9.ssc
Normal file
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
BIN
examples/07_iteration_00.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
examples/07_iteration_01.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
examples/07_iteration_02.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_03.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_04.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_05.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_06.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_07.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_08.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_09.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_10.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_11.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_12.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_13.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_14.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_15.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_16.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_17.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_18.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_19.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_20.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_21.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_22.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_23.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_24.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_25.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_26.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_27.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_28.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_29.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_30.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_31.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_32.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_33.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_34.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_35.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_36.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_37.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_38.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_39.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_40.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_41.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_42.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_43.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_44.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_45.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_46.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_47.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_48.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
examples/07_iteration_49.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 339 KiB |
|
|
@ -27,10 +27,10 @@ def main() -> None:
|
|||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute(obstacles)
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5)
|
||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.5, bend_penalty=100.0, sbend_penalty=200.0, congestion_penalty=100.0)
|
||||
|
||||
router = AStarRouter(evaluator, node_limit=10000, snap_size=10.0)
|
||||
pf = PathFinder(router, evaluator, max_iterations=20, base_congestion_penalty=500.0)
|
||||
router = AStarRouter(evaluator, node_limit=1000000, snap_size=5.0)
|
||||
pf = PathFinder(router, evaluator, max_iterations=10, base_congestion_penalty=100.0)
|
||||
|
||||
# 2. Define Netlist
|
||||
netlist = {}
|
||||
|
|
@ -51,6 +51,8 @@ def main() -> None:
|
|||
|
||||
def iteration_callback(idx, current_results):
|
||||
print(f" Iteration {idx} finished. Successes: {sum(1 for r in current_results.values() if r.is_valid)}/{len(netlist)}")
|
||||
print(pf.router.get_metrics_summary())
|
||||
pf.router.reset_metrics()
|
||||
# fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist)
|
||||
# plot_danger_map(danger_map, ax=ax)
|
||||
# fig.savefig(f"examples/07_iteration_{idx:02d}.png")
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 78 KiB |
|
|
@ -2,12 +2,16 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
import rtree
|
||||
import numpy
|
||||
from shapely.prepared import prep
|
||||
from shapely.strtree import STRtree
|
||||
from shapely.geometry import box
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.prepared import PreparedGeometry
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.components import ComponentResult
|
||||
|
||||
|
||||
class CollisionEngine:
|
||||
|
|
@ -16,8 +20,12 @@ class CollisionEngine:
|
|||
"""
|
||||
__slots__ = (
|
||||
'clearance', 'max_net_width', 'safety_zone_radius',
|
||||
'static_index', 'static_geometries', 'static_dilated', 'static_prepared', '_static_id_counter',
|
||||
'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', 'dynamic_prepared', '_dynamic_id_counter'
|
||||
'static_index', 'static_geometries', 'static_dilated', 'static_prepared',
|
||||
'static_is_rect', 'static_tree', 'static_obj_ids', 'static_safe_cache',
|
||||
'static_grid', 'grid_cell_size', '_static_id_counter',
|
||||
'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', 'dynamic_prepared',
|
||||
'dynamic_tree', 'dynamic_obj_ids', 'dynamic_grid', '_dynamic_id_counter',
|
||||
'metrics'
|
||||
)
|
||||
|
||||
clearance: float
|
||||
|
|
@ -52,16 +60,54 @@ class CollisionEngine:
|
|||
self.static_geometries: dict[int, Polygon] = {} # ID -> Raw Polygon
|
||||
self.static_dilated: dict[int, Polygon] = {} # ID -> Dilated Polygon (by clearance)
|
||||
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> Prepared Dilated
|
||||
self.static_is_rect: dict[int, bool] = {} # Optimization for ray_cast
|
||||
self.static_tree: STRtree | None = None
|
||||
self.static_obj_ids: list[int] = [] # Mapping from tree index to obj_id
|
||||
self.static_safe_cache: set[tuple] = set() # Global cache for safe move-port combinations
|
||||
self.static_grid: dict[tuple[int, int], list[int]] = {}
|
||||
self.grid_cell_size = 50.0 # 50um grid cells for broad phase
|
||||
self._static_id_counter = 0
|
||||
|
||||
# Dynamic paths for multi-net congestion
|
||||
self.dynamic_index = rtree.index.Index()
|
||||
# obj_id -> (net_id, raw_geometry)
|
||||
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
# obj_id -> dilated_geometry (by clearance/2)
|
||||
self.dynamic_dilated: dict[int, Polygon] = {}
|
||||
self.dynamic_prepared: dict[int, PreparedGeometry] = {}
|
||||
self.dynamic_tree: STRtree | None = None
|
||||
self.dynamic_obj_ids: list[int] = []
|
||||
self.dynamic_grid: dict[tuple[int, int], list[int]] = {}
|
||||
self._dynamic_id_counter = 0
|
||||
|
||||
self.metrics = {
|
||||
'static_cache_hits': 0,
|
||||
'static_grid_skips': 0,
|
||||
'static_tree_queries': 0,
|
||||
'static_straight_fast': 0,
|
||||
'congestion_grid_skips': 0,
|
||||
'congestion_tree_queries': 0,
|
||||
'safety_zone_checks': 0
|
||||
}
|
||||
|
||||
def reset_metrics(self) -> None:
|
||||
""" Reset all performance counters. """
|
||||
for k in self.metrics:
|
||||
self.metrics[k] = 0
|
||||
|
||||
def get_metrics_summary(self) -> str:
|
||||
""" Return a human-readable summary of collision performance. """
|
||||
m = self.metrics
|
||||
total_static = m['static_cache_hits'] + m['static_grid_skips'] + m['static_tree_queries'] + m['static_straight_fast']
|
||||
static_eff = ((m['static_cache_hits'] + m['static_grid_skips'] + m['static_straight_fast']) / total_static * 100) if total_static > 0 else 0
|
||||
|
||||
total_cong = m['congestion_grid_skips'] + m['congestion_tree_queries']
|
||||
cong_eff = (m['congestion_grid_skips'] / total_cong * 100) if total_cong > 0 else 0
|
||||
|
||||
return (f"Collision Performance: \n"
|
||||
f" Static: {total_static} checks, {static_eff:.1f}% bypassed STRtree\n"
|
||||
f" (Cache={m['static_cache_hits']}, Grid={m['static_grid_skips']}, StraightFast={m['static_straight_fast']}, Tree={m['static_tree_queries']})\n"
|
||||
f" Congestion: {total_cong} checks, {cong_eff:.1f}% bypassed STRtree\n"
|
||||
f" (Grid={m['congestion_grid_skips']}, Tree={m['congestion_tree_queries']})\n"
|
||||
f" Safety Zone: {m['safety_zone_checks']} full intersections performed")
|
||||
|
||||
def add_static_obstacle(self, polygon: Polygon) -> None:
|
||||
"""
|
||||
|
|
@ -73,11 +119,46 @@ class CollisionEngine:
|
|||
obj_id = self._static_id_counter
|
||||
self._static_id_counter += 1
|
||||
|
||||
dilated = polygon.buffer(self.clearance)
|
||||
# Use MITRE join style to preserve rectangularity of boxes
|
||||
dilated = polygon.buffer(self.clearance, join_style=2)
|
||||
self.static_geometries[obj_id] = polygon
|
||||
self.static_dilated[obj_id] = dilated
|
||||
self.static_prepared[obj_id] = prep(dilated)
|
||||
self.static_index.insert(obj_id, dilated.bounds)
|
||||
|
||||
# Invalidate higher-level spatial data
|
||||
self.static_tree = None
|
||||
self.static_grid = {} # Rebuild on demand
|
||||
|
||||
# Check if it's an axis-aligned rectangle (approximately)
|
||||
# Dilated rectangle of an axis-aligned rectangle IS an axis-aligned rectangle.
|
||||
b = dilated.bounds
|
||||
area = (b[2] - b[0]) * (b[3] - b[1])
|
||||
if abs(dilated.area - area) < 1e-4:
|
||||
self.static_is_rect[obj_id] = True
|
||||
else:
|
||||
self.static_is_rect[obj_id] = False
|
||||
|
||||
def _ensure_static_tree(self) -> None:
|
||||
if self.static_tree is None and self.static_dilated:
|
||||
ids = sorted(self.static_dilated.keys())
|
||||
geoms = [self.static_dilated[i] for i in ids]
|
||||
self.static_tree = STRtree(geoms)
|
||||
self.static_obj_ids = ids
|
||||
|
||||
def _ensure_static_grid(self) -> None:
|
||||
if not self.static_grid and self.static_dilated:
|
||||
cs = self.grid_cell_size
|
||||
for obj_id, poly in self.static_dilated.items():
|
||||
b = poly.bounds
|
||||
min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
|
||||
min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
|
||||
for gx in range(min_gx, max_gx + 1):
|
||||
for gy in range(min_gy, max_gy + 1):
|
||||
cell = (gx, gy)
|
||||
if cell not in self.static_grid:
|
||||
self.static_grid[cell] = []
|
||||
self.static_grid[cell].append(obj_id)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
||||
"""
|
||||
|
|
@ -99,6 +180,30 @@ class CollisionEngine:
|
|||
self.dynamic_dilated[obj_id] = dil
|
||||
self.dynamic_prepared[obj_id] = prep(dil)
|
||||
self.dynamic_index.insert(obj_id, dil.bounds)
|
||||
|
||||
self.dynamic_tree = None
|
||||
self.dynamic_grid = {}
|
||||
|
||||
def _ensure_dynamic_tree(self) -> None:
|
||||
if self.dynamic_tree is None and self.dynamic_dilated:
|
||||
ids = sorted(self.dynamic_dilated.keys())
|
||||
geoms = [self.dynamic_dilated[i] for i in ids]
|
||||
self.dynamic_tree = STRtree(geoms)
|
||||
self.dynamic_obj_ids = ids
|
||||
|
||||
def _ensure_dynamic_grid(self) -> None:
|
||||
if not self.dynamic_grid and self.dynamic_dilated:
|
||||
cs = self.grid_cell_size
|
||||
for obj_id, poly in self.dynamic_dilated.items():
|
||||
b = poly.bounds
|
||||
min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
|
||||
min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
|
||||
for gx in range(min_gx, max_gx + 1):
|
||||
for gy in range(min_gy, max_gy + 1):
|
||||
cell = (gx, gy)
|
||||
if cell not in self.dynamic_grid:
|
||||
self.dynamic_grid[cell] = []
|
||||
self.dynamic_grid[cell].append(obj_id)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -113,6 +218,10 @@ class CollisionEngine:
|
|||
dilated = self.dynamic_dilated.pop(obj_id)
|
||||
self.dynamic_prepared.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, dilated.bounds)
|
||||
|
||||
if to_remove:
|
||||
self.dynamic_tree = None
|
||||
self.dynamic_grid = {}
|
||||
|
||||
def lock_net(self, net_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -152,6 +261,279 @@ class CollisionEngine:
|
|||
res = self.check_collision(geometry, net_id, buffer_mode='congestion')
|
||||
return int(res)
|
||||
|
||||
def check_move_straight_static(
|
||||
self,
|
||||
origin: Port,
|
||||
length: float,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized fast static check for Straights.
|
||||
"""
|
||||
self.metrics['static_straight_fast'] += 1
|
||||
# FAST PATH: Grid check
|
||||
self._ensure_static_grid()
|
||||
cs = self.grid_cell_size
|
||||
|
||||
rad = numpy.radians(origin.orientation)
|
||||
dx = length * numpy.cos(rad)
|
||||
dy = length * numpy.sin(rad)
|
||||
|
||||
# Move bounds
|
||||
xmin, xmax = sorted([origin.x, origin.x + dx])
|
||||
ymin, ymax = sorted([origin.y, origin.y + dy])
|
||||
|
||||
# Inflate by clearance/2 for waveguide half-width?
|
||||
# No, static obstacles are ALREADY inflated by full clearance.
|
||||
# So we just check if the centerline hits an inflated obstacle.
|
||||
|
||||
min_gx, max_gx = int(xmin / cs), int(xmax / cs)
|
||||
min_gy, max_gy = int(ymin / cs), int(ymax / cs)
|
||||
|
||||
static_grid = self.static_grid
|
||||
static_dilated = self.static_dilated
|
||||
static_is_rect = self.static_is_rect
|
||||
static_prepared = self.static_prepared
|
||||
|
||||
inv_dx = 1.0/dx if abs(dx) > 1e-12 else 1e30
|
||||
inv_dy = 1.0/dy if abs(dy) > 1e-12 else 1e30
|
||||
|
||||
checked_ids = set()
|
||||
for gx in range(min_gx, max_gx + 1):
|
||||
for gy in range(min_gy, max_gy + 1):
|
||||
if (gx, gy) in static_grid:
|
||||
for obj_id in static_grid[(gx, gy)]:
|
||||
if obj_id in checked_ids: continue
|
||||
checked_ids.add(obj_id)
|
||||
|
||||
b = static_dilated[obj_id].bounds
|
||||
# Slab Method
|
||||
if abs(dx) < 1e-12:
|
||||
if origin.x < b[0] or origin.x > b[2]: continue
|
||||
tx_min, tx_max = -1e30, 1e30
|
||||
else:
|
||||
tx_min = (b[0] - origin.x) * inv_dx
|
||||
tx_max = (b[2] - origin.x) * inv_dx
|
||||
if tx_min > tx_max: tx_min, tx_max = tx_max, tx_min
|
||||
|
||||
if abs(dy) < 1e-12:
|
||||
if origin.y < b[1] or origin.y > b[3]: continue
|
||||
ty_min, ty_max = -1e30, 1e30
|
||||
else:
|
||||
ty_min = (b[1] - origin.y) * inv_dy
|
||||
ty_max = (b[3] - origin.y) * inv_dy
|
||||
if ty_min > ty_max: ty_min, ty_max = ty_max, ty_min
|
||||
|
||||
t_min = max(tx_min, ty_min)
|
||||
t_max = min(tx_max, ty_max)
|
||||
|
||||
if t_max < 0 or t_min > t_max or t_min > 1.0:
|
||||
continue
|
||||
|
||||
# If rectangle, slab is exact
|
||||
if static_is_rect[obj_id]:
|
||||
return True
|
||||
|
||||
# Fallback for complex obstacles
|
||||
# (We could still use ray_cast here but we want exact)
|
||||
# For now, if hits AABB, check prepared
|
||||
from shapely.geometry import LineString
|
||||
line = LineString([(origin.x, origin.y), (origin.x+dx, origin.y+dy)])
|
||||
if static_prepared[obj_id].intersects(line):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_move_static(
|
||||
self,
|
||||
result: ComponentResult,
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a move (ComponentResult) hits any static obstacles.
|
||||
"""
|
||||
# FAST PATH 1: Safety cache check
|
||||
cache_key = (result.move_type,
|
||||
round(start_port.x, 3) if start_port else 0,
|
||||
round(start_port.y, 3) if start_port else 0,
|
||||
round(end_port.x, 3) if end_port else 0,
|
||||
round(end_port.y, 3) if end_port else 0)
|
||||
|
||||
if cache_key in self.static_safe_cache:
|
||||
self.metrics['static_cache_hits'] += 1
|
||||
return False
|
||||
|
||||
# FAST PATH 2: Spatial grid check (bypasses STRtree for empty areas)
|
||||
self._ensure_static_grid()
|
||||
cs = self.grid_cell_size
|
||||
b = result.total_bounds
|
||||
min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
|
||||
min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
|
||||
|
||||
any_candidates = False
|
||||
static_grid = self.static_grid
|
||||
for gx in range(min_gx, max_gx + 1):
|
||||
for gy in range(min_gy, max_gy + 1):
|
||||
if (gx, gy) in static_grid:
|
||||
any_candidates = True
|
||||
break
|
||||
if any_candidates: break
|
||||
|
||||
if not any_candidates:
|
||||
self.metrics['static_grid_skips'] += 1
|
||||
self.static_safe_cache.add(cache_key)
|
||||
return False
|
||||
|
||||
self.metrics['static_tree_queries'] += 1
|
||||
self._ensure_static_tree()
|
||||
if self.static_tree is None:
|
||||
return False
|
||||
|
||||
# Vectorized Broad phase + Narrow phase
|
||||
# Pass all polygons in the move at once
|
||||
res_indices, tree_indices = self.static_tree.query(result.geometry, predicate='intersects')
|
||||
if tree_indices.size == 0:
|
||||
self.static_safe_cache.add(cache_key)
|
||||
return False
|
||||
|
||||
# If we have hits, we must check safety zones
|
||||
static_obj_ids = self.static_obj_ids
|
||||
for i in range(tree_indices.size):
|
||||
poly_idx = res_indices[i]
|
||||
hit_idx = tree_indices[i]
|
||||
obj_id = static_obj_ids[hit_idx]
|
||||
poly = result.geometry[poly_idx]
|
||||
if self._is_in_safety_zone(poly, obj_id, start_port, end_port):
|
||||
continue
|
||||
return True
|
||||
|
||||
self.static_safe_cache.add(cache_key)
|
||||
return False
|
||||
|
||||
def check_move_congestion(
|
||||
self,
|
||||
result: ComponentResult,
|
||||
net_id: str,
|
||||
) -> int:
|
||||
"""
|
||||
Count overlaps of a move with other dynamic paths.
|
||||
"""
|
||||
if result.total_dilated_bounds_box is None:
|
||||
return 0
|
||||
|
||||
# FAST PATH: Grid check
|
||||
self._ensure_dynamic_grid()
|
||||
if not self.dynamic_grid:
|
||||
return 0
|
||||
|
||||
cs = self.grid_cell_size
|
||||
b = result.total_dilated_bounds
|
||||
min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
|
||||
min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
|
||||
|
||||
any_candidates = False
|
||||
dynamic_grid = self.dynamic_grid
|
||||
dynamic_geometries = self.dynamic_geometries
|
||||
for gx in range(min_gx, max_gx + 1):
|
||||
for gy in range(min_gy, max_gy + 1):
|
||||
cell = (gx, gy)
|
||||
if cell in dynamic_grid:
|
||||
# Check if any obj_id in this cell belongs to another net
|
||||
for obj_id in dynamic_grid[cell]:
|
||||
other_net_id, _ = dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id:
|
||||
any_candidates = True
|
||||
break
|
||||
if any_candidates: break
|
||||
if any_candidates: break
|
||||
|
||||
if not any_candidates:
|
||||
self.metrics['congestion_grid_skips'] += 1
|
||||
return 0
|
||||
|
||||
# SLOW PATH: STRtree
|
||||
self.metrics['congestion_tree_queries'] += 1
|
||||
self._ensure_dynamic_tree()
|
||||
if self.dynamic_tree is None:
|
||||
return 0
|
||||
|
||||
# Vectorized query: pass the whole list of polygons
|
||||
# result.dilated_geometry is list[Polygon]
|
||||
# query() returns (2, M) array of [geometry_indices, tree_indices]
|
||||
res_indices, tree_indices = self.dynamic_tree.query(result.dilated_geometry, predicate='intersects')
|
||||
if tree_indices.size == 0:
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
dynamic_geometries = self.dynamic_geometries
|
||||
dynamic_obj_ids = self.dynamic_obj_ids
|
||||
|
||||
# We need to filter by net_id and count UNIQUE overlaps?
|
||||
# Actually, if a single move polygon hits multiple other net polygons, it's multiple overlaps.
|
||||
# But if multiple move polygons hit the SAME other net polygon, is it multiple overlaps?
|
||||
# Usually, yes, because cost is proportional to volume of overlap.
|
||||
for hit_idx in tree_indices:
|
||||
obj_id = dynamic_obj_ids[hit_idx]
|
||||
other_net_id, _ = dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def _is_in_safety_zone(self, geometry: Polygon, obj_id: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||
""" Helper to check if an intersection is within a port safety zone. """
|
||||
sz = self.safety_zone_radius
|
||||
static_dilated = self.static_dilated
|
||||
|
||||
# Optimization: Skip expensive intersection if neither port is near the obstacle's bounds
|
||||
is_near_port = False
|
||||
b = static_dilated[obj_id].bounds
|
||||
if start_port:
|
||||
if (b[0] - sz <= start_port.x <= b[2] + sz and
|
||||
b[1] - sz <= start_port.y <= b[3] + sz):
|
||||
is_near_port = True
|
||||
if not is_near_port and end_port:
|
||||
if (b[0] - sz <= end_port.x <= b[2] + sz and
|
||||
b[1] - sz <= end_port.y <= b[3] + sz):
|
||||
is_near_port = True
|
||||
|
||||
if not is_near_port:
|
||||
return False # Collision is NOT in safety zone
|
||||
|
||||
# Only if near port, do the expensive check
|
||||
self.metrics['safety_zone_checks'] += 1
|
||||
raw_obstacle = self.static_geometries[obj_id]
|
||||
intersection = geometry.intersection(raw_obstacle)
|
||||
if intersection.is_empty:
|
||||
return True # Not actually hitting the RAW obstacle (only the buffer)
|
||||
|
||||
ix_bounds = intersection.bounds
|
||||
# Check start port
|
||||
if start_port:
|
||||
if (abs(ix_bounds[0] - start_port.x) < sz and
|
||||
abs(ix_bounds[2] - start_port.x) < sz and
|
||||
abs(ix_bounds[1] - start_port.y) < sz and
|
||||
abs(ix_bounds[3] - start_port.y) < sz):
|
||||
return True # Is safe
|
||||
# Check end port
|
||||
if end_port:
|
||||
if (abs(ix_bounds[0] - end_port.x) < sz and
|
||||
abs(ix_bounds[2] - end_port.x) < sz and
|
||||
abs(ix_bounds[1] - end_port.y) < sz and
|
||||
abs(ix_bounds[3] - end_port.y) < sz):
|
||||
return True # Is safe
|
||||
|
||||
return False
|
||||
|
||||
def check_congestion(
|
||||
self,
|
||||
geometry: Polygon,
|
||||
net_id: str,
|
||||
dilated_geometry: Polygon | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
Alias for check_collision(buffer_mode='congestion') for backward compatibility.
|
||||
"""
|
||||
res = self.check_collision(geometry, net_id, buffer_mode='congestion', dilated_geometry=dilated_geometry)
|
||||
return int(res)
|
||||
def check_collision(
|
||||
self,
|
||||
geometry: Polygon,
|
||||
|
|
@ -160,89 +542,42 @@ class CollisionEngine:
|
|||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
dilated_geometry: Polygon | None = None,
|
||||
bounds: tuple[float, float, float, float] | None = None,
|
||||
) -> bool | int:
|
||||
"""
|
||||
Check for collisions using unified dilation logic.
|
||||
|
||||
Args:
|
||||
geometry: Raw geometry to check.
|
||||
net_id: Identifier for the net.
|
||||
buffer_mode: 'static' (full clearance) or 'congestion' (shared).
|
||||
start_port: Optional start port for safety zone.
|
||||
end_port: Optional end port for safety zone.
|
||||
dilated_geometry: Optional pre-buffered geometry (clearance/2).
|
||||
|
||||
Returns:
|
||||
Boolean if static, integer count if congestion.
|
||||
"""
|
||||
# Optimization: Pre-fetch some members
|
||||
sz = self.safety_zone_radius
|
||||
|
||||
if buffer_mode == 'static':
|
||||
# Use raw query against pre-dilated obstacles
|
||||
bounds = geometry.bounds
|
||||
candidates = self.static_index.intersection(bounds)
|
||||
|
||||
static_prepared = self.static_prepared
|
||||
static_dilated = self.static_dilated
|
||||
static_geometries = self.static_geometries
|
||||
|
||||
for obj_id in candidates:
|
||||
if static_prepared[obj_id].intersects(geometry):
|
||||
if start_port or end_port:
|
||||
# Optimization: Skip expensive intersection if neither port is near the obstacle's bounds
|
||||
is_near_port = False
|
||||
b = static_dilated[obj_id].bounds
|
||||
if start_port:
|
||||
if (b[0] - sz <= start_port.x <= b[2] + sz and
|
||||
b[1] - sz <= start_port.y <= b[3] + sz):
|
||||
is_near_port = True
|
||||
if not is_near_port and end_port:
|
||||
if (b[0] - sz <= end_port.x <= b[2] + sz and
|
||||
b[1] - sz <= end_port.y <= b[3] + sz):
|
||||
is_near_port = True
|
||||
|
||||
if not is_near_port:
|
||||
return True # Collision, and not near any port safety zone
|
||||
|
||||
# Only if near port, do the expensive check
|
||||
raw_obstacle = static_geometries[obj_id]
|
||||
intersection = geometry.intersection(raw_obstacle)
|
||||
if not intersection.is_empty:
|
||||
ix_bounds = intersection.bounds
|
||||
is_safe = False
|
||||
# Check start port
|
||||
if start_port:
|
||||
if (abs(ix_bounds[0] - start_port.x) < sz and
|
||||
abs(ix_bounds[2] - start_port.x) < sz and
|
||||
abs(ix_bounds[1] - start_port.y) < sz and
|
||||
abs(ix_bounds[3] - start_port.y) < sz):
|
||||
is_safe = True
|
||||
# Check end port
|
||||
if not is_safe and end_port:
|
||||
if (abs(ix_bounds[0] - end_port.x) < sz and
|
||||
abs(ix_bounds[2] - end_port.x) < sz and
|
||||
abs(ix_bounds[1] - end_port.y) < sz and
|
||||
abs(ix_bounds[3] - end_port.y) < sz):
|
||||
is_safe = True
|
||||
|
||||
if is_safe:
|
||||
continue
|
||||
return True
|
||||
self._ensure_static_tree()
|
||||
if self.static_tree is None:
|
||||
return False
|
||||
|
||||
hits = self.static_tree.query(geometry, predicate='intersects')
|
||||
static_obj_ids = self.static_obj_ids
|
||||
for hit_idx in hits:
|
||||
obj_id = static_obj_ids[hit_idx]
|
||||
if self._is_in_safety_zone(geometry, obj_id, start_port, end_port):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
# buffer_mode == 'congestion'
|
||||
self._ensure_dynamic_tree()
|
||||
if self.dynamic_tree is None:
|
||||
return 0
|
||||
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = dilated_geometry if dilated_geometry else geometry.buffer(dilation)
|
||||
candidates = self.dynamic_index.intersection(test_poly.bounds)
|
||||
|
||||
dynamic_geometries = self.dynamic_geometries
|
||||
dynamic_prepared = self.dynamic_prepared
|
||||
|
||||
|
||||
hits = self.dynamic_tree.query(test_poly, predicate='intersects')
|
||||
count = 0
|
||||
for obj_id in candidates:
|
||||
dynamic_geometries = self.dynamic_geometries
|
||||
dynamic_obj_ids = self.dynamic_obj_ids
|
||||
|
||||
for hit_idx in hits:
|
||||
obj_id = dynamic_obj_ids[hit_idx]
|
||||
other_net_id, _ = dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id and dynamic_prepared[obj_id].intersects(test_poly):
|
||||
if other_net_id != net_id:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
|
@ -262,50 +597,110 @@ class CollisionEngine:
|
|||
from shapely.geometry import LineString
|
||||
|
||||
rad = numpy.radians(angle_deg)
|
||||
dx = max_dist * numpy.cos(rad)
|
||||
dy = max_dist * numpy.sin(rad)
|
||||
cos_val = numpy.cos(rad)
|
||||
sin_val = numpy.sin(rad)
|
||||
dx = max_dist * cos_val
|
||||
dy = max_dist * sin_val
|
||||
|
||||
# Ray geometry
|
||||
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
|
||||
# 1. Pre-calculate ray direction inverses for fast slab intersection
|
||||
# Use a small epsilon to avoid divide by zero, but handle zero dx/dy properly.
|
||||
if abs(dx) < 1e-12:
|
||||
inv_dx = 1e30 # Represent infinity
|
||||
else:
|
||||
inv_dx = 1.0 / dx
|
||||
|
||||
if abs(dy) < 1e-12:
|
||||
inv_dy = 1e30 # Represent infinity
|
||||
else:
|
||||
inv_dy = 1.0 / dy
|
||||
|
||||
# Ray AABB for initial R-Tree query
|
||||
min_x, max_x = sorted([origin.x, origin.x + dx])
|
||||
min_y, max_y = sorted([origin.y, origin.y + dy])
|
||||
|
||||
# 1. Query R-Tree
|
||||
candidates = self.static_index.intersection(ray_line.bounds)
|
||||
candidates = list(self.static_index.intersection((min_x, min_y, max_x, max_y)))
|
||||
if not candidates:
|
||||
return max_dist
|
||||
|
||||
min_dist = max_dist
|
||||
|
||||
# 2. Check Intersections
|
||||
# Note: We intersect with DILATED obstacles to account for clearance
|
||||
static_dilated = self.static_dilated
|
||||
static_prepared = self.static_prepared
|
||||
|
||||
# Optimization: Sort candidates by approximate distance to origin
|
||||
# (Using a simpler distance measure for speed)
|
||||
def approx_dist_sq(obj_id):
|
||||
b = static_dilated[obj_id].bounds
|
||||
return (b[0] - origin.x)**2 + (b[1] - origin.y)**2
|
||||
|
||||
candidates.sort(key=approx_dist_sq)
|
||||
|
||||
ray_line = None # Lazy creation
|
||||
|
||||
for obj_id in candidates:
|
||||
obstacle = self.static_dilated[obj_id]
|
||||
# Fast check with prepared geom? intersects() is fast, intersection() gives point
|
||||
if self.static_prepared[obj_id].intersects(ray_line):
|
||||
b = static_dilated[obj_id].bounds
|
||||
|
||||
# Fast Ray-Box intersection (Slab Method)
|
||||
# Correctly handle potential for dx=0 or dy=0
|
||||
if abs(dx) < 1e-12:
|
||||
if origin.x < b[0] or origin.x > b[2]:
|
||||
continue
|
||||
tx_min, tx_max = -1e30, 1e30
|
||||
else:
|
||||
tx_min = (b[0] - origin.x) * inv_dx
|
||||
tx_max = (b[2] - origin.x) * inv_dx
|
||||
if tx_min > tx_max: tx_min, tx_max = tx_max, tx_min
|
||||
|
||||
if abs(dy) < 1e-12:
|
||||
if origin.y < b[1] or origin.y > b[3]:
|
||||
continue
|
||||
ty_min, ty_max = -1e30, 1e30
|
||||
else:
|
||||
ty_min = (b[1] - origin.y) * inv_dy
|
||||
ty_max = (b[3] - origin.y) * inv_dy
|
||||
if ty_min > ty_max: ty_min, ty_max = ty_max, ty_min
|
||||
|
||||
t_min = max(tx_min, ty_min)
|
||||
t_max = min(tx_max, ty_max)
|
||||
|
||||
# Intersection if [t_min, t_max] intersects [0, 1]
|
||||
if t_max < 0 or t_min > t_max or t_min >= (min_dist / max_dist) or t_min > 1.0:
|
||||
continue
|
||||
|
||||
# Optimization: If it's a rectangle, the slab result is exact!
|
||||
if self.static_is_rect[obj_id]:
|
||||
min_dist = max(0.0, t_min * max_dist)
|
||||
continue
|
||||
|
||||
# If we are here, the ray hits the AABB. Now check the actual polygon.
|
||||
if ray_line is None:
|
||||
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
|
||||
|
||||
if static_prepared[obj_id].intersects(ray_line):
|
||||
# Calculate exact intersection distance
|
||||
intersection = ray_line.intersection(obstacle)
|
||||
intersection = ray_line.intersection(static_dilated[obj_id])
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
||||
# Intersection could be MultiLineString or LineString or Point
|
||||
# We want the point closest to origin
|
||||
|
||||
# Helper to get dist
|
||||
def get_dist(geom):
|
||||
if hasattr(geom, 'geoms'): # Multi-part
|
||||
return min(get_dist(g) for g in geom.geoms)
|
||||
# For line string, the intersection is the segment INSIDE the obstacle.
|
||||
# The distance is the distance to the start of that segment.
|
||||
# Or if it's a touch (Point), distance to point.
|
||||
coords = geom.coords
|
||||
# Distance to the first point of the intersection geometry
|
||||
# (Assuming simple overlap, first point is entry)
|
||||
p1 = coords[0]
|
||||
return numpy.sqrt((p1[0] - origin.x)**2 + (p1[1] - origin.y)**2)
|
||||
|
||||
try:
|
||||
d = get_dist(intersection)
|
||||
# Subtract safety margin to be safe? No, let higher level handle margins.
|
||||
if d < min_dist:
|
||||
min_dist = d
|
||||
# Update ray_line for more aggressive pruning?
|
||||
# Actually just update min_dist and we use it in the t_min check.
|
||||
except Exception:
|
||||
pass # Robustness
|
||||
pass
|
||||
|
||||
return min_dist
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ class ComponentResult:
|
|||
"""
|
||||
__slots__ = (
|
||||
'geometry', 'dilated_geometry', 'proxy_geometry', 'actual_geometry',
|
||||
'end_port', 'length', 'move_type', 'bounds', 'dilated_bounds', '_t_cache'
|
||||
'end_port', 'length', 'move_type', 'bounds', 'dilated_bounds',
|
||||
'total_bounds', 'total_dilated_bounds', 'total_bounds_box', 'total_dilated_bounds_box', '_t_cache'
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
|
@ -53,7 +54,28 @@ class ComponentResult:
|
|||
if not skip_bounds:
|
||||
# Vectorized bounds calculation
|
||||
self.bounds = shapely.bounds(geometry)
|
||||
self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry is not None else None
|
||||
# Total bounds across all polygons in the move
|
||||
self.total_bounds = numpy.array([
|
||||
numpy.min(self.bounds[:, 0]),
|
||||
numpy.min(self.bounds[:, 1]),
|
||||
numpy.max(self.bounds[:, 2]),
|
||||
numpy.max(self.bounds[:, 3])
|
||||
])
|
||||
self.total_bounds_box = box(*self.total_bounds)
|
||||
|
||||
if dilated_geometry is not None:
|
||||
self.dilated_bounds = shapely.bounds(dilated_geometry)
|
||||
self.total_dilated_bounds = numpy.array([
|
||||
numpy.min(self.dilated_bounds[:, 0]),
|
||||
numpy.min(self.dilated_bounds[:, 1]),
|
||||
numpy.max(self.dilated_bounds[:, 2]),
|
||||
numpy.max(self.dilated_bounds[:, 3])
|
||||
])
|
||||
self.total_dilated_bounds_box = box(*self.total_dilated_bounds)
|
||||
else:
|
||||
self.dilated_bounds = None
|
||||
self.total_dilated_bounds = None
|
||||
self.total_dilated_bounds_box = None
|
||||
|
||||
def translate(self, dx: float, dy: float) -> ComponentResult:
|
||||
"""
|
||||
|
|
@ -96,17 +118,21 @@ class ComponentResult:
|
|||
|
||||
# Optimize: reuse and translate bounds
|
||||
res.bounds = self.bounds + [dx, dy, dx, dy]
|
||||
res.total_bounds = self.total_bounds + [dx, dy, dx, dy]
|
||||
res.total_bounds_box = box(*res.total_bounds)
|
||||
|
||||
if self.dilated_bounds is not None:
|
||||
res.dilated_bounds = self.dilated_bounds + [dx, dy, dx, dy]
|
||||
res.total_dilated_bounds = self.total_dilated_bounds + [dx, dy, dx, dy]
|
||||
res.total_dilated_bounds_box = box(*res.total_dilated_bounds)
|
||||
else:
|
||||
res.total_dilated_bounds = None
|
||||
res.total_dilated_bounds_box = None
|
||||
|
||||
self._t_cache[(dxr, dyr)] = res
|
||||
return res
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Straight:
|
||||
"""
|
||||
Move generator for straight waveguide segments.
|
||||
|
|
@ -347,6 +373,7 @@ class Bend90:
|
|||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0,
|
||||
dilation: float = 0.0,
|
||||
snap_to_grid: bool = True,
|
||||
snap_size: float = SEARCH_GRID_SNAP_UM,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
|
|
@ -371,8 +398,12 @@ class Bend90:
|
|||
# Snap the end point to the grid
|
||||
ex_raw = cx + radius * numpy.cos(t_end)
|
||||
ey_raw = cy + radius * numpy.sin(t_end)
|
||||
ex = snap_search_grid(ex_raw, snap_size)
|
||||
ey = snap_search_grid(ey_raw, snap_size)
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex_raw, snap_size)
|
||||
ey = snap_search_grid(ey_raw, snap_size)
|
||||
else:
|
||||
ex, ey = ex_raw, ey_raw
|
||||
|
||||
# Slightly adjust radius to hit snapped point exactly
|
||||
actual_radius = numpy.sqrt((ex - cx)**2 + (ey - cy)**2)
|
||||
|
|
@ -422,6 +453,7 @@ class SBend:
|
|||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0,
|
||||
dilation: float = 0.0,
|
||||
snap_to_grid: bool = True,
|
||||
snap_size: float = SEARCH_GRID_SNAP_UM,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
|
|
@ -434,9 +466,16 @@ class SBend:
|
|||
dx_init = 2 * radius * numpy.sin(theta_init)
|
||||
rad_start = numpy.radians(start_port.orientation)
|
||||
|
||||
# Snap the target point
|
||||
ex = snap_search_grid(start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start), snap_size)
|
||||
ey = snap_search_grid(start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.cos(rad_start), snap_size)
|
||||
# Target point
|
||||
ex_raw = start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start)
|
||||
ey_raw = start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.cos(rad_start)
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex_raw, snap_size)
|
||||
ey = snap_search_grid(ey_raw, snap_size)
|
||||
else:
|
||||
ex, ey = ex_raw, ey_raw
|
||||
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
# Solve for theta and radius that hit (ex, ey) exactly
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class Port:
|
|||
y: float,
|
||||
orientation: float,
|
||||
) -> None:
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.x = snap_nm(x)
|
||||
self.y = snap_nm(y)
|
||||
self.orientation = float(orientation % 360)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
@ -59,7 +59,7 @@ def rotate_port(port: Port, angle: float, origin: tuple[float, float] = (0, 0))
|
|||
px, py = port.x, port.y
|
||||
|
||||
rad = numpy.radians(angle)
|
||||
qx = ox + numpy.cos(rad) * (px - ox) - numpy.sin(rad) * (py - oy)
|
||||
qy = oy + numpy.sin(rad) * (px - ox) + numpy.cos(rad) * (py - oy)
|
||||
qx = snap_nm(ox + numpy.cos(rad) * (px - ox) - numpy.sin(rad) * (py - oy))
|
||||
qy = snap_nm(oy + numpy.sin(rad) * (px - ox) + numpy.cos(rad) * (py - oy))
|
||||
|
||||
return Port(qx, qy, port.orientation + angle)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ from typing import TYPE_CHECKING, Literal, Any
|
|||
import rtree
|
||||
|
||||
import numpy
|
||||
import shapely
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight, SEARCH_GRID_SNAP_UM
|
||||
from inire.geometry.components import Bend90, SBend, Straight, SEARCH_GRID_SNAP_UM, snap_search_grid
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.config import RouterConfig
|
||||
from inire.router.visibility import VisibilityManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
|
|
@ -23,7 +25,7 @@ class AStarNode:
|
|||
"""
|
||||
A node in the A* search tree.
|
||||
"""
|
||||
__slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result', 'path_bbox')
|
||||
__slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -39,49 +41,18 @@ class AStarNode:
|
|||
self.f_cost = g_cost + h_cost
|
||||
self.parent = parent
|
||||
self.component_result = component_result
|
||||
|
||||
if parent is None:
|
||||
self.path_bbox = None
|
||||
else:
|
||||
# Union of parent's bbox and current move's bbox
|
||||
if component_result:
|
||||
# Use pre-calculated bounds if available, avoiding numpy overhead
|
||||
# component_result.bounds is (N, 4)
|
||||
if component_result.dilated_bounds is not None:
|
||||
b = component_result.dilated_bounds
|
||||
else:
|
||||
b = component_result.bounds
|
||||
|
||||
# Fast min/max for typically 1 polygon
|
||||
if len(b) == 1:
|
||||
minx, miny, maxx, maxy = b[0]
|
||||
else:
|
||||
minx = min(row[0] for row in b)
|
||||
miny = min(row[1] for row in b)
|
||||
maxx = max(row[2] for row in b)
|
||||
maxy = max(row[3] for row in b)
|
||||
|
||||
if parent.path_bbox:
|
||||
pb = parent.path_bbox
|
||||
self.path_bbox = (
|
||||
minx if minx < pb[0] else pb[0],
|
||||
miny if miny < pb[1] else pb[1],
|
||||
maxx if maxx > pb[2] else pb[2],
|
||||
maxy if maxy > pb[3] else pb[3]
|
||||
)
|
||||
else:
|
||||
self.path_bbox = (minx, miny, maxx, maxy)
|
||||
|
||||
def __lt__(self, other: AStarNode) -> bool:
|
||||
# Tie-break with h_cost (favour nodes closer to target)
|
||||
if abs(self.f_cost - other.f_cost) < 1e-6:
|
||||
return self.h_cost < other.h_cost
|
||||
return self.f_cost < other.f_cost
|
||||
if self.f_cost < other.f_cost - 1e-6:
|
||||
return True
|
||||
if self.f_cost > other.f_cost + 1e-6:
|
||||
return False
|
||||
return self.h_cost < other.h_cost
|
||||
|
||||
|
||||
class AStarRouter:
|
||||
"""
|
||||
Waveguide router based on A* search on a continuous-state lattice.
|
||||
Waveguide router based on sparse A* search.
|
||||
"""
|
||||
def __init__(self, cost_evaluator: CostEvaluator, node_limit: int | None = None, **kwargs) -> None:
|
||||
self.cost_evaluator = cost_evaluator
|
||||
|
|
@ -96,25 +67,44 @@ class AStarRouter:
|
|||
|
||||
self.node_limit = self.config.node_limit
|
||||
|
||||
# Performance cache for collision checks
|
||||
# Key: (start_x_grid, start_y_grid, start_ori, move_type, width) -> bool
|
||||
self._collision_cache: dict[tuple, bool] = {}
|
||||
# FAST CACHE: set of keys that are known to collide (hard collisions)
|
||||
# Visibility Manager for sparse jumps
|
||||
self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine)
|
||||
|
||||
self._hard_collision_set: set[tuple] = set()
|
||||
|
||||
# New: cache for congestion overlaps within a single route session
|
||||
self._congestion_cache: dict[tuple, int] = {}
|
||||
|
||||
# Cache for generated moves (relative to origin)
|
||||
# Key: (orientation, type, params...) -> ComponentResult
|
||||
self._static_safe_cache: set[tuple] = set()
|
||||
self._move_cache: dict[tuple, ComponentResult] = {}
|
||||
|
||||
self.total_nodes_expanded = 0
|
||||
self.last_expanded_nodes: list[tuple[float, float, float]] = []
|
||||
|
||||
self.metrics = {
|
||||
'nodes_expanded': 0,
|
||||
'moves_generated': 0,
|
||||
'moves_added': 0,
|
||||
'pruned_closed_set': 0,
|
||||
'pruned_hard_collision': 0,
|
||||
'pruned_cost': 0
|
||||
}
|
||||
|
||||
def reset_metrics(self) -> None:
|
||||
""" Reset all performance counters. """
|
||||
for k in self.metrics:
|
||||
self.metrics[k] = 0
|
||||
self.cost_evaluator.collision_engine.reset_metrics()
|
||||
|
||||
def get_metrics_summary(self) -> str:
|
||||
""" Return a human-readable summary of search performance. """
|
||||
m = self.metrics
|
||||
c = self.cost_evaluator.collision_engine.get_metrics_summary()
|
||||
return (f"Search Performance: \n"
|
||||
f" Nodes Expanded: {m['nodes_expanded']}\n"
|
||||
f" Moves: Generated={m['moves_generated']}, Added={m['moves_added']}\n"
|
||||
f" Pruning: ClosedSet={m['pruned_closed_set']}, HardColl={m['pruned_hard_collision']}, Cost={m['pruned_cost']}\n"
|
||||
f" {c}")
|
||||
|
||||
@property
|
||||
def _self_dilation(self) -> float:
|
||||
""" Clearance from other paths (negotiated congestion) """
|
||||
return self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
|
||||
def route(
|
||||
|
|
@ -126,6 +116,7 @@ class AStarRouter:
|
|||
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None,
|
||||
return_partial: bool = False,
|
||||
store_expanded: bool = False,
|
||||
skip_congestion: bool = False,
|
||||
) -> list[ComponentResult] | None:
|
||||
"""
|
||||
Route a single net using A*.
|
||||
|
|
@ -140,7 +131,7 @@ class AStarRouter:
|
|||
open_set: list[AStarNode] = []
|
||||
snap = self.config.snap_size
|
||||
|
||||
# Key: (x_grid, y_grid, orientation_grid) -> min_g_cost
|
||||
# (x_grid, y_grid, orientation_grid) -> min_g_cost
|
||||
closed_set: dict[tuple[int, int, int], float] = {}
|
||||
|
||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||
|
|
@ -150,21 +141,17 @@ class AStarRouter:
|
|||
nodes_expanded = 0
|
||||
|
||||
node_limit = self.node_limit
|
||||
reconstruct_path = self._reconstruct_path
|
||||
|
||||
while open_set:
|
||||
if nodes_expanded >= node_limit:
|
||||
# logger.warning(f' AStar failed: node limit {node_limit} reached.')
|
||||
return reconstruct_path(best_node) if return_partial else None
|
||||
return self._reconstruct_path(best_node) if return_partial else None
|
||||
|
||||
current = heapq.heappop(open_set)
|
||||
|
||||
# Best effort tracking
|
||||
if current.h_cost < best_node.h_cost:
|
||||
best_node = current
|
||||
|
||||
# Prune if already visited with a better path
|
||||
state = (int(current.port.x / snap), int(current.port.y / snap), int(current.port.orientation / 1.0))
|
||||
state = (int(round(current.port.x / snap)), int(round(current.port.y / snap)), int(round(current.port.orientation / 1.0)))
|
||||
if state in closed_set and closed_set[state] <= current.g_cost + 1e-6:
|
||||
continue
|
||||
closed_set[state] = current.g_cost
|
||||
|
|
@ -174,17 +161,18 @@ class AStarRouter:
|
|||
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
self.metrics['nodes_expanded'] += 1
|
||||
|
||||
# Check if we reached the target exactly
|
||||
if (abs(current.port.x - target.x) < 1e-6 and
|
||||
abs(current.port.y - target.y) < 1e-6 and
|
||||
abs(current.port.orientation - target.orientation) < 0.1):
|
||||
return reconstruct_path(current)
|
||||
return self._reconstruct_path(current)
|
||||
|
||||
# Expansion
|
||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set, snap, nodes_expanded)
|
||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set, snap, nodes_expanded, skip_congestion=skip_congestion)
|
||||
|
||||
return reconstruct_path(best_node) if return_partial else None
|
||||
return self._reconstruct_path(best_node) if return_partial else None
|
||||
|
||||
def _expand_moves(
|
||||
self,
|
||||
|
|
@ -196,108 +184,80 @@ class AStarRouter:
|
|||
closed_set: dict[tuple[int, int, int], float],
|
||||
snap: float = 1.0,
|
||||
nodes_expanded: int = 0,
|
||||
skip_congestion: bool = False,
|
||||
) -> None:
|
||||
# 1. Snap-to-Target Look-ahead
|
||||
dx_t = target.x - current.port.x
|
||||
dy_t = target.y - current.port.y
|
||||
dist_sq = dx_t*dx_t + dy_t*dy_t
|
||||
snap_dist = self.config.snap_to_target_dist
|
||||
|
||||
if dist_sq < snap_dist * snap_dist:
|
||||
# A. Try straight exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = numpy.radians(current.port.orientation)
|
||||
cos_r = numpy.cos(rad)
|
||||
sin_r = numpy.sin(rad)
|
||||
proj = dx_t * cos_r + dy_t * sin_r
|
||||
perp = -dx_t * sin_r + dy_t * cos_r
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation, snap_size=self.config.snap_size)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight', snap=snap)
|
||||
|
||||
# B. Try SBend exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = numpy.radians(current.port.orientation)
|
||||
cos_r = numpy.cos(rad)
|
||||
sin_r = numpy.sin(rad)
|
||||
proj = dx_t * cos_r + dy_t * sin_r
|
||||
perp = -dx_t * sin_r + dy_t * cos_r
|
||||
if proj > 0 and 0.5 <= abs(perp) < snap_dist:
|
||||
# Try a few candidate radii
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation,
|
||||
snap_size=self.config.snap_size
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius, snap=snap)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. Parametric Straights
|
||||
cp = current.port
|
||||
base_ori = round(cp.orientation, 2)
|
||||
state_key = (int(cp.x / snap), int(cp.y / snap), int(base_ori / 1.0))
|
||||
dx_t = target.x - cp.x
|
||||
dy_t = target.y - cp.y
|
||||
dist_sq = dx_t*dx_t + dy_t*dy_t
|
||||
|
||||
rad = numpy.radians(base_ori)
|
||||
cos_v, sin_v = numpy.cos(rad), numpy.sin(rad)
|
||||
# 1. DIRECT JUMP TO TARGET (Priority 1)
|
||||
proj_t = dx_t * cos_v + dy_t * sin_v
|
||||
perp_t = -dx_t * sin_v + dy_t * cos_v
|
||||
|
||||
# Ray cast to find max length
|
||||
# A. Straight Jump
|
||||
if proj_t > 0 and abs(perp_t) < 1e-3 and abs(cp.orientation - target.orientation) < 0.1:
|
||||
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, proj_t + 1.0)
|
||||
if max_reach >= proj_t - 0.01:
|
||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, skip_static=True, snap_to_grid=False)
|
||||
|
||||
# B. SBend Jump (if oriented correctly but offset)
|
||||
if proj_t > 0 and abs(cp.orientation - target.orientation) < 0.1 and abs(perp_t) > 1e-3:
|
||||
if proj_t < 200.0: # Only lookahead when close
|
||||
for radius in self.config.sbend_radii:
|
||||
if abs(perp_t) < 2 * radius:
|
||||
# Try to generate it
|
||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{perp_t}R{radius}', 'SB', (perp_t, radius), skip_congestion, snap_to_grid=False)
|
||||
|
||||
# In super sparse mode, we can return here, but A* needs other options for optimality.
|
||||
# return
|
||||
|
||||
# 2. VISIBILITY JUMPS & MAX REACH (Priority 2)
|
||||
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, self.config.max_straight_length)
|
||||
# Subtract buffer for bend radius + margin
|
||||
effective_max = max(self.config.min_straight_length, max_reach - 50.0) # Assume 50um bend radius
|
||||
|
||||
# Generate samples
|
||||
lengths = [effective_max]
|
||||
if self.config.num_straight_samples > 1 and effective_max > self.config.min_straight_length * 2:
|
||||
# Add intermediate step
|
||||
lengths.append(effective_max / 2.0)
|
||||
|
||||
# Add min length for maneuvering
|
||||
lengths.append(self.config.min_straight_length)
|
||||
|
||||
# Deduplicate and sort
|
||||
lengths = sorted(list(set(lengths)), reverse=True)
|
||||
straight_lengths = set()
|
||||
if max_reach > self.config.min_straight_length:
|
||||
# milestone 1: exactly at max_reach (touching)
|
||||
straight_lengths.add(snap_search_grid(max_reach, snap))
|
||||
# milestone 2: 10um before max_reach (space to turn)
|
||||
if max_reach > self.config.min_straight_length + 10.0:
|
||||
straight_lengths.add(snap_search_grid(max_reach - 10.0, snap))
|
||||
|
||||
for length in lengths:
|
||||
# Level 1: Absolute cache (exact location)
|
||||
abs_key = (state_key, 'S', length, net_width)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}', snap=snap)
|
||||
else:
|
||||
# Level 2: Relative cache (orientation only)
|
||||
rel_key = (base_ori, 'S', length, net_width, self._self_dilation)
|
||||
|
||||
# OPTIMIZATION: Check hard collision set BEFORE anything else
|
||||
move_type = f'S{length}'
|
||||
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
|
||||
if cache_key in self._hard_collision_set:
|
||||
continue
|
||||
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
# Fast check: would translated end port be in closed set?
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=self._self_dilation, snap_size=self.config.snap_size)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, snap=snap)
|
||||
visible_corners = self.visibility_manager.get_visible_corners(cp, max_dist=max_reach)
|
||||
for cx, cy, dist in visible_corners:
|
||||
proj = (cx - cp.x) * cos_v + (cy - cp.y) * sin_v
|
||||
if proj > self.config.min_straight_length:
|
||||
straight_lengths.add(snap_search_grid(proj, snap))
|
||||
|
||||
# ALWAYS include the min length for maneuvering
|
||||
straight_lengths.add(self.config.min_straight_length)
|
||||
|
||||
# If the jump is long, add an intermediate point to allow more flexible turning
|
||||
if max_reach > self.config.min_straight_length * 4:
|
||||
straight_lengths.add(snap_search_grid(max_reach / 2.0, snap))
|
||||
|
||||
# 3. Lattice Bends
|
||||
# Backwards pruning
|
||||
angle_to_target = numpy.degrees(numpy.arctan2(dy_t, dx_t))
|
||||
allow_backwards = (dist_sq < 200*200)
|
||||
# Target alignment logic (for turning towards target) - Keep this as it's high value
|
||||
if abs(base_ori % 180) < 0.1: # Horizontal
|
||||
target_dist = abs(target.x - cp.x)
|
||||
if target_dist <= max_reach and target_dist > self.config.min_straight_length:
|
||||
straight_lengths.add(snap_search_grid(target_dist, snap))
|
||||
else: # Vertical
|
||||
target_dist = abs(target.y - cp.y)
|
||||
if target_dist <= max_reach and target_dist > self.config.min_straight_length:
|
||||
straight_lengths.add(snap_search_grid(target_dist, snap))
|
||||
|
||||
# NO standard samples here! Only milestones.
|
||||
|
||||
for length in sorted(straight_lengths, reverse=True):
|
||||
# Trust ray_cast: these lengths are <= max_reach
|
||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{length}', 'S', (length,), skip_congestion, skip_static=True)
|
||||
|
||||
# 3. BENDS & SBENDS (Priority 3)
|
||||
angle_to_target = numpy.degrees(numpy.arctan2(target.y - cp.y, target.x - cp.x))
|
||||
allow_backwards = (dist_sq < 150*150)
|
||||
|
||||
for radius in self.config.bend_radii:
|
||||
for direction in ['CW', 'CCW']:
|
||||
|
|
@ -307,102 +267,84 @@ class AStarRouter:
|
|||
new_diff = (angle_to_target - new_ori + 180) % 360 - 180
|
||||
if abs(new_diff) > 135:
|
||||
continue
|
||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion)
|
||||
|
||||
move_type = f'B{radius}{direction}'
|
||||
abs_key = (state_key, 'B', radius, direction, net_width, self.config.bend_collision_type)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
|
||||
else:
|
||||
rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, self._self_dilation)
|
||||
|
||||
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
|
||||
if cache_key in self._hard_collision_set:
|
||||
continue
|
||||
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
res_rel = Bend90.generate(
|
||||
Port(0, 0, base_ori),
|
||||
radius,
|
||||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation,
|
||||
snap_size=self.config.snap_size
|
||||
)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
|
||||
if dist_sq < 400*400:
|
||||
offsets = set(self.config.sbend_offsets)
|
||||
dx_local = (target.x - cp.x) * cos_v + (target.y - cp.y) * sin_v
|
||||
dy_local = -(target.x - cp.x) * sin_v + (target.y - cp.y) * cos_v
|
||||
if 0 < dx_local < self.config.snap_to_target_dist:
|
||||
offsets.add(dy_local)
|
||||
|
||||
for offset in offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
if abs(offset) >= 2 * radius: continue
|
||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion)
|
||||
|
||||
# 4. Parametric SBends
|
||||
# Try both positive and negative offsets
|
||||
offsets = self.config.sbend_offsets
|
||||
def _process_move(
|
||||
self,
|
||||
parent: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: dict[tuple[int, int, int], float],
|
||||
snap: float,
|
||||
move_type: str,
|
||||
move_class: Literal['S', 'B', 'SB'],
|
||||
params: tuple,
|
||||
skip_congestion: bool,
|
||||
skip_static: bool = False,
|
||||
snap_to_grid: bool = True,
|
||||
) -> None:
|
||||
cp = parent.port
|
||||
base_ori = round(cp.orientation, 2)
|
||||
state_key = (int(round(cp.x / snap)), int(round(cp.y / snap)), int(round(base_ori / 1.0)))
|
||||
|
||||
# Dynamically add target alignment offset if within range
|
||||
# Project target onto current frame
|
||||
rad = numpy.radians(cp.orientation)
|
||||
dx_local = (target.x - cp.x) * numpy.cos(rad) + (target.y - cp.y) * numpy.sin(rad)
|
||||
dy_local = -(target.x - cp.x) * numpy.sin(rad) + (target.y - cp.y) * numpy.cos(rad)
|
||||
abs_key = (state_key, move_class, params, net_width, self.config.bend_collision_type, snap_to_grid)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
if move_class == 'B': move_radius = params[0]
|
||||
elif move_class == 'SB': move_radius = params[1]
|
||||
else: move_radius = None
|
||||
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion)
|
||||
return
|
||||
|
||||
rel_key = (base_ori, move_class, params, net_width, self.config.bend_collision_type, self._self_dilation, snap_to_grid)
|
||||
|
||||
if 0 < dx_local < snap_dist:
|
||||
# If target is ahead, try to align Y
|
||||
offsets = list(offsets) + [dy_local]
|
||||
offsets = sorted(list(set(offsets))) # Uniquify
|
||||
|
||||
for offset in offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
# Validity check: offset < 2*R
|
||||
if abs(offset) >= 2 * radius:
|
||||
continue
|
||||
|
||||
move_type = f'SB{offset}R{radius}'
|
||||
abs_key = (state_key, 'SB', offset, radius, net_width, self.config.bend_collision_type)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
|
||||
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width, snap_to_grid)
|
||||
if cache_key in self._hard_collision_set:
|
||||
return
|
||||
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (int(round(ex / snap)), int(round(ey / snap)), int(round(res_rel.end_port.orientation / 1.0)))
|
||||
if end_state in closed_set and closed_set[end_state] <= parent.g_cost + 1e-6:
|
||||
return
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
try:
|
||||
if move_class == 'S':
|
||||
res_rel = Straight.generate(Port(0, 0, base_ori), params[0], net_width, dilation=self._self_dilation, snap_to_grid=snap_to_grid, snap_size=self.config.snap_size)
|
||||
elif move_class == 'B':
|
||||
res_rel = Bend90.generate(Port(0, 0, base_ori), params[0], net_width, params[1], collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin, dilation=self._self_dilation, snap_to_grid=snap_to_grid, snap_size=self.config.snap_size)
|
||||
elif move_class == 'SB':
|
||||
res_rel = SBend.generate(Port(0, 0, base_ori), params[0], params[1], net_width, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin, dilation=self._self_dilation, snap_to_grid=snap_to_grid, snap_size=self.config.snap_size)
|
||||
else:
|
||||
rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, self._self_dilation)
|
||||
|
||||
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
|
||||
if cache_key in self._hard_collision_set:
|
||||
continue
|
||||
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
try:
|
||||
res_rel = SBend.generate(
|
||||
Port(0, 0, base_ori),
|
||||
offset,
|
||||
radius,
|
||||
width=net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation,
|
||||
snap_size=self.config.snap_size
|
||||
)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
except ValueError:
|
||||
continue
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
|
||||
return
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
except (ValueError, ZeroDivisionError):
|
||||
return
|
||||
|
||||
self._move_cache[abs_key] = res
|
||||
if move_class == 'B': move_radius = params[0]
|
||||
elif move_class == 'SB': move_radius = params[1]
|
||||
else: move_radius = None
|
||||
|
||||
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion)
|
||||
|
||||
def _add_node(
|
||||
self,
|
||||
|
|
@ -416,93 +358,78 @@ class AStarRouter:
|
|||
move_type: str,
|
||||
move_radius: float | None = None,
|
||||
snap: float = 1.0,
|
||||
skip_congestion: bool = False,
|
||||
) -> None:
|
||||
self.metrics['moves_generated'] += 1
|
||||
end_p = result.end_port
|
||||
state = (int(end_p.x / snap), int(end_p.y / snap), int(end_p.orientation / 1.0))
|
||||
# No need to check closed_set here as pop checks it, but it helps avoid push
|
||||
if state in closed_set and closed_set[state] <= parent.g_cost: # Conservative
|
||||
state = (int(round(end_p.x / snap)), int(round(end_p.y / snap)), int(round(end_p.orientation / 1.0)))
|
||||
|
||||
if state in closed_set and closed_set[state] <= parent.g_cost + 1e-6:
|
||||
self.metrics['pruned_closed_set'] += 1
|
||||
return
|
||||
|
||||
parent_p = parent.port
|
||||
cache_key = (
|
||||
int(parent_p.x / snap),
|
||||
int(parent_p.y / snap),
|
||||
int(parent_p.orientation / 1.0),
|
||||
move_type,
|
||||
net_width,
|
||||
)
|
||||
cache_key = (int(round(parent_p.x / snap)), int(round(parent_p.y / snap)), int(round(parent_p.orientation / 1.0)), move_type, net_width)
|
||||
|
||||
if cache_key in self._hard_collision_set:
|
||||
self.metrics['pruned_hard_collision'] += 1
|
||||
return
|
||||
|
||||
# Safe area check
|
||||
is_safe_area = False
|
||||
danger_map = self.cost_evaluator.danger_map
|
||||
if danger_map.get_cost(parent_p.x, parent_p.y) == 0 and danger_map.get_cost(end_p.x, end_p.y) == 0:
|
||||
if result.length < (danger_map.safety_threshold - self.cost_evaluator.collision_engine.clearance):
|
||||
is_safe_area = True
|
||||
|
||||
if not is_safe_area:
|
||||
hard_coll = False
|
||||
is_static_safe = (cache_key in self._static_safe_cache)
|
||||
if not is_static_safe:
|
||||
collision_engine = self.cost_evaluator.collision_engine
|
||||
for i, poly in enumerate(result.geometry):
|
||||
dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None
|
||||
if collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='static', start_port=parent_p, end_port=end_p,
|
||||
dilated_geometry=dil_poly
|
||||
):
|
||||
hard_coll = True
|
||||
break
|
||||
if hard_coll:
|
||||
self._hard_collision_set.add(cache_key)
|
||||
return
|
||||
# Fast check for straights
|
||||
if 'S' in move_type and 'SB' not in move_type:
|
||||
if collision_engine.check_move_straight_static(parent_p, result.length):
|
||||
self._hard_collision_set.add(cache_key)
|
||||
self.metrics['pruned_hard_collision'] += 1
|
||||
return
|
||||
is_static_safe = True
|
||||
|
||||
if not is_static_safe:
|
||||
if collision_engine.check_move_static(result, start_port=parent_p, end_port=end_p):
|
||||
self._hard_collision_set.add(cache_key)
|
||||
self.metrics['pruned_hard_collision'] += 1
|
||||
return
|
||||
else:
|
||||
self._static_safe_cache.add(cache_key)
|
||||
|
||||
# Congestion Check
|
||||
total_overlaps = 0
|
||||
if cache_key in self._congestion_cache:
|
||||
total_overlaps = self._congestion_cache[cache_key]
|
||||
else:
|
||||
collision_engine = self.cost_evaluator.collision_engine
|
||||
for i, poly in enumerate(result.geometry):
|
||||
dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None
|
||||
overlaps = collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
total_overlaps += overlaps
|
||||
self._congestion_cache[cache_key] = total_overlaps
|
||||
if not skip_congestion:
|
||||
if cache_key in self._congestion_cache:
|
||||
total_overlaps = self._congestion_cache[cache_key]
|
||||
else:
|
||||
total_overlaps = self.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
||||
self._congestion_cache[cache_key] = total_overlaps
|
||||
|
||||
penalty = 0.0
|
||||
if 'SB' in move_type:
|
||||
penalty = self.config.sbend_penalty
|
||||
elif 'B' in move_type:
|
||||
penalty = self.config.bend_penalty
|
||||
if 'SB' in move_type: penalty = self.config.sbend_penalty
|
||||
elif 'B' in move_type: penalty = self.config.bend_penalty
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
start_port=parent_p,
|
||||
length=result.length,
|
||||
dilated_geometry=result.dilated_geometry,
|
||||
penalty=penalty,
|
||||
skip_static=True, # Already checked
|
||||
skip_congestion=True, # Will add below
|
||||
result.geometry, result.end_port, net_width, net_id,
|
||||
start_port=parent_p, length=result.length,
|
||||
dilated_geometry=result.dilated_geometry, penalty=penalty,
|
||||
skip_static=True, skip_congestion=True
|
||||
)
|
||||
move_cost += total_overlaps * self.cost_evaluator.congestion_penalty
|
||||
|
||||
if move_cost > 1e12:
|
||||
self.metrics['pruned_cost'] += 1
|
||||
return
|
||||
|
||||
if 'B' in move_type and move_radius is not None:
|
||||
if 'B' in move_type and move_radius is not None and move_radius > 1e-6:
|
||||
move_cost *= (10.0 / move_radius)**0.5
|
||||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
if state in closed_set and closed_set[state] <= g_cost + 1e-6:
|
||||
self.metrics['pruned_closed_set'] += 1
|
||||
return
|
||||
|
||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||
|
||||
new_node = AStarNode(result.end_port, g_cost, h_cost, parent, result)
|
||||
heapq.heappush(open_set, new_node)
|
||||
self.metrics['moves_added'] += 1
|
||||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ class RouterConfig:
|
|||
snap_size: float = 5.0
|
||||
# Sparse Sampling Configuration
|
||||
max_straight_length: float = 2000.0
|
||||
num_straight_samples: int = 3
|
||||
min_straight_length: float = 10.0
|
||||
num_straight_samples: int = 5
|
||||
min_straight_length: float = 5.0
|
||||
|
||||
# Offsets for SBends (still list-based for now, or could range)
|
||||
sbend_offsets: list[float] = field(default_factory=lambda: [-100.0, -50.0, -10.0, 10.0, 50.0, 100.0])
|
||||
|
|
|
|||
|
|
@ -90,27 +90,10 @@ class CostEvaluator:
|
|||
dy = abs(current.y - target.y)
|
||||
dist = dx + dy
|
||||
|
||||
# Mandatory turn penalty:
|
||||
# If we need to change Y and we are facing East/West (or change X and facing North/South),
|
||||
# we MUST turn at least twice to reach the target with the same orientation.
|
||||
penalty = 0.0
|
||||
|
||||
# Check if we need to change "transverse" coordinate
|
||||
needs_transverse = False
|
||||
if abs(current.orientation % 180) < 0.1: # Horizontal
|
||||
if abs(dy) > 1e-3:
|
||||
needs_transverse = True
|
||||
else: # Vertical
|
||||
if abs(dx) > 1e-3:
|
||||
needs_transverse = True
|
||||
|
||||
if needs_transverse:
|
||||
# At least 2 bends needed. Radius 50 -> 78.5 each.
|
||||
# Plus bend_penalty (default 250 each).
|
||||
penalty += 2 * (78.5 + self.config.bend_penalty)
|
||||
elif abs(current.orientation - target.orientation) > 0.1:
|
||||
if abs(current.orientation - target.orientation) > 0.1:
|
||||
# Needs at least 1 bend
|
||||
penalty += 78.5 + self.config.bend_penalty
|
||||
penalty += 10.0 + self.config.bend_penalty * 0.1
|
||||
|
||||
return self.greedy_h_weight * (dist + penalty)
|
||||
|
||||
|
|
|
|||
|
|
@ -124,54 +124,73 @@ class PathFinder:
|
|||
self.cost_evaluator.collision_engine.remove_path(net_id)
|
||||
|
||||
# 2. Reroute with current congestion info
|
||||
# Tiered Strategy: use clipped_bbox for Iteration 0 for speed if target is arc.
|
||||
target_coll_model = self.router.config.bend_collision_type
|
||||
coll_model = target_coll_model
|
||||
if self.use_tiered_strategy and iteration == 0 and target_coll_model == "arc":
|
||||
coll_model = "clipped_bbox"
|
||||
skip_cong = False
|
||||
if self.use_tiered_strategy and iteration == 0:
|
||||
skip_cong = True
|
||||
if target_coll_model == "arc":
|
||||
coll_model = "clipped_bbox"
|
||||
|
||||
# Dynamic node limit: increase if it failed previously
|
||||
base_node_limit = self.router.config.node_limit
|
||||
current_node_limit = base_node_limit
|
||||
if net_id in results and not results[net_id].reached_target:
|
||||
current_node_limit = base_node_limit * (iteration + 1)
|
||||
|
||||
net_start = time.monotonic()
|
||||
# Store expanded only in the last potential iteration or if specifically requested
|
||||
do_store = store_expanded and (iteration == self.max_iterations - 1)
|
||||
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=do_store)
|
||||
|
||||
# Temporarily override node_limit
|
||||
original_limit = self.router.node_limit
|
||||
self.router.node_limit = current_node_limit
|
||||
|
||||
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=do_store, skip_congestion=skip_cong)
|
||||
|
||||
# Restore
|
||||
self.router.node_limit = original_limit
|
||||
|
||||
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
|
||||
|
||||
if path:
|
||||
# 3. Add to index
|
||||
# Check if reached exactly
|
||||
last_p = path[-1].end_port
|
||||
reached = (abs(last_p.x - target.x) < 1e-6 and
|
||||
abs(last_p.y - target.y) < 1e-6 and
|
||||
abs(last_p.orientation - target.orientation) < 0.1)
|
||||
|
||||
all_geoms = []
|
||||
all_dilated = []
|
||||
for res in path:
|
||||
all_geoms.extend(res.geometry)
|
||||
if res.dilated_geometry:
|
||||
all_dilated.extend(res.dilated_geometry)
|
||||
else:
|
||||
# Fallback dilation
|
||||
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
all_dilated.extend([p.buffer(dilation) for p in res.geometry])
|
||||
|
||||
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||
# 3. Add to index ONLY if it reached the target
|
||||
# (Prevents failed paths from blocking others forever)
|
||||
if reached:
|
||||
for res in path:
|
||||
all_geoms.extend(res.geometry)
|
||||
if res.dilated_geometry:
|
||||
all_dilated.extend(res.dilated_geometry)
|
||||
else:
|
||||
# Fallback dilation
|
||||
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
all_dilated.extend([p.buffer(dilation) for p in res.geometry])
|
||||
|
||||
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||
|
||||
# Check if this new path has any congestion
|
||||
collision_count = 0
|
||||
for i, poly in enumerate(all_geoms):
|
||||
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=all_dilated[i]
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
collision_count += overlaps
|
||||
# Always check for congestion to decide if more iterations are needed
|
||||
if reached:
|
||||
for i, poly in enumerate(all_geoms):
|
||||
overlaps = self.cost_evaluator.collision_engine.check_congestion(
|
||||
poly, net_id, dilated_geometry=all_dilated[i]
|
||||
)
|
||||
if overlaps > 0:
|
||||
collision_count += overlaps
|
||||
|
||||
if collision_count > 0:
|
||||
any_congestion = True
|
||||
|
||||
# Check if reached target
|
||||
reached = False
|
||||
if path:
|
||||
last_p = path[-1].end_port
|
||||
reached = (abs(last_p.x - target.x) < 1e-6 and
|
||||
abs(last_p.y - target.y) < 1e-6 and
|
||||
abs(last_p.orientation - target.orientation) < 0.1)
|
||||
|
||||
results[net_id] = RoutingResult(net_id, path, (collision_count == 0 and reached), collision_count, reached_target=reached)
|
||||
results[net_id] = RoutingResult(net_id, path, (reached and collision_count == 0), collision_count, reached_target=reached)
|
||||
else:
|
||||
results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False)
|
||||
any_congestion = True
|
||||
|
|
@ -180,7 +199,10 @@ class PathFinder:
|
|||
iteration_callback(iteration, results)
|
||||
|
||||
if not any_congestion:
|
||||
break
|
||||
# Check if all reached target
|
||||
all_reached = all(r.reached_target for r in results.values())
|
||||
if all_reached:
|
||||
break
|
||||
|
||||
# 4. Inflate congestion penalty
|
||||
self.cost_evaluator.congestion_penalty *= 1.5
|
||||
|
|
|
|||
125
inire/router/visibility.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy
|
||||
from typing import TYPE_CHECKING
|
||||
import rtree
|
||||
from shapely.geometry import Point, LineString
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
class VisibilityManager:
|
||||
"""
|
||||
Manages corners of static obstacles for sparse A* / Visibility Graph jumps.
|
||||
"""
|
||||
__slots__ = ('collision_engine', 'corners', 'corner_index', '_corner_graph', '_static_visibility_cache')
|
||||
|
||||
def __init__(self, collision_engine: CollisionEngine) -> None:
|
||||
self.collision_engine = collision_engine
|
||||
self.corners: list[tuple[float, float]] = []
|
||||
self.corner_index = rtree.index.Index()
|
||||
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
|
||||
self._static_visibility_cache: dict[tuple[int, int], list[tuple[float, float, float]]] = {}
|
||||
self._build()
|
||||
|
||||
def _build(self) -> None:
|
||||
"""
|
||||
Extract corners and pre-compute corner-to-corner visibility.
|
||||
"""
|
||||
raw_corners = []
|
||||
for obj_id, poly in self.collision_engine.static_dilated.items():
|
||||
coords = list(poly.exterior.coords)
|
||||
if coords[0] == coords[-1]:
|
||||
coords = coords[:-1]
|
||||
raw_corners.extend(coords)
|
||||
for ring in poly.interiors:
|
||||
coords = list(ring.coords)
|
||||
if coords[0] == coords[-1]:
|
||||
coords = coords[:-1]
|
||||
raw_corners.extend(coords)
|
||||
|
||||
if not raw_corners:
|
||||
return
|
||||
|
||||
# Deduplicate and snap to 1nm
|
||||
seen = set()
|
||||
for x, y in raw_corners:
|
||||
sx, sy = round(x, 3), round(y, 3)
|
||||
if (sx, sy) not in seen:
|
||||
seen.add((sx, sy))
|
||||
self.corners.append((sx, sy))
|
||||
|
||||
# Build spatial index for corners
|
||||
for i, (x, y) in enumerate(self.corners):
|
||||
self.corner_index.insert(i, (x, y, x, y))
|
||||
|
||||
# Pre-compute visibility graph between corners
|
||||
num_corners = len(self.corners)
|
||||
if num_corners > 200:
|
||||
# Limit pre-computation if too many corners
|
||||
return
|
||||
|
||||
for i in range(num_corners):
|
||||
self._corner_graph[i] = []
|
||||
p1 = Port(self.corners[i][0], self.corners[i][1], 0)
|
||||
for j in range(num_corners):
|
||||
if i == j: continue
|
||||
cx, cy = self.corners[j]
|
||||
dx, dy = cx - p1.x, cy - p1.y
|
||||
dist = numpy.sqrt(dx**2 + dy**2)
|
||||
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||
reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05)
|
||||
if reach >= dist - 0.01:
|
||||
self._corner_graph[i].append((cx, cy, dist))
|
||||
|
||||
def get_visible_corners(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
|
||||
"""
|
||||
Find all corners visible from the origin.
|
||||
Returns list of (x, y, distance).
|
||||
"""
|
||||
if max_dist < 0:
|
||||
return []
|
||||
|
||||
ox, oy = round(origin.x, 3), round(origin.y, 3)
|
||||
|
||||
# 1. Exact corner check
|
||||
# Use spatial index to find if origin is AT a corner
|
||||
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
|
||||
for idx in nearby:
|
||||
cx, cy = self.corners[idx]
|
||||
if abs(cx - ox) < 1e-4 and abs(cy - oy) < 1e-4:
|
||||
# We are at a corner! Return pre-computed graph (filtered by max_dist)
|
||||
if idx in self._corner_graph:
|
||||
return [c for c in self._corner_graph[idx] if c[2] <= max_dist]
|
||||
|
||||
# 2. Cache check for arbitrary points
|
||||
# Grid-based caching for arbitrary points is tricky,
|
||||
# but since static obstacles don't change, we can cache exact coordinates.
|
||||
cache_key = (int(ox * 1000), int(oy * 1000))
|
||||
if cache_key in self._static_visibility_cache:
|
||||
return self._static_visibility_cache[cache_key]
|
||||
|
||||
# 3. Full visibility check
|
||||
bounds = (origin.x - max_dist, origin.y - max_dist, origin.x + max_dist, origin.y + max_dist)
|
||||
candidates = list(self.corner_index.intersection(bounds))
|
||||
|
||||
visible = []
|
||||
for i in candidates:
|
||||
cx, cy = self.corners[i]
|
||||
dx, dy = cx - origin.x, cy - origin.y
|
||||
dist = numpy.sqrt(dx**2 + dy**2)
|
||||
|
||||
if dist > max_dist or dist < 1e-3:
|
||||
continue
|
||||
|
||||
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||
reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05)
|
||||
if reach >= dist - 0.01:
|
||||
visible.append((cx, cy, dist))
|
||||
|
||||
self._static_visibility_cache[cache_key] = visible
|
||||
return visible
|
||||
|
|
@ -123,9 +123,9 @@ def test_arc_sagitta_precision() -> None:
|
|||
width = 2.0
|
||||
|
||||
# Coarse: 1um sagitta
|
||||
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
|
||||
res_coarse = Bend90.generate(start, radius, width, direction="CCW", sagitta=1.0)
|
||||
# Fine: 0.01um (10nm) sagitta
|
||||
res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
|
||||
res_fine = Bend90.generate(start, radius, width, direction="CCW", sagitta=0.01)
|
||||
|
||||
# Number of segments should be significantly higher for fine
|
||||
# Exterior points = (segments + 1) * 2
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
|||
|
||||
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0])
|
||||
router = AStarRouter(basic_evaluator, snap_size=1.0, bend_radii=[5.0, 10.0], sbend_radii=[5.0, 10.0])
|
||||
# Increase base penalty to force detour immediately
|
||||
pf = PathFinder(router, basic_evaluator, max_iterations=10, base_congestion_penalty=1000.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ def test_arc_resolution_sagitta() -> None:
|
|||
start = Port(0, 0, 0)
|
||||
# R=10, 90 deg bend.
|
||||
# High tolerance (0.5um) -> few segments
|
||||
res_coarse = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.5)
|
||||
# Low tolerance (0.001um = 1nm) -> many segments
|
||||
res_fine = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.001)
|
||||
res_coarse = Bend90.generate(start, radius=10.0, width=2.0, direction="CCW", sagitta=0.5)
|
||||
# Low tolerance (1nm) -> many segments
|
||||
res_fine = Bend90.generate(start, radius=10.0, width=2.0, direction="CCW", sagitta=0.001)
|
||||
|
||||
|
||||
# Check number of points in the polygon exterior
|
||||
# (num_segments + 1) * 2 points usually
|
||||
|
|
@ -28,7 +29,7 @@ def test_locked_paths() -> None:
|
|||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
router = AStarRouter(evaluator, bend_radii=[5.0, 10.0], sbend_radii=[5.0, 10.0])
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 1. Route Net A
|
||||
|
|
|
|||