improve visibility checker and add refiner

This commit is contained in:
Jan Petykiewicz 2026-03-29 15:03:15 -07:00
commit 6a28dcf312
11 changed files with 440 additions and 3 deletions

View file

@ -19,6 +19,7 @@ The `AStarContext` stores the configuration and persistent state for the A* sear
| `sbend_penalty` | `float` | 500.0 | Flat cost added for every S-bend. | | `sbend_penalty` | `float` | 500.0 | Flat cost added for every S-bend. |
| `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. | | `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. |
| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide for clipped models. | | `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide for clipped models. |
| `visibility_guidance` | `str` | `"tangent_corner"` | Visibility-driven straight candidate mode: `"off"`, `"exact_corner"`, or `"tangent_corner"`. |
## 2. AStarMetrics ## 2. AStarMetrics
@ -86,6 +87,12 @@ If the router produces many small bends instead of a long straight line:
2. Ensure `straight_lengths` includes larger values like `25.0` or `100.0`. 2. Ensure `straight_lengths` includes larger values like `25.0` or `100.0`.
3. Decrease `greedy_h_weight` closer to `1.0`. 3. Decrease `greedy_h_weight` closer to `1.0`.
### Visibility Guidance
The router can bias straight stop points using static obstacle corners.
- **`"tangent_corner"`**: Default. Proposes straight lengths that set up a clean tangent bend around nearby visible corners. This helps obstacle-dense layouts more than open space.
- **`"exact_corner"`**: Only uses precomputed corner-to-corner visibility when the current search state already lands on an obstacle corner.
- **`"off"`**: Disables visibility-derived straight candidates entirely.
### Handling Congestion ### Handling Congestion
In multi-net designs, if nets are overlapping: In multi-net designs, if nets are overlapping:
1. Increase `congestion_penalty` in `CostEvaluator`. 1. Increase `congestion_penalty` in `CostEvaluator`.

View file

@ -17,7 +17,7 @@ def main() -> None:
danger_map.precompute([]) danger_map.precompute([])
# Configure a router with high congestion penalties # Configure a router with high congestion penalties
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=50.0, sbend_penalty=150.0) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0)
context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0]) context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0])
metrics = AStarMetrics() metrics = AStarMetrics()
pf = PathFinder(context, metrics, base_congestion_penalty=1000.0) pf = PathFinder(context, metrics, base_congestion_penalty=1000.0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

View file

@ -16,7 +16,7 @@ def main() -> None:
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map) evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
context = AStarContext(evaluator, bend_radii=[10.0]) context = AStarContext(evaluator, bend_radii=[10.0])
metrics = AStarMetrics() metrics = AStarMetrics()
pf = PathFinder(context, metrics) pf = PathFinder(context, metrics)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before After
Before After

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import heapq import heapq
import logging import logging
import math
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
import shapely import shapely
@ -9,7 +10,7 @@ import shapely
from inire.constants import TOLERANCE_LINEAR from inire.constants import TOLERANCE_LINEAR
from inire.geometry.components import Bend90, SBend, Straight from inire.geometry.components import Bend90, SBend, Straight
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.config import RouterConfig from inire.router.config import RouterConfig, VisibilityGuidanceMode
from inire.router.visibility import VisibilityManager from inire.router.visibility import VisibilityManager
if TYPE_CHECKING: if TYPE_CHECKING:
@ -98,6 +99,7 @@ class AStarContext:
sbend_penalty: float | None = None, sbend_penalty: float | None = None,
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc", bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc",
bend_clip_margin: float = 10.0, bend_clip_margin: float = 10.0,
visibility_guidance: VisibilityGuidanceMode = "tangent_corner",
max_cache_size: int = 1000000, max_cache_size: int = 1000000,
) -> None: ) -> None:
actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty
@ -114,6 +116,7 @@ class AStarContext:
sbend_penalty=actual_sbend_penalty, sbend_penalty=actual_sbend_penalty,
bend_collision_type=bend_collision_type, bend_collision_type=bend_collision_type,
bend_clip_margin=bend_clip_margin, bend_clip_margin=bend_clip_margin,
visibility_guidance=visibility_guidance,
) )
self.cost_evaluator.config = self.config self.cost_evaluator.config = self.config
self.cost_evaluator._refresh_cached_config() self.cost_evaluator._refresh_cached_config()
@ -230,6 +233,91 @@ def _sbend_forward_span(offset: float, radius: float) -> float | None:
return 2.0 * radius * __import__("math").sin(theta) return 2.0 * radius * __import__("math").sin(theta)
def _visible_straight_candidates(
current: Port,
context: AStarContext,
max_reach: float,
cos_v: float,
sin_v: float,
net_width: float,
) -> list[float]:
mode = context.config.visibility_guidance
if mode == "off":
return []
if mode == "exact_corner":
max_bend_radius = max(context.config.bend_radii, default=0.0)
visibility_reach = max_reach + max_bend_radius
visible_corners = sorted(
context.visibility_manager.get_corner_visibility(current, max_dist=visibility_reach),
key=lambda corner: corner[2],
)
if not visible_corners:
return []
candidates: set[int] = set()
for cx, cy, _ in visible_corners[:12]:
dx = cx - current.x
dy = cy - current.y
local_x = dx * cos_v + dy * sin_v
if local_x <= context.config.min_straight_length:
continue
candidates.add(int(round(local_x)))
return sorted(candidates, reverse=True)
if mode != "tangent_corner":
return []
visibility_manager = context.visibility_manager
visibility_manager._ensure_current()
max_bend_radius = max(context.config.bend_radii, default=0.0)
if max_bend_radius <= 0 or not visibility_manager.corners:
return []
reach = max_reach + max_bend_radius
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach)
candidate_ids = list(visibility_manager.corner_index.intersection(bounds))
if not candidate_ids:
return []
scored: list[tuple[float, float, float, float, float]] = []
for idx in candidate_ids:
cx, cy = visibility_manager.corners[idx]
dx = cx - current.x
dy = cy - current.y
local_x = dx * cos_v + dy * sin_v
local_y = -dx * sin_v + dy * cos_v
if local_x <= context.config.min_straight_length or local_x > reach + 0.01:
continue
nearest_radius = min(context.config.bend_radii, key=lambda radius: abs(abs(local_y) - radius))
tangent_error = abs(abs(local_y) - nearest_radius)
if tangent_error > 2.0:
continue
length = local_x - nearest_radius
if length <= context.config.min_straight_length or length > max_reach + 0.01:
continue
scored.append((tangent_error, math.hypot(dx, dy), length, dx, dy))
if not scored:
return []
collision_engine = context.cost_evaluator.collision_engine
candidates: set[int] = set()
for _, dist, length, dx, dy in sorted(scored)[:4]:
angle = math.degrees(math.atan2(dy, dx))
corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width)
if corner_reach < dist - 0.01:
continue
qlen = int(round(length))
if qlen > 0:
candidates.add(qlen)
return sorted(candidates, reverse=True)
def _previous_move_metadata(node: AStarNode) -> tuple[str | None, float | None]: def _previous_move_metadata(node: AStarNode) -> tuple[str | None, float | None]:
result = node.component_result result = node.component_result
if result is None: if result is None:
@ -309,6 +397,17 @@ def expand_moves(
for radius in context.config.bend_radii: for radius in context.config.bend_radii:
candidate_lengths.extend((max_reach - radius, axis_target_dist - radius, axis_target_dist - 2.0 * radius)) candidate_lengths.extend((max_reach - radius, axis_target_dist - radius, axis_target_dist - 2.0 * radius))
candidate_lengths.extend(
_visible_straight_candidates(
cp,
context,
max_reach,
cos_v,
sin_v,
net_width,
)
)
if cp.r == target.r and dx_local > 0 and abs(dy_local) > TOLERANCE_LINEAR: if cp.r == target.r and dx_local > 0 and abs(dy_local) > TOLERANCE_LINEAR:
for radius in context.config.sbend_radii: for radius in context.config.sbend_radii:
sbend_span = _sbend_forward_span(dy_local, radius) sbend_span = _sbend_forward_span(dy_local, radius)

View file

@ -4,6 +4,9 @@ from dataclasses import dataclass, field
from typing import Literal, Any from typing import Literal, Any
VisibilityGuidanceMode = Literal["off", "exact_corner", "tangent_corner"]
@dataclass @dataclass
class RouterConfig: class RouterConfig:
@ -28,6 +31,7 @@ class RouterConfig:
sbend_penalty: float = 500.0 sbend_penalty: float = 500.0
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
bend_clip_margin: float = 10.0 bend_clip_margin: float = 10.0
visibility_guidance: VisibilityGuidanceMode = "tangent_corner"
@dataclass @dataclass

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import random import random
import time import time
from dataclasses import dataclass from dataclasses import dataclass
@ -8,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Callable, Literal
import numpy import numpy
from inire.geometry.components import Bend90, Straight
from inire.router.astar import AStarMetrics, route_astar from inire.router.astar import AStarMetrics, route_astar
if TYPE_CHECKING: if TYPE_CHECKING:
@ -38,6 +40,7 @@ class PathFinder:
"congestion_multiplier", "congestion_multiplier",
"accumulated_expanded_nodes", "accumulated_expanded_nodes",
"warm_start", "warm_start",
"refine_paths",
) )
def __init__( def __init__(
@ -49,6 +52,7 @@ class PathFinder:
congestion_multiplier: float = 1.5, congestion_multiplier: float = 1.5,
use_tiered_strategy: bool = True, use_tiered_strategy: bool = True,
warm_start: Literal["shortest", "longest", "user"] | None = "shortest", warm_start: Literal["shortest", "longest", "user"] | None = "shortest",
refine_paths: bool = False,
) -> None: ) -> None:
self.context = context self.context = context
self.metrics = metrics if metrics is not None else AStarMetrics() self.metrics = metrics if metrics is not None else AStarMetrics()
@ -57,6 +61,7 @@ class PathFinder:
self.congestion_multiplier = congestion_multiplier self.congestion_multiplier = congestion_multiplier
self.use_tiered_strategy = use_tiered_strategy self.use_tiered_strategy = use_tiered_strategy
self.warm_start = warm_start self.warm_start = warm_start
self.refine_paths = refine_paths
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
@property @property
@ -125,6 +130,139 @@ class PathFinder:
return True return True
return False return False
def _path_cost(self, path: list[ComponentResult]) -> float:
total = 0.0
bend_penalty = self.context.config.bend_penalty
sbend_penalty = self.context.config.sbend_penalty
for comp in path:
total += comp.length
if comp.move_type == "Bend90":
radius = comp.length * 2.0 / math.pi if comp.length > 0 else 0.0
if radius > 0:
total += bend_penalty * (10.0 / radius) ** 0.5
else:
total += bend_penalty
elif comp.move_type == "SBend":
total += sbend_penalty
return total
def _extract_geometry(self, path: list[ComponentResult]) -> tuple[list[Any], list[Any]]:
all_geoms = []
all_dilated = []
for res in path:
all_geoms.extend(res.geometry)
if res.dilated_geometry:
all_dilated.extend(res.dilated_geometry)
else:
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
all_dilated.extend([p.buffer(dilation) for p in res.geometry])
return all_geoms, all_dilated
def _to_local(self, start: Port, point: Port) -> tuple[int, int]:
dx = point.x - start.x
dy = point.y - start.y
if start.r == 0:
return dx, dy
if start.r == 90:
return dy, -dx
if start.r == 180:
return -dx, -dy
return -dy, dx
def _build_same_orientation_dogleg(
self,
start: Port,
target: Port,
net_width: float,
radius: float,
side_extent: float,
) -> list[ComponentResult] | None:
local_dx, local_dy = self._to_local(start, target)
if abs(local_dy) > 0 or local_dx < 4.0 * radius - 0.01:
return None
side_abs = abs(side_extent)
side_length = side_abs - 2.0 * radius
if side_length < self.context.config.min_straight_length - 0.01:
return None
forward_length = local_dx - 4.0 * radius
if forward_length < -0.01:
return None
first_dir = "CCW" if side_extent > 0 else "CW"
second_dir = "CW" if side_extent > 0 else "CCW"
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
path: list[ComponentResult] = []
curr = start
for direction, straight_len in (
(first_dir, side_length),
(second_dir, forward_length),
(second_dir, side_length),
(first_dir, None),
):
bend = Bend90.generate(curr, radius, net_width, direction, dilation=dilation)
path.append(bend)
curr = bend.end_port
if straight_len is None:
continue
if straight_len > 0.01:
straight = Straight.generate(curr, straight_len, net_width, dilation=dilation)
path.append(straight)
curr = straight.end_port
if curr != target:
return None
return path
def _refine_path(
self,
net_id: str,
start: Port,
target: Port,
net_width: float,
path: list[ComponentResult],
) -> list[ComponentResult]:
if not path or start.r != target.r:
return path
bend_count = sum(1 for comp in path if comp.move_type == "Bend90")
if bend_count < 5:
return path
side_extents = []
local_points = [self._to_local(start, start)]
local_points.extend(self._to_local(start, comp.end_port) for comp in path)
min_side = min(point[1] for point in local_points)
max_side = max(point[1] for point in local_points)
if min_side < -0.01:
side_extents.append(float(min_side))
if max_side > 0.01:
side_extents.append(float(max_side))
if not side_extents:
return path
best_path = path
best_cost = self._path_cost(path)
collision_engine = self.cost_evaluator.collision_engine
for radius in self.context.config.bend_radii:
for side_extent in side_extents:
candidate = self._build_same_orientation_dogleg(start, target, net_width, radius, side_extent)
if candidate is None:
continue
is_valid, collisions = collision_engine.verify_path(net_id, candidate)
if not is_valid or collisions != 0:
continue
candidate_cost = self._path_cost(candidate)
if candidate_cost + 1e-6 < best_cost:
best_cost = candidate_cost
best_path = candidate
return best_path
def route_all( def route_all(
self, self,
netlist: dict[str, tuple[Port, Port]], netlist: dict[str, tuple[Port, Port]],
@ -244,6 +382,25 @@ class PathFinder:
break break
self.cost_evaluator.congestion_penalty *= self.congestion_multiplier self.cost_evaluator.congestion_penalty *= self.congestion_multiplier
if self.refine_paths and results:
for net_id in all_net_ids:
res = results.get(net_id)
if not res or not res.path or not res.reached_target or not res.is_valid:
continue
start, target = netlist[net_id]
width = net_widths.get(net_id, 2.0)
self.cost_evaluator.collision_engine.remove_path(net_id)
refined_path = self._refine_path(net_id, start, target, width, res.path)
all_geoms, all_dilated = self._extract_geometry(refined_path)
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
results[net_id] = RoutingResult(
net_id=net_id,
path=refined_path,
is_valid=res.is_valid,
collisions=res.collisions,
reached_target=res.reached_target,
)
self.cost_evaluator.collision_engine.dynamic_tree = None self.cost_evaluator.collision_engine.dynamic_tree = None
self.cost_evaluator.collision_engine._ensure_dynamic_tree() self.cost_evaluator.collision_engine._ensure_dynamic_tree()
return self.verify_all_nets(results, netlist) return self.verify_all_nets(results, netlist)

View file

@ -140,3 +140,20 @@ class VisibilityManager:
self._static_visibility_cache[cache_key] = visible self._static_visibility_cache[cache_key] = visible
return visible return visible
def get_corner_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
"""
Return precomputed visibility only when the origin is already at a known corner.
This avoids the expensive arbitrary-point visibility scan in hot search paths.
"""
self._ensure_current()
if max_dist < 0:
return []
ox, oy = round(origin.x, 3), round(origin.y, 3)
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
for idx in nearby:
cx, cy = self.corners[idx]
if abs(cx - ox) < 1e-4 and abs(cy - oy) < 1e-4 and idx in self._corner_graph:
return [corner for corner in self._corner_graph[idx] if corner[2] <= max_dist]
return []

View file

@ -195,3 +195,95 @@ def test_expand_moves_adds_sbend_aligned_straight_stop_points(
assert sbend_span is not None assert sbend_span is not None
assert int(round(100.0 - sbend_span)) in straight_lengths assert int(round(100.0 - sbend_span)) in straight_lengths
assert int(round(100.0 - 2.0 * sbend_span)) in straight_lengths assert int(round(100.0 - 2.0 * sbend_span)) in straight_lengths
def test_expand_moves_adds_exact_corner_visibility_stop_points(
basic_evaluator: CostEvaluator,
monkeypatch: pytest.MonkeyPatch,
) -> None:
context = AStarContext(
basic_evaluator,
bend_radii=[10.0],
max_straight_length=150.0,
visibility_guidance="exact_corner",
)
current = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
monkeypatch.setattr(
astar_module.VisibilityManager,
"get_corner_visibility",
lambda self, origin, max_dist=0.0: [(40.0, 10.0, 41.23), (75.0, -15.0, 76.48)],
)
emitted: list[tuple[str, tuple]] = []
def fake_process_move(*args, **kwargs) -> None:
emitted.append((args[9], args[10]))
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
astar_module.expand_moves(
current,
Port(120, 20, 0),
net_width=2.0,
net_id="test",
open_set=[],
closed_set={},
context=context,
metrics=astar_module.AStarMetrics(),
congestion_cache={},
)
straight_lengths = {params[0] for move_class, params in emitted if move_class == "S"}
assert 40 in straight_lengths
assert 75 in straight_lengths
def test_expand_moves_adds_tangent_corner_visibility_stop_points(
basic_evaluator: CostEvaluator,
monkeypatch: pytest.MonkeyPatch,
) -> None:
class DummyCornerIndex:
def intersection(self, bounds: tuple[float, float, float, float]) -> list[int]:
return [0, 1]
context = AStarContext(
basic_evaluator,
bend_radii=[10.0],
sbend_radii=[],
max_straight_length=150.0,
visibility_guidance="tangent_corner",
)
current = astar_module.AStarNode(Port(0, 0, 0), g_cost=0.0, h_cost=0.0)
monkeypatch.setattr(astar_module.VisibilityManager, "_ensure_current", lambda self: None)
context.visibility_manager.corners = [(50.0, 10.0), (80.0, -10.0)]
context.visibility_manager.corner_index = DummyCornerIndex()
monkeypatch.setattr(
type(context.cost_evaluator.collision_engine),
"ray_cast",
lambda self, origin, angle_deg, max_dist=2000.0, net_width=None: max_dist,
)
emitted: list[tuple[str, tuple]] = []
def fake_process_move(*args, **kwargs) -> None:
emitted.append((args[9], args[10]))
monkeypatch.setattr(astar_module, "process_move", fake_process_move)
astar_module.expand_moves(
current,
Port(120, 20, 0),
net_width=2.0,
net_id="test",
open_set=[],
closed_set={},
context=context,
metrics=astar_module.AStarMetrics(),
congestion_cache={},
)
straight_lengths = {params[0] for move_class, params in emitted if move_class == "S"}
assert 40 in straight_lengths
assert 70 in straight_lengths

View file

@ -55,3 +55,64 @@ def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
assert not results["net2"].is_valid assert not results["net2"].is_valid
assert results["net1"].collisions > 0 assert results["net1"].collisions > 0
assert results["net2"].collisions > 0 assert results["net2"].collisions > 0
def test_pathfinder_refine_paths_reduces_locked_detour_bends() -> None:
bounds = (0, -50, 100, 50)
def build_pathfinder(*, refine_paths: bool) -> tuple[CollisionEngine, PathFinder]:
engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds)
danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0)
context = AStarContext(evaluator, bend_radii=[10.0])
return engine, PathFinder(context, refine_paths=refine_paths)
base_engine, base_pf = build_pathfinder(refine_paths=False)
base_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0})
base_engine.lock_net("netA")
base_result = base_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"]
refined_engine, refined_pf = build_pathfinder(refine_paths=True)
refined_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0})
refined_engine.lock_net("netA")
refined_result = refined_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"]
base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90")
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90")
assert base_result.is_valid
assert refined_result.is_valid
assert refined_bends < base_bends
assert refined_pf._path_cost(refined_result.path) < base_pf._path_cost(base_result.path)
def test_pathfinder_refine_paths_simplifies_triple_crossing_detours() -> None:
bounds = (0, 0, 100, 100)
netlist = {
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
}
net_widths = {net_id: 2.0 for net_id in netlist}
def build_pathfinder(*, refine_paths: bool) -> PathFinder:
engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds)
danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0)
context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0])
return PathFinder(context, base_congestion_penalty=1000.0, refine_paths=refine_paths)
base_results = build_pathfinder(refine_paths=False).route_all(netlist, net_widths)
refined_results = build_pathfinder(refine_paths=True).route_all(netlist, net_widths)
for net_id in ("vertical_up", "vertical_down"):
base_result = base_results[net_id]
refined_result = refined_results[net_id]
base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90")
refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90")
assert base_result.is_valid
assert refined_result.is_valid
assert refined_bends < base_bends