fix examples
This commit is contained in:
parent
bc218a416b
commit
e11132b51d
20 changed files with 406 additions and 101 deletions
1
DOCS.md
1
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
|
||||
|
|
|
|||
14
README.md
14
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)**.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from inire.utils.visualization import plot_routing_results
|
|||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 03: Locked Routes...")
|
||||
print("Running Example 03: Locked Paths...")
|
||||
|
||||
bounds = (0, -50, 100, 50)
|
||||
options = RoutingOptions(
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)...")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

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