From e11132b51d7f4dc871d448531eea0965abf197b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Mar 2026 21:22:20 -0700 Subject: [PATCH] fix examples --- DOCS.md | 1 + README.md | 14 ++ examples/03_locked_paths.py | 2 +- examples/06_bend_collision_models.png | Bin 85921 -> 87436 bytes examples/06_bend_collision_models.py | 12 +- examples/07_large_scale_routing.py | 37 +++-- examples/08_custom_bend_geometry.py | 77 ++++++---- examples/09_unroutable_best_effort.py | 2 +- examples/README.md | 4 +- inire/geometry/components.py | 86 ++++++----- inire/model.py | 1 + inire/router/_astar_admission.py | 2 + inire/router/_astar_types.py | 2 + inire/router/_seed_materialization.py | 3 + inire/router/cost.py | 8 ++ inire/tests/example_scenarios.py | 30 ++-- inire/tests/test_components.py | 20 ++- inire/tests/test_cost.py | 12 ++ inire/tests/test_example_performance.py | 10 +- inire/tests/test_example_regressions.py | 184 ++++++++++++++++++++++++ 20 files changed, 406 insertions(+), 101 deletions(-) create mode 100644 inire/tests/test_example_regressions.py diff --git a/DOCS.md b/DOCS.md index 338ded2..d458bda 100644 --- a/DOCS.md +++ b/DOCS.md @@ -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 diff --git a/README.md b/README.md index b93ce69..a66f699 100644 --- a/README.md +++ b/README.md @@ -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)**. diff --git a/examples/03_locked_paths.py b/examples/03_locked_paths.py index 0f60fbb..ed309a8 100644 --- a/examples/03_locked_paths.py +++ b/examples/03_locked_paths.py @@ -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( diff --git a/examples/06_bend_collision_models.png b/examples/06_bend_collision_models.png index 1a4401a5ee2bd9ed68101d62ea423773d0feb332..fa2c49f5e39b8140b68fe766c0e5a144a19c8fc1 100644 GIT binary patch delta 52098 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

l#Yjy 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 W<>A<-PSn>L>_5COd+*X8L;nrw?%i$x diff --git a/examples/06_bend_collision_models.py b/examples/06_bend_collision_models.py index 0df49ea..8c3c06a 100644 --- a/examples/06_bend_collision_models.py +++ b/examples/06_bend_collision_models.py @@ -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} diff --git a/examples/07_large_scale_routing.py b/examples/07_large_scale_routing.py index ea69812..92098fb 100644 --- a/examples/07_large_scale_routing.py +++ b/examples/07_large_scale_routing.py @@ -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") diff --git a/examples/08_custom_bend_geometry.py b/examples/08_custom_bend_geometry.py index 5acd82e..25e715b 100644 --- a/examples/08_custom_bend_geometry.py +++ b/examples/08_custom_bend_geometry.py @@ -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( diff --git a/examples/09_unroutable_best_effort.py b/examples/09_unroutable_best_effort.py index 0ff297f..1aeb152 100644 --- a/examples/09_unroutable_best_effort.py +++ b/examples/09_unroutable_best_effort.py @@ -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)...") diff --git a/examples/README.md b/examples/README.md index 94e15f6..cfea579 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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) diff --git a/inire/geometry/components.py b/inire/geometry/components.py index 693f16e..714ef55 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -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 diff --git a/inire/model.py b/inire/model.py index 20d71fe..5100899 100644 --- a/inire/model.py +++ b/inire/model.py @@ -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: diff --git a/inire/router/_astar_admission.py b/inire/router/_astar_admission.py index 594a970..ff075cd 100644 --- a/inire/router/_astar_admission.py +++ b/inire/router/_astar_admission.py @@ -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: diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py index f63fe43..6bf2b37 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -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, diff --git a/inire/router/_seed_materialization.py b/inire/router/_seed_materialization.py index b63cda9..f370db6 100644 --- a/inire/router/_seed_materialization.py +++ b/inire/router/_seed_materialization.py @@ -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: diff --git a/inire/router/cost.py b/inire/router/cost.py index 73fa121..c4b62c3 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -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 diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index 2e74ec1..06619c4 100644 --- a/inire/tests/example_scenarios.py +++ b/inire/tests/example_scenarios.py @@ -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), diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py index 9ac3c98..2708a56 100644 --- a/inire/tests/test_components.py +++ b/inire/tests/test_components.py @@ -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: diff --git a/inire/tests/test_cost.py b/inire/tests/test_cost.py index e6f73c4..6eef3bc 100644 --- a/inire/tests/test_cost.py +++ b/inire/tests/test_cost.py @@ -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) diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py index de9e9f2..7f8517b 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -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}, } diff --git a/inire/tests/test_example_regressions.py b/inire/tests/test_example_regressions.py new file mode 100644 index 0000000..1a56cd2 --- /dev/null +++ b/inire/tests/test_example_regressions.py @@ -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