Compare commits
42 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 457451d3b2 | |||
| 519dd48131 | |||
| a77ae781a7 | |||
| 62d357c147 | |||
| 148aca45d4 | |||
| 7455917b4a | |||
| bc4184693d | |||
| de7254f8f5 | |||
| d2d619fe1f | |||
| f505694523 | |||
| 7e6be50a86 | |||
| c989ab6b9f | |||
| 22ec194560 | |||
| b1feaa89f8 | |||
| 6ec953b76e | |||
| 51d8ddca51 | |||
| 064aed31a6 | |||
| 6827283886 | |||
| 8833240755 | |||
| d438c5b7c7 | |||
| 24ca402f67 | |||
| 3810e64a5c | |||
| c6116f88f3 | |||
| 91256cbcf9 | |||
| 7b0dddfe45 | |||
| 9fac436c50 | |||
| c36bce9978 | |||
| 8bf0ff279f | |||
| 8424171946 | |||
| 8eb0dbf64a | |||
| c9bb8d6469 | |||
| 58873692d6 | |||
| 4714bed9a8 | |||
| ba76589ffb | |||
| 4cbd15bc0d | |||
| 556241bae3 | |||
| 43a9a6cb3a | |||
| 41a2d9f058 | |||
| 18b2f83a7b | |||
| 82aaf066e2 | |||
| 07d079846b | |||
| f600b52f32 |
53 changed files with 5698 additions and 23 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,3 +8,7 @@ wheels/
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
.hypothesis
|
||||||
|
*.png
|
||||||
|
|
||||||
|
|
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
99
DOCS.md
Normal file
99
DOCS.md
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Inire Configuration & API Documentation
|
||||||
|
|
||||||
|
This document describes the user-tunable parameters for the `inire` auto-router.
|
||||||
|
|
||||||
|
## 1. AStarContext Parameters
|
||||||
|
|
||||||
|
The `AStarContext` stores the configuration and persistent state for the A* search. It is initialized once and passed to `route_astar` or the `PathFinder`.
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
| :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ |
|
||||||
|
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. |
|
||||||
|
| `snap_size` | `float` | 5.0 | Grid size (µm) for expansion moves. Larger values speed up search. |
|
||||||
|
| `max_straight_length` | `float` | 2000.0 | Maximum length (µm) of a single straight segment. |
|
||||||
|
| `min_straight_length` | `float` | 5.0 | Minimum length (µm) of a single straight segment. |
|
||||||
|
| `bend_radii` | `list[float]` | `[50.0, 100.0]` | Available radii for 90-degree turns (µm). |
|
||||||
|
| `sbend_radii` | `list[float]` | `[5.0, 10.0, 50.0, 100.0]` | Available radii for S-bends (µm). |
|
||||||
|
| `sbend_offsets` | `list[float] \| None` | `None` (Auto) | Lateral offsets for parametric S-bends. |
|
||||||
|
| `bend_penalty` | `float` | 250.0 | Flat cost added for every 90-degree bend. |
|
||||||
|
| `sbend_penalty` | `float` | 500.0 | Flat cost added for every S-bend. |
|
||||||
|
| `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. |
|
||||||
|
| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide for clipped models. |
|
||||||
|
|
||||||
|
## 2. AStarMetrics
|
||||||
|
|
||||||
|
The `AStarMetrics` object collects performance data during the search.
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| :--------------------- | :---- | :---------------------------------------------------- |
|
||||||
|
| `nodes_expanded` | `int` | Number of nodes expanded in the last `route_astar` call. |
|
||||||
|
| `total_nodes_expanded` | `int` | Cumulative nodes expanded across all calls. |
|
||||||
|
| `max_depth_reached` | `int` | Deepest point in the search tree reached. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. CostEvaluator Parameters
|
||||||
|
|
||||||
|
The `CostEvaluator` defines the "goodness" of a path.
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
| :------------------- | :------ | :--------- | :--------------------------------------------------------------------------------------- |
|
||||||
|
| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. |
|
||||||
|
| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g. `1.5`) speed up search. |
|
||||||
|
| `congestion_penalty` | `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. PathFinder Parameters
|
||||||
|
|
||||||
|
The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm.
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
| :------------------------ | :------ | :------ | :-------------------------------------------------------------------------------------- |
|
||||||
|
| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. |
|
||||||
|
| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. CollisionEngine Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
| :------------------- | :------ | :--------- | :------------------------------------------------------------------------------------ |
|
||||||
|
| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). |
|
||||||
|
| `safety_zone_radius` | `float` | 0.0021 | Radius (µm) around ports where collisions are ignored for PDK boundary incidence. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Physical Units & Precision
|
||||||
|
- **Coordinates**: Micrometers (µm).
|
||||||
|
- **Grid Snapping**: The router internally operates on a **1nm** grid for final ports and a **1µm** lattice for expansion moves.
|
||||||
|
- **Search Space**: Assumptions are optimized for design areas up to **20mm x 20mm**.
|
||||||
|
- **Design Bounds**: The boundary limits defined in `DangerMap` strictly constrain the **physical edges** (dilated geometry) of the waveguide. Any move that would cause the waveguide or its required clearance to extend beyond these bounds is rejected with an infinite cost.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Best Practices & Tuning Advice
|
||||||
|
|
||||||
|
### Speed vs. Optimality
|
||||||
|
The `greedy_h_weight` is your primary lever for search performance.
|
||||||
|
- **`1.0`**: Dijkstra-like behavior. Guarantees the shortest path but is very slow.
|
||||||
|
- **`1.1` to `1.2`**: Recommended range. Balances wire length with fast convergence.
|
||||||
|
- **`> 1.5`**: Extremely fast "greedy" search. May produce zig-zags or suboptimal detours.
|
||||||
|
|
||||||
|
### Avoiding "Zig-Zags"
|
||||||
|
If the router produces many small bends instead of a long straight line:
|
||||||
|
1. Increase `bend_penalty` (e.g., set to `100.0` or higher).
|
||||||
|
2. Ensure `straight_lengths` includes larger values like `25.0` or `100.0`.
|
||||||
|
3. Decrease `greedy_h_weight` closer to `1.0`.
|
||||||
|
|
||||||
|
### Handling Congestion
|
||||||
|
In multi-net designs, if nets are overlapping:
|
||||||
|
1. Increase `congestion_penalty` in `CostEvaluator`.
|
||||||
|
2. Increase `max_iterations` in `PathFinder`.
|
||||||
|
3. If a solution is still not found, check if the `clearance` is physically possible given the design's narrowest bottlenecks.
|
||||||
|
|
||||||
|
### S-Bend Usage
|
||||||
|
Parametric S-bends bridge lateral gaps without changing the waveguide's orientation.
|
||||||
|
- **Automatic Selection**: If `sbend_offsets` is set to `None` (the default), the router automatically chooses from a set of "natural" offsets (Fibonacci-aligned grid steps) and the offset needed to hit the target.
|
||||||
|
- **Specific Offsets**: To use specific offsets (e.g., 5.86µm for a 45° switchover), provide them in the `sbend_offsets` list. The router will prioritize these but will still try to align with the target if possible.
|
||||||
|
- **Constraints**: S-bends are only used for offsets $O < 2R$. For larger shifts, the router naturally combines two 90° bends and a straight segment.
|
||||||
93
README.md
93
README.md
|
|
@ -0,0 +1,93 @@
|
||||||
|
# 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 AStarContext
|
||||||
|
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(
|
||||||
|
collision_engine=engine,
|
||||||
|
danger_map=danger_map,
|
||||||
|
greedy_h_weight=1.2
|
||||||
|
)
|
||||||
|
context = AStarContext(
|
||||||
|
cost_evaluator=evaluator,
|
||||||
|
bend_penalty=10.0
|
||||||
|
)
|
||||||
|
pf = PathFinder(context)
|
||||||
|
|
||||||
|
# 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!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
For detailed visual demonstrations and architectural deep-dives, see the **[Examples README](examples/README.md)**.
|
||||||
|
|
||||||
|
Check the `examples/` directory for ready-to-run scripts. To run an example:
|
||||||
|
```bash
|
||||||
|
python3 examples/01_simple_route.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`inire` is highly tunable. Every major component (Router, CostEvaluator, PathFinder) accepts explicit named arguments in its constructor to control expansion rules, cost weights, and convergence limits. See `DOCS.md` for a full parameter reference.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GNU Affero General Public License v3. See `LICENSE.md` for details.
|
||||||
42
docs/plans/cost_and_collision_engine.md
Normal file
42
docs/plans/cost_and_collision_engine.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Cost and Collision Engine Spec
|
||||||
|
|
||||||
|
This document describes the methods for ensuring "analytic correctness" while maintaining a computationally efficient cost function.
|
||||||
|
|
||||||
|
## 1. Analytic Correctness
|
||||||
|
The router balances speed and verification using a two-tier approach.
|
||||||
|
|
||||||
|
### 1.1. R-Tree Geometry Engine
|
||||||
|
The router uses an **R-Tree of Polygons** for all geometric queries.
|
||||||
|
* **Move Validation:** Every "Move" proposed by the search is checked for intersection against the R-Tree.
|
||||||
|
* **Pre-dilation for Clearance:** All obstacles and paths use a global clearance $C$. At the start of the session, all user-provided obstacles are **pre-dilated by $(W_{max} + C)/2$** for initial broad pruning. However, individual path dilation for intersection tests uses $(W_i + C)/2$ for a specific net's width $W_i$.
|
||||||
|
* **Safety Zone:** To prevent immediate collision flags for ports placed on or near obstacle boundaries, the router ignores collisions within a radius of **2nm** of start and end ports.
|
||||||
|
* **Single Layer:** All routing and collision detection occur on a single layer.
|
||||||
|
|
||||||
|
### 1.2. Cost Calculation (Soft Constraint)
|
||||||
|
The "Danger Cost" $g_{proximity}(n)$ is a function of the distance $d$ to the nearest obstacle:
|
||||||
|
$$g_{proximity}(n) = \begin{cases} \infty & \text{if } d < (W_i + C)/2 \\ \frac{k}{d^2} & \text{if } (W_i + C)/2 \le d < \text{Safety Threshold} \\ 0 & \text{if } d \ge \text{Safety Threshold} \end{cases}$$
|
||||||
|
|
||||||
|
To optimize A* search, a **Static Danger Map (Precomputed Grid)** is used for the heuristic.
|
||||||
|
* **Grid Resolution:** Default **1000nm (1µm)**.
|
||||||
|
* **Static Nature:** The grid only accounts for fixed obstacles. It is computed once at the start of the session and is **not re-computed** during the Negotiated Congestion loop.
|
||||||
|
* **Efficiency:** For a 20x20mm layout, this results in a 20k x 20k matrix.
|
||||||
|
* **Memory:** Using a `uint8` or `float16` representation, this consumes ~400-800MB (Default < 2GB). For extremely high resolution or larger areas, the system supports up to **20GB** allocation.
|
||||||
|
* **Precision:** Strict intersection checks still use the R-Tree for "analytic correctness."
|
||||||
|
|
||||||
|
## 2. Collision Detection Implementation
|
||||||
|
The system relies on `shapely` for geometry and `rtree` for spatial indexing.
|
||||||
|
1. **Arc Resolution:** Arcs are represented as polygons approximated by segments with a maximum deviation (sagitta).
|
||||||
|
2. **Intersection Test:** A "Move" is valid only if its geometry does not intersect any obstacle in the R-Tree.
|
||||||
|
3. **Self-Intersection:** Paths from the same net must not intersect themselves.
|
||||||
|
4. **No Crossings:** Strictly 2D; no crossings (vias or bridges) are supported.
|
||||||
|
|
||||||
|
## 3. Negotiated Congestion (Path R-Tree)
|
||||||
|
To handle multiple nets, the router maintains a separate R-Tree containing the dilated geometries ($C/2$) of all currently routed paths.
|
||||||
|
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
|
||||||
|
* **Failure Policy:** If no collision-free path is found after the max iterations, the router returns the **"least-bad" (lowest cost)** path. These paths MUST be explicitly flagged as invalid (e.g., via an `is_valid=False` attribute or a non-zero `collision_count`) so the user can identify and manually fix the failure.
|
||||||
|
|
||||||
|
## 4. Handling Global Clearances
|
||||||
|
Clearances are global. Both obstacles and paths are pre-dilated once. This ensures that any two objects maintain at least $C$ distance if their dilated versions do not intersect.
|
||||||
|
|
||||||
|
## 5. Locked Paths
|
||||||
|
The router supports **Locked Paths**—existing geometries inserted into the static Obstacle R-Tree, ensuring they are never modified or rerouted.
|
||||||
37
docs/plans/geometric_representation.md
Normal file
37
docs/plans/geometric_representation.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Geometric Representation and Data Structures
|
||||||
|
|
||||||
|
This document defines the core data structures for representing the routing problem.
|
||||||
|
|
||||||
|
## 1. Port Definitions (Connectivity)
|
||||||
|
Routing requests are defined as a mapping of source ports to destination ports.
|
||||||
|
|
||||||
|
* **Port Structure:** `(x, y, orientation)`
|
||||||
|
* `x, y`: Coordinates snapped to **1nm** grid.
|
||||||
|
* `orientation`: Strictly $\{0, 90, 180, 270\}$ degrees.
|
||||||
|
* **Netlist:** A dictionary or list of tuples: `{(start_port): (end_port)}`. Each entry represents a single-layer path that must be routed.
|
||||||
|
* **Heterogeneous Widths:** The router supports different widths $W_i$ per net. Dilation for a specific net $i$ is calculated as $(W_i + C)/2$ to maintain the global clearance $C$.
|
||||||
|
|
||||||
|
## 2. Component Library (Move Generator)
|
||||||
|
The router uses a discrete set of components to expand states in the A* search.
|
||||||
|
|
||||||
|
### 2.1. Straight Waveguides
|
||||||
|
* **A* Expansion:** Generates "Straight" moves of varying lengths (e.g., $1\mu m, 5\mu m, 25\mu m$).
|
||||||
|
* **Snap-to-Target:** If the destination port $T$ is directly ahead and the current state's orientation matches $T$'s entry orientation, a special move is generated to close the gap exactly.
|
||||||
|
|
||||||
|
### 2.2. Fixed 90° Bends (PDK Cells)
|
||||||
|
* **Parameters:** `radius`, `width`.
|
||||||
|
* **A* Expansion:** A discrete move that changes orientation by $\pm 90^\circ$ and shifts the coordinate by the radius.
|
||||||
|
* **Grid Alignment:** If the bend radius $R$ is not a multiple of the search grid (default $1\mu m$), the resulting state is **snapped to the nearest grid point**, and a warning is issued to the user.
|
||||||
|
|
||||||
|
### 2.3. Parametric S-Bends (Compact)
|
||||||
|
* **Parameters:** `radius`, `width`.
|
||||||
|
* **A* Expansion:** Used ONLY for lateral offsets $O < 2R$.
|
||||||
|
* **Large Offsets ($O \ge 2R$):** The router does not use a single S-bend move for large offsets. Instead, the A* search naturally finds the optimal path by combining two 90° bends and a straight segment. This ensures maximum flexibility in obstacle avoidance for large shifts.
|
||||||
|
|
||||||
|
## 3. Obstacle Representation
|
||||||
|
Obstacles are provided as raw polygons on a single layer.
|
||||||
|
|
||||||
|
* **Pre-processing:** All input polygons are inserted into an **R-Tree**.
|
||||||
|
* **Buffer/Dilation:** Obstacles are pre-dilated by $(W_{max} + Clearance)/2$ for initial pruning, but final collision tests use the net-specific width $W_i$.
|
||||||
|
* **No Multi-layer:** The router assumes all obstacles and paths share the same plane.
|
||||||
|
* **Safety Zone:** Ignore collisions within **2nm** of start and end ports for robustness.
|
||||||
13
docs/plans/high_level_notes.md
Normal file
13
docs/plans/high_level_notes.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# High-Level Notes: Auto-Routing for Integrated Circuits
|
||||||
|
|
||||||
|
We're implementing auto-routing for photonic and RF integrated circuits. The problem space has the following features:
|
||||||
|
|
||||||
|
- **Single Layer:** All paths are on a single layer; multi-layer routing is not supported.
|
||||||
|
- **No Crossings:** Crossings are to be avoided and are not supported by the router's automatic placement. The user must manually handle any required crossings.
|
||||||
|
- **Large Bend Radii:** Bends use large radii ($R$), and are usually pre-generated (e.g. 90-degree cells).
|
||||||
|
- **Proximity Sensitivity:** Paths are sensitive to proximity to other paths and obstacles (crosstalk/coupling).
|
||||||
|
- **Manhattan Preference:** Manhattan pathing is sufficient for most cases; any-angle is rare.
|
||||||
|
- **S-Bends:** S-bends are necessary for small lateral offsets ($O < 2R$).
|
||||||
|
- **Hybrid Search:** A* state-lattice search is used for discrete component placement.
|
||||||
|
- **Dilation:** A $Clearance/2$ dilation is applied to all obstacles and paths for efficient collision avoidance.
|
||||||
|
- **Negotiated Congestion:** A multi-net "PathFinder" loop iteratively reroutes nets to resolve congestion.
|
||||||
64
docs/plans/implementation_plan.md
Normal file
64
docs/plans/implementation_plan.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
This plan outlines the step-by-step implementation of the `inire` auto-router. For detailed test cases, refer to [Testing Plan](./testing_plan.md).
|
||||||
|
|
||||||
|
## Phase 1: Core Geometry & Move Generation
|
||||||
|
**Goal:** Implement Ports, Polygons, and Component Library with high geometric fidelity.
|
||||||
|
1. **Project Setup:** Initialize `inire/` structure and `pytest` configuration. Include `hypothesis` for property-based testing.
|
||||||
|
2. **`geometry.primitives`:**
|
||||||
|
* `Port` with **1nm** snapping.
|
||||||
|
* Basic 2D transformations (rotate, translate).
|
||||||
|
* **Property-Based Tests:** Verify transform invariants (e.g., $90^\circ$ rotation cycles).
|
||||||
|
3. **`geometry.components`:**
|
||||||
|
* `Straight`, `Bend90`, `SBend`.
|
||||||
|
* **Search Grid Snapping:** Implement 1µm snapping for expanded ports.
|
||||||
|
* **Small S-Bends ($O < 2R$):** Logic for parametric generation.
|
||||||
|
* **Edge Cases:** Handle $O=2R$ and $L < 1\mu m$.
|
||||||
|
4. **Tests:**
|
||||||
|
* Verify geometric correctness (refer to Testing Plan Section 1).
|
||||||
|
* Unit tests for `Port` snapping and component transformations.
|
||||||
|
|
||||||
|
## Phase 2: Collision Engine & Cost
|
||||||
|
**Goal:** Build the R-Tree wrapper and the analytic cost function.
|
||||||
|
1. **`geometry.collision`:** Implement `CollisionEngine`.
|
||||||
|
* **Pre-dilation:** Obstacles/Paths dilated by $Clearance/2$.
|
||||||
|
* **Safety Zone:** Ignore collisions within **2nm** of start/end ports.
|
||||||
|
2. **`router.danger_map`:**
|
||||||
|
* Implement **1µm** pre-computed proximity grid.
|
||||||
|
* Optimize for design sizes up to **20x20mm** (< 2GB memory).
|
||||||
|
3. **`router.cost`:** Implement `CostEvaluator`.
|
||||||
|
* Bend cost: $10 \times (\text{Manhattan distance between ports})$.
|
||||||
|
* Integrate R-Tree for strict checks and Danger Map for heuristic.
|
||||||
|
4. **Tests:**
|
||||||
|
* Verify collision detection with simple overlapping shapes (Testing Plan Section 2.1).
|
||||||
|
* Verify Danger Map accuracy and memory footprint (Testing Plan Section 2.2).
|
||||||
|
* **Post-Route Validator:** Implement the independent `validate_path` utility.
|
||||||
|
|
||||||
|
## Phase 3: Single-Net A* Search
|
||||||
|
**Goal:** Route a single net from A to B with 1nm precision.
|
||||||
|
1. **`router.astar`:** Implement the priority queue loop.
|
||||||
|
* State representation: `(x_µm, y_µm, theta)`.
|
||||||
|
* Move expansion loop with 1µm grid.
|
||||||
|
* **Natural S-Bends:** Ensure search can find $O \ge 2R$ shifts by combining moves.
|
||||||
|
* **Look-ahead Snapping:** Actively bridge to the 1nm target when in the capture radius (10µm).
|
||||||
|
2. **Heuristic:** Manhattan distance $h(n)$ + orientation penalty + Danger Map lookup.
|
||||||
|
3. **Tests:**
|
||||||
|
* Solve simple maze problems and verify path optimality (Testing Plan Section 3).
|
||||||
|
* Verify snap-to-target precision at 1nm resolution.
|
||||||
|
* **Determinism:** Verify same seed = same path.
|
||||||
|
|
||||||
|
## Phase 4: Multi-Net PathFinder
|
||||||
|
**Goal:** Implement the "Negotiated Congestion" loop for multiple nets.
|
||||||
|
1. **`router.pathfinder`:**
|
||||||
|
* Sequential routing -> Identify congestion -> Inflate cost -> Reroute.
|
||||||
|
* **R-Tree Congestion:** Store dilated path geometries.
|
||||||
|
2. **Explicit Results:** Return `RoutingResult` objects with `is_valid` and `collisions` metadata.
|
||||||
|
3. **Tests:**
|
||||||
|
* Full multi-net benchmarks (Testing Plan Section 4).
|
||||||
|
* Verify rerouting behavior in crowded environments.
|
||||||
|
|
||||||
|
## Phase 5: Visualization, Benchmarking & Fuzzing
|
||||||
|
1. **`utils.visualization`:** Plot paths using `matplotlib`. Highlight collisions in red.
|
||||||
|
2. **Benchmarks:** Stress test with 50+ nets. Verify performance and node limits (Testing Plan Section 5).
|
||||||
|
3. **Fuzzing:** Run A* on randomized layouts to ensure stability.
|
||||||
|
4. **Final Validation:** Ensure all `is_valid=True` results pass the independent `validate_path` check.
|
||||||
57
docs/plans/package_structure.md
Normal file
57
docs/plans/package_structure.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Python Package Structure
|
||||||
|
|
||||||
|
This document outlines the directory structure and module organization for the `inire` auto-router package.
|
||||||
|
|
||||||
|
## 1. Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
inire/
|
||||||
|
├── __init__.py # Exposes the main `Router` class and key types
|
||||||
|
├── geometry/ # Core geometric primitives and operations
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── primitives.py # Point, Port, Polygon, Arc classes
|
||||||
|
│ ├── collision.py # R-Tree wrapper and intersection logic
|
||||||
|
│ └── components.py # Move generators (Straight, Bend90, SBend)
|
||||||
|
├── router/ # Search algorithms and pathfinding
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── astar.py # Hybrid State-Lattice A* implementation
|
||||||
|
│ ├── graph.py # Node, Edge, and Graph data structures
|
||||||
|
│ ├── cost.py # Cost functions (length, bend, proximity)
|
||||||
|
│ ├── danger_map.py # Pre-computed grid for heuristic proximity costs
|
||||||
|
│ └── pathfinder.py # Multi-net "Negotiated Congestion" manager
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── visualization.py # Plotting tools for debug/heatmaps (matplotlib/klayout)
|
||||||
|
└── tests/ # Unit and integration tests
|
||||||
|
├── __init__.py
|
||||||
|
├── conftest.py # Pytest fixtures (common shapes, PDK cells)
|
||||||
|
├── test_primitives.py # Tests for Port and coordinate transforms
|
||||||
|
├── test_components.py # Tests for Straight, Bend90, SBend generation
|
||||||
|
├── test_collision.py # Tests for R-Tree and dilation logic
|
||||||
|
├── test_cost.py # Tests for Danger Map and cost evaluation
|
||||||
|
├── test_astar.py # Tests for single-net routing (mazes, snapping)
|
||||||
|
└── test_pathfinder.py # Multi-net "Negotiated Congestion" benchmarks
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Module Responsibilities
|
||||||
|
|
||||||
|
### `inire.geometry`
|
||||||
|
* **`primitives.py`**: Defines the `Port` named tuple `(x, y, theta)` and helper functions for coordinate transforms.
|
||||||
|
* **`collision.py`**: Wraps the `rtree` or `shapely` library. Handles the "Analytic Correctness" checks (exact polygon distance).
|
||||||
|
* **`components.py`**: Logic to generate "Moves" from a start port. E.g., `SBend.generate(start_port, offset, radius)` returns a list of polygons and the end port. Handles $O > 2R$ logic.
|
||||||
|
|
||||||
|
### `inire.router`
|
||||||
|
* **`astar.py`**: The heavy lifter. Maintains the `OpenSet` (priority queue) and `ClosedSet`. Implements the "Snap-to-Target" logic.
|
||||||
|
* **`cost.py`**: compute $f(n) = g(n) + h(n)$. encapsulates the "Danger Map" and Path R-Tree lookups.
|
||||||
|
* **`danger_map.py`**: Manages the pre-computed proximity grid used for $O(1)$ heuristic calculations.
|
||||||
|
* **`pathfinder.py`**: Orchestrates the multi-net loop. Tracks the Path R-Tree for negotiated congestion and triggers reroutes.
|
||||||
|
|
||||||
|
### `inire.tests`
|
||||||
|
* **Structure:** Tests are co-located within the package for ease of access.
|
||||||
|
* **Fixtures:** `conftest.py` will provide standard PDK cells (e.g., a $10\mu m$ radius bend) to avoid repetition in test cases.
|
||||||
|
|
||||||
|
## 3. Dependencies
|
||||||
|
* `numpy`: Vector math.
|
||||||
|
* `shapely`: Polygon geometry and intersection.
|
||||||
|
* `rtree`: Spatial indexing.
|
||||||
|
* `networkx` (Optional): Not used for core search to ensure performance.
|
||||||
67
docs/plans/routing_architecture_decision.md
Normal file
67
docs/plans/routing_architecture_decision.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Architecture Decision Record: Auto-Routing for Photonic & RF ICs
|
||||||
|
|
||||||
|
## 1. Problem Context
|
||||||
|
Photonic and RF routing differ significantly from digital VLSI due to physical constraints:
|
||||||
|
* **Geometric Rigidity:** 90° bends are pre-rendered PDK cells with fixed bounding boxes.
|
||||||
|
* **Parametric Flexibility:** S-bends must be generated on-the-fly to handle arbitrary offsets, provided they maintain a constant radius $R$.
|
||||||
|
* **Signal Integrity:** High sensitivity to proximity (coupling/crosstalk) and a strong preference for single-layer, non-crossing paths.
|
||||||
|
* **Manual Intervention:** The router is strictly 2D and avoids all other geometries on the same layer. No crossings (e.g. vias or bridges) are supported by the automatic routing engine. The user must manually handle any required crossings by placing components (e.g. crossing cells) and splitting the net list accordingly. This simplifies the router's task to 2D obstacle avoidance and spacing optimization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Candidate Algorithms & Tradeoffs
|
||||||
|
|
||||||
|
### 2.1. Rubber-Band (Topological) Routing
|
||||||
|
This approach treats paths as elastic bands stretched around obstacles, later "inflating" them to have width and curvature.
|
||||||
|
|
||||||
|
| Feature | Analysis |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Strengths** | Excellent at "River Routing" and maintaining minimum clearances. Inherently avoids crossings. |
|
||||||
|
| **Downsides** | **The Inflation Gap:** A valid thin-line topology may be physically un-routable once the large radius $R$ is applied. It struggles to integrate rigid, pre-rendered 90° blocks into a continuous elastic model. |
|
||||||
|
| **Future Potential** | High, if a "Post-Processing" engine can reliably snap elastic curves to discrete PDK cells without breaking connectivity. |
|
||||||
|
|
||||||
|
### 2.2. Voronoi-Based (Medial Axis) Routing
|
||||||
|
Uses a Voronoi diagram to find paths that are maximally distant from all obstacles.
|
||||||
|
|
||||||
|
| Feature | Analysis |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Strengths** | Theoretically optimal for "Safety" and crosstalk reduction. Guaranteed maximum clearance. |
|
||||||
|
| **Downsides** | **Manhattan Incompatibility:** Voronoi edges are any-angle and often jagged. Mapping these to a Manhattan-heavy PDK (90° bends) requires a lossy "snapping" phase that often violates the very safety the algorithm intended to provide. |
|
||||||
|
| **Future Potential** | Useful as a "Channel Finder" to guide a more rigid router, but unsuitable as a standalone geometric solver. |
|
||||||
|
|
||||||
|
### 2.3. Integer Linear Programming (ILP)
|
||||||
|
Formulates routing as a massive optimization problem where a solver picks the best path from a pool of candidates.
|
||||||
|
|
||||||
|
| Feature | Analysis |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Strengths** | Can find the mathematically "best" layout (e.g., minimum total length or total bends) across all nets simultaneously. |
|
||||||
|
| **Downsides** | **Candidate Explosion:** Because S-bends are generated on-the-fly, the number of possible candidate shapes is infinite. To use ILP, one must "discretize" the search space, which may miss the one specific geometry needed for a tight fit. |
|
||||||
|
| **Future Potential** | Effective for small, high-congestion "Switchbox" areas where all possible geometries can be pre-tabulated. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Selected Approach: Hybrid State-Lattice A*
|
||||||
|
|
||||||
|
### 3.1. Rationale
|
||||||
|
The **State-Lattice** variant of the A* algorithm is selected as the primary routing engine. Unlike standard A* which moves between grid cells, State-Lattice moves between **states** defined as $(x, y, \theta)$.
|
||||||
|
|
||||||
|
1. **Native PDK Integration:** The router treats the pre-rendered 90° bend cell as a discrete "Move" in the search tree. The algorithm only considers placing a bend if the cell's bounding box is clear of obstacles.
|
||||||
|
2. **Parametric S-Bends:** When the search needs to bridge a lateral gap, it triggers a "Procedural Move." It calculates a fixed-radius S-bend on-the-fly. If the resulting arc is valid and collision-free, it is added as an edge in the search graph.
|
||||||
|
3. **Predictable Costing:** It allows for a sophisticated cost function $f(n) = g(n) + h(n)$ where:
|
||||||
|
* $g(n)$ penalizes path length and proximity to obstacles (using a distance-transform field).
|
||||||
|
* $h(n)$ (the heuristic) guides the search toward the destination while favoring Manhattan alignments.
|
||||||
|
|
||||||
|
### 3.2. Implementation Strategy
|
||||||
|
* **Step 1: Distance Transform.** Pre-calculate a "Danger Map" of the layout. Cells close to obstacles have a high cost; cells far away have low cost. This handles the **Proximity Sensitivity** constraint.
|
||||||
|
* **Step 2: State Expansion.** From the current point, explore:
|
||||||
|
* `Straight(length)`
|
||||||
|
* `PDK_Bend_90(direction)`
|
||||||
|
* `S_Bend(target_offset, R)`
|
||||||
|
* **Step 3: Rip-up and Reroute.** To handle the sequential nature of A*, implement a "Negotiated Congestion" scheme (PathFinder algorithm) where nets "pay" more to occupy areas that other nets also want.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Summary of Tradeoffs for Future Review
|
||||||
|
* **Why not pure Manhattan?** Photonic/RF requirements for large $R$ and S-bends make standard grid-based maze routers obsolete.
|
||||||
|
* **Why not any-angle?** While any-angle is possible, the PDK's reliance on pre-rendered 90° blocks makes a lattice-based approach more manufacturing-stable.
|
||||||
|
* **Risk:** The primary risk is **Search Time**. As the library of moves grows (more S-bend options), the branching factor increases. This must be managed with aggressive pruning and spatial indexing (e.g., R-trees).
|
||||||
66
docs/plans/routing_search_spec.md
Normal file
66
docs/plans/routing_search_spec.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Routing Search Specification
|
||||||
|
|
||||||
|
This document details the Hybrid State-Lattice A* implementation and the "PathFinder" (Negotiated Congestion) algorithm for multi-net routing.
|
||||||
|
|
||||||
|
## 1. Hybrid State-Lattice A* Search
|
||||||
|
The core router operates on states $S = (x, y, \theta)$, where $\theta \in \{0, 90, 180, 270\}$.
|
||||||
|
|
||||||
|
### 1.1. State Representation & Snapping
|
||||||
|
To ensure search stability and hash-map performance:
|
||||||
|
* **Intermediate Ports:** Every state expanded by A* is snapped to a search grid.
|
||||||
|
* **Search Grid:** Default snap is **1000nm (1µm)**.
|
||||||
|
* **Final Alignment:** The "Snap-to-Target" logic bridges the gap from the coarse search grid to the final **1nm** port coordinates.
|
||||||
|
* **Max Design Size:** Guidelines for memory/performance assume up to **20mm x 20mm** routing area.
|
||||||
|
|
||||||
|
### 1.2. Move Expansion Logic
|
||||||
|
From state $S_n$, the following moves are evaluated:
|
||||||
|
1. **Tiered Straight Steps:** Expand by a set of distances $L \in \{1\mu m, 5\mu m, 25\mu m\}$.
|
||||||
|
2. **Snap-to-Target:** A "last-inch" look-ahead move. If the target $T$ is within a **Capture Radius (Default: 10µm)** and a straight segment or single bend can reach it, a special move is generated to close the gap exactly at 1nm resolution.
|
||||||
|
3. **90° Bend:** Try clockwise/counter-clockwise turns using fixed PDK cells.
|
||||||
|
4. **Small-Offset S-Bend:**
|
||||||
|
* **Only for $O < 2R$:** Parametric S-bend (two tangent arcs).
|
||||||
|
* **$O \ge 2R$:** Search naturally finds these by combining 90° bends and straight segments.
|
||||||
|
|
||||||
|
### 1.3. Cost Function $f(n) = g(n) + h(n)$
|
||||||
|
The search uses a flexible, component-based cost model.
|
||||||
|
|
||||||
|
* **$g(n)$ (Actual Cost):** $\sum \text{ComponentCost}_i + \text{ProximityCost} + \text{CongestionCost}$
|
||||||
|
* **Straight Segment:** $L \times C_{unit\_length}$.
|
||||||
|
* **90° Bend:** $10 \times (\text{Manhattan distance between ports})$.
|
||||||
|
* **S-Bend:** $f(O, R)$.
|
||||||
|
* **Proximity Cost:** $k/d^2$ penalty (strict checks use R-Tree).
|
||||||
|
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
|
||||||
|
* **$h(n)$ (Heuristic):**
|
||||||
|
* Manhattan distance $L_1$ to the target.
|
||||||
|
* Orientation Penalty: High cost if the state's orientation doesn't match the target's entry orientation.
|
||||||
|
* **Greedy Weighting:** The A* search uses a weighted heuristic (e.g., $1.1 \times h(n)$) to prioritize search speed over strict path optimality.
|
||||||
|
* **Danger Map Heuristic:** Fast lookups from the **1µm** pre-computed proximity grid.
|
||||||
|
|
||||||
|
## 2. Multi-Net "PathFinder" Strategy (Negotiated Congestion)
|
||||||
|
1. **Iteration:** Identify "Congestion Areas" using Path R-Tree intersections.
|
||||||
|
2. **Inflation:** Increase penalty multiplier $P$ for congested areas.
|
||||||
|
3. **Repeat:** Continue until no overlaps exist or the max iteration count is reached (Default: **20 iterations**).
|
||||||
|
|
||||||
|
### 2.1. Convergence & Result Policy
|
||||||
|
* **Least Bad Attempt:** If no 100% collision-free solution is found, return the result with the lowest total cost (including overlaps).
|
||||||
|
* **Explicit Reporting:** Results MUST include a `RoutingResult` object containing:
|
||||||
|
* `path_geometry`: The actual polygon sequence.
|
||||||
|
* `is_valid`: Boolean (True only if no collisions).
|
||||||
|
* `collisions`: A count or list of detected overlap points/polygons.
|
||||||
|
* **Visualization:** Overlapping regions are highlighted (e.g., in red) in the heatmaps.
|
||||||
|
|
||||||
|
## 3. Search Limits & Scaling
|
||||||
|
* **Node Limit:** A* search is capped at **50,000 nodes** per net per iteration.
|
||||||
|
* **Dynamic Timeout:** Session-level timeout based on problem size:
|
||||||
|
* `Timeout = max(2s, 0.05s * num_nets * num_iterations)`.
|
||||||
|
* *Example:* A 100-net problem over 20 iterations times out at **100 seconds**.
|
||||||
|
|
||||||
|
## 4. Determinism
|
||||||
|
All search and rip-up operations are strictly deterministic.
|
||||||
|
* **Seed:** A user-provided `seed` (int) MUST be used to initialize any random number generators (e.g., if used for tie-breaking or initial net ordering).
|
||||||
|
* **Tie-Breaking:** If two nodes have the same $f(n)$, a consistent tie-breaking rule (e.g., based on node insertion order or state hash) must be used.
|
||||||
|
|
||||||
|
## 5. Optimizations
|
||||||
|
* **A* Pruning:** Head toward the target and prune suboptimal orientations.
|
||||||
|
* **Safety Zones:** Ignore collisions within **2nm** of start/end ports.
|
||||||
|
* **Spatial Indexing:** R-Tree queries are limited to the bounding box of the current move.
|
||||||
138
docs/plans/testing_plan.md
Normal file
138
docs/plans/testing_plan.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Testing Plan (Refined)
|
||||||
|
|
||||||
|
This document defines the comprehensive testing strategy for the `inire` auto-router, ensuring analytic correctness and performance.
|
||||||
|
|
||||||
|
## 1. Unit Tests: Geometry & Components (`inire/geometry`)
|
||||||
|
|
||||||
|
### 1.1. Primitives (`primitives.py`)
|
||||||
|
* **Port Snapping:** Verify that `Port(x, y, orientation)` snaps `x` and `y` to the nearest **1nm** grid point.
|
||||||
|
* **Coordinate Transforms:**
|
||||||
|
* Translate a port by `(dx, dy)`.
|
||||||
|
* Rotate a port by `90`, `180`, `270` degrees around an origin.
|
||||||
|
* Verify orientation wrapping (e.g., $270^\circ + 90^\circ \to 0^\circ$).
|
||||||
|
* **Polygon Creation:**
|
||||||
|
* Generate a polygon from a list of points.
|
||||||
|
* Verify bounding box calculation.
|
||||||
|
* **Property-Based Testing (Hypothesis):**
|
||||||
|
* Verify that any `Port` after transformation remains snapped to the **1nm** grid.
|
||||||
|
* Verify that $Rotate(Rotate(Port, 90), -90)$ returns the original `Port` (up to snapping).
|
||||||
|
|
||||||
|
### 1.2. Component Generation (`components.py`)
|
||||||
|
* **Straight Moves:**
|
||||||
|
* Generate a `Straight(length=10.0, width=2.0)` segment.
|
||||||
|
* Verify the end port is exactly `length` away in the correct orientation.
|
||||||
|
* Verify the resulting polygon's dimensions.
|
||||||
|
* **Edge Case:** $L < 1\mu m$ (below search grid). Verify it still generates a valid 1nm segment.
|
||||||
|
* **Bend90:**
|
||||||
|
* Generate a `Bend90(radius=10.0, width=2.0, direction='CW')`.
|
||||||
|
* Verify the end port's orientation is changed by $-90^\circ$.
|
||||||
|
* Verify the end port's position is exactly `(R, -R)` relative to the start (for $0^\circ \to 270^\circ$).
|
||||||
|
* **Grid Snapping:** Verify that if the bend radius results in a non-1µm aligned port, it is snapped to the nearest **1µm** search grid point (with a warning).
|
||||||
|
* **SBend (Small Offset $O < 2R$):**
|
||||||
|
* Generate an `SBend(offset=5.0, radius=10.0, width=2.0)`.
|
||||||
|
* Verify the total length matches the analytical $L = 2\sqrt{O(2R - O/4)}$ (or equivalent arc-based formula).
|
||||||
|
* Verify the tangent continuity at the junction of the two arcs.
|
||||||
|
* **Edge Case:** $O = 2R$. Verify it either generates two 90-degree bends or fails gracefully with a clear error.
|
||||||
|
* Verify it fails/warns if $O > 2R$.
|
||||||
|
|
||||||
|
### 1.3. Geometric Fidelity
|
||||||
|
* **Arc Resolution (Sagitta):**
|
||||||
|
* Verify that `Bend90` and `SBend` polygons are approximated by segments such that the maximum deviation (sagitta) is within a user-defined tolerance (e.g., 10nm).
|
||||||
|
* Test with varying radii to ensure segment count scales appropriately.
|
||||||
|
|
||||||
|
## 2. Unit Tests: Collision & Cost (`inire/geometry/collision` & `router/cost`)
|
||||||
|
|
||||||
|
### 2.1. Collision Engine
|
||||||
|
* **Pre-dilation Logic:**
|
||||||
|
* Verify that an obstacle (polygon) is correctly dilated by $(W_{max} + C)/2$.
|
||||||
|
* **Heterogeneous Widths:** Verify that a path for Net A (width $W_1$) is dilated by $(W_1 + C)/2$, while Net B (width $W_2$) uses $(W_2 + C)/2$.
|
||||||
|
* **Locked Paths:**
|
||||||
|
* Insert an existing path geometry into the "Static Obstacle" R-Tree.
|
||||||
|
* Verify that the router treats it as an unmovable obstacle and avoids it.
|
||||||
|
* **R-Tree Queries:**
|
||||||
|
* Test intersection detection between two overlapping polygons.
|
||||||
|
* Test non-intersection between adjacent but non-overlapping polygons (exactly $C$ distance apart).
|
||||||
|
* **Safety Zone (2nm):**
|
||||||
|
* Create a port exactly on the edge of an obstacle.
|
||||||
|
* Verify that a "Move" starting from this port is NOT flagged for collision if the intersection occurs within **2nm** of the port.
|
||||||
|
* **Self-Intersection:**
|
||||||
|
* Verify that a path consisting of multiple segments is flagged if it loops back on itself.
|
||||||
|
|
||||||
|
### 2.2. Danger Map & Cost Evaluator
|
||||||
|
* **Danger Map Generation:**
|
||||||
|
* Initialize a map for a $100\mu m \times 100\mu m$ area with a single obstacle.
|
||||||
|
* Verify the cost $g_{proximity}$ matches $k/d^2$ for cells near the obstacle.
|
||||||
|
* Verify cost is $0$ for cells beyond the **Safety Threshold**.
|
||||||
|
* **Memory Check:**
|
||||||
|
* Mock a $20mm \times 20mm$ grid and verify memory allocation stays within limits (e.g., `< 2GB` for standard `uint8` resolution).
|
||||||
|
* **Cost Calculation:**
|
||||||
|
* Verify total cost $f(n)$ correctly sums length, bend penalties ($10 \times$ Manhattan), and proximity costs.
|
||||||
|
|
||||||
|
### 2.3. Robustness & Limits
|
||||||
|
* **Design Bounds:**
|
||||||
|
* Test routing at the extreme edges of the $20mm \times 20mm$ coordinate space.
|
||||||
|
* Verify that moves extending outside the design bounds are correctly pruned or flagged.
|
||||||
|
* **Empty/Invalid Inputs:**
|
||||||
|
* Test with an empty netlist.
|
||||||
|
* Test with start and end ports at the exact same location.
|
||||||
|
|
||||||
|
## 3. Integration Tests: Single-Net A* Search (`inire/router/astar`)
|
||||||
|
|
||||||
|
### 3.1. Open Space Scenarios
|
||||||
|
* **Straight Line:** Route from `(0,0,0)` to `(100,0,0)`. Verify it uses only `Straight` moves.
|
||||||
|
* **Simple Turn:** Route from `(0,0,0)` to `(20,20,90)`. Verify it uses a `Bend90` and `Straight` segments.
|
||||||
|
* **Small S-Bend:** Route with an offset of $5\mu m$ and radius $10\mu m$. Verify it uses the `SBend` component.
|
||||||
|
* **Large S-Bend ($O \ge 2R$):** Route with an offset of $50\mu m$ and radius $10\mu m$. Verify it combines two `Bend90`s and a `Straight` segment.
|
||||||
|
|
||||||
|
### 3.2. Obstacle Avoidance (The "Maze" Tests)
|
||||||
|
* **L-Obstacle:** Place an obstacle blocking the direct path. Verify the router goes around it.
|
||||||
|
* **Narrow Channel:** Create two obstacles with a gap slightly wider than $W_i + C$. Verify the router passes through.
|
||||||
|
* **Dead End:** Create a U-shaped obstacle. Verify the search explores alternatives and fails gracefully if no path exists.
|
||||||
|
|
||||||
|
### 3.3. Snapping & Precision
|
||||||
|
* **Snap-to-Target Lookahead:**
|
||||||
|
* Route to a target at `(100.005, 0, 0)` (not on 1µm grid).
|
||||||
|
* Verify the search reaches the vicinity via the 1µm grid and the final segment bridges the **5nm** gap exactly.
|
||||||
|
* **Grid Alignment:**
|
||||||
|
* Start from a port at `(0.5, 0.5, 0)`. Verify it snaps to the 1µm search grid correctly for the first move expansion.
|
||||||
|
|
||||||
|
### 3.4. Failure Modes
|
||||||
|
* **Unreachable Target:** Create a target completely enclosed by obstacles. Verify the search terminates after exploring all options (or hitting the 50,000 node limit) and returns an invalid result.
|
||||||
|
* **Start/End Collision:** Place a port deep inside an obstacle (beyond the 2nm safety zone). Verify the router identifies the immediate collision and fails gracefully.
|
||||||
|
|
||||||
|
## 4. Integration Tests: Multi-Net PathFinder (`inire/router/pathfinder`)
|
||||||
|
|
||||||
|
### 4.1. Congestion Scenarios
|
||||||
|
* **Parallel Paths:** Route two nets that can both take straight paths. Verify no reroutes occur.
|
||||||
|
* **The "Cross" Test:** Two nets must cross paths in 2D.
|
||||||
|
* Since crossings are illegal, verify the second net finds a detour.
|
||||||
|
* Verify the `Negotiated Congestion` loop increases the cost of the shared region.
|
||||||
|
* **Bottleneck:** Force 3 nets through a channel that only fits 2.
|
||||||
|
* Verify the router returns 2 valid paths and 1 "least bad" path (with collisions flagged).
|
||||||
|
* Verify the `is_valid=False` attribute is set for the failing net.
|
||||||
|
|
||||||
|
### 4.2. Determinism & Performance
|
||||||
|
* **Seed Consistency:** Run the same multi-net problem twice with the same seed; verify identical results (pixel-perfect).
|
||||||
|
* **Node Limit Enforcement:** Trigger a complex search that exceeds **50,000 nodes**. Verify it terminates and returns the best-so-far or failure.
|
||||||
|
* **Timeout:** Verify the session-level timeout stops the process for extremely large problems.
|
||||||
|
|
||||||
|
## 5. Benchmarking & Regression
|
||||||
|
* **Standard Benchmark Suite:** A set of 5-10 layouts with varying net counts (1 to 50).
|
||||||
|
* **Metrics to Track:**
|
||||||
|
* Total wire length.
|
||||||
|
* Total number of bends.
|
||||||
|
* Execution time per net.
|
||||||
|
* Success rate (percentage of nets routed without collisions).
|
||||||
|
* **Node Expansion Rate:** Nodes per second.
|
||||||
|
* **Memory Usage:** Peak RSS during 20x20mm routing.
|
||||||
|
* **Fuzz Testing:**
|
||||||
|
* Generate random obstacles and ports within a 1mm x 1mm area.
|
||||||
|
* Verify that the router never crashes.
|
||||||
|
* Verify that every result marked `is_valid=True` is confirmed collision-free by a high-precision (slow) check.
|
||||||
|
|
||||||
|
## 6. Analytic Correctness Guarantees
|
||||||
|
* **Post-Route Validation:**
|
||||||
|
* Implement an independent `validate_path(path, obstacles, clearance)` function using `shapely`'s most precise intersection tests.
|
||||||
|
* Run this on every test result to ensure the `CollisionEngine` (which uses R-Tree for speed) hasn't missed any edge cases.
|
||||||
|
* **Orientation Check:**
|
||||||
|
* Verify that the final port of every path matches the target orientation exactly $\{0, 90, 180, 270\}$.
|
||||||
57
examples/01_simple_route.py
Normal file
57
examples/01_simple_route.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 01: Simple Route...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
# We define a 100um x 100um routing area
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
|
||||||
|
# Clearance of 2.0um between waveguides
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Precompute DangerMap for heuristic speedup
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([]) # No obstacles yet
|
||||||
|
|
||||||
|
# 2. Configure Router
|
||||||
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
|
context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 3. Define Netlist
|
||||||
|
# Start at (10, 50) pointing East (0 deg)
|
||||||
|
# Target at (90, 50) pointing East (0 deg)
|
||||||
|
netlist = {
|
||||||
|
"net1": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
|
}
|
||||||
|
net_widths = {"net1": 2.0}
|
||||||
|
|
||||||
|
# 4. Route
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 5. Check Results
|
||||||
|
res = results["net1"]
|
||||||
|
if res.is_valid:
|
||||||
|
print("Success! Route found.")
|
||||||
|
print(f"Path collisions: {res.collisions}")
|
||||||
|
else:
|
||||||
|
print("Failed to find route.")
|
||||||
|
|
||||||
|
# 6. Visualize
|
||||||
|
# plot_routing_results takes a dict of RoutingResult objects
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds)
|
||||||
|
fig.savefig("examples/01_simple_route.png")
|
||||||
|
print("Saved plot to examples/01_simple_route.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
52
examples/02_congestion_resolution.py
Normal file
52
examples/02_congestion_resolution.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
# Configure a router with high congestion penalties
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], sbend_radii=[10.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics, base_congestion_penalty=1000.0)
|
||||||
|
|
||||||
|
# 2. Define Netlist
|
||||||
|
# Three nets that must cross each other in a small area
|
||||||
|
netlist = {
|
||||||
|
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
|
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
||||||
|
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
||||||
|
}
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
# PathFinder uses Negotiated Congestion to resolve overlaps iteratively
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
all_valid = all(res.is_valid for res in results.values())
|
||||||
|
if all_valid:
|
||||||
|
print("Success! Congestion resolved for all nets.")
|
||||||
|
else:
|
||||||
|
print("Failed to resolve congestion for some nets.")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/02_congestion_resolution.png")
|
||||||
|
print("Saved plot to examples/02_congestion_resolution.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/03_locked_paths.png
Normal file
BIN
examples/03_locked_paths.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
47
examples/03_locked_paths.py
Normal file
47
examples/03_locked_paths.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 03: Locked Paths...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, -50, 100, 50)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
|
context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 2. Route Net A and 'Lock' it
|
||||||
|
# Net A is a straight path blocking the direct route for Net B
|
||||||
|
print("Routing initial net...")
|
||||||
|
netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))}
|
||||||
|
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
||||||
|
|
||||||
|
# Locking prevents Net A from being removed or rerouted during NC iterations
|
||||||
|
engine.lock_net("netA")
|
||||||
|
print("Initial net locked as static obstacle.")
|
||||||
|
|
||||||
|
# 3. Route Net B (forced to detour)
|
||||||
|
print("Routing detour net around locked path...")
|
||||||
|
netlist_b = {"netB": (Port(50, -20, 90), Port(50, 20, 90))}
|
||||||
|
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
||||||
|
|
||||||
|
# 4. Visualize
|
||||||
|
results = {**results_a, **results_b}
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds)
|
||||||
|
fig.savefig("examples/03_locked_paths.png")
|
||||||
|
print("Saved plot to examples/03_locked_paths.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
64
examples/04_sbends_and_radii.py
Normal file
64
examples/04_sbends_and_radii.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 04: S-Bends and Multiple Radii...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
# 2. Configure Router
|
||||||
|
evaluator = CostEvaluator(
|
||||||
|
engine,
|
||||||
|
danger_map,
|
||||||
|
unit_length_cost=1.0,
|
||||||
|
bend_penalty=10.0,
|
||||||
|
sbend_penalty=20.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
context = AStarContext(
|
||||||
|
evaluator,
|
||||||
|
node_limit=50000,
|
||||||
|
snap_size=1.0,
|
||||||
|
bend_radii=[10.0, 30.0],
|
||||||
|
sbend_offsets=[5.0], # Use a simpler offset
|
||||||
|
bend_penalty=10.0,
|
||||||
|
sbend_penalty=20.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 3. Define Netlist
|
||||||
|
# start (10, 50), target (60, 55) -> 5um offset
|
||||||
|
netlist = {
|
||||||
|
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
|
||||||
|
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
|
||||||
|
}
|
||||||
|
net_widths = {"sbend_only": 2.0, "multi_radii": 2.0}
|
||||||
|
|
||||||
|
# 4. Route
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 5. Check Results
|
||||||
|
for nid, res in results.items():
|
||||||
|
status = "Success" if res.is_valid else "Failed"
|
||||||
|
print(f"{nid}: {status}, collisions={res.collisions}")
|
||||||
|
|
||||||
|
# 6. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/04_sbends_and_radii.png")
|
||||||
|
print("Saved plot to examples/04_sbends_and_radii.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/05_orientation_stress.png
Normal file
BIN
examples/05_orientation_stress.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
49
examples/05_orientation_stress.py
Normal file
49
examples/05_orientation_stress.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 05: Orientation Stress Test...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 200, 200)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0)
|
||||||
|
context = AStarContext(evaluator, snap_size=5.0, bend_radii=[20.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 2. Define Netlist
|
||||||
|
# Challenging orientation combinations
|
||||||
|
netlist = {
|
||||||
|
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
||||||
|
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
||||||
|
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
||||||
|
}
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
print("Routing complex orientation nets...")
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
for nid, res in results.items():
|
||||||
|
status = "Success" if res.is_valid else "Failed"
|
||||||
|
print(f" {nid}: {status}")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/05_orientation_stress.png")
|
||||||
|
print("Saved plot to examples/05_orientation_stress.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/06_bend_collision_models.png
Normal file
BIN
examples/06_bend_collision_models.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
68
examples/06_bend_collision_models.py
Normal file
68
examples/06_bend_collision_models.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 06: Bend Collision Models...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
# Give room for 10um bends near the edges
|
||||||
|
bounds = (-20, -20, 170, 170)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
|
||||||
|
# Create three scenarios with identical obstacles
|
||||||
|
# We'll space them out vertically
|
||||||
|
obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)])
|
||||||
|
obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)])
|
||||||
|
obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)])
|
||||||
|
|
||||||
|
obstacles = [obs_arc, obs_bbox, obs_clipped]
|
||||||
|
for obs in obstacles:
|
||||||
|
engine.add_static_obstacle(obs)
|
||||||
|
danger_map.precompute(obstacles)
|
||||||
|
|
||||||
|
# We'll run three separate routers since collision_type is a router-level config
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
|
||||||
|
# Scenario 1: Standard 'arc' model (High fidelity)
|
||||||
|
context_arc = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], bend_collision_type="arc")
|
||||||
|
netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}
|
||||||
|
|
||||||
|
# Scenario 2: 'bbox' model (Conservative axis-aligned box)
|
||||||
|
context_bbox = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], bend_collision_type="bbox")
|
||||||
|
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
|
||||||
|
|
||||||
|
# Scenario 3: 'clipped_bbox' model (Balanced)
|
||||||
|
context_clipped = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], bend_collision_type="clipped_bbox", bend_clip_margin=1.0)
|
||||||
|
netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}
|
||||||
|
|
||||||
|
# 2. Route each scenario
|
||||||
|
print("Routing Scenario 1 (Arc)...")
|
||||||
|
res_arc = PathFinder(context_arc, use_tiered_strategy=False).route_all(netlist_arc, {"arc_model": 2.0})
|
||||||
|
|
||||||
|
print("Routing Scenario 2 (BBox)...")
|
||||||
|
res_bbox = PathFinder(context_bbox, use_tiered_strategy=False).route_all(netlist_bbox, {"bbox_model": 2.0})
|
||||||
|
|
||||||
|
print("Routing Scenario 3 (Clipped BBox)...")
|
||||||
|
res_clipped = PathFinder(context_clipped, use_tiered_strategy=False).route_all(netlist_clipped, {"clipped_model": 2.0})
|
||||||
|
|
||||||
|
# 3. Combine results for visualization
|
||||||
|
all_results = {**res_arc, **res_bbox, **res_clipped}
|
||||||
|
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
|
||||||
|
|
||||||
|
# 4. Visualize
|
||||||
|
fig, ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists)
|
||||||
|
fig.savefig("examples/06_bend_collision_models.png")
|
||||||
|
print("Saved plot to examples/06_bend_collision_models.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
186
examples/07_large_scale_routing.py
Normal file
186
examples/07_large_scale_routing.py
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results, plot_danger_map, plot_expanded_nodes, plot_expansion_density
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 07: Fan-Out (10 Nets, 50um Radius, 5um Grid)...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 1000, 1000)
|
||||||
|
engine = CollisionEngine(clearance=6.0)
|
||||||
|
|
||||||
|
# Bottleneck at x=500, 200um gap
|
||||||
|
obstacles = [
|
||||||
|
box(450, 0, 550, 400),
|
||||||
|
box(450, 600, 550, 1000),
|
||||||
|
]
|
||||||
|
for obs in obstacles:
|
||||||
|
engine.add_static_obstacle(obs)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute(obstacles)
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.1, bend_penalty=100.0, sbend_penalty=400.0, congestion_penalty=100.0)
|
||||||
|
|
||||||
|
context = AStarContext(evaluator, node_limit=2000000, snap_size=5.0, bend_radii=[50.0], sbend_radii=[50.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics, max_iterations=15, base_congestion_penalty=100.0, congestion_multiplier=1.4)
|
||||||
|
|
||||||
|
# 2. Define Netlist
|
||||||
|
netlist = {}
|
||||||
|
num_nets = 10
|
||||||
|
start_x = 50
|
||||||
|
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||||
|
|
||||||
|
end_x = 950
|
||||||
|
end_y_base = 100
|
||||||
|
end_y_pitch = 800.0 / (num_nets - 1)
|
||||||
|
|
||||||
|
for i in range(num_nets):
|
||||||
|
sy = round((start_y_base + i * 10.0) / 5.0) * 5.0
|
||||||
|
ey = round((end_y_base + i * end_y_pitch) / 5.0) * 5.0
|
||||||
|
netlist[f"net_{i:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
||||||
|
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
print(f"Routing {len(netlist)} nets through 200um bottleneck...")
|
||||||
|
|
||||||
|
iteration_stats = []
|
||||||
|
|
||||||
|
def iteration_callback(idx, current_results):
|
||||||
|
successes = sum(1 for r in current_results.values() if r.is_valid)
|
||||||
|
total_collisions = sum(r.collisions for r in current_results.values())
|
||||||
|
total_nodes = metrics.nodes_expanded
|
||||||
|
|
||||||
|
# Identify Hotspots
|
||||||
|
hotspots = {}
|
||||||
|
overlap_matrix = {} # (net_a, net_b) -> count
|
||||||
|
|
||||||
|
for nid, res in current_results.items():
|
||||||
|
if not res.path:
|
||||||
|
continue
|
||||||
|
for comp in res.path:
|
||||||
|
for poly in comp.geometry:
|
||||||
|
# Check what it overlaps with
|
||||||
|
overlaps = engine.dynamic_index.intersection(poly.bounds)
|
||||||
|
for other_obj_id in overlaps:
|
||||||
|
if other_obj_id in engine.dynamic_geometries:
|
||||||
|
other_nid, other_poly = engine.dynamic_geometries[other_obj_id]
|
||||||
|
if other_nid != nid:
|
||||||
|
if poly.intersects(other_poly):
|
||||||
|
# Record hotspot
|
||||||
|
cx, cy = poly.centroid.x, poly.centroid.y
|
||||||
|
grid_key = (int(cx/20)*20, int(cy/20)*20)
|
||||||
|
hotspots[grid_key] = hotspots.get(grid_key, 0) + 1
|
||||||
|
|
||||||
|
# Record pair
|
||||||
|
pair = tuple(sorted((nid, other_nid)))
|
||||||
|
overlap_matrix[pair] = overlap_matrix.get(pair, 0) + 1
|
||||||
|
|
||||||
|
print(f" Iteration {idx} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}")
|
||||||
|
if overlap_matrix:
|
||||||
|
top_pairs = sorted(overlap_matrix.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||||
|
print(f" Top Conflicts: {top_pairs}")
|
||||||
|
if hotspots:
|
||||||
|
top_hotspots = sorted(hotspots.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||||
|
print(f" Top Hotspots: {top_hotspots}")
|
||||||
|
|
||||||
|
# Adaptive Greediness: Decay from 1.5 to 1.1 over 10 iterations
|
||||||
|
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
||||||
|
evaluator.greedy_h_weight = new_greedy
|
||||||
|
print(f" Adaptive Greedy Weight for Next Iteration: {new_greedy:.3f}")
|
||||||
|
|
||||||
|
iteration_stats.append({
|
||||||
|
'Iteration': idx,
|
||||||
|
'Success': successes,
|
||||||
|
'Congestion': total_collisions,
|
||||||
|
'Nodes': total_nodes
|
||||||
|
})
|
||||||
|
|
||||||
|
# Save plots only for certain iterations to save time
|
||||||
|
# if idx % 20 == 0 or idx == pf.max_iterations - 1:
|
||||||
|
if True:
|
||||||
|
# Save a plot of this iteration's result
|
||||||
|
fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist)
|
||||||
|
plot_danger_map(danger_map, ax=ax)
|
||||||
|
|
||||||
|
# Overlay failures: show where they stopped
|
||||||
|
for nid, res in current_results.items():
|
||||||
|
if not res.is_valid and res.path:
|
||||||
|
last_p = res.path[-1].end_port
|
||||||
|
target_p = netlist[nid][1]
|
||||||
|
dist = abs(last_p.x - target_p.x) + abs(last_p.y - target_p.y)
|
||||||
|
ax.scatter(last_p.x, last_p.y, color='red', marker='x', s=100)
|
||||||
|
ax.text(last_p.x, last_p.y, f" {nid} (rem: {dist:.0f}um)", color='red', fontsize=8)
|
||||||
|
|
||||||
|
fig.savefig(f"examples/07_iteration_{idx:02d}.png")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
# Plot Expansion Density if data is available
|
||||||
|
if pf.accumulated_expanded_nodes:
|
||||||
|
fig_d, ax_d = plot_expansion_density(pf.accumulated_expanded_nodes, bounds)
|
||||||
|
fig_d.savefig(f"examples/07_iteration_{idx:02d}_density.png")
|
||||||
|
plt.close(fig_d)
|
||||||
|
|
||||||
|
metrics.reset_per_route()
|
||||||
|
|
||||||
|
import cProfile, pstats
|
||||||
|
profiler = cProfile.Profile()
|
||||||
|
profiler.enable()
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
results = pf.route_all(netlist, net_widths, store_expanded=True, iteration_callback=iteration_callback, shuffle_nets=True, seed=42)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
profiler.disable()
|
||||||
|
|
||||||
|
# Final stats
|
||||||
|
stats = pstats.Stats(profiler).sort_stats('tottime')
|
||||||
|
stats.print_stats(20)
|
||||||
|
print(f"Routing took {t1-t0:.4f}s")
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
print("\n--- Iteration Summary ---")
|
||||||
|
print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}")
|
||||||
|
print("-" * 40)
|
||||||
|
for s in iteration_stats:
|
||||||
|
print(f"{s['Iteration']:<5} | {s['Success']:<8} | {s['Congestion']:<8} | {s['Nodes']:<10}")
|
||||||
|
|
||||||
|
success_count = sum(1 for res in results.values() if res.is_valid)
|
||||||
|
print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.")
|
||||||
|
|
||||||
|
for nid, res in results.items():
|
||||||
|
target_p = netlist[nid][1]
|
||||||
|
if not res.is_valid:
|
||||||
|
last_p = res.path[-1].end_port if res.path else netlist[nid][0]
|
||||||
|
dist = abs(last_p.x - target_p.x) + abs(last_p.y - target_p.y)
|
||||||
|
print(f" FAILED: {nid} (Stopped {dist:.1f}um from target)")
|
||||||
|
else:
|
||||||
|
types = [move.move_type for move in res.path]
|
||||||
|
from collections import Counter
|
||||||
|
counts = Counter(types)
|
||||||
|
print(f" {nid}: {len(res.path)} segments, {dict(counts)}")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist)
|
||||||
|
|
||||||
|
# Overlay Danger Map
|
||||||
|
plot_danger_map(danger_map, ax=ax)
|
||||||
|
|
||||||
|
# Overlay Expanded Nodes from last routed net (as an example)
|
||||||
|
if metrics.last_expanded_nodes:
|
||||||
|
print(f"Plotting {len(metrics.last_expanded_nodes)} expanded nodes for the last net...")
|
||||||
|
plot_expanded_nodes(metrics.last_expanded_nodes, ax=ax, color='blue', alpha=0.1)
|
||||||
|
|
||||||
|
fig.savefig("examples/07_large_scale_routing.png")
|
||||||
|
print("Saved plot to examples/07_large_scale_routing.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/08_custom_bend_geometry.png
Normal file
BIN
examples/08_custom_bend_geometry.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
57
examples/08_custom_bend_geometry.py
Normal file
57
examples/08_custom_bend_geometry.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics, route_astar
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 08: Custom Bend Geometry...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 150, 150)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], sbend_radii=[])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 2. Define Netlist
|
||||||
|
netlist = {
|
||||||
|
"custom_bend": (Port(20, 20, 0), Port(100, 100, 90)),
|
||||||
|
}
|
||||||
|
net_widths = {"custom_bend": 2.0}
|
||||||
|
|
||||||
|
# 3. Route with standard arc first
|
||||||
|
print("Routing with standard arc...")
|
||||||
|
results_std = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Define a custom 'trapezoid' bend model
|
||||||
|
# (Just for demonstration - we override the collision model during search)
|
||||||
|
# Define a custom centered 20x20 box
|
||||||
|
custom_poly = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)])
|
||||||
|
|
||||||
|
print("Routing with custom collision model...")
|
||||||
|
# Override bend_collision_type with a literal Polygon
|
||||||
|
context_custom = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], bend_collision_type=custom_poly, sbend_radii=[])
|
||||||
|
metrics_custom = AStarMetrics()
|
||||||
|
results_custom = PathFinder(context_custom, metrics_custom, use_tiered_strategy=False).route_all(
|
||||||
|
{"custom_model": netlist["custom_bend"]}, {"custom_model": 2.0}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
all_results = {**results_std, **results_custom}
|
||||||
|
fig, ax = plot_routing_results(all_results, [], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/08_custom_bend_geometry.png")
|
||||||
|
print("Saved plot to examples/08_custom_bend_geometry.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
60
examples/09_unroutable_best_effort.py
Normal file
60
examples/09_unroutable_best_effort.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, AStarMetrics
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 09: Best-Effort (Unroutable Net)...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Create a 'cage' that completely blocks the target
|
||||||
|
cage = [
|
||||||
|
box(70, 30, 75, 70), # Left wall
|
||||||
|
box(70, 70, 95, 75), # Top wall
|
||||||
|
box(70, 25, 95, 30), # Bottom wall
|
||||||
|
]
|
||||||
|
for obs in cage:
|
||||||
|
engine.add_static_obstacle(obs)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute(cage)
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
# Use a low node limit to fail faster
|
||||||
|
context = AStarContext(evaluator, node_limit=2000, snap_size=1.0, bend_radii=[10.0])
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
|
||||||
|
# Enable partial path return (handled internally by PathFinder calling route_astar with return_partial=True)
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
# 2. Define Netlist: start outside, target inside the cage
|
||||||
|
netlist = {
|
||||||
|
"trapped_net": (Port(10, 50, 0), Port(85, 50, 0)),
|
||||||
|
}
|
||||||
|
net_widths = {"trapped_net": 2.0}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
print("Routing net into a cage (should fail and return partial)...")
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
res = results["trapped_net"]
|
||||||
|
if not res.is_valid:
|
||||||
|
print(f"Net failed to route as expected. Partial path length: {len(res.path)} segments.")
|
||||||
|
else:
|
||||||
|
print("Wait, it found a way in? Check the cage geometry!")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, cage, bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/09_unroutable_best_effort.png")
|
||||||
|
print("Saved plot to examples/09_unroutable_best_effort.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
38
examples/README.md
Normal file
38
examples/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Inire Routing Examples
|
||||||
|
|
||||||
|
This directory contains examples demonstrating the features and architectural capabilities of the `inire` router.
|
||||||
|
|
||||||
|
## Architectural Visualization
|
||||||
|
In all plots generated by `inire`, we distinguish between the search-time geometry and the final "actual" geometry:
|
||||||
|
* **Dashed Lines & Translucent Fill**: The **Collision Proxy** used during the A* search (e.g., `clipped_bbox` or `bbox`). This represents the conservative envelope the router used to guarantee clearance.
|
||||||
|
* **Solid Lines**: The **Actual Geometry** (high-fidelity arcs). This is the exact shape that will be used for PDK generation and fabrication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Fan-Out (Negotiated Congestion)
|
||||||
|
Demonstrates the Negotiated Congestion algorithm handling multiple intersecting nets. The router iteratively increases penalties for overlaps until a collision-free solution is found. This example shows a bundle of nets fanning out through a narrow bottleneck.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2. Custom Bend Geometry Models
|
||||||
|
`inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy:
|
||||||
|
* **Arc**: High-fidelity geometry (Highest accuracy).
|
||||||
|
* **BBox**: Simple axis-aligned bounding box (Fastest search).
|
||||||
|
* **Clipped BBox**: A balanced model that clips the corners of the AABB to better fit the arc (Optimal performance).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 3. Unroutable Nets & Best-Effort Display
|
||||||
|
When a net is physically blocked or exceeds the node limit, the router returns the "best-effort" partial path—the path that reached the point closest to the target according to the heuristic. This is critical for debugging design constraints.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 4. Orientation Stress Test
|
||||||
|
Demonstrates the router's ability to handle complex orientation requirements, including U-turns, 90-degree flips, and loops.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 5. Tiered Fidelity & Lazy Dilation
|
||||||
|
Our architecture leverages two key optimizations for high-performance routing:
|
||||||
|
1. **Tiered Fidelity**: Initial routing passes use fast `clipped_bbox` proxies. If collisions are found, the system automatically escalates to high-fidelity `arc` geometry for the affected regions.
|
||||||
|
2. **Lazy Dilation**: Geometric buffering (dilation) is deferred until a collision check is strictly necessary, avoiding thousands of redundant `buffer()` and `translate()` calls.
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""
|
"""
|
||||||
inire Wave-router
|
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'
|
__author__ = 'Jan Petykiewicz'
|
||||||
__version__ = '0.1'
|
__version__ = '0.1'
|
||||||
|
|
|
||||||
12
inire/constants.py
Normal file
12
inire/constants.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
Centralized constants for the inire routing engine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Search Grid Snap (5.0 µm default)
|
||||||
|
# TODO: Make this configurable in RouterConfig and define tolerances relative to the grid.
|
||||||
|
DEFAULT_SEARCH_GRID_SNAP_UM = 5.0
|
||||||
|
|
||||||
|
# Tolerances
|
||||||
|
TOLERANCE_LINEAR = 1e-6
|
||||||
|
TOLERANCE_ANGULAR = 1e-3
|
||||||
|
TOLERANCE_GRID = 1e-6
|
||||||
471
inire/geometry/collision.py
Normal file
471
inire/geometry/collision.py
Normal file
|
|
@ -0,0 +1,471 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
import rtree
|
||||||
|
import numpy
|
||||||
|
import shapely
|
||||||
|
from shapely.prepared import prep
|
||||||
|
from shapely.strtree import STRtree
|
||||||
|
from shapely.geometry import box, LineString
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
from shapely.prepared import PreparedGeometry
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
|
||||||
|
|
||||||
|
class CollisionEngine:
|
||||||
|
"""
|
||||||
|
Manages spatial queries for collision detection with unified dilation logic.
|
||||||
|
"""
|
||||||
|
__slots__ = (
|
||||||
|
'clearance', 'max_net_width', 'safety_zone_radius',
|
||||||
|
'static_index', 'static_geometries', 'static_dilated', 'static_prepared',
|
||||||
|
'static_is_rect', 'static_tree', 'static_obj_ids', 'static_safe_cache',
|
||||||
|
'static_grid', 'grid_cell_size', '_static_id_counter',
|
||||||
|
'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', 'dynamic_prepared',
|
||||||
|
'dynamic_tree', 'dynamic_obj_ids', 'dynamic_grid', '_dynamic_id_counter',
|
||||||
|
'metrics', '_dynamic_tree_dirty', '_dynamic_net_ids_array', '_inv_grid_cell_size',
|
||||||
|
'_static_bounds_array', '_static_is_rect_array', '_locked_nets',
|
||||||
|
'_static_raw_tree', '_static_raw_obj_ids', '_dynamic_bounds_array'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
clearance: float,
|
||||||
|
max_net_width: float = 2.0,
|
||||||
|
safety_zone_radius: float = 0.0021,
|
||||||
|
) -> None:
|
||||||
|
self.clearance = clearance
|
||||||
|
self.max_net_width = max_net_width
|
||||||
|
self.safety_zone_radius = safety_zone_radius
|
||||||
|
|
||||||
|
# Static obstacles
|
||||||
|
self.static_index = rtree.index.Index()
|
||||||
|
self.static_geometries: dict[int, Polygon] = {}
|
||||||
|
self.static_dilated: dict[int, Polygon] = {}
|
||||||
|
self.static_prepared: dict[int, PreparedGeometry] = {}
|
||||||
|
self.static_is_rect: dict[int, bool] = {}
|
||||||
|
self.static_tree: STRtree | None = None
|
||||||
|
self.static_obj_ids: list[int] = []
|
||||||
|
self._static_bounds_array: numpy.ndarray | None = None
|
||||||
|
self._static_is_rect_array: numpy.ndarray | None = None
|
||||||
|
self._static_raw_tree: STRtree | None = None
|
||||||
|
self._static_raw_obj_ids: list[int] = []
|
||||||
|
|
||||||
|
self.static_safe_cache: set[tuple] = set()
|
||||||
|
self.static_grid: dict[tuple[int, int], list[int]] = {}
|
||||||
|
self.grid_cell_size = 50.0
|
||||||
|
self._inv_grid_cell_size = 1.0 / self.grid_cell_size
|
||||||
|
self._static_id_counter = 0
|
||||||
|
|
||||||
|
# Dynamic paths
|
||||||
|
self.dynamic_index = rtree.index.Index()
|
||||||
|
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||||
|
self.dynamic_dilated: dict[int, Polygon] = {}
|
||||||
|
self.dynamic_prepared: dict[int, PreparedGeometry] = {}
|
||||||
|
self.dynamic_tree: STRtree | None = None
|
||||||
|
self.dynamic_obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
||||||
|
self.dynamic_grid: dict[tuple[int, int], list[int]] = {}
|
||||||
|
|
||||||
|
self._dynamic_id_counter = 0
|
||||||
|
self._dynamic_tree_dirty = True
|
||||||
|
self._dynamic_net_ids_array = numpy.array([], dtype='<U32')
|
||||||
|
self._dynamic_bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
||||||
|
self._locked_nets: set[str] = set()
|
||||||
|
|
||||||
|
self.metrics = {
|
||||||
|
'static_cache_hits': 0,
|
||||||
|
'static_grid_skips': 0,
|
||||||
|
'static_tree_queries': 0,
|
||||||
|
'static_straight_fast': 0,
|
||||||
|
'congestion_grid_skips': 0,
|
||||||
|
'congestion_tree_queries': 0,
|
||||||
|
'safety_zone_checks': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset_metrics(self) -> None:
|
||||||
|
for k in self.metrics:
|
||||||
|
self.metrics[k] = 0
|
||||||
|
|
||||||
|
def get_metrics_summary(self) -> str:
|
||||||
|
m = self.metrics
|
||||||
|
return (f"Collision Performance: \n"
|
||||||
|
f" Static: {m['static_tree_queries']} checks\n"
|
||||||
|
f" Congestion: {m['congestion_tree_queries']} checks\n"
|
||||||
|
f" Safety Zone: {m['safety_zone_checks']} full intersections performed")
|
||||||
|
|
||||||
|
def add_static_obstacle(self, polygon: Polygon) -> int:
|
||||||
|
obj_id = self._static_id_counter
|
||||||
|
self._static_id_counter += 1
|
||||||
|
|
||||||
|
# Consistent with Wi/2 + C/2 separation:
|
||||||
|
# Buffer static obstacles by half clearance.
|
||||||
|
# Checkers must also buffer waveguide by Wi/2 + C/2.
|
||||||
|
dilated = polygon.buffer(self.clearance / 2.0, join_style=2)
|
||||||
|
|
||||||
|
self.static_geometries[obj_id] = polygon
|
||||||
|
self.static_dilated[obj_id] = dilated
|
||||||
|
self.static_prepared[obj_id] = prep(dilated)
|
||||||
|
self.static_index.insert(obj_id, dilated.bounds)
|
||||||
|
self.static_tree = None
|
||||||
|
self._static_raw_tree = None
|
||||||
|
self.static_grid = {}
|
||||||
|
b = dilated.bounds
|
||||||
|
area = (b[2] - b[0]) * (b[3] - b[1])
|
||||||
|
self.static_is_rect[obj_id] = (abs(dilated.area - area) < 1e-4)
|
||||||
|
return obj_id
|
||||||
|
|
||||||
|
def remove_static_obstacle(self, obj_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Remove a static obstacle by ID.
|
||||||
|
"""
|
||||||
|
if obj_id not in self.static_geometries:
|
||||||
|
return
|
||||||
|
|
||||||
|
bounds = self.static_dilated[obj_id].bounds
|
||||||
|
self.static_index.delete(obj_id, bounds)
|
||||||
|
|
||||||
|
del self.static_geometries[obj_id]
|
||||||
|
del self.static_dilated[obj_id]
|
||||||
|
del self.static_prepared[obj_id]
|
||||||
|
del self.static_is_rect[obj_id]
|
||||||
|
|
||||||
|
self.static_tree = None
|
||||||
|
self._static_raw_tree = None
|
||||||
|
self.static_grid = {}
|
||||||
|
|
||||||
|
def _ensure_static_tree(self) -> None:
|
||||||
|
if self.static_tree is None and self.static_dilated:
|
||||||
|
self.static_obj_ids = sorted(self.static_dilated.keys())
|
||||||
|
geoms = [self.static_dilated[i] for i in self.static_obj_ids]
|
||||||
|
self.static_tree = STRtree(geoms)
|
||||||
|
self._static_bounds_array = numpy.array([g.bounds for g in geoms])
|
||||||
|
self._static_is_rect_array = numpy.array([self.static_is_rect[i] for i in self.static_obj_ids])
|
||||||
|
|
||||||
|
def _ensure_static_raw_tree(self) -> None:
|
||||||
|
if self._static_raw_tree is None and self.static_geometries:
|
||||||
|
self._static_raw_obj_ids = sorted(self.static_geometries.keys())
|
||||||
|
geoms = [self.static_geometries[i] for i in self._static_raw_obj_ids]
|
||||||
|
self._static_raw_tree = STRtree(geoms)
|
||||||
|
|
||||||
|
def _ensure_dynamic_tree(self) -> None:
|
||||||
|
if self.dynamic_tree is None and self.dynamic_dilated:
|
||||||
|
ids = sorted(self.dynamic_dilated.keys())
|
||||||
|
geoms = [self.dynamic_dilated[i] for i in ids]
|
||||||
|
self.dynamic_tree = STRtree(geoms)
|
||||||
|
self.dynamic_obj_ids = numpy.array(ids, dtype=numpy.int32)
|
||||||
|
self._dynamic_bounds_array = numpy.array([g.bounds for g in geoms])
|
||||||
|
nids = [self.dynamic_geometries[obj_id][0] for obj_id in self.dynamic_obj_ids]
|
||||||
|
self._dynamic_net_ids_array = numpy.array(nids, dtype='<U32')
|
||||||
|
self._dynamic_tree_dirty = False
|
||||||
|
|
||||||
|
def _ensure_dynamic_grid(self) -> None:
|
||||||
|
if not self.dynamic_grid and self.dynamic_dilated:
|
||||||
|
cs = self.grid_cell_size
|
||||||
|
for obj_id, poly in self.dynamic_dilated.items():
|
||||||
|
b = poly.bounds
|
||||||
|
for gx in range(int(b[0] / cs), int(b[2] / cs) + 1):
|
||||||
|
for gy in range(int(b[1] / cs), int(b[3] / cs) + 1):
|
||||||
|
cell = (gx, gy)
|
||||||
|
if cell not in self.dynamic_grid: self.dynamic_grid[cell] = []
|
||||||
|
self.dynamic_grid[cell].append(obj_id)
|
||||||
|
|
||||||
|
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
||||||
|
self.dynamic_tree = None
|
||||||
|
self.dynamic_grid = {}
|
||||||
|
self._dynamic_tree_dirty = True
|
||||||
|
dilation = self.clearance / 2.0
|
||||||
|
for i, poly in enumerate(geometry):
|
||||||
|
obj_id = self._dynamic_id_counter
|
||||||
|
self._dynamic_id_counter += 1
|
||||||
|
dilated = dilated_geometry[i] if dilated_geometry else poly.buffer(dilation)
|
||||||
|
self.dynamic_geometries[obj_id] = (net_id, poly)
|
||||||
|
self.dynamic_dilated[obj_id] = dilated
|
||||||
|
self.dynamic_index.insert(obj_id, dilated.bounds)
|
||||||
|
|
||||||
|
def remove_path(self, net_id: str) -> None:
|
||||||
|
if net_id in self._locked_nets: return
|
||||||
|
to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||||
|
if not to_remove: return
|
||||||
|
self.dynamic_tree = None
|
||||||
|
self.dynamic_grid = {}
|
||||||
|
self._dynamic_tree_dirty = True
|
||||||
|
for obj_id in to_remove:
|
||||||
|
self.dynamic_index.delete(obj_id, self.dynamic_dilated[obj_id].bounds)
|
||||||
|
del self.dynamic_geometries[obj_id]
|
||||||
|
del self.dynamic_dilated[obj_id]
|
||||||
|
|
||||||
|
def lock_net(self, net_id: str) -> None:
|
||||||
|
""" Convert a routed net into static obstacles. """
|
||||||
|
self._locked_nets.add(net_id)
|
||||||
|
|
||||||
|
# Move all segments of this net to static obstacles
|
||||||
|
to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||||
|
for obj_id in to_move:
|
||||||
|
poly = self.dynamic_geometries[obj_id][1]
|
||||||
|
self.add_static_obstacle(poly)
|
||||||
|
|
||||||
|
# Remove from dynamic index (without triggering the locked-net guard)
|
||||||
|
self.dynamic_tree = None
|
||||||
|
self.dynamic_grid = {}
|
||||||
|
self._dynamic_tree_dirty = True
|
||||||
|
for obj_id in to_move:
|
||||||
|
self.dynamic_index.delete(obj_id, self.dynamic_dilated[obj_id].bounds)
|
||||||
|
del self.dynamic_geometries[obj_id]
|
||||||
|
del self.dynamic_dilated[obj_id]
|
||||||
|
|
||||||
|
def unlock_net(self, net_id: str) -> None:
|
||||||
|
self._locked_nets.discard(net_id)
|
||||||
|
|
||||||
|
def check_move_straight_static(self, start_port: Port, length: float) -> bool:
|
||||||
|
self.metrics['static_straight_fast'] += 1
|
||||||
|
reach = self.ray_cast(start_port, start_port.orientation, max_dist=length + 0.01)
|
||||||
|
return reach < length - 0.001
|
||||||
|
|
||||||
|
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||||
|
""" Fast port-based check to see if a collision might be in a safety zone. """
|
||||||
|
sz = self.safety_zone_radius
|
||||||
|
b = self._static_bounds_array[idx]
|
||||||
|
if start_port:
|
||||||
|
if (b[0]-sz <= start_port.x <= b[2]+sz and
|
||||||
|
b[1]-sz <= start_port.y <= b[3]+sz): return True
|
||||||
|
if end_port:
|
||||||
|
if (b[0]-sz <= end_port.x <= b[2]+sz and
|
||||||
|
b[1]-sz <= end_port.y <= b[3]+sz): return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_move_static(self, result: ComponentResult, start_port: Port | None = None, end_port: Port | None = None) -> bool:
|
||||||
|
if not self.static_dilated: return False
|
||||||
|
self.metrics['static_tree_queries'] += 1
|
||||||
|
self._ensure_static_tree()
|
||||||
|
|
||||||
|
# 1. Fast total bounds check
|
||||||
|
tb = result.total_bounds
|
||||||
|
hits = self.static_tree.query(box(*tb))
|
||||||
|
if hits.size == 0: return False
|
||||||
|
|
||||||
|
# 2. Per-hit check
|
||||||
|
s_bounds = self._static_bounds_array
|
||||||
|
move_poly_bounds = result.bounds
|
||||||
|
for hit_idx in hits:
|
||||||
|
obs_b = s_bounds[hit_idx]
|
||||||
|
|
||||||
|
# Check if any polygon in the move actually hits THIS obstacle's AABB
|
||||||
|
poly_hits_obs_aabb = False
|
||||||
|
for pb in move_poly_bounds:
|
||||||
|
if (pb[0] < obs_b[2] and pb[2] > obs_b[0] and
|
||||||
|
pb[1] < obs_b[3] and pb[3] > obs_b[1]):
|
||||||
|
poly_hits_obs_aabb = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not poly_hits_obs_aabb: continue
|
||||||
|
|
||||||
|
# Safety zone check (Fast port-based)
|
||||||
|
if self._is_in_safety_zone_fast(hit_idx, start_port, end_port):
|
||||||
|
# If near port, we must use the high-precision check
|
||||||
|
obj_id = self.static_obj_ids[hit_idx]
|
||||||
|
# Triggers lazy evaluation of geometry only if needed
|
||||||
|
poly_move = result.geometry[0] # Simplification: assume 1 poly for now or loop
|
||||||
|
# Actually, better loop over move polygons for high-fidelity
|
||||||
|
collision_found = False
|
||||||
|
for p_move in result.geometry:
|
||||||
|
if not self._is_in_safety_zone(p_move, obj_id, start_port, end_port):
|
||||||
|
collision_found = True; break
|
||||||
|
if not collision_found: continue
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Not in safety zone and AABBs overlap - check real intersection
|
||||||
|
# This is the most common path for real collisions or near misses
|
||||||
|
obj_id = self.static_obj_ids[hit_idx]
|
||||||
|
raw_obstacle = self.static_geometries[obj_id]
|
||||||
|
test_geoms = result.dilated_geometry if result.dilated_geometry else result.geometry
|
||||||
|
|
||||||
|
for i, p_test in enumerate(test_geoms):
|
||||||
|
if p_test.intersects(raw_obstacle):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
||||||
|
if not self.dynamic_geometries: return 0
|
||||||
|
tb = result.total_dilated_bounds
|
||||||
|
if tb is None: return 0
|
||||||
|
self._ensure_dynamic_grid()
|
||||||
|
dynamic_grid = self.dynamic_grid
|
||||||
|
if not dynamic_grid: return 0
|
||||||
|
|
||||||
|
cs_inv = self._inv_grid_cell_size
|
||||||
|
gx_min = int(tb[0] * cs_inv)
|
||||||
|
gy_min = int(tb[1] * cs_inv)
|
||||||
|
gx_max = int(tb[2] * cs_inv)
|
||||||
|
gy_max = int(tb[3] * cs_inv)
|
||||||
|
|
||||||
|
dynamic_geometries = self.dynamic_geometries
|
||||||
|
|
||||||
|
# Fast path for single cell
|
||||||
|
if gx_min == gx_max and gy_min == gy_max:
|
||||||
|
cell = (gx_min, gy_min)
|
||||||
|
if cell in dynamic_grid:
|
||||||
|
for obj_id in dynamic_grid[cell]:
|
||||||
|
if dynamic_geometries[obj_id][0] != net_id:
|
||||||
|
return self._check_real_congestion(result, net_id)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# General case
|
||||||
|
any_possible = False
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
cell = (gx, gy)
|
||||||
|
if cell in dynamic_grid:
|
||||||
|
for obj_id in dynamic_grid[cell]:
|
||||||
|
if dynamic_geometries[obj_id][0] != net_id:
|
||||||
|
any_possible = True
|
||||||
|
break
|
||||||
|
if any_possible: break
|
||||||
|
if any_possible: break
|
||||||
|
|
||||||
|
if not any_possible: return 0
|
||||||
|
return self._check_real_congestion(result, net_id)
|
||||||
|
|
||||||
|
def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int:
|
||||||
|
self.metrics['congestion_tree_queries'] += 1
|
||||||
|
self._ensure_dynamic_tree()
|
||||||
|
if self.dynamic_tree is None: return 0
|
||||||
|
|
||||||
|
# 1. Fast total bounds check (LAZY SAFE)
|
||||||
|
tb = result.total_dilated_bounds
|
||||||
|
d_bounds = self._dynamic_bounds_array
|
||||||
|
possible_total = (tb[0] < d_bounds[:, 2]) & (tb[2] > d_bounds[:, 0]) & \
|
||||||
|
(tb[1] < d_bounds[:, 3]) & (tb[3] > d_bounds[:, 1])
|
||||||
|
|
||||||
|
valid_hits = (self._dynamic_net_ids_array != net_id)
|
||||||
|
if not numpy.any(possible_total & valid_hits):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 2. Per-polygon AABB check using query on geometries (LAZY triggering)
|
||||||
|
geoms_to_test = result.dilated_geometry if result.dilated_geometry else result.geometry
|
||||||
|
res_indices, tree_indices = self.dynamic_tree.query(geoms_to_test, predicate='intersects')
|
||||||
|
|
||||||
|
if tree_indices.size == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
hit_net_ids = numpy.take(self._dynamic_net_ids_array, tree_indices)
|
||||||
|
valid_geoms_hits = (hit_net_ids != net_id)
|
||||||
|
return int(numpy.sum(valid_geoms_hits))
|
||||||
|
|
||||||
|
def _is_in_safety_zone(self, geometry: Polygon, obj_id: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||||
|
"""
|
||||||
|
Only returns True if the collision is ACTUALLY inside a safety zone.
|
||||||
|
"""
|
||||||
|
raw_obstacle = self.static_geometries[obj_id]
|
||||||
|
sz = self.safety_zone_radius
|
||||||
|
|
||||||
|
# Fast path: check if ports are even near the obstacle
|
||||||
|
obs_b = raw_obstacle.bounds
|
||||||
|
near_start = start_port and (obs_b[0]-sz <= start_port.x <= obs_b[2]+sz and
|
||||||
|
obs_b[1]-sz <= start_port.y <= obs_b[3]+sz)
|
||||||
|
near_end = end_port and (obs_b[0]-sz <= end_port.x <= obs_b[2]+sz and
|
||||||
|
obs_b[1]-sz <= end_port.y <= obs_b[3]+sz)
|
||||||
|
|
||||||
|
if not near_start and not near_end:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not geometry.intersects(raw_obstacle):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.metrics['safety_zone_checks'] += 1
|
||||||
|
intersection = geometry.intersection(raw_obstacle)
|
||||||
|
if intersection.is_empty: return False
|
||||||
|
|
||||||
|
ix_bounds = intersection.bounds
|
||||||
|
if start_port and near_start:
|
||||||
|
if (abs(ix_bounds[0] - start_port.x) < sz and abs(ix_bounds[1] - start_port.y) < sz and
|
||||||
|
abs(ix_bounds[2] - start_port.x) < sz and abs(ix_bounds[3] - start_port.y) < sz): return True
|
||||||
|
if end_port and near_end:
|
||||||
|
if (abs(ix_bounds[0] - end_port.x) < sz and abs(ix_bounds[1] - end_port.y) < sz and
|
||||||
|
abs(ix_bounds[2] - end_port.x) < sz and abs(ix_bounds[3] - end_port.y) < sz): return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_collision(self, geometry: Polygon, net_id: str, buffer_mode: Literal['static', 'congestion'] = 'static', start_port: Port | None = None, end_port: Port | None = None, dilated_geometry: Polygon | None = None, bounds: tuple[float, float, float, float] | None = None, net_width: float | None = None) -> bool | int:
|
||||||
|
if buffer_mode == 'static':
|
||||||
|
self._ensure_static_tree()
|
||||||
|
if self.static_tree is None: return False
|
||||||
|
|
||||||
|
# Separation needed: (Wi + C)/2.
|
||||||
|
# static_dilated is buffered by C/2.
|
||||||
|
# So we need geometry buffered by Wi/2.
|
||||||
|
if dilated_geometry:
|
||||||
|
test_geom = dilated_geometry
|
||||||
|
else:
|
||||||
|
dist = (net_width / 2.0) if net_width is not None else 0.0
|
||||||
|
test_geom = geometry.buffer(dist + 1e-7, join_style=2) if dist >= 0 else geometry
|
||||||
|
|
||||||
|
hits = self.static_tree.query(test_geom, predicate='intersects')
|
||||||
|
for hit_idx in hits:
|
||||||
|
obj_id = self.static_obj_ids[hit_idx]
|
||||||
|
if self._is_in_safety_zone(geometry, obj_id, start_port, end_port): continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._ensure_dynamic_tree()
|
||||||
|
if self.dynamic_tree is None: return 0
|
||||||
|
test_poly = dilated_geometry if dilated_geometry else geometry.buffer(self.clearance / 2.0)
|
||||||
|
hits = self.dynamic_tree.query(test_poly, predicate='intersects')
|
||||||
|
count = 0
|
||||||
|
for hit_idx in hits:
|
||||||
|
obj_id = self.dynamic_obj_ids[hit_idx]
|
||||||
|
if self.dynamic_geometries[obj_id][0] != net_id: count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def is_collision(self, geometry: Polygon, net_id: str = 'default', net_width: float | None = None, start_port: Port | None = None, end_port: Port | None = None) -> bool:
|
||||||
|
""" Unified entry point for static collision checks. """
|
||||||
|
result = self.check_collision(geometry, net_id, buffer_mode='static', start_port=start_port, end_port=end_port, net_width=net_width)
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
def ray_cast(self, origin: Port, angle_deg: float, max_dist: float = 2000.0) -> float:
|
||||||
|
rad = numpy.radians(angle_deg)
|
||||||
|
cos_v, sin_v = numpy.cos(rad), numpy.sin(rad)
|
||||||
|
dx, dy = max_dist * cos_v, max_dist * sin_v
|
||||||
|
min_x, max_x = sorted([origin.x, origin.x + dx])
|
||||||
|
min_y, max_y = sorted([origin.y, origin.y + dy])
|
||||||
|
self._ensure_static_tree()
|
||||||
|
if self.static_tree is None: return max_dist
|
||||||
|
candidates = self.static_tree.query(box(min_x, min_y, max_x, max_y))
|
||||||
|
if candidates.size == 0: return max_dist
|
||||||
|
min_dist = max_dist
|
||||||
|
inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30
|
||||||
|
inv_dy = 1.0 / dy if abs(dy) > 1e-12 else 1e30
|
||||||
|
b_arr = self._static_bounds_array[candidates]
|
||||||
|
dist_sq = (b_arr[:, 0] - origin.x)**2 + (b_arr[:, 1] - origin.y)**2
|
||||||
|
sorted_indices = numpy.argsort(dist_sq)
|
||||||
|
ray_line = None
|
||||||
|
for i in sorted_indices:
|
||||||
|
c = candidates[i]; b = self._static_bounds_array[c]
|
||||||
|
if abs(dx) < 1e-12:
|
||||||
|
if origin.x < b[0] or origin.x > b[2]: tx_min, tx_max = 1e30, -1e30
|
||||||
|
else: tx_min, tx_max = -1e30, 1e30
|
||||||
|
else:
|
||||||
|
t1, t2 = (b[0] - origin.x) * inv_dx, (b[2] - origin.x) * inv_dx
|
||||||
|
tx_min, tx_max = min(t1, t2), max(t1, t2)
|
||||||
|
if abs(dy) < 1e-12:
|
||||||
|
if origin.y < b[1] or origin.y > b[3]: ty_min, ty_max = 1e30, -1e30
|
||||||
|
else: ty_min, ty_max = -1e30, 1e30
|
||||||
|
else:
|
||||||
|
t1, t2 = (b[1] - origin.y) * inv_dy, (b[3] - origin.y) * inv_dy
|
||||||
|
ty_min, ty_max = min(t1, t2), max(t1, t2)
|
||||||
|
t_min, t_max = max(tx_min, ty_min), min(tx_max, ty_max)
|
||||||
|
if t_max < 0 or t_min > t_max or t_min > 1.0 or t_min >= min_dist / max_dist: continue
|
||||||
|
if self._static_is_rect_array[c]:
|
||||||
|
min_dist = max(0.0, t_min * max_dist); continue
|
||||||
|
if ray_line is None: ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
|
||||||
|
obj_id = self.static_obj_ids[c]
|
||||||
|
if self.static_prepared[obj_id].intersects(ray_line):
|
||||||
|
intersection = ray_line.intersection(self.static_dilated[obj_id])
|
||||||
|
if intersection.is_empty: continue
|
||||||
|
def get_dist(geom):
|
||||||
|
if hasattr(geom, 'geoms'): return min(get_dist(g) for g in geom.geoms)
|
||||||
|
return numpy.sqrt((geom.coords[0][0] - origin.x)**2 + (geom.coords[0][1] - origin.y)**2)
|
||||||
|
d = get_dist(intersection)
|
||||||
|
if d < min_dist: min_dist = d
|
||||||
|
return min_dist
|
||||||
681
inire/geometry/components.py
Normal file
681
inire/geometry/components.py
Normal file
|
|
@ -0,0 +1,681 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Literal, cast, Any
|
||||||
|
import numpy
|
||||||
|
import shapely
|
||||||
|
from shapely.geometry import Polygon, box, MultiPolygon
|
||||||
|
from shapely.ops import unary_union
|
||||||
|
from shapely.affinity import translate
|
||||||
|
|
||||||
|
from inire.constants import DEFAULT_SEARCH_GRID_SNAP_UM, TOLERANCE_LINEAR, TOLERANCE_ANGULAR
|
||||||
|
from .primitives import Port
|
||||||
|
|
||||||
|
|
||||||
|
def snap_search_grid(value: float, snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM) -> float:
|
||||||
|
"""
|
||||||
|
Snap a coordinate to the nearest search grid unit.
|
||||||
|
"""
|
||||||
|
return round(value / snap_size) * snap_size
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentResult:
|
||||||
|
"""
|
||||||
|
Standard container for generated move geometry and state.
|
||||||
|
Supports Lazy Evaluation for translation to improve performance.
|
||||||
|
"""
|
||||||
|
__slots__ = (
|
||||||
|
'_geometry', '_dilated_geometry', '_proxy_geometry', '_actual_geometry', '_dilated_actual_geometry',
|
||||||
|
'end_port', 'length', 'move_type', '_bounds', '_dilated_bounds',
|
||||||
|
'_total_bounds', '_total_dilated_bounds', '_bounds_cached', '_total_geom_list', '_offsets', '_coords_cache',
|
||||||
|
'_base_result', '_offset', 'rel_gx', 'rel_gy', 'rel_go'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
geometry: list[Polygon] | None = None,
|
||||||
|
end_port: Port | None = None,
|
||||||
|
length: float = 0.0,
|
||||||
|
dilated_geometry: list[Polygon] | None = None,
|
||||||
|
proxy_geometry: list[Polygon] | None = None,
|
||||||
|
actual_geometry: list[Polygon] | None = None,
|
||||||
|
dilated_actual_geometry: list[Polygon] | None = None,
|
||||||
|
skip_bounds: bool = False,
|
||||||
|
move_type: str = 'Unknown',
|
||||||
|
_total_geom_list: list[Polygon] | None = None,
|
||||||
|
_offsets: list[int] | None = None,
|
||||||
|
_coords_cache: numpy.ndarray | None = None,
|
||||||
|
_base_result: ComponentResult | None = None,
|
||||||
|
_offset: tuple[float, float] | None = None,
|
||||||
|
snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM,
|
||||||
|
rel_gx: int | None = None,
|
||||||
|
rel_gy: int | None = None,
|
||||||
|
rel_go: int | None = None
|
||||||
|
) -> None:
|
||||||
|
self.end_port = end_port
|
||||||
|
self.length = length
|
||||||
|
self.move_type = move_type
|
||||||
|
|
||||||
|
self._base_result = _base_result
|
||||||
|
self._offset = _offset
|
||||||
|
self._bounds_cached = False
|
||||||
|
|
||||||
|
if rel_gx is not None:
|
||||||
|
self.rel_gx = rel_gx
|
||||||
|
self.rel_gy = rel_gy
|
||||||
|
self.rel_go = rel_go
|
||||||
|
elif end_port:
|
||||||
|
inv_snap = 1.0 / snap_size
|
||||||
|
self.rel_gx = int(round(end_port.x * inv_snap))
|
||||||
|
self.rel_gy = int(round(end_port.y * inv_snap))
|
||||||
|
self.rel_go = int(round(end_port.orientation))
|
||||||
|
else:
|
||||||
|
self.rel_gx = 0; self.rel_gy = 0; self.rel_go = 0
|
||||||
|
|
||||||
|
if _base_result is not None:
|
||||||
|
# Lazy Mode
|
||||||
|
self._geometry = None
|
||||||
|
self._dilated_geometry = None
|
||||||
|
self._proxy_geometry = None
|
||||||
|
self._actual_geometry = None
|
||||||
|
self._dilated_actual_geometry = None
|
||||||
|
self._bounds = None
|
||||||
|
self._dilated_bounds = None
|
||||||
|
self._total_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
else:
|
||||||
|
# Eager Mode (Base Component)
|
||||||
|
self._geometry = geometry
|
||||||
|
self._dilated_geometry = dilated_geometry
|
||||||
|
self._proxy_geometry = proxy_geometry
|
||||||
|
self._actual_geometry = actual_geometry
|
||||||
|
self._dilated_actual_geometry = dilated_actual_geometry
|
||||||
|
|
||||||
|
# These are mostly legacy/unused but kept for slot safety
|
||||||
|
self._total_geom_list = _total_geom_list
|
||||||
|
self._offsets = _offsets
|
||||||
|
self._coords_cache = _coords_cache
|
||||||
|
|
||||||
|
if not skip_bounds and geometry:
|
||||||
|
# Use plain tuples for bounds to avoid NumPy overhead
|
||||||
|
self._bounds = [p.bounds for p in geometry]
|
||||||
|
b0 = self._bounds[0]
|
||||||
|
minx, miny, maxx, maxy = b0
|
||||||
|
for i in range(1, len(self._bounds)):
|
||||||
|
b = self._bounds[i]
|
||||||
|
if b[0] < minx: minx = b[0]
|
||||||
|
if b[1] < miny: miny = b[1]
|
||||||
|
if b[2] > maxx: maxx = b[2]
|
||||||
|
if b[3] > maxy: maxy = b[3]
|
||||||
|
self._total_bounds = (minx, miny, maxx, maxy)
|
||||||
|
|
||||||
|
if dilated_geometry is not None:
|
||||||
|
self._dilated_bounds = [p.bounds for p in dilated_geometry]
|
||||||
|
b0 = self._dilated_bounds[0]
|
||||||
|
minx, miny, maxx, maxy = b0
|
||||||
|
for i in range(1, len(self._dilated_bounds)):
|
||||||
|
b = self._dilated_bounds[i]
|
||||||
|
if b[0] < minx: minx = b[0]
|
||||||
|
if b[1] < miny: miny = b[1]
|
||||||
|
if b[2] > maxx: maxx = b[2]
|
||||||
|
if b[3] > maxy: maxy = b[3]
|
||||||
|
self._total_dilated_bounds = (minx, miny, maxx, maxy)
|
||||||
|
else:
|
||||||
|
self._dilated_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
else:
|
||||||
|
self._bounds = None
|
||||||
|
self._total_bounds = None
|
||||||
|
self._dilated_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
self._bounds_cached = True
|
||||||
|
|
||||||
|
def _ensure_evaluated(self, attr_name: str) -> None:
|
||||||
|
if self._base_result is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if specific attribute is already translated
|
||||||
|
internal_name = f'_{attr_name}'
|
||||||
|
if getattr(self, internal_name) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Perform Translation for the specific attribute only
|
||||||
|
base_geoms = getattr(self._base_result, internal_name)
|
||||||
|
if base_geoms is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
dx, dy = self._offset
|
||||||
|
# Use shapely.affinity.translate (imported at top level)
|
||||||
|
translated_geoms = [translate(p, dx, dy) for p in base_geoms]
|
||||||
|
setattr(self, internal_name, translated_geoms)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def geometry(self) -> list[Polygon]:
|
||||||
|
self._ensure_evaluated('geometry')
|
||||||
|
return self._geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated('dilated_geometry')
|
||||||
|
return self._dilated_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated('proxy_geometry')
|
||||||
|
return self._proxy_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actual_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated('actual_geometry')
|
||||||
|
return self._actual_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_actual_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated('dilated_actual_geometry')
|
||||||
|
return self._dilated_actual_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounds(self) -> list[tuple[float, float, float, float]]:
|
||||||
|
if not self._bounds_cached:
|
||||||
|
self._ensure_bounds_evaluated()
|
||||||
|
return self._bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_bounds(self) -> tuple[float, float, float, float]:
|
||||||
|
if not self._bounds_cached:
|
||||||
|
self._ensure_bounds_evaluated()
|
||||||
|
return self._total_bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_bounds(self) -> list[tuple[float, float, float, float]] | None:
|
||||||
|
if not self._bounds_cached:
|
||||||
|
self._ensure_bounds_evaluated()
|
||||||
|
return self._dilated_bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_dilated_bounds(self) -> tuple[float, float, float, float] | None:
|
||||||
|
if not self._bounds_cached:
|
||||||
|
self._ensure_bounds_evaluated()
|
||||||
|
return self._total_dilated_bounds
|
||||||
|
|
||||||
|
def _ensure_bounds_evaluated(self) -> None:
|
||||||
|
if self._bounds_cached: return
|
||||||
|
base = self._base_result
|
||||||
|
if base is not None:
|
||||||
|
dx, dy = self._offset
|
||||||
|
# Direct tuple creation is much faster than NumPy for single AABBs
|
||||||
|
if base._bounds is not None:
|
||||||
|
self._bounds = [(b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) for b in base._bounds]
|
||||||
|
if base._total_bounds is not None:
|
||||||
|
b = base._total_bounds
|
||||||
|
self._total_bounds = (b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy)
|
||||||
|
if base._dilated_bounds is not None:
|
||||||
|
self._dilated_bounds = [(b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) for b in base._dilated_bounds]
|
||||||
|
if base._total_dilated_bounds is not None:
|
||||||
|
b = base._total_dilated_bounds
|
||||||
|
self._total_dilated_bounds = (b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy)
|
||||||
|
self._bounds_cached = True
|
||||||
|
|
||||||
|
def translate(self, dx: float, dy: float, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None) -> ComponentResult:
|
||||||
|
"""
|
||||||
|
Create a new ComponentResult translated by (dx, dy).
|
||||||
|
"""
|
||||||
|
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation, snap=False)
|
||||||
|
|
||||||
|
# LAZY TRANSLATE
|
||||||
|
if self._base_result:
|
||||||
|
base = self._base_result
|
||||||
|
current_offset = self._offset
|
||||||
|
new_offset = (current_offset[0] + dx, current_offset[1] + dy)
|
||||||
|
else:
|
||||||
|
base = self
|
||||||
|
new_offset = (dx, dy)
|
||||||
|
|
||||||
|
return ComponentResult(
|
||||||
|
end_port=new_port,
|
||||||
|
length=self.length,
|
||||||
|
move_type=self.move_type,
|
||||||
|
_base_result=base,
|
||||||
|
_offset=new_offset,
|
||||||
|
rel_gx=rel_gx,
|
||||||
|
rel_gy=rel_gy,
|
||||||
|
rel_go=rel_go
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Straight:
|
||||||
|
"""
|
||||||
|
Move generator for straight waveguide segments.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def generate(
|
||||||
|
start_port: Port,
|
||||||
|
length: float,
|
||||||
|
width: float,
|
||||||
|
snap_to_grid: bool = True,
|
||||||
|
dilation: float = 0.0,
|
||||||
|
snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM,
|
||||||
|
) -> ComponentResult:
|
||||||
|
"""
|
||||||
|
Generate a straight waveguide segment.
|
||||||
|
"""
|
||||||
|
rad = numpy.radians(start_port.orientation)
|
||||||
|
cos_val = numpy.cos(rad)
|
||||||
|
sin_val = numpy.sin(rad)
|
||||||
|
|
||||||
|
ex = start_port.x + length * cos_val
|
||||||
|
ey = start_port.y + length * sin_val
|
||||||
|
|
||||||
|
if snap_to_grid:
|
||||||
|
ex = snap_search_grid(ex, snap_size)
|
||||||
|
ey = snap_search_grid(ey, snap_size)
|
||||||
|
|
||||||
|
end_port = Port(ex, ey, start_port.orientation)
|
||||||
|
actual_length = numpy.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2)
|
||||||
|
|
||||||
|
# Create polygons using vectorized points
|
||||||
|
half_w = width / 2.0
|
||||||
|
pts_raw = numpy.array([
|
||||||
|
[0, half_w],
|
||||||
|
[actual_length, half_w],
|
||||||
|
[actual_length, -half_w],
|
||||||
|
[0, -half_w]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Rotation matrix (standard 2D rotation)
|
||||||
|
rot_matrix = numpy.array([[cos_val, -sin_val], [sin_val, cos_val]])
|
||||||
|
|
||||||
|
# Transform points: P' = R * P + T
|
||||||
|
poly_points = (pts_raw @ rot_matrix.T) + [start_port.x, start_port.y]
|
||||||
|
geom = [Polygon(poly_points)]
|
||||||
|
|
||||||
|
dilated_geom = None
|
||||||
|
if dilation > 0:
|
||||||
|
# Direct calculation of dilated rectangle instead of expensive buffer()
|
||||||
|
half_w_dil = half_w + dilation
|
||||||
|
pts_dil = numpy.array([
|
||||||
|
[-dilation, half_w_dil],
|
||||||
|
[actual_length + dilation, half_w_dil],
|
||||||
|
[actual_length + dilation, -half_w_dil],
|
||||||
|
[-dilation, -half_w_dil]
|
||||||
|
])
|
||||||
|
poly_points_dil = (pts_dil @ rot_matrix.T) + [start_port.x, start_port.y]
|
||||||
|
dilated_geom = [Polygon(poly_points_dil)]
|
||||||
|
|
||||||
|
# Pre-calculate grid indices for faster ComponentResult init
|
||||||
|
inv_snap = 1.0 / snap_size
|
||||||
|
rgx = int(round(ex * inv_snap))
|
||||||
|
rgy = int(round(ey * inv_snap))
|
||||||
|
rgo = int(round(start_port.orientation))
|
||||||
|
|
||||||
|
# For straight segments, geom IS the actual geometry
|
||||||
|
return ComponentResult(
|
||||||
|
geometry=geom, end_port=end_port, length=actual_length,
|
||||||
|
dilated_geometry=dilated_geom, actual_geometry=geom,
|
||||||
|
dilated_actual_geometry=dilated_geom, move_type='Straight',
|
||||||
|
snap_size=snap_size, rel_gx=rgx, rel_gy=rgy, rel_go=rgo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
ratio = max(0.0, min(1.0, 1.0 - sagitta / radius))
|
||||||
|
theta_max = 2.0 * numpy.arccos(ratio)
|
||||||
|
if theta_max < TOLERANCE_ANGULAR:
|
||||||
|
return 16
|
||||||
|
num = int(numpy.ceil(numpy.radians(abs(angle_deg)) / theta_max))
|
||||||
|
return max(8, num)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_arc_polygons(
|
||||||
|
cx: float,
|
||||||
|
cy: float,
|
||||||
|
radius: float,
|
||||||
|
width: float,
|
||||||
|
t_start: float,
|
||||||
|
t_end: float,
|
||||||
|
sagitta: float = 0.01,
|
||||||
|
dilation: float = 0.0,
|
||||||
|
) -> list[Polygon]:
|
||||||
|
"""
|
||||||
|
Helper to generate arc-shaped polygons using vectorized NumPy operations.
|
||||||
|
"""
|
||||||
|
num_segments = _get_num_segments(radius, float(numpy.degrees(abs(t_end - t_start))), sagitta)
|
||||||
|
angles = numpy.linspace(t_start, t_end, num_segments + 1)
|
||||||
|
|
||||||
|
cos_a = numpy.cos(angles)
|
||||||
|
sin_a = numpy.sin(angles)
|
||||||
|
|
||||||
|
inner_radius = radius - width / 2.0 - dilation
|
||||||
|
outer_radius = radius + width / 2.0 + dilation
|
||||||
|
|
||||||
|
inner_points = numpy.stack([cx + inner_radius * cos_a, cy + inner_radius * sin_a], axis=1)
|
||||||
|
outer_points = numpy.stack([cx + outer_radius * cos_a[::-1], cy + outer_radius * sin_a[::-1]], axis=1)
|
||||||
|
|
||||||
|
# Concatenate inner and outer points to form the polygon ring
|
||||||
|
poly_points = numpy.concatenate([inner_points, outer_points])
|
||||||
|
|
||||||
|
return [Polygon(poly_points)]
|
||||||
|
|
||||||
|
|
||||||
|
def _clip_bbox(
|
||||||
|
cx: float,
|
||||||
|
cy: float,
|
||||||
|
radius: float,
|
||||||
|
width: float,
|
||||||
|
t_start: float,
|
||||||
|
t_end: float,
|
||||||
|
) -> Polygon:
|
||||||
|
"""
|
||||||
|
Generates a rotationally invariant bounding polygon for an arc.
|
||||||
|
"""
|
||||||
|
sweep = abs(t_end - t_start)
|
||||||
|
if sweep > 2 * numpy.pi:
|
||||||
|
sweep = sweep % (2 * numpy.pi)
|
||||||
|
|
||||||
|
mid_angle = (t_start + t_end) / 2.0
|
||||||
|
# Handle wrap-around for mid_angle
|
||||||
|
if abs(t_end - t_start) > numpy.pi:
|
||||||
|
mid_angle += numpy.pi
|
||||||
|
|
||||||
|
r_out = radius + width / 2.0
|
||||||
|
r_in = max(0.0, radius - width / 2.0)
|
||||||
|
|
||||||
|
half_sweep = sweep / 2.0
|
||||||
|
|
||||||
|
# Define vertices in local space (center at 0,0, symmetry axis along +X)
|
||||||
|
cos_hs = numpy.cos(half_sweep)
|
||||||
|
cos_hs2 = numpy.cos(half_sweep / 2.0)
|
||||||
|
|
||||||
|
# Distance to peak from center: r_out / cos(hs/2)
|
||||||
|
peak_r = r_out / cos_hs2
|
||||||
|
|
||||||
|
local_verts = [
|
||||||
|
[r_in * numpy.cos(-half_sweep), r_in * numpy.sin(-half_sweep)],
|
||||||
|
[r_out * numpy.cos(-half_sweep), r_out * numpy.sin(-half_sweep)],
|
||||||
|
[peak_r * numpy.cos(-half_sweep/2), peak_r * numpy.sin(-half_sweep/2)],
|
||||||
|
[peak_r * numpy.cos(half_sweep/2), peak_r * numpy.sin(half_sweep/2)],
|
||||||
|
[r_out * numpy.cos(half_sweep), r_out * numpy.sin(half_sweep)],
|
||||||
|
[r_in * numpy.cos(half_sweep), r_in * numpy.sin(half_sweep)],
|
||||||
|
[r_in, 0.0]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rotate and translate to world space
|
||||||
|
cos_m = numpy.cos(mid_angle)
|
||||||
|
sin_m = numpy.sin(mid_angle)
|
||||||
|
rot = numpy.array([[cos_m, -sin_m], [sin_m, cos_m]])
|
||||||
|
|
||||||
|
world_verts = (numpy.array(local_verts) @ rot.T) + [cx, cy]
|
||||||
|
|
||||||
|
return Polygon(world_verts)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_collision_model(
|
||||||
|
arc_poly: Polygon,
|
||||||
|
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
|
||||||
|
radius: float,
|
||||||
|
width: float,
|
||||||
|
cx: float = 0.0,
|
||||||
|
cy: float = 0.0,
|
||||||
|
clip_margin: float = 10.0,
|
||||||
|
t_start: float | None = None,
|
||||||
|
t_end: float | None = None,
|
||||||
|
) -> list[Polygon]:
|
||||||
|
"""
|
||||||
|
Applies the specified collision model to an arc geometry.
|
||||||
|
"""
|
||||||
|
if isinstance(collision_type, Polygon):
|
||||||
|
# Translate the custom polygon to the bend center (cx, cy)
|
||||||
|
return [shapely.transform(collision_type, lambda x: x + [cx, cy])]
|
||||||
|
|
||||||
|
if collision_type == "arc":
|
||||||
|
return [arc_poly]
|
||||||
|
|
||||||
|
if collision_type == "clipped_bbox" and t_start is not None and t_end is not None:
|
||||||
|
return [_clip_bbox(cx, cy, radius, width, t_start, t_end)]
|
||||||
|
|
||||||
|
# Bounding box of the high-fidelity arc (fallback for bbox or missing angles)
|
||||||
|
minx, miny, maxx, maxy = arc_poly.bounds
|
||||||
|
bbox_poly = box(minx, miny, maxx, maxy)
|
||||||
|
|
||||||
|
if collision_type == "bbox":
|
||||||
|
return [bbox_poly]
|
||||||
|
|
||||||
|
return [arc_poly]
|
||||||
|
|
||||||
|
|
||||||
|
class Bend90:
|
||||||
|
"""
|
||||||
|
Move generator for 90-degree waveguide bends.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def generate(
|
||||||
|
start_port: Port,
|
||||||
|
radius: float,
|
||||||
|
width: float,
|
||||||
|
direction: Literal["CW", "CCW"],
|
||||||
|
sagitta: float = 0.01,
|
||||||
|
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||||
|
clip_margin: float = 10.0,
|
||||||
|
dilation: float = 0.0,
|
||||||
|
snap_to_grid: bool = True,
|
||||||
|
snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM,
|
||||||
|
) -> ComponentResult:
|
||||||
|
"""
|
||||||
|
Generate a 90-degree bend.
|
||||||
|
"""
|
||||||
|
rad_start = numpy.radians(start_port.orientation)
|
||||||
|
|
||||||
|
# Center of the arc
|
||||||
|
if direction == "CCW":
|
||||||
|
cx = start_port.x + radius * numpy.cos(rad_start + numpy.pi / 2)
|
||||||
|
cy = start_port.y + radius * numpy.sin(rad_start + numpy.pi / 2)
|
||||||
|
t_start = rad_start - numpy.pi / 2
|
||||||
|
t_end = t_start + numpy.pi / 2
|
||||||
|
new_ori = (start_port.orientation + 90) % 360
|
||||||
|
else:
|
||||||
|
cx = start_port.x + radius * numpy.cos(rad_start - numpy.pi / 2)
|
||||||
|
cy = start_port.y + radius * numpy.sin(rad_start - numpy.pi / 2)
|
||||||
|
t_start = rad_start + numpy.pi / 2
|
||||||
|
t_end = t_start - numpy.pi / 2
|
||||||
|
new_ori = (start_port.orientation - 90) % 360
|
||||||
|
|
||||||
|
# Snap the end point to the grid
|
||||||
|
ex_raw = cx + radius * numpy.cos(t_end)
|
||||||
|
ey_raw = cy + radius * numpy.sin(t_end)
|
||||||
|
|
||||||
|
if snap_to_grid:
|
||||||
|
ex = snap_search_grid(ex_raw, snap_size)
|
||||||
|
ey = snap_search_grid(ey_raw, snap_size)
|
||||||
|
else:
|
||||||
|
ex, ey = ex_raw, ey_raw
|
||||||
|
|
||||||
|
# Slightly adjust radius and t_end to hit snapped point exactly
|
||||||
|
dx, dy = ex - cx, ey - cy
|
||||||
|
actual_radius = numpy.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
|
t_end_snapped = numpy.arctan2(dy, dx)
|
||||||
|
# Ensure directionality and approx 90 degree sweep
|
||||||
|
if direction == "CCW":
|
||||||
|
while t_end_snapped <= t_start:
|
||||||
|
t_end_snapped += 2 * numpy.pi
|
||||||
|
while t_end_snapped > t_start + numpy.pi:
|
||||||
|
t_end_snapped -= 2 * numpy.pi
|
||||||
|
else:
|
||||||
|
while t_end_snapped >= t_start:
|
||||||
|
t_end_snapped -= 2 * numpy.pi
|
||||||
|
while t_end_snapped < t_start - numpy.pi:
|
||||||
|
t_end_snapped += 2 * numpy.pi
|
||||||
|
t_end = t_end_snapped
|
||||||
|
|
||||||
|
end_port = Port(ex, ey, new_ori)
|
||||||
|
|
||||||
|
arc_polys = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta)
|
||||||
|
collision_polys = _apply_collision_model(
|
||||||
|
arc_polys[0], collision_type, actual_radius, width, cx, cy, clip_margin, t_start, t_end
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy_geom = None
|
||||||
|
if collision_type == "arc":
|
||||||
|
# Auto-generate a clipped_bbox proxy for tiered collision checks
|
||||||
|
proxy_geom = _apply_collision_model(
|
||||||
|
arc_polys[0], "clipped_bbox", actual_radius, width, cx, cy, clip_margin, t_start, t_end
|
||||||
|
)
|
||||||
|
|
||||||
|
dilated_geom = None
|
||||||
|
dilated_actual_geom = None
|
||||||
|
if dilation > 0:
|
||||||
|
dilated_actual_geom = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta, dilation=dilation)
|
||||||
|
if collision_type == "arc":
|
||||||
|
dilated_geom = dilated_actual_geom
|
||||||
|
else:
|
||||||
|
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||||
|
|
||||||
|
# Pre-calculate grid indices for faster ComponentResult init
|
||||||
|
inv_snap = 1.0 / snap_size
|
||||||
|
rgx = int(round(ex * inv_snap))
|
||||||
|
rgy = int(round(ey * inv_snap))
|
||||||
|
rgo = int(round(new_ori))
|
||||||
|
|
||||||
|
return ComponentResult(
|
||||||
|
geometry=collision_polys,
|
||||||
|
end_port=end_port,
|
||||||
|
length=actual_radius * numpy.abs(t_end - t_start),
|
||||||
|
dilated_geometry=dilated_geom,
|
||||||
|
proxy_geometry=proxy_geom,
|
||||||
|
actual_geometry=arc_polys,
|
||||||
|
dilated_actual_geometry=dilated_actual_geom,
|
||||||
|
move_type='Bend90',
|
||||||
|
snap_size=snap_size,
|
||||||
|
rel_gx=rgx, rel_gy=rgy, rel_go=rgo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SBend:
|
||||||
|
"""
|
||||||
|
Move generator for parametric S-bends.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def generate(
|
||||||
|
start_port: Port,
|
||||||
|
offset: float,
|
||||||
|
radius: float,
|
||||||
|
width: float,
|
||||||
|
sagitta: float = 0.01,
|
||||||
|
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||||
|
clip_margin: float = 10.0,
|
||||||
|
dilation: float = 0.0,
|
||||||
|
snap_to_grid: bool = True,
|
||||||
|
snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM,
|
||||||
|
) -> ComponentResult:
|
||||||
|
"""
|
||||||
|
Generate a parametric S-bend (two tangent arcs).
|
||||||
|
"""
|
||||||
|
if abs(offset) >= 2 * radius:
|
||||||
|
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||||
|
|
||||||
|
theta_init = numpy.arccos(1 - abs(offset) / (2 * radius))
|
||||||
|
dx_init = 2 * radius * numpy.sin(theta_init)
|
||||||
|
rad_start = numpy.radians(start_port.orientation)
|
||||||
|
|
||||||
|
# Target point
|
||||||
|
ex_raw = start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start)
|
||||||
|
ey_raw = start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.cos(rad_start)
|
||||||
|
|
||||||
|
if snap_to_grid:
|
||||||
|
ex = snap_search_grid(ex_raw, snap_size)
|
||||||
|
ey = snap_search_grid(ey_raw, snap_size)
|
||||||
|
else:
|
||||||
|
ex, ey = ex_raw, ey_raw
|
||||||
|
|
||||||
|
end_port = Port(ex, ey, start_port.orientation)
|
||||||
|
|
||||||
|
# Solve for theta and radius that hit (ex, ey) exactly
|
||||||
|
local_dx = (ex - start_port.x) * numpy.cos(rad_start) + (ey - start_port.y) * numpy.sin(rad_start)
|
||||||
|
local_dy = -(ex - start_port.x) * numpy.sin(rad_start) + (ey - start_port.y) * numpy.cos(rad_start)
|
||||||
|
|
||||||
|
# tan(theta / 2) = local_dy / local_dx
|
||||||
|
theta = 2 * numpy.arctan2(abs(local_dy), local_dx)
|
||||||
|
|
||||||
|
if abs(theta) < TOLERANCE_ANGULAR:
|
||||||
|
# De-generate to straight
|
||||||
|
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
||||||
|
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation, snap_size=snap_size)
|
||||||
|
|
||||||
|
denom = (2 * (1 - numpy.cos(theta)))
|
||||||
|
if abs(denom) < TOLERANCE_LINEAR:
|
||||||
|
raise ValueError("SBend calculation failed: radius denominator zero")
|
||||||
|
|
||||||
|
actual_radius = abs(local_dy) / denom
|
||||||
|
|
||||||
|
# Safety Check: Reject SBends with tiny radii that would cause self-overlap
|
||||||
|
if actual_radius < width:
|
||||||
|
raise ValueError(f"SBend actual_radius {actual_radius:.3f} is too small (width={width})")
|
||||||
|
|
||||||
|
# Limit radius to prevent giant arcs
|
||||||
|
if actual_radius > 100000.0:
|
||||||
|
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
||||||
|
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation, snap_size=snap_size)
|
||||||
|
|
||||||
|
direction = 1 if local_dy > 0 else -1
|
||||||
|
c1_angle = rad_start + direction * numpy.pi / 2
|
||||||
|
cx1 = start_port.x + actual_radius * numpy.cos(c1_angle)
|
||||||
|
cy1 = start_port.y + actual_radius * numpy.sin(c1_angle)
|
||||||
|
ts1, te1 = c1_angle + numpy.pi, c1_angle + numpy.pi + direction * theta
|
||||||
|
|
||||||
|
c2_angle = rad_start - direction * numpy.pi / 2
|
||||||
|
cx2 = ex + actual_radius * numpy.cos(c2_angle)
|
||||||
|
cy2 = ey + actual_radius * numpy.sin(c2_angle)
|
||||||
|
te2 = c2_angle + numpy.pi
|
||||||
|
ts2 = te2 + direction * theta
|
||||||
|
|
||||||
|
arc1 = _get_arc_polygons(cx1, cy1, actual_radius, width, ts1, te1, sagitta)[0]
|
||||||
|
arc2 = _get_arc_polygons(cx2, cy2, actual_radius, width, ts2, te2, sagitta)[0]
|
||||||
|
arc_polys = [arc1, arc2]
|
||||||
|
|
||||||
|
# Use the provided collision model for primary geometry
|
||||||
|
col1 = _apply_collision_model(arc1, collision_type, actual_radius, width, cx1, cy1, clip_margin, ts1, te1)[0]
|
||||||
|
col2 = _apply_collision_model(arc2, collision_type, actual_radius, width, cx2, cy2, clip_margin, ts2, te2)[0]
|
||||||
|
collision_polys = [col1, col2]
|
||||||
|
|
||||||
|
proxy_geom = None
|
||||||
|
if collision_type == "arc":
|
||||||
|
# Auto-generate proxies
|
||||||
|
p1 = _apply_collision_model(arc1, "clipped_bbox", actual_radius, width, cx1, cy1, clip_margin, ts1, te1)[0]
|
||||||
|
p2 = _apply_collision_model(arc2, "clipped_bbox", actual_radius, width, cx2, cy2, clip_margin, ts2, te2)[0]
|
||||||
|
proxy_geom = [p1, p2]
|
||||||
|
|
||||||
|
dilated_geom = None
|
||||||
|
dilated_actual_geom = None
|
||||||
|
if dilation > 0:
|
||||||
|
d1 = _get_arc_polygons(cx1, cy1, actual_radius, width, ts1, te1, sagitta, dilation=dilation)[0]
|
||||||
|
d2 = _get_arc_polygons(cx2, cy2, actual_radius, width, ts2, te2, sagitta, dilation=dilation)[0]
|
||||||
|
dilated_actual_geom = [d1, d2]
|
||||||
|
|
||||||
|
if collision_type == "arc":
|
||||||
|
dilated_geom = dilated_actual_geom
|
||||||
|
else:
|
||||||
|
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||||
|
|
||||||
|
# Pre-calculate grid indices for faster ComponentResult init
|
||||||
|
inv_snap = 1.0 / snap_size
|
||||||
|
rgx = int(round(ex * inv_snap))
|
||||||
|
rgy = int(round(ey * inv_snap))
|
||||||
|
rgo = int(round(start_port.orientation))
|
||||||
|
|
||||||
|
return ComponentResult(
|
||||||
|
geometry=collision_polys,
|
||||||
|
end_port=end_port,
|
||||||
|
length=2 * actual_radius * theta,
|
||||||
|
dilated_geometry=dilated_geom,
|
||||||
|
proxy_geometry=proxy_geom,
|
||||||
|
actual_geometry=arc_polys,
|
||||||
|
dilated_actual_geometry=dilated_actual_geom,
|
||||||
|
move_type='SBend',
|
||||||
|
snap_size=snap_size,
|
||||||
|
rel_gx=rgx, rel_gy=rgy, rel_go=rgo
|
||||||
|
)
|
||||||
77
inire/geometry/primitives.py
Normal file
77
inire/geometry/primitives.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
# 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 * 1000) / 1000
|
||||||
|
|
||||||
|
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
|
||||||
|
class Port:
|
||||||
|
"""
|
||||||
|
A port defined by (x, y, orientation) in micrometers.
|
||||||
|
"""
|
||||||
|
__slots__ = ('x', 'y', 'orientation')
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
orientation: float,
|
||||||
|
snap: bool = True
|
||||||
|
) -> None:
|
||||||
|
if snap:
|
||||||
|
self.x = round(x * 1000) / 1000
|
||||||
|
self.y = round(y * 1000) / 1000
|
||||||
|
# Faster orientation normalization for common cases
|
||||||
|
if 0 <= orientation < 360:
|
||||||
|
self.orientation = float(orientation)
|
||||||
|
else:
|
||||||
|
self.orientation = float(orientation % 360)
|
||||||
|
else:
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.orientation = float(orientation)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'Port(x={self.x}, y={self.y}, orientation={self.orientation})'
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Port):
|
||||||
|
return False
|
||||||
|
return (abs(self.x - other.x) < TOLERANCE_LINEAR and
|
||||||
|
abs(self.y - other.y) < TOLERANCE_LINEAR and
|
||||||
|
abs(self.orientation - other.orientation) < TOLERANCE_LINEAR)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((round(self.x, 6), round(self.y, 6), round(self.orientation, 6)))
|
||||||
|
|
||||||
|
|
||||||
|
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 = numpy.radians(angle)
|
||||||
|
qx = snap_nm(ox + numpy.cos(rad) * (px - ox) - numpy.sin(rad) * (py - oy))
|
||||||
|
qy = snap_nm(oy + numpy.sin(rad) * (px - ox) + numpy.cos(rad) * (py - oy))
|
||||||
|
|
||||||
|
return Port(qx, qy, port.orientation + angle)
|
||||||
614
inire/router/astar.py
Normal file
614
inire/router/astar.py
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Literal, Any
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import shapely
|
||||||
|
|
||||||
|
from inire.geometry.components import Bend90, SBend, Straight, snap_search_grid
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.config import RouterConfig
|
||||||
|
from inire.router.visibility import VisibilityManager
|
||||||
|
from inire.constants import DEFAULT_SEARCH_GRID_SNAP_UM, TOLERANCE_LINEAR, TOLERANCE_ANGULAR
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AStarNode:
|
||||||
|
"""
|
||||||
|
A node in the A* search tree.
|
||||||
|
"""
|
||||||
|
__slots__ = ('port', 'g_cost', 'h_cost', 'fh_cost', 'parent', 'component_result')
|
||||||
|
|
||||||
|
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.fh_cost = (g_cost + h_cost, h_cost)
|
||||||
|
self.parent = parent
|
||||||
|
self.component_result = component_result
|
||||||
|
|
||||||
|
def __lt__(self, other: AStarNode) -> bool:
|
||||||
|
return self.fh_cost < other.fh_cost
|
||||||
|
|
||||||
|
|
||||||
|
class AStarMetrics:
|
||||||
|
"""
|
||||||
|
Performance metrics and instrumentation for A* search.
|
||||||
|
"""
|
||||||
|
__slots__ = ('total_nodes_expanded', 'last_expanded_nodes', 'nodes_expanded',
|
||||||
|
'moves_generated', 'moves_added', 'pruned_closed_set',
|
||||||
|
'pruned_hard_collision', 'pruned_cost')
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.total_nodes_expanded = 0
|
||||||
|
self.last_expanded_nodes: list[tuple[float, float, float]] = []
|
||||||
|
self.nodes_expanded = 0
|
||||||
|
self.moves_generated = 0
|
||||||
|
self.moves_added = 0
|
||||||
|
self.pruned_closed_set = 0
|
||||||
|
self.pruned_hard_collision = 0
|
||||||
|
self.pruned_cost = 0
|
||||||
|
|
||||||
|
def reset_per_route(self) -> None:
|
||||||
|
""" Reset metrics that are specific to a single route() call. """
|
||||||
|
self.nodes_expanded = 0
|
||||||
|
self.moves_generated = 0
|
||||||
|
self.moves_added = 0
|
||||||
|
self.pruned_closed_set = 0
|
||||||
|
self.pruned_hard_collision = 0
|
||||||
|
self.pruned_cost = 0
|
||||||
|
self.last_expanded_nodes = []
|
||||||
|
|
||||||
|
def get_summary_dict(self) -> dict[str, int]:
|
||||||
|
""" Return a dictionary of current metrics. """
|
||||||
|
return {
|
||||||
|
'nodes_expanded': self.nodes_expanded,
|
||||||
|
'moves_generated': self.moves_generated,
|
||||||
|
'moves_added': self.moves_added,
|
||||||
|
'pruned_closed_set': self.pruned_closed_set,
|
||||||
|
'pruned_hard_collision': self.pruned_hard_collision,
|
||||||
|
'pruned_cost': self.pruned_cost
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AStarContext:
|
||||||
|
"""
|
||||||
|
Persistent state for A* search, decoupled from search logic.
|
||||||
|
"""
|
||||||
|
__slots__ = ('cost_evaluator', 'config', 'visibility_manager',
|
||||||
|
'move_cache_rel', 'move_cache_abs', 'hard_collision_set', 'static_safe_cache', 'max_cache_size')
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cost_evaluator: CostEvaluator,
|
||||||
|
node_limit: int = 1000000,
|
||||||
|
snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM,
|
||||||
|
max_straight_length: float = 2000.0,
|
||||||
|
min_straight_length: float = 5.0,
|
||||||
|
bend_radii: list[float] | None = None,
|
||||||
|
sbend_radii: list[float] | None = None,
|
||||||
|
sbend_offsets: list[float] | None = None,
|
||||||
|
bend_penalty: float = 250.0,
|
||||||
|
sbend_penalty: float = 500.0,
|
||||||
|
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc",
|
||||||
|
bend_clip_margin: float = 10.0,
|
||||||
|
max_cache_size: int = 1000000,
|
||||||
|
) -> None:
|
||||||
|
self.cost_evaluator = cost_evaluator
|
||||||
|
self.max_cache_size = max_cache_size
|
||||||
|
|
||||||
|
# Use provided lists or defaults for the configuration
|
||||||
|
br = bend_radii if bend_radii is not None else [50.0, 100.0]
|
||||||
|
sr = sbend_radii if sbend_radii is not None else [5.0, 10.0, 50.0, 100.0]
|
||||||
|
|
||||||
|
self.config = RouterConfig(
|
||||||
|
node_limit=node_limit,
|
||||||
|
snap_size=snap_size,
|
||||||
|
max_straight_length=max_straight_length,
|
||||||
|
min_straight_length=min_straight_length,
|
||||||
|
bend_radii=br,
|
||||||
|
sbend_radii=sr,
|
||||||
|
sbend_offsets=sbend_offsets,
|
||||||
|
bend_penalty=bend_penalty,
|
||||||
|
sbend_penalty=sbend_penalty,
|
||||||
|
bend_collision_type=bend_collision_type,
|
||||||
|
bend_clip_margin=bend_clip_margin
|
||||||
|
)
|
||||||
|
self.cost_evaluator.config = self.config
|
||||||
|
|
||||||
|
self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine)
|
||||||
|
|
||||||
|
# Long-lived caches (shared across multiple route calls)
|
||||||
|
self.move_cache_rel: dict[tuple, ComponentResult] = {}
|
||||||
|
self.move_cache_abs: dict[tuple, ComponentResult] = {}
|
||||||
|
self.hard_collision_set: set[tuple] = set()
|
||||||
|
self.static_safe_cache: set[tuple] = set()
|
||||||
|
|
||||||
|
def clear_static_caches(self) -> None:
|
||||||
|
""" Clear caches that depend on the state of static obstacles. """
|
||||||
|
self.hard_collision_set.clear()
|
||||||
|
self.static_safe_cache.clear()
|
||||||
|
|
||||||
|
def check_cache_eviction(self) -> None:
|
||||||
|
"""
|
||||||
|
Trigger FIFO eviction of Absolute moves if cache exceeds max_cache_size.
|
||||||
|
We preserve Relative move templates.
|
||||||
|
"""
|
||||||
|
# Trigger eviction if 20% over limit to reduce frequency
|
||||||
|
if len(self.move_cache_abs) > self.max_cache_size * 1.2:
|
||||||
|
num_to_evict = int(len(self.move_cache_abs) * 0.25)
|
||||||
|
# Efficient FIFO eviction
|
||||||
|
keys_to_evict = []
|
||||||
|
it = iter(self.move_cache_abs)
|
||||||
|
for _ in range(num_to_evict):
|
||||||
|
try: keys_to_evict.append(next(it))
|
||||||
|
except StopIteration: break
|
||||||
|
for k in keys_to_evict:
|
||||||
|
del self.move_cache_abs[k]
|
||||||
|
|
||||||
|
# Decouple collision cache clearing - only clear if truly massive
|
||||||
|
if len(self.hard_collision_set) > 2000000:
|
||||||
|
self.hard_collision_set.clear()
|
||||||
|
self.static_safe_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def route_astar(
|
||||||
|
start: Port,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics | None = None,
|
||||||
|
net_id: str = 'default',
|
||||||
|
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None,
|
||||||
|
return_partial: bool = False,
|
||||||
|
store_expanded: bool = False,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
node_limit: int | None = None,
|
||||||
|
) -> list[ComponentResult] | None:
|
||||||
|
"""
|
||||||
|
Functional implementation of A* routing.
|
||||||
|
"""
|
||||||
|
if metrics is None:
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
|
||||||
|
metrics.reset_per_route()
|
||||||
|
|
||||||
|
# Enforce Grid Alignment for start and target
|
||||||
|
snap = context.config.snap_size
|
||||||
|
start_snapped = Port(snap_search_grid(start.x, snap), snap_search_grid(start.y, snap), start.orientation, snap=False)
|
||||||
|
target_snapped = Port(snap_search_grid(target.x, snap), snap_search_grid(target.y, snap), target.orientation, snap=False)
|
||||||
|
|
||||||
|
# Per-route congestion cache (not shared across different routes)
|
||||||
|
congestion_cache: dict[tuple, int] = {}
|
||||||
|
|
||||||
|
if bend_collision_type is not None:
|
||||||
|
context.config.bend_collision_type = bend_collision_type
|
||||||
|
|
||||||
|
context.cost_evaluator.set_target(target_snapped)
|
||||||
|
|
||||||
|
open_set: list[AStarNode] = []
|
||||||
|
inv_snap = 1.0 / snap
|
||||||
|
|
||||||
|
# (x_grid, y_grid, orientation_grid) -> min_g_cost
|
||||||
|
closed_set: dict[tuple[int, int, int], float] = {}
|
||||||
|
|
||||||
|
start_node = AStarNode(start_snapped, 0.0, context.cost_evaluator.h_manhattan(start_snapped, target_snapped))
|
||||||
|
heapq.heappush(open_set, start_node)
|
||||||
|
|
||||||
|
best_node = start_node
|
||||||
|
nodes_expanded = 0
|
||||||
|
|
||||||
|
effective_node_limit = node_limit if node_limit is not None else context.config.node_limit
|
||||||
|
|
||||||
|
while open_set:
|
||||||
|
if nodes_expanded >= effective_node_limit:
|
||||||
|
return reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
|
current = heapq.heappop(open_set)
|
||||||
|
|
||||||
|
# Cost Pruning (Fail Fast)
|
||||||
|
if max_cost is not None and current.fh_cost[0] > max_cost:
|
||||||
|
metrics.pruned_cost += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current.h_cost < best_node.h_cost:
|
||||||
|
best_node = current
|
||||||
|
|
||||||
|
state = (int(round(current.port.x * inv_snap)), int(round(current.port.y * inv_snap)), int(round(current.port.orientation)))
|
||||||
|
if state in closed_set and closed_set[state] <= current.g_cost + TOLERANCE_LINEAR:
|
||||||
|
continue
|
||||||
|
closed_set[state] = current.g_cost
|
||||||
|
|
||||||
|
if store_expanded:
|
||||||
|
metrics.last_expanded_nodes.append((current.port.x, current.port.y, current.port.orientation))
|
||||||
|
|
||||||
|
nodes_expanded += 1
|
||||||
|
metrics.total_nodes_expanded += 1
|
||||||
|
metrics.nodes_expanded += 1
|
||||||
|
|
||||||
|
# Check if we reached the target exactly
|
||||||
|
if (abs(current.port.x - target_snapped.x) < TOLERANCE_LINEAR and
|
||||||
|
abs(current.port.y - target_snapped.y) < TOLERANCE_LINEAR and
|
||||||
|
abs(current.port.orientation - target_snapped.orientation) < 0.1):
|
||||||
|
return reconstruct_path(current)
|
||||||
|
|
||||||
|
# Expansion
|
||||||
|
expand_moves(
|
||||||
|
current, target_snapped, net_width, net_id, open_set, closed_set,
|
||||||
|
context, metrics, congestion_cache,
|
||||||
|
snap=snap, inv_snap=inv_snap, parent_state=state,
|
||||||
|
max_cost=max_cost, skip_congestion=skip_congestion,
|
||||||
|
self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
return reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
|
|
||||||
|
def expand_moves(
|
||||||
|
current: AStarNode,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
snap: float = 1.0,
|
||||||
|
inv_snap: float | None = None,
|
||||||
|
parent_state: tuple[int, int, int] | None = None,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Extract moves and add valid successors to the open set.
|
||||||
|
"""
|
||||||
|
cp = current.port
|
||||||
|
if inv_snap is None: inv_snap = 1.0 / snap
|
||||||
|
if parent_state is None:
|
||||||
|
parent_state = (int(round(cp.x * inv_snap)), int(round(cp.y * inv_snap)), int(round(cp.orientation)))
|
||||||
|
|
||||||
|
dx_t = target.x - cp.x
|
||||||
|
dy_t = target.y - cp.y
|
||||||
|
dist_sq = dx_t*dx_t + dy_t*dy_t
|
||||||
|
|
||||||
|
rad = numpy.radians(cp.orientation)
|
||||||
|
cos_v, sin_v = numpy.cos(rad), numpy.sin(rad)
|
||||||
|
|
||||||
|
# 1. DIRECT JUMP TO TARGET
|
||||||
|
proj_t = dx_t * cos_v + dy_t * sin_v
|
||||||
|
perp_t = -dx_t * sin_v + dy_t * cos_v
|
||||||
|
|
||||||
|
# A. Straight Jump (Only if target aligns with grid state or direct jump is enabled)
|
||||||
|
if proj_t > 0 and abs(perp_t) < 1e-3 and abs(cp.orientation - target.orientation) < 0.1:
|
||||||
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, proj_t + 1.0)
|
||||||
|
if max_reach >= proj_t - 0.01:
|
||||||
|
process_move(
|
||||||
|
current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
f'S{proj_t}', 'S', (proj_t,), skip_congestion, inv_snap=inv_snap, snap_to_grid=False,
|
||||||
|
parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. VISIBILITY JUMPS & MAX REACH
|
||||||
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, context.config.max_straight_length)
|
||||||
|
|
||||||
|
straight_lengths = set()
|
||||||
|
if max_reach > context.config.min_straight_length:
|
||||||
|
straight_lengths.add(snap_search_grid(max_reach, snap))
|
||||||
|
for radius in context.config.bend_radii:
|
||||||
|
if max_reach > radius + context.config.min_straight_length:
|
||||||
|
straight_lengths.add(snap_search_grid(max_reach - radius, snap))
|
||||||
|
|
||||||
|
if max_reach > context.config.min_straight_length + 5.0:
|
||||||
|
straight_lengths.add(snap_search_grid(max_reach - 5.0, snap))
|
||||||
|
|
||||||
|
straight_lengths.add(context.config.min_straight_length)
|
||||||
|
if max_reach > context.config.min_straight_length * 4:
|
||||||
|
straight_lengths.add(snap_search_grid(max_reach / 2.0, snap))
|
||||||
|
|
||||||
|
if abs(cp.orientation % 180) < 0.1: # Horizontal
|
||||||
|
target_dist = abs(target.x - cp.x)
|
||||||
|
if target_dist <= max_reach and target_dist > context.config.min_straight_length:
|
||||||
|
sl = snap_search_grid(target_dist, snap)
|
||||||
|
if sl > 0.1: straight_lengths.add(sl)
|
||||||
|
for radius in context.config.bend_radii:
|
||||||
|
for l in [target_dist - radius, target_dist - 2*radius]:
|
||||||
|
if l > context.config.min_straight_length:
|
||||||
|
s_l = snap_search_grid(l, snap)
|
||||||
|
if s_l <= max_reach and s_l > 0.1: straight_lengths.add(s_l)
|
||||||
|
else: # Vertical
|
||||||
|
target_dist = abs(target.y - cp.y)
|
||||||
|
if target_dist <= max_reach and target_dist > context.config.min_straight_length:
|
||||||
|
sl = snap_search_grid(target_dist, snap)
|
||||||
|
if sl > 0.1: straight_lengths.add(sl)
|
||||||
|
for radius in context.config.bend_radii:
|
||||||
|
for l in [target_dist - radius, target_dist - 2*radius]:
|
||||||
|
if l > context.config.min_straight_length:
|
||||||
|
s_l = snap_search_grid(l, snap)
|
||||||
|
if s_l <= max_reach and s_l > 0.1: straight_lengths.add(s_l)
|
||||||
|
|
||||||
|
for length in sorted(straight_lengths, reverse=True):
|
||||||
|
process_move(
|
||||||
|
current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
f'S{length}', 'S', (length,), skip_congestion, inv_snap=inv_snap, parent_state=parent_state,
|
||||||
|
max_cost=max_cost, snap=snap, self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. BENDS & SBENDS
|
||||||
|
angle_to_target = numpy.degrees(numpy.arctan2(target.y - cp.y, target.x - cp.x))
|
||||||
|
allow_backwards = (dist_sq < 150*150)
|
||||||
|
|
||||||
|
for radius in context.config.bend_radii:
|
||||||
|
for direction in ['CW', 'CCW']:
|
||||||
|
if not allow_backwards:
|
||||||
|
turn = 90 if direction == 'CCW' else -90
|
||||||
|
new_ori = (cp.orientation + turn) % 360
|
||||||
|
new_diff = (angle_to_target - new_ori + 180) % 360 - 180
|
||||||
|
if abs(new_diff) > 135:
|
||||||
|
continue
|
||||||
|
process_move(
|
||||||
|
current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
f'B{radius}{direction}', 'B', (radius, direction), skip_congestion, inv_snap=inv_snap,
|
||||||
|
parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. SBENDS
|
||||||
|
max_sbend_r = max(context.config.sbend_radii) if context.config.sbend_radii else 0
|
||||||
|
if max_sbend_r > 0:
|
||||||
|
user_offsets = context.config.sbend_offsets
|
||||||
|
offsets: set[float] = set(user_offsets) if user_offsets is not None else set()
|
||||||
|
dx_local = (target.x - cp.x) * cos_v + (target.y - cp.y) * sin_v
|
||||||
|
dy_local = -(target.x - cp.x) * sin_v + (target.y - cp.y) * cos_v
|
||||||
|
|
||||||
|
if dx_local > 0 and abs(dy_local) < 2 * max_sbend_r:
|
||||||
|
min_d = numpy.sqrt(max(0, 4 * (abs(dy_local)/2.0) * abs(dy_local) - dy_local**2))
|
||||||
|
if dx_local >= min_d: offsets.add(dy_local)
|
||||||
|
|
||||||
|
if user_offsets is None:
|
||||||
|
for sign in [-1, 1]:
|
||||||
|
# Adaptive sampling: scale steps by snap_size but ensure enough range
|
||||||
|
for i in [1, 2, 5, 13, 34, 89]:
|
||||||
|
o = sign * i * snap
|
||||||
|
if abs(o) < 2 * max_sbend_r: offsets.add(o)
|
||||||
|
|
||||||
|
for offset in sorted(offsets):
|
||||||
|
for radius in context.config.sbend_radii:
|
||||||
|
if abs(offset) >= 2 * radius: continue
|
||||||
|
process_move(
|
||||||
|
current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion, inv_snap=inv_snap,
|
||||||
|
parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_move(
|
||||||
|
parent: AStarNode,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
move_type: str,
|
||||||
|
move_class: Literal['S', 'B', 'SB'],
|
||||||
|
params: tuple,
|
||||||
|
skip_congestion: bool,
|
||||||
|
inv_snap: float | None = None,
|
||||||
|
snap_to_grid: bool = True,
|
||||||
|
parent_state: tuple[int, int, int] | None = None,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
snap: float = 1.0,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Generate or retrieve geometry and delegate to add_node.
|
||||||
|
"""
|
||||||
|
cp = parent.port
|
||||||
|
if inv_snap is None: inv_snap = 1.0 / snap
|
||||||
|
base_ori = float(int(cp.orientation + 0.5))
|
||||||
|
if parent_state is None:
|
||||||
|
gx = int(round(cp.x * inv_snap))
|
||||||
|
gy = int(round(cp.y * inv_snap))
|
||||||
|
go = int(round(cp.orientation))
|
||||||
|
parent_state = (gx, gy, go)
|
||||||
|
else:
|
||||||
|
gx, gy, go = parent_state
|
||||||
|
|
||||||
|
coll_type = context.config.bend_collision_type
|
||||||
|
coll_key = id(coll_type) if isinstance(coll_type, shapely.geometry.Polygon) else coll_type
|
||||||
|
|
||||||
|
abs_key = (parent_state, move_class, params, net_width, coll_key, snap_to_grid)
|
||||||
|
if abs_key in context.move_cache_abs:
|
||||||
|
res = context.move_cache_abs[abs_key]
|
||||||
|
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
||||||
|
add_node(
|
||||||
|
parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion,
|
||||||
|
inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Trigger periodic cache eviction check (only on Absolute cache misses)
|
||||||
|
context.check_cache_eviction()
|
||||||
|
|
||||||
|
# Template Cache Key (Relative to Port 0,0,Ori)
|
||||||
|
# We snap the parameters to ensure template re-use
|
||||||
|
snapped_params = params
|
||||||
|
if move_class == 'SB':
|
||||||
|
snapped_params = (snap_search_grid(params[0], snap), params[1])
|
||||||
|
|
||||||
|
self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
rel_key = (base_ori, move_class, snapped_params, net_width, coll_key, self_dilation, snap_to_grid)
|
||||||
|
|
||||||
|
cache_key = (gx, gy, go, move_type, net_width)
|
||||||
|
if cache_key in context.hard_collision_set:
|
||||||
|
return
|
||||||
|
|
||||||
|
if rel_key in context.move_cache_rel:
|
||||||
|
res_rel = context.move_cache_rel[rel_key]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
p0 = Port(0, 0, base_ori)
|
||||||
|
if move_class == 'S':
|
||||||
|
res_rel = Straight.generate(p0, params[0], net_width, dilation=self_dilation, snap_to_grid=snap_to_grid, snap_size=snap)
|
||||||
|
elif move_class == 'B':
|
||||||
|
res_rel = Bend90.generate(p0, params[0], net_width, params[1], collision_type=context.config.bend_collision_type, clip_margin=context.config.bend_clip_margin, dilation=self_dilation, snap_to_grid=snap_to_grid, snap_size=snap)
|
||||||
|
elif move_class == 'SB':
|
||||||
|
res_rel = SBend.generate(p0, snapped_params[0], snapped_params[1], net_width, collision_type=context.config.bend_collision_type, clip_margin=context.config.bend_clip_margin, dilation=self_dilation, snap_to_grid=snap_to_grid, snap_size=snap)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
context.move_cache_rel[rel_key] = res_rel
|
||||||
|
except (ValueError, ZeroDivisionError):
|
||||||
|
return
|
||||||
|
|
||||||
|
res = res_rel.translate(cp.x, cp.y, rel_gx=res_rel.rel_gx + gx, rel_gy=res_rel.rel_gy + gy, rel_go=res_rel.rel_go)
|
||||||
|
context.move_cache_abs[abs_key] = res
|
||||||
|
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
||||||
|
add_node(
|
||||||
|
parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache,
|
||||||
|
move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion,
|
||||||
|
inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost,
|
||||||
|
self_collision_check=self_collision_check
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_node(
|
||||||
|
parent: AStarNode,
|
||||||
|
result: ComponentResult,
|
||||||
|
target: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
open_set: list[AStarNode],
|
||||||
|
closed_set: dict[tuple[int, int, int], float],
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics,
|
||||||
|
congestion_cache: dict[tuple, int],
|
||||||
|
move_type: str,
|
||||||
|
move_radius: float | None = None,
|
||||||
|
snap: float = 1.0,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
inv_snap: float | None = None,
|
||||||
|
parent_state: tuple[int, int, int] | None = None,
|
||||||
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Check collisions and costs, and add node to the open set.
|
||||||
|
"""
|
||||||
|
metrics.moves_generated += 1
|
||||||
|
state = (result.rel_gx, result.rel_gy, result.rel_go)
|
||||||
|
|
||||||
|
# Early pruning using lower-bound total cost
|
||||||
|
# child.total_g >= parent.total_g + move_length
|
||||||
|
new_lower_bound_g = parent.g_cost + result.length
|
||||||
|
if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR:
|
||||||
|
metrics.pruned_closed_set += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
parent_p = parent.port
|
||||||
|
end_p = result.end_port
|
||||||
|
if parent_state is None:
|
||||||
|
pgx, pgy, pgo = int(round(parent_p.x * inv_snap)), int(round(parent_p.y * inv_snap)), int(round(parent_p.orientation))
|
||||||
|
else:
|
||||||
|
pgx, pgy, pgo = parent_state
|
||||||
|
cache_key = (pgx, pgy, pgo, move_type, net_width)
|
||||||
|
|
||||||
|
if cache_key in context.hard_collision_set:
|
||||||
|
metrics.pruned_hard_collision += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
is_static_safe = (cache_key in context.static_safe_cache)
|
||||||
|
if not is_static_safe:
|
||||||
|
ce = context.cost_evaluator.collision_engine
|
||||||
|
collision_found = False
|
||||||
|
if 'S' in move_type and 'SB' not in move_type:
|
||||||
|
collision_found = ce.check_move_straight_static(parent_p, result.length)
|
||||||
|
else:
|
||||||
|
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p)
|
||||||
|
|
||||||
|
if collision_found:
|
||||||
|
context.hard_collision_set.add(cache_key)
|
||||||
|
metrics.pruned_hard_collision += 1
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
context.static_safe_cache.add(cache_key)
|
||||||
|
|
||||||
|
total_overlaps = 0
|
||||||
|
if not skip_congestion:
|
||||||
|
if cache_key in congestion_cache:
|
||||||
|
total_overlaps = congestion_cache[cache_key]
|
||||||
|
else:
|
||||||
|
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
||||||
|
congestion_cache[cache_key] = total_overlaps
|
||||||
|
|
||||||
|
# SELF-COLLISION CHECK (Optional for performance)
|
||||||
|
if self_collision_check:
|
||||||
|
curr_p = parent
|
||||||
|
new_tb = result.total_bounds
|
||||||
|
while curr_p and curr_p.parent:
|
||||||
|
ancestor_res = curr_p.component_result
|
||||||
|
if ancestor_res:
|
||||||
|
anc_tb = ancestor_res.total_bounds
|
||||||
|
if (new_tb[0] < anc_tb[2] and new_tb[2] > anc_tb[0] and
|
||||||
|
new_tb[1] < anc_tb[3] and new_tb[3] > anc_tb[1]):
|
||||||
|
for p_anc in ancestor_res.geometry:
|
||||||
|
for p_new in result.geometry:
|
||||||
|
if p_new.intersects(p_anc) and not p_new.touches(p_anc):
|
||||||
|
return
|
||||||
|
curr_p = curr_p.parent
|
||||||
|
|
||||||
|
penalty = 0.0
|
||||||
|
if 'SB' in move_type: penalty = context.config.sbend_penalty
|
||||||
|
elif 'B' in move_type: penalty = context.config.bend_penalty
|
||||||
|
if move_radius is not None and move_radius > TOLERANCE_LINEAR: penalty *= (10.0 / move_radius)**0.5
|
||||||
|
|
||||||
|
move_cost = context.cost_evaluator.evaluate_move(
|
||||||
|
None, result.end_port, net_width, net_id,
|
||||||
|
start_port=parent_p, length=result.length,
|
||||||
|
dilated_geometry=None, penalty=penalty,
|
||||||
|
skip_static=True, skip_congestion=True # Congestion overlaps already calculated
|
||||||
|
)
|
||||||
|
move_cost += total_overlaps * context.cost_evaluator.congestion_penalty
|
||||||
|
|
||||||
|
if move_cost > 1e12:
|
||||||
|
metrics.pruned_cost += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
g_cost = parent.g_cost + move_cost
|
||||||
|
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
||||||
|
metrics.pruned_closed_set += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
h_cost = context.cost_evaluator.h_manhattan(result.end_port, target)
|
||||||
|
heapq.heappush(open_set, AStarNode(result.end_port, g_cost, h_cost, parent, result))
|
||||||
|
metrics.moves_added += 1
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_path(end_node: AStarNode) -> list[ComponentResult]:
|
||||||
|
""" Trace back from end node to start node to get the path. """
|
||||||
|
path = []
|
||||||
|
curr: AStarNode | None = end_node
|
||||||
|
while curr and curr.component_result:
|
||||||
|
path.append(curr.component_result)
|
||||||
|
curr = curr.parent
|
||||||
|
return path[::-1]
|
||||||
43
inire/router/config.py
Normal file
43
inire/router/config.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Literal, Any
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RouterConfig:
|
||||||
|
"""Configuration parameters for the A* Router."""
|
||||||
|
|
||||||
|
node_limit: int = 1000000
|
||||||
|
snap_size: float = 5.0
|
||||||
|
# Sparse Sampling Configuration
|
||||||
|
max_straight_length: float = 2000.0
|
||||||
|
num_straight_samples: int = 5
|
||||||
|
min_straight_length: float = 5.0
|
||||||
|
|
||||||
|
# Offsets for SBends (None = automatic grid-based selection)
|
||||||
|
sbend_offsets: list[float] | None = None
|
||||||
|
|
||||||
|
# Deprecated but kept for compatibility during refactor
|
||||||
|
straight_lengths: list[float] = field(default_factory=list)
|
||||||
|
|
||||||
|
bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0])
|
||||||
|
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||||
|
snap_to_target_dist: float = 1000.0
|
||||||
|
bend_penalty: float = 250.0
|
||||||
|
sbend_penalty: float = 500.0
|
||||||
|
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
||||||
|
bend_clip_margin: float = 10.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CostConfig:
|
||||||
|
"""Configuration parameters for the Cost Evaluator."""
|
||||||
|
|
||||||
|
unit_length_cost: float = 1.0
|
||||||
|
greedy_h_weight: float = 1.5
|
||||||
|
congestion_penalty: float = 10000.0
|
||||||
|
bend_penalty: float = 250.0
|
||||||
|
sbend_penalty: float = 500.0
|
||||||
|
min_bend_radius: float = 50.0
|
||||||
254
inire/router/cost.py
Normal file
254
inire/router/cost.py
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from inire.router.config import CostConfig
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
|
||||||
|
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 path and proximity costs.
|
||||||
|
"""
|
||||||
|
__slots__ = ('collision_engine', 'danger_map', 'config', 'unit_length_cost', 'greedy_h_weight', 'congestion_penalty',
|
||||||
|
'_target_x', '_target_y', '_target_ori', '_target_cos', '_target_sin', '_min_radius')
|
||||||
|
|
||||||
|
collision_engine: CollisionEngine
|
||||||
|
""" The engine for intersection checks """
|
||||||
|
|
||||||
|
danger_map: DangerMap
|
||||||
|
""" Pre-computed grid for heuristic proximity costs """
|
||||||
|
|
||||||
|
config: Any
|
||||||
|
""" Parameter configuration (CostConfig or RouterConfig) """
|
||||||
|
|
||||||
|
unit_length_cost: float
|
||||||
|
greedy_h_weight: float
|
||||||
|
congestion_penalty: float
|
||||||
|
""" Cached weight values for performance """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
collision_engine: CollisionEngine,
|
||||||
|
danger_map: DangerMap | None = None,
|
||||||
|
unit_length_cost: float = 1.0,
|
||||||
|
greedy_h_weight: float = 1.5,
|
||||||
|
congestion_penalty: float = 10000.0,
|
||||||
|
bend_penalty: float = 250.0,
|
||||||
|
sbend_penalty: float = 500.0,
|
||||||
|
min_bend_radius: float = 50.0,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the Cost Evaluator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collision_engine: The engine for intersection checks.
|
||||||
|
danger_map: Pre-computed grid for heuristic proximity costs.
|
||||||
|
unit_length_cost: Cost multiplier per micrometer of path length.
|
||||||
|
greedy_h_weight: Heuristic weighting (A* greedy factor).
|
||||||
|
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
|
||||||
|
bend_penalty: Base cost for 90-degree bends.
|
||||||
|
sbend_penalty: Base cost for parametric S-bends.
|
||||||
|
min_bend_radius: Minimum radius for 90-degree bends (used for alignment heuristic).
|
||||||
|
"""
|
||||||
|
self.collision_engine = collision_engine
|
||||||
|
self.danger_map = danger_map
|
||||||
|
self.config = CostConfig(
|
||||||
|
unit_length_cost=unit_length_cost,
|
||||||
|
greedy_h_weight=greedy_h_weight,
|
||||||
|
congestion_penalty=congestion_penalty,
|
||||||
|
bend_penalty=bend_penalty,
|
||||||
|
sbend_penalty=sbend_penalty,
|
||||||
|
min_bend_radius=min_bend_radius,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use config values
|
||||||
|
self.unit_length_cost = self.config.unit_length_cost
|
||||||
|
self.greedy_h_weight = self.config.greedy_h_weight
|
||||||
|
self.congestion_penalty = self.config.congestion_penalty
|
||||||
|
|
||||||
|
# Pre-cache configuration flags for fast path
|
||||||
|
self._refresh_cached_config()
|
||||||
|
|
||||||
|
# Target cache
|
||||||
|
self._target_x = 0.0
|
||||||
|
self._target_y = 0.0
|
||||||
|
self._target_ori = 0.0
|
||||||
|
self._target_cos = 1.0
|
||||||
|
self._target_sin = 0.0
|
||||||
|
|
||||||
|
def _refresh_cached_config(self) -> None:
|
||||||
|
""" Sync internal caches with the current self.config object. """
|
||||||
|
if hasattr(self.config, 'min_bend_radius'):
|
||||||
|
self._min_radius = self.config.min_bend_radius
|
||||||
|
elif hasattr(self.config, 'bend_radii') and self.config.bend_radii:
|
||||||
|
self._min_radius = min(self.config.bend_radii)
|
||||||
|
else:
|
||||||
|
self._min_radius = 50.0
|
||||||
|
|
||||||
|
if hasattr(self.config, 'unit_length_cost'):
|
||||||
|
self.unit_length_cost = self.config.unit_length_cost
|
||||||
|
if hasattr(self.config, 'greedy_h_weight'):
|
||||||
|
self.greedy_h_weight = self.config.greedy_h_weight
|
||||||
|
if hasattr(self.config, 'congestion_penalty'):
|
||||||
|
self.congestion_penalty = self.config.congestion_penalty
|
||||||
|
|
||||||
|
def set_target(self, target: Port) -> None:
|
||||||
|
""" Pre-calculate target-dependent values for faster heuristic. """
|
||||||
|
self._target_x = target.x
|
||||||
|
self._target_y = target.y
|
||||||
|
self._target_ori = target.orientation
|
||||||
|
rad = np.radians(target.orientation)
|
||||||
|
self._target_cos = np.cos(rad)
|
||||||
|
self._target_sin = np.sin(rad)
|
||||||
|
|
||||||
|
def g_proximity(self, x: float, y: float) -> float:
|
||||||
|
"""
|
||||||
|
Get proximity cost from the Danger Map.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x, y: Coordinate to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Proximity cost at location.
|
||||||
|
"""
|
||||||
|
if self.danger_map is None:
|
||||||
|
return 0.0
|
||||||
|
return self.danger_map.get_cost(x, y)
|
||||||
|
|
||||||
|
def h_manhattan(self, current: Port, target: Port) -> float:
|
||||||
|
"""
|
||||||
|
Heuristic: weighted Manhattan distance + mandatory turn penalties.
|
||||||
|
"""
|
||||||
|
tx, ty = target.x, target.y
|
||||||
|
|
||||||
|
# Avoid repeated trig for target orientation
|
||||||
|
if (abs(tx - self._target_x) > TOLERANCE_LINEAR or
|
||||||
|
abs(ty - self._target_y) > TOLERANCE_LINEAR or
|
||||||
|
abs(target.orientation - self._target_ori) > 0.1):
|
||||||
|
self.set_target(target)
|
||||||
|
|
||||||
|
dx = abs(current.x - tx)
|
||||||
|
dy = abs(current.y - ty)
|
||||||
|
dist = dx + dy
|
||||||
|
|
||||||
|
bp = self.config.bend_penalty
|
||||||
|
penalty = 0.0
|
||||||
|
|
||||||
|
# 1. Orientation Difference
|
||||||
|
curr_ori = current.orientation
|
||||||
|
diff = abs(curr_ori - self._target_ori) % 360
|
||||||
|
if diff > 0.1:
|
||||||
|
if abs(diff - 180) < 0.1:
|
||||||
|
penalty += 2 * bp
|
||||||
|
else: # 90 or 270 degree rotation
|
||||||
|
penalty += 1 * bp
|
||||||
|
|
||||||
|
# 2. Side Check (Entry half-plane)
|
||||||
|
v_dx = tx - current.x
|
||||||
|
v_dy = ty - current.y
|
||||||
|
side_proj = v_dx * self._target_cos + v_dy * self._target_sin
|
||||||
|
perp_dist = abs(v_dx * self._target_sin - v_dy * self._target_cos)
|
||||||
|
|
||||||
|
if side_proj < -0.1 or (side_proj < self._min_radius and perp_dist > 0.1):
|
||||||
|
penalty += 2 * bp
|
||||||
|
|
||||||
|
# 3. Traveling Away
|
||||||
|
# Optimization: avoid np.radians/cos/sin if current_ori is standard 0,90,180,270
|
||||||
|
if curr_ori == 0: c_cos, c_sin = 1.0, 0.0
|
||||||
|
elif curr_ori == 90: c_cos, c_sin = 0.0, 1.0
|
||||||
|
elif curr_ori == 180: c_cos, c_sin = -1.0, 0.0
|
||||||
|
elif curr_ori == 270: c_cos, c_sin = 0.0, -1.0
|
||||||
|
else:
|
||||||
|
curr_rad = np.radians(curr_ori)
|
||||||
|
c_cos, c_sin = np.cos(curr_rad), np.sin(curr_rad)
|
||||||
|
|
||||||
|
move_proj = v_dx * c_cos + v_dy * c_sin
|
||||||
|
if move_proj < -0.1:
|
||||||
|
penalty += 2 * bp
|
||||||
|
|
||||||
|
# 4. Jog Alignment
|
||||||
|
if diff < 0.1:
|
||||||
|
if perp_dist > 0.1:
|
||||||
|
penalty += 2 * bp
|
||||||
|
|
||||||
|
return self.greedy_h_weight * (dist + penalty)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_move(
|
||||||
|
self,
|
||||||
|
geometry: list[Polygon] | None,
|
||||||
|
end_port: Port,
|
||||||
|
net_width: float,
|
||||||
|
net_id: str,
|
||||||
|
start_port: Port | None = None,
|
||||||
|
length: float = 0.0,
|
||||||
|
dilated_geometry: list[Polygon] | None = None,
|
||||||
|
skip_static: bool = False,
|
||||||
|
skip_congestion: bool = False,
|
||||||
|
penalty: float = 0.0,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate the cost of a single move (Straight, Bend, SBend).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geometry: List of polygons in the move.
|
||||||
|
end_port: Port at the end of the move.
|
||||||
|
net_width: Width of the waveguide (unused).
|
||||||
|
net_id: Identifier for the net.
|
||||||
|
start_port: Port at the start of the move.
|
||||||
|
length: Physical path length of the move.
|
||||||
|
dilated_geometry: Pre-calculated dilated polygons.
|
||||||
|
skip_static: If True, bypass static collision checks.
|
||||||
|
skip_congestion: If True, bypass congestion checks.
|
||||||
|
penalty: Fixed cost penalty for the move type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Total cost of the move, or 1e15 if invalid.
|
||||||
|
"""
|
||||||
|
_ = net_width # Unused
|
||||||
|
|
||||||
|
# 1. Boundary Check
|
||||||
|
danger_map = self.danger_map
|
||||||
|
if danger_map is not None:
|
||||||
|
if not danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||||
|
return 1e15
|
||||||
|
|
||||||
|
total_cost = length * self.unit_length_cost + penalty
|
||||||
|
|
||||||
|
# 2. Collision Check
|
||||||
|
if not skip_static or not skip_congestion:
|
||||||
|
collision_engine = self.collision_engine
|
||||||
|
# Ensure geometry is provided if collision checks are enabled
|
||||||
|
if geometry is None:
|
||||||
|
return 1e15
|
||||||
|
for i, poly in enumerate(geometry):
|
||||||
|
dil_poly = dilated_geometry[i] if dilated_geometry else None
|
||||||
|
# Hard Collision (Static obstacles)
|
||||||
|
if not skip_static:
|
||||||
|
if collision_engine.check_collision(
|
||||||
|
poly, net_id, buffer_mode='static', start_port=start_port, end_port=end_port,
|
||||||
|
dilated_geometry=dil_poly
|
||||||
|
):
|
||||||
|
return 1e15
|
||||||
|
|
||||||
|
# Soft Collision (Negotiated Congestion)
|
||||||
|
if not skip_congestion:
|
||||||
|
overlaps = collision_engine.check_collision(
|
||||||
|
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
|
||||||
|
)
|
||||||
|
if isinstance(overlaps, int) and overlaps > 0:
|
||||||
|
total_cost += overlaps * self.congestion_penalty
|
||||||
|
|
||||||
|
# 3. Proximity cost from Danger Map
|
||||||
|
if danger_map is not None:
|
||||||
|
total_cost += danger_map.get_cost(end_port.x, end_port.y)
|
||||||
|
return total_cost
|
||||||
134
inire/router/danger_map.py
Normal file
134
inire/router/danger_map.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import numpy
|
||||||
|
import shapely
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
|
||||||
|
class DangerMap:
|
||||||
|
"""
|
||||||
|
A pre-computed grid for heuristic proximity costs, vectorized for performance.
|
||||||
|
"""
|
||||||
|
__slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'width_cells', 'height_cells', 'grid')
|
||||||
|
|
||||||
|
minx: float
|
||||||
|
miny: float
|
||||||
|
maxx: float
|
||||||
|
maxy: float
|
||||||
|
""" Boundary coordinates of the map """
|
||||||
|
|
||||||
|
resolution: float
|
||||||
|
""" Grid cell size in micrometers """
|
||||||
|
|
||||||
|
safety_threshold: float
|
||||||
|
""" Distance below which proximity costs are applied """
|
||||||
|
|
||||||
|
k: float
|
||||||
|
""" Cost multiplier constant """
|
||||||
|
|
||||||
|
width_cells: int
|
||||||
|
height_cells: int
|
||||||
|
""" Grid dimensions in cells """
|
||||||
|
|
||||||
|
grid: numpy.ndarray
|
||||||
|
""" 2D array of pre-computed costs """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
resolution: float = 1.0,
|
||||||
|
safety_threshold: float = 10.0,
|
||||||
|
k: float = 1.0,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the Danger Map.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bounds: (minx, miny, maxx, maxy) in um.
|
||||||
|
resolution: Cell size (um).
|
||||||
|
safety_threshold: Proximity limit (um).
|
||||||
|
k: Penalty multiplier.
|
||||||
|
"""
|
||||||
|
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(numpy.ceil((self.maxx - self.minx) / self.resolution))
|
||||||
|
self.height_cells = int(numpy.ceil((self.maxy - self.miny) / self.resolution))
|
||||||
|
|
||||||
|
self.grid = numpy.zeros((self.width_cells, self.height_cells), dtype=numpy.float32)
|
||||||
|
|
||||||
|
def precompute(self, obstacles: list[Polygon]) -> None:
|
||||||
|
"""
|
||||||
|
Pre-compute the proximity costs for the entire grid using vectorized operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obstacles: List of static obstacle geometries.
|
||||||
|
"""
|
||||||
|
from scipy.ndimage import distance_transform_edt
|
||||||
|
|
||||||
|
# 1. Create a binary mask of obstacles
|
||||||
|
mask = numpy.ones((self.width_cells, self.height_cells), dtype=bool)
|
||||||
|
|
||||||
|
# Create coordinate grids
|
||||||
|
x_coords = numpy.linspace(self.minx + self.resolution/2, self.maxx - self.resolution/2, self.width_cells)
|
||||||
|
y_coords = numpy.linspace(self.miny + self.resolution/2, self.maxy - self.resolution/2, self.height_cells)
|
||||||
|
xv, yv = numpy.meshgrid(x_coords, y_coords, indexing='ij')
|
||||||
|
|
||||||
|
for poly in obstacles:
|
||||||
|
# Use shapely.contains_xy for fast vectorized point-in-polygon check
|
||||||
|
in_poly = shapely.contains_xy(poly, xv, yv)
|
||||||
|
mask[in_poly] = False
|
||||||
|
|
||||||
|
# 2. Distance transform (mask=True for empty space)
|
||||||
|
distances = distance_transform_edt(mask) * self.resolution
|
||||||
|
|
||||||
|
# 3. Proximity cost: k / d^2 if d < threshold, else 0
|
||||||
|
# Cap distances at a small epsilon (e.g. 0.1um) to avoid division by zero
|
||||||
|
safe_distances = numpy.maximum(distances, 0.1)
|
||||||
|
self.grid = numpy.where(
|
||||||
|
distances < self.safety_threshold,
|
||||||
|
self.k / (safe_distances**2),
|
||||||
|
0.0
|
||||||
|
).astype(numpy.float32)
|
||||||
|
|
||||||
|
def is_within_bounds(self, x: float, y: float) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a coordinate is within the design bounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x, y: Coordinate to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if within [min, max] for both axes.
|
||||||
|
"""
|
||||||
|
return self.minx <= x <= self.maxx and self.miny <= y <= self.maxy
|
||||||
|
|
||||||
|
def get_cost(self, x: float, y: float) -> float:
|
||||||
|
"""
|
||||||
|
Get the proximity cost at a specific coordinate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x, y: Coordinate to look up.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pre-computed cost, or 1e15 if out of bounds.
|
||||||
|
"""
|
||||||
|
# Clamp to grid range to handle upper boundary exactly
|
||||||
|
ix = int((x - self.minx) / self.resolution)
|
||||||
|
iy = int((y - self.miny) / self.resolution)
|
||||||
|
|
||||||
|
# Handle exact upper boundary
|
||||||
|
if ix == self.width_cells and abs(x - self.maxx) < 1e-9:
|
||||||
|
ix = self.width_cells - 1
|
||||||
|
if iy == self.height_cells and abs(y - self.maxy) < 1e-9:
|
||||||
|
iy = self.height_cells - 1
|
||||||
|
|
||||||
|
if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells:
|
||||||
|
return float(self.grid[ix, iy])
|
||||||
|
return 1e15 # Outside bounds
|
||||||
440
inire/router/pathfinder.py
Normal file
440
inire/router/pathfinder.py
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Callable, Literal, Any
|
||||||
|
|
||||||
|
from inire.router.astar import route_astar, AStarMetrics
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutingResult:
|
||||||
|
"""
|
||||||
|
Result of a single net routing operation.
|
||||||
|
"""
|
||||||
|
net_id: str
|
||||||
|
""" Identifier for the net """
|
||||||
|
|
||||||
|
path: list[ComponentResult]
|
||||||
|
""" List of moves forming the path """
|
||||||
|
|
||||||
|
is_valid: bool
|
||||||
|
""" Whether the path is collision-free and reached the target """
|
||||||
|
|
||||||
|
collisions: int
|
||||||
|
""" Number of detected collisions/overlaps """
|
||||||
|
|
||||||
|
reached_target: bool = False
|
||||||
|
""" Whether the final port matches the target port """
|
||||||
|
|
||||||
|
|
||||||
|
class PathFinder:
|
||||||
|
"""
|
||||||
|
Multi-net router using Negotiated Congestion.
|
||||||
|
"""
|
||||||
|
__slots__ = ('context', 'metrics', 'max_iterations', 'base_congestion_penalty',
|
||||||
|
'use_tiered_strategy', 'congestion_multiplier', 'accumulated_expanded_nodes', 'warm_start')
|
||||||
|
|
||||||
|
context: AStarContext
|
||||||
|
""" The A* persistent state (config, caches, evaluator) """
|
||||||
|
|
||||||
|
metrics: AStarMetrics
|
||||||
|
""" Performance metrics for search operations """
|
||||||
|
|
||||||
|
max_iterations: int
|
||||||
|
""" Maximum number of rip-up and reroute iterations """
|
||||||
|
|
||||||
|
base_congestion_penalty: float
|
||||||
|
""" Starting penalty for overlaps """
|
||||||
|
|
||||||
|
congestion_multiplier: float
|
||||||
|
""" Multiplier for congestion penalty per iteration """
|
||||||
|
|
||||||
|
use_tiered_strategy: bool
|
||||||
|
""" If True, use simpler collision models in early iterations for speed """
|
||||||
|
|
||||||
|
warm_start: Literal['shortest', 'longest', 'user'] | None
|
||||||
|
""" Heuristic sorting for the initial greedy pass """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: AStarContext,
|
||||||
|
metrics: AStarMetrics | None = None,
|
||||||
|
max_iterations: int = 10,
|
||||||
|
base_congestion_penalty: float = 100.0,
|
||||||
|
congestion_multiplier: float = 1.5,
|
||||||
|
use_tiered_strategy: bool = True,
|
||||||
|
warm_start: Literal['shortest', 'longest', 'user'] | None = 'shortest',
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the PathFinder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The A* search context (evaluator, config, caches).
|
||||||
|
metrics: Optional metrics container.
|
||||||
|
max_iterations: Maximum number of rip-up and reroute iterations.
|
||||||
|
base_congestion_penalty: Starting penalty for overlaps.
|
||||||
|
congestion_multiplier: Multiplier for congestion penalty per iteration.
|
||||||
|
use_tiered_strategy: Whether to use simplified collision models in early iterations.
|
||||||
|
warm_start: Initial ordering strategy for a fast greedy pass.
|
||||||
|
"""
|
||||||
|
self.context = context
|
||||||
|
self.metrics = metrics if metrics is not None else AStarMetrics()
|
||||||
|
self.max_iterations = max_iterations
|
||||||
|
self.base_congestion_penalty = base_congestion_penalty
|
||||||
|
self.congestion_multiplier = congestion_multiplier
|
||||||
|
self.use_tiered_strategy = use_tiered_strategy
|
||||||
|
self.warm_start = warm_start
|
||||||
|
self.accumulated_expanded_nodes: list[tuple[float, float, float]] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost_evaluator(self) -> CostEvaluator:
|
||||||
|
return self.context.cost_evaluator
|
||||||
|
|
||||||
|
def _perform_greedy_pass(
|
||||||
|
self,
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
net_widths: dict[str, float],
|
||||||
|
order: Literal['shortest', 'longest', 'user']
|
||||||
|
) -> dict[str, list[ComponentResult]]:
|
||||||
|
"""
|
||||||
|
Internal greedy pass: route nets sequentially and freeze them as static.
|
||||||
|
"""
|
||||||
|
all_net_ids = list(netlist.keys())
|
||||||
|
if order != 'user':
|
||||||
|
def get_dist(nid):
|
||||||
|
s, t = netlist[nid]
|
||||||
|
return abs(t.x - s.x) + abs(t.y - s.y)
|
||||||
|
all_net_ids.sort(key=get_dist, reverse=(order == 'longest'))
|
||||||
|
|
||||||
|
greedy_paths = {}
|
||||||
|
temp_obj_ids = []
|
||||||
|
|
||||||
|
logger.info(f"PathFinder: Starting Greedy Warm-Start ({order} order)...")
|
||||||
|
|
||||||
|
for net_id in all_net_ids:
|
||||||
|
start, target = netlist[net_id]
|
||||||
|
width = net_widths.get(net_id, 2.0)
|
||||||
|
|
||||||
|
# Heuristic max cost for fail-fast
|
||||||
|
h_start = self.cost_evaluator.h_manhattan(start, target)
|
||||||
|
max_cost_limit = max(h_start * 3.0, 2000.0)
|
||||||
|
|
||||||
|
path = route_astar(
|
||||||
|
start, target, width, context=self.context, metrics=self.metrics,
|
||||||
|
net_id=net_id, skip_congestion=True, max_cost=max_cost_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
if path:
|
||||||
|
greedy_paths[net_id] = path
|
||||||
|
# Freeze as static
|
||||||
|
for res in path:
|
||||||
|
geoms = res.actual_geometry if res.actual_geometry is not None else res.geometry
|
||||||
|
for poly in geoms:
|
||||||
|
obj_id = self.cost_evaluator.collision_engine.add_static_obstacle(poly)
|
||||||
|
temp_obj_ids.append(obj_id)
|
||||||
|
|
||||||
|
# Clean up temporary static obstacles
|
||||||
|
for obj_id in temp_obj_ids:
|
||||||
|
self.cost_evaluator.collision_engine.remove_static_obstacle(obj_id)
|
||||||
|
|
||||||
|
logger.info(f"PathFinder: Greedy Warm-Start finished. Seeding {len(greedy_paths)}/{len(netlist)} nets.")
|
||||||
|
return greedy_paths
|
||||||
|
|
||||||
|
def _has_self_collision(self, path: list[ComponentResult]) -> bool:
|
||||||
|
"""
|
||||||
|
Quickly check if a path intersects itself.
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
num_components = len(path)
|
||||||
|
for i in range(num_components):
|
||||||
|
comp_i = path[i]
|
||||||
|
tb_i = comp_i.total_bounds
|
||||||
|
for j in range(i + 2, num_components): # Skip immediate neighbors
|
||||||
|
comp_j = path[j]
|
||||||
|
tb_j = comp_j.total_bounds
|
||||||
|
|
||||||
|
# AABB Check
|
||||||
|
if (tb_i[0] < tb_j[2] and tb_i[2] > tb_j[0] and
|
||||||
|
tb_i[1] < tb_j[3] and tb_i[3] > tb_j[1]):
|
||||||
|
# Real geometry check
|
||||||
|
for p_i in comp_i.geometry:
|
||||||
|
for p_j in comp_j.geometry:
|
||||||
|
if p_i.intersects(p_j) and not p_i.touches(p_j):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def route_all(
|
||||||
|
self,
|
||||||
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
net_widths: dict[str, float],
|
||||||
|
store_expanded: bool = False,
|
||||||
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
|
||||||
|
shuffle_nets: bool = False,
|
||||||
|
sort_nets: Literal['shortest', 'longest', 'user', None] = None,
|
||||||
|
initial_paths: dict[str, list[ComponentResult]] | None = None,
|
||||||
|
seed: int | None = None,
|
||||||
|
) -> dict[str, RoutingResult]:
|
||||||
|
"""
|
||||||
|
Route all nets in the netlist using Negotiated Congestion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
netlist: Mapping of net_id to (start_port, target_port).
|
||||||
|
net_widths: Mapping of net_id to waveguide width.
|
||||||
|
store_expanded: Whether to store expanded nodes for ALL iterations and nets.
|
||||||
|
iteration_callback: Optional callback(iteration_idx, current_results).
|
||||||
|
shuffle_nets: Whether to randomize the order of nets each iteration.
|
||||||
|
sort_nets: Heuristic sorting for the initial iteration order (overrides self.warm_start).
|
||||||
|
initial_paths: Pre-computed paths to use for Iteration 0 (overrides warm_start).
|
||||||
|
seed: Optional seed for randomization (enables reproducibility).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping of net_id to RoutingResult.
|
||||||
|
"""
|
||||||
|
results: dict[str, RoutingResult] = {}
|
||||||
|
self.cost_evaluator.congestion_penalty = self.base_congestion_penalty
|
||||||
|
self.accumulated_expanded_nodes = []
|
||||||
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
num_nets = len(netlist)
|
||||||
|
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
||||||
|
|
||||||
|
all_net_ids = list(netlist.keys())
|
||||||
|
needs_sc = set() # Nets requiring self-collision avoidance
|
||||||
|
|
||||||
|
# Determine initial paths (Warm Start)
|
||||||
|
if initial_paths is None:
|
||||||
|
ws_order = sort_nets if sort_nets is not None else self.warm_start
|
||||||
|
if ws_order is not None:
|
||||||
|
initial_paths = self._perform_greedy_pass(netlist, net_widths, ws_order)
|
||||||
|
self.context.clear_static_caches()
|
||||||
|
|
||||||
|
# Apply initial sorting heuristic if requested (for the main NC loop)
|
||||||
|
if sort_nets:
|
||||||
|
def get_dist(nid):
|
||||||
|
s, t = netlist[nid]
|
||||||
|
return abs(t.x - s.x) + abs(t.y - s.y)
|
||||||
|
|
||||||
|
if sort_nets != 'user':
|
||||||
|
all_net_ids.sort(key=get_dist, reverse=(sort_nets == 'longest'))
|
||||||
|
|
||||||
|
for iteration in range(self.max_iterations):
|
||||||
|
any_congestion = False
|
||||||
|
# Clear accumulation for this iteration so callback gets fresh data
|
||||||
|
self.accumulated_expanded_nodes = []
|
||||||
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
|
logger.info(f'PathFinder Iteration {iteration}...')
|
||||||
|
|
||||||
|
# 0. Shuffle nets if requested
|
||||||
|
if shuffle_nets:
|
||||||
|
# Use a new seed based on iteration for deterministic different orders
|
||||||
|
it_seed = (seed + iteration) if seed is not None else None
|
||||||
|
random.Random(it_seed).shuffle(all_net_ids)
|
||||||
|
|
||||||
|
# Sequence through nets
|
||||||
|
for net_id in all_net_ids:
|
||||||
|
start, target = netlist[net_id]
|
||||||
|
# Timeout check
|
||||||
|
elapsed = time.monotonic() - start_time
|
||||||
|
if elapsed > session_timeout:
|
||||||
|
logger.warning(f'PathFinder TIMEOUT after {elapsed:.2f}s')
|
||||||
|
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 or Use Initial Path
|
||||||
|
path = None
|
||||||
|
|
||||||
|
# Warm Start Logic: Use provided path for Iteration 0
|
||||||
|
if iteration == 0 and initial_paths and net_id in initial_paths:
|
||||||
|
path = initial_paths[net_id]
|
||||||
|
logger.debug(f' Net {net_id} used Warm Start path.')
|
||||||
|
else:
|
||||||
|
# Standard Routing Logic
|
||||||
|
target_coll_model = self.context.config.bend_collision_type
|
||||||
|
coll_model = target_coll_model
|
||||||
|
skip_cong = False
|
||||||
|
if self.use_tiered_strategy and iteration == 0:
|
||||||
|
skip_cong = True
|
||||||
|
if target_coll_model == "arc":
|
||||||
|
coll_model = "clipped_bbox"
|
||||||
|
|
||||||
|
base_node_limit = self.context.config.node_limit
|
||||||
|
current_node_limit = base_node_limit
|
||||||
|
|
||||||
|
net_start = time.monotonic()
|
||||||
|
|
||||||
|
path = route_astar(
|
||||||
|
start, target, width, context=self.context, metrics=self.metrics,
|
||||||
|
net_id=net_id, bend_collision_type=coll_model, return_partial=True,
|
||||||
|
store_expanded=store_expanded, skip_congestion=skip_cong,
|
||||||
|
self_collision_check=(net_id in needs_sc),
|
||||||
|
node_limit=current_node_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
if store_expanded and self.metrics.last_expanded_nodes:
|
||||||
|
self.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
|
||||||
|
|
||||||
|
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
|
||||||
|
|
||||||
|
if path:
|
||||||
|
# Check if reached exactly (relative to snapped target)
|
||||||
|
last_p = path[-1].end_port
|
||||||
|
snap = self.context.config.snap_size
|
||||||
|
from inire.geometry.components import snap_search_grid
|
||||||
|
reached = (abs(last_p.x - snap_search_grid(target.x, snap)) < TOLERANCE_LINEAR and
|
||||||
|
abs(last_p.y - snap_search_grid(target.y, snap)) < TOLERANCE_LINEAR and
|
||||||
|
abs(last_p.orientation - target.orientation) < 0.1)
|
||||||
|
|
||||||
|
# Check for self-collision if not already handled by router
|
||||||
|
if reached and net_id not in needs_sc:
|
||||||
|
if self._has_self_collision(path):
|
||||||
|
logger.info(f' Net {net_id} detected self-collision. Enabling protection for next iteration.')
|
||||||
|
needs_sc.add(net_id)
|
||||||
|
any_congestion = True
|
||||||
|
|
||||||
|
# 3. Add to index (even if partial) so other nets negotiate around it
|
||||||
|
all_geoms = []
|
||||||
|
all_dilated = []
|
||||||
|
for res in path:
|
||||||
|
all_geoms.extend(res.geometry)
|
||||||
|
if res.dilated_geometry:
|
||||||
|
all_dilated.extend(res.dilated_geometry)
|
||||||
|
else:
|
||||||
|
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
all_dilated.extend([p.buffer(dilation) for p in res.geometry])
|
||||||
|
|
||||||
|
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||||
|
|
||||||
|
# Check if this new path has any congestion
|
||||||
|
collision_count = 0
|
||||||
|
if reached:
|
||||||
|
verif_geoms = []
|
||||||
|
verif_dilated = []
|
||||||
|
for res in path:
|
||||||
|
is_proxy = (res.actual_geometry is not None)
|
||||||
|
g = res.actual_geometry if is_proxy else res.geometry
|
||||||
|
verif_geoms.extend(g)
|
||||||
|
|
||||||
|
if is_proxy:
|
||||||
|
if res.dilated_actual_geometry:
|
||||||
|
verif_dilated.extend(res.dilated_actual_geometry)
|
||||||
|
else:
|
||||||
|
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
verif_dilated.extend([p.buffer(dilation) for p in g])
|
||||||
|
else:
|
||||||
|
if res.dilated_geometry:
|
||||||
|
verif_dilated.extend(res.dilated_geometry)
|
||||||
|
else:
|
||||||
|
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
verif_dilated.extend([p.buffer(dilation) for p in g])
|
||||||
|
|
||||||
|
self.cost_evaluator.collision_engine._ensure_dynamic_tree()
|
||||||
|
if self.cost_evaluator.collision_engine.dynamic_tree:
|
||||||
|
# Vectorized query for all polygons in the path
|
||||||
|
res_indices, tree_indices = self.cost_evaluator.collision_engine.dynamic_tree.query(verif_dilated, predicate='intersects')
|
||||||
|
for hit_idx in tree_indices:
|
||||||
|
obj_id = self.cost_evaluator.collision_engine.dynamic_obj_ids[hit_idx]
|
||||||
|
other_net_id, _ = self.cost_evaluator.collision_engine.dynamic_geometries[obj_id]
|
||||||
|
if other_net_id != net_id:
|
||||||
|
collision_count += 1
|
||||||
|
|
||||||
|
if collision_count > 0:
|
||||||
|
any_congestion = True
|
||||||
|
|
||||||
|
logger.debug(f' Net {net_id}: reached={reached}, collisions={collision_count}')
|
||||||
|
results[net_id] = RoutingResult(net_id, path, (collision_count == 0 and reached), collision_count, reached_target=reached)
|
||||||
|
else:
|
||||||
|
results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False)
|
||||||
|
any_congestion = True # Total failure might need a retry with different ordering
|
||||||
|
|
||||||
|
if iteration_callback:
|
||||||
|
iteration_callback(iteration, results)
|
||||||
|
|
||||||
|
if not any_congestion:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.cost_evaluator.congestion_penalty *= self.congestion_multiplier
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
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
|
||||||
|
|
||||||
|
if not res.reached_target:
|
||||||
|
# Skip re-verification for partial paths to avoid massive performance hit
|
||||||
|
final_results[net_id] = res
|
||||||
|
continue
|
||||||
|
|
||||||
|
collision_count = 0
|
||||||
|
verif_geoms = []
|
||||||
|
verif_dilated = []
|
||||||
|
for comp in res.path:
|
||||||
|
is_proxy = (comp.actual_geometry is not None)
|
||||||
|
g = comp.actual_geometry if is_proxy else comp.geometry
|
||||||
|
verif_geoms.extend(g)
|
||||||
|
if is_proxy:
|
||||||
|
if comp.dilated_actual_geometry:
|
||||||
|
verif_dilated.extend(comp.dilated_actual_geometry)
|
||||||
|
else:
|
||||||
|
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
verif_dilated.extend([p.buffer(dilation) for p in g])
|
||||||
|
else:
|
||||||
|
if comp.dilated_geometry:
|
||||||
|
verif_dilated.extend(comp.dilated_geometry)
|
||||||
|
else:
|
||||||
|
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
verif_dilated.extend([p.buffer(dilation) for p in g])
|
||||||
|
|
||||||
|
self.cost_evaluator.collision_engine._ensure_dynamic_tree()
|
||||||
|
if self.cost_evaluator.collision_engine.dynamic_tree:
|
||||||
|
# Vectorized query
|
||||||
|
res_indices, tree_indices = self.cost_evaluator.collision_engine.dynamic_tree.query(verif_dilated, predicate='intersects')
|
||||||
|
for hit_idx in tree_indices:
|
||||||
|
obj_id = self.cost_evaluator.collision_engine.dynamic_obj_ids[hit_idx]
|
||||||
|
other_net_id, _ = self.cost_evaluator.collision_engine.dynamic_geometries[obj_id]
|
||||||
|
if other_net_id != net_id:
|
||||||
|
collision_count += 1
|
||||||
|
|
||||||
|
target_p = netlist[net_id][1]
|
||||||
|
last_p = res.path[-1].end_port
|
||||||
|
snap = self.context.config.snap_size
|
||||||
|
from inire.geometry.components import snap_search_grid
|
||||||
|
reached = (abs(last_p.x - snap_search_grid(target_p.x, snap)) < TOLERANCE_LINEAR and
|
||||||
|
abs(last_p.y - snap_search_grid(target_p.y, snap)) < TOLERANCE_LINEAR and
|
||||||
|
abs(last_p.orientation - target_p.orientation) < 0.1)
|
||||||
|
|
||||||
|
final_results[net_id] = RoutingResult(net_id, res.path, (collision_count == 0 and reached), collision_count, reached_target=reached)
|
||||||
|
|
||||||
|
return final_results
|
||||||
125
inire/router/visibility.py
Normal file
125
inire/router/visibility.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import rtree
|
||||||
|
from shapely.geometry import Point, LineString
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
class VisibilityManager:
|
||||||
|
"""
|
||||||
|
Manages corners of static obstacles for sparse A* / Visibility Graph jumps.
|
||||||
|
"""
|
||||||
|
__slots__ = ('collision_engine', 'corners', 'corner_index', '_corner_graph', '_static_visibility_cache')
|
||||||
|
|
||||||
|
def __init__(self, collision_engine: CollisionEngine) -> None:
|
||||||
|
self.collision_engine = collision_engine
|
||||||
|
self.corners: list[tuple[float, float]] = []
|
||||||
|
self.corner_index = rtree.index.Index()
|
||||||
|
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
|
||||||
|
self._static_visibility_cache: dict[tuple[int, int], list[tuple[float, float, float]]] = {}
|
||||||
|
self._build()
|
||||||
|
|
||||||
|
def _build(self) -> None:
|
||||||
|
"""
|
||||||
|
Extract corners and pre-compute corner-to-corner visibility.
|
||||||
|
"""
|
||||||
|
raw_corners = []
|
||||||
|
for obj_id, poly in self.collision_engine.static_dilated.items():
|
||||||
|
coords = list(poly.exterior.coords)
|
||||||
|
if coords[0] == coords[-1]:
|
||||||
|
coords = coords[:-1]
|
||||||
|
raw_corners.extend(coords)
|
||||||
|
for ring in poly.interiors:
|
||||||
|
coords = list(ring.coords)
|
||||||
|
if coords[0] == coords[-1]:
|
||||||
|
coords = coords[:-1]
|
||||||
|
raw_corners.extend(coords)
|
||||||
|
|
||||||
|
if not raw_corners:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Deduplicate and snap to 1nm
|
||||||
|
seen = set()
|
||||||
|
for x, y in raw_corners:
|
||||||
|
sx, sy = round(x, 3), round(y, 3)
|
||||||
|
if (sx, sy) not in seen:
|
||||||
|
seen.add((sx, sy))
|
||||||
|
self.corners.append((sx, sy))
|
||||||
|
|
||||||
|
# Build spatial index for corners
|
||||||
|
for i, (x, y) in enumerate(self.corners):
|
||||||
|
self.corner_index.insert(i, (x, y, x, y))
|
||||||
|
|
||||||
|
# Pre-compute visibility graph between corners
|
||||||
|
num_corners = len(self.corners)
|
||||||
|
if num_corners > 200:
|
||||||
|
# Limit pre-computation if too many corners
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(num_corners):
|
||||||
|
self._corner_graph[i] = []
|
||||||
|
p1 = Port(self.corners[i][0], self.corners[i][1], 0)
|
||||||
|
for j in range(num_corners):
|
||||||
|
if i == j: continue
|
||||||
|
cx, cy = self.corners[j]
|
||||||
|
dx, dy = cx - p1.x, cy - p1.y
|
||||||
|
dist = numpy.sqrt(dx**2 + dy**2)
|
||||||
|
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||||
|
reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05)
|
||||||
|
if reach >= dist - 0.01:
|
||||||
|
self._corner_graph[i].append((cx, cy, dist))
|
||||||
|
|
||||||
|
def get_visible_corners(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
|
||||||
|
"""
|
||||||
|
Find all corners visible from the origin.
|
||||||
|
Returns list of (x, y, distance).
|
||||||
|
"""
|
||||||
|
if max_dist < 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ox, oy = round(origin.x, 3), round(origin.y, 3)
|
||||||
|
|
||||||
|
# 1. Exact corner check
|
||||||
|
# Use spatial index to find if origin is AT a corner
|
||||||
|
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
|
||||||
|
for idx in nearby:
|
||||||
|
cx, cy = self.corners[idx]
|
||||||
|
if abs(cx - ox) < 1e-4 and abs(cy - oy) < 1e-4:
|
||||||
|
# We are at a corner! Return pre-computed graph (filtered by max_dist)
|
||||||
|
if idx in self._corner_graph:
|
||||||
|
return [c for c in self._corner_graph[idx] if c[2] <= max_dist]
|
||||||
|
|
||||||
|
# 2. Cache check for arbitrary points
|
||||||
|
# Grid-based caching for arbitrary points is tricky,
|
||||||
|
# but since static obstacles don't change, we can cache exact coordinates.
|
||||||
|
cache_key = (int(ox * 1000), int(oy * 1000))
|
||||||
|
if cache_key in self._static_visibility_cache:
|
||||||
|
return self._static_visibility_cache[cache_key]
|
||||||
|
|
||||||
|
# 3. Full visibility check
|
||||||
|
bounds = (origin.x - max_dist, origin.y - max_dist, origin.x + max_dist, origin.y + max_dist)
|
||||||
|
candidates = list(self.corner_index.intersection(bounds))
|
||||||
|
|
||||||
|
visible = []
|
||||||
|
for i in candidates:
|
||||||
|
cx, cy = self.corners[i]
|
||||||
|
dx, dy = cx - origin.x, cy - origin.y
|
||||||
|
dist = numpy.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
|
if dist > max_dist or dist < 1e-3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||||
|
reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05)
|
||||||
|
if reach >= dist - 0.01:
|
||||||
|
visible.append((cx, cy, dist))
|
||||||
|
|
||||||
|
self._static_visibility_cache[cache_key] = visible
|
||||||
|
return visible
|
||||||
57
inire/tests/benchmark_scaling.py
Normal file
57
inire/tests/benchmark_scaling.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
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 AStarContext, AStarMetrics
|
||||||
|
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)
|
||||||
|
context = AStarContext(evaluator)
|
||||||
|
metrics = AStarMetrics()
|
||||||
|
pf = PathFinder(context, metrics)
|
||||||
|
|
||||||
|
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 = metrics.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()
|
||||||
89
inire/tests/test_astar.py
Normal file
89
inire/tests/test_astar.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import pytest
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, route_astar
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_evaluator() -> CostEvaluator:
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=(0, -50, 150, 150))
|
||||||
|
danger_map.precompute([])
|
||||||
|
return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0)
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(50, 0, 0)
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||||
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
assert validation["connectivity_ok"]
|
||||||
|
# Path should be exactly 50um (or slightly more if it did weird things, but here it's straight)
|
||||||
|
assert abs(validation["total_length"] - 50.0) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0, bend_radii=[10.0])
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
||||||
|
target = Port(20, 20, 0)
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||||
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
assert validation["connectivity_ok"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
# Add an obstacle in the middle of a straight path
|
||||||
|
# Obstacle from x=20 to 40, y=-20 to 20
|
||||||
|
obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)])
|
||||||
|
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||||
|
basic_evaluator.danger_map.precompute([obstacle])
|
||||||
|
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0, bend_radii=[10.0], node_limit=1000000)
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(60, 0, 0)
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||||
|
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
||||||
|
|
||||||
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
|
# Path should have detoured, so length > 50
|
||||||
|
assert validation["total_length"] > 50.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0)
|
||||||
|
# Target is NOT on 1um grid
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(10.1, 0, 0)
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||||
|
|
||||||
|
# Under the new Enforce Grid policy, the router snaps the target internally to 10.0.
|
||||||
|
# We validate against the snapped target.
|
||||||
|
from inire.geometry.components import snap_search_grid
|
||||||
|
target_snapped = Port(snap_search_grid(target.x, 1.0), snap_search_grid(target.y, 1.0), target.orientation, snap=False)
|
||||||
|
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target_snapped)
|
||||||
|
|
||||||
|
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||||
61
inire/tests/test_collision.py
Normal file
61
inire/tests/test_collision.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
|
||||||
|
|
||||||
|
def test_collision_detection() -> None:
|
||||||
|
# Clearance = 2um
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# 10x10 um obstacle at (10,10)
|
||||||
|
obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)])
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
# 1. Direct hit
|
||||||
|
test_poly = Polygon([(12, 12), (13, 12), (13, 13), (12, 13)])
|
||||||
|
assert engine.is_collision(test_poly, net_width=2.0)
|
||||||
|
|
||||||
|
# 2. Far away
|
||||||
|
test_poly_far = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
|
||||||
|
assert not engine.is_collision(test_poly_far, net_width=2.0)
|
||||||
|
|
||||||
|
# 3. Near hit (within clearance)
|
||||||
|
# Obstacle edge at x=10.
|
||||||
|
# test_poly edge at x=9.
|
||||||
|
# Distance = 1.0 um.
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
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), (20, 10), (20, 20), (10, 20)])
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
# Port exactly on the boundary
|
||||||
|
start_port = Port(10.0, 12.0, 0)
|
||||||
|
|
||||||
|
# Move starting from this port that overlaps the obstacle by 1nm
|
||||||
|
# (Inside the 2nm safety zone)
|
||||||
|
test_poly = Polygon([(9.999, 11.9995), (10.001, 11.9995), (10.001, 12.0005), (9.999, 12.0005)])
|
||||||
|
|
||||||
|
assert not engine.is_collision(test_poly, net_width=0.001, start_port=start_port)
|
||||||
|
|
||||||
|
|
||||||
|
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 not engine.is_collision(test_poly, net_width=2.0)
|
||||||
160
inire/tests/test_components.py
Normal file
160
inire/tests/test_components.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from inire.geometry.components import Bend90, SBend, Straight
|
||||||
|
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||||
|
|
||||||
|
|
||||||
|
def test_straight_generation() -> None:
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
length = 10.0
|
||||||
|
width = 2.0
|
||||||
|
result = Straight.generate(start, length, width, snap_size=1.0)
|
||||||
|
|
||||||
|
assert result.end_port.x == 10.0
|
||||||
|
assert result.end_port.y == 0.0
|
||||||
|
assert result.end_port.orientation == 0.0
|
||||||
|
assert len(result.geometry) == 1
|
||||||
|
|
||||||
|
# Bounds of the polygon
|
||||||
|
minx, miny, maxx, maxy = result.geometry[0].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
|
||||||
|
result_cw = Bend90.generate(start, radius, width, direction="CW", snap_size=1.0)
|
||||||
|
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
|
||||||
|
result_ccw = Bend90.generate(start, radius, width, direction="CCW", snap_size=1.0)
|
||||||
|
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, snap_size=1.0)
|
||||||
|
assert result.end_port.y == 5.0
|
||||||
|
assert result.end_port.orientation == 0.0
|
||||||
|
assert len(result.geometry) == 2 # Optimization: returns individual arcs
|
||||||
|
|
||||||
|
# Verify failure for large offset
|
||||||
|
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||||
|
SBend.generate(start, 25.0, 10.0, 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bend_collision_models() -> None:
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
radius = 10.0
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
# 1. BBox model
|
||||||
|
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox", snap_size=1.0)
|
||||||
|
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||||
|
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
||||||
|
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
||||||
|
assert minx <= 0.0 + 1e-6
|
||||||
|
assert maxx >= 10.0 - 1e-6
|
||||||
|
assert miny <= 0.0 + 1e-6
|
||||||
|
assert maxy >= 10.0 - 1e-6
|
||||||
|
|
||||||
|
# 2. Clipped BBox model
|
||||||
|
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0, snap_size=1.0)
|
||||||
|
# Area should be less than full bbox
|
||||||
|
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
|
||||||
|
|
||||||
|
|
||||||
|
def test_sbend_collision_models() -> None:
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
offset = 5.0
|
||||||
|
radius = 10.0
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox", snap_size=1.0)
|
||||||
|
# Geometry should be a list of individual bbox polygons for each arc
|
||||||
|
assert len(res_bbox.geometry) == 2
|
||||||
|
|
||||||
|
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc", snap_size=1.0)
|
||||||
|
area_bbox = sum(p.area for p in res_bbox.geometry)
|
||||||
|
area_arc = sum(p.area for p in res_arc.geometry)
|
||||||
|
assert area_bbox > area_arc
|
||||||
|
|
||||||
|
|
||||||
|
def test_sbend_continuity() -> None:
|
||||||
|
# Verify SBend endpoints and continuity math
|
||||||
|
start = Port(10, 20, 90) # Starting facing up
|
||||||
|
offset = 4.0
|
||||||
|
radius = 20.0
|
||||||
|
width = 1.0
|
||||||
|
|
||||||
|
# We use snap_size=1.0 so that (10-offset) = 6.0 is EXACTLY hit.
|
||||||
|
res = SBend.generate(start, offset, radius, width, snap_size=1.0)
|
||||||
|
|
||||||
|
# Target orientation should be same as start
|
||||||
|
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
||||||
|
|
||||||
|
# For a port at 90 deg, +offset is a shift in -x direction
|
||||||
|
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||||
|
|
||||||
|
# Geometry should be a list of valid polygons
|
||||||
|
assert len(res.geometry) == 2
|
||||||
|
for p in res.geometry:
|
||||||
|
assert p.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_arc_sagitta_precision() -> None:
|
||||||
|
# Verify that requested sagitta actually controls segment count
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
radius = 100.0 # Large radius to make sagitta significant
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
# Coarse: 1um sagitta
|
||||||
|
res_coarse = Bend90.generate(start, radius, width, direction="CCW", sagitta=1.0)
|
||||||
|
# Fine: 0.01um (10nm) sagitta
|
||||||
|
res_fine = Bend90.generate(start, radius, width, direction="CCW", sagitta=0.01)
|
||||||
|
|
||||||
|
# Number of segments should be significantly higher for fine
|
||||||
|
# Exterior points = (segments + 1) * 2
|
||||||
|
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||||
|
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||||
|
|
||||||
|
assert pts_fine > pts_coarse * 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_component_transform_invariance() -> None:
|
||||||
|
# Verify that generating at (0,0) then transforming
|
||||||
|
# is same as generating at the transformed port.
|
||||||
|
start0 = Port(0, 0, 0)
|
||||||
|
radius = 10.0
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
res0 = Bend90.generate(start0, radius, width, direction="CCW", snap_size=1.0)
|
||||||
|
|
||||||
|
# Transform: Translate (10, 10) then Rotate 90
|
||||||
|
dx, dy = 10.0, 5.0
|
||||||
|
angle = 90.0
|
||||||
|
|
||||||
|
# 1. Transform the generated geometry
|
||||||
|
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
||||||
|
|
||||||
|
# 2. Generate at transformed start
|
||||||
|
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
||||||
|
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW", snap_size=1.0)
|
||||||
|
|
||||||
|
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
||||||
|
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
||||||
|
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
||||||
63
inire/tests/test_congestion.py
Normal file
63
inire/tests/test_congestion.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import pytest
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext, route_astar
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_evaluator() -> CostEvaluator:
|
||||||
|
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, bend_penalty=50.0, sbend_penalty=150.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0, sbend_offsets=[2.0, 5.0])
|
||||||
|
# Start at (0,0), target at (50, 2) -> 2um lateral offset
|
||||||
|
# This matches one of our discretized SBend offsets.
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
target = Port(50, 2, 0)
|
||||||
|
path = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
# Check if any component in the path is an SBend
|
||||||
|
found_sbend = False
|
||||||
|
for res in path:
|
||||||
|
# Check if the end port orientation is same as start
|
||||||
|
# and it's not a single straight (which would have y=0)
|
||||||
|
if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.orientation - start.orientation) < 0.1:
|
||||||
|
found_sbend = True
|
||||||
|
break
|
||||||
|
assert found_sbend
|
||||||
|
|
||||||
|
|
||||||
|
def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator, snap_size=1.0, bend_radii=[5.0, 10.0])
|
||||||
|
# Increase base penalty to force detour immediately
|
||||||
|
pf = PathFinder(context, max_iterations=10, base_congestion_penalty=1000.0)
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Force them into a narrow corridor that only fits ONE.
|
||||||
|
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
|
||||||
|
obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)])
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
assert results["net1"].is_valid
|
||||||
|
assert results["net2"].is_valid
|
||||||
39
inire/tests/test_cost.py
Normal file
39
inire/tests/test_cost.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
|
||||||
|
|
||||||
|
def test_cost_calculation() -> None:
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
# 50x50 um area, 1um resolution
|
||||||
|
danger_map = DangerMap(bounds=(0, 0, 50, 50))
|
||||||
|
danger_map.precompute([])
|
||||||
|
# Use small penalties for testing
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1, bend_penalty=10.0)
|
||||||
|
|
||||||
|
p1 = Port(0, 0, 0)
|
||||||
|
p2 = Port(10, 10, 0)
|
||||||
|
|
||||||
|
h = evaluator.h_manhattan(p1, p2)
|
||||||
|
# Manhattan distance = 20.
|
||||||
|
# Jog alignment penalty = 2*bp = 20.
|
||||||
|
# Side check penalty = 2*bp = 20.
|
||||||
|
# Total = 1.1 * (20 + 40) = 66.0
|
||||||
|
assert abs(h - 66.0) < 1e-6
|
||||||
|
|
||||||
|
# Orientation difference
|
||||||
|
p3 = Port(10, 10, 90)
|
||||||
|
h_90 = evaluator.h_manhattan(p1, p3)
|
||||||
|
# diff = 90. penalty += 1*bp = 10.
|
||||||
|
# Side check: 2*bp = 20. (Total penalty = 30)
|
||||||
|
# Total = 1.1 * (20 + 30) = 55.0
|
||||||
|
assert abs(h_90 - 55.0) < 1e-6
|
||||||
|
|
||||||
|
# Traveling away
|
||||||
|
p4 = Port(10, 10, 180)
|
||||||
|
h_away = evaluator.h_manhattan(p1, p4)
|
||||||
|
# diff = 180. penalty += 2*bp = 20.
|
||||||
|
# Side check: 2*bp = 20.
|
||||||
|
# Total = 1.1 * (20 + 40) = 66.0
|
||||||
|
assert h_away >= h_90
|
||||||
70
inire/tests/test_failed_net_congestion.py
Normal file
70
inire/tests/test_failed_net_congestion.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import numpy
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.astar import AStarContext
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
|
||||||
|
def test_failed_net_visibility():
|
||||||
|
"""
|
||||||
|
Verifies that nets that fail to reach their target (return partial paths)
|
||||||
|
ARE added to the collision engine, making them visible to other nets
|
||||||
|
for negotiated congestion.
|
||||||
|
"""
|
||||||
|
# 1. Setup
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Create a simple danger map (bounds 0-100)
|
||||||
|
# We don't strictly need obstacles in it for this test.
|
||||||
|
dm = DangerMap(bounds=(0, 0, 100, 100))
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, dm)
|
||||||
|
|
||||||
|
# 2. Configure Router with low limit to FORCE failure
|
||||||
|
# node_limit=10 is extremely low, likely allowing only a few moves.
|
||||||
|
# Start (0,0) -> Target (100,0) is 100um away.
|
||||||
|
|
||||||
|
# Let's add a static obstacle that blocks the direct path.
|
||||||
|
from shapely.geometry import box
|
||||||
|
obstacle = box(40, -10, 60, 10) # Wall at x=50
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
# With obstacle, direct jump fails. A* must search around.
|
||||||
|
# Limit=10 should be enough to fail to find a path around.
|
||||||
|
context = AStarContext(evaluator, node_limit=10)
|
||||||
|
|
||||||
|
# 3. Configure PathFinder
|
||||||
|
# max_iterations=1 because we only need to check the state after the first attempt.
|
||||||
|
pf = PathFinder(context, max_iterations=1, warm_start=None)
|
||||||
|
|
||||||
|
netlist = {
|
||||||
|
"net1": (Port(0, 0, 0), Port(100, 0, 0))
|
||||||
|
}
|
||||||
|
net_widths = {"net1": 1.0}
|
||||||
|
|
||||||
|
# 4. Route
|
||||||
|
print("\nStarting Route...")
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
res = results["net1"]
|
||||||
|
print(f"Result: is_valid={res.is_valid}, reached={res.reached_target}, path_len={len(res.path)}")
|
||||||
|
|
||||||
|
# 5. Verify Failure Condition
|
||||||
|
# We expect reached_target to be False because of node_limit + obstacle
|
||||||
|
assert not res.reached_target, "Test setup failed: Net reached target despite low limit!"
|
||||||
|
assert len(res.path) > 0, "Test setup failed: No partial path returned!"
|
||||||
|
|
||||||
|
# 6. Verify Visibility
|
||||||
|
# Check if net1 is in the collision engine
|
||||||
|
found_nets = set()
|
||||||
|
# CollisionEngine.dynamic_geometries: dict[obj_id, (net_id, poly)]
|
||||||
|
for obj_id, (nid, poly) in engine.dynamic_geometries.items():
|
||||||
|
found_nets.add(nid)
|
||||||
|
|
||||||
|
print(f"Nets found in engine: {found_nets}")
|
||||||
|
|
||||||
|
# The FIX Expectation: "net1" SHOULD be present
|
||||||
|
assert "net1" in found_nets, "Bug present: Net1 is invisible despite having partial path!"
|
||||||
65
inire/tests/test_fuzz.py
Normal file
65
inire/tests/test_fuzz.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
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 AStarContext, route_astar
|
||||||
|
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: Any) -> Polygon:
|
||||||
|
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: Any) -> Port:
|
||||||
|
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: list[Polygon], start: Port, target: Port) -> 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)
|
||||||
|
context = AStarContext(evaluator, 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 = route_astar(start, target, net_width=2.0, context=context)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
expected_start=start,
|
||||||
|
expected_end=target,
|
||||||
|
)
|
||||||
|
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}")
|
||||||
35
inire/tests/test_pathfinder.py
Normal file
35
inire/tests/test_pathfinder.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_evaluator() -> CostEvaluator:
|
||||||
|
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: CostEvaluator) -> None:
|
||||||
|
context = AStarContext(basic_evaluator)
|
||||||
|
pf = PathFinder(context)
|
||||||
|
|
||||||
|
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 len(results) == 2
|
||||||
|
assert results["net1"].is_valid
|
||||||
|
assert results["net2"].is_valid
|
||||||
|
assert results["net1"].collisions == 0
|
||||||
|
assert results["net2"].collisions == 0
|
||||||
51
inire/tests/test_primitives.py
Normal file
51
inire/tests/test_primitives.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from hypothesis import given, strategies as st
|
||||||
|
|
||||||
|
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||||
|
|
||||||
|
|
||||||
|
@st.composite
|
||||||
|
def port_strategy(draw: Any) -> Port:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@given(p=port_strategy())
|
||||||
|
def test_port_transform_invariants(p: Port) -> 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 abs(p_rot.x - p.x) < 1e-6
|
||||||
|
assert abs(p_rot.y - p.y) < 1e-6
|
||||||
|
assert (p_rot.orientation % 360) == (p.orientation % 360)
|
||||||
|
|
||||||
|
|
||||||
|
@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: Port, dx: float, dy: float) -> None:
|
||||||
|
p_trans = translate_port(p, dx, dy)
|
||||||
|
# Check that snapped result is indeed multiple of GRID_SNAP_UM (0.001 um = 1nm)
|
||||||
|
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
|
||||||
63
inire/tests/test_refinements.py
Normal file
63
inire/tests/test_refinements.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.components import Bend90
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarContext
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
|
||||||
|
|
||||||
|
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, direction="CCW", sagitta=0.5)
|
||||||
|
# Low tolerance (1nm) -> many segments
|
||||||
|
res_fine = Bend90.generate(start, radius=10.0, width=2.0, direction="CCW", 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)
|
||||||
|
context = AStarContext(evaluator, bend_radii=[5.0, 10.0])
|
||||||
|
pf = PathFinder(context)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
assert not pa.intersects(pb)
|
||||||
66
inire/tests/test_variable_grid.py
Normal file
66
inire/tests/test_variable_grid.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import unittest
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import route_astar, AStarContext
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.components import snap_search_grid
|
||||||
|
|
||||||
|
class TestVariableGrid(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.ce = CollisionEngine(clearance=2.0)
|
||||||
|
self.cost = CostEvaluator(self.ce)
|
||||||
|
|
||||||
|
def test_grid_1_0(self):
|
||||||
|
""" Test routing with a 1.0um grid. """
|
||||||
|
context = AStarContext(self.cost, snap_size=1.0)
|
||||||
|
start = Port(0.0, 0.0, 0.0)
|
||||||
|
# 12.3 should snap to 12.0 on a 1.0um grid
|
||||||
|
target = Port(12.3, 0.0, 0.0, snap=False)
|
||||||
|
|
||||||
|
path = route_astar(start, target, net_width=1.0, context=context)
|
||||||
|
|
||||||
|
self.assertIsNotNone(path)
|
||||||
|
last_port = path[-1].end_port
|
||||||
|
self.assertEqual(last_port.x, 12.0)
|
||||||
|
|
||||||
|
# Verify component relative grid coordinates
|
||||||
|
# rel_gx = round(x / snap)
|
||||||
|
# For x=12.0, snap=1.0 -> rel_gx=12
|
||||||
|
self.assertEqual(path[-1].rel_gx, 12)
|
||||||
|
|
||||||
|
def test_grid_2_5(self):
|
||||||
|
""" Test routing with a 2.5um grid. """
|
||||||
|
context = AStarContext(self.cost, snap_size=2.5)
|
||||||
|
start = Port(0.0, 0.0, 0.0)
|
||||||
|
# 7.5 is a multiple of 2.5, should be reached exactly
|
||||||
|
target = Port(7.5, 0.0, 0.0, snap=False)
|
||||||
|
|
||||||
|
path = route_astar(start, target, net_width=1.0, context=context)
|
||||||
|
|
||||||
|
self.assertIsNotNone(path)
|
||||||
|
last_port = path[-1].end_port
|
||||||
|
self.assertEqual(last_port.x, 7.5)
|
||||||
|
|
||||||
|
# rel_gx = 7.5 / 2.5 = 3
|
||||||
|
self.assertEqual(path[-1].rel_gx, 3)
|
||||||
|
|
||||||
|
def test_grid_10_0(self):
|
||||||
|
""" Test routing with a large 10.0um grid. """
|
||||||
|
context = AStarContext(self.cost, snap_size=10.0)
|
||||||
|
start = Port(0.0, 0.0, 0.0)
|
||||||
|
# 15.0 should snap to 20.0 (ties usually round to even or nearest,
|
||||||
|
# but 15.0 is exactly between 10 and 20.
|
||||||
|
# snap_search_grid uses round(val/snap)*snap. round(1.5) is 2 in Python 3.
|
||||||
|
target = Port(15.0, 0.0, 0.0, snap=False)
|
||||||
|
|
||||||
|
path = route_astar(start, target, net_width=1.0, context=context)
|
||||||
|
|
||||||
|
self.assertIsNotNone(path)
|
||||||
|
last_port = path[-1].end_port
|
||||||
|
self.assertEqual(last_port.x, 20.0)
|
||||||
|
|
||||||
|
# rel_gx = 20.0 / 10.0 = 2
|
||||||
|
self.assertEqual(path[-1].rel_gx, 2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
104
inire/utils/validation.py
Normal file
104
inire/utils/validation.py
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from inire.constants import TOLERANCE_LINEAR
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.pathfinder import RoutingResult
|
||||||
|
|
||||||
|
|
||||||
|
def validate_routing_result(
|
||||||
|
result: RoutingResult,
|
||||||
|
static_obstacles: list[Polygon],
|
||||||
|
clearance: float,
|
||||||
|
expected_start: Port | None = None,
|
||||||
|
expected_end: Port | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Perform a high-precision validation of a routed path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: The routing result to validate.
|
||||||
|
static_obstacles: List of static obstacle geometries.
|
||||||
|
clearance: Required minimum distance.
|
||||||
|
expected_start: Optional expected start port.
|
||||||
|
expected_end: Optional expected end port.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary with validation results.
|
||||||
|
"""
|
||||||
|
_ = expected_start
|
||||||
|
if not result.path:
|
||||||
|
return {"is_valid": False, "reason": "No path found"}
|
||||||
|
|
||||||
|
obstacle_collision_geoms = []
|
||||||
|
self_intersection_geoms = []
|
||||||
|
connectivity_errors = []
|
||||||
|
|
||||||
|
# 1. Connectivity Check
|
||||||
|
total_length = 0.0
|
||||||
|
for comp in result.path:
|
||||||
|
total_length += comp.length
|
||||||
|
|
||||||
|
# Boundary check
|
||||||
|
if expected_end:
|
||||||
|
last_port = result.path[-1].end_port
|
||||||
|
dist_to_end = numpy.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
||||||
|
if dist_to_end > 0.005:
|
||||||
|
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
||||||
|
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
||||||
|
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
||||||
|
|
||||||
|
# 2. Geometry Buffering
|
||||||
|
dilation_half = clearance / 2.0
|
||||||
|
dilation_full = clearance
|
||||||
|
|
||||||
|
dilated_for_self = []
|
||||||
|
|
||||||
|
for comp in result.path:
|
||||||
|
for poly in comp.geometry:
|
||||||
|
# Check against obstacles
|
||||||
|
d_full = poly.buffer(dilation_full)
|
||||||
|
for obs in static_obstacles:
|
||||||
|
if d_full.intersects(obs):
|
||||||
|
intersection = d_full.intersection(obs)
|
||||||
|
if intersection.area > 1e-9:
|
||||||
|
obstacle_collision_geoms.append(intersection)
|
||||||
|
|
||||||
|
# Save for self-intersection check
|
||||||
|
dilated_for_self.append(poly.buffer(dilation_half))
|
||||||
|
|
||||||
|
# 3. Self-intersection
|
||||||
|
for i, seg_i in enumerate(dilated_for_self):
|
||||||
|
for j, seg_j in enumerate(dilated_for_self):
|
||||||
|
if j > i + 1 and seg_i.intersects(seg_j): # Non-adjacent
|
||||||
|
overlap = seg_i.intersection(seg_j)
|
||||||
|
if overlap.area > TOLERANCE_LINEAR:
|
||||||
|
self_intersection_geoms.append((i, j, overlap))
|
||||||
|
|
||||||
|
is_valid = (len(obstacle_collision_geoms) == 0 and
|
||||||
|
len(self_intersection_geoms) == 0 and
|
||||||
|
len(connectivity_errors) == 0)
|
||||||
|
|
||||||
|
reasons = []
|
||||||
|
if obstacle_collision_geoms:
|
||||||
|
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
||||||
|
if self_intersection_geoms:
|
||||||
|
# report which indices
|
||||||
|
idx_str = ", ".join([f"{i}-{j}" for i, j, _ in self_intersection_geoms[:5]])
|
||||||
|
reasons.append(f"Found {len(self_intersection_geoms)} self-intersections (e.g. {idx_str}).")
|
||||||
|
if connectivity_errors:
|
||||||
|
reasons.extend(connectivity_errors)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"is_valid": is_valid,
|
||||||
|
"reason": " ".join(reasons),
|
||||||
|
"obstacle_collisions": obstacle_collision_geoms,
|
||||||
|
"self_intersections": self_intersection_geoms,
|
||||||
|
"total_length": total_length,
|
||||||
|
"connectivity_ok": len(connectivity_errors) == 0,
|
||||||
|
}
|
||||||
218
inire/utils/visualization.py
Normal file
218
inire/utils/visualization.py
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy
|
||||||
|
from shapely.geometry import MultiPolygon, Polygon
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.pathfinder import RoutingResult
|
||||||
|
|
||||||
|
|
||||||
|
def plot_routing_results(
|
||||||
|
results: dict[str, RoutingResult],
|
||||||
|
static_obstacles: list[Polygon],
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
netlist: dict[str, tuple[Port, Port]] | None = None,
|
||||||
|
show_actual: bool = True,
|
||||||
|
) -> tuple[Figure, Axes]:
|
||||||
|
"""
|
||||||
|
Plot obstacles and routed paths using matplotlib.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: Dictionary of net_id to RoutingResult.
|
||||||
|
static_obstacles: List of static obstacle polygons.
|
||||||
|
bounds: Plot limits (minx, miny, maxx, maxy).
|
||||||
|
netlist: Optional original netlist for port visualization.
|
||||||
|
show_actual: If True, overlay high-fidelity geometry if available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The matplotlib Figure and Axes objects.
|
||||||
|
"""
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 12))
|
||||||
|
|
||||||
|
# Plot static obstacles (gray)
|
||||||
|
for poly in static_obstacles:
|
||||||
|
x, y = poly.exterior.xy
|
||||||
|
ax.fill(x, y, alpha=0.3, fc="gray", ec="black", zorder=1)
|
||||||
|
|
||||||
|
# Plot paths
|
||||||
|
colors = plt.get_cmap("tab20")
|
||||||
|
for i, (net_id, res) in enumerate(results.items()):
|
||||||
|
color: str | tuple[float, ...] = colors(i % 20)
|
||||||
|
if not res.is_valid:
|
||||||
|
color = "red"
|
||||||
|
|
||||||
|
label_added = False
|
||||||
|
for comp in res.path:
|
||||||
|
# 1. Plot Collision Geometry (Translucent fill)
|
||||||
|
# This is the geometry used during search (e.g. proxy or arc)
|
||||||
|
for poly in comp.geometry:
|
||||||
|
if isinstance(poly, MultiPolygon):
|
||||||
|
geoms = list(poly.geoms)
|
||||||
|
else:
|
||||||
|
geoms = [poly]
|
||||||
|
|
||||||
|
for g in geoms:
|
||||||
|
if hasattr(g, "exterior"):
|
||||||
|
x, y = g.exterior.xy
|
||||||
|
ax.fill(x, y, alpha=0.15, fc=color, ec=color, linestyle='--', lw=0.5, zorder=2)
|
||||||
|
else:
|
||||||
|
# Fallback for LineString or other geometries
|
||||||
|
x, y = g.xy
|
||||||
|
ax.plot(x, y, color=color, alpha=0.15, linestyle='--', lw=0.5, zorder=2)
|
||||||
|
|
||||||
|
# 2. Plot "Actual" Geometry (The high-fidelity shape used for fabrication)
|
||||||
|
# Use comp.actual_geometry if it exists (should be the arc)
|
||||||
|
actual_geoms_to_plot = comp.actual_geometry if comp.actual_geometry is not None else comp.geometry
|
||||||
|
|
||||||
|
for poly in actual_geoms_to_plot:
|
||||||
|
if isinstance(poly, MultiPolygon):
|
||||||
|
geoms = list(poly.geoms)
|
||||||
|
else:
|
||||||
|
geoms = [poly]
|
||||||
|
for g in geoms:
|
||||||
|
if hasattr(g, "exterior"):
|
||||||
|
x, y = g.exterior.xy
|
||||||
|
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "")
|
||||||
|
else:
|
||||||
|
x, y = g.xy
|
||||||
|
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "")
|
||||||
|
label_added = True
|
||||||
|
|
||||||
|
# 3. Plot subtle port orientation arrow
|
||||||
|
p = comp.end_port
|
||||||
|
rad = numpy.radians(p.orientation)
|
||||||
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
|
scale=40, width=0.002, alpha=0.2, pivot="tail", zorder=4)
|
||||||
|
|
||||||
|
if not res.path and not res.is_valid:
|
||||||
|
# Best-effort display: If the path is empty but failed, it might be unroutable.
|
||||||
|
# We don't have a partial path in RoutingResult currently.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. Plot main arrows for netlist ports
|
||||||
|
if netlist:
|
||||||
|
for net_id, (start_p, target_p) in netlist.items():
|
||||||
|
for p in [start_p, target_p]:
|
||||||
|
rad = numpy.radians(p.orientation)
|
||||||
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
|
scale=25, width=0.004, pivot="tail", zorder=6)
|
||||||
|
|
||||||
|
ax.set_xlim(bounds[0], bounds[2])
|
||||||
|
ax.set_ylim(bounds[1], bounds[3])
|
||||||
|
ax.set_aspect("equal")
|
||||||
|
ax.set_title("Inire Routing Results (Dashed: Collision Proxy, Solid: Actual Geometry)")
|
||||||
|
|
||||||
|
# Legend handling for many nets
|
||||||
|
if len(results) < 25:
|
||||||
|
handles, labels = ax.get_legend_handles_labels()
|
||||||
|
if labels:
|
||||||
|
ax.legend(loc='upper left', bbox_to_anchor=(1, 1), fontsize='small', ncol=2)
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
plt.grid(True, which='both', linestyle=':', alpha=0.5)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
def plot_danger_map(
|
||||||
|
danger_map: DangerMap,
|
||||||
|
ax: Axes | None = None,
|
||||||
|
) -> tuple[Figure, Axes]:
|
||||||
|
"""
|
||||||
|
Plot the pre-computed danger map as a heatmap.
|
||||||
|
"""
|
||||||
|
if ax is None:
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 10))
|
||||||
|
else:
|
||||||
|
fig = ax.get_figure()
|
||||||
|
|
||||||
|
# Need to transpose because grid is [x, y] and imshow expects [row, col] (y, x)
|
||||||
|
# Also origin='lower' to match coordinates
|
||||||
|
im = ax.imshow(
|
||||||
|
danger_map.grid.T,
|
||||||
|
origin='lower',
|
||||||
|
extent=[danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy],
|
||||||
|
cmap='YlOrRd',
|
||||||
|
alpha=0.6
|
||||||
|
)
|
||||||
|
plt.colorbar(im, ax=ax, label='Danger Cost')
|
||||||
|
ax.set_title("Danger Map (Proximity Costs)")
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
def plot_expanded_nodes(
|
||||||
|
nodes: list[tuple[float, float, float]],
|
||||||
|
ax: Axes | None = None,
|
||||||
|
color: str = 'gray',
|
||||||
|
alpha: float = 0.3,
|
||||||
|
) -> tuple[Figure, Axes]:
|
||||||
|
"""
|
||||||
|
Plot A* expanded nodes for debugging.
|
||||||
|
"""
|
||||||
|
if ax is None:
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 10))
|
||||||
|
else:
|
||||||
|
fig = ax.get_figure()
|
||||||
|
|
||||||
|
if not nodes:
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
x, y, _ = zip(*nodes)
|
||||||
|
ax.scatter(x, y, s=1, c=color, alpha=alpha, zorder=0)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
def plot_expansion_density(
|
||||||
|
nodes: list[tuple[float, float, float]],
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
ax: Axes | None = None,
|
||||||
|
bins: int | tuple[int, int] = 50,
|
||||||
|
) -> tuple[Figure, Axes]:
|
||||||
|
"""
|
||||||
|
Plot a density heatmap (2D histogram) of expanded nodes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: List of (x, y, orientation) tuples.
|
||||||
|
bounds: (minx, miny, maxx, maxy) for the plot range.
|
||||||
|
ax: Optional existing axes to plot on.
|
||||||
|
bins: Number of bins for the histogram (int or (nx, ny)).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Figure and Axes objects.
|
||||||
|
"""
|
||||||
|
if ax is None:
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 12))
|
||||||
|
else:
|
||||||
|
fig = ax.get_figure()
|
||||||
|
|
||||||
|
if not nodes:
|
||||||
|
ax.text(0.5, 0.5, "No Expansion Data", ha='center', va='center', transform=ax.transAxes)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
x, y, _ = zip(*nodes)
|
||||||
|
|
||||||
|
# Create 2D histogram
|
||||||
|
h, xedges, yedges = numpy.histogram2d(
|
||||||
|
x, y,
|
||||||
|
bins=bins,
|
||||||
|
range=[[bounds[0], bounds[2]], [bounds[1], bounds[3]]]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plot as image
|
||||||
|
im = ax.imshow(
|
||||||
|
h.T,
|
||||||
|
origin='lower',
|
||||||
|
extent=[bounds[0], bounds[2], bounds[1], bounds[3]],
|
||||||
|
cmap='plasma',
|
||||||
|
interpolation='nearest',
|
||||||
|
alpha=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.colorbar(im, ax=ax, label='Expansion Count')
|
||||||
|
ax.set_title("Search Expansion Density")
|
||||||
|
ax.set_xlim(bounds[0], bounds[2])
|
||||||
|
ax.set_ylim(bounds[1], bounds[3])
|
||||||
|
|
||||||
|
return fig, ax
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "inire"
|
name = "inire"
|
||||||
description = "Wave-router"
|
description = "Wave-router: Auto-routing for photonic and RF integrated circuits"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = { file = "LICENSE.md" }
|
license = { file = "LICENSE.md" }
|
||||||
|
|
@ -9,22 +9,6 @@ authors = [
|
||||||
]
|
]
|
||||||
homepage = "https://mpxd.net/code/jan/inire"
|
homepage = "https://mpxd.net/code/jan/inire"
|
||||||
repository = "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 = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
|
|
@ -36,10 +20,17 @@ classifiers = [
|
||||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"numpy",
|
||||||
|
"scipy",
|
||||||
|
"shapely",
|
||||||
|
"rtree",
|
||||||
|
"matplotlib",
|
||||||
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"hypothesis>=6.151.9",
|
||||||
"matplotlib>=3.10.8",
|
"matplotlib>=3.10.8",
|
||||||
"pytest>=9.0.2",
|
"pytest>=9.0.2",
|
||||||
"ruff>=0.15.5",
|
"ruff>=0.15.5",
|
||||||
|
|
@ -79,7 +70,7 @@ lint.ignore = [
|
||||||
"C408", # dict(x=y) instead of {'x': y}
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
"PLR09", # Too many xxx
|
"PLR09", # Too many xxx
|
||||||
"PLR2004", # magic number
|
"PLR2004", # magic number
|
||||||
"PLC0414", # import x as x
|
#"PLC0414", # import x as x
|
||||||
"TRY003", # Long exception message
|
"TRY003", # Long exception message
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
337
uv.lock
generated
337
uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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/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/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/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]]
|
[[package]]
|
||||||
|
|
@ -80,6 +107,22 @@ version = "4.61.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|
@ -118,20 +173,35 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inire"
|
name = "inire"
|
||||||
version = "0.1.0"
|
source = { editable = "." }
|
||||||
source = { virtual = "." }
|
dependencies = [
|
||||||
|
{ name = "matplotlib" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "rtree" },
|
||||||
|
{ name = "scipy" },
|
||||||
|
{ name = "shapely" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "hypothesis" },
|
||||||
{ name = "matplotlib" },
|
{ name = "matplotlib" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "matplotlib" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "rtree" },
|
||||||
|
{ name = "scipy" },
|
||||||
|
{ name = "shapely" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "hypothesis", specifier = ">=6.151.9" },
|
||||||
{ name = "matplotlib", specifier = ">=3.10.8" },
|
{ name = "matplotlib", specifier = ">=3.10.8" },
|
||||||
{ name = "pytest", specifier = ">=9.0.2" },
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
{ name = "ruff", specifier = ">=0.15.5" },
|
{ name = "ruff", specifier = ">=0.15.5" },
|
||||||
|
|
@ -143,6 +213,32 @@ version = "1.4.9"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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 +290,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/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/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/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]]
|
[[package]]
|
||||||
|
|
@ -213,6 +314,20 @@ dependencies = [
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 }
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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 +356,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/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/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/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]]
|
[[package]]
|
||||||
|
|
@ -249,6 +367,28 @@ version = "2.4.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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 +431,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/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/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/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]]
|
[[package]]
|
||||||
|
|
@ -308,6 +455,28 @@ version = "12.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 }
|
||||||
wheels = [
|
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/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/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 },
|
{ 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 +527,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/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/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/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]]
|
[[package]]
|
||||||
|
|
@ -415,6 +591,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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|
@ -440,6 +632,136 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572 },
|
{ 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 = "scipy"
|
||||||
|
version = "1.17.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|
@ -448,3 +770,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
{ 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