example fixes and improvements

This commit is contained in:
Jan Petykiewicz 2026-03-30 23:40:29 -07:00
commit e2c91076f7
18 changed files with 336 additions and 144 deletions

View file

@ -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. | | `bend_radii` | `(50.0, 100.0)` | Available radii for 90-degree bends. |
| `sbend_radii` | `(10.0,)` | Available radii for S-bends. | | `sbend_radii` | `(10.0,)` | Available radii for S-bends. |
| `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. | | `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. |
| `bend_collision_type` | `"arc"` | Bend collision model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or a custom polygon. | | `bend_collision_type` | `"arc"` | Bend collision/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. | | `bend_clip_margin` | `None` | Optional legacy shrink margin for `"clipped_bbox"`. Leave `None` for the default 8-point proxy. |
| `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. | | `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. |
@ -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 `objective.bend_penalty` to discourage ladders of small bends.
- Increase available `search.bend_radii` when larger turns are physically acceptable. - 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 ### Visibility guidance

View file

@ -22,8 +22,8 @@ def main() -> None:
greedy_h_weight=1.5, greedy_h_weight=1.5,
), ),
objective=ObjectiveWeights( objective=ObjectiveWeights(
bend_penalty=250.0, bend_penalty=50.0,
sbend_penalty=500.0, sbend_penalty=150.0,
), ),
congestion=CongestionOptions(base_penalty=1000.0), congestion=CongestionOptions(base_penalty=1000.0),
) )

View file

@ -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 from inire.utils.visualization import plot_routing_results
@ -6,31 +9,31 @@ def main() -> None:
print("Running Example 03: Locked Paths...") print("Running Example 03: Locked Paths...")
bounds = (0, -50, 100, 50) 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...") print("Routing initial net...")
results_a = route( stack = build_routing_stack(
RoutingProblem( problem=RoutingProblem(
bounds=bounds, bounds=bounds,
nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),), nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),),
), ),
options=options, options=RoutingOptions(search=SearchOptions(bend_radii=(10.0,))),
).results_by_net )
engine = stack.world
evaluator = stack.evaluator
results_a = stack.finder.route_all()
print("Routing detour net around locked path...") print("Routing detour net around locked path...")
results_b = route( for polygon in results_a["netA"].locked_geometry:
RoutingProblem( engine.add_static_obstacle(polygon)
bounds=bounds, results_b = PathFinder(
nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),), AStarContext(
static_obstacles=results_a["netA"].locked_geometry, 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, ).route_all()
).results_by_net
results = {**results_a, **results_b} results = {**results_a, **results_b}
fig, ax = plot_routing_results(results, [], bounds) fig, ax = plot_routing_results(results, [], bounds)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

View file

@ -1,6 +1,7 @@
from shapely.geometry import Polygon from shapely.geometry import Polygon
from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route
from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results
@ -8,10 +9,12 @@ from inire.utils.visualization import plot_routing_results
def _route_scenario( def _route_scenario(
bounds: tuple[float, float, float, float], bounds: tuple[float, float, float, float],
obstacles: list[Polygon], obstacles: list[Polygon],
bend_collision_type: str,
netlist: dict[str, tuple[Port, Port]], netlist: dict[str, tuple[Port, Port]],
widths: dict[str, float], widths: dict[str, float],
*, *,
bend_collision_type: BendCollisionModel = "arc",
bend_proxy_geometry: BendCollisionModel | None = None,
bend_physical_geometry: BendPhysicalGeometry | None = None,
bend_clip_margin: float | None = None, bend_clip_margin: float | None = None,
) -> dict[str, RoutingResult]: ) -> dict[str, RoutingResult]:
problem = RoutingProblem( problem = RoutingProblem(
@ -23,6 +26,8 @@ def _route_scenario(
search=SearchOptions( search=SearchOptions(
bend_radii=(10.0,), bend_radii=(10.0,),
bend_collision_type=bend_collision_type, bend_collision_type=bend_collision_type,
bend_proxy_geometry=bend_proxy_geometry,
bend_physical_geometry=bend_physical_geometry,
bend_clip_margin=bend_clip_margin, bend_clip_margin=bend_clip_margin,
), ),
objective=ObjectiveWeights( objective=ObjectiveWeights(
@ -40,29 +45,30 @@ def main() -> None:
bounds = (-20, -20, 170, 170) bounds = (-20, -20, 170, 170)
obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]) obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)])
obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]) 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_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 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)...") 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)...") print("Routing Scenario 2 (BBox)...")
res_bbox = _route_scenario(bounds, obstacles, "bbox", netlist_bbox, {"bbox_model": 2.0}) res_bbox = _route_scenario(bounds, obstacles, netlist_bbox, {"bbox_model": 2.0}, bend_collision_type="bbox")
print("Routing Scenario 3 (Clipped BBox)...") print("Routing Scenario 3 (Custom Manhattan Geometry With Matching Proxy)...")
res_clipped = _route_scenario( res_custom = _route_scenario(
bounds, bounds,
obstacles, obstacles,
"clipped_bbox", netlist_custom,
netlist_clipped, {"custom_geometry": 2.0},
{"clipped_model": 2.0}, bend_physical_geometry=custom_bend,
bend_clip_margin=1.0, bend_proxy_geometry=custom_bend,
) )
all_results = {**res_arc, **res_bbox, **res_clipped} all_results = {**res_arc, **res_bbox, **res_custom}
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped} all_netlists = {**netlist_arc, **netlist_bbox, **netlist_custom}
fig, _ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists) fig, _ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists)
fig.savefig("examples/06_bend_collision_models.png") fig.savefig("examples/06_bend_collision_models.png")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

View file

@ -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 import CongestionOptions, NetSpec, RoutingOptions, RoutingProblem, SearchOptions, route
from inire.geometry.collision import RoutingWorld from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router._astar_types import AStarContext, AStarMetrics
from inire.router._router import PathFinder
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results
def _run_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: def main() -> None:
print("Running Example 08: Custom Bend Geometry...") print("Running Example 08: Custom Bend Geometry...")
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
engine = RoutingWorld(clearance=2.0)
danger_map = DangerMap(bounds=bounds)
danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
metrics = AStarMetrics()
start = Port(20, 20, 0) start = Port(20, 20, 0)
target = Port(100, 100, 90) target = Port(100, 100, 90)
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...") print("Routing standard arc in its own session...")
results_std = PathFinder( results_std = _run_session(bounds, "standard_arc", start, target)
AStarContext( print("Routing custom geometry with a separate custom proxy in its own session...")
evaluator, results_custom = _run_session(
RoutingProblem( bounds,
bounds=bounds, "custom_geometry_and_proxy",
nets=(NetSpec("custom_bend", start, target, width=2.0),), start,
), target,
RoutingOptions( bend_physical_geometry=custom_physical,
search=SearchOptions(bend_radii=(10.0,), sbend_radii=()), bend_proxy_geometry=custom_proxy,
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()
all_results = {**results_std, **results_custom} all_results = {**results_std, **results_custom}
fig, _ax = plot_routing_results( fig, _ax = plot_routing_results(
@ -67,8 +60,8 @@ def main() -> None:
[], [],
bounds, bounds,
netlist={ netlist={
"custom_bend": (start, target), "standard_arc": (start, target),
"custom_model": (start, target), "custom_geometry_and_proxy": (start, target),
}, },
) )
fig.savefig("examples/08_custom_bend_geometry.png") fig.savefig("examples/08_custom_bend_geometry.png")

View file

@ -14,13 +14,14 @@ Demonstrates the Negotiated Congestion algorithm handling multiple intersecting
![Fan-Out Routing](07_large_scale_routing.png) ![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: `inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy:
* **Arc**: High-fidelity geometry (Highest accuracy). * **Arc**: High-fidelity geometry (Highest accuracy).
* **BBox**: Simple axis-aligned bounding box (Fastest search). * **BBox**: Simple axis-aligned bounding box (Fastest search).
* **Clipped BBox**: A balanced 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) ![Custom Bend Geometry](08_custom_bend_geometry.png)

View file

@ -17,6 +17,7 @@ from .primitives import Port, rotation_matrix2
MoveKind = Literal["straight", "bend90", "sbend"] MoveKind = Literal["straight", "bend90", "sbend"]
BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"] BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"]
BendCollisionModel = BendCollisionModelName | Polygon BendCollisionModel = BendCollisionModelName | Polygon
BendPhysicalGeometry = Literal["arc"] | Polygon
def _normalize_length(value: float) -> float: def _normalize_length(value: float) -> float:
@ -281,6 +282,7 @@ class Bend90:
direction: Literal["CW", "CCW"], direction: Literal["CW", "CCW"],
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc", collision_type: BendCollisionModel = "arc",
physical_geometry_type: BendPhysicalGeometry = "arc",
clip_margin: float | None = None, clip_margin: float | None = None,
dilation: float = 0.0, dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
@ -310,16 +312,33 @@ class Bend90:
mirror_y=(sign < 0), mirror_y=(sign < 0),
) )
physical_geometry = arc_polys if isinstance(physical_geometry_type, Polygon):
if dilation > 0: physical_geometry = _apply_collision_model(
dilated_physical_geometry = _get_arc_polygons( arc_polys[0],
(float(center_xy[0]), float(center_xy[1])), physical_geometry_type,
radius, radius,
width, width,
(float(center_xy[0]), float(center_xy[1])),
ts, ts,
sagitta, rotation_deg=float(start_port.r),
dilation=dilation, 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_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys] dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
) )
@ -349,6 +368,7 @@ class SBend:
width: float, width: float,
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: BendCollisionModel = "arc", collision_type: BendCollisionModel = "arc",
physical_geometry_type: BendPhysicalGeometry = "arc",
clip_margin: float | None = None, clip_margin: float | None = None,
dilation: float = 0.0, dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
@ -402,12 +422,41 @@ class SBend:
)[0], )[0],
] ]
physical_geometry = actual_geometry if isinstance(physical_geometry_type, Polygon):
if dilation > 0: physical_geometry = [
dilated_physical_geometry = [ _apply_collision_model(
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0], arc1,
_get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0], 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_collision_geometry = (
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry] dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry]
) )

View file

@ -1,15 +1,15 @@
from __future__ import annotations from __future__ import annotations
import warnings
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Literal 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 from inire.seeds import PathSeed
if TYPE_CHECKING: if TYPE_CHECKING:
from shapely.geometry import Polygon
from inire.geometry.components import BendCollisionModel
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
@ -43,6 +43,8 @@ class SearchOptions:
bend_radii: tuple[float, ...] = (50.0, 100.0) bend_radii: tuple[float, ...] = (50.0, 100.0)
sbend_radii: tuple[float, ...] = (10.0,) sbend_radii: tuple[float, ...] = (10.0,)
bend_collision_type: BendCollisionModel = "arc" bend_collision_type: BendCollisionModel = "arc"
bend_proxy_geometry: BendCollisionModel | None = None
bend_physical_geometry: BendPhysicalGeometry | None = None
bend_clip_margin: float | None = None bend_clip_margin: float | None = None
visibility_guidance: VisibilityGuidance = "tangent_corner" visibility_guidance: VisibilityGuidance = "tangent_corner"
@ -51,6 +53,36 @@ class SearchOptions:
object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii)) object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii))
if self.sbend_offsets is not None: if self.sbend_offsets is not None:
object.__setattr__(self, "sbend_offsets", tuple(self.sbend_offsets)) 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) @dataclass(frozen=True, slots=True)

View file

@ -33,6 +33,8 @@ def process_move(
cp = parent.port cp = parent.port
coll_type = config.bend_collision_type coll_type = config.bend_collision_type
coll_key = id(coll_type) if isinstance(coll_type, Polygon) else coll_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 self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0
abs_key = ( abs_key = (
@ -41,6 +43,7 @@ def process_move(
params, params,
net_width, net_width,
coll_key, coll_key,
physical_key,
self_dilation, self_dilation,
) )
if abs_key in context.move_cache_abs: if abs_key in context.move_cache_abs:
@ -54,6 +57,7 @@ def process_move(
params, params,
net_width, net_width,
coll_key, coll_key,
physical_key,
self_dilation, self_dilation,
) )
if rel_key in context.move_cache_rel: if rel_key in context.move_cache_rel:
@ -69,6 +73,7 @@ def process_move(
net_width, net_width,
params[1], params[1],
collision_type=coll_type, collision_type=coll_type,
physical_geometry_type=config.bend_physical_geometry,
clip_margin=config.bend_clip_margin, clip_margin=config.bend_clip_margin,
dilation=self_dilation, dilation=self_dilation,
) )
@ -79,6 +84,7 @@ def process_move(
params[1], params[1],
net_width, net_width,
collision_type=coll_type, collision_type=coll_type,
physical_geometry_type=config.bend_physical_geometry,
clip_margin=config.bend_clip_margin, clip_margin=config.bend_clip_margin,
dilation=self_dilation, dilation=self_dilation,
) )

View file

@ -3,8 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from inire.geometry.components import BendCollisionModel from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.model import RoutingOptions, RoutingProblem from inire.model import RoutingOptions, RoutingProblem, resolve_bend_geometry
from inire.results import RouteMetrics from inire.results import RouteMetrics
from inire.router.visibility import VisibilityManager from inire.router.visibility import VisibilityManager
@ -16,6 +16,7 @@ if TYPE_CHECKING:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class SearchRunConfig: class SearchRunConfig:
bend_collision_type: BendCollisionModel bend_collision_type: BendCollisionModel
bend_physical_geometry: BendPhysicalGeometry
bend_clip_margin: float | None bend_clip_margin: float | None
node_limit: int node_limit: int
return_partial: bool = False return_partial: bool = False
@ -38,8 +39,13 @@ class SearchRunConfig:
self_collision_check: bool = False, self_collision_check: bool = False,
) -> SearchRunConfig: ) -> SearchRunConfig:
search = options.search search = options.search
bend_collision_type, bend_physical_geometry = resolve_bend_geometry(
search,
bend_collision_override=bend_collision_type,
)
return cls( 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, bend_clip_margin=search.bend_clip_margin,
node_limit=search.node_limit if node_limit is None else node_limit, node_limit=search.node_limit if node_limit is None else node_limit,
return_partial=return_partial, return_partial=return_partial,

View file

@ -5,7 +5,7 @@ import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING 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.results import RoutingOutcome, RoutingReport, RoutingResult
from inire.router._astar_types import AStarContext, AStarMetrics, SearchRunConfig from inire.router._astar_types import AStarContext, AStarMetrics, SearchRunConfig
from inire.router._search import route_astar 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: if iteration == 0 and state.initial_paths and net_id in state.initial_paths:
path: Sequence[ComponentResult] | None = state.initial_paths[net_id] path: Sequence[ComponentResult] | None = state.initial_paths[net_id]
else: else:
coll_model = search.bend_collision_type coll_model, _ = resolve_bend_geometry(search)
skip_congestion = False skip_congestion = False
if congestion.use_tiered_strategy and iteration == 0: if congestion.use_tiered_strategy and iteration == 0:
skip_congestion = True skip_congestion = True

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING 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 from inire.seeds import Bend90Seed, PathSeed, SBendSeed, StraightSeed
if TYPE_CHECKING: if TYPE_CHECKING:
@ -23,7 +23,7 @@ def materialize_path_seed(
path: list[ComponentResult] = [] path: list[ComponentResult] = []
current = start current = start
dilation = clearance / 2.0 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 bend_clip_margin = search.bend_clip_margin
for segment in seed.segments: for segment in seed.segments:
@ -36,6 +36,7 @@ def materialize_path_seed(
net_width, net_width,
segment.direction, segment.direction,
collision_type=bend_collision_type, collision_type=bend_collision_type,
physical_geometry_type=bend_physical_geometry,
clip_margin=bend_clip_margin, clip_margin=bend_clip_margin,
dilation=dilation, dilation=dilation,
) )
@ -46,6 +47,7 @@ def materialize_path_seed(
segment.radius, segment.radius,
net_width, net_width,
collision_type=bend_collision_type, collision_type=bend_collision_type,
physical_geometry_type=bend_physical_geometry,
clip_margin=bend_clip_margin, clip_margin=bend_clip_margin,
dilation=dilation, dilation=dilation,
) )

View file

@ -253,6 +253,7 @@ def run_example_06() -> ScenarioOutcome:
box(40, 60, 60, 80), box(40, 60, 60, 80),
box(40, 10, 60, 30), box(40, 10, 60, 30),
] ]
custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
scenarios = [ scenarios = [
( (
_build_evaluator(bounds, obstacles=obstacles), _build_evaluator(bounds, obstacles=obstacles),
@ -268,12 +269,12 @@ def run_example_06() -> ScenarioOutcome:
), ),
( (
_build_evaluator(bounds, obstacles=obstacles), _build_evaluator(bounds, obstacles=obstacles),
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}, {"custom_geometry": (Port(10, 20, 0), Port(90, 40, 90))},
{"clipped_model": 2.0}, {"custom_geometry": 2.0},
{ {
"bend_radii": [10.0], "bend_radii": [10.0],
"bend_collision_type": "clipped_bbox", "bend_physical_geometry": custom_physical,
"bend_clip_margin": 1.0, "bend_proxy_geometry": custom_physical,
"use_tiered_strategy": False, "use_tiered_strategy": False,
}, },
), ),
@ -353,27 +354,29 @@ def run_example_07() -> ScenarioOutcome:
def run_example_08() -> ScenarioOutcome: def run_example_08() -> ScenarioOutcome:
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))} netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"custom_bend": 2.0} widths = {"standard_arc": 2.0}
custom_model = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)]) custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
evaluator = _build_evaluator(bounds) custom_proxy = box(0, -11, 11, 0)
t0 = perf_counter() t0 = perf_counter()
results_std = _build_pathfinder( results_std = _build_pathfinder(
evaluator, _build_evaluator(bounds),
bounds=bounds, bounds=bounds,
nets=_net_specs(netlist, widths), nets=_net_specs(netlist, widths),
bend_radii=[10.0], bend_radii=[10.0],
sbend_radii=[], sbend_radii=[],
max_iterations=1, max_iterations=1,
use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() ).route_all()
results_custom = _build_pathfinder( results_custom = _build_pathfinder(
evaluator, _build_evaluator(bounds),
bounds=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_radii=[10.0],
bend_collision_type=custom_model, bend_physical_geometry=custom_physical,
bend_proxy_geometry=custom_proxy,
sbend_radii=[], sbend_radii=[],
max_iterations=1, max_iterations=1,
use_tiered_strategy=False, use_tiered_strategy=False,

View file

@ -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 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)]) 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_collision_geometry is not None
assert result.dilated_physical_geometry is not None assert result.dilated_physical_geometry is not None
assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area < 1e-6
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 assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area > 1e-6

View file

@ -22,7 +22,7 @@ BASELINE_SECONDS = {
"example_05_orientation_stress": 0.5630, "example_05_orientation_stress": 0.5630,
"example_06_bend_collision_models": 5.2382, "example_06_bend_collision_models": 5.2382,
"example_07_large_scale_routing": 1.2081, "example_07_large_scale_routing": 1.2081,
"example_08_custom_bend_geometry": 4.2111, "example_08_custom_bend_geometry": 0.9848,
"example_09_unroutable_best_effort": 0.0056, "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_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
"example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, "example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3},
"example_07_large_scale_routing": {"total_results": 10, "valid_results": 10, "reached_targets": 10}, "example_07_large_scale_routing": {"total_results": 10, "valid_results": 10, "reached_targets": 10},
"example_08_custom_bend_geometry": {"total_results": 2, "valid_results": 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}, "example_09_unroutable_best_effort": {"total_results": 1, "valid_results": 0, "reached_targets": 0},
} }

View file

@ -25,7 +25,7 @@ EXPECTED_OUTCOMES = {
"example_05_orientation_stress": (3, 3, 3), "example_05_orientation_stress": (3, 3, 3),
"example_06_bend_collision_models": (3, 3, 3), "example_06_bend_collision_models": (3, 3, 3),
"example_07_large_scale_routing": (10, 10, 10), "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), "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()) 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) bounds = (0, 0, 150, 150)
netlist = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))} netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"custom_bend": 2.0} widths = {"standard_arc": 2.0}
evaluator = _build_evaluator(bounds) 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( standard = _build_pathfinder(
evaluator, _build_evaluator(bounds),
bounds=bounds, bounds=bounds,
nets=_net_specs(netlist, widths), nets=_net_specs(netlist, widths),
bend_radii=[10.0], bend_radii=[10.0],
sbend_radii=[], sbend_radii=[],
max_iterations=1, max_iterations=1,
use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() ).route_all()
custom = _build_pathfinder( custom = _build_pathfinder(
evaluator, _build_evaluator(bounds),
bounds=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_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=[], sbend_radii=[],
max_iterations=1, max_iterations=1,
use_tiered_strategy=False, use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() ).route_all()
assert standard["custom_bend"].is_valid assert standard["standard_arc"].is_valid
assert standard["custom_bend"].reached_target assert standard["standard_arc"].reached_target
assert not custom["custom_model"].is_valid assert custom["custom_geometry_and_proxy"].is_valid
assert custom["custom_model"].reached_target assert custom["custom_geometry_and_proxy"].reached_target
assert custom["custom_model"].collisions == 2 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
)