Initial buildout
This commit is contained in:
parent
34615f3aac
commit
f600b52f32
25 changed files with 1856 additions and 23 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,3 +8,5 @@ wheels/
|
|||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.hypothesis
|
||||
|
|
|
|||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
69
README.md
69
README.md
|
|
@ -0,0 +1,69 @@
|
|||
# inire: Auto-Routing for Photonic and RF Integrated Circuits
|
||||
|
||||
`inire` is a high-performance auto-router designed specifically for the physical constraints of photonic and RF integrated circuits. It utilizes a Hybrid State-Lattice A* search combined with "Negotiated Congestion" (PathFinder) to route multiple nets while maintaining strict geometric fidelity and clearance.
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Hybrid State-Lattice Search**: Routes using discrete 90° bends and parametric S-bends, ensuring manufacturing-stable paths.
|
||||
* **Negotiated Congestion**: Iteratively resolves multi-net bottlenecks by inflating costs in high-traffic regions.
|
||||
* **Analytic Correctness**: Every move is verified against an R-Tree spatial index of obstacles and other paths.
|
||||
* **1nm Precision**: All coordinates and ports are snapped to a 1nm manufacturing grid.
|
||||
* **Safety & Proximity**: Incorporates a "Danger Map" (pre-computed distance transform) to maintain optimal spacing and reduce crosstalk.
|
||||
* **Locked Paths**: Supports treating existing geometries as fixed obstacles for incremental routing sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
`inire` requires Python 3.11+. You can install the dependencies using `uv` (recommended) or `pip`:
|
||||
|
||||
```bash
|
||||
# Using uv
|
||||
uv sync
|
||||
|
||||
# Using pip
|
||||
pip install numpy scipy shapely rtree matplotlib
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
# 1. Setup Environment
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 1000, 1000))
|
||||
danger_map.precompute([]) # Add polygons here for obstacles
|
||||
|
||||
# 2. Configure Router
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 3. Define Netlist
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(100, 50, 0)),
|
||||
}
|
||||
|
||||
# 4. Route
|
||||
results = pf.route_all(netlist, {"net1": 2.0})
|
||||
|
||||
if results["net1"].is_valid:
|
||||
print("Successfully routed net1!")
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
`inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types:
|
||||
1. **Straights**: Variable-length segments.
|
||||
2. **90° Bends**: Fixed-radius PDK cells.
|
||||
3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$).
|
||||
|
||||
For multi-net problems, the **PathFinder** loop handles rip-up and reroute logic, ensuring that paths find the globally optimal configuration without crossings.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU Affero General Public License v3. See `LICENSE.md` for details.
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
"""
|
||||
inire Wave-router
|
||||
"""
|
||||
from .geometry.primitives import Port as Port # noqa: PLC0414
|
||||
from .geometry.components import Straight as Straight, Bend90 as Bend90, SBend as SBend # noqa: PLC0414
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
__version__ = '0.1'
|
||||
|
|
|
|||
140
inire/geometry/collision.py
Normal file
140
inire/geometry/collision.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import rtree
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
from shapely.prepared import prep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.prepared import PreparedGeometry
|
||||
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
|
||||
class CollisionEngine:
|
||||
"""Manages spatial queries for collision detection."""
|
||||
|
||||
def __init__(self, clearance: float, max_net_width: float = 2.0) -> None:
|
||||
self.clearance = clearance
|
||||
self.max_net_width = max_net_width
|
||||
self.static_obstacles = rtree.index.Index()
|
||||
# To store geometries for precise checks
|
||||
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||
self.prepared_obstacles: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
|
||||
self._id_counter = 0
|
||||
|
||||
# Dynamic paths for multi-net congestion
|
||||
self.dynamic_paths = rtree.index.Index()
|
||||
# obj_id -> (net_id, geometry)
|
||||
self.path_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
self._dynamic_id_counter = 0
|
||||
|
||||
def add_static_obstacle(self, polygon: Polygon, pre_dilate: bool = True) -> None:
|
||||
"""Add a static obstacle to the engine."""
|
||||
_ = pre_dilate # Keep for API compatibility
|
||||
obj_id = self._id_counter
|
||||
self._id_counter += 1
|
||||
|
||||
self.obstacle_geometries[obj_id] = polygon
|
||||
self.prepared_obstacles[obj_id] = prep(polygon)
|
||||
|
||||
# Index the bounding box of the polygon (dilated for broad prune)
|
||||
# Spec: "All user-provided obstacles are pre-dilated by (W_max + C)/2"
|
||||
dilation = (self.max_net_width + self.clearance) / 2.0
|
||||
dilated_bounds = (
|
||||
polygon.bounds[0] - dilation,
|
||||
polygon.bounds[1] - dilation,
|
||||
polygon.bounds[2] + dilation,
|
||||
polygon.bounds[3] + dilation,
|
||||
)
|
||||
self.static_obstacles.insert(obj_id, dilated_bounds)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon]) -> None:
|
||||
"""Add a net's routed path to the dynamic R-Tree."""
|
||||
# Dilate by clearance/2 for congestion
|
||||
dilation = self.clearance / 2.0
|
||||
for poly in geometry:
|
||||
dilated = poly.buffer(dilation)
|
||||
obj_id = self._dynamic_id_counter
|
||||
self._dynamic_id_counter += 1
|
||||
self.path_geometries[obj_id] = (net_id, dilated)
|
||||
self.dynamic_paths.insert(obj_id, dilated.bounds)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
"""Remove a net's path from the dynamic R-Tree."""
|
||||
to_remove = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
|
||||
for obj_id in to_remove:
|
||||
nid, dilated = self.path_geometries.pop(obj_id)
|
||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
||||
|
||||
def lock_net(self, net_id: str) -> None:
|
||||
"""Move a net's dynamic path to static obstacles permanently."""
|
||||
to_move = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
|
||||
for obj_id in to_move:
|
||||
nid, dilated = self.path_geometries.pop(obj_id)
|
||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
||||
|
||||
# Add to static (already dilated for clearance)
|
||||
new_static_id = self._id_counter
|
||||
self._id_counter += 1
|
||||
self.obstacle_geometries[new_static_id] = dilated
|
||||
self.prepared_obstacles[new_static_id] = prep(dilated)
|
||||
self.static_obstacles.insert(new_static_id, dilated.bounds)
|
||||
|
||||
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
|
||||
"""Count how many other nets collide with this geometry."""
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
candidates = self.dynamic_paths.intersection(test_poly.bounds)
|
||||
count = 0
|
||||
for obj_id in candidates:
|
||||
other_net_id, other_poly = self.path_geometries[obj_id]
|
||||
if other_net_id != net_id and test_poly.intersects(other_poly):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def is_collision(
|
||||
self,
|
||||
geometry: Polygon,
|
||||
net_width: float,
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""Check if a geometry (e.g. a Move) collides with static obstacles."""
|
||||
_ = net_width # Width is already integrated into engine dilation settings
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
|
||||
# Broad prune with R-Tree
|
||||
candidates = self.static_obstacles.intersection(test_poly.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
# Use prepared geometry for fast intersection
|
||||
if self.prepared_obstacles[obj_id].intersects(test_poly):
|
||||
# Check safety zone (2nm = 0.002 um)
|
||||
if start_port or end_port:
|
||||
obstacle = self.obstacle_geometries[obj_id]
|
||||
intersection = test_poly.intersection(obstacle)
|
||||
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
||||
# Create safety zone polygons
|
||||
safety_zones = []
|
||||
if start_port:
|
||||
safety_zones.append(Point(start_port.x, start_port.y).buffer(0.002))
|
||||
if end_port:
|
||||
safety_zones.append(Point(end_port.x, end_port.y).buffer(0.002))
|
||||
|
||||
if safety_zones:
|
||||
safe_poly = unary_union(safety_zones)
|
||||
# Remove safe zones from intersection
|
||||
remaining_collision = intersection.difference(safe_poly)
|
||||
if remaining_collision.is_empty or remaining_collision.area < 1e-9:
|
||||
continue
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
170
inire/geometry/components.py
Normal file
170
inire/geometry/components.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from .primitives import Port
|
||||
|
||||
# Search Grid Snap (1.0 µm)
|
||||
SEARCH_GRID_SNAP_UM = 1.0
|
||||
|
||||
|
||||
def snap_search_grid(value: float) -> float:
|
||||
"""Snap a coordinate to the nearest 1µm."""
|
||||
return round(value / SEARCH_GRID_SNAP_UM) * SEARCH_GRID_SNAP_UM
|
||||
|
||||
|
||||
class ComponentResult(NamedTuple):
|
||||
"""The result of a component generation: geometry and the final port."""
|
||||
|
||||
geometry: list[Polygon]
|
||||
end_port: Port
|
||||
|
||||
|
||||
class Straight:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, length: float, width: float) -> ComponentResult:
|
||||
"""Generate a straight waveguide segment."""
|
||||
# Calculate end port position
|
||||
rad = np.radians(start_port.orientation)
|
||||
dx = length * np.cos(rad)
|
||||
dy = length * np.sin(rad)
|
||||
|
||||
end_port = Port(start_port.x + dx, start_port.y + dy, start_port.orientation)
|
||||
|
||||
# Create polygon (centered on port)
|
||||
half_w = width / 2.0
|
||||
# Points relative to start port (0,0)
|
||||
points = [(0, half_w), (length, half_w), (length, -half_w), (0, -half_w)]
|
||||
|
||||
# Transform points
|
||||
cos_val = np.cos(rad)
|
||||
sin_val = np.sin(rad)
|
||||
poly_points = []
|
||||
for px, py in points:
|
||||
tx = start_port.x + px * cos_val - py * sin_val
|
||||
ty = start_port.y + px * sin_val + py * cos_val
|
||||
poly_points.append((tx, ty))
|
||||
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port)
|
||||
|
||||
|
||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||
"""Calculate number of segments for an arc to maintain a maximum sagitta."""
|
||||
if radius <= 0:
|
||||
return 1
|
||||
# angle_deg is absolute angle turned
|
||||
# s = R(1 - cos(theta/2)) => cos(theta/2) = 1 - s/R
|
||||
# theta = 2 * acos(1 - s/R)
|
||||
# n = total_angle / theta
|
||||
ratio = max(0.0, min(1.0, 1.0 - sagitta / radius))
|
||||
theta_max = 2.0 * np.arccos(ratio)
|
||||
if theta_max == 0:
|
||||
return 16
|
||||
num = int(np.ceil(np.radians(abs(angle_deg)) / theta_max))
|
||||
return max(4, num)
|
||||
|
||||
|
||||
class Bend90:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, radius: float, width: float, direction: str = "CW", sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a 90-degree bend."""
|
||||
# direction: 'CW' (-90) or 'CCW' (+90)
|
||||
turn_angle = -90 if direction == "CW" else 90
|
||||
|
||||
# Calculate center of the arc
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
center_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
cx = start_port.x + radius * np.cos(center_angle)
|
||||
cy = start_port.y + radius * np.sin(center_angle)
|
||||
|
||||
# Center to start is radius at center_angle + pi
|
||||
theta_start = center_angle + np.pi
|
||||
theta_end = theta_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
|
||||
ex = cx + radius * np.cos(theta_end)
|
||||
ey = cy + radius * np.sin(theta_end)
|
||||
|
||||
# End port orientation
|
||||
end_orientation = (start_port.orientation + turn_angle) % 360
|
||||
|
||||
snapped_ex = snap_search_grid(ex)
|
||||
snapped_ey = snap_search_grid(ey)
|
||||
|
||||
end_port = Port(snapped_ex, snapped_ey, float(end_orientation))
|
||||
|
||||
# Generate arc geometry
|
||||
num_segments = _get_num_segments(radius, 90, sagitta)
|
||||
angles = np.linspace(theta_start, theta_end, num_segments + 1)
|
||||
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
|
||||
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
|
||||
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
|
||||
|
||||
return ComponentResult(geometry=[Polygon(inner_points + outer_points)], end_port=end_port)
|
||||
|
||||
|
||||
class SBend:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, offset: float, radius: float, width: float, sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a parametric S-bend (two tangent arcs). Only for offset < 2*radius."""
|
||||
if abs(offset) >= 2 * radius:
|
||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||
|
||||
# Analytical length: L = 2 * sqrt(O * (2*R - O/4)) is for a specific S-bend type.
|
||||
# Standard S-bend with two equal arcs:
|
||||
# Offset O = 2 * R * (1 - cos(theta))
|
||||
# theta = acos(1 - O / (2*R))
|
||||
theta = np.arccos(1 - abs(offset) / (2 * radius))
|
||||
|
||||
# Length of one arc = R * theta
|
||||
# Total length of S-bend = 2 * R * theta (arc length)
|
||||
# Horizontal distance dx = 2 * R * sin(theta)
|
||||
|
||||
dx = 2 * radius * np.sin(theta)
|
||||
dy = offset
|
||||
|
||||
# End port
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
ex = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
|
||||
ey = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
|
||||
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
# Geometry: two arcs
|
||||
# First arc center
|
||||
direction = 1 if offset > 0 else -1
|
||||
center_angle1 = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(center_angle1)
|
||||
cy1 = start_port.y + radius * np.sin(center_angle1)
|
||||
|
||||
# Second arc center
|
||||
center_angle2 = rad_start - direction * np.pi / 2
|
||||
cx2 = ex + radius * np.cos(center_angle2)
|
||||
cy2 = ey + radius * np.sin(center_angle2)
|
||||
|
||||
# Generate points for both arcs
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(theta)), sagitta)
|
||||
# Arc 1: theta_start1 to theta_end1
|
||||
theta_start1 = center_angle1 + np.pi
|
||||
theta_end1 = theta_start1 - direction * theta
|
||||
|
||||
# Arc 2: theta_start2 to theta_end2
|
||||
theta_start2 = center_angle2
|
||||
theta_end2 = theta_start2 + direction * theta
|
||||
|
||||
def get_arc_points(cx: float, cy: float, r_inner: float, r_outer: float, t_start: float, t_end: float) -> list[tuple[float, float]]:
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
inner = [(cx + r_inner * np.cos(a), cy + r_inner * np.sin(a)) for a in angles]
|
||||
outer = [(cx + r_outer * np.cos(a), cy + r_outer * np.sin(a)) for a in reversed(angles)]
|
||||
return inner + outer
|
||||
|
||||
poly1 = Polygon(get_arc_points(cx1, cy1, radius - width / 2, radius + width / 2, theta_start1, theta_end1))
|
||||
poly2 = Polygon(get_arc_points(cx2, cy2, radius - width / 2, radius + width / 2, theta_end2, theta_start2))
|
||||
|
||||
return ComponentResult(geometry=[poly1, poly2], end_port=end_port)
|
||||
|
||||
50
inire/geometry/primitives.py
Normal file
50
inire/geometry/primitives.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
# 1nm snap (0.001 µm)
|
||||
GRID_SNAP_UM = 0.001
|
||||
|
||||
def snap_nm(value: float) -> float:
|
||||
"""Snap a coordinate to the nearest 1nm (0.001 um)."""
|
||||
return round(value / GRID_SNAP_UM) * GRID_SNAP_UM
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Port:
|
||||
"""A port defined by (x, y, orientation) in micrometers."""
|
||||
x: float
|
||||
y: float
|
||||
orientation: float # Degrees: 0, 90, 180, 270
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Snap x, y to 1nm
|
||||
# We need to use object.__setattr__ because the dataclass is frozen.
|
||||
snapped_x = snap_nm(self.x)
|
||||
snapped_y = snap_nm(self.y)
|
||||
|
||||
# Ensure orientation is one of {0, 90, 180, 270}
|
||||
norm_orientation = int(round(self.orientation)) % 360
|
||||
if norm_orientation not in {0, 90, 180, 270}:
|
||||
norm_orientation = (round(norm_orientation / 90) * 90) % 360
|
||||
|
||||
object.__setattr__(self, "x", snapped_x)
|
||||
object.__setattr__(self, "y", snapped_y)
|
||||
object.__setattr__(self, "orientation", float(norm_orientation))
|
||||
|
||||
|
||||
def translate_port(port: Port, dx: float, dy: float) -> Port:
|
||||
"""Translate a port by (dx, dy)."""
|
||||
return Port(port.x + dx, port.y + dy, port.orientation)
|
||||
|
||||
|
||||
def rotate_port(port: Port, angle: float, origin: tuple[float, float] = (0, 0)) -> Port:
|
||||
"""Rotate a port by a multiple of 90 degrees around an origin."""
|
||||
ox, oy = origin
|
||||
px, py = port.x, port.y
|
||||
|
||||
rad = np.radians(angle)
|
||||
qx = ox + np.cos(rad) * (px - ox) - np.sin(rad) * (py - oy)
|
||||
qy = oy + np.sin(rad) * (px - ox) + np.cos(rad) * (py - oy)
|
||||
|
||||
return Port(qx, qy, port.orientation + angle)
|
||||
|
||||
209
inire/router/astar.py
Normal file
209
inire/router/astar.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.cost import CostEvaluator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AStarNode:
|
||||
_count = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: Port,
|
||||
g_cost: float,
|
||||
h_cost: float,
|
||||
parent: AStarNode | None = None,
|
||||
component_result: ComponentResult | None = None,
|
||||
) -> None:
|
||||
self.port = port
|
||||
self.g_cost = g_cost
|
||||
self.h_cost = h_cost
|
||||
self.f_cost = g_cost + h_cost
|
||||
self.parent = parent
|
||||
self.component_result = component_result
|
||||
self.count = AStarNode._count
|
||||
AStarNode._count += 1
|
||||
|
||||
def __lt__(self, other: AStarNode) -> bool:
|
||||
# Tie-breaking: lower f first, then lower h, then order
|
||||
if abs(self.f_cost - other.f_cost) > 1e-9:
|
||||
return self.f_cost < other.f_cost
|
||||
if abs(self.h_cost - other.h_cost) > 1e-9:
|
||||
return self.h_cost < other.h_cost
|
||||
return self.count < other.count
|
||||
|
||||
|
||||
class AStarRouter:
|
||||
def __init__(self, cost_evaluator: CostEvaluator) -> None:
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.node_limit = 100000
|
||||
self.total_nodes_expanded = 0
|
||||
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
|
||||
|
||||
def route(
|
||||
self, start: Port, target: Port, net_width: float, net_id: str = "default"
|
||||
) -> list[ComponentResult] | None:
|
||||
"""Route a single net using A*."""
|
||||
self._collision_cache.clear()
|
||||
open_set: list[AStarNode] = []
|
||||
# Key: (x, y, orientation)
|
||||
closed_set: set[tuple[float, float, float]] = set()
|
||||
|
||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||
heapq.heappush(open_set, start_node)
|
||||
|
||||
nodes_expanded = 0
|
||||
|
||||
while open_set:
|
||||
if nodes_expanded >= self.node_limit:
|
||||
logger.warning(f" AStar failed: node limit {self.node_limit} reached.")
|
||||
return None
|
||||
|
||||
current = heapq.heappop(open_set)
|
||||
|
||||
state = (current.port.x, current.port.y, current.port.orientation)
|
||||
if state in closed_set:
|
||||
continue
|
||||
closed_set.add(state)
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
|
||||
# Check if we reached the target (Snap-to-Target)
|
||||
if (
|
||||
abs(current.port.x - target.x) < 1e-6
|
||||
and abs(current.port.y - target.y) < 1e-6
|
||||
and current.port.orientation == target.orientation
|
||||
):
|
||||
return self._reconstruct_path(current)
|
||||
|
||||
# Look-ahead snapping
|
||||
if self._try_snap_to_target(current, target, net_width, net_id, open_set):
|
||||
pass
|
||||
|
||||
# Expand neighbors
|
||||
self._expand_moves(current, target, net_width, net_id, open_set)
|
||||
|
||||
return None
|
||||
|
||||
def _expand_moves(
|
||||
self,
|
||||
current: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
) -> None:
|
||||
# 1. Straights
|
||||
for length in [0.5, 1.0, 5.0, 25.0]:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"S{length}")
|
||||
|
||||
# 2. Bends
|
||||
for radius in [5.0, 10.0, 20.0]:
|
||||
for direction in ["CW", "CCW"]:
|
||||
res = Bend90.generate(current.port, radius, net_width, direction)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"B{radius}{direction}")
|
||||
|
||||
# 3. Parametric SBends
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
rad = np.radians(current.port.orientation)
|
||||
local_dy = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if 0 < abs(local_dy) < 40.0: # Match max 2*R
|
||||
try:
|
||||
# Use a standard radius for expansion
|
||||
res = SBend.generate(current.port, local_dy, 20.0, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"SB{local_dy}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _add_node(
|
||||
self,
|
||||
parent: AStarNode,
|
||||
result: ComponentResult,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
move_type: str,
|
||||
) -> None:
|
||||
cache_key = (
|
||||
parent.port.x,
|
||||
parent.port.y,
|
||||
parent.port.orientation,
|
||||
move_type,
|
||||
net_width,
|
||||
net_id,
|
||||
)
|
||||
if cache_key in self._collision_cache:
|
||||
if self._collision_cache[cache_key]:
|
||||
return
|
||||
else:
|
||||
hard_coll = False
|
||||
for poly in result.geometry:
|
||||
if self.cost_evaluator.collision_engine.is_collision(poly, net_width, start_port=parent.port, end_port=result.end_port):
|
||||
hard_coll = True
|
||||
break
|
||||
self._collision_cache[cache_key] = hard_coll
|
||||
if hard_coll:
|
||||
return
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(result.geometry, result.end_port, net_width, net_id, start_port=parent.port)
|
||||
|
||||
g_cost = parent.g_cost + move_cost + self._step_cost(result)
|
||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||
|
||||
new_node = AStarNode(result.end_port, g_cost, h_cost, parent, result)
|
||||
heapq.heappush(open_set, new_node)
|
||||
|
||||
def _step_cost(self, result: ComponentResult) -> float:
|
||||
_ = result # Unused in base implementation
|
||||
return 0.0
|
||||
|
||||
def _try_snap_to_target(
|
||||
self,
|
||||
current: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
) -> bool:
|
||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||
if dist > 10.0:
|
||||
return False
|
||||
|
||||
if current.port.orientation == target.orientation:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, "SnapTarget")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
curr = end_node
|
||||
while curr.component_result:
|
||||
path.append(curr.component_result)
|
||||
curr = curr.parent
|
||||
return path[::-1]
|
||||
|
||||
62
inire/router/cost.py
Normal file
62
inire/router/cost.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.danger_map import DangerMap
|
||||
|
||||
|
||||
class CostEvaluator:
|
||||
"""Calculates total cost f(n) = g(n) + h(n)."""
|
||||
|
||||
def __init__(self, collision_engine: CollisionEngine, danger_map: DangerMap) -> None:
|
||||
self.collision_engine = collision_engine
|
||||
self.danger_map = danger_map
|
||||
# Cost weights
|
||||
self.unit_length_cost = 1.0
|
||||
self.bend_cost_multiplier = 10.0
|
||||
self.greedy_h_weight = 1.1
|
||||
self.congestion_penalty = 100.0 # Multiplier for overlaps
|
||||
|
||||
def g_proximity(self, x: float, y: float) -> float:
|
||||
"""Get proximity cost from the Danger Map."""
|
||||
return self.danger_map.get_cost(x, y)
|
||||
|
||||
def h_manhattan(self, current: Port, target: Port) -> float:
|
||||
"""Heuristic: weighted Manhattan distance + orientation penalty."""
|
||||
dist = abs(current.x - target.x) + abs(current.y - target.y)
|
||||
|
||||
# Orientation penalty if not aligned with target entry
|
||||
penalty = 0.0
|
||||
if current.orientation != target.orientation:
|
||||
penalty += 50.0 # Arbitrary high cost for mismatch
|
||||
|
||||
return self.greedy_h_weight * (dist + penalty)
|
||||
|
||||
def evaluate_move(
|
||||
self,
|
||||
geometry: list[Polygon],
|
||||
end_port: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
start_port: Port | None = None,
|
||||
) -> float:
|
||||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||
total_cost = 0.0
|
||||
# Strict collision check
|
||||
for poly in geometry:
|
||||
if self.collision_engine.is_collision(poly, net_width, start_port=start_port, end_port=end_port):
|
||||
return 1e9 # Massive cost for hard collisions
|
||||
|
||||
# Negotiated Congestion Cost
|
||||
overlaps = self.collision_engine.count_congestion(poly, net_id)
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
|
||||
# Proximity cost from Danger Map
|
||||
total_cost += self.g_proximity(end_port.x, end_port.y)
|
||||
return total_cost
|
||||
|
||||
80
inire/router/danger_map.py
Normal file
80
inire/router/danger_map.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
||||
class DangerMap:
|
||||
"""A pre-computed grid for heuristic proximity costs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bounds: tuple[float, float, float, float],
|
||||
resolution: float = 1.0,
|
||||
safety_threshold: float = 10.0,
|
||||
k: float = 1.0,
|
||||
) -> None:
|
||||
# bounds: (minx, miny, maxx, maxy)
|
||||
self.minx, self.miny, self.maxx, self.maxy = bounds
|
||||
self.resolution = resolution
|
||||
self.safety_threshold = safety_threshold
|
||||
self.k = k
|
||||
|
||||
# Grid dimensions
|
||||
self.width_cells = int(np.ceil((self.maxx - self.minx) / self.resolution))
|
||||
self.height_cells = int(np.ceil((self.maxy - self.miny) / self.resolution))
|
||||
|
||||
# Use uint8 for memory efficiency if normalized, or float16/float32.
|
||||
# Let's use float32 for simplicity and precision in the prototype.
|
||||
# For a 1000x1000 grid, this is only 4MB.
|
||||
# For 20000x20000, it's 1.6GB.
|
||||
self.grid = np.zeros((self.width_cells, self.height_cells), dtype=np.float32)
|
||||
|
||||
def precompute(self, obstacles: list[Polygon]) -> None:
|
||||
"""Pre-compute the proximity costs for the entire grid."""
|
||||
# For each cell, find distance to nearest obstacle.
|
||||
# This is a distance transform problem.
|
||||
# For the prototype, we can use a simpler approach or scipy.ndimage.distance_transform_edt.
|
||||
from scipy.ndimage import distance_transform_edt
|
||||
|
||||
# Create a binary mask of obstacles
|
||||
mask = np.ones((self.width_cells, self.height_cells), dtype=bool)
|
||||
# Rasterize obstacles (simplified: mark cells whose center is inside an obstacle)
|
||||
# This is slow for many obstacles; in a real engine, we'd use a faster rasterizer.
|
||||
from shapely.geometry import Point
|
||||
|
||||
for poly in obstacles:
|
||||
# Get bounding box in grid coordinates
|
||||
p_minx, p_miny, p_maxx, p_maxy = poly.bounds
|
||||
x_start = max(0, int((p_minx - self.minx) / self.resolution))
|
||||
x_end = min(self.width_cells, int((p_maxx - self.minx) / self.resolution) + 1)
|
||||
y_start = max(0, int((p_miny - self.miny) / self.resolution))
|
||||
y_end = min(self.height_cells, int((p_maxy - self.miny) / self.resolution) + 1)
|
||||
|
||||
for ix in range(x_start, x_end):
|
||||
cx = self.minx + (ix + 0.5) * self.resolution
|
||||
for iy in range(y_start, y_end):
|
||||
cy = self.miny + (iy + 0.5) * self.resolution
|
||||
if poly.contains(Point(cx, cy)):
|
||||
mask[ix, iy] = False
|
||||
|
||||
# Distance transform (mask=True for empty space)
|
||||
distances = distance_transform_edt(mask) * self.resolution
|
||||
|
||||
# Proximity cost: k / d^2 if d < threshold, else 0
|
||||
# To avoid division by zero, we cap distances at a small epsilon (e.g. 0.1um)
|
||||
safe_distances = np.maximum(distances, 0.1)
|
||||
self.grid = np.where(distances < self.safety_threshold, self.k / (safe_distances**2), 0.0).astype(np.float32)
|
||||
|
||||
def get_cost(self, x: float, y: float) -> float:
|
||||
"""Get the proximity cost at a specific coordinate."""
|
||||
ix = int((x - self.minx) / self.resolution)
|
||||
iy = int((y - self.miny) / self.resolution)
|
||||
|
||||
if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells:
|
||||
return float(self.grid[ix, iy])
|
||||
return 1e6 # Outside bounds is expensive
|
||||
113
inire/router/pathfinder.py
Normal file
113
inire/router/pathfinder.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoutingResult:
|
||||
net_id: str
|
||||
path: list[ComponentResult]
|
||||
is_valid: bool
|
||||
collisions: int
|
||||
|
||||
|
||||
class PathFinder:
|
||||
"""Multi-net router using Negotiated Congestion."""
|
||||
|
||||
def __init__(self, router: AStarRouter, cost_evaluator: CostEvaluator) -> None:
|
||||
self.router = router
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.max_iterations = 20
|
||||
self.base_congestion_penalty = 100.0
|
||||
|
||||
def route_all(self, netlist: dict[str, tuple[Port, Port]], net_widths: dict[str, float]) -> dict[str, RoutingResult]:
|
||||
"""Route all nets in the netlist using Negotiated Congestion."""
|
||||
results: dict[str, RoutingResult] = {}
|
||||
self.cost_evaluator.congestion_penalty = self.base_congestion_penalty
|
||||
|
||||
start_time = time.monotonic()
|
||||
num_nets = len(netlist)
|
||||
session_timeout = max(30.0, 0.5 * num_nets * self.max_iterations)
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
any_congestion = False
|
||||
logger.info(f"PathFinder Iteration {iteration}...")
|
||||
|
||||
# Sequence through nets
|
||||
for net_id, (start, target) in netlist.items():
|
||||
# Timeout check
|
||||
elapsed = time.monotonic() - start_time
|
||||
if elapsed > session_timeout:
|
||||
logger.warning(f"PathFinder TIMEOUT after {elapsed:.2f}s")
|
||||
# Return whatever we have so far
|
||||
return self._finalize_results(results, netlist)
|
||||
|
||||
width = net_widths.get(net_id, 2.0)
|
||||
|
||||
# 1. Rip-up existing path
|
||||
self.cost_evaluator.collision_engine.remove_path(net_id)
|
||||
|
||||
# 2. Reroute with current congestion info
|
||||
net_start = time.monotonic()
|
||||
path = self.router.route(start, target, width, net_id=net_id)
|
||||
logger.debug(f" Net {net_id} routed in {time.monotonic() - net_start:.4f}s")
|
||||
|
||||
if path:
|
||||
# 3. Add to R-Tree
|
||||
all_geoms = []
|
||||
for res in path:
|
||||
all_geoms.extend(res.geometry)
|
||||
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms)
|
||||
|
||||
# Check if this new path has any congestion
|
||||
collision_count = 0
|
||||
for poly in all_geoms:
|
||||
collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
|
||||
|
||||
if collision_count > 0:
|
||||
any_congestion = True
|
||||
|
||||
results[net_id] = RoutingResult(net_id, path, collision_count == 0, collision_count)
|
||||
else:
|
||||
results[net_id] = RoutingResult(net_id, [], False, 0)
|
||||
any_congestion = True
|
||||
|
||||
if not any_congestion:
|
||||
break
|
||||
|
||||
# 4. Inflate congestion penalty
|
||||
self.cost_evaluator.congestion_penalty *= 1.5
|
||||
|
||||
return self._finalize_results(results, netlist)
|
||||
|
||||
def _finalize_results(self, results: dict[str, RoutingResult], netlist: dict[str, tuple[Port, Port]]) -> dict[str, RoutingResult]:
|
||||
"""Final check: re-verify all nets against the final static paths."""
|
||||
logger.debug(f"Finalizing results for nets: {list(results.keys())}")
|
||||
final_results = {}
|
||||
# Ensure all nets in the netlist are present in final_results
|
||||
for net_id in netlist:
|
||||
res = results.get(net_id)
|
||||
if not res or not res.path:
|
||||
final_results[net_id] = RoutingResult(net_id, [], False, 0)
|
||||
continue
|
||||
|
||||
collision_count = 0
|
||||
for comp in res.path:
|
||||
for poly in comp.geometry:
|
||||
collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
|
||||
|
||||
final_results[net_id] = RoutingResult(net_id, res.path, collision_count == 0, collision_count)
|
||||
|
||||
return final_results
|
||||
|
||||
56
inire/tests/benchmark_scaling.py
Normal file
56
inire/tests/benchmark_scaling.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import time
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
def benchmark_scaling() -> None:
|
||||
print("Starting Scalability Benchmark...")
|
||||
|
||||
# 1. Memory Verification (20x20mm)
|
||||
# Resolution 1um -> 20000 x 20000 grid
|
||||
bounds = (0, 0, 20000, 20000)
|
||||
print(f"Initializing DangerMap for {bounds} area...")
|
||||
dm = DangerMap(bounds=bounds, resolution=1.0)
|
||||
# nbytes for float32: 20000 * 20000 * 4 bytes = 1.6 GB
|
||||
mem_gb = dm.grid.nbytes / (1024**3)
|
||||
print(f"DangerMap memory usage: {mem_gb:.2f} GB")
|
||||
assert mem_gb < 2.0
|
||||
|
||||
# 2. Node Expansion Rate (50 nets)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# Use a smaller area for routing benchmark to keep it fast
|
||||
routing_bounds = (0, 0, 1000, 1000)
|
||||
danger_map = DangerMap(bounds=routing_bounds)
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
num_nets = 50
|
||||
netlist = {}
|
||||
for i in range(num_nets):
|
||||
# Parallel nets spaced by 10um
|
||||
netlist[f"net{i}"] = (Port(0, i * 10, 0), Port(100, i * 10, 0))
|
||||
|
||||
print(f"Routing {num_nets} nets...")
|
||||
start_time = time.monotonic()
|
||||
results = pf.route_all(netlist, dict.fromkeys(netlist, 2.0))
|
||||
end_time = time.monotonic()
|
||||
|
||||
total_time = end_time - start_time
|
||||
print(f"Total routing time: {total_time:.2f} s")
|
||||
print(f"Time per net: {total_time/num_nets:.4f} s")
|
||||
|
||||
if total_time > 0:
|
||||
nodes_per_sec = router.total_nodes_expanded / total_time
|
||||
print(f"Node expansion rate: {nodes_per_sec:.2f} nodes/s")
|
||||
|
||||
# Success rate
|
||||
successes = sum(1 for r in results.values() if r.is_valid)
|
||||
print(f"Success rate: {successes/num_nets * 100:.1f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark_scaling()
|
||||
71
inire/tests/test_astar.py
Normal file
71
inire/tests/test_astar.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import pytest
|
||||
import numpy as np
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
def test_astar_straight(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert len(path) > 0
|
||||
# Final port should be target
|
||||
assert abs(path[-1].end_port.x - 50.0) < 1e-6
|
||||
assert path[-1].end_port.y == 0.0
|
||||
|
||||
def test_astar_bend(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(20, 20, 90)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert any("Bend90" in str(res) or hasattr(res, 'geometry') for res in path) # Loose check
|
||||
assert abs(path[-1].end_port.x - 20.0) < 1e-6
|
||||
assert abs(path[-1].end_port.y - 20.0) < 1e-6
|
||||
assert path[-1].end_port.orientation == 90.0
|
||||
|
||||
def test_astar_obstacle(basic_evaluator) -> None:
|
||||
# Add an obstacle in the middle of a straight path
|
||||
obstacle = Polygon([(20, -5), (30, -5), (30, 5), (20, 5)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||
basic_evaluator.danger_map.precompute([obstacle])
|
||||
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
# Path should have diverted (check that it's not a single straight)
|
||||
# The path should go around the 5um half-width obstacle.
|
||||
# Total wire length should be > 50.
|
||||
sum(np.sqrt((p.end_port.x - p.geometry[0].bounds[0])**2 + (p.end_port.y - p.geometry[0].bounds[1])**2) for p in path)
|
||||
# That's a rough length estimate.
|
||||
# Better: check that no part of the path collides.
|
||||
for res in path:
|
||||
for poly in res.geometry:
|
||||
assert not poly.intersects(obstacle)
|
||||
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Target is NOT on 1um grid
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(10.005, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert abs(path[-1].end_port.x - 10.005) < 1e-6
|
||||
59
inire/tests/test_collision.py
Normal file
59
inire/tests/test_collision.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
|
||||
def test_collision_detection() -> None:
|
||||
# Clearance = 2um
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
|
||||
# Static obstacle at (10, 10) with size 5x5
|
||||
obstacle = Polygon([(10,10), (15,10), (15,15), (10,15)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Net width = 2um
|
||||
# Dilation = (W+C)/2 = (2+2)/2 = 2.0um
|
||||
|
||||
# 1. Direct hit
|
||||
test_poly = Polygon([(12,12), (13,12), (13,13), (12,13)])
|
||||
assert engine.is_collision(test_poly, net_width=2.0) is True
|
||||
|
||||
# 2. Far away
|
||||
test_poly_far = Polygon([(0,0), (5,0), (5,5), (0,5)])
|
||||
assert engine.is_collision(test_poly_far, net_width=2.0) is False
|
||||
|
||||
# 3. Near hit (within clearance)
|
||||
# Obstacle is at (10,10).
|
||||
# test_poly is at (8,10) to (9,15).
|
||||
# Centerline at 8.5. Distance to 10 is 1.5.
|
||||
# Required distance (Wi+C)/2 = 2.0. Collision!
|
||||
test_poly_near = Polygon([(8,10), (9,10), (9,15), (8,15)])
|
||||
assert engine.is_collision(test_poly_near, net_width=2.0) is True
|
||||
|
||||
def test_safety_zone() -> None:
|
||||
# Use zero clearance for this test to verify the 2nm port safety zone
|
||||
# against the physical obstacle boundary.
|
||||
engine = CollisionEngine(clearance=0.0)
|
||||
obstacle = Polygon([(10,10), (15,10), (15,15), (10,15)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Port exactly on the boundary (x=10)
|
||||
start_port = Port(10.0, 12.0, 0.0)
|
||||
|
||||
# A very narrow waveguide (1nm width) that overlaps by 1nm.
|
||||
# Overlap is from x=10 to x=10.001, y=11.9995 to 12.0005.
|
||||
# This fits entirely within a 2nm radius of (10.0, 12.0).
|
||||
test_poly = Polygon([(9.999, 11.9995), (10.001, 11.9995), (10.001, 12.0005), (9.999, 12.0005)])
|
||||
|
||||
assert engine.is_collision(test_poly, net_width=0.001, start_port=start_port) is False
|
||||
|
||||
def test_configurable_max_net_width() -> None:
|
||||
# Large max_net_width (10.0) -> large pre-dilation (6.0)
|
||||
engine = CollisionEngine(clearance=2.0, max_net_width=10.0)
|
||||
obstacle = Polygon([(20, 20), (25, 20), (25, 25), (20, 25)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
test_poly = Polygon([(15, 20), (16, 20), (16, 25), (15, 25)])
|
||||
# physical check: dilated test_poly by C/2 = 1.0.
|
||||
# Dilated test_poly bounds: (14, 19, 17, 26).
|
||||
# obstacle: (20, 20, 25, 25). No physical collision.
|
||||
assert engine.is_collision(test_poly, net_width=2.0) is False
|
||||
75
inire/tests/test_components.py
Normal file
75
inire/tests/test_components.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import pytest
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.components import Straight, Bend90, SBend
|
||||
|
||||
def test_straight_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
length = 10.0
|
||||
width = 2.0
|
||||
result = Straight.generate(start, length, width)
|
||||
|
||||
# End port check
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 0.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
|
||||
# Geometry check
|
||||
poly = result.geometry[0]
|
||||
assert poly.area == length * width
|
||||
# Check bounds
|
||||
minx, miny, maxx, maxy = poly.bounds
|
||||
assert minx == 0.0
|
||||
assert maxx == 10.0
|
||||
assert miny == -1.0
|
||||
assert maxy == 1.0
|
||||
|
||||
def test_bend90_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
# CW bend (0 -> 270)
|
||||
result_cw = Bend90.generate(start, radius, width, direction='CW')
|
||||
|
||||
# End port (center is at (0, -10))
|
||||
# End port is at (10, -10) relative to center if it was 90-degree turn?
|
||||
# No, from center (0, -10), start is (0, 0) which is 90 deg.
|
||||
# Turn -90 deg -> end is at 0 deg from center -> (10, -10)
|
||||
assert result_cw.end_port.x == 10.0
|
||||
assert result_cw.end_port.y == -10.0
|
||||
assert result_cw.end_port.orientation == 270.0
|
||||
|
||||
# CCW bend (0 -> 90)
|
||||
result_ccw = Bend90.generate(start, radius, width, direction='CCW')
|
||||
assert result_ccw.end_port.x == 10.0
|
||||
assert result_ccw.end_port.y == 10.0
|
||||
assert result_ccw.end_port.orientation == 90.0
|
||||
|
||||
def test_sbend_generation() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
offset = 5.0
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
result = SBend.generate(start, offset, radius, width)
|
||||
|
||||
# End port check
|
||||
assert result.end_port.y == 5.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
|
||||
# Geometry check (two arcs)
|
||||
assert len(result.geometry) == 2
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError):
|
||||
SBend.generate(start, 25.0, 10.0, 2.0)
|
||||
|
||||
def test_bend_snapping() -> None:
|
||||
# Radius that results in non-integer coords
|
||||
radius = 10.1234
|
||||
start = Port(0, 0, 0)
|
||||
result = Bend90.generate(start, radius, 2.0, direction='CCW')
|
||||
# End port should be snapped to 1µm (SEARCH_GRID_SNAP_UM)
|
||||
# ex = 10.1234, ey = 10.1234
|
||||
# snapped: ex = 10.0, ey = 10.0 if we round to nearest 1.0?
|
||||
# SEARCH_GRID_SNAP_UM = 1.0
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 10.0
|
||||
70
inire/tests/test_congestion.py
Normal file
70
inire/tests/test_congestion.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import pytest
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# Wider bounds to allow going around (y from -40 to 40)
|
||||
danger_map = DangerMap(bounds=(0, -40, 100, 40))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
def test_astar_sbend(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Start at (0,0), target at (50, 3) -> 3um lateral offset
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 3, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
# Check if any component in the path is an SBend
|
||||
found_sbend = False
|
||||
for res in path:
|
||||
# SBend should align us with the target y=3
|
||||
if abs(res.end_port.y - 3.0) < 1e-6 and res.end_port.orientation == 0:
|
||||
found_sbend = True
|
||||
assert found_sbend
|
||||
|
||||
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
pf.max_iterations = 10
|
||||
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||
"net2": (Port(0, 10, 0), Port(50, 10, 0))
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
# Tiny obstacles to block net1 and net2 direct paths?
|
||||
# No, let's block the space BETWEEN them so they must choose
|
||||
# to either stay far apart or squeeze together.
|
||||
# Actually, let's block their direct paths and force them
|
||||
# into a narrow corridor that only fits ONE.
|
||||
|
||||
# Obstacles creating a wide wall with a narrow 2um gap at y=5.
|
||||
# Gap y: 4 to 6. Center y=5.
|
||||
# Net 1 (y=0) and Net 2 (y=10) both want to go to y=5 to pass.
|
||||
# But only ONE fits at y=5.
|
||||
|
||||
obs_top = Polygon([(20, 6), (30, 6), (30, 30), (20, 30)])
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -30), (20, -30)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_top)
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_bottom)
|
||||
basic_evaluator.danger_map.precompute([obs_top, obs_bottom])
|
||||
|
||||
# Increase base penalty to force detour immediately
|
||||
pf.base_congestion_penalty = 1000.0
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
36
inire/tests/test_cost.py
Normal file
36
inire/tests/test_cost.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from shapely.geometry import Polygon
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
def test_cost_calculation() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# 50x50 um area, 1um resolution
|
||||
danger_map = DangerMap(bounds=(0, 0, 50, 50), resolution=1.0, safety_threshold=10.0, k=1.0)
|
||||
|
||||
# Add a central obstacle
|
||||
# Grid cells are indexed from self.minx.
|
||||
obstacle = Polygon([(20,20), (30,20), (30,30), (20,30)])
|
||||
danger_map.precompute([obstacle])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
|
||||
# 1. Cost far from obstacle
|
||||
cost_far = evaluator.g_proximity(5.0, 5.0)
|
||||
assert cost_far == 0.0
|
||||
|
||||
# 2. Cost near obstacle (d=1.0)
|
||||
# Cell center (20.5, 20.5) is inside. Cell (19.5, 20.5) center to boundary (20, 20.5) is 0.5.
|
||||
# Scipy EDT gives distance to mask=False.
|
||||
cost_near = evaluator.g_proximity(19.0, 25.0)
|
||||
assert cost_near > 0.0
|
||||
|
||||
# 3. Collision cost
|
||||
engine.add_static_obstacle(obstacle)
|
||||
test_poly = Polygon([(22, 22), (23, 22), (23, 23), (22, 23)])
|
||||
# end_port at (22.5, 22.5)
|
||||
move_cost = evaluator.evaluate_move(
|
||||
[test_poly], Port(22.5, 22.5, 0), net_width=2.0, net_id="net1"
|
||||
)
|
||||
assert move_cost == 1e9
|
||||
63
inire/tests/test_fuzz.py
Normal file
63
inire/tests/test_fuzz.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
from inire.utils.validation import validate_routing_result
|
||||
|
||||
|
||||
@st.composite
|
||||
def random_obstacle(draw):
|
||||
x = draw(st.floats(min_value=0, max_value=20))
|
||||
y = draw(st.floats(min_value=0, max_value=20))
|
||||
w = draw(st.floats(min_value=1, max_value=5))
|
||||
h = draw(st.floats(min_value=1, max_value=5))
|
||||
return Polygon([(x, y), (x + w, y), (x + w, y + h), (x, y + h)])
|
||||
|
||||
|
||||
@st.composite
|
||||
def random_port(draw):
|
||||
x = draw(st.floats(min_value=0, max_value=20))
|
||||
y = draw(st.floats(min_value=0, max_value=20))
|
||||
orientation = draw(st.sampled_from([0, 90, 180, 270]))
|
||||
return Port(x, y, orientation)
|
||||
|
||||
|
||||
@settings(max_examples=3, deadline=None)
|
||||
@given(obstacles=st.lists(random_obstacle(), min_size=0, max_size=3), start=random_port(), target=random_port())
|
||||
def test_fuzz_astar_no_crash(obstacles, start, target) -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
for obs in obstacles:
|
||||
engine.add_static_obstacle(obs)
|
||||
|
||||
danger_map = DangerMap(bounds=(0, 0, 30, 30))
|
||||
danger_map.precompute(obstacles)
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
router.node_limit = 5000 # Lower limit for fuzzing stability
|
||||
|
||||
# Check if start/target are inside obstacles (safety zone check)
|
||||
# The router should handle this gracefully (either route or return None)
|
||||
try:
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
# Analytic Correctness: if path is returned, verify it's collision-free
|
||||
if path:
|
||||
result = RoutingResult(net_id="default", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(
|
||||
result,
|
||||
obstacles,
|
||||
clearance=2.0,
|
||||
start_port_coord=(start.x, start.y),
|
||||
end_port_coord=(target.x, target.y),
|
||||
)
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
except Exception as e:
|
||||
# Unexpected exceptions are failures
|
||||
pytest.fail(f"Router crashed with {type(e).__name__}: {e}")
|
||||
51
inire/tests/test_pathfinder.py
Normal file
51
inire/tests/test_pathfinder.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import pytest
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
@pytest.fixture
|
||||
def basic_evaluator():
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 100, 100))
|
||||
danger_map.precompute([])
|
||||
return CostEvaluator(engine, danger_map)
|
||||
|
||||
def test_pathfinder_parallel(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||
"net2": (Port(0, 10, 0), Port(50, 10, 0))
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
||||
def test_pathfinder_congestion(basic_evaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
|
||||
# Net1 blocks Net2
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||
"net2": (Port(25, -10, 90), Port(25, 10, 90))
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# Verify both nets are valid and collision-free
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
||||
43
inire/tests/test_primitives.py
Normal file
43
inire/tests/test_primitives.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from hypothesis import given, strategies as st
|
||||
from inire.geometry.primitives import Port, translate_port, rotate_port
|
||||
|
||||
@st.composite
|
||||
def port_strategy(draw):
|
||||
x = draw(st.floats(min_value=-1e6, max_value=1e6))
|
||||
y = draw(st.floats(min_value=-1e6, max_value=1e6))
|
||||
orientation = draw(st.sampled_from([0, 90, 180, 270]))
|
||||
return Port(x, y, orientation)
|
||||
|
||||
def test_port_snapping() -> None:
|
||||
p = Port(0.123456, 0.654321, 90)
|
||||
assert p.x == 0.123
|
||||
assert p.y == 0.654
|
||||
assert p.orientation == 90.0
|
||||
|
||||
@given(p=port_strategy())
|
||||
def test_port_transform_invariants(p) -> None:
|
||||
# Rotating 90 degrees 4 times should return to same orientation
|
||||
p_rot = p
|
||||
for _ in range(4):
|
||||
p_rot = rotate_port(p_rot, 90)
|
||||
|
||||
assert p_rot.orientation == p.orientation
|
||||
# Coordinates should be close (floating point error) but snapped to 1nm
|
||||
assert abs(p_rot.x - p.x) < 1e-9
|
||||
assert abs(p_rot.y - p.y) < 1e-9
|
||||
|
||||
@given(p=port_strategy(), dx=st.floats(min_value=-1000, max_value=1000), dy=st.floats(min_value=-1000, max_value=1000))
|
||||
def test_translate_snapping(p, dx, dy) -> None:
|
||||
p_trans = translate_port(p, dx, dy)
|
||||
# Check that snapped result is indeed multiple of GRID_SNAP_UM (0.001 um = 1nm)
|
||||
# Multiplication is more stable for this check
|
||||
assert abs(p_trans.x * 1000 - round(p_trans.x * 1000)) < 1e-6
|
||||
assert abs(p_trans.y * 1000 - round(p_trans.y * 1000)) < 1e-6
|
||||
|
||||
def test_orientation_normalization() -> None:
|
||||
p = Port(0, 0, 360)
|
||||
assert p.orientation == 0.0
|
||||
p2 = Port(0, 0, -90)
|
||||
assert p2.orientation == 270.0
|
||||
p3 = Port(0, 0, 95) # Should snap to 90
|
||||
assert p3.orientation == 90.0
|
||||
60
inire/tests/test_refinements.py
Normal file
60
inire/tests/test_refinements.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.geometry.components import Bend90
|
||||
|
||||
def test_arc_resolution_sagitta() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
# R=10, 90 deg bend.
|
||||
# High tolerance (0.5um) -> few segments
|
||||
res_coarse = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.5)
|
||||
# Low tolerance (0.001um = 1nm) -> many segments
|
||||
res_fine = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.001)
|
||||
|
||||
# Check number of points in the polygon exterior
|
||||
# (num_segments + 1) * 2 points usually
|
||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||
|
||||
assert pts_fine > pts_coarse
|
||||
|
||||
def test_locked_paths() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 1. Route Net A
|
||||
netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))}
|
||||
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
||||
assert results_a["netA"].is_valid
|
||||
|
||||
# 2. Lock Net A
|
||||
engine.lock_net("netA")
|
||||
|
||||
# 3. Route Net B through the same space. It should detour or fail.
|
||||
# We'll place Net B's start/target such that it MUST cross Net A's physical path.
|
||||
netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))}
|
||||
|
||||
# Route Net B
|
||||
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
||||
|
||||
# Net B should be is_valid (it detoured) or at least not have collisions
|
||||
# with Net A in the dynamic set (because netA is now static).
|
||||
# Since netA is static, netB will see it as a HARD collision if it tries to cross.
|
||||
# Our A* will find a detour around the static obstacle.
|
||||
assert results_b["netB"].is_valid
|
||||
|
||||
# Verify geometry doesn't intersect locked netA (physical check)
|
||||
poly_a = [p.geometry[0] for p in results_a["netA"].path]
|
||||
poly_b = [p.geometry[0] for p in results_b["netB"].path]
|
||||
|
||||
for pa in poly_a:
|
||||
for pb in poly_b:
|
||||
# Check physical clearance
|
||||
assert not pa.buffer(1.0).intersects(pb.buffer(1.0))
|
||||
57
inire/utils/validation.py
Normal file
57
inire/utils/validation.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from shapely.geometry import Point
|
||||
from shapely.ops import unary_union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
|
||||
|
||||
def validate_routing_result(
|
||||
result: RoutingResult,
|
||||
static_obstacles: list[Polygon],
|
||||
clearance: float,
|
||||
start_port_coord: tuple[float, float] | None = None,
|
||||
end_port_coord: tuple[float, float] | None = None,
|
||||
) -> dict[str, any]:
|
||||
"""
|
||||
Perform a high-precision validation of a routed path.
|
||||
Returns a dictionary with validation results.
|
||||
"""
|
||||
if not result.path:
|
||||
return {"is_valid": False, "reason": "No path found"}
|
||||
|
||||
collision_geoms = []
|
||||
# High-precision safety zones
|
||||
safe_zones = []
|
||||
if start_port_coord:
|
||||
safe_zones.append(Point(start_port_coord).buffer(0.002))
|
||||
if end_port_coord:
|
||||
safe_zones.append(Point(end_port_coord).buffer(0.002))
|
||||
safe_poly = unary_union(safe_zones) if safe_zones else None
|
||||
|
||||
# Buffer by C/2
|
||||
dilation = clearance / 2.0
|
||||
|
||||
for comp in result.path:
|
||||
for poly in comp.geometry:
|
||||
dilated = poly.buffer(dilation)
|
||||
for obs in static_obstacles:
|
||||
if dilated.intersects(obs):
|
||||
intersection = dilated.intersection(obs)
|
||||
if safe_poly:
|
||||
# Remove safe zones from intersection
|
||||
intersection = intersection.difference(safe_poly)
|
||||
|
||||
if not intersection.is_empty and intersection.area > 1e-9:
|
||||
collision_geoms.append(intersection)
|
||||
|
||||
return {
|
||||
"is_valid": len(collision_geoms) == 0,
|
||||
"collisions": collision_geoms,
|
||||
"collision_count": len(collision_geoms),
|
||||
}
|
||||
45
inire/utils/visualization.py
Normal file
45
inire/utils/visualization.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.figure import Figure
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
|
||||
|
||||
def plot_routing_results(
|
||||
results: dict[str, RoutingResult],
|
||||
static_obstacles: list[Polygon],
|
||||
bounds: tuple[float, float, float, float],
|
||||
) -> tuple[Figure, Axes]:
|
||||
"""Plot obstacles and routed paths using matplotlib."""
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
||||
# Plot static obstacles (gray)
|
||||
for poly in static_obstacles:
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.5, fc="gray", ec="black")
|
||||
|
||||
# Plot paths
|
||||
colors = plt.get_cmap("tab10")
|
||||
for i, (net_id, res) in enumerate(results.items()):
|
||||
color = colors(i)
|
||||
if not res.is_valid:
|
||||
color = "red" # Highlight failing nets
|
||||
|
||||
for comp in res.path:
|
||||
for poly in comp.geometry:
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if i == 0 else "")
|
||||
|
||||
ax.set_xlim(bounds[0], bounds[2])
|
||||
ax.set_ylim(bounds[1], bounds[3])
|
||||
ax.set_aspect("equal")
|
||||
ax.set_title("Inire Routing Results")
|
||||
plt.grid(True)
|
||||
return fig, ax
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "inire"
|
||||
description = "Wave-router"
|
||||
description = "Wave-router: Auto-routing for photonic and RF integrated circuits"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { file = "LICENSE.md" }
|
||||
|
|
@ -9,22 +9,6 @@ authors = [
|
|||
]
|
||||
homepage = "https://mpxd.net/code/jan/inire"
|
||||
repository = "https://mpxd.net/code/jan/inire"
|
||||
keywords = [
|
||||
"layout",
|
||||
"CAD",
|
||||
"EDA",
|
||||
"mask",
|
||||
"pattern",
|
||||
"lithography",
|
||||
"oas",
|
||||
"gds",
|
||||
"dxf",
|
||||
"svg",
|
||||
"OASIS",
|
||||
"gdsii",
|
||||
"gds2",
|
||||
"stream",
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Development Status :: 4 - Beta",
|
||||
|
|
@ -36,10 +20,17 @@ classifiers = [
|
|||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"scipy",
|
||||
"shapely",
|
||||
"rtree",
|
||||
"matplotlib",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"hypothesis>=6.151.9",
|
||||
"matplotlib>=3.10.8",
|
||||
"pytest>=9.0.2",
|
||||
"ruff>=0.15.5",
|
||||
|
|
@ -79,7 +70,7 @@ lint.ignore = [
|
|||
"C408", # dict(x=y) instead of {'x': y}
|
||||
"PLR09", # Too many xxx
|
||||
"PLR2004", # magic number
|
||||
"PLC0414", # import x as x
|
||||
#"PLC0414", # import x as x
|
||||
"TRY003", # Long exception message
|
||||
]
|
||||
|
||||
|
|
|
|||
264
uv.lock
generated
264
uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
|
|
@ -19,6 +19,28 @@ dependencies = [
|
|||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 },
|
||||
|
|
@ -63,6 +85,11 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -80,6 +107,22 @@ version = "4.61.1"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410 },
|
||||
|
|
@ -107,6 +150,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hypothesis"
|
||||
version = "6.151.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/e1/ef365ff480903b929d28e057f57b76cae51a30375943e33374ec9a165d9c/hypothesis-6.151.9.tar.gz", hash = "sha256:2f284428dda6c3c48c580de0e18470ff9c7f5ef628a647ee8002f38c3f9097ca", size = 463534 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f7/5cc291d701094754a1d327b44d80a44971e13962881d9a400235726171da/hypothesis-6.151.9-py3-none-any.whl", hash = "sha256:7b7220585c67759b1b1ef839b1e6e9e3d82ed468cfc1ece43c67184848d7edd9", size = 529307 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
|
|
@ -118,20 +173,33 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "inire"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "hypothesis" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "hypothesis", specifier = ">=6.151.9" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.8" },
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "ruff", specifier = ">=0.15.5" },
|
||||
|
|
@ -143,6 +211,32 @@ version = "1.4.9"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961 },
|
||||
|
|
@ -194,6 +288,11 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -213,6 +312,20 @@ dependencies = [
|
|||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474 },
|
||||
|
|
@ -241,6 +354,9 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -249,6 +365,28 @@ version = "2.4.2"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 },
|
||||
|
|
@ -291,6 +429,13 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -308,6 +453,28 @@ version = "12.1.1"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364 },
|
||||
|
|
@ -358,6 +525,13 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -415,6 +589,22 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtree"
|
||||
version = "1.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.5"
|
||||
|
|
@ -440,6 +630,65 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shapely"
|
||||
version = "2.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
@ -448,3 +697,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue