diff --git a/DOCS.md b/DOCS.md index d458bda..2cfab9d 100644 --- a/DOCS.md +++ b/DOCS.md @@ -80,7 +80,9 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are | `bend_radii` | `(50.0, 100.0)` | Available radii for 90-degree bends. | | `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_collision_type` | `"arc"` | Bend collision/proxy model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or, for backward compatibility, a custom polygon. A legacy custom polygon here is treated as both the physical bend and its proxy unless overridden by the split fields below. | +| `bend_proxy_geometry` | `None` | Optional explicit bend proxy geometry. Use this when you want a custom search/collision envelope that differs from the routed bend shape. Supplying only a custom polygon proxy warns and keeps the physical bend as the standard arc. | +| `bend_physical_geometry` | `None` | Optional explicit bend physical geometry. Use `"arc"` or a custom polygon. If you set a custom physical polygon and do not set a proxy, the proxy defaults to the same 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. | @@ -161,6 +163,7 @@ Lower-level search and collision modules are semi-private implementation details - Increase `objective.bend_penalty` to discourage ladders of small bends. - Increase available `search.bend_radii` when larger turns are physically acceptable. +- Use `search.bend_physical_geometry` and `search.bend_proxy_geometry` together when you need a real custom bend shape plus a different conservative proxy. ### Visibility guidance diff --git a/examples/02_congestion_resolution.py b/examples/02_congestion_resolution.py index 9d003bc..f6b2751 100644 --- a/examples/02_congestion_resolution.py +++ b/examples/02_congestion_resolution.py @@ -22,8 +22,8 @@ def main() -> None: greedy_h_weight=1.5, ), objective=ObjectiveWeights( - bend_penalty=250.0, - sbend_penalty=500.0, + bend_penalty=50.0, + sbend_penalty=150.0, ), congestion=CongestionOptions(base_penalty=1000.0), ) diff --git a/examples/03_locked_paths.py b/examples/03_locked_paths.py index ed309a8..b00741f 100644 --- a/examples/03_locked_paths.py +++ b/examples/03_locked_paths.py @@ -1,4 +1,7 @@ -from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire import NetSpec, Port, RoutingOptions, RoutingProblem, SearchOptions +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router._stack import build_routing_stack from inire.utils.visualization import plot_routing_results @@ -6,31 +9,31 @@ def main() -> None: print("Running Example 03: Locked Paths...") bounds = (0, -50, 100, 50) - options = RoutingOptions( - search=SearchOptions(bend_radii=(10.0,)), - objective=ObjectiveWeights( - bend_penalty=250.0, - sbend_penalty=500.0, - ), - ) print("Routing initial net...") - results_a = route( - RoutingProblem( + stack = build_routing_stack( + problem=RoutingProblem( bounds=bounds, nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),), ), - options=options, - ).results_by_net + options=RoutingOptions(search=SearchOptions(bend_radii=(10.0,))), + ) + engine = stack.world + evaluator = stack.evaluator + results_a = stack.finder.route_all() print("Routing detour net around locked path...") - results_b = route( - RoutingProblem( - bounds=bounds, - nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),), - static_obstacles=results_a["netA"].locked_geometry, + for polygon in results_a["netA"].locked_geometry: + engine.add_static_obstacle(polygon) + results_b = PathFinder( + AStarContext( + evaluator, + RoutingProblem( + bounds=bounds, + nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),), + ), + RoutingOptions(search=SearchOptions(bend_radii=(10.0,))), ), - options=options, - ).results_by_net + ).route_all() results = {**results_a, **results_b} fig, ax = plot_routing_results(results, [], bounds) diff --git a/examples/06_bend_collision_models.png b/examples/06_bend_collision_models.png index fa2c49f..178d952 100644 Binary files a/examples/06_bend_collision_models.png and b/examples/06_bend_collision_models.png differ diff --git a/examples/06_bend_collision_models.py b/examples/06_bend_collision_models.py index 8c3c06a..7fe12aa 100644 --- a/examples/06_bend_collision_models.py +++ b/examples/06_bend_collision_models.py @@ -1,6 +1,7 @@ from shapely.geometry import Polygon from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry from inire.geometry.primitives import Port from inire.utils.visualization import plot_routing_results @@ -8,10 +9,12 @@ from inire.utils.visualization import plot_routing_results def _route_scenario( bounds: tuple[float, float, float, float], obstacles: list[Polygon], - bend_collision_type: str, netlist: dict[str, tuple[Port, Port]], widths: dict[str, float], *, + bend_collision_type: BendCollisionModel = "arc", + bend_proxy_geometry: BendCollisionModel | None = None, + bend_physical_geometry: BendPhysicalGeometry | None = None, bend_clip_margin: float | None = None, ) -> dict[str, RoutingResult]: problem = RoutingProblem( @@ -23,6 +26,8 @@ def _route_scenario( search=SearchOptions( bend_radii=(10.0,), bend_collision_type=bend_collision_type, + bend_proxy_geometry=bend_proxy_geometry, + bend_physical_geometry=bend_physical_geometry, bend_clip_margin=bend_clip_margin, ), objective=ObjectiveWeights( @@ -40,29 +45,30 @@ def main() -> None: bounds = (-20, -20, 170, 170) obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]) obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]) - obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]) + obs_custom = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]) + custom_bend = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) - obstacles = [obs_arc, obs_bbox, obs_clipped] + obstacles = [obs_arc, obs_bbox, obs_custom] netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))} netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))} - netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))} + netlist_custom = {"custom_geometry": (Port(10, 20, 0), Port(90, 40, 90))} print("Routing Scenario 1 (Arc)...") - res_arc = _route_scenario(bounds, obstacles, "arc", netlist_arc, {"arc_model": 2.0}) + res_arc = _route_scenario(bounds, obstacles, netlist_arc, {"arc_model": 2.0}, bend_collision_type="arc") 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( + res_bbox = _route_scenario(bounds, obstacles, netlist_bbox, {"bbox_model": 2.0}, bend_collision_type="bbox") + print("Routing Scenario 3 (Custom Manhattan Geometry With Matching Proxy)...") + res_custom = _route_scenario( bounds, obstacles, - "clipped_bbox", - netlist_clipped, - {"clipped_model": 2.0}, - bend_clip_margin=1.0, + netlist_custom, + {"custom_geometry": 2.0}, + bend_physical_geometry=custom_bend, + bend_proxy_geometry=custom_bend, ) - all_results = {**res_arc, **res_bbox, **res_clipped} - all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped} + all_results = {**res_arc, **res_bbox, **res_custom} + all_netlists = {**netlist_arc, **netlist_bbox, **netlist_custom} fig, _ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists) fig.savefig("examples/06_bend_collision_models.png") diff --git a/examples/08_custom_bend_geometry.png b/examples/08_custom_bend_geometry.png index 48c2e5c..7088343 100644 Binary files a/examples/08_custom_bend_geometry.png and b/examples/08_custom_bend_geometry.png differ diff --git a/examples/08_custom_bend_geometry.py b/examples/08_custom_bend_geometry.py index 25e715b..d24e087 100644 --- a/examples/08_custom_bend_geometry.py +++ b/examples/08_custom_bend_geometry.py @@ -1,65 +1,58 @@ -from shapely.geometry import Polygon +from shapely.geometry import Polygon, box -from inire import CongestionOptions, NetSpec, RoutingOptions, RoutingProblem, SearchOptions -from inire.geometry.collision import RoutingWorld +from inire import CongestionOptions, NetSpec, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry 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_session( + bounds: tuple[float, float, float, float], + net_id: str, + start: Port, + target: Port, + *, + bend_collision_type: BendCollisionModel = "arc", + bend_proxy_geometry: BendCollisionModel | None = None, + bend_physical_geometry: BendPhysicalGeometry | None = None, +) -> dict[str, object]: + 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, + bend_proxy_geometry=bend_proxy_geometry, + bend_physical_geometry=bend_physical_geometry, + sbend_radii=(), + ), + congestion=CongestionOptions(max_iterations=1, 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) + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) - print("Routing with standard arc...") - 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([(-10, -10), (10, -10), (10, 10), (-10, 10)]) - - 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() + print("Routing standard arc in its own session...") + results_std = _run_session(bounds, "standard_arc", start, target) + print("Routing custom geometry with a separate custom proxy in its own session...") + results_custom = _run_session( + bounds, + "custom_geometry_and_proxy", + start, + target, + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, + ) all_results = {**results_std, **results_custom} fig, _ax = plot_routing_results( @@ -67,8 +60,8 @@ def main() -> None: [], bounds, netlist={ - "custom_bend": (start, target), - "custom_model": (start, target), + "standard_arc": (start, target), + "custom_geometry_and_proxy": (start, target), }, ) fig.savefig("examples/08_custom_bend_geometry.png") diff --git a/examples/README.md b/examples/README.md index cfea579..a079c07 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,13 +14,14 @@ Demonstrates the Negotiated Congestion algorithm handling multiple intersecting ![Fan-Out Routing](07_large_scale_routing.png) -## 2. Custom Bend Geometry Models +## 2. Bend Geometry Models `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 model that clips the corners of the AABB to better fit the arc (Optimal performance). +* **Custom Manhattan Geometry**: A custom 90-degree bend polygon with the same width as the normal waveguide. -Example 08 also demonstrates a custom polygonal bend geometry. It uses a centered `20x20` box as a custom bend collision model. +Example 06 uses the Manhattan polygon as both the true routed bend geometry and the collision proxy. +Example 08 compares the standard arc against a run that uses a custom physical bend plus a separate custom proxy polygon, with each net routed in its own session. ![Custom Bend Geometry](08_custom_bend_geometry.png) diff --git a/inire/geometry/components.py b/inire/geometry/components.py index 714ef55..48af961 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -17,6 +17,7 @@ from .primitives import Port, rotation_matrix2 MoveKind = Literal["straight", "bend90", "sbend"] BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"] BendCollisionModel = BendCollisionModelName | Polygon +BendPhysicalGeometry = Literal["arc"] | Polygon def _normalize_length(value: float) -> float: @@ -281,6 +282,7 @@ class Bend90: direction: Literal["CW", "CCW"], sagitta: float = 0.01, collision_type: BendCollisionModel = "arc", + physical_geometry_type: BendPhysicalGeometry = "arc", clip_margin: float | None = None, dilation: float = 0.0, ) -> ComponentResult: @@ -310,16 +312,33 @@ class Bend90: mirror_y=(sign < 0), ) - physical_geometry = arc_polys - if dilation > 0: - dilated_physical_geometry = _get_arc_polygons( - (float(center_xy[0]), float(center_xy[1])), + if isinstance(physical_geometry_type, Polygon): + physical_geometry = _apply_collision_model( + arc_polys[0], + physical_geometry_type, radius, width, + (float(center_xy[0]), float(center_xy[1])), ts, - sagitta, - dilation=dilation, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), ) + uses_physical_custom_geometry = True + else: + physical_geometry = arc_polys + uses_physical_custom_geometry = False + if dilation > 0: + if uses_physical_custom_geometry: + dilated_physical_geometry = [poly.buffer(dilation) for poly in 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] ) @@ -349,6 +368,7 @@ class SBend: width: float, sagitta: float = 0.01, collision_type: BendCollisionModel = "arc", + physical_geometry_type: BendPhysicalGeometry = "arc", clip_margin: float | None = None, dilation: float = 0.0, ) -> ComponentResult: @@ -402,12 +422,41 @@ class SBend: )[0], ] - physical_geometry = actual_geometry - if dilation > 0: - 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], + if isinstance(physical_geometry_type, Polygon): + physical_geometry = [ + _apply_collision_model( + arc1, + physical_geometry_type, + radius, + width, + (float(c1_xy[0]), float(c1_xy[1])), + ts1, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), + )[0], + _apply_collision_model( + arc2, + physical_geometry_type, + radius, + width, + (float(c2_xy[0]), float(c2_xy[1])), + ts2, + rotation_deg=float(start_port.r), + mirror_y=(sign > 0), + )[0], ] + uses_physical_custom_geometry = True + else: + physical_geometry = actual_geometry + uses_physical_custom_geometry = False + if dilation > 0: + if uses_physical_custom_geometry: + dilated_physical_geometry = [poly.buffer(dilation) for poly in 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] ) diff --git a/inire/model.py b/inire/model.py index 5100899..f0607ae 100644 --- a/inire/model.py +++ b/inire/model.py @@ -1,15 +1,15 @@ from __future__ import annotations +import warnings from dataclasses import dataclass, field from typing import TYPE_CHECKING, Literal -from inire.geometry.components import BendCollisionModel +from shapely.geometry import Polygon + +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry from inire.seeds import PathSeed if TYPE_CHECKING: - from shapely.geometry import Polygon - - from inire.geometry.components import BendCollisionModel from inire.geometry.primitives import Port @@ -43,6 +43,8 @@ class SearchOptions: bend_radii: tuple[float, ...] = (50.0, 100.0) sbend_radii: tuple[float, ...] = (10.0,) bend_collision_type: BendCollisionModel = "arc" + bend_proxy_geometry: BendCollisionModel | None = None + bend_physical_geometry: BendPhysicalGeometry | None = None bend_clip_margin: float | None = None visibility_guidance: VisibilityGuidance = "tangent_corner" @@ -51,6 +53,36 @@ class SearchOptions: object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii)) if self.sbend_offsets is not None: object.__setattr__(self, "sbend_offsets", tuple(self.sbend_offsets)) + if self.bend_physical_geometry is None and isinstance(self.bend_proxy_geometry, Polygon): + warnings.warn( + "Custom bend proxy provided without bend_physical_geometry; routed bends will keep standard arc geometry.", + stacklevel=2, + ) + + +def resolve_bend_geometry( + search: SearchOptions, + *, + bend_collision_override: BendCollisionModel | None = None, +) -> tuple[BendCollisionModel, BendPhysicalGeometry]: + bend_physical_geometry = search.bend_physical_geometry + if bend_physical_geometry is None and isinstance(search.bend_collision_type, Polygon) and search.bend_proxy_geometry is None: + bend_physical_geometry = search.bend_collision_type + if bend_physical_geometry is None: + bend_physical_geometry = "arc" + + if bend_collision_override is not None: + bend_proxy_geometry = bend_collision_override + elif search.bend_proxy_geometry is not None: + bend_proxy_geometry = search.bend_proxy_geometry + elif isinstance(search.bend_collision_type, Polygon): + bend_proxy_geometry = search.bend_collision_type + elif bend_physical_geometry != "arc" and search.bend_collision_type == "arc": + bend_proxy_geometry = bend_physical_geometry + else: + bend_proxy_geometry = search.bend_collision_type + + return bend_proxy_geometry, bend_physical_geometry @dataclass(frozen=True, slots=True) diff --git a/inire/router/_astar_admission.py b/inire/router/_astar_admission.py index ff075cd..8152afb 100644 --- a/inire/router/_astar_admission.py +++ b/inire/router/_astar_admission.py @@ -33,6 +33,8 @@ def process_move( cp = parent.port coll_type = config.bend_collision_type coll_key = id(coll_type) if isinstance(coll_type, Polygon) else coll_type + physical_type = config.bend_physical_geometry + physical_key = id(physical_type) if isinstance(physical_type, Polygon) else physical_type self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0 abs_key = ( @@ -41,6 +43,7 @@ def process_move( params, net_width, coll_key, + physical_key, self_dilation, ) if abs_key in context.move_cache_abs: @@ -54,6 +57,7 @@ def process_move( params, net_width, coll_key, + physical_key, self_dilation, ) if rel_key in context.move_cache_rel: @@ -69,6 +73,7 @@ def process_move( net_width, params[1], collision_type=coll_type, + physical_geometry_type=config.bend_physical_geometry, clip_margin=config.bend_clip_margin, dilation=self_dilation, ) @@ -79,6 +84,7 @@ def process_move( params[1], net_width, collision_type=coll_type, + physical_geometry_type=config.bend_physical_geometry, clip_margin=config.bend_clip_margin, dilation=self_dilation, ) diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py index 6bf2b37..2796c73 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -3,8 +3,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -from inire.geometry.components import BendCollisionModel -from inire.model import RoutingOptions, RoutingProblem +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry +from inire.model import RoutingOptions, RoutingProblem, resolve_bend_geometry from inire.results import RouteMetrics from inire.router.visibility import VisibilityManager @@ -16,6 +16,7 @@ if TYPE_CHECKING: @dataclass(frozen=True, slots=True) class SearchRunConfig: bend_collision_type: BendCollisionModel + bend_physical_geometry: BendPhysicalGeometry bend_clip_margin: float | None node_limit: int return_partial: bool = False @@ -38,8 +39,13 @@ class SearchRunConfig: self_collision_check: bool = False, ) -> SearchRunConfig: search = options.search + bend_collision_type, bend_physical_geometry = resolve_bend_geometry( + search, + bend_collision_override=bend_collision_type, + ) return cls( - bend_collision_type=search.bend_collision_type if bend_collision_type is None else bend_collision_type, + bend_collision_type=bend_collision_type, + bend_physical_geometry=bend_physical_geometry, bend_clip_margin=search.bend_clip_margin, node_limit=search.node_limit if node_limit is None else node_limit, return_partial=return_partial, diff --git a/inire/router/_router.py b/inire/router/_router.py index 5aaf00c..81bc6ef 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -5,7 +5,7 @@ import time from dataclasses import dataclass from typing import TYPE_CHECKING -from inire.model import NetOrder, NetSpec +from inire.model import NetOrder, NetSpec, resolve_bend_geometry from inire.results import RoutingOutcome, RoutingReport, RoutingResult from inire.router._astar_types import AStarContext, AStarMetrics, SearchRunConfig from inire.router._search import route_astar @@ -173,7 +173,7 @@ class PathFinder: if iteration == 0 and state.initial_paths and net_id in state.initial_paths: path: Sequence[ComponentResult] | None = state.initial_paths[net_id] else: - coll_model = search.bend_collision_type + coll_model, _ = resolve_bend_geometry(search) skip_congestion = False if congestion.use_tiered_strategy and iteration == 0: skip_congestion = True diff --git a/inire/router/_seed_materialization.py b/inire/router/_seed_materialization.py index f370db6..25a76be 100644 --- a/inire/router/_seed_materialization.py +++ b/inire/router/_seed_materialization.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from inire.model import SearchOptions +from inire.model import SearchOptions, resolve_bend_geometry from inire.seeds import Bend90Seed, PathSeed, SBendSeed, StraightSeed if TYPE_CHECKING: @@ -23,7 +23,7 @@ def materialize_path_seed( path: list[ComponentResult] = [] current = start dilation = clearance / 2.0 - bend_collision_type = search.bend_collision_type + bend_collision_type, bend_physical_geometry = resolve_bend_geometry(search) bend_clip_margin = search.bend_clip_margin for segment in seed.segments: @@ -36,6 +36,7 @@ def materialize_path_seed( net_width, segment.direction, collision_type=bend_collision_type, + physical_geometry_type=bend_physical_geometry, clip_margin=bend_clip_margin, dilation=dilation, ) @@ -46,6 +47,7 @@ def materialize_path_seed( segment.radius, net_width, collision_type=bend_collision_type, + physical_geometry_type=bend_physical_geometry, clip_margin=bend_clip_margin, dilation=dilation, ) diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index 06619c4..2dd78ed 100644 --- a/inire/tests/example_scenarios.py +++ b/inire/tests/example_scenarios.py @@ -253,6 +253,7 @@ def run_example_06() -> ScenarioOutcome: box(40, 60, 60, 80), box(40, 10, 60, 30), ] + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) scenarios = [ ( _build_evaluator(bounds, obstacles=obstacles), @@ -268,12 +269,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}, + {"custom_geometry": (Port(10, 20, 0), Port(90, 40, 90))}, + {"custom_geometry": 2.0}, { "bend_radii": [10.0], - "bend_collision_type": "clipped_bbox", - "bend_clip_margin": 1.0, + "bend_physical_geometry": custom_physical, + "bend_proxy_geometry": custom_physical, "use_tiered_strategy": False, }, ), @@ -353,27 +354,29 @@ def run_example_07() -> ScenarioOutcome: 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([(-10, -10), (10, -10), (10, 10), (-10, 10)]) - evaluator = _build_evaluator(bounds) + netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))} + widths = {"standard_arc": 2.0} + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) t0 = perf_counter() results_std = _build_pathfinder( - evaluator, + _build_evaluator(bounds), bounds=bounds, nets=_net_specs(netlist, widths), bend_radii=[10.0], sbend_radii=[], max_iterations=1, + use_tiered_strategy=False, metrics=AStarMetrics(), ).route_all() results_custom = _build_pathfinder( - evaluator, + _build_evaluator(bounds), bounds=bounds, - nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}), + nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}), bend_radii=[10.0], - bend_collision_type=custom_model, + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, sbend_radii=[], max_iterations=1, use_tiered_strategy=False, diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py index 2708a56..3c14326 100644 --- a/inire/tests/test_components.py +++ b/inire/tests/test_components.py @@ -137,13 +137,29 @@ def test_custom_bend_collision_polygon_uses_local_transform() -> None: assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6 -def test_custom_bend_collision_polygon_only_overrides_search_geometry() -> None: +def test_custom_bend_collision_polygon_is_true_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) + result = Bend90.generate( + Port(0, 0, 0), + 10.0, + 2.0, + direction="CCW", + collision_type=custom_poly, + physical_geometry_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 + + +def test_custom_bend_collision_polygon_can_differ_from_physical_geometry() -> None: + custom_proxy = Polygon([(0, -11), (11, -11), (11, 0), (0, 0)]) + result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_proxy, dilation=1.0) + + assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area > 1e-6 assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area > 1e-6 diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py index 7f8517b..80d56aa 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -22,7 +22,7 @@ BASELINE_SECONDS = { "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": 4.2111, + "example_08_custom_bend_geometry": 0.9848, "example_09_unroutable_best_effort": 0.0056, } @@ -34,7 +34,7 @@ EXPECTED_OUTCOMES = { "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": 1, "reached_targets": 2}, + "example_08_custom_bend_geometry": {"total_results": 2, "valid_results": 2, "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 index 1a56cd2..e78fcab 100644 --- a/inire/tests/test_example_regressions.py +++ b/inire/tests/test_example_regressions.py @@ -25,7 +25,7 @@ EXPECTED_OUTCOMES = { "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_08_custom_bend_geometry": (2, 2, 2), "example_09_unroutable_best_effort": (1, 0, 0), } @@ -150,35 +150,107 @@ def test_example_07_reduced_bottleneck_uses_adaptive_greedy_callback() -> None: assert all(result.reached_target for result in results.values()) -def test_example_08_custom_box_restores_legacy_collision_outcome() -> None: +def test_example_06_custom_geometry_can_be_true_physical_geometry() -> 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)]), + ) + custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + result = route( + RoutingProblem( + bounds=bounds, + nets=(NetSpec("custom_geometry", Port(10, 20, 0), Port(90, 40, 90), width=2.0),), + static_obstacles=obstacles, + ), + options=RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_physical_geometry=custom_poly, + bend_proxy_geometry=custom_poly, + ), + objective=ObjectiveWeights(bend_penalty=50.0, sbend_penalty=150.0), + congestion=CongestionOptions(use_tiered_strategy=False), + ), + ).results_by_net["custom_geometry"] + + assert result.is_valid + bends = [component for component in result.path if component.move_type == "bend90"] + assert bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area < 1e-6 + for component in bends + ) + + +def test_custom_proxy_without_physical_geometry_warns_and_keeps_arc_geometry() -> None: + custom_proxy = Polygon([(0, -11), (11, -11), (11, 0), (0, 0)]) + + with pytest.warns(UserWarning, match="Custom bend proxy provided without bend_physical_geometry"): + search = SearchOptions( + bend_radii=(10.0,), + sbend_radii=(), + bend_proxy_geometry=custom_proxy, + ) + + problem = RoutingProblem( + bounds=(0, 0, 150, 150), + nets=(NetSpec("proxy_only", Port(20, 20, 0), Port(100, 100, 90), width=2.0),), + ) + result = route( + problem, + options=RoutingOptions( + search=search, + congestion=CongestionOptions(max_iterations=1, use_tiered_strategy=False), + ), + ).results_by_net["proxy_only"] + + bends = [component for component in result.path if component.move_type == "bend90"] + assert bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area > 1e-6 + for component in bends + ) + + +def test_example_08_custom_geometry_runs_in_separate_sessions() -> 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) + netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))} + widths = {"standard_arc": 2.0} + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) standard = _build_pathfinder( - evaluator, + _build_evaluator(bounds), bounds=bounds, nets=_net_specs(netlist, widths), bend_radii=[10.0], sbend_radii=[], max_iterations=1, + use_tiered_strategy=False, metrics=AStarMetrics(), ).route_all() custom = _build_pathfinder( - evaluator, + _build_evaluator(bounds), bounds=bounds, - nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}), + nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}), bend_radii=[10.0], - bend_collision_type=Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)]), + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, 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 + assert standard["standard_arc"].is_valid + assert standard["standard_arc"].reached_target + assert custom["custom_geometry_and_proxy"].is_valid + assert custom["custom_geometry_and_proxy"].reached_target + custom_bends = [component for component in custom["custom_geometry_and_proxy"].path if component.move_type == "bend90"] + assert custom_bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area > 1e-6 + for component in custom_bends + )