update docs and perf metrics

This commit is contained in:
Jan Petykiewicz 2026-03-31 17:26:00 -07:00
commit 725980e694
26 changed files with 1183 additions and 525 deletions

57
DOCS.md
View file

@ -133,19 +133,60 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
`RoutingRunResult.metrics` is an immutable per-run snapshot. `RoutingRunResult.metrics` is an immutable per-run snapshot.
| Field | Type | Description | ### Search Counters
| :-- | :-- | :-- |
| `nodes_expanded` | `int` | Total nodes expanded during the run. | - `nodes_expanded`: Total nodes expanded during the run.
| `moves_generated` | `int` | Total candidate moves generated during the run. | - `moves_generated`: Total candidate moves generated during the run.
| `moves_added` | `int` | Total candidate moves admitted to the open set during the run. | - `moves_added`: Total candidate moves admitted to the open set.
| `pruned_closed_set` | `int` | Total moves pruned because the state was already closed at lower cost. | - `pruned_closed_set`: Total moves pruned because the state was already closed at lower cost.
| `pruned_hard_collision` | `int` | Total moves pruned by hard collision checks. | - `pruned_hard_collision`: Total moves pruned by hard collision checks.
| `pruned_cost` | `int` | Total moves pruned by cost ceilings or invalid costs. | - `pruned_cost`: Total moves pruned by cost ceilings or invalid costs.
- `route_iterations`: Number of negotiated-congestion iterations entered.
- `nets_routed`: Number of net-routing attempts executed across all iterations.
- `nets_reached_target`: Number of those attempts that reached the requested target port.
- `warm_start_paths_built`: Number of warm-start seed paths built by the greedy bootstrap pass.
- `warm_start_paths_used`: Number of routing attempts satisfied directly from an initial or warm-start path.
- `refine_path_calls`: Number of completed paths passed through the post-route refiner.
- `timeout_events`: Number of timeout exits encountered during the run.
### Cache Counters
- `move_cache_abs_hits` / `move_cache_abs_misses`: Absolute move-geometry cache activity.
- `move_cache_rel_hits` / `move_cache_rel_misses`: Relative move-geometry cache activity.
- `static_safe_cache_hits`: Reuse count for the static-safe admission cache.
- `hard_collision_cache_hits`: Reuse count for the hard-collision cache.
- `congestion_cache_hits` / `congestion_cache_misses`: Per-search congestion-cache activity.
### Index And Collision Counters
- `dynamic_path_objects_added` / `dynamic_path_objects_removed`: Dynamic-path geometry objects inserted into or removed from the live routing index.
- `dynamic_tree_rebuilds`: Number of dynamic STRtree rebuilds.
- `dynamic_grid_rebuilds`: Number of dynamic congestion-grid rebuilds.
- `static_tree_rebuilds`: Number of static dilated-obstacle STRtree rebuilds.
- `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification.
- `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds.
- `visibility_builds`: Number of static visibility-graph rebuilds.
- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building that graph.
- `visibility_corner_queries` / `visibility_corner_hits`: Precomputed-corner visibility query activity.
- `visibility_point_queries`, `visibility_point_cache_hits`, `visibility_point_cache_misses`: Arbitrary-point visibility query and cache activity.
- `ray_cast_calls`: Number of ray-cast queries issued against static obstacles.
- `ray_cast_candidate_bounds`: Total broad-phase candidate bounds considered by ray casts.
- `ray_cast_exact_geometry_checks`: Total exact non-rectangular geometry checks performed by ray casts.
- `congestion_check_calls`: Number of congestion broad-phase checks requested by search.
- `congestion_exact_pair_checks`: Number of exact geometry-pair checks performed while confirming congestion hits.
### Verification Counters
- `verify_path_report_calls`: Number of full path-verification passes.
- `verify_static_buffer_ops`: Number of static-verification `buffer()` operations.
- `verify_dynamic_exact_pair_checks`: Number of exact geometry-pair checks performed during dynamic-path verification.
## 8. Internal Modules ## 8. Internal Modules
Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`. Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`.
The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**.
## 9. Tuning Notes ## 9. Tuning Notes
### Speed vs. optimality ### Speed vs. optimality

View file

@ -8,7 +8,7 @@
* **Negotiated Congestion**: Iteratively resolves multi-net bottlenecks by inflating costs in high-traffic regions. * **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. * **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. * **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. * **Safety & Proximity**: Uses a sampled obstacle-boundary proximity model to bias routes away from nearby geometry.
* **Locked Routes**: Supports treating prior routed nets as fixed obstacles in later runs. * **Locked Routes**: Supports treating prior routed nets as fixed obstacles in later runs.
## Installation ## Installation
@ -77,7 +77,11 @@ INIRE_RUN_PERFORMANCE=1 python3 -m pytest -q inire/tests/test_example_performanc
## Documentation ## Documentation
Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**. Current documentation lives in:
* **[DOCS.md](DOCS.md)** for the public API and option reference.
* **[docs/architecture.md](docs/architecture.md)** for the current implementation structure.
* **[docs/performance.md](docs/performance.md)** for the committed performance-counter baseline.
## API Stability ## API Stability
@ -92,7 +96,7 @@ Deep-module interfaces such as `inire.router._router.PathFinder`, `inire.router.
2. **90° Bends**: Fixed-radius PDK cells. 2. **90° Bends**: Fixed-radius PDK cells.
3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$). 3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$).
For multi-net problems, the negotiated-congestion loop handles rip-up and reroute logic, ensuring that paths find the globally optimal configuration without crossings. For multi-net problems, the negotiated-congestion loop handles rip-up and reroute logic and seeks a collision-free configuration without crossings.
## Configuration ## Configuration

47
docs/architecture.md Normal file
View file

@ -0,0 +1,47 @@
# Architecture Overview
`inire` is a single-package Python router with a small stable API at the package root and a larger semi-private implementation under `inire.geometry` and `inire.router`.
## Stable Surface
- The supported entrypoint is `route(problem, options=...)`.
- Stable public types live at the package root and include `RoutingProblem`, `RoutingOptions`, `NetSpec`, `Port`, `RoutingResult`, and `RoutingRunResult`.
- Deep imports such as `inire.router._router.PathFinder` and `inire.geometry.collision.RoutingWorld` are intentionally accessible for advanced workflows, but they are unstable.
## Current Module Layout
- `inire/model.py`: Immutable request and option dataclasses.
- `inire/results.py`: Immutable routing results plus the per-run `RouteMetrics` snapshot.
- `inire/seeds.py`: Serializable path-seed primitives.
- `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers.
- `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation.
- `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic.
- `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths.
- `inire/router/_search.py`, `_astar_moves.py`, `_astar_admission.py`, `_astar_types.py`: The state-lattice A* search loop and move admission pipeline.
- `inire/router/_router.py`: The negotiated-congestion driver and refinement orchestration.
- `inire/router/refiner.py`: Post-route path simplification for completed paths.
- `inire/router/cost.py` and `inire/router/danger_map.py`: Search scoring and obstacle-proximity biasing.
- `inire/utils/visualization.py`: Plotting and diagnostics helpers.
## Routing Stack
`route(problem, options=...)` builds a routing stack composed of:
1. `RoutingWorld` for collision state.
2. `DangerMap` for static-obstacle proximity costs.
3. `CostEvaluator` for move scoring and heuristic support.
4. `AStarContext` for caches and search configuration.
5. `PathFinder` for negotiated congestion, rip-up/reroute, and refinement.
The search state is a snapped Manhattan `(x, y, r)` port. From each state the router expands straight segments, 90-degree bends, and compact S-bends, then validates candidates against static geometry, dynamic congestion, and optional self-collision checks.
## Notes On Current Behavior
- Static obstacles and routed paths are treated as single-layer geometry; automatic crossings are not supported.
- The danger-map implementation uses sampled obstacle-boundary points and a KD-tree, not a dense distance-transform grid.
- `use_tiered_strategy` can swap in a cheaper bend proxy on the first congestion iteration.
- Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning.
## Performance Visibility
`RoutingRunResult.metrics` includes both A* counters and index/cache/verification counters. The committed example-corpus baseline for those counters is tracked in `docs/performance.md` and `docs/performance_baseline.json`.

25
docs/performance.md Normal file
View file

@ -0,0 +1,25 @@
# Performance Baseline
Generated on 2026-03-31 by `scripts/record_performance_baseline.py`.
The full machine-readable snapshot lives in `docs/performance_baseline.json`.
| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |
| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |
| example_01_simple_route | 0.0042 | 1 | 1 | 1 | 1 | 1 | 2 | 22 | 11 | 7 | 2 | 2 | 0 | 3 |
| example_02_congestion_resolution | 0.3335 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 |
| example_03_locked_paths | 0.1810 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 |
| example_04_sbends_and_radii | 2.0151 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 |
| example_05_orientation_stress | 0.2438 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 |
| example_06_bend_collision_models | 4.1636 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 |
| example_07_large_scale_routing | 1.3759 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 |
| example_08_custom_bend_geometry | 0.2437 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 |
| example_09_unroutable_best_effort | 0.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 |
## Full Counter Set
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.
Tracked metric keys:
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries, visibility_corner_hits, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, ray_cast_calls, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_exact_pair_checks

View file

@ -0,0 +1,474 @@
{
"generated_on": "2026-03-31",
"generator": "scripts/record_performance_baseline.py",
"scenarios": [
{
"duration_s": 0.0041740520391613245,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 2,
"dynamic_path_objects_removed": 1,
"dynamic_tree_rebuilds": 2,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 10,
"move_cache_rel_hits": 0,
"move_cache_rel_misses": 10,
"moves_added": 7,
"moves_generated": 11,
"nets_reached_target": 1,
"nets_routed": 1,
"nodes_expanded": 2,
"pruned_closed_set": 0,
"pruned_cost": 4,
"pruned_hard_collision": 0,
"ray_cast_calls": 22,
"ray_cast_candidate_bounds": 12,
"ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 1,
"route_iterations": 1,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 1,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 3,
"verify_static_buffer_ops": 0,
"visibility_builds": 2,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 12,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 1,
"warm_start_paths_used": 1
},
"name": "example_01_simple_route",
"reached_targets": 1,
"total_results": 1,
"valid_results": 1
},
{
"duration_s": 0.3335385399404913,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 32,
"dynamic_path_objects_removed": 17,
"dynamic_tree_rebuilds": 8,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 12,
"move_cache_abs_misses": 1401,
"move_cache_rel_hits": 1293,
"move_cache_rel_misses": 108,
"moves_added": 668,
"moves_generated": 1413,
"nets_reached_target": 3,
"nets_routed": 3,
"nodes_expanded": 366,
"pruned_closed_set": 157,
"pruned_cost": 208,
"pruned_hard_collision": 380,
"ray_cast_calls": 1176,
"ray_cast_candidate_bounds": 925,
"ray_cast_exact_geometry_checks": 136,
"refine_path_calls": 3,
"route_iterations": 1,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 2,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 90,
"verify_path_report_calls": 35,
"verify_static_buffer_ops": 0,
"visibility_builds": 4,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 12,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 3,
"warm_start_paths_used": 3
},
"name": "example_02_congestion_resolution",
"reached_targets": 3,
"total_results": 3,
"valid_results": 3
},
{
"duration_s": 0.1809853739105165,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 17,
"dynamic_path_objects_removed": 10,
"dynamic_tree_rebuilds": 5,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 903,
"move_cache_rel_hits": 821,
"move_cache_rel_misses": 82,
"moves_added": 307,
"moves_generated": 904,
"nets_reached_target": 2,
"nets_routed": 2,
"nodes_expanded": 191,
"pruned_closed_set": 97,
"pruned_cost": 140,
"pruned_hard_collision": 181,
"ray_cast_calls": 681,
"ray_cast_candidate_bounds": 179,
"ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 2,
"route_iterations": 2,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 2,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 14,
"verify_static_buffer_ops": 69,
"visibility_builds": 4,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 24,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 2,
"warm_start_paths_used": 2
},
"name": "example_03_locked_paths",
"reached_targets": 2,
"total_results": 2,
"valid_results": 2
},
{
"duration_s": 2.0151148419827223,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 14,
"dynamic_path_objects_removed": 7,
"dynamic_tree_rebuilds": 4,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 122,
"move_cache_rel_hits": 80,
"move_cache_rel_misses": 42,
"moves_added": 65,
"moves_generated": 123,
"nets_reached_target": 2,
"nets_routed": 2,
"nodes_expanded": 15,
"pruned_closed_set": 2,
"pruned_cost": 25,
"pruned_hard_collision": 16,
"ray_cast_calls": 18218,
"ray_cast_candidate_bounds": 50717,
"ray_cast_exact_geometry_checks": 21265,
"refine_path_calls": 2,
"route_iterations": 1,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 2,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 6,
"verify_static_buffer_ops": 0,
"visibility_builds": 3,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 18148,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 2,
"warm_start_paths_used": 2
},
"name": "example_04_sbends_and_radii",
"reached_targets": 2,
"total_results": 2,
"valid_results": 2
},
{
"duration_s": 0.2437819039914757,
"metrics": {
"congestion_cache_hits": 2,
"congestion_cache_misses": 412,
"congestion_check_calls": 412,
"congestion_exact_pair_checks": 66,
"dynamic_grid_rebuilds": 3,
"dynamic_path_objects_added": 37,
"dynamic_path_objects_removed": 25,
"dynamic_tree_rebuilds": 12,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 253,
"move_cache_abs_misses": 1371,
"move_cache_rel_hits": 1269,
"move_cache_rel_misses": 102,
"moves_added": 681,
"moves_generated": 1624,
"nets_reached_target": 6,
"nets_routed": 6,
"nodes_expanded": 286,
"pruned_closed_set": 139,
"pruned_cost": 505,
"pruned_hard_collision": 14,
"ray_cast_calls": 1243,
"ray_cast_candidate_bounds": 45,
"ray_cast_exact_geometry_checks": 43,
"refine_path_calls": 3,
"route_iterations": 2,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 3,
"static_tree_rebuilds": 1,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 2,
"verify_path_report_calls": 12,
"verify_static_buffer_ops": 0,
"visibility_builds": 3,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 0,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 2,
"warm_start_paths_used": 2
},
"name": "example_05_orientation_stress",
"reached_targets": 3,
"total_results": 3,
"valid_results": 3
},
{
"duration_s": 4.163613382959738,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 36,
"dynamic_path_objects_removed": 18,
"dynamic_tree_rebuilds": 6,
"hard_collision_cache_hits": 18,
"move_cache_abs_hits": 186,
"move_cache_abs_misses": 840,
"move_cache_rel_hits": 702,
"move_cache_rel_misses": 138,
"moves_added": 629,
"moves_generated": 1026,
"nets_reached_target": 3,
"nets_routed": 3,
"nodes_expanded": 240,
"pruned_closed_set": 108,
"pruned_cost": 204,
"pruned_hard_collision": 85,
"ray_cast_calls": 40530,
"ray_cast_candidate_bounds": 121732,
"ray_cast_exact_geometry_checks": 36858,
"refine_path_calls": 3,
"route_iterations": 3,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 3,
"static_safe_cache_hits": 141,
"static_tree_rebuilds": 6,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 9,
"verify_static_buffer_ops": 54,
"visibility_builds": 6,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 39848,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 3,
"warm_start_paths_used": 3
},
"name": "example_06_bend_collision_models",
"reached_targets": 3,
"total_results": 3,
"valid_results": 3
},
{
"duration_s": 1.375933071016334,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 88,
"dynamic_path_objects_removed": 44,
"dynamic_tree_rebuilds": 20,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 6,
"move_cache_abs_misses": 366,
"move_cache_rel_hits": 275,
"move_cache_rel_misses": 91,
"moves_added": 227,
"moves_generated": 372,
"nets_reached_target": 10,
"nets_routed": 10,
"nodes_expanded": 78,
"pruned_closed_set": 20,
"pruned_cost": 64,
"pruned_hard_collision": 61,
"ray_cast_calls": 11151,
"ray_cast_candidate_bounds": 21198,
"ray_cast_exact_geometry_checks": 11651,
"refine_path_calls": 10,
"route_iterations": 1,
"static_net_tree_rebuilds": 10,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 6,
"static_tree_rebuilds": 10,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 30,
"verify_static_buffer_ops": 132,
"visibility_builds": 11,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 10768,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 10,
"warm_start_paths_used": 10
},
"name": "example_07_large_scale_routing",
"reached_targets": 10,
"total_results": 10,
"valid_results": 10
},
{
"duration_s": 0.2436628290452063,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 12,
"dynamic_path_objects_removed": 6,
"dynamic_tree_rebuilds": 4,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 2,
"move_cache_abs_misses": 76,
"move_cache_rel_hits": 32,
"move_cache_rel_misses": 44,
"moves_added": 56,
"moves_generated": 78,
"nets_reached_target": 2,
"nets_routed": 2,
"nodes_expanded": 18,
"pruned_closed_set": 6,
"pruned_cost": 16,
"pruned_hard_collision": 0,
"ray_cast_calls": 2308,
"ray_cast_candidate_bounds": 3802,
"ray_cast_exact_geometry_checks": 1904,
"refine_path_calls": 2,
"route_iterations": 2,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 2,
"static_tree_rebuilds": 2,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 6,
"verify_static_buffer_ops": 0,
"visibility_builds": 4,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 2252,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 2,
"warm_start_paths_used": 2
},
"name": "example_08_custom_bend_geometry",
"reached_targets": 2,
"total_results": 2,
"valid_results": 2
},
{
"duration_s": 0.0052433289820328355,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
"congestion_check_calls": 0,
"congestion_exact_pair_checks": 0,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 1,
"dynamic_path_objects_removed": 0,
"dynamic_tree_rebuilds": 1,
"hard_collision_cache_hits": 0,
"move_cache_abs_hits": 0,
"move_cache_abs_misses": 16,
"move_cache_rel_hits": 2,
"move_cache_rel_misses": 14,
"moves_added": 10,
"moves_generated": 16,
"nets_reached_target": 0,
"nets_routed": 1,
"nodes_expanded": 3,
"pruned_closed_set": 0,
"pruned_cost": 4,
"pruned_hard_collision": 2,
"ray_cast_calls": 13,
"ray_cast_candidate_bounds": 5,
"ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 0,
"route_iterations": 1,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0,
"static_tree_rebuilds": 0,
"timeout_events": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 1,
"verify_static_buffer_ops": 1,
"visibility_builds": 0,
"visibility_corner_hits": 0,
"visibility_corner_pairs_checked": 0,
"visibility_corner_queries": 0,
"visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0,
"visibility_point_queries": 0,
"warm_start_paths_built": 0,
"warm_start_paths_used": 0
},
"name": "example_09_unroutable_best_effort",
"reached_targets": 0,
"total_results": 1,
"valid_results": 0
}
]
}

View file

@ -1,42 +0,0 @@
# Cost and Collision Engine Spec
This document describes the methods for ensuring "analytic correctness" while maintaining a computationally efficient cost function.
## 1. Analytic Correctness
The router balances speed and verification using a two-tier approach.
### 1.1. R-Tree Geometry Engine
The router uses an **R-Tree of Polygons** for all geometric queries.
* **Move Validation:** Every "Move" proposed by the search is checked for intersection against the R-Tree.
* **Pre-dilation for Clearance:** All obstacles and paths use a global clearance $C$. At the start of the session, all user-provided obstacles are **pre-dilated by $(W_{max} + C)/2$** for initial broad pruning. However, individual path dilation for intersection tests uses $(W_i + C)/2$ for a specific net's width $W_i$.
* **Safety Zone:** To prevent immediate collision flags for ports placed on or near obstacle boundaries, the router ignores collisions within a radius of **2nm** of start and end ports.
* **Single Layer:** All routing and collision detection occur on a single layer.
### 1.2. Cost Calculation (Soft Constraint)
The "Danger Cost" $g_{proximity}(n)$ is a function of the distance $d$ to the nearest obstacle:
$$g_{proximity}(n) = \begin{cases} \infty & \text{if } d < (W_i + C)/2 \\ \frac{k}{d^2} & \text{if } (W_i + C)/2 \le d < \text{Safety Threshold} \\ 0 & \text{if } d \ge \text{Safety Threshold} \end{cases}$$
To optimize A* search, a **Static Danger Map (Precomputed Grid)** is used for the heuristic.
* **Grid Resolution:** Default **1000nm (1µm)**.
* **Static Nature:** The grid only accounts for fixed obstacles. It is computed once at the start of the session and is **not re-computed** during the Negotiated Congestion loop.
* **Efficiency:** For a 20x20mm layout, this results in a 20k x 20k matrix.
* **Memory:** Using a `uint8` or `float16` representation, this consumes ~400-800MB (Default < 2GB). For extremely high resolution or larger areas, the system supports up to **20GB** allocation.
* **Precision:** Strict intersection checks still use the R-Tree for "analytic correctness."
## 2. Collision Detection Implementation
The system relies on `shapely` for geometry and `rtree` for spatial indexing.
1. **Arc Resolution:** Arcs are represented as polygons approximated by segments with a maximum deviation (sagitta).
2. **Intersection Test:** A "Move" is valid only if its geometry does not intersect any obstacle in the R-Tree.
3. **Self-Intersection:** Paths from the same net must not intersect themselves.
4. **No Crossings:** Strictly 2D; no crossings (vias or bridges) are supported.
## 3. Negotiated Congestion (Path R-Tree)
To handle multiple nets, the router maintains a separate R-Tree containing the dilated geometries ($C/2$) of all currently routed paths.
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
* **Failure Policy:** If no collision-free path is found after the max iterations, the router returns the **"least-bad" (lowest cost)** path. These paths MUST be explicitly flagged as invalid (e.g., via an `is_valid=False` attribute or a non-zero `collision_count`) so the user can identify and manually fix the failure.
## 4. Handling Global Clearances
Clearances are global. Both obstacles and paths are pre-dilated once. This ensures that any two objects maintain at least $C$ distance if their dilated versions do not intersect.
## 5. Locked Paths
The router supports **Locked Paths**—existing geometries inserted into the static Obstacle R-Tree, ensuring they are never modified or rerouted.

View file

@ -1,37 +0,0 @@
# Geometric Representation and Data Structures
This document defines the core data structures for representing the routing problem.
## 1. Port Definitions (Connectivity)
Routing requests are defined as a mapping of source ports to destination ports.
* **Port Structure:** `(x, y, orientation)`
* `x, y`: Coordinates snapped to **1nm** grid.
* `orientation`: Strictly $\{0, 90, 180, 270\}$ degrees.
* **Netlist:** A dictionary or list of tuples: `{(start_port): (end_port)}`. Each entry represents a single-layer path that must be routed.
* **Heterogeneous Widths:** The router supports different widths $W_i$ per net. Dilation for a specific net $i$ is calculated as $(W_i + C)/2$ to maintain the global clearance $C$.
## 2. Component Library (Move Generator)
The router uses a discrete set of components to expand states in the A* search.
### 2.1. Straight Waveguides
* **A* Expansion:** Generates "Straight" moves of varying lengths (e.g., $1\mu m, 5\mu m, 25\mu m$).
* **Snap-to-Target:** If the destination port $T$ is directly ahead and the current state's orientation matches $T$'s entry orientation, a special move is generated to close the gap exactly.
### 2.2. Fixed 90° Bends (PDK Cells)
* **Parameters:** `radius`, `width`.
* **A* Expansion:** A discrete move that changes orientation by $\pm 90^\circ$ and shifts the coordinate by the radius.
* **Grid Alignment:** If the bend radius $R$ is not a multiple of the search grid (default $1\mu m$), the resulting state is **snapped to the nearest grid point**, and a warning is issued to the user.
### 2.3. Parametric S-Bends (Compact)
* **Parameters:** `radius`, `width`.
* **A* Expansion:** Used ONLY for lateral offsets $O < 2R$.
* **Large Offsets ($O \ge 2R$):** The router does not use a single S-bend move for large offsets. Instead, the A* search naturally finds the optimal path by combining two 90° bends and a straight segment. This ensures maximum flexibility in obstacle avoidance for large shifts.
## 3. Obstacle Representation
Obstacles are provided as raw polygons on a single layer.
* **Pre-processing:** All input polygons are inserted into an **R-Tree**.
* **Buffer/Dilation:** Obstacles are pre-dilated by $(W_{max} + Clearance)/2$ for initial pruning, but final collision tests use the net-specific width $W_i$.
* **No Multi-layer:** The router assumes all obstacles and paths share the same plane.
* **Safety Zone:** Ignore collisions within **2nm** of start and end ports for robustness.

View file

@ -1,13 +0,0 @@
# High-Level Notes: Auto-Routing for Integrated Circuits
We're implementing auto-routing for photonic and RF integrated circuits. The problem space has the following features:
- **Single Layer:** All paths are on a single layer; multi-layer routing is not supported.
- **No Crossings:** Crossings are to be avoided and are not supported by the router's automatic placement. The user must manually handle any required crossings.
- **Large Bend Radii:** Bends use large radii ($R$), and are usually pre-generated (e.g. 90-degree cells).
- **Proximity Sensitivity:** Paths are sensitive to proximity to other paths and obstacles (crosstalk/coupling).
- **Manhattan Preference:** Manhattan pathing is sufficient for most cases; any-angle is rare.
- **S-Bends:** S-bends are necessary for small lateral offsets ($O < 2R$).
- **Hybrid Search:** A* state-lattice search is used for discrete component placement.
- **Dilation:** A $Clearance/2$ dilation is applied to all obstacles and paths for efficient collision avoidance.
- **Negotiated Congestion:** A multi-net "PathFinder" loop iteratively reroutes nets to resolve congestion.

View file

@ -1,64 +0,0 @@
# Implementation Plan
This plan outlines the step-by-step implementation of the `inire` auto-router. For detailed test cases, refer to [Testing Plan](./testing_plan.md).
## Phase 1: Core Geometry & Move Generation
**Goal:** Implement Ports, Polygons, and Component Library with high geometric fidelity.
1. **Project Setup:** Initialize `inire/` structure and `pytest` configuration. Include `hypothesis` for property-based testing.
2. **`geometry.primitives`:**
* `Port` with **1nm** snapping.
* Basic 2D transformations (rotate, translate).
* **Property-Based Tests:** Verify transform invariants (e.g., $90^\circ$ rotation cycles).
3. **`geometry.components`:**
* `Straight`, `Bend90`, `SBend`.
* **Search Grid Snapping:** Implement 1µm snapping for expanded ports.
* **Small S-Bends ($O < 2R$):** Logic for parametric generation.
* **Edge Cases:** Handle $O=2R$ and $L < 1\mu m$.
4. **Tests:**
* Verify geometric correctness (refer to Testing Plan Section 1).
* Unit tests for `Port` snapping and component transformations.
## Phase 2: Collision Engine & Cost
**Goal:** Build the R-Tree wrapper and the analytic cost function.
1. **`geometry.collision`:** Implement `CollisionEngine`.
* **Pre-dilation:** Obstacles/Paths dilated by $Clearance/2$.
* **Safety Zone:** Ignore collisions within **2nm** of start/end ports.
2. **`router.danger_map`:**
* Implement **1µm** pre-computed proximity grid.
* Optimize for design sizes up to **20x20mm** (< 2GB memory).
3. **`router.cost`:** Implement `CostEvaluator`.
* Bend cost: $10 \times (\text{Manhattan distance between ports})$.
* Integrate R-Tree for strict checks and Danger Map for heuristic.
4. **Tests:**
* Verify collision detection with simple overlapping shapes (Testing Plan Section 2.1).
* Verify Danger Map accuracy and memory footprint (Testing Plan Section 2.2).
* **Post-Route Validator:** Implement the independent `validate_path` utility.
## Phase 3: Single-Net A* Search
**Goal:** Route a single net from A to B with 1nm precision.
1. **`router.astar`:** Implement the priority queue loop.
* State representation: `(x_µm, y_µm, theta)`.
* Move expansion loop with 1µm grid.
* **Natural S-Bends:** Ensure search can find $O \ge 2R$ shifts by combining moves.
* **Look-ahead Snapping:** Actively bridge to the 1nm target when in the capture radius (10µm).
2. **Heuristic:** Manhattan distance $h(n)$ + orientation penalty + Danger Map lookup.
3. **Tests:**
* Solve simple maze problems and verify path optimality (Testing Plan Section 3).
* Verify snap-to-target precision at 1nm resolution.
* **Determinism:** Verify same seed = same path.
## Phase 4: Multi-Net PathFinder
**Goal:** Implement the "Negotiated Congestion" loop for multiple nets.
1. **`router.pathfinder`:**
* Sequential routing -> Identify congestion -> Inflate cost -> Reroute.
* **R-Tree Congestion:** Store dilated path geometries.
2. **Explicit Results:** Return `RoutingResult` objects with `is_valid` and `collisions` metadata.
3. **Tests:**
* Full multi-net benchmarks (Testing Plan Section 4).
* Verify rerouting behavior in crowded environments.
## Phase 5: Visualization, Benchmarking & Fuzzing
1. **`utils.visualization`:** Plot paths using `matplotlib`. Highlight collisions in red.
2. **Benchmarks:** Stress test with 50+ nets. Verify performance and node limits (Testing Plan Section 5).
3. **Fuzzing:** Run A* on randomized layouts to ensure stability.
4. **Final Validation:** Ensure all `is_valid=True` results pass the independent `validate_path` check.

View file

@ -1,57 +0,0 @@
# Python Package Structure
This document outlines the directory structure and module organization for the `inire` auto-router package.
## 1. Directory Layout
```
inire/
├── __init__.py # Exposes the main `Router` class and key types
├── geometry/ # Core geometric primitives and operations
│ ├── __init__.py
│ ├── primitives.py # Point, Port, Polygon, Arc classes
│ ├── collision.py # R-Tree wrapper and intersection logic
│ └── components.py # Move generators (Straight, Bend90, SBend)
├── router/ # Search algorithms and pathfinding
│ ├── __init__.py
│ ├── astar.py # Hybrid State-Lattice A* implementation
│ ├── graph.py # Node, Edge, and Graph data structures
│ ├── cost.py # Cost functions (length, bend, proximity)
│ ├── danger_map.py # Pre-computed grid for heuristic proximity costs
│ └── pathfinder.py # Multi-net "Negotiated Congestion" manager
├── utils/ # Utility functions
│ ├── __init__.py
│ └── visualization.py # Plotting tools for debug/heatmaps (matplotlib/klayout)
└── tests/ # Unit and integration tests
├── __init__.py
├── conftest.py # Pytest fixtures (common shapes, PDK cells)
├── test_primitives.py # Tests for Port and coordinate transforms
├── test_components.py # Tests for Straight, Bend90, SBend generation
├── test_collision.py # Tests for R-Tree and dilation logic
├── test_cost.py # Tests for Danger Map and cost evaluation
├── test_astar.py # Tests for single-net routing (mazes, snapping)
└── test_pathfinder.py # Multi-net "Negotiated Congestion" benchmarks
```
## 2. Module Responsibilities
### `inire.geometry`
* **`primitives.py`**: Defines the `Port` named tuple `(x, y, theta)` and helper functions for coordinate transforms.
* **`collision.py`**: Wraps the `rtree` or `shapely` library. Handles the "Analytic Correctness" checks (exact polygon distance).
* **`components.py`**: Logic to generate "Moves" from a start port. E.g., `SBend.generate(start_port, offset, radius)` returns a list of polygons and the end port. Handles $O > 2R$ logic.
### `inire.router`
* **`astar.py`**: The heavy lifter. Maintains the `OpenSet` (priority queue) and `ClosedSet`. Implements the "Snap-to-Target" logic.
* **`cost.py`**: compute $f(n) = g(n) + h(n)$. encapsulates the "Danger Map" and Path R-Tree lookups.
* **`danger_map.py`**: Manages the pre-computed proximity grid used for $O(1)$ heuristic calculations.
* **`pathfinder.py`**: Orchestrates the multi-net loop. Tracks the Path R-Tree for negotiated congestion and triggers reroutes.
### `inire.tests`
* **Structure:** Tests are co-located within the package for ease of access.
* **Fixtures:** `conftest.py` will provide standard PDK cells (e.g., a $10\mu m$ radius bend) to avoid repetition in test cases.
## 3. Dependencies
* `numpy`: Vector math.
* `shapely`: Polygon geometry and intersection.
* `rtree`: Spatial indexing.
* `networkx` (Optional): Not used for core search to ensure performance.

View file

@ -1,67 +0,0 @@
# Architecture Decision Record: Auto-Routing for Photonic & RF ICs
## 1. Problem Context
Photonic and RF routing differ significantly from digital VLSI due to physical constraints:
* **Geometric Rigidity:** 90° bends are pre-rendered PDK cells with fixed bounding boxes.
* **Parametric Flexibility:** S-bends must be generated on-the-fly to handle arbitrary offsets, provided they maintain a constant radius $R$.
* **Signal Integrity:** High sensitivity to proximity (coupling/crosstalk) and a strong preference for single-layer, non-crossing paths.
* **Manual Intervention:** The router is strictly 2D and avoids all other geometries on the same layer. No crossings (e.g. vias or bridges) are supported by the automatic routing engine. The user must manually handle any required crossings by placing components (e.g. crossing cells) and splitting the net list accordingly. This simplifies the router's task to 2D obstacle avoidance and spacing optimization.
---
## 2. Candidate Algorithms & Tradeoffs
### 2.1. Rubber-Band (Topological) Routing
This approach treats paths as elastic bands stretched around obstacles, later "inflating" them to have width and curvature.
| Feature | Analysis |
| :--- | :--- |
| **Strengths** | Excellent at "River Routing" and maintaining minimum clearances. Inherently avoids crossings. |
| **Downsides** | **The Inflation Gap:** A valid thin-line topology may be physically un-routable once the large radius $R$ is applied. It struggles to integrate rigid, pre-rendered 90° blocks into a continuous elastic model. |
| **Future Potential** | High, if a "Post-Processing" engine can reliably snap elastic curves to discrete PDK cells without breaking connectivity. |
### 2.2. Voronoi-Based (Medial Axis) Routing
Uses a Voronoi diagram to find paths that are maximally distant from all obstacles.
| Feature | Analysis |
| :--- | :--- |
| **Strengths** | Theoretically optimal for "Safety" and crosstalk reduction. Guaranteed maximum clearance. |
| **Downsides** | **Manhattan Incompatibility:** Voronoi edges are any-angle and often jagged. Mapping these to a Manhattan-heavy PDK (90° bends) requires a lossy "snapping" phase that often violates the very safety the algorithm intended to provide. |
| **Future Potential** | Useful as a "Channel Finder" to guide a more rigid router, but unsuitable as a standalone geometric solver. |
### 2.3. Integer Linear Programming (ILP)
Formulates routing as a massive optimization problem where a solver picks the best path from a pool of candidates.
| Feature | Analysis |
| :--- | :--- |
| **Strengths** | Can find the mathematically "best" layout (e.g., minimum total length or total bends) across all nets simultaneously. |
| **Downsides** | **Candidate Explosion:** Because S-bends are generated on-the-fly, the number of possible candidate shapes is infinite. To use ILP, one must "discretize" the search space, which may miss the one specific geometry needed for a tight fit. |
| **Future Potential** | Effective for small, high-congestion "Switchbox" areas where all possible geometries can be pre-tabulated. |
---
## 3. Selected Approach: Hybrid State-Lattice A*
### 3.1. Rationale
The **State-Lattice** variant of the A* algorithm is selected as the primary routing engine. Unlike standard A* which moves between grid cells, State-Lattice moves between **states** defined as $(x, y, \theta)$.
1. **Native PDK Integration:** The router treats the pre-rendered 90° bend cell as a discrete "Move" in the search tree. The algorithm only considers placing a bend if the cell's bounding box is clear of obstacles.
2. **Parametric S-Bends:** When the search needs to bridge a lateral gap, it triggers a "Procedural Move." It calculates a fixed-radius S-bend on-the-fly. If the resulting arc is valid and collision-free, it is added as an edge in the search graph.
3. **Predictable Costing:** It allows for a sophisticated cost function $f(n) = g(n) + h(n)$ where:
* $g(n)$ penalizes path length and proximity to obstacles (using a distance-transform field).
* $h(n)$ (the heuristic) guides the search toward the destination while favoring Manhattan alignments.
### 3.2. Implementation Strategy
* **Step 1: Distance Transform.** Pre-calculate a "Danger Map" of the layout. Cells close to obstacles have a high cost; cells far away have low cost. This handles the **Proximity Sensitivity** constraint.
* **Step 2: State Expansion.** From the current point, explore:
* `Straight(length)`
* `PDK_Bend_90(direction)`
* `S_Bend(target_offset, R)`
* **Step 3: Rip-up and Reroute.** To handle the sequential nature of A*, implement a "Negotiated Congestion" scheme (PathFinder algorithm) where nets "pay" more to occupy areas that other nets also want.
---
## 4. Summary of Tradeoffs for Future Review
* **Why not pure Manhattan?** Photonic/RF requirements for large $R$ and S-bends make standard grid-based maze routers obsolete.
* **Why not any-angle?** While any-angle is possible, the PDK's reliance on pre-rendered 90° blocks makes a lattice-based approach more manufacturing-stable.
* **Risk:** The primary risk is **Search Time**. As the library of moves grows (more S-bend options), the branching factor increases. This must be managed with aggressive pruning and spatial indexing (e.g., R-trees).

View file

@ -1,66 +0,0 @@
# Routing Search Specification
This document details the Hybrid State-Lattice A* implementation and the "PathFinder" (Negotiated Congestion) algorithm for multi-net routing.
## 1. Hybrid State-Lattice A* Search
The core router operates on states $S = (x, y, \theta)$, where $\theta \in \{0, 90, 180, 270\}$.
### 1.1. State Representation & Snapping
To ensure search stability and hash-map performance:
* **Intermediate Ports:** Every state expanded by A* is snapped to a search grid.
* **Search Grid:** Default snap is **1000nm (1µm)**.
* **Final Alignment:** The "Snap-to-Target" logic bridges the gap from the coarse search grid to the final **1nm** port coordinates.
* **Max Design Size:** Guidelines for memory/performance assume up to **20mm x 20mm** routing area.
### 1.2. Move Expansion Logic
From state $S_n$, the following moves are evaluated:
1. **Tiered Straight Steps:** Expand by a set of distances $L \in \{1\mu m, 5\mu m, 25\mu m\}$.
2. **Snap-to-Target:** A "last-inch" look-ahead move. If the target $T$ is within a **Capture Radius (Default: 10µm)** and a straight segment or single bend can reach it, a special move is generated to close the gap exactly at 1nm resolution.
3. **90° Bend:** Try clockwise/counter-clockwise turns using fixed PDK cells.
4. **Small-Offset S-Bend:**
* **Only for $O < 2R$:** Parametric S-bend (two tangent arcs).
* **$O \ge 2R$:** Search naturally finds these by combining 90° bends and straight segments.
### 1.3. Cost Function $f(n) = g(n) + h(n)$
The search uses a flexible, component-based cost model.
* **$g(n)$ (Actual Cost):** $\sum \text{ComponentCost}_i + \text{ProximityCost} + \text{CongestionCost}$
* **Straight Segment:** $L \times C_{unit\_length}$.
* **90° Bend:** $10 \times (\text{Manhattan distance between ports})$.
* **S-Bend:** $f(O, R)$.
* **Proximity Cost:** $k/d^2$ penalty (strict checks use R-Tree).
* **Congestion Cost:** $P \times (\text{Overlaps in Path R-Tree})$.
* **$h(n)$ (Heuristic):**
* Manhattan distance $L_1$ to the target.
* Orientation Penalty: High cost if the state's orientation doesn't match the target's entry orientation.
* **Greedy Weighting:** The A* search uses a weighted heuristic (e.g., $1.1 \times h(n)$) to prioritize search speed over strict path optimality.
* **Danger Map Heuristic:** Fast lookups from the **1µm** pre-computed proximity grid.
## 2. Multi-Net "PathFinder" Strategy (Negotiated Congestion)
1. **Iteration:** Identify "Congestion Areas" using Path R-Tree intersections.
2. **Inflation:** Increase penalty multiplier $P$ for congested areas.
3. **Repeat:** Continue until no overlaps exist or the max iteration count is reached (Default: **20 iterations**).
### 2.1. Convergence & Result Policy
* **Least Bad Attempt:** If no 100% collision-free solution is found, return the result with the lowest total cost (including overlaps).
* **Explicit Reporting:** Results MUST include a `RoutingResult` object containing:
* `path_geometry`: The actual polygon sequence.
* `is_valid`: Boolean (True only if no collisions).
* `collisions`: A count or list of detected overlap points/polygons.
* **Visualization:** Overlapping regions are highlighted (e.g., in red) in the heatmaps.
## 3. Search Limits & Scaling
* **Node Limit:** A* search is capped at **50,000 nodes** per net per iteration.
* **Dynamic Timeout:** Session-level timeout based on problem size:
* `Timeout = max(2s, 0.05s * num_nets * num_iterations)`.
* *Example:* A 100-net problem over 20 iterations times out at **100 seconds**.
## 4. Determinism
All search and rip-up operations are strictly deterministic.
* **Seed:** A user-provided `seed` (int) MUST be used to initialize any random number generators (e.g., if used for tie-breaking or initial net ordering).
* **Tie-Breaking:** If two nodes have the same $f(n)$, a consistent tie-breaking rule (e.g., based on node insertion order or state hash) must be used.
## 5. Optimizations
* **A* Pruning:** Head toward the target and prune suboptimal orientations.
* **Safety Zones:** Ignore collisions within **2nm** of start/end ports.
* **Spatial Indexing:** R-Tree queries are limited to the bounding box of the current move.

View file

@ -1,138 +0,0 @@
# Testing Plan (Refined)
This document defines the comprehensive testing strategy for the `inire` auto-router, ensuring analytic correctness and performance.
## 1. Unit Tests: Geometry & Components (`inire/geometry`)
### 1.1. Primitives (`primitives.py`)
* **Port Snapping:** Verify that `Port(x, y, orientation)` snaps `x` and `y` to the nearest **1nm** grid point.
* **Coordinate Transforms:**
* Translate a port by `(dx, dy)`.
* Rotate a port by `90`, `180`, `270` degrees around an origin.
* Verify orientation wrapping (e.g., $270^\circ + 90^\circ \to 0^\circ$).
* **Polygon Creation:**
* Generate a polygon from a list of points.
* Verify bounding box calculation.
* **Property-Based Testing (Hypothesis):**
* Verify that any `Port` after transformation remains snapped to the **1nm** grid.
* Verify that $Rotate(Rotate(Port, 90), -90)$ returns the original `Port` (up to snapping).
### 1.2. Component Generation (`components.py`)
* **Straight Moves:**
* Generate a `Straight(length=10.0, width=2.0)` segment.
* Verify the end port is exactly `length` away in the correct orientation.
* Verify the resulting polygon's dimensions.
* **Edge Case:** $L < 1\mu m$ (below search grid). Verify it still generates a valid 1nm segment.
* **Bend90:**
* Generate a `Bend90(radius=10.0, width=2.0, direction='CW')`.
* Verify the end port's orientation is changed by $-90^\circ$.
* Verify the end port's position is exactly `(R, -R)` relative to the start (for $0^\circ \to 270^\circ$).
* **Grid Snapping:** Verify that if the bend radius results in a non-1µm aligned port, it is snapped to the nearest **1µm** search grid point (with a warning).
* **SBend (Small Offset $O < 2R$):**
* Generate an `SBend(offset=5.0, radius=10.0, width=2.0)`.
* Verify the total length matches the analytical $L = 2\sqrt{O(2R - O/4)}$ (or equivalent arc-based formula).
* Verify the tangent continuity at the junction of the two arcs.
* **Edge Case:** $O = 2R$. Verify it either generates two 90-degree bends or fails gracefully with a clear error.
* Verify it fails/warns if $O > 2R$.
### 1.3. Geometric Fidelity
* **Arc Resolution (Sagitta):**
* Verify that `Bend90` and `SBend` polygons are approximated by segments such that the maximum deviation (sagitta) is within a user-defined tolerance (e.g., 10nm).
* Test with varying radii to ensure segment count scales appropriately.
## 2. Unit Tests: Collision & Cost (`inire/geometry/collision` & `router/cost`)
### 2.1. Collision Engine
* **Pre-dilation Logic:**
* Verify that an obstacle (polygon) is correctly dilated by $(W_{max} + C)/2$.
* **Heterogeneous Widths:** Verify that a path for Net A (width $W_1$) is dilated by $(W_1 + C)/2$, while Net B (width $W_2$) uses $(W_2 + C)/2$.
* **Locked Paths:**
* Insert an existing path geometry into the "Static Obstacle" R-Tree.
* Verify that the router treats it as an unmovable obstacle and avoids it.
* **R-Tree Queries:**
* Test intersection detection between two overlapping polygons.
* Test non-intersection between adjacent but non-overlapping polygons (exactly $C$ distance apart).
* **Safety Zone (2nm):**
* Create a port exactly on the edge of an obstacle.
* Verify that a "Move" starting from this port is NOT flagged for collision if the intersection occurs within **2nm** of the port.
* **Self-Intersection:**
* Verify that a path consisting of multiple segments is flagged if it loops back on itself.
### 2.2. Danger Map & Cost Evaluator
* **Danger Map Generation:**
* Initialize a map for a $100\mu m \times 100\mu m$ area with a single obstacle.
* Verify the cost $g_{proximity}$ matches $k/d^2$ for cells near the obstacle.
* Verify cost is $0$ for cells beyond the **Safety Threshold**.
* **Memory Check:**
* Mock a $20mm \times 20mm$ grid and verify memory allocation stays within limits (e.g., `< 2GB` for standard `uint8` resolution).
* **Cost Calculation:**
* Verify total cost $f(n)$ correctly sums length, bend penalties ($10 \times$ Manhattan), and proximity costs.
### 2.3. Robustness & Limits
* **Design Bounds:**
* Test routing at the extreme edges of the $20mm \times 20mm$ coordinate space.
* Verify that moves extending outside the design bounds are correctly pruned or flagged.
* **Empty/Invalid Inputs:**
* Test with an empty netlist.
* Test with start and end ports at the exact same location.
## 3. Integration Tests: Single-Net A* Search (`inire/router/astar`)
### 3.1. Open Space Scenarios
* **Straight Line:** Route from `(0,0,0)` to `(100,0,0)`. Verify it uses only `Straight` moves.
* **Simple Turn:** Route from `(0,0,0)` to `(20,20,90)`. Verify it uses a `Bend90` and `Straight` segments.
* **Small S-Bend:** Route with an offset of $5\mu m$ and radius $10\mu m$. Verify it uses the `SBend` component.
* **Large S-Bend ($O \ge 2R$):** Route with an offset of $50\mu m$ and radius $10\mu m$. Verify it combines two `Bend90`s and a `Straight` segment.
### 3.2. Obstacle Avoidance (The "Maze" Tests)
* **L-Obstacle:** Place an obstacle blocking the direct path. Verify the router goes around it.
* **Narrow Channel:** Create two obstacles with a gap slightly wider than $W_i + C$. Verify the router passes through.
* **Dead End:** Create a U-shaped obstacle. Verify the search explores alternatives and fails gracefully if no path exists.
### 3.3. Snapping & Precision
* **Snap-to-Target Lookahead:**
* Route to a target at `(100.005, 0, 0)` (not on 1µm grid).
* Verify the search reaches the vicinity via the 1µm grid and the final segment bridges the **5nm** gap exactly.
* **Grid Alignment:**
* Start from a port at `(0.5, 0.5, 0)`. Verify it snaps to the 1µm search grid correctly for the first move expansion.
### 3.4. Failure Modes
* **Unreachable Target:** Create a target completely enclosed by obstacles. Verify the search terminates after exploring all options (or hitting the 50,000 node limit) and returns an invalid result.
* **Start/End Collision:** Place a port deep inside an obstacle (beyond the 2nm safety zone). Verify the router identifies the immediate collision and fails gracefully.
## 4. Integration Tests: Multi-Net PathFinder (`inire/router/pathfinder`)
### 4.1. Congestion Scenarios
* **Parallel Paths:** Route two nets that can both take straight paths. Verify no reroutes occur.
* **The "Cross" Test:** Two nets must cross paths in 2D.
* Since crossings are illegal, verify the second net finds a detour.
* Verify the `Negotiated Congestion` loop increases the cost of the shared region.
* **Bottleneck:** Force 3 nets through a channel that only fits 2.
* Verify the router returns 2 valid paths and 1 "least bad" path (with collisions flagged).
* Verify the `is_valid=False` attribute is set for the failing net.
### 4.2. Determinism & Performance
* **Seed Consistency:** Run the same multi-net problem twice with the same seed; verify identical results (pixel-perfect).
* **Node Limit Enforcement:** Trigger a complex search that exceeds **50,000 nodes**. Verify it terminates and returns the best-so-far or failure.
* **Timeout:** Verify the session-level timeout stops the process for extremely large problems.
## 5. Benchmarking & Regression
* **Standard Benchmark Suite:** A set of 5-10 layouts with varying net counts (1 to 50).
* **Metrics to Track:**
* Total wire length.
* Total number of bends.
* Execution time per net.
* Success rate (percentage of nets routed without collisions).
* **Node Expansion Rate:** Nodes per second.
* **Memory Usage:** Peak RSS during 20x20mm routing.
* **Fuzz Testing:**
* Generate random obstacles and ports within a 1mm x 1mm area.
* Verify that the router never crashes.
* Verify that every result marked `is_valid=True` is confirmed collision-free by a high-precision (slow) check.
## 6. Analytic Correctness Guarantees
* **Post-Route Validation:**
* Implement an independent `validate_path(path, obstacles, clearance)` function using `shapely`'s most precise intersection tests.
* Run this on every test result to ensure the `CollisionEngine` (which uses R-Tree for speed) hasn't missed any edge cases.
* **Orientation Check:**
* Verify that the final port of every path matches the target orientation exactly $\{0, 90, 180, 270\}$.

View file

@ -35,7 +35,5 @@ Demonstrates the router's ability to handle complex orientation requirements, in
![Orientation Stress Test](05_orientation_stress.png) ![Orientation Stress Test](05_orientation_stress.png)
## 5. Tiered Fidelity & Lazy Dilation ## 5. Tiered Fidelity
Our architecture leverages two key optimizations for high-performance routing: The current implementation can use a cheaper bend proxy on the first negotiated-congestion pass before later passes fall back to the configured bend model. This is controlled by `RoutingOptions.congestion.use_tiered_strategy` together with the bend collision settings described in `DOCS.md`.
1. **Tiered Fidelity**: Initial routing passes use fast `clipped_bbox` proxies. If collisions are found, the system automatically escalates to high-fidelity `arc` geometry for the affected regions.
2. **Lazy Dilation**: Geometric buffering (dilation) is deferred until a collision check is strictly necessary, avoiding thousands of redundant `buffer()` and `translate()` calls.

View file

@ -37,6 +37,7 @@ class RoutingWorld:
"clearance", "clearance",
"safety_zone_radius", "safety_zone_radius",
"grid_cell_size", "grid_cell_size",
"metrics",
"_dynamic_paths", "_dynamic_paths",
"_static_obstacles", "_static_obstacles",
) )
@ -50,6 +51,7 @@ class RoutingWorld:
self.safety_zone_radius = safety_zone_radius self.safety_zone_radius = safety_zone_radius
self.grid_cell_size = 50.0 self.grid_cell_size = 50.0
self.metrics = None
self._static_obstacles = StaticObstacleIndex(self) self._static_obstacles = StaticObstacleIndex(self)
self._dynamic_paths = DynamicPathIndex(self) self._dynamic_paths = DynamicPathIndex(self)
@ -265,6 +267,8 @@ class RoutingWorld:
found_real = False found_real = False
for index in range(len(sub_tree_indices)): for index in range(len(sub_tree_indices)):
if self.metrics is not None:
self.metrics.total_congestion_exact_pair_checks += 1
test_geometry = geometries_to_test[sub_res_indices[index]] test_geometry = geometries_to_test[sub_res_indices[index]]
tree_geometry = tree_geometries[sub_tree_indices[index]] tree_geometry = tree_geometries[sub_tree_indices[index]]
if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7: if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7:
@ -277,6 +281,8 @@ class RoutingWorld:
return real_hits_count return real_hits_count
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int: def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
if self.metrics is not None:
self.metrics.total_congestion_check_calls += 1
dynamic_paths = self._dynamic_paths dynamic_paths = self._dynamic_paths
if not dynamic_paths.geometries: if not dynamic_paths.geometries:
return 0 return 0
@ -316,6 +322,8 @@ class RoutingWorld:
return self._check_real_congestion(result, net_id) return self._check_real_congestion(result, net_id)
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport: def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
if self.metrics is not None:
self.metrics.total_verify_path_report_calls += 1
static_collision_count = 0 static_collision_count = 0
dynamic_collision_count = 0 dynamic_collision_count = 0
self_collision_count = 0 self_collision_count = 0
@ -329,6 +337,8 @@ class RoutingWorld:
raw_geometries = static_obstacles.raw_tree.geometries raw_geometries = static_obstacles.raw_tree.geometries
for component in components: for component in components:
for polygon in component.physical_geometry: for polygon in component.physical_geometry:
if self.metrics is not None:
self.metrics.total_verify_static_buffer_ops += 1
buffered = polygon.buffer(self.clearance, join_style="mitre") buffered = polygon.buffer(self.clearance, join_style="mitre")
hits = static_obstacles.raw_tree.query(buffered, predicate="intersects") hits = static_obstacles.raw_tree.query(buffered, predicate="intersects")
for hit_idx in hits: for hit_idx in hits:
@ -355,6 +365,8 @@ class RoutingWorld:
if hit_net_ids[index] == str(net_id): if hit_net_ids[index] == str(net_id):
continue continue
if self.metrics is not None:
self.metrics.total_verify_dynamic_exact_pair_checks += 1
new_geometry = test_geometries[res_indices[index]] new_geometry = test_geometries[res_indices[index]]
tree_geometry = tree_geometries[tree_indices[index]] tree_geometry = tree_geometries[tree_indices[index]]
if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7: if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7:
@ -382,6 +394,8 @@ class RoutingWorld:
max_dist: float = 2000.0, max_dist: float = 2000.0,
net_width: float | None = None, net_width: float | None = None,
) -> float: ) -> float:
if self.metrics is not None:
self.metrics.total_ray_cast_calls += 1
static_obstacles = self._static_obstacles static_obstacles = self._static_obstacles
tree: STRtree | None tree: STRtree | None
is_rect_array: numpy.ndarray | None is_rect_array: numpy.ndarray | None
@ -410,6 +424,8 @@ class RoutingWorld:
candidates = tree.query(box(min_x, min_y, max_x, max_y)) candidates = tree.query(box(min_x, min_y, max_x, max_y))
if candidates.size == 0: if candidates.size == 0:
return max_dist return max_dist
if self.metrics is not None:
self.metrics.total_ray_cast_candidate_bounds += int(candidates.size)
min_dist = max_dist min_dist = max_dist
inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30 inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30
@ -457,6 +473,8 @@ class RoutingWorld:
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)]) ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
obstacle = tree_geometries[candidate_id] obstacle = tree_geometries[candidate_id]
if self.metrics is not None:
self.metrics.total_ray_cast_exact_geometry_checks += 1
if not obstacle.intersects(ray_line): if not obstacle.intersects(ray_line):
continue continue

View file

@ -48,6 +48,8 @@ class DynamicPathIndex:
def ensure_tree(self) -> None: def ensure_tree(self) -> None:
if self.tree is None and self.dilated: if self.tree is None and self.dilated:
if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_tree_rebuilds += 1
ids, geometries, bounds_array = build_index_payload(self.dilated) ids, geometries, bounds_array = build_index_payload(self.dilated)
self.tree = STRtree(geometries) self.tree = STRtree(geometries)
self.obj_ids = numpy.array(ids, dtype=numpy.int32) self.obj_ids = numpy.array(ids, dtype=numpy.int32)
@ -59,6 +61,8 @@ class DynamicPathIndex:
if self.grid or not self.dilated: if self.grid or not self.dilated:
return return
if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_grid_rebuilds += 1
cell_size = self.engine.grid_cell_size cell_size = self.engine.grid_cell_size
for obj_id, polygon in self.dilated.items(): for obj_id, polygon in self.dilated.items():
for cell in iter_grid_cells(polygon.bounds, cell_size): for cell in iter_grid_cells(polygon.bounds, cell_size):
@ -66,6 +70,8 @@ class DynamicPathIndex:
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None: def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
self.invalidate_queries() self.invalidate_queries()
if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
for index, polygon in enumerate(geometry): for index, polygon in enumerate(geometry):
obj_id = self.id_counter obj_id = self.id_counter
self.id_counter += 1 self.id_counter += 1
@ -83,6 +89,8 @@ class DynamicPathIndex:
return return
self.invalidate_queries() self.invalidate_queries()
if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids)
for obj_id in obj_ids: for obj_id in obj_ids:
self.index.delete(obj_id, self.dilated[obj_id].bounds) self.index.delete(obj_id, self.dilated[obj_id].bounds)
del self.geometries[obj_id] del self.geometries[obj_id]

View file

@ -93,6 +93,8 @@ class StaticObstacleIndex:
def ensure_tree(self) -> None: def ensure_tree(self) -> None:
if self.tree is None and self.dilated: if self.tree is None and self.dilated:
if self.engine.metrics is not None:
self.engine.metrics.total_static_tree_rebuilds += 1
self.obj_ids, geometries, self.bounds_array = build_index_payload(self.dilated) self.obj_ids, geometries, self.bounds_array = build_index_payload(self.dilated)
self.tree = STRtree(geometries) self.tree = STRtree(geometries)
self.is_rect_array = numpy.array([self.is_rect[i] for i in self.obj_ids]) self.is_rect_array = numpy.array([self.is_rect[i] for i in self.obj_ids])
@ -102,6 +104,8 @@ class StaticObstacleIndex:
if key in self.net_specific_trees: if key in self.net_specific_trees:
return self.net_specific_trees[key] return self.net_specific_trees[key]
if self.engine.metrics is not None:
self.engine.metrics.total_static_net_tree_rebuilds += 1
total_dilation = net_width / 2.0 + self.engine.clearance total_dilation = net_width / 2.0 + self.engine.clearance
geometries = [] geometries = []
is_rect_list = [] is_rect_list = []
@ -122,5 +126,7 @@ class StaticObstacleIndex:
def ensure_raw_tree(self) -> None: def ensure_raw_tree(self) -> None:
if self.raw_tree is None and self.geometries: if self.raw_tree is None and self.geometries:
if self.engine.metrics is not None:
self.engine.metrics.total_static_raw_tree_rebuilds += 1
self.raw_obj_ids, geometries, _bounds_array = build_index_payload(self.geometries) self.raw_obj_ids, geometries, _bounds_array = build_index_payload(self.geometries)
self.raw_tree = STRtree(geometries) self.raw_tree = STRtree(geometries)

View file

@ -38,6 +38,43 @@ class RouteMetrics:
pruned_closed_set: int pruned_closed_set: int
pruned_hard_collision: int pruned_hard_collision: int
pruned_cost: int pruned_cost: int
route_iterations: int
nets_routed: int
nets_reached_target: int
warm_start_paths_built: int
warm_start_paths_used: int
refine_path_calls: int
timeout_events: int
move_cache_abs_hits: int
move_cache_abs_misses: int
move_cache_rel_hits: int
move_cache_rel_misses: int
static_safe_cache_hits: int
hard_collision_cache_hits: int
congestion_cache_hits: int
congestion_cache_misses: int
dynamic_path_objects_added: int
dynamic_path_objects_removed: int
dynamic_tree_rebuilds: int
dynamic_grid_rebuilds: int
static_tree_rebuilds: int
static_raw_tree_rebuilds: int
static_net_tree_rebuilds: int
visibility_builds: int
visibility_corner_pairs_checked: int
visibility_corner_queries: int
visibility_corner_hits: int
visibility_point_queries: int
visibility_point_cache_hits: int
visibility_point_cache_misses: int
ray_cast_calls: int
ray_cast_candidate_bounds: int
ray_cast_exact_geometry_checks: int
congestion_check_calls: int
congestion_exact_pair_checks: int
verify_path_report_calls: int
verify_static_buffer_ops: int
verify_dynamic_exact_pair_checks: int
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)

View file

@ -47,8 +47,10 @@ def process_move(
self_dilation, self_dilation,
) )
if abs_key in context.move_cache_abs: if abs_key in context.move_cache_abs:
context.metrics.total_move_cache_abs_hits += 1
res = context.move_cache_abs[abs_key] res = context.move_cache_abs[abs_key]
else: else:
context.metrics.total_move_cache_abs_misses += 1
context.check_cache_eviction() context.check_cache_eviction()
base_port = Port(0, 0, cp.r) base_port = Port(0, 0, cp.r)
rel_key = ( rel_key = (
@ -61,8 +63,10 @@ def process_move(
self_dilation, self_dilation,
) )
if rel_key in context.move_cache_rel: if rel_key in context.move_cache_rel:
context.metrics.total_move_cache_rel_hits += 1
res_rel = context.move_cache_rel[rel_key] res_rel = context.move_cache_rel[rel_key]
else: else:
context.metrics.total_move_cache_rel_misses += 1
try: try:
if move_class == "straight": if move_class == "straight":
res_rel = Straight.generate(base_port, params[0], net_width, dilation=self_dilation) res_rel = Straight.generate(base_port, params[0], net_width, dilation=self_dilation)
@ -139,11 +143,14 @@ def add_node(
end_p = result.end_port end_p = result.end_port
if cache_key in context.hard_collision_set: if cache_key in context.hard_collision_set:
context.metrics.total_hard_collision_cache_hits += 1
metrics.pruned_hard_collision += 1 metrics.pruned_hard_collision += 1
metrics.total_pruned_hard_collision += 1 metrics.total_pruned_hard_collision += 1
return return
is_static_safe = cache_key in context.static_safe_cache is_static_safe = cache_key in context.static_safe_cache
if is_static_safe:
context.metrics.total_static_safe_cache_hits += 1
if not is_static_safe: if not is_static_safe:
ce = context.cost_evaluator.collision_engine ce = context.cost_evaluator.collision_engine
if move_type == "straight": if move_type == "straight":
@ -160,8 +167,10 @@ def add_node(
total_overlaps = 0 total_overlaps = 0
if not config.skip_congestion: if not config.skip_congestion:
if cache_key in congestion_cache: if cache_key in congestion_cache:
context.metrics.total_congestion_cache_hits += 1
total_overlaps = congestion_cache[cache_key] total_overlaps = congestion_cache[cache_key]
else: else:
context.metrics.total_congestion_cache_misses += 1
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id) total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
congestion_cache[cache_key] = total_overlaps congestion_cache[cache_key] = total_overlaps

View file

@ -87,6 +87,43 @@ class AStarMetrics:
"total_pruned_closed_set", "total_pruned_closed_set",
"total_pruned_hard_collision", "total_pruned_hard_collision",
"total_pruned_cost", "total_pruned_cost",
"total_route_iterations",
"total_nets_routed",
"total_nets_reached_target",
"total_warm_start_paths_built",
"total_warm_start_paths_used",
"total_refine_path_calls",
"total_timeout_events",
"total_move_cache_abs_hits",
"total_move_cache_abs_misses",
"total_move_cache_rel_hits",
"total_move_cache_rel_misses",
"total_static_safe_cache_hits",
"total_hard_collision_cache_hits",
"total_congestion_cache_hits",
"total_congestion_cache_misses",
"total_dynamic_path_objects_added",
"total_dynamic_path_objects_removed",
"total_dynamic_tree_rebuilds",
"total_dynamic_grid_rebuilds",
"total_static_tree_rebuilds",
"total_static_raw_tree_rebuilds",
"total_static_net_tree_rebuilds",
"total_visibility_builds",
"total_visibility_corner_pairs_checked",
"total_visibility_corner_queries",
"total_visibility_corner_hits",
"total_visibility_point_queries",
"total_visibility_point_cache_hits",
"total_visibility_point_cache_misses",
"total_ray_cast_calls",
"total_ray_cast_candidate_bounds",
"total_ray_cast_exact_geometry_checks",
"total_congestion_check_calls",
"total_congestion_exact_pair_checks",
"total_verify_path_report_calls",
"total_verify_static_buffer_ops",
"total_verify_dynamic_exact_pair_checks",
"last_expanded_nodes", "last_expanded_nodes",
"nodes_expanded", "nodes_expanded",
"moves_generated", "moves_generated",
@ -103,6 +140,43 @@ class AStarMetrics:
self.total_pruned_closed_set = 0 self.total_pruned_closed_set = 0
self.total_pruned_hard_collision = 0 self.total_pruned_hard_collision = 0
self.total_pruned_cost = 0 self.total_pruned_cost = 0
self.total_route_iterations = 0
self.total_nets_routed = 0
self.total_nets_reached_target = 0
self.total_warm_start_paths_built = 0
self.total_warm_start_paths_used = 0
self.total_refine_path_calls = 0
self.total_timeout_events = 0
self.total_move_cache_abs_hits = 0
self.total_move_cache_abs_misses = 0
self.total_move_cache_rel_hits = 0
self.total_move_cache_rel_misses = 0
self.total_static_safe_cache_hits = 0
self.total_hard_collision_cache_hits = 0
self.total_congestion_cache_hits = 0
self.total_congestion_cache_misses = 0
self.total_dynamic_path_objects_added = 0
self.total_dynamic_path_objects_removed = 0
self.total_dynamic_tree_rebuilds = 0
self.total_dynamic_grid_rebuilds = 0
self.total_static_tree_rebuilds = 0
self.total_static_raw_tree_rebuilds = 0
self.total_static_net_tree_rebuilds = 0
self.total_visibility_builds = 0
self.total_visibility_corner_pairs_checked = 0
self.total_visibility_corner_queries = 0
self.total_visibility_corner_hits = 0
self.total_visibility_point_queries = 0
self.total_visibility_point_cache_hits = 0
self.total_visibility_point_cache_misses = 0
self.total_ray_cast_calls = 0
self.total_ray_cast_candidate_bounds = 0
self.total_ray_cast_exact_geometry_checks = 0
self.total_congestion_check_calls = 0
self.total_congestion_exact_pair_checks = 0
self.total_verify_path_report_calls = 0
self.total_verify_static_buffer_ops = 0
self.total_verify_dynamic_exact_pair_checks = 0
self.last_expanded_nodes: list[tuple[int, int, int]] = [] self.last_expanded_nodes: list[tuple[int, int, int]] = []
self.nodes_expanded = 0 self.nodes_expanded = 0
self.moves_generated = 0 self.moves_generated = 0
@ -118,6 +192,43 @@ class AStarMetrics:
self.total_pruned_closed_set = 0 self.total_pruned_closed_set = 0
self.total_pruned_hard_collision = 0 self.total_pruned_hard_collision = 0
self.total_pruned_cost = 0 self.total_pruned_cost = 0
self.total_route_iterations = 0
self.total_nets_routed = 0
self.total_nets_reached_target = 0
self.total_warm_start_paths_built = 0
self.total_warm_start_paths_used = 0
self.total_refine_path_calls = 0
self.total_timeout_events = 0
self.total_move_cache_abs_hits = 0
self.total_move_cache_abs_misses = 0
self.total_move_cache_rel_hits = 0
self.total_move_cache_rel_misses = 0
self.total_static_safe_cache_hits = 0
self.total_hard_collision_cache_hits = 0
self.total_congestion_cache_hits = 0
self.total_congestion_cache_misses = 0
self.total_dynamic_path_objects_added = 0
self.total_dynamic_path_objects_removed = 0
self.total_dynamic_tree_rebuilds = 0
self.total_dynamic_grid_rebuilds = 0
self.total_static_tree_rebuilds = 0
self.total_static_raw_tree_rebuilds = 0
self.total_static_net_tree_rebuilds = 0
self.total_visibility_builds = 0
self.total_visibility_corner_pairs_checked = 0
self.total_visibility_corner_queries = 0
self.total_visibility_corner_hits = 0
self.total_visibility_point_queries = 0
self.total_visibility_point_cache_hits = 0
self.total_visibility_point_cache_misses = 0
self.total_ray_cast_calls = 0
self.total_ray_cast_candidate_bounds = 0
self.total_ray_cast_exact_geometry_checks = 0
self.total_congestion_check_calls = 0
self.total_congestion_exact_pair_checks = 0
self.total_verify_path_report_calls = 0
self.total_verify_static_buffer_ops = 0
self.total_verify_dynamic_exact_pair_checks = 0
def reset_per_route(self) -> None: def reset_per_route(self) -> None:
self.nodes_expanded = 0 self.nodes_expanded = 0
@ -136,12 +247,50 @@ class AStarMetrics:
pruned_closed_set=self.total_pruned_closed_set, pruned_closed_set=self.total_pruned_closed_set,
pruned_hard_collision=self.total_pruned_hard_collision, pruned_hard_collision=self.total_pruned_hard_collision,
pruned_cost=self.total_pruned_cost, pruned_cost=self.total_pruned_cost,
route_iterations=self.total_route_iterations,
nets_routed=self.total_nets_routed,
nets_reached_target=self.total_nets_reached_target,
warm_start_paths_built=self.total_warm_start_paths_built,
warm_start_paths_used=self.total_warm_start_paths_used,
refine_path_calls=self.total_refine_path_calls,
timeout_events=self.total_timeout_events,
move_cache_abs_hits=self.total_move_cache_abs_hits,
move_cache_abs_misses=self.total_move_cache_abs_misses,
move_cache_rel_hits=self.total_move_cache_rel_hits,
move_cache_rel_misses=self.total_move_cache_rel_misses,
static_safe_cache_hits=self.total_static_safe_cache_hits,
hard_collision_cache_hits=self.total_hard_collision_cache_hits,
congestion_cache_hits=self.total_congestion_cache_hits,
congestion_cache_misses=self.total_congestion_cache_misses,
dynamic_path_objects_added=self.total_dynamic_path_objects_added,
dynamic_path_objects_removed=self.total_dynamic_path_objects_removed,
dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds,
dynamic_grid_rebuilds=self.total_dynamic_grid_rebuilds,
static_tree_rebuilds=self.total_static_tree_rebuilds,
static_raw_tree_rebuilds=self.total_static_raw_tree_rebuilds,
static_net_tree_rebuilds=self.total_static_net_tree_rebuilds,
visibility_builds=self.total_visibility_builds,
visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked,
visibility_corner_queries=self.total_visibility_corner_queries,
visibility_corner_hits=self.total_visibility_corner_hits,
visibility_point_queries=self.total_visibility_point_queries,
visibility_point_cache_hits=self.total_visibility_point_cache_hits,
visibility_point_cache_misses=self.total_visibility_point_cache_misses,
ray_cast_calls=self.total_ray_cast_calls,
ray_cast_candidate_bounds=self.total_ray_cast_candidate_bounds,
ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks,
congestion_check_calls=self.total_congestion_check_calls,
congestion_exact_pair_checks=self.total_congestion_exact_pair_checks,
verify_path_report_calls=self.total_verify_path_report_calls,
verify_static_buffer_ops=self.total_verify_static_buffer_ops,
verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks,
) )
class AStarContext: class AStarContext:
__slots__ = ( __slots__ = (
"cost_evaluator", "cost_evaluator",
"metrics",
"congestion_penalty", "congestion_penalty",
"min_bend_radius", "min_bend_radius",
"problem", "problem",
@ -160,9 +309,11 @@ class AStarContext:
cost_evaluator: CostEvaluator, cost_evaluator: CostEvaluator,
problem: RoutingProblem, problem: RoutingProblem,
options: RoutingOptions, options: RoutingOptions,
metrics: AStarMetrics | None = None,
max_cache_size: int = 1000000, max_cache_size: int = 1000000,
) -> None: ) -> None:
self.cost_evaluator = cost_evaluator self.cost_evaluator = cost_evaluator
self.metrics = metrics if metrics is not None else AStarMetrics()
self.congestion_penalty = 0.0 self.congestion_penalty = 0.0
self.max_cache_size = max_cache_size self.max_cache_size = max_cache_size
self.problem = problem self.problem = problem

View file

@ -45,7 +45,9 @@ class PathFinder:
metrics: AStarMetrics | None = None, metrics: AStarMetrics | None = None,
) -> None: ) -> None:
self.context = context self.context = context
self.metrics = metrics if metrics is not None else AStarMetrics() self.metrics = self.context.metrics if metrics is None else metrics
self.context.metrics = self.metrics
self.context.cost_evaluator.collision_engine.metrics = self.metrics
self.refiner = PathRefiner(self.context) self.refiner = PathRefiner(self.context)
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
@ -106,6 +108,7 @@ class PathFinder:
) )
if not path: if not path:
continue continue
self.metrics.total_warm_start_paths_built += 1
greedy_paths[net_id] = tuple(path) greedy_paths[net_id] = tuple(path)
for result in path: for result in path:
for polygon in result.physical_geometry: for polygon in result.physical_geometry:
@ -170,9 +173,11 @@ class PathFinder:
congestion = self.context.options.congestion congestion = self.context.options.congestion
diagnostics = self.context.options.diagnostics diagnostics = self.context.options.diagnostics
net = state.net_specs[net_id] net = state.net_specs[net_id]
self.metrics.total_nets_routed += 1
self.context.cost_evaluator.collision_engine.remove_path(net_id) self.context.cost_evaluator.collision_engine.remove_path(net_id)
if iteration == 0 and state.initial_paths and net_id in state.initial_paths: if iteration == 0 and state.initial_paths and net_id in state.initial_paths:
self.metrics.total_warm_start_paths_used += 1
path: Sequence[ComponentResult] | None = state.initial_paths[net_id] path: Sequence[ComponentResult] | None = state.initial_paths[net_id]
else: else:
coll_model, _ = resolve_bend_geometry(search) coll_model, _ = resolve_bend_geometry(search)
@ -208,6 +213,8 @@ class PathFinder:
return RoutingResult(net_id=net_id, path=(), reached_target=False) return RoutingResult(net_id=net_id, path=(), reached_target=False)
reached_target = path[-1].end_port == net.target reached_target = path[-1].end_port == net.target
if reached_target:
self.metrics.total_nets_reached_target += 1
report = None report = None
self._install_path(net_id, path) self._install_path(net_id, path)
if reached_target: if reached_target:
@ -230,6 +237,7 @@ class PathFinder:
) -> dict[str, RoutingOutcome] | None: ) -> dict[str, RoutingOutcome] | None:
outcomes: dict[str, RoutingOutcome] = {} outcomes: dict[str, RoutingOutcome] = {}
congestion = self.context.options.congestion congestion = self.context.options.congestion
self.metrics.total_route_iterations += 1
self.metrics.reset_per_route() self.metrics.reset_per_route()
if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None): if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None):
@ -238,6 +246,7 @@ class PathFinder:
for net_id in state.ordered_net_ids: for net_id in state.ordered_net_ids:
if time.monotonic() - state.start_time > state.timeout_s: if time.monotonic() - state.start_time > state.timeout_s:
self.metrics.total_timeout_events += 1
return None return None
result = self._route_net_once(state, iteration, net_id) result = self._route_net_once(state, iteration, net_id)
@ -272,6 +281,7 @@ class PathFinder:
if not result or not result.path or result.outcome in {"colliding", "partial", "unroutable"}: if not result or not result.path or result.outcome in {"colliding", "partial", "unroutable"}:
continue continue
net = state.net_specs[net_id] net = state.net_specs[net_id]
self.metrics.total_refine_path_calls += 1
self.context.cost_evaluator.collision_engine.remove_path(net_id) self.context.cost_evaluator.collision_engine.remove_path(net_id)
refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path) refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path)
self._install_path(net_id, refined_path) self._install_path(net_id, refined_path)

View file

@ -45,6 +45,8 @@ class VisibilityManager:
""" """
Extract corners and pre-compute corner-to-corner visibility. Extract corners and pre-compute corner-to-corner visibility.
""" """
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_builds += 1
self._built_static_version = self.collision_engine.get_static_version() self._built_static_version = self.collision_engine.get_static_version()
raw_corners = [] raw_corners = []
for poly in self.collision_engine.iter_static_dilated_geometries(): for poly in self.collision_engine.iter_static_dilated_geometries():
@ -85,6 +87,8 @@ class VisibilityManager:
for j in range(num_corners): for j in range(num_corners):
if i == j: if i == j:
continue continue
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_corner_pairs_checked += 1
cx, cy = self.corners[j] cx, cy = self.corners[j]
dx, dy = cx - p1.x, cy - p1.y dx, dy = cx - p1.x, cy - p1.y
dist = numpy.sqrt(dx**2 + dy**2) dist = numpy.sqrt(dx**2 + dy**2)
@ -107,6 +111,8 @@ class VisibilityManager:
Find visible corners from an arbitrary point. Find visible corners from an arbitrary point.
This may perform direct ray-cast scans and is not intended for hot search paths. This may perform direct ray-cast scans and is not intended for hot search paths.
""" """
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_point_queries += 1
self._ensure_current() self._ensure_current()
if max_dist < 0: if max_dist < 0:
return [] return []
@ -118,7 +124,11 @@ class VisibilityManager:
ox, oy = round(origin.x, 3), round(origin.y, 3) ox, oy = round(origin.x, 3), round(origin.y, 3)
cache_key = (int(ox * 1000), int(oy * 1000), int(round(max_dist * 1000))) cache_key = (int(ox * 1000), int(oy * 1000), int(round(max_dist * 1000)))
if cache_key in self._point_visibility_cache: if cache_key in self._point_visibility_cache:
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_point_cache_hits += 1
return self._point_visibility_cache[cache_key] return self._point_visibility_cache[cache_key]
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_point_cache_misses += 1
bounds = (origin.x - max_dist, origin.y - max_dist, origin.x + max_dist, origin.y + max_dist) bounds = (origin.x - max_dist, origin.y - max_dist, origin.x + max_dist, origin.y + max_dist)
candidates = list(self.corner_index.intersection(bounds)) candidates = list(self.corner_index.intersection(bounds))
@ -145,11 +155,15 @@ class VisibilityManager:
Return precomputed visibility only when the origin is already at a known corner. Return precomputed visibility only when the origin is already at a known corner.
This avoids the expensive arbitrary-point visibility scan in hot search paths. This avoids the expensive arbitrary-point visibility scan in hot search paths.
""" """
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_corner_queries += 1
self._ensure_current() self._ensure_current()
if max_dist < 0: if max_dist < 0:
return [] return []
corner_idx = self._corner_idx_at(origin) corner_idx = self._corner_idx_at(origin)
if corner_idx is not None and corner_idx in self._corner_graph: if corner_idx is not None and corner_idx in self._corner_graph:
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_corner_hits += 1
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist] return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
return [] return []

View file

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from time import perf_counter from time import perf_counter
from collections.abc import Callable from collections.abc import Callable
@ -18,6 +19,7 @@ from inire import (
) )
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.results import RouteMetrics
from inire.router._astar_types import AStarContext, AStarMetrics from inire.router._astar_types import AStarContext, AStarMetrics
from inire.router._router import PathFinder from inire.router._router import PathFinder
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
@ -31,6 +33,25 @@ _OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__)
ScenarioOutcome = tuple[float, int, int, int] ScenarioOutcome = tuple[float, int, int, int]
ScenarioRun = Callable[[], ScenarioOutcome] ScenarioRun = Callable[[], ScenarioOutcome]
ScenarioSnapshotRun = Callable[[], "ScenarioSnapshot"]
@dataclass(frozen=True, slots=True)
class ScenarioSnapshot:
name: str
duration_s: float
total_results: int
valid_results: int
reached_targets: int
metrics: RouteMetrics
def as_outcome(self) -> ScenarioOutcome:
return (
self.duration_s,
self.total_results,
self.valid_results,
self.reached_targets,
)
def _summarize(results: dict[str, RoutingResult], duration_s: float) -> ScenarioOutcome: def _summarize(results: dict[str, RoutingResult], duration_s: float) -> ScenarioOutcome:
@ -42,6 +63,32 @@ def _summarize(results: dict[str, RoutingResult], duration_s: float) -> Scenario
) )
def _make_snapshot(
name: str,
results: dict[str, RoutingResult],
duration_s: float,
metrics: RouteMetrics,
) -> ScenarioSnapshot:
return ScenarioSnapshot(
name=name,
duration_s=duration_s,
total_results=len(results),
valid_results=sum(1 for result in results.values() if result.is_valid),
reached_targets=sum(1 for result in results.values() if result.reached_target),
metrics=metrics,
)
def _sum_metrics(metrics_list: tuple[RouteMetrics, ...]) -> RouteMetrics:
metric_names = RouteMetrics.__dataclass_fields__
return RouteMetrics(
**{
name: sum(getattr(metrics, name) for metrics in metrics_list)
for name in metric_names
}
)
def _build_evaluator( def _build_evaluator(
bounds: tuple[float, float, float, float], bounds: tuple[float, float, float, float],
*, *,
@ -93,13 +140,15 @@ def _build_pathfinder(
metrics: AStarMetrics | None = None, metrics: AStarMetrics | None = None,
**request_kwargs: object, **request_kwargs: object,
) -> PathFinder: ) -> PathFinder:
resolved_metrics = AStarMetrics() if metrics is None else metrics
return PathFinder( return PathFinder(
AStarContext( AStarContext(
evaluator, evaluator,
RoutingProblem(bounds=bounds, nets=nets), RoutingProblem(bounds=bounds, nets=nets),
_build_options(**request_kwargs), _build_options(**request_kwargs),
metrics=resolved_metrics,
), ),
metrics=metrics, metrics=resolved_metrics,
) )
@ -133,7 +182,7 @@ def _build_routing_stack(
return engine, evaluator, metrics, pathfinder return engine, evaluator, metrics, pathfinder
def run_example_01() -> ScenarioOutcome: def snapshot_example_01() -> ScenarioSnapshot:
netlist = {"net1": (Port(10, 50, 0), Port(90, 50, 0))} netlist = {"net1": (Port(10, 50, 0), Port(90, 50, 0))}
widths = {"net1": 2.0} widths = {"net1": 2.0}
_, _, _, pathfinder = _build_routing_stack( _, _, _, pathfinder = _build_routing_stack(
@ -145,10 +194,14 @@ def run_example_01() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_01_simple_route", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_02() -> ScenarioOutcome: def run_example_01() -> ScenarioOutcome:
return snapshot_example_01().as_outcome()
def snapshot_example_02() -> ScenarioSnapshot:
netlist = { netlist = {
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)), "horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
@ -173,10 +226,14 @@ def run_example_02() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_02_congestion_resolution", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_03() -> ScenarioOutcome: def run_example_02() -> ScenarioOutcome:
return snapshot_example_02().as_outcome()
def snapshot_example_03() -> ScenarioSnapshot:
netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))} netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))}
widths_a = {"netA": 2.0} widths_a = {"netA": 2.0}
engine, evaluator, _, pathfinder = _build_routing_stack( engine, evaluator, _, pathfinder = _build_routing_stack(
@ -187,19 +244,26 @@ def run_example_03() -> ScenarioOutcome:
) )
t0 = perf_counter() t0 = perf_counter()
results_a = pathfinder.route_all() results_a = pathfinder.route_all()
metrics_a = pathfinder.metrics.snapshot()
for polygon in results_a["netA"].locked_geometry: for polygon in results_a["netA"].locked_geometry:
engine.add_static_obstacle(polygon) engine.add_static_obstacle(polygon)
results_b = _build_pathfinder( pathfinder_b = _build_pathfinder(
evaluator, evaluator,
bounds=(0, -50, 100, 50), bounds=(0, -50, 100, 50),
nets=_net_specs({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0}), nets=_net_specs({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0}),
bend_radii=[10.0], bend_radii=[10.0],
).route_all() )
results_b = pathfinder_b.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize({**results_a, **results_b}, t1 - t0) combined_metrics = _sum_metrics((metrics_a, pathfinder_b.metrics.snapshot()))
return _make_snapshot("example_03_locked_paths", {**results_a, **results_b}, t1 - t0, combined_metrics)
def run_example_04() -> ScenarioOutcome: def run_example_03() -> ScenarioOutcome:
return snapshot_example_03().as_outcome()
def snapshot_example_04() -> ScenarioSnapshot:
netlist = { netlist = {
"sbend_only": (Port(10, 50, 0), Port(60, 55, 0)), "sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
"multi_radii": (Port(10, 10, 0), Port(90, 90, 0)), "multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
@ -223,10 +287,14 @@ def run_example_04() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_04_sbends_and_radii", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_05() -> ScenarioOutcome: def run_example_04() -> ScenarioOutcome:
return snapshot_example_04().as_outcome()
def snapshot_example_05() -> ScenarioSnapshot:
netlist = { netlist = {
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)), "u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
"loop": (Port(100, 100, 90), Port(100, 80, 270)), "loop": (Port(100, 100, 90), Port(100, 80, 270)),
@ -243,10 +311,14 @@ def run_example_05() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_05_orientation_stress", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_06() -> ScenarioOutcome: def run_example_05() -> ScenarioOutcome:
return snapshot_example_05().as_outcome()
def snapshot_example_06() -> ScenarioSnapshot:
bounds = (-20, -20, 170, 170) bounds = (-20, -20, 170, 170)
obstacles = [ obstacles = [
box(40, 110, 60, 130), box(40, 110, 60, 130),
@ -282,6 +354,7 @@ def run_example_06() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
combined_results: dict[str, RoutingResult] = {} combined_results: dict[str, RoutingResult] = {}
route_metrics: list[RouteMetrics] = []
for evaluator, netlist, net_widths, request_kwargs in scenarios: for evaluator, netlist, net_widths, request_kwargs in scenarios:
pathfinder = _build_pathfinder( pathfinder = _build_pathfinder(
evaluator, evaluator,
@ -290,11 +363,21 @@ def run_example_06() -> ScenarioOutcome:
**request_kwargs, **request_kwargs,
) )
combined_results.update(pathfinder.route_all()) combined_results.update(pathfinder.route_all())
route_metrics.append(pathfinder.metrics.snapshot())
t1 = perf_counter() t1 = perf_counter()
return _summarize(combined_results, t1 - t0) return _make_snapshot(
"example_06_bend_collision_models",
combined_results,
t1 - t0,
_sum_metrics(tuple(route_metrics)),
)
def run_example_07() -> ScenarioOutcome: def run_example_06() -> ScenarioOutcome:
return snapshot_example_06().as_outcome()
def snapshot_example_07() -> ScenarioSnapshot:
bounds = (0, 0, 1000, 1000) bounds = (0, 0, 1000, 1000)
obstacles = [ obstacles = [
box(450, 0, 550, 400), box(450, 0, 550, 400),
@ -349,10 +432,14 @@ def run_example_07() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all(iteration_callback=iteration_callback) results = pathfinder.route_all(iteration_callback=iteration_callback)
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_07_large_scale_routing", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_08() -> ScenarioOutcome: def run_example_07() -> ScenarioOutcome:
return snapshot_example_07().as_outcome()
def snapshot_example_08() -> ScenarioSnapshot:
bounds = (0, 0, 150, 150) bounds = (0, 0, 150, 150)
netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))} netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))}
widths = {"standard_arc": 2.0} widths = {"standard_arc": 2.0}
@ -360,7 +447,7 @@ def run_example_08() -> ScenarioOutcome:
custom_proxy = box(0, -11, 11, 0) custom_proxy = box(0, -11, 11, 0)
t0 = perf_counter() t0 = perf_counter()
results_std = _build_pathfinder( pathfinder_std = _build_pathfinder(
_build_evaluator(bounds), _build_evaluator(bounds),
bounds=bounds, bounds=bounds,
nets=_net_specs(netlist, widths), nets=_net_specs(netlist, widths),
@ -369,8 +456,9 @@ def run_example_08() -> ScenarioOutcome:
max_iterations=1, max_iterations=1,
use_tiered_strategy=False, use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() )
results_custom = _build_pathfinder( results_std = pathfinder_std.route_all()
pathfinder_custom = _build_pathfinder(
_build_evaluator(bounds), _build_evaluator(bounds),
bounds=bounds, bounds=bounds,
nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}), nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}),
@ -381,12 +469,23 @@ def run_example_08() -> ScenarioOutcome:
max_iterations=1, max_iterations=1,
use_tiered_strategy=False, use_tiered_strategy=False,
metrics=AStarMetrics(), metrics=AStarMetrics(),
).route_all() )
results_custom = pathfinder_custom.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize({**results_std, **results_custom}, t1 - t0) combined_metrics = _sum_metrics((pathfinder_std.metrics.snapshot(), pathfinder_custom.metrics.snapshot()))
return _make_snapshot(
"example_08_custom_bend_geometry",
{**results_std, **results_custom},
t1 - t0,
combined_metrics,
)
def run_example_09() -> ScenarioOutcome: def run_example_08() -> ScenarioOutcome:
return snapshot_example_08().as_outcome()
def snapshot_example_09() -> ScenarioSnapshot:
obstacles = [ obstacles = [
box(35, 35, 45, 65), box(35, 35, 45, 65),
box(55, 35, 65, 65), box(55, 35, 65, 65),
@ -404,7 +503,11 @@ def run_example_09() -> ScenarioOutcome:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all() results = pathfinder.route_all()
t1 = perf_counter() t1 = perf_counter()
return _summarize(results, t1 - t0) return _make_snapshot("example_09_unroutable_best_effort", results, t1 - t0, pathfinder.metrics.snapshot())
def run_example_09() -> ScenarioOutcome:
return snapshot_example_09().as_outcome()
SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = ( SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
@ -418,3 +521,19 @@ SCENARIOS: tuple[tuple[str, ScenarioRun], ...] = (
("example_08_custom_bend_geometry", run_example_08), ("example_08_custom_bend_geometry", run_example_08),
("example_09_unroutable_best_effort", run_example_09), ("example_09_unroutable_best_effort", run_example_09),
) )
SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
("example_01_simple_route", snapshot_example_01),
("example_02_congestion_resolution", snapshot_example_02),
("example_03_locked_paths", snapshot_example_03),
("example_04_sbends_and_radii", snapshot_example_04),
("example_05_orientation_stress", snapshot_example_05),
("example_06_bend_collision_models", snapshot_example_06),
("example_07_large_scale_routing", snapshot_example_07),
("example_08_custom_bend_geometry", snapshot_example_08),
("example_09_unroutable_best_effort", snapshot_example_09),
)
def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]:
return tuple(run() for _, run in SCENARIO_SNAPSHOTS)

View file

@ -76,6 +76,13 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
assert run.results_by_net["net1"].reached_target assert run.results_by_net["net1"].reached_target
assert run.expanded_nodes assert run.expanded_nodes
assert run.metrics.nodes_expanded > 0 assert run.metrics.nodes_expanded > 0
assert run.metrics.route_iterations >= 1
assert run.metrics.nets_routed >= 1
assert run.metrics.move_cache_abs_misses >= 0
assert run.metrics.ray_cast_calls >= 0
assert run.metrics.dynamic_tree_rebuilds >= 0
assert run.metrics.visibility_builds >= 0
assert run.metrics.verify_path_report_calls >= 0
def test_route_problem_locked_routes_become_static_obstacles() -> None: def test_route_problem_locked_routes_become_static_obstacles() -> None:

View file

@ -0,0 +1,45 @@
import json
import subprocess
import sys
from pathlib import Path
from inire.tests.example_scenarios import snapshot_example_01
def test_snapshot_example_01_exposes_metrics() -> None:
snapshot = snapshot_example_01()
assert snapshot.name == "example_01_simple_route"
assert snapshot.total_results == 1
assert snapshot.valid_results == 1
assert snapshot.reached_targets == 1
assert snapshot.metrics.route_iterations >= 1
assert snapshot.metrics.nets_routed >= 1
assert snapshot.metrics.nodes_expanded > 0
assert snapshot.metrics.move_cache_abs_misses >= 0
assert snapshot.metrics.ray_cast_calls >= 0
assert snapshot.metrics.dynamic_tree_rebuilds >= 0
assert snapshot.metrics.visibility_builds >= 0
def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None:
repo_root = Path(__file__).resolve().parents[2]
script_path = repo_root / "scripts" / "record_performance_baseline.py"
subprocess.run(
[
sys.executable,
str(script_path),
"--output-dir",
str(tmp_path),
"--scenario",
"example_01_simple_route",
],
check=True,
)
payload = json.loads((tmp_path / "performance_baseline.json").read_text())
assert payload["generated_on"]
assert payload["generator"] == "scripts/record_performance_baseline.py"
assert [entry["name"] for entry in payload["scenarios"]] == ["example_01_simple_route"]
assert (tmp_path / "performance.md").exists()

View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from dataclasses import asdict
from datetime import date
from pathlib import Path
from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS
SUMMARY_METRICS = (
"route_iterations",
"nets_routed",
"nodes_expanded",
"ray_cast_calls",
"moves_generated",
"moves_added",
"dynamic_tree_rebuilds",
"visibility_builds",
"congestion_check_calls",
"verify_path_report_calls",
)
def _build_payload(selected_scenarios: tuple[str, ...] | None = None) -> dict[str, object]:
allowed = None if selected_scenarios is None else set(selected_scenarios)
snapshots = []
for name, run in SCENARIO_SNAPSHOTS:
if allowed is not None and name not in allowed:
continue
snapshots.append(run())
return {
"generated_on": date.today().isoformat(),
"generator": "scripts/record_performance_baseline.py",
"scenarios": [asdict(snapshot) for snapshot in snapshots],
}
def _render_markdown(payload: dict[str, object]) -> str:
rows = payload["scenarios"]
lines = [
"# Performance Baseline",
"",
f"Generated on {payload['generated_on']} by `{payload['generator']}`.",
"",
"The full machine-readable snapshot lives in `docs/performance_baseline.json`.",
"",
"| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |",
"| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |",
]
for row in rows:
metrics = row["metrics"]
lines.append(
"| "
f"{row['name']} | "
f"{row['duration_s']:.4f} | "
f"{row['total_results']} | "
f"{row['valid_results']} | "
f"{row['reached_targets']} | "
f"{metrics['route_iterations']} | "
f"{metrics['nets_routed']} | "
f"{metrics['nodes_expanded']} | "
f"{metrics['ray_cast_calls']} | "
f"{metrics['moves_generated']} | "
f"{metrics['moves_added']} | "
f"{metrics['dynamic_tree_rebuilds']} | "
f"{metrics['visibility_builds']} | "
f"{metrics['congestion_check_calls']} | "
f"{metrics['verify_path_report_calls']} |"
)
lines.extend(
[
"",
"## Full Counter Set",
"",
"Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.",
"",
"Tracked metric keys:",
"",
", ".join(rows[0]["metrics"].keys()) if rows else "",
]
)
return "\n".join(lines) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description="Record the example-scenario performance baseline.")
parser.add_argument(
"--output-dir",
type=Path,
default=None,
help="Directory to write performance_baseline.json and performance.md into. Defaults to <repo>/docs.",
)
parser.add_argument(
"--scenario",
action="append",
dest="scenarios",
default=[],
help="Optional scenario name to include. May be passed more than once.",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1]
docs_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
docs_dir.mkdir(exist_ok=True)
selected = tuple(args.scenarios) if args.scenarios else None
payload = _build_payload(selected)
json_path = docs_dir / "performance_baseline.json"
markdown_path = docs_dir / "performance.md"
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
markdown_path.write_text(_render_markdown(payload))
if json_path.is_relative_to(repo_root):
print(f"Wrote {json_path.relative_to(repo_root)}")
else:
print(f"Wrote {json_path}")
if markdown_path.is_relative_to(repo_root):
print(f"Wrote {markdown_path.relative_to(repo_root)}")
else:
print(f"Wrote {markdown_path}")
if __name__ == "__main__":
main()