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_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_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. |
## 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
```
## 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
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:
print("Running Example 03: Locked Routes...")
print("Running Example 03: Locked Paths...")
bounds = (0, -50, 100, 50)
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,
netlist: dict[str, tuple[Port, Port]],
widths: dict[str, float],
*,
bend_clip_margin: float | None = None,
) -> dict[str, RoutingResult]:
problem = RoutingProblem(
bounds=bounds,
@ -21,6 +23,7 @@ def _route_scenario(
search=SearchOptions(
bend_radii=(10.0,),
bend_collision_type=bend_collision_type,
bend_clip_margin=bend_clip_margin,
),
objective=ObjectiveWeights(
bend_penalty=50.0,
@ -49,7 +52,14 @@ def main() -> None:
print("Routing Scenario 2 (BBox)...")
res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0})
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_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}

View file

@ -3,17 +3,12 @@ import time
from shapely.geometry import box
from inire import (
CongestionOptions,
DiagnosticsOptions,
NetSpec,
ObjectiveWeights,
Port,
RoutingOptions,
RoutingProblem,
RoutingResult,
SearchOptions,
route,
)
from inire.router._stack import build_routing_stack
from inire.utils.visualization import plot_expanded_nodes, plot_routing_results
@ -45,12 +40,15 @@ def main() -> None:
static_obstacles=tuple(obstacles),
clearance=6.0,
)
from inire import CongestionOptions, DiagnosticsOptions, ObjectiveWeights, RoutingOptions, SearchOptions
options = RoutingOptions(
search=SearchOptions(
node_limit=2_000_000,
bend_radii=(50.0,),
sbend_radii=(50.0,),
greedy_h_weight=1.5,
bend_clip_margin=10.0,
),
objective=ObjectiveWeights(
unit_length_cost=0.1,
@ -61,48 +59,59 @@ def main() -> None:
max_iterations=15,
base_penalty=100.0,
multiplier=1.4,
net_order="shortest",
shuffle_nets=True,
seed=42,
),
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]] = []
def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None:
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_nodes = metrics.nodes_expanded
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": iteration,
"Success": successes,
"Congestion": total_collisions,
"Nodes": total_nodes,
}
)
metrics.reset_per_route()
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
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()
print(f"Routing took {end_time - start_time:.4f}s")
print("\n--- Iteration Summary ---")
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8}")
print("-" * 30)
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}")
print("-" * 43)
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.")
for net_id, result in run.results_by_net.items():
for net_id, result in results.items():
if not result.is_valid:
print(f" FAILED: {net_id}, collisions={result.collisions}")
else:
print(f" {net_id}: SUCCESS")
fig, ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist=netlist)
plot_expanded_nodes(list(run.expanded_nodes), ax=ax)
fig, ax = plot_routing_results(results, list(obstacles), bounds, netlist=netlist)
plot_expanded_nodes(list(finder.accumulated_expanded_nodes), ax=ax)
fig.savefig("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 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.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
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:
print("Running Example 08: Custom Bend Geometry...")
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)
target = Port(100, 100, 90)
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...")
results_custom = _run_request(bounds, custom_poly, "custom_model", start, target)
print("Routing with custom collision model...")
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}
fig, _ax = plot_routing_results(

View file

@ -26,7 +26,7 @@ def main() -> None:
bend_penalty=50.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)...")

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:
* **Arc**: High-fidelity geometry (Highest accuracy).
* **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)

View file

@ -134,7 +134,21 @@ def _get_arc_polygons(
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.
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))
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(
collision_poly: Polygon,
cxy: tuple[float, float],
@ -186,6 +212,7 @@ def _apply_collision_model(
width: float,
cxy: tuple[float, float],
ts: tuple[float, float],
clip_margin: float | None = None,
rotation_deg: float = 0.0,
mirror_y: bool = False,
) -> list[Polygon]:
@ -194,7 +221,7 @@ def _apply_collision_model(
if collision_type == "arc":
return [arc_poly]
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 [box(*arc_poly.bounds)]
@ -254,11 +281,11 @@ class Bend90:
direction: Literal["CW", "CCW"],
sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc",
clip_margin: float | None = None,
dilation: float = 0.0,
) -> ComponentResult:
rot2 = rotation_matrix2(start_port.r)
sign = 1 if direction == "CCW" else -1
uses_custom_geometry = isinstance(collision_type, Polygon)
center_local = numpy.array((0.0, sign * radius))
end_local = numpy.array((radius, sign * radius))
@ -278,27 +305,24 @@ class Bend90:
width,
(float(center_xy[0]), float(center_xy[1])),
ts,
clip_margin=clip_margin,
rotation_deg=float(start_port.r),
mirror_y=(sign < 0),
)
physical_geometry = collision_polys if uses_custom_geometry else arc_polys
physical_geometry = arc_polys
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(
(float(center_xy[0]), float(center_xy[1])),
radius,
width,
ts,
sagitta,
dilation=dilation,
)
dilated_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
)
dilated_physical_geometry = _get_arc_polygons(
(float(center_xy[0]), float(center_xy[1])),
radius,
width,
ts,
sagitta,
dilation=dilation,
)
dilated_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
)
else:
dilated_physical_geometry = physical_geometry
dilated_collision_geometry = collision_polys
@ -325,13 +349,13 @@ class SBend:
width: float,
sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc",
clip_margin: float | None = None,
dilation: float = 0.0,
) -> ComponentResult:
if abs(offset) >= 2 * radius:
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
sign = 1 if offset >= 0 else -1
uses_custom_geometry = isinstance(collision_type, Polygon)
theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius))
dx = 2.0 * radius * numpy.sin(theta)
theta_deg = float(numpy.degrees(theta))
@ -361,6 +385,7 @@ class SBend:
width,
(float(c1_xy[0]), float(c1_xy[1])),
ts1,
clip_margin=clip_margin,
rotation_deg=float(start_port.r),
mirror_y=(sign < 0),
)[0],
@ -371,24 +396,21 @@ class SBend:
width,
(float(c2_xy[0]), float(c2_xy[1])),
ts2,
clip_margin=clip_margin,
rotation_deg=float(start_port.r),
mirror_y=(sign > 0),
)[0],
]
physical_geometry = geometry if uses_custom_geometry else actual_geometry
physical_geometry = actual_geometry
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 = [
_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],
]
dilated_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in 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(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0],
]
dilated_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry]
)
else:
dilated_physical_geometry = physical_geometry
dilated_collision_geometry = geometry

View file

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

View file

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

View file

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

View file

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

View file

@ -57,6 +57,14 @@ class CostEvaluator:
def default_weights(self) -> ObjectiveWeights:
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:
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),
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))},
{"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,
"bend_radii": [50.0],
"sbend_radii": [50.0],
"bend_clip_margin": 10.0,
"max_iterations": 15,
"base_penalty": 100.0,
"multiplier": 1.4,
"net_order": "shortest",
"capture_expanded": True,
"shuffle_nets": True,
"seed": 42,
@ -333,7 +340,10 @@ def run_example_07() -> ScenarioOutcome:
)
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()
results = pathfinder.route_all(iteration_callback=iteration_callback)
@ -345,27 +355,27 @@ def run_example_08() -> ScenarioOutcome:
bounds = (0, 0, 150, 150)
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"custom_bend": 2.0}
custom_model = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
standard_evaluator = _build_evaluator(bounds)
custom_evaluator = _build_evaluator(bounds)
custom_model = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)])
evaluator = _build_evaluator(bounds)
t0 = perf_counter()
results_std = _build_pathfinder(
standard_evaluator,
evaluator,
bounds=bounds,
nets=_net_specs(netlist, widths),
bend_radii=[10.0],
sbend_radii=[],
use_tiered_strategy=False,
max_iterations=1,
metrics=AStarMetrics(),
).route_all()
results_custom = _build_pathfinder(
custom_evaluator,
evaluator,
bounds=bounds,
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
bend_radii=[10.0],
bend_collision_type=custom_model,
sbend_radii=[],
max_iterations=1,
use_tiered_strategy=False,
metrics=AStarMetrics(),
).route_all()
@ -386,7 +396,7 @@ def run_example_09() -> ScenarioOutcome:
widths=widths,
obstacles=obstacles,
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()
results = pathfinder.route_all()
@ -397,7 +407,7 @@ def run_example_09() -> ScenarioOutcome:
SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
("example_01_simple_route", run_example_01),
("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_05_orientation_stress", run_example_05),
("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")
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:
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])
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)])
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_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:

View file

@ -40,6 +40,18 @@ def test_cost_calculation() -> None:
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:
# Test that KD-Tree based danger map works and uses cache
bounds = (0, 0, 1000, 1000)

View file

@ -13,28 +13,28 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
PERFORMANCE_REPEATS = 3
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 = {
"example_01_simple_route": 0.0035,
"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_05_orientation_stress": 0.5630,
"example_06_bend_collision_models": 5.2382,
"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,
}
EXPECTED_OUTCOMES = {
"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_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_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_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},
}

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