Compare commits
No commits in common. "devel" and "master" have entirely different histories.
39 changed files with 23 additions and 3676 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,5 +8,3 @@ wheels/
|
|||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.hypothesis
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
3.13
|
||||
90
DOCS.md
90
DOCS.md
|
|
@ -1,90 +0,0 @@
|
|||
# Inire Configuration & API Documentation
|
||||
|
||||
This document describes the user-tunable parameters for the `inire` auto-router.
|
||||
|
||||
## 1. AStarRouter Parameters
|
||||
|
||||
The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ |
|
||||
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. |
|
||||
| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search. |
|
||||
| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow best-fit selection. |
|
||||
| `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). |
|
||||
| `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). |
|
||||
| `snap_to_target_dist` | `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. |
|
||||
| `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. |
|
||||
| `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. |
|
||||
| `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 before the bounding box corners are clipped. |
|
||||
|
||||
### Bend Collision Models
|
||||
* `"arc"`: High-fidelity model following the exact curved waveguide geometry.
|
||||
* `"bbox"`: Conservative model using the axis-aligned bounding box of the bend. Fast but blocks more space.
|
||||
* `"clipped_bbox"`: A middle ground that starts with the bounding box but applies 45-degree linear cuts to the inner and outer corners. The `bend_clip_margin` defines the extra safety distance from the waveguide edge to the cut line.
|
||||
|
||||
---
|
||||
|
||||
## 2. 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 are triggered by the `sbend_offsets` list. If you need a specific lateral shift (e.g., 5.86µm for a 45° switchover), add it to `sbend_offsets`. The router will only use an S-bend if it can reach a state that is exactly on the lattice or the target.
|
||||
103
README.md
103
README.md
|
|
@ -1,103 +0,0 @@
|
|||
# inire: Auto-Routing for Photonic and RF Integrated Circuits
|
||||
|
||||
`inire` is a high-performance auto-router designed specifically for the physical constraints of photonic and RF integrated circuits. It utilizes a Hybrid State-Lattice A* search combined with "Negotiated Congestion" (PathFinder) to route multiple nets while maintaining strict geometric fidelity and clearance.
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Hybrid State-Lattice Search**: Routes using discrete 90° bends and parametric S-bends, ensuring manufacturing-stable paths.
|
||||
* **Negotiated Congestion**: Iteratively resolves multi-net bottlenecks by inflating costs in high-traffic regions.
|
||||
* **Analytic Correctness**: Every move is verified against an R-Tree spatial index of obstacles and other paths.
|
||||
* **1nm Precision**: All coordinates and ports are snapped to a 1nm manufacturing grid.
|
||||
* **Safety & Proximity**: Incorporates a "Danger Map" (pre-computed distance transform) to maintain optimal spacing and reduce crosstalk.
|
||||
* **Locked Paths**: Supports treating existing geometries as fixed obstacles for incremental routing sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
`inire` requires Python 3.11+. You can install the dependencies using `uv` (recommended) or `pip`:
|
||||
|
||||
```bash
|
||||
# Using uv
|
||||
uv sync
|
||||
|
||||
# Using pip
|
||||
pip install numpy scipy shapely rtree matplotlib
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
# 1. Setup Environment
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, 0, 1000, 1000))
|
||||
danger_map.precompute([]) # Add polygons here for obstacles
|
||||
|
||||
# 2. Configure Router
|
||||
evaluator = CostEvaluator(
|
||||
collision_engine=engine,
|
||||
danger_map=danger_map,
|
||||
greedy_h_weight=1.2
|
||||
)
|
||||
router = AStarRouter(
|
||||
cost_evaluator=evaluator,
|
||||
bend_penalty=10.0
|
||||
)
|
||||
pf = PathFinder(
|
||||
router=router,
|
||||
cost_evaluator=evaluator
|
||||
)
|
||||
|
||||
# 3. Define Netlist
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(100, 50, 0)),
|
||||
}
|
||||
|
||||
# 4. Route
|
||||
results = pf.route_all(netlist, {"net1": 2.0})
|
||||
|
||||
if results["net1"].is_valid:
|
||||
print("Successfully routed net1!")
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Check the `examples/` directory for ready-to-run scripts demonstrating core features:
|
||||
|
||||
* **`examples/01_simple_route.py`**: Basic single-net routing with visualization. Generates `01_simple_route.png`.
|
||||
* **`examples/02_congestion_resolution.py`**: Multi-net routing resolving bottlenecks using Negotiated Congestion. Generates `02_congestion_resolution.png`.
|
||||
* **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths. Generates `03_locked_paths.png`.
|
||||
* **`examples/04_sbends_and_radii.py`**: Complex paths using parametric S-bends and multiple bend radii. Generates `04_sbends_and_radii.png`.
|
||||
* **`examples/05_orientation_stress.py`**: Stress test for various port orientation combinations (U-turns, opposite directions). Generates `05_orientation_stress.png`.
|
||||
* **`examples/06_bend_collision_models.py`**: Comparison of different collision models for bends (Arc vs. BBox vs. Clipped BBox). Generates `06_bend_collision_models.png`.
|
||||
|
||||
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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
|
|
@ -1,58 +0,0 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.utils.visualization import plot_routing_results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 01: Simple Route...")
|
||||
|
||||
# 1. Setup Environment
|
||||
# Define the routing area bounds (minx, miny, maxx, maxy)
|
||||
bounds = (0, 0, 100, 100)
|
||||
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
|
||||
# Add a simple rectangular obstacle
|
||||
obstacle = Polygon([(30, 20), (70, 20), (70, 40), (30, 40)])
|
||||
engine.add_static_obstacle(obstacle)
|
||||
|
||||
# Precompute the danger map (distance field) for heuristics
|
||||
danger_map.precompute([obstacle])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Define Netlist
|
||||
# Route from (10, 10) to (90, 50)
|
||||
# The obstacle at y=20-40 blocks the direct path.
|
||||
netlist = {
|
||||
"simple_net": (Port(10, 10, 0), Port(90, 50, 0)),
|
||||
}
|
||||
net_widths = {"simple_net": 2.0}
|
||||
|
||||
# 3. Route
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 4. Check Results
|
||||
if results["simple_net"].is_valid:
|
||||
print("Success! Route found.")
|
||||
print(f"Path collisions: {results['simple_net'].collisions}")
|
||||
else:
|
||||
print("Failed to route.")
|
||||
|
||||
# 5. Visualize
|
||||
fig, ax = plot_routing_results(results, [obstacle], bounds, netlist=netlist)
|
||||
fig.savefig("examples/01_simple_route.png")
|
||||
print("Saved plot to examples/01_simple_route.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
|
|
@ -1,54 +0,0 @@
|
|||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.utils.visualization import plot_routing_results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
|
||||
|
||||
# 1. Setup Environment (Open space)
|
||||
bounds = (0, 0, 100, 100)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute([])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Define Netlist
|
||||
# Three nets that all converge on the same central area.
|
||||
# Negotiated Congestion must find non-overlapping paths for all of them.
|
||||
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 = dict.fromkeys(netlist, 2.0)
|
||||
|
||||
# 3. Route with Negotiated Congestion
|
||||
# We increase the base penalty to encourage faster divergence
|
||||
pf = PathFinder(router, evaluator, base_congestion_penalty=1000.0)
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 4. Check Results
|
||||
all_valid = all(r.is_valid for r in results.values())
|
||||
if all_valid:
|
||||
print("Success! Congestion resolved for all nets.")
|
||||
else:
|
||||
print("Some nets failed or have collisions.")
|
||||
for nid, res in results.items():
|
||||
print(f" {nid}: valid={res.is_valid}, collisions={res.collisions}")
|
||||
|
||||
# 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()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
|
|
@ -1,73 +0,0 @@
|
|||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.utils.visualization import plot_routing_results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 03: Locked Paths (Incremental Routing - Bus Scenario)...")
|
||||
|
||||
# 1. Setup Environment
|
||||
bounds = (0, 0, 120, 120)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute([]) # Start with empty space
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.2)
|
||||
router = AStarRouter(evaluator, node_limit=200000)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Phase 1: Route a "Bus" of 3 parallel nets
|
||||
# We give them a small jog to make the locked geometry more interesting
|
||||
netlist_p1 = {
|
||||
"bus_0": (Port(10, 40, 0), Port(110, 45, 0)),
|
||||
"bus_1": (Port(10, 50, 0), Port(110, 55, 0)),
|
||||
"bus_2": (Port(10, 60, 0), Port(110, 65, 0)),
|
||||
}
|
||||
print("Phase 1: Routing bus (3 nets)...")
|
||||
results_p1 = pf.route_all(netlist_p1, dict.fromkeys(netlist_p1, 2.0))
|
||||
|
||||
# Lock all Phase 1 nets
|
||||
path_polys = []
|
||||
for nid, res in results_p1.items():
|
||||
if res.is_valid:
|
||||
print(f" Locking {nid}...")
|
||||
engine.lock_net(nid)
|
||||
path_polys.extend([p for comp in res.path for p in comp.geometry])
|
||||
else:
|
||||
print(f" Warning: {nid} failed to route correctly.")
|
||||
|
||||
# Update danger map with the newly locked geometry
|
||||
print("Updating DangerMap with locked paths...")
|
||||
danger_map.precompute(path_polys)
|
||||
|
||||
# 3. Phase 2: Route secondary nets that must navigate around the locked bus
|
||||
# These nets cross the bus vertically.
|
||||
netlist_p2 = {
|
||||
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
|
||||
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
|
||||
}
|
||||
|
||||
print("Phase 2: Routing crossing nets around locked bus...")
|
||||
# We use a slightly different width for variety
|
||||
results_p2 = pf.route_all(netlist_p2, dict.fromkeys(netlist_p2, 1.5))
|
||||
|
||||
# 4. Check Results
|
||||
for nid, res in results_p2.items():
|
||||
status = "Success" if res.is_valid else "Failed"
|
||||
print(f" {nid:12}: {status}, collisions={res.collisions}")
|
||||
|
||||
# 5. Visualize
|
||||
all_results = {**results_p1, **results_p2}
|
||||
all_netlists = {**netlist_p1, **netlist_p2}
|
||||
|
||||
fig, ax = plot_routing_results(all_results, [], bounds, netlist=all_netlists)
|
||||
fig.savefig("examples/03_locked_paths.png")
|
||||
print("Saved plot to examples/03_locked_paths.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
|
|
@ -1,68 +0,0 @@
|
|||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import 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,
|
||||
greedy_h_weight=1.5,
|
||||
)
|
||||
|
||||
# We want a 45 degree switchover for S-bend.
|
||||
# Offset O = 2 * R * (1 - cos(theta))
|
||||
# If R = 10, O = 5.86
|
||||
|
||||
router = AStarRouter(
|
||||
evaluator,
|
||||
node_limit=50000,
|
||||
bend_radii=[10.0, 30.0],
|
||||
sbend_offsets=[5.0], # Use a simpler offset
|
||||
sbend_radii=[10.0],
|
||||
bend_penalty=10.0,
|
||||
sbend_penalty=20.0,
|
||||
snap_to_target_dist=50.0, # Large snap range
|
||||
)
|
||||
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 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()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
|
|
@ -1,57 +0,0 @@
|
|||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.utils.visualization import plot_routing_results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 05: Orientation Stress Test...")
|
||||
|
||||
# 1. Setup Environment
|
||||
# Give some breathing room (-20 to 120) for U-turns and flips (R=10)
|
||||
bounds = (-20, -20, 120, 120)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
danger_map.precompute([])
|
||||
|
||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1)
|
||||
# router = AStarRouter(evaluator, node_limit=100000)
|
||||
router = AStarRouter(evaluator, node_limit=100000, bend_collision_type="clipped_bbox", bend_clip_margin=1.0)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 2. Define Netlist with various orientation challenges
|
||||
netlist = {
|
||||
# Opposite directions: requires two 90-degree bends to flip orientation
|
||||
"opposite": (Port(10, 80, 0), Port(90, 80, 180)),
|
||||
|
||||
# 90-degree turn: standard L-shape
|
||||
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
|
||||
|
||||
# Output behind input: requires a full U-turn
|
||||
"behind": (Port(80, 40, 0), Port(20, 40, 0)),
|
||||
|
||||
# Sharp return: output is behind and oriented towards the input
|
||||
"return_loop": (Port(80, 20, 0), Port(40, 10, 180)),
|
||||
}
|
||||
net_widths = dict.fromkeys(netlist, 2.0)
|
||||
|
||||
# 3. Route
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 4. Check Results
|
||||
for nid, res in results.items():
|
||||
status = "Success" if res.is_valid else "Failed"
|
||||
total_len = sum(comp.length for comp in res.path) if res.path else 0
|
||||
print(f" {nid:12}: {status}, total_length={total_len:.1f}")
|
||||
|
||||
# 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()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
|
|
@ -1,70 +0,0 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import 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)
|
||||
|
||||
# Scenario 1: Standard 'arc' model (High fidelity)
|
||||
router_arc = AStarRouter(evaluator, bend_collision_type="arc")
|
||||
netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}
|
||||
|
||||
# Scenario 2: 'bbox' model (Conservative axis-aligned box)
|
||||
router_bbox = AStarRouter(evaluator, bend_collision_type="bbox")
|
||||
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
|
||||
|
||||
# Scenario 3: 'clipped_bbox' model (Balanced)
|
||||
router_clipped = AStarRouter(evaluator, 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(router_arc, evaluator).route_all(netlist_arc, {"arc_model": 2.0})
|
||||
|
||||
print("Routing Scenario 2 (BBox)...")
|
||||
res_bbox = PathFinder(router_bbox, evaluator).route_all(netlist_bbox, {"bbox_model": 2.0})
|
||||
|
||||
print("Routing Scenario 3 (Clipped BBox)...")
|
||||
res_clipped = PathFinder(router_clipped, evaluator).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
|
||||
# Note: plot_routing_results will show the 'collision geometry' used by the router
|
||||
# since that's what's stored in res.path[i].geometry
|
||||
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()
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
inire Wave-router
|
||||
"""
|
||||
from .geometry.primitives import Port as Port # noqa: PLC0414
|
||||
from .geometry.components import Straight as Straight, Bend90 as Bend90, SBend as SBend # noqa: PLC0414
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
__version__ = '0.1'
|
||||
|
|
|
|||
|
|
@ -1,230 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
import rtree
|
||||
from shapely.prepared import prep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.prepared import PreparedGeometry
|
||||
from inire.geometry.primitives import Port
|
||||
|
||||
|
||||
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_id_counter',
|
||||
'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', 'dynamic_prepared', '_dynamic_id_counter'
|
||||
)
|
||||
|
||||
clearance: float
|
||||
""" Minimum required distance between any two waveguides or obstacles """
|
||||
|
||||
max_net_width: float
|
||||
""" Maximum width of any net in the session (used for pre-dilation) """
|
||||
|
||||
safety_zone_radius: float
|
||||
""" Radius around ports where collisions are ignored """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
clearance: float,
|
||||
max_net_width: float = 2.0,
|
||||
safety_zone_radius: float = 0.0021,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the Collision Engine.
|
||||
|
||||
Args:
|
||||
clearance: Minimum required distance (um).
|
||||
max_net_width: Maximum net width (um).
|
||||
safety_zone_radius: Safety radius around ports (um).
|
||||
"""
|
||||
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] = {} # ID -> Raw Polygon
|
||||
self.static_dilated: dict[int, Polygon] = {} # ID -> Dilated Polygon (by clearance)
|
||||
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> Prepared Dilated
|
||||
self._static_id_counter = 0
|
||||
|
||||
# Dynamic paths for multi-net congestion
|
||||
self.dynamic_index = rtree.index.Index()
|
||||
# obj_id -> (net_id, raw_geometry)
|
||||
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
# obj_id -> dilated_geometry (by clearance/2)
|
||||
self.dynamic_dilated: dict[int, Polygon] = {}
|
||||
self.dynamic_prepared: dict[int, PreparedGeometry] = {}
|
||||
self._dynamic_id_counter = 0
|
||||
|
||||
def add_static_obstacle(self, polygon: Polygon) -> None:
|
||||
"""
|
||||
Add a static obstacle to the engine.
|
||||
|
||||
Args:
|
||||
polygon: Raw obstacle geometry.
|
||||
"""
|
||||
obj_id = self._static_id_counter
|
||||
self._static_id_counter += 1
|
||||
|
||||
dilated = polygon.buffer(self.clearance)
|
||||
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)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
||||
"""
|
||||
Add a net's routed path to the dynamic index.
|
||||
|
||||
Args:
|
||||
net_id: Identifier for the net.
|
||||
geometry: List of raw polygons in the path.
|
||||
dilated_geometry: Optional list of pre-dilated polygons (by clearance/2).
|
||||
"""
|
||||
dilation = self.clearance / 2.0
|
||||
for i, poly in enumerate(geometry):
|
||||
obj_id = self._dynamic_id_counter
|
||||
self._dynamic_id_counter += 1
|
||||
|
||||
dil = dilated_geometry[i] if dilated_geometry else poly.buffer(dilation)
|
||||
|
||||
self.dynamic_geometries[obj_id] = (net_id, poly)
|
||||
self.dynamic_dilated[obj_id] = dil
|
||||
self.dynamic_prepared[obj_id] = prep(dil)
|
||||
self.dynamic_index.insert(obj_id, dil.bounds)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
"""
|
||||
Remove a net's path from the dynamic index.
|
||||
|
||||
Args:
|
||||
net_id: Identifier for the net to remove.
|
||||
"""
|
||||
to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||
for obj_id in to_remove:
|
||||
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||
dilated = self.dynamic_dilated.pop(obj_id)
|
||||
self.dynamic_prepared.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, dilated.bounds)
|
||||
|
||||
def lock_net(self, net_id: str) -> None:
|
||||
"""
|
||||
Move a net's dynamic path to static obstacles permanently.
|
||||
|
||||
Args:
|
||||
net_id: Identifier for the net to lock.
|
||||
"""
|
||||
to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||
for obj_id in to_move:
|
||||
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||
dilated = self.dynamic_dilated.pop(obj_id)
|
||||
self.dynamic_prepared.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, dilated.bounds)
|
||||
# Re-buffer for static clearance if necessary.
|
||||
# Note: dynamic is clearance/2, static is clearance.
|
||||
self.add_static_obstacle(poly)
|
||||
|
||||
def is_collision(
|
||||
self,
|
||||
geometry: Polygon,
|
||||
net_width: float = 2.0,
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Alias for check_collision(buffer_mode='static') for backward compatibility.
|
||||
"""
|
||||
_ = net_width
|
||||
res = self.check_collision(geometry, 'default', buffer_mode='static', start_port=start_port, end_port=end_port)
|
||||
return bool(res)
|
||||
|
||||
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
|
||||
"""
|
||||
Alias for check_collision(buffer_mode='congestion') for backward compatibility.
|
||||
"""
|
||||
res = self.check_collision(geometry, net_id, buffer_mode='congestion')
|
||||
return int(res)
|
||||
|
||||
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,
|
||||
) -> bool | int:
|
||||
"""
|
||||
Check for collisions using unified dilation logic.
|
||||
|
||||
Args:
|
||||
geometry: Raw geometry to check.
|
||||
net_id: Identifier for the net.
|
||||
buffer_mode: 'static' (full clearance) or 'congestion' (shared).
|
||||
start_port: Optional start port for safety zone.
|
||||
end_port: Optional end port for safety zone.
|
||||
dilated_geometry: Optional pre-buffered geometry (clearance/2).
|
||||
|
||||
Returns:
|
||||
Boolean if static, integer count if congestion.
|
||||
"""
|
||||
if buffer_mode == 'static':
|
||||
# Use raw query against pre-dilated obstacles
|
||||
candidates = self.static_index.intersection(geometry.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
if self.static_prepared[obj_id].intersects(geometry):
|
||||
if start_port or end_port:
|
||||
# Optimization: Skip expensive intersection if neither port is near the obstacle's bounds
|
||||
# (Plus a small margin for safety zone)
|
||||
sz = self.safety_zone_radius
|
||||
is_near_port = False
|
||||
for p in [start_port, end_port]:
|
||||
if p:
|
||||
# Quick bounds check
|
||||
b = self.static_dilated[obj_id].bounds
|
||||
if (b[0] - sz <= p.x <= b[2] + sz and
|
||||
b[1] - sz <= p.y <= b[3] + sz):
|
||||
is_near_port = True
|
||||
break
|
||||
|
||||
if not is_near_port:
|
||||
return True # Collision, and not near any port safety zone
|
||||
|
||||
# Only if near port, do the expensive check
|
||||
raw_obstacle = self.static_geometries[obj_id]
|
||||
intersection = geometry.intersection(raw_obstacle)
|
||||
if not intersection.is_empty:
|
||||
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
|
||||
is_safe = False
|
||||
for p in [start_port, end_port]:
|
||||
if p and (abs(ix_minx - p.x) < sz and
|
||||
abs(ix_maxx - p.x) < sz and
|
||||
abs(ix_miny - p.y) < sz and
|
||||
abs(ix_maxy - p.y) < sz):
|
||||
is_safe = True
|
||||
break
|
||||
|
||||
if is_safe:
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
# buffer_mode == 'congestion'
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = dilated_geometry if dilated_geometry else geometry.buffer(dilation)
|
||||
candidates = self.dynamic_index.intersection(test_poly.bounds)
|
||||
|
||||
count = 0
|
||||
for obj_id in candidates:
|
||||
other_net_id, _ = self.dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id and self.dynamic_prepared[obj_id].intersects(test_poly):
|
||||
count += 1
|
||||
return count
|
||||
|
|
@ -1,491 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
import numpy
|
||||
import shapely
|
||||
from shapely.geometry import Polygon, box
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from .primitives import Port
|
||||
|
||||
|
||||
|
||||
# Search Grid Snap (1.0 µm)
|
||||
SEARCH_GRID_SNAP_UM = 1.0
|
||||
|
||||
|
||||
def snap_search_grid(value: float) -> float:
|
||||
"""
|
||||
Snap a coordinate to the nearest search grid unit.
|
||||
|
||||
Args:
|
||||
value: Value to snap.
|
||||
|
||||
Returns:
|
||||
Snapped value.
|
||||
"""
|
||||
return round(value / SEARCH_GRID_SNAP_UM) * SEARCH_GRID_SNAP_UM
|
||||
|
||||
|
||||
class ComponentResult:
|
||||
"""
|
||||
The result of a component generation: geometry, final port, and physical length.
|
||||
"""
|
||||
__slots__ = ('geometry', 'dilated_geometry', 'end_port', 'length', 'bounds', 'dilated_bounds')
|
||||
|
||||
geometry: list[Polygon]
|
||||
""" List of polygons representing the component geometry """
|
||||
|
||||
dilated_geometry: list[Polygon] | None
|
||||
""" Optional list of pre-dilated polygons for collision optimization """
|
||||
|
||||
end_port: Port
|
||||
""" The final port after the component """
|
||||
|
||||
length: float
|
||||
""" Physical length of the component path """
|
||||
|
||||
bounds: numpy.ndarray
|
||||
""" Pre-calculated bounds for each polygon in geometry """
|
||||
|
||||
dilated_bounds: numpy.ndarray | None
|
||||
""" Pre-calculated bounds for each polygon in dilated_geometry """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
geometry: list[Polygon],
|
||||
end_port: Port,
|
||||
length: float,
|
||||
dilated_geometry: list[Polygon] | None = None,
|
||||
) -> None:
|
||||
self.geometry = geometry
|
||||
self.dilated_geometry = dilated_geometry
|
||||
self.end_port = end_port
|
||||
self.length = length
|
||||
# Vectorized bounds calculation
|
||||
self.bounds = shapely.bounds(geometry)
|
||||
self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry is not None else None
|
||||
|
||||
def translate(self, dx: float, dy: float) -> ComponentResult:
|
||||
"""
|
||||
Create a new ComponentResult translated by (dx, dy).
|
||||
"""
|
||||
# Vectorized translation if possible, else list comp
|
||||
# Shapely 2.x affinity functions still work on single geometries efficiently
|
||||
geoms = list(self.geometry)
|
||||
num_geom = len(self.geometry)
|
||||
if self.dilated_geometry is not None:
|
||||
geoms.extend(self.dilated_geometry)
|
||||
|
||||
from shapely.affinity import translate
|
||||
translated = [translate(p, dx, dy) for p in geoms]
|
||||
|
||||
new_geom = translated[:num_geom]
|
||||
new_dil = translated[num_geom:] if self.dilated_geometry is not None else None
|
||||
|
||||
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
|
||||
return ComponentResult(new_geom, new_port, self.length, new_dil)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a straight waveguide segment.
|
||||
|
||||
Args:
|
||||
start_port: Port to start from.
|
||||
length: Requested length.
|
||||
width: Waveguide width.
|
||||
snap_to_grid: Whether to snap the end port to the search grid.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the straight 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)
|
||||
ey = snap_search_grid(ey)
|
||||
|
||||
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)]
|
||||
|
||||
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
radius: Arc radius.
|
||||
angle_deg: Total angle turned.
|
||||
sagitta: Maximum allowed deviation.
|
||||
|
||||
Returns:
|
||||
Minimum number of segments needed.
|
||||
"""
|
||||
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 < 1e-9:
|
||||
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.
|
||||
|
||||
Args:
|
||||
cx, cy: Center coordinates.
|
||||
radius: Arc radius.
|
||||
width: Waveguide width.
|
||||
t_start, t_end: Start and end angles (radians).
|
||||
sagitta: Geometric fidelity.
|
||||
dilation: Optional dilation to apply directly to the arc.
|
||||
|
||||
Returns:
|
||||
List containing the arc polygon.
|
||||
"""
|
||||
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(
|
||||
bbox: Polygon,
|
||||
cx: float,
|
||||
cy: float,
|
||||
radius: float,
|
||||
width: float,
|
||||
clip_margin: float,
|
||||
arc_poly: Polygon,
|
||||
) -> Polygon:
|
||||
"""
|
||||
Clips corners of a bounding box for better collision modeling using direct vertex manipulation.
|
||||
"""
|
||||
# Determination of which corners to clip
|
||||
ac = arc_poly.centroid
|
||||
qsx = 1.0 if ac.x >= cx else -1.0
|
||||
qsy = 1.0 if ac.y >= cy else -1.0
|
||||
|
||||
r_out_cut = radius + width / 2.0 + clip_margin
|
||||
r_in_cut = radius - width / 2.0 - clip_margin
|
||||
|
||||
minx, miny, maxx, maxy = bbox.bounds
|
||||
# Initial vertices: [minx,miny], [maxx,miny], [maxx,maxy], [minx,maxy]
|
||||
verts = [
|
||||
numpy.array([minx, miny]),
|
||||
numpy.array([maxx, miny]),
|
||||
numpy.array([maxx, maxy]),
|
||||
numpy.array([minx, maxy])
|
||||
]
|
||||
|
||||
new_verts = []
|
||||
for p in verts:
|
||||
dx, dy = p[0] - cx, p[1] - cy
|
||||
dist = numpy.sqrt(dx**2 + dy**2)
|
||||
|
||||
# Normal vector components from center to corner
|
||||
sx = 1.0 if dx > 1e-6 else (-1.0 if dx < -1e-6 else qsx)
|
||||
sy = 1.0 if dy > 1e-6 else (-1.0 if dy < -1e-6 else qsy)
|
||||
|
||||
d_line = -1.0
|
||||
if dist > r_out_cut:
|
||||
d_line = r_out_cut * numpy.sqrt(2)
|
||||
elif r_in_cut > 0 and dist < r_in_cut:
|
||||
d_line = r_in_cut
|
||||
|
||||
if d_line > 0:
|
||||
# This corner needs clipping. Replace one vertex with two at intersection of line and edges.
|
||||
# Line: sx*(x-cx) + sy*(y-cy) = d_line
|
||||
# Edge x=px: y = cy + (d_line - sx*(px-cx))/sy
|
||||
# Edge y=py: x = cx + (d_line - sy*(py-cy))/sx
|
||||
try:
|
||||
p_edge_x = numpy.array([p[0], cy + (d_line - sx * (p[0] - cx)) / sy])
|
||||
p_edge_y = numpy.array([cx + (d_line - sy * (p[1] - cy)) / sx, p[1]])
|
||||
# Order matters for polygon winding.
|
||||
# If we are at [minx, miny] and moving CCW towards [maxx, miny]:
|
||||
# If we clip this corner, we should add p_edge_y then p_edge_x (or vice versa depending on orientation)
|
||||
# For simplicity, we can just add both and let Polygon sort it out if it's convex,
|
||||
# but better to be precise.
|
||||
# Since we know the bounding box orientation, we can determine order.
|
||||
# BUT: Difference was safer. Let's try a simpler approach:
|
||||
# Just collect all possible vertices and use convex_hull if it's guaranteed convex.
|
||||
# A clipped bbox is always convex.
|
||||
new_verts.append(p_edge_x)
|
||||
new_verts.append(p_edge_y)
|
||||
except ZeroDivisionError:
|
||||
new_verts.append(p)
|
||||
else:
|
||||
new_verts.append(p)
|
||||
|
||||
return Polygon(new_verts).convex_hull
|
||||
|
||||
|
||||
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,
|
||||
) -> list[Polygon]:
|
||||
"""
|
||||
Applies the specified collision model to an arc geometry.
|
||||
|
||||
Args:
|
||||
arc_poly: High-fidelity arc.
|
||||
collision_type: Model type or custom polygon.
|
||||
radius: Arc radius.
|
||||
width: Waveguide width.
|
||||
cx, cy: Arc center.
|
||||
clip_margin: Safety margin for clipping.
|
||||
|
||||
Returns:
|
||||
List of polygons representing the collision model.
|
||||
"""
|
||||
if isinstance(collision_type, Polygon):
|
||||
return [collision_type]
|
||||
|
||||
if collision_type == "arc":
|
||||
return [arc_poly]
|
||||
|
||||
# Get bounding box
|
||||
minx, miny, maxx, maxy = arc_poly.bounds
|
||||
bbox = box(minx, miny, maxx, maxy)
|
||||
|
||||
if collision_type == "bbox":
|
||||
return [bbox]
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
return [_clip_bbox(bbox, cx, cy, radius, width, clip_margin, arc_poly)]
|
||||
|
||||
return [arc_poly]
|
||||
|
||||
|
||||
class Bend90:
|
||||
"""
|
||||
Move generator for 90-degree bends.
|
||||
"""
|
||||
@staticmethod
|
||||
def generate(
|
||||
start_port: Port,
|
||||
radius: float,
|
||||
width: float,
|
||||
direction: str = "CW",
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0,
|
||||
dilation: float = 0.0,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a 90-degree bend.
|
||||
|
||||
Args:
|
||||
start_port: Port to start from.
|
||||
radius: Bend radius.
|
||||
width: Waveguide width.
|
||||
direction: "CW" or "CCW".
|
||||
sagitta: Geometric fidelity.
|
||||
collision_type: Collision model.
|
||||
clip_margin: Margin for clipped_bbox.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the bend.
|
||||
"""
|
||||
turn_angle = -90 if direction == "CW" else 90
|
||||
rad_start = numpy.radians(start_port.orientation)
|
||||
c_angle = rad_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
||||
cx = start_port.x + radius * numpy.cos(c_angle)
|
||||
cy = start_port.y + radius * numpy.sin(c_angle)
|
||||
t_start = c_angle + numpy.pi
|
||||
t_end = t_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
||||
|
||||
ex = snap_search_grid(cx + radius * numpy.cos(t_end))
|
||||
ey = snap_search_grid(cy + radius * numpy.sin(t_end))
|
||||
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
|
||||
|
||||
arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta)
|
||||
collision_polys = _apply_collision_model(
|
||||
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
|
||||
)
|
||||
|
||||
dilated_geom = None
|
||||
if dilation > 0:
|
||||
if collision_type == "arc":
|
||||
dilated_geom = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta, dilation=dilation)
|
||||
else:
|
||||
# For bbox or clipped_bbox, buffer the model itself (which is simpler than buffering the high-fidelity arc)
|
||||
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||
|
||||
return ComponentResult(
|
||||
geometry=collision_polys,
|
||||
end_port=end_port,
|
||||
length=radius * numpy.pi / 2.0,
|
||||
dilated_geometry=dilated_geom
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a parametric S-bend (two tangent arcs).
|
||||
|
||||
Args:
|
||||
start_port: Port to start from.
|
||||
offset: Lateral offset.
|
||||
radius: Arc radii.
|
||||
width: Waveguide width.
|
||||
sagitta: Geometric fidelity.
|
||||
collision_type: Collision model.
|
||||
clip_margin: Margin for clipped_bbox.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the S-bend.
|
||||
"""
|
||||
if abs(offset) >= 2 * radius:
|
||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||
|
||||
theta = numpy.arccos(1 - abs(offset) / (2 * radius))
|
||||
dx = 2 * radius * numpy.sin(theta)
|
||||
dy = offset
|
||||
rad_start = numpy.radians(start_port.orientation)
|
||||
ex = snap_search_grid(start_port.x + dx * numpy.cos(rad_start) - dy * numpy.sin(rad_start))
|
||||
ey = snap_search_grid(start_port.y + dx * numpy.sin(rad_start) + dy * numpy.cos(rad_start))
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
direction = 1 if offset > 0 else -1
|
||||
c1_angle = rad_start + direction * numpy.pi / 2
|
||||
cx1 = start_port.x + radius * numpy.cos(c1_angle)
|
||||
cy1 = start_port.y + radius * numpy.sin(c1_angle)
|
||||
ts1, te1 = c1_angle + numpy.pi, c1_angle + numpy.pi + direction * theta
|
||||
|
||||
ex_raw = start_port.x + dx * numpy.cos(rad_start) - dy * numpy.sin(rad_start)
|
||||
ey_raw = start_port.y + dx * numpy.sin(rad_start) + dy * numpy.cos(rad_start)
|
||||
c2_angle = rad_start - direction * numpy.pi / 2
|
||||
cx2 = ex_raw + radius * numpy.cos(c2_angle)
|
||||
cy2 = ey_raw + radius * numpy.sin(c2_angle)
|
||||
te2 = c2_angle + numpy.pi
|
||||
ts2 = te2 + direction * theta
|
||||
|
||||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
||||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
||||
# Optimization: keep as list instead of unary_union for search efficiency
|
||||
collision_polys = [col1, col2]
|
||||
else:
|
||||
# For other models, we can either combine or keep separate.
|
||||
# Keeping separate is generally better for CollisionEngine.
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
||||
collision_polys = [col1, col2]
|
||||
|
||||
dilated_geom = None
|
||||
if dilation > 0:
|
||||
if collision_type == "arc":
|
||||
d1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta, dilation=dilation)[0]
|
||||
d2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta, dilation=dilation)[0]
|
||||
dilated_geom = [d1, d2]
|
||||
else:
|
||||
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||
|
||||
return ComponentResult(
|
||||
geometry=collision_polys,
|
||||
end_port=end_port,
|
||||
length=2 * radius * theta,
|
||||
dilated_geometry=dilated_geom
|
||||
)
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
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).
|
||||
|
||||
Args:
|
||||
value: Coordinate value to snap.
|
||||
|
||||
Returns:
|
||||
Snapped coordinate value.
|
||||
"""
|
||||
return round(value / GRID_SNAP_UM) * GRID_SNAP_UM
|
||||
|
||||
|
||||
class Port:
|
||||
"""
|
||||
A port defined by (x, y, orientation) in micrometers.
|
||||
"""
|
||||
__slots__ = ('x', 'y', 'orientation')
|
||||
|
||||
x: float
|
||||
""" x-coordinate in micrometers """
|
||||
|
||||
y: float
|
||||
""" y-coordinate in micrometers """
|
||||
|
||||
orientation: float
|
||||
""" Orientation in degrees: 0, 90, 180, 270 """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
x: float,
|
||||
y: float,
|
||||
orientation: float,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize and snap a Port.
|
||||
|
||||
Args:
|
||||
x: Initial x-coordinate.
|
||||
y: Initial y-coordinate.
|
||||
orientation: Initial orientation in degrees.
|
||||
"""
|
||||
# Snap x, y to 1nm
|
||||
self.x = snap_nm(x)
|
||||
self.y = snap_nm(y)
|
||||
|
||||
# Ensure orientation is one of {0, 90, 180, 270}
|
||||
norm_orientation = int(round(orientation)) % 360
|
||||
if norm_orientation not in {0, 90, 180, 270}:
|
||||
norm_orientation = (round(norm_orientation / 90) * 90) % 360
|
||||
|
||||
self.orientation = float(norm_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 (self.x == other.x and
|
||||
self.y == other.y and
|
||||
self.orientation == other.orientation)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.x, self.y, self.orientation))
|
||||
|
||||
|
||||
def translate_port(port: Port, dx: float, dy: float) -> Port:
|
||||
"""
|
||||
Translate a port by (dx, dy).
|
||||
|
||||
Args:
|
||||
port: Port to translate.
|
||||
dx: x-offset.
|
||||
dy: y-offset.
|
||||
|
||||
Returns:
|
||||
A new translated Port.
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
port: Port to rotate.
|
||||
angle: Angle to rotate by (degrees).
|
||||
origin: (x, y) origin to rotate around.
|
||||
|
||||
Returns:
|
||||
A new rotated Port.
|
||||
"""
|
||||
ox, oy = origin
|
||||
px, py = port.x, port.y
|
||||
|
||||
rad = numpy.radians(angle)
|
||||
qx = ox + numpy.cos(rad) * (px - ox) - numpy.sin(rad) * (py - oy)
|
||||
qy = oy + numpy.sin(rad) * (px - ox) + numpy.cos(rad) * (py - oy)
|
||||
|
||||
return Port(qx, qy, port.orientation + angle)
|
||||
|
|
@ -1,548 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
import functools
|
||||
from typing import TYPE_CHECKING, Literal, Any
|
||||
import rtree
|
||||
|
||||
import numpy
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.config import RouterConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
from inire.router.cost import CostEvaluator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class AStarNode:
|
||||
"""
|
||||
A node in the A* search graph.
|
||||
"""
|
||||
__slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result', 'count', 'path_bbox')
|
||||
|
||||
port: Port
|
||||
""" Port representing the state at this node """
|
||||
|
||||
g_cost: float
|
||||
""" Actual cost from start to this node """
|
||||
|
||||
h_cost: float
|
||||
""" Heuristic cost from this node to target """
|
||||
|
||||
f_cost: float
|
||||
""" Total estimated cost (g + h) """
|
||||
|
||||
parent: AStarNode | None
|
||||
""" Parent node in the search tree """
|
||||
|
||||
component_result: ComponentResult | None
|
||||
""" The component move that led to this node """
|
||||
|
||||
count: int
|
||||
""" Unique insertion order for tie-breaking """
|
||||
|
||||
path_bbox: tuple[float, float, float, float] | None
|
||||
""" Bounding box of the entire path up to this node """
|
||||
|
||||
_count = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: Port,
|
||||
g_cost: float,
|
||||
h_cost: float,
|
||||
parent: AStarNode | None = None,
|
||||
component_result: ComponentResult | None = None,
|
||||
) -> None:
|
||||
self.port = port
|
||||
self.g_cost = g_cost
|
||||
self.h_cost = h_cost
|
||||
self.f_cost = g_cost + h_cost
|
||||
self.parent = parent
|
||||
self.component_result = component_result
|
||||
self.count = AStarNode._count
|
||||
AStarNode._count += 1
|
||||
|
||||
# Calculate path_bbox
|
||||
if parent is None:
|
||||
self.path_bbox = None
|
||||
else:
|
||||
# Union of parent's bbox and current move's bbox
|
||||
if component_result:
|
||||
# Merge all polygon bounds in the result
|
||||
minx, miny, maxx, maxy = 1e15, 1e15, -1e15, -1e15
|
||||
for b in component_result.dilated_bounds if component_result.dilated_bounds is not None else component_result.bounds:
|
||||
minx = min(minx, b[0])
|
||||
miny = min(miny, b[1])
|
||||
maxx = max(maxx, b[2])
|
||||
maxy = max(maxy, b[3])
|
||||
|
||||
if parent.path_bbox:
|
||||
self.path_bbox = (
|
||||
min(minx, parent.path_bbox[0]),
|
||||
min(miny, parent.path_bbox[1]),
|
||||
max(maxx, parent.path_bbox[2]),
|
||||
max(maxy, parent.path_bbox[3])
|
||||
)
|
||||
else:
|
||||
self.path_bbox = (minx, miny, maxx, maxy)
|
||||
else:
|
||||
self.path_bbox = parent.path_bbox
|
||||
|
||||
|
||||
|
||||
|
||||
def __lt__(self, other: AStarNode) -> bool:
|
||||
# Tie-breaking: lower f first, then lower h, then order
|
||||
if abs(self.f_cost - other.f_cost) > 1e-9:
|
||||
return self.f_cost < other.f_cost
|
||||
if abs(self.h_cost - other.h_cost) > 1e-9:
|
||||
return self.h_cost < other.h_cost
|
||||
return self.count < other.count
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, AStarNode):
|
||||
return False
|
||||
return self.count == other.count
|
||||
|
||||
|
||||
class AStarRouter:
|
||||
"""
|
||||
Hybrid State-Lattice A* Router.
|
||||
"""
|
||||
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'total_nodes_expanded', '_collision_cache', '_move_cache', '_self_dilation')
|
||||
|
||||
cost_evaluator: CostEvaluator
|
||||
""" The evaluator for path and proximity costs """
|
||||
|
||||
config: RouterConfig
|
||||
""" Search configuration parameters """
|
||||
|
||||
node_limit: int
|
||||
""" Maximum nodes to expand before failure """
|
||||
|
||||
total_nodes_expanded: int
|
||||
""" Counter for debugging/profiling """
|
||||
|
||||
_collision_cache: dict[tuple[float, float, float, str, float, str], bool]
|
||||
""" Internal cache for move collision checks """
|
||||
|
||||
_move_cache: dict[tuple[Any, ...], ComponentResult]
|
||||
""" Internal cache for component generation """
|
||||
|
||||
_self_dilation: float
|
||||
""" Cached dilation value for collision checks (clearance / 2.0) """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cost_evaluator: CostEvaluator,
|
||||
node_limit: int = 1000000,
|
||||
straight_lengths: list[float] | None = None,
|
||||
bend_radii: list[float] | None = None,
|
||||
sbend_offsets: list[float] | None = None,
|
||||
sbend_radii: list[float] | None = None,
|
||||
snap_to_target_dist: float = 20.0,
|
||||
bend_penalty: float = 50.0,
|
||||
sbend_penalty: float = 100.0,
|
||||
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] = 'arc',
|
||||
bend_clip_margin: float = 10.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the A* Router.
|
||||
|
||||
Args:
|
||||
cost_evaluator: Path cost evaluator.
|
||||
node_limit: Node expansion limit.
|
||||
straight_lengths: Allowed straight lengths (um).
|
||||
bend_radii: Allowed 90-deg radii (um).
|
||||
sbend_offsets: Allowed S-bend lateral offsets (um).
|
||||
sbend_radii: Allowed S-bend radii (um).
|
||||
snap_to_target_dist: Radius for target lookahead (um).
|
||||
bend_penalty: Penalty for 90-degree turns.
|
||||
sbend_penalty: Penalty for S-bends.
|
||||
bend_collision_type: Collision model for bends.
|
||||
bend_clip_margin: Margin for clipped_bbox model.
|
||||
"""
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.config = RouterConfig(
|
||||
node_limit=node_limit,
|
||||
straight_lengths=straight_lengths if straight_lengths is not None else [1.0, 5.0, 25.0, 100.0],
|
||||
bend_radii=bend_radii if bend_radii is not None else [10.0],
|
||||
sbend_offsets=sbend_offsets if sbend_offsets is not None else [-5.0, -2.0, 2.0, 5.0],
|
||||
sbend_radii=sbend_radii if sbend_radii is not None else [10.0],
|
||||
snap_to_target_dist=snap_to_target_dist,
|
||||
bend_penalty=bend_penalty,
|
||||
sbend_penalty=sbend_penalty,
|
||||
bend_collision_type=bend_collision_type,
|
||||
bend_clip_margin=bend_clip_margin,
|
||||
)
|
||||
self.node_limit = self.config.node_limit
|
||||
self.total_nodes_expanded = 0
|
||||
self._collision_cache = {}
|
||||
self._move_cache = {}
|
||||
self._self_dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
|
||||
def route(
|
||||
self,
|
||||
start: Port,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str = 'default',
|
||||
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None,
|
||||
) -> list[ComponentResult] | None:
|
||||
"""
|
||||
Route a single net using A*.
|
||||
|
||||
Args:
|
||||
start: Starting port.
|
||||
target: Target port.
|
||||
net_width: Waveguide width (um).
|
||||
net_id: Optional net identifier.
|
||||
bend_collision_type: Override collision model for this route.
|
||||
|
||||
Returns:
|
||||
List of moves forming the path, or None if failed.
|
||||
"""
|
||||
if bend_collision_type is not None:
|
||||
self.config.bend_collision_type = bend_collision_type
|
||||
|
||||
self._collision_cache.clear()
|
||||
open_set: list[AStarNode] = []
|
||||
# Key: (x, y, orientation) rounded to 1nm
|
||||
closed_set: set[tuple[float, float, float]] = set()
|
||||
|
||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||
heapq.heappush(open_set, start_node)
|
||||
|
||||
nodes_expanded = 0
|
||||
|
||||
while open_set:
|
||||
if nodes_expanded >= self.node_limit:
|
||||
logger.warning(f' AStar failed: node limit {self.node_limit} reached.')
|
||||
return None
|
||||
|
||||
current = heapq.heappop(open_set)
|
||||
|
||||
# Prune if already visited
|
||||
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2))
|
||||
if state in closed_set:
|
||||
continue
|
||||
closed_set.add(state)
|
||||
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
|
||||
if nodes_expanded % 5000 == 0:
|
||||
logger.info(f'Nodes expanded: {nodes_expanded}, current: {current.port}, g: {current.g_cost:.1f}')
|
||||
|
||||
# Check if we reached the target exactly
|
||||
if (abs(current.port.x - target.x) < 1e-6 and
|
||||
abs(current.port.y - target.y) < 1e-6 and
|
||||
abs(current.port.orientation - target.orientation) < 0.1):
|
||||
return self._reconstruct_path(current)
|
||||
|
||||
# Expansion
|
||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set)
|
||||
|
||||
return None
|
||||
|
||||
def _expand_moves(
|
||||
self,
|
||||
current: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
) -> None:
|
||||
# 1. Snap-to-Target Look-ahead
|
||||
dist = numpy.sqrt((current.port.x - target.x)**2 + (current.port.y - target.y)**2)
|
||||
if dist < self.config.snap_to_target_dist:
|
||||
# A. Try straight exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = numpy.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
|
||||
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight')
|
||||
|
||||
# B. Try SBend exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = numpy.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
|
||||
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
|
||||
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Move Cache
|
||||
cp = current.port
|
||||
base_ori = round(cp.orientation % 360, 2)
|
||||
state_key = (round(cp.x, 3), round(cp.y, 3), base_ori)
|
||||
|
||||
# 2. Lattice Straights
|
||||
lengths = self.config.straight_lengths
|
||||
if dist < 5.0:
|
||||
fine_steps = [0.1, 0.5]
|
||||
lengths = sorted(set(lengths + fine_steps))
|
||||
|
||||
for length in lengths:
|
||||
# Level 1: Absolute cache (exact location)
|
||||
abs_key = (state_key, 'S', length, net_width)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
else:
|
||||
# Level 2: Relative cache (orientation only)
|
||||
# Dilation is now 0.0 for caching to save translation time.
|
||||
# It will be calculated lazily in _add_node if needed.
|
||||
rel_key = (base_ori, 'S', length, net_width, 0.0)
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
# Check closed set before translating
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=0.0)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}')
|
||||
|
||||
# 3. Lattice Bends
|
||||
for radius in self.config.bend_radii:
|
||||
for direction in ['CW', 'CCW']:
|
||||
abs_key = (state_key, 'B', radius, direction, net_width, self.config.bend_collision_type)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
else:
|
||||
rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, 0.0)
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
# Check closed set before translating
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
res_rel = Bend90.generate(
|
||||
Port(0, 0, base_ori),
|
||||
radius,
|
||||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=0.0
|
||||
)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'B{radius}{direction}', move_radius=radius)
|
||||
|
||||
# 4. Discrete SBends
|
||||
for offset in self.config.sbend_offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
abs_key = (state_key, 'SB', offset, radius, net_width, self.config.bend_collision_type)
|
||||
if abs_key in self._move_cache:
|
||||
res = self._move_cache[abs_key]
|
||||
else:
|
||||
rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, 0.0)
|
||||
if rel_key in self._move_cache:
|
||||
res_rel = self._move_cache[rel_key]
|
||||
# Check closed set before translating
|
||||
ex = res_rel.end_port.x + cp.x
|
||||
ey = res_rel.end_port.y + cp.y
|
||||
end_state = (round(ex, 3), round(ey, 3), round(res_rel.end_port.orientation, 2))
|
||||
if end_state in closed_set:
|
||||
continue
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
else:
|
||||
try:
|
||||
res_rel = SBend.generate(
|
||||
Port(0, 0, base_ori),
|
||||
offset,
|
||||
radius,
|
||||
width=net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=0.0
|
||||
)
|
||||
self._move_cache[rel_key] = res_rel
|
||||
res = res_rel.translate(cp.x, cp.y)
|
||||
except ValueError:
|
||||
continue
|
||||
self._move_cache[abs_key] = res
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius)
|
||||
|
||||
def _add_node(
|
||||
self,
|
||||
parent: AStarNode,
|
||||
result: ComponentResult,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
move_type: str,
|
||||
move_radius: float | None = None,
|
||||
) -> None:
|
||||
# Check closed set before adding to open set
|
||||
state = (round(result.end_port.x, 3), round(result.end_port.y, 3), round(result.end_port.orientation, 2))
|
||||
if state in closed_set:
|
||||
return
|
||||
|
||||
cache_key = (
|
||||
round(parent.port.x, 3),
|
||||
round(parent.port.y, 3),
|
||||
round(parent.port.orientation, 2),
|
||||
move_type,
|
||||
net_width,
|
||||
net_id,
|
||||
)
|
||||
if cache_key in self._collision_cache:
|
||||
if self._collision_cache[cache_key]:
|
||||
return
|
||||
else:
|
||||
# Lazy Dilation: compute dilated polygons only if we need a collision check
|
||||
if result.dilated_geometry is None:
|
||||
# We need to update the ComponentResult with dilated geometry
|
||||
# For simplicity, we'll just buffer the polygons here.
|
||||
# In a more optimized version, ComponentResult might have a .dilate() method.
|
||||
dilated = [p.buffer(self._self_dilation) for p in result.geometry]
|
||||
result.dilated_geometry = dilated
|
||||
# Re-calculate dilated bounds
|
||||
import shapely
|
||||
result.dilated_bounds = shapely.bounds(dilated)
|
||||
|
||||
hard_coll = False
|
||||
for i, poly in enumerate(result.geometry):
|
||||
dil_poly = result.dilated_geometry[i]
|
||||
if self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='static', start_port=parent.port, end_port=result.end_port,
|
||||
dilated_geometry=dil_poly
|
||||
):
|
||||
hard_coll = True
|
||||
break
|
||||
self._collision_cache[cache_key] = hard_coll
|
||||
if hard_coll:
|
||||
return
|
||||
|
||||
# Lazy Dilation for self-intersection and cost evaluation
|
||||
if result.dilated_geometry is None:
|
||||
dilated = [p.buffer(self._self_dilation) for p in result.geometry]
|
||||
result.dilated_geometry = dilated
|
||||
import shapely
|
||||
result.dilated_bounds = shapely.bounds(dilated)
|
||||
|
||||
# 3. Check for Self-Intersection (Limited to last 100 segments for performance)
|
||||
if result.dilated_geometry:
|
||||
# Union of current move's bounds for fast path-wide pruning
|
||||
m_minx, m_miny, m_maxx, m_maxy = 1e15, 1e15, -1e15, -1e15
|
||||
for b in result.dilated_bounds if result.dilated_bounds is not None else result.bounds:
|
||||
|
||||
m_minx = min(m_minx, b[0])
|
||||
m_miny = min(m_miny, b[1])
|
||||
m_maxx = max(m_maxx, b[2])
|
||||
m_maxy = max(m_maxy, b[3])
|
||||
|
||||
# If current move doesn't overlap the entire parent path bbox, we can skip individual checks
|
||||
# (Except the immediate parent which we usually skip anyway)
|
||||
if parent.path_bbox and not (m_minx > parent.path_bbox[2] or
|
||||
m_maxx < parent.path_bbox[0] or
|
||||
m_miny > parent.path_bbox[3] or
|
||||
m_maxy < parent.path_bbox[1]):
|
||||
|
||||
for dm_idx, dilated_move in enumerate(result.dilated_geometry):
|
||||
dm_bounds = result.dilated_bounds[dm_idx]
|
||||
curr_p: AStarNode | None = parent
|
||||
seg_idx = 0
|
||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
||||
# Skip immediate parent to avoid tangent/port-safety issues
|
||||
if seg_idx > 0:
|
||||
res_p = curr_p.component_result
|
||||
if res_p.dilated_geometry:
|
||||
for dp_idx, dilated_prev in enumerate(res_p.dilated_geometry):
|
||||
dp_bounds = res_p.dilated_bounds[dp_idx]
|
||||
# Quick bounds overlap check
|
||||
if not (dm_bounds[0] > dp_bounds[2] or
|
||||
dm_bounds[2] < dp_bounds[0] or
|
||||
dm_bounds[1] > dp_bounds[3] or
|
||||
dm_bounds[3] < dp_bounds[1]):
|
||||
# Use intersects() which is much faster than intersection()
|
||||
if dilated_move.intersects(dilated_prev):
|
||||
# Only do expensive area check if absolutely necessary
|
||||
overlap = dilated_move.intersection(dilated_prev)
|
||||
if not overlap.is_empty and overlap.area > 1e-6:
|
||||
return
|
||||
curr_p = curr_p.parent
|
||||
seg_idx += 1
|
||||
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
start_port=parent.port,
|
||||
length=result.length,
|
||||
dilated_geometry=result.dilated_geometry,
|
||||
skip_static=True
|
||||
)
|
||||
|
||||
if move_cost > 1e12:
|
||||
return
|
||||
|
||||
# Turn penalties scaled by radius to favor larger turns
|
||||
ref_radius = 10.0
|
||||
if 'B' in move_type and move_radius is not None:
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.bend_penalty * penalty_factor
|
||||
elif 'SB' in move_type and move_radius is not None:
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.sbend_penalty * penalty_factor
|
||||
elif 'B' in move_type:
|
||||
move_cost += self.config.bend_penalty
|
||||
elif 'SB' in move_type:
|
||||
move_cost += self.config.sbend_penalty
|
||||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||
|
||||
new_node = AStarNode(result.end_port, g_cost, h_cost, parent, result)
|
||||
heapq.heappush(open_set, new_node)
|
||||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
curr: AStarNode | None = end_node
|
||||
while curr and curr.component_result:
|
||||
path.append(curr.component_result)
|
||||
curr = curr.parent
|
||||
return path[::-1]
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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
|
||||
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0])
|
||||
bend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0])
|
||||
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||
snap_to_target_dist: float = 20.0
|
||||
bend_penalty: float = 50.0
|
||||
sbend_penalty: float = 100.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.1
|
||||
congestion_penalty: float = 10000.0
|
||||
bend_penalty: float = 50.0
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from inire.router.config import CostConfig
|
||||
|
||||
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')
|
||||
|
||||
collision_engine: CollisionEngine
|
||||
""" The engine for intersection checks """
|
||||
|
||||
danger_map: DangerMap
|
||||
""" Pre-computed grid for heuristic proximity costs """
|
||||
|
||||
config: CostConfig
|
||||
""" Parameter configuration """
|
||||
|
||||
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,
|
||||
unit_length_cost: float = 1.0,
|
||||
greedy_h_weight: float = 1.1,
|
||||
congestion_penalty: float = 10000.0,
|
||||
bend_penalty: 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.
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
return self.danger_map.get_cost(x, y)
|
||||
|
||||
def h_manhattan(self, current: Port, target: Port) -> float:
|
||||
"""
|
||||
Heuristic: weighted Manhattan distance + orientation penalty.
|
||||
|
||||
Args:
|
||||
current: Current port state.
|
||||
target: Target port state.
|
||||
|
||||
Returns:
|
||||
Heuristic cost estimate.
|
||||
"""
|
||||
dx = abs(current.x - target.x)
|
||||
dy = abs(current.y - target.y)
|
||||
dist = dx + dy
|
||||
|
||||
# Orientation penalty if not aligned with target entry
|
||||
# If we need to turn, the cost is at least min_bend_radius * pi/2
|
||||
# But we also need to account for the physical distance required for the turn.
|
||||
penalty = 0.0
|
||||
if current.orientation != target.orientation:
|
||||
# 90-degree turn cost: radius 10 -> ~15.7 um + penalty
|
||||
penalty += 15.7 + self.config.bend_penalty
|
||||
|
||||
# Add 1.5 multiplier for greediness (faster search)
|
||||
return 1.5 * (dist + penalty)
|
||||
|
||||
|
||||
def evaluate_move(
|
||||
self,
|
||||
geometry: list[Polygon],
|
||||
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,
|
||||
) -> 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 (e.g. if already done).
|
||||
|
||||
Returns:
|
||||
Total cost of the move, or 1e15 if invalid.
|
||||
"""
|
||||
_ = net_width # Unused
|
||||
total_cost = length * self.unit_length_cost
|
||||
|
||||
# 1. Boundary Check
|
||||
if not self.danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||
return 1e15
|
||||
|
||||
# 2. Collision Check
|
||||
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 self.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)
|
||||
overlaps = self.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
|
||||
total_cost += self.g_proximity(end_port.x, end_port.y)
|
||||
return total_cost
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
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.
|
||||
"""
|
||||
ix = int((x - self.minx) / self.resolution)
|
||||
iy = int((y - self.miny) / self.resolution)
|
||||
|
||||
if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells:
|
||||
return float(self.grid[ix, iy])
|
||||
return 1e15 # Outside bounds is impossible
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.components import ComponentResult
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoutingResult:
|
||||
"""
|
||||
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 """
|
||||
|
||||
collisions: int
|
||||
""" Number of detected collisions/overlaps """
|
||||
|
||||
|
||||
class PathFinder:
|
||||
"""
|
||||
Multi-net router using Negotiated Congestion.
|
||||
"""
|
||||
__slots__ = ('router', 'cost_evaluator', 'max_iterations', 'base_congestion_penalty')
|
||||
|
||||
router: AStarRouter
|
||||
""" The A* search engine """
|
||||
|
||||
cost_evaluator: CostEvaluator
|
||||
""" The evaluator for path costs """
|
||||
|
||||
max_iterations: int
|
||||
""" Maximum number of rip-up and reroute iterations """
|
||||
|
||||
base_congestion_penalty: float
|
||||
""" Starting penalty for overlaps """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
router: AStarRouter,
|
||||
cost_evaluator: CostEvaluator,
|
||||
max_iterations: int = 10,
|
||||
base_congestion_penalty: float = 100.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the PathFinder.
|
||||
|
||||
Args:
|
||||
router: The A* search engine.
|
||||
cost_evaluator: The evaluator for path costs.
|
||||
max_iterations: Maximum number of rip-up and reroute iterations.
|
||||
base_congestion_penalty: Starting penalty for overlaps.
|
||||
"""
|
||||
self.router = router
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.max_iterations = max_iterations
|
||||
self.base_congestion_penalty = base_congestion_penalty
|
||||
|
||||
def route_all(
|
||||
self,
|
||||
netlist: dict[str, tuple[Port, Port]],
|
||||
net_widths: dict[str, float],
|
||||
) -> dict[str, RoutingResult]:
|
||||
"""
|
||||
Route all nets in the netlist using Negotiated Congestion.
|
||||
|
||||
Args:
|
||||
netlist: Mapping of net_id to (start_port, target_port).
|
||||
net_widths: Mapping of net_id to waveguide width.
|
||||
|
||||
Returns:
|
||||
Mapping of net_id to RoutingResult.
|
||||
"""
|
||||
results: dict[str, RoutingResult] = {}
|
||||
self.cost_evaluator.congestion_penalty = self.base_congestion_penalty
|
||||
|
||||
start_time = time.monotonic()
|
||||
num_nets = len(netlist)
|
||||
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
any_congestion = False
|
||||
logger.info(f'PathFinder Iteration {iteration}...')
|
||||
|
||||
# Sequence through nets
|
||||
for net_id, (start, target) in netlist.items():
|
||||
# Timeout check
|
||||
elapsed = time.monotonic() - start_time
|
||||
if elapsed > session_timeout:
|
||||
logger.warning(f'PathFinder TIMEOUT after {elapsed:.2f}s')
|
||||
return self._finalize_results(results, netlist)
|
||||
|
||||
width = net_widths.get(net_id, 2.0)
|
||||
|
||||
# 1. Rip-up existing path
|
||||
self.cost_evaluator.collision_engine.remove_path(net_id)
|
||||
|
||||
# 2. Reroute with current congestion info
|
||||
# Tiered Strategy: use clipped_bbox for Iteration 0 for speed.
|
||||
# Switch to arc for higher iterations if collisions persist.
|
||||
coll_model = "clipped_bbox" if iteration == 0 else "arc"
|
||||
|
||||
net_start = time.monotonic()
|
||||
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model)
|
||||
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
|
||||
|
||||
if path:
|
||||
# 3. Add to index
|
||||
all_geoms = []
|
||||
all_dilated = []
|
||||
for res in path:
|
||||
all_geoms.extend(res.geometry)
|
||||
if res.dilated_geometry:
|
||||
all_dilated.extend(res.dilated_geometry)
|
||||
else:
|
||||
# Fallback dilation
|
||||
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
|
||||
for i, poly in enumerate(all_geoms):
|
||||
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=all_dilated[i]
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
collision_count += overlaps
|
||||
|
||||
if collision_count > 0:
|
||||
any_congestion = True
|
||||
|
||||
results[net_id] = RoutingResult(net_id, path, collision_count == 0, collision_count)
|
||||
else:
|
||||
results[net_id] = RoutingResult(net_id, [], False, 0)
|
||||
any_congestion = True
|
||||
|
||||
if not any_congestion:
|
||||
break
|
||||
|
||||
# 4. Inflate congestion penalty
|
||||
self.cost_evaluator.congestion_penalty *= 1.5
|
||||
|
||||
return self._finalize_results(results, netlist)
|
||||
|
||||
def _finalize_results(
|
||||
self,
|
||||
results: dict[str, RoutingResult],
|
||||
netlist: dict[str, tuple[Port, Port]],
|
||||
) -> dict[str, RoutingResult]:
|
||||
"""
|
||||
Final check: re-verify all nets against the final static paths.
|
||||
|
||||
Args:
|
||||
results: Results from the routing loop.
|
||||
netlist: The original netlist.
|
||||
|
||||
Returns:
|
||||
Refined results with final collision counts.
|
||||
"""
|
||||
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
|
||||
|
||||
collision_count = 0
|
||||
for comp in res.path:
|
||||
for i, poly in enumerate(comp.geometry):
|
||||
dil_poly = comp.dilated_geometry[i] if comp.dilated_geometry else None
|
||||
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
collision_count += overlaps
|
||||
|
||||
final_results[net_id] = RoutingResult(net_id, res.path, collision_count == 0, collision_count)
|
||||
|
||||
return final_results
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import time
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.pathfinder import PathFinder
|
||||
|
||||
def benchmark_scaling() -> None:
|
||||
print("Starting Scalability Benchmark...")
|
||||
|
||||
# 1. Memory Verification (20x20mm)
|
||||
# Resolution 1um -> 20000 x 20000 grid
|
||||
bounds = (0, 0, 20000, 20000)
|
||||
print(f"Initializing DangerMap for {bounds} area...")
|
||||
dm = DangerMap(bounds=bounds, resolution=1.0)
|
||||
# nbytes for float32: 20000 * 20000 * 4 bytes = 1.6 GB
|
||||
mem_gb = dm.grid.nbytes / (1024**3)
|
||||
print(f"DangerMap memory usage: {mem_gb:.2f} GB")
|
||||
assert mem_gb < 2.0
|
||||
|
||||
# 2. Node Expansion Rate (50 nets)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
# Use a smaller area for routing benchmark to keep it fast
|
||||
routing_bounds = (0, 0, 1000, 1000)
|
||||
danger_map = DangerMap(bounds=routing_bounds)
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
num_nets = 50
|
||||
netlist = {}
|
||||
for i in range(num_nets):
|
||||
# Parallel nets spaced by 10um
|
||||
netlist[f"net{i}"] = (Port(0, i * 10, 0), Port(100, i * 10, 0))
|
||||
|
||||
print(f"Routing {num_nets} nets...")
|
||||
start_time = time.monotonic()
|
||||
results = pf.route_all(netlist, dict.fromkeys(netlist, 2.0))
|
||||
end_time = time.monotonic()
|
||||
|
||||
total_time = end_time - start_time
|
||||
print(f"Total routing time: {total_time:.2f} s")
|
||||
print(f"Time per net: {total_time/num_nets:.4f} s")
|
||||
|
||||
if total_time > 0:
|
||||
nodes_per_sec = router.total_nodes_expanded / total_time
|
||||
print(f"Node expansion rate: {nodes_per_sec:.2f} nodes/s")
|
||||
|
||||
# Success rate
|
||||
successes = sum(1 for r in results.values() if r.is_valid)
|
||||
print(f"Success rate: {successes/num_nets * 100:.1f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark_scaling()
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
from inire.utils.validation import validate_routing_result
|
||||
|
||||
|
||||
@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_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
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:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
||||
# From (0,0,0) -> Bend90 CW R=10 -> (10, -10, 270) ??? No.
|
||||
# Try: (0,0,0) -> Bend90 CCW R=10 -> (10, 10, 90) -> Straight 10 -> (10, 20, 90) -> Bend90 CW R=10 -> (20, 30, 0)
|
||||
target = Port(20, 20, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
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])
|
||||
|
||||
router = AStarRouter(basic_evaluator)
|
||||
router.node_limit = 1000000 # Give it more room for detour
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(60, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
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:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Target is NOT on 1um grid
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(10.1, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
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')}"
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
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)
|
||||
|
||||
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")
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
# 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)
|
||||
# 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")
|
||||
# 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")
|
||||
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
|
||||
|
||||
res = SBend.generate(start, offset, radius, width)
|
||||
|
||||
# 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, sagitta=1.0)
|
||||
# Fine: 0.01um (10nm) sagitta
|
||||
res_fine = Bend90.generate(start, radius, width, 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")
|
||||
|
||||
# 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")
|
||||
|
||||
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
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import 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)
|
||||
|
||||
|
||||
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# 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 = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
# Check if any component in the path is an SBend
|
||||
found_sbend = False
|
||||
for res in path:
|
||||
# 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:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Increase base penalty to force detour immediately
|
||||
pf = PathFinder(router, basic_evaluator, 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
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
|
||||
p1 = Port(0, 0, 0)
|
||||
p2 = Port(10, 10, 0)
|
||||
|
||||
h = evaluator.h_manhattan(p1, p2)
|
||||
# Manhattan distance = 20. Orientation penalty = 0.
|
||||
# Weighted by 1.1 -> 22.0
|
||||
assert abs(h - 22.0) < 1e-6
|
||||
|
||||
# Orientation penalty
|
||||
p3 = Port(10, 10, 90)
|
||||
h_wrong = evaluator.h_manhattan(p1, p3)
|
||||
assert h_wrong > h
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
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 AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
from inire.utils.validation import validate_routing_result
|
||||
|
||||
|
||||
@st.composite
|
||||
def random_obstacle(draw: 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)
|
||||
router = AStarRouter(evaluator)
|
||||
router.node_limit = 5000 # Lower limit for fuzzing stability
|
||||
|
||||
# Check if start/target are inside obstacles (safety zone check)
|
||||
# The router should handle this gracefully (either route or return None)
|
||||
try:
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
# Analytic Correctness: if path is returned, verify it's collision-free
|
||||
if path:
|
||||
result = RoutingResult(net_id="default", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(
|
||||
result,
|
||||
obstacles,
|
||||
clearance=2.0,
|
||||
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}")
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import 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:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
pf = PathFinder(router, basic_evaluator)
|
||||
|
||||
netlist = {
|
||||
"net1": (Port(0, 0, 0), Port(50, 0, 0)),
|
||||
"net2": (Port(0, 10, 0), Port(50, 10, 0)),
|
||||
}
|
||||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results["net1"].is_valid
|
||||
assert results["net2"].is_valid
|
||||
assert results["net1"].collisions == 0
|
||||
assert results["net2"].collisions == 0
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.components import Bend90
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import 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, sagitta=0.5)
|
||||
# Low tolerance (0.001um = 1nm) -> many segments
|
||||
res_fine = Bend90.generate(start, radius=10.0, width=2.0, sagitta=0.001)
|
||||
|
||||
# Check number of points in the polygon exterior
|
||||
# (num_segments + 1) * 2 points usually
|
||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||
|
||||
assert pts_fine > pts_coarse
|
||||
|
||||
|
||||
def test_locked_paths() -> None:
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=(0, -50, 100, 50))
|
||||
danger_map.precompute([])
|
||||
evaluator = CostEvaluator(engine, danger_map)
|
||||
router = AStarRouter(evaluator)
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 1. Route Net A
|
||||
netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))}
|
||||
results_a = pf.route_all(netlist_a, {"netA": 2.0})
|
||||
assert results_a["netA"].is_valid
|
||||
|
||||
# 2. Lock Net A
|
||||
engine.lock_net("netA")
|
||||
|
||||
# 3. Route Net B through the same space. It should detour or fail.
|
||||
# We'll place Net B's start/target such that it MUST cross Net A's physical path.
|
||||
netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))}
|
||||
|
||||
# Route Net B
|
||||
results_b = pf.route_all(netlist_b, {"netB": 2.0})
|
||||
|
||||
# Net B should be is_valid (it detoured) or at least not have collisions
|
||||
# with Net A in the dynamic set (because netA is now static).
|
||||
# Since netA is static, netB will see it as a HARD collision if it tries to cross.
|
||||
|
||||
# Our A* will find a detour around the static obstacle.
|
||||
assert results_b["netB"].is_valid
|
||||
|
||||
# Verify geometry doesn't intersect locked netA (physical check)
|
||||
poly_a = [p.geometry[0] for p in results_a["netA"].path]
|
||||
poly_b = [p.geometry[0] for p in results_b["netB"].path]
|
||||
|
||||
for pa in poly_a:
|
||||
for pb in poly_b:
|
||||
assert not pa.intersects(pb)
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import numpy
|
||||
|
||||
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 > 1e-6:
|
||||
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,
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
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,
|
||||
) -> 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.
|
||||
|
||||
Returns:
|
||||
The matplotlib Figure and Axes objects.
|
||||
"""
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
||||
# Plot static obstacles (gray)
|
||||
for poly in static_obstacles:
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.5, fc="gray", ec="black")
|
||||
|
||||
# Plot paths
|
||||
colors = plt.get_cmap("tab10")
|
||||
for i, (net_id, res) in enumerate(results.items()):
|
||||
# Use modulo to avoid index out of range for many nets
|
||||
color: str | tuple[float, ...] = colors(i % 10)
|
||||
if not res.is_valid:
|
||||
color = "red" # Highlight failing nets
|
||||
|
||||
label_added = False
|
||||
for _j, comp in enumerate(res.path):
|
||||
# 1. Plot geometry
|
||||
for poly in comp.geometry:
|
||||
# Handle both Polygon and MultiPolygon (e.g. from SBend)
|
||||
if isinstance(poly, MultiPolygon):
|
||||
geoms = list(poly.geoms)
|
||||
else:
|
||||
geoms = [poly]
|
||||
|
||||
for g in geoms:
|
||||
x, y = g.exterior.xy
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "")
|
||||
label_added = True
|
||||
|
||||
# 2. Plot subtle port orientation arrow for internal ports
|
||||
p = comp.end_port
|
||||
rad = numpy.radians(p.orientation)
|
||||
u = numpy.cos(rad)
|
||||
v = numpy.sin(rad)
|
||||
ax.quiver(p.x, p.y, u, v, color="black", scale=40, width=0.003, alpha=0.3, pivot="tail", zorder=4)
|
||||
|
||||
# 3. Plot main arrows for netlist ports (if provided)
|
||||
if netlist and net_id in netlist:
|
||||
start_p, target_p = netlist[net_id]
|
||||
for p in [start_p, target_p]:
|
||||
rad = numpy.radians(p.orientation)
|
||||
u = numpy.cos(rad)
|
||||
v = numpy.sin(rad)
|
||||
ax.quiver(p.x, p.y, u, v, color="black", scale=25, width=0.005, 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")
|
||||
handles, labels = ax.get_legend_handles_labels()
|
||||
if labels:
|
||||
ax.legend()
|
||||
plt.grid(True)
|
||||
return fig, ax
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "inire"
|
||||
description = "Wave-router: Auto-routing for photonic and RF integrated circuits"
|
||||
description = "Wave-router"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { file = "LICENSE.md" }
|
||||
|
|
@ -9,6 +9,22 @@ authors = [
|
|||
]
|
||||
homepage = "https://mpxd.net/code/jan/inire"
|
||||
repository = "https://mpxd.net/code/jan/inire"
|
||||
keywords = [
|
||||
"layout",
|
||||
"CAD",
|
||||
"EDA",
|
||||
"mask",
|
||||
"pattern",
|
||||
"lithography",
|
||||
"oas",
|
||||
"gds",
|
||||
"dxf",
|
||||
"svg",
|
||||
"OASIS",
|
||||
"gdsii",
|
||||
"gds2",
|
||||
"stream",
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Development Status :: 4 - Beta",
|
||||
|
|
@ -20,17 +36,10 @@ classifiers = [
|
|||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"scipy",
|
||||
"shapely",
|
||||
"rtree",
|
||||
"matplotlib",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"hypothesis>=6.151.9",
|
||||
"matplotlib>=3.10.8",
|
||||
"pytest>=9.0.2",
|
||||
"ruff>=0.15.5",
|
||||
|
|
@ -70,7 +79,7 @@ lint.ignore = [
|
|||
"C408", # dict(x=y) instead of {'x': y}
|
||||
"PLR09", # Too many xxx
|
||||
"PLR2004", # magic number
|
||||
#"PLC0414", # import x as x
|
||||
"PLC0414", # import x as x
|
||||
"TRY003", # Long exception message
|
||||
]
|
||||
|
||||
|
|
|
|||
337
uv.lock
generated
337
uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
|
|
@ -19,28 +19,6 @@ dependencies = [
|
|||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 },
|
||||
|
|
@ -85,11 +63,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -107,22 +80,6 @@ version = "4.61.1"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410 },
|
||||
|
|
@ -150,18 +107,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hypothesis"
|
||||
version = "6.151.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/e1/ef365ff480903b929d28e057f57b76cae51a30375943e33374ec9a165d9c/hypothesis-6.151.9.tar.gz", hash = "sha256:2f284428dda6c3c48c580de0e18470ff9c7f5ef628a647ee8002f38c3f9097ca", size = 463534 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f7/5cc291d701094754a1d327b44d80a44971e13962881d9a400235726171da/hypothesis-6.151.9-py3-none-any.whl", hash = "sha256:7b7220585c67759b1b1ef839b1e6e9e3d82ed468cfc1ece43c67184848d7edd9", size = 529307 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
|
|
@ -173,35 +118,20 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "inire"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "scipy" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "hypothesis" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "rtree" },
|
||||
{ name = "scipy" },
|
||||
{ name = "shapely" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "hypothesis", specifier = ">=6.151.9" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.8" },
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "ruff", specifier = ">=0.15.5" },
|
||||
|
|
@ -213,32 +143,6 @@ version = "1.4.9"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961 },
|
||||
|
|
@ -290,11 +194,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -314,20 +213,6 @@ dependencies = [
|
|||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474 },
|
||||
|
|
@ -356,9 +241,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -367,28 +249,6 @@ version = "2.4.2"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 },
|
||||
|
|
@ -431,13 +291,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -455,28 +308,6 @@ version = "12.1.1"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364 },
|
||||
|
|
@ -527,13 +358,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -591,22 +415,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtree"
|
||||
version = "1.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.5"
|
||||
|
|
@ -632,136 +440,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
@ -770,12 +448,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue