go to a tunable 5um grid

This commit is contained in:
jan 2026-03-11 09:37:54 -07:00
commit 91256cbcf9
21 changed files with 222 additions and 233 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

View file

@ -26,8 +26,8 @@ def main() -> None:
# Precompute the danger map (distance field) for heuristics # Precompute the danger map (distance field) for heuristics
danger_map.precompute([obstacle]) danger_map.precompute([obstacle])
evaluator = CostEvaluator(engine, danger_map) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
router = AStarRouter(evaluator) router = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist # 2. Define Netlist

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before After
Before After

View file

@ -16,8 +16,8 @@ def main() -> None:
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
router = AStarRouter(evaluator) router = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist # 2. Define Netlist

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

View file

@ -1,3 +1,5 @@
from shapely.geometry import Polygon
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter from inire.router.astar import AStarRouter
@ -8,63 +10,51 @@ from inire.utils.visualization import plot_routing_results
def main() -> None: def main() -> None:
print("Running Example 03: Locked Paths (Incremental Routing - Bus Scenario)...") print("Running Example 03: Locked Paths...")
# 1. Setup Environment # 1. Setup Environment
bounds = (0, 0, 120, 120) bounds = (0, 0, 100, 100)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) # Start with empty space danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.2) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
router = AStarRouter(evaluator, node_limit=200000) router = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Phase 1: Route a "Bus" of 3 parallel nets # 2. Add a 'Pre-routed' net and lock it
# We give them a small jog to make the locked geometry more interesting # Net 'fixed' goes right through the middle
netlist_p1 = { fixed_start = Port(10, 50, 0)
"bus_0": (Port(10, 40, 0), Port(110, 45, 0)), fixed_target = Port(90, 50, 0)
"bus_1": (Port(10, 50, 0), Port(110, 55, 0)),
"bus_2": (Port(10, 60, 0), Port(110, 65, 0)), print("Routing initial net...")
res_fixed = router.route(fixed_start, fixed_target, net_width=2.0)
if res_fixed:
# 3. Lock this net! It now behaves like a static obstacle
geoms = [comp.geometry[0] for comp in res_fixed]
engine.add_path("locked_net", geoms)
engine.lock_net("locked_net")
print("Initial net locked as static obstacle.")
# Update danger map to reflect the new static obstacle
danger_map.precompute(list(engine.static_geometries.values()))
# 4. Route a new net that must detour around the locked one
netlist = {
"detour_net": (Port(50, 10, 90), Port(50, 90, 90)),
} }
print("Phase 1: Routing bus (3 nets)...") net_widths = {"detour_net": 2.0}
results_p1 = pf.route_all(netlist_p1, dict.fromkeys(netlist_p1, 2.0))
# Lock all Phase 1 nets print("Routing detour net around locked path...")
path_polys = [] results = pf.route_all(netlist, net_widths)
for nid, res in results_p1.items():
if res.is_valid:
print(f" Locking {nid}...")
engine.lock_net(nid)
path_polys.extend([p for comp in res.path for p in comp.geometry])
else:
print(f" Warning: {nid} failed to route correctly.")
# Update danger map with the newly locked geometry
print("Updating DangerMap with locked paths...")
danger_map.precompute(path_polys)
# 3. Phase 2: Route secondary nets that must navigate around the locked bus
# These nets cross the bus vertically.
netlist_p2 = {
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
}
print("Phase 2: Routing crossing nets around locked bus...")
# We use a slightly different width for variety
results_p2 = pf.route_all(netlist_p2, dict.fromkeys(netlist_p2, 1.5))
# 4. Check Results
for nid, res in results_p2.items():
status = "Success" if res.is_valid else "Failed"
print(f" {nid:12}: {status}, collisions={res.collisions}")
# 5. Visualize # 5. Visualize
all_results = {**results_p1, **results_p2} # Add the locked net back to results for display
all_netlists = {**netlist_p1, **netlist_p2} from inire.router.pathfinder import RoutingResult
display_results = {**results, "locked_net": RoutingResult("locked_net", res_fixed or [], True, 0)}
fig, ax = plot_routing_results(all_results, [], bounds, netlist=all_netlists)
fig, ax = plot_routing_results(display_results, list(engine.static_geometries.values()), bounds, netlist=netlist)
fig.savefig("examples/03_locked_paths.png") fig.savefig("examples/03_locked_paths.png")
print("Saved plot to examples/03_locked_paths.png") print("Saved plot to examples/03_locked_paths.png")

View file

@ -1,4 +1,3 @@
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter from inire.router.astar import AStarRouter
@ -23,15 +22,14 @@ def main() -> None:
danger_map, danger_map,
unit_length_cost=1.0, unit_length_cost=1.0,
greedy_h_weight=1.5, greedy_h_weight=1.5,
bend_penalty=10.0,
sbend_penalty=20.0,
) )
# We want a 45 degree switchover for S-bend.
# Offset O = 2 * R * (1 - cos(theta))
# If R = 10, O = 5.86
router = AStarRouter( router = AStarRouter(
evaluator, evaluator,
node_limit=50000, node_limit=50000,
snap_size=1.0,
bend_radii=[10.0, 30.0], bend_radii=[10.0, 30.0],
sbend_offsets=[5.0], # Use a simpler offset sbend_offsets=[5.0], # Use a simpler offset
sbend_radii=[10.0], sbend_radii=[10.0],

View file

@ -11,42 +11,31 @@ def main() -> None:
print("Running Example 05: Orientation Stress Test...") print("Running Example 05: Orientation Stress Test...")
# 1. Setup Environment # 1. Setup Environment
# Give some breathing room (-20 to 120) for U-turns and flips (R=10) bounds = (0, 0, 200, 200)
bounds = (-20, -20, 120, 120)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
router = AStarRouter(evaluator, node_limit=100000) router = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
router.config.bend_collision_type = "clipped_bbox"
router.config.bend_clip_margin = 1.0
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist with various orientation challenges # 2. Define Netlist: Complex orientation challenges
netlist = { netlist = {
# Opposite directions: requires two 90-degree bends to flip orientation "u_turn": (Port(50, 100, 0), Port(30, 100, 180)),
"opposite": (Port(10, 80, 0), Port(90, 80, 180)), "loop": (Port(150, 50, 90), Port(150, 40, 90)),
"zig_zag": (Port(20, 20, 0), Port(180, 180, 0)),
# 90-degree turn: standard L-shape
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
# Output behind input: requires a full U-turn
"behind": (Port(80, 40, 0), Port(20, 40, 0)),
# Sharp return: output is behind and oriented towards the input
"return_loop": (Port(80, 20, 0), Port(40, 10, 180)),
} }
net_widths = dict.fromkeys(netlist, 2.0) net_widths = {nid: 2.0 for nid in netlist}
# 3. Route # 3. Route
print("Routing complex orientation nets...")
results = pf.route_all(netlist, net_widths) results = pf.route_all(netlist, net_widths)
# 4. Check Results # 4. Check Results
for nid, res in results.items(): for nid, res in results.items():
status = "Success" if res.is_valid else "Failed" status = "Success" if res.is_valid else "Failed"
total_len = sum(comp.length for comp in res.path) if res.path else 0 print(f" {nid}: {status}")
print(f" {nid:12}: {status}, total_length={total_len:.1f}")
# 5. Visualize # 5. Visualize
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist) fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)

View file

@ -30,18 +30,18 @@ def main() -> None:
danger_map.precompute(obstacles) danger_map.precompute(obstacles)
# We'll run three separate routers since collision_type is a router-level config # We'll run three separate routers since collision_type is a router-level config
evaluator = CostEvaluator(engine, danger_map) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
# Scenario 1: Standard 'arc' model (High fidelity) # Scenario 1: Standard 'arc' model (High fidelity)
router_arc = AStarRouter(evaluator, bend_collision_type="arc") router_arc = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0], bend_collision_type="arc")
netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))} netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}
# Scenario 2: 'bbox' model (Conservative axis-aligned box) # Scenario 2: 'bbox' model (Conservative axis-aligned box)
router_bbox = AStarRouter(evaluator, bend_collision_type="bbox") router_bbox = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0], bend_collision_type="bbox")
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))} netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
# Scenario 3: 'clipped_bbox' model (Balanced) # Scenario 3: 'clipped_bbox' model (Balanced)
router_clipped = AStarRouter(evaluator, bend_collision_type="clipped_bbox", bend_clip_margin=1.0) router_clipped = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0], bend_collision_type="clipped_bbox", bend_clip_margin=1.0)
netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))} netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}
# 2. Route each scenario # 2. Route each scenario

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Before After
Before After

View file

@ -9,17 +9,16 @@ from inire.utils.visualization import plot_routing_results
from shapely.geometry import box from shapely.geometry import box
def main() -> None: def main() -> None:
print("Running Example 07: Fan-Out (5 Nets)...") print("Running Example 07: Fan-Out (10 Nets, 50um Radius, 5um Grid)...")
# 1. Setup Environment # 1. Setup Environment
# Small area for fast and reliable demonstration bounds = (0, 0, 1000, 1000)
bounds = (0, 0, 100, 100) engine = CollisionEngine(clearance=6.0)
engine = CollisionEngine(clearance=2.0)
# Wide bottleneck at x=50, 60um gap (from y=20 to y=80) # Bottleneck at x=500, 200um gap
obstacles = [ obstacles = [
box(50, 0, 55, 20), box(450, 0, 550, 400),
box(50, 80, 55, 100), box(450, 600, 550, 1000),
] ]
for obs in obstacles: for obs in obstacles:
engine.add_static_obstacle(obs) engine.add_static_obstacle(obs)
@ -29,32 +28,28 @@ def main() -> None:
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5)
# Increase node_limit for more complex search router = AStarRouter(evaluator, node_limit=50000, snap_size=5.0)
router = AStarRouter(evaluator, node_limit=50000) pf = PathFinder(router, evaluator, max_iterations=10)
pf = PathFinder(router, evaluator, max_iterations=2)
# 2. Define Netlist: Fan-Out Configuration # 2. Define Netlist
netlist = {} netlist = {}
num_nets = 10 num_nets = 10
start_x = 10 start_x = 50
# Bundle centered at y=50, 4um pitch start_y_base = 500 - (num_nets * 10.0) / 2.0
start_y_base = 50 - (num_nets * 4.0) / 2.0
end_x = 90 end_x = 950
end_y_base = 10 end_y_base = 100
end_y_pitch = 80.0 / (num_nets - 1) end_y_pitch = 800.0 / (num_nets - 1)
for i in range(num_nets): for i in range(num_nets):
sy = start_y_base + i * 4.0 sy = round((start_y_base + i * 10.0) / 5.0) * 5.0
ey = end_y_base + i * end_y_pitch ey = round((end_y_base + i * end_y_pitch) / 5.0) * 5.0
netlist[f"net_{i:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
net_id = f"net_{i:02d}"
netlist[net_id] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
net_widths = {nid: 2.0 for nid in netlist} net_widths = {nid: 2.0 for nid in netlist}
# 3. Route # 3. Route
print(f"Routing {len(netlist)} nets through 60um bottleneck...") print(f"Routing {len(netlist)} nets through 200um bottleneck...")
results = pf.route_all(netlist, net_widths) results = pf.route_all(netlist, net_widths)
# 4. Check Results # 4. Check Results

View file

@ -1,4 +1,5 @@
from shapely.geometry import Polygon from shapely.geometry import Polygon
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter from inire.router.astar import AStarRouter
@ -7,60 +8,47 @@ from inire.router.danger_map import DangerMap
from inire.router.pathfinder import PathFinder from inire.router.pathfinder import PathFinder
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results
def main() -> None:
print("Running Example 08: Custom Bend Geometry Models...")
def main() -> None:
print("Running Example 08: Custom Bend Geometry...")
# 1. Setup Environment
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
# Static obstacle to force specific bend paths
obstacle = Polygon([(60, 40), (90, 40), (90, 110), (60, 110)])
engine.add_static_obstacle(obstacle)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([obstacle]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map)
# We will route three nets, each with a DIFFERENT collision model evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
# To do this cleanly with the current architecture, we'll use one router router = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
# but change its config per route call (or use tiered escalation in PathFinder).
# Since AStarRouter.route now accepts bend_collision_type, we can do it directly.
router = AStarRouter(evaluator)
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist
netlist = { netlist = {
"model_arc": (Port(10, 130, 0), Port(130, 100, -90)), "custom_bend": (Port(20, 20, 0), Port(100, 100, 90)),
"model_bbox": (Port(10, 80, 0), Port(130, 50, -90)),
"model_clipped": (Port(10, 30, 0), Port(130, 10, -90)),
} }
net_widths = {nid: 2.0 for nid in netlist} net_widths = {"custom_bend": 2.0}
# Manual routing to specify different models per net # 3. Route with standard arc first
results = {} print("Routing with standard arc...")
results_std = pf.route_all(netlist, net_widths)
print("Routing with 'arc' model...")
results["model_arc"] = pf.router.route(netlist["model_arc"][0], netlist["model_arc"][1], 2.0,
net_id="model_arc", bend_collision_type="arc")
print("Routing with 'bbox' model...")
results["model_bbox"] = pf.router.route(netlist["model_bbox"][0], netlist["model_bbox"][1], 2.0,
net_id="model_bbox", bend_collision_type="bbox")
print("Routing with 'clipped_bbox' model...")
results["model_clipped"] = pf.router.route(netlist["model_clipped"][0], netlist["model_clipped"][1], 2.0,
net_id="model_clipped", bend_collision_type="clipped_bbox")
# Wrap in RoutingResult for visualization # 4. Define a custom 'trapezoid' bend model
from inire.router.pathfinder import RoutingResult # (Just for demonstration - we override the collision model during search)
final_results = { custom_poly = Polygon([(0, 0), (20, 0), (20, 20), (0, 20)]) # Oversized box
nid: RoutingResult(nid, path if path else [], path is not None, 0)
for nid, path in results.items() print("Routing with custom collision model...")
} # Override bend_collision_type with a literal Polygon
router_custom = AStarRouter(evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0], bend_collision_type=custom_poly)
results_custom = PathFinder(router_custom, evaluator, use_tiered_strategy=False).route_all(
{"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}
)
fig, ax = plot_routing_results(final_results, [obstacle], bounds, netlist=netlist) # 5. Visualize
all_results = {**results_std, **results_custom}
fig, ax = plot_routing_results(all_results, [], bounds, netlist=netlist)
fig.savefig("examples/08_custom_bend_geometry.png") fig.savefig("examples/08_custom_bend_geometry.png")
print("Saved plot to examples/08_custom_bend_geometry.png") print("Saved plot to examples/08_custom_bend_geometry.png")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -3,42 +3,55 @@ from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter from inire.router.astar import AStarRouter
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap from inire.router.danger_map import DangerMap
from inire.router.pathfinder import PathFinder, RoutingResult from inire.router.pathfinder import PathFinder
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results
from shapely.geometry import box from shapely.geometry import box
def main() -> None: def main() -> None:
print("Running Example 09: Unroutable Nets & Best Effort Display...") print("Running Example 09: Best-Effort (Unroutable Net)...")
# 1. Setup Environment
bounds = (0, 0, 100, 100) bounds = (0, 0, 100, 100)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
# A large obstacle that completely blocks the target port # Create a 'cage' that completely blocks the target
blocking_obs = box(40, 0, 60, 100) cage = [
engine.add_static_obstacle(blocking_obs) box(70, 30, 75, 70), # Left wall
box(70, 70, 95, 75), # Top wall
danger_map = DangerMap(bounds=bounds) box(70, 25, 95, 30), # Bottom wall
danger_map.precompute([blocking_obs]) ]
evaluator = CostEvaluator(engine, danger_map) for obs in cage:
engine.add_static_obstacle(obs)
# Use a low node limit to fail quickly
router = AStarRouter(evaluator, node_limit=5000)
netlist = {
"blocked_net": (Port(10, 50, 0), Port(90, 50, 180))
}
print("Routing blocked net (expecting failure)...")
# Manually call route with return_partial=True
path = router.route(netlist["blocked_net"][0], netlist["blocked_net"][1], 2.0,
net_id="blocked_net", return_partial=True)
# Wrap in RoutingResult. Even if path is returned, is_valid=False
results = {
"blocked_net": RoutingResult("blocked_net", path if path else [], False, 1)
}
fig, ax = plot_routing_results(results, [blocking_obs], bounds, netlist=netlist) danger_map = DangerMap(bounds=bounds)
danger_map.precompute(cage)
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
# Use a low node limit to fail faster
router = AStarRouter(evaluator, node_limit=2000, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
# Enable partial path return
pf = PathFinder(router, evaluator)
# 2. Define Netlist: start outside, target inside the cage
netlist = {
"trapped_net": (Port(10, 50, 0), Port(85, 50, 0)),
}
net_widths = {"trapped_net": 2.0}
# 3. Route
print("Routing net into a cage (should fail and return partial)...")
results = pf.route_all(netlist, net_widths)
# 4. Check Results
res = results["trapped_net"]
if not res.is_valid:
print(f"Net failed to route as expected. Partial path length: {len(res.path)} segments.")
else:
print("Wait, it found a way in? Check the cage geometry!")
# 5. Visualize
fig, ax = plot_routing_results(results, cage, bounds, netlist=netlist)
fig.savefig("examples/09_unroutable_best_effort.png") fig.savefig("examples/09_unroutable_best_effort.png")
print("Saved plot to examples/09_unroutable_best_effort.png") print("Saved plot to examples/09_unroutable_best_effort.png")

View file

@ -10,21 +10,24 @@ from .primitives import Port
# Search Grid Snap (1.0 µm) # Search Grid Snap (5.0 µm default)
SEARCH_GRID_SNAP_UM = 1.0 SEARCH_GRID_SNAP_UM = 5.0
def snap_search_grid(value: float) -> float: def snap_search_grid(value: float, snap_size: float = SEARCH_GRID_SNAP_UM) -> float:
""" """
Snap a coordinate to the nearest search grid unit. Snap a coordinate to the nearest search grid unit.
Args: Args:
value: Value to snap. value: Value to snap.
snap_size: The grid size to snap to.
Returns: Returns:
Snapped value. Snapped value.
""" """
return round(value / SEARCH_GRID_SNAP_UM) * SEARCH_GRID_SNAP_UM if snap_size <= 0:
return value
return round(value / snap_size) * snap_size
class ComponentResult: class ComponentResult:
@ -144,6 +147,7 @@ class Straight:
width: float, width: float,
snap_to_grid: bool = True, snap_to_grid: bool = True,
dilation: float = 0.0, dilation: float = 0.0,
snap_size: float = SEARCH_GRID_SNAP_UM,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a straight waveguide segment. Generate a straight waveguide segment.
@ -154,6 +158,7 @@ class Straight:
width: Waveguide width. width: Waveguide width.
snap_to_grid: Whether to snap the end port to the search grid. snap_to_grid: Whether to snap the end port to the search grid.
dilation: Optional dilation distance for pre-calculating collision geometry. dilation: Optional dilation distance for pre-calculating collision geometry.
snap_size: Grid size for snapping.
Returns: Returns:
A ComponentResult containing the straight segment. A ComponentResult containing the straight segment.
@ -166,8 +171,8 @@ class Straight:
ey = start_port.y + length * sin_val ey = start_port.y + length * sin_val
if snap_to_grid: if snap_to_grid:
ex = snap_search_grid(ex) ex = snap_search_grid(ex, snap_size)
ey = snap_search_grid(ey) ey = snap_search_grid(ey, snap_size)
end_port = Port(ex, ey, start_port.orientation) end_port = Port(ex, ey, start_port.orientation)
actual_length = numpy.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2) actual_length = numpy.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2)
@ -415,6 +420,7 @@ class Bend90:
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0, clip_margin: float = 10.0,
dilation: float = 0.0, dilation: float = 0.0,
snap_size: float = SEARCH_GRID_SNAP_UM,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a 90-degree bend. Generate a 90-degree bend.
@ -430,8 +436,8 @@ class Bend90:
t_end_init = t_start_init + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2) t_end_init = t_start_init + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
# Snap the target point # Snap the target point
ex = snap_search_grid(cx_init + radius * numpy.cos(t_end_init)) ex = snap_search_grid(cx_init + radius * numpy.cos(t_end_init), snap_size)
ey = snap_search_grid(cy_init + radius * numpy.sin(t_end_init)) ey = snap_search_grid(cy_init + radius * numpy.sin(t_end_init), snap_size)
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360)) end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
# Adjust geometry to perfectly hit snapped port # Adjust geometry to perfectly hit snapped port
@ -503,6 +509,7 @@ class SBend:
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0, clip_margin: float = 10.0,
dilation: float = 0.0, dilation: float = 0.0,
snap_size: float = SEARCH_GRID_SNAP_UM,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a parametric S-bend (two tangent arcs). Generate a parametric S-bend (two tangent arcs).
@ -515,8 +522,8 @@ class SBend:
rad_start = numpy.radians(start_port.orientation) rad_start = numpy.radians(start_port.orientation)
# Snap the target point # Snap the target point
ex = snap_search_grid(start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start)) ex = snap_search_grid(start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start), snap_size)
ey = snap_search_grid(start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.cos(rad_start)) ey = snap_search_grid(start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.cos(rad_start), snap_size)
end_port = Port(ex, ey, start_port.orientation) end_port = Port(ex, ey, start_port.orientation)
# Solve for theta and radius that hit (ex, ey) exactly # Solve for theta and radius that hit (ex, ey) exactly

View file

@ -8,7 +8,7 @@ import rtree
import numpy import numpy
from inire.geometry.components import Bend90, SBend, Straight from inire.geometry.components import Bend90, SBend, Straight, SEARCH_GRID_SNAP_UM
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.config import RouterConfig from inire.router.config import RouterConfig
@ -132,7 +132,11 @@ class AStarRouter:
# self._collision_cache.clear() # self._collision_cache.clear()
open_set: list[AStarNode] = [] open_set: list[AStarNode] = []
# Key: (x, y, orientation) rounded to 1nm # Calculate rounding precision based on search grid
# e.g. 1.0 -> 0, 0.1 -> 1, 0.001 -> 3
state_precision = int(numpy.ceil(-numpy.log10(SEARCH_GRID_SNAP_UM))) if SEARCH_GRID_SNAP_UM < 1.0 else 0
# Key: (x, y, orientation) rounded to search grid
closed_set: set[tuple[float, float, float]] = set() closed_set: set[tuple[float, float, float]] = set()
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target)) start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
@ -153,7 +157,7 @@ class AStarRouter:
best_node = current best_node = current
# Prune if already visited # Prune if already visited
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2)) state = (round(current.port.x, state_precision), round(current.port.y, state_precision), round(current.port.orientation, 2))
if state in closed_set: if state in closed_set:
continue continue
closed_set.add(state) closed_set.add(state)
@ -171,7 +175,7 @@ class AStarRouter:
return self._reconstruct_path(current) return self._reconstruct_path(current)
# Expansion # Expansion
self._expand_moves(current, target, net_width, net_id, open_set, closed_set) self._expand_moves(current, target, net_width, net_id, open_set, closed_set, state_precision)
return self._reconstruct_path(best_node) if return_partial else None return self._reconstruct_path(best_node) if return_partial else None
@ -183,6 +187,7 @@ class AStarRouter:
net_id: str, net_id: str,
open_set: list[AStarNode], open_set: list[AStarNode],
closed_set: set[tuple[float, float, float]], closed_set: set[tuple[float, float, float]],
state_precision: int = 0,
) -> None: ) -> None:
# 1. Snap-to-Target Look-ahead # 1. Snap-to-Target Look-ahead
dist = numpy.sqrt((current.port.x - target.x)**2 + (current.port.y - target.y)**2) dist = numpy.sqrt((current.port.x - target.x)**2 + (current.port.y - target.y)**2)
@ -195,8 +200,8 @@ class AStarRouter:
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad) proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad) perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
if proj > 0 and abs(perp) < 1e-6: if proj > 0 and abs(perp) < 1e-6:
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=0.0) res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation, snap_size=self.config.snap_size)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight') self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight', state_precision=state_precision)
# B. Try SBend exact reach # B. Try SBend exact reach
if abs(current.port.orientation - target.orientation) < 0.1: if abs(current.port.orientation - target.orientation) < 0.1:
@ -205,7 +210,7 @@ class AStarRouter:
dy = target.y - current.port.y dy = target.y - current.port.y
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad) proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad) perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
if proj > 0 and 0.5 <= abs(perp) < 20.0: if proj > 0 and 0.5 <= abs(perp) < 100.0: # Match snap_to_target_dist
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate( res = SBend.generate(
@ -215,16 +220,17 @@ class AStarRouter:
net_width, net_width,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin, clip_margin=self.config.bend_clip_margin,
dilation=0.0 dilation=self._self_dilation,
snap_size=self.config.snap_size
) )
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius, state_precision=state_precision)
except ValueError: except ValueError:
pass pass
# 2. Lattice Straights # 2. Lattice Straights
cp = current.port cp = current.port
base_ori = round(cp.orientation, 2) base_ori = round(cp.orientation, 2)
state_key = (round(cp.x, 3), round(cp.y, 3), base_ori) state_key = (round(cp.x, state_precision), round(cp.y, state_precision), base_ori)
lengths = self.config.straight_lengths lengths = self.config.straight_lengths
if dist < 5.0: if dist < 5.0:
@ -244,16 +250,16 @@ class AStarRouter:
# Check closed set before translating # Check closed set before translating
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2)) end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
else: else:
res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=self._self_dilation) res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=self._self_dilation, snap_size=self.config.snap_size)
self._move_cache[rel_key] = res_rel self._move_cache[rel_key] = res_rel
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
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, closed_set, f'S{length}', state_precision=state_precision)
# 3. Lattice Bends # 3. Lattice Bends
for radius in self.config.bend_radii: for radius in self.config.bend_radii:
@ -268,7 +274,7 @@ class AStarRouter:
# Check closed set before translating # Check closed set before translating
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2)) end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
@ -280,12 +286,13 @@ class AStarRouter:
direction, direction,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin, clip_margin=self.config.bend_clip_margin,
dilation=self._self_dilation dilation=self._self_dilation,
snap_size=self.config.snap_size
) )
self._move_cache[rel_key] = res_rel self._move_cache[rel_key] = res_rel
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'B{radius}{direction}', move_radius=radius) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'B{radius}{direction}', move_radius=radius, state_precision=state_precision)
# 4. Discrete SBends # 4. Discrete SBends
for offset in self.config.sbend_offsets: for offset in self.config.sbend_offsets:
@ -300,7 +307,7 @@ class AStarRouter:
# Check closed set before translating # Check closed set before translating
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2)) end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
@ -313,14 +320,15 @@ class AStarRouter:
width=net_width, width=net_width,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin, clip_margin=self.config.bend_clip_margin,
dilation=self._self_dilation dilation=self._self_dilation,
snap_size=self.config.snap_size
) )
self._move_cache[rel_key] = res_rel self._move_cache[rel_key] = res_rel
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
except ValueError: except ValueError:
continue continue
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius, state_precision=state_precision)
def _add_node( def _add_node(
self, self,
@ -333,15 +341,16 @@ class AStarRouter:
closed_set: set[tuple[float, float, float]], closed_set: set[tuple[float, float, float]],
move_type: str, move_type: str,
move_radius: float | None = None, move_radius: float | None = None,
state_precision: int = 0,
) -> None: ) -> None:
# Check closed set before adding to open set # 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)) state = (round(result.end_port.x, state_precision), round(result.end_port.y, state_precision), round(result.end_port.orientation, 2))
if state in closed_set: if state in closed_set:
return return
cache_key = ( cache_key = (
round(parent.port.x, 3), round(parent.port.x, state_precision),
round(parent.port.y, 3), round(parent.port.y, state_precision),
round(parent.port.orientation, 2), round(parent.port.orientation, 2),
move_type, move_type,
net_width, net_width,

View file

@ -10,13 +10,14 @@ class RouterConfig:
"""Configuration parameters for the A* Router.""" """Configuration parameters for the A* Router."""
node_limit: int = 1000000 node_limit: int = 1000000
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0]) snap_size: float = 5.0
bend_radii: list[float] = field(default_factory=lambda: [10.0]) straight_lengths: list[float] = field(default_factory=lambda: [5.0, 10.0, 100.0])
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0]) bend_radii: list[float] = field(default_factory=lambda: [50.0])
sbend_radii: list[float] = field(default_factory=lambda: [10.0]) sbend_offsets: list[float] = field(default_factory=lambda: [-10.0, -5.0, 5.0, 10.0])
snap_to_target_dist: float = 20.0 sbend_radii: list[float] = field(default_factory=lambda: [50.0])
bend_penalty: float = 50.0 snap_to_target_dist: float = 100.0
sbend_penalty: float = 150.0 bend_penalty: float = 250.0
sbend_penalty: float = 500.0
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
bend_clip_margin: float = 10.0 bend_clip_margin: float = 10.0
@ -28,5 +29,5 @@ class CostConfig:
unit_length_cost: float = 1.0 unit_length_cost: float = 1.0
greedy_h_weight: float = 1.1 greedy_h_weight: float = 1.1
congestion_penalty: float = 10000.0 congestion_penalty: float = 10000.0
bend_penalty: float = 50.0 bend_penalty: float = 250.0
sbend_penalty: float = 150.0 sbend_penalty: float = 500.0

View file

@ -39,8 +39,8 @@ class CostEvaluator:
unit_length_cost: float = 1.0, unit_length_cost: float = 1.0,
greedy_h_weight: float = 1.1, greedy_h_weight: float = 1.1,
congestion_penalty: float = 10000.0, congestion_penalty: float = 10000.0,
bend_penalty: float = 50.0, bend_penalty: float = 250.0,
sbend_penalty: float = 150.0, sbend_penalty: float = 500.0,
) -> None: ) -> None:
""" """
Initialize the Cost Evaluator. Initialize the Cost Evaluator.
@ -102,8 +102,8 @@ class CostEvaluator:
# But we also need to account for the physical distance required for the turn. # But we also need to account for the physical distance required for the turn.
penalty = 0.0 penalty = 0.0
if current.orientation != target.orientation: if current.orientation != target.orientation:
# 90-degree turn cost: radius 10 -> ~15.7 um + penalty # 90-degree turn cost: radius 50 -> ~78.5 um + penalty
penalty += 15.7 + self.config.bend_penalty penalty += 78.5 + self.config.bend_penalty
return self.greedy_h_weight * (dist + penalty) return self.greedy_h_weight * (dist + penalty)

View file

@ -124,7 +124,7 @@ class PathFinder:
coll_model = "clipped_bbox" coll_model = "clipped_bbox"
net_start = time.monotonic() net_start = time.monotonic()
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model) path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True)
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}') logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
if path: if path:

View file

@ -15,11 +15,11 @@ def basic_evaluator() -> CostEvaluator:
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=(0, 0, 100, 100)) danger_map = DangerMap(bounds=(0, 0, 100, 100))
danger_map.precompute([]) danger_map.precompute([])
return CostEvaluator(engine, danger_map) return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
def test_astar_straight(basic_evaluator: CostEvaluator) -> None: def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0])
start = Port(0, 0, 0) start = Port(0, 0, 0)
target = Port(50, 0, 0) target = Port(50, 0, 0)
path = router.route(start, target, net_width=2.0) path = router.route(start, target, net_width=2.0)
@ -35,11 +35,9 @@ def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
def test_astar_bend(basic_evaluator: CostEvaluator) -> None: def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
start = Port(0, 0, 0) start = Port(0, 0, 0)
# 20um right, 20um up. Needs a 10um bend and a 10um bend. # 20um right, 20um up. Needs a 10um bend and a 10um bend.
# 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, 0)
path = router.route(start, target, net_width=2.0) path = router.route(start, target, net_width=2.0)
@ -58,7 +56,7 @@ def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
basic_evaluator.collision_engine.add_static_obstacle(obstacle) basic_evaluator.collision_engine.add_static_obstacle(obstacle)
basic_evaluator.danger_map.precompute([obstacle]) basic_evaluator.danger_map.precompute([obstacle])
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0], bend_radii=[10.0])
router.node_limit = 1000000 # Give it more room for detour router.node_limit = 1000000 # Give it more room for detour
start = Port(0, 0, 0) start = Port(0, 0, 0)
target = Port(60, 0, 0) target = Port(60, 0, 0)
@ -74,7 +72,7 @@ def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None: def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None:
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0)
# Target is NOT on 1um grid # Target is NOT on 1um grid
start = Port(0, 0, 0) start = Port(0, 0, 0)
target = Port(10.1, 0, 0) target = Port(10.1, 0, 0)

View file

@ -8,7 +8,7 @@ def test_straight_generation() -> None:
start = Port(0, 0, 0) start = Port(0, 0, 0)
length = 10.0 length = 10.0
width = 2.0 width = 2.0
result = Straight.generate(start, length, width) result = Straight.generate(start, length, width, snap_size=1.0)
assert result.end_port.x == 10.0 assert result.end_port.x == 10.0
assert result.end_port.y == 0.0 assert result.end_port.y == 0.0
@ -29,13 +29,13 @@ def test_bend90_generation() -> None:
width = 2.0 width = 2.0
# CW bend # CW bend
result_cw = Bend90.generate(start, radius, width, direction="CW") result_cw = Bend90.generate(start, radius, width, direction="CW", snap_size=1.0)
assert result_cw.end_port.x == 10.0 assert result_cw.end_port.x == 10.0
assert result_cw.end_port.y == -10.0 assert result_cw.end_port.y == -10.0
assert result_cw.end_port.orientation == 270.0 assert result_cw.end_port.orientation == 270.0
# CCW bend # CCW bend
result_ccw = Bend90.generate(start, radius, width, direction="CCW") result_ccw = Bend90.generate(start, radius, width, direction="CCW", snap_size=1.0)
assert result_ccw.end_port.x == 10.0 assert result_ccw.end_port.x == 10.0
assert result_ccw.end_port.y == 10.0 assert result_ccw.end_port.y == 10.0
assert result_ccw.end_port.orientation == 90.0 assert result_ccw.end_port.orientation == 90.0
@ -47,7 +47,7 @@ def test_sbend_generation() -> None:
radius = 10.0 radius = 10.0
width = 2.0 width = 2.0
result = SBend.generate(start, offset, radius, width) result = SBend.generate(start, offset, radius, width, snap_size=1.0)
assert result.end_port.y == 5.0 assert result.end_port.y == 5.0
assert result.end_port.orientation == 0.0 assert result.end_port.orientation == 0.0
assert len(result.geometry) == 2 # Optimization: returns individual arcs assert len(result.geometry) == 2 # Optimization: returns individual arcs
@ -63,7 +63,7 @@ def test_bend_collision_models() -> None:
width = 2.0 width = 2.0
# 1. BBox model # 1. BBox model
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox") res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox", snap_size=1.0)
# Arc CCW R=10 from (0,0,0) ends at (10,10,90). # Arc CCW R=10 from (0,0,0) ends at (10,10,90).
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10) # Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
@ -73,7 +73,7 @@ def test_bend_collision_models() -> None:
assert maxy >= 10.0 - 1e-6 assert maxy >= 10.0 - 1e-6
# 2. Clipped BBox model # 2. Clipped BBox model
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0) res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0, snap_size=1.0)
# Area should be less than full bbox # Area should be less than full bbox
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
@ -84,11 +84,11 @@ def test_sbend_collision_models() -> None:
radius = 10.0 radius = 10.0
width = 2.0 width = 2.0
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox") res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox", snap_size=1.0)
# Geometry should be a list of individual bbox polygons for each arc # Geometry should be a list of individual bbox polygons for each arc
assert len(res_bbox.geometry) == 2 assert len(res_bbox.geometry) == 2
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc") res_arc = SBend.generate(start, offset, radius, width, collision_type="arc", snap_size=1.0)
area_bbox = sum(p.area for p in res_bbox.geometry) area_bbox = sum(p.area for p in res_bbox.geometry)
area_arc = sum(p.area for p in res_arc.geometry) area_arc = sum(p.area for p in res_arc.geometry)
assert area_bbox > area_arc assert area_bbox > area_arc
@ -101,7 +101,8 @@ def test_sbend_continuity() -> None:
radius = 20.0 radius = 20.0
width = 1.0 width = 1.0
res = SBend.generate(start, offset, radius, width) # We use snap_size=1.0 so that (10-offset) = 6.0 is EXACTLY hit.
res = SBend.generate(start, offset, radius, width, snap_size=1.0)
# Target orientation should be same as start # Target orientation should be same as start
assert abs(res.end_port.orientation - 90.0) < 1e-6 assert abs(res.end_port.orientation - 90.0) < 1e-6
@ -141,7 +142,7 @@ def test_component_transform_invariance() -> None:
radius = 10.0 radius = 10.0
width = 2.0 width = 2.0
res0 = Bend90.generate(start0, radius, width, direction="CCW") res0 = Bend90.generate(start0, radius, width, direction="CCW", snap_size=1.0)
# Transform: Translate (10, 10) then Rotate 90 # Transform: Translate (10, 10) then Rotate 90
dx, dy = 10.0, 5.0 dx, dy = 10.0, 5.0
@ -152,7 +153,7 @@ def test_component_transform_invariance() -> None:
# 2. Generate at transformed start # 2. Generate at transformed start
start_transformed = rotate_port(translate_port(start0, dx, dy), angle) start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW") res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW", snap_size=1.0)
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6 assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6 assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6

View file

@ -15,11 +15,11 @@ def basic_evaluator() -> CostEvaluator:
# Wider bounds to allow going around (y from -40 to 40) # Wider bounds to allow going around (y from -40 to 40)
danger_map = DangerMap(bounds=(0, -40, 100, 40)) danger_map = DangerMap(bounds=(0, -40, 100, 40))
danger_map.precompute([]) danger_map.precompute([])
return CostEvaluator(engine, danger_map) return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None: def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0, sbend_offsets=[2.0, 5.0])
# Start at (0,0), target at (50, 2) -> 2um lateral offset # Start at (0,0), target at (50, 2) -> 2um lateral offset
# This matches one of our discretized SBend offsets. # This matches one of our discretized SBend offsets.
start = Port(0, 0, 0) start = Port(0, 0, 0)
@ -39,7 +39,7 @@ def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None: def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
router = AStarRouter(basic_evaluator) router = AStarRouter(basic_evaluator, snap_size=1.0, straight_lengths=[1.0, 5.0, 25.0])
# Increase base penalty to force detour immediately # Increase base penalty to force detour immediately
pf = PathFinder(router, basic_evaluator, max_iterations=10, base_congestion_penalty=1000.0) pf = PathFinder(router, basic_evaluator, max_iterations=10, base_congestion_penalty=1000.0)