Compare commits
No commits in common. "43a9a6cb3afe9051da208315bcbbe1cc13fd43b4" and "f600b52f32438f4368484094de2d01d16bcf4953" have entirely different histories.
43a9a6cb3a
...
f600b52f32
28 changed files with 442 additions and 1139 deletions
53
DOCS.md
53
DOCS.md
|
|
@ -1,53 +0,0 @@
|
|||
# Inire Configuration & API Documentation
|
||||
|
||||
This document describes the user-tunable parameters for the `inire` auto-router.
|
||||
|
||||
## 1. AStarRouter Parameters
|
||||
|
||||
The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. |
|
||||
| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search in open space. |
|
||||
| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow the router to pick the best fit. |
|
||||
| `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). |
|
||||
| `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). |
|
||||
| `snap_to_target_dist`| `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. |
|
||||
| `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. |
|
||||
| `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. |
|
||||
| `bend_collision_type`| `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. |
|
||||
| `bend_clip_margin` | `float` | 10.0 | Margin (µm) for the `"clipped_bbox"` collision model. |
|
||||
|
||||
### Bend Collision Models
|
||||
* `"arc"`: High-fidelity model following the exact curved waveguide geometry.
|
||||
* `"bbox"`: Conservative model using the axis-aligned bounding box of the bend. Fast but blocks more space.
|
||||
* `"clipped_bbox"`: A middle ground that uses the bounding box but clips corners that are far from the waveguide.
|
||||
|
||||
---
|
||||
|
||||
## 2. CostEvaluator Parameters
|
||||
|
||||
The `CostEvaluator` defines the "goodness" of a path.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `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`) are faster but may produce longer paths. |
|
||||
| `congestion_penalty`| `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. |
|
||||
|
||||
---
|
||||
|
||||
## 3. CollisionEngine Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). |
|
||||
| `safety_zone_radius`| `float` | 0.0021 | Radius (µm) around ports where collisions are ignored to allow PDK boundary incidence. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Physical Units & Precision
|
||||
- **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**.
|
||||
13
README.md
13
README.md
|
|
@ -55,19 +55,6 @@ if results["net1"].is_valid:
|
|||
print("Successfully routed net1!")
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Check the `examples/` directory for ready-to-run scripts demonstrating core features:
|
||||
|
||||
* **`examples/01_simple_route.py`**: Basic single-net routing with visualization.
|
||||
* **`examples/02_congestion_resolution.py`**: Multi-net routing resolving bottlenecks using Negotiated Congestion.
|
||||
* **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths.
|
||||
|
||||
Run an example:
|
||||
```bash
|
||||
python3 examples/01_simple_route.py
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
`inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types:
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 01: Simple Route...")
|
||||
|
||||
# 1. Setup Environment
|
||||
# Define the routing area bounds (minx, miny, maxx, maxy)
|
||||
bounds = (0, 0, 100, 100)
|
||||
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
|
||||
# Add a simple rectangular obstacle
|
||||
obstacle = Polygon([(30, 20), (70, 20), (70, 40), (30, 40)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Precompute the danger map (distance field) for heuristics
|
||||
danger_map.precompute([obstacle])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Define Netlist
|
||||
# Route from (10, 10) to (90, 50)
|
||||
# The obstacle at y=20-40 blocks the direct path.
|
||||
netlist = {
|
||||
"simple_net": (Port(10, 10, 0), Port(90, 50, 0)),
|
||||
}
|
||||
net_widths = {"simple_net": 2.0}
|
||||
|
||||
# 3. Route
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 4. Check Results
|
||||
if results["simple_net"].is_valid:
|
||||
print("Success! Route found.")
|
||||
print(f"Path collisions: {results['simple_net'].collisions}")
|
||||
else:
|
||||
print("Failed to route.")
|
||||
|
||||
# 5. Visualize
|
||||
fig, ax = plot_routing_results(results, [obstacle], bounds, netlist=netlist)
|
||||
fig.savefig("examples/simple_route.png")
|
||||
print("Saved plot to examples/simple_route.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 02: Congestion Resolution (Crossing)...")
|
||||
|
||||
# 1. Setup Environment (Open space)
|
||||
bounds = (0, 0, 100, 100)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute([])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Define Netlist
|
||||
# Two nets that MUST cross.
|
||||
# Since crossings are illegal in single-layer routing, one net must detour around the other.
|
||||
netlist = {
|
||||
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||
"vertical": (Port(50, 10, 90), Port(50, 90, 90)),
|
||||
}
|
||||
net_widths = {"horizontal": 2.0, "vertical": 2.0}
|
||||
|
||||
# 3. Route with Negotiated Congestion
|
||||
# We increase the base penalty to encourage faster divergence
|
||||
pf.base_congestion_penalty = 500.0
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 4. Check Results
|
||||
all_valid = all(r.is_valid for r in results.values())
|
||||
if all_valid:
|
||||
print("Success! Congestion resolved (one net detoured).")
|
||||
else:
|
||||
print("Some nets failed or have collisions.")
|
||||
for nid, res in results.items():
|
||||
print(f" {nid}: valid={res.is_valid}, collisions={res.collisions}")
|
||||
|
||||
# 5. Visualize
|
||||
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
||||
fig.savefig("examples/congestion.png")
|
||||
print("Saved plot to examples/congestion.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 03: Locked Paths (Incremental Routing)...")
|
||||
|
||||
# 1. Setup Environment
|
||||
bounds = (0, 0, 100, 100)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute([]) # No initial obstacles
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Phase 1: Route a "Critical" Net
|
||||
# This net gets priority and takes the best path.
|
||||
netlist_phase1 = {
|
||||
"critical_net": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||
}
|
||||
print("Phase 1: Routing critical_net...")
|
||||
results1 = pf.route_all(netlist_phase1, {"critical_net": 3.0}) # Wider trace
|
||||
|
||||
if not results1["critical_net"].is_valid:
|
||||
print("Error: Phase 1 failed.")
|
||||
return
|
||||
|
||||
# 3. Lock the Critical Net
|
||||
# This converts the dynamic path into a static obstacle in the collision engine.
|
||||
print("Locking critical_net...")
|
||||
engine.lock_net("critical_net")
|
||||
|
||||
# Update danger map to reflect the new obstacle (optional but recommended for heuristics)
|
||||
# Extract polygons from result
|
||||
path_polys = [p for comp in results1["critical_net"].path for p in comp.geometry]
|
||||
danger_map.precompute(path_polys)
|
||||
|
||||
# 4. Phase 2: Route a Secondary Net
|
||||
# This net must route *around* the locked critical_net.
|
||||
# Start and end points force a crossing path if it were straight.
|
||||
netlist_phase2 = {
|
||||
"secondary_net": (Port(50, 10, 90), Port(50, 90, 90)),
|
||||
}
|
||||
|
||||
print("Phase 2: Routing secondary_net around locked path...")
|
||||
results2 = pf.route_all(netlist_phase2, {"secondary_net": 2.0})
|
||||
|
||||
if results2["secondary_net"].is_valid:
|
||||
print("Success! Secondary net routed around locked path.")
|
||||
else:
|
||||
print("Failed to route secondary net.")
|
||||
|
||||
# 5. Visualize
|
||||
# Combine results and netlists for plotting
|
||||
all_results = {**results1, **results2}
|
||||
all_netlists = {**netlist_phase1, **netlist_phase2}
|
||||
|
||||
# Note: 'critical_net' is now in engine.static_obstacles internally,
|
||||
# but for visualization we plot it from the result object to see it clearly.
|
||||
# We pass an empty list for 'static_obstacles' to plot_routing_results
|
||||
# because we want to see the path colored, not grayed out as an obstacle.
|
||||
|
||||
fig, ax = plot_routing_results(all_results, [], bounds, netlist=all_netlists)
|
||||
fig.savefig("examples/locked.png")
|
||||
|
||||
print("Saved plot to examples/locked.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.config import CostConfig, RouterConfig
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 04: S-Bends and Multiple Radii...")
|
||||
|
||||
# 1. Setup Environment
|
||||
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,
|
||||
greedy_h_weight=1.5,
|
||||
)
|
||||
|
||||
# We want a 45 degree switchover for S-bend.
|
||||
# Offset O = 2 * R * (1 - cos(theta))
|
||||
# If R = 10, O = 5.86
|
||||
|
||||
router = AStarRouter(
|
||||
evaluator,
|
||||
node_limit=50000,
|
||||
bend_radii=[10.0, 30.0],
|
||||
sbend_offsets=[5.0], # Use a simpler offset
|
||||
sbend_radii=[10.0],
|
||||
bend_penalty=10.0,
|
||||
sbend_penalty=20.0,
|
||||
snap_to_target_dist=50.0, # Large snap range
|
||||
)
|
||||
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 3. Define Netlist
|
||||
# start (10, 50), target (60, 55) -> 5um offset
|
||||
netlist = {
|
||||
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
||||
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
||||
}
|
||||
net_widths = {"sbend_only": 2.0, "multi_radii": 2.0}
|
||||
|
||||
# 4. Route
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 5. Check Results
|
||||
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/sbends_radii.png")
|
||||
print("Saved plot to examples/sbends_radii.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
|
|
@ -16,10 +16,9 @@ if TYPE_CHECKING:
|
|||
class CollisionEngine:
|
||||
"""Manages spatial queries for collision detection."""
|
||||
|
||||
def __init__(self, clearance: float, max_net_width: float = 2.0, safety_zone_radius: float = 0.0021) -> None:
|
||||
def __init__(self, clearance: float, max_net_width: float = 2.0) -> None:
|
||||
self.clearance = clearance
|
||||
self.max_net_width = max_net_width
|
||||
self.safety_zone_radius = safety_zone_radius
|
||||
self.static_obstacles = rtree.index.Index()
|
||||
# To store geometries for precise checks
|
||||
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||
|
|
@ -41,9 +40,16 @@ class CollisionEngine:
|
|||
self.obstacle_geometries[obj_id] = polygon
|
||||
self.prepared_obstacles[obj_id] = prep(polygon)
|
||||
|
||||
# Index the bounding box of the original polygon
|
||||
# We query with dilated moves, so original bounds are enough
|
||||
self.static_obstacles.insert(obj_id, polygon.bounds)
|
||||
# Index the bounding box of the polygon (dilated for broad prune)
|
||||
# Spec: "All user-provided obstacles are pre-dilated by (W_max + C)/2"
|
||||
dilation = (self.max_net_width + self.clearance) / 2.0
|
||||
dilated_bounds = (
|
||||
polygon.bounds[0] - dilation,
|
||||
polygon.bounds[1] - dilation,
|
||||
polygon.bounds[2] + dilation,
|
||||
polygon.bounds[3] + dilation,
|
||||
)
|
||||
self.static_obstacles.insert(obj_id, dilated_bounds)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon]) -> None:
|
||||
"""Add a net's routed path to the dynamic R-Tree."""
|
||||
|
|
@ -81,15 +87,11 @@ class CollisionEngine:
|
|||
"""Count how many other nets collide with this geometry."""
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
return self.count_congestion_prebuffered(test_poly, net_id)
|
||||
|
||||
def count_congestion_prebuffered(self, dilated_geometry: Polygon, net_id: str) -> int:
|
||||
"""Count how many other nets collide with this pre-dilated geometry."""
|
||||
candidates = self.dynamic_paths.intersection(dilated_geometry.bounds)
|
||||
candidates = self.dynamic_paths.intersection(test_poly.bounds)
|
||||
count = 0
|
||||
for obj_id in candidates:
|
||||
other_net_id, other_poly = self.path_geometries[obj_id]
|
||||
if other_net_id != net_id and dilated_geometry.intersects(other_poly):
|
||||
if other_net_id != net_id and test_poly.intersects(other_poly):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
|
@ -104,46 +106,35 @@ class CollisionEngine:
|
|||
_ = net_width # Width is already integrated into engine dilation settings
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
return self.is_collision_prebuffered(test_poly, start_port=start_port, end_port=end_port)
|
||||
|
||||
def is_collision_prebuffered(
|
||||
self,
|
||||
dilated_geometry: Polygon,
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""Check if a pre-dilated geometry collides with static obstacles."""
|
||||
# Query R-Tree using the bounds of the dilated move
|
||||
candidates = self.static_obstacles.intersection(dilated_geometry.bounds)
|
||||
# Broad prune with R-Tree
|
||||
candidates = self.static_obstacles.intersection(test_poly.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
# Use prepared geometry for fast intersection
|
||||
if self.prepared_obstacles[obj_id].intersects(dilated_geometry):
|
||||
# Check safety zone (2nm radius)
|
||||
if self.prepared_obstacles[obj_id].intersects(test_poly):
|
||||
# Check safety zone (2nm = 0.002 um)
|
||||
if start_port or end_port:
|
||||
obstacle = self.obstacle_geometries[obj_id]
|
||||
intersection = dilated_geometry.intersection(obstacle)
|
||||
intersection = test_poly.intersection(obstacle)
|
||||
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
||||
# Precise check: is every point in the intersection close to either port?
|
||||
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
|
||||
|
||||
is_near_start = False
|
||||
# Create safety zone polygons
|
||||
safety_zones = []
|
||||
if start_port:
|
||||
if (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius):
|
||||
is_near_start = True
|
||||
|
||||
is_near_end = False
|
||||
safety_zones.append(Point(start_port.x, start_port.y).buffer(0.002))
|
||||
if end_port:
|
||||
if (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius):
|
||||
is_near_end = True
|
||||
safety_zones.append(Point(end_port.x, end_port.y).buffer(0.002))
|
||||
|
||||
if is_near_start or is_near_end:
|
||||
continue
|
||||
if safety_zones:
|
||||
safe_poly = unary_union(safety_zones)
|
||||
# Remove safe zones from intersection
|
||||
remaining_collision = intersection.difference(safe_poly)
|
||||
if remaining_collision.is_empty or remaining_collision.area < 1e-9:
|
||||
continue
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Literal, Union
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon, box
|
||||
from shapely.ops import unary_union
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from .primitives import Port
|
||||
|
||||
|
|
@ -13,40 +12,32 @@ SEARCH_GRID_SNAP_UM = 1.0
|
|||
|
||||
|
||||
def snap_search_grid(value: float) -> float:
|
||||
"""Snap a coordinate to the nearest search grid unit."""
|
||||
"""Snap a coordinate to the nearest 1µm."""
|
||||
return round(value / SEARCH_GRID_SNAP_UM) * SEARCH_GRID_SNAP_UM
|
||||
|
||||
|
||||
class ComponentResult(NamedTuple):
|
||||
"""The result of a component generation: geometry, final port, and physical length."""
|
||||
"""The result of a component generation: geometry and the final port."""
|
||||
|
||||
geometry: list[Polygon]
|
||||
end_port: Port
|
||||
length: float
|
||||
|
||||
|
||||
class Straight:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, length: float, width: float, snap_to_grid: bool = True) -> ComponentResult:
|
||||
def generate(start_port: Port, length: float, width: float) -> ComponentResult:
|
||||
"""Generate a straight waveguide segment."""
|
||||
# Calculate end port position
|
||||
rad = np.radians(start_port.orientation)
|
||||
dx = length * np.cos(rad)
|
||||
dy = length * np.sin(rad)
|
||||
|
||||
ex = start_port.x + dx
|
||||
ey = start_port.y + dy
|
||||
end_port = Port(start_port.x + dx, start_port.y + dy, start_port.orientation)
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex)
|
||||
ey = snap_search_grid(ey)
|
||||
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
actual_length = np.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2)
|
||||
|
||||
# Create polygon
|
||||
# Create polygon (centered on port)
|
||||
half_w = width / 2.0
|
||||
# Points relative to start port (0,0)
|
||||
points = [(0, half_w), (actual_length, half_w), (actual_length, -half_w), (0, -half_w)]
|
||||
points = [(0, half_w), (length, half_w), (length, -half_w), (0, -half_w)]
|
||||
|
||||
# Transform points
|
||||
cos_val = np.cos(rad)
|
||||
|
|
@ -57,129 +48,123 @@ class Straight:
|
|||
ty = start_port.y + px * sin_val + py * cos_val
|
||||
poly_points.append((tx, ty))
|
||||
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port, length=actual_length)
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port)
|
||||
|
||||
|
||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||
"""Calculate number of segments for an arc to maintain a maximum sagitta."""
|
||||
if radius <= 0:
|
||||
return 1
|
||||
# angle_deg is absolute angle turned
|
||||
# s = R(1 - cos(theta/2)) => cos(theta/2) = 1 - s/R
|
||||
# theta = 2 * acos(1 - s/R)
|
||||
# n = total_angle / theta
|
||||
ratio = max(0.0, min(1.0, 1.0 - sagitta / radius))
|
||||
theta_max = 2.0 * np.arccos(ratio)
|
||||
if theta_max < 1e-9:
|
||||
if theta_max == 0:
|
||||
return 16
|
||||
num = int(np.ceil(np.radians(abs(angle_deg)) / theta_max))
|
||||
return max(8, num)
|
||||
|
||||
|
||||
def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start: float, t_end: float, sagitta: float = 0.01) -> list[Polygon]:
|
||||
"""Helper to generate arc-shaped polygons."""
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(abs(t_end - t_start))), sagitta)
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
|
||||
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
|
||||
return [Polygon(inner_points + outer_points)]
|
||||
|
||||
|
||||
def _apply_collision_model(
|
||||
arc_poly: Polygon,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
|
||||
radius: float,
|
||||
clip_margin: float = 10.0
|
||||
) -> list[Polygon]:
|
||||
"""Applies the specified collision model to an arc geometry."""
|
||||
if isinstance(collision_type, Polygon):
|
||||
return [collision_type]
|
||||
|
||||
if collision_type == "arc":
|
||||
return [arc_poly]
|
||||
|
||||
# Get bounding box
|
||||
minx, miny, maxx, maxy = arc_poly.bounds
|
||||
bbox = box(minx, miny, maxx, maxy)
|
||||
|
||||
if collision_type == "bbox":
|
||||
return [bbox]
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
safe_zone = arc_poly.buffer(clip_margin)
|
||||
return [bbox.intersection(safe_zone)]
|
||||
|
||||
return [arc_poly]
|
||||
return max(4, num)
|
||||
|
||||
|
||||
class Bend90:
|
||||
@staticmethod
|
||||
def generate(
|
||||
start_port: Port,
|
||||
radius: float,
|
||||
width: float,
|
||||
direction: str = "CW",
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
) -> ComponentResult:
|
||||
def generate(start_port: Port, radius: float, width: float, direction: str = "CW", sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a 90-degree bend."""
|
||||
# direction: 'CW' (-90) or 'CCW' (+90)
|
||||
turn_angle = -90 if direction == "CW" else 90
|
||||
|
||||
# Calculate center of the arc
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
c_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
cx = start_port.x + radius * np.cos(c_angle)
|
||||
cy = start_port.y + radius * np.sin(c_angle)
|
||||
t_start = c_angle + np.pi
|
||||
t_end = t_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
center_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
cx = start_port.x + radius * np.cos(center_angle)
|
||||
cy = start_port.y + radius * np.sin(center_angle)
|
||||
|
||||
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
||||
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
||||
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
|
||||
# Center to start is radius at center_angle + pi
|
||||
theta_start = center_angle + np.pi
|
||||
theta_end = theta_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
|
||||
arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta)
|
||||
collision_polys = _apply_collision_model(arc_polys[0], collision_type, radius, clip_margin)
|
||||
ex = cx + radius * np.cos(theta_end)
|
||||
ey = cy + radius * np.sin(theta_end)
|
||||
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
|
||||
# End port orientation
|
||||
end_orientation = (start_port.orientation + turn_angle) % 360
|
||||
|
||||
snapped_ex = snap_search_grid(ex)
|
||||
snapped_ey = snap_search_grid(ey)
|
||||
|
||||
end_port = Port(snapped_ex, snapped_ey, float(end_orientation))
|
||||
|
||||
# Generate arc geometry
|
||||
num_segments = _get_num_segments(radius, 90, sagitta)
|
||||
angles = np.linspace(theta_start, theta_end, num_segments + 1)
|
||||
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
|
||||
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
|
||||
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
|
||||
|
||||
return ComponentResult(geometry=[Polygon(inner_points + outer_points)], end_port=end_port)
|
||||
|
||||
|
||||
class SBend:
|
||||
@staticmethod
|
||||
def generate(
|
||||
start_port: Port,
|
||||
offset: float,
|
||||
radius: float,
|
||||
width: float,
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
) -> ComponentResult:
|
||||
"""Generate a parametric S-bend (two tangent arcs)."""
|
||||
def generate(start_port: Port, offset: float, radius: float, width: float, sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a parametric S-bend (two tangent arcs). Only for offset < 2*radius."""
|
||||
if abs(offset) >= 2 * radius:
|
||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||
|
||||
# Analytical length: L = 2 * sqrt(O * (2*R - O/4)) is for a specific S-bend type.
|
||||
# Standard S-bend with two equal arcs:
|
||||
# Offset O = 2 * R * (1 - cos(theta))
|
||||
# theta = acos(1 - O / (2*R))
|
||||
theta = np.arccos(1 - abs(offset) / (2 * radius))
|
||||
|
||||
# Length of one arc = R * theta
|
||||
# Total length of S-bend = 2 * R * theta (arc length)
|
||||
# Horizontal distance dx = 2 * R * sin(theta)
|
||||
|
||||
dx = 2 * radius * np.sin(theta)
|
||||
dy = offset
|
||||
|
||||
# End port
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
||||
ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start))
|
||||
ex = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
|
||||
ey = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
|
||||
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
# Geometry: two arcs
|
||||
# First arc center
|
||||
direction = 1 if offset > 0 else -1
|
||||
c1_angle = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(c1_angle)
|
||||
cy1 = start_port.y + radius * np.sin(c1_angle)
|
||||
ts1, te1 = c1_angle + np.pi, c1_angle + np.pi + direction * theta
|
||||
center_angle1 = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(center_angle1)
|
||||
cy1 = start_port.y + radius * np.sin(center_angle1)
|
||||
|
||||
ex_raw = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
|
||||
ey_raw = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
|
||||
c2_angle = rad_start - direction * np.pi / 2
|
||||
cx2 = ex_raw + radius * np.cos(c2_angle)
|
||||
cy2 = ey_raw + radius * np.sin(c2_angle)
|
||||
te2 = c2_angle + np.pi
|
||||
ts2 = te2 + direction * theta
|
||||
# Second arc center
|
||||
center_angle2 = rad_start - direction * np.pi / 2
|
||||
cx2 = ex + radius * np.cos(center_angle2)
|
||||
cy2 = ey + radius * np.sin(center_angle2)
|
||||
|
||||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
||||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
||||
combined_arc = unary_union([arc1, arc2])
|
||||
# Generate points for both arcs
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(theta)), sagitta)
|
||||
# Arc 1: theta_start1 to theta_end1
|
||||
theta_start1 = center_angle1 + np.pi
|
||||
theta_end1 = theta_start1 - direction * theta
|
||||
|
||||
# Arc 2: theta_start2 to theta_end2
|
||||
theta_start2 = center_angle2
|
||||
theta_end2 = theta_start2 + direction * theta
|
||||
|
||||
def get_arc_points(cx: float, cy: float, r_inner: float, r_outer: float, t_start: float, t_end: float) -> list[tuple[float, float]]:
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
inner = [(cx + r_inner * np.cos(a), cy + r_inner * np.sin(a)) for a in angles]
|
||||
outer = [(cx + r_outer * np.cos(a), cy + r_outer * np.sin(a)) for a in reversed(angles)]
|
||||
return inner + outer
|
||||
|
||||
poly1 = Polygon(get_arc_points(cx1, cy1, radius - width / 2, radius + width / 2, theta_start1, theta_end1))
|
||||
poly2 = Polygon(get_arc_points(cx2, cy2, radius - width / 2, radius + width / 2, theta_end2, theta_start2))
|
||||
|
||||
return ComponentResult(geometry=[poly1, poly2], end_port=end_port)
|
||||
|
||||
collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import heapq
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
from inire.router.config import RouterConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
|
|
@ -47,60 +46,19 @@ class AStarNode:
|
|||
|
||||
|
||||
class AStarRouter:
|
||||
"""Hybrid State-Lattice A* Router."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cost_evaluator: CostEvaluator,
|
||||
node_limit: int = 1000000,
|
||||
straight_lengths: list[float] | None = None,
|
||||
bend_radii: list[float] | None = None,
|
||||
sbend_offsets: list[float] | None = None,
|
||||
sbend_radii: list[float] | None = None,
|
||||
snap_to_target_dist: float = 20.0,
|
||||
bend_penalty: float = 50.0,
|
||||
sbend_penalty: float = 100.0,
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] = "arc",
|
||||
bend_clip_margin: float = 10.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the A* Router.
|
||||
|
||||
Args:
|
||||
cost_evaluator: The evaluator for path and proximity costs.
|
||||
node_limit: Maximum number of nodes to expand before failing.
|
||||
straight_lengths: List of lengths for straight move expansion.
|
||||
bend_radii: List of radii for 90-degree bend moves.
|
||||
sbend_offsets: List of lateral offsets for S-bend moves.
|
||||
sbend_radii: List of radii for S-bend moves.
|
||||
snap_to_target_dist: Distance threshold for lookahead snapping.
|
||||
bend_penalty: Flat cost penalty for each 90-degree bend.
|
||||
sbend_penalty: Flat cost penalty for each S-bend.
|
||||
bend_collision_type: Type of collision model for bends ('arc', 'bbox', 'clipped_bbox').
|
||||
bend_clip_margin: Margin for 'clipped_bbox' collision model.
|
||||
"""
|
||||
def __init__(self, cost_evaluator: CostEvaluator) -> None:
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.config = RouterConfig(
|
||||
node_limit=node_limit,
|
||||
straight_lengths=straight_lengths if straight_lengths is not None else [1.0, 5.0, 25.0],
|
||||
bend_radii=bend_radii if bend_radii is not None else [10.0],
|
||||
sbend_offsets=sbend_offsets if sbend_offsets is not None else [-5.0, -2.0, 2.0, 5.0],
|
||||
sbend_radii=sbend_radii if sbend_radii is not None else [10.0],
|
||||
snap_to_target_dist=snap_to_target_dist,
|
||||
bend_penalty=bend_penalty,
|
||||
sbend_penalty=sbend_penalty,
|
||||
bend_collision_type=bend_collision_type,
|
||||
bend_clip_margin=bend_clip_margin,
|
||||
)
|
||||
self.node_limit = self.config.node_limit
|
||||
self.node_limit = 100000
|
||||
self.total_nodes_expanded = 0
|
||||
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
|
||||
|
||||
def route(self, start: Port, target: Port, net_width: float, net_id: str = "default") -> list[ComponentResult] | None:
|
||||
def route(
|
||||
self, start: Port, target: Port, net_width: float, net_id: str = "default"
|
||||
) -> list[ComponentResult] | None:
|
||||
"""Route a single net using A*."""
|
||||
self._collision_cache.clear()
|
||||
open_set: list[AStarNode] = []
|
||||
# Key: (x, y, orientation) rounded to 1nm
|
||||
# Key: (x, y, orientation)
|
||||
closed_set: set[tuple[float, float, float]] = set()
|
||||
|
||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||
|
|
@ -115,28 +73,27 @@ class AStarRouter:
|
|||
|
||||
current = heapq.heappop(open_set)
|
||||
|
||||
# Prune if already visited
|
||||
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2))
|
||||
state = (current.port.x, current.port.y, current.port.orientation)
|
||||
if state in closed_set:
|
||||
continue
|
||||
closed_set.add(state)
|
||||
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
|
||||
if nodes_expanded % 5000 == 0:
|
||||
logger.info(f"Nodes expanded: {nodes_expanded}, current port: {current.port}, g: {current.g_cost:.1f}, h: {current.h_cost:.1f}")
|
||||
|
||||
# Check if we reached the target exactly
|
||||
# Check if we reached the target (Snap-to-Target)
|
||||
if (
|
||||
abs(current.port.x - target.x) < 1e-6
|
||||
and abs(current.port.y - target.y) < 1e-6
|
||||
and abs(current.port.orientation - target.orientation) < 0.1
|
||||
and current.port.orientation == target.orientation
|
||||
):
|
||||
return self._reconstruct_path(current)
|
||||
|
||||
# Expansion
|
||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set)
|
||||
# Look-ahead snapping
|
||||
if self._try_snap_to_target(current, target, net_width, net_id, open_set):
|
||||
pass
|
||||
|
||||
# Expand neighbors
|
||||
self._expand_moves(current, target, net_width, net_id, open_set)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -147,82 +104,31 @@ class AStarRouter:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
) -> None:
|
||||
# 1. Snap-to-Target Look-ahead
|
||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||
if dist < self.config.snap_to_target_dist:
|
||||
# A. Try straight exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapStraight")
|
||||
|
||||
# B. Try SBend exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend", move_radius=radius)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. Lattice Straights
|
||||
lengths = self.config.straight_lengths
|
||||
if dist < 5.0:
|
||||
fine_steps = [0.1, 0.5]
|
||||
lengths = sorted(list(set(lengths + fine_steps)))
|
||||
|
||||
for length in lengths:
|
||||
# 1. Straights
|
||||
for length in [0.5, 1.0, 5.0, 25.0]:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"S{length}")
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"S{length}")
|
||||
|
||||
# 3. Lattice Bends
|
||||
for radius in self.config.bend_radii:
|
||||
# 2. Bends
|
||||
for radius in [5.0, 10.0, 20.0]:
|
||||
for direction in ["CW", "CCW"]:
|
||||
res = Bend90.generate(
|
||||
current.port,
|
||||
radius,
|
||||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}", move_radius=radius)
|
||||
res = Bend90.generate(current.port, radius, net_width, direction)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"B{radius}{direction}")
|
||||
|
||||
# 4. Discrete SBends
|
||||
for offset in self.config.sbend_offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
offset,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}", move_radius=radius)
|
||||
except ValueError:
|
||||
pass
|
||||
# 3. Parametric SBends
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
rad = np.radians(current.port.orientation)
|
||||
local_dy = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if 0 < abs(local_dy) < 40.0: # Match max 2*R
|
||||
try:
|
||||
# Use a standard radius for expansion
|
||||
res = SBend.generate(current.port, local_dy, 20.0, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"SB{local_dy}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _add_node(
|
||||
self,
|
||||
|
|
@ -232,19 +138,12 @@ class AStarRouter:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
move_type: str,
|
||||
move_radius: float | None = None,
|
||||
) -> None:
|
||||
# Check closed set before adding to open set
|
||||
state = (round(result.end_port.x, 3), round(result.end_port.y, 3), round(result.end_port.orientation, 2))
|
||||
if state in closed_set:
|
||||
return
|
||||
|
||||
cache_key = (
|
||||
round(parent.port.x, 3),
|
||||
round(parent.port.y, 3),
|
||||
round(parent.port.orientation, 2),
|
||||
parent.port.x,
|
||||
parent.port.y,
|
||||
parent.port.orientation,
|
||||
move_type,
|
||||
net_width,
|
||||
net_id,
|
||||
|
|
@ -262,66 +161,49 @@ class AStarRouter:
|
|||
if hard_coll:
|
||||
return
|
||||
|
||||
# 3. Check for Self-Intersection (Limited to last 100 segments for performance)
|
||||
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
for move_poly in result.geometry:
|
||||
dilated_move = move_poly.buffer(dilation)
|
||||
curr_p = parent
|
||||
seg_idx = 0
|
||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
||||
if seg_idx > 0:
|
||||
for prev_poly in curr_p.component_result.geometry:
|
||||
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \
|
||||
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \
|
||||
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
|
||||
dilated_move.bounds[3] < prev_poly.bounds[1] - dilation:
|
||||
continue
|
||||
move_cost = self.cost_evaluator.evaluate_move(result.geometry, result.end_port, net_width, net_id, start_port=parent.port)
|
||||
|
||||
dilated_prev = prev_poly.buffer(dilation)
|
||||
if dilated_move.intersects(dilated_prev):
|
||||
overlap = dilated_move.intersection(dilated_prev)
|
||||
if overlap.area > 1e-6:
|
||||
return
|
||||
curr_p = curr_p.parent
|
||||
seg_idx += 1
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
start_port=parent.port,
|
||||
length=result.length
|
||||
)
|
||||
|
||||
if move_cost > 1e12:
|
||||
return
|
||||
|
||||
# Turn penalties scaled by radius to favor larger turns
|
||||
ref_radius = 10.0
|
||||
if "B" in move_type and move_radius is not None:
|
||||
# Scale penalty: larger radius -> smaller penalty
|
||||
# e.g. radius 10 -> factor 1.0, radius 30 -> factor 0.33
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.bend_penalty * penalty_factor
|
||||
elif "SB" in move_type and move_radius is not None:
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.sbend_penalty * penalty_factor
|
||||
elif "B" in move_type:
|
||||
move_cost += self.config.bend_penalty
|
||||
elif "SB" in move_type:
|
||||
move_cost += self.config.sbend_penalty
|
||||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
g_cost = parent.g_cost + move_cost + self._step_cost(result)
|
||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||
|
||||
new_node = AStarNode(result.end_port, g_cost, h_cost, parent, result)
|
||||
heapq.heappush(open_set, new_node)
|
||||
|
||||
def _step_cost(self, result: ComponentResult) -> float:
|
||||
_ = result # Unused in base implementation
|
||||
return 0.0
|
||||
|
||||
def _try_snap_to_target(
|
||||
self,
|
||||
current: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
) -> bool:
|
||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||
if dist > 10.0:
|
||||
return False
|
||||
|
||||
if current.port.orientation == target.orientation:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, "SnapTarget")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
curr: AStarNode | None = end_node
|
||||
while curr and curr.component_result:
|
||||
curr = end_node
|
||||
while curr.component_result:
|
||||
path.append(curr.component_result)
|
||||
curr = curr.parent
|
||||
return path[::-1]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouterConfig:
|
||||
"""Configuration parameters for the A* Router."""
|
||||
|
||||
node_limit: int = 1000000
|
||||
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0])
|
||||
bend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0])
|
||||
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||
snap_to_target_dist: float = 20.0
|
||||
bend_penalty: float = 50.0
|
||||
sbend_penalty: float = 100.0
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
||||
bend_clip_margin: float = 10.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CostConfig:
|
||||
"""Configuration parameters for the Cost Evaluator."""
|
||||
|
||||
unit_length_cost: float = 1.0
|
||||
greedy_h_weight: float = 1.1
|
||||
congestion_penalty: float = 10000.0
|
||||
|
|
@ -2,8 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from inire.router.config import CostConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
|
@ -13,38 +11,16 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class CostEvaluator:
|
||||
"""Calculates total path and proximity costs."""
|
||||
"""Calculates total cost f(n) = g(n) + h(n)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collision_engine: CollisionEngine,
|
||||
danger_map: DangerMap,
|
||||
unit_length_cost: float = 1.0,
|
||||
greedy_h_weight: float = 1.1,
|
||||
congestion_penalty: float = 10000.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the Cost Evaluator.
|
||||
|
||||
Args:
|
||||
collision_engine: The engine for intersection checks.
|
||||
danger_map: Pre-computed grid for heuristic proximity costs.
|
||||
unit_length_cost: Cost multiplier per micrometer of path length.
|
||||
greedy_h_weight: Heuristic weighting (A* greedy factor).
|
||||
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
|
||||
"""
|
||||
def __init__(self, collision_engine: CollisionEngine, danger_map: DangerMap) -> None:
|
||||
self.collision_engine = collision_engine
|
||||
self.danger_map = danger_map
|
||||
self.config = CostConfig(
|
||||
unit_length_cost=unit_length_cost,
|
||||
greedy_h_weight=greedy_h_weight,
|
||||
congestion_penalty=congestion_penalty,
|
||||
)
|
||||
|
||||
# Use config values
|
||||
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
|
||||
# Cost weights
|
||||
self.unit_length_cost = 1.0
|
||||
self.bend_cost_multiplier = 10.0
|
||||
self.greedy_h_weight = 1.1
|
||||
self.congestion_penalty = 100.0 # Multiplier for overlaps
|
||||
|
||||
def g_proximity(self, x: float, y: float) -> float:
|
||||
"""Get proximity cost from the Danger Map."""
|
||||
|
|
@ -68,30 +44,19 @@ class CostEvaluator:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
start_port: Port | None = None,
|
||||
length: float = 0.0,
|
||||
) -> float:
|
||||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||
_ = net_width # Unused
|
||||
total_cost = length * self.unit_length_cost
|
||||
|
||||
# 1. Hard Collision check (Static obstacles)
|
||||
# We buffer by the full clearance to ensure distance >= clearance
|
||||
hard_dilation = self.collision_engine.clearance
|
||||
total_cost = 0.0
|
||||
# Strict collision check
|
||||
for poly in geometry:
|
||||
dilated_poly = poly.buffer(hard_dilation)
|
||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
||||
return 1e15 # Impossible cost for hard collisions
|
||||
if self.collision_engine.is_collision(poly, net_width, start_port=start_port, end_port=end_port):
|
||||
return 1e9 # Massive cost for hard collisions
|
||||
|
||||
# 2. Soft Collision check (Negotiated Congestion)
|
||||
# We buffer by clearance/2 because both paths are buffered by clearance/2
|
||||
soft_dilation = self.collision_engine.clearance / 2.0
|
||||
for poly in geometry:
|
||||
dilated_poly = poly.buffer(soft_dilation)
|
||||
overlaps = self.collision_engine.count_congestion_prebuffered(dilated_poly, net_id)
|
||||
if overlaps > 0:
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
# Negotiated Congestion Cost
|
||||
overlaps = self.collision_engine.count_congestion(poly, net_id)
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
|
||||
# 3. Proximity cost from Danger Map
|
||||
# Proximity cost from Danger Map
|
||||
total_cost += self.g_proximity(end_port.x, end_port.y)
|
||||
return total_cost
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class PathFinder:
|
|||
def __init__(self, router: AStarRouter, cost_evaluator: CostEvaluator) -> None:
|
||||
self.router = router
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.max_iterations = 10
|
||||
self.max_iterations = 20
|
||||
self.base_congestion_penalty = 100.0
|
||||
|
||||
def route_all(self, netlist: dict[str, tuple[Port, Port]], net_widths: dict[str, float]) -> dict[str, RoutingResult]:
|
||||
|
|
@ -38,7 +38,7 @@ class PathFinder:
|
|||
|
||||
start_time = time.monotonic()
|
||||
num_nets = len(netlist)
|
||||
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
||||
session_timeout = max(30.0, 0.5 * num_nets * self.max_iterations)
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
any_congestion = False
|
||||
|
|
|
|||
|
|
@ -1,87 +1,71 @@
|
|||
import pytest
|
||||
import numpy as np
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
from inire.utils.validation import validate_routing_result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
|
||||
def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_astar_straight(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||
assert len(path) > 0
|
||||
# Final port should be target
|
||||
assert abs(path[-1].end_port.x - 50.0) < 1e-6
|
||||
assert path[-1].end_port.y == 0.0
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
assert validation["connectivity_ok"]
|
||||
# Path should be exactly 50um (or slightly more if it did weird things, but here it's straight)
|
||||
assert abs(validation["total_length"] - 50.0) < 1e-6
|
||||
|
||||
|
||||
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_astar_bend(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
||||
# From (0,0,0) -> Bend90 CW R=10 -> (10, -10, 270) ??? No.
|
||||
# Try: (0,0,0) -> Bend90 CCW R=10 -> (10, 10, 90) -> Straight 10 -> (10, 20, 90) -> Bend90 CW R=10 -> (20, 30, 0)
|
||||
target = Port(20, 20, 0)
|
||||
target = Port(20, 20, 90)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||
assert any("Bend90" in str(res) or hasattr(res, 'geometry') for res in path) # Loose check
|
||||
assert abs(path[-1].end_port.x - 20.0) < 1e-6
|
||||
assert abs(path[-1].end_port.y - 20.0) < 1e-6
|
||||
assert path[-1].end_port.orientation == 90.0
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
assert validation["connectivity_ok"]
|
||||
|
||||
|
||||
def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_astar_obstacle(basic_evaluator) -> None:
|
||||
# Add an obstacle in the middle of a straight path
|
||||
# Obstacle from x=20 to 40, y=-20 to 20
|
||||
obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)])
|
||||
obstacle = Polygon([(20, -5), (30, -5), (30, 5), (20, 5)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||
basic_evaluator.danger_map.precompute([obstacle])
|
||||
|
||||
router = AStarRouter(basic_evaluator)
|
||||
router.node_limit = 1000000 # Give it more room for detour
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(60, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
||||
# Path should have diverted (check that it's not a single straight)
|
||||
# The path should go around the 5um half-width obstacle.
|
||||
# Total wire length should be > 50.
|
||||
sum(np.sqrt((p.end_port.x - p.geometry[0].bounds[0])**2 + (p.end_port.y - p.geometry[0].bounds[1])**2) for p in path)
|
||||
# That's a rough length estimate.
|
||||
# Better: check that no part of the path collides.
|
||||
for res in path:
|
||||
for poly in res.geometry:
|
||||
assert not poly.intersects(obstacle)
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
# Path should have detoured, so length > 50
|
||||
assert validation["total_length"] > 50.0
|
||||
|
||||
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Target is NOT on 1um grid
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(10.1, 0, 0)
|
||||
target = Port(10.005, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
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 abs(path[-1].end_port.x - 10.005) < 1e-6
|
||||
|
|
|
|||
|
|
@ -1,56 +1,54 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
|
||||
def test_collision_detection() -> None:
|
||||
# Clearance = 2um
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
|
||||
# 10x10 um obstacle at (10,10)
|
||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||
# Static obstacle at (10, 10) with size 5x5
|
||||
obstacle = Polygon([(10,10), (15,10), (15,15), (10,15)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Net width = 2um
|
||||
# Dilation = (W+C)/2 = (2+2)/2 = 2.0um
|
||||
|
||||
# 1. Direct hit
|
||||
test_poly = Polygon([(12, 12), (13, 12), (13, 13), (12, 13)])
|
||||
assert engine.is_collision(test_poly, net_width=2.0)
|
||||
test_poly = Polygon([(12,12), (13,12), (13,13), (12,13)])
|
||||
assert engine.is_collision(test_poly, net_width=2.0) is True
|
||||
|
||||
# 2. Far away
|
||||
test_poly_far = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
|
||||
assert not engine.is_collision(test_poly_far, net_width=2.0)
|
||||
test_poly_far = Polygon([(0,0), (5,0), (5,5), (0,5)])
|
||||
assert engine.is_collision(test_poly_far, net_width=2.0) is False
|
||||
|
||||
# 3. Near hit (within clearance)
|
||||
# Obstacle edge at x=10.
|
||||
# test_poly edge at x=9.
|
||||
# Distance = 1.0 um.
|
||||
# Obstacle is at (10,10).
|
||||
# test_poly is at (8,10) to (9,15).
|
||||
# Centerline at 8.5. Distance to 10 is 1.5.
|
||||
# Required distance (Wi+C)/2 = 2.0. Collision!
|
||||
test_poly_near = Polygon([(8, 10), (9, 10), (9, 15), (8, 15)])
|
||||
assert engine.is_collision(test_poly_near, net_width=2.0)
|
||||
|
||||
test_poly_near = Polygon([(8,10), (9,10), (9,15), (8,15)])
|
||||
assert engine.is_collision(test_poly_near, net_width=2.0) is True
|
||||
|
||||
def test_safety_zone() -> None:
|
||||
# Use zero clearance for this test to verify the 2nm port safety zone
|
||||
# against the physical obstacle boundary.
|
||||
engine = CollisionEngine(clearance=0.0)
|
||||
|
||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||
obstacle = Polygon([(10,10), (15,10), (15,15), (10,15)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Port exactly on the boundary
|
||||
start_port = Port(10.0, 12.0, 0)
|
||||
# Port exactly on the boundary (x=10)
|
||||
start_port = Port(10.0, 12.0, 0.0)
|
||||
|
||||
# Move starting from this port that overlaps the obstacle by 1nm
|
||||
# (Inside the 2nm safety zone)
|
||||
# A very narrow waveguide (1nm width) that overlaps by 1nm.
|
||||
# Overlap is from x=10 to x=10.001, y=11.9995 to 12.0005.
|
||||
# This fits entirely within a 2nm radius of (10.0, 12.0).
|
||||
test_poly = Polygon([(9.999, 11.9995), (10.001, 11.9995), (10.001, 12.0005), (9.999, 12.0005)])
|
||||
|
||||
assert not engine.is_collision(test_poly, net_width=0.001, start_port=start_port)
|
||||
|
||||
assert engine.is_collision(test_poly, net_width=0.001, start_port=start_port) is False
|
||||
|
||||
def test_configurable_max_net_width() -> None:
|
||||
# Large max_net_width (10.0) -> large pre-dilation (6.0)
|
||||
engine = CollisionEngine(clearance=2.0, max_net_width=10.0)
|
||||
|
||||
obstacle = Polygon([(20, 20), (25, 20), (25, 25), (20, 25)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
|
|
@ -58,4 +56,4 @@ def test_configurable_max_net_width() -> None:
|
|||
# physical check: dilated test_poly by C/2 = 1.0.
|
||||
# Dilated test_poly bounds: (14, 19, 17, 26).
|
||||
# obstacle: (20, 20, 25, 25). No physical collision.
|
||||
assert not engine.is_collision(test_poly, net_width=2.0)
|
||||
assert engine.is_collision(test_poly, net_width=2.0) is False
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
from inire.geometry.components import Straight, Bend90, SBend
|
||||
|
||||
def test_straight_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
|
|
@ -10,83 +8,68 @@ def test_straight_generation() -> None:
|
|||
width = 2.0
|
||||
result = Straight.generate(start, length, width)
|
||||
|
||||
# End port check
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 0.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
assert len(result.geometry) == 1
|
||||
|
||||
# Bounds of the polygon
|
||||
minx, miny, maxx, maxy = result.geometry[0].bounds
|
||||
# Geometry check
|
||||
poly = result.geometry[0]
|
||||
assert poly.area == length * width
|
||||
# Check bounds
|
||||
minx, miny, maxx, maxy = poly.bounds
|
||||
assert minx == 0.0
|
||||
assert maxx == 10.0
|
||||
assert miny == -1.0
|
||||
assert maxy == 1.0
|
||||
|
||||
|
||||
def test_bend90_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
# CW bend (0 -> 270)
|
||||
result_cw = Bend90.generate(start, radius, width, direction='CW')
|
||||
|
||||
# CW bend
|
||||
result_cw = Bend90.generate(start, radius, width, direction="CW")
|
||||
# End port (center is at (0, -10))
|
||||
# End port is at (10, -10) relative to center if it was 90-degree turn?
|
||||
# No, from center (0, -10), start is (0, 0) which is 90 deg.
|
||||
# Turn -90 deg -> end is at 0 deg from center -> (10, -10)
|
||||
assert result_cw.end_port.x == 10.0
|
||||
assert result_cw.end_port.y == -10.0
|
||||
assert result_cw.end_port.orientation == 270.0
|
||||
|
||||
# CCW bend
|
||||
result_ccw = Bend90.generate(start, radius, width, direction="CCW")
|
||||
# CCW bend (0 -> 90)
|
||||
result_ccw = Bend90.generate(start, radius, width, direction='CCW')
|
||||
assert result_ccw.end_port.x == 10.0
|
||||
assert result_ccw.end_port.y == 10.0
|
||||
assert result_ccw.end_port.orientation == 90.0
|
||||
|
||||
|
||||
def test_sbend_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
offset = 5.0
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
result = SBend.generate(start, offset, radius, width)
|
||||
|
||||
# End port check
|
||||
assert result.end_port.y == 5.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
assert len(result.geometry) == 1 # Now uses unary_union
|
||||
|
||||
# Geometry check (two arcs)
|
||||
assert len(result.geometry) == 2
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||
with pytest.raises(ValueError):
|
||||
SBend.generate(start, 25.0, 10.0, 2.0)
|
||||
|
||||
|
||||
def test_bend_collision_models() -> None:
|
||||
def test_bend_snapping() -> None:
|
||||
# Radius that results in non-integer coords
|
||||
radius = 10.1234
|
||||
start = Port(0, 0, 0)
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
# 1. BBox model
|
||||
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).
|
||||
# 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
|
||||
assert minx <= 0.0 + 1e-6
|
||||
assert maxx >= 10.0 - 1e-6
|
||||
assert miny <= 0.0 + 1e-6
|
||||
assert maxy >= 10.0 - 1e-6
|
||||
|
||||
# 2. Clipped BBox model
|
||||
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0)
|
||||
# Area should be less than full bbox
|
||||
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
|
||||
|
||||
|
||||
def test_sbend_collision_models() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
offset = 5.0
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
||||
# Geometry should be a single bounding box polygon
|
||||
assert len(res_bbox.geometry) == 1
|
||||
|
||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
||||
result = Bend90.generate(start, radius, 2.0, direction='CCW')
|
||||
# End port should be snapped to 1µm (SEARCH_GRID_SNAP_UM)
|
||||
# ex = 10.1234, ey = 10.1234
|
||||
# snapped: ex = 10.0, ey = 10.0 if we round to nearest 1.0?
|
||||
# SEARCH_GRID_SNAP_UM = 1.0
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 10.0
|
||||
|
|
|
|||
|
|
@ -1,58 +1,60 @@
|
|||
import pytest
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# Wider bounds to allow going around (y from -40 to 40)
|
||||
danger_map = DangerMap(bounds=(0, -40, 100, 40))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
|
||||
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_astar_sbend(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Start at (0,0), target at (50, 2) -> 2um lateral offset
|
||||
# This matches one of our discretized SBend offsets.
|
||||
# Start at (0,0), target at (50, 3) -> 3um lateral offset
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 2, 0)
|
||||
target = Port(50, 3, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
# Check if any component in the path is an SBend
|
||||
found_sbend = False
|
||||
for res in path:
|
||||
# Check if the end port orientation is same as start
|
||||
# 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:
|
||||
found_sbend = True
|
||||
break
|
||||
# SBend should align us with the target y=3
|
||||
if abs(res.end_port.y - 3.0) < 1e-6 and res.end_port.orientation == 0:
|
||||
found_sbend = True
|
||||
assert found_sbend
|
||||
|
||||
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
pf.max_iterations = 10
|
||||
|
||||
netlist = {
|
||||
"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}
|
||||
|
||||
# Force them into a narrow corridor that only fits ONE.
|
||||
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)])
|
||||
# Tiny obstacles to block net1 and net2 direct paths?
|
||||
# No, let's block the space BETWEEN them so they must choose
|
||||
# to either stay far apart or squeeze together.
|
||||
# Actually, let's block their direct paths and force them
|
||||
# into a narrow corridor that only fits ONE.
|
||||
|
||||
# Obstacles creating a wide wall with a narrow 2um gap at y=5.
|
||||
# Gap y: 4 to 6. Center y=5.
|
||||
# Net 1 (y=0) and Net 2 (y=10) both want to go to y=5 to pass.
|
||||
# But only ONE fits at y=5.
|
||||
|
||||
obs_top = Polygon([(20, 6), (30, 6), (30, 30), (20, 30)])
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -30), (20, -30)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_top)
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_bottom)
|
||||
basic_evaluator.danger_map.precompute([obs_top, obs_bottom])
|
||||
|
|
@ -64,3 +66,5 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvalua
|
|||
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
|
|
|||
|
|
@ -1,25 +1,36 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
def test_cost_calculation() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# 50x50 um area, 1um resolution
|
||||
danger_map = DangerMap(bounds=(0, 0, 50, 50))
|
||||
danger_map.precompute([])
|
||||
danger_map = DangerMap(bounds=(0, 0, 50, 50), resolution=1.0, safety_threshold=10.0, k=1.0)
|
||||
|
||||
# Add a central obstacle
|
||||
# Grid cells are indexed from self.minx.
|
||||
obstacle = Polygon([(20,20), (30,20), (30,30), (20,30)])
|
||||
danger_map.precompute([obstacle])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
|
||||
p1 = Port(0, 0, 0)
|
||||
p2 = Port(10, 10, 0)
|
||||
# 1. Cost far from obstacle
|
||||
cost_far = evaluator.g_proximity(5.0, 5.0)
|
||||
assert cost_far == 0.0
|
||||
|
||||
h = evaluator.h_manhattan(p1, p2)
|
||||
# Manhattan distance = 20. Orientation penalty = 0.
|
||||
# Weighted by 1.1 -> 22.0
|
||||
assert abs(h - 22.0) < 1e-6
|
||||
# 2. Cost near obstacle (d=1.0)
|
||||
# Cell center (20.5, 20.5) is inside. Cell (19.5, 20.5) center to boundary (20, 20.5) is 0.5.
|
||||
# Scipy EDT gives distance to mask=False.
|
||||
cost_near = evaluator.g_proximity(19.0, 25.0)
|
||||
assert cost_near > 0.0
|
||||
|
||||
# Orientation penalty
|
||||
p3 = Port(10, 10, 90)
|
||||
h_wrong = evaluator.h_manhattan(p1, p3)
|
||||
assert h_wrong > h
|
||||
# 3. Collision cost
|
||||
engine.add_static_obstacle(obstacle)
|
||||
test_poly = Polygon([(22, 22), (23, 22), (23, 23), (22, 23)])
|
||||
# end_port at (22.5, 22.5)
|
||||
move_cost = evaluator.evaluate_move(
|
||||
[test_poly], Port(22.5, 22.5, 0), net_width=2.0, net_id="net1"
|
||||
)
|
||||
assert move_cost == 1e9
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from shapely.geometry import Polygon
|
||||
|
|
@ -14,7 +12,7 @@ from inire.utils.validation import validate_routing_result
|
|||
|
||||
|
||||
@st.composite
|
||||
def random_obstacle(draw: Any) -> Polygon:
|
||||
def random_obstacle(draw):
|
||||
x = draw(st.floats(min_value=0, max_value=20))
|
||||
y = draw(st.floats(min_value=0, max_value=20))
|
||||
w = draw(st.floats(min_value=1, max_value=5))
|
||||
|
|
@ -23,7 +21,7 @@ def random_obstacle(draw: Any) -> Polygon:
|
|||
|
||||
|
||||
@st.composite
|
||||
def random_port(draw: Any) -> Port:
|
||||
def random_port(draw):
|
||||
x = draw(st.floats(min_value=0, max_value=20))
|
||||
y = draw(st.floats(min_value=0, max_value=20))
|
||||
orientation = draw(st.sampled_from([0, 90, 180, 270]))
|
||||
|
|
@ -32,7 +30,7 @@ def random_port(draw: Any) -> Port:
|
|||
|
||||
@settings(max_examples=3, deadline=None)
|
||||
@given(obstacles=st.lists(random_obstacle(), min_size=0, max_size=3), start=random_port(), target=random_port())
|
||||
def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port) -> None:
|
||||
def test_fuzz_astar_no_crash(obstacles, start, target) -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
for obs in obstacles:
|
||||
engine.add_static_obstacle(obs)
|
||||
|
|
@ -56,11 +54,10 @@ def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port
|
|||
result,
|
||||
obstacles,
|
||||
clearance=2.0,
|
||||
expected_start=start,
|
||||
expected_end=target,
|
||||
start_port_coord=(start.x, start.y),
|
||||
end_port_coord=(target.x, target.y),
|
||||
)
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
|
||||
except Exception as e:
|
||||
# Unexpected exceptions are failures
|
||||
pytest.fail(f"Router crashed with {type(e).__name__}: {e}")
|
||||
|
|
|
|||
|
|
@ -1,35 +1,51 @@
|
|||
import pytest
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
|
||||
def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> None:
|
||||
def test_pathfinder_parallel(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
|
||||
netlist = {
|
||||
"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}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
||||
def test_pathfinder_congestion(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
|
||||
# Net1 blocks Net2
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||
"net2": (Port(25, -10, 90), Port(25, 10, 90))
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# Verify both nets are valid and collision-free
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,43 @@
|
|||
from typing import Any
|
||||
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||
|
||||
from inire.geometry.primitives import Port, translate_port, rotate_port
|
||||
|
||||
@st.composite
|
||||
def port_strategy(draw: Any) -> Port:
|
||||
def port_strategy(draw):
|
||||
x = draw(st.floats(min_value=-1e6, max_value=1e6))
|
||||
y = draw(st.floats(min_value=-1e6, max_value=1e6))
|
||||
orientation = draw(st.sampled_from([0, 90, 180, 270]))
|
||||
return Port(x, y, orientation)
|
||||
|
||||
|
||||
def test_port_snapping() -> None:
|
||||
p = Port(0.123456, 0.654321, 90)
|
||||
assert p.x == 0.123
|
||||
assert p.y == 0.654
|
||||
|
||||
assert p.orientation == 90.0
|
||||
|
||||
@given(p=port_strategy())
|
||||
def test_port_transform_invariants(p: Port) -> None:
|
||||
def test_port_transform_invariants(p) -> None:
|
||||
# Rotating 90 degrees 4 times should return to same orientation
|
||||
p_rot = p
|
||||
for _ in range(4):
|
||||
p_rot = rotate_port(p_rot, 90)
|
||||
|
||||
assert abs(p_rot.x - p.x) < 1e-6
|
||||
assert abs(p_rot.y - p.y) < 1e-6
|
||||
assert (p_rot.orientation % 360) == (p.orientation % 360)
|
||||
assert p_rot.orientation == p.orientation
|
||||
# Coordinates should be close (floating point error) but snapped to 1nm
|
||||
assert abs(p_rot.x - p.x) < 1e-9
|
||||
assert abs(p_rot.y - p.y) < 1e-9
|
||||
|
||||
|
||||
@given(
|
||||
p=port_strategy(),
|
||||
dx=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:
|
||||
@given(p=port_strategy(), dx=st.floats(min_value=-1000, max_value=1000), dy=st.floats(min_value=-1000, max_value=1000))
|
||||
def test_translate_snapping(p, dx, dy) -> None:
|
||||
p_trans = translate_port(p, dx, dy)
|
||||
# Check that snapped result is indeed multiple of GRID_SNAP_UM (0.001 um = 1nm)
|
||||
# Multiplication is more stable for this check
|
||||
assert abs(p_trans.x * 1000 - round(p_trans.x * 1000)) < 1e-6
|
||||
assert abs(p_trans.y * 1000 - round(p_trans.y * 1000)) < 1e-6
|
||||
|
||||
|
||||
def test_orientation_normalization() -> None:
|
||||
p = Port(0, 0, 360)
|
||||
assert p.orientation == 0.0
|
||||
|
||||
p2 = Port(0, 0, -90)
|
||||
assert p2.orientation == 270.0
|
||||
p3 = Port(0, 0, 95) # Should snap to 90
|
||||
assert p3.orientation == 90.0
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.components import Bend90
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
from inire.geometry.components import Bend90
|
||||
|
||||
def test_arc_resolution_sagitta() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
|
|
@ -22,7 +21,6 @@ def test_arc_resolution_sagitta() -> None:
|
|||
|
||||
assert pts_fine > pts_coarse
|
||||
|
||||
|
||||
def test_locked_paths() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||
|
|
@ -49,7 +47,6 @@ def test_locked_paths() -> None:
|
|||
# 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).
|
||||
# Since netA is static, netB will see it as a HARD collision if it tries to cross.
|
||||
|
||||
# Our A* will find a detour around the static obstacle.
|
||||
assert results_b["netB"].is_valid
|
||||
|
||||
|
|
@ -59,4 +56,5 @@ def test_locked_paths() -> None:
|
|||
|
||||
for pa in poly_a:
|
||||
for pb in poly_b:
|
||||
assert not pa.intersects(pb)
|
||||
# Check physical clearance
|
||||
assert not pa.buffer(1.0).intersects(pb.buffer(1.0))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.geometry import Point
|
||||
from shapely.ops import unary_union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.primitives import Port
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
|
||||
|
||||
|
|
@ -15,9 +15,9 @@ def validate_routing_result(
|
|||
result: RoutingResult,
|
||||
static_obstacles: list[Polygon],
|
||||
clearance: float,
|
||||
expected_start: Port | None = None,
|
||||
expected_end: Port | None = None,
|
||||
) -> dict[str, Any]:
|
||||
start_port_coord: tuple[float, float] | None = None,
|
||||
end_port_coord: tuple[float, float] | None = None,
|
||||
) -> dict[str, any]:
|
||||
"""
|
||||
Perform a high-precision validation of a routed path.
|
||||
Returns a dictionary with validation results.
|
||||
|
|
@ -25,71 +25,33 @@ def validate_routing_result(
|
|||
if not result.path:
|
||||
return {"is_valid": False, "reason": "No path found"}
|
||||
|
||||
obstacle_collision_geoms = []
|
||||
self_intersection_geoms = []
|
||||
connectivity_errors = []
|
||||
collision_geoms = []
|
||||
# High-precision safety zones
|
||||
safe_zones = []
|
||||
if start_port_coord:
|
||||
safe_zones.append(Point(start_port_coord).buffer(0.002))
|
||||
if end_port_coord:
|
||||
safe_zones.append(Point(end_port_coord).buffer(0.002))
|
||||
safe_poly = unary_union(safe_zones) if safe_zones else None
|
||||
|
||||
# 1. Connectivity Check
|
||||
total_length = 0.0
|
||||
for i, comp in enumerate(result.path):
|
||||
total_length += comp.length
|
||||
# Buffer by C/2
|
||||
dilation = clearance / 2.0
|
||||
|
||||
# Boundary check
|
||||
if expected_end:
|
||||
last_port = result.path[-1].end_port
|
||||
dist_to_end = np.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
||||
if dist_to_end > 0.005:
|
||||
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
||||
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
||||
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
||||
|
||||
# 2. Geometry Buffering
|
||||
dilation_half = clearance / 2.0
|
||||
dilation_full = clearance
|
||||
|
||||
dilated_for_self = []
|
||||
|
||||
for i, comp in enumerate(result.path):
|
||||
for comp in result.path:
|
||||
for poly in comp.geometry:
|
||||
# Check against obstacles
|
||||
d_full = poly.buffer(dilation_full)
|
||||
dilated = poly.buffer(dilation)
|
||||
for obs in static_obstacles:
|
||||
if d_full.intersects(obs):
|
||||
intersection = d_full.intersection(obs)
|
||||
if intersection.area > 1e-9:
|
||||
obstacle_collision_geoms.append(intersection)
|
||||
if dilated.intersects(obs):
|
||||
intersection = dilated.intersection(obs)
|
||||
if safe_poly:
|
||||
# Remove safe zones from intersection
|
||||
intersection = intersection.difference(safe_poly)
|
||||
|
||||
# Save for self-intersection check
|
||||
dilated_for_self.append(poly.buffer(dilation_half))
|
||||
|
||||
# 3. Self-intersection
|
||||
for i, seg_i in enumerate(dilated_for_self):
|
||||
for j, seg_j in enumerate(dilated_for_self):
|
||||
if j > i + 1: # Non-adjacent
|
||||
if seg_i.intersects(seg_j):
|
||||
overlap = seg_i.intersection(seg_j)
|
||||
if overlap.area > 1e-6:
|
||||
self_intersection_geoms.append((i, j, overlap))
|
||||
|
||||
is_valid = (len(obstacle_collision_geoms) == 0 and
|
||||
len(self_intersection_geoms) == 0 and
|
||||
len(connectivity_errors) == 0)
|
||||
|
||||
reasons = []
|
||||
if obstacle_collision_geoms:
|
||||
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
||||
if self_intersection_geoms:
|
||||
# report which indices
|
||||
idx_str = ", ".join([f"{i}-{j}" for i, j, _ in self_intersection_geoms[:5]])
|
||||
reasons.append(f"Found {len(self_intersection_geoms)} self-intersections (e.g. {idx_str}).")
|
||||
if connectivity_errors:
|
||||
reasons.extend(connectivity_errors)
|
||||
if not intersection.is_empty and intersection.area > 1e-9:
|
||||
collision_geoms.append(intersection)
|
||||
|
||||
return {
|
||||
"is_valid": is_valid,
|
||||
"reason": " ".join(reasons),
|
||||
"obstacle_collisions": obstacle_collision_geoms,
|
||||
"self_intersections": self_intersection_geoms,
|
||||
"total_length": total_length,
|
||||
"connectivity_ok": len(connectivity_errors) == 0,
|
||||
"is_valid": len(collision_geoms) == 0,
|
||||
"collisions": collision_geoms,
|
||||
"collision_count": len(collision_geoms),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.figure import Figure
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
|
||||
|
||||
|
|
@ -18,7 +16,6 @@ def plot_routing_results(
|
|||
results: dict[str, RoutingResult],
|
||||
static_obstacles: list[Polygon],
|
||||
bounds: tuple[float, float, float, float],
|
||||
netlist: dict[str, tuple[Port, Port]] | None = None,
|
||||
) -> tuple[Figure, Axes]:
|
||||
"""Plot obstacles and routed paths using matplotlib."""
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
|
@ -31,49 +28,18 @@ def plot_routing_results(
|
|||
# Plot paths
|
||||
colors = plt.get_cmap("tab10")
|
||||
for i, (net_id, res) in enumerate(results.items()):
|
||||
# Use modulo to avoid index out of range for many nets
|
||||
color: str | tuple[float, ...] = colors(i % 10)
|
||||
color = colors(i)
|
||||
if not res.is_valid:
|
||||
color = "red" # Highlight failing nets
|
||||
|
||||
label_added = False
|
||||
for j, comp in enumerate(res.path):
|
||||
# 1. Plot geometry
|
||||
for comp in res.path:
|
||||
for poly in comp.geometry:
|
||||
# Handle both Polygon and MultiPolygon (e.g. from SBend)
|
||||
geoms = [poly] if hasattr(poly, "exterior") else poly.geoms
|
||||
for g in geoms:
|
||||
x, y = g.exterior.xy
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "")
|
||||
label_added = True
|
||||
|
||||
# 2. Plot subtle port orientation arrow for internal ports
|
||||
# (Every segment's end_port except possibly the last one if it matches target)
|
||||
p = comp.end_port
|
||||
rad = np.radians(p.orientation)
|
||||
u = np.cos(rad)
|
||||
v = np.sin(rad)
|
||||
|
||||
# Internal ports get smaller, narrower, semi-transparent arrows
|
||||
ax.quiver(p.x, p.y, u, v, color="black", scale=40, width=0.003, alpha=0.3, pivot="tail", zorder=4)
|
||||
|
||||
# 3. Plot main arrows for netlist ports (if provided)
|
||||
if netlist and net_id in netlist:
|
||||
start_p, target_p = netlist[net_id]
|
||||
for p in [start_p, target_p]:
|
||||
rad = np.radians(p.orientation)
|
||||
u = np.cos(rad)
|
||||
v = np.sin(rad)
|
||||
# Netlist ports get prominent arrows
|
||||
ax.quiver(p.x, p.y, u, v, color="black", scale=25, width=0.005, pivot="tail", zorder=6)
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if i == 0 else "")
|
||||
|
||||
ax.set_xlim(bounds[0], bounds[2])
|
||||
ax.set_ylim(bounds[1], bounds[3])
|
||||
ax.set_aspect("equal")
|
||||
ax.set_title("Inire Routing Results")
|
||||
# Only show legend if we have labels
|
||||
handles, labels = ax.get_legend_handles_labels()
|
||||
if labels:
|
||||
ax.legend()
|
||||
ax.grid(alpha=0.6)
|
||||
plt.grid(True)
|
||||
return fig, ax
|
||||
|
|
|
|||
73
uv.lock
generated
73
uv.lock
generated
|
|
@ -178,7 +178,6 @@ dependencies = [
|
|||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "scipy" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
|
||||
|
|
@ -195,7 +194,6 @@ requires-dist = [
|
|||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "scipy" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
|
||||
|
|
@ -632,77 +630,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shapely"
|
||||
version = "2.1.2"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue