fix examples

This commit is contained in:
Jan Petykiewicz 2026-03-30 21:22:20 -07:00
commit e11132b51d
20 changed files with 406 additions and 101 deletions

View file

@ -81,6 +81,7 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
| `sbend_radii` | `(10.0,)` | Available radii for S-bends. | | `sbend_radii` | `(10.0,)` | Available radii for S-bends. |
| `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. | | `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. |
| `bend_collision_type` | `"arc"` | Bend collision model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or a custom polygon. | | `bend_collision_type` | `"arc"` | Bend collision model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or a custom polygon. |
| `bend_clip_margin` | `None` | Optional legacy shrink margin for `"clipped_bbox"`. Leave `None` for the default 8-point proxy. |
| `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. | | `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. |
## 3. Objective Weights ## 3. Objective Weights

View file

@ -61,6 +61,20 @@ Check the `examples/` directory for ready-to-run scripts. To run an example:
python3 examples/01_simple_route.py python3 examples/01_simple_route.py
``` ```
## Testing
Run the default correctness suite with:
```bash
python3 -m pytest
```
Runtime regression checks for the example scenarios are opt-in and require:
```bash
INIRE_RUN_PERFORMANCE=1 python3 -m pytest -q inire/tests/test_example_performance.py
```
## Documentation ## Documentation
Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**. Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**.

View file

@ -3,7 +3,7 @@ from inire.utils.visualization import plot_routing_results
def main() -> None: def main() -> None:
print("Running Example 03: Locked Routes...") print("Running Example 03: Locked Paths...")
bounds = (0, -50, 100, 50) bounds = (0, -50, 100, 50)
options = RoutingOptions( options = RoutingOptions(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before After
Before After

View file

@ -11,6 +11,8 @@ def _route_scenario(
bend_collision_type: str, bend_collision_type: str,
netlist: dict[str, tuple[Port, Port]], netlist: dict[str, tuple[Port, Port]],
widths: dict[str, float], widths: dict[str, float],
*,
bend_clip_margin: float | None = None,
) -> dict[str, RoutingResult]: ) -> dict[str, RoutingResult]:
problem = RoutingProblem( problem = RoutingProblem(
bounds=bounds, bounds=bounds,
@ -21,6 +23,7 @@ def _route_scenario(
search=SearchOptions( search=SearchOptions(
bend_radii=(10.0,), bend_radii=(10.0,),
bend_collision_type=bend_collision_type, bend_collision_type=bend_collision_type,
bend_clip_margin=bend_clip_margin,
), ),
objective=ObjectiveWeights( objective=ObjectiveWeights(
bend_penalty=50.0, bend_penalty=50.0,
@ -49,7 +52,14 @@ def main() -> None:
print("Routing Scenario 2 (BBox)...") print("Routing Scenario 2 (BBox)...")
res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0}) res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0})
print("Routing Scenario 3 (Clipped BBox)...") print("Routing Scenario 3 (Clipped BBox)...")
res_clipped = _route_scenario(bounds, obstacles, "clipped_bbox", netlist_clipped, {"clipped_model": 2.0}) res_clipped = _route_scenario(
bounds,
obstacles,
"clipped_bbox",
netlist_clipped,
{"clipped_model": 2.0},
bend_clip_margin=1.0,
)
all_results = {**res_arc, **res_bbox, **res_clipped} all_results = {**res_arc, **res_bbox, **res_clipped}
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped} all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}

View file

@ -3,17 +3,12 @@ import time
from shapely.geometry import box from shapely.geometry import box
from inire import ( from inire import (
CongestionOptions,
DiagnosticsOptions,
NetSpec, NetSpec,
ObjectiveWeights,
Port, Port,
RoutingOptions,
RoutingProblem, RoutingProblem,
RoutingResult, RoutingResult,
SearchOptions,
route,
) )
from inire.router._stack import build_routing_stack
from inire.utils.visualization import plot_expanded_nodes, plot_routing_results from inire.utils.visualization import plot_expanded_nodes, plot_routing_results
@ -45,12 +40,15 @@ def main() -> None:
static_obstacles=tuple(obstacles), static_obstacles=tuple(obstacles),
clearance=6.0, clearance=6.0,
) )
from inire import CongestionOptions, DiagnosticsOptions, ObjectiveWeights, RoutingOptions, SearchOptions
options = RoutingOptions( options = RoutingOptions(
search=SearchOptions( search=SearchOptions(
node_limit=2_000_000, node_limit=2_000_000,
bend_radii=(50.0,), bend_radii=(50.0,),
sbend_radii=(50.0,), sbend_radii=(50.0,),
greedy_h_weight=1.5, greedy_h_weight=1.5,
bend_clip_margin=10.0,
), ),
objective=ObjectiveWeights( objective=ObjectiveWeights(
unit_length_cost=0.1, unit_length_cost=0.1,
@ -61,48 +59,59 @@ def main() -> None:
max_iterations=15, max_iterations=15,
base_penalty=100.0, base_penalty=100.0,
multiplier=1.4, multiplier=1.4,
net_order="shortest",
shuffle_nets=True, shuffle_nets=True,
seed=42, seed=42,
), ),
diagnostics=DiagnosticsOptions(capture_expanded=True), diagnostics=DiagnosticsOptions(capture_expanded=True),
) )
stack = build_routing_stack(problem, options)
evaluator = stack.evaluator
finder = stack.finder
metrics = finder.metrics
iteration_stats: list[dict[str, int]] = [] iteration_stats: list[dict[str, int]] = []
def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None: def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None:
successes = sum(1 for result in current_results.values() if result.is_valid) successes = sum(1 for result in current_results.values() if result.is_valid)
total_collisions = sum(result.collisions for result in current_results.values()) total_collisions = sum(result.collisions for result in current_results.values())
total_nodes = metrics.nodes_expanded
print(f" Iteration {iteration} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}") print(f" Iteration {iteration} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}")
new_greedy = max(1.1, 1.5 - ((iteration + 1) / 10.0) * 0.4)
evaluator.greedy_h_weight = new_greedy
print(f" Adaptive Greedy Weight for Next Iteration: {new_greedy:.3f}")
iteration_stats.append( iteration_stats.append(
{ {
"Iteration": iteration, "Iteration": iteration,
"Success": successes, "Success": successes,
"Congestion": total_collisions, "Congestion": total_collisions,
"Nodes": total_nodes,
} }
) )
metrics.reset_per_route()
print(f"Routing {len(netlist)} nets through 200um bottleneck...") print(f"Routing {len(netlist)} nets through 200um bottleneck...")
start_time = time.perf_counter() start_time = time.perf_counter()
run = route(problem, options=options, iteration_callback=iteration_callback) results = finder.route_all(iteration_callback=iteration_callback)
end_time = time.perf_counter() end_time = time.perf_counter()
print(f"Routing took {end_time - start_time:.4f}s") print(f"Routing took {end_time - start_time:.4f}s")
print("\n--- Iteration Summary ---") print("\n--- Iteration Summary ---")
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8}") print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}")
print("-" * 30) print("-" * 43)
for stats in iteration_stats: for stats in iteration_stats:
print(f"{stats['Iteration']:<5} | {stats['Success']:<8} | {stats['Congestion']:<8}") print(f"{stats['Iteration']:<5} | {stats['Success']:<8} | {stats['Congestion']:<8} | {stats['Nodes']:<10}")
success_count = sum(1 for result in run.results_by_net.values() if result.is_valid) success_count = sum(1 for result in results.values() if result.is_valid)
print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.") print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.")
for net_id, result in run.results_by_net.items(): for net_id, result in results.items():
if not result.is_valid: if not result.is_valid:
print(f" FAILED: {net_id}, collisions={result.collisions}") print(f" FAILED: {net_id}, collisions={result.collisions}")
else: else:
print(f" {net_id}: SUCCESS") print(f" {net_id}: SUCCESS")
fig, ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist=netlist) fig, ax = plot_routing_results(results, list(obstacles), bounds, netlist=netlist)
plot_expanded_nodes(list(run.expanded_nodes), ax=ax) plot_expanded_nodes(list(finder.accumulated_expanded_nodes), ax=ax)
fig.savefig("examples/07_large_scale_routing.png") fig.savefig("examples/07_large_scale_routing.png")
print("Saved plot to examples/07_large_scale_routing.png") print("Saved plot to examples/07_large_scale_routing.png")

View file

@ -1,50 +1,65 @@
from shapely.geometry import Polygon from shapely.geometry import Polygon
from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route from inire import CongestionOptions, NetSpec, RoutingOptions, RoutingProblem, SearchOptions
from inire.geometry.collision import RoutingWorld
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router._astar_types import AStarContext, AStarMetrics
from inire.router._router import PathFinder
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results
def _run_request(
bounds: tuple[float, float, float, float],
bend_collision_type: object,
net_id: str,
start: Port,
target: Port,
) -> dict[str, RoutingResult]:
problem = RoutingProblem(
bounds=bounds,
nets=(NetSpec(net_id, start, target, width=2.0),),
)
options = RoutingOptions(
search=SearchOptions(
bend_radii=(10.0,),
bend_collision_type=bend_collision_type,
sbend_radii=(),
),
objective=ObjectiveWeights(
bend_penalty=50.0,
sbend_penalty=150.0,
),
congestion=CongestionOptions(use_tiered_strategy=False),
)
return route(problem, options=options).results_by_net
def main() -> None: def main() -> None:
print("Running Example 08: Custom Bend Geometry...") print("Running Example 08: Custom Bend Geometry...")
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
engine = RoutingWorld(clearance=2.0)
danger_map = DangerMap(bounds=bounds)
danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
metrics = AStarMetrics()
start = Port(20, 20, 0) start = Port(20, 20, 0)
target = Port(100, 100, 90) target = Port(100, 100, 90)
print("Routing with standard arc...") print("Routing with standard arc...")
results_std = _run_request(bounds, "arc", "custom_bend", start, target) results_std = PathFinder(
AStarContext(
evaluator,
RoutingProblem(
bounds=bounds,
nets=(NetSpec("custom_bend", start, target, width=2.0),),
),
RoutingOptions(
search=SearchOptions(bend_radii=(10.0,), sbend_radii=()),
congestion=CongestionOptions(max_iterations=1),
),
),
metrics=metrics,
).route_all()
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) custom_poly = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)])
print("Routing with custom bend geometry...") print("Routing with custom collision model...")
results_custom = _run_request(bounds, custom_poly, "custom_model", start, target) results_custom = PathFinder(
AStarContext(
evaluator,
RoutingProblem(
bounds=bounds,
nets=(NetSpec("custom_model", start, target, width=2.0),),
),
RoutingOptions(
search=SearchOptions(
bend_radii=(10.0,),
bend_collision_type=custom_poly,
sbend_radii=(),
),
congestion=CongestionOptions(max_iterations=1, use_tiered_strategy=False),
),
),
metrics=AStarMetrics(),
use_tiered_strategy=False,
).route_all()
all_results = {**results_std, **results_custom} all_results = {**results_std, **results_custom}
fig, _ax = plot_routing_results( fig, _ax = plot_routing_results(

View file

@ -26,7 +26,7 @@ def main() -> None:
bend_penalty=50.0, bend_penalty=50.0,
sbend_penalty=150.0, sbend_penalty=150.0,
), ),
congestion=CongestionOptions(warm_start_enabled=False), congestion=CongestionOptions(warm_start_enabled=False, max_iterations=1),
) )
print("Routing with a deliberately tiny node budget (should return a partial path)...") print("Routing with a deliberately tiny node budget (should return a partial path)...")

View file

@ -18,9 +18,9 @@ Demonstrates the Negotiated Congestion algorithm handling multiple intersecting
`inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy: `inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy:
* **Arc**: High-fidelity geometry (Highest accuracy). * **Arc**: High-fidelity geometry (Highest accuracy).
* **BBox**: Simple axis-aligned bounding box (Fastest search). * **BBox**: Simple axis-aligned bounding box (Fastest search).
* **Clipped BBox**: A balanced 8-point conservative polygonal approximation of the arc (Optimal performance). * **Clipped BBox**: A balanced model that clips the corners of the AABB to better fit the arc (Optimal performance).
Example 08 also demonstrates a custom polygonal bend geometry. Custom polygons are defined in bend-local coordinates around the bend center, mirrored for CW bends, and rotated with the bend orientation before being placed. The example uses a 6-point Manhattan 90-degree bend with the same width as the normal waveguide, and that polygon now serves as both the routed geometry and the search-time collision shape. Example 08 also demonstrates a custom polygonal bend geometry. It uses a centered `20x20` box as a custom bend collision model.
![Custom Bend Geometry](08_custom_bend_geometry.png) ![Custom Bend Geometry](08_custom_bend_geometry.png)

View file

@ -134,7 +134,21 @@ def _get_arc_polygons(
return [Polygon(numpy.concatenate((inner_points, outer_points), axis=0))] return [Polygon(numpy.concatenate((inner_points, outer_points), axis=0))]
def _clip_bbox(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon: def _clip_bbox_legacy(
cxy: tuple[float, float],
radius: float,
width: float,
ts: tuple[float, float],
clip_margin: float,
) -> Polygon:
arc_poly = _get_arc_polygons(cxy, radius, width, ts)[0]
minx, miny, maxx, maxy = arc_poly.bounds
bbox_poly = box(minx, miny, maxx, maxy)
shrink = min(clip_margin, max(radius, width))
return bbox_poly.buffer(-shrink, join_style=2) if shrink > 0 else bbox_poly
def _clip_bbox_polygonal(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon:
"""Return a conservative 8-point polygonal proxy for the arc. """Return a conservative 8-point polygonal proxy for the arc.
The polygon uses 4 points along the outer edge and 4 along the inner edge. The polygon uses 4 points along the outer edge and 4 along the inner edge.
@ -165,6 +179,18 @@ def _clip_bbox(cxy: tuple[float, float], radius: float, width: float, ts: tuple[
return Polygon(numpy.concatenate((outer_points, inner_points), axis=0)) return Polygon(numpy.concatenate((outer_points, inner_points), axis=0))
def _clip_bbox(
cxy: tuple[float, float],
radius: float,
width: float,
ts: tuple[float, float],
clip_margin: float | None,
) -> Polygon:
if clip_margin is not None:
return _clip_bbox_legacy(cxy, radius, width, ts, clip_margin)
return _clip_bbox_polygonal(cxy, radius, width, ts)
def _transform_custom_collision_polygon( def _transform_custom_collision_polygon(
collision_poly: Polygon, collision_poly: Polygon,
cxy: tuple[float, float], cxy: tuple[float, float],
@ -186,6 +212,7 @@ def _apply_collision_model(
width: float, width: float,
cxy: tuple[float, float], cxy: tuple[float, float],
ts: tuple[float, float], ts: tuple[float, float],
clip_margin: float | None = None,
rotation_deg: float = 0.0, rotation_deg: float = 0.0,
mirror_y: bool = False, mirror_y: bool = False,
) -> list[Polygon]: ) -> list[Polygon]:
@ -194,7 +221,7 @@ def _apply_collision_model(
if collision_type == "arc": if collision_type == "arc":
return [arc_poly] return [arc_poly]
if collision_type == "clipped_bbox": if collision_type == "clipped_bbox":
clipped = _clip_bbox(cxy, radius, width, ts) clipped = _clip_bbox(cxy, radius, width, ts, clip_margin)
return [clipped if not clipped.is_empty else box(*arc_poly.bounds)] return [clipped if not clipped.is_empty else box(*arc_poly.bounds)]
return [box(*arc_poly.bounds)] return [box(*arc_poly.bounds)]
@ -254,11 +281,11 @@ class Bend90:
direction: Literal["CW", "CCW"], direction: Literal["CW", "CCW"],
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc", collision_type: BendCollisionModel = "arc",
clip_margin: float | None = None,
dilation: float = 0.0, dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
rot2 = rotation_matrix2(start_port.r) rot2 = rotation_matrix2(start_port.r)
sign = 1 if direction == "CCW" else -1 sign = 1 if direction == "CCW" else -1
uses_custom_geometry = isinstance(collision_type, Polygon)
center_local = numpy.array((0.0, sign * radius)) center_local = numpy.array((0.0, sign * radius))
end_local = numpy.array((radius, sign * radius)) end_local = numpy.array((radius, sign * radius))
@ -278,16 +305,13 @@ class Bend90:
width, width,
(float(center_xy[0]), float(center_xy[1])), (float(center_xy[0]), float(center_xy[1])),
ts, ts,
clip_margin=clip_margin,
rotation_deg=float(start_port.r), rotation_deg=float(start_port.r),
mirror_y=(sign < 0), mirror_y=(sign < 0),
) )
physical_geometry = collision_polys if uses_custom_geometry else arc_polys physical_geometry = arc_polys
if dilation > 0: if dilation > 0:
if uses_custom_geometry:
dilated_physical_geometry = [poly.buffer(dilation) for poly in collision_polys]
dilated_collision_geometry = dilated_physical_geometry
else:
dilated_physical_geometry = _get_arc_polygons( dilated_physical_geometry = _get_arc_polygons(
(float(center_xy[0]), float(center_xy[1])), (float(center_xy[0]), float(center_xy[1])),
radius, radius,
@ -325,13 +349,13 @@ class SBend:
width: float, width: float,
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc", collision_type: BendCollisionModel = "arc",
clip_margin: float | None = None,
dilation: float = 0.0, dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
if abs(offset) >= 2 * radius: if abs(offset) >= 2 * radius:
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}") raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
sign = 1 if offset >= 0 else -1 sign = 1 if offset >= 0 else -1
uses_custom_geometry = isinstance(collision_type, Polygon)
theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius)) theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius))
dx = 2.0 * radius * numpy.sin(theta) dx = 2.0 * radius * numpy.sin(theta)
theta_deg = float(numpy.degrees(theta)) theta_deg = float(numpy.degrees(theta))
@ -361,6 +385,7 @@ class SBend:
width, width,
(float(c1_xy[0]), float(c1_xy[1])), (float(c1_xy[0]), float(c1_xy[1])),
ts1, ts1,
clip_margin=clip_margin,
rotation_deg=float(start_port.r), rotation_deg=float(start_port.r),
mirror_y=(sign < 0), mirror_y=(sign < 0),
)[0], )[0],
@ -371,17 +396,14 @@ class SBend:
width, width,
(float(c2_xy[0]), float(c2_xy[1])), (float(c2_xy[0]), float(c2_xy[1])),
ts2, ts2,
clip_margin=clip_margin,
rotation_deg=float(start_port.r), rotation_deg=float(start_port.r),
mirror_y=(sign > 0), mirror_y=(sign > 0),
)[0], )[0],
] ]
physical_geometry = geometry if uses_custom_geometry else actual_geometry physical_geometry = actual_geometry
if dilation > 0: if dilation > 0:
if uses_custom_geometry:
dilated_physical_geometry = [poly.buffer(dilation) for poly in geometry]
dilated_collision_geometry = dilated_physical_geometry
else:
dilated_physical_geometry = [ dilated_physical_geometry = [
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0], _get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0],
_get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0], _get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0],

View file

@ -43,6 +43,7 @@ class SearchOptions:
bend_radii: tuple[float, ...] = (50.0, 100.0) bend_radii: tuple[float, ...] = (50.0, 100.0)
sbend_radii: tuple[float, ...] = (10.0,) sbend_radii: tuple[float, ...] = (10.0,)
bend_collision_type: BendCollisionModel = "arc" bend_collision_type: BendCollisionModel = "arc"
bend_clip_margin: float | None = None
visibility_guidance: VisibilityGuidance = "tangent_corner" visibility_guidance: VisibilityGuidance = "tangent_corner"
def __post_init__(self) -> None: def __post_init__(self) -> None:

View file

@ -69,6 +69,7 @@ def process_move(
net_width, net_width,
params[1], params[1],
collision_type=coll_type, collision_type=coll_type,
clip_margin=config.bend_clip_margin,
dilation=self_dilation, dilation=self_dilation,
) )
else: else:
@ -78,6 +79,7 @@ def process_move(
params[1], params[1],
net_width, net_width,
collision_type=coll_type, collision_type=coll_type,
clip_margin=config.bend_clip_margin,
dilation=self_dilation, dilation=self_dilation,
) )
except ValueError: except ValueError:

View file

@ -16,6 +16,7 @@ if TYPE_CHECKING:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class SearchRunConfig: class SearchRunConfig:
bend_collision_type: BendCollisionModel bend_collision_type: BendCollisionModel
bend_clip_margin: float | None
node_limit: int node_limit: int
return_partial: bool = False return_partial: bool = False
store_expanded: bool = False store_expanded: bool = False
@ -39,6 +40,7 @@ class SearchRunConfig:
search = options.search search = options.search
return cls( return cls(
bend_collision_type=search.bend_collision_type if bend_collision_type is None else bend_collision_type, bend_collision_type=search.bend_collision_type if bend_collision_type is None else bend_collision_type,
bend_clip_margin=search.bend_clip_margin,
node_limit=search.node_limit if node_limit is None else node_limit, node_limit=search.node_limit if node_limit is None else node_limit,
return_partial=return_partial, return_partial=return_partial,
store_expanded=store_expanded, store_expanded=store_expanded,

View file

@ -24,6 +24,7 @@ def materialize_path_seed(
current = start current = start
dilation = clearance / 2.0 dilation = clearance / 2.0
bend_collision_type = search.bend_collision_type bend_collision_type = search.bend_collision_type
bend_clip_margin = search.bend_clip_margin
for segment in seed.segments: for segment in seed.segments:
if isinstance(segment, StraightSeed): if isinstance(segment, StraightSeed):
@ -35,6 +36,7 @@ def materialize_path_seed(
net_width, net_width,
segment.direction, segment.direction,
collision_type=bend_collision_type, collision_type=bend_collision_type,
clip_margin=bend_clip_margin,
dilation=dilation, dilation=dilation,
) )
elif isinstance(segment, SBendSeed): elif isinstance(segment, SBendSeed):
@ -44,6 +46,7 @@ def materialize_path_seed(
segment.radius, segment.radius,
net_width, net_width,
collision_type=bend_collision_type, collision_type=bend_collision_type,
clip_margin=bend_clip_margin,
dilation=dilation, dilation=dilation,
) )
else: else:

View file

@ -57,6 +57,14 @@ class CostEvaluator:
def default_weights(self) -> ObjectiveWeights: def default_weights(self) -> ObjectiveWeights:
return self._search_weights return self._search_weights
@property
def greedy_h_weight(self) -> float:
return self._greedy_h_weight
@greedy_h_weight.setter
def greedy_h_weight(self, value: float) -> None:
self._greedy_h_weight = float(value)
def _resolve_weights(self, weights: ObjectiveWeights | None) -> ObjectiveWeights: def _resolve_weights(self, weights: ObjectiveWeights | None) -> ObjectiveWeights:
return self._search_weights if weights is None else weights return self._search_weights if weights is None else weights

View file

@ -270,7 +270,12 @@ def run_example_06() -> ScenarioOutcome:
_build_evaluator(bounds, obstacles=obstacles), _build_evaluator(bounds, obstacles=obstacles),
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}, {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))},
{"clipped_model": 2.0}, {"clipped_model": 2.0},
{"bend_radii": [10.0], "bend_collision_type": "clipped_bbox", "use_tiered_strategy": False}, {
"bend_radii": [10.0],
"bend_collision_type": "clipped_bbox",
"bend_clip_margin": 1.0,
"use_tiered_strategy": False,
},
), ),
] ]
@ -323,9 +328,11 @@ def run_example_07() -> ScenarioOutcome:
"node_limit": 2000000, "node_limit": 2000000,
"bend_radii": [50.0], "bend_radii": [50.0],
"sbend_radii": [50.0], "sbend_radii": [50.0],
"bend_clip_margin": 10.0,
"max_iterations": 15, "max_iterations": 15,
"base_penalty": 100.0, "base_penalty": 100.0,
"multiplier": 1.4, "multiplier": 1.4,
"net_order": "shortest",
"capture_expanded": True, "capture_expanded": True,
"shuffle_nets": True, "shuffle_nets": True,
"seed": 42, "seed": 42,
@ -333,7 +340,10 @@ def run_example_07() -> ScenarioOutcome:
) )
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None: def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
_ = idx, current_results _ = current_results
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
evaluator.greedy_h_weight = new_greedy
metrics.reset_per_route()
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all(iteration_callback=iteration_callback) results = pathfinder.route_all(iteration_callback=iteration_callback)
@ -345,27 +355,27 @@ def run_example_08() -> ScenarioOutcome:
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))} netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"custom_bend": 2.0} widths = {"custom_bend": 2.0}
custom_model = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) custom_model = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)])
standard_evaluator = _build_evaluator(bounds) evaluator = _build_evaluator(bounds)
custom_evaluator = _build_evaluator(bounds)
t0 = perf_counter() t0 = perf_counter()
results_std = _build_pathfinder( results_std = _build_pathfinder(
standard_evaluator, evaluator,
bounds=bounds, bounds=bounds,
nets=_net_specs(netlist, widths), nets=_net_specs(netlist, widths),
bend_radii=[10.0], bend_radii=[10.0],
sbend_radii=[], sbend_radii=[],
use_tiered_strategy=False, max_iterations=1,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() ).route_all()
results_custom = _build_pathfinder( results_custom = _build_pathfinder(
custom_evaluator, evaluator,
bounds=bounds, bounds=bounds,
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}), nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
bend_radii=[10.0], bend_radii=[10.0],
bend_collision_type=custom_model, bend_collision_type=custom_model,
sbend_radii=[], sbend_radii=[],
max_iterations=1,
use_tiered_strategy=False, use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() ).route_all()
@ -386,7 +396,7 @@ def run_example_09() -> ScenarioOutcome:
widths=widths, widths=widths,
obstacles=obstacles, obstacles=obstacles,
evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0}, evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0},
request_kwargs={"node_limit": 3, "bend_radii": [10.0], "warm_start_enabled": False}, request_kwargs={"node_limit": 3, "bend_radii": [10.0], "warm_start_enabled": False, "max_iterations": 1},
) )
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
@ -397,7 +407,7 @@ def run_example_09() -> ScenarioOutcome:
SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = ( SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
("example_01_simple_route", run_example_01), ("example_01_simple_route", run_example_01),
("example_02_congestion_resolution", run_example_02), ("example_02_congestion_resolution", run_example_02),
("example_03_locked_routes", run_example_03), ("example_03_locked_paths", run_example_03),
("example_04_sbends_and_radii", run_example_04), ("example_04_sbends_and_radii", run_example_04),
("example_05_orientation_stress", run_example_05), ("example_05_orientation_stress", run_example_05),
("example_06_bend_collision_models", run_example_06), ("example_06_bend_collision_models", run_example_06),

View file

@ -102,6 +102,19 @@ def test_bend_collision_models() -> None:
res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc") res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc")
assert res_clipped.collision_geometry[0].covers(res_arc.collision_geometry[0]) assert res_clipped.collision_geometry[0].covers(res_arc.collision_geometry[0])
# 3. Legacy clip-margin mode should still be available when explicitly requested.
res_clipped_margin = Bend90.generate(
start,
radius,
width,
direction="CCW",
collision_type="clipped_bbox",
clip_margin=1.0,
)
assert len(res_clipped_margin.collision_geometry[0].exterior.coords) - 1 == 4
assert abs(res_clipped_margin.collision_geometry[0].area - 81.0) < 1e-6
assert res_clipped_margin.collision_geometry[0].area > res_clipped.collision_geometry[0].area
def test_custom_bend_collision_polygon_uses_local_transform() -> None: def test_custom_bend_collision_polygon_uses_local_transform() -> None:
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
@ -122,17 +135,16 @@ def test_custom_bend_collision_polygon_uses_local_transform() -> None:
expected = shapely_translate(expected, center_xy[0], center_xy[1]) expected = shapely_translate(expected, center_xy[0], center_xy[1])
assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6 assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6
assert result.physical_geometry[0].symmetric_difference(expected).area < 1e-6
def test_custom_bend_collision_polygon_keeps_collision_and_physical_geometry_aligned() -> None: def test_custom_bend_collision_polygon_only_overrides_search_geometry() -> None:
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_poly, dilation=1.0) result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_poly, dilation=1.0)
assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area < 1e-6 assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area > 1e-6
assert result.dilated_collision_geometry is not None assert result.dilated_collision_geometry is not None
assert result.dilated_physical_geometry is not None assert result.dilated_physical_geometry is not None
assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area < 1e-6 assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area > 1e-6
def test_sbend_collision_models() -> None: def test_sbend_collision_models() -> None:

View file

@ -40,6 +40,18 @@ def test_cost_calculation() -> None:
assert h_away >= h_90 assert h_away >= h_90
def test_greedy_h_weight_is_mutable() -> None:
engine = RoutingWorld(clearance=2.0)
danger_map = DangerMap(bounds=(0, 0, 50, 50))
danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=10.0)
assert evaluator.greedy_h_weight == 1.5
evaluator.greedy_h_weight = 1.2
assert evaluator.greedy_h_weight == 1.2
assert abs(evaluator.h_manhattan(Port(0, 0, 0), Port(10, 10, 0)) - 72.0) < 1e-6
def test_danger_map_kd_tree_and_cache() -> None: def test_danger_map_kd_tree_and_cache() -> None:
# Test that KD-Tree based danger map works and uses cache # Test that KD-Tree based danger map works and uses cache
bounds = (0, 0, 1000, 1000) bounds = (0, 0, 1000, 1000)

View file

@ -13,28 +13,28 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
PERFORMANCE_REPEATS = 3 PERFORMANCE_REPEATS = 3
REGRESSION_FACTOR = 1.5 REGRESSION_FACTOR = 1.5
# Baselines are measured from the current code path without plotting. # Baselines are measured from clean 6a28dcf-style runs without plotting.
BASELINE_SECONDS = { BASELINE_SECONDS = {
"example_01_simple_route": 0.0035, "example_01_simple_route": 0.0035,
"example_02_congestion_resolution": 0.2666, "example_02_congestion_resolution": 0.2666,
"example_03_locked_routes": 0.2304, "example_03_locked_paths": 0.2304,
"example_04_sbends_and_radii": 1.8734, "example_04_sbends_and_radii": 1.8734,
"example_05_orientation_stress": 0.5630, "example_05_orientation_stress": 0.5630,
"example_06_bend_collision_models": 5.2382, "example_06_bend_collision_models": 5.2382,
"example_07_large_scale_routing": 1.2081, "example_07_large_scale_routing": 1.2081,
"example_08_custom_bend_geometry": 0.9848, "example_08_custom_bend_geometry": 4.2111,
"example_09_unroutable_best_effort": 0.0056, "example_09_unroutable_best_effort": 0.0056,
} }
EXPECTED_OUTCOMES = { EXPECTED_OUTCOMES = {
"example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1}, "example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1},
"example_02_congestion_resolution": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, "example_02_congestion_resolution": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
"example_03_locked_routes": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, "example_03_locked_paths": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
"example_04_sbends_and_radii": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, "example_04_sbends_and_radii": {"total_results": 2, "valid_results": 2, "reached_targets": 2},
"example_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, "example_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
"example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, "example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
"example_07_large_scale_routing": {"total_results": 10, "valid_results": 10, "reached_targets": 10}, "example_07_large_scale_routing": {"total_results": 10, "valid_results": 10, "reached_targets": 10},
"example_08_custom_bend_geometry": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, "example_08_custom_bend_geometry": {"total_results": 2, "valid_results": 1, "reached_targets": 2},
"example_09_unroutable_best_effort": {"total_results": 1, "valid_results": 0, "reached_targets": 0}, "example_09_unroutable_best_effort": {"total_results": 1, "valid_results": 0, "reached_targets": 0},
} }

View file

@ -0,0 +1,184 @@
import pytest
from shapely.geometry import Polygon, box
from inire import (
CongestionOptions,
DiagnosticsOptions,
NetSpec,
ObjectiveWeights,
Port,
RoutingOptions,
RoutingProblem,
SearchOptions,
route,
)
from inire.router._stack import build_routing_stack
from inire.seeds import Bend90Seed, PathSeed, StraightSeed
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics
EXPECTED_OUTCOMES = {
"example_01_simple_route": (1, 1, 1),
"example_02_congestion_resolution": (3, 3, 3),
"example_03_locked_paths": (2, 2, 2),
"example_04_sbends_and_radii": (2, 2, 2),
"example_05_orientation_stress": (3, 3, 3),
"example_06_bend_collision_models": (3, 3, 3),
"example_07_large_scale_routing": (10, 10, 10),
"example_08_custom_bend_geometry": (2, 1, 2),
"example_09_unroutable_best_effort": (1, 0, 0),
}
@pytest.mark.parametrize(("name", "run"), SCENARIOS, ids=[name for name, _ in SCENARIOS])
def test_examples_match_legacy_expected_outcomes(name: str, run) -> None:
outcome = run()
assert outcome[1:] == EXPECTED_OUTCOMES[name]
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
bounds = (-20, -20, 170, 170)
obstacles = (
Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]),
Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]),
Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]),
)
problem = RoutingProblem(
bounds=bounds,
nets=(NetSpec("clipped_model", Port(10, 20, 0), Port(90, 40, 90), width=2.0),),
static_obstacles=obstacles,
)
common_kwargs = {
"objective": ObjectiveWeights(bend_penalty=50.0, sbend_penalty=150.0),
"congestion": CongestionOptions(use_tiered_strategy=False),
}
no_margin = route(
problem,
options=RoutingOptions(
search=SearchOptions(
bend_radii=(10.0,),
bend_collision_type="clipped_bbox",
),
**common_kwargs,
),
).results_by_net["clipped_model"]
legacy_margin = route(
problem,
options=RoutingOptions(
search=SearchOptions(
bend_radii=(10.0,),
bend_collision_type="clipped_bbox",
bend_clip_margin=1.0,
),
**common_kwargs,
),
).results_by_net["clipped_model"]
assert no_margin.is_valid
assert legacy_margin.is_valid
assert legacy_margin.as_seed() != no_margin.as_seed()
assert legacy_margin.as_seed() == PathSeed(
(
StraightSeed(5.0),
Bend90Seed(10.0, "CW"),
Bend90Seed(10.0, "CCW"),
StraightSeed(45.0),
Bend90Seed(10.0, "CCW"),
StraightSeed(30.0),
)
)
def test_example_07_reduced_bottleneck_uses_adaptive_greedy_callback() -> None:
bounds = (0, 0, 500, 300)
obstacles = (
box(220, 0, 280, 100),
box(220, 200, 280, 300),
)
netlist = {
"net_00": (Port(30, 130, 0), Port(470, 60, 0)),
"net_01": (Port(30, 140, 0), Port(470, 120, 0)),
"net_02": (Port(30, 150, 0), Port(470, 180, 0)),
"net_03": (Port(30, 160, 0), Port(470, 240, 0)),
}
problem = RoutingProblem(
bounds=bounds,
nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()),
static_obstacles=obstacles,
clearance=6.0,
)
options = RoutingOptions(
search=SearchOptions(
node_limit=200000,
bend_radii=(30.0,),
sbend_radii=(30.0,),
greedy_h_weight=1.5,
bend_clip_margin=10.0,
),
objective=ObjectiveWeights(
unit_length_cost=0.1,
bend_penalty=100.0,
sbend_penalty=400.0,
),
congestion=CongestionOptions(
max_iterations=6,
base_penalty=100.0,
multiplier=1.4,
net_order="shortest",
shuffle_nets=True,
seed=42,
),
diagnostics=DiagnosticsOptions(capture_expanded=False),
)
stack = build_routing_stack(problem, options)
evaluator = stack.evaluator
finder = stack.finder
weights: list[float] = []
def iteration_callback(iteration: int, current_results: dict[str, object]) -> None:
_ = current_results
new_greedy = max(1.1, 1.5 - ((iteration + 1) / 10.0) * 0.4)
evaluator.greedy_h_weight = new_greedy
weights.append(new_greedy)
finder.metrics.reset_per_route()
results = finder.route_all(iteration_callback=iteration_callback)
assert weights == [1.46]
assert evaluator.greedy_h_weight == 1.46
assert all(result.is_valid for result in results.values())
assert all(result.reached_target for result in results.values())
def test_example_08_custom_box_restores_legacy_collision_outcome() -> None:
bounds = (0, 0, 150, 150)
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"custom_bend": 2.0}
evaluator = _build_evaluator(bounds)
standard = _build_pathfinder(
evaluator,
bounds=bounds,
nets=_net_specs(netlist, widths),
bend_radii=[10.0],
sbend_radii=[],
max_iterations=1,
metrics=AStarMetrics(),
).route_all()
custom = _build_pathfinder(
evaluator,
bounds=bounds,
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
bend_radii=[10.0],
bend_collision_type=Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)]),
sbend_radii=[],
max_iterations=1,
use_tiered_strategy=False,
metrics=AStarMetrics(),
).route_all()
assert standard["custom_bend"].is_valid
assert standard["custom_bend"].reached_target
assert not custom["custom_model"].is_valid
assert custom["custom_model"].reached_target
assert custom["custom_model"].collisions == 2