Initial buildout
This commit is contained in:
parent
34615f3aac
commit
f600b52f32
25 changed files with 1856 additions and 23 deletions
140
inire/geometry/collision.py
Normal file
140
inire/geometry/collision.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import rtree
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
from shapely.prepared import prep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.prepared import PreparedGeometry
|
||||
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
|
||||
class CollisionEngine:
|
||||
"""Manages spatial queries for collision detection."""
|
||||
|
||||
def __init__(self, clearance: float, max_net_width: float = 2.0) -> None:
|
||||
self.clearance = clearance
|
||||
self.max_net_width = max_net_width
|
||||
self.static_obstacles = rtree.index.Index()
|
||||
# To store geometries for precise checks
|
||||
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||
self.prepared_obstacles: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
|
||||
self._id_counter = 0
|
||||
|
||||
# Dynamic paths for multi-net congestion
|
||||
self.dynamic_paths = rtree.index.Index()
|
||||
# obj_id -> (net_id, geometry)
|
||||
self.path_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
self._dynamic_id_counter = 0
|
||||
|
||||
def add_static_obstacle(self, polygon: Polygon, pre_dilate: bool = True) -> None:
|
||||
"""Add a static obstacle to the engine."""
|
||||
_ = pre_dilate # Keep for API compatibility
|
||||
obj_id = self._id_counter
|
||||
self._id_counter += 1
|
||||
|
||||
self.obstacle_geometries[obj_id] = polygon
|
||||
self.prepared_obstacles[obj_id] = prep(polygon)
|
||||
|
||||
# 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."""
|
||||
# Dilate by clearance/2 for congestion
|
||||
dilation = self.clearance / 2.0
|
||||
for poly in geometry:
|
||||
dilated = poly.buffer(dilation)
|
||||
obj_id = self._dynamic_id_counter
|
||||
self._dynamic_id_counter += 1
|
||||
self.path_geometries[obj_id] = (net_id, dilated)
|
||||
self.dynamic_paths.insert(obj_id, dilated.bounds)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
"""Remove a net's path from the dynamic R-Tree."""
|
||||
to_remove = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
|
||||
for obj_id in to_remove:
|
||||
nid, dilated = self.path_geometries.pop(obj_id)
|
||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
||||
|
||||
def lock_net(self, net_id: str) -> None:
|
||||
"""Move a net's dynamic path to static obstacles permanently."""
|
||||
to_move = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
|
||||
for obj_id in to_move:
|
||||
nid, dilated = self.path_geometries.pop(obj_id)
|
||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
||||
|
||||
# Add to static (already dilated for clearance)
|
||||
new_static_id = self._id_counter
|
||||
self._id_counter += 1
|
||||
self.obstacle_geometries[new_static_id] = dilated
|
||||
self.prepared_obstacles[new_static_id] = prep(dilated)
|
||||
self.static_obstacles.insert(new_static_id, dilated.bounds)
|
||||
|
||||
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
|
||||
"""Count how many other nets collide with this geometry."""
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
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 test_poly.intersects(other_poly):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def is_collision(
|
||||
self,
|
||||
geometry: Polygon,
|
||||
net_width: float,
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""Check if a geometry (e.g. a Move) collides with static obstacles."""
|
||||
_ = net_width # Width is already integrated into engine dilation settings
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
|
||||
# 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(test_poly):
|
||||
# Check safety zone (2nm = 0.002 um)
|
||||
if start_port or end_port:
|
||||
obstacle = self.obstacle_geometries[obj_id]
|
||||
intersection = test_poly.intersection(obstacle)
|
||||
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
||||
# Create safety zone polygons
|
||||
safety_zones = []
|
||||
if start_port:
|
||||
safety_zones.append(Point(start_port.x, start_port.y).buffer(0.002))
|
||||
if end_port:
|
||||
safety_zones.append(Point(end_port.x, end_port.y).buffer(0.002))
|
||||
|
||||
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
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue