inire/inire/router/pathfinder.py

429 lines
17 KiB
Python

from __future__ import annotations
import logging
import math
import random
import time
from dataclasses import dataclass
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:
from inire.geometry.components import ComponentResult
from inire.geometry.primitives import Port
from inire.router.astar import AStarContext
from inire.router.cost import CostEvaluator
logger = logging.getLogger(__name__)
@dataclass
class RoutingResult:
net_id: str
path: list[ComponentResult]
is_valid: bool
collisions: int
reached_target: bool = False
class PathFinder:
__slots__ = (
"context",
"metrics",
"max_iterations",
"base_congestion_penalty",
"use_tiered_strategy",
"congestion_multiplier",
"accumulated_expanded_nodes",
"warm_start",
"refine_paths",
)
def __init__(
self,
context: AStarContext,
metrics: AStarMetrics | None = None,
max_iterations: int = 10,
base_congestion_penalty: float = 100.0,
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()
self.max_iterations = max_iterations
self.base_congestion_penalty = base_congestion_penalty
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
def cost_evaluator(self) -> CostEvaluator:
return self.context.cost_evaluator
def _perform_greedy_pass(
self,
netlist: dict[str, tuple[Port, Port]],
net_widths: dict[str, float],
order: Literal["shortest", "longest", "user"],
) -> dict[str, list[ComponentResult]]:
all_net_ids = list(netlist.keys())
if order != "user":
all_net_ids.sort(
key=lambda nid: abs(netlist[nid][1].x - netlist[nid][0].x) + abs(netlist[nid][1].y - netlist[nid][0].y),
reverse=(order == "longest"),
)
greedy_paths: dict[str, list[ComponentResult]] = {}
temp_obj_ids: list[int] = []
greedy_node_limit = min(self.context.config.node_limit, 2000)
for net_id in all_net_ids:
start, target = netlist[net_id]
width = net_widths.get(net_id, 2.0)
h_start = self.cost_evaluator.h_manhattan(start, target)
max_cost_limit = max(h_start * 3.0, 2000.0)
path = route_astar(
start,
target,
width,
context=self.context,
metrics=self.metrics,
net_id=net_id,
skip_congestion=True,
max_cost=max_cost_limit,
self_collision_check=True,
node_limit=greedy_node_limit,
)
if not path:
continue
greedy_paths[net_id] = path
for res in path:
geoms = res.actual_geometry if res.actual_geometry is not None else res.geometry
dilated_geoms = res.dilated_actual_geometry if res.dilated_actual_geometry else res.dilated_geometry
for i, poly in enumerate(geoms):
dilated = dilated_geoms[i] if dilated_geoms else None
obj_id = self.cost_evaluator.collision_engine.add_static_obstacle(poly, dilated_geometry=dilated)
temp_obj_ids.append(obj_id)
self.context.clear_static_caches()
for obj_id in temp_obj_ids:
self.cost_evaluator.collision_engine.remove_static_obstacle(obj_id)
return greedy_paths
def _has_self_collision(self, path: list[ComponentResult]) -> bool:
for i, comp_i in enumerate(path):
tb_i = comp_i.total_bounds
for j in range(i + 2, len(path)):
comp_j = path[j]
tb_j = comp_j.total_bounds
if tb_i[0] < tb_j[2] and tb_i[2] > tb_j[0] and tb_i[1] < tb_j[3] and tb_i[3] > tb_j[1]:
for p_i in comp_i.geometry:
for p_j in comp_j.geometry:
if p_i.intersects(p_j) and not p_i.touches(p_j):
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]],
net_widths: dict[str, float],
store_expanded: bool = False,
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
shuffle_nets: bool = False,
sort_nets: Literal["shortest", "longest", "user", None] = None,
initial_paths: dict[str, list[ComponentResult]] | None = None,
seed: int | None = None,
) -> dict[str, RoutingResult]:
results: dict[str, RoutingResult] = {}
self.cost_evaluator.congestion_penalty = self.base_congestion_penalty
self.accumulated_expanded_nodes = []
self.metrics.reset_per_route()
start_time = time.monotonic()
num_nets = len(netlist)
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
all_net_ids = list(netlist.keys())
needs_sc: set[str] = set()
if initial_paths is None:
ws_order = sort_nets if sort_nets is not None else self.warm_start
if ws_order is not None:
initial_paths = self._perform_greedy_pass(netlist, net_widths, ws_order)
self.context.clear_static_caches()
if sort_nets and sort_nets != "user":
all_net_ids.sort(
key=lambda nid: abs(netlist[nid][1].x - netlist[nid][0].x) + abs(netlist[nid][1].y - netlist[nid][0].y),
reverse=(sort_nets == "longest"),
)
for iteration in range(self.max_iterations):
any_congestion = False
self.accumulated_expanded_nodes = []
self.metrics.reset_per_route()
if shuffle_nets and (iteration > 0 or initial_paths is None):
it_seed = (seed + iteration) if seed is not None else None
random.Random(it_seed).shuffle(all_net_ids)
for net_id in all_net_ids:
start, target = netlist[net_id]
if time.monotonic() - start_time > session_timeout:
self.cost_evaluator.collision_engine.dynamic_tree = None
self.cost_evaluator.collision_engine._ensure_dynamic_tree()
return self.verify_all_nets(results, netlist)
width = net_widths.get(net_id, 2.0)
self.cost_evaluator.collision_engine.remove_path(net_id)
path: list[ComponentResult] | None = None
if iteration == 0 and initial_paths and net_id in initial_paths:
path = initial_paths[net_id]
else:
target_coll_model = self.context.config.bend_collision_type
coll_model = target_coll_model
skip_cong = False
if self.use_tiered_strategy and iteration == 0:
skip_cong = True
if target_coll_model == "arc":
coll_model = "clipped_bbox"
path = route_astar(
start,
target,
width,
context=self.context,
metrics=self.metrics,
net_id=net_id,
bend_collision_type=coll_model,
return_partial=True,
store_expanded=store_expanded,
skip_congestion=skip_cong,
self_collision_check=(net_id in needs_sc),
node_limit=self.context.config.node_limit,
)
if store_expanded and self.metrics.last_expanded_nodes:
self.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
if not path:
results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False)
any_congestion = True
continue
last_p = path[-1].end_port
reached = last_p == target
if reached and net_id not in needs_sc and self._has_self_collision(path):
needs_sc.add(net_id)
any_congestion = True
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])
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
collision_count = 0
if reached:
is_valid, collision_count = self.cost_evaluator.collision_engine.verify_path(net_id, path)
any_congestion = any_congestion or not is_valid
results[net_id] = RoutingResult(net_id, path, reached and collision_count == 0, collision_count, reached_target=reached)
if iteration_callback:
iteration_callback(iteration, results)
if not any_congestion:
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)
def verify_all_nets(
self,
results: dict[str, RoutingResult],
netlist: dict[str, tuple[Port, Port]],
) -> dict[str, RoutingResult]:
final_results: dict[str, RoutingResult] = {}
for net_id, (_, target_p) in netlist.items():
res = results.get(net_id)
if not res or not res.path:
final_results[net_id] = RoutingResult(net_id, [], False, 0)
continue
last_p = res.path[-1].end_port
reached = last_p == target_p
is_valid, collisions = self.cost_evaluator.collision_engine.verify_path(net_id, res.path)
final_results[net_id] = RoutingResult(
net_id=net_id,
path=res.path,
is_valid=(is_valid and reached),
collisions=collisions,
reached_target=reached,
)
return final_results