From 1849075b119a29e7b72ee05a0feffd6217aedbbc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Mar 2026 23:54:30 -0700 Subject: [PATCH] linter fixes --- examples/09_unroutable_best_effort.py | 7 +++++- inire/geometry/collision.py | 27 ++++++++++++++++------ inire/geometry/component_overlap.py | 5 +++-- inire/geometry/components.py | 15 ++++++++----- inire/geometry/index_helpers.py | 13 ++++++----- inire/geometry/primitives.py | 8 ++++--- inire/geometry/static_obstacle_index.py | 4 ++-- inire/model.py | 3 +-- inire/results.py | 2 +- inire/router/_astar_admission.py | 1 - inire/router/_astar_moves.py | 15 ++++++++----- inire/router/_astar_types.py | 9 ++++---- inire/router/_router.py | 10 +++++---- inire/router/_search.py | 2 +- inire/router/_stack.py | 19 +++++++++++----- inire/router/cost.py | 5 +++-- inire/router/danger_map.py | 4 ++-- inire/router/refiner.py | 12 +++++----- inire/router/visibility.py | 2 +- inire/tests/example_scenarios.py | 6 ++--- inire/tests/test_clearance_precision.py | 14 +++++------- inire/tests/test_collision.py | 14 ++++++------ inire/tests/test_cost.py | 12 +++++----- inire/tests/test_example_performance.py | 5 ++++- inire/utils/visualization.py | 30 ++++++++++++------------- pyproject.toml | 12 ++++++++++ 26 files changed, 152 insertions(+), 104 deletions(-) diff --git a/examples/09_unroutable_best_effort.py b/examples/09_unroutable_best_effort.py index 1aeb152..227f5bf 100644 --- a/examples/09_unroutable_best_effort.py +++ b/examples/09_unroutable_best_effort.py @@ -37,7 +37,12 @@ def main() -> None: else: 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") print("Saved plot to examples/09_unroutable_best_effort.png") diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py index 5d5b13b..02aaede 100644 --- a/inire/geometry/collision.py +++ b/inire/geometry/collision.py @@ -105,9 +105,16 @@ class RoutingWorld: return reach < length - 0.001 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 - 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 bool( end_port @@ -176,15 +183,18 @@ class RoutingWorld: return False 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: return False - static_bounds = static_obstacles.bounds_array move_poly_bounds = result.dilated_bounds for hit_idx in hits: - obstacle_bounds = static_bounds[hit_idx] + obstacle_bounds = bounds_array[hit_idx] poly_hits_obstacle_aabb = False for poly_bounds in move_poly_bounds: if ( @@ -319,7 +329,7 @@ class RoutingWorld: raw_geometries = static_obstacles.raw_tree.geometries for component in components: 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") for hit_idx in hits: obstacle = raw_geometries[hit_idx] @@ -373,6 +383,9 @@ class RoutingWorld: net_width: float | None = None, ) -> float: static_obstacles = self._static_obstacles + tree: STRtree | None + is_rect_array: numpy.ndarray | None + bounds_array: numpy.ndarray | None radians = numpy.radians(angle_deg) cos_v, sin_v = numpy.cos(radians), numpy.sin(radians) @@ -391,7 +404,7 @@ class RoutingWorld: is_rect_array = static_obstacles.is_rect_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 candidates = tree.query(box(min_x, min_y, max_x, max_y)) diff --git a/inire/geometry/component_overlap.py b/inire/geometry/component_overlap.py index 816508d..f72603f 100644 --- a/inire/geometry/component_overlap.py +++ b/inire/geometry/component_overlap.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: + from collections.abc import Sequence from shapely.geometry import Polygon from inire.geometry.components import ComponentResult @@ -13,8 +14,8 @@ def components_overlap( component_b: ComponentResult, prefer_actual: bool = False, ) -> bool: - polygons_a: tuple[Polygon, ...] - polygons_b: tuple[Polygon, ...] + polygons_a: Sequence[Polygon] + polygons_b: Sequence[Polygon] if prefer_actual: polygons_a = component_a.physical_geometry polygons_b = component_b.physical_geometry diff --git a/inire/geometry/components.py b/inire/geometry/components.py index 48af961..86b7282 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Literal +from typing import TYPE_CHECKING, Literal import numpy 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 .primitives import Port, rotation_matrix2 +if TYPE_CHECKING: + from collections.abc import Sequence + MoveKind = Literal["straight", "bend90", "sbend"] BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"] @@ -27,14 +30,14 @@ def _normalize_length(value: float) -> float: @dataclass(frozen=True, slots=True) class ComponentResult: start_port: Port - collision_geometry: tuple[Polygon, ...] + collision_geometry: Sequence[Polygon] end_port: Port length: float move_type: MoveKind move_spec: PathSegmentSeed - physical_geometry: tuple[Polygon, ...] - dilated_collision_geometry: tuple[Polygon, ...] - dilated_physical_geometry: tuple[Polygon, ...] + physical_geometry: Sequence[Polygon] + dilated_collision_geometry: Sequence[Polygon] + dilated_physical_geometry: Sequence[Polygon] _bounds: tuple[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) @@ -146,7 +149,7 @@ def _clip_bbox_legacy( minx, miny, maxx, maxy = arc_poly.bounds bbox_poly = box(minx, miny, maxx, maxy) 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: diff --git a/inire/geometry/index_helpers.py b/inire/geometry/index_helpers.py index dad186b..201dafe 100644 --- a/inire/geometry/index_helpers.py +++ b/inire/geometry/index_helpers.py @@ -1,17 +1,18 @@ from __future__ import annotations import math -from collections.abc import Iterator, Mapping -from typing import TypeVar +from typing import TYPE_CHECKING import numpy -GeometryT = TypeVar("GeometryT") +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + from shapely.geometry.base import BaseGeometry def build_index_payload( - geometries: Mapping[int, GeometryT], -) -> tuple[list[int], list[GeometryT], numpy.ndarray]: + geometries: Mapping[int, BaseGeometry], +) -> tuple[list[int], list[BaseGeometry], numpy.ndarray]: obj_ids = sorted(geometries) 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) @@ -42,7 +43,7 @@ def iter_grid_cells( 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 area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1]) return abs(geometry.area - area) < tolerance diff --git a/inire/geometry/primitives.py b/inire/geometry/primitives.py index b42b267..db61198 100644 --- a/inire/geometry/primitives.py +++ b/inire/geometry/primitives.py @@ -1,10 +1,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Self +from typing import TYPE_CHECKING, Self import numpy -from numpy.typing import NDArray + +if TYPE_CHECKING: + from numpy.typing import NDArray 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) -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 return (ROT2_0, ROT2_90, ROT2_180, ROT2_270)[quadrant] diff --git a/inire/geometry/static_obstacle_index.py b/inire/geometry/static_obstacle_index.py index 3f3ab38..32cffba 100644 --- a/inire/geometry/static_obstacle_index.py +++ b/inire/geometry/static_obstacle_index.py @@ -59,7 +59,7 @@ class StaticObstacleIndex: if dilated_geometry is not None: dilated = dilated_geometry 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.dilated[obj_id] = dilated @@ -109,7 +109,7 @@ class StaticObstacleIndex: for obj_id in sorted(self.geometries.keys()): polygon = self.geometries[obj_id] - dilated = polygon.buffer(total_dilation, join_style=2) + dilated = polygon.buffer(total_dilation, join_style="mitre") geometries.append(dilated) bounds_list.append(dilated.bounds) is_rect_list.append(is_axis_aligned_rect(dilated)) diff --git a/inire/model.py b/inire/model.py index f0607ae..05e923a 100644 --- a/inire/model.py +++ b/inire/model.py @@ -5,11 +5,10 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Literal from shapely.geometry import Polygon - -from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry from inire.seeds import PathSeed if TYPE_CHECKING: + from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry from inire.geometry.primitives import Port diff --git a/inire/results.py b/inire/results.py index d16e92c..e9d178b 100644 --- a/inire/results.py +++ b/inire/results.py @@ -70,7 +70,7 @@ class RoutingResult: @property def locked_geometry(self) -> tuple[Polygon, ...]: - polygons = [] + polygons: list[Polygon] = [] for component in self.path: polygons.extend(component.physical_geometry) return tuple(polygons) diff --git a/inire/router/_astar_admission.py b/inire/router/_astar_admission.py index 8152afb..d10a755 100644 --- a/inire/router/_astar_admission.py +++ b/inire/router/_astar_admission.py @@ -94,7 +94,6 @@ def process_move( res = res_rel.translate(cp.x, cp.y) 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( parent, res, diff --git a/inire/router/_astar_moves.py b/inire/router/_astar_moves.py index 71ca920..56aae96 100644 --- a/inire/router/_astar_moves.py +++ b/inire/router/_astar_moves.py @@ -1,13 +1,16 @@ from __future__ import annotations import math +from typing import TYPE_CHECKING 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_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]: @@ -96,7 +99,7 @@ def _visible_straight_candidates( return [] 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]: angle = math.degrees(math.atan2(dy, dx)) 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 qlen = int(round(length)) 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]: diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py index 2796c73..96f9aa9 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -3,13 +3,14 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry -from inire.model import RoutingOptions, RoutingProblem, resolve_bend_geometry +from inire.model import resolve_bend_geometry from inire.results import RouteMetrics from inire.router.visibility import VisibilityManager 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 @@ -61,7 +62,7 @@ class AStarNode: def __init__( self, - port, + port: Port, g_cost: float, h_cost: float, parent: AStarNode | None = None, diff --git a/inire/router/_router.py b/inire/router/_router.py index 81bc6ef..a52d8d6 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -15,6 +15,8 @@ from inire.router.refiner import PathRefiner if TYPE_CHECKING: from collections.abc import Callable, Sequence + from shapely.geometry import Polygon + from inire.geometry.components import ComponentResult @@ -48,8 +50,8 @@ class PathFinder: self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None: - all_geoms = [] - all_dilated = [] + all_geoms: list[Polygon] = [] + all_dilated: list[Polygon] = [] for result in path: all_geoms.extend(result.collision_geometry) all_dilated.extend(result.dilated_collision_geometry) @@ -215,7 +217,7 @@ class PathFinder: return RoutingResult( net_id=net_id, - path=path, + path=tuple(path), reached_target=reached_target, 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) state.results[net_id] = RoutingResult( net_id=net_id, - path=refined_path, + path=tuple(refined_path), reached_target=result.reached_target, report=report, ) diff --git a/inire/router/_search.py b/inire/router/_search.py index 2cf7daa..8e7a0ab 100644 --- a/inire/router/_search.py +++ b/inire/router/_search.py @@ -4,13 +4,13 @@ import heapq from typing import TYPE_CHECKING from inire.constants import TOLERANCE_LINEAR -from inire.geometry.primitives import Port from ._astar_moves import expand_moves as _expand_moves from ._astar_types import AStarContext, AStarMetrics, AStarNode as _AStarNode, SearchRunConfig if TYPE_CHECKING: from inire.geometry.components import ComponentResult + from inire.geometry.primitives import Port def _reconstruct_path(end_node: _AStarNode) -> list[ComponentResult]: diff --git a/inire/router/_stack.py b/inire/router/_stack.py index 71aa119..9bf67a2 100644 --- a/inire/router/_stack.py +++ b/inire/router/_stack.py @@ -1,17 +1,24 @@ from __future__ import annotations 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) class RoutingStack: - world: object - danger_map: object - evaluator: object - context: object - finder: object + world: RoutingWorld + danger_map: DangerMap + evaluator: CostEvaluator + context: AStarContext + finder: PathFinder def build_routing_stack(problem: RoutingProblem, options: RoutingOptions) -> RoutingStack: diff --git a/inire/router/cost.py b/inire/router/cost.py index c4b62c3..eaf9d66 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -8,6 +8,7 @@ from inire.constants import TOLERANCE_LINEAR from inire.model import ObjectiveWeights if TYPE_CHECKING: + from collections.abc import Sequence from inire.geometry.collision import RoutingWorld from inire.geometry.components import ComponentResult, MoveKind from inire.geometry.primitives import Port @@ -71,7 +72,7 @@ class CostEvaluator: def set_target(self, target: Port) -> None: self._target_x = target.x self._target_y = target.y - self._target_r = target.r + self._target_r = int(target.r) rad = np.radians(target.r) self._target_cos = np.cos(rad) self._target_sin = np.sin(rad) @@ -176,7 +177,7 @@ class CostEvaluator: def path_cost( self, start_port: Port, - path: list[ComponentResult], + path: Sequence[ComponentResult], *, weights: ObjectiveWeights | None = None, ) -> float: diff --git a/inire/router/danger_map.py b/inire/router/danger_map.py index 03b8a2a..12b3b14 100644 --- a/inire/router/danger_map.py +++ b/inire/router/danger_map.py @@ -51,14 +51,14 @@ class DangerMap: for poly in obstacles: # Sample exterior exterior = poly.exterior - dist = 0 + dist = 0.0 while dist < exterior.length: pt = exterior.interpolate(dist) all_points.append((pt.x, pt.y)) dist += self.resolution # Sample interiors (holes) for interior in poly.interiors: - dist = 0 + dist = 0.0 while dist < interior.length: pt = interior.interpolate(dist) all_points.append((pt.x, pt.y)) diff --git a/inire/router/refiner.py b/inire/router/refiner.py index 6aa5d1f..ee9c9e9 100644 --- a/inire/router/refiner.py +++ b/inire/router/refiner.py @@ -1,7 +1,7 @@ from __future__ import annotations 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.components import Bend90, Straight @@ -55,7 +55,7 @@ class PathRefiner: ports.extend(comp.end_port for comp in path) 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 dy = point.y - start.y if start.r == 0: @@ -197,8 +197,8 @@ class PathRefiner: if 0.01 < forward_length < min_straight - 0.01: return None - first_dir = "CCW" if side_extent > 0 else "CW" - second_dir = "CW" if side_extent > 0 else "CCW" + first_dir: Literal["CW", "CCW"] = "CCW" if side_extent > 0 else "CW" + second_dir: Literal["CW", "CCW"] = "CW" if side_extent > 0 else "CCW" dilation = self.collision_engine.clearance / 2.0 path: list[ComponentResult] = [] @@ -288,10 +288,10 @@ class PathRefiner: net_id: str, start: Port, net_width: float, - path: list[ComponentResult], + path: Sequence[ComponentResult], ) -> list[ComponentResult]: if not path: - return path + return list(path) path = list(path) diff --git a/inire/router/visibility.py b/inire/router/visibility.py index ed83d00..d149f42 100644 --- a/inire/router/visibility.py +++ b/inire/router/visibility.py @@ -23,7 +23,7 @@ class VisibilityManager: self.corners: list[tuple[float, float]] = [] self.corner_index = rtree.index.Index() 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._build() diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index 2dd78ed..bad876c 100644 --- a/inire/tests/example_scenarios.py +++ b/inire/tests/example_scenarios.py @@ -1,7 +1,7 @@ from __future__ import annotations from time import perf_counter -from typing import Callable +from collections.abc import Callable 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_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( bounds=(0, 0, 100, 100), netlist=netlist, @@ -232,7 +232,7 @@ def run_example_05() -> ScenarioOutcome: "loop": (Port(100, 100, 90), Port(100, 80, 270)), "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( bounds=(0, 0, 200, 200), netlist=netlist, diff --git a/inire/tests/test_clearance_precision.py b/inire/tests/test_clearance_precision.py index 67264cc..2829d8c 100644 --- a/inire/tests/test_clearance_precision.py +++ b/inire/tests/test_clearance_precision.py @@ -1,6 +1,3 @@ -import pytest -import numpy -from shapely.geometry import Polygon from inire import CongestionOptions, RoutingOptions, RoutingProblem, SearchOptions from inire.geometry.collision import RoutingWorld 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.cost import CostEvaluator from inire.router.danger_map import DangerMap -from inire import RoutingResult def _build_pathfinder( @@ -45,19 +41,19 @@ def test_clearance_thresholds(): # Clearance = 2.0, Width = 2.0 # Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0 ce = RoutingWorld(clearance=2.0) - + # Net 1: Centerline at y=0 p1 = Port(0, 0, 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) - + # Net 2: Parallel to Net 1 # 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK. p2_ok = Port(0, 5, 0) res2_ok = Straight.generate(p2_ok, 50.0, width=2.0, dilation=1.0) 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" - + # 2. Exactly at: y=4.0. Gap = 4.0 - 2.0 = 2.0. OK. p2_exact = Port(0, 4, 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 engine.remove_path("net1") engine.remove_path("net2") - + results_p = _build_pathfinder( evaluator, bounds=(0, 0, 100, 100), @@ -124,7 +120,7 @@ def test_verify_all_nets_cases(): } engine.remove_path("net3") engine.remove_path("net4") - + results_c = _build_pathfinder( evaluator, bounds=(0, 0, 100, 100), diff --git a/inire/tests/test_collision.py b/inire/tests/test_collision.py index 7eb0e4f..284055d 100644 --- a/inire/tests/test_collision.py +++ b/inire/tests/test_collision.py @@ -43,15 +43,15 @@ def test_ray_cast_width_clearance() -> None: # Clearance = 2.0um, Width = 2.0um. # Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um. engine = RoutingWorld(clearance=2.0) - + # Obstacle at x=10 to 20 _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. start_ok = Port(6, 50, 90) reach_ok = engine.ray_cast(start_ok, 90, max_dist=10.0, net_width=2.0) assert reach_ok >= 10.0 - + # 2. Parallel move at x=8. Gap = 10 - 8 = 2.0. COLLISION. start_fail = Port(8, 50, 90) 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: engine = RoutingWorld(clearance=2.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) # Gap = 10 - 8 = 2.0 < 3.0. COLLISION. start = Port(8, 0, 90) res = Straight.generate(start, 10.0, width=2.0, dilation=1.0) # dilation = C/2 - + assert engine.check_move_static(res, start_port=start) - + # Move at x=7. Gap = 3.0 == minimum. OK. start_ok = Port(7, 0, 90) 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) - + # 3. Same exact-boundary case. start_exact = Port(7, 0, 90) res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0) diff --git a/inire/tests/test_cost.py b/inire/tests/test_cost.py index 6eef3bc..05976a9 100644 --- a/inire/tests/test_cost.py +++ b/inire/tests/test_cost.py @@ -17,7 +17,7 @@ def test_cost_calculation() -> None: p2 = Port(10, 10, 0) h = evaluator.h_manhattan(p1, p2) - # Manhattan distance = 20. + # Manhattan distance = 20. # Jog alignment penalty = 2*bp = 20. # Side check penalty = 2*bp = 20. # 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 bounds = (0, 0, 1000, 1000) dm = DangerMap(bounds, resolution=1.0, safety_threshold=10.0) - + # Square obstacle at (100, 100) to (110, 110) obstacle = Polygon([(100, 100), (110, 100), (110, 110), (100, 110)]) dm.precompute([obstacle]) - + # 1. High cost near boundary cost_near = dm.get_cost(100.5, 100.5) assert cost_near > 1.0 - + # 2. Zero cost far away cost_far = dm.get_cost(500, 500) assert cost_far == 0.0 - + # 3. Check cache usage (internal detail check) # 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) assert cost_near_2 == cost_near assert len(dm._cost_cache) == 2 - + # 4. Out of bounds assert dm.get_cost(-1, -1) >= 1e12 diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py index 80d56aa..2d44d11 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -2,12 +2,15 @@ from __future__ import annotations import os import statistics -from collections.abc import Callable +from typing import TYPE_CHECKING import pytest 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" PERFORMANCE_REPEATS = 3 diff --git a/inire/utils/visualization.py b/inire/utils/visualization.py index 8268c47..d828d1e 100644 --- a/inire/utils/visualization.py +++ b/inire/utils/visualization.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import matplotlib.pyplot as plt import numpy from shapely.geometry import MultiPolygon, Polygon @@ -129,7 +129,7 @@ def plot_danger_map( if ax is None: fig, ax = plt.subplots(figsize=(10, 10)) else: - fig = ax.get_figure() + fig = cast("Figure", ax.get_figure()) # 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) @@ -155,12 +155,12 @@ def plot_danger_map( # Need to transpose because grid is [x, y] and imshow expects [row, col] (y, x) im = ax.imshow( grid.T, - origin='lower', - extent=[danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy], - cmap='YlOrRd', - alpha=0.6 + origin="lower", + extent=(danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy), + cmap="YlOrRd", + 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)") return fig, ax @@ -176,7 +176,7 @@ def plot_expanded_nodes( if ax is None: fig, ax = plt.subplots(figsize=(10, 10)) else: - fig = ax.get_figure() + fig = cast("Figure", ax.get_figure()) if not nodes: return fig, ax @@ -206,7 +206,7 @@ def plot_expansion_density( if ax is None: fig, ax = plt.subplots(figsize=(12, 12)) else: - fig = ax.get_figure() + fig = cast("Figure", ax.get_figure()) if not nodes: 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 im = ax.imshow( h.T, - origin='lower', - extent=[bounds[0], bounds[2], bounds[1], bounds[3]], - cmap='plasma', - interpolation='nearest', - alpha=0.7 + origin="lower", + extent=(bounds[0], bounds[2], bounds[1], bounds[3]), + cmap="plasma", + interpolation="nearest", + 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_xlim(bounds[0], bounds[2]) ax.set_ylim(bounds[1], bounds[3]) diff --git a/pyproject.toml b/pyproject.toml index efbd939..e4031c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,18 @@ lint.ignore = [ "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] addopts = "-rsXx" testpaths = ["inire"]