rework structure of everything
This commit is contained in:
parent
dcc4d6436c
commit
941d3e01df
64 changed files with 3819 additions and 3559 deletions
210
DOCS.md
210
DOCS.md
|
|
@ -1,107 +1,159 @@
|
||||||
# Inire Configuration & API Documentation
|
# Inire Configuration & API Documentation
|
||||||
|
|
||||||
This document describes the user-tunable parameters for the `inire` auto-router.
|
This document describes the current public API for `inire`.
|
||||||
|
|
||||||
## 1. AStarContext Parameters
|
## 1. Primary API
|
||||||
|
|
||||||
The `AStarContext` stores the configuration and persistent state for the A* search. It is initialized once and passed to `route_astar` or the `PathFinder`.
|
### `RoutingProblem`
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
`RoutingProblem` describes the physical routing problem:
|
||||||
| :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ |
|
|
||||||
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. |
|
|
||||||
| `max_straight_length` | `float` | 2000.0 | Maximum length (µm) of a single straight segment. |
|
|
||||||
| `min_straight_length` | `float` | 5.0 | Minimum length (µm) of a single straight segment. |
|
|
||||||
| `bend_radii` | `list[float]` | `[50.0, 100.0]` | Available radii for 90-degree turns (µm). |
|
|
||||||
| `sbend_radii` | `list[float]` | `[5.0, 10.0, 50.0, 100.0]` | Available radii for S-bends (µm). |
|
|
||||||
| `sbend_offsets` | `list[float] \| None` | `None` (Auto) | Lateral offsets for parametric S-bends. |
|
|
||||||
| `bend_penalty` | `float` | 250.0 | Flat cost added for every 90-degree bend. |
|
|
||||||
| `sbend_penalty` | `float` | 500.0 | Flat cost added for every S-bend. |
|
|
||||||
| `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"` (an 8-point conservative arc proxy). |
|
|
||||||
| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide for clipped models. |
|
|
||||||
| `visibility_guidance` | `str` | `"tangent_corner"` | Visibility-driven straight candidate mode: `"off"`, `"exact_corner"`, or `"tangent_corner"`. |
|
|
||||||
|
|
||||||
## 2. AStarMetrics
|
- `bounds`
|
||||||
|
- `nets`
|
||||||
|
- `static_obstacles`
|
||||||
|
- `locked_routes`
|
||||||
|
- `clearance`
|
||||||
|
- `max_net_width`
|
||||||
|
- `safety_zone_radius`
|
||||||
|
|
||||||
The `AStarMetrics` object collects performance data during the search.
|
### `RoutingOptions`
|
||||||
|
|
||||||
| Property | Type | Description |
|
`RoutingOptions` groups all expert controls for the routing engine:
|
||||||
| :--------------------- | :---- | :---------------------------------------------------- |
|
|
||||||
| `nodes_expanded` | `int` | Number of nodes expanded in the last `route_astar` call. |
|
|
||||||
| `total_nodes_expanded` | `int` | Cumulative nodes expanded across all calls. |
|
|
||||||
| `max_depth_reached` | `int` | Deepest point in the search tree reached. |
|
|
||||||
|
|
||||||
---
|
- `search`
|
||||||
|
- `objective`
|
||||||
|
- `congestion`
|
||||||
|
- `refinement`
|
||||||
|
- `diagnostics`
|
||||||
|
|
||||||
## 3. CostEvaluator Parameters
|
Route a problem with:
|
||||||
|
|
||||||
The `CostEvaluator` defines the "goodness" of a path.
|
```python
|
||||||
|
run = route(problem, options=options)
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
If you omit `options`, `route(problem)` uses `RoutingOptions()` defaults.
|
||||||
| :------------------- | :------ | :--------- | :--------------------------------------------------------------------------------------- |
|
|
||||||
| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. |
|
|
||||||
| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g. `1.5`) speed up search. |
|
|
||||||
| `congestion_penalty` | `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. |
|
|
||||||
|
|
||||||
---
|
### Incremental routing with `LockedRoute`
|
||||||
|
|
||||||
## 3. PathFinder Parameters
|
For incremental workflows, route one problem, convert a result into a `LockedRoute`, and feed it into the next problem:
|
||||||
|
|
||||||
The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm.
|
```python
|
||||||
|
run_a = route(problem_a)
|
||||||
|
problem_b = RoutingProblem(
|
||||||
|
bounds=problem_a.bounds,
|
||||||
|
nets=(...),
|
||||||
|
locked_routes={"netA": run_a.results_by_net["netA"].as_locked_route()},
|
||||||
|
)
|
||||||
|
run_b = route(problem_b)
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
`LockedRoute` stores canonical physical geometry only. The next run applies its own clearance rules when treating it as a static obstacle.
|
||||||
| :------------------------ | :------ | :------ | :-------------------------------------------------------------------------------------- |
|
|
||||||
| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. |
|
|
||||||
| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.|
|
|
||||||
| `refine_paths` | `bool` | `True` | Run the post-route path simplifier that removes unnecessary bend ladders when it finds a valid lower-cost replacement. |
|
|
||||||
|
|
||||||
---
|
## 2. Search Options
|
||||||
|
|
||||||
## 4. CollisionEngine Parameters
|
`RoutingOptions.search` is a `SearchOptions` object.
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Field | Default | Description |
|
||||||
| :------------------- | :------ | :--------- | :------------------------------------------------------------------------------------ |
|
| :-- | :-- | :-- |
|
||||||
| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). |
|
| `node_limit` | `1_000_000` | Maximum number of states to explore per net. |
|
||||||
| `safety_zone_radius` | `float` | 0.0021 | Radius (µm) around ports where collisions are ignored for PDK boundary incidence. |
|
| `max_straight_length` | `2000.0` | Maximum length of a single straight segment. |
|
||||||
|
| `min_straight_length` | `5.0` | Minimum length of a single straight segment. |
|
||||||
|
| `greedy_h_weight` | `1.5` | Heuristic weight. `1.0` is optimal but slower. |
|
||||||
|
| `bend_radii` | `(50.0, 100.0)` | Available radii for 90-degree bends. |
|
||||||
|
| `sbend_radii` | `(10.0,)` | Available radii for S-bends. |
|
||||||
|
| `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. |
|
||||||
|
| `bend_collision_type` | `"arc"` | Bend collision model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or a custom polygon. |
|
||||||
|
| `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. |
|
||||||
|
| `initial_paths` | `None` | Optional user-supplied initial paths for warm starts. |
|
||||||
|
|
||||||
---
|
## 3. Objective Weights
|
||||||
|
|
||||||
## 4. Physical Units & Precision
|
`RoutingOptions.objective` and `RoutingOptions.refinement.objective` use `ObjectiveWeights`.
|
||||||
- **Coordinates**: Micrometers (µm).
|
|
||||||
- **Grid Snapping**: The router internally operates on a **1nm** grid for final ports and a **1µm** lattice for expansion moves.
|
|
||||||
- **Search Space**: Assumptions are optimized for design areas up to **20mm x 20mm**.
|
|
||||||
- **Design Bounds**: The boundary limits defined in `DangerMap` strictly constrain the **physical edges** (dilated geometry) of the waveguide. Any move that would cause the waveguide or its required clearance to extend beyond these bounds is rejected with an infinite cost.
|
|
||||||
|
|
||||||
---
|
| Field | Default | Description |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `unit_length_cost` | `1.0` | Cost per unit length. |
|
||||||
|
| `bend_penalty` | `250.0` | Flat bend penalty before radius scaling. |
|
||||||
|
| `sbend_penalty` | `500.0` | Flat S-bend penalty. |
|
||||||
|
| `danger_weight` | `1.0` | Weight applied to danger-map proximity costs. |
|
||||||
|
| `congestion_penalty` | `0.0` | Congestion weight used when explicitly scoring complete paths. |
|
||||||
|
|
||||||
## 5. Best Practices & Tuning Advice
|
## 4. Congestion Options
|
||||||
|
|
||||||
### Speed vs. Optimality
|
`RoutingOptions.congestion` is a `CongestionOptions` object.
|
||||||
The `greedy_h_weight` is your primary lever for search performance.
|
|
||||||
- **`1.0`**: Dijkstra-like behavior. Guarantees the shortest path but is very slow.
|
|
||||||
- **`1.1` to `1.2`**: Recommended range. Balances wire length with fast convergence.
|
|
||||||
- **`> 1.5`**: Extremely fast "greedy" search. May produce zig-zags or suboptimal detours.
|
|
||||||
|
|
||||||
### Avoiding "Zig-Zags"
|
| Field | Default | Description |
|
||||||
If the router produces many small bends instead of a long straight line:
|
| :-- | :-- | :-- |
|
||||||
1. Increase `bend_penalty` (e.g., set to `100.0` or higher).
|
| `max_iterations` | `10` | Maximum rip-up and reroute iterations. |
|
||||||
2. Increase available `bend_radii` if larger turns are physically acceptable.
|
| `base_penalty` | `100.0` | Starting overlap penalty for negotiated congestion. |
|
||||||
3. Decrease `greedy_h_weight` closer to `1.0`.
|
| `multiplier` | `1.5` | Multiplier applied after an iteration still needs retries. |
|
||||||
|
| `use_tiered_strategy` | `True` | Use cheaper collision proxies in the first pass when applicable. |
|
||||||
|
| `warm_start` | `"shortest"` | Optional greedy warm-start ordering. |
|
||||||
|
| `shuffle_nets` | `False` | Shuffle routing order between iterations. |
|
||||||
|
| `sort_nets` | `None` | Optional deterministic routing order. |
|
||||||
|
| `seed` | `None` | RNG seed for shuffled routing order. |
|
||||||
|
|
||||||
### Visibility Guidance
|
## 5. Refinement Options
|
||||||
The router can bias straight stop points using static obstacle corners.
|
|
||||||
- **`"tangent_corner"`**: Default. Proposes straight lengths that set up a clean tangent bend around nearby visible corners. This helps obstacle-dense layouts more than open space.
|
|
||||||
- **`"exact_corner"`**: Only uses precomputed corner-to-corner visibility when the current search state already lands on an obstacle corner.
|
|
||||||
- **`"off"`**: Disables visibility-derived straight candidates entirely.
|
|
||||||
The arbitrary-point visibility scan remains available for diagnostics, but the router hot path intentionally uses the exact-corner / tangent-corner forms only.
|
|
||||||
|
|
||||||
### Handling Congestion
|
`RoutingOptions.refinement` is a `RefinementOptions` object.
|
||||||
In multi-net designs, if nets are overlapping:
|
|
||||||
1. Increase `congestion_penalty` in `CostEvaluator`.
|
|
||||||
2. Increase `max_iterations` in `PathFinder`.
|
|
||||||
3. If a solution is still not found, check if the `clearance` is physically possible given the design's narrowest bottlenecks.
|
|
||||||
|
|
||||||
### S-Bend Usage
|
| Field | Default | Description |
|
||||||
Parametric S-bends bridge lateral gaps without changing the waveguide's orientation.
|
| :-- | :-- | :-- |
|
||||||
- **Automatic Selection**: If `sbend_offsets` is set to `None` (the default), the router automatically chooses from a set of "natural" offsets (Fibonacci-aligned grid steps) and the offset needed to hit the target.
|
| `enabled` | `True` | Enable post-route refinement. |
|
||||||
- **Specific Offsets**: To use specific offsets (e.g., 5.86µm for a 45° switchover), provide them in the `sbend_offsets` list. The router will prioritize these but will still try to align with the target if possible.
|
| `objective` | `None` | Optional override objective for refinement. `None` reuses the search objective. |
|
||||||
- **Constraints**: S-bends are only used for offsets $O < 2R$. For larger shifts, the router naturally combines two 90° bends and a straight segment.
|
|
||||||
|
## 6. Diagnostics Options
|
||||||
|
|
||||||
|
`RoutingOptions.diagnostics` is a `DiagnosticsOptions` object.
|
||||||
|
|
||||||
|
| Field | Default | Description |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `capture_expanded` | `False` | Record expanded nodes for diagnostics and visualization. |
|
||||||
|
|
||||||
|
## 7. RouteMetrics
|
||||||
|
|
||||||
|
`RoutingRunResult.metrics` is an immutable per-run snapshot.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `nodes_expanded` | `int` | Total nodes expanded during the run. |
|
||||||
|
| `moves_generated` | `int` | Total candidate moves generated during the run. |
|
||||||
|
| `moves_added` | `int` | Total candidate moves admitted to the open set during the run. |
|
||||||
|
| `pruned_closed_set` | `int` | Total moves pruned because the state was already closed at lower cost. |
|
||||||
|
| `pruned_hard_collision` | `int` | Total moves pruned by hard collision checks. |
|
||||||
|
| `pruned_cost` | `int` | Total moves pruned by cost ceilings or invalid costs. |
|
||||||
|
|
||||||
|
## 8. Internal Modules
|
||||||
|
|
||||||
|
Lower-level search and collision modules are internal implementation details. The supported entrypoint is `route(problem, options=...)`.
|
||||||
|
|
||||||
|
## 9. Tuning Notes
|
||||||
|
|
||||||
|
### Speed vs. optimality
|
||||||
|
|
||||||
|
- Lower `search.greedy_h_weight` toward `1.0` for better optimality.
|
||||||
|
- Raise `search.greedy_h_weight` for faster, greedier routing.
|
||||||
|
|
||||||
|
### Congestion handling
|
||||||
|
|
||||||
|
- Increase `congestion.base_penalty` to separate nets more aggressively in the first iteration.
|
||||||
|
- Increase `congestion.max_iterations` if congestion needs more reroute passes.
|
||||||
|
- Increase `congestion.multiplier` if later iterations need to escalate more quickly.
|
||||||
|
|
||||||
|
### Bend-heavy routes
|
||||||
|
|
||||||
|
- Increase `objective.bend_penalty` to discourage ladders of small bends.
|
||||||
|
- Increase available `search.bend_radii` when larger turns are physically acceptable.
|
||||||
|
|
||||||
|
### Visibility guidance
|
||||||
|
|
||||||
|
- `"tangent_corner"` is the default and best general-purpose setting in obstacle-dense layouts.
|
||||||
|
- `"exact_corner"` is more conservative.
|
||||||
|
- `"off"` disables visibility-derived straight candidates.
|
||||||
|
|
||||||
|
### S-bends
|
||||||
|
|
||||||
|
- Leave `search.sbend_offsets=None` to let the router derive natural offsets automatically.
|
||||||
|
- Provide explicit `search.sbend_offsets` for known process-preferred offsets.
|
||||||
|
- S-bends are only used for offsets smaller than `2R`.
|
||||||
|
|
|
||||||
54
README.md
54
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# inire: Auto-Routing for Photonic and RF Integrated Circuits
|
# inire: Auto-Routing for Photonic and RF Integrated Circuits
|
||||||
|
|
||||||
`inire` is a high-performance auto-router designed specifically for the physical constraints of photonic and RF integrated circuits. It utilizes a Hybrid State-Lattice A* search combined with "Negotiated Congestion" (PathFinder) to route multiple nets while maintaining strict geometric fidelity and clearance.
|
`inire` is a high-performance auto-router designed specifically for the physical constraints of photonic and RF integrated circuits. It uses a Hybrid State-Lattice A* search combined with negotiated congestion to route multiple nets while maintaining strict geometric fidelity and clearance.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
* **Analytic Correctness**: Every move is verified against an R-Tree spatial index of obstacles and other paths.
|
* **Analytic Correctness**: Every move is verified against an R-Tree spatial index of obstacles and other paths.
|
||||||
* **1nm Precision**: All coordinates and ports are snapped to a 1nm manufacturing grid.
|
* **1nm Precision**: All coordinates and ports are snapped to a 1nm manufacturing grid.
|
||||||
* **Safety & Proximity**: Incorporates a "Danger Map" (pre-computed distance transform) to maintain optimal spacing and reduce crosstalk.
|
* **Safety & Proximity**: Incorporates a "Danger Map" (pre-computed distance transform) to maintain optimal spacing and reduce crosstalk.
|
||||||
* **Locked Paths**: Supports treating existing geometries as fixed obstacles for incremental routing sessions.
|
* **Locked Routes**: Supports treating prior routed nets as fixed obstacles in later runs.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -26,42 +26,32 @@ pip install numpy scipy shapely rtree matplotlib
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from inire.geometry.primitives import Port
|
from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.astar import AStarContext
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
|
|
||||||
# 1. Setup Environment
|
problem = RoutingProblem(
|
||||||
engine = CollisionEngine(clearance=2.0)
|
bounds=(0, 0, 1000, 1000),
|
||||||
danger_map = DangerMap(bounds=(0, 0, 1000, 1000))
|
nets=(
|
||||||
danger_map.precompute([]) # Add polygons here for obstacles
|
NetSpec("net1", Port(0, 0, 0), Port(100, 50, 0), width=2.0),
|
||||||
|
),
|
||||||
# 2. Configure Router
|
|
||||||
evaluator = CostEvaluator(
|
|
||||||
collision_engine=engine,
|
|
||||||
danger_map=danger_map,
|
|
||||||
greedy_h_weight=1.2
|
|
||||||
)
|
)
|
||||||
context = AStarContext(
|
options = RoutingOptions(
|
||||||
cost_evaluator=evaluator,
|
search=SearchOptions(
|
||||||
bend_penalty=10.0
|
bend_radii=(50.0, 100.0),
|
||||||
|
greedy_h_weight=1.2,
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=10.0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
pf = PathFinder(context)
|
|
||||||
|
|
||||||
# 3. Define Netlist
|
run = route(problem, options=options)
|
||||||
netlist = {
|
|
||||||
"net1": (Port(0, 0, 0), Port(100, 50, 0)),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. Route
|
if run.results_by_net["net1"].is_valid:
|
||||||
results = pf.route_all(netlist, {"net1": 2.0})
|
|
||||||
|
|
||||||
if results["net1"].is_valid:
|
|
||||||
print("Successfully routed net1!")
|
print("Successfully routed net1!")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For incremental workflows, feed prior routed results back into a new `RoutingProblem` via `locked_routes` using `RoutingResult.as_locked_route()`.
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
For detailed visual demonstrations and architectural deep-dives, see the **[Examples README](examples/README.md)**.
|
For detailed visual demonstrations and architectural deep-dives, see the **[Examples README](examples/README.md)**.
|
||||||
|
|
@ -82,11 +72,11 @@ Full documentation for all user-tunable parameters, cost functions, and collisio
|
||||||
2. **90° Bends**: Fixed-radius PDK cells.
|
2. **90° Bends**: Fixed-radius PDK cells.
|
||||||
3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$).
|
3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$).
|
||||||
|
|
||||||
For multi-net problems, the **PathFinder** loop handles rip-up and reroute logic, ensuring that paths find the globally optimal configuration without crossings.
|
For multi-net problems, the negotiated-congestion loop handles rip-up and reroute logic, ensuring that paths find the globally optimal configuration without crossings.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`inire` is highly tunable. Every major component (Router, CostEvaluator, PathFinder) accepts explicit named arguments in its constructor to control expansion rules, cost weights, and convergence limits. See `DOCS.md` for a full parameter reference.
|
`inire` is highly tunable. The public API is `RoutingProblem` plus `RoutingOptions`, routed via `route(problem, options=...)`. Search internals remain available only for internal tests and development work; they are not a supported integration surface. See `DOCS.md` for a full parameter reference.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,29 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 01: Simple Route...")
|
print("Running Example 01: Simple Route...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
# We define a 100um x 100um routing area
|
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 100, 100)
|
||||||
|
|
||||||
# Clearance of 2.0um between waveguides
|
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
|
|
||||||
# Precompute DangerMap for heuristic speedup
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute([]) # No obstacles yet
|
|
||||||
|
|
||||||
# 2. Configure Router
|
|
||||||
evaluator = CostEvaluator(engine, danger_map)
|
|
||||||
context = AStarContext(evaluator, bend_radii=[10.0])
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics)
|
|
||||||
|
|
||||||
# 3. Define Netlist
|
|
||||||
# Start at (10, 50) pointing East (0 deg)
|
|
||||||
# Target at (90, 50) pointing East (0 deg)
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"net1": (Port(10, 50, 0), Port(90, 50, 0)),
|
"net1": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 2.0}
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=(NetSpec("net1", *netlist["net1"], width=2.0),),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(search=SearchOptions(bend_radii=(10.0,)))
|
||||||
|
|
||||||
# 4. Route
|
run = route(problem, options=options)
|
||||||
results = pf.route_all(netlist, net_widths)
|
result = run.results_by_net["net1"]
|
||||||
|
if result.is_valid:
|
||||||
# 5. Check Results
|
|
||||||
res = results["net1"]
|
|
||||||
if res.is_valid:
|
|
||||||
print("Success! Route found.")
|
print("Success! Route found.")
|
||||||
print(f"Path collisions: {res.collisions}")
|
print(f"Path collisions: {result.collisions}")
|
||||||
else:
|
else:
|
||||||
print("Failed to find route.")
|
print("Failed to find route.")
|
||||||
|
|
||||||
# 6. Visualize
|
fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist)
|
||||||
# plot_routing_results takes a dict of RoutingResult objects
|
|
||||||
fig, ax = plot_routing_results(results, [], bounds)
|
|
||||||
fig.savefig("examples/01_simple_route.png")
|
fig.savefig("examples/01_simple_route.png")
|
||||||
print("Saved plot to examples/01_simple_route.png")
|
print("Saved plot to examples/01_simple_route.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,41 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import CongestionOptions, NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
|
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 100, 100)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute([])
|
|
||||||
|
|
||||||
# Configure a router with high congestion penalties
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0)
|
|
||||||
context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0])
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics, base_congestion_penalty=1000.0)
|
|
||||||
|
|
||||||
# 2. Define Netlist
|
|
||||||
# Three nets that must cross each other in a small area
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
||||||
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
||||||
}
|
}
|
||||||
net_widths = {nid: 2.0 for nid in netlist}
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
sbend_radii=(10.0,),
|
||||||
|
greedy_h_weight=1.5,
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=250.0,
|
||||||
|
sbend_penalty=500.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(base_penalty=1000.0),
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Route
|
run = route(problem, options=options)
|
||||||
# PathFinder uses Negotiated Congestion to resolve overlaps iteratively
|
all_valid = all(result.is_valid for result in run.results_by_net.values())
|
||||||
results = pf.route_all(netlist, net_widths)
|
|
||||||
|
|
||||||
# 4. Check Results
|
|
||||||
all_valid = all(res.is_valid for res in results.values())
|
|
||||||
if all_valid:
|
if all_valid:
|
||||||
print("Success! Congestion resolved for all nets.")
|
print("Success! Congestion resolved for all nets.")
|
||||||
else:
|
else:
|
||||||
print("Failed to resolve congestion for some nets.")
|
print("Failed to resolve congestion for some nets.")
|
||||||
|
|
||||||
# 5. Visualize
|
fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist)
|
||||||
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
|
||||||
fig.savefig("examples/02_congestion_resolution.png")
|
fig.savefig("examples/02_congestion_resolution.png")
|
||||||
print("Saved plot to examples/02_congestion_resolution.png")
|
print("Saved plot to examples/02_congestion_resolution.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,37 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 03: Locked Paths...")
|
print("Running Example 03: Locked Routes...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, -50, 100, 50)
|
bounds = (0, -50, 100, 50)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
options = RoutingOptions(
|
||||||
danger_map = DangerMap(bounds=bounds)
|
search=SearchOptions(bend_radii=(10.0,)),
|
||||||
danger_map.precompute([])
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=250.0,
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
sbend_penalty=500.0,
|
||||||
context = AStarContext(evaluator, bend_radii=[10.0])
|
),
|
||||||
metrics = AStarMetrics()
|
)
|
||||||
pf = PathFinder(context, metrics)
|
|
||||||
|
|
||||||
# 2. Route Net A and 'Lock' it
|
|
||||||
# Net A is a straight path blocking the direct route for Net B
|
|
||||||
print("Routing initial net...")
|
print("Routing initial net...")
|
||||||
netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))}
|
results_a = route(
|
||||||
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),),
|
||||||
|
),
|
||||||
|
options=options,
|
||||||
|
).results_by_net
|
||||||
|
|
||||||
# Locking prevents Net A from being removed or rerouted during NC iterations
|
|
||||||
engine.lock_net("netA")
|
|
||||||
print("Initial net locked as static obstacle.")
|
|
||||||
|
|
||||||
# 3. Route Net B (forced to detour)
|
|
||||||
print("Routing detour net around locked path...")
|
print("Routing detour net around locked path...")
|
||||||
netlist_b = {"netB": (Port(50, -20, 90), Port(50, 20, 90))}
|
results_b = route(
|
||||||
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),),
|
||||||
|
locked_routes={"netA": results_a["netA"].as_locked_route()},
|
||||||
|
),
|
||||||
|
options=options,
|
||||||
|
).results_by_net
|
||||||
|
|
||||||
# 4. Visualize
|
|
||||||
results = {**results_a, **results_b}
|
results = {**results_a, **results_b}
|
||||||
fig, ax = plot_routing_results(results, [], bounds)
|
fig, ax = plot_routing_results(results, [], bounds)
|
||||||
fig.savefig("examples/03_locked_paths.png")
|
fig.savefig("examples/03_locked_paths.png")
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,38 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 04: S-Bends and Multiple Radii...")
|
print("Running Example 04: S-Bends and Multiple Radii...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 100, 100)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute([])
|
|
||||||
|
|
||||||
# 2. Configure Router
|
|
||||||
evaluator = CostEvaluator(
|
|
||||||
engine,
|
|
||||||
danger_map,
|
|
||||||
unit_length_cost=1.0,
|
|
||||||
bend_penalty=10.0,
|
|
||||||
sbend_penalty=20.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
context = AStarContext(
|
|
||||||
evaluator,
|
|
||||||
node_limit=50000,
|
|
||||||
bend_radii=[10.0, 30.0],
|
|
||||||
sbend_offsets=[5.0], # Use a simpler offset
|
|
||||||
bend_penalty=10.0,
|
|
||||||
sbend_penalty=20.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics)
|
|
||||||
|
|
||||||
# 3. Define Netlist
|
|
||||||
# start (10, 50), target (60, 55) -> 5um offset
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
||||||
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
||||||
}
|
}
|
||||||
net_widths = {"sbend_only": 2.0, "multi_radii": 2.0}
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
node_limit=50000,
|
||||||
|
bend_radii=(10.0, 30.0),
|
||||||
|
sbend_offsets=(5.0,),
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
unit_length_cost=1.0,
|
||||||
|
bend_penalty=10.0,
|
||||||
|
sbend_penalty=20.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# 4. Route
|
run = route(problem, options=options)
|
||||||
results = pf.route_all(netlist, net_widths)
|
for net_id, result in run.results_by_net.items():
|
||||||
|
status = "Success" if result.is_valid else "Failed"
|
||||||
|
print(f"{net_id}: {status}, collisions={result.collisions}")
|
||||||
|
|
||||||
# 5. Check Results
|
fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist)
|
||||||
for nid, res in results.items():
|
|
||||||
status = "Success" if res.is_valid else "Failed"
|
|
||||||
print(f"{nid}: {status}, collisions={res.collisions}")
|
|
||||||
|
|
||||||
# 6. Visualize
|
|
||||||
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
|
||||||
fig.savefig("examples/04_sbends_and_radii.png")
|
fig.savefig("examples/04_sbends_and_radii.png")
|
||||||
print("Saved plot to examples/04_sbends_and_radii.png")
|
print("Saved plot to examples/04_sbends_and_radii.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,32 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 05: Orientation Stress Test...")
|
print("Running Example 05: Orientation Stress Test...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 200, 200)
|
bounds = (0, 0, 200, 200)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute([])
|
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0)
|
|
||||||
context = AStarContext(evaluator, bend_radii=[20.0])
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics)
|
|
||||||
|
|
||||||
# 2. Define Netlist
|
|
||||||
# Challenging orientation combinations
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
||||||
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
||||||
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
||||||
}
|
}
|
||||||
net_widths = {nid: 2.0 for nid in netlist}
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(bend_radii=(20.0,)),
|
||||||
|
objective=ObjectiveWeights(bend_penalty=50.0),
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Route
|
|
||||||
print("Routing complex orientation nets...")
|
print("Routing complex orientation nets...")
|
||||||
results = pf.route_all(netlist, net_widths)
|
run = route(problem, options=options)
|
||||||
|
for net_id, result in run.results_by_net.items():
|
||||||
|
status = "Success" if result.is_valid else "Failed"
|
||||||
|
print(f" {net_id}: {status}")
|
||||||
|
|
||||||
# 4. Check Results
|
fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist)
|
||||||
for nid, res in results.items():
|
|
||||||
status = "Success" if res.is_valid else "Failed"
|
|
||||||
print(f" {nid}: {status}")
|
|
||||||
|
|
||||||
# 5. Visualize
|
|
||||||
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
|
||||||
fig.savefig("examples/05_orientation_stress.png")
|
fig.savefig("examples/05_orientation_stress.png")
|
||||||
print("Saved plot to examples/05_orientation_stress.png")
|
print("Saved plot to examples/05_orientation_stress.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,34 +11,30 @@ def _route_scenario(
|
||||||
bend_collision_type: str,
|
bend_collision_type: str,
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
widths: dict[str, float],
|
widths: dict[str, float],
|
||||||
*,
|
) -> dict[str, RoutingResult]:
|
||||||
bend_clip_margin: float = 10.0,
|
problem = RoutingProblem(
|
||||||
) -> dict[str, object]:
|
bounds=bounds,
|
||||||
engine = CollisionEngine(clearance=2.0)
|
nets=tuple(NetSpec(net_id, start, target, width=widths[net_id]) for net_id, (start, target) in netlist.items()),
|
||||||
for obstacle in obstacles:
|
static_obstacles=tuple(obstacles),
|
||||||
engine.add_static_obstacle(obstacle)
|
|
||||||
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute(obstacles)
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
|
||||||
context = AStarContext(
|
|
||||||
evaluator,
|
|
||||||
bend_radii=[10.0],
|
|
||||||
bend_collision_type=bend_collision_type,
|
|
||||||
bend_clip_margin=bend_clip_margin,
|
|
||||||
)
|
)
|
||||||
return PathFinder(context, use_tiered_strategy=False).route_all(netlist, widths)
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
bend_collision_type=bend_collision_type,
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=50.0,
|
||||||
|
sbend_penalty=150.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(use_tiered_strategy=False),
|
||||||
|
)
|
||||||
|
return route(problem, options=options).results_by_net
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 06: Bend Collision Models...")
|
print("Running Example 06: Bend Collision Models...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
# Give room for 10um bends near the edges
|
|
||||||
bounds = (-20, -20, 170, 170)
|
bounds = (-20, -20, 170, 170)
|
||||||
|
|
||||||
# Create three scenarios with identical obstacles
|
|
||||||
# We'll space them out vertically
|
|
||||||
obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)])
|
obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)])
|
||||||
obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)])
|
obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)])
|
||||||
obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)])
|
obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)])
|
||||||
|
|
@ -52,29 +44,17 @@ def main() -> None:
|
||||||
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
|
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
|
||||||
netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}
|
netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}
|
||||||
|
|
||||||
# 2. Route each scenario
|
|
||||||
print("Routing Scenario 1 (Arc)...")
|
print("Routing Scenario 1 (Arc)...")
|
||||||
res_arc = _route_scenario(bounds, obstacles, "arc", netlist_arc, {"arc_model": 2.0})
|
res_arc = _route_scenario(bounds, obstacles, "arc", netlist_arc, {"arc_model": 2.0})
|
||||||
|
|
||||||
print("Routing Scenario 2 (BBox)...")
|
print("Routing Scenario 2 (BBox)...")
|
||||||
res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0})
|
res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0})
|
||||||
|
|
||||||
print("Routing Scenario 3 (Clipped BBox)...")
|
print("Routing Scenario 3 (Clipped BBox)...")
|
||||||
res_clipped = _route_scenario(
|
res_clipped = _route_scenario(bounds, obstacles, "clipped_bbox", netlist_clipped, {"clipped_model": 2.0})
|
||||||
bounds,
|
|
||||||
obstacles,
|
|
||||||
"clipped_bbox",
|
|
||||||
netlist_clipped,
|
|
||||||
{"clipped_model": 2.0},
|
|
||||||
bend_clip_margin=1.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Combine results for visualization
|
|
||||||
all_results = {**res_arc, **res_bbox, **res_clipped}
|
all_results = {**res_arc, **res_bbox, **res_clipped}
|
||||||
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
|
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
|
||||||
|
|
||||||
# 4. Visualize
|
fig, _ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists)
|
||||||
fig, ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists)
|
|
||||||
fig.savefig("examples/06_bend_collision_models.png")
|
fig.savefig("examples/06_bend_collision_models.png")
|
||||||
print("Saved plot to examples/06_bend_collision_models.png")
|
print("Saved plot to examples/06_bend_collision_models.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,111 @@
|
||||||
import numpy as np
|
|
||||||
import time
|
import time
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results, plot_danger_map, plot_expanded_nodes, plot_expansion_density
|
|
||||||
from shapely.geometry import box
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
from inire import (
|
||||||
|
CongestionOptions,
|
||||||
|
DiagnosticsOptions,
|
||||||
|
NetSpec,
|
||||||
|
ObjectiveWeights,
|
||||||
|
Port,
|
||||||
|
RoutingOptions,
|
||||||
|
RoutingProblem,
|
||||||
|
RoutingResult,
|
||||||
|
SearchOptions,
|
||||||
|
route,
|
||||||
|
)
|
||||||
|
from inire.utils.visualization import plot_expanded_nodes, plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 07: Fan-Out (10 Nets, 50um Radius)...")
|
print("Running Example 07: Fan-Out (10 Nets, 50um Radius)...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 1000, 1000)
|
bounds = (0, 0, 1000, 1000)
|
||||||
engine = CollisionEngine(clearance=6.0)
|
|
||||||
|
|
||||||
# Bottleneck at x=500, 200um gap
|
|
||||||
obstacles = [
|
obstacles = [
|
||||||
box(450, 0, 550, 400),
|
box(450, 0, 550, 400),
|
||||||
box(450, 600, 550, 1000),
|
box(450, 600, 550, 1000),
|
||||||
]
|
]
|
||||||
for obs in obstacles:
|
|
||||||
engine.add_static_obstacle(obs)
|
|
||||||
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute(obstacles)
|
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.1, bend_penalty=100.0, sbend_penalty=400.0, congestion_penalty=100.0)
|
|
||||||
|
|
||||||
context = AStarContext(evaluator, node_limit=2000000, bend_radii=[50.0], sbend_radii=[50.0])
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics, max_iterations=15, base_congestion_penalty=100.0, congestion_multiplier=1.4)
|
|
||||||
|
|
||||||
# 2. Define Netlist
|
|
||||||
netlist = {}
|
|
||||||
num_nets = 10
|
num_nets = 10
|
||||||
start_x = 50
|
start_x = 50
|
||||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||||
|
|
||||||
end_x = 950
|
end_x = 950
|
||||||
end_y_base = 100
|
end_y_base = 100
|
||||||
end_y_pitch = 800.0 / (num_nets - 1)
|
end_y_pitch = 800.0 / (num_nets - 1)
|
||||||
|
|
||||||
for i in range(num_nets):
|
netlist: dict[str, tuple[Port, Port]] = {}
|
||||||
sy = int(round(start_y_base + i * 10.0))
|
for index in range(num_nets):
|
||||||
ey = int(round(end_y_base + i * end_y_pitch))
|
start_y = int(round(start_y_base + index * 10.0))
|
||||||
netlist[f"net_{i:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
end_y = int(round(end_y_base + index * end_y_pitch))
|
||||||
|
netlist[f"net_{index:02d}"] = (Port(start_x, start_y, 0), Port(end_x, end_y, 0))
|
||||||
|
|
||||||
net_widths = {nid: 2.0 for nid in netlist}
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()),
|
||||||
|
static_obstacles=tuple(obstacles),
|
||||||
|
clearance=6.0,
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
node_limit=2_000_000,
|
||||||
|
bend_radii=(50.0,),
|
||||||
|
sbend_radii=(50.0,),
|
||||||
|
greedy_h_weight=1.5,
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
unit_length_cost=0.1,
|
||||||
|
bend_penalty=100.0,
|
||||||
|
sbend_penalty=400.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(
|
||||||
|
max_iterations=15,
|
||||||
|
base_penalty=100.0,
|
||||||
|
multiplier=1.4,
|
||||||
|
shuffle_nets=True,
|
||||||
|
seed=42,
|
||||||
|
),
|
||||||
|
diagnostics=DiagnosticsOptions(capture_expanded=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
iteration_stats: list[dict[str, int]] = []
|
||||||
|
|
||||||
|
def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None:
|
||||||
|
successes = sum(1 for result in current_results.values() if result.is_valid)
|
||||||
|
total_collisions = sum(result.collisions for result in current_results.values())
|
||||||
|
print(f" Iteration {iteration} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}")
|
||||||
|
iteration_stats.append(
|
||||||
|
{
|
||||||
|
"Iteration": iteration,
|
||||||
|
"Success": successes,
|
||||||
|
"Congestion": total_collisions,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Route
|
|
||||||
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
|
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
run = route(problem, options=options, iteration_callback=iteration_callback)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
iteration_stats = []
|
print(f"Routing took {end_time - start_time:.4f}s")
|
||||||
|
|
||||||
def iteration_callback(idx, current_results):
|
|
||||||
successes = sum(1 for r in current_results.values() if r.is_valid)
|
|
||||||
total_collisions = sum(r.collisions for r in current_results.values())
|
|
||||||
total_nodes = metrics.nodes_expanded
|
|
||||||
|
|
||||||
print(f" Iteration {idx} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}")
|
|
||||||
|
|
||||||
# Adaptive Greediness: Decay from 1.5 to 1.1 over 10 iterations
|
|
||||||
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
|
||||||
evaluator.greedy_h_weight = new_greedy
|
|
||||||
print(f" Adaptive Greedy Weight for Next Iteration: {new_greedy:.3f}")
|
|
||||||
|
|
||||||
iteration_stats.append({
|
|
||||||
'Iteration': idx,
|
|
||||||
'Success': successes,
|
|
||||||
'Congestion': total_collisions,
|
|
||||||
'Nodes': total_nodes
|
|
||||||
})
|
|
||||||
metrics.reset_per_route()
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
results = pf.route_all(netlist, net_widths, store_expanded=True, iteration_callback=iteration_callback, shuffle_nets=True, seed=42)
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
|
|
||||||
print(f"Routing took {t1-t0:.4f}s")
|
|
||||||
|
|
||||||
# 4. Check Results
|
|
||||||
print("\n--- Iteration Summary ---")
|
print("\n--- Iteration Summary ---")
|
||||||
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}")
|
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8}")
|
||||||
print("-" * 40)
|
print("-" * 30)
|
||||||
for s in iteration_stats:
|
for stats in iteration_stats:
|
||||||
print(f"{s['Iteration']:<5} | {s['Success']:<8} | {s['Congestion']:<8} | {s['Nodes']:<10}")
|
print(f"{stats['Iteration']:<5} | {stats['Success']:<8} | {stats['Congestion']:<8}")
|
||||||
|
|
||||||
success_count = sum(1 for res in results.values() if res.is_valid)
|
success_count = sum(1 for result in run.results_by_net.values() if result.is_valid)
|
||||||
print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.")
|
print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.")
|
||||||
|
for net_id, result in run.results_by_net.items():
|
||||||
for nid, res in results.items():
|
if not result.is_valid:
|
||||||
if not res.is_valid:
|
print(f" FAILED: {net_id}, collisions={result.collisions}")
|
||||||
print(f" FAILED: {nid}, collisions={res.collisions}")
|
|
||||||
else:
|
else:
|
||||||
print(f" {nid}: SUCCESS")
|
print(f" {net_id}: SUCCESS")
|
||||||
|
|
||||||
# 5. Visualize
|
|
||||||
fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist)
|
|
||||||
plot_danger_map(danger_map, ax=ax)
|
|
||||||
|
|
||||||
|
fig, ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist=netlist)
|
||||||
|
plot_expanded_nodes(list(run.expanded_nodes), ax=ax)
|
||||||
fig.savefig("examples/07_large_scale_routing.png")
|
fig.savefig("examples/07_large_scale_routing.png")
|
||||||
print("Saved plot to examples/07_large_scale_routing.png")
|
print("Saved plot to examples/07_large_scale_routing.png")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,61 @@
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def _route_with_context(
|
def _run_request(
|
||||||
context: AStarContext,
|
bounds: tuple[float, float, float, float],
|
||||||
metrics: AStarMetrics,
|
bend_collision_type: object,
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
net_id: str,
|
||||||
net_widths: dict[str, float],
|
start: Port,
|
||||||
) -> dict[str, object]:
|
target: Port,
|
||||||
return PathFinder(context, metrics, use_tiered_strategy=False).route_all(netlist, net_widths)
|
) -> dict[str, RoutingResult]:
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=(NetSpec(net_id, start, target, width=2.0),),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
bend_collision_type=bend_collision_type,
|
||||||
|
sbend_radii=(),
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=50.0,
|
||||||
|
sbend_penalty=150.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(use_tiered_strategy=False),
|
||||||
|
)
|
||||||
|
return route(problem, options=options).results_by_net
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 08: Custom Bend Geometry...")
|
print("Running Example 08: Custom Bend Geometry...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 150, 150)
|
bounds = (0, 0, 150, 150)
|
||||||
|
start = Port(20, 20, 0)
|
||||||
|
target = Port(100, 100, 90)
|
||||||
|
|
||||||
# 2. Define Netlist
|
|
||||||
netlist = {
|
|
||||||
"custom_bend": (Port(20, 20, 0), Port(100, 100, 90)),
|
|
||||||
}
|
|
||||||
net_widths = {"custom_bend": 2.0}
|
|
||||||
|
|
||||||
def build_context(bend_collision_type: object = "arc") -> tuple[AStarContext, AStarMetrics]:
|
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute([])
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
|
||||||
return AStarContext(evaluator, bend_radii=[10.0], bend_collision_type=bend_collision_type, sbend_radii=[]), AStarMetrics()
|
|
||||||
|
|
||||||
# 3. Route with standard arc first
|
|
||||||
print("Routing with standard arc...")
|
print("Routing with standard arc...")
|
||||||
context_std, metrics_std = build_context()
|
results_std = _run_request(bounds, "arc", "custom_bend", start, target)
|
||||||
results_std = _route_with_context(context_std, metrics_std, netlist, net_widths)
|
|
||||||
|
|
||||||
# 4. Define a custom Manhattan 90-degree bend proxy in bend-local coordinates.
|
|
||||||
# The polygon origin is the bend center. It is mirrored for CW bends and
|
|
||||||
# rotated with the bend orientation before being translated into place.
|
|
||||||
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
||||||
|
|
||||||
print("Routing with custom bend geometry...")
|
print("Routing with custom bend geometry...")
|
||||||
context_custom, metrics_custom = build_context(custom_poly)
|
results_custom = _run_request(bounds, custom_poly, "custom_model", start, target)
|
||||||
results_custom = _route_with_context(context_custom, metrics_custom, {"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0})
|
|
||||||
|
|
||||||
# 5. Visualize
|
|
||||||
all_results = {**results_std, **results_custom}
|
all_results = {**results_std, **results_custom}
|
||||||
fig, ax = plot_routing_results(all_results, [], bounds, netlist=netlist)
|
fig, _ax = plot_routing_results(
|
||||||
|
all_results,
|
||||||
|
[],
|
||||||
|
bounds,
|
||||||
|
netlist={
|
||||||
|
"custom_bend": (start, target),
|
||||||
|
"custom_model": (start, target),
|
||||||
|
},
|
||||||
|
)
|
||||||
fig.savefig("examples/08_custom_bend_geometry.png")
|
fig.savefig("examples/08_custom_bend_geometry.png")
|
||||||
print("Saved plot to examples/08_custom_bend_geometry.png")
|
print("Saved plot to examples/08_custom_bend_geometry.png")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,46 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
from inire.router.danger_map import DangerMap
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.utils.visualization import plot_routing_results
|
|
||||||
from shapely.geometry import box
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
from inire import CongestionOptions, NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 09: Best-Effort Under Tight Search Budget...")
|
print("Running Example 09: Best-Effort Under Tight Search Budget...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 100, 100)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
|
||||||
|
|
||||||
# A small obstacle cluster keeps the partial route visually interesting.
|
|
||||||
obstacles = [
|
obstacles = [
|
||||||
box(35, 35, 45, 65),
|
box(35, 35, 45, 65),
|
||||||
box(55, 35, 65, 65),
|
box(55, 35, 65, 65),
|
||||||
]
|
]
|
||||||
for obs in obstacles:
|
problem = RoutingProblem(
|
||||||
engine.add_static_obstacle(obs)
|
bounds=bounds,
|
||||||
|
nets=(NetSpec("budget_limited_net", Port(10, 50, 0), Port(85, 60, 180), width=2.0),),
|
||||||
|
static_obstacles=tuple(obstacles),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
node_limit=3,
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=50.0,
|
||||||
|
sbend_penalty=150.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(warm_start=None),
|
||||||
|
)
|
||||||
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute(obstacles)
|
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
|
||||||
# Keep the search budget intentionally tiny so the router returns a partial path.
|
|
||||||
context = AStarContext(evaluator, node_limit=3, bend_radii=[10.0])
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
|
|
||||||
pf = PathFinder(context, metrics, warm_start=None)
|
|
||||||
|
|
||||||
# 2. Define Netlist: reaching the target requires additional turns the search budget cannot afford.
|
|
||||||
netlist = {
|
|
||||||
"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180)),
|
|
||||||
}
|
|
||||||
net_widths = {"budget_limited_net": 2.0}
|
|
||||||
|
|
||||||
# 3. Route
|
|
||||||
print("Routing with a deliberately tiny node budget (should return a partial path)...")
|
print("Routing with a deliberately tiny node budget (should return a partial path)...")
|
||||||
results = pf.route_all(netlist, net_widths)
|
run = route(problem, options=options)
|
||||||
|
result = run.results_by_net["budget_limited_net"]
|
||||||
# 4. Check Results
|
if not result.reached_target:
|
||||||
res = results["budget_limited_net"]
|
print(f"Target not reached as expected. Partial path length: {len(result.path)} segments.")
|
||||||
if not res.reached_target:
|
|
||||||
print(f"Target not reached as expected. Partial path length: {len(res.path)} segments.")
|
|
||||||
else:
|
else:
|
||||||
print("The route unexpectedly reached the target. Increase difficulty or reduce the node budget further.")
|
print("The route unexpectedly reached the target. Increase difficulty or reduce the node budget further.")
|
||||||
|
|
||||||
# 5. Visualize
|
fig, _ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist={"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))})
|
||||||
fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist)
|
|
||||||
fig.savefig("examples/09_unroutable_best_effort.png")
|
fig.savefig("examples/09_unroutable_best_effort.png")
|
||||||
print("Saved plot to examples/09_unroutable_best_effort.png")
|
print("Saved plot to examples/09_unroutable_best_effort.png")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,43 @@
|
||||||
"""
|
"""
|
||||||
inire Wave-router
|
inire Wave-router
|
||||||
"""
|
"""
|
||||||
|
from .api import (
|
||||||
|
CongestionOptions as CongestionOptions,
|
||||||
|
DiagnosticsOptions as DiagnosticsOptions,
|
||||||
|
LockedRoute as LockedRoute,
|
||||||
|
NetSpec as NetSpec,
|
||||||
|
ObjectiveWeights as ObjectiveWeights,
|
||||||
|
RefinementOptions as RefinementOptions,
|
||||||
|
RoutingOptions as RoutingOptions,
|
||||||
|
RoutingProblem as RoutingProblem,
|
||||||
|
RoutingRunResult as RoutingRunResult,
|
||||||
|
SearchOptions as SearchOptions,
|
||||||
|
route as route,
|
||||||
|
) # noqa: PLC0414
|
||||||
from .geometry.primitives import Port as Port # noqa: PLC0414
|
from .geometry.primitives import Port as Port # noqa: PLC0414
|
||||||
from .geometry.components import Straight as Straight, Bend90 as Bend90, SBend as SBend # noqa: PLC0414
|
from .geometry.components import Straight as Straight, Bend90 as Bend90, SBend as SBend # noqa: PLC0414
|
||||||
|
from .router.results import RouteMetrics as RouteMetrics, RoutingReport as RoutingReport, RoutingResult as RoutingResult # noqa: PLC0414
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
__version__ = '0.1'
|
__version__ = '0.1'
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Bend90",
|
||||||
|
"CongestionOptions",
|
||||||
|
"DiagnosticsOptions",
|
||||||
|
"LockedRoute",
|
||||||
|
"NetSpec",
|
||||||
|
"ObjectiveWeights",
|
||||||
|
"Port",
|
||||||
|
"RefinementOptions",
|
||||||
|
"RoutingOptions",
|
||||||
|
"RoutingProblem",
|
||||||
|
"RoutingReport",
|
||||||
|
"RoutingResult",
|
||||||
|
"RoutingRunResult",
|
||||||
|
"RouteMetrics",
|
||||||
|
"SBend",
|
||||||
|
"SearchOptions",
|
||||||
|
"Straight",
|
||||||
|
"route",
|
||||||
|
]
|
||||||
|
|
|
||||||
97
inire/api.py
Normal file
97
inire/api.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
from inire.model import (
|
||||||
|
CongestionOptions,
|
||||||
|
DiagnosticsOptions,
|
||||||
|
LockedRoute,
|
||||||
|
NetSpec,
|
||||||
|
ObjectiveWeights,
|
||||||
|
RefinementOptions,
|
||||||
|
RoutingOptions,
|
||||||
|
RoutingProblem,
|
||||||
|
RoutingRunResult,
|
||||||
|
SearchOptions,
|
||||||
|
)
|
||||||
|
from inire.router._astar_types import AStarContext
|
||||||
|
from inire.router._router import PathFinder
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.results import RouteMetrics, RoutingReport, RoutingResult
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
|
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CongestionOptions",
|
||||||
|
"DiagnosticsOptions",
|
||||||
|
"LockedRoute",
|
||||||
|
"NetSpec",
|
||||||
|
"ObjectiveWeights",
|
||||||
|
"RefinementOptions",
|
||||||
|
"RouteMetrics",
|
||||||
|
"RoutingOptions",
|
||||||
|
"RoutingProblem",
|
||||||
|
"RoutingReport",
|
||||||
|
"RoutingResult",
|
||||||
|
"RoutingRunResult",
|
||||||
|
"SearchOptions",
|
||||||
|
"route",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_locked_polygons(
|
||||||
|
locked_routes: dict[str, LockedRoute],
|
||||||
|
) -> Iterable[Polygon]:
|
||||||
|
for route in locked_routes.values():
|
||||||
|
yield from route.geometry
|
||||||
|
|
||||||
|
|
||||||
|
def _build_context(problem: RoutingProblem, options: RoutingOptions) -> AStarContext:
|
||||||
|
world = RoutingWorld(
|
||||||
|
clearance=problem.clearance,
|
||||||
|
max_net_width=problem.max_net_width,
|
||||||
|
safety_zone_radius=problem.safety_zone_radius,
|
||||||
|
)
|
||||||
|
for obstacle in problem.static_obstacles:
|
||||||
|
world.add_static_obstacle(obstacle)
|
||||||
|
for polygon in _iter_locked_polygons(problem.locked_routes):
|
||||||
|
world.add_static_obstacle(polygon)
|
||||||
|
|
||||||
|
danger_obstacles = list(problem.static_obstacles)
|
||||||
|
danger_obstacles.extend(_iter_locked_polygons(problem.locked_routes))
|
||||||
|
danger_map = DangerMap(bounds=problem.bounds)
|
||||||
|
danger_map.precompute(danger_obstacles)
|
||||||
|
|
||||||
|
objective = options.objective
|
||||||
|
evaluator = CostEvaluator(
|
||||||
|
world,
|
||||||
|
danger_map,
|
||||||
|
unit_length_cost=objective.unit_length_cost,
|
||||||
|
greedy_h_weight=options.search.greedy_h_weight,
|
||||||
|
bend_penalty=objective.bend_penalty,
|
||||||
|
sbend_penalty=objective.sbend_penalty,
|
||||||
|
danger_weight=objective.danger_weight,
|
||||||
|
)
|
||||||
|
return AStarContext(evaluator, problem, options)
|
||||||
|
|
||||||
|
|
||||||
|
def route(
|
||||||
|
problem: RoutingProblem,
|
||||||
|
*,
|
||||||
|
options: RoutingOptions | None = None,
|
||||||
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
|
||||||
|
) -> RoutingRunResult:
|
||||||
|
resolved_options = RoutingOptions() if options is None else options
|
||||||
|
finder = PathFinder(_build_context(problem, resolved_options))
|
||||||
|
results = finder.route_all(iteration_callback=iteration_callback)
|
||||||
|
return RoutingRunResult(
|
||||||
|
results_by_net=results,
|
||||||
|
metrics=finder.metrics.snapshot(),
|
||||||
|
expanded_nodes=tuple(finder.accumulated_expanded_nodes),
|
||||||
|
)
|
||||||
|
|
@ -3,7 +3,7 @@ Centralized constants for the inire routing engine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Search Grid Snap (5.0 µm default)
|
# Search Grid Snap (5.0 µm default)
|
||||||
# TODO: Make this configurable in RouterConfig and define tolerances relative to the grid.
|
# TODO: Make this configurable in SearchOptions and define tolerances relative to the grid.
|
||||||
DEFAULT_SEARCH_GRID_SNAP_UM = 5.0
|
DEFAULT_SEARCH_GRID_SNAP_UM = 5.0
|
||||||
|
|
||||||
# Tolerances
|
# Tolerances
|
||||||
|
|
|
||||||
|
|
@ -3,67 +3,69 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
from shapely.geometry import LineString, box
|
||||||
|
|
||||||
from inire.geometry.collision_query_checker import CollisionQueryChecker
|
from inire.geometry.component_overlap import components_overlap
|
||||||
from inire.geometry.dynamic_congestion_checker import DynamicCongestionChecker
|
|
||||||
from inire.geometry.dynamic_path_index import DynamicPathIndex
|
from inire.geometry.dynamic_path_index import DynamicPathIndex
|
||||||
from inire.geometry.path_verifier import PathVerificationReport, PathVerifier
|
from inire.geometry.index_helpers import grid_cell_span
|
||||||
from inire.geometry.ray_caster import RayCaster
|
|
||||||
from inire.geometry.static_obstacle_index import StaticObstacleIndex
|
from inire.geometry.static_obstacle_index import StaticObstacleIndex
|
||||||
from inire.geometry.static_move_checker import StaticMoveChecker
|
from inire.router.results import RoutingReport
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Sequence
|
||||||
|
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
from shapely.geometry.base import BaseGeometry
|
||||||
from shapely.strtree import STRtree
|
from shapely.strtree import STRtree
|
||||||
|
|
||||||
from inire.geometry.components import ComponentResult
|
from inire.geometry.components import ComponentResult
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
class CollisionEngine:
|
def _intersection_distance(origin: Port, geometry: BaseGeometry) -> float:
|
||||||
|
if hasattr(geometry, "geoms"):
|
||||||
|
return min(_intersection_distance(origin, sub_geometry) for sub_geometry in geometry.geoms)
|
||||||
|
return float(numpy.sqrt((geometry.coords[0][0] - origin.x) ** 2 + (geometry.coords[0][1] - origin.y) ** 2))
|
||||||
|
|
||||||
|
|
||||||
|
class RoutingWorld:
|
||||||
"""
|
"""
|
||||||
Manages spatial queries for collision detection with unified dilation logic.
|
Internal spatial state for collision detection, congestion, and verification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'clearance', 'max_net_width', 'safety_zone_radius',
|
"clearance",
|
||||||
'metrics', 'grid_cell_size', '_inv_grid_cell_size', '_dynamic_bounds_array',
|
"max_net_width",
|
||||||
'_path_verifier', '_dynamic_paths', '_static_obstacles', '_ray_caster', '_static_move_checker',
|
"safety_zone_radius",
|
||||||
'_dynamic_congestion_checker', '_collision_query_checker',
|
"metrics",
|
||||||
|
"grid_cell_size",
|
||||||
|
"_dynamic_paths",
|
||||||
|
"_static_obstacles",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
clearance: float,
|
clearance: float,
|
||||||
max_net_width: float = 2.0,
|
max_net_width: float = 2.0,
|
||||||
safety_zone_radius: float = 0.0021,
|
safety_zone_radius: float = 0.0021,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.clearance = clearance
|
self.clearance = clearance
|
||||||
self.max_net_width = max_net_width
|
self.max_net_width = max_net_width
|
||||||
self.safety_zone_radius = safety_zone_radius
|
self.safety_zone_radius = safety_zone_radius
|
||||||
|
|
||||||
self.grid_cell_size = 50.0
|
self.grid_cell_size = 50.0
|
||||||
self._inv_grid_cell_size = 1.0 / self.grid_cell_size
|
|
||||||
self._static_obstacles = StaticObstacleIndex(self)
|
self._static_obstacles = StaticObstacleIndex(self)
|
||||||
|
|
||||||
self._dynamic_bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
|
||||||
self._dynamic_paths = DynamicPathIndex(self)
|
self._dynamic_paths = DynamicPathIndex(self)
|
||||||
|
|
||||||
self.metrics = {
|
self.metrics = {
|
||||||
'static_cache_hits': 0,
|
"static_cache_hits": 0,
|
||||||
'static_grid_skips': 0,
|
"static_grid_skips": 0,
|
||||||
'static_tree_queries': 0,
|
"static_tree_queries": 0,
|
||||||
'static_straight_fast': 0,
|
"static_straight_fast": 0,
|
||||||
'congestion_grid_skips': 0,
|
"congestion_grid_skips": 0,
|
||||||
'congestion_tree_queries': 0,
|
"congestion_tree_queries": 0,
|
||||||
'safety_zone_checks': 0
|
"safety_zone_checks": 0,
|
||||||
}
|
}
|
||||||
self._path_verifier = PathVerifier(self)
|
|
||||||
self._ray_caster = RayCaster(self)
|
|
||||||
self._static_move_checker = StaticMoveChecker(self)
|
|
||||||
self._dynamic_congestion_checker = DynamicCongestionChecker(self)
|
|
||||||
self._collision_query_checker = CollisionQueryChecker(self)
|
|
||||||
|
|
||||||
def get_static_version(self) -> int:
|
def get_static_version(self) -> int:
|
||||||
return self._static_obstacles.version
|
return self._static_obstacles.version
|
||||||
|
|
@ -89,23 +91,22 @@ class CollisionEngine:
|
||||||
return self._dynamic_paths.geometries.values()
|
return self._dynamic_paths.geometries.values()
|
||||||
|
|
||||||
def reset_metrics(self) -> None:
|
def reset_metrics(self) -> None:
|
||||||
for k in self.metrics:
|
for key in self.metrics:
|
||||||
self.metrics[k] = 0
|
self.metrics[key] = 0
|
||||||
|
|
||||||
def get_metrics_summary(self) -> str:
|
def get_metrics_summary(self) -> str:
|
||||||
m = self.metrics
|
metrics = self.metrics
|
||||||
return (f"Collision Performance: \n"
|
return (
|
||||||
f" Static: {m['static_tree_queries']} checks\n"
|
"Collision Performance: \n"
|
||||||
f" Congestion: {m['congestion_tree_queries']} checks\n"
|
f" Static: {metrics['static_tree_queries']} checks\n"
|
||||||
f" Safety Zone: {m['safety_zone_checks']} full intersections performed")
|
f" Congestion: {metrics['congestion_tree_queries']} checks\n"
|
||||||
|
f" Safety Zone: {metrics['safety_zone_checks']} full intersections performed"
|
||||||
|
)
|
||||||
|
|
||||||
def add_static_obstacle(self, polygon: Polygon, dilated_geometry: Polygon | None = None) -> int:
|
def add_static_obstacle(self, polygon: Polygon, dilated_geometry: Polygon | None = None) -> int:
|
||||||
return self._static_obstacles.add_obstacle(polygon, dilated_geometry=dilated_geometry)
|
return self._static_obstacles.add_obstacle(polygon, dilated_geometry=dilated_geometry)
|
||||||
|
|
||||||
def remove_static_obstacle(self, obj_id: int) -> None:
|
def remove_static_obstacle(self, obj_id: int) -> None:
|
||||||
"""
|
|
||||||
Remove a static obstacle by ID.
|
|
||||||
"""
|
|
||||||
self._static_obstacles.remove_obstacle(obj_id)
|
self._static_obstacles.remove_obstacle(obj_id)
|
||||||
|
|
||||||
def _invalidate_static_caches(self) -> None:
|
def _invalidate_static_caches(self) -> None:
|
||||||
|
|
@ -115,9 +116,6 @@ class CollisionEngine:
|
||||||
self._static_obstacles.ensure_tree()
|
self._static_obstacles.ensure_tree()
|
||||||
|
|
||||||
def _ensure_net_static_tree(self, net_width: float) -> STRtree:
|
def _ensure_net_static_tree(self, net_width: float) -> STRtree:
|
||||||
"""
|
|
||||||
Lazily generate a tree where obstacles are dilated by (net_width/2 + clearance).
|
|
||||||
"""
|
|
||||||
return self._static_obstacles.ensure_net_tree(net_width)
|
return self._static_obstacles.ensure_net_tree(net_width)
|
||||||
|
|
||||||
def _ensure_static_raw_tree(self) -> None:
|
def _ensure_static_raw_tree(self) -> None:
|
||||||
|
|
@ -125,7 +123,6 @@ class CollisionEngine:
|
||||||
|
|
||||||
def _ensure_dynamic_tree(self) -> None:
|
def _ensure_dynamic_tree(self) -> None:
|
||||||
self._dynamic_paths.ensure_tree()
|
self._dynamic_paths.ensure_tree()
|
||||||
self._dynamic_bounds_array = self._dynamic_paths.bounds_array
|
|
||||||
|
|
||||||
def _ensure_dynamic_grid(self) -> None:
|
def _ensure_dynamic_grid(self) -> None:
|
||||||
self._dynamic_paths.ensure_grid()
|
self._dynamic_paths.ensure_grid()
|
||||||
|
|
@ -134,45 +131,28 @@ class CollisionEngine:
|
||||||
self._dynamic_paths.tree = None
|
self._dynamic_paths.tree = None
|
||||||
self._ensure_dynamic_tree()
|
self._ensure_dynamic_tree()
|
||||||
|
|
||||||
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
||||||
self._dynamic_paths.add_path(net_id, geometry, dilated_geometry=dilated_geometry)
|
self._dynamic_paths.add_path(net_id, geometry, dilated_geometry=dilated_geometry)
|
||||||
|
|
||||||
def remove_path(self, net_id: str) -> None:
|
def remove_path(self, net_id: str) -> None:
|
||||||
self._dynamic_paths.remove_path(net_id)
|
self._dynamic_paths.remove_path(net_id)
|
||||||
|
|
||||||
def lock_net(self, net_id: str) -> None:
|
|
||||||
""" Convert a routed net into static obstacles. """
|
|
||||||
self._dynamic_paths.lock_net(net_id)
|
|
||||||
|
|
||||||
def unlock_net(self, net_id: str) -> None:
|
|
||||||
self._dynamic_paths.unlock_net(net_id)
|
|
||||||
|
|
||||||
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
|
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
|
||||||
return self._static_move_checker.check_move_straight_static(start_port, length, net_width)
|
self.metrics["static_straight_fast"] += 1
|
||||||
|
reach = self.ray_cast(start_port, start_port.r, max_dist=length + 0.01, net_width=net_width)
|
||||||
|
return reach < length - 0.001
|
||||||
|
|
||||||
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||||
return self._static_move_checker.is_in_safety_zone_fast(idx, start_port, end_port)
|
bounds = self._static_obstacles.bounds_array[idx]
|
||||||
|
safety_zone = self.safety_zone_radius
|
||||||
def check_move_static(
|
if start_port and bounds[0] - safety_zone <= start_port.x <= bounds[2] + safety_zone and bounds[1] - safety_zone <= start_port.y <= bounds[3] + safety_zone:
|
||||||
self,
|
return True
|
||||||
result: ComponentResult,
|
return bool(
|
||||||
start_port: Port | None = None,
|
end_port
|
||||||
end_port: Port | None = None,
|
and bounds[0] - safety_zone <= end_port.x <= bounds[2] + safety_zone
|
||||||
net_width: float | None = None,
|
and bounds[1] - safety_zone <= end_port.y <= bounds[3] + safety_zone
|
||||||
) -> bool:
|
|
||||||
return self._static_move_checker.check_move_static(
|
|
||||||
result,
|
|
||||||
start_port=start_port,
|
|
||||||
end_port=end_port,
|
|
||||||
net_width=net_width,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
|
||||||
return self._dynamic_congestion_checker.check_move_congestion(result, net_id)
|
|
||||||
|
|
||||||
def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int:
|
|
||||||
return self._dynamic_congestion_checker.check_real_congestion(result, net_id)
|
|
||||||
|
|
||||||
def _is_in_safety_zone(
|
def _is_in_safety_zone(
|
||||||
self,
|
self,
|
||||||
geometry: Polygon,
|
geometry: Polygon,
|
||||||
|
|
@ -180,7 +160,247 @@ class CollisionEngine:
|
||||||
start_port: Port | None,
|
start_port: Port | None,
|
||||||
end_port: Port | None,
|
end_port: Port | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return self._static_move_checker.is_in_safety_zone(geometry, obj_id, start_port, end_port)
|
raw_obstacle = self._static_obstacles.geometries[obj_id]
|
||||||
|
safety_zone = self.safety_zone_radius
|
||||||
|
|
||||||
|
obstacle_bounds = raw_obstacle.bounds
|
||||||
|
near_start = start_port and (
|
||||||
|
obstacle_bounds[0] - safety_zone <= start_port.x <= obstacle_bounds[2] + safety_zone
|
||||||
|
and obstacle_bounds[1] - safety_zone <= start_port.y <= obstacle_bounds[3] + safety_zone
|
||||||
|
)
|
||||||
|
near_end = end_port and (
|
||||||
|
obstacle_bounds[0] - safety_zone <= end_port.x <= obstacle_bounds[2] + safety_zone
|
||||||
|
and obstacle_bounds[1] - safety_zone <= end_port.y <= obstacle_bounds[3] + safety_zone
|
||||||
|
)
|
||||||
|
|
||||||
|
if not near_start and not near_end:
|
||||||
|
return False
|
||||||
|
if not geometry.intersects(raw_obstacle):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.metrics["safety_zone_checks"] += 1
|
||||||
|
intersection = geometry.intersection(raw_obstacle)
|
||||||
|
if intersection.is_empty:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ix_bounds = intersection.bounds
|
||||||
|
if (
|
||||||
|
start_port
|
||||||
|
and near_start
|
||||||
|
and abs(ix_bounds[0] - start_port.x) < safety_zone
|
||||||
|
and abs(ix_bounds[1] - start_port.y) < safety_zone
|
||||||
|
and abs(ix_bounds[2] - start_port.x) < safety_zone
|
||||||
|
and abs(ix_bounds[3] - start_port.y) < safety_zone
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return bool(
|
||||||
|
end_port
|
||||||
|
and near_end
|
||||||
|
and abs(ix_bounds[0] - end_port.x) < safety_zone
|
||||||
|
and abs(ix_bounds[1] - end_port.y) < safety_zone
|
||||||
|
and abs(ix_bounds[2] - end_port.x) < safety_zone
|
||||||
|
and abs(ix_bounds[3] - end_port.y) < safety_zone
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_move_static(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
start_port: Port | None = None,
|
||||||
|
end_port: Port | None = None,
|
||||||
|
net_width: float | None = None,
|
||||||
|
) -> bool:
|
||||||
|
del net_width
|
||||||
|
|
||||||
|
static_obstacles = self._static_obstacles
|
||||||
|
if not static_obstacles.dilated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.metrics["static_tree_queries"] += 1
|
||||||
|
self._ensure_static_tree()
|
||||||
|
|
||||||
|
hits = static_obstacles.tree.query(box(*result.total_dilated_bounds))
|
||||||
|
if hits.size == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
static_bounds = static_obstacles.bounds_array
|
||||||
|
move_poly_bounds = result.dilated_bounds
|
||||||
|
for hit_idx in hits:
|
||||||
|
obstacle_bounds = static_bounds[hit_idx]
|
||||||
|
poly_hits_obstacle_aabb = False
|
||||||
|
for poly_bounds in move_poly_bounds:
|
||||||
|
if (
|
||||||
|
poly_bounds[0] < obstacle_bounds[2]
|
||||||
|
and poly_bounds[2] > obstacle_bounds[0]
|
||||||
|
and poly_bounds[1] < obstacle_bounds[3]
|
||||||
|
and poly_bounds[3] > obstacle_bounds[1]
|
||||||
|
):
|
||||||
|
poly_hits_obstacle_aabb = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not poly_hits_obstacle_aabb:
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj_id = static_obstacles.obj_ids[hit_idx]
|
||||||
|
if self._is_in_safety_zone_fast(hit_idx, start_port, end_port):
|
||||||
|
collision_found = False
|
||||||
|
for polygon in result.collision_geometry:
|
||||||
|
if not self._is_in_safety_zone(polygon, obj_id, start_port, end_port):
|
||||||
|
collision_found = True
|
||||||
|
break
|
||||||
|
if collision_found:
|
||||||
|
return True
|
||||||
|
continue
|
||||||
|
|
||||||
|
static_obstacle = static_obstacles.dilated[obj_id]
|
||||||
|
for polygon in result.dilated_collision_geometry:
|
||||||
|
if polygon.intersects(static_obstacle) and not polygon.touches(static_obstacle):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
self.metrics["congestion_tree_queries"] += 1
|
||||||
|
self._ensure_dynamic_tree()
|
||||||
|
if dynamic_paths.tree is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_bounds = result.total_dilated_bounds
|
||||||
|
dynamic_bounds = dynamic_paths.bounds_array
|
||||||
|
possible_total = (
|
||||||
|
(total_bounds[0] < dynamic_bounds[:, 2])
|
||||||
|
& (total_bounds[2] > dynamic_bounds[:, 0])
|
||||||
|
& (total_bounds[1] < dynamic_bounds[:, 3])
|
||||||
|
& (total_bounds[3] > dynamic_bounds[:, 1])
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_hits_mask = dynamic_paths.net_ids_array != net_id
|
||||||
|
if not numpy.any(possible_total & valid_hits_mask):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
geometries_to_test = result.dilated_collision_geometry
|
||||||
|
res_indices, tree_indices = dynamic_paths.tree.query(geometries_to_test, predicate="intersects")
|
||||||
|
if tree_indices.size == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
||||||
|
unique_other_nets = numpy.unique(hit_net_ids[hit_net_ids != net_id])
|
||||||
|
if unique_other_nets.size == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
tree_geometries = dynamic_paths.tree.geometries
|
||||||
|
real_hits_count = 0
|
||||||
|
for other_net_id in unique_other_nets:
|
||||||
|
other_mask = hit_net_ids == other_net_id
|
||||||
|
sub_tree_indices = tree_indices[other_mask]
|
||||||
|
sub_res_indices = res_indices[other_mask]
|
||||||
|
|
||||||
|
found_real = False
|
||||||
|
for index in range(len(sub_tree_indices)):
|
||||||
|
test_geometry = geometries_to_test[sub_res_indices[index]]
|
||||||
|
tree_geometry = tree_geometries[sub_tree_indices[index]]
|
||||||
|
if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7:
|
||||||
|
found_real = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_real:
|
||||||
|
real_hits_count += 1
|
||||||
|
|
||||||
|
return real_hits_count
|
||||||
|
|
||||||
|
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
if not dynamic_paths.geometries:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_bounds = result.total_dilated_bounds
|
||||||
|
self._ensure_dynamic_grid()
|
||||||
|
dynamic_grid = dynamic_paths.grid
|
||||||
|
if not dynamic_grid:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(total_bounds, self.grid_cell_size)
|
||||||
|
|
||||||
|
if gx_min == gx_max and gy_min == gy_max:
|
||||||
|
cell = (gx_min, gy_min)
|
||||||
|
if cell in dynamic_grid:
|
||||||
|
for obj_id in dynamic_grid[cell]:
|
||||||
|
if dynamic_paths.geometries[obj_id][0] != net_id:
|
||||||
|
return self._check_real_congestion(result, net_id)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
any_possible = False
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
cell = (gx, gy)
|
||||||
|
if cell in dynamic_grid:
|
||||||
|
for obj_id in dynamic_grid[cell]:
|
||||||
|
if dynamic_paths.geometries[obj_id][0] != net_id:
|
||||||
|
any_possible = True
|
||||||
|
break
|
||||||
|
if any_possible:
|
||||||
|
break
|
||||||
|
if any_possible:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not any_possible:
|
||||||
|
return 0
|
||||||
|
return self._check_real_congestion(result, net_id)
|
||||||
|
|
||||||
|
def _check_static_collision(
|
||||||
|
self,
|
||||||
|
geometry: Polygon,
|
||||||
|
start_port: Port | None = None,
|
||||||
|
end_port: Port | None = None,
|
||||||
|
dilated_geometry: Polygon | None = None,
|
||||||
|
) -> bool:
|
||||||
|
static_obstacles = self._static_obstacles
|
||||||
|
self._ensure_static_tree()
|
||||||
|
if static_obstacles.tree is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if dilated_geometry is not None:
|
||||||
|
test_geometry = dilated_geometry
|
||||||
|
else:
|
||||||
|
distance = self.clearance / 2.0
|
||||||
|
test_geometry = geometry.buffer(distance + 1e-7, join_style=2) if distance > 0 else geometry
|
||||||
|
|
||||||
|
hits = static_obstacles.tree.query(test_geometry, predicate="intersects")
|
||||||
|
tree_geometries = static_obstacles.tree.geometries
|
||||||
|
for hit_idx in hits:
|
||||||
|
if test_geometry.touches(tree_geometries[hit_idx]):
|
||||||
|
continue
|
||||||
|
obj_id = static_obstacles.obj_ids[hit_idx]
|
||||||
|
if self._is_in_safety_zone(geometry, obj_id, start_port, end_port):
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_dynamic_collision(
|
||||||
|
self,
|
||||||
|
geometry: Polygon,
|
||||||
|
net_id: str,
|
||||||
|
dilated_geometry: Polygon | None = None,
|
||||||
|
) -> int:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
self._ensure_dynamic_tree()
|
||||||
|
if dynamic_paths.tree is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
test_geometry = dilated_geometry if dilated_geometry else geometry.buffer(self.clearance / 2.0)
|
||||||
|
hits = dynamic_paths.tree.query(test_geometry, predicate="intersects")
|
||||||
|
tree_geometries = dynamic_paths.tree.geometries
|
||||||
|
hit_net_ids: list[str] = []
|
||||||
|
for hit_idx in hits:
|
||||||
|
if test_geometry.touches(tree_geometries[hit_idx]):
|
||||||
|
continue
|
||||||
|
obj_id = dynamic_paths.obj_ids[hit_idx]
|
||||||
|
other_net_id = dynamic_paths.geometries[obj_id][0]
|
||||||
|
if other_net_id != net_id:
|
||||||
|
hit_net_ids.append(other_net_id)
|
||||||
|
if not hit_net_ids:
|
||||||
|
return 0
|
||||||
|
return len(numpy.unique(hit_net_ids))
|
||||||
|
|
||||||
def check_collision(
|
def check_collision(
|
||||||
self,
|
self,
|
||||||
|
|
@ -193,16 +413,16 @@ class CollisionEngine:
|
||||||
bounds: tuple[float, float, float, float] | None = None,
|
bounds: tuple[float, float, float, float] | None = None,
|
||||||
net_width: float | None = None,
|
net_width: float | None = None,
|
||||||
) -> bool | int:
|
) -> bool | int:
|
||||||
return self._collision_query_checker.check_collision(
|
del bounds, net_width
|
||||||
geometry,
|
|
||||||
net_id,
|
if buffer_mode == "static":
|
||||||
buffer_mode=buffer_mode,
|
return self._check_static_collision(
|
||||||
start_port=start_port,
|
geometry,
|
||||||
end_port=end_port,
|
start_port=start_port,
|
||||||
dilated_geometry=dilated_geometry,
|
end_port=end_port,
|
||||||
bounds=bounds,
|
dilated_geometry=dilated_geometry,
|
||||||
net_width=net_width,
|
)
|
||||||
)
|
return self._check_dynamic_collision(geometry, net_id, dilated_geometry=dilated_geometry)
|
||||||
|
|
||||||
def is_collision(
|
def is_collision(
|
||||||
self,
|
self,
|
||||||
|
|
@ -212,7 +432,6 @@ class CollisionEngine:
|
||||||
start_port: Port | None = None,
|
start_port: Port | None = None,
|
||||||
end_port: Port | None = None,
|
end_port: Port | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
""" Unified entry point for static collision checks. """
|
|
||||||
result = self.check_collision(
|
result = self.check_collision(
|
||||||
geometry,
|
geometry,
|
||||||
net_id,
|
net_id,
|
||||||
|
|
@ -223,12 +442,157 @@ class CollisionEngine:
|
||||||
)
|
)
|
||||||
return bool(result)
|
return bool(result)
|
||||||
|
|
||||||
def verify_path_report(self, net_id: str, components: list[ComponentResult]) -> PathVerificationReport:
|
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
|
||||||
return self._path_verifier.verify_path_report(net_id, components)
|
static_collision_count = 0
|
||||||
|
dynamic_collision_count = 0
|
||||||
|
self_collision_count = 0
|
||||||
|
total_length = sum(component.length for component in components)
|
||||||
|
|
||||||
def verify_path(self, net_id: str, components: list[ComponentResult]) -> tuple[bool, int]:
|
static_obstacles = self._static_obstacles
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
|
||||||
|
self._ensure_static_raw_tree()
|
||||||
|
if static_obstacles.raw_tree is not None:
|
||||||
|
raw_geometries = static_obstacles.raw_tree.geometries
|
||||||
|
for component in components:
|
||||||
|
for polygon in component.physical_geometry:
|
||||||
|
buffered = polygon.buffer(self.clearance, join_style=2)
|
||||||
|
hits = static_obstacles.raw_tree.query(buffered, predicate="intersects")
|
||||||
|
for hit_idx in hits:
|
||||||
|
obstacle = raw_geometries[hit_idx]
|
||||||
|
if buffered.touches(obstacle):
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj_id = static_obstacles.raw_obj_ids[hit_idx]
|
||||||
|
if not self._is_in_safety_zone(polygon, obj_id, None, None):
|
||||||
|
static_collision_count += 1
|
||||||
|
|
||||||
|
self._ensure_dynamic_tree()
|
||||||
|
if dynamic_paths.tree is not None:
|
||||||
|
tree_geometries = dynamic_paths.tree.geometries
|
||||||
|
for component in components:
|
||||||
|
test_geometries = component.dilated_physical_geometry
|
||||||
|
res_indices, tree_indices = dynamic_paths.tree.query(test_geometries, predicate="intersects")
|
||||||
|
if tree_indices.size == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
||||||
|
component_hits = []
|
||||||
|
for index in range(len(tree_indices)):
|
||||||
|
if hit_net_ids[index] == str(net_id):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_geometry = test_geometries[res_indices[index]]
|
||||||
|
tree_geometry = tree_geometries[tree_indices[index]]
|
||||||
|
if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7:
|
||||||
|
component_hits.append(hit_net_ids[index])
|
||||||
|
|
||||||
|
if component_hits:
|
||||||
|
dynamic_collision_count += len(numpy.unique(component_hits))
|
||||||
|
|
||||||
|
for index, component in enumerate(components):
|
||||||
|
for other_index in range(index + 2, len(components)):
|
||||||
|
if components_overlap(component, components[other_index], prefer_actual=True):
|
||||||
|
self_collision_count += 1
|
||||||
|
|
||||||
|
return RoutingReport(
|
||||||
|
static_collision_count=static_collision_count,
|
||||||
|
dynamic_collision_count=dynamic_collision_count,
|
||||||
|
self_collision_count=self_collision_count,
|
||||||
|
total_length=total_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
def verify_path(self, net_id: str, components: Sequence[ComponentResult]) -> tuple[bool, int]:
|
||||||
report = self.verify_path_report(net_id, components)
|
report = self.verify_path_report(net_id, components)
|
||||||
return report.is_valid, report.collision_count
|
return report.is_valid, report.collision_count
|
||||||
|
|
||||||
def ray_cast(self, origin: Port, angle_deg: float, max_dist: float = 2000.0, net_width: float | None = None) -> float:
|
def ray_cast(
|
||||||
return self._ray_caster.ray_cast(origin, angle_deg, max_dist=max_dist, net_width=net_width)
|
self,
|
||||||
|
origin: Port,
|
||||||
|
angle_deg: float,
|
||||||
|
max_dist: float = 2000.0,
|
||||||
|
net_width: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
static_obstacles = self._static_obstacles
|
||||||
|
|
||||||
|
radians = numpy.radians(angle_deg)
|
||||||
|
cos_v, sin_v = numpy.cos(radians), numpy.sin(radians)
|
||||||
|
dx, dy = max_dist * cos_v, max_dist * sin_v
|
||||||
|
min_x, max_x = sorted([origin.x, origin.x + dx])
|
||||||
|
min_y, max_y = sorted([origin.y, origin.y + dy])
|
||||||
|
|
||||||
|
if net_width is not None:
|
||||||
|
tree = self._ensure_net_static_tree(net_width)
|
||||||
|
key = (round(net_width, 4), round(self.clearance, 4))
|
||||||
|
is_rect_array = static_obstacles.net_specific_is_rect[key]
|
||||||
|
bounds_array = static_obstacles.net_specific_bounds[key]
|
||||||
|
else:
|
||||||
|
self._ensure_static_tree()
|
||||||
|
tree = static_obstacles.tree
|
||||||
|
is_rect_array = static_obstacles.is_rect_array
|
||||||
|
bounds_array = static_obstacles.bounds_array
|
||||||
|
|
||||||
|
if tree is None:
|
||||||
|
return max_dist
|
||||||
|
|
||||||
|
candidates = tree.query(box(min_x, min_y, max_x, max_y))
|
||||||
|
if candidates.size == 0:
|
||||||
|
return max_dist
|
||||||
|
|
||||||
|
min_dist = max_dist
|
||||||
|
inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30
|
||||||
|
inv_dy = 1.0 / dy if abs(dy) > 1e-12 else 1e30
|
||||||
|
tree_geometries = tree.geometries
|
||||||
|
ray_line = None
|
||||||
|
|
||||||
|
candidates_bounds = bounds_array[candidates]
|
||||||
|
dist_sq = (candidates_bounds[:, 0] - origin.x) ** 2 + (candidates_bounds[:, 1] - origin.y) ** 2
|
||||||
|
sorted_indices = numpy.argsort(dist_sq)
|
||||||
|
|
||||||
|
for idx in sorted_indices:
|
||||||
|
candidate_id = candidates[idx]
|
||||||
|
bounds = bounds_array[candidate_id]
|
||||||
|
|
||||||
|
if abs(dx) < 1e-12:
|
||||||
|
if origin.x < bounds[0] or origin.x > bounds[2]:
|
||||||
|
tx_min, tx_max = 1e30, -1e30
|
||||||
|
else:
|
||||||
|
tx_min, tx_max = -1e30, 1e30
|
||||||
|
else:
|
||||||
|
t1, t2 = (bounds[0] - origin.x) * inv_dx, (bounds[2] - origin.x) * inv_dx
|
||||||
|
tx_min, tx_max = min(t1, t2), max(t1, t2)
|
||||||
|
|
||||||
|
if abs(dy) < 1e-12:
|
||||||
|
if origin.y < bounds[1] or origin.y > bounds[3]:
|
||||||
|
ty_min, ty_max = 1e30, -1e30
|
||||||
|
else:
|
||||||
|
ty_min, ty_max = -1e30, 1e30
|
||||||
|
else:
|
||||||
|
t1, t2 = (bounds[1] - origin.y) * inv_dy, (bounds[3] - origin.y) * inv_dy
|
||||||
|
ty_min, ty_max = min(t1, t2), max(t1, t2)
|
||||||
|
|
||||||
|
t_min, t_max = max(tx_min, ty_min), min(tx_max, ty_max)
|
||||||
|
if t_max < 0 or t_min > t_max or t_min > 1.0:
|
||||||
|
continue
|
||||||
|
if t_min * max_dist >= min_dist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_rect_array[candidate_id]:
|
||||||
|
min_dist = max(0.0, t_min * max_dist)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ray_line is None:
|
||||||
|
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
|
||||||
|
|
||||||
|
obstacle = tree_geometries[candidate_id]
|
||||||
|
if not obstacle.intersects(ray_line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
intersection = ray_line.intersection(obstacle)
|
||||||
|
if intersection.is_empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
distance = _intersection_distance(origin, intersection)
|
||||||
|
min_dist = min(min_dist, distance)
|
||||||
|
|
||||||
|
return min_dist
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from shapely.geometry import Polygon
|
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
|
|
||||||
|
|
||||||
class CollisionQueryChecker:
|
|
||||||
__slots__ = ("engine",)
|
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
def check_collision(
|
|
||||||
self,
|
|
||||||
geometry: Polygon,
|
|
||||||
net_id: str,
|
|
||||||
buffer_mode: Literal["static", "congestion"] = "static",
|
|
||||||
start_port: Port | None = None,
|
|
||||||
end_port: Port | None = None,
|
|
||||||
dilated_geometry: Polygon | None = None,
|
|
||||||
bounds: tuple[float, float, float, float] | None = None,
|
|
||||||
net_width: float | None = None,
|
|
||||||
) -> bool | int:
|
|
||||||
del bounds, net_width
|
|
||||||
|
|
||||||
if buffer_mode == "static":
|
|
||||||
return self._check_static_collision(
|
|
||||||
geometry,
|
|
||||||
start_port=start_port,
|
|
||||||
end_port=end_port,
|
|
||||||
dilated_geometry=dilated_geometry,
|
|
||||||
)
|
|
||||||
return self._check_dynamic_collision(geometry, net_id, dilated_geometry=dilated_geometry)
|
|
||||||
|
|
||||||
def _check_static_collision(
|
|
||||||
self,
|
|
||||||
geometry: Polygon,
|
|
||||||
start_port: Port | None = None,
|
|
||||||
end_port: Port | None = None,
|
|
||||||
dilated_geometry: Polygon | None = None,
|
|
||||||
) -> bool:
|
|
||||||
engine = self.engine
|
|
||||||
static_obstacles = engine._static_obstacles
|
|
||||||
engine._ensure_static_tree()
|
|
||||||
if static_obstacles.tree is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if dilated_geometry is not None:
|
|
||||||
test_geometry = dilated_geometry
|
|
||||||
else:
|
|
||||||
distance = engine.clearance / 2.0
|
|
||||||
test_geometry = geometry.buffer(distance + 1e-7, join_style=2) if distance > 0 else geometry
|
|
||||||
|
|
||||||
hits = static_obstacles.tree.query(test_geometry, predicate="intersects")
|
|
||||||
tree_geometries = static_obstacles.tree.geometries
|
|
||||||
for hit_idx in hits:
|
|
||||||
if test_geometry.touches(tree_geometries[hit_idx]):
|
|
||||||
continue
|
|
||||||
obj_id = static_obstacles.obj_ids[hit_idx]
|
|
||||||
if engine._is_in_safety_zone(geometry, obj_id, start_port, end_port):
|
|
||||||
continue
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_dynamic_collision(
|
|
||||||
self,
|
|
||||||
geometry: Polygon,
|
|
||||||
net_id: str,
|
|
||||||
dilated_geometry: Polygon | None = None,
|
|
||||||
) -> int:
|
|
||||||
engine = self.engine
|
|
||||||
dynamic_paths = engine._dynamic_paths
|
|
||||||
engine._ensure_dynamic_tree()
|
|
||||||
if dynamic_paths.tree is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
test_geometry = dilated_geometry if dilated_geometry else geometry.buffer(engine.clearance / 2.0)
|
|
||||||
hits = dynamic_paths.tree.query(test_geometry, predicate="intersects")
|
|
||||||
tree_geometries = dynamic_paths.tree.geometries
|
|
||||||
hit_net_ids: list[str] = []
|
|
||||||
for hit_idx in hits:
|
|
||||||
if test_geometry.touches(tree_geometries[hit_idx]):
|
|
||||||
continue
|
|
||||||
obj_id = dynamic_paths.obj_ids[hit_idx]
|
|
||||||
other_net_id = dynamic_paths.geometries[obj_id][0]
|
|
||||||
if other_net_id != net_id:
|
|
||||||
hit_net_ids.append(other_net_id)
|
|
||||||
if not hit_net_ids:
|
|
||||||
return 0
|
|
||||||
return len(numpy.unique(hit_net_ids))
|
|
||||||
|
|
@ -9,9 +9,9 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
def component_polygons(component: ComponentResult, prefer_actual: bool = False) -> list[Polygon]:
|
def component_polygons(component: ComponentResult, prefer_actual: bool = False) -> list[Polygon]:
|
||||||
if prefer_actual and component.actual_geometry is not None:
|
if prefer_actual:
|
||||||
return component.actual_geometry
|
return list(component.physical_geometry)
|
||||||
return component.geometry
|
return list(component.collision_geometry)
|
||||||
|
|
||||||
|
|
||||||
def component_bounds(component: ComponentResult, prefer_actual: bool = False) -> tuple[float, float, float, float]:
|
def component_bounds(component: ComponentResult, prefer_actual: bool = False) -> tuple[float, float, float, float]:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -12,61 +13,52 @@ from inire.constants import TOLERANCE_ANGULAR
|
||||||
from .primitives import Port, rotation_matrix2
|
from .primitives import Port, rotation_matrix2
|
||||||
|
|
||||||
|
|
||||||
|
MoveKind = Literal["straight", "bend90", "sbend"]
|
||||||
|
BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"]
|
||||||
|
BendCollisionModel = BendCollisionModelName | Polygon
|
||||||
|
|
||||||
|
|
||||||
def _normalize_length(value: float) -> float:
|
def _normalize_length(value: float) -> float:
|
||||||
return float(value)
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
class ComponentResult:
|
class ComponentResult:
|
||||||
__slots__ = (
|
start_port: Port
|
||||||
"start_port",
|
collision_geometry: tuple[Polygon, ...]
|
||||||
"geometry",
|
end_port: Port
|
||||||
"dilated_geometry",
|
length: float
|
||||||
"proxy_geometry",
|
move_type: MoveKind
|
||||||
"actual_geometry",
|
physical_geometry: tuple[Polygon, ...]
|
||||||
"dilated_actual_geometry",
|
dilated_collision_geometry: tuple[Polygon, ...]
|
||||||
"end_port",
|
dilated_physical_geometry: tuple[Polygon, ...]
|
||||||
"length",
|
_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False)
|
||||||
"move_type",
|
_total_bounds: tuple[float, float, float, float] = field(init=False, repr=False)
|
||||||
"_bounds",
|
_dilated_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False)
|
||||||
"_total_bounds",
|
_total_dilated_bounds: tuple[float, float, float, float] = field(init=False, repr=False)
|
||||||
"_dilated_bounds",
|
|
||||||
"_total_dilated_bounds",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __post_init__(self) -> None:
|
||||||
self,
|
collision_geometry = tuple(self.collision_geometry)
|
||||||
start_port: Port,
|
physical_geometry = tuple(self.physical_geometry)
|
||||||
geometry: list[Polygon],
|
dilated_collision_geometry = tuple(self.dilated_collision_geometry)
|
||||||
end_port: Port,
|
dilated_physical_geometry = tuple(self.dilated_physical_geometry)
|
||||||
length: float,
|
|
||||||
move_type: str,
|
|
||||||
dilated_geometry: list[Polygon] | None = None,
|
|
||||||
proxy_geometry: list[Polygon] | None = None,
|
|
||||||
actual_geometry: list[Polygon] | None = None,
|
|
||||||
dilated_actual_geometry: list[Polygon] | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.start_port = start_port
|
|
||||||
self.geometry = geometry
|
|
||||||
self.dilated_geometry = dilated_geometry
|
|
||||||
self.proxy_geometry = proxy_geometry
|
|
||||||
self.actual_geometry = actual_geometry
|
|
||||||
self.dilated_actual_geometry = dilated_actual_geometry
|
|
||||||
self.end_port = end_port
|
|
||||||
self.length = float(length)
|
|
||||||
self.move_type = move_type
|
|
||||||
|
|
||||||
self._bounds = [poly.bounds for poly in self.geometry]
|
object.__setattr__(self, "collision_geometry", collision_geometry)
|
||||||
self._total_bounds = _combine_bounds(self._bounds)
|
object.__setattr__(self, "physical_geometry", physical_geometry)
|
||||||
|
object.__setattr__(self, "dilated_collision_geometry", dilated_collision_geometry)
|
||||||
|
object.__setattr__(self, "dilated_physical_geometry", dilated_physical_geometry)
|
||||||
|
object.__setattr__(self, "length", float(self.length))
|
||||||
|
|
||||||
if self.dilated_geometry is None:
|
bounds = tuple(poly.bounds for poly in collision_geometry)
|
||||||
self._dilated_bounds = None
|
object.__setattr__(self, "_bounds", bounds)
|
||||||
self._total_dilated_bounds = None
|
object.__setattr__(self, "_total_bounds", _combine_bounds(list(bounds)))
|
||||||
else:
|
|
||||||
self._dilated_bounds = [poly.bounds for poly in self.dilated_geometry]
|
dilated_bounds = tuple(poly.bounds for poly in dilated_collision_geometry)
|
||||||
self._total_dilated_bounds = _combine_bounds(self._dilated_bounds)
|
object.__setattr__(self, "_dilated_bounds", dilated_bounds)
|
||||||
|
object.__setattr__(self, "_total_dilated_bounds", _combine_bounds(list(dilated_bounds)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bounds(self) -> list[tuple[float, float, float, float]]:
|
def bounds(self) -> tuple[tuple[float, float, float, float], ...]:
|
||||||
return self._bounds
|
return self._bounds
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -74,28 +66,23 @@ class ComponentResult:
|
||||||
return self._total_bounds
|
return self._total_bounds
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dilated_bounds(self) -> list[tuple[float, float, float, float]] | None:
|
def dilated_bounds(self) -> tuple[tuple[float, float, float, float], ...]:
|
||||||
return self._dilated_bounds
|
return self._dilated_bounds
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_dilated_bounds(self) -> tuple[float, float, float, float] | None:
|
def total_dilated_bounds(self) -> tuple[float, float, float, float]:
|
||||||
return self._total_dilated_bounds
|
return self._total_dilated_bounds
|
||||||
|
|
||||||
def translate(self, dx: int | float, dy: int | float) -> ComponentResult:
|
def translate(self, dx: int | float, dy: int | float) -> ComponentResult:
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
start_port=self.start_port + [dx, dy, 0],
|
start_port=self.start_port.translate(dx, dy),
|
||||||
geometry=[shapely_translate(poly, dx, dy) for poly in self.geometry],
|
collision_geometry=[shapely_translate(poly, dx, dy) for poly in self.collision_geometry],
|
||||||
end_port=self.end_port + [dx, dy, 0],
|
end_port=self.end_port.translate(dx, dy),
|
||||||
length=self.length,
|
length=self.length,
|
||||||
move_type=self.move_type,
|
move_type=self.move_type,
|
||||||
dilated_geometry=None if self.dilated_geometry is None else [shapely_translate(poly, dx, dy) for poly in self.dilated_geometry],
|
physical_geometry=[shapely_translate(poly, dx, dy) for poly in self.physical_geometry],
|
||||||
proxy_geometry=None if self.proxy_geometry is None else [shapely_translate(poly, dx, dy) for poly in self.proxy_geometry],
|
dilated_collision_geometry=[shapely_translate(poly, dx, dy) for poly in self.dilated_collision_geometry],
|
||||||
actual_geometry=None if self.actual_geometry is None else [shapely_translate(poly, dx, dy) for poly in self.actual_geometry],
|
dilated_physical_geometry=[shapely_translate(poly, dx, dy) for poly in self.dilated_physical_geometry],
|
||||||
dilated_actual_geometry=(
|
|
||||||
None
|
|
||||||
if self.dilated_actual_geometry is None
|
|
||||||
else [shapely_translate(poly, dx, dy) for poly in self.dilated_actual_geometry]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -144,16 +131,13 @@ def _get_arc_polygons(
|
||||||
return [Polygon(numpy.concatenate((inner_points, outer_points), axis=0))]
|
return [Polygon(numpy.concatenate((inner_points, outer_points), axis=0))]
|
||||||
|
|
||||||
|
|
||||||
def _clip_bbox(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float], clip_margin: float) -> Polygon:
|
def _clip_bbox(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon:
|
||||||
"""Return a conservative 8-point polygonal proxy for the arc.
|
"""Return a conservative 8-point polygonal proxy for the arc.
|
||||||
|
|
||||||
The polygon uses 4 points along the outer edge and 4 along the inner edge.
|
The polygon uses 4 points along the outer edge and 4 along the inner edge.
|
||||||
The outer edge is a circumscribed polyline and the inner edge is an
|
The outer edge is a circumscribed polyline and the inner edge is an
|
||||||
inscribed polyline, so the result conservatively contains the true arc.
|
inscribed polyline, so the result conservatively contains the true arc.
|
||||||
`clip_margin` is kept for API compatibility but is not used by this proxy.
|
|
||||||
"""
|
"""
|
||||||
del clip_margin
|
|
||||||
|
|
||||||
cx, cy = cxy
|
cx, cy = cxy
|
||||||
sample_count = 4
|
sample_count = 4
|
||||||
angle_span = abs(float(ts[1]) - float(ts[0]))
|
angle_span = abs(float(ts[1]) - float(ts[0]))
|
||||||
|
|
@ -194,11 +178,10 @@ def _transform_custom_collision_polygon(
|
||||||
|
|
||||||
def _apply_collision_model(
|
def _apply_collision_model(
|
||||||
arc_poly: Polygon,
|
arc_poly: Polygon,
|
||||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
|
collision_type: BendCollisionModel,
|
||||||
radius: float,
|
radius: float,
|
||||||
width: float,
|
width: float,
|
||||||
cxy: tuple[float, float],
|
cxy: tuple[float, float],
|
||||||
clip_margin: float,
|
|
||||||
ts: tuple[float, float],
|
ts: tuple[float, float],
|
||||||
rotation_deg: float = 0.0,
|
rotation_deg: float = 0.0,
|
||||||
mirror_y: bool = False,
|
mirror_y: bool = False,
|
||||||
|
|
@ -208,7 +191,7 @@ def _apply_collision_model(
|
||||||
if collision_type == "arc":
|
if collision_type == "arc":
|
||||||
return [arc_poly]
|
return [arc_poly]
|
||||||
if collision_type == "clipped_bbox":
|
if collision_type == "clipped_bbox":
|
||||||
clipped = _clip_bbox(cxy, radius, width, ts, clip_margin)
|
clipped = _clip_bbox(cxy, radius, width, ts)
|
||||||
return [clipped if not clipped.is_empty else box(*arc_poly.bounds)]
|
return [clipped if not clipped.is_empty else box(*arc_poly.bounds)]
|
||||||
return [box(*arc_poly.bounds)]
|
return [box(*arc_poly.bounds)]
|
||||||
|
|
||||||
|
|
@ -231,7 +214,6 @@ class Straight:
|
||||||
poly_points = (pts @ rot2.T) + numpy.array((start_port.x, start_port.y))
|
poly_points = (pts @ rot2.T) + numpy.array((start_port.x, start_port.y))
|
||||||
geometry = [Polygon(poly_points)]
|
geometry = [Polygon(poly_points)]
|
||||||
|
|
||||||
dilated_geometry = None
|
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
half_w_d = half_w + dilation
|
half_w_d = half_w + dilation
|
||||||
pts_d = numpy.array(
|
pts_d = numpy.array(
|
||||||
|
|
@ -244,16 +226,18 @@ class Straight:
|
||||||
)
|
)
|
||||||
poly_points_d = (pts_d @ rot2.T) + numpy.array((start_port.x, start_port.y))
|
poly_points_d = (pts_d @ rot2.T) + numpy.array((start_port.x, start_port.y))
|
||||||
dilated_geometry = [Polygon(poly_points_d)]
|
dilated_geometry = [Polygon(poly_points_d)]
|
||||||
|
else:
|
||||||
|
dilated_geometry = geometry
|
||||||
|
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
start_port=start_port,
|
start_port=start_port,
|
||||||
geometry=geometry,
|
collision_geometry=geometry,
|
||||||
end_port=end_port,
|
end_port=end_port,
|
||||||
length=abs(length_f),
|
length=abs(length_f),
|
||||||
move_type="Straight",
|
move_type="straight",
|
||||||
dilated_geometry=dilated_geometry,
|
physical_geometry=geometry,
|
||||||
actual_geometry=geometry,
|
dilated_collision_geometry=dilated_geometry,
|
||||||
dilated_actual_geometry=dilated_geometry,
|
dilated_physical_geometry=dilated_geometry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -265,8 +249,7 @@ class Bend90:
|
||||||
width: float,
|
width: float,
|
||||||
direction: Literal["CW", "CCW"],
|
direction: Literal["CW", "CCW"],
|
||||||
sagitta: float = 0.01,
|
sagitta: float = 0.01,
|
||||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
collision_type: BendCollisionModel = "arc",
|
||||||
clip_margin: float = 10.0,
|
|
||||||
dilation: float = 0.0,
|
dilation: float = 0.0,
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
rot2 = rotation_matrix2(start_port.r)
|
rot2 = rotation_matrix2(start_port.r)
|
||||||
|
|
@ -290,32 +273,18 @@ class Bend90:
|
||||||
radius,
|
radius,
|
||||||
width,
|
width,
|
||||||
(float(center_xy[0]), float(center_xy[1])),
|
(float(center_xy[0]), float(center_xy[1])),
|
||||||
clip_margin,
|
|
||||||
ts,
|
ts,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign < 0),
|
mirror_y=(sign < 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
proxy_geometry = None
|
physical_geometry = collision_polys if uses_custom_geometry else arc_polys
|
||||||
if collision_type == "arc":
|
|
||||||
proxy_geometry = _apply_collision_model(
|
|
||||||
arc_polys[0],
|
|
||||||
"clipped_bbox",
|
|
||||||
radius,
|
|
||||||
width,
|
|
||||||
(float(center_xy[0]), float(center_xy[1])),
|
|
||||||
clip_margin,
|
|
||||||
ts,
|
|
||||||
)
|
|
||||||
|
|
||||||
dilated_actual_geometry = None
|
|
||||||
dilated_geometry = None
|
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if uses_custom_geometry:
|
if uses_custom_geometry:
|
||||||
dilated_actual_geometry = [poly.buffer(dilation) for poly in collision_polys]
|
dilated_physical_geometry = [poly.buffer(dilation) for poly in collision_polys]
|
||||||
dilated_geometry = dilated_actual_geometry
|
dilated_collision_geometry = dilated_physical_geometry
|
||||||
else:
|
else:
|
||||||
dilated_actual_geometry = _get_arc_polygons(
|
dilated_physical_geometry = _get_arc_polygons(
|
||||||
(float(center_xy[0]), float(center_xy[1])),
|
(float(center_xy[0]), float(center_xy[1])),
|
||||||
radius,
|
radius,
|
||||||
width,
|
width,
|
||||||
|
|
@ -323,18 +292,22 @@ class Bend90:
|
||||||
sagitta,
|
sagitta,
|
||||||
dilation=dilation,
|
dilation=dilation,
|
||||||
)
|
)
|
||||||
dilated_geometry = dilated_actual_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
|
dilated_collision_geometry = (
|
||||||
|
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dilated_physical_geometry = physical_geometry
|
||||||
|
dilated_collision_geometry = collision_polys
|
||||||
|
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
start_port=start_port,
|
start_port=start_port,
|
||||||
geometry=collision_polys,
|
collision_geometry=collision_polys,
|
||||||
end_port=end_port,
|
end_port=end_port,
|
||||||
length=abs(radius) * numpy.pi / 2.0,
|
length=abs(radius) * numpy.pi / 2.0,
|
||||||
move_type="Bend90",
|
move_type="bend90",
|
||||||
dilated_geometry=dilated_geometry,
|
physical_geometry=physical_geometry,
|
||||||
proxy_geometry=proxy_geometry,
|
dilated_collision_geometry=dilated_collision_geometry,
|
||||||
actual_geometry=collision_polys if uses_custom_geometry else arc_polys,
|
dilated_physical_geometry=dilated_physical_geometry,
|
||||||
dilated_actual_geometry=dilated_actual_geometry,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -346,8 +319,7 @@ class SBend:
|
||||||
radius: float,
|
radius: float,
|
||||||
width: float,
|
width: float,
|
||||||
sagitta: float = 0.01,
|
sagitta: float = 0.01,
|
||||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
collision_type: BendCollisionModel = "arc",
|
||||||
clip_margin: float = 10.0,
|
|
||||||
dilation: float = 0.0,
|
dilation: float = 0.0,
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
if abs(offset) >= 2 * radius:
|
if abs(offset) >= 2 * radius:
|
||||||
|
|
@ -383,7 +355,6 @@ class SBend:
|
||||||
radius,
|
radius,
|
||||||
width,
|
width,
|
||||||
(float(c1_xy[0]), float(c1_xy[1])),
|
(float(c1_xy[0]), float(c1_xy[1])),
|
||||||
clip_margin,
|
|
||||||
ts1,
|
ts1,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign < 0),
|
mirror_y=(sign < 0),
|
||||||
|
|
@ -394,41 +365,36 @@ class SBend:
|
||||||
radius,
|
radius,
|
||||||
width,
|
width,
|
||||||
(float(c2_xy[0]), float(c2_xy[1])),
|
(float(c2_xy[0]), float(c2_xy[1])),
|
||||||
clip_margin,
|
|
||||||
ts2,
|
ts2,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign > 0),
|
mirror_y=(sign > 0),
|
||||||
)[0],
|
)[0],
|
||||||
]
|
]
|
||||||
|
|
||||||
proxy_geometry = None
|
physical_geometry = geometry if uses_custom_geometry else actual_geometry
|
||||||
if collision_type == "arc":
|
|
||||||
proxy_geometry = [
|
|
||||||
_apply_collision_model(arc1, "clipped_bbox", radius, width, (float(c1_xy[0]), float(c1_xy[1])), clip_margin, ts1)[0],
|
|
||||||
_apply_collision_model(arc2, "clipped_bbox", radius, width, (float(c2_xy[0]), float(c2_xy[1])), clip_margin, ts2)[0],
|
|
||||||
]
|
|
||||||
|
|
||||||
dilated_actual_geometry = None
|
|
||||||
dilated_geometry = None
|
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if uses_custom_geometry:
|
if uses_custom_geometry:
|
||||||
dilated_actual_geometry = [poly.buffer(dilation) for poly in geometry]
|
dilated_physical_geometry = [poly.buffer(dilation) for poly in geometry]
|
||||||
dilated_geometry = dilated_actual_geometry
|
dilated_collision_geometry = dilated_physical_geometry
|
||||||
else:
|
else:
|
||||||
dilated_actual_geometry = [
|
dilated_physical_geometry = [
|
||||||
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0],
|
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0],
|
||||||
_get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0],
|
_get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0],
|
||||||
]
|
]
|
||||||
dilated_geometry = dilated_actual_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry]
|
dilated_collision_geometry = (
|
||||||
|
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dilated_physical_geometry = physical_geometry
|
||||||
|
dilated_collision_geometry = geometry
|
||||||
|
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
start_port=start_port,
|
start_port=start_port,
|
||||||
geometry=geometry,
|
collision_geometry=geometry,
|
||||||
end_port=end_port,
|
end_port=end_port,
|
||||||
length=2.0 * radius * theta,
|
length=2.0 * radius * theta,
|
||||||
move_type="SBend",
|
move_type="sbend",
|
||||||
dilated_geometry=dilated_geometry,
|
physical_geometry=physical_geometry,
|
||||||
proxy_geometry=proxy_geometry,
|
dilated_collision_geometry=dilated_collision_geometry,
|
||||||
actual_geometry=geometry if uses_custom_geometry else actual_geometry,
|
dilated_physical_geometry=dilated_physical_geometry,
|
||||||
dilated_actual_geometry=dilated_actual_geometry,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicCongestionChecker:
|
|
||||||
__slots__ = ("engine",)
|
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
|
||||||
engine = self.engine
|
|
||||||
dynamic_paths = engine._dynamic_paths
|
|
||||||
if not dynamic_paths.geometries:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
total_bounds = result.total_dilated_bounds
|
|
||||||
if total_bounds is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
engine._ensure_dynamic_grid()
|
|
||||||
dynamic_grid = dynamic_paths.grid
|
|
||||||
if not dynamic_grid:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
cell_size_inv = engine._inv_grid_cell_size
|
|
||||||
gx_min = int(total_bounds[0] * cell_size_inv)
|
|
||||||
gy_min = int(total_bounds[1] * cell_size_inv)
|
|
||||||
gx_max = int(total_bounds[2] * cell_size_inv)
|
|
||||||
gy_max = int(total_bounds[3] * cell_size_inv)
|
|
||||||
|
|
||||||
dynamic_geometries = dynamic_paths.geometries
|
|
||||||
|
|
||||||
if gx_min == gx_max and gy_min == gy_max:
|
|
||||||
cell = (gx_min, gy_min)
|
|
||||||
if cell in dynamic_grid:
|
|
||||||
for obj_id in dynamic_grid[cell]:
|
|
||||||
if dynamic_geometries[obj_id][0] != net_id:
|
|
||||||
return self.check_real_congestion(result, net_id)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
any_possible = False
|
|
||||||
for gx in range(gx_min, gx_max + 1):
|
|
||||||
for gy in range(gy_min, gy_max + 1):
|
|
||||||
cell = (gx, gy)
|
|
||||||
if cell in dynamic_grid:
|
|
||||||
for obj_id in dynamic_grid[cell]:
|
|
||||||
if dynamic_geometries[obj_id][0] != net_id:
|
|
||||||
any_possible = True
|
|
||||||
break
|
|
||||||
if any_possible:
|
|
||||||
break
|
|
||||||
if any_possible:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not any_possible:
|
|
||||||
return 0
|
|
||||||
return self.check_real_congestion(result, net_id)
|
|
||||||
|
|
||||||
def check_real_congestion(self, result: ComponentResult, net_id: str) -> int:
|
|
||||||
engine = self.engine
|
|
||||||
dynamic_paths = engine._dynamic_paths
|
|
||||||
engine.metrics["congestion_tree_queries"] += 1
|
|
||||||
engine._ensure_dynamic_tree()
|
|
||||||
if dynamic_paths.tree is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
total_bounds = result.total_dilated_bounds
|
|
||||||
dynamic_bounds = engine._dynamic_bounds_array
|
|
||||||
possible_total = (
|
|
||||||
(total_bounds[0] < dynamic_bounds[:, 2])
|
|
||||||
& (total_bounds[2] > dynamic_bounds[:, 0])
|
|
||||||
& (total_bounds[1] < dynamic_bounds[:, 3])
|
|
||||||
& (total_bounds[3] > dynamic_bounds[:, 1])
|
|
||||||
)
|
|
||||||
|
|
||||||
valid_hits_mask = dynamic_paths.net_ids_array != net_id
|
|
||||||
if not numpy.any(possible_total & valid_hits_mask):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
geoms_to_test = result.dilated_geometry if result.dilated_geometry else result.geometry
|
|
||||||
res_indices, tree_indices = dynamic_paths.tree.query(geoms_to_test, predicate="intersects")
|
|
||||||
if tree_indices.size == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
|
||||||
unique_other_nets = numpy.unique(hit_net_ids[hit_net_ids != net_id])
|
|
||||||
if unique_other_nets.size == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
tree_geometries = dynamic_paths.tree.geometries
|
|
||||||
real_hits_count = 0
|
|
||||||
|
|
||||||
for other_net_id in unique_other_nets:
|
|
||||||
other_mask = hit_net_ids == other_net_id
|
|
||||||
sub_tree_indices = tree_indices[other_mask]
|
|
||||||
sub_res_indices = res_indices[other_mask]
|
|
||||||
|
|
||||||
found_real = False
|
|
||||||
for index in range(len(sub_tree_indices)):
|
|
||||||
test_geometry = geoms_to_test[sub_res_indices[index]]
|
|
||||||
tree_geometry = tree_geometries[sub_tree_indices[index]]
|
|
||||||
if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7:
|
|
||||||
found_real = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if found_real:
|
|
||||||
real_hits_count += 1
|
|
||||||
|
|
||||||
return real_hits_count
|
|
||||||
|
|
@ -6,11 +6,14 @@ import numpy
|
||||||
import rtree
|
import rtree
|
||||||
from shapely.strtree import STRtree
|
from shapely.strtree import STRtree
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
from inire.geometry.index_helpers import build_index_payload, iter_grid_cells
|
||||||
from shapely.geometry import Polygon
|
|
||||||
from shapely.prepared import PreparedGeometry
|
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
|
||||||
|
|
||||||
class DynamicPathIndex:
|
class DynamicPathIndex:
|
||||||
|
|
@ -19,47 +22,38 @@ class DynamicPathIndex:
|
||||||
"index",
|
"index",
|
||||||
"geometries",
|
"geometries",
|
||||||
"dilated",
|
"dilated",
|
||||||
"prepared",
|
|
||||||
"tree",
|
"tree",
|
||||||
"obj_ids",
|
"obj_ids",
|
||||||
"grid",
|
"grid",
|
||||||
"id_counter",
|
"id_counter",
|
||||||
"tree_dirty",
|
|
||||||
"net_ids_array",
|
"net_ids_array",
|
||||||
"bounds_array",
|
"bounds_array",
|
||||||
"locked_nets",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
def __init__(self, engine: RoutingWorld) -> None:
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self.index = rtree.index.Index()
|
self.index = rtree.index.Index()
|
||||||
self.geometries: dict[int, tuple[str, Polygon]] = {}
|
self.geometries: dict[int, tuple[str, Polygon]] = {}
|
||||||
self.dilated: dict[int, Polygon] = {}
|
self.dilated: dict[int, Polygon] = {}
|
||||||
self.prepared: dict[int, PreparedGeometry] = {}
|
|
||||||
self.tree: STRtree | None = None
|
self.tree: STRtree | None = None
|
||||||
self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
||||||
self.grid: dict[tuple[int, int], list[int]] = {}
|
self.grid: dict[tuple[int, int], list[int]] = {}
|
||||||
self.id_counter = 0
|
self.id_counter = 0
|
||||||
self.tree_dirty = True
|
self.net_ids_array = numpy.array([], dtype=object)
|
||||||
self.net_ids_array = numpy.array([], dtype="<U32")
|
|
||||||
self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
||||||
self.locked_nets: set[str] = set()
|
|
||||||
|
|
||||||
def invalidate_queries(self) -> None:
|
def invalidate_queries(self) -> None:
|
||||||
self.tree = None
|
self.tree = None
|
||||||
self.grid = {}
|
self.grid = {}
|
||||||
self.tree_dirty = True
|
|
||||||
|
|
||||||
def ensure_tree(self) -> None:
|
def ensure_tree(self) -> None:
|
||||||
if self.tree is None and self.dilated:
|
if self.tree is None and self.dilated:
|
||||||
ids = sorted(self.dilated.keys())
|
ids, geometries, bounds_array = build_index_payload(self.dilated)
|
||||||
geometries = [self.dilated[i] for i in ids]
|
|
||||||
self.tree = STRtree(geometries)
|
self.tree = STRtree(geometries)
|
||||||
self.obj_ids = numpy.array(ids, dtype=numpy.int32)
|
self.obj_ids = numpy.array(ids, dtype=numpy.int32)
|
||||||
self.bounds_array = numpy.array([geometry.bounds for geometry in geometries])
|
self.bounds_array = bounds_array
|
||||||
net_ids = [self.geometries[obj_id][0] for obj_id in self.obj_ids]
|
net_ids = [self.geometries[obj_id][0] for obj_id in self.obj_ids]
|
||||||
self.net_ids_array = numpy.array(net_ids, dtype="<U32")
|
self.net_ids_array = numpy.array(net_ids, dtype=object)
|
||||||
self.tree_dirty = False
|
|
||||||
|
|
||||||
def ensure_grid(self) -> None:
|
def ensure_grid(self) -> None:
|
||||||
if self.grid or not self.dilated:
|
if self.grid or not self.dilated:
|
||||||
|
|
@ -67,27 +61,20 @@ class DynamicPathIndex:
|
||||||
|
|
||||||
cell_size = self.engine.grid_cell_size
|
cell_size = self.engine.grid_cell_size
|
||||||
for obj_id, polygon in self.dilated.items():
|
for obj_id, polygon in self.dilated.items():
|
||||||
bounds = polygon.bounds
|
for cell in iter_grid_cells(polygon.bounds, cell_size):
|
||||||
for gx in range(int(bounds[0] / cell_size), int(bounds[2] / cell_size) + 1):
|
self.grid.setdefault(cell, []).append(obj_id)
|
||||||
for gy in range(int(bounds[1] / cell_size), int(bounds[3] / cell_size) + 1):
|
|
||||||
cell = (gx, gy)
|
|
||||||
self.grid.setdefault(cell, []).append(obj_id)
|
|
||||||
|
|
||||||
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
||||||
self.invalidate_queries()
|
self.invalidate_queries()
|
||||||
dilation = self.engine.clearance / 2.0
|
|
||||||
for index, polygon in enumerate(geometry):
|
for index, polygon in enumerate(geometry):
|
||||||
obj_id = self.id_counter
|
obj_id = self.id_counter
|
||||||
self.id_counter += 1
|
self.id_counter += 1
|
||||||
dilated = dilated_geometry[index] if dilated_geometry else polygon.buffer(dilation)
|
dilated = dilated_geometry[index]
|
||||||
self.geometries[obj_id] = (net_id, polygon)
|
self.geometries[obj_id] = (net_id, polygon)
|
||||||
self.dilated[obj_id] = dilated
|
self.dilated[obj_id] = dilated
|
||||||
self.index.insert(obj_id, dilated.bounds)
|
self.index.insert(obj_id, dilated.bounds)
|
||||||
|
|
||||||
def remove_path(self, net_id: str) -> None:
|
def remove_path(self, net_id: str) -> None:
|
||||||
if net_id in self.locked_nets:
|
|
||||||
return
|
|
||||||
|
|
||||||
to_remove = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id]
|
to_remove = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id]
|
||||||
self.remove_obj_ids(to_remove)
|
self.remove_obj_ids(to_remove)
|
||||||
|
|
||||||
|
|
@ -101,14 +88,7 @@ class DynamicPathIndex:
|
||||||
del self.geometries[obj_id]
|
del self.geometries[obj_id]
|
||||||
del self.dilated[obj_id]
|
del self.dilated[obj_id]
|
||||||
|
|
||||||
def lock_net(self, net_id: str) -> None:
|
def clear_paths(self) -> None:
|
||||||
self.locked_nets.add(net_id)
|
if not self.geometries:
|
||||||
to_move = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id]
|
return
|
||||||
for obj_id in to_move:
|
self.remove_obj_ids(list(self.geometries))
|
||||||
polygon = self.geometries[obj_id][1]
|
|
||||||
dilated = self.dilated[obj_id]
|
|
||||||
self.engine.add_static_obstacle(polygon, dilated_geometry=dilated)
|
|
||||||
self.remove_obj_ids(to_move)
|
|
||||||
|
|
||||||
def unlock_net(self, net_id: str) -> None:
|
|
||||||
self.locked_nets.discard(net_id)
|
|
||||||
|
|
|
||||||
48
inire/geometry/index_helpers.py
Normal file
48
inire/geometry/index_helpers.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections.abc import Iterator, Mapping
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
GeometryT = TypeVar("GeometryT")
|
||||||
|
|
||||||
|
|
||||||
|
def build_index_payload(
|
||||||
|
geometries: Mapping[int, GeometryT],
|
||||||
|
) -> tuple[list[int], list[GeometryT], numpy.ndarray]:
|
||||||
|
obj_ids = sorted(geometries)
|
||||||
|
ordered_geometries = [geometries[obj_id] for obj_id in obj_ids]
|
||||||
|
bounds_array = numpy.array([geometry.bounds for geometry in ordered_geometries], dtype=numpy.float64)
|
||||||
|
if not ordered_geometries:
|
||||||
|
bounds_array = bounds_array.reshape(0, 4)
|
||||||
|
return obj_ids, ordered_geometries, bounds_array
|
||||||
|
|
||||||
|
|
||||||
|
def grid_cell_span(
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
cell_size: float,
|
||||||
|
) -> tuple[int, int, int, int]:
|
||||||
|
return (
|
||||||
|
math.floor(bounds[0] / cell_size),
|
||||||
|
math.floor(bounds[1] / cell_size),
|
||||||
|
math.floor(bounds[2] / cell_size),
|
||||||
|
math.floor(bounds[3] / cell_size),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_grid_cells(
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
cell_size: float,
|
||||||
|
) -> Iterator[tuple[int, int]]:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, cell_size)
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
yield (gx, gy)
|
||||||
|
|
||||||
|
|
||||||
|
def is_axis_aligned_rect(geometry, *, tolerance: float = 1e-4) -> bool:
|
||||||
|
bounds = geometry.bounds
|
||||||
|
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
||||||
|
return abs(geometry.area - area) < tolerance
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
from inire.geometry.component_overlap import components_overlap
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PathVerificationReport:
|
|
||||||
static_collision_count: int
|
|
||||||
dynamic_collision_count: int
|
|
||||||
self_collision_count: int
|
|
||||||
total_length: float
|
|
||||||
|
|
||||||
@property
|
|
||||||
def collision_count(self) -> int:
|
|
||||||
return self.static_collision_count + self.dynamic_collision_count + self.self_collision_count
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_valid(self) -> bool:
|
|
||||||
return self.collision_count == 0
|
|
||||||
|
|
||||||
class PathVerifier:
|
|
||||||
__slots__ = ("engine",)
|
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
def verify_path_report(self, net_id: str, components: list[ComponentResult]) -> PathVerificationReport:
|
|
||||||
"""
|
|
||||||
Non-approximated, full-polygon intersection check of a path against all
|
|
||||||
static obstacles, other nets, and itself.
|
|
||||||
"""
|
|
||||||
static_collision_count = 0
|
|
||||||
dynamic_collision_count = 0
|
|
||||||
self_collision_count = 0
|
|
||||||
total_length = sum(component.length for component in components)
|
|
||||||
|
|
||||||
engine = self.engine
|
|
||||||
static_obstacles = engine._static_obstacles
|
|
||||||
dynamic_paths = engine._dynamic_paths
|
|
||||||
|
|
||||||
# 1. Check against static obstacles.
|
|
||||||
engine._ensure_static_raw_tree()
|
|
||||||
if static_obstacles.raw_tree is not None:
|
|
||||||
raw_geoms = static_obstacles.raw_tree.geometries
|
|
||||||
for comp in components:
|
|
||||||
polygons = comp.actual_geometry if comp.actual_geometry is not None else comp.geometry
|
|
||||||
for polygon in polygons:
|
|
||||||
# Physical separation must be >= clearance.
|
|
||||||
buffered = polygon.buffer(engine.clearance, join_style=2)
|
|
||||||
hits = static_obstacles.raw_tree.query(buffered, predicate="intersects")
|
|
||||||
for hit_idx in hits:
|
|
||||||
obstacle = raw_geoms[hit_idx]
|
|
||||||
# If they only touch, gap is exactly clearance. Valid.
|
|
||||||
if buffered.touches(obstacle):
|
|
||||||
continue
|
|
||||||
|
|
||||||
obj_id = static_obstacles.raw_obj_ids[hit_idx]
|
|
||||||
if not engine._is_in_safety_zone(polygon, obj_id, None, None):
|
|
||||||
static_collision_count += 1
|
|
||||||
|
|
||||||
# 2. Check against other nets.
|
|
||||||
engine._ensure_dynamic_tree()
|
|
||||||
if dynamic_paths.tree is not None:
|
|
||||||
tree_geoms = dynamic_paths.tree.geometries
|
|
||||||
for comp in components:
|
|
||||||
# Robust fallback chain to ensure crossings are caught even with zero clearance.
|
|
||||||
test_geometries = comp.dilated_actual_geometry or comp.dilated_geometry or comp.actual_geometry or comp.geometry
|
|
||||||
if not test_geometries:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not isinstance(test_geometries, list | tuple | numpy.ndarray):
|
|
||||||
test_geometries = [test_geometries]
|
|
||||||
|
|
||||||
res_indices, tree_indices = dynamic_paths.tree.query(test_geometries, predicate="intersects")
|
|
||||||
if tree_indices.size == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
|
||||||
comp_hits = []
|
|
||||||
for i in range(len(tree_indices)):
|
|
||||||
if hit_net_ids[i] == str(net_id):
|
|
||||||
continue
|
|
||||||
|
|
||||||
p_new = test_geometries[res_indices[i]]
|
|
||||||
p_tree = tree_geoms[tree_indices[i]]
|
|
||||||
if not p_new.touches(p_tree) and p_new.intersection(p_tree).area > 1e-7:
|
|
||||||
comp_hits.append(hit_net_ids[i])
|
|
||||||
|
|
||||||
if comp_hits:
|
|
||||||
dynamic_collision_count += len(numpy.unique(comp_hits))
|
|
||||||
|
|
||||||
# 3. Check for self collisions between non-adjacent components.
|
|
||||||
for i, comp_i in enumerate(components):
|
|
||||||
for j in range(i + 2, len(components)):
|
|
||||||
if components_overlap(comp_i, components[j], prefer_actual=True):
|
|
||||||
self_collision_count += 1
|
|
||||||
|
|
||||||
return PathVerificationReport(
|
|
||||||
static_collision_count=static_collision_count,
|
|
||||||
dynamic_collision_count=dynamic_collision_count,
|
|
||||||
self_collision_count=self_collision_count,
|
|
||||||
total_length=total_length,
|
|
||||||
)
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from dataclasses import dataclass
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
def _normalize_angle(angle_deg: int | float) -> int:
|
def _normalize_angle(angle_deg: int | float) -> int:
|
||||||
|
|
@ -13,119 +13,43 @@ def _normalize_angle(angle_deg: int | float) -> int:
|
||||||
raise ValueError(f"Port angle must be Manhattan (multiple of 90), got {angle_deg!r}")
|
raise ValueError(f"Port angle must be Manhattan (multiple of 90), got {angle_deg!r}")
|
||||||
return angle
|
return angle
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
def _as_int32_triplet(value: ArrayLike) -> NDArray[numpy.int32]:
|
|
||||||
arr = numpy.asarray(value, dtype=numpy.int32)
|
|
||||||
if arr.shape != (3,):
|
|
||||||
raise ValueError(f"Port array must have shape (3,), got {arr.shape}")
|
|
||||||
arr = arr.copy()
|
|
||||||
arr[2] = _normalize_angle(int(arr[2]))
|
|
||||||
return arr
|
|
||||||
|
|
||||||
|
|
||||||
class Port:
|
class Port:
|
||||||
"""
|
"""
|
||||||
Port represented as an ndarray-backed (x, y, r) triple with int32 storage.
|
Port represented as a normalized integer (x, y, r) triple.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_xyr",)
|
x: int | float
|
||||||
|
y: int | float
|
||||||
|
r: int | float
|
||||||
|
|
||||||
def __init__(self, x: int | float, y: int | float, r: int | float) -> None:
|
def __post_init__(self) -> None:
|
||||||
self._xyr = numpy.array(
|
object.__setattr__(self, "x", int(round(self.x)))
|
||||||
(int(round(x)), int(round(y)), _normalize_angle(r)),
|
object.__setattr__(self, "y", int(round(self.y)))
|
||||||
dtype=numpy.int32,
|
object.__setattr__(self, "r", _normalize_angle(self.r))
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_array(cls, xyr: ArrayLike) -> Self:
|
|
||||||
obj = cls.__new__(cls)
|
|
||||||
obj._xyr = _as_int32_triplet(xyr)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@property
|
|
||||||
def x(self) -> int:
|
|
||||||
return int(self._xyr[0])
|
|
||||||
|
|
||||||
@x.setter
|
|
||||||
def x(self, val: int | float) -> None:
|
|
||||||
self._xyr[0] = int(round(val))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def y(self) -> int:
|
|
||||||
return int(self._xyr[1])
|
|
||||||
|
|
||||||
@y.setter
|
|
||||||
def y(self, val: int | float) -> None:
|
|
||||||
self._xyr[1] = int(round(val))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def r(self) -> int:
|
|
||||||
return int(self._xyr[2])
|
|
||||||
|
|
||||||
@r.setter
|
|
||||||
def r(self, val: int | float) -> None:
|
|
||||||
self._xyr[2] = _normalize_angle(val)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def orientation(self) -> int:
|
|
||||||
return self.r
|
|
||||||
|
|
||||||
@orientation.setter
|
|
||||||
def orientation(self, val: int | float) -> None:
|
|
||||||
self.r = val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def xyr(self) -> NDArray[numpy.int32]:
|
|
||||||
return self._xyr
|
|
||||||
|
|
||||||
@xyr.setter
|
|
||||||
def xyr(self, val: ArrayLike) -> None:
|
|
||||||
self._xyr = _as_int32_triplet(val)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"Port(x={self.x}, y={self.y}, r={self.r})"
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[int]:
|
|
||||||
return iter((self.x, self.y, self.r))
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return 3
|
|
||||||
|
|
||||||
def __getitem__(self, item: int | slice) -> int | NDArray[numpy.int32]:
|
|
||||||
return self._xyr[item]
|
|
||||||
|
|
||||||
def __array__(self, dtype: numpy.dtype | None = None) -> NDArray[numpy.int32]:
|
|
||||||
return numpy.asarray(self._xyr, dtype=dtype)
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, Port):
|
|
||||||
return False
|
|
||||||
return bool(numpy.array_equal(self._xyr, other._xyr))
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self.as_tuple())
|
|
||||||
|
|
||||||
def copy(self) -> Self:
|
|
||||||
return type(self).from_array(self._xyr.copy())
|
|
||||||
|
|
||||||
def as_tuple(self) -> tuple[int, int, int]:
|
def as_tuple(self) -> tuple[int, int, int]:
|
||||||
return (self.x, self.y, self.r)
|
return (int(self.x), int(self.y), int(self.r))
|
||||||
|
|
||||||
def translate(self, dxy: ArrayLike) -> Self:
|
def translate(
|
||||||
dxy_arr = numpy.asarray(dxy, dtype=numpy.int32)
|
self,
|
||||||
if dxy_arr.shape == (2,):
|
dx: int | float = 0,
|
||||||
return type(self)(self.x + int(dxy_arr[0]), self.y + int(dxy_arr[1]), self.r)
|
dy: int | float = 0,
|
||||||
if dxy_arr.shape == (3,):
|
rotation: int | float = 0,
|
||||||
return type(self)(self.x + int(dxy_arr[0]), self.y + int(dxy_arr[1]), self.r + int(dxy_arr[2]))
|
) -> Self:
|
||||||
raise ValueError(f"Translation must have shape (2,) or (3,), got {dxy_arr.shape}")
|
return type(self)(self.x + dx, self.y + dy, self.r + rotation)
|
||||||
|
|
||||||
def __add__(self, other: ArrayLike) -> Self:
|
def rotated(
|
||||||
return self.translate(other)
|
self,
|
||||||
|
angle: int | float,
|
||||||
def __sub__(self, other: ArrayLike | Self) -> NDArray[numpy.int32]:
|
origin: tuple[int | float, int | float] = (0, 0),
|
||||||
if isinstance(other, Port):
|
) -> Self:
|
||||||
return self._xyr - other._xyr
|
angle_i = _normalize_angle(angle)
|
||||||
return self._xyr - numpy.asarray(other, dtype=numpy.int32)
|
rot = rotation_matrix2(angle_i)
|
||||||
|
origin_xy = numpy.array((int(round(origin[0])), int(round(origin[1]))), dtype=numpy.int32)
|
||||||
|
rel = numpy.array((self.x, self.y), dtype=numpy.int32) - origin_xy
|
||||||
|
rotated = origin_xy + rot @ rel
|
||||||
|
return type(self)(int(rotated[0]), int(rotated[1]), self.r + angle_i)
|
||||||
|
|
||||||
|
|
||||||
ROT2_0 = numpy.array(((1, 0), (0, 1)), dtype=numpy.int32)
|
ROT2_0 = numpy.array(((1, 0), (0, 1)), dtype=numpy.int32)
|
||||||
|
|
@ -145,16 +69,3 @@ def rotation_matrix3(rotation_deg: int) -> NDArray[numpy.int32]:
|
||||||
rot3[:2, :2] = rot2
|
rot3[:2, :2] = rot2
|
||||||
rot3[2, 2] = 1
|
rot3[2, 2] = 1
|
||||||
return rot3
|
return rot3
|
||||||
|
|
||||||
|
|
||||||
def translate_port(port: Port, dx: int | float, dy: int | float) -> Port:
|
|
||||||
return Port(port.x + dx, port.y + dy, port.r)
|
|
||||||
|
|
||||||
|
|
||||||
def rotate_port(port: Port, angle: int | float, origin: tuple[int | float, int | float] = (0, 0)) -> Port:
|
|
||||||
angle_i = _normalize_angle(angle)
|
|
||||||
rot = rotation_matrix2(angle_i)
|
|
||||||
origin_xy = numpy.array((int(round(origin[0])), int(round(origin[1]))), dtype=numpy.int32)
|
|
||||||
rel = numpy.array((port.x, port.y), dtype=numpy.int32) - origin_xy
|
|
||||||
rotated = origin_xy + rot @ rel
|
|
||||||
return Port(int(rotated[0]), int(rotated[1]), port.r + angle_i)
|
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
from shapely.geometry import LineString, box
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from shapely.geometry.base import BaseGeometry
|
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
|
|
||||||
|
|
||||||
class RayCaster:
|
|
||||||
__slots__ = ("engine",)
|
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
def ray_cast(self, origin: Port, angle_deg: float, max_dist: float = 2000.0, net_width: float | None = None) -> float:
|
|
||||||
engine = self.engine
|
|
||||||
static_obstacles = engine._static_obstacles
|
|
||||||
|
|
||||||
rad = numpy.radians(angle_deg)
|
|
||||||
cos_v, sin_v = numpy.cos(rad), numpy.sin(rad)
|
|
||||||
dx, dy = max_dist * cos_v, max_dist * sin_v
|
|
||||||
min_x, max_x = sorted([origin.x, origin.x + dx])
|
|
||||||
min_y, max_y = sorted([origin.y, origin.y + dy])
|
|
||||||
|
|
||||||
key = None
|
|
||||||
if net_width is not None:
|
|
||||||
tree = engine._ensure_net_static_tree(net_width)
|
|
||||||
key = (round(net_width, 4), round(engine.clearance, 4))
|
|
||||||
is_rect_arr = static_obstacles.net_specific_is_rect[key]
|
|
||||||
bounds_arr = static_obstacles.net_specific_bounds[key]
|
|
||||||
else:
|
|
||||||
engine._ensure_static_tree()
|
|
||||||
tree = static_obstacles.tree
|
|
||||||
is_rect_arr = static_obstacles.is_rect_array
|
|
||||||
bounds_arr = static_obstacles.bounds_array
|
|
||||||
|
|
||||||
if tree is None:
|
|
||||||
return max_dist
|
|
||||||
|
|
||||||
candidates = tree.query(box(min_x, min_y, max_x, max_y))
|
|
||||||
if candidates.size == 0:
|
|
||||||
return max_dist
|
|
||||||
|
|
||||||
min_dist = max_dist
|
|
||||||
inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30
|
|
||||||
inv_dy = 1.0 / dy if abs(dy) > 1e-12 else 1e30
|
|
||||||
tree_geoms = tree.geometries
|
|
||||||
ray_line = None
|
|
||||||
|
|
||||||
# Distance to the AABB min corner is a cheap ordering heuristic.
|
|
||||||
candidates_bounds = bounds_arr[candidates]
|
|
||||||
dist_sq = (candidates_bounds[:, 0] - origin.x) ** 2 + (candidates_bounds[:, 1] - origin.y) ** 2
|
|
||||||
sorted_indices = numpy.argsort(dist_sq)
|
|
||||||
|
|
||||||
for idx in sorted_indices:
|
|
||||||
candidate_id = candidates[idx]
|
|
||||||
bounds = bounds_arr[candidate_id]
|
|
||||||
|
|
||||||
if abs(dx) < 1e-12:
|
|
||||||
if origin.x < bounds[0] or origin.x > bounds[2]:
|
|
||||||
tx_min, tx_max = 1e30, -1e30
|
|
||||||
else:
|
|
||||||
tx_min, tx_max = -1e30, 1e30
|
|
||||||
else:
|
|
||||||
t1, t2 = (bounds[0] - origin.x) * inv_dx, (bounds[2] - origin.x) * inv_dx
|
|
||||||
tx_min, tx_max = min(t1, t2), max(t1, t2)
|
|
||||||
|
|
||||||
if abs(dy) < 1e-12:
|
|
||||||
if origin.y < bounds[1] or origin.y > bounds[3]:
|
|
||||||
ty_min, ty_max = 1e30, -1e30
|
|
||||||
else:
|
|
||||||
ty_min, ty_max = -1e30, 1e30
|
|
||||||
else:
|
|
||||||
t1, t2 = (bounds[1] - origin.y) * inv_dy, (bounds[3] - origin.y) * inv_dy
|
|
||||||
ty_min, ty_max = min(t1, t2), max(t1, t2)
|
|
||||||
|
|
||||||
t_min, t_max = max(tx_min, ty_min), min(tx_max, ty_max)
|
|
||||||
if t_max < 0 or t_min > t_max or t_min > 1.0:
|
|
||||||
continue
|
|
||||||
if t_min * max_dist >= min_dist:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if is_rect_arr[candidate_id]:
|
|
||||||
min_dist = max(0.0, t_min * max_dist)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ray_line is None:
|
|
||||||
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
|
|
||||||
|
|
||||||
obstacle = tree_geoms[candidate_id]
|
|
||||||
if not obstacle.intersects(ray_line):
|
|
||||||
continue
|
|
||||||
|
|
||||||
intersection = ray_line.intersection(obstacle)
|
|
||||||
if intersection.is_empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
distance = self._intersection_distance(origin, intersection)
|
|
||||||
min_dist = min(min_dist, distance)
|
|
||||||
|
|
||||||
return min_dist
|
|
||||||
|
|
||||||
def _intersection_distance(self, origin: Port, geometry: BaseGeometry) -> float:
|
|
||||||
if hasattr(geometry, "geoms"):
|
|
||||||
return min(self._intersection_distance(origin, sub_geom) for sub_geom in geometry.geoms)
|
|
||||||
return float(numpy.sqrt((geometry.coords[0][0] - origin.x) ** 2 + (geometry.coords[0][1] - origin.y) ** 2))
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from shapely.geometry import box
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from shapely.geometry import Polygon
|
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
|
|
||||||
|
|
||||||
class StaticMoveChecker:
|
|
||||||
__slots__ = ("engine",)
|
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
|
|
||||||
engine = self.engine
|
|
||||||
engine.metrics["static_straight_fast"] += 1
|
|
||||||
reach = engine.ray_cast(start_port, start_port.orientation, max_dist=length + 0.01, net_width=net_width)
|
|
||||||
return reach < length - 0.001
|
|
||||||
|
|
||||||
def is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
|
||||||
engine = self.engine
|
|
||||||
sz = engine.safety_zone_radius
|
|
||||||
bounds = engine._static_obstacles.bounds_array[idx]
|
|
||||||
if start_port and bounds[0] - sz <= start_port.x <= bounds[2] + sz and bounds[1] - sz <= start_port.y <= bounds[3] + sz:
|
|
||||||
return True
|
|
||||||
return bool(
|
|
||||||
end_port
|
|
||||||
and bounds[0] - sz <= end_port.x <= bounds[2] + sz
|
|
||||||
and bounds[1] - sz <= end_port.y <= bounds[3] + sz
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_move_static(
|
|
||||||
self,
|
|
||||||
result: ComponentResult,
|
|
||||||
start_port: Port | None = None,
|
|
||||||
end_port: Port | None = None,
|
|
||||||
net_width: float | None = None,
|
|
||||||
) -> bool:
|
|
||||||
del net_width
|
|
||||||
|
|
||||||
engine = self.engine
|
|
||||||
static_obstacles = engine._static_obstacles
|
|
||||||
if not static_obstacles.dilated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
engine.metrics["static_tree_queries"] += 1
|
|
||||||
engine._ensure_static_tree()
|
|
||||||
|
|
||||||
total_bounds = result.total_dilated_bounds if result.total_dilated_bounds else result.total_bounds
|
|
||||||
hits = static_obstacles.tree.query(box(*total_bounds))
|
|
||||||
if hits.size == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
static_bounds = static_obstacles.bounds_array
|
|
||||||
move_poly_bounds = result.dilated_bounds if result.dilated_bounds else result.bounds
|
|
||||||
for hit_idx in hits:
|
|
||||||
obstacle_bounds = static_bounds[hit_idx]
|
|
||||||
poly_hits_obstacle_aabb = False
|
|
||||||
for poly_bounds in move_poly_bounds:
|
|
||||||
if (
|
|
||||||
poly_bounds[0] < obstacle_bounds[2]
|
|
||||||
and poly_bounds[2] > obstacle_bounds[0]
|
|
||||||
and poly_bounds[1] < obstacle_bounds[3]
|
|
||||||
and poly_bounds[3] > obstacle_bounds[1]
|
|
||||||
):
|
|
||||||
poly_hits_obstacle_aabb = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not poly_hits_obstacle_aabb:
|
|
||||||
continue
|
|
||||||
|
|
||||||
obj_id = static_obstacles.obj_ids[hit_idx]
|
|
||||||
if self.is_in_safety_zone_fast(hit_idx, start_port, end_port):
|
|
||||||
collision_found = False
|
|
||||||
for polygon in result.geometry:
|
|
||||||
if not self.is_in_safety_zone(polygon, obj_id, start_port, end_port):
|
|
||||||
collision_found = True
|
|
||||||
break
|
|
||||||
if collision_found:
|
|
||||||
return True
|
|
||||||
continue
|
|
||||||
|
|
||||||
test_geometries = result.dilated_geometry if result.dilated_geometry else result.geometry
|
|
||||||
static_obstacle = static_obstacles.dilated[obj_id]
|
|
||||||
for polygon in test_geometries:
|
|
||||||
if polygon.intersects(static_obstacle) and not polygon.touches(static_obstacle):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_in_safety_zone(
|
|
||||||
self,
|
|
||||||
geometry: Polygon,
|
|
||||||
obj_id: int,
|
|
||||||
start_port: Port | None,
|
|
||||||
end_port: Port | None,
|
|
||||||
) -> bool:
|
|
||||||
engine = self.engine
|
|
||||||
raw_obstacle = engine._static_obstacles.geometries[obj_id]
|
|
||||||
sz = engine.safety_zone_radius
|
|
||||||
|
|
||||||
obstacle_bounds = raw_obstacle.bounds
|
|
||||||
near_start = start_port and (
|
|
||||||
obstacle_bounds[0] - sz <= start_port.x <= obstacle_bounds[2] + sz
|
|
||||||
and obstacle_bounds[1] - sz <= start_port.y <= obstacle_bounds[3] + sz
|
|
||||||
)
|
|
||||||
near_end = end_port and (
|
|
||||||
obstacle_bounds[0] - sz <= end_port.x <= obstacle_bounds[2] + sz
|
|
||||||
and obstacle_bounds[1] - sz <= end_port.y <= obstacle_bounds[3] + sz
|
|
||||||
)
|
|
||||||
|
|
||||||
if not near_start and not near_end:
|
|
||||||
return False
|
|
||||||
if not geometry.intersects(raw_obstacle):
|
|
||||||
return False
|
|
||||||
|
|
||||||
engine.metrics["safety_zone_checks"] += 1
|
|
||||||
intersection = geometry.intersection(raw_obstacle)
|
|
||||||
if intersection.is_empty:
|
|
||||||
return False
|
|
||||||
|
|
||||||
ix_bounds = intersection.bounds
|
|
||||||
if (
|
|
||||||
start_port
|
|
||||||
and near_start
|
|
||||||
and abs(ix_bounds[0] - start_port.x) < sz
|
|
||||||
and abs(ix_bounds[1] - start_port.y) < sz
|
|
||||||
and abs(ix_bounds[2] - start_port.x) < sz
|
|
||||||
and abs(ix_bounds[3] - start_port.y) < sz
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return bool(
|
|
||||||
end_port
|
|
||||||
and near_end
|
|
||||||
and abs(ix_bounds[0] - end_port.x) < sz
|
|
||||||
and abs(ix_bounds[1] - end_port.y) < sz
|
|
||||||
and abs(ix_bounds[2] - end_port.x) < sz
|
|
||||||
and abs(ix_bounds[3] - end_port.y) < sz
|
|
||||||
)
|
|
||||||
|
|
@ -4,14 +4,14 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import rtree
|
import rtree
|
||||||
from shapely.prepared import prep
|
|
||||||
from shapely.strtree import STRtree
|
from shapely.strtree import STRtree
|
||||||
|
|
||||||
|
from inire.geometry.index_helpers import build_index_payload, is_axis_aligned_rect
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
from shapely.prepared import PreparedGeometry
|
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
|
||||||
|
|
||||||
class StaticObstacleIndex:
|
class StaticObstacleIndex:
|
||||||
|
|
@ -20,7 +20,6 @@ class StaticObstacleIndex:
|
||||||
"index",
|
"index",
|
||||||
"geometries",
|
"geometries",
|
||||||
"dilated",
|
"dilated",
|
||||||
"prepared",
|
|
||||||
"is_rect",
|
"is_rect",
|
||||||
"tree",
|
"tree",
|
||||||
"obj_ids",
|
"obj_ids",
|
||||||
|
|
@ -31,18 +30,15 @@ class StaticObstacleIndex:
|
||||||
"net_specific_trees",
|
"net_specific_trees",
|
||||||
"net_specific_is_rect",
|
"net_specific_is_rect",
|
||||||
"net_specific_bounds",
|
"net_specific_bounds",
|
||||||
"safe_cache",
|
|
||||||
"grid",
|
|
||||||
"id_counter",
|
"id_counter",
|
||||||
"version",
|
"version",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, engine: CollisionEngine) -> None:
|
def __init__(self, engine: RoutingWorld) -> None:
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self.index = rtree.index.Index()
|
self.index = rtree.index.Index()
|
||||||
self.geometries: dict[int, Polygon] = {}
|
self.geometries: dict[int, Polygon] = {}
|
||||||
self.dilated: dict[int, Polygon] = {}
|
self.dilated: dict[int, Polygon] = {}
|
||||||
self.prepared: dict[int, PreparedGeometry] = {}
|
|
||||||
self.is_rect: dict[int, bool] = {}
|
self.is_rect: dict[int, bool] = {}
|
||||||
self.tree: STRtree | None = None
|
self.tree: STRtree | None = None
|
||||||
self.obj_ids: list[int] = []
|
self.obj_ids: list[int] = []
|
||||||
|
|
@ -53,8 +49,6 @@ class StaticObstacleIndex:
|
||||||
self.net_specific_trees: dict[tuple[float, float], STRtree] = {}
|
self.net_specific_trees: dict[tuple[float, float], STRtree] = {}
|
||||||
self.net_specific_is_rect: dict[tuple[float, float], numpy.ndarray] = {}
|
self.net_specific_is_rect: dict[tuple[float, float], numpy.ndarray] = {}
|
||||||
self.net_specific_bounds: dict[tuple[float, float], numpy.ndarray] = {}
|
self.net_specific_bounds: dict[tuple[float, float], numpy.ndarray] = {}
|
||||||
self.safe_cache: set[tuple] = set()
|
|
||||||
self.grid: dict[tuple[int, int], list[int]] = {}
|
|
||||||
self.id_counter = 0
|
self.id_counter = 0
|
||||||
self.version = 0
|
self.version = 0
|
||||||
|
|
||||||
|
|
@ -69,12 +63,9 @@ class StaticObstacleIndex:
|
||||||
|
|
||||||
self.geometries[obj_id] = polygon
|
self.geometries[obj_id] = polygon
|
||||||
self.dilated[obj_id] = dilated
|
self.dilated[obj_id] = dilated
|
||||||
self.prepared[obj_id] = prep(dilated)
|
self.is_rect[obj_id] = is_axis_aligned_rect(dilated)
|
||||||
self.index.insert(obj_id, dilated.bounds)
|
self.index.insert(obj_id, dilated.bounds)
|
||||||
self.invalidate_caches()
|
self.invalidate_caches()
|
||||||
bounds = dilated.bounds
|
|
||||||
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
|
||||||
self.is_rect[obj_id] = abs(dilated.area - area) < 1e-4
|
|
||||||
return obj_id
|
return obj_id
|
||||||
|
|
||||||
def remove_obstacle(self, obj_id: int) -> None:
|
def remove_obstacle(self, obj_id: int) -> None:
|
||||||
|
|
@ -85,7 +76,6 @@ class StaticObstacleIndex:
|
||||||
self.index.delete(obj_id, bounds)
|
self.index.delete(obj_id, bounds)
|
||||||
del self.geometries[obj_id]
|
del self.geometries[obj_id]
|
||||||
del self.dilated[obj_id]
|
del self.dilated[obj_id]
|
||||||
del self.prepared[obj_id]
|
|
||||||
del self.is_rect[obj_id]
|
del self.is_rect[obj_id]
|
||||||
self.invalidate_caches()
|
self.invalidate_caches()
|
||||||
|
|
||||||
|
|
@ -96,19 +86,15 @@ class StaticObstacleIndex:
|
||||||
self.obj_ids = []
|
self.obj_ids = []
|
||||||
self.raw_tree = None
|
self.raw_tree = None
|
||||||
self.raw_obj_ids = []
|
self.raw_obj_ids = []
|
||||||
self.grid = {}
|
|
||||||
self.net_specific_trees.clear()
|
self.net_specific_trees.clear()
|
||||||
self.net_specific_is_rect.clear()
|
self.net_specific_is_rect.clear()
|
||||||
self.net_specific_bounds.clear()
|
self.net_specific_bounds.clear()
|
||||||
self.safe_cache.clear()
|
|
||||||
self.version += 1
|
self.version += 1
|
||||||
|
|
||||||
def ensure_tree(self) -> None:
|
def ensure_tree(self) -> None:
|
||||||
if self.tree is None and self.dilated:
|
if self.tree is None and self.dilated:
|
||||||
self.obj_ids = sorted(self.dilated.keys())
|
self.obj_ids, geometries, self.bounds_array = build_index_payload(self.dilated)
|
||||||
geometries = [self.dilated[i] for i in self.obj_ids]
|
|
||||||
self.tree = STRtree(geometries)
|
self.tree = STRtree(geometries)
|
||||||
self.bounds_array = numpy.array([geometry.bounds for geometry in geometries])
|
|
||||||
self.is_rect_array = numpy.array([self.is_rect[i] for i in self.obj_ids])
|
self.is_rect_array = numpy.array([self.is_rect[i] for i in self.obj_ids])
|
||||||
|
|
||||||
def ensure_net_tree(self, net_width: float) -> STRtree:
|
def ensure_net_tree(self, net_width: float) -> STRtree:
|
||||||
|
|
@ -125,19 +111,16 @@ class StaticObstacleIndex:
|
||||||
polygon = self.geometries[obj_id]
|
polygon = self.geometries[obj_id]
|
||||||
dilated = polygon.buffer(total_dilation, join_style=2)
|
dilated = polygon.buffer(total_dilation, join_style=2)
|
||||||
geometries.append(dilated)
|
geometries.append(dilated)
|
||||||
bounds = dilated.bounds
|
bounds_list.append(dilated.bounds)
|
||||||
bounds_list.append(bounds)
|
is_rect_list.append(is_axis_aligned_rect(dilated))
|
||||||
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
|
||||||
is_rect_list.append(abs(dilated.area - area) < 1e-4)
|
|
||||||
|
|
||||||
tree = STRtree(geometries)
|
tree = STRtree(geometries)
|
||||||
self.net_specific_trees[key] = tree
|
self.net_specific_trees[key] = tree
|
||||||
self.net_specific_is_rect[key] = numpy.array(is_rect_list, dtype=bool)
|
self.net_specific_is_rect[key] = numpy.array(is_rect_list, dtype=bool)
|
||||||
self.net_specific_bounds[key] = numpy.array(bounds_list)
|
self.net_specific_bounds[key] = numpy.array(bounds_list, dtype=numpy.float64)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
def ensure_raw_tree(self) -> None:
|
def ensure_raw_tree(self) -> None:
|
||||||
if self.raw_tree is None and self.geometries:
|
if self.raw_tree is None and self.geometries:
|
||||||
self.raw_obj_ids = sorted(self.geometries.keys())
|
self.raw_obj_ids, geometries, _bounds_array = build_index_payload(self.geometries)
|
||||||
geometries = [self.geometries[i] for i in self.raw_obj_ids]
|
|
||||||
self.raw_tree = STRtree(geometries)
|
self.raw_tree = STRtree(geometries)
|
||||||
|
|
|
||||||
145
inire/model.py
Normal file
145
inire/model.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.geometry.components import BendCollisionModel
|
||||||
|
from inire.router.results import RouteMetrics, RoutingResult
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class NetSpec:
|
||||||
|
net_id: str
|
||||||
|
start: Port
|
||||||
|
target: Port
|
||||||
|
width: float = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LockedRoute:
|
||||||
|
geometry: tuple[Polygon, ...]
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "geometry", tuple(self.geometry))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_path(cls, path: tuple[ComponentResult, ...] | list[ComponentResult]) -> LockedRoute:
|
||||||
|
polygons = []
|
||||||
|
for component in path:
|
||||||
|
polygons.extend(component.physical_geometry)
|
||||||
|
return cls(geometry=tuple(polygons))
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_locked_route(route: LockedRoute | tuple | list) -> LockedRoute:
|
||||||
|
if isinstance(route, LockedRoute):
|
||||||
|
return route
|
||||||
|
route_items = tuple(route)
|
||||||
|
if route_items and hasattr(route_items[0], "physical_geometry"):
|
||||||
|
return LockedRoute.from_path(route_items) # type: ignore[arg-type]
|
||||||
|
return LockedRoute(geometry=route_items)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ObjectiveWeights:
|
||||||
|
unit_length_cost: float = 1.0
|
||||||
|
bend_penalty: float = 250.0
|
||||||
|
sbend_penalty: float = 500.0
|
||||||
|
danger_weight: float = 1.0
|
||||||
|
congestion_penalty: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SearchOptions:
|
||||||
|
node_limit: int = 1000000
|
||||||
|
max_straight_length: float = 2000.0
|
||||||
|
min_straight_length: float = 5.0
|
||||||
|
greedy_h_weight: float = 1.5
|
||||||
|
sbend_offsets: tuple[float, ...] | None = None
|
||||||
|
bend_radii: tuple[float, ...] = (50.0, 100.0)
|
||||||
|
sbend_radii: tuple[float, ...] = (10.0,)
|
||||||
|
bend_collision_type: BendCollisionModel = "arc"
|
||||||
|
visibility_guidance: str = "tangent_corner"
|
||||||
|
initial_paths: dict[str, tuple[ComponentResult, ...]] | None = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "bend_radii", tuple(self.bend_radii))
|
||||||
|
object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii))
|
||||||
|
if self.sbend_offsets is not None:
|
||||||
|
object.__setattr__(self, "sbend_offsets", tuple(self.sbend_offsets))
|
||||||
|
if self.initial_paths is not None:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"initial_paths",
|
||||||
|
{
|
||||||
|
net_id: tuple(path)
|
||||||
|
for net_id, path in self.initial_paths.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CongestionOptions:
|
||||||
|
max_iterations: int = 10
|
||||||
|
base_penalty: float = 100.0
|
||||||
|
multiplier: float = 1.5
|
||||||
|
use_tiered_strategy: bool = True
|
||||||
|
warm_start: str | None = "shortest"
|
||||||
|
shuffle_nets: bool = False
|
||||||
|
sort_nets: str | None = None
|
||||||
|
seed: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RefinementOptions:
|
||||||
|
enabled: bool = True
|
||||||
|
objective: ObjectiveWeights | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DiagnosticsOptions:
|
||||||
|
capture_expanded: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RoutingOptions:
|
||||||
|
search: SearchOptions = field(default_factory=SearchOptions)
|
||||||
|
objective: ObjectiveWeights = field(default_factory=ObjectiveWeights)
|
||||||
|
congestion: CongestionOptions = field(default_factory=CongestionOptions)
|
||||||
|
refinement: RefinementOptions = field(default_factory=RefinementOptions)
|
||||||
|
diagnostics: DiagnosticsOptions = field(default_factory=DiagnosticsOptions)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RoutingProblem:
|
||||||
|
bounds: tuple[float, float, float, float]
|
||||||
|
nets: tuple[NetSpec, ...] = ()
|
||||||
|
static_obstacles: tuple[Polygon, ...] = ()
|
||||||
|
locked_routes: dict[str, LockedRoute] = field(default_factory=dict)
|
||||||
|
clearance: float = 2.0
|
||||||
|
max_net_width: float = 2.0
|
||||||
|
safety_zone_radius: float = 0.0021
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "nets", tuple(self.nets))
|
||||||
|
object.__setattr__(self, "static_obstacles", tuple(self.static_obstacles))
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"locked_routes",
|
||||||
|
{
|
||||||
|
net_id: _coerce_locked_route(route)
|
||||||
|
for net_id, route in self.locked_routes.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RoutingRunResult:
|
||||||
|
results_by_net: dict[str, RoutingResult]
|
||||||
|
metrics: RouteMetrics
|
||||||
|
expanded_nodes: tuple[tuple[int, int, int], ...] = ()
|
||||||
210
inire/router/_astar_admission.py
Normal file
210
inire/router/_astar_admission.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
from inire.geometry.components import Bend90, SBend, Straight, BendCollisionModel, MoveKind
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.refiner import component_hits_ancestor_chain
|
||||||
|
|
||||||
|
from ._astar_types import AStarContext, AStarMetrics, AStarNode
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
|
||||||
|
|
||||||
|
def process_move(
|
||||||
|
parent: AStarNode,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
move_class: MoveKind,
|
||||||
|
params: tuple,
|
||||||
|
skip_congestion: bool,
|
||||||
|
bend_collision_type: BendCollisionModel,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
cp = parent.port
|
||||||
|
coll_type = bend_collision_type
|
||||||
|
coll_key = id(coll_type) if isinstance(coll_type, Polygon) else coll_type
|
||||||
|
self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
|
||||||
|
abs_key = (
|
||||||
|
cp.as_tuple(),
|
||||||
|
move_class,
|
||||||
|
params,
|
||||||
|
net_width,
|
||||||
|
coll_key,
|
||||||
|
self_dilation,
|
||||||
|
)
|
||||||
|
if abs_key in context.move_cache_abs:
|
||||||
|
res = context.move_cache_abs[abs_key]
|
||||||
|
else:
|
||||||
|
context.check_cache_eviction()
|
||||||
|
base_port = Port(0, 0, cp.r)
|
||||||
|
rel_key = (
|
||||||
|
cp.r,
|
||||||
|
move_class,
|
||||||
|
params,
|
||||||
|
net_width,
|
||||||
|
coll_key,
|
||||||
|
self_dilation,
|
||||||
|
)
|
||||||
|
if rel_key in context.move_cache_rel:
|
||||||
|
res_rel = context.move_cache_rel[rel_key]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if move_class == "straight":
|
||||||
|
res_rel = Straight.generate(base_port, params[0], net_width, dilation=self_dilation)
|
||||||
|
elif move_class == "bend90":
|
||||||
|
res_rel = Bend90.generate(
|
||||||
|
base_port,
|
||||||
|
params[0],
|
||||||
|
net_width,
|
||||||
|
params[1],
|
||||||
|
collision_type=coll_type,
|
||||||
|
dilation=self_dilation,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res_rel = SBend.generate(
|
||||||
|
base_port,
|
||||||
|
params[0],
|
||||||
|
params[1],
|
||||||
|
net_width,
|
||||||
|
collision_type=coll_type,
|
||||||
|
dilation=self_dilation,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
context.move_cache_rel[rel_key] = res_rel
|
||||||
|
res = res_rel.translate(cp.x, cp.y)
|
||||||
|
context.move_cache_abs[abs_key] = res
|
||||||
|
|
||||||
|
move_radius = params[0] if move_class == "bend90" else (params[1] if move_class == "sbend" else None)
|
||||||
|
add_node(
|
||||||
|
parent,
|
||||||
|
res,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
move_class,
|
||||||
|
abs_key,
|
||||||
|
move_radius=move_radius,
|
||||||
|
skip_congestion=skip_congestion,
|
||||||
|
max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_node(
|
||||||
|
parent: AStarNode,
|
||||||
|
result: ComponentResult,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
move_type: MoveKind,
|
||||||
|
cache_key: tuple,
|
||||||
|
move_radius: float | None = None,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
metrics.moves_generated += 1
|
||||||
|
metrics.total_moves_generated += 1
|
||||||
|
state = result.end_port.as_tuple()
|
||||||
|
new_lower_bound_g = parent.g_cost + result.length
|
||||||
|
if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR:
|
||||||
|
metrics.pruned_closed_set += 1
|
||||||
|
metrics.total_pruned_closed_set += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
parent_p = parent.port
|
||||||
|
end_p = result.end_port
|
||||||
|
|
||||||
|
if cache_key in context.hard_collision_set:
|
||||||
|
metrics.pruned_hard_collision += 1
|
||||||
|
metrics.total_pruned_hard_collision += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
is_static_safe = cache_key in context.static_safe_cache
|
||||||
|
if not is_static_safe:
|
||||||
|
ce = context.cost_evaluator.collision_engine
|
||||||
|
if move_type == "straight":
|
||||||
|
collision_found = ce.check_move_straight_static(parent_p, result.length, net_width=net_width)
|
||||||
|
else:
|
||||||
|
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p, net_width=net_width)
|
||||||
|
if collision_found:
|
||||||
|
context.hard_collision_set.add(cache_key)
|
||||||
|
metrics.pruned_hard_collision += 1
|
||||||
|
metrics.total_pruned_hard_collision += 1
|
||||||
|
return
|
||||||
|
context.static_safe_cache.add(cache_key)
|
||||||
|
|
||||||
|
total_overlaps = 0
|
||||||
|
if not skip_congestion:
|
||||||
|
if cache_key in congestion_cache:
|
||||||
|
total_overlaps = congestion_cache[cache_key]
|
||||||
|
else:
|
||||||
|
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
||||||
|
congestion_cache[cache_key] = total_overlaps
|
||||||
|
|
||||||
|
if self_collision_check and component_hits_ancestor_chain(result, parent):
|
||||||
|
return
|
||||||
|
|
||||||
|
penalty = context.cost_evaluator.component_penalty(
|
||||||
|
move_type,
|
||||||
|
move_radius=move_radius,
|
||||||
|
)
|
||||||
|
|
||||||
|
move_cost = context.cost_evaluator.evaluate_move(
|
||||||
|
result.collision_geometry,
|
||||||
|
result.end_port,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
start_port=parent_p,
|
||||||
|
length=result.length,
|
||||||
|
dilated_geometry=result.dilated_collision_geometry,
|
||||||
|
penalty=penalty,
|
||||||
|
skip_static=True,
|
||||||
|
skip_congestion=True,
|
||||||
|
)
|
||||||
|
move_cost += total_overlaps * context.cost_evaluator.congestion_penalty
|
||||||
|
|
||||||
|
if max_cost is not None and parent.g_cost + move_cost > max_cost:
|
||||||
|
metrics.pruned_cost += 1
|
||||||
|
metrics.total_pruned_cost += 1
|
||||||
|
return
|
||||||
|
if move_cost > 1e12:
|
||||||
|
metrics.pruned_cost += 1
|
||||||
|
metrics.total_pruned_cost += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
g_cost = parent.g_cost + move_cost
|
||||||
|
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
||||||
|
metrics.pruned_closed_set += 1
|
||||||
|
metrics.total_pruned_closed_set += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
h_cost = context.cost_evaluator.h_manhattan(result.end_port, target)
|
||||||
|
heapq.heappush(open_set, AStarNode(result.end_port, g_cost, h_cost, parent, result))
|
||||||
|
metrics.moves_added += 1
|
||||||
|
metrics.total_moves_added += 1
|
||||||
302
inire/router/_astar_moves.py
Normal file
302
inire/router/_astar_moves.py
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
from inire.geometry.components import BendCollisionModel, MoveKind
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
from ._astar_admission import process_move
|
||||||
|
from ._astar_types import AStarContext, AStarMetrics, AStarNode
|
||||||
|
|
||||||
|
|
||||||
|
def _quantized_lengths(values: list[float], max_reach: float) -> list[int]:
|
||||||
|
out = {int(round(v)) for v in values if v > 0 and v <= max_reach + 0.01}
|
||||||
|
return sorted((v for v in out if v > 0), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _sbend_forward_span(offset: float, radius: float) -> float | None:
|
||||||
|
abs_offset = abs(offset)
|
||||||
|
if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius:
|
||||||
|
return None
|
||||||
|
theta = math.acos(1.0 - abs_offset / (2.0 * radius))
|
||||||
|
return 2.0 * radius * math.sin(theta)
|
||||||
|
|
||||||
|
|
||||||
|
def _visible_straight_candidates(
|
||||||
|
current: Port,
|
||||||
|
context: AStarContext,
|
||||||
|
max_reach: float,
|
||||||
|
cos_v: float,
|
||||||
|
sin_v: float,
|
||||||
|
net_width: float,
|
||||||
|
) -> list[float]:
|
||||||
|
search_options = context.options.search
|
||||||
|
mode = search_options.visibility_guidance
|
||||||
|
if mode == "off":
|
||||||
|
return []
|
||||||
|
|
||||||
|
if mode == "exact_corner":
|
||||||
|
max_bend_radius = max(search_options.bend_radii, default=0.0)
|
||||||
|
visibility_reach = max_reach + max_bend_radius
|
||||||
|
visible_corners = sorted(
|
||||||
|
context.visibility_manager.get_corner_visibility(current, max_dist=visibility_reach),
|
||||||
|
key=lambda corner: corner[2],
|
||||||
|
)
|
||||||
|
if not visible_corners:
|
||||||
|
return []
|
||||||
|
|
||||||
|
candidates: set[int] = set()
|
||||||
|
for cx, cy, _ in visible_corners[:12]:
|
||||||
|
dx = cx - current.x
|
||||||
|
dy = cy - current.y
|
||||||
|
local_x = dx * cos_v + dy * sin_v
|
||||||
|
if local_x <= search_options.min_straight_length:
|
||||||
|
continue
|
||||||
|
candidates.add(int(round(local_x)))
|
||||||
|
return sorted(candidates, reverse=True)
|
||||||
|
|
||||||
|
if mode != "tangent_corner":
|
||||||
|
return []
|
||||||
|
|
||||||
|
visibility_manager = context.visibility_manager
|
||||||
|
visibility_manager._ensure_current()
|
||||||
|
max_bend_radius = max(search_options.bend_radii, default=0.0)
|
||||||
|
if max_bend_radius <= 0 or not visibility_manager.corners:
|
||||||
|
return []
|
||||||
|
|
||||||
|
reach = max_reach + max_bend_radius
|
||||||
|
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach)
|
||||||
|
candidate_ids = list(visibility_manager.corner_index.intersection(bounds))
|
||||||
|
if not candidate_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
scored: list[tuple[float, float, float, float, float]] = []
|
||||||
|
for idx in candidate_ids:
|
||||||
|
cx, cy = visibility_manager.corners[idx]
|
||||||
|
dx = cx - current.x
|
||||||
|
dy = cy - current.y
|
||||||
|
local_x = dx * cos_v + dy * sin_v
|
||||||
|
local_y = -dx * sin_v + dy * cos_v
|
||||||
|
if local_x <= search_options.min_straight_length or local_x > reach + 0.01:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nearest_radius = min(search_options.bend_radii, key=lambda radius: abs(abs(local_y) - radius))
|
||||||
|
tangent_error = abs(abs(local_y) - nearest_radius)
|
||||||
|
if tangent_error > 2.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
length = local_x - nearest_radius
|
||||||
|
if length <= search_options.min_straight_length or length > max_reach + 0.01:
|
||||||
|
continue
|
||||||
|
|
||||||
|
scored.append((tangent_error, math.hypot(dx, dy), length, dx, dy))
|
||||||
|
|
||||||
|
if not scored:
|
||||||
|
return []
|
||||||
|
|
||||||
|
collision_engine = context.cost_evaluator.collision_engine
|
||||||
|
candidates: set[int] = set()
|
||||||
|
for _, dist, length, dx, dy in sorted(scored)[:4]:
|
||||||
|
angle = math.degrees(math.atan2(dy, dx))
|
||||||
|
corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width)
|
||||||
|
if corner_reach < dist - 0.01:
|
||||||
|
continue
|
||||||
|
qlen = int(round(length))
|
||||||
|
if qlen > 0:
|
||||||
|
candidates.add(qlen)
|
||||||
|
|
||||||
|
return sorted(candidates, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _previous_move_metadata(node: AStarNode) -> tuple[MoveKind | None, float | None]:
|
||||||
|
result = node.component_result
|
||||||
|
if result is None:
|
||||||
|
return None, None
|
||||||
|
move_type = result.move_type
|
||||||
|
if move_type == "straight":
|
||||||
|
return move_type, result.length
|
||||||
|
return move_type, None
|
||||||
|
|
||||||
|
|
||||||
|
def expand_moves(
|
||||||
|
current: AStarNode,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
bend_collision_type: BendCollisionModel | None = None,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
search_options = context.options.search
|
||||||
|
effective_bend_collision_type = bend_collision_type if bend_collision_type is not None else search_options.bend_collision_type
|
||||||
|
cp = current.port
|
||||||
|
prev_move_type, prev_straight_length = _previous_move_metadata(current)
|
||||||
|
dx_t = target.x - cp.x
|
||||||
|
dy_t = target.y - cp.y
|
||||||
|
dist_sq = dx_t * dx_t + dy_t * dy_t
|
||||||
|
|
||||||
|
if cp.r == 0:
|
||||||
|
cos_v, sin_v = 1.0, 0.0
|
||||||
|
elif cp.r == 90:
|
||||||
|
cos_v, sin_v = 0.0, 1.0
|
||||||
|
elif cp.r == 180:
|
||||||
|
cos_v, sin_v = -1.0, 0.0
|
||||||
|
else:
|
||||||
|
cos_v, sin_v = 0.0, -1.0
|
||||||
|
|
||||||
|
proj_t = dx_t * cos_v + dy_t * sin_v
|
||||||
|
perp_t = -dx_t * sin_v + dy_t * cos_v
|
||||||
|
dx_local = proj_t
|
||||||
|
dy_local = perp_t
|
||||||
|
|
||||||
|
if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r:
|
||||||
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, proj_t + 1.0, net_width=net_width)
|
||||||
|
if max_reach >= proj_t - 0.01 and (
|
||||||
|
prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR
|
||||||
|
):
|
||||||
|
process_move(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
"straight",
|
||||||
|
(int(round(proj_t)),),
|
||||||
|
skip_congestion,
|
||||||
|
bend_collision_type=effective_bend_collision_type,
|
||||||
|
max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, search_options.max_straight_length, net_width=net_width)
|
||||||
|
candidate_lengths = [
|
||||||
|
search_options.min_straight_length,
|
||||||
|
max_reach,
|
||||||
|
max_reach / 2.0,
|
||||||
|
max_reach - 5.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
axis_target_dist = abs(dx_t) if cp.r in (0, 180) else abs(dy_t)
|
||||||
|
candidate_lengths.append(axis_target_dist)
|
||||||
|
for radius in search_options.bend_radii:
|
||||||
|
candidate_lengths.extend((max_reach - radius, axis_target_dist - radius, axis_target_dist - 2.0 * radius))
|
||||||
|
|
||||||
|
candidate_lengths.extend(
|
||||||
|
_visible_straight_candidates(
|
||||||
|
cp,
|
||||||
|
context,
|
||||||
|
max_reach,
|
||||||
|
cos_v,
|
||||||
|
sin_v,
|
||||||
|
net_width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if cp.r == target.r and dx_local > 0 and abs(dy_local) > TOLERANCE_LINEAR:
|
||||||
|
for radius in search_options.sbend_radii:
|
||||||
|
sbend_span = _sbend_forward_span(dy_local, radius)
|
||||||
|
if sbend_span is None:
|
||||||
|
continue
|
||||||
|
candidate_lengths.extend((dx_local - sbend_span, dx_local - 2.0 * sbend_span))
|
||||||
|
|
||||||
|
for length in _quantized_lengths(candidate_lengths, max_reach):
|
||||||
|
if length < search_options.min_straight_length:
|
||||||
|
continue
|
||||||
|
if prev_straight_length is not None and length >= prev_straight_length - TOLERANCE_LINEAR:
|
||||||
|
continue
|
||||||
|
process_move(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
"straight",
|
||||||
|
(length,),
|
||||||
|
skip_congestion,
|
||||||
|
bend_collision_type=effective_bend_collision_type,
|
||||||
|
max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
angle_to_target = 0.0
|
||||||
|
if dx_t != 0 or dy_t != 0:
|
||||||
|
angle_to_target = float((round((180.0 / math.pi) * math.atan2(dy_t, dx_t)) + 360.0) % 360.0)
|
||||||
|
allow_backwards = dist_sq < 150 * 150
|
||||||
|
|
||||||
|
for radius in search_options.bend_radii:
|
||||||
|
for direction in ("CW", "CCW"):
|
||||||
|
if not allow_backwards:
|
||||||
|
turn = 90 if direction == "CCW" else -90
|
||||||
|
new_ori = (cp.r + turn) % 360
|
||||||
|
new_diff = (angle_to_target - new_ori + 180.0) % 360.0 - 180.0
|
||||||
|
if abs(new_diff) > 135.0:
|
||||||
|
continue
|
||||||
|
process_move(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
"bend90",
|
||||||
|
(radius, direction),
|
||||||
|
skip_congestion,
|
||||||
|
bend_collision_type=effective_bend_collision_type,
|
||||||
|
max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
max_sbend_r = max(search_options.sbend_radii) if search_options.sbend_radii else 0.0
|
||||||
|
if max_sbend_r <= 0 or prev_move_type == "sbend":
|
||||||
|
return
|
||||||
|
|
||||||
|
explicit_offsets = search_options.sbend_offsets
|
||||||
|
offsets: set[int] = {int(round(v)) for v in explicit_offsets or []}
|
||||||
|
|
||||||
|
if target.r == cp.r and 0 < dx_local <= 4 * max_sbend_r and 0 < abs(dy_local) < 2 * max_sbend_r:
|
||||||
|
offsets.add(int(round(dy_local)))
|
||||||
|
|
||||||
|
if not offsets:
|
||||||
|
return
|
||||||
|
|
||||||
|
for offset in sorted(offsets):
|
||||||
|
if offset == 0:
|
||||||
|
continue
|
||||||
|
for radius in search_options.sbend_radii:
|
||||||
|
if abs(offset) >= 2 * radius:
|
||||||
|
continue
|
||||||
|
process_move(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
"sbend",
|
||||||
|
(offset, radius),
|
||||||
|
skip_congestion,
|
||||||
|
bend_collision_type=effective_bend_collision_type,
|
||||||
|
max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
152
inire/router/_astar_types.py
Normal file
152
inire/router/_astar_types.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.model import RoutingOptions, RoutingProblem
|
||||||
|
from inire.router.visibility import VisibilityManager
|
||||||
|
from inire.router.results import RouteMetrics
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
|
||||||
|
|
||||||
|
class AStarNode:
|
||||||
|
__slots__ = ("port", "g_cost", "h_cost", "fh_cost", "parent", "component_result")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
port,
|
||||||
|
g_cost: float,
|
||||||
|
h_cost: float,
|
||||||
|
parent: AStarNode | None = None,
|
||||||
|
component_result: ComponentResult | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.port = port
|
||||||
|
self.g_cost = g_cost
|
||||||
|
self.h_cost = h_cost
|
||||||
|
self.fh_cost = (g_cost + h_cost, h_cost)
|
||||||
|
self.parent = parent
|
||||||
|
self.component_result = component_result
|
||||||
|
|
||||||
|
def __lt__(self, other: AStarNode) -> bool:
|
||||||
|
return self.fh_cost < other.fh_cost
|
||||||
|
|
||||||
|
|
||||||
|
class AStarMetrics:
|
||||||
|
__slots__ = (
|
||||||
|
"total_nodes_expanded",
|
||||||
|
"total_moves_generated",
|
||||||
|
"total_moves_added",
|
||||||
|
"total_pruned_closed_set",
|
||||||
|
"total_pruned_hard_collision",
|
||||||
|
"total_pruned_cost",
|
||||||
|
"last_expanded_nodes",
|
||||||
|
"nodes_expanded",
|
||||||
|
"moves_generated",
|
||||||
|
"moves_added",
|
||||||
|
"pruned_closed_set",
|
||||||
|
"pruned_hard_collision",
|
||||||
|
"pruned_cost",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.total_nodes_expanded = 0
|
||||||
|
self.total_moves_generated = 0
|
||||||
|
self.total_moves_added = 0
|
||||||
|
self.total_pruned_closed_set = 0
|
||||||
|
self.total_pruned_hard_collision = 0
|
||||||
|
self.total_pruned_cost = 0
|
||||||
|
self.last_expanded_nodes: list[tuple[int, int, int]] = []
|
||||||
|
self.nodes_expanded = 0
|
||||||
|
self.moves_generated = 0
|
||||||
|
self.moves_added = 0
|
||||||
|
self.pruned_closed_set = 0
|
||||||
|
self.pruned_hard_collision = 0
|
||||||
|
self.pruned_cost = 0
|
||||||
|
|
||||||
|
def reset_totals(self) -> None:
|
||||||
|
self.total_nodes_expanded = 0
|
||||||
|
self.total_moves_generated = 0
|
||||||
|
self.total_moves_added = 0
|
||||||
|
self.total_pruned_closed_set = 0
|
||||||
|
self.total_pruned_hard_collision = 0
|
||||||
|
self.total_pruned_cost = 0
|
||||||
|
|
||||||
|
def reset_per_route(self) -> None:
|
||||||
|
self.nodes_expanded = 0
|
||||||
|
self.moves_generated = 0
|
||||||
|
self.moves_added = 0
|
||||||
|
self.pruned_closed_set = 0
|
||||||
|
self.pruned_hard_collision = 0
|
||||||
|
self.pruned_cost = 0
|
||||||
|
self.last_expanded_nodes = []
|
||||||
|
|
||||||
|
def snapshot(self) -> RouteMetrics:
|
||||||
|
return RouteMetrics(
|
||||||
|
nodes_expanded=self.total_nodes_expanded,
|
||||||
|
moves_generated=self.total_moves_generated,
|
||||||
|
moves_added=self.total_moves_added,
|
||||||
|
pruned_closed_set=self.total_pruned_closed_set,
|
||||||
|
pruned_hard_collision=self.total_pruned_hard_collision,
|
||||||
|
pruned_cost=self.total_pruned_cost,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AStarContext:
|
||||||
|
__slots__ = (
|
||||||
|
"cost_evaluator",
|
||||||
|
"problem",
|
||||||
|
"options",
|
||||||
|
"max_cache_size",
|
||||||
|
"visibility_manager",
|
||||||
|
"move_cache_rel",
|
||||||
|
"move_cache_abs",
|
||||||
|
"hard_collision_set",
|
||||||
|
"static_safe_cache",
|
||||||
|
"static_cache_version",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cost_evaluator: CostEvaluator,
|
||||||
|
problem: RoutingProblem,
|
||||||
|
options: RoutingOptions,
|
||||||
|
max_cache_size: int = 1000000,
|
||||||
|
) -> None:
|
||||||
|
self.cost_evaluator = cost_evaluator
|
||||||
|
self.max_cache_size = max_cache_size
|
||||||
|
self.problem = problem
|
||||||
|
self.options = options
|
||||||
|
self.cost_evaluator.set_min_bend_radius(min(self.options.search.bend_radii, default=50.0))
|
||||||
|
self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine)
|
||||||
|
self.move_cache_rel: dict[tuple, ComponentResult] = {}
|
||||||
|
self.move_cache_abs: dict[tuple, ComponentResult] = {}
|
||||||
|
self.hard_collision_set: set[tuple] = set()
|
||||||
|
self.static_safe_cache: set[tuple] = set()
|
||||||
|
self.static_cache_version = self.cost_evaluator.collision_engine.get_static_version()
|
||||||
|
|
||||||
|
def clear_static_caches(self) -> None:
|
||||||
|
self.hard_collision_set.clear()
|
||||||
|
self.static_safe_cache.clear()
|
||||||
|
self.visibility_manager.clear_cache()
|
||||||
|
self.static_cache_version = self.cost_evaluator.collision_engine.get_static_version()
|
||||||
|
|
||||||
|
def ensure_static_caches_current(self) -> None:
|
||||||
|
current_version = self.cost_evaluator.collision_engine.get_static_version()
|
||||||
|
if self.static_cache_version != current_version:
|
||||||
|
self.clear_static_caches()
|
||||||
|
|
||||||
|
def _evict_cache(self, cache: dict[tuple, ComponentResult]) -> None:
|
||||||
|
if len(cache) <= self.max_cache_size * 1.2:
|
||||||
|
return
|
||||||
|
|
||||||
|
num_to_evict = max(1, int(len(cache) * 0.25))
|
||||||
|
for idx, key in enumerate(tuple(cache.keys())):
|
||||||
|
if idx >= num_to_evict:
|
||||||
|
break
|
||||||
|
del cache[key]
|
||||||
|
|
||||||
|
def check_cache_eviction(self) -> None:
|
||||||
|
self._evict_cache(self.move_cache_rel)
|
||||||
|
self._evict_cache(self.move_cache_abs)
|
||||||
362
inire/router/_router.py
Normal file
362
inire/router/_router.py
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.model import NetSpec, RoutingOptions, RoutingProblem
|
||||||
|
from inire.router._astar_types import AStarContext, AStarMetrics
|
||||||
|
from inire.router._search import route_astar
|
||||||
|
from inire.router.outcomes import RoutingOutcome, routing_outcome_needs_retry
|
||||||
|
from inire.router.refiner import PathRefiner
|
||||||
|
from inire.router.results import RoutingReport, RoutingResult
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class _RoutingState:
|
||||||
|
net_specs: dict[str, NetSpec]
|
||||||
|
ordered_net_ids: list[str]
|
||||||
|
results: dict[str, RoutingResult]
|
||||||
|
needs_self_collision_check: set[str]
|
||||||
|
start_time: float
|
||||||
|
timeout_s: float
|
||||||
|
initial_paths: dict[str, tuple[ComponentResult, ...]] | None
|
||||||
|
accumulated_expanded_nodes: list[tuple[int, int, int]]
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["PathFinder"]
|
||||||
|
|
||||||
|
|
||||||
|
class PathFinder:
|
||||||
|
__slots__ = (
|
||||||
|
"context",
|
||||||
|
"metrics",
|
||||||
|
"refiner",
|
||||||
|
"accumulated_expanded_nodes",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.context = context
|
||||||
|
self.metrics = metrics if metrics is not None else AStarMetrics()
|
||||||
|
self.refiner = PathRefiner(self.context)
|
||||||
|
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def problem(self) -> RoutingProblem:
|
||||||
|
return self.context.problem
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> RoutingOptions:
|
||||||
|
return self.context.options
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost_evaluator(self) -> CostEvaluator:
|
||||||
|
return self.context.cost_evaluator
|
||||||
|
|
||||||
|
def _path_cost(self, path: Sequence[ComponentResult]) -> float:
|
||||||
|
return self.refiner.path_cost(path)
|
||||||
|
|
||||||
|
def _refine_path(
|
||||||
|
self,
|
||||||
|
net_id: str,
|
||||||
|
start: Port,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
path: Sequence[ComponentResult],
|
||||||
|
) -> list[ComponentResult]:
|
||||||
|
return self.refiner.refine_path(net_id, start, target, net_width, path)
|
||||||
|
|
||||||
|
def _extract_path_geometry(self, path: Sequence[ComponentResult]) -> tuple[list, list]:
|
||||||
|
all_geoms = []
|
||||||
|
all_dilated = []
|
||||||
|
for result in path:
|
||||||
|
all_geoms.extend(result.collision_geometry)
|
||||||
|
all_dilated.extend(result.dilated_collision_geometry)
|
||||||
|
return all_geoms, all_dilated
|
||||||
|
|
||||||
|
def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None:
|
||||||
|
all_geoms, all_dilated = self._extract_path_geometry(path)
|
||||||
|
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||||
|
|
||||||
|
def _stage_path_as_static(self, path: Sequence[ComponentResult]) -> list[int]:
|
||||||
|
obj_ids: list[int] = []
|
||||||
|
for result in path:
|
||||||
|
for polygon in result.physical_geometry:
|
||||||
|
obj_ids.append(self.cost_evaluator.collision_engine.add_static_obstacle(polygon))
|
||||||
|
return obj_ids
|
||||||
|
|
||||||
|
def _remove_static_obstacles(self, obj_ids: list[int]) -> None:
|
||||||
|
for obj_id in obj_ids:
|
||||||
|
self.cost_evaluator.collision_engine.remove_static_obstacle(obj_id)
|
||||||
|
|
||||||
|
def _remove_path(self, net_id: str) -> None:
|
||||||
|
self.cost_evaluator.collision_engine.remove_path(net_id)
|
||||||
|
|
||||||
|
def _verify_path_report(self, net_id: str, path: Sequence[ComponentResult]) -> RoutingReport:
|
||||||
|
return self.cost_evaluator.collision_engine.verify_path_report(net_id, path)
|
||||||
|
|
||||||
|
def _finalize_dynamic_tree(self) -> None:
|
||||||
|
self.cost_evaluator.collision_engine.rebuild_dynamic_tree()
|
||||||
|
|
||||||
|
def _build_routing_result(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
net_id: str,
|
||||||
|
path: Sequence[ComponentResult],
|
||||||
|
reached_target: bool | None = None,
|
||||||
|
report: RoutingReport | None = None,
|
||||||
|
) -> RoutingResult:
|
||||||
|
resolved_reached_target = bool(path) if reached_target is None else reached_target
|
||||||
|
return RoutingResult(
|
||||||
|
net_id=net_id,
|
||||||
|
path=path,
|
||||||
|
reached_target=resolved_reached_target,
|
||||||
|
report=report if report is not None else RoutingReport(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _routing_order(
|
||||||
|
self,
|
||||||
|
net_specs: dict[str, NetSpec],
|
||||||
|
order: str,
|
||||||
|
) -> list[str]:
|
||||||
|
ordered_net_ids = list(net_specs.keys())
|
||||||
|
if order == "user":
|
||||||
|
return ordered_net_ids
|
||||||
|
ordered_net_ids.sort(
|
||||||
|
key=lambda net_id: abs(net_specs[net_id].target.x - net_specs[net_id].start.x)
|
||||||
|
+ abs(net_specs[net_id].target.y - net_specs[net_id].start.y),
|
||||||
|
reverse=(order == "longest"),
|
||||||
|
)
|
||||||
|
return ordered_net_ids
|
||||||
|
|
||||||
|
def _build_greedy_warm_start_paths(
|
||||||
|
self,
|
||||||
|
net_specs: dict[str, NetSpec],
|
||||||
|
order: str,
|
||||||
|
) -> dict[str, tuple[ComponentResult, ...]]:
|
||||||
|
greedy_paths: dict[str, tuple[ComponentResult, ...]] = {}
|
||||||
|
temp_obj_ids: list[int] = []
|
||||||
|
greedy_node_limit = min(self.options.search.node_limit, 2000)
|
||||||
|
for net_id in self._routing_order(net_specs, order):
|
||||||
|
net = net_specs[net_id]
|
||||||
|
h_start = self.cost_evaluator.h_manhattan(net.start, net.target)
|
||||||
|
max_cost_limit = max(h_start * 3.0, 2000.0)
|
||||||
|
path = route_astar(
|
||||||
|
net.start,
|
||||||
|
net.target,
|
||||||
|
net.width,
|
||||||
|
context=self.context,
|
||||||
|
metrics=self.metrics,
|
||||||
|
net_id=net_id,
|
||||||
|
skip_congestion=True,
|
||||||
|
max_cost=max_cost_limit,
|
||||||
|
self_collision_check=True,
|
||||||
|
node_limit=greedy_node_limit,
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
greedy_paths[net_id] = tuple(path)
|
||||||
|
temp_obj_ids.extend(self._stage_path_as_static(path))
|
||||||
|
self.context.clear_static_caches()
|
||||||
|
|
||||||
|
self._remove_static_obstacles(temp_obj_ids)
|
||||||
|
return greedy_paths
|
||||||
|
|
||||||
|
def _prepare_state(self) -> _RoutingState:
|
||||||
|
problem = self.problem
|
||||||
|
congestion = self.options.congestion
|
||||||
|
initial_paths = self.options.search.initial_paths
|
||||||
|
net_specs = {net.net_id: net for net in problem.nets}
|
||||||
|
num_nets = len(net_specs)
|
||||||
|
state = _RoutingState(
|
||||||
|
net_specs=net_specs,
|
||||||
|
ordered_net_ids=list(net_specs.keys()),
|
||||||
|
results={},
|
||||||
|
needs_self_collision_check=set(),
|
||||||
|
start_time=time.monotonic(),
|
||||||
|
timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations),
|
||||||
|
initial_paths=initial_paths,
|
||||||
|
accumulated_expanded_nodes=[],
|
||||||
|
)
|
||||||
|
if state.initial_paths is None:
|
||||||
|
warm_start_order = congestion.sort_nets if congestion.sort_nets is not None else congestion.warm_start
|
||||||
|
if warm_start_order is not None:
|
||||||
|
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, warm_start_order)
|
||||||
|
self.context.clear_static_caches()
|
||||||
|
|
||||||
|
if congestion.sort_nets and congestion.sort_nets != "user":
|
||||||
|
state.ordered_net_ids = self._routing_order(net_specs, congestion.sort_nets)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _route_net_once(
|
||||||
|
self,
|
||||||
|
state: _RoutingState,
|
||||||
|
iteration: int,
|
||||||
|
net_id: str,
|
||||||
|
) -> RoutingResult:
|
||||||
|
search = self.options.search
|
||||||
|
congestion = self.options.congestion
|
||||||
|
diagnostics = self.options.diagnostics
|
||||||
|
net = state.net_specs[net_id]
|
||||||
|
self._remove_path(net_id)
|
||||||
|
|
||||||
|
if iteration == 0 and state.initial_paths and net_id in state.initial_paths:
|
||||||
|
path: Sequence[ComponentResult] | None = state.initial_paths[net_id]
|
||||||
|
else:
|
||||||
|
coll_model = search.bend_collision_type
|
||||||
|
skip_congestion = False
|
||||||
|
if congestion.use_tiered_strategy and iteration == 0:
|
||||||
|
skip_congestion = True
|
||||||
|
if coll_model == "arc":
|
||||||
|
coll_model = "clipped_bbox"
|
||||||
|
|
||||||
|
path = route_astar(
|
||||||
|
net.start,
|
||||||
|
net.target,
|
||||||
|
net.width,
|
||||||
|
context=self.context,
|
||||||
|
metrics=self.metrics,
|
||||||
|
net_id=net_id,
|
||||||
|
bend_collision_type=coll_model,
|
||||||
|
return_partial=True,
|
||||||
|
store_expanded=diagnostics.capture_expanded,
|
||||||
|
skip_congestion=skip_congestion,
|
||||||
|
self_collision_check=(net_id in state.needs_self_collision_check),
|
||||||
|
node_limit=search.node_limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
if diagnostics.capture_expanded and self.metrics.last_expanded_nodes:
|
||||||
|
state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
return self._build_routing_result(net_id=net_id, path=[], reached_target=False)
|
||||||
|
|
||||||
|
reached_target = path[-1].end_port == net.target
|
||||||
|
report = None
|
||||||
|
self._install_path(net_id, path)
|
||||||
|
if reached_target:
|
||||||
|
report = self._verify_path_report(net_id, path)
|
||||||
|
if report.self_collision_count > 0:
|
||||||
|
state.needs_self_collision_check.add(net_id)
|
||||||
|
|
||||||
|
return self._build_routing_result(
|
||||||
|
net_id=net_id,
|
||||||
|
path=path,
|
||||||
|
reached_target=reached_target,
|
||||||
|
report=report,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_iteration(
|
||||||
|
self,
|
||||||
|
state: _RoutingState,
|
||||||
|
iteration: int,
|
||||||
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
||||||
|
) -> dict[str, RoutingOutcome] | None:
|
||||||
|
outcomes: dict[str, RoutingOutcome] = {}
|
||||||
|
congestion = self.options.congestion
|
||||||
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
|
if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None):
|
||||||
|
iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None
|
||||||
|
random.Random(iteration_seed).shuffle(state.ordered_net_ids)
|
||||||
|
|
||||||
|
for net_id in state.ordered_net_ids:
|
||||||
|
if time.monotonic() - state.start_time > state.timeout_s:
|
||||||
|
self._finalize_dynamic_tree()
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = self._route_net_once(state, iteration, net_id)
|
||||||
|
state.results[net_id] = result
|
||||||
|
outcomes[net_id] = result.outcome
|
||||||
|
|
||||||
|
if iteration_callback:
|
||||||
|
iteration_callback(iteration, state.results)
|
||||||
|
return outcomes
|
||||||
|
|
||||||
|
def _run_iterations(
|
||||||
|
self,
|
||||||
|
state: _RoutingState,
|
||||||
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
||||||
|
) -> bool:
|
||||||
|
congestion = self.options.congestion
|
||||||
|
for iteration in range(congestion.max_iterations):
|
||||||
|
outcomes = self._run_iteration(state, iteration, iteration_callback)
|
||||||
|
if outcomes is None:
|
||||||
|
return True
|
||||||
|
if not any(routing_outcome_needs_retry(outcome) for outcome in outcomes.values()):
|
||||||
|
return False
|
||||||
|
self.cost_evaluator.congestion_penalty *= congestion.multiplier
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _refine_results(self, state: _RoutingState) -> None:
|
||||||
|
if not self.options.refinement.enabled or not state.results:
|
||||||
|
return
|
||||||
|
|
||||||
|
for net_id in state.ordered_net_ids:
|
||||||
|
result = state.results.get(net_id)
|
||||||
|
if not result or not result.path or routing_outcome_needs_retry(result.outcome):
|
||||||
|
continue
|
||||||
|
net = state.net_specs[net_id]
|
||||||
|
self._remove_path(net_id)
|
||||||
|
refined_path = self.refiner.refine_path(net_id, net.start, net.target, net.width, result.path)
|
||||||
|
self._install_path(net_id, refined_path)
|
||||||
|
report = self._verify_path_report(net_id, refined_path)
|
||||||
|
state.results[net_id] = self._build_routing_result(
|
||||||
|
net_id=net_id,
|
||||||
|
path=refined_path,
|
||||||
|
reached_target=result.reached_target,
|
||||||
|
report=report,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
|
||||||
|
final_results: dict[str, RoutingResult] = {}
|
||||||
|
for net in self.problem.nets:
|
||||||
|
result = state.results.get(net.net_id)
|
||||||
|
if not result or not result.path:
|
||||||
|
final_results[net.net_id] = self._build_routing_result(
|
||||||
|
net_id=net.net_id,
|
||||||
|
path=[],
|
||||||
|
reached_target=False,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
report = self._verify_path_report(net.net_id, result.path)
|
||||||
|
final_results[net.net_id] = self._build_routing_result(
|
||||||
|
net_id=net.net_id,
|
||||||
|
path=result.path,
|
||||||
|
reached_target=result.reached_target,
|
||||||
|
report=report,
|
||||||
|
)
|
||||||
|
return final_results
|
||||||
|
|
||||||
|
def route_all(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
|
||||||
|
) -> dict[str, RoutingResult]:
|
||||||
|
self.cost_evaluator.congestion_penalty = self.options.congestion.base_penalty
|
||||||
|
self.accumulated_expanded_nodes = []
|
||||||
|
self.metrics.reset_totals()
|
||||||
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
|
state = self._prepare_state()
|
||||||
|
timed_out = self._run_iterations(state, iteration_callback)
|
||||||
|
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
|
||||||
|
|
||||||
|
if timed_out:
|
||||||
|
return self._verify_results(state)
|
||||||
|
|
||||||
|
self._refine_results(state)
|
||||||
|
self._finalize_dynamic_tree()
|
||||||
|
return self._verify_results(state)
|
||||||
112
inire/router/_search.py
Normal file
112
inire/router/_search.py
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
from inire.geometry.components import BendCollisionModel
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
from ._astar_moves import expand_moves as _expand_moves
|
||||||
|
from ._astar_types import AStarContext, AStarMetrics, AStarNode as _AStarNode
|
||||||
|
from .results import RouteMetrics
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
|
||||||
|
|
||||||
|
def _reconstruct_path(end_node: _AStarNode) -> list[ComponentResult]:
|
||||||
|
path = []
|
||||||
|
curr: _AStarNode | None = end_node
|
||||||
|
while curr and curr.component_result:
|
||||||
|
path.append(curr.component_result)
|
||||||
|
curr = curr.parent
|
||||||
|
return path[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
def route_astar(
|
||||||
|
start: Port,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics | None = None,
|
||||||
|
net_id: str = "default",
|
||||||
|
bend_collision_type: BendCollisionModel | None = None,
|
||||||
|
return_partial: bool = False,
|
||||||
|
store_expanded: bool = False,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
node_limit: int | None = None,
|
||||||
|
) -> list[ComponentResult] | None:
|
||||||
|
if metrics is None:
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
metrics.reset_per_route()
|
||||||
|
search_options = context.options.search
|
||||||
|
effective_bend_collision_type = bend_collision_type if bend_collision_type is not None else search_options.bend_collision_type
|
||||||
|
|
||||||
|
context.ensure_static_caches_current()
|
||||||
|
context.cost_evaluator.set_target(target)
|
||||||
|
open_set: list[_AStarNode] = []
|
||||||
|
closed_set: dict[tuple[int, int, int], float] = {}
|
||||||
|
congestion_cache: dict[tuple, int] = {}
|
||||||
|
|
||||||
|
start_node = _AStarNode(start, 0.0, context.cost_evaluator.h_manhattan(start, target))
|
||||||
|
heapq.heappush(open_set, start_node)
|
||||||
|
best_node = start_node
|
||||||
|
effective_node_limit = node_limit if node_limit is not None else search_options.node_limit
|
||||||
|
nodes_expanded = 0
|
||||||
|
|
||||||
|
while open_set:
|
||||||
|
if nodes_expanded >= effective_node_limit:
|
||||||
|
return _reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
|
current = heapq.heappop(open_set)
|
||||||
|
if max_cost is not None and current.fh_cost[0] > max_cost:
|
||||||
|
metrics.pruned_cost += 1
|
||||||
|
metrics.total_pruned_cost += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current.h_cost < best_node.h_cost:
|
||||||
|
best_node = current
|
||||||
|
|
||||||
|
state = current.port.as_tuple()
|
||||||
|
if state in closed_set and closed_set[state] <= current.g_cost + TOLERANCE_LINEAR:
|
||||||
|
continue
|
||||||
|
closed_set[state] = current.g_cost
|
||||||
|
|
||||||
|
if store_expanded:
|
||||||
|
metrics.last_expanded_nodes.append(state)
|
||||||
|
|
||||||
|
nodes_expanded += 1
|
||||||
|
metrics.total_nodes_expanded += 1
|
||||||
|
metrics.nodes_expanded += 1
|
||||||
|
|
||||||
|
if current.port == target:
|
||||||
|
return _reconstruct_path(current)
|
||||||
|
|
||||||
|
_expand_moves(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
net_width,
|
||||||
|
net_id,
|
||||||
|
open_set,
|
||||||
|
closed_set,
|
||||||
|
context,
|
||||||
|
metrics,
|
||||||
|
congestion_cache,
|
||||||
|
bend_collision_type=effective_bend_collision_type,
|
||||||
|
max_cost=max_cost,
|
||||||
|
skip_congestion=skip_congestion,
|
||||||
|
self_collision_check=self_collision_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AStarContext",
|
||||||
|
"AStarMetrics",
|
||||||
|
"RouteMetrics",
|
||||||
|
"route_astar",
|
||||||
|
]
|
||||||
|
|
@ -1,721 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import heapq
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
from typing import TYPE_CHECKING, Any, Literal
|
|
||||||
|
|
||||||
import shapely
|
|
||||||
|
|
||||||
from inire.constants import TOLERANCE_LINEAR
|
|
||||||
from inire.geometry.components import Bend90, SBend, Straight
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.config import RouterConfig, VisibilityGuidanceMode
|
|
||||||
from inire.router.refiner import component_hits_ancestor_chain
|
|
||||||
from inire.router.visibility import VisibilityManager
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AStarNode:
|
|
||||||
__slots__ = ("port", "g_cost", "h_cost", "fh_cost", "parent", "component_result")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
port: Port,
|
|
||||||
g_cost: float,
|
|
||||||
h_cost: float,
|
|
||||||
parent: AStarNode | None = None,
|
|
||||||
component_result: ComponentResult | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.port = port
|
|
||||||
self.g_cost = g_cost
|
|
||||||
self.h_cost = h_cost
|
|
||||||
self.fh_cost = (g_cost + h_cost, h_cost)
|
|
||||||
self.parent = parent
|
|
||||||
self.component_result = component_result
|
|
||||||
|
|
||||||
def __lt__(self, other: AStarNode) -> bool:
|
|
||||||
return self.fh_cost < other.fh_cost
|
|
||||||
|
|
||||||
|
|
||||||
class AStarMetrics:
|
|
||||||
__slots__ = (
|
|
||||||
"total_nodes_expanded",
|
|
||||||
"last_expanded_nodes",
|
|
||||||
"nodes_expanded",
|
|
||||||
"moves_generated",
|
|
||||||
"moves_added",
|
|
||||||
"pruned_closed_set",
|
|
||||||
"pruned_hard_collision",
|
|
||||||
"pruned_cost",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.total_nodes_expanded = 0
|
|
||||||
self.last_expanded_nodes: list[tuple[int, int, int]] = []
|
|
||||||
self.nodes_expanded = 0
|
|
||||||
self.moves_generated = 0
|
|
||||||
self.moves_added = 0
|
|
||||||
self.pruned_closed_set = 0
|
|
||||||
self.pruned_hard_collision = 0
|
|
||||||
self.pruned_cost = 0
|
|
||||||
|
|
||||||
def reset_per_route(self) -> None:
|
|
||||||
self.nodes_expanded = 0
|
|
||||||
self.moves_generated = 0
|
|
||||||
self.moves_added = 0
|
|
||||||
self.pruned_closed_set = 0
|
|
||||||
self.pruned_hard_collision = 0
|
|
||||||
self.pruned_cost = 0
|
|
||||||
self.last_expanded_nodes = []
|
|
||||||
|
|
||||||
|
|
||||||
class AStarContext:
|
|
||||||
__slots__ = (
|
|
||||||
"cost_evaluator",
|
|
||||||
"config",
|
|
||||||
"visibility_manager",
|
|
||||||
"move_cache_rel",
|
|
||||||
"move_cache_abs",
|
|
||||||
"hard_collision_set",
|
|
||||||
"static_safe_cache",
|
|
||||||
"max_cache_size",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
cost_evaluator: CostEvaluator,
|
|
||||||
node_limit: int = 1000000,
|
|
||||||
max_straight_length: float = 2000.0,
|
|
||||||
min_straight_length: float = 5.0,
|
|
||||||
bend_radii: list[float] | None = None,
|
|
||||||
sbend_radii: list[float] | None = None,
|
|
||||||
sbend_offsets: list[float] | None = None,
|
|
||||||
bend_penalty: float = 250.0,
|
|
||||||
sbend_penalty: float | None = None,
|
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc",
|
|
||||||
bend_clip_margin: float = 10.0,
|
|
||||||
visibility_guidance: VisibilityGuidanceMode = "tangent_corner",
|
|
||||||
max_cache_size: int = 1000000,
|
|
||||||
) -> None:
|
|
||||||
actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty
|
|
||||||
self.cost_evaluator = cost_evaluator
|
|
||||||
self.max_cache_size = max_cache_size
|
|
||||||
self.config = RouterConfig(
|
|
||||||
node_limit=node_limit,
|
|
||||||
max_straight_length=max_straight_length,
|
|
||||||
min_straight_length=min_straight_length,
|
|
||||||
bend_radii=bend_radii if bend_radii is not None else [50.0, 100.0],
|
|
||||||
sbend_radii=sbend_radii if sbend_radii is not None else [5.0, 10.0, 50.0, 100.0],
|
|
||||||
sbend_offsets=sbend_offsets,
|
|
||||||
bend_penalty=bend_penalty,
|
|
||||||
sbend_penalty=actual_sbend_penalty,
|
|
||||||
bend_collision_type=bend_collision_type,
|
|
||||||
bend_clip_margin=bend_clip_margin,
|
|
||||||
visibility_guidance=visibility_guidance,
|
|
||||||
)
|
|
||||||
self.cost_evaluator.apply_routing_costs(
|
|
||||||
bend_penalty=self.config.bend_penalty,
|
|
||||||
sbend_penalty=self.config.sbend_penalty,
|
|
||||||
bend_radii=self.config.bend_radii,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine)
|
|
||||||
self.move_cache_rel: dict[tuple, ComponentResult] = {}
|
|
||||||
self.move_cache_abs: dict[tuple, ComponentResult] = {}
|
|
||||||
self.hard_collision_set: set[tuple] = set()
|
|
||||||
self.static_safe_cache: set[tuple] = set()
|
|
||||||
|
|
||||||
def clear_static_caches(self) -> None:
|
|
||||||
self.hard_collision_set.clear()
|
|
||||||
self.static_safe_cache.clear()
|
|
||||||
self.visibility_manager.clear_cache()
|
|
||||||
|
|
||||||
def check_cache_eviction(self) -> None:
|
|
||||||
if len(self.move_cache_abs) <= self.max_cache_size * 1.2:
|
|
||||||
return
|
|
||||||
num_to_evict = int(len(self.move_cache_abs) * 0.25)
|
|
||||||
for idx, key in enumerate(list(self.move_cache_abs.keys())):
|
|
||||||
if idx >= num_to_evict:
|
|
||||||
break
|
|
||||||
del self.move_cache_abs[key]
|
|
||||||
|
|
||||||
|
|
||||||
def route_astar(
|
|
||||||
start: Port,
|
|
||||||
target: Port,
|
|
||||||
net_width: float,
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: AStarMetrics | None = None,
|
|
||||||
net_id: str = "default",
|
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | None = None,
|
|
||||||
return_partial: bool = False,
|
|
||||||
store_expanded: bool = False,
|
|
||||||
skip_congestion: bool = False,
|
|
||||||
max_cost: float | None = None,
|
|
||||||
self_collision_check: bool = False,
|
|
||||||
node_limit: int | None = None,
|
|
||||||
) -> list[ComponentResult] | None:
|
|
||||||
if metrics is None:
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
metrics.reset_per_route()
|
|
||||||
effective_bend_collision_type = bend_collision_type if bend_collision_type is not None else context.config.bend_collision_type
|
|
||||||
|
|
||||||
context.cost_evaluator.set_target(target)
|
|
||||||
open_set: list[AStarNode] = []
|
|
||||||
closed_set: dict[tuple[int, int, int], float] = {}
|
|
||||||
congestion_cache: dict[tuple, int] = {}
|
|
||||||
|
|
||||||
start_node = AStarNode(start, 0.0, context.cost_evaluator.h_manhattan(start, target))
|
|
||||||
heapq.heappush(open_set, start_node)
|
|
||||||
best_node = start_node
|
|
||||||
effective_node_limit = node_limit if node_limit is not None else context.config.node_limit
|
|
||||||
nodes_expanded = 0
|
|
||||||
|
|
||||||
while open_set:
|
|
||||||
if nodes_expanded >= effective_node_limit:
|
|
||||||
return reconstruct_path(best_node) if return_partial else None
|
|
||||||
|
|
||||||
current = heapq.heappop(open_set)
|
|
||||||
if max_cost is not None and current.fh_cost[0] > max_cost:
|
|
||||||
metrics.pruned_cost += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if current.h_cost < best_node.h_cost:
|
|
||||||
best_node = current
|
|
||||||
|
|
||||||
state = current.port.as_tuple()
|
|
||||||
if state in closed_set and closed_set[state] <= current.g_cost + TOLERANCE_LINEAR:
|
|
||||||
continue
|
|
||||||
closed_set[state] = current.g_cost
|
|
||||||
|
|
||||||
if store_expanded:
|
|
||||||
metrics.last_expanded_nodes.append(state)
|
|
||||||
|
|
||||||
nodes_expanded += 1
|
|
||||||
metrics.total_nodes_expanded += 1
|
|
||||||
metrics.nodes_expanded += 1
|
|
||||||
|
|
||||||
if current.port == target:
|
|
||||||
return reconstruct_path(current)
|
|
||||||
|
|
||||||
expand_moves(
|
|
||||||
current,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
effective_bend_collision_type,
|
|
||||||
max_cost=max_cost,
|
|
||||||
skip_congestion=skip_congestion,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
return reconstruct_path(best_node) if return_partial else None
|
|
||||||
|
|
||||||
|
|
||||||
def _quantized_lengths(values: list[float], max_reach: float) -> list[int]:
|
|
||||||
out = {int(round(v)) for v in values if v > 0 and v <= max_reach + 0.01}
|
|
||||||
return sorted((v for v in out if v > 0), reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _sbend_forward_span(offset: float, radius: float) -> float | None:
|
|
||||||
abs_offset = abs(offset)
|
|
||||||
if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius:
|
|
||||||
return None
|
|
||||||
theta = __import__("math").acos(1.0 - abs_offset / (2.0 * radius))
|
|
||||||
return 2.0 * radius * __import__("math").sin(theta)
|
|
||||||
|
|
||||||
|
|
||||||
def _visible_straight_candidates(
|
|
||||||
current: Port,
|
|
||||||
context: AStarContext,
|
|
||||||
max_reach: float,
|
|
||||||
cos_v: float,
|
|
||||||
sin_v: float,
|
|
||||||
net_width: float,
|
|
||||||
) -> list[float]:
|
|
||||||
mode = context.config.visibility_guidance
|
|
||||||
if mode == "off":
|
|
||||||
return []
|
|
||||||
|
|
||||||
if mode == "exact_corner":
|
|
||||||
max_bend_radius = max(context.config.bend_radii, default=0.0)
|
|
||||||
visibility_reach = max_reach + max_bend_radius
|
|
||||||
visible_corners = sorted(
|
|
||||||
context.visibility_manager.get_corner_visibility(current, max_dist=visibility_reach),
|
|
||||||
key=lambda corner: corner[2],
|
|
||||||
)
|
|
||||||
if not visible_corners:
|
|
||||||
return []
|
|
||||||
|
|
||||||
candidates: set[int] = set()
|
|
||||||
for cx, cy, _ in visible_corners[:12]:
|
|
||||||
dx = cx - current.x
|
|
||||||
dy = cy - current.y
|
|
||||||
local_x = dx * cos_v + dy * sin_v
|
|
||||||
if local_x <= context.config.min_straight_length:
|
|
||||||
continue
|
|
||||||
candidates.add(int(round(local_x)))
|
|
||||||
return sorted(candidates, reverse=True)
|
|
||||||
|
|
||||||
if mode != "tangent_corner":
|
|
||||||
return []
|
|
||||||
|
|
||||||
visibility_manager = context.visibility_manager
|
|
||||||
visibility_manager._ensure_current()
|
|
||||||
max_bend_radius = max(context.config.bend_radii, default=0.0)
|
|
||||||
if max_bend_radius <= 0 or not visibility_manager.corners:
|
|
||||||
return []
|
|
||||||
|
|
||||||
reach = max_reach + max_bend_radius
|
|
||||||
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach)
|
|
||||||
candidate_ids = list(visibility_manager.corner_index.intersection(bounds))
|
|
||||||
if not candidate_ids:
|
|
||||||
return []
|
|
||||||
|
|
||||||
scored: list[tuple[float, float, float, float, float]] = []
|
|
||||||
for idx in candidate_ids:
|
|
||||||
cx, cy = visibility_manager.corners[idx]
|
|
||||||
dx = cx - current.x
|
|
||||||
dy = cy - current.y
|
|
||||||
local_x = dx * cos_v + dy * sin_v
|
|
||||||
local_y = -dx * sin_v + dy * cos_v
|
|
||||||
if local_x <= context.config.min_straight_length or local_x > reach + 0.01:
|
|
||||||
continue
|
|
||||||
|
|
||||||
nearest_radius = min(context.config.bend_radii, key=lambda radius: abs(abs(local_y) - radius))
|
|
||||||
tangent_error = abs(abs(local_y) - nearest_radius)
|
|
||||||
if tangent_error > 2.0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
length = local_x - nearest_radius
|
|
||||||
if length <= context.config.min_straight_length or length > max_reach + 0.01:
|
|
||||||
continue
|
|
||||||
|
|
||||||
scored.append((tangent_error, math.hypot(dx, dy), length, dx, dy))
|
|
||||||
|
|
||||||
if not scored:
|
|
||||||
return []
|
|
||||||
|
|
||||||
collision_engine = context.cost_evaluator.collision_engine
|
|
||||||
candidates: set[int] = set()
|
|
||||||
for _, dist, length, dx, dy in sorted(scored)[:4]:
|
|
||||||
angle = math.degrees(math.atan2(dy, dx))
|
|
||||||
corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width)
|
|
||||||
if corner_reach < dist - 0.01:
|
|
||||||
continue
|
|
||||||
qlen = int(round(length))
|
|
||||||
if qlen > 0:
|
|
||||||
candidates.add(qlen)
|
|
||||||
|
|
||||||
return sorted(candidates, reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _previous_move_metadata(node: AStarNode) -> tuple[str | None, float | None]:
|
|
||||||
result = node.component_result
|
|
||||||
if result is None:
|
|
||||||
return None, None
|
|
||||||
move_type = result.move_type
|
|
||||||
if move_type == "Straight":
|
|
||||||
return move_type, result.length
|
|
||||||
return move_type, None
|
|
||||||
|
|
||||||
|
|
||||||
def expand_moves(
|
|
||||||
current: AStarNode,
|
|
||||||
target: Port,
|
|
||||||
net_width: float,
|
|
||||||
net_id: str,
|
|
||||||
open_set: list[AStarNode],
|
|
||||||
closed_set: dict[tuple[int, int, int], float],
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: AStarMetrics,
|
|
||||||
congestion_cache: dict[tuple, int],
|
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any | None = None,
|
|
||||||
max_cost: float | None = None,
|
|
||||||
skip_congestion: bool = False,
|
|
||||||
self_collision_check: bool = False,
|
|
||||||
) -> None:
|
|
||||||
effective_bend_collision_type = bend_collision_type if bend_collision_type is not None else context.config.bend_collision_type
|
|
||||||
cp = current.port
|
|
||||||
prev_move_type, prev_straight_length = _previous_move_metadata(current)
|
|
||||||
dx_t = target.x - cp.x
|
|
||||||
dy_t = target.y - cp.y
|
|
||||||
dist_sq = dx_t * dx_t + dy_t * dy_t
|
|
||||||
|
|
||||||
if cp.r == 0:
|
|
||||||
cos_v, sin_v = 1.0, 0.0
|
|
||||||
elif cp.r == 90:
|
|
||||||
cos_v, sin_v = 0.0, 1.0
|
|
||||||
elif cp.r == 180:
|
|
||||||
cos_v, sin_v = -1.0, 0.0
|
|
||||||
else:
|
|
||||||
cos_v, sin_v = 0.0, -1.0
|
|
||||||
|
|
||||||
proj_t = dx_t * cos_v + dy_t * sin_v
|
|
||||||
perp_t = -dx_t * sin_v + dy_t * cos_v
|
|
||||||
dx_local = proj_t
|
|
||||||
dy_local = perp_t
|
|
||||||
|
|
||||||
if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r:
|
|
||||||
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, proj_t + 1.0, net_width=net_width)
|
|
||||||
if max_reach >= proj_t - 0.01 and (
|
|
||||||
prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR
|
|
||||||
):
|
|
||||||
process_move(
|
|
||||||
current,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
"S",
|
|
||||||
(int(round(proj_t)),),
|
|
||||||
skip_congestion,
|
|
||||||
bend_collision_type=effective_bend_collision_type,
|
|
||||||
max_cost=max_cost,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, context.config.max_straight_length, net_width=net_width)
|
|
||||||
candidate_lengths = [
|
|
||||||
context.config.min_straight_length,
|
|
||||||
max_reach,
|
|
||||||
max_reach / 2.0,
|
|
||||||
max_reach - 5.0,
|
|
||||||
]
|
|
||||||
|
|
||||||
axis_target_dist = abs(dx_t) if cp.r in (0, 180) else abs(dy_t)
|
|
||||||
candidate_lengths.append(axis_target_dist)
|
|
||||||
for radius in context.config.bend_radii:
|
|
||||||
candidate_lengths.extend((max_reach - radius, axis_target_dist - radius, axis_target_dist - 2.0 * radius))
|
|
||||||
|
|
||||||
candidate_lengths.extend(
|
|
||||||
_visible_straight_candidates(
|
|
||||||
cp,
|
|
||||||
context,
|
|
||||||
max_reach,
|
|
||||||
cos_v,
|
|
||||||
sin_v,
|
|
||||||
net_width,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if cp.r == target.r and dx_local > 0 and abs(dy_local) > TOLERANCE_LINEAR:
|
|
||||||
for radius in context.config.sbend_radii:
|
|
||||||
sbend_span = _sbend_forward_span(dy_local, radius)
|
|
||||||
if sbend_span is None:
|
|
||||||
continue
|
|
||||||
candidate_lengths.extend((dx_local - sbend_span, dx_local - 2.0 * sbend_span))
|
|
||||||
|
|
||||||
for length in _quantized_lengths(candidate_lengths, max_reach):
|
|
||||||
if length < context.config.min_straight_length:
|
|
||||||
continue
|
|
||||||
if prev_straight_length is not None and length >= prev_straight_length - TOLERANCE_LINEAR:
|
|
||||||
continue
|
|
||||||
process_move(
|
|
||||||
current,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
"S",
|
|
||||||
(length,),
|
|
||||||
skip_congestion,
|
|
||||||
bend_collision_type=effective_bend_collision_type,
|
|
||||||
max_cost=max_cost,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
angle_to_target = 0.0
|
|
||||||
if dx_t != 0 or dy_t != 0:
|
|
||||||
angle_to_target = float((round((180.0 / 3.141592653589793) * __import__("math").atan2(dy_t, dx_t)) + 360.0) % 360.0)
|
|
||||||
allow_backwards = dist_sq < 150 * 150
|
|
||||||
|
|
||||||
for radius in context.config.bend_radii:
|
|
||||||
for direction in ("CW", "CCW"):
|
|
||||||
if not allow_backwards:
|
|
||||||
turn = 90 if direction == "CCW" else -90
|
|
||||||
new_ori = (cp.r + turn) % 360
|
|
||||||
new_diff = (angle_to_target - new_ori + 180.0) % 360.0 - 180.0
|
|
||||||
if abs(new_diff) > 135.0:
|
|
||||||
continue
|
|
||||||
process_move(
|
|
||||||
current,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
"B",
|
|
||||||
(radius, direction),
|
|
||||||
skip_congestion,
|
|
||||||
bend_collision_type=effective_bend_collision_type,
|
|
||||||
max_cost=max_cost,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
max_sbend_r = max(context.config.sbend_radii) if context.config.sbend_radii else 0.0
|
|
||||||
if max_sbend_r <= 0 or prev_move_type == "SBend":
|
|
||||||
return
|
|
||||||
|
|
||||||
explicit_offsets = context.config.sbend_offsets
|
|
||||||
offsets: set[int] = set(int(round(v)) for v in explicit_offsets or [])
|
|
||||||
|
|
||||||
# S-bends preserve orientation, so the implicit search only makes sense
|
|
||||||
# when the target is ahead in local coordinates and keeps the same
|
|
||||||
# orientation. Generating generic speculative offsets on the integer lattice
|
|
||||||
# explodes the search space without contributing useful moves.
|
|
||||||
if target.r == cp.r and 0 < dx_local <= 4 * max_sbend_r:
|
|
||||||
if 0 < abs(dy_local) < 2 * max_sbend_r:
|
|
||||||
offsets.add(int(round(dy_local)))
|
|
||||||
|
|
||||||
if not offsets:
|
|
||||||
return
|
|
||||||
|
|
||||||
for offset in sorted(offsets):
|
|
||||||
if offset == 0:
|
|
||||||
continue
|
|
||||||
for radius in context.config.sbend_radii:
|
|
||||||
if abs(offset) >= 2 * radius:
|
|
||||||
continue
|
|
||||||
process_move(
|
|
||||||
current,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
"SB",
|
|
||||||
(offset, radius),
|
|
||||||
skip_congestion,
|
|
||||||
bend_collision_type=effective_bend_collision_type,
|
|
||||||
max_cost=max_cost,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_move(
|
|
||||||
parent: AStarNode,
|
|
||||||
target: Port,
|
|
||||||
net_width: float,
|
|
||||||
net_id: str,
|
|
||||||
open_set: list[AStarNode],
|
|
||||||
closed_set: dict[tuple[int, int, int], float],
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: AStarMetrics,
|
|
||||||
congestion_cache: dict[tuple, int],
|
|
||||||
move_class: Literal["S", "B", "SB"],
|
|
||||||
params: tuple,
|
|
||||||
skip_congestion: bool,
|
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any,
|
|
||||||
max_cost: float | None = None,
|
|
||||||
self_collision_check: bool = False,
|
|
||||||
) -> None:
|
|
||||||
cp = parent.port
|
|
||||||
coll_type = bend_collision_type
|
|
||||||
coll_key = id(coll_type) if isinstance(coll_type, shapely.geometry.Polygon) else coll_type
|
|
||||||
self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0
|
|
||||||
|
|
||||||
abs_key = (
|
|
||||||
cp.as_tuple(),
|
|
||||||
move_class,
|
|
||||||
params,
|
|
||||||
net_width,
|
|
||||||
coll_key,
|
|
||||||
context.config.bend_clip_margin,
|
|
||||||
self_dilation,
|
|
||||||
)
|
|
||||||
if abs_key in context.move_cache_abs:
|
|
||||||
res = context.move_cache_abs[abs_key]
|
|
||||||
else:
|
|
||||||
context.check_cache_eviction()
|
|
||||||
base_port = Port(0, 0, cp.r)
|
|
||||||
rel_key = (
|
|
||||||
cp.r,
|
|
||||||
move_class,
|
|
||||||
params,
|
|
||||||
net_width,
|
|
||||||
coll_key,
|
|
||||||
context.config.bend_clip_margin,
|
|
||||||
self_dilation,
|
|
||||||
)
|
|
||||||
if rel_key in context.move_cache_rel:
|
|
||||||
res_rel = context.move_cache_rel[rel_key]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if move_class == "S":
|
|
||||||
res_rel = Straight.generate(base_port, params[0], net_width, dilation=self_dilation)
|
|
||||||
elif move_class == "B":
|
|
||||||
res_rel = Bend90.generate(
|
|
||||||
base_port,
|
|
||||||
params[0],
|
|
||||||
net_width,
|
|
||||||
params[1],
|
|
||||||
collision_type=coll_type,
|
|
||||||
clip_margin=context.config.bend_clip_margin,
|
|
||||||
dilation=self_dilation,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
res_rel = SBend.generate(
|
|
||||||
base_port,
|
|
||||||
params[0],
|
|
||||||
params[1],
|
|
||||||
net_width,
|
|
||||||
collision_type=coll_type,
|
|
||||||
clip_margin=context.config.bend_clip_margin,
|
|
||||||
dilation=self_dilation,
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
context.move_cache_rel[rel_key] = res_rel
|
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
|
||||||
context.move_cache_abs[abs_key] = res
|
|
||||||
|
|
||||||
move_radius = params[0] if move_class == "B" else (params[1] if move_class == "SB" else None)
|
|
||||||
add_node(
|
|
||||||
parent,
|
|
||||||
res,
|
|
||||||
target,
|
|
||||||
net_width,
|
|
||||||
net_id,
|
|
||||||
open_set,
|
|
||||||
closed_set,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
congestion_cache,
|
|
||||||
move_class,
|
|
||||||
abs_key,
|
|
||||||
move_radius=move_radius,
|
|
||||||
skip_congestion=skip_congestion,
|
|
||||||
max_cost=max_cost,
|
|
||||||
self_collision_check=self_collision_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_node(
|
|
||||||
parent: AStarNode,
|
|
||||||
result: ComponentResult,
|
|
||||||
target: Port,
|
|
||||||
net_width: float,
|
|
||||||
net_id: str,
|
|
||||||
open_set: list[AStarNode],
|
|
||||||
closed_set: dict[tuple[int, int, int], float],
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: AStarMetrics,
|
|
||||||
congestion_cache: dict[tuple, int],
|
|
||||||
move_type: str,
|
|
||||||
cache_key: tuple,
|
|
||||||
move_radius: float | None = None,
|
|
||||||
skip_congestion: bool = False,
|
|
||||||
max_cost: float | None = None,
|
|
||||||
self_collision_check: bool = False,
|
|
||||||
) -> None:
|
|
||||||
metrics.moves_generated += 1
|
|
||||||
state = result.end_port.as_tuple()
|
|
||||||
new_lower_bound_g = parent.g_cost + result.length
|
|
||||||
if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR:
|
|
||||||
metrics.pruned_closed_set += 1
|
|
||||||
return
|
|
||||||
|
|
||||||
parent_p = parent.port
|
|
||||||
end_p = result.end_port
|
|
||||||
|
|
||||||
if cache_key in context.hard_collision_set:
|
|
||||||
metrics.pruned_hard_collision += 1
|
|
||||||
return
|
|
||||||
|
|
||||||
is_static_safe = cache_key in context.static_safe_cache
|
|
||||||
if not is_static_safe:
|
|
||||||
ce = context.cost_evaluator.collision_engine
|
|
||||||
if move_type == "S":
|
|
||||||
collision_found = ce.check_move_straight_static(parent_p, result.length, net_width=net_width)
|
|
||||||
else:
|
|
||||||
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p, net_width=net_width)
|
|
||||||
if collision_found:
|
|
||||||
context.hard_collision_set.add(cache_key)
|
|
||||||
metrics.pruned_hard_collision += 1
|
|
||||||
return
|
|
||||||
context.static_safe_cache.add(cache_key)
|
|
||||||
|
|
||||||
total_overlaps = 0
|
|
||||||
if not skip_congestion:
|
|
||||||
if cache_key in congestion_cache:
|
|
||||||
total_overlaps = congestion_cache[cache_key]
|
|
||||||
else:
|
|
||||||
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
|
||||||
congestion_cache[cache_key] = total_overlaps
|
|
||||||
|
|
||||||
if self_collision_check:
|
|
||||||
if component_hits_ancestor_chain(result, parent):
|
|
||||||
return
|
|
||||||
|
|
||||||
penalty = 0.0
|
|
||||||
if move_type == "SB":
|
|
||||||
penalty = context.config.sbend_penalty
|
|
||||||
elif move_type == "B":
|
|
||||||
penalty = context.config.bend_penalty
|
|
||||||
if move_radius is not None and move_radius > TOLERANCE_LINEAR:
|
|
||||||
penalty *= (10.0 / move_radius) ** 0.5
|
|
||||||
|
|
||||||
move_cost = context.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,
|
|
||||||
skip_congestion=True,
|
|
||||||
)
|
|
||||||
move_cost += total_overlaps * context.cost_evaluator.congestion_penalty
|
|
||||||
|
|
||||||
if max_cost is not None and parent.g_cost + move_cost > max_cost:
|
|
||||||
metrics.pruned_cost += 1
|
|
||||||
return
|
|
||||||
if move_cost > 1e12:
|
|
||||||
metrics.pruned_cost += 1
|
|
||||||
return
|
|
||||||
|
|
||||||
g_cost = parent.g_cost + move_cost
|
|
||||||
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
|
||||||
metrics.pruned_closed_set += 1
|
|
||||||
return
|
|
||||||
|
|
||||||
h_cost = context.cost_evaluator.h_manhattan(result.end_port, target)
|
|
||||||
heapq.heappush(open_set, AStarNode(result.end_port, g_cost, h_cost, parent, result))
|
|
||||||
metrics.moves_added += 1
|
|
||||||
|
|
||||||
|
|
||||||
def reconstruct_path(end_node: AStarNode) -> list[ComponentResult]:
|
|
||||||
path = []
|
|
||||||
curr: AStarNode | None = end_node
|
|
||||||
while curr and curr.component_result:
|
|
||||||
path.append(curr.component_result)
|
|
||||||
curr = curr.parent
|
|
||||||
return path[::-1]
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Literal, Any
|
|
||||||
|
|
||||||
|
|
||||||
VisibilityGuidanceMode = Literal["off", "exact_corner", "tangent_corner"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RouterConfig:
|
|
||||||
"""Configuration parameters for the A* Router."""
|
|
||||||
|
|
||||||
node_limit: int = 1000000
|
|
||||||
max_straight_length: float = 2000.0
|
|
||||||
min_straight_length: float = 5.0
|
|
||||||
|
|
||||||
sbend_offsets: list[float] | None = None
|
|
||||||
|
|
||||||
bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0])
|
|
||||||
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
|
||||||
snap_to_target_dist: float = 1000.0
|
|
||||||
bend_penalty: float = 250.0
|
|
||||||
sbend_penalty: float = 500.0
|
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
|
||||||
bend_clip_margin: float = 10.0
|
|
||||||
visibility_guidance: VisibilityGuidanceMode = "tangent_corner"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CostConfig:
|
|
||||||
"""Configuration parameters for the Cost Evaluator."""
|
|
||||||
|
|
||||||
unit_length_cost: float = 1.0
|
|
||||||
greedy_h_weight: float = 1.5
|
|
||||||
congestion_penalty: float = 10000.0
|
|
||||||
bend_penalty: float = 250.0
|
|
||||||
sbend_penalty: float = 500.0
|
|
||||||
min_bend_radius: float = 50.0
|
|
||||||
|
|
@ -5,12 +5,15 @@ from typing import TYPE_CHECKING
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from inire.constants import TOLERANCE_LINEAR
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
from inire.router.config import CostConfig
|
from inire.model import ObjectiveWeights, RoutingOptions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
from inire.geometry.components import ComponentResult, MoveKind
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
|
|
||||||
|
|
@ -19,10 +22,12 @@ class CostEvaluator:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"collision_engine",
|
"collision_engine",
|
||||||
"danger_map",
|
"danger_map",
|
||||||
"config",
|
"_unit_length_cost",
|
||||||
"unit_length_cost",
|
"_greedy_h_weight",
|
||||||
"greedy_h_weight",
|
"_bend_penalty",
|
||||||
"congestion_penalty",
|
"_sbend_penalty",
|
||||||
|
"_danger_weight",
|
||||||
|
"_congestion_penalty",
|
||||||
"_target_x",
|
"_target_x",
|
||||||
"_target_y",
|
"_target_y",
|
||||||
"_target_r",
|
"_target_r",
|
||||||
|
|
@ -33,53 +38,102 @@ class CostEvaluator:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
collision_engine: CollisionEngine,
|
collision_engine: RoutingWorld,
|
||||||
danger_map: DangerMap | None = None,
|
danger_map: DangerMap | None = None,
|
||||||
unit_length_cost: float = 1.0,
|
unit_length_cost: float = 1.0,
|
||||||
greedy_h_weight: float = 1.5,
|
greedy_h_weight: float = 1.5,
|
||||||
congestion_penalty: float = 10000.0,
|
|
||||||
bend_penalty: float = 250.0,
|
bend_penalty: float = 250.0,
|
||||||
sbend_penalty: float | None = None,
|
sbend_penalty: float | None = None,
|
||||||
min_bend_radius: float = 50.0,
|
danger_weight: float = 1.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty
|
actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty
|
||||||
self.collision_engine = collision_engine
|
self.collision_engine = collision_engine
|
||||||
self.danger_map = danger_map
|
self.danger_map = danger_map
|
||||||
self.config = CostConfig(
|
self._unit_length_cost = float(unit_length_cost)
|
||||||
unit_length_cost=unit_length_cost,
|
self._greedy_h_weight = float(greedy_h_weight)
|
||||||
greedy_h_weight=greedy_h_weight,
|
self._bend_penalty = float(bend_penalty)
|
||||||
congestion_penalty=congestion_penalty,
|
self._sbend_penalty = float(actual_sbend_penalty)
|
||||||
bend_penalty=bend_penalty,
|
self._danger_weight = float(danger_weight)
|
||||||
sbend_penalty=actual_sbend_penalty,
|
self._congestion_penalty = 0.0
|
||||||
min_bend_radius=min_bend_radius,
|
|
||||||
)
|
|
||||||
self.unit_length_cost = self.config.unit_length_cost
|
|
||||||
self.greedy_h_weight = self.config.greedy_h_weight
|
|
||||||
self.congestion_penalty = self.config.congestion_penalty
|
|
||||||
self._refresh_cached_config()
|
|
||||||
self._target_x = 0.0
|
self._target_x = 0.0
|
||||||
self._target_y = 0.0
|
self._target_y = 0.0
|
||||||
self._target_r = 0
|
self._target_r = 0
|
||||||
self._target_cos = 1.0
|
self._target_cos = 1.0
|
||||||
self._target_sin = 0.0
|
self._target_sin = 0.0
|
||||||
|
|
||||||
def apply_routing_costs(
|
self._min_radius = 50.0
|
||||||
self,
|
|
||||||
*,
|
|
||||||
bend_penalty: float,
|
|
||||||
sbend_penalty: float,
|
|
||||||
bend_radii: list[float],
|
|
||||||
) -> None:
|
|
||||||
self.config.bend_penalty = bend_penalty
|
|
||||||
self.config.sbend_penalty = sbend_penalty
|
|
||||||
self.config.min_bend_radius = min(bend_radii) if bend_radii else 50.0
|
|
||||||
self._refresh_cached_config()
|
|
||||||
|
|
||||||
def _refresh_cached_config(self) -> None:
|
@property
|
||||||
self._min_radius = self.config.min_bend_radius
|
def unit_length_cost(self) -> float:
|
||||||
self.unit_length_cost = self.config.unit_length_cost
|
return self._unit_length_cost
|
||||||
self.greedy_h_weight = self.config.greedy_h_weight
|
|
||||||
self.congestion_penalty = self.config.congestion_penalty
|
@unit_length_cost.setter
|
||||||
|
def unit_length_cost(self, value: float) -> None:
|
||||||
|
self._unit_length_cost = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def greedy_h_weight(self) -> float:
|
||||||
|
return self._greedy_h_weight
|
||||||
|
|
||||||
|
@greedy_h_weight.setter
|
||||||
|
def greedy_h_weight(self, value: float) -> None:
|
||||||
|
self._greedy_h_weight = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def congestion_penalty(self) -> float:
|
||||||
|
return self._congestion_penalty
|
||||||
|
|
||||||
|
@congestion_penalty.setter
|
||||||
|
def congestion_penalty(self, value: float) -> None:
|
||||||
|
self._congestion_penalty = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bend_penalty(self) -> float:
|
||||||
|
return self._bend_penalty
|
||||||
|
|
||||||
|
@bend_penalty.setter
|
||||||
|
def bend_penalty(self, value: float) -> None:
|
||||||
|
self._bend_penalty = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sbend_penalty(self) -> float:
|
||||||
|
return self._sbend_penalty
|
||||||
|
|
||||||
|
@sbend_penalty.setter
|
||||||
|
def sbend_penalty(self, value: float) -> None:
|
||||||
|
self._sbend_penalty = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def danger_weight(self) -> float:
|
||||||
|
return self._danger_weight
|
||||||
|
|
||||||
|
@danger_weight.setter
|
||||||
|
def danger_weight(self, value: float) -> None:
|
||||||
|
self._danger_weight = float(value)
|
||||||
|
|
||||||
|
def set_min_bend_radius(self, radius: float) -> None:
|
||||||
|
self._min_radius = float(radius) if radius > 0 else 50.0
|
||||||
|
|
||||||
|
def objective_weights(self, *, congestion_penalty: float | None = None) -> ObjectiveWeights:
|
||||||
|
return ObjectiveWeights(
|
||||||
|
unit_length_cost=self._unit_length_cost,
|
||||||
|
bend_penalty=self._bend_penalty,
|
||||||
|
sbend_penalty=self._sbend_penalty,
|
||||||
|
danger_weight=self._danger_weight,
|
||||||
|
congestion_penalty=self._congestion_penalty if congestion_penalty is None else float(congestion_penalty),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_refiner_weights(self, options: RoutingOptions) -> ObjectiveWeights:
|
||||||
|
refinement_objective = options.refinement.objective
|
||||||
|
if refinement_objective is None:
|
||||||
|
return ObjectiveWeights(
|
||||||
|
unit_length_cost=self._unit_length_cost,
|
||||||
|
bend_penalty=self._bend_penalty,
|
||||||
|
sbend_penalty=self._sbend_penalty,
|
||||||
|
danger_weight=self._danger_weight,
|
||||||
|
congestion_penalty=0.0,
|
||||||
|
)
|
||||||
|
return refinement_objective
|
||||||
|
|
||||||
def set_target(self, target: Port) -> None:
|
def set_target(self, target: Port) -> None:
|
||||||
self._target_x = target.x
|
self._target_x = target.x
|
||||||
|
|
@ -92,7 +146,7 @@ class CostEvaluator:
|
||||||
def g_proximity(self, x: float, y: float) -> float:
|
def g_proximity(self, x: float, y: float) -> float:
|
||||||
if self.danger_map is None:
|
if self.danger_map is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
return self.danger_map.get_cost(x, y)
|
return self._danger_weight * self.danger_map.get_cost(x, y)
|
||||||
|
|
||||||
def h_manhattan(self, current: Port, target: Port) -> float:
|
def h_manhattan(self, current: Port, target: Port) -> float:
|
||||||
tx, ty = target.x, target.y
|
tx, ty = target.x, target.y
|
||||||
|
|
@ -102,7 +156,7 @@ class CostEvaluator:
|
||||||
dx = abs(current.x - tx)
|
dx = abs(current.x - tx)
|
||||||
dy = abs(current.y - ty)
|
dy = abs(current.y - ty)
|
||||||
dist = dx + dy
|
dist = dx + dy
|
||||||
bp = self.config.bend_penalty
|
bp = self._bend_penalty
|
||||||
penalty = 0.0
|
penalty = 0.0
|
||||||
|
|
||||||
curr_r = current.r
|
curr_r = current.r
|
||||||
|
|
@ -132,27 +186,29 @@ class CostEvaluator:
|
||||||
if diff == 0 and perp_dist > 0:
|
if diff == 0 and perp_dist > 0:
|
||||||
penalty += 2 * bp
|
penalty += 2 * bp
|
||||||
|
|
||||||
return self.greedy_h_weight * (dist + penalty)
|
return self._greedy_h_weight * (dist + penalty)
|
||||||
|
|
||||||
def evaluate_move(
|
def evaluate_move(
|
||||||
self,
|
self,
|
||||||
geometry: list[Polygon] | None,
|
geometry: Sequence[Polygon] | None,
|
||||||
end_port: Port,
|
end_port: Port,
|
||||||
net_width: float,
|
net_width: float,
|
||||||
net_id: str,
|
net_id: str,
|
||||||
start_port: Port | None = None,
|
start_port: Port | None = None,
|
||||||
length: float = 0.0,
|
length: float = 0.0,
|
||||||
dilated_geometry: list[Polygon] | None = None,
|
dilated_geometry: Sequence[Polygon] | None = None,
|
||||||
skip_static: bool = False,
|
skip_static: bool = False,
|
||||||
skip_congestion: bool = False,
|
skip_congestion: bool = False,
|
||||||
penalty: float = 0.0,
|
penalty: float = 0.0,
|
||||||
|
weights: ObjectiveWeights | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
|
active_weights = self.objective_weights() if weights is None else weights
|
||||||
_ = net_width
|
_ = net_width
|
||||||
danger_map = self.danger_map
|
danger_map = self.danger_map
|
||||||
if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y):
|
if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||||
return 1e15
|
return 1e15
|
||||||
|
|
||||||
total_cost = length * self.unit_length_cost + penalty
|
total_cost = length * active_weights.unit_length_cost + penalty
|
||||||
if not skip_static or not skip_congestion:
|
if not skip_static or not skip_congestion:
|
||||||
if geometry is None:
|
if geometry is None:
|
||||||
return 1e15
|
return 1e15
|
||||||
|
|
@ -171,16 +227,71 @@ class CostEvaluator:
|
||||||
if not skip_congestion:
|
if not skip_congestion:
|
||||||
overlaps = collision_engine.check_collision(poly, net_id, buffer_mode="congestion", dilated_geometry=dil_poly)
|
overlaps = collision_engine.check_collision(poly, net_id, buffer_mode="congestion", dilated_geometry=dil_poly)
|
||||||
if isinstance(overlaps, int) and overlaps > 0:
|
if isinstance(overlaps, int) and overlaps > 0:
|
||||||
total_cost += overlaps * self.congestion_penalty
|
total_cost += overlaps * active_weights.congestion_penalty
|
||||||
|
|
||||||
if danger_map is not None:
|
if danger_map is not None and active_weights.danger_weight:
|
||||||
cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0
|
cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0
|
||||||
cost_e = danger_map.get_cost(end_port.x, end_port.y)
|
cost_e = danger_map.get_cost(end_port.x, end_port.y)
|
||||||
if start_port:
|
if start_port:
|
||||||
mid_x = (start_port.x + end_port.x) / 2.0
|
mid_x = (start_port.x + end_port.x) / 2.0
|
||||||
mid_y = (start_port.y + end_port.y) / 2.0
|
mid_y = (start_port.y + end_port.y) / 2.0
|
||||||
cost_m = danger_map.get_cost(mid_x, mid_y)
|
cost_m = danger_map.get_cost(mid_x, mid_y)
|
||||||
total_cost += length * (cost_s + cost_m + cost_e) / 3.0
|
total_cost += length * active_weights.danger_weight * (cost_s + cost_m + cost_e) / 3.0
|
||||||
else:
|
else:
|
||||||
total_cost += length * cost_e
|
total_cost += length * active_weights.danger_weight * cost_e
|
||||||
return total_cost
|
return total_cost
|
||||||
|
|
||||||
|
def component_penalty(
|
||||||
|
self,
|
||||||
|
move_type: MoveKind,
|
||||||
|
*,
|
||||||
|
move_radius: float | None = None,
|
||||||
|
weights: ObjectiveWeights | None = None,
|
||||||
|
) -> float:
|
||||||
|
active_weights = self.objective_weights() if weights is None else weights
|
||||||
|
penalty = 0.0
|
||||||
|
if move_type == "sbend":
|
||||||
|
penalty = active_weights.sbend_penalty
|
||||||
|
elif move_type == "bend90":
|
||||||
|
penalty = active_weights.bend_penalty
|
||||||
|
if move_radius is not None and move_radius > TOLERANCE_LINEAR and penalty > 0:
|
||||||
|
penalty *= (10.0 / move_radius) ** 0.5
|
||||||
|
return penalty
|
||||||
|
|
||||||
|
def path_cost(
|
||||||
|
self,
|
||||||
|
net_id: str,
|
||||||
|
start_port: Port,
|
||||||
|
path: list[ComponentResult],
|
||||||
|
*,
|
||||||
|
weights: ObjectiveWeights | None = None,
|
||||||
|
) -> float:
|
||||||
|
active_weights = self.objective_weights() if weights is None else weights
|
||||||
|
total = 0.0
|
||||||
|
current_port = start_port
|
||||||
|
for component in path:
|
||||||
|
move_radius = None
|
||||||
|
if component.move_type == "bend90":
|
||||||
|
move_radius = component.length * 2.0 / np.pi if component.length > 0 else None
|
||||||
|
elif component.move_type == "sbend":
|
||||||
|
move_radius = None
|
||||||
|
penalty = self.component_penalty(
|
||||||
|
component.move_type,
|
||||||
|
move_radius=move_radius,
|
||||||
|
weights=active_weights,
|
||||||
|
)
|
||||||
|
total += self.evaluate_move(
|
||||||
|
component.collision_geometry,
|
||||||
|
component.end_port,
|
||||||
|
net_width=0.0,
|
||||||
|
net_id=net_id,
|
||||||
|
start_port=current_port,
|
||||||
|
length=component.length,
|
||||||
|
dilated_geometry=component.dilated_collision_geometry,
|
||||||
|
skip_static=True,
|
||||||
|
skip_congestion=(active_weights.congestion_penalty <= 0.0),
|
||||||
|
penalty=penalty,
|
||||||
|
weights=active_weights,
|
||||||
|
)
|
||||||
|
current_port = component.end_port
|
||||||
|
return total
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import shapely
|
|
||||||
from scipy.spatial import cKDTree
|
from scipy.spatial import cKDTree
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
|
||||||
|
_COST_CACHE_SIZE = 100000
|
||||||
|
|
||||||
|
|
||||||
class DangerMap:
|
class DangerMap:
|
||||||
"""
|
"""
|
||||||
A proximity cost evaluator using a KD-Tree of obstacle boundary points.
|
A proximity cost evaluator using a KD-Tree of obstacle boundary points.
|
||||||
Scales with obstacle perimeter rather than design area.
|
Scales with obstacle perimeter rather than design area.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree')
|
__slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -38,6 +41,7 @@ class DangerMap:
|
||||||
self.safety_threshold = safety_threshold
|
self.safety_threshold = safety_threshold
|
||||||
self.k = k
|
self.k = k
|
||||||
self.tree: cKDTree | None = None
|
self.tree: cKDTree | None = None
|
||||||
|
self._cost_cache: OrderedDict[tuple[int, int], float] = OrderedDict()
|
||||||
|
|
||||||
def precompute(self, obstacles: list[Polygon]) -> None:
|
def precompute(self, obstacles: list[Polygon]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -65,8 +69,7 @@ class DangerMap:
|
||||||
else:
|
else:
|
||||||
self.tree = None
|
self.tree = None
|
||||||
|
|
||||||
# Clear cache when tree changes
|
self._cost_cache.clear()
|
||||||
self._get_cost_quantized.cache_clear()
|
|
||||||
|
|
||||||
def is_within_bounds(self, x: float, y: float) -> bool:
|
def is_within_bounds(self, x: float, y: float) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -81,10 +84,18 @@ class DangerMap:
|
||||||
"""
|
"""
|
||||||
qx_milli = int(round(x * 1000))
|
qx_milli = int(round(x * 1000))
|
||||||
qy_milli = int(round(y * 1000))
|
qy_milli = int(round(y * 1000))
|
||||||
return self._get_cost_quantized(qx_milli, qy_milli)
|
key = (qx_milli, qy_milli)
|
||||||
|
if key in self._cost_cache:
|
||||||
|
self._cost_cache.move_to_end(key)
|
||||||
|
return self._cost_cache[key]
|
||||||
|
|
||||||
@lru_cache(maxsize=100000)
|
cost = self._compute_cost_quantized(qx_milli, qy_milli)
|
||||||
def _get_cost_quantized(self, qx_milli: int, qy_milli: int) -> float:
|
self._cost_cache[key] = cost
|
||||||
|
if len(self._cost_cache) > _COST_CACHE_SIZE:
|
||||||
|
self._cost_cache.popitem(last=False)
|
||||||
|
return cost
|
||||||
|
|
||||||
|
def _compute_cost_quantized(self, qx_milli: int, qy_milli: int) -> float:
|
||||||
qx = qx_milli / 1000.0
|
qx = qx_milli / 1000.0
|
||||||
qy = qy_milli / 1000.0
|
qy = qy_milli / 1000.0
|
||||||
if not self.is_within_bounds(qx, qy):
|
if not self.is_within_bounds(qx, qy):
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from inire.geometry.collision import CollisionEngine, PathVerificationReport
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
|
|
||||||
|
|
||||||
class PathStateManager:
|
|
||||||
__slots__ = ("collision_engine",)
|
|
||||||
|
|
||||||
def __init__(self, collision_engine: CollisionEngine) -> None:
|
|
||||||
self.collision_engine = collision_engine
|
|
||||||
|
|
||||||
def extract_geometry(self, path: list[ComponentResult]) -> tuple[list[Any], list[Any]]:
|
|
||||||
all_geoms = []
|
|
||||||
all_dilated = []
|
|
||||||
for res in path:
|
|
||||||
all_geoms.extend(res.geometry)
|
|
||||||
if res.dilated_geometry:
|
|
||||||
all_dilated.extend(res.dilated_geometry)
|
|
||||||
else:
|
|
||||||
dilation = self.collision_engine.clearance / 2.0
|
|
||||||
all_dilated.extend([poly.buffer(dilation) for poly in res.geometry])
|
|
||||||
return all_geoms, all_dilated
|
|
||||||
|
|
||||||
def install_path(self, net_id: str, path: list[ComponentResult]) -> None:
|
|
||||||
all_geoms, all_dilated = self.extract_geometry(path)
|
|
||||||
self.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
|
||||||
|
|
||||||
def stage_path_as_static(self, path: list[ComponentResult]) -> list[int]:
|
|
||||||
obj_ids: list[int] = []
|
|
||||||
for res in path:
|
|
||||||
geoms = res.actual_geometry if res.actual_geometry is not None else res.geometry
|
|
||||||
dilated_geoms = res.dilated_actual_geometry if res.dilated_actual_geometry else res.dilated_geometry
|
|
||||||
for index, poly in enumerate(geoms):
|
|
||||||
dilated = dilated_geoms[index] if dilated_geoms else None
|
|
||||||
obj_ids.append(self.collision_engine.add_static_obstacle(poly, dilated_geometry=dilated))
|
|
||||||
return obj_ids
|
|
||||||
|
|
||||||
def remove_static_obstacles(self, obj_ids: list[int]) -> None:
|
|
||||||
for obj_id in obj_ids:
|
|
||||||
self.collision_engine.remove_static_obstacle(obj_id)
|
|
||||||
|
|
||||||
def remove_path(self, net_id: str) -> None:
|
|
||||||
self.collision_engine.remove_path(net_id)
|
|
||||||
|
|
||||||
def verify_path(self, net_id: str, path: list[ComponentResult]) -> tuple[bool, int]:
|
|
||||||
return self.collision_engine.verify_path(net_id, path)
|
|
||||||
|
|
||||||
def verify_path_report(self, net_id: str, path: list[ComponentResult]) -> PathVerificationReport:
|
|
||||||
return self.collision_engine.verify_path_report(net_id, path)
|
|
||||||
|
|
||||||
def finalize_dynamic_tree(self) -> None:
|
|
||||||
self.collision_engine.rebuild_dynamic_tree()
|
|
||||||
|
|
@ -1,310 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
|
||||||
|
|
||||||
from inire.router.astar import AStarMetrics, route_astar
|
|
||||||
from inire.router.outcomes import RoutingOutcome, infer_routing_outcome, routing_outcome_needs_retry
|
|
||||||
from inire.router.refiner import PathRefiner
|
|
||||||
from inire.router.path_state import PathStateManager
|
|
||||||
from inire.router.session import (
|
|
||||||
create_routing_session_state,
|
|
||||||
finalize_routing_session_results,
|
|
||||||
prepare_routing_session_state,
|
|
||||||
refine_routing_session_results,
|
|
||||||
run_routing_iteration,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.astar import AStarContext
|
|
||||||
from inire.router.cost import CostEvaluator
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RoutingResult:
|
|
||||||
net_id: str
|
|
||||||
path: list[ComponentResult]
|
|
||||||
is_valid: bool
|
|
||||||
collisions: int
|
|
||||||
reached_target: bool = False
|
|
||||||
outcome: RoutingOutcome = "unroutable"
|
|
||||||
|
|
||||||
class PathFinder:
|
|
||||||
__slots__ = (
|
|
||||||
"context",
|
|
||||||
"metrics",
|
|
||||||
"max_iterations",
|
|
||||||
"base_congestion_penalty",
|
|
||||||
"use_tiered_strategy",
|
|
||||||
"congestion_multiplier",
|
|
||||||
"accumulated_expanded_nodes",
|
|
||||||
"warm_start",
|
|
||||||
"refine_paths",
|
|
||||||
"refiner",
|
|
||||||
"path_state",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: AStarMetrics | None = None,
|
|
||||||
max_iterations: int = 10,
|
|
||||||
base_congestion_penalty: float = 100.0,
|
|
||||||
congestion_multiplier: float = 1.5,
|
|
||||||
use_tiered_strategy: bool = True,
|
|
||||||
warm_start: Literal["shortest", "longest", "user"] | None = "shortest",
|
|
||||||
refine_paths: bool = True,
|
|
||||||
) -> None:
|
|
||||||
self.context = context
|
|
||||||
self.metrics = metrics if metrics is not None else AStarMetrics()
|
|
||||||
self.max_iterations = max_iterations
|
|
||||||
self.base_congestion_penalty = base_congestion_penalty
|
|
||||||
self.congestion_multiplier = congestion_multiplier
|
|
||||||
self.use_tiered_strategy = use_tiered_strategy
|
|
||||||
self.warm_start = warm_start
|
|
||||||
self.refine_paths = refine_paths
|
|
||||||
self.refiner = PathRefiner(context)
|
|
||||||
self.path_state = PathStateManager(context.cost_evaluator.collision_engine)
|
|
||||||
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cost_evaluator(self) -> CostEvaluator:
|
|
||||||
return self.context.cost_evaluator
|
|
||||||
|
|
||||||
def _build_greedy_warm_start_paths(
|
|
||||||
self,
|
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
|
||||||
net_widths: dict[str, float],
|
|
||||||
order: Literal["shortest", "longest", "user"],
|
|
||||||
) -> dict[str, list[ComponentResult]]:
|
|
||||||
all_net_ids = list(netlist.keys())
|
|
||||||
if order != "user":
|
|
||||||
all_net_ids.sort(
|
|
||||||
key=lambda nid: abs(netlist[nid][1].x - netlist[nid][0].x) + abs(netlist[nid][1].y - netlist[nid][0].y),
|
|
||||||
reverse=(order == "longest"),
|
|
||||||
)
|
|
||||||
|
|
||||||
greedy_paths: dict[str, list[ComponentResult]] = {}
|
|
||||||
temp_obj_ids: list[int] = []
|
|
||||||
greedy_node_limit = min(self.context.config.node_limit, 2000)
|
|
||||||
for net_id in all_net_ids:
|
|
||||||
start, target = netlist[net_id]
|
|
||||||
width = net_widths.get(net_id, 2.0)
|
|
||||||
h_start = self.cost_evaluator.h_manhattan(start, target)
|
|
||||||
max_cost_limit = max(h_start * 3.0, 2000.0)
|
|
||||||
path = route_astar(
|
|
||||||
start,
|
|
||||||
target,
|
|
||||||
width,
|
|
||||||
context=self.context,
|
|
||||||
metrics=self.metrics,
|
|
||||||
net_id=net_id,
|
|
||||||
skip_congestion=True,
|
|
||||||
max_cost=max_cost_limit,
|
|
||||||
self_collision_check=True,
|
|
||||||
node_limit=greedy_node_limit,
|
|
||||||
)
|
|
||||||
if not path:
|
|
||||||
continue
|
|
||||||
greedy_paths[net_id] = path
|
|
||||||
temp_obj_ids.extend(self.path_state.stage_path_as_static(path))
|
|
||||||
self.context.clear_static_caches()
|
|
||||||
|
|
||||||
self.path_state.remove_static_obstacles(temp_obj_ids)
|
|
||||||
return greedy_paths
|
|
||||||
|
|
||||||
def _path_cost(self, path: list[ComponentResult]) -> float:
|
|
||||||
return self.refiner.path_cost(path)
|
|
||||||
|
|
||||||
def _install_path(self, net_id: str, path: list[ComponentResult]) -> None:
|
|
||||||
self.path_state.install_path(net_id, path)
|
|
||||||
|
|
||||||
def _build_routing_result(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
net_id: str,
|
|
||||||
path: list[ComponentResult],
|
|
||||||
reached_target: bool,
|
|
||||||
collisions: int,
|
|
||||||
outcome: RoutingOutcome | None = None,
|
|
||||||
) -> RoutingResult:
|
|
||||||
resolved_outcome = (
|
|
||||||
infer_routing_outcome(
|
|
||||||
has_path=bool(path),
|
|
||||||
reached_target=reached_target,
|
|
||||||
collision_count=collisions,
|
|
||||||
)
|
|
||||||
if outcome is None
|
|
||||||
else outcome
|
|
||||||
)
|
|
||||||
return RoutingResult(
|
|
||||||
net_id=net_id,
|
|
||||||
path=path,
|
|
||||||
is_valid=(resolved_outcome == "completed"),
|
|
||||||
collisions=collisions,
|
|
||||||
reached_target=reached_target,
|
|
||||||
outcome=resolved_outcome,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _refine_path(
|
|
||||||
self,
|
|
||||||
net_id: str,
|
|
||||||
start: Port,
|
|
||||||
target: Port,
|
|
||||||
net_width: float,
|
|
||||||
path: list[ComponentResult],
|
|
||||||
) -> list[ComponentResult]:
|
|
||||||
return self.refiner.refine_path(net_id, start, target, net_width, path)
|
|
||||||
|
|
||||||
def _route_net_once(
|
|
||||||
self,
|
|
||||||
net_id: str,
|
|
||||||
start: Port,
|
|
||||||
target: Port,
|
|
||||||
width: float,
|
|
||||||
iteration: int,
|
|
||||||
initial_paths: dict[str, list[ComponentResult]] | None,
|
|
||||||
store_expanded: bool,
|
|
||||||
needs_self_collision_check: set[str],
|
|
||||||
) -> tuple[RoutingResult, RoutingOutcome]:
|
|
||||||
self.path_state.remove_path(net_id)
|
|
||||||
path: list[ComponentResult] | None = None
|
|
||||||
|
|
||||||
if iteration == 0 and initial_paths and net_id in initial_paths:
|
|
||||||
path = initial_paths[net_id]
|
|
||||||
else:
|
|
||||||
target_coll_model = self.context.config.bend_collision_type
|
|
||||||
coll_model = target_coll_model
|
|
||||||
skip_cong = False
|
|
||||||
if self.use_tiered_strategy and iteration == 0:
|
|
||||||
skip_cong = True
|
|
||||||
if target_coll_model == "arc":
|
|
||||||
coll_model = "clipped_bbox"
|
|
||||||
|
|
||||||
path = route_astar(
|
|
||||||
start,
|
|
||||||
target,
|
|
||||||
width,
|
|
||||||
context=self.context,
|
|
||||||
metrics=self.metrics,
|
|
||||||
net_id=net_id,
|
|
||||||
bend_collision_type=coll_model,
|
|
||||||
return_partial=True,
|
|
||||||
store_expanded=store_expanded,
|
|
||||||
skip_congestion=skip_cong,
|
|
||||||
self_collision_check=(net_id in needs_self_collision_check),
|
|
||||||
node_limit=self.context.config.node_limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
if store_expanded and self.metrics.last_expanded_nodes:
|
|
||||||
self.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
|
|
||||||
|
|
||||||
if not path:
|
|
||||||
outcome = infer_routing_outcome(has_path=False, reached_target=False, collision_count=0)
|
|
||||||
return self._build_routing_result(net_id=net_id, path=[], reached_target=False, collisions=0, outcome=outcome), outcome
|
|
||||||
|
|
||||||
last_p = path[-1].end_port
|
|
||||||
reached = last_p == target
|
|
||||||
collision_count = 0
|
|
||||||
|
|
||||||
self._install_path(net_id, path)
|
|
||||||
if reached:
|
|
||||||
report = self.path_state.verify_path_report(net_id, path)
|
|
||||||
collision_count = report.collision_count
|
|
||||||
if report.self_collision_count > 0:
|
|
||||||
needs_self_collision_check.add(net_id)
|
|
||||||
|
|
||||||
outcome = infer_routing_outcome(
|
|
||||||
has_path=bool(path),
|
|
||||||
reached_target=reached,
|
|
||||||
collision_count=collision_count,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
self._build_routing_result(
|
|
||||||
net_id=net_id,
|
|
||||||
path=path,
|
|
||||||
reached_target=reached,
|
|
||||||
collisions=collision_count,
|
|
||||||
outcome=outcome,
|
|
||||||
),
|
|
||||||
outcome,
|
|
||||||
)
|
|
||||||
|
|
||||||
def route_all(
|
|
||||||
self,
|
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
|
||||||
net_widths: dict[str, float],
|
|
||||||
store_expanded: bool = False,
|
|
||||||
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
|
|
||||||
shuffle_nets: bool = False,
|
|
||||||
sort_nets: Literal["shortest", "longest", "user", None] = None,
|
|
||||||
initial_paths: dict[str, list[ComponentResult]] | None = None,
|
|
||||||
seed: int | None = None,
|
|
||||||
) -> dict[str, RoutingResult]:
|
|
||||||
self.cost_evaluator.congestion_penalty = self.base_congestion_penalty
|
|
||||||
self.accumulated_expanded_nodes = []
|
|
||||||
self.metrics.reset_per_route()
|
|
||||||
|
|
||||||
state = create_routing_session_state(
|
|
||||||
self,
|
|
||||||
netlist,
|
|
||||||
net_widths,
|
|
||||||
store_expanded=store_expanded,
|
|
||||||
iteration_callback=iteration_callback,
|
|
||||||
shuffle_nets=shuffle_nets,
|
|
||||||
sort_nets=sort_nets,
|
|
||||||
initial_paths=initial_paths,
|
|
||||||
seed=seed,
|
|
||||||
)
|
|
||||||
prepare_routing_session_state(self, state)
|
|
||||||
|
|
||||||
for iteration in range(self.max_iterations):
|
|
||||||
iteration_outcomes = run_routing_iteration(self, state, iteration)
|
|
||||||
if iteration_outcomes is None:
|
|
||||||
return self.verify_all_nets(state.results, state.netlist)
|
|
||||||
if not any(routing_outcome_needs_retry(outcome) for outcome in iteration_outcomes.values()):
|
|
||||||
break
|
|
||||||
self.cost_evaluator.congestion_penalty *= self.congestion_multiplier
|
|
||||||
|
|
||||||
refine_routing_session_results(self, state)
|
|
||||||
return finalize_routing_session_results(self, state)
|
|
||||||
|
|
||||||
def verify_all_nets(
|
|
||||||
self,
|
|
||||||
results: dict[str, RoutingResult],
|
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
|
||||||
) -> dict[str, RoutingResult]:
|
|
||||||
final_results: dict[str, RoutingResult] = {}
|
|
||||||
for net_id, (_, target_p) in netlist.items():
|
|
||||||
res = results.get(net_id)
|
|
||||||
if not res or not res.path:
|
|
||||||
final_results[net_id] = self._build_routing_result(
|
|
||||||
net_id=net_id,
|
|
||||||
path=[],
|
|
||||||
reached_target=False,
|
|
||||||
collisions=0,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
last_p = res.path[-1].end_port
|
|
||||||
reached = last_p == target_p
|
|
||||||
report = self.path_state.verify_path_report(net_id, res.path)
|
|
||||||
final_results[net_id] = RoutingResult(
|
|
||||||
net_id=net_id,
|
|
||||||
path=res.path,
|
|
||||||
is_valid=(reached and report.is_valid),
|
|
||||||
collisions=report.collision_count,
|
|
||||||
reached_target=reached,
|
|
||||||
outcome=infer_routing_outcome(
|
|
||||||
has_path=True,
|
|
||||||
reached_target=reached,
|
|
||||||
collision_count=report.collision_count,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return final_results
|
|
||||||
|
|
@ -7,10 +7,12 @@ from inire.geometry.component_overlap import components_overlap, has_self_overla
|
||||||
from inire.geometry.components import Bend90, Straight
|
from inire.geometry.components import Bend90, Straight
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from inire.geometry.collision import CollisionEngine
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.components import ComponentResult
|
from inire.geometry.components import ComponentResult
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext
|
from inire.router._search import AStarContext
|
||||||
|
|
||||||
def component_hits_ancestor_chain(component: ComponentResult, parent_node: Any) -> bool:
|
def component_hits_ancestor_chain(component: ComponentResult, parent_node: Any) -> bool:
|
||||||
current = parent_node
|
current = parent_node
|
||||||
|
|
@ -22,7 +24,7 @@ def component_hits_ancestor_chain(component: ComponentResult, parent_node: Any)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def has_self_collision(path: list[ComponentResult]) -> bool:
|
def has_self_collision(path: Sequence[ComponentResult]) -> bool:
|
||||||
return has_self_overlap(path)
|
return has_self_overlap(path)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,26 +35,26 @@ class PathRefiner:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collision_engine(self) -> CollisionEngine:
|
def collision_engine(self) -> RoutingWorld:
|
||||||
return self.context.cost_evaluator.collision_engine
|
return self.context.cost_evaluator.collision_engine
|
||||||
|
|
||||||
def path_cost(self, path: list[ComponentResult]) -> float:
|
def path_cost(
|
||||||
total = 0.0
|
self,
|
||||||
bend_penalty = self.context.config.bend_penalty
|
path: Sequence[ComponentResult],
|
||||||
sbend_penalty = self.context.config.sbend_penalty
|
*,
|
||||||
for comp in path:
|
net_id: str = "default",
|
||||||
total += comp.length
|
start: Port | None = None,
|
||||||
if comp.move_type == "Bend90":
|
) -> float:
|
||||||
radius = comp.length * 2.0 / math.pi if comp.length > 0 else 0.0
|
if not path:
|
||||||
if radius > 0:
|
return 0.0
|
||||||
total += bend_penalty * (10.0 / radius) ** 0.5
|
actual_start = path[0].start_port if start is None else start
|
||||||
else:
|
return self.score_path(net_id, actual_start, path)
|
||||||
total += bend_penalty
|
|
||||||
elif comp.move_type == "SBend":
|
|
||||||
total += sbend_penalty
|
|
||||||
return total
|
|
||||||
|
|
||||||
def _path_ports(self, start: Port, path: list[ComponentResult]) -> list[Port]:
|
def score_path(self, net_id: str, start: Port, path: Sequence[ComponentResult]) -> float:
|
||||||
|
weights = self.context.cost_evaluator.resolve_refiner_weights(self.context.options)
|
||||||
|
return self.context.cost_evaluator.path_cost(net_id, start, path, weights=weights)
|
||||||
|
|
||||||
|
def _path_ports(self, start: Port, path: Sequence[ComponentResult]) -> list[Port]:
|
||||||
ports = [start]
|
ports = [start]
|
||||||
ports.extend(comp.end_port for comp in path)
|
ports.extend(comp.end_port for comp in path)
|
||||||
return ports
|
return ports
|
||||||
|
|
@ -79,7 +81,7 @@ class PathRefiner:
|
||||||
return -dx, -dy
|
return -dx, -dy
|
||||||
return -dy, dx
|
return -dy, dx
|
||||||
|
|
||||||
def _window_query_bounds(self, start: Port, target: Port, path: list[ComponentResult], pad: float) -> tuple[float, float, float, float]:
|
def _window_query_bounds(self, start: Port, target: Port, path: Sequence[ComponentResult], pad: float) -> tuple[float, float, float, float]:
|
||||||
min_x = float(min(start.x, target.x))
|
min_x = float(min(start.x, target.x))
|
||||||
min_y = float(min(start.y, target.y))
|
min_y = float(min(start.y, target.y))
|
||||||
max_x = float(max(start.x, target.x))
|
max_x = float(max(start.x, target.x))
|
||||||
|
|
@ -96,7 +98,7 @@ class PathRefiner:
|
||||||
self,
|
self,
|
||||||
start: Port,
|
start: Port,
|
||||||
target: Port,
|
target: Port,
|
||||||
window_path: list[ComponentResult],
|
window_path: Sequence[ComponentResult],
|
||||||
net_width: float,
|
net_width: float,
|
||||||
radius: float,
|
radius: float,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
|
|
@ -187,7 +189,7 @@ class PathRefiner:
|
||||||
second_straight = side_abs - 2.0 * radius - math.copysign(float(local_dy), side_extent)
|
second_straight = side_abs - 2.0 * radius - math.copysign(float(local_dy), side_extent)
|
||||||
if first_straight < -0.01 or second_straight < -0.01:
|
if first_straight < -0.01 or second_straight < -0.01:
|
||||||
return None
|
return None
|
||||||
min_straight = self.context.config.min_straight_length
|
min_straight = self.context.options.search.min_straight_length
|
||||||
if 0.01 < first_straight < min_straight - 0.01:
|
if 0.01 < first_straight < min_straight - 0.01:
|
||||||
return None
|
return None
|
||||||
if 0.01 < second_straight < min_straight - 0.01:
|
if 0.01 < second_straight < min_straight - 0.01:
|
||||||
|
|
@ -226,16 +228,16 @@ class PathRefiner:
|
||||||
return None
|
return None
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _iter_refinement_windows(self, start: Port, path: list[ComponentResult]) -> list[tuple[int, int]]:
|
def _iter_refinement_windows(self, start: Port, path: Sequence[ComponentResult]) -> list[tuple[int, int]]:
|
||||||
ports = self._path_ports(start, path)
|
ports = self._path_ports(start, path)
|
||||||
windows: list[tuple[int, int]] = []
|
windows: list[tuple[int, int]] = []
|
||||||
min_radius = min(self.context.config.bend_radii, default=0.0)
|
min_radius = min(self.context.options.search.bend_radii, default=0.0)
|
||||||
|
|
||||||
for window_size in range(len(path), 0, -1):
|
for window_size in range(len(path), 0, -1):
|
||||||
for start_idx in range(len(path) - window_size + 1):
|
for start_idx in range(len(path) - window_size + 1):
|
||||||
end_idx = start_idx + window_size
|
end_idx = start_idx + window_size
|
||||||
window = path[start_idx:end_idx]
|
window = path[start_idx:end_idx]
|
||||||
bend_count = sum(1 for comp in window if comp.move_type == "Bend90")
|
bend_count = sum(1 for comp in window if comp.move_type == "bend90")
|
||||||
if bend_count < 4:
|
if bend_count < 4:
|
||||||
continue
|
continue
|
||||||
window_start = ports[start_idx]
|
window_start = ports[start_idx]
|
||||||
|
|
@ -266,7 +268,7 @@ class PathRefiner:
|
||||||
best_path: list[ComponentResult] | None = None
|
best_path: list[ComponentResult] | None = None
|
||||||
best_candidate_cost = best_cost
|
best_candidate_cost = best_cost
|
||||||
|
|
||||||
for radius in self.context.config.bend_radii:
|
for radius in self.context.options.search.bend_radii:
|
||||||
side_extents = self._candidate_side_extents(window_start, window_end, window_path, net_width, radius)
|
side_extents = self._candidate_side_extents(window_start, window_end, window_path, net_width, radius)
|
||||||
for side_extent in side_extents:
|
for side_extent in side_extents:
|
||||||
replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent)
|
replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent)
|
||||||
|
|
@ -297,12 +299,14 @@ class PathRefiner:
|
||||||
if not path:
|
if not path:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
bend_count = sum(1 for comp in path if comp.move_type == "Bend90")
|
path = list(path)
|
||||||
|
|
||||||
|
bend_count = sum(1 for comp in path if comp.move_type == "bend90")
|
||||||
if bend_count < 4:
|
if bend_count < 4:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
best_path = path
|
best_path = path
|
||||||
best_cost = self.path_cost(path)
|
best_cost = self.score_path(net_id, start, path)
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
improved = False
|
improved = False
|
||||||
|
|
|
||||||
68
inire/router/results.py
Normal file
68
inire/router/results.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.router.outcomes import RoutingOutcome, infer_routing_outcome
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.model import LockedRoute
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RoutingReport:
|
||||||
|
static_collision_count: int = 0
|
||||||
|
dynamic_collision_count: int = 0
|
||||||
|
self_collision_count: int = 0
|
||||||
|
total_length: float = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collision_count(self) -> int:
|
||||||
|
return self.static_collision_count + self.dynamic_collision_count + self.self_collision_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return self.collision_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RouteMetrics:
|
||||||
|
nodes_expanded: int
|
||||||
|
moves_generated: int
|
||||||
|
moves_added: int
|
||||||
|
pruned_closed_set: int
|
||||||
|
pruned_hard_collision: int
|
||||||
|
pruned_cost: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RoutingResult:
|
||||||
|
net_id: str
|
||||||
|
path: tuple[ComponentResult, ...]
|
||||||
|
reached_target: bool = False
|
||||||
|
report: RoutingReport = field(default_factory=RoutingReport)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "path", tuple(self.path))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collisions(self) -> int:
|
||||||
|
return self.report.collision_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outcome(self) -> RoutingOutcome:
|
||||||
|
return infer_routing_outcome(
|
||||||
|
has_path=bool(self.path),
|
||||||
|
reached_target=self.reached_target,
|
||||||
|
collision_count=self.report.collision_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return self.outcome == "completed"
|
||||||
|
|
||||||
|
def as_locked_route(self) -> LockedRoute:
|
||||||
|
from inire.model import LockedRoute
|
||||||
|
|
||||||
|
return LockedRoute.from_path(self.path)
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
|
||||||
|
|
||||||
from inire.router.outcomes import RoutingOutcome, routing_outcome_needs_retry
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
from inire.geometry.components import ComponentResult
|
|
||||||
from inire.geometry.primitives import Port
|
|
||||||
from inire.router.pathfinder import PathFinder, RoutingResult
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RoutingSessionState:
|
|
||||||
netlist: dict[str, tuple[Port, Port]]
|
|
||||||
net_widths: dict[str, float]
|
|
||||||
results: dict[str, RoutingResult]
|
|
||||||
all_net_ids: list[str]
|
|
||||||
needs_self_collision_check: set[str]
|
|
||||||
start_time: float
|
|
||||||
session_timeout: float
|
|
||||||
initial_paths: dict[str, list[ComponentResult]] | None
|
|
||||||
store_expanded: bool
|
|
||||||
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None
|
|
||||||
shuffle_nets: bool
|
|
||||||
sort_nets: Literal["shortest", "longest", "user", None]
|
|
||||||
seed: int | None
|
|
||||||
|
|
||||||
|
|
||||||
def create_routing_session_state(
|
|
||||||
finder: PathFinder,
|
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
|
||||||
net_widths: dict[str, float],
|
|
||||||
*,
|
|
||||||
store_expanded: bool,
|
|
||||||
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
|
||||||
shuffle_nets: bool,
|
|
||||||
sort_nets: Literal["shortest", "longest", "user", None],
|
|
||||||
initial_paths: dict[str, list[ComponentResult]] | None,
|
|
||||||
seed: int | None,
|
|
||||||
) -> RoutingSessionState:
|
|
||||||
num_nets = len(netlist)
|
|
||||||
return RoutingSessionState(
|
|
||||||
netlist=netlist,
|
|
||||||
net_widths=net_widths,
|
|
||||||
results={},
|
|
||||||
all_net_ids=list(netlist.keys()),
|
|
||||||
needs_self_collision_check=set(),
|
|
||||||
start_time=time.monotonic(),
|
|
||||||
session_timeout=max(60.0, 10.0 * num_nets * finder.max_iterations),
|
|
||||||
initial_paths=initial_paths,
|
|
||||||
store_expanded=store_expanded,
|
|
||||||
iteration_callback=iteration_callback,
|
|
||||||
shuffle_nets=shuffle_nets,
|
|
||||||
sort_nets=sort_nets,
|
|
||||||
seed=seed,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_routing_session_state(
|
|
||||||
finder: PathFinder,
|
|
||||||
state: RoutingSessionState,
|
|
||||||
) -> None:
|
|
||||||
if state.initial_paths is None:
|
|
||||||
warm_start_order = state.sort_nets if state.sort_nets is not None else finder.warm_start
|
|
||||||
if warm_start_order is not None:
|
|
||||||
state.initial_paths = finder._build_greedy_warm_start_paths(state.netlist, state.net_widths, warm_start_order)
|
|
||||||
finder.context.clear_static_caches()
|
|
||||||
|
|
||||||
if state.sort_nets and state.sort_nets != "user":
|
|
||||||
state.all_net_ids.sort(
|
|
||||||
key=lambda net_id: abs(state.netlist[net_id][1].x - state.netlist[net_id][0].x)
|
|
||||||
+ abs(state.netlist[net_id][1].y - state.netlist[net_id][0].y),
|
|
||||||
reverse=(state.sort_nets == "longest"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_routing_iteration(
|
|
||||||
finder: PathFinder,
|
|
||||||
state: RoutingSessionState,
|
|
||||||
iteration: int,
|
|
||||||
) -> dict[str, RoutingOutcome] | None:
|
|
||||||
outcomes: dict[str, RoutingOutcome] = {}
|
|
||||||
finder.accumulated_expanded_nodes = []
|
|
||||||
finder.metrics.reset_per_route()
|
|
||||||
|
|
||||||
if state.shuffle_nets and (iteration > 0 or state.initial_paths is None):
|
|
||||||
iteration_seed = (state.seed + iteration) if state.seed is not None else None
|
|
||||||
random.Random(iteration_seed).shuffle(state.all_net_ids)
|
|
||||||
|
|
||||||
for net_id in state.all_net_ids:
|
|
||||||
start, target = state.netlist[net_id]
|
|
||||||
if time.monotonic() - state.start_time > state.session_timeout:
|
|
||||||
finder.path_state.finalize_dynamic_tree()
|
|
||||||
return None
|
|
||||||
|
|
||||||
width = state.net_widths.get(net_id, 2.0)
|
|
||||||
result, net_congestion = finder._route_net_once(
|
|
||||||
net_id,
|
|
||||||
start,
|
|
||||||
target,
|
|
||||||
width,
|
|
||||||
iteration,
|
|
||||||
state.initial_paths,
|
|
||||||
state.store_expanded,
|
|
||||||
state.needs_self_collision_check,
|
|
||||||
)
|
|
||||||
state.results[net_id] = result
|
|
||||||
outcomes[net_id] = net_congestion
|
|
||||||
|
|
||||||
if state.iteration_callback:
|
|
||||||
state.iteration_callback(iteration, state.results)
|
|
||||||
return outcomes
|
|
||||||
|
|
||||||
|
|
||||||
def refine_routing_session_results(
|
|
||||||
finder: PathFinder,
|
|
||||||
state: RoutingSessionState,
|
|
||||||
) -> None:
|
|
||||||
if not finder.refine_paths or not state.results:
|
|
||||||
return
|
|
||||||
|
|
||||||
for net_id in state.all_net_ids:
|
|
||||||
res = state.results.get(net_id)
|
|
||||||
if not res or not res.path or routing_outcome_needs_retry(res.outcome):
|
|
||||||
continue
|
|
||||||
start, target = state.netlist[net_id]
|
|
||||||
width = state.net_widths.get(net_id, 2.0)
|
|
||||||
finder.path_state.remove_path(net_id)
|
|
||||||
refined_path = finder._refine_path(net_id, start, target, width, res.path)
|
|
||||||
finder._install_path(net_id, refined_path)
|
|
||||||
report = finder.path_state.verify_path_report(net_id, refined_path)
|
|
||||||
state.results[net_id] = finder._build_routing_result(
|
|
||||||
net_id=net_id,
|
|
||||||
path=refined_path,
|
|
||||||
reached_target=res.reached_target,
|
|
||||||
collisions=report.collision_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def finalize_routing_session_results(
|
|
||||||
finder: PathFinder,
|
|
||||||
state: RoutingSessionState,
|
|
||||||
) -> dict[str, RoutingResult]:
|
|
||||||
finder.path_state.finalize_dynamic_tree()
|
|
||||||
return finder.verify_all_nets(state.results, state.netlist)
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
||||||
import rtree
|
import rtree
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
|
|
@ -18,7 +18,7 @@ class VisibilityManager:
|
||||||
"""
|
"""
|
||||||
__slots__ = ("collision_engine", "corners", "corner_index", "_corner_graph", "_point_visibility_cache", "_built_static_version")
|
__slots__ = ("collision_engine", "corners", "corner_index", "_corner_graph", "_point_visibility_cache", "_built_static_version")
|
||||||
|
|
||||||
def __init__(self, collision_engine: CollisionEngine) -> None:
|
def __init__(self, collision_engine: RoutingWorld) -> None:
|
||||||
self.collision_engine = collision_engine
|
self.collision_engine = collision_engine
|
||||||
self.corners: list[tuple[float, float]] = []
|
self.corners: list[tuple[float, float]] = []
|
||||||
self.corner_index = rtree.index.Index()
|
self.corner_index = rtree.index.Index()
|
||||||
|
|
@ -153,10 +153,3 @@ class VisibilityManager:
|
||||||
if corner_idx is not None and corner_idx in self._corner_graph:
|
if corner_idx is not None and corner_idx in self._corner_graph:
|
||||||
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_visible_corners(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
|
|
||||||
"""
|
|
||||||
Backward-compatible alias for arbitrary-point visibility queries.
|
|
||||||
Prefer `get_corner_visibility()` in routing code and `get_point_visibility()` elsewhere.
|
|
||||||
"""
|
|
||||||
return self.get_point_visibility(origin, max_dist=max_dist)
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from inire import NetSpec
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
from inire.router._astar_types import AStarMetrics
|
||||||
from inire.router.pathfinder import PathFinder
|
from inire.router._router import PathFinder
|
||||||
|
from inire.tests.support import build_context
|
||||||
|
|
||||||
def benchmark_scaling() -> None:
|
def benchmark_scaling() -> None:
|
||||||
print("Starting Scalability Benchmark...")
|
print("Starting Scalability Benchmark...")
|
||||||
|
|
@ -20,25 +23,33 @@ def benchmark_scaling() -> None:
|
||||||
assert mem_gb < 2.0
|
assert mem_gb < 2.0
|
||||||
|
|
||||||
# 2. Node Expansion Rate (50 nets)
|
# 2. Node Expansion Rate (50 nets)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
# Use a smaller area for routing benchmark to keep it fast
|
# Use a smaller area for routing benchmark to keep it fast
|
||||||
routing_bounds = (0, 0, 1000, 1000)
|
routing_bounds = (0, 0, 1000, 1000)
|
||||||
danger_map = DangerMap(bounds=routing_bounds)
|
danger_map = DangerMap(bounds=routing_bounds)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map)
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
context = AStarContext(evaluator)
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pf = PathFinder(context, metrics)
|
|
||||||
|
|
||||||
num_nets = 50
|
num_nets = 50
|
||||||
netlist = {}
|
netlist = {}
|
||||||
for i in range(num_nets):
|
for i in range(num_nets):
|
||||||
# Parallel nets spaced by 10um
|
# Parallel nets spaced by 10um
|
||||||
netlist[f"net{i}"] = (Port(0, i * 10, 0), Port(100, i * 10, 0))
|
netlist[f"net{i}"] = (Port(0, i * 10, 0), Port(100, i * 10, 0))
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(
|
||||||
|
build_context(
|
||||||
|
evaluator,
|
||||||
|
bounds=routing_bounds,
|
||||||
|
nets=(
|
||||||
|
NetSpec(net_id=net_id, start=start, target=target, width=2.0)
|
||||||
|
for net_id, (start, target) in netlist.items()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metrics=metrics,
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Routing {num_nets} nets...")
|
print(f"Routing {num_nets} nets...")
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
results = pf.route_all(netlist, dict.fromkeys(netlist, 2.0))
|
results = pf.route_all()
|
||||||
end_time = time.monotonic()
|
end_time = time.monotonic()
|
||||||
|
|
||||||
total_time = end_time - start_time
|
total_time = end_time - start_time
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ from typing import Callable
|
||||||
|
|
||||||
from shapely.geometry import Polygon, box
|
from shapely.geometry import Polygon, box
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec, RoutingResult
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext, AStarMetrics
|
from inire.router._astar_types import AStarMetrics
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import PathFinder, RoutingResult
|
from inire.tests.support import build_context, build_pathfinder
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -28,30 +29,6 @@ class ScenarioDefinition:
|
||||||
run: Callable[[], ScenarioOutcome]
|
run: Callable[[], ScenarioOutcome]
|
||||||
|
|
||||||
|
|
||||||
def _build_router(
|
|
||||||
*,
|
|
||||||
bounds: tuple[float, float, float, float],
|
|
||||||
clearance: float = 2.0,
|
|
||||||
obstacles: list[Polygon] | None = None,
|
|
||||||
evaluator_kwargs: dict[str, float] | None = None,
|
|
||||||
context_kwargs: dict[str, object] | None = None,
|
|
||||||
pathfinder_kwargs: dict[str, object] | None = None,
|
|
||||||
) -> tuple[CollisionEngine, CostEvaluator, AStarContext, AStarMetrics, PathFinder]:
|
|
||||||
static_obstacles = obstacles or []
|
|
||||||
engine = CollisionEngine(clearance=clearance)
|
|
||||||
for obstacle in static_obstacles:
|
|
||||||
engine.add_static_obstacle(obstacle)
|
|
||||||
|
|
||||||
danger_map = DangerMap(bounds=bounds)
|
|
||||||
danger_map.precompute(static_obstacles)
|
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, **(evaluator_kwargs or {}))
|
|
||||||
context = AStarContext(evaluator, **(context_kwargs or {}))
|
|
||||||
metrics = AStarMetrics()
|
|
||||||
pathfinder = PathFinder(context, metrics, **(pathfinder_kwargs or {}))
|
|
||||||
return engine, evaluator, context, metrics, pathfinder
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize(results: dict[str, RoutingResult], duration_s: float) -> ScenarioOutcome:
|
def _summarize(results: dict[str, RoutingResult], duration_s: float) -> ScenarioOutcome:
|
||||||
return ScenarioOutcome(
|
return ScenarioOutcome(
|
||||||
duration_s=duration_s,
|
duration_s=duration_s,
|
||||||
|
|
@ -70,7 +47,7 @@ def _build_evaluator(
|
||||||
sbend_penalty: float = 150.0,
|
sbend_penalty: float = 150.0,
|
||||||
) -> CostEvaluator:
|
) -> CostEvaluator:
|
||||||
static_obstacles = obstacles or []
|
static_obstacles = obstacles or []
|
||||||
engine = CollisionEngine(clearance=clearance)
|
engine = RoutingWorld(clearance=clearance)
|
||||||
for obstacle in static_obstacles:
|
for obstacle in static_obstacles:
|
||||||
engine.add_static_obstacle(obstacle)
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
|
@ -79,92 +56,155 @@ def _build_evaluator(
|
||||||
return CostEvaluator(engine, danger_map, bend_penalty=bend_penalty, sbend_penalty=sbend_penalty)
|
return CostEvaluator(engine, danger_map, bend_penalty=bend_penalty, sbend_penalty=sbend_penalty)
|
||||||
|
|
||||||
|
|
||||||
|
def _net_specs(
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
widths: dict[str, float],
|
||||||
|
) -> tuple[NetSpec, ...]:
|
||||||
|
return tuple(
|
||||||
|
NetSpec(net_id=net_id, start=start, target=target, width=widths.get(net_id, 2.0))
|
||||||
|
for net_id, (start, target) in netlist.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_routing_stack(
|
||||||
|
*,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
widths: dict[str, float],
|
||||||
|
clearance: float = 2.0,
|
||||||
|
obstacles: list[Polygon] | None = None,
|
||||||
|
evaluator_kwargs: dict[str, float] | None = None,
|
||||||
|
request_kwargs: dict[str, object] | None = None,
|
||||||
|
) -> tuple[RoutingWorld, CostEvaluator, AStarMetrics, object]:
|
||||||
|
static_obstacles = obstacles or []
|
||||||
|
engine = RoutingWorld(clearance=clearance)
|
||||||
|
for obstacle in static_obstacles:
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute(static_obstacles)
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, **(evaluator_kwargs or {}))
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pathfinder = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=bounds,
|
||||||
|
nets=_net_specs(netlist, widths),
|
||||||
|
metrics=metrics,
|
||||||
|
**(request_kwargs or {}),
|
||||||
|
)
|
||||||
|
return engine, evaluator, metrics, pathfinder
|
||||||
|
|
||||||
|
|
||||||
def run_example_01() -> ScenarioOutcome:
|
def run_example_01() -> ScenarioOutcome:
|
||||||
_, _, _, _, pathfinder = _build_router(bounds=(0, 0, 100, 100), context_kwargs={"bend_radii": [10.0]})
|
|
||||||
netlist = {"net1": (Port(10, 50, 0), Port(90, 50, 0))}
|
netlist = {"net1": (Port(10, 50, 0), Port(90, 50, 0))}
|
||||||
|
widths = {"net1": 2.0}
|
||||||
|
_, _, _, pathfinder = _build_routing_stack(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
|
request_kwargs={"bend_radii": [10.0]},
|
||||||
|
)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(netlist, {"net1": 2.0})
|
results = pathfinder.route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
||||||
def run_example_02() -> ScenarioOutcome:
|
def run_example_02() -> ScenarioOutcome:
|
||||||
_, _, _, _, pathfinder = _build_router(
|
|
||||||
bounds=(0, 0, 100, 100),
|
|
||||||
evaluator_kwargs={
|
|
||||||
"greedy_h_weight": 1.5,
|
|
||||||
"bend_penalty": 50.0,
|
|
||||||
"sbend_penalty": 150.0,
|
|
||||||
},
|
|
||||||
context_kwargs={
|
|
||||||
"bend_radii": [10.0],
|
|
||||||
"sbend_radii": [10.0],
|
|
||||||
},
|
|
||||||
pathfinder_kwargs={"base_congestion_penalty": 1000.0},
|
|
||||||
)
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
||||||
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
||||||
}
|
}
|
||||||
widths = {net_id: 2.0 for net_id in netlist}
|
widths = {net_id: 2.0 for net_id in netlist}
|
||||||
|
_, _, _, pathfinder = _build_routing_stack(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
|
evaluator_kwargs={
|
||||||
|
"greedy_h_weight": 1.5,
|
||||||
|
"bend_penalty": 50.0,
|
||||||
|
"sbend_penalty": 150.0,
|
||||||
|
},
|
||||||
|
request_kwargs={
|
||||||
|
"bend_radii": [10.0],
|
||||||
|
"sbend_radii": [10.0],
|
||||||
|
"base_penalty": 1000.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(netlist, widths)
|
results = pathfinder.route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
||||||
def run_example_03() -> ScenarioOutcome:
|
def run_example_03() -> ScenarioOutcome:
|
||||||
engine, _, _, _, pathfinder = _build_router(bounds=(0, -50, 100, 50), context_kwargs={"bend_radii": [10.0]})
|
netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))}
|
||||||
|
widths_a = {"netA": 2.0}
|
||||||
|
engine, evaluator, _, pathfinder = _build_routing_stack(
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
netlist=netlist_a,
|
||||||
|
widths=widths_a,
|
||||||
|
request_kwargs={"bend_radii": [10.0]},
|
||||||
|
)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results_a = pathfinder.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0})
|
results_a = pathfinder.route_all()
|
||||||
engine.lock_net("netA")
|
for polygon in results_a["netA"].as_locked_route().geometry:
|
||||||
results_b = pathfinder.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})
|
engine.add_static_obstacle(polygon)
|
||||||
|
results_b = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
nets=_net_specs({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0}),
|
||||||
|
bend_radii=[10.0],
|
||||||
|
).route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize({**results_a, **results_b}, t1 - t0)
|
return _summarize({**results_a, **results_b}, t1 - t0)
|
||||||
|
|
||||||
|
|
||||||
def run_example_04() -> ScenarioOutcome:
|
def run_example_04() -> ScenarioOutcome:
|
||||||
_, _, _, _, pathfinder = _build_router(
|
|
||||||
bounds=(0, 0, 100, 100),
|
|
||||||
evaluator_kwargs={
|
|
||||||
"unit_length_cost": 1.0,
|
|
||||||
"bend_penalty": 10.0,
|
|
||||||
"sbend_penalty": 20.0,
|
|
||||||
},
|
|
||||||
context_kwargs={
|
|
||||||
"node_limit": 50000,
|
|
||||||
"bend_radii": [10.0, 30.0],
|
|
||||||
"sbend_offsets": [5.0],
|
|
||||||
"bend_penalty": 10.0,
|
|
||||||
"sbend_penalty": 20.0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
||||||
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
||||||
}
|
}
|
||||||
widths = {"sbend_only": 2.0, "multi_radii": 2.0}
|
widths = {"sbend_only": 2.0, "multi_radii": 2.0}
|
||||||
|
_, _, _, pathfinder = _build_routing_stack(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
|
evaluator_kwargs={
|
||||||
|
"unit_length_cost": 1.0,
|
||||||
|
"bend_penalty": 10.0,
|
||||||
|
"sbend_penalty": 20.0,
|
||||||
|
},
|
||||||
|
request_kwargs={
|
||||||
|
"node_limit": 50000,
|
||||||
|
"bend_radii": [10.0, 30.0],
|
||||||
|
"sbend_offsets": [5.0],
|
||||||
|
},
|
||||||
|
)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(netlist, widths)
|
results = pathfinder.route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
||||||
def run_example_05() -> ScenarioOutcome:
|
def run_example_05() -> ScenarioOutcome:
|
||||||
_, _, _, _, pathfinder = _build_router(
|
|
||||||
bounds=(0, 0, 200, 200),
|
|
||||||
evaluator_kwargs={"bend_penalty": 50.0},
|
|
||||||
context_kwargs={"bend_radii": [20.0]},
|
|
||||||
)
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
||||||
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
||||||
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
||||||
}
|
}
|
||||||
widths = {net_id: 2.0 for net_id in netlist}
|
widths = {net_id: 2.0 for net_id in netlist}
|
||||||
|
_, _, _, pathfinder = _build_routing_stack(
|
||||||
|
bounds=(0, 0, 200, 200),
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
|
evaluator_kwargs={"bend_penalty": 50.0},
|
||||||
|
request_kwargs={"bend_radii": [20.0]},
|
||||||
|
)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(netlist, widths)
|
results = pathfinder.route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
@ -178,32 +218,35 @@ def run_example_06() -> ScenarioOutcome:
|
||||||
]
|
]
|
||||||
scenarios = [
|
scenarios = [
|
||||||
(
|
(
|
||||||
AStarContext(_build_evaluator(bounds, obstacles=obstacles), bend_radii=[10.0], bend_collision_type="arc"),
|
_build_evaluator(bounds, obstacles=obstacles),
|
||||||
{"arc_model": (Port(10, 120, 0), Port(90, 140, 90))},
|
{"arc_model": (Port(10, 120, 0), Port(90, 140, 90))},
|
||||||
{"arc_model": 2.0},
|
{"arc_model": 2.0},
|
||||||
|
{"bend_radii": [10.0], "bend_collision_type": "arc", "use_tiered_strategy": False},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
AStarContext(_build_evaluator(bounds, obstacles=obstacles), bend_radii=[10.0], bend_collision_type="bbox"),
|
_build_evaluator(bounds, obstacles=obstacles),
|
||||||
{"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))},
|
{"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))},
|
||||||
{"bbox_model": 2.0},
|
{"bbox_model": 2.0},
|
||||||
|
{"bend_radii": [10.0], "bend_collision_type": "bbox", "use_tiered_strategy": False},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
AStarContext(
|
_build_evaluator(bounds, obstacles=obstacles),
|
||||||
_build_evaluator(bounds, obstacles=obstacles),
|
|
||||||
bend_radii=[10.0],
|
|
||||||
bend_collision_type="clipped_bbox",
|
|
||||||
bend_clip_margin=1.0,
|
|
||||||
),
|
|
||||||
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))},
|
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))},
|
||||||
{"clipped_model": 2.0},
|
{"clipped_model": 2.0},
|
||||||
|
{"bend_radii": [10.0], "bend_collision_type": "clipped_bbox", "use_tiered_strategy": False},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
combined_results: dict[str, RoutingResult] = {}
|
combined_results: dict[str, RoutingResult] = {}
|
||||||
for context, netlist, net_widths in scenarios:
|
for evaluator, netlist, net_widths, request_kwargs in scenarios:
|
||||||
pathfinder = PathFinder(context, use_tiered_strategy=False)
|
pathfinder = build_pathfinder(
|
||||||
combined_results.update(pathfinder.route_all(netlist, net_widths))
|
evaluator,
|
||||||
|
bounds=bounds,
|
||||||
|
nets=_net_specs(netlist, net_widths),
|
||||||
|
**request_kwargs,
|
||||||
|
)
|
||||||
|
combined_results.update(pathfinder.route_all())
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(combined_results, t1 - t0)
|
return _summarize(combined_results, t1 - t0)
|
||||||
|
|
||||||
|
|
@ -214,29 +257,6 @@ def run_example_07() -> ScenarioOutcome:
|
||||||
box(450, 0, 550, 400),
|
box(450, 0, 550, 400),
|
||||||
box(450, 600, 550, 1000),
|
box(450, 600, 550, 1000),
|
||||||
]
|
]
|
||||||
_, evaluator, _, metrics, pathfinder = _build_router(
|
|
||||||
bounds=bounds,
|
|
||||||
clearance=6.0,
|
|
||||||
obstacles=obstacles,
|
|
||||||
evaluator_kwargs={
|
|
||||||
"greedy_h_weight": 1.5,
|
|
||||||
"unit_length_cost": 0.1,
|
|
||||||
"bend_penalty": 100.0,
|
|
||||||
"sbend_penalty": 400.0,
|
|
||||||
"congestion_penalty": 100.0,
|
|
||||||
},
|
|
||||||
context_kwargs={
|
|
||||||
"node_limit": 2000000,
|
|
||||||
"bend_radii": [50.0],
|
|
||||||
"sbend_radii": [50.0],
|
|
||||||
},
|
|
||||||
pathfinder_kwargs={
|
|
||||||
"max_iterations": 15,
|
|
||||||
"base_congestion_penalty": 100.0,
|
|
||||||
"congestion_multiplier": 1.4,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
num_nets = 10
|
num_nets = 10
|
||||||
start_x = 50
|
start_x = 50
|
||||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||||
|
|
@ -249,6 +269,31 @@ def run_example_07() -> ScenarioOutcome:
|
||||||
sy = int(round(start_y_base + index * 10.0))
|
sy = int(round(start_y_base + index * 10.0))
|
||||||
ey = int(round(end_y_base + index * end_y_pitch))
|
ey = int(round(end_y_base + index * end_y_pitch))
|
||||||
netlist[f"net_{index:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
netlist[f"net_{index:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
||||||
|
widths = dict.fromkeys(netlist, 2.0)
|
||||||
|
_, evaluator, metrics, pathfinder = _build_routing_stack(
|
||||||
|
bounds=bounds,
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
|
clearance=6.0,
|
||||||
|
obstacles=obstacles,
|
||||||
|
evaluator_kwargs={
|
||||||
|
"greedy_h_weight": 1.5,
|
||||||
|
"unit_length_cost": 0.1,
|
||||||
|
"bend_penalty": 100.0,
|
||||||
|
"sbend_penalty": 400.0,
|
||||||
|
},
|
||||||
|
request_kwargs={
|
||||||
|
"node_limit": 2000000,
|
||||||
|
"bend_radii": [50.0],
|
||||||
|
"sbend_radii": [50.0],
|
||||||
|
"max_iterations": 15,
|
||||||
|
"base_penalty": 100.0,
|
||||||
|
"multiplier": 1.4,
|
||||||
|
"capture_expanded": True,
|
||||||
|
"shuffle_nets": True,
|
||||||
|
"seed": 42,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
||||||
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
||||||
|
|
@ -256,14 +301,7 @@ def run_example_07() -> ScenarioOutcome:
|
||||||
metrics.reset_per_route()
|
metrics.reset_per_route()
|
||||||
|
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(
|
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||||
netlist,
|
|
||||||
dict.fromkeys(netlist, 2.0),
|
|
||||||
store_expanded=True,
|
|
||||||
iteration_callback=iteration_callback,
|
|
||||||
shuffle_nets=True,
|
|
||||||
seed=42,
|
|
||||||
)
|
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
@ -272,21 +310,30 @@ def run_example_08() -> ScenarioOutcome:
|
||||||
bounds = (0, 0, 150, 150)
|
bounds = (0, 0, 150, 150)
|
||||||
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
|
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
|
||||||
widths = {"custom_bend": 2.0}
|
widths = {"custom_bend": 2.0}
|
||||||
|
custom_model = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
||||||
context_std = AStarContext(_build_evaluator(bounds), bend_radii=[10.0], sbend_radii=[])
|
standard_evaluator = _build_evaluator(bounds)
|
||||||
context_custom = AStarContext(
|
custom_evaluator = _build_evaluator(bounds)
|
||||||
_build_evaluator(bounds),
|
|
||||||
bend_radii=[10.0],
|
|
||||||
bend_collision_type=Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]),
|
|
||||||
sbend_radii=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results_std = PathFinder(context_std, AStarMetrics(), use_tiered_strategy=False).route_all(netlist, widths)
|
results_std = build_pathfinder(
|
||||||
results_custom = PathFinder(context_custom, AStarMetrics(), use_tiered_strategy=False).route_all(
|
standard_evaluator,
|
||||||
{"custom_model": netlist["custom_bend"]},
|
bounds=bounds,
|
||||||
{"custom_model": 2.0},
|
nets=_net_specs(netlist, widths),
|
||||||
)
|
bend_radii=[10.0],
|
||||||
|
sbend_radii=[],
|
||||||
|
use_tiered_strategy=False,
|
||||||
|
metrics=AStarMetrics(),
|
||||||
|
).route_all()
|
||||||
|
results_custom = build_pathfinder(
|
||||||
|
custom_evaluator,
|
||||||
|
bounds=bounds,
|
||||||
|
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
|
||||||
|
bend_radii=[10.0],
|
||||||
|
bend_collision_type=custom_model,
|
||||||
|
sbend_radii=[],
|
||||||
|
use_tiered_strategy=False,
|
||||||
|
metrics=AStarMetrics(),
|
||||||
|
).route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize({**results_std, **results_custom}, t1 - t0)
|
return _summarize({**results_std, **results_custom}, t1 - t0)
|
||||||
|
|
||||||
|
|
@ -296,16 +343,18 @@ def run_example_09() -> ScenarioOutcome:
|
||||||
box(35, 35, 45, 65),
|
box(35, 35, 45, 65),
|
||||||
box(55, 35, 65, 65),
|
box(55, 35, 65, 65),
|
||||||
]
|
]
|
||||||
_, _, _, _, pathfinder = _build_router(
|
netlist = {"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))}
|
||||||
|
widths = {"budget_limited_net": 2.0}
|
||||||
|
_, _, _, pathfinder = _build_routing_stack(
|
||||||
bounds=(0, 0, 100, 100),
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist,
|
||||||
|
widths=widths,
|
||||||
obstacles=obstacles,
|
obstacles=obstacles,
|
||||||
evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0},
|
evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0},
|
||||||
context_kwargs={"node_limit": 3, "bend_radii": [10.0]},
|
request_kwargs={"node_limit": 3, "bend_radii": [10.0], "warm_start": None},
|
||||||
pathfinder_kwargs={"warm_start": None},
|
|
||||||
)
|
)
|
||||||
netlist = {"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))}
|
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(netlist, {"budget_limited_net": 2.0})
|
results = pathfinder.route_all()
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _summarize(results, t1 - t0)
|
return _summarize(results, t1 - t0)
|
||||||
|
|
||||||
|
|
@ -313,7 +362,7 @@ def run_example_09() -> ScenarioOutcome:
|
||||||
SCENARIOS: tuple[ScenarioDefinition, ...] = (
|
SCENARIOS: tuple[ScenarioDefinition, ...] = (
|
||||||
ScenarioDefinition("example_01_simple_route", run_example_01),
|
ScenarioDefinition("example_01_simple_route", run_example_01),
|
||||||
ScenarioDefinition("example_02_congestion_resolution", run_example_02),
|
ScenarioDefinition("example_02_congestion_resolution", run_example_02),
|
||||||
ScenarioDefinition("example_03_locked_paths", run_example_03),
|
ScenarioDefinition("example_03_locked_routes", run_example_03),
|
||||||
ScenarioDefinition("example_04_sbends_and_radii", run_example_04),
|
ScenarioDefinition("example_04_sbends_and_radii", run_example_04),
|
||||||
ScenarioDefinition("example_05_orientation_stress", run_example_05),
|
ScenarioDefinition("example_05_orientation_stress", run_example_05),
|
||||||
ScenarioDefinition("example_06_bend_collision_models", run_example_06),
|
ScenarioDefinition("example_06_bend_collision_models", run_example_06),
|
||||||
|
|
|
||||||
162
inire/tests/support.py
Normal file
162
inire/tests/support.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from inire.model import (
|
||||||
|
CongestionOptions,
|
||||||
|
DiagnosticsOptions,
|
||||||
|
NetSpec,
|
||||||
|
ObjectiveWeights,
|
||||||
|
RefinementOptions,
|
||||||
|
RoutingOptions,
|
||||||
|
RoutingProblem,
|
||||||
|
SearchOptions,
|
||||||
|
)
|
||||||
|
from inire.router._astar_types import AStarContext
|
||||||
|
from inire.router._router import PathFinder
|
||||||
|
|
||||||
|
|
||||||
|
def build_problem(
|
||||||
|
*,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
nets: Iterable[NetSpec] = (),
|
||||||
|
**overrides: object,
|
||||||
|
) -> RoutingProblem:
|
||||||
|
return RoutingProblem(
|
||||||
|
bounds=bounds,
|
||||||
|
nets=tuple(nets),
|
||||||
|
**overrides,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_request(
|
||||||
|
*,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
nets: Iterable[NetSpec] = (),
|
||||||
|
**overrides: object,
|
||||||
|
) -> RoutingProblem:
|
||||||
|
return build_problem(bounds=bounds, nets=nets, **overrides)
|
||||||
|
|
||||||
|
|
||||||
|
def build_options(
|
||||||
|
*,
|
||||||
|
objective: ObjectiveWeights | None = None,
|
||||||
|
search: SearchOptions | None = None,
|
||||||
|
congestion: CongestionOptions | None = None,
|
||||||
|
refinement: RefinementOptions | None = None,
|
||||||
|
diagnostics: DiagnosticsOptions | None = None,
|
||||||
|
**overrides: object,
|
||||||
|
) -> RoutingOptions:
|
||||||
|
if objective is None:
|
||||||
|
objective = ObjectiveWeights()
|
||||||
|
if search is None:
|
||||||
|
search = SearchOptions()
|
||||||
|
if congestion is None:
|
||||||
|
congestion = CongestionOptions()
|
||||||
|
if refinement is None:
|
||||||
|
refinement = RefinementOptions()
|
||||||
|
if diagnostics is None:
|
||||||
|
diagnostics = DiagnosticsOptions()
|
||||||
|
|
||||||
|
search_fields = set(SearchOptions.__dataclass_fields__)
|
||||||
|
congestion_fields = set(CongestionOptions.__dataclass_fields__)
|
||||||
|
refinement_fields = set(RefinementOptions.__dataclass_fields__)
|
||||||
|
diagnostics_fields = set(DiagnosticsOptions.__dataclass_fields__)
|
||||||
|
objective_fields = set(ObjectiveWeights.__dataclass_fields__)
|
||||||
|
|
||||||
|
search_overrides = {key: value for key, value in overrides.items() if key in search_fields}
|
||||||
|
congestion_overrides = {key: value for key, value in overrides.items() if key in congestion_fields}
|
||||||
|
refinement_overrides = {key: value for key, value in overrides.items() if key in refinement_fields}
|
||||||
|
diagnostics_overrides = {key: value for key, value in overrides.items() if key in diagnostics_fields}
|
||||||
|
objective_overrides = {key: value for key, value in overrides.items() if key in objective_fields}
|
||||||
|
|
||||||
|
unknown = set(overrides) - search_fields - congestion_fields - refinement_fields - diagnostics_fields - objective_fields
|
||||||
|
if unknown:
|
||||||
|
unknown_names = ", ".join(sorted(unknown))
|
||||||
|
raise TypeError(f"Unsupported RoutingOptions overrides: {unknown_names}")
|
||||||
|
|
||||||
|
resolved_objective = objective if not objective_overrides else ObjectiveWeights(
|
||||||
|
**{
|
||||||
|
field: getattr(objective, field)
|
||||||
|
for field in objective_fields
|
||||||
|
}
|
||||||
|
| objective_overrides
|
||||||
|
)
|
||||||
|
resolved_search = search if not search_overrides else SearchOptions(
|
||||||
|
**{
|
||||||
|
field: getattr(search, field)
|
||||||
|
for field in search_fields
|
||||||
|
}
|
||||||
|
| search_overrides
|
||||||
|
)
|
||||||
|
resolved_congestion = congestion if not congestion_overrides else CongestionOptions(
|
||||||
|
**{
|
||||||
|
field: getattr(congestion, field)
|
||||||
|
for field in congestion_fields
|
||||||
|
}
|
||||||
|
| congestion_overrides
|
||||||
|
)
|
||||||
|
resolved_refinement = refinement if not refinement_overrides else RefinementOptions(
|
||||||
|
**{
|
||||||
|
field: getattr(refinement, field)
|
||||||
|
for field in refinement_fields
|
||||||
|
}
|
||||||
|
| refinement_overrides
|
||||||
|
)
|
||||||
|
resolved_diagnostics = diagnostics if not diagnostics_overrides else DiagnosticsOptions(
|
||||||
|
**{
|
||||||
|
field: getattr(diagnostics, field)
|
||||||
|
for field in diagnostics_fields
|
||||||
|
}
|
||||||
|
| diagnostics_overrides
|
||||||
|
)
|
||||||
|
return RoutingOptions(
|
||||||
|
search=resolved_search,
|
||||||
|
objective=resolved_objective,
|
||||||
|
congestion=resolved_congestion,
|
||||||
|
refinement=resolved_refinement,
|
||||||
|
diagnostics=resolved_diagnostics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_context(
|
||||||
|
evaluator,
|
||||||
|
*,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
nets: Iterable[NetSpec] = (),
|
||||||
|
problem: RoutingProblem | None = None,
|
||||||
|
options: RoutingOptions | None = None,
|
||||||
|
**overrides: object,
|
||||||
|
) -> AStarContext:
|
||||||
|
resolved_problem = problem if problem is not None else build_problem(bounds=bounds, nets=nets)
|
||||||
|
resolved_options = options if options is not None else build_options(**overrides)
|
||||||
|
return AStarContext(
|
||||||
|
evaluator,
|
||||||
|
resolved_problem,
|
||||||
|
resolved_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
*,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
nets: Iterable[NetSpec] = (),
|
||||||
|
netlist: dict[str, tuple[object, object]] | None = None,
|
||||||
|
net_widths: dict[str, float] | None = None,
|
||||||
|
problem: RoutingProblem | None = None,
|
||||||
|
options: RoutingOptions | None = None,
|
||||||
|
**overrides: object,
|
||||||
|
) -> PathFinder:
|
||||||
|
resolved_problem = problem
|
||||||
|
if resolved_problem is None:
|
||||||
|
resolved_nets = tuple(nets)
|
||||||
|
if netlist is not None:
|
||||||
|
widths = {} if net_widths is None else net_widths
|
||||||
|
resolved_nets = tuple(
|
||||||
|
NetSpec(net_id=net_id, start=start, target=target, width=widths.get(net_id, 2.0))
|
||||||
|
for net_id, (start, target) in netlist.items()
|
||||||
|
)
|
||||||
|
resolved_problem = build_problem(bounds=bounds, nets=resolved_nets)
|
||||||
|
resolved_options = options if options is not None else build_options(**overrides)
|
||||||
|
return PathFinder(build_context(evaluator, bounds=bounds, problem=resolved_problem, options=resolved_options))
|
||||||
108
inire/tests/test_api.py
Normal file
108
inire/tests/test_api.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
from inire import (
|
||||||
|
CongestionOptions,
|
||||||
|
DiagnosticsOptions,
|
||||||
|
LockedRoute,
|
||||||
|
NetSpec,
|
||||||
|
ObjectiveWeights,
|
||||||
|
Port,
|
||||||
|
RefinementOptions,
|
||||||
|
RoutingOptions,
|
||||||
|
RoutingProblem,
|
||||||
|
SearchOptions,
|
||||||
|
route,
|
||||||
|
)
|
||||||
|
from inire.geometry.components import Straight
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_problem_smoke() -> None:
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
nets=(NetSpec("net1", Port(10, 50, 0), Port(90, 50, 0), width=2.0),),
|
||||||
|
)
|
||||||
|
|
||||||
|
run = route(problem)
|
||||||
|
|
||||||
|
assert set(run.results_by_net) == {"net1"}
|
||||||
|
assert run.results_by_net["net1"].is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_problem_supports_configs_and_debug_data() -> None:
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
nets=(NetSpec("net1", Port(10, 10, 0), Port(90, 90, 0), width=2.0),),
|
||||||
|
static_obstacles=(box(40, 0, 60, 70),),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
search=SearchOptions(
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
node_limit=50000,
|
||||||
|
greedy_h_weight=1.2,
|
||||||
|
),
|
||||||
|
objective=ObjectiveWeights(
|
||||||
|
bend_penalty=50.0,
|
||||||
|
sbend_penalty=150.0,
|
||||||
|
),
|
||||||
|
congestion=CongestionOptions(warm_start=None),
|
||||||
|
refinement=RefinementOptions(enabled=True),
|
||||||
|
diagnostics=DiagnosticsOptions(capture_expanded=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
run = route(problem, options=options)
|
||||||
|
|
||||||
|
assert run.results_by_net["net1"].reached_target
|
||||||
|
assert run.expanded_nodes
|
||||||
|
assert run.metrics.nodes_expanded > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_problem_locked_routes_become_static_obstacles() -> None:
|
||||||
|
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
nets=(NetSpec("crossing", Port(50, 10, 90), Port(50, 90, 90), width=2.0),),
|
||||||
|
locked_routes={"locked": LockedRoute.from_path(locked)},
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
congestion=CongestionOptions(max_iterations=1, warm_start=None),
|
||||||
|
refinement=RefinementOptions(enabled=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
run = route(problem, options=options)
|
||||||
|
result = run.results_by_net["crossing"]
|
||||||
|
|
||||||
|
assert not result.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_locked_routes_enable_incremental_requests_without_sessions() -> None:
|
||||||
|
problem_a = RoutingProblem(
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(search=SearchOptions(bend_radii=(10.0,)))
|
||||||
|
results_a = route(problem_a, options=options)
|
||||||
|
assert results_a.results_by_net["netA"].is_valid
|
||||||
|
|
||||||
|
problem_b = RoutingProblem(
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),),
|
||||||
|
locked_routes={"netA": results_a.results_by_net["netA"].as_locked_route()},
|
||||||
|
)
|
||||||
|
results_b = route(problem_b, options=options)
|
||||||
|
|
||||||
|
assert results_b.results_by_net["netB"].is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_results_metrics_are_snapshots() -> None:
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
nets=(NetSpec("net1", Port(10, 50, 0), Port(90, 50, 0), width=2.0),),
|
||||||
|
)
|
||||||
|
options = RoutingOptions()
|
||||||
|
run1 = route(problem, options=options)
|
||||||
|
first_metrics = run1.metrics
|
||||||
|
run2 = route(problem, options=options)
|
||||||
|
|
||||||
|
assert first_metrics == run1.metrics
|
||||||
|
assert run1.metrics is not run2.metrics
|
||||||
|
assert first_metrics.nodes_expanded > 0
|
||||||
|
|
@ -1,34 +1,36 @@
|
||||||
import pytest
|
import pytest
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
import inire.router.astar as astar_module
|
from inire import RoutingResult
|
||||||
from inire.geometry.components import Bend90, SBend, Straight
|
from inire.geometry.components import Bend90, Straight
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext, route_astar
|
from inire.router._astar_types import AStarContext
|
||||||
from inire.router.config import CostConfig
|
from inire.router._search import route_astar
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import RoutingResult
|
from inire.tests.support import build_context, build_options, build_problem
|
||||||
from inire.utils.validation import validate_routing_result
|
from inire.utils.validation import validate_routing_result
|
||||||
|
|
||||||
|
BOUNDS = (0, -50, 150, 150)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def basic_evaluator() -> CostEvaluator:
|
def basic_evaluator() -> CostEvaluator:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(0, -50, 150, 150))
|
danger_map = DangerMap(bounds=BOUNDS)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
|
||||||
|
|
||||||
def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator)
|
context = build_context(basic_evaluator, bounds=BOUNDS)
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
target = Port(50, 0, 0)
|
target = Port(50, 0, 0)
|
||||||
path = route_astar(start, target, net_width=2.0, context=context)
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
assert path is not None
|
assert path is not None
|
||||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
|
@ -38,14 +40,14 @@ def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator, bend_radii=[10.0])
|
context = build_context(basic_evaluator, bounds=BOUNDS, bend_radii=[10.0])
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
||||||
target = Port(20, 20, 0)
|
target = Port(20, 20, 0)
|
||||||
path = route_astar(start, target, net_width=2.0, context=context)
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
assert path is not None
|
assert path is not None
|
||||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
|
@ -59,13 +61,13 @@ def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||||
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||||
basic_evaluator.danger_map.precompute([obstacle])
|
basic_evaluator.danger_map.precompute([obstacle])
|
||||||
|
|
||||||
context = AStarContext(basic_evaluator, bend_radii=[10.0], node_limit=1000000)
|
context = build_context(basic_evaluator, bounds=BOUNDS, bend_radii=[10.0], node_limit=1000000)
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
target = Port(60, 0, 0)
|
target = Port(60, 0, 0)
|
||||||
path = route_astar(start, target, net_width=2.0, context=context)
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
assert path is not None
|
assert path is not None
|
||||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
|
@ -74,13 +76,13 @@ def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_astar_uses_integerized_ports(basic_evaluator: CostEvaluator) -> None:
|
def test_astar_uses_integerized_ports(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator)
|
context = build_context(basic_evaluator, bounds=BOUNDS)
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
target = Port(10.1, 0, 0)
|
target = Port(10.1, 0, 0)
|
||||||
path = route_astar(start, target, net_width=2.0, context=context)
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
assert path is not None
|
assert path is not None
|
||||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
assert target.x == 10
|
assert target.x == 10
|
||||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
|
|
@ -89,7 +91,7 @@ def test_astar_uses_integerized_ports(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
|
||||||
def test_validate_routing_result_checks_expected_start() -> None:
|
def test_validate_routing_result_checks_expected_start() -> None:
|
||||||
path = [Straight.generate(Port(100, 0, 0), 10.0, width=2.0, dilation=1.0)]
|
path = [Straight.generate(Port(100, 0, 0), 10.0, width=2.0, dilation=1.0)]
|
||||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
|
|
||||||
validation = validate_routing_result(
|
validation = validate_routing_result(
|
||||||
result,
|
result,
|
||||||
|
|
@ -105,7 +107,7 @@ def test_validate_routing_result_checks_expected_start() -> None:
|
||||||
|
|
||||||
def test_validate_routing_result_uses_exact_component_geometry() -> None:
|
def test_validate_routing_result_uses_exact_component_geometry() -> None:
|
||||||
bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox", dilation=1.0)
|
bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox", dilation=1.0)
|
||||||
result = RoutingResult(net_id="test", path=[bend], is_valid=True, collisions=0)
|
result = RoutingResult(net_id="test", path=[bend], reached_target=True)
|
||||||
obstacle = Polygon([(2.0, 7.0), (4.0, 7.0), (4.0, 9.0), (2.0, 9.0)])
|
obstacle = Polygon([(2.0, 7.0), (4.0, 7.0), (4.0, 9.0), (2.0, 9.0)])
|
||||||
|
|
||||||
validation = validate_routing_result(
|
validation = validate_routing_result(
|
||||||
|
|
@ -119,18 +121,19 @@ def test_validate_routing_result_uses_exact_component_geometry() -> None:
|
||||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
|
||||||
|
|
||||||
def test_astar_context_keeps_cost_config_separate(basic_evaluator: CostEvaluator) -> None:
|
def test_astar_context_keeps_evaluator_weights_separate(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator, bend_radii=[5.0], bend_penalty=120.0, sbend_penalty=240.0)
|
basic_evaluator.bend_penalty = 120.0
|
||||||
|
basic_evaluator.sbend_penalty = 240.0
|
||||||
|
context = build_context(basic_evaluator, bounds=BOUNDS, bend_radii=[5.0])
|
||||||
|
|
||||||
assert isinstance(basic_evaluator.config, CostConfig)
|
assert basic_evaluator.bend_penalty == 120.0
|
||||||
assert basic_evaluator.config is not context.config
|
assert basic_evaluator.sbend_penalty == 240.0
|
||||||
assert basic_evaluator.config.bend_penalty == 120.0
|
assert context.options.search.bend_radii == (5.0,)
|
||||||
assert basic_evaluator.config.sbend_penalty == 240.0
|
assert basic_evaluator.h_manhattan(Port(0, 0, 0), Port(10, 10, 0)) > 0.0
|
||||||
assert basic_evaluator.config.min_bend_radius == 5.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_route_astar_bend_collision_override_does_not_persist(basic_evaluator: CostEvaluator) -> None:
|
def test_route_astar_bend_collision_override_does_not_persist(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator, bend_radii=[10.0], bend_collision_type="arc")
|
context = build_context(basic_evaluator, bounds=BOUNDS, bend_radii=[10.0], bend_collision_type="arc")
|
||||||
|
|
||||||
route_astar(
|
route_astar(
|
||||||
Port(0, 0, 0),
|
Port(0, 0, 0),
|
||||||
|
|
@ -141,254 +144,92 @@ def test_route_astar_bend_collision_override_does_not_persist(basic_evaluator: C
|
||||||
return_partial=True,
|
return_partial=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert context.config.bend_collision_type == "arc"
|
assert context.options.search.bend_collision_type == "arc"
|
||||||
|
|
||||||
|
|
||||||
def test_expand_moves_only_shortens_consecutive_straights(
|
def test_route_astar_returns_partial_path_when_node_limited(basic_evaluator: CostEvaluator) -> None:
|
||||||
basic_evaluator: CostEvaluator,
|
obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)])
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||||
) -> None:
|
basic_evaluator.danger_map.precompute([obstacle])
|
||||||
context = AStarContext(basic_evaluator, min_straight_length=5.0, max_straight_length=100.0)
|
context = build_context(basic_evaluator, bounds=BOUNDS, bend_radii=[10.0], node_limit=2)
|
||||||
prev_result = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
|
start = Port(0, 0, 0)
|
||||||
current = astar_module.AStarNode(
|
target = Port(60, 0, 0)
|
||||||
prev_result.end_port,
|
|
||||||
g_cost=prev_result.length,
|
|
||||||
h_cost=0.0,
|
|
||||||
component_result=prev_result,
|
|
||||||
)
|
|
||||||
|
|
||||||
emitted: list[tuple[str, tuple]] = []
|
partial_path = route_astar(start, target, net_width=2.0, context=context, return_partial=True)
|
||||||
|
no_partial_path = route_astar(start, target, net_width=2.0, context=context, return_partial=False)
|
||||||
|
|
||||||
def fake_process_move(*args, **kwargs) -> None:
|
assert partial_path is not None
|
||||||
emitted.append((args[9], args[10]))
|
assert partial_path
|
||||||
|
assert partial_path[-1].end_port != target
|
||||||
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
|
assert no_partial_path is None
|
||||||
|
|
||||||
astar_module.expand_moves(
|
|
||||||
current,
|
|
||||||
Port(80, 0, 0),
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=[],
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=astar_module.AStarMetrics(),
|
|
||||||
congestion_cache={},
|
|
||||||
)
|
|
||||||
|
|
||||||
straight_lengths = [params[0] for move_class, params in emitted if move_class == "S"]
|
|
||||||
assert straight_lengths
|
|
||||||
assert all(length < prev_result.length for length in straight_lengths)
|
|
||||||
|
|
||||||
|
|
||||||
def test_expand_moves_does_not_chain_sbends(
|
def test_route_astar_uses_single_sbend_for_same_orientation_offset(basic_evaluator: CostEvaluator) -> None:
|
||||||
basic_evaluator: CostEvaluator,
|
context = build_context(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
|
||||||
context = AStarContext(basic_evaluator, sbend_radii=[10.0], sbend_offsets=[5.0], max_straight_length=100.0)
|
|
||||||
prev_result = SBend.generate(Port(0, 0, 0), 5.0, 10.0, width=2.0, dilation=1.0)
|
|
||||||
current = astar_module.AStarNode(
|
|
||||||
prev_result.end_port,
|
|
||||||
g_cost=prev_result.length,
|
|
||||||
h_cost=0.0,
|
|
||||||
component_result=prev_result,
|
|
||||||
)
|
|
||||||
|
|
||||||
emitted: list[str] = []
|
|
||||||
|
|
||||||
def fake_process_move(*args, **kwargs) -> None:
|
|
||||||
emitted.append(args[9])
|
|
||||||
|
|
||||||
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
|
|
||||||
|
|
||||||
astar_module.expand_moves(
|
|
||||||
current,
|
|
||||||
Port(60, 10, 0),
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=[],
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=astar_module.AStarMetrics(),
|
|
||||||
congestion_cache={},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "SB" not in emitted
|
|
||||||
assert emitted
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_node_rejects_self_collision_against_ancestor(
|
|
||||||
basic_evaluator: CostEvaluator,
|
|
||||||
) -> None:
|
|
||||||
context = AStarContext(basic_evaluator)
|
|
||||||
metrics = astar_module.AStarMetrics()
|
|
||||||
target = Port(100, 0, 0)
|
|
||||||
|
|
||||||
root = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
|
|
||||||
ancestor = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
|
|
||||||
ancestor_node = astar_module.AStarNode(
|
|
||||||
ancestor.end_port,
|
|
||||||
g_cost=ancestor.length,
|
|
||||||
h_cost=0.0,
|
|
||||||
parent=root,
|
|
||||||
component_result=ancestor,
|
|
||||||
)
|
|
||||||
parent_result = Straight.generate(Port(30, 0, 0), 10.0, width=2.0, dilation=1.0)
|
|
||||||
parent_node = astar_module.AStarNode(
|
|
||||||
parent_result.end_port,
|
|
||||||
g_cost=ancestor.length + parent_result.length,
|
|
||||||
h_cost=0.0,
|
|
||||||
parent=ancestor_node,
|
|
||||||
component_result=parent_result,
|
|
||||||
)
|
|
||||||
overlapping_move = Straight.generate(Port(5, 0, 0), 10.0, width=2.0, dilation=1.0)
|
|
||||||
|
|
||||||
open_set: list[astar_module.AStarNode] = []
|
|
||||||
astar_module.add_node(
|
|
||||||
parent_node,
|
|
||||||
overlapping_move,
|
|
||||||
target,
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=open_set,
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=metrics,
|
|
||||||
congestion_cache={},
|
|
||||||
move_type="S",
|
|
||||||
cache_key=("self_collision",),
|
|
||||||
self_collision_check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert not open_set
|
|
||||||
assert metrics.moves_added == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_expand_moves_adds_sbend_aligned_straight_stop_points(
|
|
||||||
basic_evaluator: CostEvaluator,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
|
||||||
context = AStarContext(
|
|
||||||
basic_evaluator,
|
basic_evaluator,
|
||||||
|
bounds=BOUNDS,
|
||||||
bend_radii=[10.0],
|
bend_radii=[10.0],
|
||||||
sbend_radii=[10.0],
|
sbend_radii=[10.0],
|
||||||
|
sbend_offsets=[10.0],
|
||||||
max_straight_length=150.0,
|
max_straight_length=150.0,
|
||||||
)
|
)
|
||||||
current = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(100, 10, 0)
|
||||||
|
|
||||||
emitted: list[tuple[str, tuple]] = []
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
def fake_process_move(*args, **kwargs) -> None:
|
assert path is not None
|
||||||
emitted.append((args[9], args[10]))
|
assert path[-1].end_port == target
|
||||||
|
assert sum(1 for component in path if component.move_type == "sbend") == 1
|
||||||
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
|
assert not any(
|
||||||
|
first.move_type == second.move_type == "sbend"
|
||||||
astar_module.expand_moves(
|
for first, second in zip(path, path[1:], strict=False)
|
||||||
current,
|
|
||||||
Port(100, 10, 0),
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=[],
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=astar_module.AStarMetrics(),
|
|
||||||
congestion_cache={},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
straight_lengths = {params[0] for move_class, params in emitted if move_class == "S"}
|
|
||||||
sbend_span = astar_module._sbend_forward_span(10.0, 10.0)
|
|
||||||
assert sbend_span is not None
|
|
||||||
assert int(round(100.0 - sbend_span)) in straight_lengths
|
|
||||||
assert int(round(100.0 - 2.0 * sbend_span)) in straight_lengths
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("visibility_guidance", ["off", "exact_corner", "tangent_corner"])
|
||||||
def test_expand_moves_adds_exact_corner_visibility_stop_points(
|
def test_route_astar_supports_all_visibility_guidance_modes(
|
||||||
basic_evaluator: CostEvaluator,
|
basic_evaluator: CostEvaluator,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
visibility_guidance: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
context = AStarContext(
|
obstacle = Polygon([(30, 10), (50, 10), (50, 40), (30, 40)])
|
||||||
basic_evaluator,
|
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||||
bend_radii=[10.0],
|
basic_evaluator.danger_map.precompute([obstacle])
|
||||||
max_straight_length=150.0,
|
context = build_context(
|
||||||
visibility_guidance="exact_corner",
|
|
||||||
)
|
|
||||||
current = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
astar_module.VisibilityManager,
|
|
||||||
"get_corner_visibility",
|
|
||||||
lambda self, origin, max_dist=0.0: [(40.0, 10.0, 41.23), (75.0, -15.0, 76.48)],
|
|
||||||
)
|
|
||||||
|
|
||||||
emitted: list[tuple[str, tuple]] = []
|
|
||||||
|
|
||||||
def fake_process_move(*args, **kwargs) -> None:
|
|
||||||
emitted.append((args[9], args[10]))
|
|
||||||
|
|
||||||
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
|
|
||||||
|
|
||||||
astar_module.expand_moves(
|
|
||||||
current,
|
|
||||||
Port(120, 20, 0),
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=[],
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=astar_module.AStarMetrics(),
|
|
||||||
congestion_cache={},
|
|
||||||
)
|
|
||||||
|
|
||||||
straight_lengths = {params[0] for move_class, params in emitted if move_class == "S"}
|
|
||||||
assert 40 in straight_lengths
|
|
||||||
assert 75 in straight_lengths
|
|
||||||
|
|
||||||
|
|
||||||
def test_expand_moves_adds_tangent_corner_visibility_stop_points(
|
|
||||||
basic_evaluator: CostEvaluator,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
|
||||||
class DummyCornerIndex:
|
|
||||||
def intersection(self, bounds: tuple[float, float, float, float]) -> list[int]:
|
|
||||||
return [0, 1]
|
|
||||||
|
|
||||||
context = AStarContext(
|
|
||||||
basic_evaluator,
|
basic_evaluator,
|
||||||
|
bounds=BOUNDS,
|
||||||
bend_radii=[10.0],
|
bend_radii=[10.0],
|
||||||
sbend_radii=[],
|
sbend_radii=[],
|
||||||
max_straight_length=150.0,
|
max_straight_length=150.0,
|
||||||
visibility_guidance="tangent_corner",
|
visibility_guidance=visibility_guidance,
|
||||||
)
|
)
|
||||||
current = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(80, 50, 0)
|
||||||
|
|
||||||
monkeypatch.setattr(astar_module.VisibilityManager, "_ensure_current", lambda self: None)
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
context.visibility_manager.corners = [(50.0, 10.0), (80.0, -10.0)]
|
|
||||||
context.visibility_manager.corner_index = DummyCornerIndex()
|
assert path is not None
|
||||||
monkeypatch.setattr(
|
result = RoutingResult(net_id="test", path=path, reached_target=True)
|
||||||
type(context.cost_evaluator.collision_engine),
|
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
"ray_cast",
|
|
||||||
lambda self, origin, angle_deg, max_dist=2000.0, net_width=None: max_dist,
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
assert validation["connectivity_ok"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(
|
||||||
|
basic_evaluator,
|
||||||
|
build_problem(bounds=BOUNDS),
|
||||||
|
build_options(
|
||||||
|
min_straight_length=1.0,
|
||||||
|
max_straight_length=100.0,
|
||||||
|
),
|
||||||
|
max_cache_size=2,
|
||||||
)
|
)
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
targets = [Port(length, 0, 0) for length in range(10, 70, 10)]
|
||||||
|
|
||||||
emitted: list[tuple[str, tuple]] = []
|
for target in targets:
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
def fake_process_move(*args, **kwargs) -> None:
|
assert path is not None
|
||||||
emitted.append((args[9], args[10]))
|
assert path[-1].end_port == target
|
||||||
|
|
||||||
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
|
|
||||||
|
|
||||||
astar_module.expand_moves(
|
|
||||||
current,
|
|
||||||
Port(120, 20, 0),
|
|
||||||
net_width=2.0,
|
|
||||||
net_id="test",
|
|
||||||
open_set=[],
|
|
||||||
closed_set={},
|
|
||||||
context=context,
|
|
||||||
metrics=astar_module.AStarMetrics(),
|
|
||||||
congestion_cache={},
|
|
||||||
)
|
|
||||||
|
|
||||||
straight_lengths = {params[0] for move_class, params in emitted if move_class == "S"}
|
|
||||||
assert 40 in straight_lengths
|
|
||||||
assert 70 in straight_lengths
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import pytest
|
import pytest
|
||||||
import numpy
|
import numpy
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.geometry.components import Straight
|
from inire.geometry.components import Straight
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.astar import AStarContext
|
from inire import RoutingResult
|
||||||
from inire.router.pathfinder import PathFinder, RoutingResult
|
from inire.tests.support import build_pathfinder
|
||||||
|
|
||||||
def test_clearance_thresholds():
|
def test_clearance_thresholds():
|
||||||
"""
|
"""
|
||||||
|
|
@ -16,12 +16,12 @@ def test_clearance_thresholds():
|
||||||
"""
|
"""
|
||||||
# Clearance = 2.0, Width = 2.0
|
# Clearance = 2.0, Width = 2.0
|
||||||
# Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0
|
# Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0
|
||||||
ce = CollisionEngine(clearance=2.0)
|
ce = RoutingWorld(clearance=2.0)
|
||||||
|
|
||||||
# Net 1: Centerline at y=0
|
# Net 1: Centerline at y=0
|
||||||
p1 = Port(0, 0, 0)
|
p1 = Port(0, 0, 0)
|
||||||
res1 = Straight.generate(p1, 50.0, width=2.0, dilation=1.0)
|
res1 = Straight.generate(p1, 50.0, width=2.0, dilation=1.0)
|
||||||
ce.add_path("net1", res1.geometry, dilated_geometry=res1.dilated_geometry)
|
ce.add_path("net1", res1.collision_geometry, dilated_geometry=res1.dilated_collision_geometry)
|
||||||
|
|
||||||
# Net 2: Parallel to Net 1
|
# Net 2: Parallel to Net 1
|
||||||
# 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK.
|
# 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK.
|
||||||
|
|
@ -47,12 +47,10 @@ def test_verify_all_nets_cases():
|
||||||
"""
|
"""
|
||||||
Validate that verify_all_nets catches some common cases and doesn't flag reasonable non-failing cases.
|
Validate that verify_all_nets catches some common cases and doesn't flag reasonable non-failing cases.
|
||||||
"""
|
"""
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(collision_engine=engine, danger_map=danger_map)
|
evaluator = CostEvaluator(collision_engine=engine, danger_map=danger_map)
|
||||||
context = AStarContext(cost_evaluator=evaluator)
|
|
||||||
pf = PathFinder(context, warm_start=None, max_iterations=1)
|
|
||||||
|
|
||||||
# Case 1: Parallel paths exactly at clearance (Should be VALID)
|
# Case 1: Parallel paths exactly at clearance (Should be VALID)
|
||||||
netlist_parallel_ok = {
|
netlist_parallel_ok = {
|
||||||
|
|
@ -61,7 +59,14 @@ def test_verify_all_nets_cases():
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||||
|
|
||||||
results = pf.route_all(netlist_parallel_ok, net_widths)
|
results = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist_parallel_ok,
|
||||||
|
net_widths=net_widths,
|
||||||
|
warm_start=None,
|
||||||
|
max_iterations=1,
|
||||||
|
).route_all()
|
||||||
assert results["net1"].is_valid, f"Exactly at clearance should be valid, collisions={results['net1'].collisions}"
|
assert results["net1"].is_valid, f"Exactly at clearance should be valid, collisions={results['net1'].collisions}"
|
||||||
assert results["net2"].is_valid
|
assert results["net2"].is_valid
|
||||||
|
|
||||||
|
|
@ -74,7 +79,14 @@ def test_verify_all_nets_cases():
|
||||||
engine.remove_path("net1")
|
engine.remove_path("net1")
|
||||||
engine.remove_path("net2")
|
engine.remove_path("net2")
|
||||||
|
|
||||||
results_p = pf.route_all(netlist_parallel_fail, net_widths)
|
results_p = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist_parallel_fail,
|
||||||
|
net_widths=net_widths,
|
||||||
|
warm_start=None,
|
||||||
|
max_iterations=1,
|
||||||
|
).route_all()
|
||||||
# verify_all_nets should flag both as invalid because they cross-collide
|
# verify_all_nets should flag both as invalid because they cross-collide
|
||||||
assert not results_p["net3"].is_valid
|
assert not results_p["net3"].is_valid
|
||||||
assert not results_p["net4"].is_valid
|
assert not results_p["net4"].is_valid
|
||||||
|
|
@ -87,6 +99,13 @@ def test_verify_all_nets_cases():
|
||||||
engine.remove_path("net3")
|
engine.remove_path("net3")
|
||||||
engine.remove_path("net4")
|
engine.remove_path("net4")
|
||||||
|
|
||||||
results_c = pf.route_all(netlist_cross, net_widths)
|
results_c = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist_cross,
|
||||||
|
net_widths=net_widths,
|
||||||
|
warm_start=None,
|
||||||
|
max_iterations=1,
|
||||||
|
).route_all()
|
||||||
assert not results_c["net5"].is_valid
|
assert not results_c["net5"].is_valid
|
||||||
assert not results_c["net6"].is_valid
|
assert not results_c["net6"].is_valid
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.geometry.components import Straight
|
from inire.geometry.components import Straight
|
||||||
|
|
||||||
|
|
||||||
def test_collision_detection() -> None:
|
def test_collision_detection() -> None:
|
||||||
# Clearance = 2um
|
# Clearance = 2um
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
|
||||||
# 10x10 um obstacle at (10,10)
|
# 10x10 um obstacle at (10,10)
|
||||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||||
|
|
@ -33,7 +33,7 @@ def test_collision_detection() -> None:
|
||||||
def test_safety_zone() -> None:
|
def test_safety_zone() -> None:
|
||||||
# Use zero clearance for this test to verify the 2nm port safety zone
|
# Use zero clearance for this test to verify the 2nm port safety zone
|
||||||
# against the physical obstacle boundary.
|
# against the physical obstacle boundary.
|
||||||
engine = CollisionEngine(clearance=0.0)
|
engine = RoutingWorld(clearance=0.0)
|
||||||
|
|
||||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||||
engine.add_static_obstacle(obstacle)
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
@ -50,7 +50,7 @@ def test_safety_zone() -> None:
|
||||||
|
|
||||||
def test_configurable_max_net_width() -> None:
|
def test_configurable_max_net_width() -> None:
|
||||||
# Large max_net_width (10.0) -> large pre-dilation (6.0)
|
# Large max_net_width (10.0) -> large pre-dilation (6.0)
|
||||||
engine = CollisionEngine(clearance=2.0, max_net_width=10.0)
|
engine = RoutingWorld(clearance=2.0, max_net_width=10.0)
|
||||||
|
|
||||||
obstacle = Polygon([(20, 20), (25, 20), (25, 25), (20, 25)])
|
obstacle = Polygon([(20, 20), (25, 20), (25, 25), (20, 25)])
|
||||||
engine.add_static_obstacle(obstacle)
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
@ -65,7 +65,7 @@ def test_configurable_max_net_width() -> None:
|
||||||
def test_ray_cast_width_clearance() -> None:
|
def test_ray_cast_width_clearance() -> None:
|
||||||
# Clearance = 2.0um, Width = 2.0um.
|
# Clearance = 2.0um, Width = 2.0um.
|
||||||
# Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um.
|
# Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um.
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
|
||||||
# Obstacle at x=10 to 20
|
# Obstacle at x=10 to 20
|
||||||
obstacle = Polygon([(10, 0), (20, 0), (20, 100), (10, 100)])
|
obstacle = Polygon([(10, 0), (20, 0), (20, 100), (10, 100)])
|
||||||
|
|
@ -83,7 +83,7 @@ def test_ray_cast_width_clearance() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_check_move_static_clearance() -> None:
|
def test_check_move_static_clearance() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
obstacle = Polygon([(10, 0), (20, 0), (20, 100), (10, 100)])
|
obstacle = Polygon([(10, 0), (20, 0), (20, 100), (10, 100)])
|
||||||
engine.add_static_obstacle(obstacle)
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
|
@ -103,3 +103,54 @@ def test_check_move_static_clearance() -> None:
|
||||||
start_exact = Port(7, 0, 90)
|
start_exact = Port(7, 0, 90)
|
||||||
res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0)
|
res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0)
|
||||||
assert not engine.check_move_static(res_exact, start_port=start_exact, net_width=2.0)
|
assert not engine.check_move_static(res_exact, start_port=start_exact, net_width=2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_path_report_preserves_long_net_id() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789"
|
||||||
|
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
geoms = [poly for component in path for poly in component.collision_geometry]
|
||||||
|
dilated = [poly for component in path for poly in component.dilated_collision_geometry]
|
||||||
|
|
||||||
|
engine.add_path(net_id, geoms, dilated_geometry=dilated)
|
||||||
|
report = engine.verify_path_report(net_id, path)
|
||||||
|
|
||||||
|
assert report.dynamic_collision_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_"
|
||||||
|
net_a = f"{shared_prefix}A"
|
||||||
|
net_b = f"{shared_prefix}B"
|
||||||
|
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
|
||||||
|
engine.add_path(
|
||||||
|
net_a,
|
||||||
|
[poly for component in path_a for poly in component.collision_geometry],
|
||||||
|
dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
engine.add_path(
|
||||||
|
net_b,
|
||||||
|
[poly for component in path_b for poly in component.collision_geometry],
|
||||||
|
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
report = engine.verify_path_report(net_a, path_a)
|
||||||
|
|
||||||
|
assert report.dynamic_collision_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_path_clears_dynamic_path() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
geoms = [poly for component in path for poly in component.collision_geometry]
|
||||||
|
dilated = [poly for component in path for poly in component.dilated_collision_geometry]
|
||||||
|
|
||||||
|
engine.add_path("netA", geoms, dilated_geometry=dilated)
|
||||||
|
assert {net_id for net_id, _ in engine.iter_dynamic_paths()} == {"netA"}
|
||||||
|
|
||||||
|
engine.remove_path("netA")
|
||||||
|
assert list(engine.iter_dynamic_paths()) == []
|
||||||
|
assert len(engine._static_obstacles.geometries) == 0
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from dataclasses import FrozenInstanceError
|
||||||
from shapely.affinity import rotate as shapely_rotate
|
from shapely.affinity import rotate as shapely_rotate
|
||||||
from shapely.affinity import scale as shapely_scale
|
from shapely.affinity import scale as shapely_scale
|
||||||
from shapely.affinity import translate as shapely_translate
|
from shapely.affinity import translate as shapely_translate
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.components import Bend90, SBend, Straight
|
from inire.geometry.components import Bend90, SBend, Straight
|
||||||
from inire.geometry.primitives import Port, rotate_port, translate_port
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
def test_straight_generation() -> None:
|
def test_straight_generation() -> None:
|
||||||
|
|
@ -16,15 +17,16 @@ def test_straight_generation() -> None:
|
||||||
|
|
||||||
assert result.end_port.x == 10.0
|
assert result.end_port.x == 10.0
|
||||||
assert result.end_port.y == 0.0
|
assert result.end_port.y == 0.0
|
||||||
assert result.end_port.orientation == 0.0
|
assert result.end_port.r == 0.0
|
||||||
assert len(result.geometry) == 1
|
assert len(result.collision_geometry) == 1
|
||||||
|
|
||||||
# Bounds of the polygon
|
# Bounds of the polygon
|
||||||
minx, miny, maxx, maxy = result.geometry[0].bounds
|
minx, miny, maxx, maxy = result.collision_geometry[0].bounds
|
||||||
assert minx == 0.0
|
assert minx == 0.0
|
||||||
assert maxx == 10.0
|
assert maxx == 10.0
|
||||||
assert miny == -1.0
|
assert miny == -1.0
|
||||||
assert maxy == 1.0
|
assert maxy == 1.0
|
||||||
|
assert isinstance(result.collision_geometry, tuple)
|
||||||
|
|
||||||
|
|
||||||
def test_bend90_generation() -> None:
|
def test_bend90_generation() -> None:
|
||||||
|
|
@ -36,13 +38,13 @@ def test_bend90_generation() -> None:
|
||||||
result_cw = Bend90.generate(start, radius, width, direction="CW")
|
result_cw = Bend90.generate(start, radius, width, direction="CW")
|
||||||
assert result_cw.end_port.x == 10.0
|
assert result_cw.end_port.x == 10.0
|
||||||
assert result_cw.end_port.y == -10.0
|
assert result_cw.end_port.y == -10.0
|
||||||
assert result_cw.end_port.orientation == 270.0
|
assert result_cw.end_port.r == 270.0
|
||||||
|
|
||||||
# CCW bend
|
# CCW bend
|
||||||
result_ccw = Bend90.generate(start, radius, width, direction="CCW")
|
result_ccw = Bend90.generate(start, radius, width, direction="CCW")
|
||||||
assert result_ccw.end_port.x == 10.0
|
assert result_ccw.end_port.x == 10.0
|
||||||
assert result_ccw.end_port.y == 10.0
|
assert result_ccw.end_port.y == 10.0
|
||||||
assert result_ccw.end_port.orientation == 90.0
|
assert result_ccw.end_port.r == 90.0
|
||||||
|
|
||||||
|
|
||||||
def test_sbend_generation() -> None:
|
def test_sbend_generation() -> None:
|
||||||
|
|
@ -53,8 +55,8 @@ def test_sbend_generation() -> None:
|
||||||
|
|
||||||
result = SBend.generate(start, offset, radius, width)
|
result = SBend.generate(start, offset, radius, width)
|
||||||
assert result.end_port.y == 5.0
|
assert result.end_port.y == 5.0
|
||||||
assert result.end_port.orientation == 0.0
|
assert result.end_port.r == 0.0
|
||||||
assert len(result.geometry) == 2 # Optimization: returns individual arcs
|
assert len(result.collision_geometry) == 2 # Optimization: returns individual arcs
|
||||||
|
|
||||||
# Verify failure for large offset
|
# Verify failure for large offset
|
||||||
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||||
|
|
@ -70,7 +72,7 @@ def test_sbend_generation_negative_offset_keeps_second_arc_below_centerline() ->
|
||||||
result = SBend.generate(start, offset, radius, width)
|
result = SBend.generate(start, offset, radius, width)
|
||||||
|
|
||||||
assert result.end_port.y == -5.0
|
assert result.end_port.y == -5.0
|
||||||
second_arc_minx, second_arc_miny, second_arc_maxx, second_arc_maxy = result.geometry[1].bounds
|
second_arc_minx, second_arc_miny, second_arc_maxx, second_arc_maxy = result.collision_geometry[1].bounds
|
||||||
assert second_arc_maxy <= width / 2.0 + 1e-6
|
assert second_arc_maxy <= width / 2.0 + 1e-6
|
||||||
assert second_arc_miny < -width / 2.0
|
assert second_arc_miny < -width / 2.0
|
||||||
|
|
||||||
|
|
@ -84,21 +86,21 @@ def test_bend_collision_models() -> None:
|
||||||
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
||||||
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||||
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
||||||
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
minx, miny, maxx, maxy = res_bbox.collision_geometry[0].bounds
|
||||||
assert minx <= 0.0 + 1e-6
|
assert minx <= 0.0 + 1e-6
|
||||||
assert maxx >= 10.0 - 1e-6
|
assert maxx >= 10.0 - 1e-6
|
||||||
assert miny <= 0.0 + 1e-6
|
assert miny <= 0.0 + 1e-6
|
||||||
assert maxy >= 10.0 - 1e-6
|
assert maxy >= 10.0 - 1e-6
|
||||||
|
|
||||||
# 2. Clipped BBox model
|
# 2. Clipped BBox model
|
||||||
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0)
|
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox")
|
||||||
# Conservative 8-point approximation should still be tighter than the full bbox.
|
# Conservative 8-point approximation should still be tighter than the full bbox.
|
||||||
assert len(res_clipped.geometry[0].exterior.coords) - 1 == 8
|
assert len(res_clipped.collision_geometry[0].exterior.coords) - 1 == 8
|
||||||
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
|
assert res_clipped.collision_geometry[0].area < res_bbox.collision_geometry[0].area
|
||||||
|
|
||||||
# It should also conservatively contain the true arc.
|
# It should also conservatively contain the true arc.
|
||||||
res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc")
|
res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc")
|
||||||
assert res_clipped.geometry[0].covers(res_arc.geometry[0])
|
assert res_clipped.collision_geometry[0].covers(res_arc.collision_geometry[0])
|
||||||
|
|
||||||
|
|
||||||
def test_custom_bend_collision_polygon_uses_local_transform() -> None:
|
def test_custom_bend_collision_polygon_uses_local_transform() -> None:
|
||||||
|
|
@ -119,20 +121,18 @@ def test_custom_bend_collision_polygon_uses_local_transform() -> None:
|
||||||
expected = shapely_rotate(expected, rotation_deg, origin=(0.0, 0.0), use_radians=False)
|
expected = shapely_rotate(expected, rotation_deg, origin=(0.0, 0.0), use_radians=False)
|
||||||
expected = shapely_translate(expected, center_xy[0], center_xy[1])
|
expected = shapely_translate(expected, center_xy[0], center_xy[1])
|
||||||
|
|
||||||
assert result.geometry[0].symmetric_difference(expected).area < 1e-6
|
assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6
|
||||||
assert result.actual_geometry is not None
|
assert result.physical_geometry[0].symmetric_difference(expected).area < 1e-6
|
||||||
assert result.actual_geometry[0].symmetric_difference(expected).area < 1e-6
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_bend_collision_polygon_becomes_actual_geometry() -> None:
|
def test_custom_bend_collision_polygon_keeps_collision_and_physical_geometry_aligned() -> None:
|
||||||
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
||||||
result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_poly, dilation=1.0)
|
result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_poly, dilation=1.0)
|
||||||
|
|
||||||
assert result.actual_geometry is not None
|
assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area < 1e-6
|
||||||
assert result.dilated_actual_geometry is not None
|
assert result.dilated_collision_geometry is not None
|
||||||
assert result.geometry[0].symmetric_difference(result.actual_geometry[0]).area < 1e-6
|
assert result.dilated_physical_geometry is not None
|
||||||
assert result.dilated_geometry is not None
|
assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area < 1e-6
|
||||||
assert result.dilated_geometry[0].symmetric_difference(result.dilated_actual_geometry[0]).area < 1e-6
|
|
||||||
|
|
||||||
|
|
||||||
def test_sbend_collision_models() -> None:
|
def test_sbend_collision_models() -> None:
|
||||||
|
|
@ -143,11 +143,11 @@ def test_sbend_collision_models() -> None:
|
||||||
|
|
||||||
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
||||||
# Geometry should be a list of individual bbox polygons for each arc
|
# Geometry should be a list of individual bbox polygons for each arc
|
||||||
assert len(res_bbox.geometry) == 2
|
assert len(res_bbox.collision_geometry) == 2
|
||||||
|
|
||||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||||
area_bbox = sum(p.area for p in res_bbox.geometry)
|
area_bbox = sum(p.area for p in res_bbox.collision_geometry)
|
||||||
area_arc = sum(p.area for p in res_arc.geometry)
|
area_arc = sum(p.area for p in res_arc.collision_geometry)
|
||||||
assert area_bbox > area_arc
|
assert area_bbox > area_arc
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -161,14 +161,14 @@ def test_sbend_continuity() -> None:
|
||||||
res = SBend.generate(start, offset, radius, width)
|
res = SBend.generate(start, offset, radius, width)
|
||||||
|
|
||||||
# Target orientation should be same as start
|
# Target orientation should be same as start
|
||||||
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
assert abs(res.end_port.r - 90.0) < 1e-6
|
||||||
|
|
||||||
# For a port at 90 deg, +offset is a shift in -x direction
|
# For a port at 90 deg, +offset is a shift in -x direction
|
||||||
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||||
|
|
||||||
# Geometry should be a list of valid polygons
|
# Geometry should be a list of valid polygons
|
||||||
assert len(res.geometry) == 2
|
assert len(res.collision_geometry) == 2
|
||||||
for p in res.geometry:
|
for p in res.collision_geometry:
|
||||||
assert p.is_valid
|
assert p.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -185,8 +185,8 @@ def test_arc_sagitta_precision() -> None:
|
||||||
|
|
||||||
# Number of segments should be significantly higher for fine
|
# Number of segments should be significantly higher for fine
|
||||||
# Exterior points = (segments + 1) * 2
|
# Exterior points = (segments + 1) * 2
|
||||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
pts_coarse = len(res_coarse.collision_geometry[0].exterior.coords)
|
||||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
pts_fine = len(res_fine.collision_geometry[0].exterior.coords)
|
||||||
|
|
||||||
assert pts_fine > pts_coarse * 2
|
assert pts_fine > pts_coarse * 2
|
||||||
|
|
||||||
|
|
@ -205,12 +205,19 @@ def test_component_transform_invariance() -> None:
|
||||||
angle = 90.0
|
angle = 90.0
|
||||||
|
|
||||||
# 1. Transform the generated geometry
|
# 1. Transform the generated geometry
|
||||||
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
p_end_transformed = res0.end_port.translate(dx, dy).rotated(angle)
|
||||||
|
|
||||||
# 2. Generate at transformed start
|
# 2. Generate at transformed start
|
||||||
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
start_transformed = start0.translate(dx, dy).rotated(angle)
|
||||||
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
||||||
|
|
||||||
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
||||||
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
||||||
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
assert abs(res_transformed.end_port.r - p_end_transformed.r) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_component_result_is_immutable_value_type() -> None:
|
||||||
|
result = Straight.generate(Port(0, 0, 0), 10.0, 2.0)
|
||||||
|
|
||||||
|
with pytest.raises(FrozenInstanceError):
|
||||||
|
result.length = 42.0
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
import pytest
|
import pytest
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext, route_astar
|
from inire.router._router import PathFinder
|
||||||
|
from inire.router._search import route_astar
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import PathFinder
|
from inire.tests.support import build_context, build_pathfinder
|
||||||
|
|
||||||
|
BOUNDS = (0, -40, 100, 40)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def basic_evaluator() -> CostEvaluator:
|
def basic_evaluator() -> CostEvaluator:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
# Wider bounds to allow going around (y from -40 to 40)
|
# Wider bounds to allow going around (y from -40 to 40)
|
||||||
danger_map = DangerMap(bounds=(0, -40, 100, 40))
|
danger_map = DangerMap(bounds=BOUNDS)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
|
||||||
|
|
||||||
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator, sbend_offsets=[2.0, 5.0])
|
context = build_context(basic_evaluator, bounds=BOUNDS, sbend_offsets=[2.0, 5.0])
|
||||||
# Start at (0,0), target at (50, 2) -> 2um lateral offset
|
# Start at (0,0), target at (50, 2) -> 2um lateral offset
|
||||||
# This matches one of our discretized SBend offsets.
|
# This matches one of our discretized SBend offsets.
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
|
|
@ -32,22 +35,27 @@ def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||||
for res in path:
|
for res in path:
|
||||||
# Check if the end port orientation is same as start
|
# Check if the end port orientation is same as start
|
||||||
# and it's not a single straight (which would have y=0)
|
# and it's not a single straight (which would have y=0)
|
||||||
if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.orientation - start.orientation) < 0.1:
|
if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.r - start.r) < 0.1:
|
||||||
found_sbend = True
|
found_sbend = True
|
||||||
break
|
break
|
||||||
assert found_sbend
|
assert found_sbend
|
||||||
|
|
||||||
|
|
||||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
|
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator, bend_radii=[5.0, 10.0])
|
|
||||||
# Increase base penalty to force detour immediately
|
|
||||||
pf = PathFinder(context, max_iterations=10, base_congestion_penalty=1000.0)
|
|
||||||
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||||
"net2": (Port(0, 10, 0), Port(50, 10, 0)),
|
"net2": (Port(0, 10, 0), Port(50, 10, 0)),
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||||
|
pf = build_pathfinder(
|
||||||
|
basic_evaluator,
|
||||||
|
bounds=BOUNDS,
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths=net_widths,
|
||||||
|
bend_radii=[5.0, 10.0],
|
||||||
|
max_iterations=10,
|
||||||
|
base_penalty=1000.0,
|
||||||
|
)
|
||||||
|
|
||||||
# Force them into a narrow corridor that only fits ONE.
|
# Force them into a narrow corridor that only fits ONE.
|
||||||
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
|
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
|
||||||
|
|
@ -57,7 +65,7 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvalua
|
||||||
basic_evaluator.collision_engine.add_static_obstacle(obs_bottom)
|
basic_evaluator.collision_engine.add_static_obstacle(obs_bottom)
|
||||||
basic_evaluator.danger_map.precompute([obs_top, obs_bottom])
|
basic_evaluator.danger_map.precompute([obs_top, obs_bottom])
|
||||||
|
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all()
|
||||||
|
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
assert results["net1"].reached_target
|
assert results["net1"].reached_target
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
|
|
||||||
|
|
||||||
def test_cost_calculation() -> None:
|
def test_cost_calculation() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
# 50x50 um area, 1um resolution
|
# 50x50 um area, 1um resolution
|
||||||
danger_map = DangerMap(bounds=(0, 0, 50, 50))
|
danger_map = DangerMap(bounds=(0, 0, 50, 50))
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
|
|
@ -61,7 +61,23 @@ def test_danger_map_kd_tree_and_cache() -> None:
|
||||||
# We can check if calling it again is fast or just verify it returns same result
|
# We can check if calling it again is fast or just verify it returns same result
|
||||||
cost_near_2 = dm.get_cost(100.5, 100.5)
|
cost_near_2 = dm.get_cost(100.5, 100.5)
|
||||||
assert cost_near_2 == cost_near
|
assert cost_near_2 == cost_near
|
||||||
|
assert len(dm._cost_cache) == 2
|
||||||
|
|
||||||
# 4. Out of bounds
|
# 4. Out of bounds
|
||||||
assert dm.get_cost(-1, -1) >= 1e12
|
assert dm.get_cost(-1, -1) >= 1e12
|
||||||
|
|
||||||
|
|
||||||
|
def test_danger_map_cache_is_instance_local_and_cleared_on_precompute() -> None:
|
||||||
|
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||||
|
dm_a = DangerMap((0, 0, 100, 100))
|
||||||
|
dm_b = DangerMap((0, 0, 100, 100))
|
||||||
|
|
||||||
|
dm_a.precompute([obstacle])
|
||||||
|
dm_b.precompute([])
|
||||||
|
|
||||||
|
dm_a.get_cost(15.0, 15.0)
|
||||||
|
assert len(dm_a._cost_cache) == 1
|
||||||
|
assert len(dm_b._cost_cache) == 0
|
||||||
|
|
||||||
|
dm_a.precompute([])
|
||||||
|
assert len(dm_a._cost_cache) == 0
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ REGRESSION_FACTOR = 1.5
|
||||||
BASELINE_SECONDS = {
|
BASELINE_SECONDS = {
|
||||||
"example_01_simple_route": 0.0035,
|
"example_01_simple_route": 0.0035,
|
||||||
"example_02_congestion_resolution": 0.2666,
|
"example_02_congestion_resolution": 0.2666,
|
||||||
"example_03_locked_paths": 0.2304,
|
"example_03_locked_routes": 0.2304,
|
||||||
"example_04_sbends_and_radii": 1.8734,
|
"example_04_sbends_and_radii": 1.8734,
|
||||||
"example_05_orientation_stress": 0.5630,
|
"example_05_orientation_stress": 0.5630,
|
||||||
"example_06_bend_collision_models": 5.2382,
|
"example_06_bend_collision_models": 5.2382,
|
||||||
|
|
@ -28,7 +28,7 @@ BASELINE_SECONDS = {
|
||||||
EXPECTED_OUTCOMES = {
|
EXPECTED_OUTCOMES = {
|
||||||
"example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1},
|
"example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1},
|
||||||
"example_02_congestion_resolution": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
"example_02_congestion_resolution": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
||||||
"example_03_locked_paths": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
|
"example_03_locked_routes": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
|
||||||
"example_04_sbends_and_radii": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
|
"example_04_sbends_and_radii": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
|
||||||
"example_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
"example_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
||||||
"example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
"example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.astar import AStarContext
|
|
||||||
from inire.router.pathfinder import PathFinder
|
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.tests.support import build_pathfinder
|
||||||
|
|
||||||
def test_failed_net_visibility() -> None:
|
def test_failed_net_visibility() -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -12,7 +11,7 @@ def test_failed_net_visibility() -> None:
|
||||||
for negotiated congestion.
|
for negotiated congestion.
|
||||||
"""
|
"""
|
||||||
# 1. Setup
|
# 1. Setup
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
|
||||||
# Create a simple danger map (bounds 0-100)
|
# Create a simple danger map (bounds 0-100)
|
||||||
# We don't strictly need obstacles in it for this test.
|
# We don't strictly need obstacles in it for this test.
|
||||||
|
|
@ -32,20 +31,23 @@ def test_failed_net_visibility() -> None:
|
||||||
|
|
||||||
# With obstacle, direct jump fails. A* must search around.
|
# With obstacle, direct jump fails. A* must search around.
|
||||||
# Limit=10 should be enough to fail to find a path around.
|
# Limit=10 should be enough to fail to find a path around.
|
||||||
context = AStarContext(evaluator, node_limit=10)
|
|
||||||
|
|
||||||
# 3. Configure PathFinder
|
|
||||||
# max_iterations=1 because we only need to check the state after the first attempt.
|
|
||||||
pf = PathFinder(context, max_iterations=1, warm_start=None)
|
|
||||||
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"net1": (Port(0, 0, 0), Port(100, 0, 0))
|
"net1": (Port(0, 0, 0), Port(100, 0, 0))
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 1.0}
|
net_widths = {"net1": 1.0}
|
||||||
|
pf = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths=net_widths,
|
||||||
|
node_limit=10,
|
||||||
|
max_iterations=1,
|
||||||
|
warm_start=None,
|
||||||
|
)
|
||||||
|
|
||||||
# 4. Route
|
# 4. Route
|
||||||
print("\nStarting Route...")
|
print("\nStarting Route...")
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all()
|
||||||
|
|
||||||
res = results["net1"]
|
res = results["net1"]
|
||||||
print(f"Result: is_valid={res.is_valid}, reached={res.reached_target}, path_len={len(res.path)}")
|
print(f"Result: is_valid={res.is_valid}, reached={res.reached_target}, path_len={len(res.path)}")
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ import pytest
|
||||||
from hypothesis import given, settings, strategies as st
|
from hypothesis import given, settings, strategies as st
|
||||||
from shapely.geometry import Point, Polygon
|
from shapely.geometry import Point, Polygon
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext, route_astar
|
from inire.router._search import route_astar
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.tests.support import build_context
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
@st.composite
|
||||||
|
|
@ -39,7 +40,7 @@ def _port_has_required_clearance(port: Port, obstacles: list[Polygon], clearance
|
||||||
def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port) -> None:
|
def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port) -> None:
|
||||||
net_width = 2.0
|
net_width = 2.0
|
||||||
clearance = 2.0
|
clearance = 2.0
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
for obs in obstacles:
|
for obs in obstacles:
|
||||||
engine.add_static_obstacle(obs)
|
engine.add_static_obstacle(obs)
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port
|
||||||
danger_map.precompute(obstacles)
|
danger_map.precompute(obstacles)
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map)
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
context = AStarContext(evaluator, node_limit=5000) # Lower limit for fuzzing stability
|
context = build_context(evaluator, bounds=(0, 0, 30, 30), node_limit=5000) # Lower limit for fuzzing stability
|
||||||
|
|
||||||
# Check if start/target are inside obstacles (safety zone check)
|
# Check if start/target are inside obstacles (safety zone check)
|
||||||
# The router should handle this gracefully (either route or return None)
|
# The router should handle this gracefully (either route or return None)
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,57 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire import NetSpec
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.components import Bend90, Straight
|
from inire.geometry.components import Bend90, Straight
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext
|
from inire.router._astar_types import AStarContext
|
||||||
|
from inire.router._router import PathFinder
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.outcomes import RoutingOutcome
|
from inire.tests.support import build_context
|
||||||
from inire.router.pathfinder import PathFinder, RoutingResult
|
|
||||||
from inire.router.session import (
|
DEFAULT_BOUNDS = (0, 0, 100, 100)
|
||||||
create_routing_session_state,
|
|
||||||
prepare_routing_session_state,
|
|
||||||
run_routing_iteration,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def basic_evaluator() -> CostEvaluator:
|
def basic_evaluator() -> CostEvaluator:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
danger_map = DangerMap(bounds=DEFAULT_BOUNDS)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
return CostEvaluator(engine, danger_map)
|
return CostEvaluator(engine, danger_map)
|
||||||
|
|
||||||
|
|
||||||
|
def _request_nets(
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
net_widths: dict[str, float],
|
||||||
|
) -> tuple[NetSpec, ...]:
|
||||||
|
return tuple(
|
||||||
|
NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0))
|
||||||
|
for net_id, (start, target) in netlist.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_pathfinder(
|
||||||
|
evaluator: CostEvaluator,
|
||||||
|
*,
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
net_widths: dict[str, float],
|
||||||
|
bounds: tuple[float, float, float, float] = DEFAULT_BOUNDS,
|
||||||
|
metrics=None,
|
||||||
|
**request_overrides: object,
|
||||||
|
) -> PathFinder:
|
||||||
|
return PathFinder(
|
||||||
|
build_context(
|
||||||
|
evaluator,
|
||||||
|
bounds=bounds,
|
||||||
|
nets=_request_nets(netlist, net_widths),
|
||||||
|
**request_overrides,
|
||||||
|
),
|
||||||
|
metrics=metrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_manual_path(start: Port, width: float, clearance: float, steps: list[tuple[str, float | str]]) -> list:
|
def _build_manual_path(start: Port, width: float, clearance: float, steps: list[tuple[str, float | str]]) -> list:
|
||||||
path = []
|
path = []
|
||||||
curr = start
|
curr = start
|
||||||
|
|
@ -37,17 +66,22 @@ def _build_manual_path(start: Port, width: float, clearance: float, steps: list[
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> None:
|
def _path_signature(path: list) -> list[tuple[str, tuple[int, int, int], tuple[int, int, int]]]:
|
||||||
context = AStarContext(basic_evaluator)
|
return [
|
||||||
pf = PathFinder(context)
|
(component.move_type, component.start_port.as_tuple(), component.end_port.as_tuple())
|
||||||
|
for component in path
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> None:
|
||||||
netlist = {
|
netlist = {
|
||||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||||
"net2": (Port(0, 10, 0), Port(50, 10, 0)),
|
"net2": (Port(0, 10, 0), Port(50, 10, 0)),
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||||
|
pf = _build_pathfinder(basic_evaluator, netlist=netlist, net_widths=net_widths)
|
||||||
|
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all()
|
||||||
|
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
assert results["net1"].is_valid
|
assert results["net1"].is_valid
|
||||||
|
|
@ -57,10 +91,6 @@ def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
|
def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(basic_evaluator)
|
|
||||||
# Force a crossing by setting low iterations and low penalty
|
|
||||||
pf = PathFinder(context, max_iterations=1, base_congestion_penalty=1.0, warm_start=None)
|
|
||||||
|
|
||||||
# Net 1: (0, 25) -> (100, 25) Horizontal
|
# Net 1: (0, 25) -> (100, 25) Horizontal
|
||||||
# Net 2: (50, 0) -> (50, 50) Vertical
|
# Net 2: (50, 0) -> (50, 50) Vertical
|
||||||
netlist = {
|
netlist = {
|
||||||
|
|
@ -68,8 +98,16 @@ def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
|
||||||
"net2": (Port(50, 0, 90), Port(50, 50, 90)),
|
"net2": (Port(50, 0, 90), Port(50, 50, 90)),
|
||||||
}
|
}
|
||||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||||
|
pf = _build_pathfinder(
|
||||||
|
basic_evaluator,
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths=net_widths,
|
||||||
|
max_iterations=1,
|
||||||
|
base_penalty=1.0,
|
||||||
|
warm_start=None,
|
||||||
|
)
|
||||||
|
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all()
|
||||||
|
|
||||||
# Both should be invalid because they cross
|
# Both should be invalid because they cross
|
||||||
assert not results["net1"].is_valid
|
assert not results["net1"].is_valid
|
||||||
|
|
@ -78,231 +116,211 @@ def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
|
||||||
assert results["net2"].collisions > 0
|
assert results["net2"].collisions > 0
|
||||||
|
|
||||||
|
|
||||||
def test_prepare_routing_session_state_builds_warm_start_and_sorts_nets(
|
def test_route_all_respects_requested_net_order_in_callback(
|
||||||
basic_evaluator: CostEvaluator,
|
basic_evaluator: CostEvaluator,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
context = AStarContext(basic_evaluator)
|
callback_orders: list[list[str]] = []
|
||||||
pf = PathFinder(context)
|
|
||||||
calls: list[tuple[str, list[str]]] = []
|
|
||||||
cleared: list[bool] = []
|
|
||||||
|
|
||||||
def fake_build(
|
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
|
||||||
net_widths: dict[str, float],
|
|
||||||
order: str,
|
|
||||||
) -> dict[str, list]:
|
|
||||||
calls.append((order, list(netlist.keys())))
|
|
||||||
return {"warm": []}
|
|
||||||
|
|
||||||
monkeypatch.setattr(PathFinder, "_build_greedy_warm_start_paths", lambda self, netlist, net_widths, order: fake_build(netlist, net_widths, order))
|
|
||||||
monkeypatch.setattr(AStarContext, "clear_static_caches", lambda self: cleared.append(True))
|
|
||||||
|
|
||||||
netlist = {
|
netlist = {
|
||||||
"short": (Port(0, 0, 0), Port(10, 0, 0)),
|
"short": (Port(0, 0, 0), Port(10, 0, 0)),
|
||||||
"long": (Port(0, 0, 0), Port(40, 10, 0)),
|
"long": (Port(0, 0, 0), Port(40, 10, 0)),
|
||||||
"mid": (Port(0, 0, 0), Port(20, 0, 0)),
|
"mid": (Port(0, 0, 0), Port(20, 0, 0)),
|
||||||
}
|
}
|
||||||
state = create_routing_session_state(
|
pf = _build_pathfinder(
|
||||||
pf,
|
basic_evaluator,
|
||||||
netlist,
|
netlist=netlist,
|
||||||
{net_id: 2.0 for net_id in netlist},
|
net_widths={net_id: 2.0 for net_id in netlist},
|
||||||
store_expanded=False,
|
max_iterations=1,
|
||||||
iteration_callback=None,
|
warm_start=None,
|
||||||
shuffle_nets=False,
|
|
||||||
sort_nets="longest",
|
sort_nets="longest",
|
||||||
initial_paths=None,
|
enabled=False,
|
||||||
seed=None,
|
)
|
||||||
|
pf.route_all(
|
||||||
|
iteration_callback=lambda iteration, results: callback_orders.append(list(results)),
|
||||||
)
|
)
|
||||||
|
|
||||||
prepare_routing_session_state(pf, state)
|
assert callback_orders == [["long", "mid", "short"]]
|
||||||
|
|
||||||
assert calls == [("longest", ["short", "long", "mid"])]
|
|
||||||
assert cleared == [True]
|
|
||||||
assert state.initial_paths == {"warm": []}
|
|
||||||
assert state.all_net_ids == ["long", "mid", "short"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_routing_iteration_updates_results_and_invokes_callback(
|
def test_route_all_invokes_iteration_callback_with_results(
|
||||||
basic_evaluator: CostEvaluator,
|
basic_evaluator: CostEvaluator,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
context = AStarContext(basic_evaluator)
|
callback_results: list[dict[str, object]] = []
|
||||||
pf = PathFinder(context)
|
netlist = {
|
||||||
callback_results: list[dict[str, RoutingResult]] = []
|
"net1": (Port(0, 0, 0), Port(10, 0, 0)),
|
||||||
|
"net2": (Port(0, 10, 0), Port(10, 10, 0)),
|
||||||
|
}
|
||||||
|
pf = _build_pathfinder(
|
||||||
|
basic_evaluator,
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths={"net1": 2.0, "net2": 2.0},
|
||||||
|
)
|
||||||
|
|
||||||
def fake_route_once(
|
results = pf.route_all(
|
||||||
net_id: str,
|
iteration_callback=lambda iteration, iteration_results: callback_results.append(dict(iteration_results)),
|
||||||
start: Port,
|
)
|
||||||
target: Port,
|
|
||||||
width: float,
|
assert len(callback_results) == 1
|
||||||
iteration: int,
|
assert set(callback_results[0]) == {"net1", "net2"}
|
||||||
initial_paths: dict[str, list] | None,
|
assert callback_results[0]["net1"].is_valid
|
||||||
store_expanded: bool,
|
assert callback_results[0]["net2"].is_valid
|
||||||
needs_self_collision_check: set[str],
|
assert results["net1"].reached_target
|
||||||
) -> tuple[RoutingResult, RoutingOutcome]:
|
assert results["net2"].reached_target
|
||||||
_ = (start, target, width, iteration, initial_paths, store_expanded, needs_self_collision_check)
|
|
||||||
result = RoutingResult(
|
|
||||||
net_id,
|
def test_route_all_uses_complete_initial_paths_without_rerouting(
|
||||||
[],
|
basic_evaluator: CostEvaluator,
|
||||||
net_id == "net1",
|
) -> None:
|
||||||
int(net_id == "net2"),
|
start = Port(0, 0, 0)
|
||||||
reached_target=True,
|
target = Port(20, 20, 0)
|
||||||
outcome="completed" if net_id == "net1" else "colliding",
|
initial_path = _build_manual_path(
|
||||||
|
start,
|
||||||
|
2.0,
|
||||||
|
basic_evaluator.collision_engine.clearance,
|
||||||
|
[("S", 10.0), ("B", "CCW"), ("S", 10.0), ("B", "CW")],
|
||||||
|
)
|
||||||
|
pf = _build_pathfinder(
|
||||||
|
basic_evaluator,
|
||||||
|
netlist={"net": (start, target)},
|
||||||
|
net_widths={"net": 2.0},
|
||||||
|
bend_radii=[5.0],
|
||||||
|
max_iterations=1,
|
||||||
|
warm_start=None,
|
||||||
|
initial_paths={"net": tuple(initial_path)},
|
||||||
|
enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pf.route_all()["net"]
|
||||||
|
|
||||||
|
assert result.is_valid
|
||||||
|
assert result.reached_target
|
||||||
|
assert _path_signature(result.path) == _path_signature(initial_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_all_retries_partial_initial_paths_across_iterations(
|
||||||
|
basic_evaluator: CostEvaluator,
|
||||||
|
) -> None:
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(10, 0, 0)
|
||||||
|
partial_path = [Straight.generate(start, 5.0, 2.0, dilation=basic_evaluator.collision_engine.clearance / 2.0)]
|
||||||
|
pf = _build_pathfinder(
|
||||||
|
basic_evaluator,
|
||||||
|
netlist={"net": (start, target)},
|
||||||
|
net_widths={"net": 2.0},
|
||||||
|
max_iterations=2,
|
||||||
|
warm_start=None,
|
||||||
|
capture_expanded=True,
|
||||||
|
initial_paths={"net": tuple(partial_path)},
|
||||||
|
enabled=False,
|
||||||
|
)
|
||||||
|
iterations: list[int] = []
|
||||||
|
|
||||||
|
result = pf.route_all(iteration_callback=lambda iteration, results: iterations.append(iteration))["net"]
|
||||||
|
|
||||||
|
assert iterations == [0, 1]
|
||||||
|
assert result.is_valid
|
||||||
|
assert result.reached_target
|
||||||
|
assert result.outcome == "completed"
|
||||||
|
assert _path_signature(result.path) != _path_signature(partial_path)
|
||||||
|
assert pf.accumulated_expanded_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_all_refreshes_static_caches_after_static_topology_changes() -> None:
|
||||||
|
netlist = {"net": (Port(0, 0, 0), Port(10, 10, 90))}
|
||||||
|
widths = {"net": 2.0}
|
||||||
|
|
||||||
|
def build_router() -> tuple[RoutingWorld, AStarContext, PathFinder]:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=(-20, -20, 60, 60))
|
||||||
|
danger_map.precompute([])
|
||||||
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
|
context = build_context(
|
||||||
|
evaluator,
|
||||||
|
bounds=(-20, -20, 60, 60),
|
||||||
|
nets=_request_nets(netlist, widths),
|
||||||
|
bend_radii=[10.0],
|
||||||
|
max_straight_length=50.0,
|
||||||
|
node_limit=50,
|
||||||
|
warm_start=None,
|
||||||
|
max_iterations=1,
|
||||||
|
enabled=False,
|
||||||
)
|
)
|
||||||
return result, result.outcome
|
return engine, context, PathFinder(context)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
engine_auto, _context_auto, pf_auto = build_router()
|
||||||
PathFinder,
|
assert pf_auto.route_all()["net"].is_valid
|
||||||
"_route_net_once",
|
engine_auto.add_static_obstacle(box(4, 4, 8, 12))
|
||||||
lambda self, net_id, start, target, width, iteration, initial_paths, store_expanded, needs_self_collision_check: fake_route_once(
|
auto_result = pf_auto.route_all()["net"]
|
||||||
net_id,
|
|
||||||
start,
|
|
||||||
target,
|
|
||||||
width,
|
|
||||||
iteration,
|
|
||||||
initial_paths,
|
|
||||||
store_expanded,
|
|
||||||
needs_self_collision_check,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
state = create_routing_session_state(
|
|
||||||
pf,
|
|
||||||
{"net1": (Port(0, 0, 0), Port(10, 0, 0)), "net2": (Port(0, 10, 0), Port(10, 10, 0))},
|
|
||||||
{"net1": 2.0, "net2": 2.0},
|
|
||||||
store_expanded=True,
|
|
||||||
iteration_callback=lambda iteration, results: callback_results.append(dict(results)),
|
|
||||||
shuffle_nets=False,
|
|
||||||
sort_nets=None,
|
|
||||||
initial_paths={"seeded": []},
|
|
||||||
seed=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
outcomes = run_routing_iteration(pf, state, iteration=0)
|
engine_manual, context_manual, pf_manual = build_router()
|
||||||
|
assert pf_manual.route_all()["net"].is_valid
|
||||||
|
engine_manual.add_static_obstacle(box(4, 4, 8, 12))
|
||||||
|
context_manual.clear_static_caches()
|
||||||
|
manual_result = pf_manual.route_all()["net"]
|
||||||
|
|
||||||
assert outcomes == {"net1": "completed", "net2": "colliding"}
|
assert auto_result.reached_target == manual_result.reached_target
|
||||||
assert set(state.results) == {"net1", "net2"}
|
assert auto_result.collisions == manual_result.collisions
|
||||||
assert callback_results and set(callback_results[0]) == {"net1", "net2"}
|
assert auto_result.outcome == manual_result.outcome
|
||||||
assert state.results["net1"].is_valid
|
assert [(comp.move_type, comp.start_port.as_tuple(), comp.end_port.as_tuple()) for comp in auto_result.path] == [
|
||||||
assert not state.results["net2"].is_valid
|
(comp.move_type, comp.start_port.as_tuple(), comp.end_port.as_tuple()) for comp in manual_result.path
|
||||||
assert state.results["net2"].outcome == "colliding"
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_run_routing_iteration_timeout_finalizes_tree(
|
|
||||||
basic_evaluator: CostEvaluator,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
|
||||||
context = AStarContext(basic_evaluator)
|
|
||||||
pf = PathFinder(context)
|
|
||||||
finalized: list[bool] = []
|
|
||||||
monkeypatch.setattr(type(pf.path_state), "finalize_dynamic_tree", lambda self: finalized.append(True))
|
|
||||||
|
|
||||||
state = create_routing_session_state(
|
|
||||||
pf,
|
|
||||||
{"net1": (Port(0, 0, 0), Port(10, 0, 0))},
|
|
||||||
{"net1": 2.0},
|
|
||||||
store_expanded=False,
|
|
||||||
iteration_callback=None,
|
|
||||||
shuffle_nets=False,
|
|
||||||
sort_nets=None,
|
|
||||||
initial_paths={},
|
|
||||||
seed=None,
|
|
||||||
)
|
|
||||||
state.start_time = 0.0
|
|
||||||
state.session_timeout = 0.0
|
|
||||||
|
|
||||||
result = run_routing_iteration(pf, state, iteration=0)
|
|
||||||
|
|
||||||
assert result is None
|
|
||||||
assert finalized == [True]
|
|
||||||
|
|
||||||
|
|
||||||
def test_route_all_retries_partial_paths_across_iterations(
|
|
||||||
basic_evaluator: CostEvaluator,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
) -> None:
|
|
||||||
context = AStarContext(basic_evaluator)
|
|
||||||
pf = PathFinder(context, max_iterations=3, warm_start=None, refine_paths=False)
|
|
||||||
calls: list[int] = []
|
|
||||||
|
|
||||||
class FakeComponent:
|
|
||||||
def __init__(self, start_port: Port, end_port: Port) -> None:
|
|
||||||
self.start_port = start_port
|
|
||||||
self.end_port = end_port
|
|
||||||
|
|
||||||
def fake_route_astar(
|
|
||||||
start: Port,
|
|
||||||
target: Port,
|
|
||||||
width: float,
|
|
||||||
*,
|
|
||||||
context: AStarContext,
|
|
||||||
metrics: object,
|
|
||||||
net_id: str,
|
|
||||||
bend_collision_type: str,
|
|
||||||
return_partial: bool,
|
|
||||||
store_expanded: bool,
|
|
||||||
skip_congestion: bool,
|
|
||||||
self_collision_check: bool,
|
|
||||||
node_limit: int,
|
|
||||||
) -> list[FakeComponent]:
|
|
||||||
_ = (
|
|
||||||
width,
|
|
||||||
context,
|
|
||||||
metrics,
|
|
||||||
net_id,
|
|
||||||
bend_collision_type,
|
|
||||||
return_partial,
|
|
||||||
store_expanded,
|
|
||||||
skip_congestion,
|
|
||||||
self_collision_check,
|
|
||||||
node_limit,
|
|
||||||
)
|
|
||||||
calls.append(len(calls))
|
|
||||||
if len(calls) == 1:
|
|
||||||
return [FakeComponent(start, Port(5, 0, 0))]
|
|
||||||
return [FakeComponent(start, target)]
|
|
||||||
|
|
||||||
monkeypatch.setattr("inire.router.pathfinder.route_astar", fake_route_astar)
|
|
||||||
monkeypatch.setattr(type(pf.path_state), "install_path", lambda self, net_id, path: None)
|
|
||||||
monkeypatch.setattr(type(pf.path_state), "remove_path", lambda self, net_id: None)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
type(pf.path_state),
|
|
||||||
"verify_path_report",
|
|
||||||
lambda self, net_id, path: basic_evaluator.collision_engine.verify_path_report(net_id, []),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(type(pf.path_state), "finalize_dynamic_tree", lambda self: None)
|
|
||||||
|
|
||||||
results = pf.route_all({"net": (Port(0, 0, 0), Port(10, 0, 0))}, {"net": 2.0})
|
|
||||||
|
|
||||||
assert calls == [0, 1]
|
|
||||||
assert results["net"].reached_target
|
|
||||||
assert results["net"].is_valid
|
|
||||||
assert results["net"].outcome == "completed"
|
|
||||||
|
|
||||||
|
|
||||||
def test_pathfinder_refine_paths_reduces_locked_detour_bends() -> None:
|
def test_pathfinder_refine_paths_reduces_locked_detour_bends() -> None:
|
||||||
bounds = (0, -50, 100, 50)
|
bounds = (0, -50, 100, 50)
|
||||||
|
|
||||||
def build_pathfinder(*, refine_paths: bool) -> tuple[CollisionEngine, PathFinder]:
|
def build_pathfinder(
|
||||||
engine = CollisionEngine(clearance=2.0)
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
net_widths: dict[str, float],
|
||||||
|
*,
|
||||||
|
refinement_enabled: bool,
|
||||||
|
) -> tuple[RoutingWorld, PathFinder]:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=bounds)
|
danger_map = DangerMap(bounds=bounds)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
||||||
context = AStarContext(evaluator, bend_radii=[10.0])
|
return engine, _build_pathfinder(
|
||||||
return engine, PathFinder(context, refine_paths=refine_paths)
|
evaluator,
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths=net_widths,
|
||||||
|
bounds=bounds,
|
||||||
|
bend_radii=[10.0],
|
||||||
|
enabled=refinement_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
base_engine, base_pf = build_pathfinder(refine_paths=False)
|
net_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))}
|
||||||
base_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0})
|
width_a = {"netA": 2.0}
|
||||||
base_engine.lock_net("netA")
|
net_b = {"netB": (Port(50, -20, 90), Port(50, 20, 90))}
|
||||||
base_result = base_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"]
|
width_b = {"netB": 2.0}
|
||||||
|
|
||||||
refined_engine, refined_pf = build_pathfinder(refine_paths=True)
|
base_engine, base_pf = build_pathfinder(net_a, width_a, refinement_enabled=False)
|
||||||
refined_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0})
|
base_results = base_pf.route_all()
|
||||||
refined_engine.lock_net("netA")
|
for polygon in base_results["netA"].as_locked_route().geometry:
|
||||||
refined_result = refined_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"]
|
base_engine.add_static_obstacle(polygon)
|
||||||
|
base_result = _build_pathfinder(
|
||||||
|
base_pf.cost_evaluator,
|
||||||
|
netlist=net_b,
|
||||||
|
net_widths=width_b,
|
||||||
|
bounds=bounds,
|
||||||
|
bend_radii=[10.0],
|
||||||
|
enabled=False,
|
||||||
|
).route_all()["netB"]
|
||||||
|
|
||||||
base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90")
|
refined_engine, refined_pf = build_pathfinder(net_a, width_a, refinement_enabled=True)
|
||||||
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90")
|
refined_results = refined_pf.route_all()
|
||||||
|
for polygon in refined_results["netA"].as_locked_route().geometry:
|
||||||
|
refined_engine.add_static_obstacle(polygon)
|
||||||
|
refined_result = _build_pathfinder(
|
||||||
|
refined_pf.cost_evaluator,
|
||||||
|
netlist=net_b,
|
||||||
|
net_widths=width_b,
|
||||||
|
bounds=bounds,
|
||||||
|
bend_radii=[10.0],
|
||||||
|
enabled=True,
|
||||||
|
).route_all()["netB"]
|
||||||
|
|
||||||
|
base_bends = sum(1 for comp in base_result.path if comp.move_type == "bend90")
|
||||||
|
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "bend90")
|
||||||
|
|
||||||
assert base_result.is_valid
|
assert base_result.is_valid
|
||||||
assert refined_result.is_valid
|
assert refined_result.is_valid
|
||||||
|
|
@ -319,22 +337,30 @@ def test_pathfinder_refine_paths_simplifies_triple_crossing_detours() -> None:
|
||||||
}
|
}
|
||||||
net_widths = {net_id: 2.0 for net_id in netlist}
|
net_widths = {net_id: 2.0 for net_id in netlist}
|
||||||
|
|
||||||
def build_pathfinder(*, refine_paths: bool) -> PathFinder:
|
def build_pathfinder(*, refinement_enabled: bool) -> PathFinder:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=bounds)
|
danger_map = DangerMap(bounds=bounds)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0)
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0)
|
||||||
context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0])
|
return _build_pathfinder(
|
||||||
return PathFinder(context, base_congestion_penalty=1000.0, refine_paths=refine_paths)
|
evaluator,
|
||||||
|
netlist=netlist,
|
||||||
|
net_widths=net_widths,
|
||||||
|
bounds=bounds,
|
||||||
|
bend_radii=[10.0],
|
||||||
|
sbend_radii=[10.0],
|
||||||
|
base_penalty=1000.0,
|
||||||
|
enabled=refinement_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
base_results = build_pathfinder(refine_paths=False).route_all(netlist, net_widths)
|
base_results = build_pathfinder(refinement_enabled=False).route_all()
|
||||||
refined_results = build_pathfinder(refine_paths=True).route_all(netlist, net_widths)
|
refined_results = build_pathfinder(refinement_enabled=True).route_all()
|
||||||
|
|
||||||
for net_id in ("vertical_up", "vertical_down"):
|
for net_id in ("vertical_up", "vertical_down"):
|
||||||
base_result = base_results[net_id]
|
base_result = base_results[net_id]
|
||||||
refined_result = refined_results[net_id]
|
refined_result = refined_results[net_id]
|
||||||
base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90")
|
base_bends = sum(1 for comp in base_result.path if comp.move_type == "bend90")
|
||||||
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90")
|
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "bend90")
|
||||||
|
|
||||||
assert base_result.is_valid
|
assert base_result.is_valid
|
||||||
assert refined_result.is_valid
|
assert refined_result.is_valid
|
||||||
|
|
@ -342,12 +368,18 @@ def test_pathfinder_refine_paths_simplifies_triple_crossing_detours() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_refine_path_handles_same_orientation_lateral_offset() -> None:
|
def test_refine_path_handles_same_orientation_lateral_offset() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(-20, -20, 120, 120))
|
danger_map = DangerMap(bounds=(-20, -20, 120, 120))
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
||||||
context = AStarContext(evaluator, bend_radii=[5.0, 10.0])
|
pf = _build_pathfinder(
|
||||||
pf = PathFinder(context, refine_paths=True)
|
evaluator,
|
||||||
|
netlist={"net": (Port(0, 0, 0), Port(60, 15, 0))},
|
||||||
|
net_widths={"net": 2.0},
|
||||||
|
bounds=(-20, -20, 120, 120),
|
||||||
|
bend_radii=[5.0, 10.0],
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
width = 2.0
|
width = 2.0
|
||||||
|
|
@ -374,19 +406,25 @@ def test_refine_path_handles_same_orientation_lateral_offset() -> None:
|
||||||
refined = pf._refine_path("net", start, target, width, path)
|
refined = pf._refine_path("net", start, target, width, path)
|
||||||
|
|
||||||
assert target == Port(60, 15, 0)
|
assert target == Port(60, 15, 0)
|
||||||
assert sum(1 for comp in path if comp.move_type == "Bend90") == 6
|
assert sum(1 for comp in path if comp.move_type == "bend90") == 6
|
||||||
assert sum(1 for comp in refined if comp.move_type == "Bend90") == 4
|
assert sum(1 for comp in refined if comp.move_type == "bend90") == 4
|
||||||
assert refined[-1].end_port == target
|
assert refined[-1].end_port == target
|
||||||
assert pf._path_cost(refined) < pf._path_cost(path)
|
assert pf._path_cost(refined) < pf._path_cost(path)
|
||||||
|
|
||||||
|
|
||||||
def test_refine_path_can_simplify_subpath_with_different_global_orientation() -> None:
|
def test_refine_path_can_simplify_subpath_with_different_global_orientation() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(-20, -20, 120, 120))
|
danger_map = DangerMap(bounds=(-20, -20, 120, 120))
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
|
||||||
context = AStarContext(evaluator, bend_radii=[5.0, 10.0])
|
pf = _build_pathfinder(
|
||||||
pf = PathFinder(context, refine_paths=True)
|
evaluator,
|
||||||
|
netlist={"net": (Port(0, 0, 0), Port(65, 30, 90))},
|
||||||
|
net_widths={"net": 2.0},
|
||||||
|
bounds=(-20, -20, 120, 120),
|
||||||
|
bend_radii=[5.0, 10.0],
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
width = 2.0
|
width = 2.0
|
||||||
|
|
@ -415,7 +453,7 @@ def test_refine_path_can_simplify_subpath_with_different_global_orientation() ->
|
||||||
refined = pf._refine_path("net", start, target, width, path)
|
refined = pf._refine_path("net", start, target, width, path)
|
||||||
|
|
||||||
assert target == Port(65, 30, 90)
|
assert target == Port(65, 30, 90)
|
||||||
assert sum(1 for comp in path if comp.move_type == "Bend90") == 7
|
assert sum(1 for comp in path if comp.move_type == "bend90") == 7
|
||||||
assert sum(1 for comp in refined if comp.move_type == "Bend90") == 5
|
assert sum(1 for comp in refined if comp.move_type == "bend90") == 5
|
||||||
assert refined[-1].end_port == target
|
assert refined[-1].end_port == target
|
||||||
assert pf._path_cost(refined) < pf._path_cost(path)
|
assert pf._path_cost(refined) < pf._path_cost(path)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
from dataclasses import FrozenInstanceError
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from hypothesis import given, strategies as st
|
from hypothesis import given, strategies as st
|
||||||
|
import pytest
|
||||||
|
|
||||||
from inire.geometry.primitives import Port, rotate_port, translate_port
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
@st.composite
|
||||||
|
|
@ -24,11 +26,11 @@ def test_port_transform_invariants(p: Port) -> None:
|
||||||
# Rotating 90 degrees 4 times should return to same orientation
|
# Rotating 90 degrees 4 times should return to same orientation
|
||||||
p_rot = p
|
p_rot = p
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
p_rot = rotate_port(p_rot, 90)
|
p_rot = p_rot.rotated(90)
|
||||||
|
|
||||||
assert abs(p_rot.x - p.x) < 1e-6
|
assert abs(p_rot.x - p.x) < 1e-6
|
||||||
assert abs(p_rot.y - p.y) < 1e-6
|
assert abs(p_rot.y - p.y) < 1e-6
|
||||||
assert (p_rot.orientation % 360) == (p.orientation % 360)
|
assert (p_rot.r % 360) == (p.r % 360)
|
||||||
|
|
||||||
|
|
||||||
@given(
|
@given(
|
||||||
|
|
@ -37,14 +39,21 @@ def test_port_transform_invariants(p: Port) -> None:
|
||||||
dy=st.floats(min_value=-1000, max_value=1000),
|
dy=st.floats(min_value=-1000, max_value=1000),
|
||||||
)
|
)
|
||||||
def test_translate_snapping(p: Port, dx: float, dy: float) -> None:
|
def test_translate_snapping(p: Port, dx: float, dy: float) -> None:
|
||||||
p_trans = translate_port(p, dx, dy)
|
p_trans = p.translate(dx, dy)
|
||||||
assert isinstance(p_trans.x, int)
|
assert isinstance(p_trans.x, int)
|
||||||
assert isinstance(p_trans.y, int)
|
assert isinstance(p_trans.y, int)
|
||||||
|
|
||||||
|
|
||||||
def test_orientation_normalization() -> None:
|
def test_orientation_normalization() -> None:
|
||||||
p = Port(0, 0, 360)
|
p = Port(0, 0, 360)
|
||||||
assert p.orientation == 0
|
assert p.r == 0
|
||||||
|
|
||||||
p2 = Port(0, 0, -90)
|
p2 = Port(0, 0, -90)
|
||||||
assert p2.orientation == 270
|
assert p2.r == 270
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_is_immutable_value_type() -> None:
|
||||||
|
p = Port(1, 2, 90)
|
||||||
|
|
||||||
|
with pytest.raises(FrozenInstanceError):
|
||||||
|
p.x = 3
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.components import Bend90
|
from inire.geometry.components import Bend90
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarContext
|
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import PathFinder
|
from inire.tests.support import build_pathfinder
|
||||||
|
|
||||||
|
|
||||||
def test_arc_resolution_sagitta() -> None:
|
def test_arc_resolution_sagitta() -> None:
|
||||||
|
|
@ -18,34 +17,45 @@ def test_arc_resolution_sagitta() -> None:
|
||||||
|
|
||||||
# Check number of points in the polygon exterior
|
# Check number of points in the polygon exterior
|
||||||
# (num_segments + 1) * 2 points usually
|
# (num_segments + 1) * 2 points usually
|
||||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
pts_coarse = len(res_coarse.collision_geometry[0].exterior.coords)
|
||||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
pts_fine = len(res_fine.collision_geometry[0].exterior.coords)
|
||||||
|
|
||||||
assert pts_fine > pts_coarse
|
assert pts_fine > pts_coarse
|
||||||
|
|
||||||
|
|
||||||
def test_locked_paths() -> None:
|
def test_locked_routes() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
evaluator = CostEvaluator(engine, danger_map)
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
context = AStarContext(evaluator, bend_radii=[5.0, 10.0])
|
|
||||||
pf = PathFinder(context)
|
|
||||||
|
|
||||||
# 1. Route Net A
|
# 1. Route Net A
|
||||||
netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))}
|
netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))}
|
||||||
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
results_a = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
netlist=netlist_a,
|
||||||
|
net_widths={"netA": 2.0},
|
||||||
|
bend_radii=[5.0, 10.0],
|
||||||
|
).route_all()
|
||||||
assert results_a["netA"].is_valid
|
assert results_a["netA"].is_valid
|
||||||
|
|
||||||
# 2. Lock Net A
|
# 2. Treat Net A as locked geometry in the next run.
|
||||||
engine.lock_net("netA")
|
for polygon in results_a["netA"].as_locked_route().geometry:
|
||||||
|
engine.add_static_obstacle(polygon)
|
||||||
|
|
||||||
# 3. Route Net B through the same space. It should detour or fail.
|
# 3. Route Net B through the same space. It should detour or fail.
|
||||||
# We'll place Net B's start/target such that it MUST cross Net A's physical path.
|
# We'll place Net B's start/target such that it MUST cross Net A's physical path.
|
||||||
netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))}
|
netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))}
|
||||||
|
|
||||||
# Route Net B
|
# Route Net B
|
||||||
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
results_b = build_pathfinder(
|
||||||
|
evaluator,
|
||||||
|
bounds=(0, -50, 100, 50),
|
||||||
|
netlist=netlist_b,
|
||||||
|
net_widths={"netB": 2.0},
|
||||||
|
bend_radii=[5.0, 10.0],
|
||||||
|
).route_all()
|
||||||
|
|
||||||
# Net B should be is_valid (it detoured) or at least not have collisions
|
# Net B should be is_valid (it detoured) or at least not have collisions
|
||||||
# with Net A in the dynamic set (because netA is now static).
|
# with Net A in the dynamic set (because netA is now static).
|
||||||
|
|
@ -55,8 +65,8 @@ def test_locked_paths() -> None:
|
||||||
assert results_b["netB"].is_valid
|
assert results_b["netB"].is_valid
|
||||||
|
|
||||||
# Verify geometry doesn't intersect locked netA (physical check)
|
# Verify geometry doesn't intersect locked netA (physical check)
|
||||||
poly_a = [p.geometry[0] for p in results_a["netA"].path]
|
poly_a = [p.physical_geometry[0] for p in results_a["netA"].path]
|
||||||
poly_b = [p.geometry[0] for p in results_b["netB"].path]
|
poly_b = [p.physical_geometry[0] for p in results_b["netB"].path]
|
||||||
|
|
||||||
for pa in poly_a:
|
for pa in poly_a:
|
||||||
for pb in poly_b:
|
for pb in poly_b:
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import route_astar, AStarContext
|
from inire.router._search import route_astar
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
from inire.tests.support import build_context
|
||||||
|
|
||||||
|
|
||||||
class TestIntegerPorts(unittest.TestCase):
|
class TestIntegerPorts(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.ce = CollisionEngine(clearance=2.0)
|
self.ce = RoutingWorld(clearance=2.0)
|
||||||
self.cost = CostEvaluator(self.ce)
|
self.cost = CostEvaluator(self.ce)
|
||||||
|
self.bounds = (0, 0, 100, 100)
|
||||||
|
|
||||||
def test_route_reaches_integer_target(self):
|
def test_route_reaches_integer_target(self):
|
||||||
context = AStarContext(self.cost)
|
context = build_context(self.cost, bounds=self.bounds)
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
target = Port(12, 0, 0)
|
target = Port(12, 0, 0)
|
||||||
|
|
||||||
|
|
@ -24,7 +27,7 @@ class TestIntegerPorts(unittest.TestCase):
|
||||||
self.assertEqual(last_port.r, 0)
|
self.assertEqual(last_port.r, 0)
|
||||||
|
|
||||||
def test_port_constructor_rounds_to_integer_lattice(self):
|
def test_port_constructor_rounds_to_integer_lattice(self):
|
||||||
context = AStarContext(self.cost)
|
context = build_context(self.cost, bounds=self.bounds)
|
||||||
start = Port(0.0, 0.0, 0.0)
|
start = Port(0.0, 0.0, 0.0)
|
||||||
target = Port(12.3, 0.0, 0.0)
|
target = Port(12.3, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
@ -36,7 +39,7 @@ class TestIntegerPorts(unittest.TestCase):
|
||||||
self.assertEqual(last_port.x, 12)
|
self.assertEqual(last_port.x, 12)
|
||||||
|
|
||||||
def test_half_step_inputs_use_integerized_targets(self):
|
def test_half_step_inputs_use_integerized_targets(self):
|
||||||
context = AStarContext(self.cost)
|
context = build_context(self.cost, bounds=self.bounds)
|
||||||
start = Port(0.0, 0.0, 0.0)
|
start = Port(0.0, 0.0, 0.0)
|
||||||
target = Port(7.5, 0.0, 0.0)
|
target = Port(7.5, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
from shapely.geometry import box
|
from shapely.geometry import box
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.visibility import VisibilityManager
|
from inire.router.visibility import VisibilityManager
|
||||||
|
|
||||||
|
|
||||||
def test_point_visibility_cache_respects_max_distance() -> None:
|
def test_point_visibility_cache_respects_max_distance() -> None:
|
||||||
engine = CollisionEngine(clearance=0.0)
|
engine = RoutingWorld(clearance=0.0)
|
||||||
engine.add_static_obstacle(box(10, 20, 20, 30))
|
engine.add_static_obstacle(box(10, 20, 20, 30))
|
||||||
engine.add_static_obstacle(box(100, 20, 110, 30))
|
engine.add_static_obstacle(box(100, 20, 110, 30))
|
||||||
visibility = VisibilityManager(engine)
|
visibility = VisibilityManager(engine)
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ matplotlib.use("Agg")
|
||||||
|
|
||||||
from inire.geometry.components import Bend90
|
from inire.geometry.components import Bend90
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.pathfinder import RoutingResult
|
from inire import RoutingResult
|
||||||
from inire.utils.visualization import plot_routing_results
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def test_plot_routing_results_respects_show_actual() -> None:
|
def test_plot_routing_results_respects_show_actual() -> None:
|
||||||
bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox")
|
bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox")
|
||||||
result = RoutingResult("net", [bend], True, 0, reached_target=True)
|
result = RoutingResult("net", [bend], reached_target=True)
|
||||||
|
|
||||||
fig_actual, ax_actual = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=True)
|
fig_actual, ax_actual = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=True)
|
||||||
fig_proxy, ax_proxy = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=False)
|
fig_proxy, ax_proxy = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=False)
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.pathfinder import RoutingResult
|
from inire.router.results import RoutingResult
|
||||||
|
|
||||||
|
|
||||||
def validate_routing_result(
|
def validate_routing_result(
|
||||||
|
|
@ -38,21 +38,21 @@ def validate_routing_result(
|
||||||
|
|
||||||
if expected_start:
|
if expected_start:
|
||||||
first_port = result.path[0].start_port
|
first_port = result.path[0].start_port
|
||||||
dist_to_start = numpy.sqrt(((first_port[:2] - expected_start[:2])**2).sum())
|
dist_to_start = numpy.sqrt((first_port.x - expected_start.x) ** 2 + (first_port.y - expected_start.y) ** 2)
|
||||||
if dist_to_start > 0.005:
|
if dist_to_start > 0.005:
|
||||||
connectivity_errors.append(f"Initial port position mismatch: {dist_to_start*1000:.2f}nm")
|
connectivity_errors.append(f"Initial port position mismatch: {dist_to_start*1000:.2f}nm")
|
||||||
if abs(first_port[2] - expected_start[2]) > 0.1:
|
if abs(first_port.r - expected_start.r) > 0.1:
|
||||||
connectivity_errors.append(f"Initial port orientation mismatch: {first_port[2]} vs {expected_start[2]}")
|
connectivity_errors.append(f"Initial port orientation mismatch: {first_port.r} vs {expected_start.r}")
|
||||||
|
|
||||||
if expected_end:
|
if expected_end:
|
||||||
last_port = result.path[-1].end_port
|
last_port = result.path[-1].end_port
|
||||||
dist_to_end = numpy.sqrt(((last_port[:2] - expected_end[:2])**2).sum())
|
dist_to_end = numpy.sqrt((last_port.x - expected_end.x) ** 2 + (last_port.y - expected_end.y) ** 2)
|
||||||
if dist_to_end > 0.005:
|
if dist_to_end > 0.005:
|
||||||
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
||||||
if abs(last_port[2] - expected_end[2]) > 0.1:
|
if abs(last_port.r - expected_end.r) > 0.1:
|
||||||
connectivity_errors.append(f"Final port orientation mismatch: {last_port[2]} vs {expected_end[2]}")
|
connectivity_errors.append(f"Final port orientation mismatch: {last_port.r} vs {expected_end.r}")
|
||||||
|
|
||||||
engine = CollisionEngine(clearance=clearance)
|
engine = RoutingWorld(clearance=clearance)
|
||||||
for obstacle in static_obstacles:
|
for obstacle in static_obstacles:
|
||||||
engine.add_static_obstacle(obstacle)
|
engine.add_static_obstacle(obstacle)
|
||||||
report = engine.verify_path_report("validation", result.path)
|
report = engine.verify_path_report("validation", result.path)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import RoutingResult
|
from inire.router.results import RoutingResult
|
||||||
|
|
||||||
|
|
||||||
def plot_routing_results(
|
def plot_routing_results(
|
||||||
|
|
@ -51,8 +51,7 @@ def plot_routing_results(
|
||||||
label_added = False
|
label_added = False
|
||||||
for comp in res.path:
|
for comp in res.path:
|
||||||
# 1. Plot Collision Geometry (Translucent fill)
|
# 1. Plot Collision Geometry (Translucent fill)
|
||||||
# This is the geometry used during search (e.g. proxy or arc)
|
for poly in comp.collision_geometry:
|
||||||
for poly in comp.geometry:
|
|
||||||
if isinstance(poly, MultiPolygon):
|
if isinstance(poly, MultiPolygon):
|
||||||
geoms = list(poly.geoms)
|
geoms = list(poly.geoms)
|
||||||
else:
|
else:
|
||||||
|
|
@ -67,13 +66,7 @@ def plot_routing_results(
|
||||||
x, y = g.xy
|
x, y = g.xy
|
||||||
ax.plot(x, y, color=color, alpha=0.15, linestyle='--', lw=0.5, zorder=2)
|
ax.plot(x, y, color=color, alpha=0.15, linestyle='--', lw=0.5, zorder=2)
|
||||||
|
|
||||||
# 2. Plot "Actual" Geometry (The high-fidelity shape used for fabrication)
|
actual_geoms_to_plot = comp.physical_geometry if show_actual else comp.collision_geometry
|
||||||
# Use comp.actual_geometry if it exists (should be the arc)
|
|
||||||
actual_geoms_to_plot = (
|
|
||||||
comp.actual_geometry
|
|
||||||
if show_actual and comp.actual_geometry is not None
|
|
||||||
else comp.geometry
|
|
||||||
)
|
|
||||||
|
|
||||||
for poly in actual_geoms_to_plot:
|
for poly in actual_geoms_to_plot:
|
||||||
if isinstance(poly, MultiPolygon):
|
if isinstance(poly, MultiPolygon):
|
||||||
|
|
@ -91,21 +84,20 @@ def plot_routing_results(
|
||||||
|
|
||||||
# 3. Plot subtle port orientation arrow
|
# 3. Plot subtle port orientation arrow
|
||||||
p = comp.end_port
|
p = comp.end_port
|
||||||
rad = numpy.radians(p.orientation)
|
rad = numpy.radians(p.r)
|
||||||
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
scale=40, width=0.002, alpha=0.2, pivot="tail", zorder=4)
|
scale=40, width=0.002, alpha=0.2, pivot="tail", zorder=4)
|
||||||
|
|
||||||
if not res.path and not res.is_valid:
|
if not res.path and not res.is_valid:
|
||||||
# Best-effort display: If the path is empty but failed, it might be unroutable.
|
# Empty failed paths are typically unroutable.
|
||||||
# We don't have a partial path in RoutingResult currently.
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 4. Plot main arrows for netlist ports
|
# 4. Plot main arrows for netlist ports
|
||||||
if netlist:
|
if netlist:
|
||||||
for _net_id, (start_p, target_p) in netlist.items():
|
for _net_id, (start_p, target_p) in netlist.items():
|
||||||
for p in [start_p, target_p]:
|
for p in [start_p, target_p]:
|
||||||
rad = numpy.radians(p[2])
|
rad = numpy.radians(p.r)
|
||||||
ax.quiver(*p[:2], numpy.cos(rad), numpy.sin(rad), color="black",
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
scale=25, width=0.004, pivot="tail", zorder=6)
|
scale=25, width=0.004, pivot="tail", zorder=6)
|
||||||
|
|
||||||
ax.set_xlim(bounds[0], bounds[2])
|
ax.set_xlim(bounds[0], bounds[2])
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ lint.ignore = [
|
||||||
"C408", # dict(x=y) instead of {'x': y}
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
"PLR09", # Too many xxx
|
"PLR09", # Too many xxx
|
||||||
"PLR2004", # magic number
|
"PLR2004", # magic number
|
||||||
#"PLC0414", # import x as x
|
"PLC0414", # import x as x
|
||||||
"TRY003", # Long exception message
|
"TRY003", # Long exception message
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue