Initial buildout
This commit is contained in:
parent
34615f3aac
commit
f600b52f32
25 changed files with 1856 additions and 23 deletions
56
inire/tests/benchmark_scaling.py
Normal file
56
inire/tests/benchmark_scaling.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import time
|
||||
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
|
||||
|
||||
def benchmark_scaling() -> None:
|
||||
print("Starting Scalability Benchmark...")
|
||||
|
||||
# 1. Memory Verification (20x20mm)
|
||||
# Resolution 1um -> 20000 x 20000 grid
|
||||
bounds = (0, 0, 20000, 20000)
|
||||
print(f"Initializing DangerMap for {bounds} area...")
|
||||
dm = DangerMap(bounds=bounds, resolution=1.0)
|
||||
# nbytes for float32: 20000 * 20000 * 4 bytes = 1.6 GB
|
||||
mem_gb = dm.grid.nbytes / (1024**3)
|
||||
print(f"DangerMap memory usage: {mem_gb:.2f} GB")
|
||||
assert mem_gb < 2.0
|
||||
|
||||
# 2. Node Expansion Rate (50 nets)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# Use a smaller area for routing benchmark to keep it fast
|
||||
routing_bounds = (0, 0, 1000, 1000)
|
||||
danger_map = DangerMap(bounds=routing_bounds)
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
num_nets = 50
|
||||
netlist = {}
|
||||
for i in range(num_nets):
|
||||
# Parallel nets spaced by 10um
|
||||
netlist[f"net{i}"] = (Port(0, i * 10, 0), Port(100, i * 10, 0))
|
||||
|
||||
print(f"Routing {num_nets} nets...")
|
||||
start_time = time.monotonic()
|
||||
results = pf.route_all(netlist, dict.fromkeys(netlist, 2.0))
|
||||
end_time = time.monotonic()
|
||||
|
||||
total_time = end_time - start_time
|
||||
print(f"Total routing time: {total_time:.2f} s")
|
||||
print(f"Time per net: {total_time/num_nets:.4f} s")
|
||||
|
||||
if total_time > 0:
|
||||
nodes_per_sec = router.total_nodes_expanded / total_time
|
||||
print(f"Node expansion rate: {nodes_per_sec:.2f} nodes/s")
|
||||
|
||||
# Success rate
|
||||
successes = sum(1 for r in results.values() if r.is_valid)
|
||||
print(f"Success rate: {successes/num_nets * 100:.1f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark_scaling()
|
||||
71
inire/tests/test_astar.py
Normal file
71
inire/tests/test_astar.py
Normal file
|
|
@ -0,0 +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
|
||||
|
||||
@pytest.fixture
|
||||
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) -> 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
|
||||
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
|
||||
|
||||
def test_astar_bend(basic_evaluator) -> 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:
|
||||
# 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)
|
||||
basic_evaluator.danger_map.precompute([obstacle])
|
||||
|
||||
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
|
||||
# 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)
|
||||
|
||||
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.005, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert abs(path[-1].end_port.x - 10.005) < 1e-6
|
||||
59
inire/tests/test_collision.py
Normal file
59
inire/tests/test_collision.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
|
||||
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)])
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
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)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Port exactly on the boundary (x=10)
|
||||
start_port = Port(10.0, 12.0, 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).
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
test_poly = Polygon([(15, 20), (16, 20), (16, 25), (15, 25)])
|
||||
# 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
|
||||
75
inire/tests/test_components.py
Normal file
75
inire/tests/test_components.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import pytest
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.components import Straight, Bend90, SBend
|
||||
|
||||
def test_straight_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
length = 10.0
|
||||
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
|
||||
|
||||
# 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')
|
||||
|
||||
# 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 (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
|
||||
|
||||
# Geometry check (two arcs)
|
||||
assert len(result.geometry) == 2
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError):
|
||||
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
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 10.0
|
||||
70
inire/tests/test_congestion.py
Normal file
70
inire/tests/test_congestion.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
|
||||
@pytest.fixture
|
||||
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) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Start at (0,0), target at (50, 3) -> 3um lateral offset
|
||||
start = Port(0, 0, 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:
|
||||
# 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) -> 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))
|
||||
}
|
||||
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.
|
||||
|
||||
# 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])
|
||||
|
||||
# Increase base penalty to force detour immediately
|
||||
pf.base_congestion_penalty = 1000.0
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
36
inire/tests/test_cost.py
Normal file
36
inire/tests/test_cost.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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
|
||||
|
||||
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])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
|
||||
# 1. Cost far from obstacle
|
||||
cost_far = evaluator.g_proximity(5.0, 5.0)
|
||||
assert cost_far == 0.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
|
||||
|
||||
# 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
|
||||
63
inire/tests/test_fuzz.py
Normal file
63
inire/tests/test_fuzz.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
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
|
||||
|
||||
|
||||
@st.composite
|
||||
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))
|
||||
h = draw(st.floats(min_value=1, max_value=5))
|
||||
return Polygon([(x, y), (x + w, y), (x + w, y + h), (x, y + h)])
|
||||
|
||||
|
||||
@st.composite
|
||||
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]))
|
||||
return Port(x, y, orientation)
|
||||
|
||||
|
||||
@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:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
for obs in obstacles:
|
||||
engine.add_static_obstacle(obs)
|
||||
|
||||
danger_map = DangerMap(bounds=(0, 0, 30, 30))
|
||||
danger_map.precompute(obstacles)
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
router.node_limit = 5000 # Lower limit for fuzzing stability
|
||||
|
||||
# Check if start/target are inside obstacles (safety zone check)
|
||||
# The router should handle this gracefully (either route or return None)
|
||||
try:
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
# Analytic Correctness: if path is returned, verify it's collision-free
|
||||
if path:
|
||||
result = RoutingResult(net_id="default", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(
|
||||
result,
|
||||
obstacles,
|
||||
clearance=2.0,
|
||||
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}")
|
||||
51
inire/tests/test_pathfinder.py
Normal file
51
inire/tests/test_pathfinder.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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
|
||||
|
||||
@pytest.fixture
|
||||
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) -> 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))
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
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
|
||||
|
||||
43
inire/tests/test_primitives.py
Normal file
43
inire/tests/test_primitives.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from hypothesis import given, strategies as st
|
||||
from inire.geometry.primitives import Port, translate_port, rotate_port
|
||||
|
||||
@st.composite
|
||||
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) -> 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
|
||||
|
||||
@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
|
||||
60
inire/tests/test_refinements.py
Normal file
60
inire/tests/test_refinements.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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
|
||||
|
||||
def test_arc_resolution_sagitta() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
# R=10, 90 deg bend.
|
||||
# High tolerance (0.5um) -> few segments
|
||||
res_coarse = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.5)
|
||||
# Low tolerance (0.001um = 1nm) -> many segments
|
||||
res_fine = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.001)
|
||||
|
||||
# Check number of points in the polygon exterior
|
||||
# (num_segments + 1) * 2 points usually
|
||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||
|
||||
assert pts_fine > pts_coarse
|
||||
|
||||
def test_locked_paths() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 1. Route Net A
|
||||
netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))}
|
||||
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
||||
assert results_a["netA"].is_valid
|
||||
|
||||
# 2. Lock Net A
|
||||
engine.lock_net("netA")
|
||||
|
||||
# 3. Route Net B through the same space. It should detour or fail.
|
||||
# We'll place Net B's start/target such that it MUST cross Net A's physical path.
|
||||
netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))}
|
||||
|
||||
# Route Net B
|
||||
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
||||
|
||||
# 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
|
||||
|
||||
# Verify geometry doesn't intersect locked netA (physical check)
|
||||
poly_a = [p.geometry[0] for p in results_a["netA"].path]
|
||||
poly_b = [p.geometry[0] for p in results_b["netB"].path]
|
||||
|
||||
for pa in poly_a:
|
||||
for pb in poly_b:
|
||||
# Check physical clearance
|
||||
assert not pa.buffer(1.0).intersects(pb.buffer(1.0))
|
||||
Loading…
Add table
Add a link
Reference in a new issue