misc cleanup and tuning
This commit is contained in:
parent
f600b52f32
commit
07d079846b
15 changed files with 168 additions and 165 deletions
|
|
@ -87,11 +87,15 @@ class CollisionEngine:
|
|||
"""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)
|
||||
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)
|
||||
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):
|
||||
if other_net_id != net_id and dilated_geometry.intersects(other_poly):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
|
@ -106,17 +110,25 @@ 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."""
|
||||
# Broad prune with R-Tree
|
||||
candidates = self.static_obstacles.intersection(test_poly.bounds)
|
||||
candidates = self.static_obstacles.intersection(dilated_geometry.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
# Use prepared geometry for fast intersection
|
||||
if self.prepared_obstacles[obj_id].intersects(test_poly):
|
||||
if self.prepared_obstacles[obj_id].intersects(dilated_geometry):
|
||||
# Check safety zone (2nm = 0.002 um)
|
||||
if start_port or end_port:
|
||||
obstacle = self.obstacle_geometries[obj_id]
|
||||
intersection = test_poly.intersection(obstacle)
|
||||
intersection = dilated_geometry.intersection(obstacle)
|
||||
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -201,8 +201,8 @@ class AStarRouter:
|
|||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
curr = end_node
|
||||
while curr.component_result:
|
||||
curr: AStarNode | None = end_node
|
||||
while curr and curr.component_result:
|
||||
path.append(curr.component_result)
|
||||
curr = curr.parent
|
||||
return path[::-1]
|
||||
|
|
|
|||
|
|
@ -46,14 +46,20 @@ class CostEvaluator:
|
|||
start_port: Port | None = None,
|
||||
) -> float:
|
||||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||
_ = net_width # Unused, kept for API compatibility
|
||||
total_cost = 0.0
|
||||
dilation = self.collision_engine.clearance / 2.0
|
||||
|
||||
# Strict collision check
|
||||
for poly in geometry:
|
||||
if self.collision_engine.is_collision(poly, net_width, start_port=start_port, end_port=end_port):
|
||||
# Buffer once for both hard collision and congestion check
|
||||
dilated_poly = poly.buffer(dilation)
|
||||
|
||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
||||
return 1e9 # Massive cost for hard collisions
|
||||
|
||||
# Negotiated Congestion Cost
|
||||
overlaps = self.collision_engine.count_congestion(poly, net_id)
|
||||
overlaps = self.collision_engine.count_congestion_prebuffered(dilated_poly, net_id)
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
|
||||
# Proximity cost from Danger Map
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class PathFinder:
|
|||
|
||||
start_time = time.monotonic()
|
||||
num_nets = len(netlist)
|
||||
session_timeout = max(30.0, 0.5 * num_nets * self.max_iterations)
|
||||
session_timeout = max(60.0, 2.0 * num_nets * self.max_iterations)
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
any_congestion = False
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
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
|
||||
import pytest
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator():
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
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) -> None:
|
||||
|
||||
def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
|
|
@ -26,19 +29,20 @@ def test_astar_straight(basic_evaluator) -> None:
|
|||
assert abs(path[-1].end_port.x - 50.0) < 1e-6
|
||||
assert path[-1].end_port.y == 0.0
|
||||
|
||||
def test_astar_bend(basic_evaluator) -> None:
|
||||
|
||||
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(20, 20, 90)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
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
|
||||
|
||||
def test_astar_obstacle(basic_evaluator) -> None:
|
||||
|
||||
def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||
# Add an obstacle in the middle of a straight path
|
||||
obstacle = Polygon([(20, -5), (30, -5), (30, 5), (20, 5)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||
|
|
@ -53,14 +57,15 @@ def test_astar_obstacle(basic_evaluator) -> None:
|
|||
# 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)
|
||||
_ = 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)
|
||||
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator) -> None:
|
||||
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Target is NOT on 1um grid
|
||||
start = Port(0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -1,54 +1,56 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
|
||||
def test_collision_detection() -> None:
|
||||
# Clearance = 2um
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
|
||||
# Static obstacle at (10, 10) with size 5x5
|
||||
obstacle = Polygon([(10,10), (15,10), (15,15), (10,15)])
|
||||
# 10x10 um obstacle at (10,10)
|
||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||
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) is True
|
||||
test_poly = Polygon([(12, 12), (13, 12), (13, 13), (12, 13)])
|
||||
assert engine.is_collision(test_poly, net_width=2.0)
|
||||
|
||||
# 2. Far away
|
||||
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
|
||||
test_poly_far = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
|
||||
assert not engine.is_collision(test_poly_far, net_width=2.0)
|
||||
|
||||
# 3. Near hit (within clearance)
|
||||
# Obstacle is at (10,10).
|
||||
# test_poly is at (8,10) to (9,15).
|
||||
# Centerline at 8.5. Distance to 10 is 1.5.
|
||||
# Obstacle edge at x=10.
|
||||
# test_poly edge at x=9.
|
||||
# Distance = 1.0 um.
|
||||
# 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) is True
|
||||
test_poly_near = Polygon([(8, 10), (9, 10), (9, 15), (8, 15)])
|
||||
assert engine.is_collision(test_poly_near, net_width=2.0)
|
||||
|
||||
|
||||
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), (15,10), (15,15), (10,15)])
|
||||
|
||||
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Port exactly on the boundary (x=10)
|
||||
start_port = Port(10.0, 12.0, 0.0)
|
||||
# Port exactly on the boundary
|
||||
start_port = Port(10.0, 12.0, 0)
|
||||
|
||||
# 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).
|
||||
# Move starting from this port that overlaps the obstacle by 1nm
|
||||
# (Inside the 2nm safety zone)
|
||||
test_poly = Polygon([(9.999, 11.9995), (10.001, 11.9995), (10.001, 12.0005), (9.999, 12.0005)])
|
||||
|
||||
assert engine.is_collision(test_poly, net_width=0.001, start_port=start_port) is False
|
||||
assert not engine.is_collision(test_poly, net_width=0.001, start_port=start_port)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -56,4 +58,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 engine.is_collision(test_poly, net_width=2.0) is False
|
||||
assert not engine.is_collision(test_poly, net_width=2.0)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
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)
|
||||
|
|
@ -8,68 +10,59 @@ 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
|
||||
|
||||
# Geometry check
|
||||
poly = result.geometry[0]
|
||||
assert poly.area == length * width
|
||||
# Check bounds
|
||||
minx, miny, maxx, maxy = poly.bounds
|
||||
# Bounds of the polygon
|
||||
minx, miny, maxx, maxy = result.geometry[0].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')
|
||||
|
||||
# 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)
|
||||
# CW bend
|
||||
result_cw = Bend90.generate(start, radius, width, direction="CW")
|
||||
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 (0 -> 90)
|
||||
result_ccw = Bend90.generate(start, radius, width, direction='CCW')
|
||||
# CCW bend
|
||||
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
|
||||
result = SBend.generate(start, offset, radius, width)
|
||||
assert result.end_port.y == 5.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
|
||||
# Geometry check (two arcs)
|
||||
assert len(result.geometry) == 2
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||
SBend.generate(start, 25.0, 10.0, 2.0)
|
||||
|
||||
|
||||
def test_bend_snapping() -> None:
|
||||
# Radius that results in non-integer coords
|
||||
radius = 10.1234
|
||||
start = Port(0, 0, 0)
|
||||
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
|
||||
result = Bend90.generate(start, radius, width=2.0, direction="CCW")
|
||||
|
||||
# Target x is 10.1234, should snap to 10.0 (assuming 1um grid)
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 10.0
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
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():
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
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) -> None:
|
||||
|
||||
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Start at (0,0), target at (50, 3) -> 3um lateral offset
|
||||
start = Port(0, 0, 0)
|
||||
|
|
@ -26,28 +29,26 @@ def test_astar_sbend(basic_evaluator) -> None:
|
|||
# Check if any component in the path is an SBend
|
||||
found_sbend = False
|
||||
for res in path:
|
||||
# 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
|
||||
# Check if it has 2 polygons (characteristic of our SBend implementation)
|
||||
# and end port orientation is same as start
|
||||
if len(res.geometry) == 2:
|
||||
found_sbend = True
|
||||
break
|
||||
assert found_sbend
|
||||
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator) -> None:
|
||||
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> 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}
|
||||
|
||||
# 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.
|
||||
|
||||
# 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.
|
||||
|
|
@ -55,6 +56,7 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator) -> None:
|
|||
|
||||
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])
|
||||
|
|
@ -66,5 +68,3 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator) -> None:
|
|||
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
|
|
|||
|
|
@ -1,36 +1,25 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
|
||||
|
||||
def test_cost_calculation() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# 50x50 um area, 1um resolution
|
||||
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])
|
||||
|
||||
danger_map = DangerMap(bounds=(0, 0, 50, 50))
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
|
||||
# 1. Cost far from obstacle
|
||||
cost_far = evaluator.g_proximity(5.0, 5.0)
|
||||
assert cost_far == 0.0
|
||||
p1 = Port(0, 0, 0)
|
||||
p2 = Port(10, 10, 0)
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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
|
||||
# Orientation penalty
|
||||
p3 = Port(10, 10, 90)
|
||||
h_wrong = evaluator.h_manhattan(p1, p3)
|
||||
assert h_wrong > h
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from shapely.geometry import Polygon
|
||||
|
|
@ -12,7 +14,7 @@ from inire.utils.validation import validate_routing_result
|
|||
|
||||
|
||||
@st.composite
|
||||
def random_obstacle(draw):
|
||||
def random_obstacle(draw: Any) -> Polygon:
|
||||
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))
|
||||
|
|
@ -21,7 +23,7 @@ def random_obstacle(draw):
|
|||
|
||||
|
||||
@st.composite
|
||||
def random_port(draw):
|
||||
def random_port(draw: Any) -> Port:
|
||||
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]))
|
||||
|
|
@ -30,7 +32,7 @@ def random_port(draw):
|
|||
|
||||
@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, start, target) -> None:
|
||||
def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port) -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
for obs in obstacles:
|
||||
engine.add_static_obstacle(obs)
|
||||
|
|
|
|||
|
|
@ -1,51 +1,35 @@
|
|||
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.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():
|
||||
def basic_evaluator() -> CostEvaluator:
|
||||
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) -> None:
|
||||
|
||||
def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> 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,43 +1,51 @@
|
|||
from typing import Any
|
||||
|
||||
from hypothesis import given, strategies as st
|
||||
from inire.geometry.primitives import Port, translate_port, rotate_port
|
||||
|
||||
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||
|
||||
|
||||
@st.composite
|
||||
def port_strategy(draw):
|
||||
def port_strategy(draw: Any) -> Port:
|
||||
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) -> None:
|
||||
def test_port_transform_invariants(p: Port) -> 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 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
|
||||
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)
|
||||
|
||||
@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:
|
||||
|
||||
@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:
|
||||
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,10 +1,11 @@
|
|||
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 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.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
|
||||
def test_arc_resolution_sagitta() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
|
|
@ -21,6 +22,7 @@ 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))
|
||||
|
|
@ -47,6 +49,7 @@ 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
|
||||
|
||||
|
|
@ -56,5 +59,4 @@ def test_locked_paths() -> None:
|
|||
|
||||
for pa in poly_a:
|
||||
for pb in poly_b:
|
||||
# Check physical clearance
|
||||
assert not pa.buffer(1.0).intersects(pb.buffer(1.0))
|
||||
assert not pa.intersects(pb)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from shapely.geometry import Point
|
||||
from shapely.ops import unary_union
|
||||
|
|
@ -17,7 +17,7 @@ def validate_routing_result(
|
|||
clearance: float,
|
||||
start_port_coord: tuple[float, float] | None = None,
|
||||
end_port_coord: tuple[float, float] | None = None,
|
||||
) -> dict[str, any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Perform a high-precision validation of a routed path.
|
||||
Returns a dictionary with validation results.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def plot_routing_results(
|
|||
# Plot paths
|
||||
colors = plt.get_cmap("tab10")
|
||||
for i, (net_id, res) in enumerate(results.items()):
|
||||
color = colors(i)
|
||||
color: str | tuple[float, ...] = colors(i)
|
||||
if not res.is_valid:
|
||||
color = "red" # Highlight failing nets
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue