429 lines
17 KiB
Python
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
|