linter fixes

This commit is contained in:
Jan Petykiewicz 2026-03-30 23:54:30 -07:00
commit 1849075b11
26 changed files with 152 additions and 104 deletions

View file

@ -37,7 +37,12 @@ def main() -> None:
else: else:
print("The route unexpectedly reached the target. Increase difficulty or reduce the node budget further.") print("The route unexpectedly reached the target. Increase difficulty or reduce the node budget further.")
fig, _ax = plot_routing_results(run.results_by_net, list(obstacles), bounds, netlist={"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))}) fig, _ax = plot_routing_results(
run.results_by_net,
list(obstacles),
bounds,
netlist={"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))},
)
fig.savefig("examples/09_unroutable_best_effort.png") fig.savefig("examples/09_unroutable_best_effort.png")
print("Saved plot to examples/09_unroutable_best_effort.png") print("Saved plot to examples/09_unroutable_best_effort.png")

View file

@ -105,9 +105,16 @@ class RoutingWorld:
return reach < length - 0.001 return reach < length - 0.001
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool: def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
bounds = self._static_obstacles.bounds_array[idx] bounds_array = self._static_obstacles.bounds_array
if bounds_array is None:
return False
bounds = bounds_array[idx]
safety_zone = self.safety_zone_radius safety_zone = self.safety_zone_radius
if start_port and bounds[0] - safety_zone <= start_port.x <= bounds[2] + safety_zone and bounds[1] - safety_zone <= start_port.y <= bounds[3] + safety_zone: if (
start_port
and bounds[0] - safety_zone <= start_port.x <= bounds[2] + safety_zone
and bounds[1] - safety_zone <= start_port.y <= bounds[3] + safety_zone
):
return True return True
return bool( return bool(
end_port end_port
@ -176,15 +183,18 @@ class RoutingWorld:
return False return False
self._ensure_static_tree() self._ensure_static_tree()
tree = static_obstacles.tree
bounds_array = static_obstacles.bounds_array
if tree is None or bounds_array is None:
return False
hits = static_obstacles.tree.query(box(*result.total_dilated_bounds)) hits = tree.query(box(*result.total_dilated_bounds))
if hits.size == 0: if hits.size == 0:
return False return False
static_bounds = static_obstacles.bounds_array
move_poly_bounds = result.dilated_bounds move_poly_bounds = result.dilated_bounds
for hit_idx in hits: for hit_idx in hits:
obstacle_bounds = static_bounds[hit_idx] obstacle_bounds = bounds_array[hit_idx]
poly_hits_obstacle_aabb = False poly_hits_obstacle_aabb = False
for poly_bounds in move_poly_bounds: for poly_bounds in move_poly_bounds:
if ( if (
@ -319,7 +329,7 @@ class RoutingWorld:
raw_geometries = static_obstacles.raw_tree.geometries raw_geometries = static_obstacles.raw_tree.geometries
for component in components: for component in components:
for polygon in component.physical_geometry: for polygon in component.physical_geometry:
buffered = polygon.buffer(self.clearance, join_style=2) buffered = polygon.buffer(self.clearance, join_style="mitre")
hits = static_obstacles.raw_tree.query(buffered, predicate="intersects") hits = static_obstacles.raw_tree.query(buffered, predicate="intersects")
for hit_idx in hits: for hit_idx in hits:
obstacle = raw_geometries[hit_idx] obstacle = raw_geometries[hit_idx]
@ -373,6 +383,9 @@ class RoutingWorld:
net_width: float | None = None, net_width: float | None = None,
) -> float: ) -> float:
static_obstacles = self._static_obstacles static_obstacles = self._static_obstacles
tree: STRtree | None
is_rect_array: numpy.ndarray | None
bounds_array: numpy.ndarray | None
radians = numpy.radians(angle_deg) radians = numpy.radians(angle_deg)
cos_v, sin_v = numpy.cos(radians), numpy.sin(radians) cos_v, sin_v = numpy.cos(radians), numpy.sin(radians)
@ -391,7 +404,7 @@ class RoutingWorld:
is_rect_array = static_obstacles.is_rect_array is_rect_array = static_obstacles.is_rect_array
bounds_array = static_obstacles.bounds_array bounds_array = static_obstacles.bounds_array
if tree is None: if tree is None or is_rect_array is None or bounds_array is None:
return max_dist return max_dist
candidates = tree.query(box(min_x, min_y, max_x, max_y)) candidates = tree.query(box(min_x, min_y, max_x, max_y))

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from shapely.geometry import Polygon from shapely.geometry import Polygon
from inire.geometry.components import ComponentResult from inire.geometry.components import ComponentResult
@ -13,8 +14,8 @@ def components_overlap(
component_b: ComponentResult, component_b: ComponentResult,
prefer_actual: bool = False, prefer_actual: bool = False,
) -> bool: ) -> bool:
polygons_a: tuple[Polygon, ...] polygons_a: Sequence[Polygon]
polygons_b: tuple[Polygon, ...] polygons_b: Sequence[Polygon]
if prefer_actual: if prefer_actual:
polygons_a = component_a.physical_geometry polygons_a = component_a.physical_geometry
polygons_b = component_b.physical_geometry polygons_b = component_b.physical_geometry

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Literal from typing import TYPE_CHECKING, Literal
import numpy import numpy
from shapely.affinity import rotate as shapely_rotate from shapely.affinity import rotate as shapely_rotate
@ -13,6 +13,9 @@ from inire.constants import TOLERANCE_ANGULAR
from inire.seeds import Bend90Seed, PathSegmentSeed, SBendSeed, StraightSeed from inire.seeds import Bend90Seed, PathSegmentSeed, SBendSeed, StraightSeed
from .primitives import Port, rotation_matrix2 from .primitives import Port, rotation_matrix2
if TYPE_CHECKING:
from collections.abc import Sequence
MoveKind = Literal["straight", "bend90", "sbend"] MoveKind = Literal["straight", "bend90", "sbend"]
BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"] BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"]
@ -27,14 +30,14 @@ def _normalize_length(value: float) -> float:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class ComponentResult: class ComponentResult:
start_port: Port start_port: Port
collision_geometry: tuple[Polygon, ...] collision_geometry: Sequence[Polygon]
end_port: Port end_port: Port
length: float length: float
move_type: MoveKind move_type: MoveKind
move_spec: PathSegmentSeed move_spec: PathSegmentSeed
physical_geometry: tuple[Polygon, ...] physical_geometry: Sequence[Polygon]
dilated_collision_geometry: tuple[Polygon, ...] dilated_collision_geometry: Sequence[Polygon]
dilated_physical_geometry: tuple[Polygon, ...] dilated_physical_geometry: Sequence[Polygon]
_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False) _bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False)
_total_bounds: tuple[float, float, float, float] = field(init=False, repr=False) _total_bounds: tuple[float, float, float, float] = field(init=False, repr=False)
_dilated_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False) _dilated_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False)
@ -146,7 +149,7 @@ def _clip_bbox_legacy(
minx, miny, maxx, maxy = arc_poly.bounds minx, miny, maxx, maxy = arc_poly.bounds
bbox_poly = box(minx, miny, maxx, maxy) bbox_poly = box(minx, miny, maxx, maxy)
shrink = min(clip_margin, max(radius, width)) shrink = min(clip_margin, max(radius, width))
return bbox_poly.buffer(-shrink, join_style=2) if shrink > 0 else bbox_poly return bbox_poly.buffer(-shrink, join_style="mitre") if shrink > 0 else bbox_poly
def _clip_bbox_polygonal(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon: def _clip_bbox_polygonal(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon:

View file

@ -1,17 +1,18 @@
from __future__ import annotations from __future__ import annotations
import math import math
from collections.abc import Iterator, Mapping from typing import TYPE_CHECKING
from typing import TypeVar
import numpy import numpy
GeometryT = TypeVar("GeometryT") if TYPE_CHECKING:
from collections.abc import Iterator, Mapping
from shapely.geometry.base import BaseGeometry
def build_index_payload( def build_index_payload(
geometries: Mapping[int, GeometryT], geometries: Mapping[int, BaseGeometry],
) -> tuple[list[int], list[GeometryT], numpy.ndarray]: ) -> tuple[list[int], list[BaseGeometry], numpy.ndarray]:
obj_ids = sorted(geometries) obj_ids = sorted(geometries)
ordered_geometries = [geometries[obj_id] for obj_id in obj_ids] ordered_geometries = [geometries[obj_id] for obj_id in obj_ids]
bounds_array = numpy.array([geometry.bounds for geometry in ordered_geometries], dtype=numpy.float64) bounds_array = numpy.array([geometry.bounds for geometry in ordered_geometries], dtype=numpy.float64)
@ -42,7 +43,7 @@ def iter_grid_cells(
yield (gx, gy) yield (gx, gy)
def is_axis_aligned_rect(geometry, *, tolerance: float = 1e-4) -> bool: def is_axis_aligned_rect(geometry: BaseGeometry, *, tolerance: float = 1e-4) -> bool:
bounds = geometry.bounds bounds = geometry.bounds
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1]) area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
return abs(geometry.area - area) < tolerance return abs(geometry.area - area) < tolerance

View file

@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Self from typing import TYPE_CHECKING, Self
import numpy import numpy
from numpy.typing import NDArray
if TYPE_CHECKING:
from numpy.typing import NDArray
def _normalize_angle(angle_deg: int | float) -> int: def _normalize_angle(angle_deg: int | float) -> int:
@ -58,6 +60,6 @@ ROT2_180 = numpy.array(((-1, 0), (0, -1)), dtype=numpy.int32)
ROT2_270 = numpy.array(((0, 1), (-1, 0)), dtype=numpy.int32) ROT2_270 = numpy.array(((0, 1), (-1, 0)), dtype=numpy.int32)
def rotation_matrix2(rotation_deg: int) -> NDArray[numpy.int32]: def rotation_matrix2(rotation_deg: int | float) -> NDArray[numpy.int32]:
quadrant = (_normalize_angle(rotation_deg) // 90) % 4 quadrant = (_normalize_angle(rotation_deg) // 90) % 4
return (ROT2_0, ROT2_90, ROT2_180, ROT2_270)[quadrant] return (ROT2_0, ROT2_90, ROT2_180, ROT2_270)[quadrant]

View file

@ -59,7 +59,7 @@ class StaticObstacleIndex:
if dilated_geometry is not None: if dilated_geometry is not None:
dilated = dilated_geometry dilated = dilated_geometry
else: else:
dilated = polygon.buffer(self.engine.clearance / 2.0, join_style=2) dilated = polygon.buffer(self.engine.clearance / 2.0, join_style="mitre")
self.geometries[obj_id] = polygon self.geometries[obj_id] = polygon
self.dilated[obj_id] = dilated self.dilated[obj_id] = dilated
@ -109,7 +109,7 @@ class StaticObstacleIndex:
for obj_id in sorted(self.geometries.keys()): for obj_id in sorted(self.geometries.keys()):
polygon = self.geometries[obj_id] polygon = self.geometries[obj_id]
dilated = polygon.buffer(total_dilation, join_style=2) dilated = polygon.buffer(total_dilation, join_style="mitre")
geometries.append(dilated) geometries.append(dilated)
bounds_list.append(dilated.bounds) bounds_list.append(dilated.bounds)
is_rect_list.append(is_axis_aligned_rect(dilated)) is_rect_list.append(is_axis_aligned_rect(dilated))

View file

@ -5,11 +5,10 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from shapely.geometry import Polygon from shapely.geometry import Polygon
from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.seeds import PathSeed from inire.seeds import PathSeed
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.geometry.primitives import Port from inire.geometry.primitives import Port

View file

@ -70,7 +70,7 @@ class RoutingResult:
@property @property
def locked_geometry(self) -> tuple[Polygon, ...]: def locked_geometry(self) -> tuple[Polygon, ...]:
polygons = [] polygons: list[Polygon] = []
for component in self.path: for component in self.path:
polygons.extend(component.physical_geometry) polygons.extend(component.physical_geometry)
return tuple(polygons) return tuple(polygons)

View file

@ -94,7 +94,6 @@ def process_move(
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
context.move_cache_abs[abs_key] = res context.move_cache_abs[abs_key] = res
move_radius = params[0] if move_class == "bend90" else (params[1] if move_class == "sbend" else None)
add_node( add_node(
parent, parent,
res, res,

View file

@ -1,13 +1,16 @@
from __future__ import annotations from __future__ import annotations
import math import math
from typing import TYPE_CHECKING
from inire.constants import TOLERANCE_LINEAR from inire.constants import TOLERANCE_LINEAR
from inire.geometry.components import MoveKind
from inire.geometry.primitives import Port
from ._astar_admission import process_move from ._astar_admission import process_move
from ._astar_types import AStarContext, AStarMetrics, AStarNode, SearchRunConfig
if TYPE_CHECKING:
from inire.geometry.components import MoveKind
from inire.geometry.primitives import Port
from ._astar_types import AStarContext, AStarMetrics, AStarNode, SearchRunConfig
def _quantized_lengths(values: list[float], max_reach: float) -> list[int]: def _quantized_lengths(values: list[float], max_reach: float) -> list[int]:
@ -96,7 +99,7 @@ def _visible_straight_candidates(
return [] return []
collision_engine = context.cost_evaluator.collision_engine collision_engine = context.cost_evaluator.collision_engine
candidates: set[int] = set() tangent_candidates: set[int] = set()
for _, dist, length, dx, dy in sorted(scored)[:4]: for _, dist, length, dx, dy in sorted(scored)[:4]:
angle = math.degrees(math.atan2(dy, dx)) angle = math.degrees(math.atan2(dy, dx))
corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width) corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width)
@ -104,9 +107,9 @@ def _visible_straight_candidates(
continue continue
qlen = int(round(length)) qlen = int(round(length))
if qlen > 0: if qlen > 0:
candidates.add(qlen) tangent_candidates.add(qlen)
return sorted(candidates, reverse=True) return sorted(tangent_candidates, reverse=True)
def _previous_move_metadata(node: AStarNode) -> tuple[MoveKind | None, float | None]: def _previous_move_metadata(node: AStarNode) -> tuple[MoveKind | None, float | None]:

View file

@ -3,13 +3,14 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry from inire.model import resolve_bend_geometry
from inire.model import RoutingOptions, RoutingProblem, resolve_bend_geometry
from inire.results import RouteMetrics from inire.results import RouteMetrics
from inire.router.visibility import VisibilityManager from inire.router.visibility import VisibilityManager
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.components import ComponentResult from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry, ComponentResult
from inire.geometry.primitives import Port
from inire.model import RoutingOptions, RoutingProblem
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
@ -61,7 +62,7 @@ class AStarNode:
def __init__( def __init__(
self, self,
port, port: Port,
g_cost: float, g_cost: float,
h_cost: float, h_cost: float,
parent: AStarNode | None = None, parent: AStarNode | None = None,

View file

@ -15,6 +15,8 @@ from inire.router.refiner import PathRefiner
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from shapely.geometry import Polygon
from inire.geometry.components import ComponentResult from inire.geometry.components import ComponentResult
@ -48,8 +50,8 @@ class PathFinder:
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None: def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None:
all_geoms = [] all_geoms: list[Polygon] = []
all_dilated = [] all_dilated: list[Polygon] = []
for result in path: for result in path:
all_geoms.extend(result.collision_geometry) all_geoms.extend(result.collision_geometry)
all_dilated.extend(result.dilated_collision_geometry) all_dilated.extend(result.dilated_collision_geometry)
@ -215,7 +217,7 @@ class PathFinder:
return RoutingResult( return RoutingResult(
net_id=net_id, net_id=net_id,
path=path, path=tuple(path),
reached_target=reached_target, reached_target=reached_target,
report=RoutingReport() if report is None else report, report=RoutingReport() if report is None else report,
) )
@ -276,7 +278,7 @@ class PathFinder:
report = self.context.cost_evaluator.collision_engine.verify_path_report(net_id, refined_path) report = self.context.cost_evaluator.collision_engine.verify_path_report(net_id, refined_path)
state.results[net_id] = RoutingResult( state.results[net_id] = RoutingResult(
net_id=net_id, net_id=net_id,
path=refined_path, path=tuple(refined_path),
reached_target=result.reached_target, reached_target=result.reached_target,
report=report, report=report,
) )

View file

@ -4,13 +4,13 @@ import heapq
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from inire.constants import TOLERANCE_LINEAR from inire.constants import TOLERANCE_LINEAR
from inire.geometry.primitives import Port
from ._astar_moves import expand_moves as _expand_moves from ._astar_moves import expand_moves as _expand_moves
from ._astar_types import AStarContext, AStarMetrics, AStarNode as _AStarNode, SearchRunConfig from ._astar_types import AStarContext, AStarMetrics, AStarNode as _AStarNode, SearchRunConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.components import ComponentResult from inire.geometry.components import ComponentResult
from inire.geometry.primitives import Port
def _reconstruct_path(end_node: _AStarNode) -> list[ComponentResult]: def _reconstruct_path(end_node: _AStarNode) -> list[ComponentResult]:

View file

@ -1,17 +1,24 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING
from inire.model import RoutingOptions, RoutingProblem if TYPE_CHECKING:
from inire.geometry.collision import RoutingWorld
from inire.model import RoutingOptions, RoutingProblem
from inire.router._astar_types import AStarContext
from inire.router._router import PathFinder
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class RoutingStack: class RoutingStack:
world: object world: RoutingWorld
danger_map: object danger_map: DangerMap
evaluator: object evaluator: CostEvaluator
context: object context: AStarContext
finder: object finder: PathFinder
def build_routing_stack(problem: RoutingProblem, options: RoutingOptions) -> RoutingStack: def build_routing_stack(problem: RoutingProblem, options: RoutingOptions) -> RoutingStack:

View file

@ -8,6 +8,7 @@ from inire.constants import TOLERANCE_LINEAR
from inire.model import ObjectiveWeights from inire.model import ObjectiveWeights
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.components import ComponentResult, MoveKind from inire.geometry.components import ComponentResult, MoveKind
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
@ -71,7 +72,7 @@ class CostEvaluator:
def set_target(self, target: Port) -> None: def set_target(self, target: Port) -> None:
self._target_x = target.x self._target_x = target.x
self._target_y = target.y self._target_y = target.y
self._target_r = target.r self._target_r = int(target.r)
rad = np.radians(target.r) rad = np.radians(target.r)
self._target_cos = np.cos(rad) self._target_cos = np.cos(rad)
self._target_sin = np.sin(rad) self._target_sin = np.sin(rad)
@ -176,7 +177,7 @@ class CostEvaluator:
def path_cost( def path_cost(
self, self,
start_port: Port, start_port: Port,
path: list[ComponentResult], path: Sequence[ComponentResult],
*, *,
weights: ObjectiveWeights | None = None, weights: ObjectiveWeights | None = None,
) -> float: ) -> float:

View file

@ -51,14 +51,14 @@ class DangerMap:
for poly in obstacles: for poly in obstacles:
# Sample exterior # Sample exterior
exterior = poly.exterior exterior = poly.exterior
dist = 0 dist = 0.0
while dist < exterior.length: while dist < exterior.length:
pt = exterior.interpolate(dist) pt = exterior.interpolate(dist)
all_points.append((pt.x, pt.y)) all_points.append((pt.x, pt.y))
dist += self.resolution dist += self.resolution
# Sample interiors (holes) # Sample interiors (holes)
for interior in poly.interiors: for interior in poly.interiors:
dist = 0 dist = 0.0
while dist < interior.length: while dist < interior.length:
pt = interior.interpolate(dist) pt = interior.interpolate(dist)
all_points.append((pt.x, pt.y)) all_points.append((pt.x, pt.y))

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Literal
from inire.geometry.component_overlap import components_overlap from inire.geometry.component_overlap import components_overlap
from inire.geometry.components import Bend90, Straight from inire.geometry.components import Bend90, Straight
@ -55,7 +55,7 @@ class PathRefiner:
ports.extend(comp.end_port for comp in path) ports.extend(comp.end_port for comp in path)
return ports return ports
def _to_local(self, start: Port, point: Port) -> tuple[int, int]: def _to_local(self, start: Port, point: Port) -> tuple[float, float]:
dx = point.x - start.x dx = point.x - start.x
dy = point.y - start.y dy = point.y - start.y
if start.r == 0: if start.r == 0:
@ -197,8 +197,8 @@ class PathRefiner:
if 0.01 < forward_length < min_straight - 0.01: if 0.01 < forward_length < min_straight - 0.01:
return None return None
first_dir = "CCW" if side_extent > 0 else "CW" first_dir: Literal["CW", "CCW"] = "CCW" if side_extent > 0 else "CW"
second_dir = "CW" if side_extent > 0 else "CCW" second_dir: Literal["CW", "CCW"] = "CW" if side_extent > 0 else "CCW"
dilation = self.collision_engine.clearance / 2.0 dilation = self.collision_engine.clearance / 2.0
path: list[ComponentResult] = [] path: list[ComponentResult] = []
@ -288,10 +288,10 @@ class PathRefiner:
net_id: str, net_id: str,
start: Port, start: Port,
net_width: float, net_width: float,
path: list[ComponentResult], path: Sequence[ComponentResult],
) -> list[ComponentResult]: ) -> list[ComponentResult]:
if not path: if not path:
return path return list(path)
path = list(path) path = list(path)

View file

@ -23,7 +23,7 @@ class VisibilityManager:
self.corners: list[tuple[float, float]] = [] self.corners: list[tuple[float, float]] = []
self.corner_index = rtree.index.Index() self.corner_index = rtree.index.Index()
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {} self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
self._point_visibility_cache: dict[tuple[int, int], list[tuple[float, float, float]]] = {} self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {}
self._built_static_version = -1 self._built_static_version = -1
self._build() self._build()

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from time import perf_counter from time import perf_counter
from typing import Callable from collections.abc import Callable
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box
@ -154,7 +154,7 @@ def run_example_02() -> ScenarioOutcome:
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
} }
widths = {net_id: 2.0 for net_id in netlist} widths = dict.fromkeys(netlist, 2.0)
_, _, _, pathfinder = _build_routing_stack( _, _, _, pathfinder = _build_routing_stack(
bounds=(0, 0, 100, 100), bounds=(0, 0, 100, 100),
netlist=netlist, netlist=netlist,
@ -232,7 +232,7 @@ def run_example_05() -> ScenarioOutcome:
"loop": (Port(100, 100, 90), Port(100, 80, 270)), "loop": (Port(100, 100, 90), Port(100, 80, 270)),
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)), "zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
} }
widths = {net_id: 2.0 for net_id in netlist} widths = dict.fromkeys(netlist, 2.0)
_, _, _, pathfinder = _build_routing_stack( _, _, _, pathfinder = _build_routing_stack(
bounds=(0, 0, 200, 200), bounds=(0, 0, 200, 200),
netlist=netlist, netlist=netlist,

View file

@ -1,6 +1,3 @@
import pytest
import numpy
from shapely.geometry import Polygon
from inire import CongestionOptions, RoutingOptions, RoutingProblem, SearchOptions from inire import CongestionOptions, RoutingOptions, RoutingProblem, SearchOptions
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
@ -10,7 +7,6 @@ from inire.router._astar_types import AStarContext
from inire.router._router import PathFinder from inire.router._router import PathFinder
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap from inire.router.danger_map import DangerMap
from inire import RoutingResult
def _build_pathfinder( def _build_pathfinder(
@ -45,19 +41,19 @@ def test_clearance_thresholds():
# Clearance = 2.0, Width = 2.0 # Clearance = 2.0, Width = 2.0
# Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0 # Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0
ce = RoutingWorld(clearance=2.0) ce = RoutingWorld(clearance=2.0)
# Net 1: Centerline at y=0 # Net 1: Centerline at y=0
p1 = Port(0, 0, 0) p1 = Port(0, 0, 0)
res1 = Straight.generate(p1, 50.0, width=2.0, dilation=1.0) res1 = Straight.generate(p1, 50.0, width=2.0, dilation=1.0)
ce.add_path("net1", res1.collision_geometry, dilated_geometry=res1.dilated_collision_geometry) ce.add_path("net1", res1.collision_geometry, dilated_geometry=res1.dilated_collision_geometry)
# Net 2: Parallel to Net 1 # Net 2: Parallel to Net 1
# 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK. # 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK.
p2_ok = Port(0, 5, 0) p2_ok = Port(0, 5, 0)
res2_ok = Straight.generate(p2_ok, 50.0, width=2.0, dilation=1.0) res2_ok = Straight.generate(p2_ok, 50.0, width=2.0, dilation=1.0)
report_ok = ce.verify_path_report("net2", [res2_ok]) report_ok = ce.verify_path_report("net2", [res2_ok])
assert report_ok.is_valid, f"Gap 3 should be valid, but got {report_ok.collision_count} collisions" assert report_ok.is_valid, f"Gap 3 should be valid, but got {report_ok.collision_count} collisions"
# 2. Exactly at: y=4.0. Gap = 4.0 - 2.0 = 2.0. OK. # 2. Exactly at: y=4.0. Gap = 4.0 - 2.0 = 2.0. OK.
p2_exact = Port(0, 4, 0) p2_exact = Port(0, 4, 0)
res2_exact = Straight.generate(p2_exact, 50.0, width=2.0, dilation=1.0) res2_exact = Straight.generate(p2_exact, 50.0, width=2.0, dilation=1.0)
@ -105,7 +101,7 @@ def test_verify_all_nets_cases():
# Reset engine # Reset engine
engine.remove_path("net1") engine.remove_path("net1")
engine.remove_path("net2") engine.remove_path("net2")
results_p = _build_pathfinder( results_p = _build_pathfinder(
evaluator, evaluator,
bounds=(0, 0, 100, 100), bounds=(0, 0, 100, 100),
@ -124,7 +120,7 @@ def test_verify_all_nets_cases():
} }
engine.remove_path("net3") engine.remove_path("net3")
engine.remove_path("net4") engine.remove_path("net4")
results_c = _build_pathfinder( results_c = _build_pathfinder(
evaluator, evaluator,
bounds=(0, 0, 100, 100), bounds=(0, 0, 100, 100),

View file

@ -43,15 +43,15 @@ def test_ray_cast_width_clearance() -> None:
# Clearance = 2.0um, Width = 2.0um. # Clearance = 2.0um, Width = 2.0um.
# Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um. # Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um.
engine = RoutingWorld(clearance=2.0) engine = RoutingWorld(clearance=2.0)
# Obstacle at x=10 to 20 # Obstacle at x=10 to 20
_install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0) _install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0)
# 1. Parallel move at x=6. Gap = 10 - 6 = 4.0. Clearly OK. # 1. Parallel move at x=6. Gap = 10 - 6 = 4.0. Clearly OK.
start_ok = Port(6, 50, 90) start_ok = Port(6, 50, 90)
reach_ok = engine.ray_cast(start_ok, 90, max_dist=10.0, net_width=2.0) reach_ok = engine.ray_cast(start_ok, 90, max_dist=10.0, net_width=2.0)
assert reach_ok >= 10.0 assert reach_ok >= 10.0
# 2. Parallel move at x=8. Gap = 10 - 8 = 2.0. COLLISION. # 2. Parallel move at x=8. Gap = 10 - 8 = 2.0. COLLISION.
start_fail = Port(8, 50, 90) start_fail = Port(8, 50, 90)
reach_fail = engine.ray_cast(start_fail, 90, max_dist=10.0, net_width=2.0) reach_fail = engine.ray_cast(start_fail, 90, max_dist=10.0, net_width=2.0)
@ -61,19 +61,19 @@ def test_ray_cast_width_clearance() -> None:
def test_check_move_static_clearance() -> None: def test_check_move_static_clearance() -> None:
engine = RoutingWorld(clearance=2.0) engine = RoutingWorld(clearance=2.0)
_install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0, dilation=1.0) _install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0, dilation=1.0)
# Straight move of length 10 at x=8 (Width 2.0) # Straight move of length 10 at x=8 (Width 2.0)
# Gap = 10 - 8 = 2.0 < 3.0. COLLISION. # Gap = 10 - 8 = 2.0 < 3.0. COLLISION.
start = Port(8, 0, 90) start = Port(8, 0, 90)
res = Straight.generate(start, 10.0, width=2.0, dilation=1.0) # dilation = C/2 res = Straight.generate(start, 10.0, width=2.0, dilation=1.0) # dilation = C/2
assert engine.check_move_static(res, start_port=start) assert engine.check_move_static(res, start_port=start)
# Move at x=7. Gap = 3.0 == minimum. OK. # Move at x=7. Gap = 3.0 == minimum. OK.
start_ok = Port(7, 0, 90) start_ok = Port(7, 0, 90)
res_ok = Straight.generate(start_ok, 10.0, width=2.0, dilation=1.0) res_ok = Straight.generate(start_ok, 10.0, width=2.0, dilation=1.0)
assert not engine.check_move_static(res_ok, start_port=start_ok) assert not engine.check_move_static(res_ok, start_port=start_ok)
# 3. Same exact-boundary case. # 3. Same exact-boundary case.
start_exact = Port(7, 0, 90) start_exact = Port(7, 0, 90)
res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0) res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0)

View file

@ -17,7 +17,7 @@ def test_cost_calculation() -> None:
p2 = Port(10, 10, 0) p2 = Port(10, 10, 0)
h = evaluator.h_manhattan(p1, p2) h = evaluator.h_manhattan(p1, p2)
# Manhattan distance = 20. # Manhattan distance = 20.
# Jog alignment penalty = 2*bp = 20. # Jog alignment penalty = 2*bp = 20.
# Side check penalty = 2*bp = 20. # Side check penalty = 2*bp = 20.
# Total = 1.1 * (20 + 40) = 66.0 # Total = 1.1 * (20 + 40) = 66.0
@ -56,25 +56,25 @@ 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)
dm = DangerMap(bounds, resolution=1.0, safety_threshold=10.0) dm = DangerMap(bounds, resolution=1.0, safety_threshold=10.0)
# Square obstacle at (100, 100) to (110, 110) # Square obstacle at (100, 100) to (110, 110)
obstacle = Polygon([(100, 100), (110, 100), (110, 110), (100, 110)]) obstacle = Polygon([(100, 100), (110, 100), (110, 110), (100, 110)])
dm.precompute([obstacle]) dm.precompute([obstacle])
# 1. High cost near boundary # 1. High cost near boundary
cost_near = dm.get_cost(100.5, 100.5) cost_near = dm.get_cost(100.5, 100.5)
assert cost_near > 1.0 assert cost_near > 1.0
# 2. Zero cost far away # 2. Zero cost far away
cost_far = dm.get_cost(500, 500) cost_far = dm.get_cost(500, 500)
assert cost_far == 0.0 assert cost_far == 0.0
# 3. Check cache usage (internal detail check) # 3. Check cache usage (internal detail check)
# We can check if calling it again is fast or just verify it returns same result # We can check if calling it again is fast or just verify it returns same result
cost_near_2 = dm.get_cost(100.5, 100.5) cost_near_2 = dm.get_cost(100.5, 100.5)
assert cost_near_2 == cost_near assert cost_near_2 == cost_near
assert len(dm._cost_cache) == 2 assert len(dm._cost_cache) == 2
# 4. Out of bounds # 4. Out of bounds
assert dm.get_cost(-1, -1) >= 1e12 assert dm.get_cost(-1, -1) >= 1e12

View file

@ -2,12 +2,15 @@ from __future__ import annotations
import os import os
import statistics import statistics
from collections.abc import Callable from typing import TYPE_CHECKING
import pytest import pytest
from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome
if TYPE_CHECKING:
from collections.abc import Callable
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
PERFORMANCE_REPEATS = 3 PERFORMANCE_REPEATS = 3

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, cast
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy import numpy
from shapely.geometry import MultiPolygon, Polygon from shapely.geometry import MultiPolygon, Polygon
@ -129,7 +129,7 @@ def plot_danger_map(
if ax is None: if ax is None:
fig, ax = plt.subplots(figsize=(10, 10)) fig, ax = plt.subplots(figsize=(10, 10))
else: else:
fig = ax.get_figure() fig = cast("Figure", ax.get_figure())
# Generate a temporary grid for visualization # Generate a temporary grid for visualization
res = resolution if resolution is not None else max(1.0, (danger_map.maxx - danger_map.minx) / 200.0) res = resolution if resolution is not None else max(1.0, (danger_map.maxx - danger_map.minx) / 200.0)
@ -155,12 +155,12 @@ def plot_danger_map(
# Need to transpose because grid is [x, y] and imshow expects [row, col] (y, x) # Need to transpose because grid is [x, y] and imshow expects [row, col] (y, x)
im = ax.imshow( im = ax.imshow(
grid.T, grid.T,
origin='lower', origin="lower",
extent=[danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy], extent=(danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy),
cmap='YlOrRd', cmap="YlOrRd",
alpha=0.6 alpha=0.6,
) )
plt.colorbar(im, ax=ax, label='Danger Cost') plt.colorbar(im, ax=ax, label="Danger Cost")
ax.set_title("Danger Map (Proximity Costs)") ax.set_title("Danger Map (Proximity Costs)")
return fig, ax return fig, ax
@ -176,7 +176,7 @@ def plot_expanded_nodes(
if ax is None: if ax is None:
fig, ax = plt.subplots(figsize=(10, 10)) fig, ax = plt.subplots(figsize=(10, 10))
else: else:
fig = ax.get_figure() fig = cast("Figure", ax.get_figure())
if not nodes: if not nodes:
return fig, ax return fig, ax
@ -206,7 +206,7 @@ def plot_expansion_density(
if ax is None: if ax is None:
fig, ax = plt.subplots(figsize=(12, 12)) fig, ax = plt.subplots(figsize=(12, 12))
else: else:
fig = ax.get_figure() fig = cast("Figure", ax.get_figure())
if not nodes: if not nodes:
ax.text(0.5, 0.5, "No Expansion Data", ha='center', va='center', transform=ax.transAxes) ax.text(0.5, 0.5, "No Expansion Data", ha='center', va='center', transform=ax.transAxes)
@ -224,14 +224,14 @@ def plot_expansion_density(
# Plot as image # Plot as image
im = ax.imshow( im = ax.imshow(
h.T, h.T,
origin='lower', origin="lower",
extent=[bounds[0], bounds[2], bounds[1], bounds[3]], extent=(bounds[0], bounds[2], bounds[1], bounds[3]),
cmap='plasma', cmap="plasma",
interpolation='nearest', interpolation="nearest",
alpha=0.7 alpha=0.7,
) )
plt.colorbar(im, ax=ax, label='Expansion Count') plt.colorbar(im, ax=ax, label="Expansion Count")
ax.set_title("Search Expansion Density") ax.set_title("Search Expansion Density")
ax.set_xlim(bounds[0], bounds[2]) ax.set_xlim(bounds[0], bounds[2])
ax.set_ylim(bounds[1], bounds[3]) ax.set_ylim(bounds[1], bounds[3])

View file

@ -74,6 +74,18 @@ lint.ignore = [
"TRY003", # Long exception message "TRY003", # Long exception message
] ]
[tool.ruff.lint.per-file-ignores]
"inire/tests/*.py" = ["ANN", "ARG005", "PT009"]
[tool.mypy]
python_version = "3.11"
warn_unused_configs = true
exclude = ["^examples/", "^inire/tests/"]
[[tool.mypy.overrides]]
module = ["scipy.*"]
ignore_missing_imports = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-rsXx" addopts = "-rsXx"
testpaths = ["inire"] testpaths = ["inire"]