inire/inire/tests/test_example_regressions.py

256 lines
8.7 KiB
Python

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, 2, 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_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 = {"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(
_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(
_build_evaluator(bounds),
bounds=bounds,
nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}),
bend_radii=[10.0],
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["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
)