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_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 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. |
|
| `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. |
|
||||||
|
|
||||||
## 3. Objective Weights
|
## 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
|
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
|
## Documentation
|
||||||
|
|
||||||
Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**.
|
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:
|
def main() -> None:
|
||||||
print("Running Example 03: Locked Routes...")
|
print("Running Example 03: Locked Paths...")
|
||||||
|
|
||||||
bounds = (0, -50, 100, 50)
|
bounds = (0, -50, 100, 50)
|
||||||
options = RoutingOptions(
|
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,
|
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_clip_margin: float | None = None,
|
||||||
) -> dict[str, RoutingResult]:
|
) -> dict[str, RoutingResult]:
|
||||||
problem = RoutingProblem(
|
problem = RoutingProblem(
|
||||||
bounds=bounds,
|
bounds=bounds,
|
||||||
|
|
@ -21,6 +23,7 @@ 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_clip_margin=bend_clip_margin,
|
||||||
),
|
),
|
||||||
objective=ObjectiveWeights(
|
objective=ObjectiveWeights(
|
||||||
bend_penalty=50.0,
|
bend_penalty=50.0,
|
||||||
|
|
@ -49,7 +52,14 @@ def main() -> None:
|
||||||
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, "bbox", netlist_bbox, {"bbox_model": 2.0})
|
||||||
print("Routing Scenario 3 (Clipped BBox)...")
|
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_results = {**res_arc, **res_bbox, **res_clipped}
|
||||||
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
|
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,12 @@ import time
|
||||||
from shapely.geometry import box
|
from shapely.geometry import box
|
||||||
|
|
||||||
from inire import (
|
from inire import (
|
||||||
CongestionOptions,
|
|
||||||
DiagnosticsOptions,
|
|
||||||
NetSpec,
|
NetSpec,
|
||||||
ObjectiveWeights,
|
|
||||||
Port,
|
Port,
|
||||||
RoutingOptions,
|
|
||||||
RoutingProblem,
|
RoutingProblem,
|
||||||
RoutingResult,
|
RoutingResult,
|
||||||
SearchOptions,
|
|
||||||
route,
|
|
||||||
)
|
)
|
||||||
|
from inire.router._stack import build_routing_stack
|
||||||
from inire.utils.visualization import plot_expanded_nodes, plot_routing_results
|
from inire.utils.visualization import plot_expanded_nodes, plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -45,12 +40,15 @@ def main() -> None:
|
||||||
static_obstacles=tuple(obstacles),
|
static_obstacles=tuple(obstacles),
|
||||||
clearance=6.0,
|
clearance=6.0,
|
||||||
)
|
)
|
||||||
|
from inire import CongestionOptions, DiagnosticsOptions, ObjectiveWeights, RoutingOptions, SearchOptions
|
||||||
|
|
||||||
options = RoutingOptions(
|
options = RoutingOptions(
|
||||||
search=SearchOptions(
|
search=SearchOptions(
|
||||||
node_limit=2_000_000,
|
node_limit=2_000_000,
|
||||||
bend_radii=(50.0,),
|
bend_radii=(50.0,),
|
||||||
sbend_radii=(50.0,),
|
sbend_radii=(50.0,),
|
||||||
greedy_h_weight=1.5,
|
greedy_h_weight=1.5,
|
||||||
|
bend_clip_margin=10.0,
|
||||||
),
|
),
|
||||||
objective=ObjectiveWeights(
|
objective=ObjectiveWeights(
|
||||||
unit_length_cost=0.1,
|
unit_length_cost=0.1,
|
||||||
|
|
@ -61,48 +59,59 @@ def main() -> None:
|
||||||
max_iterations=15,
|
max_iterations=15,
|
||||||
base_penalty=100.0,
|
base_penalty=100.0,
|
||||||
multiplier=1.4,
|
multiplier=1.4,
|
||||||
|
net_order="shortest",
|
||||||
shuffle_nets=True,
|
shuffle_nets=True,
|
||||||
seed=42,
|
seed=42,
|
||||||
),
|
),
|
||||||
diagnostics=DiagnosticsOptions(capture_expanded=True),
|
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]] = []
|
iteration_stats: list[dict[str, int]] = []
|
||||||
|
|
||||||
def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None:
|
def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None:
|
||||||
successes = sum(1 for result in current_results.values() if result.is_valid)
|
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_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}")
|
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_stats.append(
|
||||||
{
|
{
|
||||||
"Iteration": iteration,
|
"Iteration": iteration,
|
||||||
"Success": successes,
|
"Success": successes,
|
||||||
"Congestion": total_collisions,
|
"Congestion": total_collisions,
|
||||||
|
"Nodes": total_nodes,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
metrics.reset_per_route()
|
||||||
|
|
||||||
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
|
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
|
||||||
start_time = time.perf_counter()
|
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()
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
print(f"Routing took {end_time - start_time:.4f}s")
|
print(f"Routing took {end_time - start_time:.4f}s")
|
||||||
print("\n--- Iteration Summary ---")
|
print("\n--- Iteration Summary ---")
|
||||||
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8}")
|
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}")
|
||||||
print("-" * 30)
|
print("-" * 43)
|
||||||
for stats in iteration_stats:
|
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.")
|
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:
|
if not result.is_valid:
|
||||||
print(f" FAILED: {net_id}, collisions={result.collisions}")
|
print(f" FAILED: {net_id}, collisions={result.collisions}")
|
||||||
else:
|
else:
|
||||||
print(f" {net_id}: SUCCESS")
|
print(f" {net_id}: SUCCESS")
|
||||||
|
|
||||||
fig, ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist=netlist)
|
fig, ax = plot_routing_results(results, list(obstacles), bounds, netlist=netlist)
|
||||||
plot_expanded_nodes(list(run.expanded_nodes), ax=ax)
|
plot_expanded_nodes(list(finder.accumulated_expanded_nodes), ax=ax)
|
||||||
fig.savefig("examples/07_large_scale_routing.png")
|
fig.savefig("examples/07_large_scale_routing.png")
|
||||||
print("Saved plot to 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 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.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_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:
|
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)
|
||||||
|
|
||||||
print("Routing with standard arc...")
|
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...")
|
print("Routing with custom collision model...")
|
||||||
results_custom = _run_request(bounds, custom_poly, "custom_model", start, target)
|
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(
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ def main() -> None:
|
||||||
bend_penalty=50.0,
|
bend_penalty=50.0,
|
||||||
sbend_penalty=150.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)...")
|
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:
|
`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 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))]
|
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.
|
"""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.
|
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))
|
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(
|
def _transform_custom_collision_polygon(
|
||||||
collision_poly: Polygon,
|
collision_poly: Polygon,
|
||||||
cxy: tuple[float, float],
|
cxy: tuple[float, float],
|
||||||
|
|
@ -186,6 +212,7 @@ def _apply_collision_model(
|
||||||
width: float,
|
width: float,
|
||||||
cxy: tuple[float, float],
|
cxy: tuple[float, float],
|
||||||
ts: tuple[float, float],
|
ts: tuple[float, float],
|
||||||
|
clip_margin: float | None = None,
|
||||||
rotation_deg: float = 0.0,
|
rotation_deg: float = 0.0,
|
||||||
mirror_y: bool = False,
|
mirror_y: bool = False,
|
||||||
) -> list[Polygon]:
|
) -> list[Polygon]:
|
||||||
|
|
@ -194,7 +221,7 @@ def _apply_collision_model(
|
||||||
if collision_type == "arc":
|
if collision_type == "arc":
|
||||||
return [arc_poly]
|
return [arc_poly]
|
||||||
if collision_type == "clipped_bbox":
|
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 [clipped if not clipped.is_empty else box(*arc_poly.bounds)]
|
||||||
return [box(*arc_poly.bounds)]
|
return [box(*arc_poly.bounds)]
|
||||||
|
|
||||||
|
|
@ -254,11 +281,11 @@ 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",
|
||||||
|
clip_margin: float | None = None,
|
||||||
dilation: float = 0.0,
|
dilation: float = 0.0,
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
rot2 = rotation_matrix2(start_port.r)
|
rot2 = rotation_matrix2(start_port.r)
|
||||||
sign = 1 if direction == "CCW" else -1
|
sign = 1 if direction == "CCW" else -1
|
||||||
uses_custom_geometry = isinstance(collision_type, Polygon)
|
|
||||||
|
|
||||||
center_local = numpy.array((0.0, sign * radius))
|
center_local = numpy.array((0.0, sign * radius))
|
||||||
end_local = numpy.array((radius, sign * radius))
|
end_local = numpy.array((radius, sign * radius))
|
||||||
|
|
@ -278,27 +305,24 @@ class Bend90:
|
||||||
width,
|
width,
|
||||||
(float(center_xy[0]), float(center_xy[1])),
|
(float(center_xy[0]), float(center_xy[1])),
|
||||||
ts,
|
ts,
|
||||||
|
clip_margin=clip_margin,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign < 0),
|
mirror_y=(sign < 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
physical_geometry = collision_polys if uses_custom_geometry else arc_polys
|
physical_geometry = arc_polys
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if uses_custom_geometry:
|
dilated_physical_geometry = _get_arc_polygons(
|
||||||
dilated_physical_geometry = [poly.buffer(dilation) for poly in collision_polys]
|
(float(center_xy[0]), float(center_xy[1])),
|
||||||
dilated_collision_geometry = dilated_physical_geometry
|
radius,
|
||||||
else:
|
width,
|
||||||
dilated_physical_geometry = _get_arc_polygons(
|
ts,
|
||||||
(float(center_xy[0]), float(center_xy[1])),
|
sagitta,
|
||||||
radius,
|
dilation=dilation,
|
||||||
width,
|
)
|
||||||
ts,
|
dilated_collision_geometry = (
|
||||||
sagitta,
|
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
|
||||||
dilation=dilation,
|
)
|
||||||
)
|
|
||||||
dilated_collision_geometry = (
|
|
||||||
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
dilated_physical_geometry = physical_geometry
|
dilated_physical_geometry = physical_geometry
|
||||||
dilated_collision_geometry = collision_polys
|
dilated_collision_geometry = collision_polys
|
||||||
|
|
@ -325,13 +349,13 @@ class SBend:
|
||||||
width: float,
|
width: float,
|
||||||
sagitta: float = 0.01,
|
sagitta: float = 0.01,
|
||||||
collision_type: BendCollisionModel = "arc",
|
collision_type: BendCollisionModel = "arc",
|
||||||
|
clip_margin: float | None = None,
|
||||||
dilation: float = 0.0,
|
dilation: float = 0.0,
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
if abs(offset) >= 2 * radius:
|
if abs(offset) >= 2 * radius:
|
||||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||||
|
|
||||||
sign = 1 if offset >= 0 else -1
|
sign = 1 if offset >= 0 else -1
|
||||||
uses_custom_geometry = isinstance(collision_type, Polygon)
|
|
||||||
theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius))
|
theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius))
|
||||||
dx = 2.0 * radius * numpy.sin(theta)
|
dx = 2.0 * radius * numpy.sin(theta)
|
||||||
theta_deg = float(numpy.degrees(theta))
|
theta_deg = float(numpy.degrees(theta))
|
||||||
|
|
@ -361,6 +385,7 @@ class SBend:
|
||||||
width,
|
width,
|
||||||
(float(c1_xy[0]), float(c1_xy[1])),
|
(float(c1_xy[0]), float(c1_xy[1])),
|
||||||
ts1,
|
ts1,
|
||||||
|
clip_margin=clip_margin,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign < 0),
|
mirror_y=(sign < 0),
|
||||||
)[0],
|
)[0],
|
||||||
|
|
@ -371,24 +396,21 @@ class SBend:
|
||||||
width,
|
width,
|
||||||
(float(c2_xy[0]), float(c2_xy[1])),
|
(float(c2_xy[0]), float(c2_xy[1])),
|
||||||
ts2,
|
ts2,
|
||||||
|
clip_margin=clip_margin,
|
||||||
rotation_deg=float(start_port.r),
|
rotation_deg=float(start_port.r),
|
||||||
mirror_y=(sign > 0),
|
mirror_y=(sign > 0),
|
||||||
)[0],
|
)[0],
|
||||||
]
|
]
|
||||||
|
|
||||||
physical_geometry = geometry if uses_custom_geometry else actual_geometry
|
physical_geometry = actual_geometry
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if uses_custom_geometry:
|
dilated_physical_geometry = [
|
||||||
dilated_physical_geometry = [poly.buffer(dilation) for poly in geometry]
|
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0],
|
||||||
dilated_collision_geometry = dilated_physical_geometry
|
_get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0],
|
||||||
else:
|
]
|
||||||
dilated_physical_geometry = [
|
dilated_collision_geometry = (
|
||||||
_get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0],
|
dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry]
|
||||||
_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]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
dilated_physical_geometry = physical_geometry
|
dilated_physical_geometry = physical_geometry
|
||||||
dilated_collision_geometry = geometry
|
dilated_collision_geometry = geometry
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ 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_clip_margin: float | None = None
|
||||||
visibility_guidance: VisibilityGuidance = "tangent_corner"
|
visibility_guidance: VisibilityGuidance = "tangent_corner"
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ def process_move(
|
||||||
net_width,
|
net_width,
|
||||||
params[1],
|
params[1],
|
||||||
collision_type=coll_type,
|
collision_type=coll_type,
|
||||||
|
clip_margin=config.bend_clip_margin,
|
||||||
dilation=self_dilation,
|
dilation=self_dilation,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -78,6 +79,7 @@ def process_move(
|
||||||
params[1],
|
params[1],
|
||||||
net_width,
|
net_width,
|
||||||
collision_type=coll_type,
|
collision_type=coll_type,
|
||||||
|
clip_margin=config.bend_clip_margin,
|
||||||
dilation=self_dilation,
|
dilation=self_dilation,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
|
||||||
|
|
@ -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_clip_margin: float | None
|
||||||
node_limit: int
|
node_limit: int
|
||||||
return_partial: bool = False
|
return_partial: bool = False
|
||||||
store_expanded: bool = False
|
store_expanded: bool = False
|
||||||
|
|
@ -39,6 +40,7 @@ class SearchRunConfig:
|
||||||
search = options.search
|
search = options.search
|
||||||
return cls(
|
return cls(
|
||||||
bend_collision_type=search.bend_collision_type if bend_collision_type is None else bend_collision_type,
|
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,
|
node_limit=search.node_limit if node_limit is None else node_limit,
|
||||||
return_partial=return_partial,
|
return_partial=return_partial,
|
||||||
store_expanded=store_expanded,
|
store_expanded=store_expanded,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ def materialize_path_seed(
|
||||||
current = start
|
current = start
|
||||||
dilation = clearance / 2.0
|
dilation = clearance / 2.0
|
||||||
bend_collision_type = search.bend_collision_type
|
bend_collision_type = search.bend_collision_type
|
||||||
|
bend_clip_margin = search.bend_clip_margin
|
||||||
|
|
||||||
for segment in seed.segments:
|
for segment in seed.segments:
|
||||||
if isinstance(segment, StraightSeed):
|
if isinstance(segment, StraightSeed):
|
||||||
|
|
@ -35,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,
|
||||||
|
clip_margin=bend_clip_margin,
|
||||||
dilation=dilation,
|
dilation=dilation,
|
||||||
)
|
)
|
||||||
elif isinstance(segment, SBendSeed):
|
elif isinstance(segment, SBendSeed):
|
||||||
|
|
@ -44,6 +46,7 @@ def materialize_path_seed(
|
||||||
segment.radius,
|
segment.radius,
|
||||||
net_width,
|
net_width,
|
||||||
collision_type=bend_collision_type,
|
collision_type=bend_collision_type,
|
||||||
|
clip_margin=bend_clip_margin,
|
||||||
dilation=dilation,
|
dilation=dilation,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ class CostEvaluator:
|
||||||
def default_weights(self) -> ObjectiveWeights:
|
def default_weights(self) -> ObjectiveWeights:
|
||||||
return self._search_weights
|
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:
|
def _resolve_weights(self, weights: ObjectiveWeights | None) -> ObjectiveWeights:
|
||||||
return self._search_weights if weights is None else weights
|
return self._search_weights if weights is None else weights
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,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))},
|
{"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))},
|
||||||
{"clipped_model": 2.0},
|
{"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,
|
"node_limit": 2000000,
|
||||||
"bend_radii": [50.0],
|
"bend_radii": [50.0],
|
||||||
"sbend_radii": [50.0],
|
"sbend_radii": [50.0],
|
||||||
|
"bend_clip_margin": 10.0,
|
||||||
"max_iterations": 15,
|
"max_iterations": 15,
|
||||||
"base_penalty": 100.0,
|
"base_penalty": 100.0,
|
||||||
"multiplier": 1.4,
|
"multiplier": 1.4,
|
||||||
|
"net_order": "shortest",
|
||||||
"capture_expanded": True,
|
"capture_expanded": True,
|
||||||
"shuffle_nets": True,
|
"shuffle_nets": True,
|
||||||
"seed": 42,
|
"seed": 42,
|
||||||
|
|
@ -333,7 +340,10 @@ def run_example_07() -> ScenarioOutcome:
|
||||||
)
|
)
|
||||||
|
|
||||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
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()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||||
|
|
@ -345,27 +355,27 @@ 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 = {"custom_bend": (Port(20, 20, 0), Port(100, 100, 90))}
|
||||||
widths = {"custom_bend": 2.0}
|
widths = {"custom_bend": 2.0}
|
||||||
custom_model = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
custom_model = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)])
|
||||||
standard_evaluator = _build_evaluator(bounds)
|
evaluator = _build_evaluator(bounds)
|
||||||
custom_evaluator = _build_evaluator(bounds)
|
|
||||||
|
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
results_std = _build_pathfinder(
|
results_std = _build_pathfinder(
|
||||||
standard_evaluator,
|
evaluator,
|
||||||
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=[],
|
||||||
use_tiered_strategy=False,
|
max_iterations=1,
|
||||||
metrics=AStarMetrics(),
|
metrics=AStarMetrics(),
|
||||||
).route_all()
|
).route_all()
|
||||||
results_custom = _build_pathfinder(
|
results_custom = _build_pathfinder(
|
||||||
custom_evaluator,
|
evaluator,
|
||||||
bounds=bounds,
|
bounds=bounds,
|
||||||
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
|
nets=_net_specs({"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}),
|
||||||
bend_radii=[10.0],
|
bend_radii=[10.0],
|
||||||
bend_collision_type=custom_model,
|
bend_collision_type=custom_model,
|
||||||
sbend_radii=[],
|
sbend_radii=[],
|
||||||
|
max_iterations=1,
|
||||||
use_tiered_strategy=False,
|
use_tiered_strategy=False,
|
||||||
metrics=AStarMetrics(),
|
metrics=AStarMetrics(),
|
||||||
).route_all()
|
).route_all()
|
||||||
|
|
@ -386,7 +396,7 @@ def run_example_09() -> ScenarioOutcome:
|
||||||
widths=widths,
|
widths=widths,
|
||||||
obstacles=obstacles,
|
obstacles=obstacles,
|
||||||
evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0},
|
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()
|
t0 = perf_counter()
|
||||||
results = pathfinder.route_all()
|
results = pathfinder.route_all()
|
||||||
|
|
@ -397,7 +407,7 @@ def run_example_09() -> ScenarioOutcome:
|
||||||
SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
|
SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
|
||||||
("example_01_simple_route", run_example_01),
|
("example_01_simple_route", run_example_01),
|
||||||
("example_02_congestion_resolution", run_example_02),
|
("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_04_sbends_and_radii", run_example_04),
|
||||||
("example_05_orientation_stress", run_example_05),
|
("example_05_orientation_stress", run_example_05),
|
||||||
("example_06_bend_collision_models", run_example_06),
|
("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")
|
res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc")
|
||||||
assert res_clipped.collision_geometry[0].covers(res_arc.collision_geometry[0])
|
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:
|
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)])
|
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])
|
expected = shapely_translate(expected, center_xy[0], center_xy[1])
|
||||||
|
|
||||||
assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6
|
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)])
|
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, 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
|
assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area > 1e-6
|
||||||
|
|
||||||
|
|
||||||
def test_sbend_collision_models() -> None:
|
def test_sbend_collision_models() -> None:
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,18 @@ def test_cost_calculation() -> None:
|
||||||
assert h_away >= h_90
|
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:
|
def test_danger_map_kd_tree_and_cache() -> None:
|
||||||
# Test that KD-Tree based danger map works and uses cache
|
# Test that KD-Tree based danger map works and uses cache
|
||||||
bounds = (0, 0, 1000, 1000)
|
bounds = (0, 0, 1000, 1000)
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,28 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
|
||||||
PERFORMANCE_REPEATS = 3
|
PERFORMANCE_REPEATS = 3
|
||||||
REGRESSION_FACTOR = 1.5
|
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 = {
|
BASELINE_SECONDS = {
|
||||||
"example_01_simple_route": 0.0035,
|
"example_01_simple_route": 0.0035,
|
||||||
"example_02_congestion_resolution": 0.2666,
|
"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_04_sbends_and_radii": 1.8734,
|
||||||
"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": 0.9848,
|
"example_08_custom_bend_geometry": 4.2111,
|
||||||
"example_09_unroutable_best_effort": 0.0056,
|
"example_09_unroutable_best_effort": 0.0056,
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_OUTCOMES = {
|
EXPECTED_OUTCOMES = {
|
||||||
"example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1},
|
"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_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_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_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": 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},
|
"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