256 lines
8.7 KiB
Python
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
|
|
)
|