improve visibility checker and add refiner
This commit is contained in:
parent
a8c876ae69
commit
6a28dcf312
11 changed files with 440 additions and 3 deletions
7
DOCS.md
7
DOCS.md
|
|
@ -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. |
|
||||
| `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. |
|
||||
| `visibility_guidance` | `str` | `"tangent_corner"` | Visibility-driven straight candidate mode: `"off"`, `"exact_corner"`, or `"tangent_corner"`. |
|
||||
|
||||
## 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`.
|
||||
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
|
||||
In multi-net designs, if nets are overlapping:
|
||||
1. Increase `congestion_penalty` in `CostEvaluator`.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def main() -> None:
|
|||
danger_map.precompute([])
|
||||
|
||||
# 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])
|
||||
metrics = AStarMetrics()
|
||||
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 |
|
|
@ -16,7 +16,7 @@ def main() -> None:
|
|||
danger_map = DangerMap(bounds=bounds)
|
||||
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])
|
||||
metrics = AStarMetrics()
|
||||
pf = PathFinder(context, metrics)
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 84 KiB |
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import heapq
|
||||
import logging
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import shapely
|
||||
|
|
@ -9,7 +10,7 @@ import shapely
|
|||
from inire.constants import TOLERANCE_LINEAR
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -98,6 +99,7 @@ class AStarContext:
|
|||
sbend_penalty: float | None = None,
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc",
|
||||
bend_clip_margin: float = 10.0,
|
||||
visibility_guidance: VisibilityGuidanceMode = "tangent_corner",
|
||||
max_cache_size: int = 1000000,
|
||||
) -> None:
|
||||
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,
|
||||
bend_collision_type=bend_collision_type,
|
||||
bend_clip_margin=bend_clip_margin,
|
||||
visibility_guidance=visibility_guidance,
|
||||
)
|
||||
self.cost_evaluator.config = self.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)
|
||||
|
||||
|
||||
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]:
|
||||
result = node.component_result
|
||||
if result is None:
|
||||
|
|
@ -309,6 +397,17 @@ def expand_moves(
|
|||
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(
|
||||
_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:
|
||||
for radius in context.config.sbend_radii:
|
||||
sbend_span = _sbend_forward_span(dy_local, radius)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ from dataclasses import dataclass, field
|
|||
from typing import Literal, Any
|
||||
|
||||
|
||||
VisibilityGuidanceMode = Literal["off", "exact_corner", "tangent_corner"]
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouterConfig:
|
||||
|
|
@ -28,6 +31,7 @@ class RouterConfig:
|
|||
sbend_penalty: float = 500.0
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
||||
bend_clip_margin: float = 10.0
|
||||
visibility_guidance: VisibilityGuidanceMode = "tangent_corner"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -8,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Callable, Literal
|
|||
|
||||
import numpy
|
||||
|
||||
from inire.geometry.components import Bend90, Straight
|
||||
from inire.router.astar import AStarMetrics, route_astar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -38,6 +40,7 @@ class PathFinder:
|
|||
"congestion_multiplier",
|
||||
"accumulated_expanded_nodes",
|
||||
"warm_start",
|
||||
"refine_paths",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
|
@ -49,6 +52,7 @@ class PathFinder:
|
|||
congestion_multiplier: float = 1.5,
|
||||
use_tiered_strategy: bool = True,
|
||||
warm_start: Literal["shortest", "longest", "user"] | None = "shortest",
|
||||
refine_paths: bool = False,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.metrics = metrics if metrics is not None else AStarMetrics()
|
||||
|
|
@ -57,6 +61,7 @@ class PathFinder:
|
|||
self.congestion_multiplier = congestion_multiplier
|
||||
self.use_tiered_strategy = use_tiered_strategy
|
||||
self.warm_start = warm_start
|
||||
self.refine_paths = refine_paths
|
||||
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
|
||||
|
||||
@property
|
||||
|
|
@ -125,6 +130,139 @@ class PathFinder:
|
|||
return True
|
||||
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(
|
||||
self,
|
||||
netlist: dict[str, tuple[Port, Port]],
|
||||
|
|
@ -244,6 +382,25 @@ class PathFinder:
|
|||
break
|
||||
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._ensure_dynamic_tree()
|
||||
return self.verify_all_nets(results, netlist)
|
||||
|
|
|
|||
|
|
@ -140,3 +140,20 @@ class VisibilityManager:
|
|||
|
||||
self._static_visibility_cache[cache_key] = 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 []
|
||||
|
|
|
|||
|
|
@ -195,3 +195,95 @@ def test_expand_moves_adds_sbend_aligned_straight_stop_points(
|
|||
assert sbend_span is not None
|
||||
assert int(round(100.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
|
||||
|
|
|
|||
|
|
@ -55,3 +55,64 @@ def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None:
|
|||
assert not results["net2"].is_valid
|
||||
assert results["net1"].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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue