inire/inire/model.py

140 lines
4.9 KiB
Python

from __future__ import annotations
import warnings
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Literal
from shapely.geometry import Polygon
from inire.seeds import PathSeed
if TYPE_CHECKING:
from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry
from inire.geometry.primitives import Port
NetOrder = Literal["user", "shortest", "longest"]
VisibilityGuidance = Literal["off", "exact_corner", "tangent_corner"]
@dataclass(frozen=True, slots=True)
class NetSpec:
net_id: str
start: Port
target: Port
width: float = 2.0
@dataclass(frozen=True, slots=True)
class ObjectiveWeights:
unit_length_cost: float = 1.0
bend_penalty: float = 250.0
sbend_penalty: float = 500.0
danger_weight: float = 1.0
@dataclass(frozen=True, slots=True)
class SearchOptions:
node_limit: int = 1000000
max_straight_length: float = 2000.0
min_straight_length: float = 5.0
greedy_h_weight: float = 1.5
sbend_offsets: tuple[float, ...] | None = None
bend_radii: tuple[float, ...] = (50.0, 100.0)
sbend_radii: tuple[float, ...] = (10.0,)
bend_collision_type: BendCollisionModel = "arc"
bend_proxy_geometry: BendCollisionModel | None = None
bend_physical_geometry: BendPhysicalGeometry | None = None
bend_clip_margin: float | None = None
visibility_guidance: VisibilityGuidance = "tangent_corner"
def __post_init__(self) -> None:
object.__setattr__(self, "bend_radii", tuple(self.bend_radii))
object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii))
if self.sbend_offsets is not None:
object.__setattr__(self, "sbend_offsets", tuple(self.sbend_offsets))
if self.bend_physical_geometry is None and isinstance(self.bend_proxy_geometry, Polygon):
warnings.warn(
"Custom bend proxy provided without bend_physical_geometry; routed bends will keep standard arc geometry.",
stacklevel=2,
)
def resolve_bend_geometry(
search: SearchOptions,
*,
bend_collision_override: BendCollisionModel | None = None,
) -> tuple[BendCollisionModel, BendPhysicalGeometry]:
bend_physical_geometry = search.bend_physical_geometry
if bend_physical_geometry is None and isinstance(search.bend_collision_type, Polygon) and search.bend_proxy_geometry is None:
bend_physical_geometry = search.bend_collision_type
if bend_physical_geometry is None:
bend_physical_geometry = "arc"
if bend_collision_override is not None:
bend_proxy_geometry = bend_collision_override
elif search.bend_proxy_geometry is not None:
bend_proxy_geometry = search.bend_proxy_geometry
elif isinstance(search.bend_collision_type, Polygon):
bend_proxy_geometry = search.bend_collision_type
elif bend_physical_geometry != "arc" and search.bend_collision_type == "arc":
bend_proxy_geometry = bend_physical_geometry
else:
bend_proxy_geometry = search.bend_collision_type
return bend_proxy_geometry, bend_physical_geometry
@dataclass(frozen=True, slots=True)
class CongestionOptions:
max_iterations: int = 10
base_penalty: float = 100.0
multiplier: float = 1.5
use_tiered_strategy: bool = True
net_order: NetOrder = "user"
warm_start_enabled: bool = True
shuffle_nets: bool = False
seed: int | None = None
@dataclass(frozen=True, slots=True)
class RefinementOptions:
enabled: bool = True
objective: ObjectiveWeights | None = None
@dataclass(frozen=True, slots=True)
class DiagnosticsOptions:
capture_expanded: bool = False
capture_conflict_trace: bool = False
capture_frontier_trace: bool = False
@dataclass(frozen=True, slots=True)
class RoutingOptions:
search: SearchOptions = field(default_factory=SearchOptions)
objective: ObjectiveWeights = field(default_factory=ObjectiveWeights)
congestion: CongestionOptions = field(default_factory=CongestionOptions)
refinement: RefinementOptions = field(default_factory=RefinementOptions)
diagnostics: DiagnosticsOptions = field(default_factory=DiagnosticsOptions)
@dataclass(frozen=True, slots=True)
class RoutingProblem:
bounds: tuple[float, float, float, float]
nets: tuple[NetSpec, ...] = ()
static_obstacles: tuple[Polygon, ...] = ()
initial_paths: dict[str, PathSeed] = field(default_factory=dict)
clearance: float = 2.0
safety_zone_radius: float = 0.0021
def __post_init__(self) -> None:
object.__setattr__(self, "nets", tuple(self.nets))
object.__setattr__(self, "static_obstacles", tuple(self.static_obstacles))
initial_paths = dict(self.initial_paths)
if any(not isinstance(seed, PathSeed) for seed in initial_paths.values()):
raise TypeError("RoutingProblem.initial_paths values must be PathSeed instances")
object.__setattr__(
self,
"initial_paths",
initial_paths,
)