diff --git a/.gitignore b/.gitignore index 505a3b1..89d2522 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ wheels/ # Virtual environments .venv + +.hypothesis +*.png + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..75a7cba --- /dev/null +++ b/DOCS.md @@ -0,0 +1,363 @@ +# Inire Configuration & API Documentation + +This document describes the current public API for `inire`. + +## 1. Primary API + +### `RoutingProblem` + +`RoutingProblem` describes the physical routing problem: + +- `bounds` +- `nets` +- `static_obstacles` +- `initial_paths` +- `clearance` +- `safety_zone_radius` + +### `RoutingOptions` + +`RoutingOptions` groups all expert controls for the routing engine: + +- `search` +- `objective` +- `congestion` +- `refinement` +- `diagnostics` + +Route a problem with: + +```python +run = route(problem, options=options) +``` + +If you omit `options`, `route(problem)` uses `RoutingOptions()` defaults. + +The package root is the stable API surface. Deep imports under `inire.router.*` and `inire.geometry.*` remain accessible for advanced use, but they are unstable semi-private interfaces and may change without notice. + +Stable example: + +```python +from inire import route, RoutingOptions, RoutingProblem +``` + +Unstable example: + +```python +from inire.router._router import PathFinder +``` + +### Incremental routing with locked geometry + +For incremental workflows, route one problem, reuse the result's locked geometry, and feed it into the next problem: + +```python +run_a = route(problem_a) +problem_b = RoutingProblem( + bounds=problem_a.bounds, + nets=(...), + static_obstacles=run_a.results_by_net["netA"].locked_geometry, +) +run_b = route(problem_b) +``` + +`RoutingResult.locked_geometry` stores canonical physical geometry only. The next run applies its own clearance rules when treating it as a static obstacle. + +### Initial paths with `PathSeed` + +Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are materialized with the current width, clearance, and bend collision settings for the run, and partial seeds are retried by normal routing in later iterations. + +## 2. Search Options + +`RoutingOptions.search` is a `SearchOptions` object. + +| Field | Default | Description | +| :-- | :-- | :-- | +| `node_limit` | `1_000_000` | Maximum number of states to explore per net. | +| `max_straight_length` | `2000.0` | Maximum length of a single straight segment. | +| `min_straight_length` | `5.0` | Minimum length of a single straight segment. | +| `greedy_h_weight` | `1.5` | Heuristic weight. `1.0` is optimal but slower. | +| `bend_radii` | `(50.0, 100.0)` | Available radii for 90-degree bends. | +| `sbend_radii` | `(10.0,)` | Available radii for S-bends. | +| `sbend_offsets` | `None` | Optional explicit lateral offsets for S-bends. | +| `bend_collision_type` | `"arc"` | Bend collision/proxy model: `"arc"`, `"bbox"`, `"clipped_bbox"`, or, for backward compatibility, a custom polygon. A legacy custom polygon here is treated as both the physical bend and its proxy unless overridden by the split fields below. | +| `bend_proxy_geometry` | `None` | Optional explicit bend proxy geometry. Use this when you want a custom search/collision envelope that differs from the routed bend shape. Supplying only a custom polygon proxy warns and keeps the physical bend as the standard arc. | +| `bend_physical_geometry` | `None` | Optional explicit bend physical geometry. Use `"arc"` or a custom polygon. If you set a custom physical polygon and do not set a proxy, the proxy defaults to the same polygon. | +| `bend_clip_margin` | `None` | Optional legacy shrink margin for `"clipped_bbox"`. Leave `None` for the default 8-point proxy. | +| `visibility_guidance` | `"tangent_corner"` | Visibility-derived straight candidate strategy. | + +## 3. Objective Weights + +`RoutingOptions.objective` and `RoutingOptions.refinement.objective` use `ObjectiveWeights`. + +| Field | Default | Description | +| :-- | :-- | :-- | +| `unit_length_cost` | `1.0` | Cost per unit length. | +| `bend_penalty` | `250.0` | Flat bend penalty before radius scaling. | +| `sbend_penalty` | `500.0` | Flat S-bend penalty. | +| `danger_weight` | `1.0` | Weight applied to danger-map proximity costs. | + +## 4. Congestion Options + +`RoutingOptions.congestion` is a `CongestionOptions` object. + +| Field | Default | Description | +| :-- | :-- | :-- | +| `max_iterations` | `10` | Maximum rip-up and reroute iterations. | +| `base_penalty` | `100.0` | Starting overlap penalty for negotiated congestion. | +| `multiplier` | `1.5` | Multiplier applied after an iteration still needs retries. | +| `use_tiered_strategy` | `True` | Use cheaper collision proxies in the first pass when applicable. | +| `net_order` | `"user"` | Net ordering strategy for warm-start seeding and routed iterations. | +| `warm_start_enabled` | `True` | Run the greedy warm-start seeding pass before negotiated congestion iterations. | +| `shuffle_nets` | `False` | Shuffle routing order between iterations. | +| `seed` | `None` | RNG seed for shuffled routing order. | + +## 5. Refinement Options + +`RoutingOptions.refinement` is a `RefinementOptions` object. + +| Field | Default | Description | +| :-- | :-- | :-- | +| `enabled` | `True` | Enable post-route refinement. | +| `objective` | `None` | Optional override objective for refinement. `None` reuses the search objective. | + +## 6. Diagnostics Options + +`RoutingOptions.diagnostics` is a `DiagnosticsOptions` object. + +| Field | Default | Description | +| :-- | :-- | :-- | +| `capture_expanded` | `False` | Record expanded nodes for diagnostics and visualization. | +| `capture_conflict_trace` | `False` | Capture authoritative post-reverify conflict trace entries for debugging negotiated-congestion failures. | +| `capture_frontier_trace` | `False` | Run an analysis-only reroute for reached-but-colliding nets and capture prune causes near their final conflict hotspots. | +| `capture_iteration_trace` | `False` | Capture per-iteration and per-net route-attempt attribution for negotiated-congestion diagnosis. | +| `capture_pre_pair_frontier_trace` | `False` | Capture the final unresolved pre-pair-local subset iteration plus hotspot-adjacent frontier prunes for the routed nets in that basin. | + +## 7. Conflict Trace + +`RoutingRunResult.conflict_trace` is an immutable tuple of post-reverify conflict snapshots. It is empty unless `RoutingOptions.diagnostics.capture_conflict_trace=True`. + +Trace types: + +- `ConflictTraceEntry` + - `stage`: `"iteration"`, `"restored_best"`, or `"final"` + - `iteration`: Iteration index for `"iteration"` entries, otherwise `None` + - `completed_net_ids`: Nets with collision-free reached-target paths at that stage + - `conflict_edges`: Undirected dynamic-conflict net pairs seen after full reverify + - `nets`: Per-net trace payloads in routing-order order +- `NetConflictTrace` + - `net_id` + - `outcome` + - `reached_target` + - `report` + - `conflicting_net_ids`: Dynamic conflicting nets for that stage + - `component_conflicts`: Dynamic component-pair overlaps for that stage +- `ComponentConflictTrace` + - `other_net_id` + - `self_component_index` + - `other_component_index` + +The conflict trace only records dynamic net-vs-net component overlaps. Static-obstacle and self-collision details remain count-only in `RoutingReport`. + +Use `scripts/record_conflict_trace.py` to capture JSON and Markdown trace artifacts for the built-in trace scenarios. The default target is `example_07_large_scale_routing_no_warm_start`. + +## 8. Frontier Trace + +`RoutingRunResult.frontier_trace` is an immutable tuple of per-net post-run frontier analyses. It is empty unless `RoutingOptions.diagnostics.capture_frontier_trace=True`. + +Trace types: + +- `NetFrontierTrace` + - `net_id` + - `hotspot_bounds`: Buffered bounds around the net's final dynamic component-overlap hotspots + - `pruned_closed_set` + - `pruned_hard_collision` + - `pruned_self_collision` + - `pruned_cost` + - `samples`: First traced prune events near those hotspots +- `FrontierPruneSample` + - `reason`: `"closed_set"`, `"hard_collision"`, `"self_collision"`, or `"cost"` + - `move_type` + - `hotspot_index` + - `parent_state` + - `end_state` + +The frontier trace is observational only. It reruns only the final reached-but-colliding nets in analysis mode, with scratch metrics, after the routed result is already fixed. + +Use `scripts/record_frontier_trace.py` to capture JSON and Markdown frontier-prune artifacts for the built-in trace scenarios. The default target is `example_07_large_scale_routing_no_warm_start`. + +Separately from the observational trace tooling, the router may run a bounded post-loop pair-local scratch reroute before refinement when the restored best snapshot ends with final two-net reached-target dynamic conflicts. That repair phase is part of normal routing behavior and is reported through the `pair_local_search_*` counters below. + +## 9. Pre-Pair Frontier Trace + +`RoutingRunResult.pre_pair_frontier_trace` is either a single immutable trace entry or `None`. It is populated only when `RoutingOptions.diagnostics.capture_pre_pair_frontier_trace=True`. + +Trace types: + +- `PrePairFrontierTraceEntry` + - `iteration`: The final unresolved subset-reroute iteration immediately before pair-local handoff + - `routed_net_ids`: Nets rerouted in that iteration, in routing order + - `conflict_edges`: Dynamic conflict edges reported for that unresolved basin + - `nets`: Per-net attempt attribution plus hotspot-adjacent frontier rerun data +- `PrePairNetTrace` + - `net_id` + - `nodes_expanded` + - `congestion_check_calls` + - `pruned_closed_set` + - `pruned_cost` + - `pruned_hard_collision` + - `guidance_seed_present` + - `frontier`: A `NetFrontierTrace` captured against the restored best unresolved state + +Use `scripts/record_pre_pair_frontier_trace.py` to capture JSON and Markdown artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the heavier seed-43 no-warm canary. + +## 10. Iteration Trace + +`RoutingRunResult.iteration_trace` is an immutable tuple of negotiated-congestion iteration summaries. It is empty unless `RoutingOptions.diagnostics.capture_iteration_trace=True`. + +Trace types: + +- `IterationTraceEntry` + - `iteration` + - `congestion_penalty`: Penalty in effect for that iteration + - `routed_net_ids`: Nets rerouted during that iteration, in routing order + - `completed_nets` + - `conflict_edges` + - `total_dynamic_collisions` + - `nodes_expanded` + - `congestion_check_calls` + - `congestion_candidate_ids` + - `congestion_exact_pair_checks` + - `net_attempts`: Per-net attribution for that iteration +- `IterationNetAttemptTrace` + - `net_id` + - `reached_target` + - `nodes_expanded` + - `congestion_check_calls` + - `pruned_closed_set` + - `pruned_cost` + - `pruned_hard_collision` + - `guidance_seed_present` + +Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-attribution artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the pathological seed-43 no-warm canary. + +## 11. RouteMetrics + +`RoutingRunResult.metrics` is an immutable per-run snapshot. + +### Search Counters + +- `nodes_expanded`: Total nodes expanded during the run. +- `moves_generated`: Total candidate moves generated during the run. +- `moves_added`: Total candidate moves admitted to the open set. +- `pruned_closed_set`: Total moves pruned because the state was already closed at lower cost. +- `pruned_hard_collision`: Total moves pruned by hard collision checks. +- `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. +- `iteration_reverify_calls`: Number of end-of-iteration full reverify passes against the final installed dynamic geometry. +- `iteration_reverified_nets`: Number of reached-target nets reverified at iteration boundaries. +- `iteration_conflicting_nets`: Total unique nets found in end-of-iteration dynamic conflicts. +- `iteration_conflict_edges`: Total undirected dynamic-conflict edges observed at iteration boundaries. +- `nets_carried_forward`: Number of nets retained unchanged between iterations. + +### 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. +- `guidance_match_moves`: Number of moves that matched the reroute guidance seed and received the guidance bonus. +- `guidance_match_moves_straight`, `guidance_match_moves_bend90`, `guidance_match_moves_sbend`: Guidance-match counts split by move type. +- `guidance_bonus_applied`: Total reroute-guidance bonus subtracted from move costs across the run. +- `guidance_bonus_applied_straight`, `guidance_bonus_applied_bend90`, `guidance_bonus_applied_sbend`: Guidance bonus totals split by move type. +- `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_corner_index_builds`: Number of lazy corner-index rebuilds. +- `visibility_builds`: Number of exact corner-visibility graph rebuilds. +- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building the exact graph. +- `visibility_corner_queries_exact` / `visibility_corner_hits_exact`: Exact-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_presence_cache_hits` / `congestion_presence_cache_misses`: Reuse of cached per-span booleans indicating whether a move polygon could overlap any other routed net at all. +- `congestion_presence_skips`: Number of moves that bypassed full congestion evaluation because the presence precheck found no other routed nets in any covered dynamic-grid span. +- `congestion_candidate_precheck_hits` / `congestion_candidate_precheck_misses`: Reuse of cached conservative per-span booleans indicating whether any candidate nets survive the net-envelope and grid-net broad phases. +- `congestion_candidate_precheck_skips`: Number of moves that bypassed full congestion evaluation because the candidate-net precheck found no surviving candidate nets after those broad phases. +- `congestion_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during routing. +- `congestion_net_envelope_cache_hits` / `congestion_net_envelope_cache_misses`: Reuse of cached dynamic net-envelope candidate sets keyed by the queried grid-cell span. +- `congestion_grid_net_cache_hits` / `congestion_grid_net_cache_misses`: Reuse of cached per-span candidate net ids gathered from dynamic grid occupancy. +- `congestion_grid_span_cache_hits` / `congestion_grid_span_cache_misses`: Reuse of cached dynamic-path candidate unions keyed by the queried grid-cell span. +- `congestion_lazy_resolutions`: Number of popped nodes whose pending congestion was resolved lazily. +- `congestion_lazy_requeues`: Number of lazily resolved nodes requeued after a positive congestion penalty was applied. +- `congestion_candidate_ids`: Total dynamic-path object ids returned by the congestion broad phase before exact confirmation. +- `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_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during final verification. +- `verify_dynamic_exact_pair_checks`: Number of exact geometry-pair checks performed during dynamic-path verification. + +### Local Search Counters + +- `pair_local_search_pairs_considered`: Number of final reached-target conflict pairs considered by the bounded post-loop pair-local-search phase. +- `pair_local_search_attempts`: Number of pair-local-search reroute attempts executed across all considered pairs. +- `pair_local_search_accepts`: Number of pair-local-search attempts accepted into the whole routed result set. +- `pair_local_search_nodes_expanded`: Total A* node expansions spent inside pair-local-search attempts. +- `late_phase_capped_nets`: Number of late all-reached heavy-net reroutes run under the bounded node-limit cap before pair-local handoff. +- `late_phase_capped_fallbacks`: Number of those capped late-phase reroutes that fell back to the incumbent reached-target path instead of replacing it. + +## 10. 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=...)`. + +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)**. +Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, `scripts/record_pre_pair_frontier_trace.py` for the final unresolved pre-pair basin, `scripts/record_iteration_trace.py` for per-iteration negotiated-congestion attribution, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate. + +## 11. Tuning Notes + +### Speed vs. optimality + +- Lower `search.greedy_h_weight` toward `1.0` for better optimality. +- Raise `search.greedy_h_weight` for faster, greedier routing. + +### Congestion handling + +- Increase `congestion.base_penalty` to separate nets more aggressively in the first iteration. +- Increase `congestion.max_iterations` if congestion needs more reroute passes. +- Increase `congestion.multiplier` if later iterations need to escalate more quickly. + +### Bend-heavy routes + +- Increase `objective.bend_penalty` to discourage ladders of small bends. +- Increase available `search.bend_radii` when larger turns are physically acceptable. +- Use `search.bend_physical_geometry` and `search.bend_proxy_geometry` together when you need a real custom bend shape plus a different conservative proxy. + +### Visibility guidance + +- `"tangent_corner"` is the default and best general-purpose setting in obstacle-dense layouts. +- `"exact_corner"` is more conservative. +- `"off"` disables visibility-derived straight candidates. + +### S-bends + +- Leave `search.sbend_offsets=None` to let the router derive natural offsets automatically. +- Provide explicit `search.sbend_offsets` for known process-preferred offsets. +- S-bends are only used for offsets smaller than `2R`. diff --git a/README.md b/README.md index e69de29..96cfbc7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,107 @@ +# inire: Auto-Routing for Photonic and RF Integrated Circuits + +`inire` is a high-performance auto-router designed specifically for the physical constraints of photonic and RF integrated circuits. It uses a Hybrid State-Lattice A* search combined with negotiated congestion to route multiple nets while maintaining strict geometric fidelity and clearance. + +## Key Features + +* **Hybrid State-Lattice Search**: Routes using discrete 90° bends and parametric S-bends, ensuring manufacturing-stable paths. +* **Negotiated Congestion**: Iteratively resolves multi-net bottlenecks by inflating costs in high-traffic regions. +* **Analytic Correctness**: Every move is verified against an R-Tree spatial index of obstacles and other paths. +* **1nm Precision**: All coordinates and ports are snapped to a 1nm manufacturing grid. +* **Safety & Proximity**: 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. + +## Installation + +`inire` requires Python 3.11+. You can install the dependencies using `uv` (recommended) or `pip`: + +```bash +# Using uv +uv sync + +# Using pip +pip install numpy scipy shapely rtree matplotlib +``` + +## Quick Start + +```python +from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route + +problem = RoutingProblem( + bounds=(0, 0, 1000, 1000), + nets=( + NetSpec("net1", Port(0, 0, 0), Port(100, 50, 0), width=2.0), + ), +) +options = RoutingOptions( + search=SearchOptions( + bend_radii=(50.0, 100.0), + greedy_h_weight=1.2, + ), + objective=ObjectiveWeights( + bend_penalty=10.0, + ), +) + +run = route(problem, options=options) + +if run.results_by_net["net1"].is_valid: + print("Successfully routed net1!") +``` + +For incremental workflows, feed prior routed results back into a new `RoutingProblem` via `static_obstacles` using `RoutingResult.locked_geometry`. + +## Usage Examples + +For detailed visual demonstrations and architectural deep-dives, see the **[Examples README](examples/README.md)**. + +Check the `examples/` directory for ready-to-run scripts. To run an example: +```bash +python3 examples/01_simple_route.py +``` + +## Testing + +Run the default correctness suite with: + +```bash +python3 -m pytest +``` + +Runtime regression checks for the example scenarios are opt-in and require: + +```bash +INIRE_RUN_PERFORMANCE=1 python3 -m pytest -q inire/tests/test_example_performance.py +``` + +## Documentation + +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 + +The stable API lives at the package root and is centered on `route(problem, options=...)`. + +Deep-module interfaces such as `inire.router._router.PathFinder`, `inire.router._search.route_astar`, and `inire.geometry.collision.RoutingWorld` remain accessible for advanced use, but they are unstable semi-private interfaces and may change without notice. + +## Architecture + +`inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types: +1. **Straights**: Variable-length segments. +2. **90° Bends**: Fixed-radius PDK cells. +3. **Parametric S-Bends**: Procedural arcs for bridging small lateral offsets ($O < 2R$). + +For multi-net problems, the negotiated-congestion loop handles rip-up and reroute logic and seeks a collision-free configuration without crossings. + +## Configuration + +`inire` is highly tunable. The stable API is `RoutingProblem` plus `RoutingOptions`, routed via `route(problem, options=...)`. Deep modules remain accessible for advanced workflows, but they are unstable and may change without notice. See `DOCS.md` for a full parameter reference. + +## License + +This project is licensed under the GNU Affero General Public License v3. See `LICENSE.md` for details. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..5e147d1 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,50 @@ +# 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, including dynamic per-object indices, per-net grid occupancy, congestion grid membership, and per-net dynamic envelopes. +- `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. +- The visibility subsystem keeps a lazy static corner index for default `tangent_corner` guidance and only builds the exact corner-to-corner graph on demand for `exact_corner` queries. +- `use_tiered_strategy` can swap in a cheaper bend proxy on the first congestion iteration. +- Negotiated congestion now re-verifies every reached-target path at the end of each iteration against the final installed dynamic geometry, and it stops early if the conflict graph stalls for consecutive iterations. +- After best-snapshot restoration, the router runs a bounded pair-local scratch reroute on final two-net reached-target conflict pairs. That repair phase clones static obstacles from the live collision world, treats all outside-pair geometry as fixed blockers, tries both pair orders, and only keeps the result if whole-set reverify improves. +- 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`. diff --git a/docs/conflict_trace.json b/docs/conflict_trace.json new file mode 100644 index 0000000..90fc817 --- /dev/null +++ b/docs/conflict_trace.json @@ -0,0 +1,2533 @@ +{ + "generated_at": "2026-04-02T14:24:39-07:00", + "generator": "scripts/record_conflict_trace.py", + "scenarios": [ + { + "conflict_trace": [ + { + "completed_net_ids": [ + "net_04" + ], + "conflict_edges": [ + [ + "net_00", + "net_01" + ], + [ + "net_00", + "net_02" + ], + [ + "net_00", + "net_03" + ], + [ + "net_00", + "net_04" + ], + [ + "net_01", + "net_02" + ], + [ + "net_01", + "net_03" + ], + [ + "net_02", + "net_03" + ], + [ + "net_03", + "net_04" + ], + [ + "net_05", + "net_06" + ], + [ + "net_05", + "net_09" + ], + [ + "net_06", + "net_07" + ], + [ + "net_06", + "net_08" + ], + [ + "net_06", + "net_09" + ], + [ + "net_07", + "net_08" + ], + [ + "net_07", + "net_09" + ], + [ + "net_08", + "net_09" + ] + ], + "iteration": 0, + "nets": [ + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 0, + "other_net_id": "net_09", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_07", + "net_09" + ], + "net_id": "net_08", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_05", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 1 + }, + { + "other_component_index": 0, + "other_net_id": "net_09", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_05", + "net_07", + "net_08", + "net_09" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 9, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_04", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_01", + "net_02", + "net_04" + ], + "net_id": "net_03", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 8, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_03", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_04", + "self_component_index": 1 + } + ], + "conflicting_net_ids": [ + "net_01", + "net_02", + "net_03", + "net_04" + ], + "net_id": "net_00", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 4, + "self_collision_count": 1, + "static_collision_count": 0, + "total_length": 1621.236812357892 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 1 + }, + { + "other_component_index": 0, + "other_net_id": "net_09", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_08", + "net_09" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 7, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_03", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_02", + "net_03" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 5, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 0 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_07", + "net_08" + ], + "net_id": "net_09", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 1, + "static_collision_count": 0, + "total_length": 1631.236812357892 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_03", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_01", + "net_03" + ], + "net_id": "net_02", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 7, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_09", + "self_component_index": 1 + } + ], + "conflicting_net_ids": [ + "net_09" + ], + "net_id": "net_05", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 1, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_04", + "net_05" + ], + "conflict_edges": [ + [ + "net_00", + "net_01" + ], + [ + "net_00", + "net_02" + ], + [ + "net_00", + "net_03" + ], + [ + "net_01", + "net_02" + ], + [ + "net_01", + "net_03" + ], + [ + "net_02", + "net_03" + ], + [ + "net_06", + "net_07" + ], + [ + "net_06", + "net_08" + ], + [ + "net_06", + "net_09" + ], + [ + "net_07", + "net_08" + ], + [ + "net_07", + "net_09" + ], + [ + "net_08", + "net_09" + ] + ], + "iteration": 1, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 0, + "other_net_id": "net_08", + "self_component_index": 2 + }, + { + "other_component_index": 0, + "other_net_id": "net_09", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 5 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 5 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 5 + } + ], + "conflicting_net_ids": [ + "net_07", + "net_08", + "net_09" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 5, + "other_net_id": "net_03", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_02", + "net_03" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 7, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 1 + }, + { + "other_component_index": 5, + "other_net_id": "net_06", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_07", + "net_08" + ], + "net_id": "net_09", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 5, + "other_net_id": "net_03", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_01", + "net_03" + ], + "net_id": "net_02", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 8, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_01", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 5, + "other_net_id": "net_03", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_01", + "net_02", + "net_03" + ], + "net_id": "net_00", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 0, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 0, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 5 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 5 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 5 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_01", + "net_02" + ], + "net_id": "net_03", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_09", + "self_component_index": 1 + }, + { + "other_component_index": 5, + "other_net_id": "net_06", + "self_component_index": 2 + }, + { + "other_component_index": 1, + "other_net_id": "net_08", + "self_component_index": 2 + }, + { + "other_component_index": 1, + "other_net_id": "net_09", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_08", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_08", + "net_09" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 8, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 0 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_09", + "self_component_index": 1 + }, + { + "other_component_index": 5, + "other_net_id": "net_06", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 1, + "other_net_id": "net_09", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_09", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_06", + "net_07", + "net_09" + ], + "net_id": "net_08", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 7, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_04", + "net_05", + "net_08", + "net_09" + ], + "conflict_edges": [ + [ + "net_00", + "net_01" + ], + [ + "net_00", + "net_02" + ], + [ + "net_00", + "net_03" + ], + [ + "net_01", + "net_02" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 2, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 3 + }, + { + "other_component_index": 3, + "other_net_id": "net_01", + "self_component_index": 4 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 5 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_01" + ], + "net_id": "net_02", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 5, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 0, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 3, + "other_net_id": "net_02", + "self_component_index": 3 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 5 + } + ], + "conflicting_net_ids": [ + "net_00", + "net_02" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 4, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 1, + "other_net_id": "net_00", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_00" + ], + "net_id": "net_03", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_07" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 0 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 0 + }, + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 1 + }, + { + "other_component_index": 5, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 5, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 3, + "other_net_id": "net_03", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_01", + "net_02", + "net_03" + ], + "net_id": "net_00", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 6, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_02", + "net_03", + "net_04", + "net_05", + "net_08", + "net_09" + ], + "conflict_edges": [ + [ + "net_00", + "net_01" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 3, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_02", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + }, + { + "other_component_index": 3, + "other_net_id": "net_00", + "self_component_index": 4 + } + ], + "conflicting_net_ids": [ + "net_00" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 3, + "other_net_id": "net_01", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_01" + ], + "net_id": "net_00", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_07" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_03", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_00", + "net_03", + "net_04", + "net_05", + "net_08", + "net_09" + ], + "conflict_edges": [ + [ + "net_01", + "net_02" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 4, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_00", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_07" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_03", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 3, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 3, + "other_net_id": "net_01", + "self_component_index": 3 + }, + { + "other_component_index": 4, + "other_net_id": "net_01", + "self_component_index": 4 + } + ], + "conflicting_net_ids": [ + "net_01" + ], + "net_id": "net_02", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 3 + }, + { + "other_component_index": 3, + "other_net_id": "net_02", + "self_component_index": 4 + } + ], + "conflicting_net_ids": [ + "net_02" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_00", + "net_01", + "net_04", + "net_05", + "net_08", + "net_09" + ], + "conflict_edges": [ + [ + "net_02", + "net_03" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 5, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_00", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_01", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_03", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_03", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_03" + ], + "net_id": "net_02", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_07" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_02", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_02", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_02" + ], + "net_id": "net_03", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + } + ], + "stage": "iteration" + }, + { + "completed_net_ids": [ + "net_02", + "net_03", + "net_04", + "net_05", + "net_08", + "net_09" + ], + "conflict_edges": [ + [ + "net_00", + "net_01" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": null, + "nets": [ + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_01", + "self_component_index": 2 + }, + { + "other_component_index": 3, + "other_net_id": "net_01", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_01" + ], + "net_id": "net_00", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_00", + "self_component_index": 3 + }, + { + "other_component_index": 3, + "other_net_id": "net_00", + "self_component_index": 4 + } + ], + "conflicting_net_ids": [ + "net_00" + ], + "net_id": "net_01", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_02", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_06", + "self_component_index": 1 + }, + { + "other_component_index": 2, + "other_net_id": "net_06", + "self_component_index": 2 + } + ], + "conflicting_net_ids": [ + "net_06" + ], + "net_id": "net_07", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 2, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [ + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 1 + }, + { + "other_component_index": 1, + "other_net_id": "net_07", + "self_component_index": 2 + }, + { + "other_component_index": 2, + "other_net_id": "net_07", + "self_component_index": 3 + } + ], + "conflicting_net_ids": [ + "net_07" + ], + "net_id": "net_06", + "outcome": "colliding", + "reached_target": true, + "report": { + "dynamic_collision_count": 3, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_03", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + } + ], + "stage": "restored_best" + }, + { + "completed_net_ids": [ + "net_00", + "net_01", + "net_02", + "net_03", + "net_04", + "net_05", + "net_06", + "net_07", + "net_08", + "net_09" + ], + "conflict_edges": [], + "iteration": null, + "nets": [ + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_00", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_01", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_04", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_09", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_02", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_05", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_08", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_07", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_06", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + { + "component_conflicts": [], + "conflicting_net_ids": [], + "net_id": "net_03", + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + } + ], + "stage": "final" + } + ], + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4625, + "congestion_candidate_ids": 9924, + "congestion_candidate_nets": 9979, + "congestion_candidate_precheck_hits": 2562, + "congestion_candidate_precheck_misses": 2165, + "congestion_candidate_precheck_skips": 71, + "congestion_check_calls": 4625, + "congestion_exact_pair_checks": 8122, + "congestion_grid_net_cache_hits": 2457, + "congestion_grid_net_cache_misses": 3942, + "congestion_grid_span_cache_hits": 2283, + "congestion_grid_span_cache_misses": 1948, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2673, + "congestion_net_envelope_cache_misses": 4139, + "congestion_presence_cache_hits": 2858, + "congestion_presence_cache_misses": 2556, + "congestion_presence_skips": 687, + "danger_map_cache_hits": 16878, + "danger_map_cache_misses": 7425, + "danger_map_lookup_calls": 24303, + "danger_map_query_calls": 7425, + "danger_map_total_ns": 222763960, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 471, + "dynamic_path_objects_removed": 423, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 11000.0, + "guidance_bonus_applied_bend90": 3500.0, + "guidance_bonus_applied_sbend": 625.0, + "guidance_bonus_applied_straight": 6875.0, + "guidance_match_moves": 176, + "guidance_match_moves_bend90": 56, + "guidance_match_moves_sbend": 10, + "guidance_match_moves_straight": 110, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 36, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 2559, + "move_cache_abs_misses": 6494, + "move_cache_rel_hits": 5872, + "move_cache_rel_misses": 622, + "moves_added": 8081, + "moves_generated": 9053, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 1764, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 439, + "pruned_cost": 533, + "pruned_hard_collision": 0, + "ray_cast_calls": 5477, + "ray_cast_calls_expand_forward": 1704, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3721, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 305, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 8634, + "score_component_total_ns": 252458856, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2482, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2106, + "verify_dynamic_exact_pair_checks": 558, + "verify_path_report_calls": 190, + "verify_static_buffer_ops": 895, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1704, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "summary": { + "reached_targets": 10, + "results_by_net": { + "net_00": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1207.0796326794896 + } + }, + "net_01": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1128.0796326794896 + } + }, + "net_02": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1049.0796326794896 + } + }, + "net_03": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 970.0796326794897 + } + }, + "net_04": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 909.9977565924808 + } + }, + "net_05": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 914.6410526793834 + } + }, + "net_06": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 980.0796326794897 + } + }, + "net_07": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1059.0796326794896 + } + }, + "net_08": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1138.0796326794896 + } + }, + "net_09": { + "outcome": "completed", + "reached_target": true, + "report": { + "dynamic_collision_count": 0, + "self_collision_count": 0, + "static_collision_count": 0, + "total_length": 1217.0796326794896 + } + } + }, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/conflict_trace.md b/docs/conflict_trace.md new file mode 100644 index 0000000..75d3c8b --- /dev/null +++ b/docs/conflict_trace.md @@ -0,0 +1,57 @@ +# Conflict Trace + +Generated at 2026-04-02T14:24:39-07:00 by `scripts/record_conflict_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +| Stage | Iteration | Conflicting Nets | Conflict Edges | Completed Nets | +| :-- | --: | --: | --: | --: | +| iteration | 0 | 9 | 16 | 1 | +| iteration | 1 | 8 | 12 | 2 | +| iteration | 2 | 6 | 5 | 4 | +| iteration | 3 | 4 | 2 | 6 | +| iteration | 4 | 4 | 2 | 6 | +| iteration | 5 | 4 | 2 | 6 | +| restored_best | | 4 | 2 | 6 | +| final | | 0 | 0 | 10 | + +Top nets by traced dynamic-collision stages: + +- `net_06`: 7 +- `net_07`: 7 +- `net_01`: 6 +- `net_00`: 5 +- `net_02`: 5 +- `net_03`: 4 +- `net_08`: 2 +- `net_09`: 2 +- `net_05`: 1 + +Top net pairs by frequency: + +- `net_06` <-> `net_07`: 7 +- `net_00` <-> `net_01`: 5 +- `net_01` <-> `net_02`: 4 +- `net_00` <-> `net_02`: 3 +- `net_00` <-> `net_03`: 3 +- `net_02` <-> `net_03`: 3 +- `net_01` <-> `net_03`: 2 +- `net_06` <-> `net_08`: 2 +- `net_06` <-> `net_09`: 2 +- `net_07` <-> `net_08`: 2 + +Top component pairs by frequency: + +- `net_06[2]` <-> `net_07[2]`: 6 +- `net_06[3]` <-> `net_07[2]`: 6 +- `net_06[1]` <-> `net_07[1]`: 6 +- `net_06[2]` <-> `net_07[1]`: 5 +- `net_00[2]` <-> `net_01[3]`: 4 +- `net_01[2]` <-> `net_02[2]`: 3 +- `net_01[2]` <-> `net_02[3]`: 3 +- `net_00[2]` <-> `net_01[2]`: 3 +- `net_07[3]` <-> `net_08[2]`: 2 +- `net_02[1]` <-> `net_03[1]`: 2 + diff --git a/docs/frontier_trace.json b/docs/frontier_trace.json new file mode 100644 index 0000000..130350c --- /dev/null +++ b/docs/frontier_trace.json @@ -0,0 +1,120 @@ +{ + "generated_at": "2026-04-02T14:24:39-07:00", + "generator": "scripts/record_frontier_trace.py", + "scenarios": [ + { + "frontier_trace": [], + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4625, + "congestion_candidate_ids": 9924, + "congestion_candidate_nets": 9979, + "congestion_candidate_precheck_hits": 2562, + "congestion_candidate_precheck_misses": 2165, + "congestion_candidate_precheck_skips": 71, + "congestion_check_calls": 4625, + "congestion_exact_pair_checks": 8122, + "congestion_grid_net_cache_hits": 2457, + "congestion_grid_net_cache_misses": 3942, + "congestion_grid_span_cache_hits": 2283, + "congestion_grid_span_cache_misses": 1948, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2673, + "congestion_net_envelope_cache_misses": 4139, + "congestion_presence_cache_hits": 2858, + "congestion_presence_cache_misses": 2556, + "congestion_presence_skips": 687, + "danger_map_cache_hits": 16878, + "danger_map_cache_misses": 7425, + "danger_map_lookup_calls": 24303, + "danger_map_query_calls": 7425, + "danger_map_total_ns": 212814061, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 471, + "dynamic_path_objects_removed": 423, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 11000.0, + "guidance_bonus_applied_bend90": 3500.0, + "guidance_bonus_applied_sbend": 625.0, + "guidance_bonus_applied_straight": 6875.0, + "guidance_match_moves": 176, + "guidance_match_moves_bend90": 56, + "guidance_match_moves_sbend": 10, + "guidance_match_moves_straight": 110, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 36, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 2559, + "move_cache_abs_misses": 6494, + "move_cache_rel_hits": 5872, + "move_cache_rel_misses": 622, + "moves_added": 8081, + "moves_generated": 9053, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 1764, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 439, + "pruned_cost": 533, + "pruned_hard_collision": 0, + "ray_cast_calls": 5477, + "ray_cast_calls_expand_forward": 1704, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3721, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 305, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 8634, + "score_component_total_ns": 241025335, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2482, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2106, + "verify_dynamic_exact_pair_checks": 558, + "verify_path_report_calls": 190, + "verify_static_buffer_ops": 895, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1704, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/frontier_trace.md b/docs/frontier_trace.md new file mode 100644 index 0000000..4351f37 --- /dev/null +++ b/docs/frontier_trace.md @@ -0,0 +1,23 @@ +# Frontier Trace + +Generated at 2026-04-02T14:24:39-07:00 by `scripts/record_frontier_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +| Net | Hotspots | Closed-Set | Hard Collision | Self Collision | Cost | Samples | +| :-- | --: | --: | --: | --: | --: | --: | + +Prune totals by reason: + +- None + +Top traced hotspots by sample count: + +- None + +Per-net sampled reason/move breakdown: + +- None + diff --git a/docs/iteration_trace.json b/docs/iteration_trace.json new file mode 100644 index 0000000..2bebaa6 --- /dev/null +++ b/docs/iteration_trace.json @@ -0,0 +1,1395 @@ +{ + "generated_at": "2026-04-02T18:51:01-07:00", + "generator": "scripts/record_iteration_trace.py", + "scenarios": [ + { + "iteration_trace": [ + { + "completed_nets": 1, + "conflict_edges": 16, + "congestion_candidate_ids": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_penalty": 100.0, + "iteration": 0, + "net_attempts": [ + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_08", + "nodes_expanded": 106, + "pruned_closed_set": 58, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_06", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_03", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_00", + "nodes_expanded": 158, + "pruned_closed_set": 72, + "pruned_cost": 74, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_07", + "nodes_expanded": 14, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_01", + "nodes_expanded": 38, + "pruned_closed_set": 14, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_09", + "nodes_expanded": 209, + "pruned_closed_set": 93, + "pruned_cost": 99, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_02", + "nodes_expanded": 14, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_04", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_05", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 571, + "routed_net_ids": [ + "net_08", + "net_06", + "net_03", + "net_00", + "net_07", + "net_01", + "net_09", + "net_02", + "net_04", + "net_05" + ], + "total_dynamic_collisions": 50 + }, + { + "completed_nets": 2, + "conflict_edges": 12, + "congestion_candidate_ids": 2378, + "congestion_check_calls": 974, + "congestion_exact_pair_checks": 1998, + "congestion_penalty": 140.0, + "iteration": 1, + "net_attempts": [ + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 317, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 81, + "pruned_closed_set": 14, + "pruned_cost": 22, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 45, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 8, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 36, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 8, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 33, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 372, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 117, + "pruned_closed_set": 19, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 54, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 253, + "routed_net_ids": [ + "net_04", + "net_06", + "net_01", + "net_09", + "net_05", + "net_02", + "net_00", + "net_03", + "net_07", + "net_08" + ], + "total_dynamic_collisions": 54 + }, + { + "completed_nets": 4, + "conflict_edges": 5, + "congestion_candidate_ids": 1928, + "congestion_check_calls": 993, + "congestion_exact_pair_checks": 1571, + "congestion_penalty": 196.0, + "iteration": 2, + "net_attempts": [ + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 399, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 120, + "pruned_closed_set": 31, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 275, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 63, + "pruned_closed_set": 15, + "pruned_cost": 3, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 68, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 16, + "pruned_closed_set": 6, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 59, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 16, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 6, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 38, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 29, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 54, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 12, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 253, + "routed_net_ids": [ + "net_05", + "net_02", + "net_04", + "net_01", + "net_08", + "net_09", + "net_03", + "net_06", + "net_07", + "net_00" + ], + "total_dynamic_collisions": 22 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 852, + "congestion_check_calls": 437, + "congestion_exact_pair_checks": 698, + "congestion_penalty": 274.4, + "iteration": 3, + "net_attempts": [ + { + "congestion_check_calls": 79, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 15, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 68, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 20, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 34, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 73, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 14, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 35, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 22, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 6, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 56, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 13, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 100, + "routed_net_ids": [ + "net_02", + "net_01", + "net_09", + "net_00", + "net_07", + "net_05", + "net_04", + "net_06", + "net_03", + "net_08" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 627, + "congestion_check_calls": 332, + "congestion_exact_pair_checks": 513, + "congestion_penalty": 384.15999999999997, + "iteration": 4, + "net_attempts": [ + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 179, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 46, + "pruned_closed_set": 7, + "pruned_cost": 15, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 43, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 80, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 18, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 81, + "routed_net_ids": [ + "net_07", + "net_06", + "net_00", + "net_01" + ], + "total_dynamic_collisions": 10 + } + ], + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 2736, + "congestion_candidate_ids": 5785, + "congestion_candidate_nets": 6163, + "congestion_candidate_precheck_hits": 1383, + "congestion_candidate_precheck_misses": 1418, + "congestion_candidate_precheck_skips": 34, + "congestion_check_calls": 2736, + "congestion_exact_pair_checks": 4780, + "congestion_grid_net_cache_hits": 1356, + "congestion_grid_net_cache_misses": 2608, + "congestion_grid_span_cache_hits": 1247, + "congestion_grid_span_cache_misses": 1308, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1452, + "congestion_net_envelope_cache_misses": 2720, + "congestion_presence_cache_hits": 1541, + "congestion_presence_cache_misses": 1642, + "congestion_presence_skips": 382, + "danger_map_cache_hits": 11547, + "danger_map_cache_misses": 6063, + "danger_map_lookup_calls": 17610, + "danger_map_query_calls": 6063, + "danger_map_total_ns": 171226180, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 399, + "dynamic_path_objects_removed": 351, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 6750.0, + "guidance_bonus_applied_bend90": 2250.0, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 4125.0, + "guidance_match_moves": 108, + "guidance_match_moves_bend90": 36, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 66, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 37, + "iteration_conflicting_nets": 32, + "iteration_reverified_nets": 50, + "iteration_reverify_calls": 5, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, + "move_cache_abs_hits": 1200, + "move_cache_abs_misses": 5338, + "move_cache_rel_hits": 4768, + "move_cache_rel_misses": 570, + "moves_added": 5853, + "moves_generated": 6538, + "nets_carried_forward": 6, + "nets_reached_target": 44, + "nets_routed": 44, + "nodes_expanded": 1258, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 374, + "pruned_cost": 311, + "pruned_hard_collision": 0, + "ray_cast_calls": 4310, + "ray_cast_calls_expand_forward": 1214, + "ray_cast_calls_expand_snap": 39, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3051, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 159, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 5, + "score_component_calls": 6181, + "score_component_total_ns": 191650546, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1170, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1822, + "verify_dynamic_exact_pair_checks": 504, + "verify_path_report_calls": 164, + "verify_static_buffer_ops": 779, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1214, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "iteration_trace": [ + { + "completed_nets": 1, + "conflict_edges": 16, + "congestion_candidate_ids": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_penalty": 100.0, + "iteration": 0, + "net_attempts": [ + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_00", + "nodes_expanded": 158, + "pruned_closed_set": 72, + "pruned_cost": 74, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_05", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_07", + "nodes_expanded": 14, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_01", + "nodes_expanded": 38, + "pruned_closed_set": 14, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_09", + "nodes_expanded": 209, + "pruned_closed_set": 93, + "pruned_cost": 99, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_08", + "nodes_expanded": 106, + "pruned_closed_set": 58, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_06", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_03", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_02", + "nodes_expanded": 14, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_04", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 571, + "routed_net_ids": [ + "net_00", + "net_05", + "net_07", + "net_01", + "net_09", + "net_08", + "net_06", + "net_03", + "net_02", + "net_04" + ], + "total_dynamic_collisions": 50 + }, + { + "completed_nets": 1, + "conflict_edges": 13, + "congestion_candidate_ids": 2562, + "congestion_check_calls": 961, + "congestion_exact_pair_checks": 2032, + "congestion_penalty": 140.0, + "iteration": 1, + "net_attempts": [ + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 32, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 259, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 86, + "pruned_closed_set": 19, + "pruned_cost": 44, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 43, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 7, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 372, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 117, + "pruned_closed_set": 19, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 27, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 5, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 84, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 20, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 47, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 10, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 269, + "routed_net_ids": [ + "net_09", + "net_08", + "net_00", + "net_07", + "net_04", + "net_01", + "net_03", + "net_05", + "net_02", + "net_06" + ], + "total_dynamic_collisions": 53 + }, + { + "completed_nets": 4, + "conflict_edges": 3, + "congestion_candidate_ids": 1610, + "congestion_check_calls": 643, + "congestion_exact_pair_checks": 1224, + "congestion_penalty": 196.0, + "iteration": 2, + "net_attempts": [ + { + "congestion_check_calls": 121, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 26, + "pruned_closed_set": 9, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 50, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 92, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 20, + "pruned_closed_set": 7, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 121, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 29, + "pruned_closed_set": 0, + "pruned_cost": 5, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 58, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 13, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 72, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 15, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 50, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 13, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 39, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 140, + "routed_net_ids": [ + "net_08", + "net_07", + "net_01", + "net_06", + "net_02", + "net_09", + "net_00", + "net_05", + "net_03", + "net_04" + ], + "total_dynamic_collisions": 15 + }, + { + "completed_nets": 4, + "conflict_edges": 3, + "congestion_candidate_ids": 557, + "congestion_check_calls": 250, + "congestion_exact_pair_checks": 428, + "congestion_penalty": 274.4, + "iteration": 3, + "net_attempts": [ + { + "congestion_check_calls": 36, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 49, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 70, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 13, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 35, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 54, + "routed_net_ids": [ + "net_03", + "net_07", + "net_02", + "net_09", + "net_08", + "net_06" + ], + "total_dynamic_collisions": 15 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 1025, + "congestion_check_calls": 505, + "congestion_exact_pair_checks": 829, + "congestion_penalty": 384.15999999999997, + "iteration": 4, + "net_attempts": [ + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 32, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 8, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 179, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 46, + "pruned_closed_set": 7, + "pruned_cost": 15, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 192, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 54, + "pruned_closed_set": 7, + "pruned_cost": 21, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 12, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 136, + "routed_net_ids": [ + "net_07", + "net_02", + "net_06", + "net_03", + "net_09", + "net_08" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 419, + "congestion_check_calls": 171, + "congestion_exact_pair_checks": 287, + "congestion_penalty": 537.824, + "iteration": 5, + "net_attempts": [ + { + "congestion_check_calls": 85, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 16, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 86, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 17, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 33, + "routed_net_ids": [ + "net_07", + "net_02", + "net_06", + "net_03" + ], + "total_dynamic_collisions": 10 + } + ], + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 2530, + "congestion_candidate_ids": 6173, + "congestion_candidate_nets": 5869, + "congestion_candidate_precheck_hits": 1152, + "congestion_candidate_precheck_misses": 1460, + "congestion_candidate_precheck_skips": 74, + "congestion_check_calls": 2530, + "congestion_exact_pair_checks": 4800, + "congestion_grid_net_cache_hits": 1192, + "congestion_grid_net_cache_misses": 2676, + "congestion_grid_span_cache_hits": 1065, + "congestion_grid_span_cache_misses": 1366, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1234, + "congestion_net_envelope_cache_misses": 2769, + "congestion_presence_cache_hits": 1302, + "congestion_presence_cache_misses": 1664, + "congestion_presence_skips": 354, + "danger_map_cache_hits": 11485, + "danger_map_cache_misses": 5474, + "danger_map_lookup_calls": 16959, + "danger_map_query_calls": 5474, + "danger_map_total_ns": 145721703, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 397, + "dynamic_path_objects_removed": 350, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 7562.5, + "guidance_bonus_applied_bend90": 2937.5, + "guidance_bonus_applied_sbend": 250.0, + "guidance_bonus_applied_straight": 4375.0, + "guidance_match_moves": 121, + "guidance_match_moves_bend90": 47, + "guidance_match_moves_sbend": 4, + "guidance_match_moves_straight": 70, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 39, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "late_phase_capped_fallbacks": 2, + "late_phase_capped_nets": 2, + "move_cache_abs_hits": 1304, + "move_cache_abs_misses": 4997, + "move_cache_rel_hits": 4419, + "move_cache_rel_misses": 578, + "moves_added": 5638, + "moves_generated": 6301, + "nets_carried_forward": 14, + "nets_reached_target": 44, + "nets_routed": 46, + "nodes_expanded": 1203, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 39, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 354, + "pruned_cost": 309, + "pruned_hard_collision": 0, + "ray_cast_calls": 4059, + "ray_cast_calls_expand_forward": 1159, + "ray_cast_calls_expand_snap": 13, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2881, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 170, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 5962, + "score_component_total_ns": 164785883, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1276, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1884, + "verify_dynamic_exact_pair_checks": 557, + "verify_path_report_calls": 174, + "verify_static_buffer_ops": 805, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1159, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start_seed43", + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/iteration_trace.md b/docs/iteration_trace.md new file mode 100644 index 0000000..54e4967 --- /dev/null +++ b/docs/iteration_trace.md @@ -0,0 +1,81 @@ +# Iteration Trace + +Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_iteration_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs | +| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | +| 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 | +| 1 | 140.0 | 10 | 2 | 12 | 54 | 253 | 974 | 2378 | 1998 | +| 2 | 196.0 | 10 | 4 | 5 | 22 | 253 | 993 | 1928 | 1571 | +| 3 | 274.4 | 10 | 6 | 2 | 10 | 100 | 437 | 852 | 698 | +| 4 | 384.2 | 4 | 6 | 2 | 10 | 81 | 332 | 627 | 513 | + +Top nets by iteration-attributed nodes expanded: + +- `net_09`: 242 +- `net_00`: 201 +- `net_02`: 157 +- `net_06`: 155 +- `net_01`: 147 +- `net_08`: 144 +- `net_03`: 141 +- `net_07`: 45 +- `net_04`: 13 +- `net_05`: 13 + +Top nets by iteration-attributed congestion checks: + +- `net_06`: 569 +- `net_02`: 514 +- `net_01`: 468 +- `net_03`: 425 +- `net_00`: 203 +- `net_08`: 170 +- `net_07`: 143 +- `net_09`: 124 +- `net_04`: 60 +- `net_05`: 60 + +## example_07_large_scale_routing_no_warm_start_seed43 + +Results: 10 valid / 10 reached / 10 total. + +| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs | +| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | +| 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 | +| 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 | +| 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | +| 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 | +| 4 | 384.2 | 6 | 6 | 2 | 10 | 136 | 505 | 1025 | 829 | +| 5 | 537.8 | 4 | 6 | 2 | 10 | 33 | 171 | 419 | 287 | + +Top nets by iteration-attributed nodes expanded: + +- `net_09`: 250 +- `net_03`: 199 +- `net_00`: 177 +- `net_08`: 166 +- `net_07`: 140 +- `net_06`: 105 +- `net_02`: 79 +- `net_01`: 65 +- `net_05`: 12 +- `net_04`: 10 + +Top nets by iteration-attributed congestion checks: + +- `net_03`: 639 +- `net_07`: 454 +- `net_06`: 382 +- `net_02`: 290 +- `net_08`: 283 +- `net_09`: 178 +- `net_01`: 135 +- `net_00`: 82 +- `net_05`: 47 +- `net_04`: 40 + diff --git a/docs/optimization_pass_01_log.md b/docs/optimization_pass_01_log.md new file mode 100644 index 0000000..12ffdb6 --- /dev/null +++ b/docs/optimization_pass_01_log.md @@ -0,0 +1,3741 @@ +# Optimization Pass 01 Log + +This log records the step-by-step measurements for the first visibility-focused optimization pass. +Each section is appended after a discrete code change using `scripts/diff_performance_baseline.py`. +## Step 0 - Pre-optimization baseline + +Measured on 2026-03-31T18:02:56-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Current default tangent-corner routing still pays for eager exact visibility-graph construction. +- Visibility-build ray casts dominate all three hotspot scenarios before any routing changes. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_04_sbends_and_radii | duration_s | 1.9938 | 2.0940 | +0.1002 | +| example_04_sbends_and_radii | nodes_expanded | 15.0000 | 15.0000 | +0.0000 | +| example_04_sbends_and_radii | visibility_builds | 3.0000 | 3.0000 | +0.0000 | +| example_04_sbends_and_radii | visibility_corner_pairs_checked | 18148.0000 | 18148.0000 | +0.0000 | +| example_04_sbends_and_radii | ray_cast_calls | 18218.0000 | 18218.0000 | +0.0000 | +| example_04_sbends_and_radii | ray_cast_calls_visibility_build | 18148.0000 | 18148.0000 | +0.0000 | +| example_04_sbends_and_radii | ray_cast_exact_geometry_checks | 21265.0000 | 21265.0000 | +0.0000 | +| example_04_sbends_and_radii | ray_cast_candidate_bounds | 50717.0000 | 50717.0000 | +0.0000 | +| example_06_bend_collision_models | duration_s | 4.1186 | 4.2483 | +0.1297 | +| example_06_bend_collision_models | nodes_expanded | 240.0000 | 240.0000 | +0.0000 | +| example_06_bend_collision_models | visibility_builds | 6.0000 | 6.0000 | +0.0000 | +| example_06_bend_collision_models | visibility_corner_pairs_checked | 39848.0000 | 39848.0000 | +0.0000 | +| example_06_bend_collision_models | ray_cast_calls | 40530.0000 | 40530.0000 | +0.0000 | +| example_06_bend_collision_models | ray_cast_calls_visibility_build | 39848.0000 | 39848.0000 | +0.0000 | +| example_06_bend_collision_models | ray_cast_exact_geometry_checks | 36858.0000 | 36858.0000 | +0.0000 | +| example_06_bend_collision_models | ray_cast_candidate_bounds | 121732.0000 | 121732.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 1.3734 | 1.4031 | +0.0297 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_builds | 11.0000 | 11.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_corner_pairs_checked | 10768.0000 | 10768.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls | 11151.0000 | 11151.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls_visibility_build | 10768.0000 | 10768.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_exact_geometry_checks | 11651.0000 | 11651.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_candidate_bounds | 21198.0000 | 21198.0000 | +0.0000 | +## Step 1 - Lazy visibility state split + +Measured on 2026-03-31T18:05:49-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Corner-index rebuilds are now measured separately from exact visibility-graph builds. +- Default tangent-corner routing still triggers exact graph work at query time, so visibility-build counters remain hot in this step. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_04_sbends_and_radii | duration_s | 1.9938 | 0.2543 | -1.7395 | +| example_04_sbends_and_radii | nodes_expanded | 15.0000 | 15.0000 | +0.0000 | +| example_04_sbends_and_radii | visibility_corner_index_builds | - | 2.0000 | - | +| example_04_sbends_and_radii | visibility_builds | 3.0000 | 2.0000 | -1.0000 | +| example_04_sbends_and_radii | visibility_corner_pairs_checked | 18148.0000 | 1892.0000 | -16256.0000 | +| example_04_sbends_and_radii | ray_cast_calls | 18218.0000 | 1962.0000 | -16256.0000 | +| example_04_sbends_and_radii | ray_cast_calls_visibility_build | 18148.0000 | 1892.0000 | -16256.0000 | +| example_04_sbends_and_radii | ray_cast_exact_geometry_checks | 21265.0000 | 2445.0000 | -18820.0000 | +| example_04_sbends_and_radii | ray_cast_candidate_bounds | 50717.0000 | 3864.0000 | -46853.0000 | +| example_06_bend_collision_models | duration_s | 4.1186 | 0.2055 | -3.9131 | +| example_06_bend_collision_models | nodes_expanded | 240.0000 | 240.0000 | +0.0000 | +| example_06_bend_collision_models | visibility_corner_index_builds | - | 3.0000 | - | +| example_06_bend_collision_models | visibility_builds | 6.0000 | 3.0000 | -3.0000 | +| example_06_bend_collision_models | visibility_corner_pairs_checked | 39848.0000 | 396.0000 | -39452.0000 | +| example_06_bend_collision_models | ray_cast_calls | 40530.0000 | 1078.0000 | -39452.0000 | +| example_06_bend_collision_models | ray_cast_calls_visibility_build | 39848.0000 | 396.0000 | -39452.0000 | +| example_06_bend_collision_models | ray_cast_exact_geometry_checks | 36858.0000 | 0.0000 | -36858.0000 | +| example_06_bend_collision_models | ray_cast_candidate_bounds | 121732.0000 | 877.0000 | -120855.0000 | +| example_07_large_scale_routing | duration_s | 1.3734 | 1.3958 | +0.0224 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_corner_index_builds | - | 10.0000 | - | +| example_07_large_scale_routing | visibility_builds | 11.0000 | 10.0000 | -1.0000 | +| example_07_large_scale_routing | visibility_corner_pairs_checked | 10768.0000 | 10768.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls | 11151.0000 | 11151.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls_visibility_build | 10768.0000 | 10768.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_exact_geometry_checks | 11651.0000 | 11651.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_candidate_bounds | 21198.0000 | 21198.0000 | +0.0000 | +## Step 2 - Tangent-corner cheap path + +Measured on 2026-03-31T18:06:53-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Default tangent-corner expansion now uses only the corner index and never requests the exact corner graph. +- The expected win is zero visibility-build ray casts in the hotspot trio while keeping node counts stable. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_04_sbends_and_radii | duration_s | 1.9938 | 0.0280 | -1.9659 | +| example_04_sbends_and_radii | nodes_expanded | 15.0000 | 15.0000 | +0.0000 | +| example_04_sbends_and_radii | visibility_corner_index_builds | - | 2.0000 | - | +| example_04_sbends_and_radii | visibility_builds | 3.0000 | 0.0000 | -3.0000 | +| example_04_sbends_and_radii | visibility_corner_pairs_checked | 18148.0000 | 0.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_calls | 18218.0000 | 70.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_calls_visibility_build | 18148.0000 | 0.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_exact_geometry_checks | 21265.0000 | 0.0000 | -21265.0000 | +| example_04_sbends_and_radii | ray_cast_candidate_bounds | 50717.0000 | 4.0000 | -50713.0000 | +| example_06_bend_collision_models | duration_s | 4.1186 | 0.1900 | -3.9286 | +| example_06_bend_collision_models | nodes_expanded | 240.0000 | 240.0000 | +0.0000 | +| example_06_bend_collision_models | visibility_corner_index_builds | - | 3.0000 | - | +| example_06_bend_collision_models | visibility_builds | 6.0000 | 0.0000 | -6.0000 | +| example_06_bend_collision_models | visibility_corner_pairs_checked | 39848.0000 | 0.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_calls | 40530.0000 | 682.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_calls_visibility_build | 39848.0000 | 0.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_exact_geometry_checks | 36858.0000 | 0.0000 | -36858.0000 | +| example_06_bend_collision_models | ray_cast_candidate_bounds | 121732.0000 | 97.0000 | -121635.0000 | +| example_07_large_scale_routing | duration_s | 1.3734 | 0.2042 | -1.1693 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_corner_index_builds | - | 10.0000 | - | +| example_07_large_scale_routing | visibility_builds | 11.0000 | 0.0000 | -11.0000 | +| example_07_large_scale_routing | visibility_corner_pairs_checked | 10768.0000 | 0.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_calls | 11151.0000 | 383.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_calls_visibility_build | 10768.0000 | 0.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_exact_geometry_checks | 11651.0000 | 150.0000 | -11501.0000 | +| example_07_large_scale_routing | ray_cast_candidate_bounds | 21198.0000 | 683.0000 | -20515.0000 | +## Step 3 - Final optimized baseline + +Measured on 2026-03-31T18:08:19-07:00. +Baseline: `/tmp/inire_pre_optimization_baseline.json`. + +Findings: + +- Committed baseline artifacts were regenerated from the optimized router after the tangent-corner change landed. +- The hotspot trio now reaches the same node counts with zero exact visibility-graph builds in default tangent-corner mode. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_04_sbends_and_radii | duration_s | 1.9938 | 0.0279 | -1.9659 | +| example_04_sbends_and_radii | nodes_expanded | 15.0000 | 15.0000 | +0.0000 | +| example_04_sbends_and_radii | visibility_corner_index_builds | - | 2.0000 | - | +| example_04_sbends_and_radii | visibility_builds | 3.0000 | 0.0000 | -3.0000 | +| example_04_sbends_and_radii | visibility_corner_pairs_checked | 18148.0000 | 0.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_calls | 18218.0000 | 70.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_calls_visibility_build | 18148.0000 | 0.0000 | -18148.0000 | +| example_04_sbends_and_radii | ray_cast_exact_geometry_checks | 21265.0000 | 0.0000 | -21265.0000 | +| example_04_sbends_and_radii | ray_cast_candidate_bounds | 50717.0000 | 4.0000 | -50713.0000 | +| example_06_bend_collision_models | duration_s | 4.1186 | 0.1900 | -3.9286 | +| example_06_bend_collision_models | nodes_expanded | 240.0000 | 240.0000 | +0.0000 | +| example_06_bend_collision_models | visibility_corner_index_builds | - | 3.0000 | - | +| example_06_bend_collision_models | visibility_builds | 6.0000 | 0.0000 | -6.0000 | +| example_06_bend_collision_models | visibility_corner_pairs_checked | 39848.0000 | 0.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_calls | 40530.0000 | 682.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_calls_visibility_build | 39848.0000 | 0.0000 | -39848.0000 | +| example_06_bend_collision_models | ray_cast_exact_geometry_checks | 36858.0000 | 0.0000 | -36858.0000 | +| example_06_bend_collision_models | ray_cast_candidate_bounds | 121732.0000 | 97.0000 | -121635.0000 | +| example_07_large_scale_routing | duration_s | 1.3734 | 0.2004 | -1.1730 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_corner_index_builds | - | 10.0000 | - | +| example_07_large_scale_routing | visibility_builds | 11.0000 | 0.0000 | -11.0000 | +| example_07_large_scale_routing | visibility_corner_pairs_checked | 10768.0000 | 0.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_calls | 11151.0000 | 383.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_calls_visibility_build | 10768.0000 | 0.0000 | -10768.0000 | +| example_07_large_scale_routing | ray_cast_exact_geometry_checks | 11651.0000 | 150.0000 | -11501.0000 | +| example_07_large_scale_routing | ray_cast_candidate_bounds | 21198.0000 | 683.0000 | -20515.0000 | +## Step 4 - Tangent candidate scan baseline + +Measured on 2026-03-31T18:33:15-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The next hotspot is tangent-corner candidate scanning, especially example_02 and example_07. +- This baseline captures current candidate-check volume before narrowing the corner-index query window. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3365 | 0.3321 | -0.0044 | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_scans | 363.0000 | 363.0000 | +0.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_corner_checks | 18991.0000 | 18991.0000 | +0.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_ray_tests | 253.0000 | 253.0000 | +0.0000 | +| example_02_congestion_resolution | ray_cast_calls | 1164.0000 | 1164.0000 | +0.0000 | +| example_02_congestion_resolution | danger_map_lookup_calls | 2208.0000 | 2208.0000 | +0.0000 | +| example_02_congestion_resolution | score_component_calls | 976.0000 | 976.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2503 | 0.2404 | -0.0099 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_scans | 280.0000 | 280.0000 | +0.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_corner_checks | 1483.0000 | 1483.0000 | +0.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_ray_tests | 9.0000 | 9.0000 | +0.0000 | +| example_05_orientation_stress | ray_cast_calls | 1243.0000 | 1243.0000 | +0.0000 | +| example_05_orientation_stress | danger_map_lookup_calls | 2079.0000 | 2079.0000 | +0.0000 | +| example_05_orientation_stress | score_component_calls | 1198.0000 | 1198.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2034 | 0.1962 | -0.0072 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_scans | 68.0000 | 68.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_corner_checks | 34735.0000 | 34735.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_ray_tests | 77.0000 | 77.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls | 383.0000 | 383.0000 | +0.0000 | +| example_07_large_scale_routing | danger_map_lookup_calls | 681.0000 | 681.0000 | +0.0000 | +| example_07_large_scale_routing | score_component_calls | 291.0000 | 291.0000 | +0.0000 | +## Step 5 - Tangent candidate strip query + +Measured on 2026-03-31T18:34:10-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tangent-corner candidate collection now queries orientation-aware radius strips instead of scanning a full square around each search state. +- The main acceptance signal is lower tangent corner checks with unchanged node counts and route outcomes. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3365 | 0.3361 | -0.0004 | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_scans | 363.0000 | 363.0000 | +0.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_corner_checks | 18991.0000 | 873.0000 | -18118.0000 | +| example_02_congestion_resolution | visibility_tangent_candidate_ray_tests | 253.0000 | 253.0000 | +0.0000 | +| example_02_congestion_resolution | ray_cast_calls | 1164.0000 | 1164.0000 | +0.0000 | +| example_02_congestion_resolution | danger_map_lookup_calls | 2208.0000 | 2208.0000 | +0.0000 | +| example_02_congestion_resolution | score_component_calls | 976.0000 | 976.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2503 | 0.2500 | -0.0003 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_scans | 280.0000 | 280.0000 | +0.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_corner_checks | 1483.0000 | 70.0000 | -1413.0000 | +| example_05_orientation_stress | visibility_tangent_candidate_ray_tests | 9.0000 | 9.0000 | +0.0000 | +| example_05_orientation_stress | ray_cast_calls | 1243.0000 | 1243.0000 | +0.0000 | +| example_05_orientation_stress | danger_map_lookup_calls | 2079.0000 | 2079.0000 | +0.0000 | +| example_05_orientation_stress | score_component_calls | 1198.0000 | 1198.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2034 | 0.1874 | -0.0160 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_scans | 68.0000 | 68.0000 | +0.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_corner_checks | 34735.0000 | 321.0000 | -34414.0000 | +| example_07_large_scale_routing | visibility_tangent_candidate_ray_tests | 77.0000 | 77.0000 | +0.0000 | +| example_07_large_scale_routing | ray_cast_calls | 383.0000 | 383.0000 | +0.0000 | +| example_07_large_scale_routing | danger_map_lookup_calls | 681.0000 | 681.0000 | +0.0000 | +| example_07_large_scale_routing | score_component_calls | 291.0000 | 291.0000 | +0.0000 | +## Step 6 - Empty danger-map fast path + +Measured on 2026-03-31T18:50:25-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Scoring now skips danger-map sampling when the KD-tree is empty, which should primarily help obstacle-free scenarios. +- The no-warm-start example_07 variant is included as a canary; it is not part of the default baseline corpus, so baseline values may be absent on first measurement. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3325 | 0.3260 | -0.0065 | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | score_component_calls | 976.0000 | 976.0000 | +0.0000 | +| example_02_congestion_resolution | danger_map_lookup_calls | 2208.0000 | 0.0000 | -2208.0000 | +| example_02_congestion_resolution | danger_map_cache_hits | 1433.0000 | 0.0000 | -1433.0000 | +| example_02_congestion_resolution | danger_map_cache_misses | 775.0000 | 0.0000 | -775.0000 | +| example_02_congestion_resolution | warm_start_paths_built | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | warm_start_paths_used | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2404 | 0.2375 | -0.0029 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | score_component_calls | 1198.0000 | 1198.0000 | +0.0000 | +| example_05_orientation_stress | danger_map_lookup_calls | 2079.0000 | 0.0000 | -2079.0000 | +| example_05_orientation_stress | danger_map_cache_hits | 1386.0000 | 0.0000 | -1386.0000 | +| example_05_orientation_stress | danger_map_cache_misses | 693.0000 | 0.0000 | -693.0000 | +| example_05_orientation_stress | warm_start_paths_built | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | warm_start_paths_used | 2.0000 | 2.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 7 - Verification baseline + +Measured on 2026-03-31T19:00:03-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The next low-risk optimization target is redundant verification during refinement, especially in example_02. +- The no-warm-start example_07 canary stays in the measurement set even though it is not part of the default baseline corpus. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3220 | 0.3304 | +0.0084 | +| example_02_congestion_resolution | verify_path_report_calls | 35.0000 | 35.0000 | +0.0000 | +| example_02_congestion_resolution | refine_path_calls | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | refinement_candidates_verified | 26.0000 | 26.0000 | +0.0000 | +| example_02_congestion_resolution | refinement_candidates_accepted | 2.0000 | 2.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | warm_start_paths_built | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | warm_start_paths_used | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2340 | 0.2348 | +0.0008 | +| example_05_orientation_stress | verify_path_report_calls | 12.0000 | 12.0000 | +0.0000 | +| example_05_orientation_stress | refine_path_calls | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | refinement_candidates_verified | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | refinement_candidates_accepted | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | warm_start_paths_built | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | warm_start_paths_used | 2.0000 | 2.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 8 - Deferred refinement verification + +Measured on 2026-03-31T19:02:46-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Per-net verification inside _refine_results() is now deferred to the final verification pass to avoid verifying the same refined path twice. +- The main expected signal is fewer verify_path_report_calls with unchanged route outcomes. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3220 | 0.3273 | +0.0052 | +| example_02_congestion_resolution | verify_path_report_calls | 35.0000 | 32.0000 | -3.0000 | +| example_02_congestion_resolution | refine_path_calls | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | refinement_candidates_verified | 26.0000 | 26.0000 | +0.0000 | +| example_02_congestion_resolution | refinement_candidates_accepted | 2.0000 | 2.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | warm_start_paths_built | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | warm_start_paths_used | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2340 | 0.2350 | +0.0011 | +| example_05_orientation_stress | verify_path_report_calls | 12.0000 | 9.0000 | -3.0000 | +| example_05_orientation_stress | refine_path_calls | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | refinement_candidates_verified | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | refinement_candidates_accepted | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | warm_start_paths_built | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | warm_start_paths_used | 2.0000 | 2.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 9 - Dynamic rtree for congestion and verification + +Measured on 2026-03-31T20:12:56-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Dynamic congestion confirmation and dynamic-path verification now use the mutable rtree index instead of rebuilding a transient STRtree. +- The expected signal is dynamic_tree_rebuilds dropping to zero on the normal corpus while route outcomes stay unchanged. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3211 | 0.3273 | +0.0062 | +| example_02_congestion_resolution | dynamic_tree_rebuilds | 6.0000 | 0.0000 | -6.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | verify_path_report_calls | 32.0000 | 32.0000 | +0.0000 | +| example_02_congestion_resolution | verify_dynamic_exact_pair_checks | 90.0000 | 130.0000 | +40.0000 | +| example_05_orientation_stress | duration_s | 0.2351 | 0.2324 | -0.0028 | +| example_05_orientation_stress | dynamic_tree_rebuilds | 10.0000 | 0.0000 | -10.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 66.0000 | 68.0000 | +2.0000 | +| example_05_orientation_stress | verify_path_report_calls | 9.0000 | 9.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_exact_pair_checks | 2.0000 | 4.0000 | +2.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 10 - Incremental dynamic congestion grid + +Measured on 2026-03-31T20:29:38-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Dynamic path removal is now net-owned and the congestion grid is updated incrementally instead of being invalidated and rebuilt. +- The expected signal is dynamic_grid_rebuilds dropping to zero and better performance on congestion-heavy or no-warm-start routing. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2394 | 0.2605 | +0.0211 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 68.0000 | 68.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 3.0000 | 0.0000 | -3.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 11 - Per-polygon congestion broad phase + +Measured on 2026-03-31T20:36:35-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion candidate collection now scans per dilated polygon bounds instead of the move-wide union bounds. +- The main expected signal is fewer broad-phase candidate ids, especially on large dynamic-path states such as example_07 without warm start. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2541 | 0.2488 | -0.0053 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | - | 83.0000 | - | +| example_05_orientation_stress | congestion_exact_pair_checks | 68.0000 | 70.0000 | +2.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1984 | 0.1867 | -0.0118 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | dynamic_path_objects_added | 88.0000 | 88.0000 | +0.0000 | +| example_07_large_scale_routing | dynamic_path_objects_removed | 44.0000 | 44.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | added | - | - | - | +## Step 12 - Cheap exact congestion predicates + +Measured on 2026-03-31T20:47:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion and dynamic verification now use non-constructive overlap predicates instead of building intersection geometries for every exact pair check. +- The no-warm-start example_07 canary now reports its current metrics directly in the log, which makes the congestion hot path measurable even though it is not part of the committed baseline corpus. +- The canary runtime dropped materially with unchanged node and congestion counts, which confirms that exact confirmation cost was a major part of the remaining congestion overhead. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2541 | 0.2682 | +0.0141 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | - | 83.0000 | - | +| example_05_orientation_stress | congestion_exact_pair_checks | 68.0000 | 70.0000 | +2.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 109.2839 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 641300.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 1737551.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 1192907.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 5379.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 641300.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_grid_rebuilds | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_added | - | 1601.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_removed | - | 1462.0000 | - | +## Step 13 - Relevant-polygon exact congestion checks + +Measured on 2026-03-31T21:03:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion candidate tracking now remembers which dilated move polygons produced each candidate object, so exact confirmation no longer retests candidates against unrelated polygons. +- This slice was largely neutral in aggregate counters: the no-warm-start canary still spent most of its time in congestion, which means the next win had to come from reducing how often congestion is queried at all. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2523 | 0.2753 | +0.0230 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 83.0000 | 83.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 70.0000 | 70.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 104.0661 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 641300.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 1737551.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 1208409.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 5379.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 641300.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_grid_rebuilds | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_added | - | 1601.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_removed | - | 1462.0000 | - | +## Step 14 - Self-collision before congestion + +Measured on 2026-03-31T21:08:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Moves that self-intersect the ancestor chain are now rejected before congestion scoring, so the search no longer spends congestion work on moves that will be discarded anyway. +- This is the first slice that materially cut congestion volume on the no-warm-start example_07 canary. +- Relative to Step 13, the canary dropped from `641300` to `529038` congestion checks, from `1737551` to `1164421` candidate ids, and from `1208409` to `838008` exact pair checks while runtime improved from `104.07s` to `102.02s`. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2523 | 0.2662 | +0.0139 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 83.0000 | 83.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 70.0000 | 70.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 412.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 102.0202 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 529038.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 1164421.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 838008.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 3933.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 529038.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_grid_rebuilds | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_added | - | 1601.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_removed | - | 1462.0000 | - | +## Step 15 - Uncongested dominance before congestion + +Measured on 2026-03-31T21:18:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Admission now computes the uncongested component score before congestion and prunes moves that are already closed-set-dominated even with zero congestion penalty. +- This slice materially reduced congestion misses without increasing `score_component_calls`, so it removed congestion work instead of shifting it into scoring. +- Relative to Step 14, the no-warm-start example_07 canary dropped from `529038` to `344747` congestion checks, from `1164421` to `375624` candidate ids, and from `838008` to `314367` exact pair checks while runtime improved from `102.02s` to `88.86s`. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2494 | 0.2619 | +0.0125 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 412.0000 | 213.0000 | -199.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 83.0000 | 19.0000 | -64.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 70.0000 | 18.0000 | -52.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 412.0000 | 213.0000 | -199.0000 | +| example_05_orientation_stress | dynamic_grid_rebuilds | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_added | 37.0000 | 37.0000 | +0.0000 | +| example_05_orientation_stress | dynamic_path_objects_removed | 25.0000 | 25.0000 | +0.0000 | +| example_05_orientation_stress | score_component_calls | 1198.0000 | 1198.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 88.8639 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 375624.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 314367.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 3300.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_grid_rebuilds | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_added | - | 1601.0000 | - | +| example_07_large_scale_routing_no_warm_start | dynamic_path_objects_removed | - | 1462.0000 | - | +| example_07_large_scale_routing_no_warm_start | score_component_calls | - | 534994.0000 | - | +## Step 16 - Lazy congestion on pop (rejected) + +Measured on 2026-03-31T23:55:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion resolution was moved from move generation to node pop, with penalized nodes requeued after their overlap count was resolved. +- The first cut reduced raw congestion misses on the no-warm-start example_07 canary from `344747` to `331308`, but runtime still regressed to about `99.00s` and nodes expanded rose to `184853`. +- Tightening lazy-requeue bookkeeping did not recover the search-order penalty. A later measurement pushed the same canary to `153.63s`, `247887` expanded nodes, `427874` congestion misses, and `166395` lazy requeues. +- This pass was rejected and reverted. The remaining congestion misses appear structural, but optimistic unresolved nodes created too much extra search churn. +## Step 17 - Grid-span congestion broad-phase cache + +Measured on 2026-03-31T22:44:31-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion broad-phase candidate unions are now cached by queried grid-cell span within a single A* run. +- The exact overlap cache still misses at the previous rate, but the new grid-span cache hits heavily on repeated local congestion probes. +- Relative to the reverted Step 15 state, the no-warm-start example_07 canary improved from about 87.20s to 84.71s with unchanged nodes expanded and congestion check calls. +- The canary's broad-phase work also dropped modestly, from 375624 to 364731 candidate ids and from 314367 to 305397 exact pair checks. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2386 | 0.2380 | -0.0007 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | moves_generated | 1624.0000 | 1624.0000 | +0.0000 | +| example_05_orientation_stress | moves_added | 681.0000 | 681.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_grid_span_cache_hits | - | 133.0000 | - | +| example_05_orientation_stress | congestion_grid_span_cache_misses | - | 22.0000 | - | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1885 | 0.1841 | -0.0044 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | moves_generated | 372.0000 | 372.0000 | +0.0000 | +| example_07_large_scale_routing | moves_added | 227.0000 | 227.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_span_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_span_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 84.8404 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | moves_generated | - | 857732.0000 | - | +| example_07_large_scale_routing_no_warm_start | moves_added | - | 348559.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 3300.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_hits | - | 199762.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_misses | - | 26740.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +## Step 18 - Net-envelope maintenance and counters + +Measured on 2026-03-31T23:05:08-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Added per-net dynamic envelope state and public counters without changing congestion or verification query behavior yet. +- The expected result for this slice is unchanged routing behavior; the new envelope counters should stay at zero until the broad phase starts using them. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2312 | 0.2343 | +0.0031 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | - | 0.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_hits | - | 0.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_misses | - | 0.0000 | - | +| example_05_orientation_stress | congestion_grid_span_cache_hits | 133.0000 | 133.0000 | +0.0000 | +| example_05_orientation_stress | congestion_grid_span_cache_misses | 22.0000 | 22.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing | duration_s | 0.1858 | 0.1833 | -0.0026 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_span_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_span_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | verify_dynamic_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 84.6663 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_hits | - | 199762.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_misses | - | 26740.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_candidate_nets | - | 0.0000 | - | +## Step 19 - Route-time net-envelope broad phase + +Measured on 2026-03-31T23:10:12-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion checks now query per-net dynamic envelopes before descending into cached per-object candidate sets. +- Search order is unchanged in this slice; the acceptance signal is lower candidate-net and exact-pair work at the same node count. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2312 | 0.2443 | +0.0131 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | - | 15.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_hits | - | 133.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_misses | - | 22.0000 | - | +| example_05_orientation_stress | congestion_grid_span_cache_hits | 133.0000 | 11.0000 | -122.0000 | +| example_05_orientation_stress | congestion_grid_span_cache_misses | 22.0000 | 4.0000 | -18.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing | duration_s | 0.1858 | 0.1940 | +0.0081 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_span_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_span_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | verify_dynamic_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 85.7274 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 557244.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_hits | - | 199762.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_misses | - | 26740.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_hits | - | 193229.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_misses | - | 25872.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_candidate_nets | - | 0.0000 | - | +## Step 20 - Verification net-envelope broad phase + +Measured on 2026-03-31T23:14:19-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Final verification now queries dynamic net envelopes before descending into per-object overlap checks. +- This slice should leave routing search metrics stable and reduce dynamic verification scans when non-overlapping nets are present. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2312 | 0.2370 | +0.0058 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | - | 15.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_hits | - | 133.0000 | - | +| example_05_orientation_stress | congestion_net_envelope_cache_misses | - | 22.0000 | - | +| example_05_orientation_stress | congestion_grid_span_cache_hits | 133.0000 | 11.0000 | -122.0000 | +| example_05_orientation_stress | congestion_grid_span_cache_misses | 22.0000 | 4.0000 | -18.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_candidate_nets | - | 3.0000 | - | +| example_05_orientation_stress | verify_dynamic_exact_pair_checks | 4.0000 | 4.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1858 | 0.1843 | -0.0016 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_net_envelope_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_span_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_span_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | verify_dynamic_candidate_nets | - | 158.0000 | - | +| example_07_large_scale_routing | verify_dynamic_exact_pair_checks | 27.0000 | 24.0000 | -3.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 85.5035 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 557244.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_hits | - | 199762.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_misses | - | 26740.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_hits | - | 193229.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_misses | - | 25872.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_candidate_nets | - | 3723.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_exact_pair_checks | - | 1428.0000 | - | +## Step 21 - Per-net grid occupancy before object descent + +Measured on 2026-03-31T23:45:12-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Congestion checks now cache candidate net ids from dynamic grid occupancy before building the heavier per-span object unions. +- On the no-warm-start example_07 canary, candidate nets fell materially from the prior net-envelope pass while nodes expanded and congestion check calls stayed flat. +- Object-level candidate ids and exact pair checks stayed essentially unchanged, so the next likely win is a finer-grained dynamic structure per long net rather than more cache layering. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2327 | 0.2399 | +0.0072 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_net_envelope_cache_hits | 133.0000 | 133.0000 | +0.0000 | +| example_05_orientation_stress | congestion_net_envelope_cache_misses | 22.0000 | 22.0000 | +0.0000 | +| example_05_orientation_stress | congestion_grid_net_cache_hits | - | 11.0000 | - | +| example_05_orientation_stress | congestion_grid_net_cache_misses | - | 4.0000 | - | +| example_05_orientation_stress | congestion_grid_span_cache_hits | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | congestion_grid_span_cache_misses | 4.0000 | 4.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_candidate_nets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | verify_dynamic_exact_pair_checks | 4.0000 | 4.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1865 | 0.1881 | +0.0015 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_net_envelope_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_net_envelope_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_net_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_net_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_grid_span_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_grid_span_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | verify_dynamic_candidate_nets | 158.0000 | 158.0000 | +0.0000 | +| example_07_large_scale_routing | verify_dynamic_exact_pair_checks | 24.0000 | 24.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 85.4211 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 386147.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_hits | - | 199762.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_net_envelope_cache_misses | - | 26740.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_net_cache_hits | - | 193229.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_net_cache_misses | - | 25872.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_hits | - | 189741.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_grid_span_cache_misses | - | 25579.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_candidate_nets | - | 3723.0000 | - | +| example_07_large_scale_routing_no_warm_start | verify_dynamic_exact_pair_checks | - | 1436.0000 | - | +## Step 22 - Segmented per-net dynamic envelopes (rejected) + +Measured on 2026-04-01T00:02:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Dynamic objects were grouped into small per-net segment envelopes and congestion/verification descended through those groups before raw object checks. +- This was the first pass aimed at reducing object-level confirmation work directly, but it regressed the no-warm-start example_07 canary instead of helping it. +- Relative to the accepted per-net grid-occupancy state, the canary worsened from about `85.36s` to `99.81s`, from `173498` to `187339` expanded nodes, from `344747` to `378630` congestion checks, and from `364731` to `392058` candidate ids. +- The segment layer appears to have increased search churn and broad-phase overhead enough to outweigh any local pruning benefit, so this pass was rejected and reverted. +## Step 23 - End-of-iteration reverify only + +Measured on 2026-04-01T19:00:59-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Added full end-of-iteration reverify using final installed geometry before deciding whether negotiated congestion should continue. +- This slice still reroutes every net every iteration; it only changes conflict truth and iteration metrics. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3241 | 0.3359 | +0.0118 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | - | 1.0000 | - | +| example_02_congestion_resolution | iteration_reverified_nets | - | 3.0000 | - | +| example_02_congestion_resolution | iteration_conflicting_nets | - | 0.0000 | - | +| example_02_congestion_resolution | iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2283 | 0.2312 | +0.0029 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nets_carried_forward | - | 0.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | iteration_reverify_calls | - | 2.0000 | - | +| example_05_orientation_stress | iteration_reverified_nets | - | 6.0000 | - | +| example_05_orientation_stress | iteration_conflicting_nets | - | 2.0000 | - | +| example_05_orientation_stress | iteration_conflict_edges | - | 1.0000 | - | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1888 | 0.1921 | +0.0032 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | - | 1.0000 | - | +| example_07_large_scale_routing | iteration_reverified_nets | - | 10.0000 | - | +| example_07_large_scale_routing | iteration_conflicting_nets | - | 0.0000 | - | +| example_07_large_scale_routing | iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 85.3822 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 15.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 150.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 173498.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 344747.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 15.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverified_nets | - | 150.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 145.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 165.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 386147.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 364731.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 305397.0000 | - | +## Step 24 - Early stop on stalled conflict graph + +Measured on 2026-04-01T19:16:22-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Rejected selective reroute working-set policies after they made the no-warm-start canary dramatically slower. +- Kept end-of-iteration reverify and now stop negotiated-congestion once the final conflict graph repeats twice with no structural change. +- On the no-warm-start canary this cut runtime from about 85.9s to 5.45s, with route iterations dropping from 15 to 4 and congestion checks from 344747 to 12096. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3241 | 0.3460 | +0.0219 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | - | 1.0000 | - | +| example_02_congestion_resolution | iteration_reverified_nets | - | 3.0000 | - | +| example_02_congestion_resolution | iteration_conflicting_nets | - | 0.0000 | - | +| example_02_congestion_resolution | iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2283 | 0.3251 | +0.0968 | +| example_05_orientation_stress | route_iterations | 2.0000 | 3.0000 | +1.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 9.0000 | +3.0000 | +| example_05_orientation_stress | nets_carried_forward | - | 0.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 571.0000 | +285.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 306.0000 | +93.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 306.0000 | +93.0000 | +| example_05_orientation_stress | iteration_reverify_calls | - | 3.0000 | - | +| example_05_orientation_stress | iteration_reverified_nets | - | 9.0000 | - | +| example_05_orientation_stress | iteration_conflicting_nets | - | 6.0000 | - | +| example_05_orientation_stress | iteration_conflict_edges | - | 3.0000 | - | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 92.0000 | +77.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 80.0000 | +61.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 68.0000 | +50.0000 | +| example_07_large_scale_routing | duration_s | 0.1888 | 0.1998 | +0.0110 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | - | 1.0000 | - | +| example_07_large_scale_routing | iteration_reverified_nets | - | 10.0000 | - | +| example_07_large_scale_routing | iteration_conflicting_nets | - | 0.0000 | - | +| example_07_large_scale_routing | iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.4956 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4580.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 12096.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 12096.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverified_nets | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 34.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 24413.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 21820.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 18030.0000 | - | +## Step 25 - Finalize stalled conflict stop + +Measured on 2026-04-01T19:17:59-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Removed the leftover full-reroute pre-eviction from the rejected working-set experiment so normal multi-net cases keep their previous search behavior. +- Accepted state: end-of-iteration reverify plus early termination when the conflict graph repeats twice without structural change. +- The no-warm-start example_07 canary now runs in about 5.8s with 4 iterations and 9865 congestion checks, while example_05 returns to 2 iterations and 213 congestion checks. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3241 | 0.3451 | +0.0209 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | - | 1.0000 | - | +| example_02_congestion_resolution | iteration_reverified_nets | - | 3.0000 | - | +| example_02_congestion_resolution | iteration_conflicting_nets | - | 0.0000 | - | +| example_02_congestion_resolution | iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2283 | 0.2433 | +0.0150 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nets_carried_forward | - | 0.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | iteration_reverify_calls | - | 2.0000 | - | +| example_05_orientation_stress | iteration_reverified_nets | - | 6.0000 | - | +| example_05_orientation_stress | iteration_conflicting_nets | - | 2.0000 | - | +| example_05_orientation_stress | iteration_conflict_edges | - | 1.0000 | - | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1888 | 0.1982 | +0.0094 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | - | 1.0000 | - | +| example_07_large_scale_routing | iteration_reverified_nets | - | 10.0000 | - | +| example_07_large_scale_routing | iteration_conflicting_nets | - | 0.0000 | - | +| example_07_large_scale_routing | iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.7283 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 9865.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 9865.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverified_nets | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 35.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | +## Step 24 - Conflict-directed reroute working set + +Measured on 2026-04-01T19:30:43-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Later iterations now reroute only unresolved nets plus a deterministic greedy cover of the end-of-iteration conflict graph. +- Repeated conflict signatures widen the working set to all conflicting nets and then all nets once if the graph keeps stalling. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3241 | 0.3261 | +0.0020 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | - | 1.0000 | - | +| example_02_congestion_resolution | iteration_reverified_nets | - | 3.0000 | - | +| example_02_congestion_resolution | iteration_conflicting_nets | - | 0.0000 | - | +| example_02_congestion_resolution | iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2283 | 0.2246 | -0.0037 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 5.0000 | -1.0000 | +| example_05_orientation_stress | nets_carried_forward | - | 1.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 284.0000 | -2.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 207.0000 | -6.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 207.0000 | -6.0000 | +| example_05_orientation_stress | iteration_reverify_calls | - | 2.0000 | - | +| example_05_orientation_stress | iteration_reverified_nets | - | 6.0000 | - | +| example_05_orientation_stress | iteration_conflicting_nets | - | 2.0000 | - | +| example_05_orientation_stress | iteration_conflict_edges | - | 1.0000 | - | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1888 | 0.1884 | -0.0004 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | - | 1.0000 | - | +| example_07_large_scale_routing | iteration_reverified_nets | - | 10.0000 | - | +| example_07_large_scale_routing | iteration_conflicting_nets | - | 0.0000 | - | +| example_07_large_scale_routing | iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 1626.2304 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 13.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 108.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 13.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1559998.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3699692.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 3699692.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 12.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverified_nets | - | 120.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 113.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 138.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 3444090.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 2987961.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 2440828.0000 | - | +## Step 26 rejected - Progressive freezing reverted + +Measured on 2026-04-01T20:20:32-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The progressive-freezing experiment was reverted after bounded no-warm-start probes reached only 2 valid routes after 4 iterations and consumed the one-shot thaw without restoring correctness. +- The tree below is the restored pre-freezing state so later passes can continue from the last accepted congestion baseline. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3384 | 0.3500 | +0.0115 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_conflicting_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_conflict_edges | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2366 | 0.2547 | +0.0180 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 213.0000 | +0.0000 | +| example_05_orientation_stress | iteration_reverify_calls | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | iteration_conflicting_nets | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | iteration_conflict_edges | 1.0000 | 1.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1994 | 0.2061 | +0.0067 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_conflicting_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_conflict_edges | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.9146 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 9865.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 35.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | +## Step 26 - Progressive freezing and frozen hard prunes + +Measured on 2026-04-01T20:33:10-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Completed nets are now frozen after end-of-iteration reverify, later iterations reroute only the remaining unlocked nets, and overlaps with frozen nets are rejected as hard collisions. +- This slice also tracks best-so-far iteration quality so later slices can safely restore the strongest partial solution. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3384 | 0.3202 | -0.0182 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | frozen_nets_promoted | - | 3.0000 | - | +| example_02_congestion_resolution | frozen_nets_thawed | - | 0.0000 | - | +| example_02_congestion_resolution | frozen_net_hard_prunes | - | 0.0000 | - | +| example_02_congestion_resolution | best_iteration_completed_nets | - | 3.0000 | - | +| example_02_congestion_resolution | best_iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | best_iteration_dynamic_collisions | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2366 | 11.5966 | +11.3600 | +| example_05_orientation_stress | route_iterations | 2.0000 | 6.0000 | +4.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 14.0000 | +8.0000 | +| example_05_orientation_stress | nets_carried_forward | 0.0000 | 4.0000 | +4.0000 | +| example_05_orientation_stress | frozen_nets_promoted | - | 2.0000 | - | +| example_05_orientation_stress | frozen_nets_thawed | - | 1.0000 | - | +| example_05_orientation_stress | frozen_net_hard_prunes | - | 865.0000 | - | +| example_05_orientation_stress | best_iteration_completed_nets | - | 1.0000 | - | +| example_05_orientation_stress | best_iteration_conflict_edges | - | 1.0000 | - | +| example_05_orientation_stress | best_iteration_dynamic_collisions | - | 2.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 14395.0000 | +14109.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 24765.0000 | +24552.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 8658.0000 | +8643.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 10770.0000 | +10751.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 8579.0000 | +8561.0000 | +| example_07_large_scale_routing | duration_s | 0.1994 | 0.1901 | -0.0092 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | frozen_nets_promoted | - | 10.0000 | - | +| example_07_large_scale_routing | frozen_nets_thawed | - | 0.0000 | - | +| example_07_large_scale_routing | frozen_net_hard_prunes | - | 0.0000 | - | +| example_07_large_scale_routing | best_iteration_completed_nets | - | 10.0000 | - | +| example_07_large_scale_routing | best_iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | best_iteration_dynamic_collisions | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 2136.7523 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 62.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_nets_promoted | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_nets_thawed | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_net_hard_prunes | - | 76321.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_completed_nets | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_conflict_edges | - | 12.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_dynamic_collisions | - | 50.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1849024.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4049028.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 4889029.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 4032868.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 3215112.0000 | - | +## Step 26 - Progressive freezing and frozen hard prunes + +Measured on 2026-04-01T20:36:40-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Completed nets are frozen after end-of-iteration reverify, later iterations reroute only the remaining unlocked nets, and overlaps with frozen nets are rejected as hard collisions. +- The router also restores the strongest reverified iteration snapshot before final refinement and verification. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_02_congestion_resolution | duration_s | 0.3384 | 0.3401 | +0.0017 | +| example_02_congestion_resolution | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | nets_routed | 3.0000 | 3.0000 | +0.0000 | +| example_02_congestion_resolution | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | frozen_nets_promoted | - | 3.0000 | - | +| example_02_congestion_resolution | frozen_nets_thawed | - | 0.0000 | - | +| example_02_congestion_resolution | frozen_net_hard_prunes | - | 0.0000 | - | +| example_02_congestion_resolution | best_iteration_completed_nets | - | 3.0000 | - | +| example_02_congestion_resolution | best_iteration_conflict_edges | - | 0.0000 | - | +| example_02_congestion_resolution | best_iteration_dynamic_collisions | - | 0.0000 | - | +| example_02_congestion_resolution | nodes_expanded | 366.0000 | 366.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_reverify_calls | 1.0000 | 1.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_conflicting_nets | 0.0000 | 0.0000 | +0.0000 | +| example_02_congestion_resolution | iteration_conflict_edges | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | duration_s | 0.2366 | 12.3369 | +12.1002 | +| example_05_orientation_stress | route_iterations | 2.0000 | 6.0000 | +4.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 14.0000 | +8.0000 | +| example_05_orientation_stress | nets_carried_forward | 0.0000 | 4.0000 | +4.0000 | +| example_05_orientation_stress | frozen_nets_promoted | - | 2.0000 | - | +| example_05_orientation_stress | frozen_nets_thawed | - | 1.0000 | - | +| example_05_orientation_stress | frozen_net_hard_prunes | - | 865.0000 | - | +| example_05_orientation_stress | best_iteration_completed_nets | - | 1.0000 | - | +| example_05_orientation_stress | best_iteration_conflict_edges | - | 1.0000 | - | +| example_05_orientation_stress | best_iteration_dynamic_collisions | - | 2.0000 | - | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 14395.0000 | +14109.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 24765.0000 | +24552.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 8658.0000 | +8643.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 10770.0000 | +10751.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 8579.0000 | +8561.0000 | +| example_05_orientation_stress | iteration_reverify_calls | 2.0000 | 6.0000 | +4.0000 | +| example_05_orientation_stress | iteration_conflicting_nets | 2.0000 | 12.0000 | +10.0000 | +| example_05_orientation_stress | iteration_conflict_edges | 1.0000 | 6.0000 | +5.0000 | +| example_07_large_scale_routing | duration_s | 0.1994 | 0.1978 | -0.0016 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | frozen_nets_promoted | - | 10.0000 | - | +| example_07_large_scale_routing | frozen_nets_thawed | - | 0.0000 | - | +| example_07_large_scale_routing | frozen_net_hard_prunes | - | 0.0000 | - | +| example_07_large_scale_routing | best_iteration_completed_nets | - | 10.0000 | - | +| example_07_large_scale_routing | best_iteration_conflict_edges | - | 0.0000 | - | +| example_07_large_scale_routing | best_iteration_dynamic_collisions | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_reverify_calls | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_conflicting_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_conflict_edges | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 1500.4410 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 7.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 60.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_nets_promoted | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_nets_thawed | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | frozen_net_hard_prunes | - | 37879.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_completed_nets | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_conflict_edges | - | 12.0000 | - | +| example_07_large_scale_routing_no_warm_start | best_iteration_dynamic_collisions | - | 50.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1282078.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 2860073.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 3432589.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 2740129.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 2266598.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_reverify_calls | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflicting_nets | - | 51.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_conflict_edges | - | 77.0000 | - | +## Step 27 - Congestion presence precheck + +Measured on 2026-04-01T20:49:16-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- A cached per-span presence precheck now skips full congestion evaluation when a move's dilated polygons only cover dynamic-grid cells with no other routed nets. +- The goal of this slice is to reduce congestion_check_calls without changing search outcomes or the dynamic exact-check path. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2366 | 0.2573 | +0.0206 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 213.0000 | 155.0000 | -58.0000 | +| example_05_orientation_stress | congestion_presence_cache_hits | - | 185.0000 | - | +| example_05_orientation_stress | congestion_presence_cache_misses | - | 30.0000 | - | +| example_05_orientation_stress | congestion_presence_skips | - | 58.0000 | - | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 213.0000 | 155.0000 | -58.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1994 | 0.1977 | -0.0017 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_cache_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_presence_cache_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_presence_skips | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.6221 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4549.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_hits | - | 7568.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_misses | - | 2480.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_skips | - | 5482.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 16.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 4549.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | +## Step 28 - Candidate-net congestion precheck + +Measured on 2026-04-01T20:59:46-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- After the dynamic-grid occupancy precheck passes, search now asks whether any candidate nets survive the existing envelope and grid-net filters before paying for full congestion evaluation. +- This slice should reduce congestion_check_calls further if many occupied spans still have no candidate nets after the broad phases. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2491 | 0.2500 | +0.0009 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 18.0000 | -137.0000 | +| example_05_orientation_stress | congestion_presence_cache_hits | 185.0000 | 185.0000 | +0.0000 | +| example_05_orientation_stress | congestion_presence_cache_misses | 30.0000 | 30.0000 | +0.0000 | +| example_05_orientation_stress | congestion_presence_skips | 58.0000 | 58.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_precheck_hits | - | 135.0000 | - | +| example_05_orientation_stress | congestion_candidate_precheck_misses | - | 22.0000 | - | +| example_05_orientation_stress | congestion_candidate_precheck_skips | - | 139.0000 | - | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 0.0000 | -2.0000 | +| example_05_orientation_stress | congestion_cache_misses | 155.0000 | 18.0000 | -137.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 14.0000 | -1.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 18.0000 | -1.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 17.0000 | -1.0000 | +| example_07_large_scale_routing | duration_s | 0.1978 | 0.1941 | -0.0037 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_skips | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_precheck_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_precheck_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_precheck_skips | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 89.2302 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 9.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 90.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 113735.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 136225.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_hits | - | 217089.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_misses | - | 18365.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_skips | - | 86782.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_hits | - | 135690.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_misses | - | 12826.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_skips | - | 10244.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 1893.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 136225.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 243951.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 228721.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 190301.0000 | - | +## Step 28b - Candidate-net congestion precheck (corrected) + +Measured on 2026-04-01T21:00:54-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The first candidate-net precheck attempt cached exact-bounds results by span and was not safe; this corrected slice uses a conservative span-based precheck. +- Acceptance requires the no-warm-start canary to stay near the current 4-iteration / 40-net routed shape while still reducing congestion_check_calls. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2491 | 0.2461 | -0.0030 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_05_orientation_stress | congestion_presence_cache_hits | 185.0000 | 185.0000 | +0.0000 | +| example_05_orientation_stress | congestion_presence_cache_misses | 30.0000 | 30.0000 | +0.0000 | +| example_05_orientation_stress | congestion_presence_skips | 58.0000 | 58.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_precheck_hits | - | 135.0000 | - | +| example_05_orientation_stress | congestion_candidate_precheck_misses | - | 22.0000 | - | +| example_05_orientation_stress | congestion_candidate_precheck_skips | - | 0.0000 | - | +| example_05_orientation_stress | congestion_cache_hits | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | congestion_cache_misses | 155.0000 | 155.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1978 | 0.1979 | +0.0001 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_presence_skips | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_precheck_hits | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_precheck_misses | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_precheck_skips | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_cache_hits | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_cache_misses | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.6758 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_hits | - | 7568.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_cache_misses | - | 2480.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_presence_skips | - | 5482.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_hits | - | 2828.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_misses | - | 1737.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_precheck_skips | - | 129.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_hits | - | 16.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_cache_misses | - | 4420.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | +## Step 29 - Correctness-aware measurement logging + +Measured on 2026-04-01T21:18:56-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The diff script now logs top-level outcome counts so future routing-loop changes can be judged on returned result quality as well as runtime and congestion counters. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2370 | 0.2507 | +0.0137 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1950 | 0.1992 | +0.0042 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.7234 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 1.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +## Step 30 - Best iteration snapshot restoration + +Measured on 2026-04-01T21:20:51-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The routing loop now snapshots the strongest reverified intermediate result set and restores it before final refinement/final verification, including timeout exits. +- This slice keeps the old repeated-conflict stop rule so any quality change can be attributed to snapshot restoration alone. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2370 | 0.2437 | +0.0067 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1950 | 0.1937 | -0.0013 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.5246 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +## Step 31 - Improvement-based stagnation stop + +Measured on 2026-04-01T21:23:19-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The negotiated-congestion loop now stops after two consecutive iterations with no improvement in the best-so-far reverified snapshot instead of using repeated conflict signatures. +- Best-snapshot restoration remains enabled, so the returned results should reflect the strongest intermediate iteration even if later iterations stall. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2370 | 0.2393 | +0.0023 | +| example_05_orientation_stress | valid_results | 3.0000 | 1.0000 | -2.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1950 | 0.1884 | -0.0066 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.4360 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +## Step 31 rejected - Improvement-based stagnation reverted + +Measured on 2026-04-01T21:25:17-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- The no-improvement stop rule was reverted because it regressed example_05 from 3 valid routes to 1 even though the no-warm-start canary stayed flat. +- The tree below is the restored best-snapshot state with the older repeated-conflict stop rule still in place. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2370 | 0.2425 | +0.0055 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1950 | 0.1936 | -0.0014 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.5321 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +## Step 32 tiered-iteration dynamic hard blocks + +Measured on 2026-04-01T21:42:45-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Treat already-routed dynamic paths as hard blockers only during iteration 0 when warm-start is disabled. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2466 | +0.0118 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 226.0000 | +71.0000 | +| example_05_orientation_stress | tiered_dynamic_hard_prunes | - | 0.0000 | - | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 51.0000 | +36.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 67.0000 | +48.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 56.0000 | +38.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1931 | -0.0014 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | tiered_dynamic_hard_prunes | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 8.7624 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 11200.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 5355.0000 | - | +| example_07_large_scale_routing_no_warm_start | tiered_dynamic_hard_prunes | - | 632.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 13623.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13469.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11346.0000 | - | +## Step 32 reverted + +Measured on 2026-04-01T21:44:47-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted tiered-iteration dynamic hard blockers after regressions in example_05 and the no-warm-start example_07 canary. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2462 | +0.0114 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1940 | -0.0005 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.5497 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | + +## Step 34 rejected + +Measured on 2026-04-01T22:08:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Explored order/seed and reroute-heuristic sensitivity before the slice. One-iteration no-warm-start probes were flat at `1` valid route across `user`, `shortest`, `longest`, and seeds `41`-`44`; two-iteration probes were flat at `2` valid routes across the same variants. +- Implemented a conflict-weighted congestion objective that weighted overlapping nets by prior completed status, conflict degree, and dynamic collision count. +- Rejected the slice after the no-warm-start canary failed to finish within a reasonable window and ran far longer than the accepted `~5.6s` state. The code was reverted before accepting any documentation or baseline changes. +## Step 33 carry forward completed nets + +Measured on 2026-04-01T21:58:50-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- After each reverify, reroute only nets that are still colliding, partial, or unroutable; keep completed nets installed as ordinary soft blockers. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2528 | +0.0179 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 5.0000 | -1.0000 | +| example_05_orientation_stress | nets_carried_forward | 0.0000 | 1.0000 | +1.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 284.0000 | -2.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 152.0000 | -3.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1960 | +0.0015 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 49.8770 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 70.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 71162.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 80342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 152845.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 136026.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 116066.0000 | - | +## Step 33 reverted + +Measured on 2026-04-01T21:59:33-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted completed-net carry-forward after the no-warm-start canary exploded in iterations, nodes, and congestion checks without improving validity. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2572 | +0.0224 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 286.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 155.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.2024 | +0.0079 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nets_carried_forward | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 5.6270 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_carried_forward | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 6567.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4420.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 12879.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 13342.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 11116.0000 | - | +## Step 35 reroute seed guidance + +Measured on 2026-04-01T22:22:54-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Guide post-tiered reroutes with the previous reached-target path seed by giving matching path-prefix moves a small cost bonus. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2313 | -0.0036 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 299.0000 | +13.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 149.0000 | -6.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 23.0000 | +8.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 32.0000 | +13.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 30.0000 | +12.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1881 | -0.0064 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5099 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | + +## Step 39 rejected + +Measured on 2026-04-01T22:58:58-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried adding a reroute-only uncongested `max_cost` ceiling derived from the previous reached-target path cost plus slack for two extra bends. +- Rejected the slice before running the standard performance probe because it broke the stalled-conflict guardrail: the simple crossing case in `test_reverify_iterations_stop_early_on_stalled_conflict_graph` ran all `10` iterations instead of stopping early. +- Reverted the slice and restored the accepted Step 35 full-seed guidance state. + +## Step 36 rejected + +Measured on 2026-04-01T22:31:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Explored adding the longest still-valid prefix of the prior reached-target path as an additional seeded start node for later reroutes. +- Rejected the slice on runtime: the no-warm-start `example_07` measurement ran far beyond the accepted Step 35 `~3.5s` state before completion, indicating the extra seeded branch was expanding too much search. +- Reverted the slice and kept only the lighter Step 35 reroute-seed-guidance behavior. +## Step 34 conflict-weighted congestion objective + +Measured on 2026-04-01T22:38:11-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Weight congestion overlap cost by prior-iteration completed status, conflict degree, and dynamic collision count instead of counting every overlapping net equally. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2390 | +0.0041 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 282.0000 | -4.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 145.0000 | -10.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 15.0000 | 15.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 19.0000 | 19.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 18.0000 | 18.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1918 | -0.0027 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 1795.1890 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 1.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 16.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1501303.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 1622620.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 5074171.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 4556689.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 4129698.0000 | - | +## Step 36 reusable prefix start nodes + +Measured on 2026-04-01T22:49:16-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- On later reroutes, add the longest still-valid prefix of the previous reached-target path as an additional seeded start node while keeping the original start branch available. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2249 | +0.0010 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | reroute_prefix_paths_used | - | 3.0000 | - | +| example_05_orientation_stress | reroute_prefix_components_reused | - | 8.0000 | - | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 292.0000 | -7.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 125.0000 | -24.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 22.0000 | -1.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 31.0000 | -1.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 29.0000 | -1.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1916 | +0.0004 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reroute_prefix_paths_used | - | 0.0000 | - | +| example_07_large_scale_routing | reroute_prefix_components_reused | - | 0.0000 | - | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 229.2009 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 9.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 90.0000 | - | +| example_07_large_scale_routing_no_warm_start | reroute_prefix_paths_used | - | 65.0000 | - | +| example_07_large_scale_routing_no_warm_start | reroute_prefix_components_reused | - | 172.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 350958.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 470234.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 929523.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 853594.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 712783.0000 | - | +## Step 37 capped guidance rejected + +Measured on 2026-04-01T22:53:55-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Exploratory probe showed a mild runtime win when reroute guidance was capped to 4 segments, but the code/test slice did not land cleanly and was reverted before acceptance. +- Restored the accepted Step 35 full-seed guidance state and remeasured the standard probe set. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2304 | +0.0065 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1862 | -0.0050 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.4899 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 38 capped reroute guidance + +Measured on 2026-04-01T22:55:29-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Cap reroute guidance seeds to the first 4 segments on later iterations; keep the existing prefix-match bonus unchanged. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2388 | +0.0150 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1924 | +0.0012 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.6174 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 38 reverted + +Measured on 2026-04-01T22:56:22-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted the 4-segment reroute-guidance cap after the standard probe set showed no counter improvement and only runtime noise. +- Restored the accepted Step 35 full-seed guidance state. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2463 | +0.0224 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1912 | -0.0000 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5580 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 40 partial straight guidance + +Measured on 2026-04-01T23:01:12-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Allow reroute guidance to consume straight seeds incrementally so split straight moves can continue following a prior reached-target path. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2329 | +0.0090 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1879 | -0.0033 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5305 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 40 reverted + +Measured on 2026-04-01T23:02:26-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted partial straight-seed guidance after the standard probe set showed no counter deltas and no correctness gain. +- Restored the accepted Step 35 full-seed exact-match guidance state. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2353 | +0.0115 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1897 | -0.0015 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5522 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 41 guidance counters + +Measured on 2026-04-01T23:04:31-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Add public counters for reroute guidance matches and total guidance bonus applied so later guidance changes can be measured directly. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2239 | 0.2544 | +0.0306 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | - | 11.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied | - | 687.5000 | - | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1912 | 0.1940 | +0.0027 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.7438 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 42 scaled guidance bonus + +Measured on 2026-04-02T00:18:11-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Scale reroute guidance bonus by matched move cost, capped at 50% of the move's own cost, so matched straights no longer become nearly free. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2276 | -0.0024 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 275.0000 | -24.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 10.0000 | -1.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 368.6994 | -318.8006 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 120.0000 | -29.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 15.0000 | -8.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 19.0000 | -13.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 18.0000 | -12.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1861 | -0.0182 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 4.0027 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4747.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 2854.9698 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 11537.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 12027.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 9988.0000 | - | +## Step 42 reverted + +Measured on 2026-04-02T00:19:47-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted the scaled guidance bonus after the no-warm-start example_07 canary regressed in runtime, nodes, and congestion work without any validity gain. +- Restored the accepted fixed-bonus Step 35 guidance behavior. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2339 | +0.0039 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1895 | -0.0148 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5410 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 43 guidance counters by move type + +Measured on 2026-04-02T00:24:23-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Split guidance matches and total applied bonus by move type so the next selective-guidance slice can target the dominant match class on the no-warm-start canary. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2329 | +0.0030 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | - | 3.0000 | - | +| example_05_orientation_stress | guidance_match_moves_bend90 | - | 8.0000 | - | +| example_05_orientation_stress | guidance_match_moves_sbend | - | 0.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | - | 187.5000 | - | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | - | 500.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1899 | -0.0143 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5484 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 50 terminal repair rejected + +Measured on 2026-04-02T10:18:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Added a bounded terminal repair phase for reached-but-colliding nets after best-snapshot restoration, with up to two sequential repair sweeps and completed nets treated as hard blockers during repair reroutes. +- The slice stayed within the node and congestion guardrails, but it did not improve the only important outcome: the no-warm-start `example_07` canary remained at `2/10/10`. +- Rejected the slice and reverted the code. The tree was restored to the accepted fixed-guidance branch without refreshing `docs/performance.md` or `docs/performance_baseline.json`. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2400 | +0.0125 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | terminal_repair_sweeps | - | 0.0000 | - | +| example_05_orientation_stress | terminal_repair_nets_considered | - | 0.0000 | - | +| example_05_orientation_stress | terminal_repair_nets_accepted | - | 0.0000 | - | +| example_05_orientation_stress | terminal_repair_nets_rejected | - | 0.0000 | - | +| example_05_orientation_stress | terminal_repair_completed_promotions | - | 0.0000 | - | +| example_05_orientation_stress | repair_frozen_net_prunes | - | 0.0000 | - | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1969 | +0.0062 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | terminal_repair_sweeps | - | 0.0000 | - | +| example_07_large_scale_routing | terminal_repair_nets_considered | - | 0.0000 | - | +| example_07_large_scale_routing | terminal_repair_nets_accepted | - | 0.0000 | - | +| example_07_large_scale_routing | terminal_repair_nets_rejected | - | 0.0000 | - | +| example_07_large_scale_routing | terminal_repair_completed_promotions | - | 0.0000 | - | +| example_07_large_scale_routing | repair_frozen_net_prunes | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 6.5413 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 56.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 10691.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4805.0000 | - | +| example_07_large_scale_routing_no_warm_start | terminal_repair_sweeps | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | terminal_repair_nets_considered | - | 16.0000 | - | +| example_07_large_scale_routing_no_warm_start | terminal_repair_nets_accepted | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | terminal_repair_nets_rejected | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | terminal_repair_completed_promotions | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | repair_frozen_net_prunes | - | 300.0000 | - | +## Step 50 rejected + +Measured on 2026-04-02T10:02:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried per-net incumbent acceptance with a second accepted-state reverify after each full candidate iteration. +- Rejected the slice immediately: the no-warm-start `example_07` canary stayed at `2/10/10` valid but regressed to `179.0440s`, `8` iterations, `80` nets routed, `234493` expanded nodes, and `255901` congestion checks. +- Reverted the code and restored the prior accepted fixed-guidance state without refreshing the committed baseline artifacts. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2406 | +0.0132 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | iteration_candidate_results_accepted | - | 5.0000 | - | +| example_05_orientation_stress | iteration_incumbent_results_kept | - | 1.0000 | - | +| example_05_orientation_stress | iteration_accept_reverify_calls | - | 2.0000 | - | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.2029 | +0.0122 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | iteration_candidate_results_accepted | - | 10.0000 | - | +| example_07_large_scale_routing | iteration_incumbent_results_kept | - | 0.0000 | - | +| example_07_large_scale_routing | iteration_accept_reverify_calls | - | 1.0000 | - | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 179.0440 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 8.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 80.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 234493.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 255901.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_candidate_results_accepted | - | 30.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_incumbent_results_kept | - | 50.0000 | - | +| example_07_large_scale_routing_no_warm_start | iteration_accept_reverify_calls | - | 8.0000 | - | +## Step 44 selective straight guidance + +Measured on 2026-04-02T00:25:53-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Keep full reroute guidance bonus for bend90 and sbend matches, but cut straight-match bonus in half. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2361 | +0.0061 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | - | 3.0000 | - | +| example_05_orientation_stress | guidance_match_moves_bend90 | - | 8.0000 | - | +| example_05_orientation_stress | guidance_match_moves_sbend | - | 0.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 593.7500 | -93.7500 | +| example_05_orientation_stress | guidance_bonus_applied_straight | - | 93.7500 | - | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | - | 500.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1912 | -0.0131 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 4.0413 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4747.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 4687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 1625.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3721.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 10835.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 11093.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 9328.0000 | - | +## Step 45 selective bend guidance + +Measured on 2026-04-02T00:27:37-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Restore full straight guidance, but cut bend90-match bonus in half while keeping sbend matches at full bonus. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2211 | -0.0089 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 272.0000 | -27.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 10.0000 | -1.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | - | 3.0000 | - | +| example_05_orientation_stress | guidance_match_moves_bend90 | - | 7.0000 | - | +| example_05_orientation_stress | guidance_match_moves_sbend | - | 0.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 406.2500 | -281.2500 | +| example_05_orientation_stress | guidance_bonus_applied_straight | - | 187.5000 | - | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | - | 218.7500 | - | +| example_05_orientation_stress | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 108.0000 | -41.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 15.0000 | -8.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 19.0000 | -13.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 18.0000 | -12.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1873 | -0.0170 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5628 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4320.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 4968.7500 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 1343.7500 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3594.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 10245.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10859.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8944.0000 | - | +## Step 45 reverted + +Measured on 2026-04-02T00:29:03-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted bend-selective guidance after the no-warm-start canary still regressed in nodes and congestion work without any validity gain. +- Restored the accepted fixed-bonus guidance state while keeping the move-type counters. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2300 | 0.2387 | +0.0088 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | - | 3.0000 | - | +| example_05_orientation_stress | guidance_match_moves_bend90 | - | 8.0000 | - | +| example_05_orientation_stress | guidance_match_moves_sbend | - | 0.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | - | 187.5000 | - | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | - | 500.0000 | - | +| example_05_orientation_stress | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2043 | 0.1906 | -0.0137 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_match_moves_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | - | 0.0000 | - | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | - | 0.0000 | - | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5968 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 46 later-iteration guidance decay + +Measured on 2026-04-02T00:38:26-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Keep the first reroute guidance bonus unchanged, then halve the guidance bonus on iteration 2 and later reroutes. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2316 | +0.0041 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1923 | +0.0016 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 4.1601 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4850.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 4093.7500 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 2125.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 1718.7500 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3917.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 11398.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 11839.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 9870.0000 | - | +## Step 46 reverted + +Measured on 2026-04-02T00:39:37-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted later-iteration guidance decay after the no-warm-start canary regressed in runtime, nodes, and congestion work with no validity gain. +- Restored the accepted fixed-bonus guidance state while keeping the move-type counters. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2379 | +0.0105 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.2109 | +0.0202 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.6956 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 47 conflict-aware guidance decay + +Measured on 2026-04-02T00:44:37-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reduce reroute guidance bonus only when the net's own prior reached-target path still had dynamic collisions; keep full bonus for already-clean prior paths. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2439 | +0.0164 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 286.0000 | -13.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 10.0000 | -1.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 7.0000 | -1.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 500.0000 | -187.5000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 171.8750 | -15.6250 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 328.1250 | -171.8750 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 130.0000 | -19.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 15.0000 | -8.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 19.0000 | -13.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 18.0000 | -12.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1986 | +0.0079 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 4.0364 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4421.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 4890.6250 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 2515.6250 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2015.6250 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 359.3750 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3565.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 10202.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10558.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8814.0000 | - | +## Step 47 reverted + +Measured on 2026-04-02T00:45:52-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted conflict-aware guidance decay after the no-warm-start canary still regressed in runtime, nodes, and congestion work without any validity gain. +- Restored the accepted fixed-bonus guidance state while keeping the move-type counters. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2330 | +0.0056 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1878 | -0.0029 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5878 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 48 best reverified guidance source + +Measured on 2026-04-02T08:41:26-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Prefer the best reverified per-net path snapshot as the reroute guidance seed, falling back to the most recent reached-target path only when no best snapshot exists. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2347 | +0.0073 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1934 | +0.0027 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5687 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 49 per-net best reverified guidance + +Measured on 2026-04-02T08:43:09-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Track the best reverified reached-target path per net, ranked by dynamic collisions, total collisions, then total length, and prefer that path as reroute guidance. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2432 | +0.0158 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1923 | +0.0016 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 4.7028 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 5664.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 95.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 49.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 5937.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3062.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2500.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3406.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9679.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10059.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8361.0000 | - | +## Step 49 reverted + +Measured on 2026-04-02T08:44:09-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Reverted per-net best reverified guidance after the no-warm-start canary regressed sharply in runtime and nodes without any validity gain. +- Restored the accepted fixed-bonus guidance state while keeping the move-type counters. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2367 | +0.0093 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves | 11.0000 | 11.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_straight | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_bend90 | 8.0000 | 8.0000 | +0.0000 | +| example_05_orientation_stress | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied | 687.5000 | 687.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_straight | 187.5000 | 187.5000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_bend90 | 500.0000 | 500.0000 | +0.0000 | +| example_05_orientation_stress | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_nets | 23.0000 | 23.0000 | +0.0000 | +| example_05_orientation_stress | congestion_candidate_ids | 32.0000 | 32.0000 | +0.0000 | +| example_05_orientation_stress | congestion_exact_pair_checks | 30.0000 | 30.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1904 | -0.0003 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_match_moves_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_straight | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_bend90 | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | guidance_bonus_applied_sbend | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_nets | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_candidate_ids | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_exact_pair_checks | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.6154 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves | - | 101.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_straight | - | 52.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_bend90 | - | 43.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_match_moves_sbend | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied | - | 6312.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_straight | - | 3250.0000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_bend90 | - | 2687.5000 | - | +| example_07_large_scale_routing_no_warm_start | guidance_bonus_applied_sbend | - | 375.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_nets | - | 9604.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_candidate_ids | - | 10010.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_exact_pair_checks | - | 8312.0000 | - | +## Step 51 conflict trace instrumentation + +Measured on 2026-04-02T11:20:41-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Added opt-in conflict-trace capture and a separate recorder script without changing routing policy. +- The no-warm-start canary still finishes 2/10/10 with 4 iterations; tracing shows the same conflict structure at iteration, restored-best, and final stages. +- Recurring hotspot edges cluster in two groups: net_00-net_03 and net_06-net_09. +- Recurring component pairs also repeat across all traced stages, which points to stable geometric bottlenecks rather than seed noise. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2338 | +0.0064 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1896 | -0.0011 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5423 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 52 prefix-preserving hotspot repair rejected + +Measured on 2026-04-02T11:45:37-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried a bounded post-loop hotspot repair that preserved clean prefixes and rerouted only conflicted suffixes. +- The no-warm-start canary stayed at 2/10/10 and the repair counters showed zero accepted repairs, so the slice did not move correctness. +- The experiment temporarily raised the canary to 4258 nodes with one repair pass, two clusters attempted, and eight nets considered before it was reverted. +- The tree is restored to the accepted fixed-guidance state: example_07_no_warm_start is back at 2 valid, 10 reached, 4250 nodes, and 3388 congestion checks. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2384 | +0.0110 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1909 | +0.0002 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5590 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 53 cluster repack repair rejected + +Measured on 2026-04-02T12:04:21-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried bounded post-loop clique reroute against frozen outside geometry. +- No-warm-start canary stayed 2/10/10. +- Final conflict edges improved 12 -> 10, but correctness did not improve and search cost rose materially. +- Tree restored to the accepted fixed-guidance state after revert. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2364 | +0.0089 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1915 | +0.0008 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5998 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 54 hotspot keep-out repair rejected + +Measured on 2026-04-02T12:22:14-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried bounded post-loop clique reroute with temporary hotspot keep-out obstacles and frozen outside geometry. +- The no-warm-start canary failed a bounded probe: a single-scenario diff run timed out at 20 seconds, far above the accepted ~3.5 second state. +- Because the slice was clearly losing on runtime before showing any correctness gain, it was reverted. +- Tree restored to the accepted fixed-guidance branch after revert. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2490 | +0.0216 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.2004 | +0.0097 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.8387 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 55 bounded portfolio rerun rejected + +Measured on 2026-04-02T12:50:58-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried a conservative top-level best-of-three portfolio for warm-start-disabled runs using alternate shuffle/order settings. +- The no-warm-start canary was unchanged at 2/10/10, 4250 nodes, and 3388 congestion checks. +- Because the added control-path complexity produced no correctness or cost improvement, the slice was reverted. +- Tree restored to the accepted fixed-guidance branch after revert. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2408 | +0.0133 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1953 | +0.0046 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.6741 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 56 frontier hotspot instrumentation + +Measured on 2026-04-02T13:00:47-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Added opt-in frontier_trace diagnostics and recorder tooling without changing routing behavior. +- No-warm-start canary stayed on the accepted 2/10/10, 4250-node, 3388-congestion-check shape. +- Frontier trace shows hotspot-adjacent prunes are dominated by cost (3412) and closed-set (626), with zero hard-collision prunes near the traced hotspots. +- Self-collision pruning is concentrated in net_02 (455 traced self-collision prunes); the other hotspot nets are mostly cost- and closed-set-limited. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2300 | +0.0026 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1891 | -0.0016 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.5576 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 57 frontier hotspot readout refinement + +Measured on 2026-04-02T13:11:47-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Extended frontier_trace reporting with per-net sampled reason x move_type and hotspot splits. +- net_02 samples split across hotspots 0 and 6 and include heavy straight self-collision pruning; sampled mix is 36 cost, 23 self-collision, 5 closed-set. +- net_03 and net_06 are almost entirely straight-move cost prunes at hotspot 0; their few traced bend90 samples are closed-set dominated, not geometry-blocked. +- The sampled cost prunes are overshoot moves well outside the 0..1000 board bounds, so the current cost bucket is largely boundary-rejection rather than local corridor pricing. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2275 | 0.2365 | +0.0090 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.1907 | 0.1927 | +0.0020 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 3.6435 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 4.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 40.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 4250.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 3388.0000 | - | +## Step 58 large no-warm straight-bound cap + +Measured on 2026-04-02T13:22:28-07:00. +Baseline: `/tmp/inire_pre_step58_baseline.json`. + +Findings: + +- Applied a board-boundary cap to straight candidate reach only for large warm-start-disabled multi-net runs. +- The no-warm-start example_07 canary improved from 2/10/10 to 6/10/10. +- Nodes fell from 4250 to 1764 and runtime fell to about 1.95s, while congestion checks rose modestly from 3388 to 4625 as the search converged through six lighter iterations. +- Warmed example_07 stayed 10/10/10 and example_05 kept identical routing counters and validity. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2348 | 0.2387 | +0.0038 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 286.0000 | 299.0000 | +13.0000 | +| example_05_orientation_stress | congestion_check_calls | 155.0000 | 149.0000 | -6.0000 | +| example_07_large_scale_routing | duration_s | 0.1945 | 0.1944 | -0.0000 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 1.9279 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 60.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1764.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4625.0000 | - | +## Step 59 pair-local exact-conflict repair rejected + +Measured on 2026-04-02T13:50:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried a bounded post-loop two-net repair portfolio on the final conflict pairs, with ranked vs reversed reroute order and optional exact-overlap keep-outs. +- The no-warm-start canary stayed at `6/10/10`; the repair phase accepted `0` whole-set improvements across `8` attempts on `2` pairs. +- Cost regressed sharply during the failed slice to about `9.56s`, `11126` expanded nodes, and `9087` congestion checks, with `4257` frozen-net hard prunes. +- The slice was reverted and the tree restored to the accepted straight-boundary-cap branch. + +## Step 60 trimmed exploratory straight heuristics rejected + +Measured on 2026-04-02T14:00:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried removing the generic `max_reach / 2` and `max_reach - 5` straight candidates only for the same large no-warm runs that use the accepted straight-boundary cap. +- The no-warm-start canary regressed from `6/10/10` to `2/10/10` and runtime climbed to about `26s`. +- Search work exploded to about `27619` expanded nodes and `53804` congestion checks, so the generic exploratory straight candidates are still necessary for the accepted branch. +- The slice was reverted and the tree restored to the accepted straight-boundary-cap branch. + +## Step 61 staggered bend-anchor diversification rejected + +Measured on 2026-04-02T14:10:00-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Tried adding deterministic staggered pre-bend straight candidates for odd-indexed nets only in the large no-warm regime to separate the remaining colliding pairs. +- The no-warm-start canary regressed from `6/10/10` to `3/10/10` and runtime rose to about `8.6s`. +- The heuristic fired heavily (`1051` staggered bend-anchor candidates and `785` staggered visible candidates) but search still got worse, with about `6724` expanded nodes and `21577` congestion checks. +- The slice was reverted and the tree restored to the accepted straight-boundary-cap branch. +## Step 62 pair-local scratch reroute accepted + +Measured on 2026-04-02T14:15:04-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Built a bounded post-loop pair-local reroute that treats all outside-pair nets as frozen static blockers in a scratch routing world. The no-warm-start example_07 canary improved from 6/10/10 to 9/10/10 with two accepted pair repairs and only 33 extra search expansions. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2591 | 0.2440 | -0.0151 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | pair_local_search_pairs_considered | - | 0.0000 | - | +| example_05_orientation_stress | pair_local_search_attempts | - | 0.0000 | - | +| example_05_orientation_stress | pair_local_search_accepts | - | 0.0000 | - | +| example_05_orientation_stress | pair_local_search_nodes_expanded | - | 0.0000 | - | +| example_07_large_scale_routing | duration_s | 0.2027 | 0.2029 | +0.0002 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | pair_local_search_pairs_considered | - | 0.0000 | - | +| example_07_large_scale_routing | pair_local_search_attempts | - | 0.0000 | - | +| example_07_large_scale_routing | pair_local_search_accepts | - | 0.0000 | - | +| example_07_large_scale_routing | pair_local_search_nodes_expanded | - | 0.0000 | - | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 2.1244 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 9.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 60.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1764.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4625.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_pairs_considered | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_attempts | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_accepts | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_nodes_expanded | - | 33.0000 | - | +## Step 63 pair-local static obstacle cloning fix + +Measured on 2026-04-02T14:24:40-07:00. +Baseline: `docs/performance_baseline.json`. + +Findings: + +- Fixed the scratch pair-local routing world to clone static obstacles from the live collision engine instead of the RoutingProblem wrapper. That removed the false-clean net_00 reroute and lifted the no-warm-start example_07 canary from 9/10/10 to 10/10/10 with the same main search workload. + +# Performance Baseline Diff + +| Scenario | Metric | Baseline | Current | Delta | +| :-- | :-- | --: | --: | --: | +| example_05_orientation_stress | duration_s | 0.2361 | 0.2495 | +0.0134 | +| example_05_orientation_stress | valid_results | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | reached_targets | 3.0000 | 3.0000 | +0.0000 | +| example_05_orientation_stress | route_iterations | 2.0000 | 2.0000 | +0.0000 | +| example_05_orientation_stress | nets_routed | 6.0000 | 6.0000 | +0.0000 | +| example_05_orientation_stress | nodes_expanded | 299.0000 | 299.0000 | +0.0000 | +| example_05_orientation_stress | congestion_check_calls | 149.0000 | 149.0000 | +0.0000 | +| example_05_orientation_stress | pair_local_search_pairs_considered | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | pair_local_search_attempts | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | pair_local_search_accepts | 0.0000 | 0.0000 | +0.0000 | +| example_05_orientation_stress | pair_local_search_nodes_expanded | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | duration_s | 0.2025 | 0.2059 | +0.0034 | +| example_07_large_scale_routing | valid_results | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | reached_targets | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | route_iterations | 1.0000 | 1.0000 | +0.0000 | +| example_07_large_scale_routing | nets_routed | 10.0000 | 10.0000 | +0.0000 | +| example_07_large_scale_routing | nodes_expanded | 78.0000 | 78.0000 | +0.0000 | +| example_07_large_scale_routing | congestion_check_calls | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | pair_local_search_pairs_considered | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | pair_local_search_attempts | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | pair_local_search_accepts | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing | pair_local_search_nodes_expanded | 0.0000 | 0.0000 | +0.0000 | +| example_07_large_scale_routing_no_warm_start | duration_s | - | 2.1497 | - | +| example_07_large_scale_routing_no_warm_start | valid_results | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | reached_targets | - | 10.0000 | - | +| example_07_large_scale_routing_no_warm_start | route_iterations | - | 6.0000 | - | +| example_07_large_scale_routing_no_warm_start | nets_routed | - | 60.0000 | - | +| example_07_large_scale_routing_no_warm_start | nodes_expanded | - | 1764.0000 | - | +| example_07_large_scale_routing_no_warm_start | congestion_check_calls | - | 4625.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_pairs_considered | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_attempts | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_accepts | - | 2.0000 | - | +| example_07_large_scale_routing_no_warm_start | pair_local_search_nodes_expanded | - | 68.0000 | - | + +## Step 64 seed-43 iteration-trace diagnosis + +Measured on 2026-04-02T16:11:39-07:00. + +Findings: + +- Added `capture_iteration_trace` plus `scripts/record_iteration_trace.py` and tracked the first `seed 42` vs `seed 43` no-warm comparison in `docs/iteration_trace.json` and `docs/iteration_trace.md`. +- The pathological `seed 43` basin is not front-loaded. It matches the solved `seed 42` path through iteration `5`, then falls into three extra iterations with only `4` completed nets and `4` conflict edges. +- The late blowup is concentrated in two nets, not the whole routing set: `net_06` contributes `31604` attributed nodes and `83752` congestion checks, while `net_03` contributes `27532` nodes and `75019` congestion checks. +- This points the next optimization work at late-iteration reroute behavior for a small subset of nets rather than another global congestion or pair-local-search change. + +## Step 65 stop after fully reached two-edge plateau + +Measured on 2026-04-02T16:21:02-07:00. + +Findings: + +- Added a narrow late-iteration stop rule: once every net already reaches target and the best snapshot is down to the final `<=2` dynamic-conflict-edge basin, stop after the first no-improvement iteration and hand off to bounded pair-local repair. +- The solved seed-42 no-warm canary improved from `6` to `5` negotiated-congestion iterations and dropped from about `1764` to `1303` nodes and from `4625` to `2921` congestion checks, while staying `10/10/10`. +- The former seed-43 pathological basin collapsed from about `50s`, `61259` nodes, and `165223` congestion checks to about `2.53s`, `1691` nodes, and `4330` congestion checks, still finishing `10/10/10`. +- Guardrails held unchanged: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3`. + +## Step 66 reroute only current conflict nets in late all-reached phase + +Measured on 2026-04-02T16:46:00-07:00. + +Findings: + +- Once all nets already reach target and the live conflict graph is down to `<=3` edges, the next negotiated iteration now reroutes only the currently conflicting nets instead of all nets. +- The solved seed-42 no-warm canary stayed `10/10/10` and improved from `50` routed nets / `1303` nodes / `2921` congestion checks to `44` routed nets / `1258` nodes / `2736` congestion checks. +- The seed-43 no-warm canary stayed `10/10/10` and improved from `60` routed nets / `1691` nodes / `4330` congestion checks to `46` routed nets / `1582` nodes / `3881` congestion checks. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` while trimming slightly to `5` routed nets, `297` nodes, and `146` congestion checks. + +## Step 67 route lighter late conflict nets first + +Measured on 2026-04-02T17:16:54-07:00. + +Findings: + +- Kept the late all-reached conflict-set reroute, but now order those subset reroutes by the previous iteration's attributed work ascending so the lighter partner nets settle first and the heavier nets route later against a more stable late-phase context. +- The solved seed-42 no-warm canary stayed effectively flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks. +- The seed-43 no-warm canary stayed `10/10/10` and improved slightly from `1582` nodes / `3881` congestion checks to `1576` nodes / `3836` congestion checks. +- The remaining late-phase hotspot is still concentrated in `net_03` and `net_06`, especially the final 4-net iteration in the seed-43 trace. + +## Step 68 pre-pair frontier diagnostics kept, scratch reroute rejected + +Measured on 2026-04-02T17:57:48-07:00. + +Findings: + +- Kept a new opt-in `capture_pre_pair_frontier_trace` surface plus `scripts/record_pre_pair_frontier_trace.py`, and tracked the first seed-42 vs seed-43 artifacts in `docs/pre_pair_frontier_trace.json` and `docs/pre_pair_frontier_trace.md`. +- The final unresolved subset iteration is now explicit: seed `42` captures iteration `4` with routed nets `net_07`, `net_06`, `net_00`, `net_01`; seed `43` captures iteration `5` with routed nets `net_07`, `net_02`, `net_06`, `net_03`. +- The seed-43 heavy-net concentration is confirmed by the new trace: `net_03` and `net_06` account for most of the last unresolved iteration's work, and the hotspot-adjacent sampled prunes in that basin are closed-set dominated rather than hard-collision dominated. +- I also measured a bounded pre-pair scratch reroute for the two heaviest traced nets, but rejected it: it added runtime, produced `0` accepted repairs, and left the solved canaries at the same `1258 / 2736` and `1576 / 3836` node/check totals after revert. + +## Step 69 cap heavy late-phase reroutes with incumbent fallback + +Measured on 2026-04-02T18:20:00-07:00. + +Findings: + +- In the final all-reached 4-net subset iteration, the router now caps only the heavy reroute endpoints whose previous-iteration attributed work is already pathological, and falls back to their incumbent reached-target paths if the capped reroute does not finish cleanly. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves from `1576` nodes / `3836` congestion checks to `1459` nodes / `3455` congestion checks, with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity. + +## Step 70 tighten late-phase cap from 128 to 64 + +Measured on 2026-04-02T18:33:00-07:00. + +Findings: + +- Tightened the bounded heavy-net late-phase reroute cap from `128` nodes to `64`, keeping the same incumbent fallback behavior and the same heavy-net selection rule. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1459` nodes / `3455` congestion checks to `1331` nodes / `3012` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity. + +## Step 71 tighten late-phase cap from 64 to 32 after cap sweep + +Measured on 2026-04-02T18:43:00-07:00. + +Findings: + +- Ran a cap sweep across `32`, `48`, `64`, `96`, `128`, and uncapped behavior for the two no-warm seeds. The winner was `32`: it preserved both `10/10/10` canaries and gave the best seed-43 node/check totals while leaving seed-42 flat. +- Landed that tighter cap in the router. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1331` nodes / `3012` congestion checks to `1267` nodes / `2813` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. + +## Step 72 tighten late-phase cap from 32 to 1 after floor sweep + +Measured on 2026-04-02T18:45:00-07:00. + +Findings: + +- Extended the cap sweep below `32` and found the same pattern continued all the way down to `1`: seed-42 stayed flat because the cap never fires there, while seed-43 kept getting cheaper and still converged through the same incumbent-fallback path. +- Landed the tightest safe setting, `1`, so late pathological reroutes now act as a minimal probe before immediately falling back to the incumbent reached-target path if they do not finish cleanly. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1267` nodes / `2813` congestion checks to `1205` nodes / `2548` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. + +## Step 73 skip capped late-phase reroutes and carry the incumbent directly + +Measured on 2026-04-02T18:52:00-07:00. + +Findings: + +- Characterization showed the two capped late seed-43 reroutes were pure churn even at a `1`-node cap: they always fell back to the incumbent reached-target path and pair-local repair still resolved the final pairs. +- Moved that behavior into `_route_net_once()` directly: when a late heavy reroute is already capped and has a reached-target incumbent fallback, the router now reinstalls the incumbent immediately instead of calling `route_astar()` for a doomed probe. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1205` nodes / `2548` congestion checks to `1203` nodes / `2530` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. diff --git a/docs/pair_local_characterization.json b/docs/pair_local_characterization.json new file mode 100644 index 0000000..160ca48 --- /dev/null +++ b/docs/pair_local_characterization.json @@ -0,0 +1,2108 @@ +{ + "cases": [ + { + "duration_s": 0.5461949198506773, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 674, + "congestion_candidate_ids": 1105, + "congestion_candidate_nets": 1045, + "congestion_candidate_precheck_hits": 266, + "congestion_candidate_precheck_misses": 416, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 674, + "congestion_exact_pair_checks": 952, + "congestion_grid_net_cache_hits": 287, + "congestion_grid_net_cache_misses": 755, + "congestion_grid_span_cache_hits": 251, + "congestion_grid_span_cache_misses": 375, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 308, + "congestion_net_envelope_cache_misses": 807, + "congestion_presence_cache_hits": 282, + "congestion_presence_cache_misses": 475, + "congestion_presence_skips": 75, + "danger_map_cache_hits": 3159, + "danger_map_cache_misses": 1632, + "danger_map_lookup_calls": 4791, + "danger_map_query_calls": 1632, + "danger_map_total_ns": 46195989, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 459, + "move_cache_abs_misses": 2216, + "move_cache_rel_hits": 2062, + "move_cache_rel_misses": 154, + "moves_added": 1597, + "moves_generated": 2675, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 500, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 174, + "pruned_cost": 904, + "pruned_hard_collision": 0, + "ray_cast_calls": 1780, + "ray_cast_calls_expand_forward": 476, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1300, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 86, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2501, + "score_component_total_ns": 52377581, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 444, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 296, + "verify_dynamic_exact_pair_checks": 320, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 476, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 0, + "seed": 41, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 0.5255939338821918, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 674, + "congestion_candidate_ids": 1105, + "congestion_candidate_nets": 1045, + "congestion_candidate_precheck_hits": 266, + "congestion_candidate_precheck_misses": 416, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 674, + "congestion_exact_pair_checks": 952, + "congestion_grid_net_cache_hits": 287, + "congestion_grid_net_cache_misses": 755, + "congestion_grid_span_cache_hits": 251, + "congestion_grid_span_cache_misses": 375, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 308, + "congestion_net_envelope_cache_misses": 807, + "congestion_presence_cache_hits": 282, + "congestion_presence_cache_misses": 475, + "congestion_presence_skips": 75, + "danger_map_cache_hits": 3159, + "danger_map_cache_misses": 1632, + "danger_map_lookup_calls": 4791, + "danger_map_query_calls": 1632, + "danger_map_total_ns": 42685332, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 459, + "move_cache_abs_misses": 2216, + "move_cache_rel_hits": 2062, + "move_cache_rel_misses": 154, + "moves_added": 1597, + "moves_generated": 2675, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 500, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 174, + "pruned_cost": 904, + "pruned_hard_collision": 0, + "ray_cast_calls": 1780, + "ray_cast_calls_expand_forward": 476, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1300, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 86, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2501, + "score_component_total_ns": 48631281, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 444, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 296, + "verify_dynamic_exact_pair_checks": 320, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 476, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 1, + "seed": 41, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 0.5240974249318242, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 683, + "congestion_candidate_ids": 1119, + "congestion_candidate_nets": 1059, + "congestion_candidate_precheck_hits": 271, + "congestion_candidate_precheck_misses": 420, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 683, + "congestion_exact_pair_checks": 974, + "congestion_grid_net_cache_hits": 294, + "congestion_grid_net_cache_misses": 761, + "congestion_grid_span_cache_hits": 257, + "congestion_grid_span_cache_misses": 378, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 315, + "congestion_net_envelope_cache_misses": 813, + "congestion_presence_cache_hits": 287, + "congestion_presence_cache_misses": 479, + "congestion_presence_skips": 75, + "danger_map_cache_hits": 3186, + "danger_map_cache_misses": 1632, + "danger_map_lookup_calls": 4818, + "danger_map_query_calls": 1632, + "danger_map_total_ns": 42252659, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 472, + "move_cache_abs_misses": 2216, + "move_cache_rel_hits": 2062, + "move_cache_rel_misses": 154, + "moves_added": 1606, + "moves_generated": 2688, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 503, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 174, + "pruned_cost": 908, + "pruned_hard_collision": 0, + "ray_cast_calls": 1783, + "ray_cast_calls_expand_forward": 479, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1300, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 86, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2514, + "score_component_total_ns": 48076710, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 457, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 292, + "verify_dynamic_exact_pair_checks": 324, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 479, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 0, + "seed": 42, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 0.547705102013424, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 683, + "congestion_candidate_ids": 1119, + "congestion_candidate_nets": 1059, + "congestion_candidate_precheck_hits": 271, + "congestion_candidate_precheck_misses": 420, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 683, + "congestion_exact_pair_checks": 974, + "congestion_grid_net_cache_hits": 294, + "congestion_grid_net_cache_misses": 761, + "congestion_grid_span_cache_hits": 257, + "congestion_grid_span_cache_misses": 378, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 315, + "congestion_net_envelope_cache_misses": 813, + "congestion_presence_cache_hits": 287, + "congestion_presence_cache_misses": 479, + "congestion_presence_skips": 75, + "danger_map_cache_hits": 3186, + "danger_map_cache_misses": 1632, + "danger_map_lookup_calls": 4818, + "danger_map_query_calls": 1632, + "danger_map_total_ns": 43365736, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 472, + "move_cache_abs_misses": 2216, + "move_cache_rel_hits": 2062, + "move_cache_rel_misses": 154, + "moves_added": 1606, + "moves_generated": 2688, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 503, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 174, + "pruned_cost": 908, + "pruned_hard_collision": 0, + "ray_cast_calls": 1783, + "ray_cast_calls_expand_forward": 479, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1300, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 86, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2514, + "score_component_total_ns": 49498429, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 457, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 292, + "verify_dynamic_exact_pair_checks": 324, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 479, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 1, + "seed": 42, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 0.5199470021761954, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 654, + "congestion_candidate_ids": 1048, + "congestion_candidate_nets": 995, + "congestion_candidate_precheck_hits": 250, + "congestion_candidate_precheck_misses": 412, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 654, + "congestion_exact_pair_checks": 915, + "congestion_grid_net_cache_hits": 272, + "congestion_grid_net_cache_misses": 746, + "congestion_grid_span_cache_hits": 234, + "congestion_grid_span_cache_misses": 372, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 293, + "congestion_net_envelope_cache_misses": 796, + "congestion_presence_cache_hits": 261, + "congestion_presence_cache_misses": 466, + "congestion_presence_skips": 65, + "danger_map_cache_hits": 3103, + "danger_map_cache_misses": 1598, + "danger_map_lookup_calls": 4701, + "danger_map_query_calls": 1598, + "danger_map_total_ns": 41631872, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 451, + "move_cache_abs_misses": 2192, + "move_cache_rel_hits": 2038, + "move_cache_rel_misses": 154, + "moves_added": 1567, + "moves_generated": 2643, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 493, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 172, + "pruned_cost": 904, + "pruned_hard_collision": 0, + "ray_cast_calls": 1763, + "ray_cast_calls_expand_forward": 469, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1290, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 80, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2471, + "score_component_total_ns": 47519774, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 436, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 293, + "verify_dynamic_exact_pair_checks": 317, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 469, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 0, + "seed": 43, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 0.5160259250551462, + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 654, + "congestion_candidate_ids": 1048, + "congestion_candidate_nets": 995, + "congestion_candidate_precheck_hits": 250, + "congestion_candidate_precheck_misses": 412, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 654, + "congestion_exact_pair_checks": 915, + "congestion_grid_net_cache_hits": 272, + "congestion_grid_net_cache_misses": 746, + "congestion_grid_span_cache_hits": 234, + "congestion_grid_span_cache_misses": 372, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 293, + "congestion_net_envelope_cache_misses": 796, + "congestion_presence_cache_hits": 261, + "congestion_presence_cache_misses": 466, + "congestion_presence_skips": 65, + "danger_map_cache_hits": 3103, + "danger_map_cache_misses": 1598, + "danger_map_lookup_calls": 4701, + "danger_map_query_calls": 1598, + "danger_map_total_ns": 41135911, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 131, + "dynamic_path_objects_removed": 106, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 3312.5, + "guidance_bonus_applied_bend90": 1187.5, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 1750.0, + "guidance_match_moves": 53, + "guidance_match_moves_bend90": 19, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 28, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 18, + "iteration_conflicting_nets": 21, + "iteration_reverified_nets": 24, + "iteration_reverify_calls": 4, + "move_cache_abs_hits": 451, + "move_cache_abs_misses": 2192, + "move_cache_rel_hits": 2038, + "move_cache_rel_misses": 154, + "moves_added": 1567, + "moves_generated": 2643, + "nets_carried_forward": 0, + "nets_reached_target": 24, + "nets_routed": 24, + "nodes_expanded": 493, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 172, + "pruned_cost": 904, + "pruned_hard_collision": 0, + "ray_cast_calls": 1763, + "ray_cast_calls_expand_forward": 469, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 1290, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 4, + "ray_cast_candidate_bounds": 80, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 4, + "score_component_calls": 2471, + "score_component_total_ns": 47047772, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 436, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 293, + "verify_dynamic_exact_pair_checks": 317, + "verify_path_report_calls": 60, + "verify_static_buffer_ops": 256, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 4, + "visibility_tangent_candidate_ray_tests": 4, + "visibility_tangent_candidate_scans": 469, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 6, + "repeat": 1, + "seed": 43, + "summary": { + "reached_targets": 6, + "total_results": 6, + "valid_results": 1 + } + }, + { + "duration_s": 1.8817617469467223, + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4313, + "congestion_candidate_ids": 7976, + "congestion_candidate_nets": 7240, + "congestion_candidate_precheck_hits": 2327, + "congestion_candidate_precheck_misses": 2036, + "congestion_candidate_precheck_skips": 19, + "congestion_check_calls": 4313, + "congestion_exact_pair_checks": 6217, + "congestion_grid_net_cache_hits": 2369, + "congestion_grid_net_cache_misses": 3714, + "congestion_grid_span_cache_hits": 2055, + "congestion_grid_span_cache_misses": 1837, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2527, + "congestion_net_envelope_cache_misses": 3896, + "congestion_presence_cache_hits": 2955, + "congestion_presence_cache_misses": 2536, + "congestion_presence_skips": 1126, + "danger_map_cache_hits": 16937, + "danger_map_cache_misses": 6391, + "danger_map_lookup_calls": 23328, + "danger_map_query_calls": 6391, + "danger_map_total_ns": 192446411, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 450, + "dynamic_path_objects_removed": 416, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 10812.5, + "guidance_bonus_applied_bend90": 3125.0, + "guidance_bonus_applied_sbend": 750.0, + "guidance_bonus_applied_straight": 6937.5, + "guidance_match_moves": 173, + "guidance_match_moves_bend90": 50, + "guidance_match_moves_sbend": 12, + "guidance_match_moves_straight": 111, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 33, + "iteration_conflicting_nets": 38, + "iteration_reverified_nets": 56, + "iteration_reverify_calls": 7, + "move_cache_abs_hits": 2874, + "move_cache_abs_misses": 5680, + "move_cache_rel_hits": 4979, + "move_cache_rel_misses": 701, + "moves_added": 7771, + "moves_generated": 8554, + "nets_carried_forward": 0, + "nets_reached_target": 56, + "nets_routed": 56, + "nodes_expanded": 1558, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 38, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 461, + "pruned_cost": 322, + "pruned_hard_collision": 0, + "ray_cast_calls": 4910, + "ray_cast_calls_expand_forward": 1502, + "ray_cast_calls_expand_snap": 39, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3363, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 237, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 7, + "score_component_calls": 8098, + "score_component_total_ns": 218008329, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2788, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1244, + "verify_dynamic_exact_pair_checks": 464, + "verify_path_report_calls": 144, + "verify_static_buffer_ops": 732, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1502, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 0, + "seed": 41, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 1.8618457240518183, + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4313, + "congestion_candidate_ids": 7976, + "congestion_candidate_nets": 7240, + "congestion_candidate_precheck_hits": 2327, + "congestion_candidate_precheck_misses": 2036, + "congestion_candidate_precheck_skips": 19, + "congestion_check_calls": 4313, + "congestion_exact_pair_checks": 6217, + "congestion_grid_net_cache_hits": 2369, + "congestion_grid_net_cache_misses": 3714, + "congestion_grid_span_cache_hits": 2055, + "congestion_grid_span_cache_misses": 1837, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2527, + "congestion_net_envelope_cache_misses": 3896, + "congestion_presence_cache_hits": 2955, + "congestion_presence_cache_misses": 2536, + "congestion_presence_skips": 1126, + "danger_map_cache_hits": 16937, + "danger_map_cache_misses": 6391, + "danger_map_lookup_calls": 23328, + "danger_map_query_calls": 6391, + "danger_map_total_ns": 172147520, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 450, + "dynamic_path_objects_removed": 416, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 10812.5, + "guidance_bonus_applied_bend90": 3125.0, + "guidance_bonus_applied_sbend": 750.0, + "guidance_bonus_applied_straight": 6937.5, + "guidance_match_moves": 173, + "guidance_match_moves_bend90": 50, + "guidance_match_moves_sbend": 12, + "guidance_match_moves_straight": 111, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 33, + "iteration_conflicting_nets": 38, + "iteration_reverified_nets": 56, + "iteration_reverify_calls": 7, + "move_cache_abs_hits": 2874, + "move_cache_abs_misses": 5680, + "move_cache_rel_hits": 4979, + "move_cache_rel_misses": 701, + "moves_added": 7771, + "moves_generated": 8554, + "nets_carried_forward": 0, + "nets_reached_target": 56, + "nets_routed": 56, + "nodes_expanded": 1558, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 38, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 461, + "pruned_cost": 322, + "pruned_hard_collision": 0, + "ray_cast_calls": 4910, + "ray_cast_calls_expand_forward": 1502, + "ray_cast_calls_expand_snap": 39, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3363, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 237, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 7, + "score_component_calls": 8098, + "score_component_total_ns": 197998309, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2788, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1244, + "verify_dynamic_exact_pair_checks": 464, + "verify_path_report_calls": 144, + "verify_static_buffer_ops": 732, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1502, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 1, + "seed": 41, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 1.4850338851101696, + "metrics": { + "congestion_cache_hits": 37, + "congestion_cache_misses": 3799, + "congestion_candidate_ids": 7050, + "congestion_candidate_nets": 6725, + "congestion_candidate_precheck_hits": 1823, + "congestion_candidate_precheck_misses": 2035, + "congestion_candidate_precheck_skips": 22, + "congestion_check_calls": 3799, + "congestion_exact_pair_checks": 5684, + "congestion_grid_net_cache_hits": 1836, + "congestion_grid_net_cache_misses": 3693, + "congestion_grid_span_cache_hits": 1591, + "congestion_grid_span_cache_misses": 1804, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1992, + "congestion_net_envelope_cache_misses": 3901, + "congestion_presence_cache_hits": 2321, + "congestion_presence_cache_misses": 2616, + "congestion_presence_skips": 1079, + "danger_map_cache_hits": 16398, + "danger_map_cache_misses": 5274, + "danger_map_lookup_calls": 21672, + "danger_map_query_calls": 5274, + "danger_map_total_ns": 142151688, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 435, + "dynamic_path_objects_removed": 400, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 14187.5, + "guidance_bonus_applied_bend90": 4187.5, + "guidance_bonus_applied_sbend": 875.0, + "guidance_bonus_applied_straight": 9125.0, + "guidance_match_moves": 227, + "guidance_match_moves_bend90": 67, + "guidance_match_moves_sbend": 14, + "guidance_match_moves_straight": 146, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 31, + "iteration_conflicting_nets": 35, + "iteration_reverified_nets": 64, + "iteration_reverify_calls": 8, + "move_cache_abs_hits": 3181, + "move_cache_abs_misses": 4740, + "move_cache_rel_hits": 4171, + "move_cache_rel_misses": 569, + "moves_added": 7220, + "moves_generated": 7921, + "nets_carried_forward": 0, + "nets_reached_target": 64, + "nets_routed": 64, + "nodes_expanded": 1440, + "pair_local_search_accepts": 1, + "pair_local_search_attempts": 1, + "pair_local_search_nodes_expanded": 19, + "pair_local_search_pairs_considered": 1, + "path_cost_calls": 0, + "pruned_closed_set": 407, + "pruned_cost": 294, + "pruned_hard_collision": 0, + "ray_cast_calls": 4201, + "ray_cast_calls_expand_forward": 1376, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2773, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 187, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 8, + "score_component_calls": 7518, + "score_component_total_ns": 164933249, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 3079, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1295, + "verify_dynamic_exact_pair_checks": 415, + "verify_path_report_calls": 152, + "verify_static_buffer_ops": 755, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1376, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 0, + "seed": 42, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 1.4636062050703913, + "metrics": { + "congestion_cache_hits": 37, + "congestion_cache_misses": 3799, + "congestion_candidate_ids": 7050, + "congestion_candidate_nets": 6725, + "congestion_candidate_precheck_hits": 1823, + "congestion_candidate_precheck_misses": 2035, + "congestion_candidate_precheck_skips": 22, + "congestion_check_calls": 3799, + "congestion_exact_pair_checks": 5684, + "congestion_grid_net_cache_hits": 1836, + "congestion_grid_net_cache_misses": 3693, + "congestion_grid_span_cache_hits": 1591, + "congestion_grid_span_cache_misses": 1804, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1992, + "congestion_net_envelope_cache_misses": 3901, + "congestion_presence_cache_hits": 2321, + "congestion_presence_cache_misses": 2616, + "congestion_presence_skips": 1079, + "danger_map_cache_hits": 16398, + "danger_map_cache_misses": 5274, + "danger_map_lookup_calls": 21672, + "danger_map_query_calls": 5274, + "danger_map_total_ns": 142480011, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 435, + "dynamic_path_objects_removed": 400, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 14187.5, + "guidance_bonus_applied_bend90": 4187.5, + "guidance_bonus_applied_sbend": 875.0, + "guidance_bonus_applied_straight": 9125.0, + "guidance_match_moves": 227, + "guidance_match_moves_bend90": 67, + "guidance_match_moves_sbend": 14, + "guidance_match_moves_straight": 146, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 31, + "iteration_conflicting_nets": 35, + "iteration_reverified_nets": 64, + "iteration_reverify_calls": 8, + "move_cache_abs_hits": 3181, + "move_cache_abs_misses": 4740, + "move_cache_rel_hits": 4171, + "move_cache_rel_misses": 569, + "moves_added": 7220, + "moves_generated": 7921, + "nets_carried_forward": 0, + "nets_reached_target": 64, + "nets_routed": 64, + "nodes_expanded": 1440, + "pair_local_search_accepts": 1, + "pair_local_search_attempts": 1, + "pair_local_search_nodes_expanded": 19, + "pair_local_search_pairs_considered": 1, + "path_cost_calls": 0, + "pruned_closed_set": 407, + "pruned_cost": 294, + "pruned_hard_collision": 0, + "ray_cast_calls": 4201, + "ray_cast_calls_expand_forward": 1376, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2773, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 187, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 8, + "score_component_calls": 7518, + "score_component_total_ns": 165301696, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 3079, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1295, + "verify_dynamic_exact_pair_checks": 415, + "verify_path_report_calls": 152, + "verify_static_buffer_ops": 755, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1376, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 1, + "seed": 42, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 1.065187250962481, + "metrics": { + "congestion_cache_hits": 10, + "congestion_cache_misses": 1844, + "congestion_candidate_ids": 3568, + "congestion_candidate_nets": 3390, + "congestion_candidate_precheck_hits": 757, + "congestion_candidate_precheck_misses": 1113, + "congestion_candidate_precheck_skips": 16, + "congestion_check_calls": 1844, + "congestion_exact_pair_checks": 2796, + "congestion_grid_net_cache_hits": 772, + "congestion_grid_net_cache_misses": 2030, + "congestion_grid_span_cache_hits": 678, + "congestion_grid_span_cache_misses": 1016, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 838, + "congestion_net_envelope_cache_misses": 2135, + "congestion_presence_cache_hits": 850, + "congestion_presence_cache_misses": 1349, + "congestion_presence_skips": 329, + "danger_map_cache_hits": 9125, + "danger_map_cache_misses": 4333, + "danger_map_lookup_calls": 13458, + "danger_map_query_calls": 4333, + "danger_map_total_ns": 126835582, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 256, + "dynamic_path_objects_removed": 218, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 5625.0, + "guidance_bonus_applied_bend90": 2000.0, + "guidance_bonus_applied_sbend": 500.0, + "guidance_bonus_applied_straight": 3125.0, + "guidance_match_moves": 90, + "guidance_match_moves_bend90": 32, + "guidance_match_moves_sbend": 8, + "guidance_match_moves_straight": 50, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 27, + "iteration_conflicting_nets": 26, + "iteration_reverified_nets": 40, + "iteration_reverify_calls": 5, + "move_cache_abs_hits": 1094, + "move_cache_abs_misses": 3955, + "move_cache_rel_hits": 3487, + "move_cache_rel_misses": 468, + "moves_added": 4475, + "moves_generated": 5049, + "nets_carried_forward": 0, + "nets_reached_target": 40, + "nets_routed": 40, + "nodes_expanded": 939, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 319, + "pruned_cost": 255, + "pruned_hard_collision": 0, + "ray_cast_calls": 3212, + "ray_cast_calls_expand_forward": 899, + "ray_cast_calls_expand_snap": 20, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2287, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 140, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 5, + "score_component_calls": 4741, + "score_component_total_ns": 141872767, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1057, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 751, + "verify_dynamic_exact_pair_checks": 363, + "verify_path_report_calls": 96, + "verify_static_buffer_ops": 436, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 899, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 0, + "seed": 43, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 1.0501909658778459, + "metrics": { + "congestion_cache_hits": 10, + "congestion_cache_misses": 1844, + "congestion_candidate_ids": 3568, + "congestion_candidate_nets": 3390, + "congestion_candidate_precheck_hits": 757, + "congestion_candidate_precheck_misses": 1113, + "congestion_candidate_precheck_skips": 16, + "congestion_check_calls": 1844, + "congestion_exact_pair_checks": 2796, + "congestion_grid_net_cache_hits": 772, + "congestion_grid_net_cache_misses": 2030, + "congestion_grid_span_cache_hits": 678, + "congestion_grid_span_cache_misses": 1016, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 838, + "congestion_net_envelope_cache_misses": 2135, + "congestion_presence_cache_hits": 850, + "congestion_presence_cache_misses": 1349, + "congestion_presence_skips": 329, + "danger_map_cache_hits": 9125, + "danger_map_cache_misses": 4333, + "danger_map_lookup_calls": 13458, + "danger_map_query_calls": 4333, + "danger_map_total_ns": 112792178, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 256, + "dynamic_path_objects_removed": 218, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 5625.0, + "guidance_bonus_applied_bend90": 2000.0, + "guidance_bonus_applied_sbend": 500.0, + "guidance_bonus_applied_straight": 3125.0, + "guidance_match_moves": 90, + "guidance_match_moves_bend90": 32, + "guidance_match_moves_sbend": 8, + "guidance_match_moves_straight": 50, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 27, + "iteration_conflicting_nets": 26, + "iteration_reverified_nets": 40, + "iteration_reverify_calls": 5, + "move_cache_abs_hits": 1094, + "move_cache_abs_misses": 3955, + "move_cache_rel_hits": 3487, + "move_cache_rel_misses": 468, + "moves_added": 4475, + "moves_generated": 5049, + "nets_carried_forward": 0, + "nets_reached_target": 40, + "nets_routed": 40, + "nodes_expanded": 939, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 319, + "pruned_cost": 255, + "pruned_hard_collision": 0, + "ray_cast_calls": 3212, + "ray_cast_calls_expand_forward": 899, + "ray_cast_calls_expand_snap": 20, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2287, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 140, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 5, + "score_component_calls": 4741, + "score_component_total_ns": 127782063, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1057, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 751, + "verify_dynamic_exact_pair_checks": 363, + "verify_path_report_calls": 96, + "verify_static_buffer_ops": 436, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 899, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 8, + "repeat": 1, + "seed": 43, + "summary": { + "reached_targets": 8, + "total_results": 8, + "valid_results": 8 + } + }, + { + "duration_s": 2.861715276958421, + "metrics": { + "congestion_cache_hits": 181, + "congestion_cache_misses": 6208, + "congestion_candidate_ids": 14969, + "congestion_candidate_nets": 14477, + "congestion_candidate_precheck_hits": 4046, + "congestion_candidate_precheck_misses": 2450, + "congestion_candidate_precheck_skips": 107, + "congestion_check_calls": 6208, + "congestion_exact_pair_checks": 12175, + "congestion_grid_net_cache_hits": 3733, + "congestion_grid_net_cache_misses": 4394, + "congestion_grid_span_cache_hits": 3484, + "congestion_grid_span_cache_misses": 2188, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 4110, + "congestion_net_envelope_cache_misses": 4614, + "congestion_presence_cache_hits": 4474, + "congestion_presence_cache_misses": 2857, + "congestion_presence_skips": 835, + "danger_map_cache_hits": 21570, + "danger_map_cache_misses": 8484, + "danger_map_lookup_calls": 30054, + "danger_map_query_calls": 8484, + "danger_map_total_ns": 229555102, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 470, + "dynamic_path_objects_removed": 422, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 12937.5, + "guidance_bonus_applied_bend90": 4187.5, + "guidance_bonus_applied_sbend": 750.0, + "guidance_bonus_applied_straight": 8000.0, + "guidance_match_moves": 207, + "guidance_match_moves_bend90": 67, + "guidance_match_moves_sbend": 12, + "guidance_match_moves_straight": 128, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 43, + "iteration_conflicting_nets": 43, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 3714, + "move_cache_abs_misses": 7526, + "move_cache_rel_hits": 6718, + "move_cache_rel_misses": 808, + "moves_added": 9950, + "moves_generated": 11240, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 2223, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 41, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 566, + "pruned_cost": 724, + "pruned_hard_collision": 0, + "ray_cast_calls": 6546, + "ray_cast_calls_expand_forward": 2163, + "ray_cast_calls_expand_snap": 43, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 4334, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 438, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 10742, + "score_component_total_ns": 262906657, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 3614, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1812, + "verify_dynamic_exact_pair_checks": 590, + "verify_path_report_calls": 160, + "verify_static_buffer_ops": 771, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 2163, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 0, + "seed": 41, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 8 + } + }, + { + "duration_s": 2.8281878910493106, + "metrics": { + "congestion_cache_hits": 181, + "congestion_cache_misses": 6208, + "congestion_candidate_ids": 14969, + "congestion_candidate_nets": 14477, + "congestion_candidate_precheck_hits": 4046, + "congestion_candidate_precheck_misses": 2450, + "congestion_candidate_precheck_skips": 107, + "congestion_check_calls": 6208, + "congestion_exact_pair_checks": 12175, + "congestion_grid_net_cache_hits": 3733, + "congestion_grid_net_cache_misses": 4394, + "congestion_grid_span_cache_hits": 3484, + "congestion_grid_span_cache_misses": 2188, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 4110, + "congestion_net_envelope_cache_misses": 4614, + "congestion_presence_cache_hits": 4474, + "congestion_presence_cache_misses": 2857, + "congestion_presence_skips": 835, + "danger_map_cache_hits": 21570, + "danger_map_cache_misses": 8484, + "danger_map_lookup_calls": 30054, + "danger_map_query_calls": 8484, + "danger_map_total_ns": 223711146, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 470, + "dynamic_path_objects_removed": 422, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 12937.5, + "guidance_bonus_applied_bend90": 4187.5, + "guidance_bonus_applied_sbend": 750.0, + "guidance_bonus_applied_straight": 8000.0, + "guidance_match_moves": 207, + "guidance_match_moves_bend90": 67, + "guidance_match_moves_sbend": 12, + "guidance_match_moves_straight": 128, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 43, + "iteration_conflicting_nets": 43, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 3714, + "move_cache_abs_misses": 7526, + "move_cache_rel_hits": 6718, + "move_cache_rel_misses": 808, + "moves_added": 9950, + "moves_generated": 11240, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 2223, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 41, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 566, + "pruned_cost": 724, + "pruned_hard_collision": 0, + "ray_cast_calls": 6546, + "ray_cast_calls_expand_forward": 2163, + "ray_cast_calls_expand_snap": 43, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 4334, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 438, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 8, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 10742, + "score_component_total_ns": 257092830, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 3614, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1812, + "verify_dynamic_exact_pair_checks": 590, + "verify_path_report_calls": 160, + "verify_static_buffer_ops": 771, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 2163, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 1, + "seed": 41, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 8 + } + }, + { + "duration_s": 2.035589807201177, + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4625, + "congestion_candidate_ids": 9924, + "congestion_candidate_nets": 9979, + "congestion_candidate_precheck_hits": 2562, + "congestion_candidate_precheck_misses": 2165, + "congestion_candidate_precheck_skips": 71, + "congestion_check_calls": 4625, + "congestion_exact_pair_checks": 8122, + "congestion_grid_net_cache_hits": 2457, + "congestion_grid_net_cache_misses": 3942, + "congestion_grid_span_cache_hits": 2283, + "congestion_grid_span_cache_misses": 1948, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2673, + "congestion_net_envelope_cache_misses": 4139, + "congestion_presence_cache_hits": 2858, + "congestion_presence_cache_misses": 2556, + "congestion_presence_skips": 687, + "danger_map_cache_hits": 16878, + "danger_map_cache_misses": 7425, + "danger_map_lookup_calls": 24303, + "danger_map_query_calls": 7425, + "danger_map_total_ns": 197092109, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 471, + "dynamic_path_objects_removed": 423, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 11000.0, + "guidance_bonus_applied_bend90": 3500.0, + "guidance_bonus_applied_sbend": 625.0, + "guidance_bonus_applied_straight": 6875.0, + "guidance_match_moves": 176, + "guidance_match_moves_bend90": 56, + "guidance_match_moves_sbend": 10, + "guidance_match_moves_straight": 110, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 36, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 2559, + "move_cache_abs_misses": 6494, + "move_cache_rel_hits": 5872, + "move_cache_rel_misses": 622, + "moves_added": 8081, + "moves_generated": 9053, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 1764, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 439, + "pruned_cost": 533, + "pruned_hard_collision": 0, + "ray_cast_calls": 5477, + "ray_cast_calls_expand_forward": 1704, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3721, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 305, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 8634, + "score_component_total_ns": 223430894, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2482, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1756, + "verify_dynamic_exact_pair_checks": 510, + "verify_path_report_calls": 160, + "verify_static_buffer_ops": 751, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1704, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 0, + "seed": 42, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "duration_s": 2.0052393269725144, + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4625, + "congestion_candidate_ids": 9924, + "congestion_candidate_nets": 9979, + "congestion_candidate_precheck_hits": 2562, + "congestion_candidate_precheck_misses": 2165, + "congestion_candidate_precheck_skips": 71, + "congestion_check_calls": 4625, + "congestion_exact_pair_checks": 8122, + "congestion_grid_net_cache_hits": 2457, + "congestion_grid_net_cache_misses": 3942, + "congestion_grid_span_cache_hits": 2283, + "congestion_grid_span_cache_misses": 1948, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2673, + "congestion_net_envelope_cache_misses": 4139, + "congestion_presence_cache_hits": 2858, + "congestion_presence_cache_misses": 2556, + "congestion_presence_skips": 687, + "danger_map_cache_hits": 16878, + "danger_map_cache_misses": 7425, + "danger_map_lookup_calls": 24303, + "danger_map_query_calls": 7425, + "danger_map_total_ns": 196041391, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 471, + "dynamic_path_objects_removed": 423, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 11000.0, + "guidance_bonus_applied_bend90": 3500.0, + "guidance_bonus_applied_sbend": 625.0, + "guidance_bonus_applied_straight": 6875.0, + "guidance_match_moves": 176, + "guidance_match_moves_bend90": 56, + "guidance_match_moves_sbend": 10, + "guidance_match_moves_straight": 110, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 36, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 2559, + "move_cache_abs_misses": 6494, + "move_cache_rel_hits": 5872, + "move_cache_rel_misses": 622, + "moves_added": 8081, + "moves_generated": 9053, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 1764, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 439, + "pruned_cost": 533, + "pruned_hard_collision": 0, + "ray_cast_calls": 5477, + "ray_cast_calls_expand_forward": 1704, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3721, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 305, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 8634, + "score_component_total_ns": 222749506, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2482, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1756, + "verify_dynamic_exact_pair_checks": 510, + "verify_path_report_calls": 160, + "verify_static_buffer_ops": 751, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1704, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 1, + "seed": 42, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "duration_s": 50.18633775901981, + "metrics": { + "congestion_cache_hits": 1713, + "congestion_cache_misses": 165223, + "congestion_candidate_ids": 312161, + "congestion_candidate_nets": 300960, + "congestion_candidate_precheck_hits": 159843, + "congestion_candidate_precheck_misses": 7810, + "congestion_candidate_precheck_skips": 717, + "congestion_check_calls": 165223, + "congestion_exact_pair_checks": 249697, + "congestion_grid_net_cache_hits": 152369, + "congestion_grid_net_cache_misses": 13481, + "congestion_grid_span_cache_hits": 144618, + "congestion_grid_span_cache_misses": 6885, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 160549, + "congestion_net_envelope_cache_misses": 14205, + "congestion_presence_cache_hits": 186369, + "congestion_presence_cache_misses": 9324, + "congestion_presence_skips": 27588, + "danger_map_cache_hits": 453553, + "danger_map_cache_misses": 140744, + "danger_map_lookup_calls": 594297, + "danger_map_query_calls": 140744, + "danger_map_total_ns": 4360608337, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 683, + "dynamic_path_objects_removed": 637, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 23437.5, + "guidance_bonus_applied_bend90": 8437.5, + "guidance_bonus_applied_sbend": 1000.0, + "guidance_bonus_applied_straight": 14000.0, + "guidance_match_moves": 375, + "guidance_match_moves_bend90": 135, + "guidance_match_moves_sbend": 16, + "guidance_match_moves_straight": 224, + "hard_collision_cache_hits": 859, + "iteration_conflict_edges": 51, + "iteration_conflicting_nets": 57, + "iteration_reverified_nets": 90, + "iteration_reverify_calls": 9, + "move_cache_abs_hits": 140734, + "move_cache_abs_misses": 183345, + "move_cache_rel_hits": 180943, + "move_cache_rel_misses": 2402, + "moves_added": 197062, + "moves_generated": 324079, + "nets_carried_forward": 0, + "nets_reached_target": 90, + "nets_routed": 90, + "nodes_expanded": 61259, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 38, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 49632, + "pruned_cost": 5502, + "pruned_hard_collision": 2123, + "ray_cast_calls": 166977, + "ray_cast_calls_expand_forward": 61169, + "ray_cast_calls_expand_snap": 735, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 103738, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 1335, + "ray_cast_candidate_bounds": 11494, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 9, + "score_component_calls": 203601, + "score_component_total_ns": 5065858683, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 120231, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2653, + "verify_dynamic_exact_pair_checks": 714, + "verify_path_report_calls": 220, + "verify_static_buffer_ops": 1185, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 1405, + "visibility_tangent_candidate_ray_tests": 1335, + "visibility_tangent_candidate_scans": 61169, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 0, + "seed": 43, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "duration_s": 50.40192667697556, + "metrics": { + "congestion_cache_hits": 1713, + "congestion_cache_misses": 165223, + "congestion_candidate_ids": 312161, + "congestion_candidate_nets": 300960, + "congestion_candidate_precheck_hits": 159843, + "congestion_candidate_precheck_misses": 7810, + "congestion_candidate_precheck_skips": 717, + "congestion_check_calls": 165223, + "congestion_exact_pair_checks": 249697, + "congestion_grid_net_cache_hits": 152369, + "congestion_grid_net_cache_misses": 13481, + "congestion_grid_span_cache_hits": 144618, + "congestion_grid_span_cache_misses": 6885, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 160549, + "congestion_net_envelope_cache_misses": 14205, + "congestion_presence_cache_hits": 186369, + "congestion_presence_cache_misses": 9324, + "congestion_presence_skips": 27588, + "danger_map_cache_hits": 453553, + "danger_map_cache_misses": 140744, + "danger_map_lookup_calls": 594297, + "danger_map_query_calls": 140744, + "danger_map_total_ns": 4932050184, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 683, + "dynamic_path_objects_removed": 637, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 23437.5, + "guidance_bonus_applied_bend90": 8437.5, + "guidance_bonus_applied_sbend": 1000.0, + "guidance_bonus_applied_straight": 14000.0, + "guidance_match_moves": 375, + "guidance_match_moves_bend90": 135, + "guidance_match_moves_sbend": 16, + "guidance_match_moves_straight": 224, + "hard_collision_cache_hits": 859, + "iteration_conflict_edges": 51, + "iteration_conflicting_nets": 57, + "iteration_reverified_nets": 90, + "iteration_reverify_calls": 9, + "move_cache_abs_hits": 140734, + "move_cache_abs_misses": 183345, + "move_cache_rel_hits": 180943, + "move_cache_rel_misses": 2402, + "moves_added": 197062, + "moves_generated": 324079, + "nets_carried_forward": 0, + "nets_reached_target": 90, + "nets_routed": 90, + "nodes_expanded": 61259, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 38, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 49632, + "pruned_cost": 5502, + "pruned_hard_collision": 2123, + "ray_cast_calls": 166977, + "ray_cast_calls_expand_forward": 61169, + "ray_cast_calls_expand_snap": 735, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 103738, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 1335, + "ray_cast_candidate_bounds": 11494, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 9, + "score_component_calls": 203601, + "score_component_total_ns": 5635427939, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 120231, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2653, + "verify_dynamic_exact_pair_checks": 714, + "verify_path_report_calls": 220, + "verify_static_buffer_ops": 1185, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 1405, + "visibility_tangent_candidate_ray_tests": 1335, + "visibility_tangent_candidate_scans": 61169, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "num_nets": 10, + "repeat": 1, + "seed": 43, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ], + "generated_at": "2026-04-02T15:53:29-07:00", + "generator": "scripts/characterize_pair_local_search.py", + "grid": { + "num_nets": [ + 6, + 8, + 10 + ], + "repeats": 2, + "seeds": [ + 41, + 42, + 43 + ] + }, + "recommended_smoke_scenario": null +} diff --git a/docs/pair_local_characterization.md b/docs/pair_local_characterization.md new file mode 100644 index 0000000..753cbdf --- /dev/null +++ b/docs/pair_local_characterization.md @@ -0,0 +1,30 @@ +# Pair-Local Search Characterization + +Generated at 2026-04-02T15:53:29-07:00 by `scripts/characterize_pair_local_search.py`. + +Grid: `num_nets=[6, 8, 10]`, `seed=[41, 42, 43]`, repeats=2. + +| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks | +| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: | +| 6 | 41 | 0 | 0.5462 | 1 | 6 | 0 | 0 | 0 | 500 | 674 | +| 6 | 41 | 1 | 0.5256 | 1 | 6 | 0 | 0 | 0 | 500 | 674 | +| 6 | 42 | 0 | 0.5241 | 1 | 6 | 0 | 0 | 0 | 503 | 683 | +| 6 | 42 | 1 | 0.5477 | 1 | 6 | 0 | 0 | 0 | 503 | 683 | +| 6 | 43 | 0 | 0.5199 | 1 | 6 | 0 | 0 | 0 | 493 | 654 | +| 6 | 43 | 1 | 0.5160 | 1 | 6 | 0 | 0 | 0 | 493 | 654 | +| 8 | 41 | 0 | 1.8818 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 | +| 8 | 41 | 1 | 1.8618 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 | +| 8 | 42 | 0 | 1.4850 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 | +| 8 | 42 | 1 | 1.4636 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 | +| 8 | 43 | 0 | 1.0652 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 | +| 8 | 43 | 1 | 1.0502 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 | +| 10 | 41 | 0 | 2.8617 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 | +| 10 | 41 | 1 | 2.8282 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 | +| 10 | 42 | 0 | 2.0356 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 | +| 10 | 42 | 1 | 2.0052 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 | +| 10 | 43 | 0 | 50.1863 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 | +| 10 | 43 | 1 | 50.4019 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 | + +## Recommendation + +No smaller stable pair-local smoke scenario satisfied the rule `valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats. diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..3e801d9 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,27 @@ +# Performance Baseline + +Generated on 2026-04-02 by `scripts/record_performance_baseline.py`. + +The full machine-readable snapshot lives in `docs/performance_baseline.json`. +Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot. + +| 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.0037 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 5 | +| example_02_congestion_resolution | 0.3361 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 41 | +| example_03_locked_paths | 0.1877 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 18 | +| example_04_sbends_and_radii | 0.0269 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 10 | +| example_05_orientation_stress | 0.2311 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 20 | +| example_06_bend_collision_models | 0.1988 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 15 | +| example_07_large_scale_routing | 0.2088 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 50 | +| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 10 | +| example_09_unroutable_best_effort | 0.0057 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 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. +These counters are currently observational only and are not enforced as CI regression gates. + +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, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_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_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded, late_phase_capped_nets, late_phase_capped_fallbacks diff --git a/docs/performance_baseline.json b/docs/performance_baseline.json new file mode 100644 index 0000000..952600d --- /dev/null +++ b/docs/performance_baseline.json @@ -0,0 +1,1032 @@ +{ + "generated_on": "2026-04-02", + "generator": "scripts/record_performance_baseline.py", + "scenarios": [ + { + "duration_s": 0.003715757979080081, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 3, + "dynamic_path_objects_removed": 2, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 1, + "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 1, + "nets_routed": 1, + "nodes_expanded": 2, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 0, + "pruned_cost": 4, + "pruned_hard_collision": 0, + "ray_cast_calls": 10, + "ray_cast_calls_expand_forward": 1, + "ray_cast_calls_expand_snap": 1, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 8, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, + "ray_cast_candidate_bounds": 0, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 1, + "score_component_calls": 11, + "score_component_total_ns": 16864, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 0, + "static_safe_cache_hits": 1, + "static_tree_rebuilds": 0, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, + "verify_dynamic_exact_pair_checks": 0, + "verify_path_report_calls": 5, + "verify_static_buffer_ops": 0, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 0, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 1, + "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.33605348505079746, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 49, + "dynamic_path_objects_removed": 34, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 3, + "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 3, + "nets_routed": 3, + "nodes_expanded": 366, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 14, + "pruned_closed_set": 157, + "pruned_cost": 208, + "pruned_hard_collision": 380, + "ray_cast_calls": 1164, + "ray_cast_calls_expand_forward": 363, + "ray_cast_calls_expand_snap": 19, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 529, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 253, + "ray_cast_candidate_bounds": 913, + "ray_cast_exact_geometry_checks": 136, + "refine_path_calls": 3, + "refinement_candidate_side_extents": 26, + "refinement_candidates_accepted": 2, + "refinement_candidates_built": 26, + "refinement_candidates_verified": 26, + "refinement_dynamic_bounds_checked": 20, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 10, + "route_iterations": 1, + "score_component_calls": 976, + "score_component_total_ns": 1109505, + "static_net_tree_rebuilds": 3, + "static_raw_tree_rebuilds": 0, + "static_safe_cache_hits": 1, + "static_tree_rebuilds": 2, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 92, + "verify_dynamic_exact_pair_checks": 90, + "verify_path_report_calls": 41, + "verify_static_buffer_ops": 0, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 3, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 873, + "visibility_tangent_candidate_ray_tests": 253, + "visibility_tangent_candidate_scans": 363, + "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.18771230895072222, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 27, + "dynamic_path_objects_removed": 20, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 2, + "nets_routed": 2, + "nodes_expanded": 191, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 9, + "pruned_closed_set": 97, + "pruned_cost": 140, + "pruned_hard_collision": 181, + "ray_cast_calls": 657, + "ray_cast_calls_expand_forward": 189, + "ray_cast_calls_expand_snap": 8, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 407, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 53, + "ray_cast_candidate_bounds": 155, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 2, + "refinement_candidate_side_extents": 8, + "refinement_candidates_accepted": 1, + "refinement_candidates_built": 8, + "refinement_candidates_verified": 8, + "refinement_dynamic_bounds_checked": 2, + "refinement_static_bounds_checked": 2, + "refinement_windows_considered": 2, + "route_iterations": 2, + "score_component_calls": 504, + "score_component_total_ns": 546567, + "static_net_tree_rebuilds": 2, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 10, + "verify_dynamic_exact_pair_checks": 10, + "verify_path_report_calls": 18, + "verify_static_buffer_ops": 90, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 2, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 56, + "visibility_tangent_candidate_ray_tests": 53, + "visibility_tangent_candidate_scans": 189, + "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": 0.026945222169160843, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 21, + "dynamic_path_objects_removed": 14, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 2, + "nets_routed": 2, + "nodes_expanded": 15, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 2, + "pruned_cost": 25, + "pruned_hard_collision": 16, + "ray_cast_calls": 70, + "ray_cast_calls_expand_forward": 13, + "ray_cast_calls_expand_snap": 1, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 56, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, + "ray_cast_candidate_bounds": 4, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 2, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 1, + "score_component_calls": 90, + "score_component_total_ns": 97710, + "static_net_tree_rebuilds": 2, + "static_raw_tree_rebuilds": 0, + "static_safe_cache_hits": 1, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 12, + "verify_dynamic_exact_pair_checks": 0, + "verify_path_report_calls": 10, + "verify_static_buffer_ops": 0, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 2, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 50, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 13, + "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.23108969815075397, + "metrics": { + "congestion_cache_hits": 3, + "congestion_cache_misses": 146, + "congestion_candidate_ids": 32, + "congestion_candidate_nets": 23, + "congestion_candidate_precheck_hits": 129, + "congestion_candidate_precheck_misses": 20, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 146, + "congestion_exact_pair_checks": 30, + "congestion_grid_net_cache_hits": 16, + "congestion_grid_net_cache_misses": 26, + "congestion_grid_span_cache_hits": 15, + "congestion_grid_span_cache_misses": 7, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 127, + "congestion_net_envelope_cache_misses": 39, + "congestion_presence_cache_hits": 196, + "congestion_presence_cache_misses": 27, + "congestion_presence_skips": 74, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 48, + "dynamic_path_objects_removed": 36, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 562.5, + "guidance_bonus_applied_bend90": 500.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 62.5, + "guidance_match_moves": 9, + "guidance_match_moves_bend90": 8, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 1, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 1, + "iteration_conflicting_nets": 2, + "iteration_reverified_nets": 6, + "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, + "move_cache_abs_hits": 374, + "move_cache_abs_misses": 1306, + "move_cache_rel_hits": 1204, + "move_cache_rel_misses": 102, + "moves_added": 689, + "moves_generated": 1680, + "nets_carried_forward": 1, + "nets_reached_target": 5, + "nets_routed": 5, + "nodes_expanded": 297, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 2, + "pruned_closed_set": 159, + "pruned_cost": 533, + "pruned_hard_collision": 14, + "ray_cast_calls": 1274, + "ray_cast_calls_expand_forward": 292, + "ray_cast_calls_expand_snap": 2, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 971, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 9, + "ray_cast_candidate_bounds": 45, + "ray_cast_exact_geometry_checks": 43, + "refine_path_calls": 3, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 2, + "score_component_calls": 1234, + "score_component_total_ns": 1223569, + "static_net_tree_rebuilds": 3, + "static_raw_tree_rebuilds": 0, + "static_safe_cache_hits": 8, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 8, + "verify_dynamic_exact_pair_checks": 12, + "verify_path_report_calls": 20, + "verify_static_buffer_ops": 0, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 3, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 70, + "visibility_tangent_candidate_ray_tests": 9, + "visibility_tangent_candidate_scans": 292, + "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": 0.19879506202414632, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 1183, + "danger_map_cache_misses": 731, + "danger_map_lookup_calls": 1914, + "danger_map_query_calls": 731, + "danger_map_total_ns": 19050142, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 54, + "dynamic_path_objects_removed": 36, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 18, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 3, + "iteration_reverify_calls": 3, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, + "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_carried_forward": 0, + "nets_reached_target": 3, + "nets_routed": 3, + "nodes_expanded": 240, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 108, + "pruned_cost": 204, + "pruned_hard_collision": 85, + "ray_cast_calls": 682, + "ray_cast_calls_expand_forward": 237, + "ray_cast_calls_expand_snap": 3, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 408, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 34, + "ray_cast_candidate_bounds": 97, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 3, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 3, + "score_component_calls": 842, + "score_component_total_ns": 21353240, + "static_net_tree_rebuilds": 3, + "static_raw_tree_rebuilds": 3, + "static_safe_cache_hits": 141, + "static_tree_rebuilds": 3, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, + "verify_dynamic_exact_pair_checks": 0, + "verify_path_report_calls": 15, + "verify_static_buffer_ops": 90, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 3, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 84, + "visibility_tangent_candidate_ray_tests": 34, + "visibility_tangent_candidate_scans": 237, + "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": 0.20880168909206986, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 233, + "danger_map_cache_misses": 448, + "danger_map_lookup_calls": 681, + "danger_map_query_calls": 448, + "danger_map_total_ns": 11025527, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 132, + "dynamic_path_objects_removed": 88, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 10, + "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 10, + "nets_routed": 10, + "nodes_expanded": 78, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 20, + "pruned_cost": 64, + "pruned_hard_collision": 61, + "ray_cast_calls": 383, + "ray_cast_calls_expand_forward": 68, + "ray_cast_calls_expand_snap": 6, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 232, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 77, + "ray_cast_candidate_bounds": 683, + "ray_cast_exact_geometry_checks": 150, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 1, + "score_component_calls": 291, + "score_component_total_ns": 11875928, + "static_net_tree_rebuilds": 10, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 6, + "static_tree_rebuilds": 10, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 476, + "verify_dynamic_exact_pair_checks": 72, + "verify_path_report_calls": 50, + "verify_static_buffer_ops": 220, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 10, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 321, + "visibility_tangent_candidate_ray_tests": 77, + "visibility_tangent_candidate_scans": 68, + "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.017696003895252943, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, + "danger_map_query_calls": 0, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 18, + "dynamic_path_objects_removed": 12, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 2, + "nets_routed": 2, + "nodes_expanded": 18, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 6, + "pruned_cost": 16, + "pruned_hard_collision": 0, + "ray_cast_calls": 56, + "ray_cast_calls_expand_forward": 16, + "ray_cast_calls_expand_snap": 2, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 38, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, + "ray_cast_candidate_bounds": 0, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 2, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 2, + "score_component_calls": 72, + "score_component_total_ns": 87742, + "static_net_tree_rebuilds": 2, + "static_raw_tree_rebuilds": 0, + "static_safe_cache_hits": 2, + "static_tree_rebuilds": 0, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, + "verify_dynamic_exact_pair_checks": 0, + "verify_path_report_calls": 10, + "verify_static_buffer_ops": 0, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 2, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 0, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 16, + "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.005660973023623228, + "metrics": { + "congestion_cache_hits": 0, + "congestion_cache_misses": 0, + "congestion_candidate_ids": 0, + "congestion_candidate_nets": 0, + "congestion_candidate_precheck_hits": 0, + "congestion_candidate_precheck_misses": 0, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_grid_net_cache_hits": 0, + "congestion_grid_net_cache_misses": 0, + "congestion_grid_span_cache_hits": 0, + "congestion_grid_span_cache_misses": 0, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 0, + "congestion_net_envelope_cache_misses": 0, + "congestion_presence_cache_hits": 0, + "congestion_presence_cache_misses": 0, + "congestion_presence_skips": 0, + "danger_map_cache_hits": 10, + "danger_map_cache_misses": 20, + "danger_map_lookup_calls": 30, + "danger_map_query_calls": 20, + "danger_map_total_ns": 515133, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 2, + "dynamic_path_objects_removed": 1, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 0.0, + "guidance_bonus_applied_bend90": 0.0, + "guidance_bonus_applied_sbend": 0.0, + "guidance_bonus_applied_straight": 0.0, + "guidance_match_moves": 0, + "guidance_match_moves_bend90": 0, + "guidance_match_moves_sbend": 0, + "guidance_match_moves_straight": 0, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 0, + "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 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_carried_forward": 0, + "nets_reached_target": 0, + "nets_routed": 1, + "nodes_expanded": 3, + "pair_local_search_accepts": 0, + "pair_local_search_attempts": 0, + "pair_local_search_nodes_expanded": 0, + "pair_local_search_pairs_considered": 0, + "path_cost_calls": 0, + "pruned_closed_set": 0, + "pruned_cost": 4, + "pruned_hard_collision": 2, + "ray_cast_calls": 13, + "ray_cast_calls_expand_forward": 3, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 10, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, + "ray_cast_candidate_bounds": 5, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 0, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 1, + "score_component_calls": 14, + "score_component_total_ns": 554809, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 0, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, + "verify_dynamic_exact_pair_checks": 0, + "verify_path_report_calls": 1, + "verify_static_buffer_ops": 1, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 0, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 3, + "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 + } + ] +} diff --git a/docs/pre_pair_frontier_trace.json b/docs/pre_pair_frontier_trace.json new file mode 100644 index 0000000..42d83c6 --- /dev/null +++ b/docs/pre_pair_frontier_trace.json @@ -0,0 +1,1005 @@ +{ + "generated_at": "2026-04-02T18:51:01-07:00", + "generator": "scripts/record_pre_pair_frontier_trace.py", + "scenarios": [ + { + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 2736, + "congestion_candidate_ids": 5785, + "congestion_candidate_nets": 6163, + "congestion_candidate_precheck_hits": 1383, + "congestion_candidate_precheck_misses": 1418, + "congestion_candidate_precheck_skips": 34, + "congestion_check_calls": 2736, + "congestion_exact_pair_checks": 4780, + "congestion_grid_net_cache_hits": 1356, + "congestion_grid_net_cache_misses": 2608, + "congestion_grid_span_cache_hits": 1247, + "congestion_grid_span_cache_misses": 1308, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1452, + "congestion_net_envelope_cache_misses": 2720, + "congestion_presence_cache_hits": 1541, + "congestion_presence_cache_misses": 1642, + "congestion_presence_skips": 382, + "danger_map_cache_hits": 11547, + "danger_map_cache_misses": 6063, + "danger_map_lookup_calls": 17610, + "danger_map_query_calls": 6063, + "danger_map_total_ns": 171779571, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 399, + "dynamic_path_objects_removed": 351, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 6750.0, + "guidance_bonus_applied_bend90": 2250.0, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 4125.0, + "guidance_match_moves": 108, + "guidance_match_moves_bend90": 36, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 66, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 37, + "iteration_conflicting_nets": 32, + "iteration_reverified_nets": 50, + "iteration_reverify_calls": 5, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, + "move_cache_abs_hits": 1200, + "move_cache_abs_misses": 5338, + "move_cache_rel_hits": 4768, + "move_cache_rel_misses": 570, + "moves_added": 5853, + "moves_generated": 6538, + "nets_carried_forward": 6, + "nets_reached_target": 44, + "nets_routed": 44, + "nodes_expanded": 1258, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 374, + "pruned_cost": 311, + "pruned_hard_collision": 0, + "ray_cast_calls": 4310, + "ray_cast_calls_expand_forward": 1214, + "ray_cast_calls_expand_snap": 39, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3051, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 159, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 5, + "score_component_calls": 6181, + "score_component_total_ns": 192508906, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1170, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1822, + "verify_dynamic_exact_pair_checks": 504, + "verify_path_report_calls": 164, + "verify_static_buffer_ops": 779, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1214, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "pre_pair_frontier_trace": { + "conflict_edges": [ + [ + "net_01", + "net_02" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 4, + "nets": [ + { + "congestion_check_calls": 30, + "frontier": { + "hotspot_bounds": [ + [ + 827.6047906391041, + 482.9684848604278, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 555.0, + 916.0, + 598.0 + ] + ], + "net_id": "net_07", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 179, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947606, + 482.8851198636423, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 545.0, + 916.2379632934325, + 582.0 + ], + [ + 883.7620367065675, + 571.0, + 916.0, + 652.2850117535756 + ] + ], + "net_id": "net_06", + "pruned_closed_set": 5, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 510, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 46, + "pruned_closed_set": 7, + "pruned_cost": 15, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 43, + "frontier": { + "hotspot_bounds": [ + [ + 506.3396407947511, + 398.0, + 597.3906878342618, + 487.1148801363561 + ], + [ + 564.0, + 224.0, + 596.0, + 415.0 + ] + ], + "net_id": "net_00", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 80, + "frontier": { + "hotspot_bounds": [ + [ + 506.3396407947511, + 398.0, + 597.3906878342618, + 487.1148801363561 + ], + [ + 564.0, + 388.0, + 596.2379632934325, + 425.0 + ], + [ + 563.7620367065675, + 169.71498824645388, + 596.0, + 251.0 + ] + ], + "net_id": "net_01", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 18, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0 + } + ], + "routed_net_ids": [ + "net_07", + "net_06", + "net_00", + "net_01" + ] + }, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 2530, + "congestion_candidate_ids": 6173, + "congestion_candidate_nets": 5869, + "congestion_candidate_precheck_hits": 1152, + "congestion_candidate_precheck_misses": 1460, + "congestion_candidate_precheck_skips": 74, + "congestion_check_calls": 2530, + "congestion_exact_pair_checks": 4800, + "congestion_grid_net_cache_hits": 1192, + "congestion_grid_net_cache_misses": 2676, + "congestion_grid_span_cache_hits": 1065, + "congestion_grid_span_cache_misses": 1366, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1234, + "congestion_net_envelope_cache_misses": 2769, + "congestion_presence_cache_hits": 1302, + "congestion_presence_cache_misses": 1664, + "congestion_presence_skips": 354, + "danger_map_cache_hits": 11485, + "danger_map_cache_misses": 5474, + "danger_map_lookup_calls": 16959, + "danger_map_query_calls": 5474, + "danger_map_total_ns": 143896014, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 397, + "dynamic_path_objects_removed": 350, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 7562.5, + "guidance_bonus_applied_bend90": 2937.5, + "guidance_bonus_applied_sbend": 250.0, + "guidance_bonus_applied_straight": 4375.0, + "guidance_match_moves": 121, + "guidance_match_moves_bend90": 47, + "guidance_match_moves_sbend": 4, + "guidance_match_moves_straight": 70, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 39, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "late_phase_capped_fallbacks": 2, + "late_phase_capped_nets": 2, + "move_cache_abs_hits": 1304, + "move_cache_abs_misses": 4997, + "move_cache_rel_hits": 4419, + "move_cache_rel_misses": 578, + "moves_added": 5638, + "moves_generated": 6301, + "nets_carried_forward": 14, + "nets_reached_target": 44, + "nets_routed": 46, + "nodes_expanded": 1203, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 39, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 354, + "pruned_cost": 309, + "pruned_hard_collision": 0, + "ray_cast_calls": 4059, + "ray_cast_calls_expand_forward": 1159, + "ray_cast_calls_expand_snap": 13, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2881, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 170, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 5962, + "score_component_total_ns": 163113517, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1276, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1884, + "verify_dynamic_exact_pair_checks": 557, + "verify_path_report_calls": 174, + "verify_static_buffer_ops": 805, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1159, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start_seed43", + "pre_pair_frontier_trace": { + "conflict_edges": [ + [ + "net_02", + "net_03" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 5, + "nets": [ + { + "congestion_check_calls": 85, + "frontier": { + "hotspot_bounds": [ + [ + 827.6047906391041, + 482.9684848604278, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 555.0, + 916.0, + 598.0 + ] + ], + "net_id": "net_07", + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 520, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 520, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 520, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 520, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 16, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 86, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947511, + 418.0, + 917.3906878342618, + 507.1148801363561 + ], + [ + 884.0, + 402.0, + 916.0, + 435.0 + ] + ], + "net_id": "net_02", + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 470, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 470, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 470, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 17, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 0, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947606, + 482.8851198636423, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 545.0, + 916.2379632934325, + 582.0 + ], + [ + 883.7620367065675, + 571.0, + 916.0, + 652.2850117535756 + ] + ], + "net_id": "net_06", + "pruned_closed_set": 8, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 895, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 895, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 880, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 880, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 827, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 832, + 270 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 0, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947511, + 418.0, + 917.3906878342618, + 507.1148801363561 + ], + [ + 884.0, + 408.0, + 916.2379632934325, + 445.0 + ], + [ + 883.7620367065675, + 347.71498824645397, + 916.0, + 429.0 + ] + ], + "net_id": "net_03", + "pruned_closed_set": 12, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 480, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 480, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 480, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 480, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 895, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 895, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 880, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 880, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 896, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 896, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 891, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 891, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 886, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 886, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 882, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 882, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 163, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 158, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 267, + 90 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0 + } + ], + "routed_net_ids": [ + "net_07", + "net_02", + "net_06", + "net_03" + ] + }, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/pre_pair_frontier_trace.md b/docs/pre_pair_frontier_trace.md new file mode 100644 index 0000000..a760f82 --- /dev/null +++ b/docs/pre_pair_frontier_trace.md @@ -0,0 +1,48 @@ +# Pre-Pair Frontier Trace + +Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_pre_pair_frontier_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +Captured iteration: `4` + +Conflict edges: `(('net_01', 'net_02'), ('net_06', 'net_07'))` + +| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples | +| :-- | --: | --: | --: | --: | --: | :--: | --: | +| net_07 | 7 | 30 | 1 | 0 | 0 | yes | 0 | +| net_06 | 46 | 179 | 7 | 15 | 0 | yes | 5 | +| net_00 | 10 | 43 | 1 | 0 | 0 | yes | 0 | +| net_01 | 18 | 80 | 3 | 0 | 0 | yes | 0 | + +Frontier prune totals by reason: + +- `closed_set`: 5 +- `hard_collision`: 0 +- `self_collision`: 0 +- `cost`: 0 + +## example_07_large_scale_routing_no_warm_start_seed43 + +Results: 10 valid / 10 reached / 10 total. + +Captured iteration: `5` + +Conflict edges: `(('net_02', 'net_03'), ('net_06', 'net_07'))` + +| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples | +| :-- | --: | --: | --: | --: | --: | :--: | --: | +| net_07 | 16 | 85 | 3 | 0 | 0 | yes | 2 | +| net_02 | 17 | 86 | 4 | 0 | 0 | yes | 3 | +| net_06 | 0 | 0 | 0 | 0 | 0 | yes | 8 | +| net_03 | 0 | 0 | 0 | 0 | 0 | yes | 12 | + +Frontier prune totals by reason: + +- `closed_set`: 25 +- `hard_collision`: 0 +- `self_collision`: 0 +- `cost`: 0 + diff --git a/examples/01_simple_route.py b/examples/01_simple_route.py new file mode 100644 index 0000000..2f43065 --- /dev/null +++ b/examples/01_simple_route.py @@ -0,0 +1,32 @@ +from inire import NetSpec, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 01: Simple Route...") + + bounds = (0, 0, 100, 100) + netlist = { + "net1": (Port(10, 50, 0), Port(90, 50, 0)), + } + problem = RoutingProblem( + bounds=bounds, + nets=(NetSpec("net1", *netlist["net1"], width=2.0),), + ) + options = RoutingOptions(search=SearchOptions(bend_radii=(10.0,))) + + run = route(problem, options=options) + result = run.results_by_net["net1"] + if result.is_valid: + print("Success! Route found.") + print(f"Path collisions: {result.collisions}") + else: + print("Failed to find route.") + + fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist) + fig.savefig("examples/01_simple_route.png") + print("Saved plot to examples/01_simple_route.png") + + +if __name__ == "__main__": + main() diff --git a/examples/02_congestion_resolution.py b/examples/02_congestion_resolution.py new file mode 100644 index 0000000..f6b2751 --- /dev/null +++ b/examples/02_congestion_resolution.py @@ -0,0 +1,44 @@ +from inire import CongestionOptions, NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 02: Congestion Resolution (Triple Crossing)...") + + bounds = (0, 0, 100, 100) + netlist = { + "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), + "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), + "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), + } + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()), + ) + options = RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + sbend_radii=(10.0,), + greedy_h_weight=1.5, + ), + objective=ObjectiveWeights( + bend_penalty=50.0, + sbend_penalty=150.0, + ), + congestion=CongestionOptions(base_penalty=1000.0), + ) + + run = route(problem, options=options) + all_valid = all(result.is_valid for result in run.results_by_net.values()) + if all_valid: + print("Success! Congestion resolved for all nets.") + else: + print("Failed to resolve congestion for some nets.") + + fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist) + fig.savefig("examples/02_congestion_resolution.png") + print("Saved plot to examples/02_congestion_resolution.png") + + +if __name__ == "__main__": + main() diff --git a/examples/03_locked_paths.png b/examples/03_locked_paths.png new file mode 100644 index 0000000..687dad6 Binary files /dev/null and b/examples/03_locked_paths.png differ diff --git a/examples/03_locked_paths.py b/examples/03_locked_paths.py new file mode 100644 index 0000000..b00741f --- /dev/null +++ b/examples/03_locked_paths.py @@ -0,0 +1,45 @@ +from inire import NetSpec, Port, RoutingOptions, RoutingProblem, SearchOptions +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router._stack import build_routing_stack +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 03: Locked Paths...") + + bounds = (0, -50, 100, 50) + print("Routing initial net...") + stack = build_routing_stack( + problem=RoutingProblem( + bounds=bounds, + nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),), + ), + options=RoutingOptions(search=SearchOptions(bend_radii=(10.0,))), + ) + engine = stack.world + evaluator = stack.evaluator + results_a = stack.finder.route_all() + + print("Routing detour net around locked path...") + for polygon in results_a["netA"].locked_geometry: + engine.add_static_obstacle(polygon) + results_b = PathFinder( + AStarContext( + evaluator, + RoutingProblem( + bounds=bounds, + nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),), + ), + RoutingOptions(search=SearchOptions(bend_radii=(10.0,))), + ), + ).route_all() + + results = {**results_a, **results_b} + fig, ax = plot_routing_results(results, [], bounds) + fig.savefig("examples/03_locked_paths.png") + print("Saved plot to examples/03_locked_paths.png") + + +if __name__ == "__main__": + main() diff --git a/examples/04_sbends_and_radii.py b/examples/04_sbends_and_radii.py new file mode 100644 index 0000000..42eac9c --- /dev/null +++ b/examples/04_sbends_and_radii.py @@ -0,0 +1,41 @@ +from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 04: S-Bends and Multiple Radii...") + + bounds = (0, 0, 100, 100) + netlist = { + "sbend_only": (Port(10, 50, 0), Port(60, 55, 0)), + "multi_radii": (Port(10, 10, 0), Port(90, 90, 0)), + } + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()), + ) + options = RoutingOptions( + search=SearchOptions( + node_limit=50000, + bend_radii=(10.0, 30.0), + sbend_offsets=(5.0,), + ), + objective=ObjectiveWeights( + unit_length_cost=1.0, + bend_penalty=10.0, + sbend_penalty=20.0, + ), + ) + + run = route(problem, options=options) + for net_id, result in run.results_by_net.items(): + status = "Success" if result.is_valid else "Failed" + print(f"{net_id}: {status}, collisions={result.collisions}") + + fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist) + fig.savefig("examples/04_sbends_and_radii.png") + print("Saved plot to examples/04_sbends_and_radii.png") + + +if __name__ == "__main__": + main() diff --git a/examples/05_orientation_stress.png b/examples/05_orientation_stress.png new file mode 100644 index 0000000..b750c6a Binary files /dev/null and b/examples/05_orientation_stress.png differ diff --git a/examples/05_orientation_stress.py b/examples/05_orientation_stress.py new file mode 100644 index 0000000..eab3c0e --- /dev/null +++ b/examples/05_orientation_stress.py @@ -0,0 +1,35 @@ +from inire import NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 05: Orientation Stress Test...") + + bounds = (0, 0, 200, 200) + netlist = { + "u_turn": (Port(50, 50, 0), Port(50, 70, 180)), + "loop": (Port(100, 100, 90), Port(100, 80, 270)), + "zig_zag": (Port(20, 150, 0), Port(180, 150, 0)), + } + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()), + ) + options = RoutingOptions( + search=SearchOptions(bend_radii=(20.0,)), + objective=ObjectiveWeights(bend_penalty=50.0), + ) + + print("Routing complex orientation nets...") + run = route(problem, options=options) + for net_id, result in run.results_by_net.items(): + status = "Success" if result.is_valid else "Failed" + print(f" {net_id}: {status}") + + fig, _ax = plot_routing_results(run.results_by_net, [], bounds, netlist=netlist) + fig.savefig("examples/05_orientation_stress.png") + print("Saved plot to examples/05_orientation_stress.png") + + +if __name__ == "__main__": + main() diff --git a/examples/06_bend_collision_models.png b/examples/06_bend_collision_models.png new file mode 100644 index 0000000..178d952 Binary files /dev/null and b/examples/06_bend_collision_models.png differ diff --git a/examples/06_bend_collision_models.py b/examples/06_bend_collision_models.py new file mode 100644 index 0000000..7fe12aa --- /dev/null +++ b/examples/06_bend_collision_models.py @@ -0,0 +1,79 @@ +from shapely.geometry import Polygon + +from inire import CongestionOptions, NetSpec, ObjectiveWeights, RoutingOptions, RoutingProblem, RoutingResult, SearchOptions, route +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry +from inire.geometry.primitives import Port +from inire.utils.visualization import plot_routing_results + + +def _route_scenario( + bounds: tuple[float, float, float, float], + obstacles: list[Polygon], + netlist: dict[str, tuple[Port, Port]], + widths: dict[str, float], + *, + bend_collision_type: BendCollisionModel = "arc", + bend_proxy_geometry: BendCollisionModel | None = None, + bend_physical_geometry: BendPhysicalGeometry | None = None, + bend_clip_margin: float | None = None, +) -> dict[str, RoutingResult]: + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=widths[net_id]) for net_id, (start, target) in netlist.items()), + static_obstacles=tuple(obstacles), + ) + options = RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_collision_type=bend_collision_type, + bend_proxy_geometry=bend_proxy_geometry, + bend_physical_geometry=bend_physical_geometry, + bend_clip_margin=bend_clip_margin, + ), + objective=ObjectiveWeights( + bend_penalty=50.0, + sbend_penalty=150.0, + ), + congestion=CongestionOptions(use_tiered_strategy=False), + ) + return route(problem, options=options).results_by_net + + +def main() -> None: + print("Running Example 06: Bend Collision Models...") + + bounds = (-20, -20, 170, 170) + obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]) + obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]) + obs_custom = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]) + custom_bend = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + + obstacles = [obs_arc, obs_bbox, obs_custom] + netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))} + netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))} + netlist_custom = {"custom_geometry": (Port(10, 20, 0), Port(90, 40, 90))} + + print("Routing Scenario 1 (Arc)...") + res_arc = _route_scenario(bounds, obstacles, netlist_arc, {"arc_model": 2.0}, bend_collision_type="arc") + print("Routing Scenario 2 (BBox)...") + res_bbox = _route_scenario(bounds, obstacles, netlist_bbox, {"bbox_model": 2.0}, bend_collision_type="bbox") + print("Routing Scenario 3 (Custom Manhattan Geometry With Matching Proxy)...") + res_custom = _route_scenario( + bounds, + obstacles, + netlist_custom, + {"custom_geometry": 2.0}, + bend_physical_geometry=custom_bend, + bend_proxy_geometry=custom_bend, + ) + + all_results = {**res_arc, **res_bbox, **res_custom} + all_netlists = {**netlist_arc, **netlist_bbox, **netlist_custom} + + fig, _ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists) + fig.savefig("examples/06_bend_collision_models.png") + print("Saved plot to examples/06_bend_collision_models.png") + + +if __name__ == "__main__": + main() diff --git a/examples/07_large_scale_routing.py b/examples/07_large_scale_routing.py new file mode 100644 index 0000000..92098fb --- /dev/null +++ b/examples/07_large_scale_routing.py @@ -0,0 +1,120 @@ +import time + +from shapely.geometry import box + +from inire import ( + NetSpec, + Port, + RoutingProblem, + RoutingResult, +) +from inire.router._stack import build_routing_stack +from inire.utils.visualization import plot_expanded_nodes, plot_routing_results + + +def main() -> None: + print("Running Example 07: Fan-Out (10 Nets, 50um Radius)...") + + bounds = (0, 0, 1000, 1000) + obstacles = [ + box(450, 0, 550, 400), + box(450, 600, 550, 1000), + ] + + num_nets = 10 + start_x = 50 + start_y_base = 500 - (num_nets * 10.0) / 2.0 + end_x = 950 + end_y_base = 100 + end_y_pitch = 800.0 / (num_nets - 1) + + netlist: dict[str, tuple[Port, Port]] = {} + for index in range(num_nets): + start_y = int(round(start_y_base + index * 10.0)) + end_y = int(round(end_y_base + index * end_y_pitch)) + netlist[f"net_{index:02d}"] = (Port(start_x, start_y, 0), Port(end_x, end_y, 0)) + + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()), + static_obstacles=tuple(obstacles), + clearance=6.0, + ) + from inire import CongestionOptions, DiagnosticsOptions, ObjectiveWeights, RoutingOptions, SearchOptions + + options = RoutingOptions( + search=SearchOptions( + node_limit=2_000_000, + bend_radii=(50.0,), + sbend_radii=(50.0,), + greedy_h_weight=1.5, + bend_clip_margin=10.0, + ), + objective=ObjectiveWeights( + unit_length_cost=0.1, + bend_penalty=100.0, + sbend_penalty=400.0, + ), + congestion=CongestionOptions( + max_iterations=15, + base_penalty=100.0, + multiplier=1.4, + net_order="shortest", + shuffle_nets=True, + seed=42, + ), + diagnostics=DiagnosticsOptions(capture_expanded=True), + ) + stack = build_routing_stack(problem, options) + evaluator = stack.evaluator + finder = stack.finder + metrics = finder.metrics + + iteration_stats: list[dict[str, int]] = [] + + def iteration_callback(iteration: int, current_results: dict[str, RoutingResult]) -> None: + successes = sum(1 for result in current_results.values() if result.is_valid) + total_collisions = sum(result.collisions for result in current_results.values()) + total_nodes = metrics.nodes_expanded + print(f" Iteration {iteration} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}") + new_greedy = max(1.1, 1.5 - ((iteration + 1) / 10.0) * 0.4) + evaluator.greedy_h_weight = new_greedy + print(f" Adaptive Greedy Weight for Next Iteration: {new_greedy:.3f}") + iteration_stats.append( + { + "Iteration": iteration, + "Success": successes, + "Congestion": total_collisions, + "Nodes": total_nodes, + } + ) + metrics.reset_per_route() + + print(f"Routing {len(netlist)} nets through 200um bottleneck...") + start_time = time.perf_counter() + results = finder.route_all(iteration_callback=iteration_callback) + end_time = time.perf_counter() + + print(f"Routing took {end_time - start_time:.4f}s") + print("\n--- Iteration Summary ---") + print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}") + print("-" * 43) + for stats in iteration_stats: + print(f"{stats['Iteration']:<5} | {stats['Success']:<8} | {stats['Congestion']:<8} | {stats['Nodes']:<10}") + + success_count = sum(1 for result in results.values() if result.is_valid) + print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.") + for net_id, result in results.items(): + if not result.is_valid: + print(f" FAILED: {net_id}, collisions={result.collisions}") + else: + print(f" {net_id}: SUCCESS") + + fig, ax = plot_routing_results(results, list(obstacles), bounds, netlist=netlist) + plot_expanded_nodes(list(finder.accumulated_expanded_nodes), ax=ax) + fig.savefig("examples/07_large_scale_routing.png") + print("Saved plot to examples/07_large_scale_routing.png") + + +if __name__ == "__main__": + main() diff --git a/examples/08_custom_bend_geometry.png b/examples/08_custom_bend_geometry.png new file mode 100644 index 0000000..7088343 Binary files /dev/null and b/examples/08_custom_bend_geometry.png differ diff --git a/examples/08_custom_bend_geometry.py b/examples/08_custom_bend_geometry.py new file mode 100644 index 0000000..d24e087 --- /dev/null +++ b/examples/08_custom_bend_geometry.py @@ -0,0 +1,72 @@ +from shapely.geometry import Polygon, box + +from inire import CongestionOptions, NetSpec, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry +from inire.geometry.primitives import Port +from inire.utils.visualization import plot_routing_results + + +def _run_session( + bounds: tuple[float, float, float, float], + net_id: str, + start: Port, + target: Port, + *, + bend_collision_type: BendCollisionModel = "arc", + bend_proxy_geometry: BendCollisionModel | None = None, + bend_physical_geometry: BendPhysicalGeometry | None = None, +) -> dict[str, object]: + problem = RoutingProblem( + bounds=bounds, + nets=(NetSpec(net_id, start, target, width=2.0),), + ) + options = RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_collision_type=bend_collision_type, + bend_proxy_geometry=bend_proxy_geometry, + bend_physical_geometry=bend_physical_geometry, + sbend_radii=(), + ), + congestion=CongestionOptions(max_iterations=1, use_tiered_strategy=False), + ) + return route(problem, options=options).results_by_net + + +def main() -> None: + print("Running Example 08: Custom Bend Geometry...") + + bounds = (0, 0, 150, 150) + start = Port(20, 20, 0) + target = Port(100, 100, 90) + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) + + print("Routing standard arc in its own session...") + results_std = _run_session(bounds, "standard_arc", start, target) + print("Routing custom geometry with a separate custom proxy in its own session...") + results_custom = _run_session( + bounds, + "custom_geometry_and_proxy", + start, + target, + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, + ) + + all_results = {**results_std, **results_custom} + fig, _ax = plot_routing_results( + all_results, + [], + bounds, + netlist={ + "standard_arc": (start, target), + "custom_geometry_and_proxy": (start, target), + }, + ) + fig.savefig("examples/08_custom_bend_geometry.png") + print("Saved plot to examples/08_custom_bend_geometry.png") + + +if __name__ == "__main__": + main() diff --git a/examples/09_unroutable_best_effort.py b/examples/09_unroutable_best_effort.py new file mode 100644 index 0000000..227f5bf --- /dev/null +++ b/examples/09_unroutable_best_effort.py @@ -0,0 +1,51 @@ +from shapely.geometry import box + +from inire import CongestionOptions, NetSpec, ObjectiveWeights, Port, RoutingOptions, RoutingProblem, SearchOptions, route +from inire.utils.visualization import plot_routing_results + + +def main() -> None: + print("Running Example 09: Best-Effort Under Tight Search Budget...") + + bounds = (0, 0, 100, 100) + obstacles = [ + box(35, 35, 45, 65), + box(55, 35, 65, 65), + ] + problem = RoutingProblem( + bounds=bounds, + nets=(NetSpec("budget_limited_net", Port(10, 50, 0), Port(85, 60, 180), width=2.0),), + static_obstacles=tuple(obstacles), + ) + options = RoutingOptions( + search=SearchOptions( + node_limit=3, + bend_radii=(10.0,), + ), + objective=ObjectiveWeights( + bend_penalty=50.0, + sbend_penalty=150.0, + ), + congestion=CongestionOptions(warm_start_enabled=False, max_iterations=1), + ) + + print("Routing with a deliberately tiny node budget (should return a partial path)...") + run = route(problem, options=options) + result = run.results_by_net["budget_limited_net"] + if not result.reached_target: + print(f"Target not reached as expected. Partial path length: {len(result.path)} segments.") + else: + print("The route unexpectedly reached the target. Increase difficulty or reduce the node budget further.") + + fig, _ax = plot_routing_results( + run.results_by_net, + list(obstacles), + bounds, + netlist={"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))}, + ) + fig.savefig("examples/09_unroutable_best_effort.png") + print("Saved plot to examples/09_unroutable_best_effort.png") + + +if __name__ == "__main__": + main() diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4040cd6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,39 @@ +# Inire Routing Examples + +This directory contains examples demonstrating the features and architectural capabilities of the `inire` router. + +## Architectural Visualization +In all plots generated by `inire`, we distinguish between the search-time geometry and the final "actual" geometry: +* **Dashed Lines & Translucent Fill**: The **Collision Proxy** used during the A* search (e.g., `clipped_bbox` or `bbox`). This represents the conservative envelope the router used to guarantee clearance. +* **Solid Lines**: The **Actual Geometry** (high-fidelity arcs). This is the exact shape that will be used for PDK generation and fabrication. + +--- + +## 1. Fan-Out (Negotiated Congestion) +Demonstrates the Negotiated Congestion algorithm handling multiple intersecting nets. The router iteratively increases penalties for overlaps until a collision-free solution is found. This example shows a bundle of nets fanning out through a narrow bottleneck. + +![Fan-Out Routing](07_large_scale_routing.png) + +## 2. Bend Geometry Models +`inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy: +* **Arc**: High-fidelity geometry (Highest accuracy). +* **BBox**: Simple axis-aligned bounding box (Fastest search). +* **Custom Manhattan Geometry**: A custom 90-degree bend polygon with the same width as the normal waveguide. + +Example 06 uses the Manhattan polygon as both the true routed bend geometry and the collision proxy. +Example 08 compares the standard arc against a run that uses a custom physical bend plus a separate custom proxy polygon, with each net routed in its own session. + +![Custom Bend Geometry](08_custom_bend_geometry.png) + +## 3. Unroutable Nets & Best-Effort Display +When a net is physically blocked or exceeds the node limit, the router returns the "best-effort" partial path—the path that reached the point closest to the target according to the heuristic. This is critical for debugging design constraints. + +![Best Effort Display](09_unroutable_best_effort.png) + +## 4. Orientation Stress Test +Demonstrates the router's ability to handle complex orientation requirements, including U-turns, 90-degree flips, and loops. + +![Orientation Stress Test](05_orientation_stress.png) + +## 5. Tiered Fidelity +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`. diff --git a/inire/__init__.py b/inire/__init__.py index 2e37c92..a53b46a 100644 --- a/inire/__init__.py +++ b/inire/__init__.py @@ -1,6 +1,84 @@ """ - inire Wave-router +inire Wave-router """ +from collections.abc import Callable + +from .geometry.primitives import Port as Port # noqa: PLC0414 +from .model import ( + CongestionOptions as CongestionOptions, + DiagnosticsOptions as DiagnosticsOptions, + NetSpec as NetSpec, + ObjectiveWeights as ObjectiveWeights, + RefinementOptions as RefinementOptions, + RoutingOptions as RoutingOptions, + RoutingProblem as RoutingProblem, + SearchOptions as SearchOptions, +) # noqa: PLC0414 +from .results import ( # noqa: PLC0414 + ComponentConflictTrace as ComponentConflictTrace, + ConflictTraceEntry as ConflictTraceEntry, + FrontierPruneSample as FrontierPruneSample, + IterationNetAttemptTrace as IterationNetAttemptTrace, + IterationTraceEntry as IterationTraceEntry, + NetConflictTrace as NetConflictTrace, + NetFrontierTrace as NetFrontierTrace, + PrePairFrontierTraceEntry as PrePairFrontierTraceEntry, + PrePairNetTrace as PrePairNetTrace, + RoutingResult as RoutingResult, + RoutingRunResult as RoutingRunResult, +) +from .seeds import Bend90Seed as Bend90Seed, PathSeed as PathSeed, SBendSeed as SBendSeed, StraightSeed as StraightSeed # noqa: PLC0414 __author__ = 'Jan Petykiewicz' __version__ = '0.1' + + +def route( + problem: RoutingProblem, + *, + options: RoutingOptions | None = None, + iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None, +) -> RoutingRunResult: + from .router._stack import build_routing_stack + + resolved_options = RoutingOptions() if options is None else options + stack = build_routing_stack(problem, resolved_options) + finder = stack.finder + results = finder.route_all(iteration_callback=iteration_callback) + return RoutingRunResult( + results_by_net=results, + metrics=finder.metrics.snapshot(), + expanded_nodes=tuple(finder.accumulated_expanded_nodes), + conflict_trace=tuple(finder.conflict_trace), + frontier_trace=tuple(finder.frontier_trace), + pre_pair_frontier_trace=finder.pre_pair_frontier_trace, + iteration_trace=tuple(finder.iteration_trace), + ) + +__all__ = [ + "Bend90Seed", + "CongestionOptions", + "ComponentConflictTrace", + "ConflictTraceEntry", + "DiagnosticsOptions", + "NetSpec", + "NetConflictTrace", + "NetFrontierTrace", + "ObjectiveWeights", + "PathSeed", + "Port", + "FrontierPruneSample", + "IterationNetAttemptTrace", + "IterationTraceEntry", + "PrePairFrontierTraceEntry", + "PrePairNetTrace", + "RefinementOptions", + "RoutingOptions", + "RoutingProblem", + "RoutingResult", + "RoutingRunResult", + "SBendSeed", + "SearchOptions", + "StraightSeed", + "route", +] diff --git a/inire/constants.py b/inire/constants.py new file mode 100644 index 0000000..bfbebe6 --- /dev/null +++ b/inire/constants.py @@ -0,0 +1,6 @@ +""" +Centralized constants for the inire routing engine. +""" + +TOLERANCE_LINEAR = 1e-6 +TOLERANCE_ANGULAR = 1e-3 diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py new file mode 100644 index 0000000..60b008a --- /dev/null +++ b/inire/geometry/collision.py @@ -0,0 +1,864 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import numpy +from shapely.geometry import LineString, box + +from inire.geometry.component_overlap import components_overlap +from inire.geometry.dynamic_path_index import DynamicPathIndex +from inire.geometry.index_helpers import grid_cell_span +from inire.results import RoutingReport +from inire.geometry.static_obstacle_index import StaticObstacleIndex + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from shapely.geometry import Polygon + from shapely.geometry.base import BaseGeometry + from shapely.strtree import STRtree + + from inire.geometry.components import ComponentResult + from inire.geometry.primitives import Port + + +def _intersection_distance(origin: Port, geometry: BaseGeometry) -> float: + if hasattr(geometry, "geoms"): + return min(_intersection_distance(origin, sub_geometry) for sub_geometry in geometry.geoms) + return float(numpy.sqrt((geometry.coords[0][0] - origin.x) ** 2 + (geometry.coords[0][1] - origin.y) ** 2)) + + +def _bounds_overlap( + left: tuple[float, float, float, float], + right: tuple[float, float, float, float], +) -> bool: + return ( + left[0] < right[2] + and left[2] > right[0] + and left[1] < right[3] + and left[3] > right[1] + ) + + +def _has_non_touching_overlap(left: BaseGeometry, right: BaseGeometry) -> bool: + return left.intersects(right) and not left.touches(right) + + +def _span_to_bounds( + gx_min: int, + gy_min: int, + gx_max: int, + gy_max: int, + cell_size: float, +) -> tuple[float, float, float, float]: + return ( + gx_min * cell_size, + gy_min * cell_size, + (gx_max + 1) * cell_size, + (gy_max + 1) * cell_size, + ) + + +@dataclass(frozen=True, slots=True) +class PathVerificationDetail: + report: RoutingReport + conflicting_net_ids: tuple[str, ...] = () + component_conflicts: tuple[tuple[int, str, int], ...] = () + + +@dataclass(frozen=True, slots=True) +class DynamicCongestionDetail: + soft_overlap_count: int = 0 + hits_frozen_net: bool = False + + +class RoutingWorld: + """ + Internal spatial state for collision detection, congestion, and verification. + """ + + __slots__ = ( + "clearance", + "safety_zone_radius", + "grid_cell_size", + "metrics", + "_dynamic_paths", + "_static_obstacles", + ) + + def __init__( + self, + clearance: float, + safety_zone_radius: float = 0.0021, + ) -> None: + self.clearance = clearance + self.safety_zone_radius = safety_zone_radius + + self.grid_cell_size = 50.0 + self.metrics = None + self._static_obstacles = StaticObstacleIndex(self) + self._dynamic_paths = DynamicPathIndex(self) + + def get_static_version(self) -> int: + return self._static_obstacles.version + + def iter_static_dilated_geometries(self) -> Iterable[Polygon]: + return self._static_obstacles.dilated.values() + + def iter_static_obstacle_bounds( + self, + query_bounds: tuple[float, float, float, float], + ) -> Iterable[tuple[float, float, float, float]]: + for obj_id in self._static_obstacles.index.intersection(query_bounds): + yield self._static_obstacles.geometries[obj_id].bounds + + def iter_dynamic_path_bounds( + self, + query_bounds: tuple[float, float, float, float], + ) -> Iterable[tuple[float, float, float, float]]: + for obj_id in self._dynamic_paths.index.intersection(query_bounds): + yield self._dynamic_paths.geometries[obj_id][1].bounds + + def add_static_obstacle(self, polygon: Polygon, dilated_geometry: Polygon | None = None) -> int: + return self._static_obstacles.add_obstacle(polygon, dilated_geometry=dilated_geometry) + + def remove_static_obstacle(self, obj_id: int) -> None: + self._static_obstacles.remove_obstacle(obj_id) + + def _ensure_static_tree(self) -> None: + self._static_obstacles.ensure_tree() + + def _ensure_net_static_tree(self, net_width: float) -> STRtree: + return self._static_obstacles.ensure_net_tree(net_width) + + def _ensure_static_raw_tree(self) -> None: + self._static_obstacles.ensure_raw_tree() + + def _ensure_dynamic_tree(self) -> None: + self._dynamic_paths.ensure_tree() + + def _ensure_dynamic_grid(self) -> None: + self._dynamic_paths.ensure_grid() + + def add_path( + self, + net_id: str, + geometry: Sequence[Polygon], + dilated_geometry: Sequence[Polygon], + component_indexes: Sequence[int] | None = None, + ) -> None: + self._dynamic_paths.add_path( + net_id, + geometry, + dilated_geometry=dilated_geometry, + component_indexes=component_indexes, + ) + + def remove_path(self, net_id: str) -> None: + self._dynamic_paths.remove_path(net_id) + + def has_dynamic_paths(self) -> bool: + return bool(self._dynamic_paths.geometries) + + def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool: + reach = self.ray_cast( + start_port, + start_port.r, + max_dist=length + 0.01, + net_width=net_width, + caller="straight_static", + ) + return reach < length - 0.001 + + def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool: + bounds_array = self._static_obstacles.bounds_array + if bounds_array is None: + return False + bounds = bounds_array[idx] + safety_zone = self.safety_zone_radius + if ( + start_port + and bounds[0] - safety_zone <= start_port.x <= bounds[2] + safety_zone + and bounds[1] - safety_zone <= start_port.y <= bounds[3] + safety_zone + ): + return True + return bool( + end_port + and bounds[0] - safety_zone <= end_port.x <= bounds[2] + safety_zone + and bounds[1] - safety_zone <= end_port.y <= bounds[3] + safety_zone + ) + + def _is_in_safety_zone( + self, + geometry: Polygon, + obj_id: int, + start_port: Port | None, + end_port: Port | None, + ) -> bool: + raw_obstacle = self._static_obstacles.geometries[obj_id] + safety_zone = self.safety_zone_radius + + obstacle_bounds = raw_obstacle.bounds + near_start = start_port and ( + obstacle_bounds[0] - safety_zone <= start_port.x <= obstacle_bounds[2] + safety_zone + and obstacle_bounds[1] - safety_zone <= start_port.y <= obstacle_bounds[3] + safety_zone + ) + near_end = end_port and ( + obstacle_bounds[0] - safety_zone <= end_port.x <= obstacle_bounds[2] + safety_zone + and obstacle_bounds[1] - safety_zone <= end_port.y <= obstacle_bounds[3] + safety_zone + ) + + if not near_start and not near_end: + return False + if not geometry.intersects(raw_obstacle): + return False + + intersection = geometry.intersection(raw_obstacle) + if intersection.is_empty: + return False + + ix_bounds = intersection.bounds + if ( + start_port + and near_start + and abs(ix_bounds[0] - start_port.x) < safety_zone + and abs(ix_bounds[1] - start_port.y) < safety_zone + and abs(ix_bounds[2] - start_port.x) < safety_zone + and abs(ix_bounds[3] - start_port.y) < safety_zone + ): + return True + return bool( + end_port + and near_end + and abs(ix_bounds[0] - end_port.x) < safety_zone + and abs(ix_bounds[1] - end_port.y) < safety_zone + and abs(ix_bounds[2] - end_port.x) < safety_zone + and abs(ix_bounds[3] - end_port.y) < safety_zone + ) + + def check_move_static( + self, + result: ComponentResult, + start_port: Port | None = None, + end_port: Port | None = None, + ) -> bool: + # TODO: If static buffering becomes net-width-specific, add dedicated + # width-aware geometry/index handling instead of reviving dead args here. + static_obstacles = self._static_obstacles + if not static_obstacles.dilated: + return False + + self._ensure_static_tree() + tree = static_obstacles.tree + bounds_array = static_obstacles.bounds_array + if tree is None or bounds_array is None: + return False + + hits = tree.query(box(*result.total_dilated_bounds)) + if hits.size == 0: + return False + + move_poly_bounds = result.dilated_bounds + for hit_idx in hits: + obstacle_bounds = bounds_array[hit_idx] + poly_hits_obstacle_aabb = False + for poly_bounds in move_poly_bounds: + if ( + poly_bounds[0] < obstacle_bounds[2] + and poly_bounds[2] > obstacle_bounds[0] + and poly_bounds[1] < obstacle_bounds[3] + and poly_bounds[3] > obstacle_bounds[1] + ): + poly_hits_obstacle_aabb = True + break + + if not poly_hits_obstacle_aabb: + continue + + obj_id = static_obstacles.obj_ids[hit_idx] + if self._is_in_safety_zone_fast(hit_idx, start_port, end_port): + collision_found = False + for polygon in result.collision_geometry: + if not self._is_in_safety_zone(polygon, obj_id, start_port, end_port): + collision_found = True + break + if collision_found: + return True + continue + + static_obstacle = static_obstacles.dilated[obj_id] + for polygon in result.dilated_collision_geometry: + if polygon.intersects(static_obstacle) and not polygon.touches(static_obstacle): + return True + + return False + + def _check_real_congestion( + self, + result: ComponentResult, + candidates_by_net: dict[str, dict[int, tuple[int, ...]]], + frozen_net_ids: frozenset[str] = frozenset(), + ) -> DynamicCongestionDetail: + if not candidates_by_net: + return DynamicCongestionDetail() + + dynamic_paths = self._dynamic_paths + geometries_to_test = result.dilated_collision_geometry + + real_hits_count = 0 + for other_net_id, other_obj_ids in candidates_by_net.items(): + found_real = False + for obj_id, test_geometry_indexes in other_obj_ids.items(): + tree_geometry = dynamic_paths.dilated[obj_id] + for test_geometry_index in test_geometry_indexes: + test_geometry = geometries_to_test[test_geometry_index] + if self.metrics is not None: + self.metrics.total_congestion_exact_pair_checks += 1 + if _has_non_touching_overlap(test_geometry, tree_geometry): + found_real = True + break + if found_real: + break + if found_real: + if other_net_id in frozen_net_ids: + return DynamicCongestionDetail( + soft_overlap_count=real_hits_count, + hits_frozen_net=True, + ) + real_hits_count += 1 + + return DynamicCongestionDetail(soft_overlap_count=real_hits_count) + + def _collect_congestion_candidates( + self, + result: ComponentResult, + net_id: str, + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None, + ) -> dict[str, dict[int, tuple[int, ...]]]: + dynamic_paths = self._dynamic_paths + if not dynamic_paths.dilated: + return {} + + self._ensure_dynamic_grid() + if not dynamic_paths.grid: + return {} + + candidates_by_net: dict[str, dict[int, set[int]]] = {} + for test_geometry_index, test_bounds in enumerate(result.dilated_bounds): + if not self._has_possible_congestion_in_grid(test_bounds, net_id): + continue + envelope_net_ids = self._get_net_envelope_candidates( + test_bounds, + net_id, + net_envelope_cache, + ) + if not envelope_net_ids: + continue + grid_net_ids = self._get_grid_span_net_candidates( + test_bounds, + net_id, + grid_net_cache, + ) + if not grid_net_ids: + continue + candidate_net_ids = tuple(sorted(set(envelope_net_ids) & set(grid_net_ids))) + if not candidate_net_ids: + continue + grid_candidates = self._get_grid_span_candidates( + test_bounds, + net_id, + broad_phase_cache, + ) + for other_net_id in candidate_net_ids: + if self.metrics is not None: + self.metrics.total_congestion_candidate_nets += 1 + obj_ids = grid_candidates.get(other_net_id) + if not obj_ids: + continue + for obj_id in obj_ids: + if not _bounds_overlap(test_bounds, dynamic_paths.dilated_bounds[obj_id]): + continue + if self.metrics is not None: + self.metrics.total_congestion_candidate_ids += 1 + candidate_indexes = candidates_by_net.setdefault(other_net_id, {}).setdefault(obj_id, set()) + candidate_indexes.add(test_geometry_index) + + return { + other_net_id: { + obj_id: tuple(sorted(test_geometry_indexes)) + for obj_id, test_geometry_indexes in sorted(obj_ids.items()) + } + for other_net_id, obj_ids in candidates_by_net.items() + } + + def _has_possible_congestion_in_grid( + self, + bounds: tuple[float, float, float, float], + net_id: str, + ) -> bool: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + dynamic_paths = self._dynamic_paths + + if gx_min == gx_max and gy_min == gy_max: + net_counts = dynamic_paths.grid_net_counts.get((gx_min, gy_min)) + return bool(net_counts and (len(net_counts) > 1 or net_id not in net_counts)) + + for gx in range(gx_min, gx_max + 1): + for gy in range(gy_min, gy_max + 1): + net_counts = dynamic_paths.grid_net_counts.get((gx, gy)) + if net_counts and (len(net_counts) > 1 or net_id not in net_counts): + return True + return False + + def has_possible_move_congestion( + self, + result: ComponentResult, + net_id: str, + presence_cache: dict[tuple[str, int, int, int, int], bool] | None = None, + ) -> bool: + dynamic_paths = self._dynamic_paths + if not dynamic_paths.dilated: + return False + + self._ensure_dynamic_grid() + if not dynamic_paths.grid: + return False + + for bounds in result.dilated_bounds: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + cache_key = (net_id, gx_min, gy_min, gx_max, gy_max) + if presence_cache is not None and cache_key in presence_cache: + if self.metrics is not None: + self.metrics.total_congestion_presence_cache_hits += 1 + has_possible = presence_cache[cache_key] + else: + if self.metrics is not None: + self.metrics.total_congestion_presence_cache_misses += 1 + has_possible = self._has_possible_congestion_in_grid(bounds, net_id) + if presence_cache is not None: + presence_cache[cache_key] = has_possible + if has_possible: + return True + return False + + def has_candidate_move_congestion( + self, + result: ComponentResult, + net_id: str, + candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] | None = None, + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + ) -> bool: + dynamic_paths = self._dynamic_paths + if not dynamic_paths.dilated: + return False + + for bounds in result.dilated_bounds: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + cache_key = (net_id, gx_min, gy_min, gx_max, gy_max) + if candidate_precheck_cache is not None and cache_key in candidate_precheck_cache: + if self.metrics is not None: + self.metrics.total_congestion_candidate_precheck_hits += 1 + has_candidates = candidate_precheck_cache[cache_key] + else: + if self.metrics is not None: + self.metrics.total_congestion_candidate_precheck_misses += 1 + span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size) + envelope_net_ids = self._get_net_envelope_candidates( + span_bounds, + net_id, + net_envelope_cache, + ) + if not envelope_net_ids: + has_candidates = False + else: + grid_net_ids = self._get_grid_span_net_candidates( + span_bounds, + net_id, + grid_net_cache, + ) + if not grid_net_ids: + has_candidates = False + else: + has_candidates = bool(set(envelope_net_ids) & set(grid_net_ids)) + if candidate_precheck_cache is not None: + candidate_precheck_cache[cache_key] = has_candidates + if has_candidates: + return True + return False + + def _get_grid_span_candidates( + self, + bounds: tuple[float, float, float, float], + net_id: str, + broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None, + ) -> dict[str, tuple[int, ...]]: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + cache_key = (net_id, gx_min, gy_min, gx_max, gy_max) + if broad_phase_cache is not None and cache_key in broad_phase_cache: + if self.metrics is not None: + self.metrics.total_congestion_grid_span_cache_hits += 1 + return broad_phase_cache[cache_key] + + if self.metrics is not None: + self.metrics.total_congestion_grid_span_cache_misses += 1 + + dynamic_paths = self._dynamic_paths + candidates_by_net: dict[str, set[int]] = {} + for gx in range(gx_min, gx_max + 1): + for gy in range(gy_min, gy_max + 1): + for other_net_id, obj_ids in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).items(): + if other_net_id == net_id: + continue + candidates_by_net.setdefault(other_net_id, set()).update(obj_ids) + + frozen = { + other_net_id: tuple(sorted(obj_ids)) + for other_net_id, obj_ids in sorted(candidates_by_net.items()) + } + if broad_phase_cache is not None: + broad_phase_cache[cache_key] = frozen + return frozen + + def _get_grid_span_net_candidates( + self, + bounds: tuple[float, float, float, float], + net_id: str, + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None, + ) -> tuple[str, ...]: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + cache_key = (net_id, gx_min, gy_min, gx_max, gy_max) + if grid_net_cache is not None and cache_key in grid_net_cache: + if self.metrics is not None: + self.metrics.total_congestion_grid_net_cache_hits += 1 + return grid_net_cache[cache_key] + + if self.metrics is not None: + self.metrics.total_congestion_grid_net_cache_misses += 1 + + dynamic_paths = self._dynamic_paths + candidate_net_ids: set[str] = set() + for gx in range(gx_min, gx_max + 1): + for gy in range(gy_min, gy_max + 1): + for other_net_id in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}): + if other_net_id != net_id: + candidate_net_ids.add(other_net_id) + + frozen = tuple(sorted(candidate_net_ids)) + if grid_net_cache is not None: + grid_net_cache[cache_key] = frozen + return frozen + + def _get_net_envelope_candidates( + self, + bounds: tuple[float, float, float, float], + net_id: str, + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None, + ) -> tuple[str, ...]: + dynamic_paths = self._dynamic_paths + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + cache_key = (net_id, gx_min, gy_min, gx_max, gy_max) + if net_envelope_cache is not None and cache_key in net_envelope_cache: + if self.metrics is not None: + self.metrics.total_congestion_net_envelope_cache_hits += 1 + cached_net_ids = net_envelope_cache[cache_key] + else: + if self.metrics is not None: + self.metrics.total_congestion_net_envelope_cache_misses += 1 + span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size) + cached_net_ids = tuple( + sorted( + dynamic_paths.net_envelope_obj_to_net[obj_id] + for obj_id in dynamic_paths.net_envelope_index.intersection(span_bounds) + if dynamic_paths.net_envelope_obj_to_net[obj_id] != net_id + ) + ) + if net_envelope_cache is not None: + net_envelope_cache[cache_key] = cached_net_ids + + return tuple( + other_net_id + for other_net_id in cached_net_ids + if _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id]) + ) + + def _get_verify_net_envelope_candidates( + self, + bounds: tuple[float, float, float, float], + net_id: str, + ) -> tuple[str, ...]: + dynamic_paths = self._dynamic_paths + candidate_net_ids: list[str] = [] + for obj_id in dynamic_paths.net_envelope_index.intersection(bounds): + other_net_id = dynamic_paths.net_envelope_obj_to_net[obj_id] + if other_net_id == net_id: + continue + if not _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id]): + continue + candidate_net_ids.append(other_net_id) + if self.metrics is not None: + self.metrics.total_verify_dynamic_candidate_nets += len(candidate_net_ids) + return tuple(candidate_net_ids) + + def _get_verify_grid_span_obj_ids( + self, + bounds: tuple[float, float, float, float], + other_net_id: str, + ) -> tuple[int, ...]: + dynamic_paths = self._dynamic_paths + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size) + obj_ids: set[int] = set() + for gx in range(gx_min, gx_max + 1): + for gy in range(gy_min, gy_max + 1): + obj_ids.update(dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).get(other_net_id, ())) + return tuple(sorted(obj_ids)) + + def check_move_congestion( + self, + result: ComponentResult, + net_id: str, + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None, + ) -> int: + return self.check_move_congestion_detail( + result, + net_id, + net_envelope_cache=net_envelope_cache, + grid_net_cache=grid_net_cache, + broad_phase_cache=broad_phase_cache, + ).soft_overlap_count + + def check_move_congestion_detail( + self, + result: ComponentResult, + net_id: str, + *, + frozen_net_ids: frozenset[str] = frozenset(), + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None, + broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None, + ) -> DynamicCongestionDetail: + if self.metrics is not None: + self.metrics.total_congestion_check_calls += 1 + dynamic_paths = self._dynamic_paths + if not dynamic_paths.geometries: + return DynamicCongestionDetail() + + candidates_by_net = self._collect_congestion_candidates( + result, + net_id, + net_envelope_cache, + grid_net_cache, + broad_phase_cache, + ) + if not candidates_by_net: + return DynamicCongestionDetail() + return self._check_real_congestion( + result, + candidates_by_net, + frozen_net_ids=frozen_net_ids, + ) + + def verify_path_details( + self, + net_id: str, + components: Sequence[ComponentResult], + *, + capture_component_conflicts: bool = False, + ) -> PathVerificationDetail: + if self.metrics is not None: + self.metrics.total_verify_path_report_calls += 1 + static_collision_count = 0 + dynamic_collision_count = 0 + self_collision_count = 0 + total_length = sum(component.length for component in components) + conflicting_net_ids: set[str] = set() + component_conflicts: set[tuple[int, str, int]] = set() + + static_obstacles = self._static_obstacles + dynamic_paths = self._dynamic_paths + + self._ensure_static_raw_tree() + if static_obstacles.raw_tree is not None: + raw_geometries = static_obstacles.raw_tree.geometries + for component in components: + 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") + hits = static_obstacles.raw_tree.query(buffered, predicate="intersects") + for hit_idx in hits: + obstacle = raw_geometries[hit_idx] + if buffered.touches(obstacle): + continue + + obj_id = static_obstacles.raw_obj_ids[hit_idx] + if not self._is_in_safety_zone(polygon, obj_id, None, None): + static_collision_count += 1 + + if dynamic_paths.dilated: + for component_index, component in enumerate(components): + test_geometries = component.dilated_physical_geometry + component_hits = [] + for new_geometry in test_geometries: + for hit_net_id in self._get_verify_net_envelope_candidates(new_geometry.bounds, str(net_id)): + for obj_id in self._get_verify_grid_span_obj_ids(new_geometry.bounds, hit_net_id): + if not _bounds_overlap(new_geometry.bounds, dynamic_paths.dilated_bounds[obj_id]): + continue + if self.metrics is not None: + self.metrics.total_verify_dynamic_exact_pair_checks += 1 + tree_geometry = dynamic_paths.dilated[obj_id] + if _has_non_touching_overlap(new_geometry, tree_geometry): + component_hits.append(hit_net_id) + if capture_component_conflicts: + component_conflicts.add( + ( + component_index, + hit_net_id, + dynamic_paths.component_indexes[obj_id], + ) + ) + break + + if component_hits: + unique_hits = tuple(sorted(set(component_hits))) + dynamic_collision_count += len(unique_hits) + conflicting_net_ids.update(unique_hits) + + for index, component in enumerate(components): + for other_index in range(index + 2, len(components)): + if components_overlap(component, components[other_index], prefer_actual=True): + self_collision_count += 1 + + return PathVerificationDetail( + report=RoutingReport( + static_collision_count=static_collision_count, + dynamic_collision_count=dynamic_collision_count, + self_collision_count=self_collision_count, + total_length=total_length, + ), + conflicting_net_ids=tuple(sorted(conflicting_net_ids)), + component_conflicts=tuple(sorted(component_conflicts)), + ) + + def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport: + return self.verify_path_details(net_id, components).report + + def ray_cast( + self, + origin: Port, + angle_deg: float, + max_dist: float = 2000.0, + net_width: float | None = None, + caller: str = "other", + ) -> float: + if self.metrics is not None: + self.metrics.total_ray_cast_calls += 1 + if caller == "straight_static": + self.metrics.total_ray_cast_calls_straight_static += 1 + elif caller == "expand_snap": + self.metrics.total_ray_cast_calls_expand_snap += 1 + elif caller == "expand_forward": + self.metrics.total_ray_cast_calls_expand_forward += 1 + elif caller == "visibility_build": + self.metrics.total_ray_cast_calls_visibility_build += 1 + elif caller == "visibility_query": + self.metrics.total_ray_cast_calls_visibility_query += 1 + elif caller == "visibility_tangent": + self.metrics.total_ray_cast_calls_visibility_tangent += 1 + else: + self.metrics.total_ray_cast_calls_other += 1 + static_obstacles = self._static_obstacles + tree: STRtree | None + is_rect_array: numpy.ndarray | None + bounds_array: numpy.ndarray | None + + radians = numpy.radians(angle_deg) + cos_v, sin_v = numpy.cos(radians), numpy.sin(radians) + dx, dy = max_dist * cos_v, max_dist * sin_v + min_x, max_x = sorted([origin.x, origin.x + dx]) + min_y, max_y = sorted([origin.y, origin.y + dy]) + + if net_width is not None: + tree = self._ensure_net_static_tree(net_width) + key = (round(net_width, 4), round(self.clearance, 4)) + is_rect_array = static_obstacles.net_specific_is_rect[key] + bounds_array = static_obstacles.net_specific_bounds[key] + else: + self._ensure_static_tree() + tree = static_obstacles.tree + is_rect_array = static_obstacles.is_rect_array + bounds_array = static_obstacles.bounds_array + + if tree is None or is_rect_array is None or bounds_array is None: + return max_dist + + candidates = tree.query(box(min_x, min_y, max_x, max_y)) + if candidates.size == 0: + return max_dist + if self.metrics is not None: + self.metrics.total_ray_cast_candidate_bounds += int(candidates.size) + + min_dist = max_dist + inv_dx = 1.0 / dx if abs(dx) > 1e-12 else 1e30 + inv_dy = 1.0 / dy if abs(dy) > 1e-12 else 1e30 + tree_geometries = tree.geometries + ray_line = None + + candidates_bounds = bounds_array[candidates] + dist_sq = (candidates_bounds[:, 0] - origin.x) ** 2 + (candidates_bounds[:, 1] - origin.y) ** 2 + sorted_indices = numpy.argsort(dist_sq) + + for idx in sorted_indices: + candidate_id = candidates[idx] + bounds = bounds_array[candidate_id] + + if abs(dx) < 1e-12: + if origin.x < bounds[0] or origin.x > bounds[2]: + tx_min, tx_max = 1e30, -1e30 + else: + tx_min, tx_max = -1e30, 1e30 + else: + t1, t2 = (bounds[0] - origin.x) * inv_dx, (bounds[2] - origin.x) * inv_dx + tx_min, tx_max = min(t1, t2), max(t1, t2) + + if abs(dy) < 1e-12: + if origin.y < bounds[1] or origin.y > bounds[3]: + ty_min, ty_max = 1e30, -1e30 + else: + ty_min, ty_max = -1e30, 1e30 + else: + t1, t2 = (bounds[1] - origin.y) * inv_dy, (bounds[3] - origin.y) * inv_dy + ty_min, ty_max = min(t1, t2), max(t1, t2) + + t_min, t_max = max(tx_min, ty_min), min(tx_max, ty_max) + if t_max < 0 or t_min > t_max or t_min > 1.0: + continue + if t_min * max_dist >= min_dist: + continue + + if is_rect_array[candidate_id]: + min_dist = max(0.0, t_min * max_dist) + continue + + if ray_line is None: + ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)]) + + 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): + continue + + intersection = ray_line.intersection(obstacle) + if intersection.is_empty: + continue + + distance = _intersection_distance(origin, intersection) + min_dist = min(min_dist, distance) + + return min_dist diff --git a/inire/geometry/component_overlap.py b/inire/geometry/component_overlap.py new file mode 100644 index 0000000..f72603f --- /dev/null +++ b/inire/geometry/component_overlap.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + from shapely.geometry import Polygon + + from inire.geometry.components import ComponentResult + + +def components_overlap( + component_a: ComponentResult, + component_b: ComponentResult, + prefer_actual: bool = False, +) -> bool: + polygons_a: Sequence[Polygon] + polygons_b: Sequence[Polygon] + if prefer_actual: + polygons_a = component_a.physical_geometry + polygons_b = component_b.physical_geometry + bounds_a = ( + min(polygon.bounds[0] for polygon in polygons_a), + min(polygon.bounds[1] for polygon in polygons_a), + max(polygon.bounds[2] for polygon in polygons_a), + max(polygon.bounds[3] for polygon in polygons_a), + ) + bounds_b = ( + min(polygon.bounds[0] for polygon in polygons_b), + min(polygon.bounds[1] for polygon in polygons_b), + max(polygon.bounds[2] for polygon in polygons_b), + max(polygon.bounds[3] for polygon in polygons_b), + ) + else: + polygons_a = component_a.collision_geometry + polygons_b = component_b.collision_geometry + bounds_a = component_a.total_bounds + bounds_b = component_b.total_bounds + + if not ( + bounds_a[0] < bounds_b[2] + and bounds_a[2] > bounds_b[0] + and bounds_a[1] < bounds_b[3] + and bounds_a[3] > bounds_b[1] + ): + return False + + for polygon_a in polygons_a: + for polygon_b in polygons_b: + if polygon_a.intersects(polygon_b) and not polygon_a.touches(polygon_b): + return True + return False diff --git a/inire/geometry/components.py b/inire/geometry/components.py new file mode 100644 index 0000000..86b7282 --- /dev/null +++ b/inire/geometry/components.py @@ -0,0 +1,480 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +import numpy +from shapely.affinity import rotate as shapely_rotate +from shapely.affinity import scale as shapely_scale +from shapely.affinity import translate as shapely_translate +from shapely.geometry import Polygon, box + +from inire.constants import TOLERANCE_ANGULAR +from inire.seeds import Bend90Seed, PathSegmentSeed, SBendSeed, StraightSeed +from .primitives import Port, rotation_matrix2 + +if TYPE_CHECKING: + from collections.abc import Sequence + + +MoveKind = Literal["straight", "bend90", "sbend"] +BendCollisionModelName = Literal["arc", "bbox", "clipped_bbox"] +BendCollisionModel = BendCollisionModelName | Polygon +BendPhysicalGeometry = Literal["arc"] | Polygon + + +def _normalize_length(value: float) -> float: + return float(value) + + +@dataclass(frozen=True, slots=True) +class ComponentResult: + start_port: Port + collision_geometry: Sequence[Polygon] + end_port: Port + length: float + move_type: MoveKind + move_spec: PathSegmentSeed + physical_geometry: Sequence[Polygon] + dilated_collision_geometry: Sequence[Polygon] + dilated_physical_geometry: Sequence[Polygon] + _bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False) + _total_bounds: tuple[float, float, float, float] = field(init=False, repr=False) + _dilated_bounds: tuple[tuple[float, float, float, float], ...] = field(init=False, repr=False) + _total_dilated_bounds: tuple[float, float, float, float] = field(init=False, repr=False) + + def __post_init__(self) -> None: + collision_geometry = tuple(self.collision_geometry) + physical_geometry = tuple(self.physical_geometry) + dilated_collision_geometry = tuple(self.dilated_collision_geometry) + dilated_physical_geometry = tuple(self.dilated_physical_geometry) + + object.__setattr__(self, "collision_geometry", collision_geometry) + object.__setattr__(self, "physical_geometry", physical_geometry) + object.__setattr__(self, "dilated_collision_geometry", dilated_collision_geometry) + object.__setattr__(self, "dilated_physical_geometry", dilated_physical_geometry) + object.__setattr__(self, "length", float(self.length)) + + bounds = tuple(poly.bounds for poly in collision_geometry) + object.__setattr__(self, "_bounds", bounds) + object.__setattr__(self, "_total_bounds", _combine_bounds(list(bounds))) + + dilated_bounds = tuple(poly.bounds for poly in dilated_collision_geometry) + object.__setattr__(self, "_dilated_bounds", dilated_bounds) + object.__setattr__(self, "_total_dilated_bounds", _combine_bounds(list(dilated_bounds))) + + @property + def bounds(self) -> tuple[tuple[float, float, float, float], ...]: + return self._bounds + + @property + def total_bounds(self) -> tuple[float, float, float, float]: + return self._total_bounds + + @property + def dilated_bounds(self) -> tuple[tuple[float, float, float, float], ...]: + return self._dilated_bounds + + @property + def total_dilated_bounds(self) -> tuple[float, float, float, float]: + return self._total_dilated_bounds + + def translate(self, dx: int | float, dy: int | float) -> ComponentResult: + return ComponentResult( + start_port=self.start_port.translate(dx, dy), + collision_geometry=[shapely_translate(poly, dx, dy) for poly in self.collision_geometry], + end_port=self.end_port.translate(dx, dy), + length=self.length, + move_type=self.move_type, + move_spec=self.move_spec, + physical_geometry=[shapely_translate(poly, dx, dy) for poly in self.physical_geometry], + dilated_collision_geometry=[shapely_translate(poly, dx, dy) for poly in self.dilated_collision_geometry], + dilated_physical_geometry=[shapely_translate(poly, dx, dy) for poly in self.dilated_physical_geometry], + ) + + +def _combine_bounds(bounds_list: list[tuple[float, float, float, float]]) -> tuple[float, float, float, float]: + arr = numpy.asarray(bounds_list, dtype=numpy.float64) + return ( + float(arr[:, 0].min()), + float(arr[:, 1].min()), + float(arr[:, 2].max()), + float(arr[:, 3].max()), + ) + + +def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int: + if radius <= 0: + return 1 + ratio = max(0.0, min(1.0, 1.0 - sagitta / radius)) + theta_max = 2.0 * numpy.arccos(ratio) + if theta_max < TOLERANCE_ANGULAR: + return 16 + num = int(numpy.ceil(numpy.radians(abs(angle_deg)) / theta_max)) + return max(8, num) + + +def _get_arc_polygons( + cxy: tuple[float, float], + radius: float, + width: float, + ts: tuple[float, float], + sagitta: float = 0.01, + dilation: float = 0.0, +) -> list[Polygon]: + t_start, t_end = numpy.radians(ts[0]), numpy.radians(ts[1]) + num_segments = _get_num_segments(radius, abs(ts[1] - ts[0]), sagitta) + angles = numpy.linspace(t_start, t_end, num_segments + 1) + + cx, cy = cxy + inner_radius = radius - width / 2.0 - dilation + outer_radius = radius + width / 2.0 + dilation + + cos_a = numpy.cos(angles) + sin_a = numpy.sin(angles) + + inner_points = numpy.column_stack((cx + inner_radius * cos_a, cy + inner_radius * sin_a)) + outer_points = numpy.column_stack((cx + outer_radius * cos_a[::-1], cy + outer_radius * sin_a[::-1])) + return [Polygon(numpy.concatenate((inner_points, outer_points), axis=0))] + + +def _clip_bbox_legacy( + cxy: tuple[float, float], + radius: float, + width: float, + ts: tuple[float, float], + clip_margin: float, +) -> Polygon: + arc_poly = _get_arc_polygons(cxy, radius, width, ts)[0] + minx, miny, maxx, maxy = arc_poly.bounds + bbox_poly = box(minx, miny, maxx, maxy) + shrink = min(clip_margin, max(radius, width)) + return bbox_poly.buffer(-shrink, join_style="mitre") if shrink > 0 else bbox_poly + + +def _clip_bbox_polygonal(cxy: tuple[float, float], radius: float, width: float, ts: tuple[float, float]) -> Polygon: + """Return a conservative 8-point polygonal proxy for the arc. + + The polygon uses 4 points along the outer edge and 4 along the inner edge. + The outer edge is a circumscribed polyline and the inner edge is an + inscribed polyline, so the result conservatively contains the true arc. + """ + cx, cy = cxy + sample_count = 4 + angle_span = abs(float(ts[1]) - float(ts[0])) + if angle_span < TOLERANCE_ANGULAR: + return box(*_get_arc_polygons(cxy, radius, width, ts)[0].bounds) + + segment_half_angle = numpy.radians(angle_span / (2.0 * (sample_count - 1))) + cos_half = max(float(numpy.cos(segment_half_angle)), 1e-9) + + inner_radius = max(0.0, radius - width / 2.0) + outer_radius = radius + width / 2.0 + tolerance = max(1e-3, radius * 1e-4) + conservative_inner_radius = max(0.0, inner_radius * cos_half - tolerance) + conservative_outer_radius = outer_radius / cos_half + tolerance + + angles = numpy.radians(numpy.linspace(ts[0], ts[1], sample_count)) + cos_a = numpy.cos(angles) + sin_a = numpy.sin(angles) + + outer_points = numpy.column_stack((cx + conservative_outer_radius * cos_a, cy + conservative_outer_radius * sin_a)) + inner_points = numpy.column_stack((cx + conservative_inner_radius * cos_a[::-1], cy + conservative_inner_radius * sin_a[::-1])) + return Polygon(numpy.concatenate((outer_points, inner_points), axis=0)) + + +def _clip_bbox( + cxy: tuple[float, float], + radius: float, + width: float, + ts: tuple[float, float], + clip_margin: float | None, +) -> Polygon: + if clip_margin is not None: + return _clip_bbox_legacy(cxy, radius, width, ts, clip_margin) + return _clip_bbox_polygonal(cxy, radius, width, ts) + + +def _transform_custom_collision_polygon( + collision_poly: Polygon, + cxy: tuple[float, float], + rotation_deg: float, + mirror_y: bool, +) -> Polygon: + poly = collision_poly + if mirror_y: + poly = shapely_scale(poly, xfact=1.0, yfact=-1.0, origin=(0.0, 0.0)) + if rotation_deg % 360: + poly = shapely_rotate(poly, rotation_deg, origin=(0.0, 0.0), use_radians=False) + return shapely_translate(poly, cxy[0], cxy[1]) + + +def _apply_collision_model( + arc_poly: Polygon, + collision_type: BendCollisionModel, + radius: float, + width: float, + cxy: tuple[float, float], + ts: tuple[float, float], + clip_margin: float | None = None, + rotation_deg: float = 0.0, + mirror_y: bool = False, +) -> list[Polygon]: + if isinstance(collision_type, Polygon): + return [_transform_custom_collision_polygon(collision_type, cxy, rotation_deg, mirror_y)] + if collision_type == "arc": + return [arc_poly] + if collision_type == "clipped_bbox": + clipped = _clip_bbox(cxy, radius, width, ts, clip_margin) + return [clipped if not clipped.is_empty else box(*arc_poly.bounds)] + return [box(*arc_poly.bounds)] + + +class Straight: + @staticmethod + def generate( + start_port: Port, + length: float, + width: float, + dilation: float = 0.0, + ) -> ComponentResult: + rot2 = rotation_matrix2(start_port.r) + length_f = _normalize_length(length) + disp = rot2 @ numpy.array((length_f, 0.0)) + end_port = Port(start_port.x + disp[0], start_port.y + disp[1], start_port.r) + + half_w = width / 2.0 + pts = numpy.array(((0.0, half_w), (length_f, half_w), (length_f, -half_w), (0.0, -half_w))) + poly_points = (pts @ rot2.T) + numpy.array((start_port.x, start_port.y)) + geometry = [Polygon(poly_points)] + + if dilation > 0: + half_w_d = half_w + dilation + pts_d = numpy.array( + ( + (-dilation, half_w_d), + (length_f + dilation, half_w_d), + (length_f + dilation, -half_w_d), + (-dilation, -half_w_d), + ) + ) + poly_points_d = (pts_d @ rot2.T) + numpy.array((start_port.x, start_port.y)) + dilated_geometry = [Polygon(poly_points_d)] + else: + dilated_geometry = geometry + + return ComponentResult( + start_port=start_port, + collision_geometry=geometry, + end_port=end_port, + length=abs(length_f), + move_type="straight", + move_spec=StraightSeed(length=length_f), + physical_geometry=geometry, + dilated_collision_geometry=dilated_geometry, + dilated_physical_geometry=dilated_geometry, + ) + + +class Bend90: + @staticmethod + def generate( + start_port: Port, + radius: float, + width: float, + direction: Literal["CW", "CCW"], + sagitta: float = 0.01, + collision_type: BendCollisionModel = "arc", + physical_geometry_type: BendPhysicalGeometry = "arc", + clip_margin: float | None = None, + dilation: float = 0.0, + ) -> ComponentResult: + rot2 = rotation_matrix2(start_port.r) + sign = 1 if direction == "CCW" else -1 + + center_local = numpy.array((0.0, sign * radius)) + end_local = numpy.array((radius, sign * radius)) + center_xy = (rot2 @ center_local) + numpy.array((start_port.x, start_port.y)) + end_xy = (rot2 @ end_local) + numpy.array((start_port.x, start_port.y)) + end_port = Port(end_xy[0], end_xy[1], start_port.r + sign * 90) + + start_theta = start_port.r - sign * 90 + end_theta = start_port.r + ts = (float(start_theta), float(end_theta)) + + arc_polys = _get_arc_polygons((float(center_xy[0]), float(center_xy[1])), radius, width, ts, sagitta) + collision_polys = _apply_collision_model( + arc_polys[0], + collision_type, + radius, + width, + (float(center_xy[0]), float(center_xy[1])), + ts, + clip_margin=clip_margin, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), + ) + + if isinstance(physical_geometry_type, Polygon): + physical_geometry = _apply_collision_model( + arc_polys[0], + physical_geometry_type, + radius, + width, + (float(center_xy[0]), float(center_xy[1])), + ts, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), + ) + uses_physical_custom_geometry = True + else: + physical_geometry = arc_polys + uses_physical_custom_geometry = False + if dilation > 0: + if uses_physical_custom_geometry: + dilated_physical_geometry = [poly.buffer(dilation) for poly in physical_geometry] + else: + dilated_physical_geometry = _get_arc_polygons( + (float(center_xy[0]), float(center_xy[1])), + radius, + width, + ts, + sagitta, + dilation=dilation, + ) + dilated_collision_geometry = ( + dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in collision_polys] + ) + else: + dilated_physical_geometry = physical_geometry + dilated_collision_geometry = collision_polys + + return ComponentResult( + start_port=start_port, + collision_geometry=collision_polys, + end_port=end_port, + length=abs(radius) * numpy.pi / 2.0, + move_type="bend90", + move_spec=Bend90Seed(radius=radius, direction=direction), + physical_geometry=physical_geometry, + dilated_collision_geometry=dilated_collision_geometry, + dilated_physical_geometry=dilated_physical_geometry, + ) + + +class SBend: + @staticmethod + def generate( + start_port: Port, + offset: float, + radius: float, + width: float, + sagitta: float = 0.01, + collision_type: BendCollisionModel = "arc", + physical_geometry_type: BendPhysicalGeometry = "arc", + clip_margin: float | None = None, + dilation: float = 0.0, + ) -> ComponentResult: + if abs(offset) >= 2 * radius: + raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}") + + sign = 1 if offset >= 0 else -1 + theta = numpy.arccos(1.0 - abs(offset) / (2.0 * radius)) + dx = 2.0 * radius * numpy.sin(theta) + theta_deg = float(numpy.degrees(theta)) + + rot2 = rotation_matrix2(start_port.r) + end_local = numpy.array((dx, offset)) + end_xy = (rot2 @ end_local) + numpy.array((start_port.x, start_port.y)) + end_port = Port(end_xy[0], end_xy[1], start_port.r) + + c1_local = numpy.array((0.0, sign * radius)) + c2_local = numpy.array((dx, offset - sign * radius)) + c1_xy = (rot2 @ c1_local) + numpy.array((start_port.x, start_port.y)) + c2_xy = (rot2 @ c2_local) + numpy.array((start_port.x, start_port.y)) + + ts1 = (float(start_port.r - sign * 90), float(start_port.r - sign * 90 + sign * theta_deg)) + second_base = start_port.r + (90 if sign > 0 else 270) + ts2 = (float(second_base + sign * theta_deg), float(second_base)) + + arc1 = _get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta)[0] + arc2 = _get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta)[0] + actual_geometry = [arc1, arc2] + geometry = [ + _apply_collision_model( + arc1, + collision_type, + radius, + width, + (float(c1_xy[0]), float(c1_xy[1])), + ts1, + clip_margin=clip_margin, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), + )[0], + _apply_collision_model( + arc2, + collision_type, + radius, + width, + (float(c2_xy[0]), float(c2_xy[1])), + ts2, + clip_margin=clip_margin, + rotation_deg=float(start_port.r), + mirror_y=(sign > 0), + )[0], + ] + + if isinstance(physical_geometry_type, Polygon): + physical_geometry = [ + _apply_collision_model( + arc1, + physical_geometry_type, + radius, + width, + (float(c1_xy[0]), float(c1_xy[1])), + ts1, + rotation_deg=float(start_port.r), + mirror_y=(sign < 0), + )[0], + _apply_collision_model( + arc2, + physical_geometry_type, + radius, + width, + (float(c2_xy[0]), float(c2_xy[1])), + ts2, + rotation_deg=float(start_port.r), + mirror_y=(sign > 0), + )[0], + ] + uses_physical_custom_geometry = True + else: + physical_geometry = actual_geometry + uses_physical_custom_geometry = False + if dilation > 0: + if uses_physical_custom_geometry: + dilated_physical_geometry = [poly.buffer(dilation) for poly in physical_geometry] + else: + dilated_physical_geometry = [ + _get_arc_polygons((float(c1_xy[0]), float(c1_xy[1])), radius, width, ts1, sagitta, dilation=dilation)[0], + _get_arc_polygons((float(c2_xy[0]), float(c2_xy[1])), radius, width, ts2, sagitta, dilation=dilation)[0], + ] + dilated_collision_geometry = ( + dilated_physical_geometry if collision_type == "arc" else [poly.buffer(dilation) for poly in geometry] + ) + else: + dilated_physical_geometry = physical_geometry + dilated_collision_geometry = geometry + + return ComponentResult( + start_port=start_port, + collision_geometry=geometry, + end_port=end_port, + length=2.0 * radius * theta, + move_type="sbend", + move_spec=SBendSeed(offset=offset, radius=radius), + physical_geometry=physical_geometry, + dilated_collision_geometry=dilated_collision_geometry, + dilated_physical_geometry=dilated_physical_geometry, + ) diff --git a/inire/geometry/dynamic_path_index.py b/inire/geometry/dynamic_path_index.py new file mode 100644 index 0000000..70d4b9f --- /dev/null +++ b/inire/geometry/dynamic_path_index.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy +import rtree +from shapely.strtree import STRtree + +from inire.geometry.index_helpers import build_index_payload, iter_grid_cells + +if TYPE_CHECKING: + from collections.abc import Sequence + + from shapely.geometry import Polygon + + from inire.geometry.collision import RoutingWorld + + +class DynamicPathIndex: + __slots__ = ( + "engine", + "index", + "geometries", + "component_indexes", + "dilated", + "dilated_bounds", + "net_envelope_index", + "net_envelopes", + "net_envelope_obj_ids", + "net_envelope_obj_to_net", + "tree", + "obj_ids", + "grid", + "grid_net_obj_ids", + "grid_net_counts", + "obj_cells", + "net_to_obj_ids", + "id_counter", + "net_ids_array", + "bounds_array", + ) + + def __init__(self, engine: RoutingWorld) -> None: + self.engine = engine + self.index = rtree.index.Index() + self.geometries: dict[int, tuple[str, Polygon]] = {} + self.component_indexes: dict[int, int] = {} + self.dilated: dict[int, Polygon] = {} + self.dilated_bounds: dict[int, tuple[float, float, float, float]] = {} + self.net_envelope_index = rtree.index.Index() + self.net_envelopes: dict[str, tuple[float, float, float, float]] = {} + self.net_envelope_obj_ids: dict[str, int] = {} + self.net_envelope_obj_to_net: dict[int, str] = {} + self.tree: STRtree | None = None + self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32) + self.grid: dict[tuple[int, int], list[int]] = {} + self.grid_net_obj_ids: dict[tuple[int, int], dict[str, set[int]]] = {} + self.grid_net_counts: dict[tuple[int, int], dict[str, int]] = {} + self.obj_cells: dict[int, tuple[tuple[int, int], ...]] = {} + self.net_to_obj_ids: dict[str, set[int]] = {} + self.id_counter = 0 + self.net_ids_array = numpy.array([], dtype=object) + self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4) + + def _combine_net_bounds(self, obj_ids: set[int]) -> tuple[float, float, float, float]: + first_obj_id = next(iter(obj_ids)) + minx, miny, maxx, maxy = self.dilated_bounds[first_obj_id] + for obj_id in obj_ids: + bounds = self.dilated_bounds[obj_id] + minx = min(minx, bounds[0]) + miny = min(miny, bounds[1]) + maxx = max(maxx, bounds[2]) + maxy = max(maxy, bounds[3]) + return (minx, miny, maxx, maxy) + + def _set_net_envelope(self, net_id: str, bounds: tuple[float, float, float, float]) -> None: + old_bounds = self.net_envelopes.get(net_id) + if old_bounds is not None: + obj_id = self.net_envelope_obj_ids[net_id] + self.net_envelope_index.delete(obj_id, old_bounds) + else: + obj_id = len(self.net_envelope_obj_ids) + while obj_id in self.net_envelope_obj_to_net: + obj_id += 1 + self.net_envelope_obj_ids[net_id] = obj_id + self.net_envelope_obj_to_net[obj_id] = net_id + + self.net_envelopes[net_id] = bounds + self.net_envelope_index.insert(self.net_envelope_obj_ids[net_id], bounds) + + def _clear_net_envelope(self, net_id: str) -> None: + old_bounds = self.net_envelopes.pop(net_id, None) + obj_id = self.net_envelope_obj_ids.pop(net_id, None) + if old_bounds is None or obj_id is None: + return + self.net_envelope_index.delete(obj_id, old_bounds) + self.net_envelope_obj_to_net.pop(obj_id, None) + + def _refresh_net_envelope(self, net_id: str) -> None: + obj_ids = self.net_to_obj_ids.get(net_id) + if not obj_ids: + self._clear_net_envelope(net_id) + return + self._set_net_envelope(net_id, self._combine_net_bounds(obj_ids)) + + def invalidate_queries(self) -> None: + self.tree = None + self.grid = {} + self.grid_net_obj_ids = {} + self.grid_net_counts = {} + self.obj_cells = {} + + def ensure_tree(self) -> None: + 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) + self.tree = STRtree(geometries) + self.obj_ids = numpy.array(ids, dtype=numpy.int32) + self.bounds_array = bounds_array + net_ids = [self.geometries[obj_id][0] for obj_id in self.obj_ids] + self.net_ids_array = numpy.array(net_ids, dtype=object) + + def ensure_grid(self) -> None: + if self.grid or not self.dilated: + return + + if self.engine.metrics is not None: + self.engine.metrics.total_dynamic_grid_rebuilds += 1 + cell_size = self.engine.grid_cell_size + for obj_id, polygon in self.dilated.items(): + self._register_grid_membership(obj_id, self.geometries[obj_id][0], polygon.bounds, cell_size=cell_size) + + def _register_grid_membership( + self, + obj_id: int, + net_id: str, + bounds: tuple[float, float, float, float], + *, + cell_size: float, + ) -> None: + cells = tuple(iter_grid_cells(bounds, cell_size)) + self.obj_cells[obj_id] = cells + for cell in cells: + self.grid.setdefault(cell, []).append(obj_id) + net_obj_ids = self.grid_net_obj_ids.setdefault(cell, {}) + net_obj_ids.setdefault(net_id, set()).add(obj_id) + net_counts = self.grid_net_counts.setdefault(cell, {}) + net_counts[net_id] = net_counts.get(net_id, 0) + 1 + + def _unregister_grid_membership(self, obj_id: int, net_id: str) -> None: + cells = self.obj_cells.pop(obj_id, ()) + for cell in cells: + obj_ids = self.grid.get(cell) + if obj_ids is not None: + try: + obj_ids.remove(obj_id) + except ValueError: + pass + if not obj_ids: + self.grid.pop(cell, None) + net_obj_ids = self.grid_net_obj_ids.get(cell) + if net_obj_ids is not None: + member_ids = net_obj_ids.get(net_id) + if member_ids is not None: + member_ids.discard(obj_id) + if not member_ids: + net_obj_ids.pop(net_id, None) + if not net_obj_ids: + self.grid_net_obj_ids.pop(cell, None) + net_counts = self.grid_net_counts.get(cell) + if net_counts is not None: + remaining = net_counts.get(net_id, 0) - 1 + if remaining > 0: + net_counts[net_id] = remaining + else: + net_counts.pop(net_id, None) + if not net_counts: + self.grid_net_counts.pop(cell, None) + + def add_path( + self, + net_id: str, + geometry: Sequence[Polygon], + dilated_geometry: Sequence[Polygon], + component_indexes: Sequence[int] | None = None, + ) -> None: + if self.engine.metrics is not None: + self.engine.metrics.total_dynamic_path_objects_added += len(geometry) + cell_size = self.engine.grid_cell_size + for index, polygon in enumerate(geometry): + obj_id = self.id_counter + self.id_counter += 1 + dilated = dilated_geometry[index] + dilated_bounds = dilated.bounds + self.geometries[obj_id] = (net_id, polygon) + self.component_indexes[obj_id] = index if component_indexes is None else component_indexes[index] + self.dilated[obj_id] = dilated + self.dilated_bounds[obj_id] = dilated_bounds + self.index.insert(obj_id, dilated_bounds) + self.net_to_obj_ids.setdefault(net_id, set()).add(obj_id) + self._register_grid_membership(obj_id, net_id, dilated_bounds, cell_size=cell_size) + self._refresh_net_envelope(net_id) + self.tree = None + + def remove_path(self, net_id: str) -> None: + to_remove = list(self.net_to_obj_ids.get(net_id, ())) + self.remove_obj_ids(to_remove) + + def remove_obj_ids(self, obj_ids: list[int]) -> None: + if not obj_ids: + return + + if self.engine.metrics is not None: + self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids) + affected_nets: set[str] = set() + for obj_id in obj_ids: + net_id, _ = self.geometries[obj_id] + affected_nets.add(net_id) + self._unregister_grid_membership(obj_id, net_id) + self.index.delete(obj_id, self.dilated_bounds[obj_id]) + del self.geometries[obj_id] + del self.component_indexes[obj_id] + del self.dilated[obj_id] + del self.dilated_bounds[obj_id] + obj_id_set = self.net_to_obj_ids.get(net_id) + if obj_id_set is not None: + obj_id_set.discard(obj_id) + if not obj_id_set: + self.net_to_obj_ids.pop(net_id, None) + for net_id in affected_nets: + self._refresh_net_envelope(net_id) + self.tree = None diff --git a/inire/geometry/index_helpers.py b/inire/geometry/index_helpers.py new file mode 100644 index 0000000..201dafe --- /dev/null +++ b/inire/geometry/index_helpers.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import numpy + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + from shapely.geometry.base import BaseGeometry + + +def build_index_payload( + geometries: Mapping[int, BaseGeometry], +) -> tuple[list[int], list[BaseGeometry], numpy.ndarray]: + obj_ids = sorted(geometries) + ordered_geometries = [geometries[obj_id] for obj_id in obj_ids] + bounds_array = numpy.array([geometry.bounds for geometry in ordered_geometries], dtype=numpy.float64) + if not ordered_geometries: + bounds_array = bounds_array.reshape(0, 4) + return obj_ids, ordered_geometries, bounds_array + + +def grid_cell_span( + bounds: tuple[float, float, float, float], + cell_size: float, +) -> tuple[int, int, int, int]: + return ( + math.floor(bounds[0] / cell_size), + math.floor(bounds[1] / cell_size), + math.floor(bounds[2] / cell_size), + math.floor(bounds[3] / cell_size), + ) + + +def iter_grid_cells( + bounds: tuple[float, float, float, float], + cell_size: float, +) -> Iterator[tuple[int, int]]: + gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, cell_size) + for gx in range(gx_min, gx_max + 1): + for gy in range(gy_min, gy_max + 1): + yield (gx, gy) + + +def is_axis_aligned_rect(geometry: BaseGeometry, *, tolerance: float = 1e-4) -> bool: + bounds = geometry.bounds + area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1]) + return abs(geometry.area - area) < tolerance diff --git a/inire/geometry/primitives.py b/inire/geometry/primitives.py new file mode 100644 index 0000000..db61198 --- /dev/null +++ b/inire/geometry/primitives.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Self + +import numpy + +if TYPE_CHECKING: + from numpy.typing import NDArray + + +def _normalize_angle(angle_deg: int | float) -> int: + angle = int(round(angle_deg)) % 360 + if angle % 90 != 0: + raise ValueError(f"Port angle must be Manhattan (multiple of 90), got {angle_deg!r}") + return angle + +@dataclass(frozen=True, slots=True) +class Port: + """ + Port represented as a normalized integer (x, y, r) triple. + """ + + x: int | float + y: int | float + r: int | float + + def __post_init__(self) -> None: + object.__setattr__(self, "x", int(round(self.x))) + object.__setattr__(self, "y", int(round(self.y))) + object.__setattr__(self, "r", _normalize_angle(self.r)) + + def as_tuple(self) -> tuple[int, int, int]: + return (int(self.x), int(self.y), int(self.r)) + + def translate( + self, + dx: int | float = 0, + dy: int | float = 0, + rotation: int | float = 0, + ) -> Self: + return type(self)(self.x + dx, self.y + dy, self.r + rotation) + + def rotated( + self, + angle: int | float, + origin: tuple[int | float, int | float] = (0, 0), + ) -> Self: + angle_i = _normalize_angle(angle) + rot = rotation_matrix2(angle_i) + origin_xy = numpy.array((int(round(origin[0])), int(round(origin[1]))), dtype=numpy.int32) + rel = numpy.array((self.x, self.y), dtype=numpy.int32) - origin_xy + rotated = origin_xy + rot @ rel + return type(self)(int(rotated[0]), int(rotated[1]), self.r + angle_i) + + +ROT2_0 = numpy.array(((1, 0), (0, 1)), dtype=numpy.int32) +ROT2_90 = numpy.array(((0, -1), (1, 0)), dtype=numpy.int32) +ROT2_180 = numpy.array(((-1, 0), (0, -1)), dtype=numpy.int32) +ROT2_270 = numpy.array(((0, 1), (-1, 0)), dtype=numpy.int32) + + +def rotation_matrix2(rotation_deg: int | float) -> NDArray[numpy.int32]: + quadrant = (_normalize_angle(rotation_deg) // 90) % 4 + return (ROT2_0, ROT2_90, ROT2_180, ROT2_270)[quadrant] diff --git a/inire/geometry/static_obstacle_index.py b/inire/geometry/static_obstacle_index.py new file mode 100644 index 0000000..d0b4ad6 --- /dev/null +++ b/inire/geometry/static_obstacle_index.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy +import rtree +from shapely.strtree import STRtree + +from inire.geometry.index_helpers import build_index_payload, is_axis_aligned_rect + +if TYPE_CHECKING: + from shapely.geometry import Polygon + + from inire.geometry.collision import RoutingWorld + + +class StaticObstacleIndex: + __slots__ = ( + "engine", + "index", + "geometries", + "dilated", + "is_rect", + "tree", + "obj_ids", + "bounds_array", + "is_rect_array", + "raw_tree", + "raw_obj_ids", + "net_specific_trees", + "net_specific_is_rect", + "net_specific_bounds", + "id_counter", + "version", + ) + + def __init__(self, engine: RoutingWorld) -> None: + self.engine = engine + self.index = rtree.index.Index() + self.geometries: dict[int, Polygon] = {} + self.dilated: dict[int, Polygon] = {} + self.is_rect: dict[int, bool] = {} + self.tree: STRtree | None = None + self.obj_ids: list[int] = [] + self.bounds_array: numpy.ndarray | None = None + self.is_rect_array: numpy.ndarray | None = None + self.raw_tree: STRtree | None = None + self.raw_obj_ids: list[int] = [] + self.net_specific_trees: dict[tuple[float, float], STRtree] = {} + self.net_specific_is_rect: dict[tuple[float, float], numpy.ndarray] = {} + self.net_specific_bounds: dict[tuple[float, float], numpy.ndarray] = {} + self.id_counter = 0 + self.version = 0 + + def add_obstacle(self, polygon: Polygon, dilated_geometry: Polygon | None = None) -> int: + obj_id = self.id_counter + self.id_counter += 1 + + if dilated_geometry is not None: + dilated = dilated_geometry + else: + dilated = polygon.buffer(self.engine.clearance / 2.0, join_style="mitre") + + self.geometries[obj_id] = polygon + self.dilated[obj_id] = dilated + self.is_rect[obj_id] = is_axis_aligned_rect(dilated) + self.index.insert(obj_id, dilated.bounds) + self.invalidate_caches() + return obj_id + + def remove_obstacle(self, obj_id: int) -> None: + if obj_id not in self.geometries: + return + + bounds = self.dilated[obj_id].bounds + self.index.delete(obj_id, bounds) + del self.geometries[obj_id] + del self.dilated[obj_id] + del self.is_rect[obj_id] + self.invalidate_caches() + + def invalidate_caches(self) -> None: + self.tree = None + self.bounds_array = None + self.is_rect_array = None + self.obj_ids = [] + self.raw_tree = None + self.raw_obj_ids = [] + self.net_specific_trees.clear() + self.net_specific_is_rect.clear() + self.net_specific_bounds.clear() + self.version += 1 + + def ensure_tree(self) -> None: + 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.tree = STRtree(geometries) + self.is_rect_array = numpy.array([self.is_rect[i] for i in self.obj_ids]) + + def ensure_net_tree(self, net_width: float) -> STRtree: + key = (round(net_width, 4), round(self.engine.clearance, 4)) + if key in self.net_specific_trees: + 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 + geometries = [] + is_rect_list = [] + bounds_list = [] + + for obj_id in sorted(self.geometries.keys()): + polygon = self.geometries[obj_id] + dilated = polygon.buffer(total_dilation, join_style="mitre") + geometries.append(dilated) + bounds_list.append(dilated.bounds) + is_rect_list.append(is_axis_aligned_rect(dilated)) + + tree = STRtree(geometries) + self.net_specific_trees[key] = tree + self.net_specific_is_rect[key] = numpy.array(is_rect_list, dtype=bool) + self.net_specific_bounds[key] = numpy.array(bounds_list, dtype=numpy.float64) + return tree + + def ensure_raw_tree(self) -> None: + 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_tree = STRtree(geometries) diff --git a/inire/model.py b/inire/model.py new file mode 100644 index 0000000..f9c8020 --- /dev/null +++ b/inire/model.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import warnings +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from shapely.geometry import Polygon +from inire.seeds import PathSeed + +if TYPE_CHECKING: + from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry + from inire.geometry.primitives import Port + + +NetOrder = Literal["user", "shortest", "longest"] +VisibilityGuidance = Literal["off", "exact_corner", "tangent_corner"] + + +@dataclass(frozen=True, slots=True) +class NetSpec: + net_id: str + start: Port + target: Port + width: float = 2.0 + + +@dataclass(frozen=True, slots=True) +class ObjectiveWeights: + unit_length_cost: float = 1.0 + bend_penalty: float = 250.0 + sbend_penalty: float = 500.0 + danger_weight: float = 1.0 + + +@dataclass(frozen=True, slots=True) +class SearchOptions: + node_limit: int = 1000000 + max_straight_length: float = 2000.0 + min_straight_length: float = 5.0 + greedy_h_weight: float = 1.5 + sbend_offsets: tuple[float, ...] | None = None + bend_radii: tuple[float, ...] = (50.0, 100.0) + sbend_radii: tuple[float, ...] = (10.0,) + bend_collision_type: BendCollisionModel = "arc" + bend_proxy_geometry: BendCollisionModel | None = None + bend_physical_geometry: BendPhysicalGeometry | None = None + bend_clip_margin: float | None = None + visibility_guidance: VisibilityGuidance = "tangent_corner" + + def __post_init__(self) -> None: + object.__setattr__(self, "bend_radii", tuple(self.bend_radii)) + object.__setattr__(self, "sbend_radii", tuple(self.sbend_radii)) + if self.sbend_offsets is not None: + object.__setattr__(self, "sbend_offsets", tuple(self.sbend_offsets)) + if self.bend_physical_geometry is None and isinstance(self.bend_proxy_geometry, Polygon): + warnings.warn( + "Custom bend proxy provided without bend_physical_geometry; routed bends will keep standard arc geometry.", + stacklevel=2, + ) + + +def resolve_bend_geometry( + search: SearchOptions, + *, + bend_collision_override: BendCollisionModel | None = None, +) -> tuple[BendCollisionModel, BendPhysicalGeometry]: + bend_physical_geometry = search.bend_physical_geometry + if bend_physical_geometry is None and isinstance(search.bend_collision_type, Polygon) and search.bend_proxy_geometry is None: + bend_physical_geometry = search.bend_collision_type + if bend_physical_geometry is None: + bend_physical_geometry = "arc" + + if bend_collision_override is not None: + bend_proxy_geometry = bend_collision_override + elif search.bend_proxy_geometry is not None: + bend_proxy_geometry = search.bend_proxy_geometry + elif isinstance(search.bend_collision_type, Polygon): + bend_proxy_geometry = search.bend_collision_type + elif bend_physical_geometry != "arc" and search.bend_collision_type == "arc": + bend_proxy_geometry = bend_physical_geometry + else: + bend_proxy_geometry = search.bend_collision_type + + return bend_proxy_geometry, bend_physical_geometry + + +@dataclass(frozen=True, slots=True) +class CongestionOptions: + max_iterations: int = 10 + base_penalty: float = 100.0 + multiplier: float = 1.5 + use_tiered_strategy: bool = True + net_order: NetOrder = "user" + warm_start_enabled: bool = True + shuffle_nets: bool = False + seed: int | None = None + + +@dataclass(frozen=True, slots=True) +class RefinementOptions: + enabled: bool = True + objective: ObjectiveWeights | None = None + + +@dataclass(frozen=True, slots=True) +class DiagnosticsOptions: + capture_expanded: bool = False + capture_conflict_trace: bool = False + capture_frontier_trace: bool = False + capture_iteration_trace: bool = False + capture_pre_pair_frontier_trace: bool = False + + +@dataclass(frozen=True, slots=True) +class RoutingOptions: + search: SearchOptions = field(default_factory=SearchOptions) + objective: ObjectiveWeights = field(default_factory=ObjectiveWeights) + congestion: CongestionOptions = field(default_factory=CongestionOptions) + refinement: RefinementOptions = field(default_factory=RefinementOptions) + diagnostics: DiagnosticsOptions = field(default_factory=DiagnosticsOptions) + + +@dataclass(frozen=True, slots=True) +class RoutingProblem: + bounds: tuple[float, float, float, float] + nets: tuple[NetSpec, ...] = () + static_obstacles: tuple[Polygon, ...] = () + initial_paths: dict[str, PathSeed] = field(default_factory=dict) + clearance: float = 2.0 + safety_zone_radius: float = 0.0021 + + def __post_init__(self) -> None: + object.__setattr__(self, "nets", tuple(self.nets)) + object.__setattr__(self, "static_obstacles", tuple(self.static_obstacles)) + initial_paths = dict(self.initial_paths) + if any(not isinstance(seed, PathSeed) for seed in initial_paths.values()): + raise TypeError("RoutingProblem.initial_paths values must be PathSeed instances") + object.__setattr__( + self, + "initial_paths", + initial_paths, + ) diff --git a/inire/results.py b/inire/results.py new file mode 100644 index 0000000..d273756 --- /dev/null +++ b/inire/results.py @@ -0,0 +1,284 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from inire.seeds import PathSeed + +if TYPE_CHECKING: + from shapely.geometry import Polygon + + from inire.geometry.components import ComponentResult + + +RoutingOutcome = Literal["completed", "colliding", "partial", "unroutable"] +ConflictTraceStage = Literal["iteration", "restored_best", "final"] +FrontierTraceReason = Literal["closed_set", "hard_collision", "self_collision", "cost"] + + +@dataclass(frozen=True, slots=True) +class RoutingReport: + static_collision_count: int = 0 + dynamic_collision_count: int = 0 + self_collision_count: int = 0 + total_length: float = 0.0 + + @property + def collision_count(self) -> int: + return self.static_collision_count + self.dynamic_collision_count + self.self_collision_count + + @property + def is_valid(self) -> bool: + return self.collision_count == 0 + + +@dataclass(frozen=True, slots=True) +class ComponentConflictTrace: + other_net_id: str + self_component_index: int + other_component_index: int + + +@dataclass(frozen=True, slots=True) +class NetConflictTrace: + net_id: str + outcome: RoutingOutcome + reached_target: bool + report: RoutingReport + conflicting_net_ids: tuple[str, ...] = () + component_conflicts: tuple[ComponentConflictTrace, ...] = () + + +@dataclass(frozen=True, slots=True) +class ConflictTraceEntry: + stage: ConflictTraceStage + iteration: int | None + completed_net_ids: tuple[str, ...] + conflict_edges: tuple[tuple[str, str], ...] + nets: tuple[NetConflictTrace, ...] + + +@dataclass(frozen=True, slots=True) +class FrontierPruneSample: + reason: FrontierTraceReason + move_type: str + hotspot_index: int + parent_state: tuple[int, int, int] + end_state: tuple[int, int, int] + + +@dataclass(frozen=True, slots=True) +class NetFrontierTrace: + net_id: str + hotspot_bounds: tuple[tuple[float, float, float, float], ...] + pruned_closed_set: int + pruned_hard_collision: int + pruned_self_collision: int + pruned_cost: int + samples: tuple[FrontierPruneSample, ...] = () + + +@dataclass(frozen=True, slots=True) +class PrePairNetTrace: + net_id: str + nodes_expanded: int + congestion_check_calls: int + pruned_closed_set: int + pruned_cost: int + pruned_hard_collision: int + guidance_seed_present: bool + frontier: NetFrontierTrace + + +@dataclass(frozen=True, slots=True) +class PrePairFrontierTraceEntry: + iteration: int + routed_net_ids: tuple[str, ...] + conflict_edges: tuple[tuple[str, str], ...] + nets: tuple[PrePairNetTrace, ...] + + +@dataclass(frozen=True, slots=True) +class IterationNetAttemptTrace: + net_id: str + reached_target: bool + nodes_expanded: int + congestion_check_calls: int + pruned_closed_set: int + pruned_cost: int + pruned_hard_collision: int + guidance_seed_present: bool + + +@dataclass(frozen=True, slots=True) +class IterationTraceEntry: + iteration: int + congestion_penalty: float + routed_net_ids: tuple[str, ...] + completed_nets: int + conflict_edges: int + total_dynamic_collisions: int + nodes_expanded: int + congestion_check_calls: int + congestion_candidate_ids: int + congestion_exact_pair_checks: int + net_attempts: tuple[IterationNetAttemptTrace, ...] = () + + +@dataclass(frozen=True, slots=True) +class RouteMetrics: + nodes_expanded: int + moves_generated: int + moves_added: int + pruned_closed_set: int + pruned_hard_collision: 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 + iteration_reverify_calls: int + iteration_reverified_nets: int + iteration_conflicting_nets: int + iteration_conflict_edges: int + nets_carried_forward: int + score_component_calls: int + score_component_total_ns: int + path_cost_calls: int + danger_map_lookup_calls: int + danger_map_cache_hits: int + danger_map_cache_misses: int + danger_map_query_calls: int + danger_map_total_ns: int + move_cache_abs_hits: int + move_cache_abs_misses: int + move_cache_rel_hits: int + move_cache_rel_misses: int + guidance_match_moves: int + guidance_match_moves_straight: int + guidance_match_moves_bend90: int + guidance_match_moves_sbend: int + guidance_bonus_applied: float + guidance_bonus_applied_straight: float + guidance_bonus_applied_bend90: float + guidance_bonus_applied_sbend: float + static_safe_cache_hits: int + hard_collision_cache_hits: int + congestion_cache_hits: int + congestion_cache_misses: int + congestion_presence_cache_hits: int + congestion_presence_cache_misses: int + congestion_presence_skips: int + congestion_candidate_precheck_hits: int + congestion_candidate_precheck_misses: int + congestion_candidate_precheck_skips: int + congestion_grid_net_cache_hits: int + congestion_grid_net_cache_misses: int + congestion_grid_span_cache_hits: int + congestion_grid_span_cache_misses: int + congestion_candidate_nets: int + congestion_net_envelope_cache_hits: int + congestion_net_envelope_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_corner_index_builds: int + visibility_builds: int + visibility_corner_pairs_checked: int + visibility_corner_queries_exact: int + visibility_corner_hits_exact: int + visibility_point_queries: int + visibility_point_cache_hits: int + visibility_point_cache_misses: int + visibility_tangent_candidate_scans: int + visibility_tangent_candidate_corner_checks: int + visibility_tangent_candidate_ray_tests: int + ray_cast_calls: int + ray_cast_calls_straight_static: int + ray_cast_calls_expand_snap: int + ray_cast_calls_expand_forward: int + ray_cast_calls_visibility_build: int + ray_cast_calls_visibility_query: int + ray_cast_calls_visibility_tangent: int + ray_cast_calls_other: int + ray_cast_candidate_bounds: int + ray_cast_exact_geometry_checks: int + congestion_check_calls: int + congestion_lazy_resolutions: int + congestion_lazy_requeues: int + congestion_candidate_ids: int + congestion_exact_pair_checks: int + verify_path_report_calls: int + verify_static_buffer_ops: int + verify_dynamic_candidate_nets: int + verify_dynamic_exact_pair_checks: int + refinement_windows_considered: int + refinement_static_bounds_checked: int + refinement_dynamic_bounds_checked: int + refinement_candidate_side_extents: int + refinement_candidates_built: int + refinement_candidates_verified: int + refinement_candidates_accepted: int + pair_local_search_pairs_considered: int + pair_local_search_attempts: int + pair_local_search_accepts: int + pair_local_search_nodes_expanded: int + late_phase_capped_nets: int + late_phase_capped_fallbacks: int + + +@dataclass(frozen=True, slots=True) +class RoutingResult: + net_id: str + path: tuple[ComponentResult, ...] + reached_target: bool = False + report: RoutingReport = field(default_factory=RoutingReport) + + def __post_init__(self) -> None: + object.__setattr__(self, "path", tuple(self.path)) + + @property + def collisions(self) -> int: + return self.report.collision_count + + @property + def outcome(self) -> RoutingOutcome: + if not self.path: + return "unroutable" + if not self.reached_target: + return "partial" + if self.report.collision_count > 0: + return "colliding" + return "completed" + + @property + def is_valid(self) -> bool: + return self.outcome == "completed" + + @property + def locked_geometry(self) -> tuple[Polygon, ...]: + polygons: list[Polygon] = [] + for component in self.path: + polygons.extend(component.physical_geometry) + return tuple(polygons) + + def as_seed(self) -> PathSeed: + return PathSeed(tuple(component.move_spec for component in self.path)) + + +@dataclass(frozen=True, slots=True) +class RoutingRunResult: + results_by_net: dict[str, RoutingResult] + metrics: RouteMetrics + expanded_nodes: tuple[tuple[int, int, int], ...] = () + conflict_trace: tuple[ConflictTraceEntry, ...] = () + frontier_trace: tuple[NetFrontierTrace, ...] = () + pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None + iteration_trace: tuple[IterationTraceEntry, ...] = () diff --git a/inire/router/_astar_admission.py b/inire/router/_astar_admission.py new file mode 100644 index 0000000..bb07505 --- /dev/null +++ b/inire/router/_astar_admission.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +import heapq +from typing import TYPE_CHECKING + +from shapely.geometry import Polygon + +from inire.constants import TOLERANCE_LINEAR +from inire.geometry.components import Bend90, SBend, Straight, MoveKind +from inire.geometry.primitives import Port +from inire.router.refiner import component_hits_ancestor_chain + +from ._astar_types import AStarContext, AStarMetrics, AStarNode, SearchRunConfig + +if TYPE_CHECKING: + from inire.geometry.components import ComponentResult + + +def process_move( + parent: AStarNode, + target: Port, + net_width: float, + net_id: str, + open_set: list[AStarNode], + closed_set: dict[tuple[int, int, int], float], + context: AStarContext, + metrics: AStarMetrics, + congestion_cache: dict[tuple, int], + congestion_presence_cache: dict[tuple[str, int, int, int, int], bool], + congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool], + congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]], + config: SearchRunConfig, + move_class: MoveKind, + params: tuple, +) -> None: + cp = parent.port + coll_type = config.bend_collision_type + coll_key = id(coll_type) if isinstance(coll_type, Polygon) else coll_type + physical_type = config.bend_physical_geometry + physical_key = id(physical_type) if isinstance(physical_type, Polygon) else physical_type + self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0 + + abs_key = ( + cp.as_tuple(), + move_class, + params, + net_width, + coll_key, + physical_key, + self_dilation, + ) + if abs_key in context.move_cache_abs: + context.metrics.total_move_cache_abs_hits += 1 + res = context.move_cache_abs[abs_key] + else: + context.metrics.total_move_cache_abs_misses += 1 + context.check_cache_eviction() + base_port = Port(0, 0, cp.r) + rel_key = ( + cp.r, + move_class, + params, + net_width, + coll_key, + physical_key, + self_dilation, + ) + if rel_key in context.move_cache_rel: + context.metrics.total_move_cache_rel_hits += 1 + res_rel = context.move_cache_rel[rel_key] + else: + context.metrics.total_move_cache_rel_misses += 1 + try: + if move_class == "straight": + res_rel = Straight.generate(base_port, params[0], net_width, dilation=self_dilation) + elif move_class == "bend90": + res_rel = Bend90.generate( + base_port, + params[0], + net_width, + params[1], + collision_type=coll_type, + physical_geometry_type=config.bend_physical_geometry, + clip_margin=config.bend_clip_margin, + dilation=self_dilation, + ) + else: + res_rel = SBend.generate( + base_port, + params[0], + params[1], + net_width, + collision_type=coll_type, + physical_geometry_type=config.bend_physical_geometry, + clip_margin=config.bend_clip_margin, + dilation=self_dilation, + ) + except ValueError: + return + context.move_cache_rel[rel_key] = res_rel + res = res_rel.translate(cp.x, cp.y) + context.move_cache_abs[abs_key] = res + + add_node( + parent, + res, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config, + move_class, + abs_key, + ) + + +def add_node( + parent: AStarNode, + result: ComponentResult, + target: Port, + net_width: float, + net_id: str, + open_set: list[AStarNode], + closed_set: dict[tuple[int, int, int], float], + context: AStarContext, + metrics: AStarMetrics, + congestion_cache: dict[tuple, int], + congestion_presence_cache: dict[tuple[str, int, int, int, int], bool], + congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool], + congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]], + config: SearchRunConfig, + move_type: MoveKind, + cache_key: tuple, +) -> None: + frontier_trace = config.frontier_trace + metrics.moves_generated += 1 + metrics.total_moves_generated += 1 + state = result.end_port.as_tuple() + new_lower_bound_g = parent.g_cost + result.length + if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR: + if frontier_trace is not None: + frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_closed_set += 1 + metrics.total_pruned_closed_set += 1 + return + + parent_p = parent.port + end_p = result.end_port + + if cache_key in context.hard_collision_set: + if frontier_trace is not None: + frontier_trace.record("hard_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + context.metrics.total_hard_collision_cache_hits += 1 + metrics.pruned_hard_collision += 1 + metrics.total_pruned_hard_collision += 1 + return + + 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: + ce = context.cost_evaluator.collision_engine + if move_type == "straight": + collision_found = ce.check_move_straight_static(parent_p, result.length, net_width=net_width) + else: + collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p) + if collision_found: + context.hard_collision_set.add(cache_key) + if frontier_trace is not None: + frontier_trace.record("hard_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_hard_collision += 1 + metrics.total_pruned_hard_collision += 1 + return + context.static_safe_cache.add(cache_key) + + if config.self_collision_check and component_hits_ancestor_chain(result, parent): + if frontier_trace is not None: + frontier_trace.record("self_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + return + + move_cost = context.cost_evaluator.score_component( + result, + start_port=parent_p, + ) + next_seed_index = None + if ( + config.guidance_seed is not None + and parent.seed_index is not None + and parent.seed_index < len(config.guidance_seed) + and result.move_spec == config.guidance_seed[parent.seed_index] + ): + context.metrics.total_guidance_match_moves += 1 + if result.move_type == "straight": + context.metrics.total_guidance_match_moves_straight += 1 + applied_bonus = config.guidance_bonus + context.metrics.total_guidance_bonus_applied_straight += applied_bonus + elif result.move_type == "bend90": + context.metrics.total_guidance_match_moves_bend90 += 1 + applied_bonus = config.guidance_bonus + context.metrics.total_guidance_bonus_applied_bend90 += applied_bonus + else: + context.metrics.total_guidance_match_moves_sbend += 1 + applied_bonus = config.guidance_bonus + context.metrics.total_guidance_bonus_applied_sbend += applied_bonus + context.metrics.total_guidance_bonus_applied += applied_bonus + move_cost = max(0.001, move_cost - applied_bonus) + next_seed_index = parent.seed_index + 1 + + if config.max_cost is not None and parent.g_cost + move_cost > config.max_cost: + if frontier_trace is not None: + frontier_trace.record("cost", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_cost += 1 + metrics.total_pruned_cost += 1 + return + if move_cost > 1e12: + if frontier_trace is not None: + frontier_trace.record("cost", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_cost += 1 + metrics.total_pruned_cost += 1 + return + + if state in closed_set and closed_set[state] <= parent.g_cost + move_cost + TOLERANCE_LINEAR: + if frontier_trace is not None: + frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_closed_set += 1 + metrics.total_pruned_closed_set += 1 + return + + total_overlaps = 0 + if not config.skip_congestion and context.cost_evaluator.collision_engine.has_dynamic_paths(): + ce = context.cost_evaluator.collision_engine + if ce.has_possible_move_congestion(result, net_id, congestion_presence_cache): + if ce.has_candidate_move_congestion( + result, + net_id, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + ): + if cache_key in congestion_cache: + context.metrics.total_congestion_cache_hits += 1 + total_overlaps = congestion_cache[cache_key] + else: + context.metrics.total_congestion_cache_misses += 1 + total_overlaps = ce.check_move_congestion( + result, + net_id, + net_envelope_cache=congestion_net_envelope_cache, + grid_net_cache=congestion_grid_net_cache, + broad_phase_cache=congestion_grid_span_cache, + ) + congestion_cache[cache_key] = total_overlaps + else: + context.metrics.total_congestion_candidate_precheck_skips += 1 + else: + context.metrics.total_congestion_presence_skips += 1 + move_cost += total_overlaps * context.congestion_penalty + + g_cost = parent.g_cost + move_cost + if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR: + if frontier_trace is not None: + frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds) + metrics.pruned_closed_set += 1 + metrics.total_pruned_closed_set += 1 + return + + h_cost = context.cost_evaluator.h_manhattan( + result.end_port, + target, + min_bend_radius=context.min_bend_radius, + ) + heapq.heappush( + open_set, + AStarNode( + result.end_port, + g_cost, + h_cost, + parent, + result, + seed_index=next_seed_index, + ), + ) + metrics.moves_added += 1 + metrics.total_moves_added += 1 diff --git a/inire/router/_astar_moves.py b/inire/router/_astar_moves.py new file mode 100644 index 0000000..8b8a60c --- /dev/null +++ b/inire/router/_astar_moves.py @@ -0,0 +1,362 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +from inire.constants import TOLERANCE_LINEAR + +from ._astar_admission import process_move + +if TYPE_CHECKING: + from inire.geometry.components import MoveKind + from inire.geometry.primitives import Port + from ._astar_types import AStarContext, AStarMetrics, AStarNode, SearchRunConfig + + +def _quantized_lengths(values: list[float], max_reach: float) -> list[int]: + out = {int(round(v)) for v in values if v > 0 and v <= max_reach + 0.01} + return sorted((v for v in out if v > 0), reverse=True) + + +def _distance_to_bounds_in_heading( + current: Port, + bounds: tuple[float, float, float, float], +) -> float: + min_x, min_y, max_x, max_y = bounds + if current.r == 0: + return max(0.0, max_x - current.x) + if current.r == 90: + return max(0.0, max_y - current.y) + if current.r == 180: + return max(0.0, current.x - min_x) + return max(0.0, current.y - min_y) + + +def _should_cap_straights_to_bounds(context: AStarContext) -> bool: + return ( + not context.options.congestion.warm_start_enabled + and len(context.problem.nets) >= 8 + ) + + +def _sbend_forward_span(offset: float, radius: float) -> float | None: + abs_offset = abs(offset) + if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius: + return None + theta = math.acos(1.0 - abs_offset / (2.0 * radius)) + return 2.0 * radius * math.sin(theta) + + +def _visible_straight_candidates( + current: Port, + context: AStarContext, + max_reach: float, + cos_v: float, + sin_v: float, + net_width: float, +) -> list[float]: + search_options = context.options.search + mode = search_options.visibility_guidance + if mode == "off": + return [] + + if mode == "exact_corner": + max_bend_radius = max(search_options.bend_radii, default=0.0) + visibility_reach = max_reach + max_bend_radius + visible_corners = sorted( + context.visibility_manager.get_corner_visibility(current, max_dist=visibility_reach), + key=lambda corner: corner[2], + ) + if not visible_corners: + return [] + + candidates: set[int] = set() + for cx, cy, _ in visible_corners[:12]: + dx = cx - current.x + dy = cy - current.y + local_x = dx * cos_v + dy * sin_v + if local_x <= search_options.min_straight_length: + continue + candidates.add(int(round(local_x))) + return sorted(candidates, reverse=True) + + if mode != "tangent_corner": + return [] + + visibility_manager = context.visibility_manager + visibility_manager.ensure_corner_index_current() + context.metrics.total_visibility_tangent_candidate_scans += 1 + max_bend_radius = max(search_options.bend_radii, default=0.0) + if max_bend_radius <= 0 or not visibility_manager.corners: + return [] + + reach = max_reach + max_bend_radius + candidate_ids = visibility_manager.get_tangent_corner_candidates( + current, + min_forward=search_options.min_straight_length, + max_forward=reach, + radii=search_options.bend_radii, + ) + if not candidate_ids: + return [] + + scored: list[tuple[float, float, float, float, float]] = [] + for idx in candidate_ids: + context.metrics.total_visibility_tangent_candidate_corner_checks += 1 + cx, cy = visibility_manager.corners[idx] + dx = cx - current.x + dy = cy - current.y + local_x = dx * cos_v + dy * sin_v + local_y = -dx * sin_v + dy * cos_v + if local_x <= search_options.min_straight_length or local_x > reach + 0.01: + continue + + nearest_radius = min(search_options.bend_radii, key=lambda radius: abs(abs(local_y) - radius)) + tangent_error = abs(abs(local_y) - nearest_radius) + if tangent_error > 2.0: + continue + + length = local_x - nearest_radius + if length <= search_options.min_straight_length or length > max_reach + 0.01: + continue + + scored.append((tangent_error, math.hypot(dx, dy), length, dx, dy)) + + if not scored: + return [] + + collision_engine = context.cost_evaluator.collision_engine + tangent_candidates: set[int] = set() + for _, dist, length, dx, dy in sorted(scored)[:4]: + context.metrics.total_visibility_tangent_candidate_ray_tests += 1 + angle = math.degrees(math.atan2(dy, dx)) + corner_reach = collision_engine.ray_cast( + current, + angle, + max_dist=dist + 0.05, + net_width=net_width, + caller="visibility_tangent", + ) + if corner_reach < dist - 0.01: + continue + qlen = int(round(length)) + if qlen > 0: + tangent_candidates.add(qlen) + + return sorted(tangent_candidates, reverse=True) + + +def _previous_move_metadata(node: AStarNode) -> tuple[MoveKind | None, float | None]: + result = node.component_result + if result is None: + return None, None + move_type = result.move_type + if move_type == "straight": + return move_type, result.length + return move_type, None + + +def expand_moves( + current: AStarNode, + target: Port, + net_width: float, + net_id: str, + open_set: list[AStarNode], + closed_set: dict[tuple[int, int, int], float], + context: AStarContext, + metrics: AStarMetrics, + congestion_cache: dict[tuple, int], + congestion_presence_cache: dict[tuple[str, int, int, int, int], bool], + congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool], + congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]], + congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]], + config: SearchRunConfig, +) -> None: + search_options = context.options.search + cp = current.port + prev_move_type, prev_straight_length = _previous_move_metadata(current) + dx_t = target.x - cp.x + dy_t = target.y - cp.y + dist_sq = dx_t * dx_t + dy_t * dy_t + + if cp.r == 0: + cos_v, sin_v = 1.0, 0.0 + elif cp.r == 90: + cos_v, sin_v = 0.0, 1.0 + elif cp.r == 180: + cos_v, sin_v = -1.0, 0.0 + else: + cos_v, sin_v = 0.0, -1.0 + + proj_t = dx_t * cos_v + dy_t * sin_v + perp_t = -dx_t * sin_v + dy_t * cos_v + dx_local = proj_t + dy_local = perp_t + + if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r: + max_reach = context.cost_evaluator.collision_engine.ray_cast( + cp, + cp.r, + proj_t + 1.0, + net_width=net_width, + caller="expand_snap", + ) + if max_reach >= proj_t - 0.01 and ( + prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR + ): + process_move( + current, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config, + "straight", + (int(round(proj_t)),), + ) + + max_reach = context.cost_evaluator.collision_engine.ray_cast( + cp, + cp.r, + search_options.max_straight_length, + net_width=net_width, + caller="expand_forward", + ) + if _should_cap_straights_to_bounds(context): + max_reach = min(max_reach, _distance_to_bounds_in_heading(cp, context.problem.bounds)) + candidate_lengths = [ + search_options.min_straight_length, + max_reach, + max_reach / 2.0, + max_reach - 5.0, + ] + + axis_target_dist = abs(dx_t) if cp.r in (0, 180) else abs(dy_t) + candidate_lengths.append(axis_target_dist) + for radius in search_options.bend_radii: + candidate_lengths.extend((max_reach - radius, axis_target_dist - radius, axis_target_dist - 2.0 * radius)) + + candidate_lengths.extend( + _visible_straight_candidates( + cp, + context, + max_reach, + cos_v, + sin_v, + net_width, + ) + ) + + if cp.r == target.r and dx_local > 0 and abs(dy_local) > TOLERANCE_LINEAR: + for radius in search_options.sbend_radii: + sbend_span = _sbend_forward_span(dy_local, radius) + if sbend_span is None: + continue + candidate_lengths.extend((dx_local - sbend_span, dx_local - 2.0 * sbend_span)) + + for length in _quantized_lengths(candidate_lengths, max_reach): + if length < search_options.min_straight_length: + continue + if prev_straight_length is not None and length >= prev_straight_length - TOLERANCE_LINEAR: + continue + process_move( + current, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config, + "straight", + (length,), + ) + + angle_to_target = 0.0 + if dx_t != 0 or dy_t != 0: + angle_to_target = float((round((180.0 / math.pi) * math.atan2(dy_t, dx_t)) + 360.0) % 360.0) + allow_backwards = dist_sq < 150 * 150 + + for radius in search_options.bend_radii: + for direction in ("CW", "CCW"): + if not allow_backwards: + turn = 90 if direction == "CCW" else -90 + new_ori = (cp.r + turn) % 360 + new_diff = (angle_to_target - new_ori + 180.0) % 360.0 - 180.0 + if abs(new_diff) > 135.0: + continue + process_move( + current, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config, + "bend90", + (radius, direction), + ) + + max_sbend_r = max(search_options.sbend_radii) if search_options.sbend_radii else 0.0 + if max_sbend_r <= 0 or prev_move_type == "sbend": + return + + explicit_offsets = search_options.sbend_offsets + offsets: set[int] = {int(round(v)) for v in explicit_offsets or []} + + if target.r == cp.r and 0 < dx_local <= 4 * max_sbend_r and 0 < abs(dy_local) < 2 * max_sbend_r: + offsets.add(int(round(dy_local))) + + if not offsets: + return + + for offset in sorted(offsets): + if offset == 0: + continue + for radius in search_options.sbend_radii: + if abs(offset) >= 2 * radius: + continue + process_move( + current, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config, + "sbend", + (offset, radius), + ) diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py new file mode 100644 index 0000000..1c3b7a1 --- /dev/null +++ b/inire/router/_astar_types.py @@ -0,0 +1,674 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from inire.model import resolve_bend_geometry +from inire.results import RouteMetrics +from inire.router.visibility import VisibilityManager + +if TYPE_CHECKING: + from inire.geometry.components import BendCollisionModel, BendPhysicalGeometry, ComponentResult + from inire.geometry.primitives import Port + from inire.model import RoutingOptions, RoutingProblem + from inire.router.cost import CostEvaluator + from inire.seeds import PathSegmentSeed + + +@dataclass(slots=True) +class FrontierTraceCollector: + hotspot_bounds: tuple[tuple[float, float, float, float], ...] + sample_limit: int = 64 + pruned_closed_set: int = 0 + pruned_hard_collision: int = 0 + pruned_self_collision: int = 0 + pruned_cost: int = 0 + samples: list[tuple[str, str, int, tuple[int, int, int], tuple[int, int, int]]] = field(default_factory=list) + + def _matching_hotspot_index(self, bounds: tuple[float, float, float, float]) -> int | None: + for idx, hotspot_bounds in enumerate(self.hotspot_bounds): + if ( + bounds[0] < hotspot_bounds[2] + and bounds[2] > hotspot_bounds[0] + and bounds[1] < hotspot_bounds[3] + and bounds[3] > hotspot_bounds[1] + ): + return idx + return None + + def record( + self, + reason: str, + move_type: str, + parent_state: tuple[int, int, int], + end_state: tuple[int, int, int], + bounds: tuple[float, float, float, float], + ) -> None: + hotspot_index = self._matching_hotspot_index(bounds) + if hotspot_index is None: + return + if reason == "closed_set": + self.pruned_closed_set += 1 + elif reason == "hard_collision": + self.pruned_hard_collision += 1 + elif reason == "self_collision": + self.pruned_self_collision += 1 + else: + self.pruned_cost += 1 + if len(self.samples) < self.sample_limit: + self.samples.append((reason, move_type, hotspot_index, parent_state, end_state)) + + +@dataclass(frozen=True, slots=True) +class SearchRunConfig: + bend_collision_type: BendCollisionModel + bend_physical_geometry: BendPhysicalGeometry + bend_clip_margin: float | None + node_limit: int + guidance_seed: tuple[PathSegmentSeed, ...] | None = None + guidance_bonus: float = 0.0 + frontier_trace: FrontierTraceCollector | None = None + return_partial: bool = False + store_expanded: bool = False + skip_congestion: bool = False + max_cost: float | None = None + self_collision_check: bool = False + + @classmethod + def from_options( + cls, + options: RoutingOptions, + *, + bend_collision_type: BendCollisionModel | None = None, + node_limit: int | None = None, + guidance_seed: tuple[PathSegmentSeed, ...] | None = None, + guidance_bonus: float = 0.0, + frontier_trace: FrontierTraceCollector | None = None, + return_partial: bool = False, + store_expanded: bool = False, + skip_congestion: bool = False, + max_cost: float | None = None, + self_collision_check: bool = False, + ) -> SearchRunConfig: + search = options.search + bend_collision_type, bend_physical_geometry = resolve_bend_geometry( + search, + bend_collision_override=bend_collision_type, + ) + return cls( + bend_collision_type=bend_collision_type, + bend_physical_geometry=bend_physical_geometry, + bend_clip_margin=search.bend_clip_margin, + node_limit=search.node_limit if node_limit is None else node_limit, + guidance_seed=guidance_seed, + guidance_bonus=float(guidance_bonus), + frontier_trace=frontier_trace, + return_partial=return_partial, + store_expanded=store_expanded, + skip_congestion=skip_congestion, + max_cost=max_cost, + self_collision_check=self_collision_check, + ) + + +class AStarNode: + __slots__ = ( + "port", + "g_cost", + "h_cost", + "fh_cost", + "parent", + "component_result", + "base_move_cost", + "cache_key", + "seed_index", + "congestion_resolved", + ) + + def __init__( + self, + port: Port, + g_cost: float, + h_cost: float, + parent: AStarNode | None = None, + component_result: ComponentResult | None = None, + *, + base_move_cost: float = 0.0, + cache_key: tuple | None = None, + seed_index: int | None = None, + congestion_resolved: bool = True, + ) -> None: + self.port = port + self.g_cost = g_cost + self.h_cost = h_cost + self.fh_cost = (g_cost + h_cost, h_cost) + self.parent = parent + self.component_result = component_result + self.base_move_cost = base_move_cost + self.cache_key = cache_key + self.seed_index = seed_index + self.congestion_resolved = congestion_resolved + + def __lt__(self, other: AStarNode) -> bool: + return self.fh_cost < other.fh_cost + + +class AStarMetrics: + __slots__ = ( + "total_nodes_expanded", + "total_moves_generated", + "total_moves_added", + "total_pruned_closed_set", + "total_pruned_hard_collision", + "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_iteration_reverify_calls", + "total_iteration_reverified_nets", + "total_iteration_conflicting_nets", + "total_iteration_conflict_edges", + "total_nets_carried_forward", + "total_score_component_calls", + "total_score_component_total_ns", + "total_path_cost_calls", + "total_danger_map_lookup_calls", + "total_danger_map_cache_hits", + "total_danger_map_cache_misses", + "total_danger_map_query_calls", + "total_danger_map_total_ns", + "total_move_cache_abs_hits", + "total_move_cache_abs_misses", + "total_move_cache_rel_hits", + "total_move_cache_rel_misses", + "total_guidance_match_moves", + "total_guidance_match_moves_straight", + "total_guidance_match_moves_bend90", + "total_guidance_match_moves_sbend", + "total_guidance_bonus_applied", + "total_guidance_bonus_applied_straight", + "total_guidance_bonus_applied_bend90", + "total_guidance_bonus_applied_sbend", + "total_static_safe_cache_hits", + "total_hard_collision_cache_hits", + "total_congestion_cache_hits", + "total_congestion_cache_misses", + "total_congestion_presence_cache_hits", + "total_congestion_presence_cache_misses", + "total_congestion_presence_skips", + "total_congestion_candidate_precheck_hits", + "total_congestion_candidate_precheck_misses", + "total_congestion_candidate_precheck_skips", + "total_congestion_grid_net_cache_hits", + "total_congestion_grid_net_cache_misses", + "total_congestion_grid_span_cache_hits", + "total_congestion_grid_span_cache_misses", + "total_congestion_candidate_nets", + "total_congestion_net_envelope_cache_hits", + "total_congestion_net_envelope_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_corner_index_builds", + "total_visibility_builds", + "total_visibility_corner_pairs_checked", + "total_visibility_corner_queries_exact", + "total_visibility_corner_hits_exact", + "total_visibility_point_queries", + "total_visibility_point_cache_hits", + "total_visibility_point_cache_misses", + "total_visibility_tangent_candidate_scans", + "total_visibility_tangent_candidate_corner_checks", + "total_visibility_tangent_candidate_ray_tests", + "total_ray_cast_calls", + "total_ray_cast_calls_straight_static", + "total_ray_cast_calls_expand_snap", + "total_ray_cast_calls_expand_forward", + "total_ray_cast_calls_visibility_build", + "total_ray_cast_calls_visibility_query", + "total_ray_cast_calls_visibility_tangent", + "total_ray_cast_calls_other", + "total_ray_cast_candidate_bounds", + "total_ray_cast_exact_geometry_checks", + "total_congestion_check_calls", + "total_congestion_lazy_resolutions", + "total_congestion_lazy_requeues", + "total_congestion_candidate_ids", + "total_congestion_exact_pair_checks", + "total_verify_path_report_calls", + "total_verify_static_buffer_ops", + "total_verify_dynamic_candidate_nets", + "total_verify_dynamic_exact_pair_checks", + "total_refinement_windows_considered", + "total_refinement_static_bounds_checked", + "total_refinement_dynamic_bounds_checked", + "total_refinement_candidate_side_extents", + "total_refinement_candidates_built", + "total_refinement_candidates_verified", + "total_refinement_candidates_accepted", + "total_pair_local_search_pairs_considered", + "total_pair_local_search_attempts", + "total_pair_local_search_accepts", + "total_pair_local_search_nodes_expanded", + "total_late_phase_capped_nets", + "total_late_phase_capped_fallbacks", + "last_expanded_nodes", + "nodes_expanded", + "moves_generated", + "moves_added", + "pruned_closed_set", + "pruned_hard_collision", + "pruned_cost", + ) + + def __init__(self) -> None: + self.total_nodes_expanded = 0 + self.total_moves_generated = 0 + self.total_moves_added = 0 + self.total_pruned_closed_set = 0 + self.total_pruned_hard_collision = 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_iteration_reverify_calls = 0 + self.total_iteration_reverified_nets = 0 + self.total_iteration_conflicting_nets = 0 + self.total_iteration_conflict_edges = 0 + self.total_nets_carried_forward = 0 + self.total_score_component_calls = 0 + self.total_score_component_total_ns = 0 + self.total_path_cost_calls = 0 + self.total_danger_map_lookup_calls = 0 + self.total_danger_map_cache_hits = 0 + self.total_danger_map_cache_misses = 0 + self.total_danger_map_query_calls = 0 + self.total_danger_map_total_ns = 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_guidance_match_moves = 0 + self.total_guidance_match_moves_straight = 0 + self.total_guidance_match_moves_bend90 = 0 + self.total_guidance_match_moves_sbend = 0 + self.total_guidance_bonus_applied = 0.0 + self.total_guidance_bonus_applied_straight = 0.0 + self.total_guidance_bonus_applied_bend90 = 0.0 + self.total_guidance_bonus_applied_sbend = 0.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_congestion_presence_cache_hits = 0 + self.total_congestion_presence_cache_misses = 0 + self.total_congestion_presence_skips = 0 + self.total_congestion_candidate_precheck_hits = 0 + self.total_congestion_candidate_precheck_misses = 0 + self.total_congestion_candidate_precheck_skips = 0 + self.total_congestion_grid_net_cache_hits = 0 + self.total_congestion_grid_net_cache_misses = 0 + self.total_congestion_grid_span_cache_hits = 0 + self.total_congestion_grid_span_cache_misses = 0 + self.total_congestion_candidate_nets = 0 + self.total_congestion_net_envelope_cache_hits = 0 + self.total_congestion_net_envelope_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_corner_index_builds = 0 + self.total_visibility_builds = 0 + self.total_visibility_corner_pairs_checked = 0 + self.total_visibility_corner_queries_exact = 0 + self.total_visibility_corner_hits_exact = 0 + self.total_visibility_point_queries = 0 + self.total_visibility_point_cache_hits = 0 + self.total_visibility_point_cache_misses = 0 + self.total_visibility_tangent_candidate_scans = 0 + self.total_visibility_tangent_candidate_corner_checks = 0 + self.total_visibility_tangent_candidate_ray_tests = 0 + self.total_ray_cast_calls = 0 + self.total_ray_cast_calls_straight_static = 0 + self.total_ray_cast_calls_expand_snap = 0 + self.total_ray_cast_calls_expand_forward = 0 + self.total_ray_cast_calls_visibility_build = 0 + self.total_ray_cast_calls_visibility_query = 0 + self.total_ray_cast_calls_visibility_tangent = 0 + self.total_ray_cast_calls_other = 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_lazy_resolutions = 0 + self.total_congestion_lazy_requeues = 0 + self.total_congestion_candidate_ids = 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_candidate_nets = 0 + self.total_verify_dynamic_exact_pair_checks = 0 + self.total_refinement_windows_considered = 0 + self.total_refinement_static_bounds_checked = 0 + self.total_refinement_dynamic_bounds_checked = 0 + self.total_refinement_candidate_side_extents = 0 + self.total_refinement_candidates_built = 0 + self.total_refinement_candidates_verified = 0 + self.total_refinement_candidates_accepted = 0 + self.total_pair_local_search_pairs_considered = 0 + self.total_pair_local_search_attempts = 0 + self.total_pair_local_search_accepts = 0 + self.total_pair_local_search_nodes_expanded = 0 + self.total_late_phase_capped_nets = 0 + self.total_late_phase_capped_fallbacks = 0 + self.last_expanded_nodes: list[tuple[int, int, int]] = [] + self.nodes_expanded = 0 + self.moves_generated = 0 + self.moves_added = 0 + self.pruned_closed_set = 0 + self.pruned_hard_collision = 0 + self.pruned_cost = 0 + + def reset_totals(self) -> None: + self.total_nodes_expanded = 0 + self.total_moves_generated = 0 + self.total_moves_added = 0 + self.total_pruned_closed_set = 0 + self.total_pruned_hard_collision = 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_iteration_reverify_calls = 0 + self.total_iteration_reverified_nets = 0 + self.total_iteration_conflicting_nets = 0 + self.total_iteration_conflict_edges = 0 + self.total_nets_carried_forward = 0 + self.total_score_component_calls = 0 + self.total_score_component_total_ns = 0 + self.total_path_cost_calls = 0 + self.total_danger_map_lookup_calls = 0 + self.total_danger_map_cache_hits = 0 + self.total_danger_map_cache_misses = 0 + self.total_danger_map_query_calls = 0 + self.total_danger_map_total_ns = 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_guidance_match_moves = 0 + self.total_guidance_match_moves_straight = 0 + self.total_guidance_match_moves_bend90 = 0 + self.total_guidance_match_moves_sbend = 0 + self.total_guidance_bonus_applied = 0.0 + self.total_guidance_bonus_applied_straight = 0.0 + self.total_guidance_bonus_applied_bend90 = 0.0 + self.total_guidance_bonus_applied_sbend = 0.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_congestion_presence_cache_hits = 0 + self.total_congestion_presence_cache_misses = 0 + self.total_congestion_presence_skips = 0 + self.total_congestion_candidate_precheck_hits = 0 + self.total_congestion_candidate_precheck_misses = 0 + self.total_congestion_candidate_precheck_skips = 0 + self.total_congestion_grid_net_cache_hits = 0 + self.total_congestion_grid_net_cache_misses = 0 + self.total_congestion_grid_span_cache_hits = 0 + self.total_congestion_grid_span_cache_misses = 0 + self.total_congestion_candidate_nets = 0 + self.total_congestion_net_envelope_cache_hits = 0 + self.total_congestion_net_envelope_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_corner_index_builds = 0 + self.total_visibility_builds = 0 + self.total_visibility_corner_pairs_checked = 0 + self.total_visibility_corner_queries_exact = 0 + self.total_visibility_corner_hits_exact = 0 + self.total_visibility_point_queries = 0 + self.total_visibility_point_cache_hits = 0 + self.total_visibility_point_cache_misses = 0 + self.total_visibility_tangent_candidate_scans = 0 + self.total_visibility_tangent_candidate_corner_checks = 0 + self.total_visibility_tangent_candidate_ray_tests = 0 + self.total_ray_cast_calls = 0 + self.total_ray_cast_calls_straight_static = 0 + self.total_ray_cast_calls_expand_snap = 0 + self.total_ray_cast_calls_expand_forward = 0 + self.total_ray_cast_calls_visibility_build = 0 + self.total_ray_cast_calls_visibility_query = 0 + self.total_ray_cast_calls_visibility_tangent = 0 + self.total_ray_cast_calls_other = 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_lazy_resolutions = 0 + self.total_congestion_lazy_requeues = 0 + self.total_congestion_candidate_ids = 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_candidate_nets = 0 + self.total_verify_dynamic_exact_pair_checks = 0 + self.total_refinement_windows_considered = 0 + self.total_refinement_static_bounds_checked = 0 + self.total_refinement_dynamic_bounds_checked = 0 + self.total_refinement_candidate_side_extents = 0 + self.total_refinement_candidates_built = 0 + self.total_refinement_candidates_verified = 0 + self.total_refinement_candidates_accepted = 0 + self.total_pair_local_search_pairs_considered = 0 + self.total_pair_local_search_attempts = 0 + self.total_pair_local_search_accepts = 0 + self.total_pair_local_search_nodes_expanded = 0 + self.total_late_phase_capped_nets = 0 + self.total_late_phase_capped_fallbacks = 0 + + def reset_per_route(self) -> None: + self.nodes_expanded = 0 + self.moves_generated = 0 + self.moves_added = 0 + self.pruned_closed_set = 0 + self.pruned_hard_collision = 0 + self.pruned_cost = 0 + self.last_expanded_nodes = [] + + def snapshot(self) -> RouteMetrics: + return RouteMetrics( + nodes_expanded=self.total_nodes_expanded, + moves_generated=self.total_moves_generated, + moves_added=self.total_moves_added, + pruned_closed_set=self.total_pruned_closed_set, + pruned_hard_collision=self.total_pruned_hard_collision, + 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, + iteration_reverify_calls=self.total_iteration_reverify_calls, + iteration_reverified_nets=self.total_iteration_reverified_nets, + iteration_conflicting_nets=self.total_iteration_conflicting_nets, + iteration_conflict_edges=self.total_iteration_conflict_edges, + nets_carried_forward=self.total_nets_carried_forward, + score_component_calls=self.total_score_component_calls, + score_component_total_ns=self.total_score_component_total_ns, + path_cost_calls=self.total_path_cost_calls, + danger_map_lookup_calls=self.total_danger_map_lookup_calls, + danger_map_cache_hits=self.total_danger_map_cache_hits, + danger_map_cache_misses=self.total_danger_map_cache_misses, + danger_map_query_calls=self.total_danger_map_query_calls, + danger_map_total_ns=self.total_danger_map_total_ns, + 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, + guidance_match_moves=self.total_guidance_match_moves, + guidance_match_moves_straight=self.total_guidance_match_moves_straight, + guidance_match_moves_bend90=self.total_guidance_match_moves_bend90, + guidance_match_moves_sbend=self.total_guidance_match_moves_sbend, + guidance_bonus_applied=self.total_guidance_bonus_applied, + guidance_bonus_applied_straight=self.total_guidance_bonus_applied_straight, + guidance_bonus_applied_bend90=self.total_guidance_bonus_applied_bend90, + guidance_bonus_applied_sbend=self.total_guidance_bonus_applied_sbend, + 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, + congestion_presence_cache_hits=self.total_congestion_presence_cache_hits, + congestion_presence_cache_misses=self.total_congestion_presence_cache_misses, + congestion_presence_skips=self.total_congestion_presence_skips, + congestion_candidate_precheck_hits=self.total_congestion_candidate_precheck_hits, + congestion_candidate_precheck_misses=self.total_congestion_candidate_precheck_misses, + congestion_candidate_precheck_skips=self.total_congestion_candidate_precheck_skips, + congestion_grid_net_cache_hits=self.total_congestion_grid_net_cache_hits, + congestion_grid_net_cache_misses=self.total_congestion_grid_net_cache_misses, + congestion_grid_span_cache_hits=self.total_congestion_grid_span_cache_hits, + congestion_grid_span_cache_misses=self.total_congestion_grid_span_cache_misses, + congestion_candidate_nets=self.total_congestion_candidate_nets, + congestion_net_envelope_cache_hits=self.total_congestion_net_envelope_cache_hits, + congestion_net_envelope_cache_misses=self.total_congestion_net_envelope_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_corner_index_builds=self.total_visibility_corner_index_builds, + visibility_builds=self.total_visibility_builds, + visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked, + visibility_corner_queries_exact=self.total_visibility_corner_queries_exact, + visibility_corner_hits_exact=self.total_visibility_corner_hits_exact, + 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, + visibility_tangent_candidate_scans=self.total_visibility_tangent_candidate_scans, + visibility_tangent_candidate_corner_checks=self.total_visibility_tangent_candidate_corner_checks, + visibility_tangent_candidate_ray_tests=self.total_visibility_tangent_candidate_ray_tests, + ray_cast_calls=self.total_ray_cast_calls, + ray_cast_calls_straight_static=self.total_ray_cast_calls_straight_static, + ray_cast_calls_expand_snap=self.total_ray_cast_calls_expand_snap, + ray_cast_calls_expand_forward=self.total_ray_cast_calls_expand_forward, + ray_cast_calls_visibility_build=self.total_ray_cast_calls_visibility_build, + ray_cast_calls_visibility_query=self.total_ray_cast_calls_visibility_query, + ray_cast_calls_visibility_tangent=self.total_ray_cast_calls_visibility_tangent, + ray_cast_calls_other=self.total_ray_cast_calls_other, + 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_lazy_resolutions=self.total_congestion_lazy_resolutions, + congestion_lazy_requeues=self.total_congestion_lazy_requeues, + congestion_candidate_ids=self.total_congestion_candidate_ids, + 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_candidate_nets=self.total_verify_dynamic_candidate_nets, + verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks, + refinement_windows_considered=self.total_refinement_windows_considered, + refinement_static_bounds_checked=self.total_refinement_static_bounds_checked, + refinement_dynamic_bounds_checked=self.total_refinement_dynamic_bounds_checked, + refinement_candidate_side_extents=self.total_refinement_candidate_side_extents, + refinement_candidates_built=self.total_refinement_candidates_built, + refinement_candidates_verified=self.total_refinement_candidates_verified, + refinement_candidates_accepted=self.total_refinement_candidates_accepted, + pair_local_search_pairs_considered=self.total_pair_local_search_pairs_considered, + pair_local_search_attempts=self.total_pair_local_search_attempts, + pair_local_search_accepts=self.total_pair_local_search_accepts, + pair_local_search_nodes_expanded=self.total_pair_local_search_nodes_expanded, + late_phase_capped_nets=self.total_late_phase_capped_nets, + late_phase_capped_fallbacks=self.total_late_phase_capped_fallbacks, + ) + + +class AStarContext: + __slots__ = ( + "cost_evaluator", + "metrics", + "congestion_penalty", + "min_bend_radius", + "problem", + "options", + "max_cache_size", + "visibility_manager", + "move_cache_rel", + "move_cache_abs", + "hard_collision_set", + "static_safe_cache", + "static_cache_version", + ) + + def __init__( + self, + cost_evaluator: CostEvaluator, + problem: RoutingProblem, + options: RoutingOptions, + metrics: AStarMetrics | None = None, + max_cache_size: int = 1000000, + ) -> None: + self.cost_evaluator = cost_evaluator + self.metrics = metrics if metrics is not None else AStarMetrics() + self.congestion_penalty = 0.0 + self.max_cache_size = max_cache_size + self.problem = problem + self.options = options + self.min_bend_radius = min(self.options.search.bend_radii, default=50.0) + self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine) + self.move_cache_rel: dict[tuple, ComponentResult] = {} + self.move_cache_abs: dict[tuple, ComponentResult] = {} + self.hard_collision_set: set[tuple] = set() + self.static_safe_cache: set[tuple] = set() + self.static_cache_version = self.cost_evaluator.collision_engine.get_static_version() + + def clear_static_caches(self) -> None: + self.hard_collision_set.clear() + self.static_safe_cache.clear() + self.visibility_manager.clear_cache() + self.static_cache_version = self.cost_evaluator.collision_engine.get_static_version() + + def ensure_static_caches_current(self) -> None: + current_version = self.cost_evaluator.collision_engine.get_static_version() + if self.static_cache_version != current_version: + self.clear_static_caches() + + def _evict_cache(self, cache: dict[tuple, ComponentResult]) -> None: + if len(cache) <= self.max_cache_size * 1.2: + return + + num_to_evict = max(1, int(len(cache) * 0.25)) + for idx, key in enumerate(tuple(cache.keys())): + if idx >= num_to_evict: + break + del cache[key] + + def check_cache_eviction(self) -> None: + self._evict_cache(self.move_cache_rel) + self._evict_cache(self.move_cache_abs) diff --git a/inire/router/_router.py b/inire/router/_router.py new file mode 100644 index 0000000..bc8aa97 --- /dev/null +++ b/inire/router/_router.py @@ -0,0 +1,1354 @@ +from __future__ import annotations + +import random +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from inire.geometry.collision import RoutingWorld +from inire.model import NetOrder, NetSpec, RoutingProblem, resolve_bend_geometry +from inire.results import ( + ComponentConflictTrace, + ConflictTraceEntry, + FrontierPruneSample, + IterationNetAttemptTrace, + IterationTraceEntry, + NetConflictTrace, + NetFrontierTrace, + PrePairFrontierTraceEntry, + PrePairNetTrace, + RoutingOutcome, + RoutingReport, + RoutingResult, +) +from inire.router._astar_types import AStarContext, AStarMetrics, FrontierTraceCollector, SearchRunConfig +from inire.router._search import route_astar +from inire.router._seed_materialization import materialize_path_seed +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap +from inire.router.refiner import PathRefiner + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from shapely.geometry import Polygon + + from inire.geometry.collision import PathVerificationDetail + from inire.geometry.components import ComponentResult + + +@dataclass(slots=True) +class _RoutingState: + net_specs: dict[str, NetSpec] + ordered_net_ids: list[str] + results: dict[str, RoutingResult] + needs_self_collision_check: set[str] + start_time: float + timeout_s: float + initial_paths: dict[str, tuple[ComponentResult, ...]] | None + accumulated_expanded_nodes: list[tuple[int, int, int]] + best_results: dict[str, RoutingResult] + best_completed_nets: int + best_conflict_edges: int + best_dynamic_collisions: int + last_conflict_signature: tuple[tuple[str, str], ...] + last_conflict_edge_count: int + repeated_conflict_count: int + pair_local_plateau_count: int + recent_attempt_work: dict[str, int] + pre_pair_candidate: _PrePairCandidate | None + + +@dataclass(slots=True) +class _IterationReview: + conflicting_nets: set[str] + conflict_edges: set[tuple[str, str]] + completed_net_ids: set[str] + total_dynamic_collisions: int + + +@dataclass(frozen=True, slots=True) +class _PairLocalTarget: + net_ids: tuple[str, str] + + +@dataclass(frozen=True, slots=True) +class _PrePairCandidate: + iteration: int + routed_net_ids: tuple[str, ...] + conflict_edges: tuple[tuple[str, str], ...] + net_attempts: tuple[IterationNetAttemptTrace, ...] + + +_ITERATION_TRACE_TOTALS = ( + "nodes_expanded", + "congestion_check_calls", + "congestion_candidate_ids", + "congestion_exact_pair_checks", +) + +_ATTEMPT_TRACE_TOTALS = ( + "nodes_expanded", + "congestion_check_calls", + "pruned_closed_set", + "pruned_cost", + "pruned_hard_collision", +) + + +class PathFinder: + __slots__ = ( + "context", + "metrics", + "refiner", + "accumulated_expanded_nodes", + "conflict_trace", + "frontier_trace", + "pre_pair_frontier_trace", + "iteration_trace", + ) + + def __init__( + self, + context: AStarContext, + metrics: AStarMetrics | None = None, + ) -> None: + self.context = context + 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 + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = self.metrics + self.refiner = PathRefiner(self.context) + self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] + self.conflict_trace: list[ConflictTraceEntry] = [] + self.frontier_trace: list[NetFrontierTrace] = [] + self.pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None + self.iteration_trace: list[IterationTraceEntry] = [] + + def _metric_total(self, metric_name: str) -> int: + return int(getattr(self.metrics, f"total_{metric_name}")) + + def _capture_metric_totals(self, metric_names: tuple[str, ...]) -> dict[str, int]: + return {metric_name: self._metric_total(metric_name) for metric_name in metric_names} + + def _metric_deltas(self, before: dict[str, int], after: dict[str, int]) -> dict[str, int]: + return {metric_name: after[metric_name] - before[metric_name] for metric_name in before} + + def _results_all_reached_target(self, state: _RoutingState) -> bool: + return ( + len(state.results) == len(state.ordered_net_ids) + and all(result.reached_target for result in state.results.values()) + ) + + def _has_incumbent_fallback(self, result: RoutingResult | None) -> bool: + return bool(result and result.reached_target and result.path) + + def _restore_incumbent_fallback( + self, + net_id: str, + result: RoutingResult, + guidance_seed_present: bool, + ) -> tuple[RoutingResult, bool]: + self.metrics.total_late_phase_capped_fallbacks += 1 + self._install_path(net_id, result.path) + return result, guidance_seed_present + + def _guidance_for_result( + self, + result: RoutingResult | None, + ) -> tuple[Sequence[ComponentResult] | None, float, bool]: + if result is None or not result.reached_target or not result.path: + return None, 0.0, False + return ( + result.as_seed().segments, + max(10.0, self.context.options.objective.bend_penalty * 0.25), + True, + ) + + def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None: + all_geoms: list[Polygon] = [] + all_dilated: list[Polygon] = [] + component_indexes: list[int] = [] + for component_index, result in enumerate(path): + all_geoms.extend(result.collision_geometry) + all_dilated.extend(result.dilated_collision_geometry) + component_indexes.extend([component_index] * len(result.collision_geometry)) + self.context.cost_evaluator.collision_engine.add_path( + net_id, + all_geoms, + dilated_geometry=all_dilated, + component_indexes=component_indexes, + ) + + def _routing_order( + self, + net_specs: dict[str, NetSpec], + order: NetOrder, + ) -> list[str]: + ordered_net_ids = list(net_specs.keys()) + if order == "user": + return ordered_net_ids + ordered_net_ids.sort( + key=lambda net_id: abs(net_specs[net_id].target.x - net_specs[net_id].start.x) + + abs(net_specs[net_id].target.y - net_specs[net_id].start.y), + reverse=(order == "longest"), + ) + return ordered_net_ids + + def _build_greedy_warm_start_paths( + self, + net_specs: dict[str, NetSpec], + order: NetOrder, + ) -> dict[str, tuple[ComponentResult, ...]]: + greedy_paths: dict[str, tuple[ComponentResult, ...]] = {} + temp_obj_ids: list[int] = [] + greedy_node_limit = min(self.context.options.search.node_limit, 2000) + for net_id in self._routing_order(net_specs, order): + net = net_specs[net_id] + h_start = self.context.cost_evaluator.h_manhattan( + net.start, + net.target, + min_bend_radius=self.context.min_bend_radius, + ) + max_cost_limit = max(h_start * 3.0, 2000.0) + run_config = SearchRunConfig.from_options( + self.context.options, + skip_congestion=True, + max_cost=max_cost_limit, + self_collision_check=True, + node_limit=greedy_node_limit, + ) + path = route_astar( + net.start, + net.target, + net.width, + context=self.context, + metrics=self.metrics, + net_id=net_id, + config=run_config, + ) + if not path: + continue + self.metrics.total_warm_start_paths_built += 1 + greedy_paths[net_id] = tuple(path) + for result in path: + for polygon in result.physical_geometry: + temp_obj_ids.append(self.context.cost_evaluator.collision_engine.add_static_obstacle(polygon)) + self.context.clear_static_caches() + + for obj_id in temp_obj_ids: + self.context.cost_evaluator.collision_engine.remove_static_obstacle(obj_id) + return greedy_paths + + def _prepare_state(self) -> _RoutingState: + problem = self.context.problem + congestion = self.context.options.congestion + initial_paths = self._materialize_problem_initial_paths() + net_specs = {net.net_id: net for net in problem.nets} + num_nets = len(net_specs) + state = _RoutingState( + net_specs=net_specs, + ordered_net_ids=list(net_specs.keys()), + results={}, + needs_self_collision_check=set(), + start_time=time.monotonic(), + timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations), + initial_paths=initial_paths, + accumulated_expanded_nodes=[], + best_results={}, + best_completed_nets=-1, + best_conflict_edges=10**9, + best_dynamic_collisions=10**9, + last_conflict_signature=(), + last_conflict_edge_count=0, + repeated_conflict_count=0, + pair_local_plateau_count=0, + recent_attempt_work={}, + pre_pair_candidate=None, + ) + if state.initial_paths is None and congestion.warm_start_enabled: + state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order) + self.context.clear_static_caches() + + if congestion.net_order != "user": + state.ordered_net_ids = self._routing_order(net_specs, congestion.net_order) + return state + + def _materialize_problem_initial_paths(self) -> dict[str, tuple[ComponentResult, ...]] | None: + if not self.context.problem.initial_paths: + return None + + search = self.context.options.search + net_specs = {net.net_id: net for net in self.context.problem.nets} + initial_paths: dict[str, tuple[ComponentResult, ...]] = {} + for net_id, seed in self.context.problem.initial_paths.items(): + if net_id not in net_specs: + raise ValueError(f"Initial path provided for unknown net: {net_id}") + net = net_specs[net_id] + initial_paths[net_id] = materialize_path_seed( + seed, + start=net.start, + net_width=net.width, + search=search, + clearance=self.context.cost_evaluator.collision_engine.clearance, + ) + return initial_paths + + def _replace_installed_paths(self, state: _RoutingState, results: dict[str, RoutingResult]) -> None: + for net_id in state.ordered_net_ids: + self.context.cost_evaluator.collision_engine.remove_path(net_id) + for net_id in state.ordered_net_ids: + result = results.get(net_id) + if result and result.path: + self._install_path(net_id, result.path) + + def _analyze_restored_best( + self, + state: _RoutingState, + ) -> tuple[dict[str, PathVerificationDetail], _IterationReview]: + capture_component_conflicts = ( + self.context.options.diagnostics.capture_conflict_trace + or self.context.options.diagnostics.capture_pre_pair_frontier_trace + ) + state.results, details_by_net, review = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=capture_component_conflicts, + count_iteration_metrics=False, + ) + if self.context.options.diagnostics.capture_conflict_trace: + self._capture_conflict_trace_entry( + state, + stage="restored_best", + iteration=None, + results=state.results, + details_by_net=details_by_net, + review=review, + ) + if self.context.options.diagnostics.capture_pre_pair_frontier_trace: + self.pre_pair_frontier_trace = self._materialize_pre_pair_frontier_trace( + state, + state.results, + details_by_net, + review, + ) + return details_by_net, review + + def _update_best_iteration(self, state: _RoutingState, review: _IterationReview) -> bool: + completed_nets = len(review.completed_net_ids) + conflict_edges = len(review.conflict_edges) + dynamic_collisions = review.total_dynamic_collisions + is_better = ( + completed_nets > state.best_completed_nets + or ( + completed_nets == state.best_completed_nets + and ( + conflict_edges < state.best_conflict_edges + or ( + conflict_edges == state.best_conflict_edges + and dynamic_collisions < state.best_dynamic_collisions + ) + ) + ) + ) + if not is_better: + return False + + state.best_results = dict(state.results) + state.best_completed_nets = completed_nets + state.best_conflict_edges = conflict_edges + state.best_dynamic_collisions = dynamic_collisions + return True + + def _restore_best_iteration(self, state: _RoutingState) -> None: + if not state.best_results: + return + state.results = dict(state.best_results) + self._replace_installed_paths(state, state.results) + + def _capture_conflict_trace_entry( + self, + state: _RoutingState, + *, + stage: str, + iteration: int | None, + results: dict[str, RoutingResult], + details_by_net: dict[str, PathVerificationDetail], + review: _IterationReview, + ) -> None: + if not self.context.options.diagnostics.capture_conflict_trace: + return + + nets = [] + for net_id in state.ordered_net_ids: + result = results.get(net_id) + if result is None: + result = RoutingResult(net_id=net_id, path=(), reached_target=False) + detail = details_by_net.get(net_id) + component_conflicts = () + conflicting_net_ids = () + if detail is not None: + conflicting_net_ids = detail.conflicting_net_ids + component_conflicts = tuple( + ComponentConflictTrace( + other_net_id=other_net_id, + self_component_index=self_component_index, + other_component_index=other_component_index, + ) + for self_component_index, other_net_id, other_component_index in detail.component_conflicts + ) + nets.append( + NetConflictTrace( + net_id=net_id, + outcome=result.outcome, + reached_target=result.reached_target, + report=result.report, + conflicting_net_ids=tuple(conflicting_net_ids), + component_conflicts=component_conflicts, + ) + ) + + self.conflict_trace.append( + ConflictTraceEntry( + stage=stage, # type: ignore[arg-type] + iteration=iteration, + completed_net_ids=tuple(sorted(review.completed_net_ids)), + conflict_edges=tuple(sorted(review.conflict_edges)), + nets=tuple(nets), + ) + ) + + def _build_frontier_hotspot_bounds( + self, + state: _RoutingState, + net_id: str, + details_by_net: dict[str, PathVerificationDetail], + ) -> tuple[tuple[float, float, float, float], ...]: + result = state.results.get(net_id) + detail = details_by_net.get(net_id) + if result is None or detail is None or not result.path: + return () + + hotspot_bounds: list[tuple[float, float, float, float]] = [] + seen: set[tuple[float, float, float, float]] = set() + margin = max(5.0, self.context.cost_evaluator.collision_engine.clearance * 2.0) + + for self_component_index, other_net_id, other_component_index in detail.component_conflicts: + other_result = state.results.get(other_net_id) + if other_result is None or not other_result.path: + continue + if self_component_index >= len(result.path) or other_component_index >= len(other_result.path): + continue + left_component = result.path[self_component_index] + right_component = other_result.path[other_component_index] + overlap_found = False + for left_poly in left_component.dilated_physical_geometry: + for right_poly in right_component.dilated_physical_geometry: + if not left_poly.intersects(right_poly) or left_poly.touches(right_poly): + continue + overlap = left_poly.intersection(right_poly) + if overlap.is_empty: + continue + buffered = overlap.buffer(margin, join_style="mitre").bounds + if buffered not in seen: + seen.add(buffered) + hotspot_bounds.append(buffered) + overlap_found = True + if overlap_found: + continue + + left_bounds = left_component.total_dilated_bounds + right_bounds = right_component.total_dilated_bounds + if ( + left_bounds[0] < right_bounds[2] + and left_bounds[2] > right_bounds[0] + and left_bounds[1] < right_bounds[3] + and left_bounds[3] > right_bounds[1] + ): + buffered = ( + max(left_bounds[0], right_bounds[0]) - margin, + max(left_bounds[1], right_bounds[1]) - margin, + min(left_bounds[2], right_bounds[2]) + margin, + min(left_bounds[3], right_bounds[3]) + margin, + ) + if buffered not in seen: + seen.add(buffered) + hotspot_bounds.append(buffered) + + return tuple(hotspot_bounds) + + def _capture_single_frontier_trace( + self, + state: _RoutingState, + net_id: str, + result: RoutingResult, + hotspot_bounds: tuple[tuple[float, float, float, float], ...], + ) -> NetFrontierTrace: + if not hotspot_bounds: + return NetFrontierTrace( + net_id=net_id, + hotspot_bounds=(), + pruned_closed_set=0, + pruned_hard_collision=0, + pruned_self_collision=0, + pruned_cost=0, + ) + + original_metrics = self.metrics + original_context_metrics = self.context.metrics + original_engine_metrics = self.context.cost_evaluator.collision_engine.metrics + original_danger_metrics = None + if self.context.cost_evaluator.danger_map is not None: + original_danger_metrics = self.context.cost_evaluator.danger_map.metrics + + try: + scratch_metrics = AStarMetrics() + self.context.metrics = scratch_metrics + self.context.cost_evaluator.collision_engine.metrics = scratch_metrics + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = scratch_metrics + + guidance_seed = result.as_seed().segments if result.path else None + guidance_bonus = 0.0 + if guidance_seed: + guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) + collector = FrontierTraceCollector(hotspot_bounds=hotspot_bounds) + run_config = SearchRunConfig.from_options( + self.context.options, + return_partial=True, + store_expanded=False, + guidance_seed=guidance_seed, + guidance_bonus=guidance_bonus, + frontier_trace=collector, + self_collision_check=(net_id in state.needs_self_collision_check), + node_limit=self.context.options.search.node_limit, + ) + + self.context.cost_evaluator.collision_engine.remove_path(net_id) + try: + route_astar( + state.net_specs[net_id].start, + state.net_specs[net_id].target, + state.net_specs[net_id].width, + context=self.context, + metrics=scratch_metrics, + net_id=net_id, + config=run_config, + ) + finally: + if result.path: + self._install_path(net_id, result.path) + + return NetFrontierTrace( + net_id=net_id, + hotspot_bounds=hotspot_bounds, + pruned_closed_set=collector.pruned_closed_set, + pruned_hard_collision=collector.pruned_hard_collision, + pruned_self_collision=collector.pruned_self_collision, + pruned_cost=collector.pruned_cost, + samples=tuple( + FrontierPruneSample( + reason=reason, # type: ignore[arg-type] + move_type=move_type, + hotspot_index=hotspot_index, + parent_state=parent_state, + end_state=end_state, + ) + for reason, move_type, hotspot_index, parent_state, end_state in collector.samples + ), + ) + finally: + self.metrics = original_metrics + self.context.metrics = original_context_metrics + self.context.cost_evaluator.collision_engine.metrics = original_engine_metrics + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = original_danger_metrics + + def _analyze_results( + self, + ordered_net_ids: Sequence[str], + results: dict[str, RoutingResult], + *, + capture_component_conflicts: bool, + count_iteration_metrics: bool, + ) -> tuple[dict[str, RoutingResult], dict[str, PathVerificationDetail], _IterationReview]: + if count_iteration_metrics: + self.metrics.total_iteration_reverify_calls += 1 + conflict_edges: set[tuple[str, str]] = set() + conflicting_nets: set[str] = set() + completed_net_ids: set[str] = set() + total_dynamic_collisions = 0 + analyzed_results = dict(results) + details_by_net: dict[str, PathVerificationDetail] = {} + + for net_id in ordered_net_ids: + result = results.get(net_id) + if not result or not result.path or not result.reached_target: + continue + + if count_iteration_metrics: + self.metrics.total_iteration_reverified_nets += 1 + detail = self.context.cost_evaluator.collision_engine.verify_path_details( + net_id, + result.path, + capture_component_conflicts=capture_component_conflicts, + ) + details_by_net[net_id] = detail + analyzed_results[net_id] = RoutingResult( + net_id=net_id, + path=result.path, + reached_target=result.reached_target, + report=detail.report, + ) + total_dynamic_collisions += detail.report.dynamic_collision_count + if analyzed_results[net_id].outcome == "completed": + completed_net_ids.add(net_id) + if not detail.conflicting_net_ids: + continue + conflicting_nets.add(net_id) + for other_net_id in detail.conflicting_net_ids: + conflicting_nets.add(other_net_id) + if other_net_id == net_id: + continue + conflict_edges.add(tuple(sorted((net_id, other_net_id)))) + + if count_iteration_metrics: + self.metrics.total_iteration_conflicting_nets += len(conflicting_nets) + self.metrics.total_iteration_conflict_edges += len(conflict_edges) + return ( + analyzed_results, + details_by_net, + _IterationReview( + conflicting_nets=conflicting_nets, + conflict_edges=conflict_edges, + completed_net_ids=completed_net_ids, + total_dynamic_collisions=total_dynamic_collisions, + ), + ) + + def _capture_frontier_trace( + self, + state: _RoutingState, + final_results: dict[str, RoutingResult], + ) -> None: + if not self.context.options.diagnostics.capture_frontier_trace: + return + + self.frontier_trace = [] + state.results = dict(final_results) + state.results, details_by_net, _ = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=True, + count_iteration_metrics=False, + ) + for net_id in state.ordered_net_ids: + result = state.results.get(net_id) + detail = details_by_net.get(net_id) + if result is None or detail is None or not result.reached_target: + continue + if detail.report.dynamic_collision_count == 0 or not detail.component_conflicts: + continue + + hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, details_by_net) + if not hotspot_bounds: + continue + + self.frontier_trace.append( + self._capture_single_frontier_trace( + state, + net_id, + result, + hotspot_bounds, + ) + ) + + def _whole_set_is_better( + self, + candidate_results: dict[str, RoutingResult], + candidate_review: _IterationReview, + incumbent_results: dict[str, RoutingResult], + incumbent_review: _IterationReview, + ) -> bool: + candidate_completed = len(candidate_review.completed_net_ids) + incumbent_completed = len(incumbent_review.completed_net_ids) + if candidate_completed != incumbent_completed: + return candidate_completed > incumbent_completed + + candidate_edges = len(candidate_review.conflict_edges) + incumbent_edges = len(incumbent_review.conflict_edges) + if candidate_edges != incumbent_edges: + return candidate_edges < incumbent_edges + + if candidate_review.total_dynamic_collisions != incumbent_review.total_dynamic_collisions: + return candidate_review.total_dynamic_collisions < incumbent_review.total_dynamic_collisions + + candidate_length = sum( + result.report.total_length + for result in candidate_results.values() + if result.reached_target + ) + incumbent_length = sum( + result.report.total_length + for result in incumbent_results.values() + if result.reached_target + ) + if abs(candidate_length - incumbent_length) > 1e-6: + return candidate_length < incumbent_length + return False + + def _pair_local_attempt_orders(self, target: _PairLocalTarget) -> tuple[tuple[str, str], tuple[str, str]]: + return target.net_ids, target.net_ids[::-1] + + def _collect_pair_local_targets( + self, + state: _RoutingState, + results: dict[str, RoutingResult], + review: _IterationReview, + ) -> list[_PairLocalTarget]: + if not review.conflict_edges: + return [] + order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)} + seen_net_ids: set[str] = set() + targets: list[_PairLocalTarget] = [] + for left_net_id, right_net_id in sorted(review.conflict_edges): + if left_net_id in seen_net_ids or right_net_id in seen_net_ids: + return [] + left_result = results.get(left_net_id) + right_result = results.get(right_net_id) + if ( + left_result is None + or right_result is None + or not left_result.reached_target + or not right_result.reached_target + ): + continue + seen_net_ids.update((left_net_id, right_net_id)) + targets.append(_PairLocalTarget(net_ids=(left_net_id, right_net_id))) + targets.sort(key=lambda target: min(order_index[target.net_ids[0]], order_index[target.net_ids[1]])) + return targets + + def _build_pair_local_context( + self, + state: _RoutingState, + incumbent_results: dict[str, RoutingResult], + pair_net_ids: tuple[str, str], + ) -> AStarContext: + problem = self.context.problem + objective = self.context.options.objective + static_obstacles = tuple(self.context.cost_evaluator.collision_engine._static_obstacles.geometries.values()) + engine = RoutingWorld( + clearance=self.context.cost_evaluator.collision_engine.clearance, + safety_zone_radius=self.context.cost_evaluator.collision_engine.safety_zone_radius, + ) + for obstacle in static_obstacles: + engine.add_static_obstacle(obstacle) + for net_id in state.ordered_net_ids: + if net_id in pair_net_ids: + continue + result = incumbent_results.get(net_id) + if result is None or not result.path: + continue + for component in result.path: + for polygon in component.physical_geometry: + engine.add_static_obstacle(polygon) + + danger_map = DangerMap(bounds=problem.bounds) + danger_map.precompute(list(static_obstacles)) + evaluator = CostEvaluator( + engine, + danger_map, + unit_length_cost=objective.unit_length_cost, + greedy_h_weight=self.context.cost_evaluator.greedy_h_weight, + bend_penalty=objective.bend_penalty, + sbend_penalty=objective.sbend_penalty, + danger_weight=objective.danger_weight, + ) + return AStarContext( + evaluator, + RoutingProblem( + bounds=problem.bounds, + nets=tuple(state.net_specs[net_id] for net_id in state.ordered_net_ids), + static_obstacles=static_obstacles, + clearance=problem.clearance, + safety_zone_radius=problem.safety_zone_radius, + ), + self.context.options, + metrics=AStarMetrics(), + ) + + def _materialize_pre_pair_frontier_trace( + self, + state: _RoutingState, + results: dict[str, RoutingResult], + details_by_net: dict[str, PathVerificationDetail], + review: _IterationReview, + ) -> PrePairFrontierTraceEntry | None: + candidate = state.pre_pair_candidate + if candidate is None: + return None + + result_by_net = dict(results) + detail_by_net = dict(details_by_net) + nets: list[PrePairNetTrace] = [] + attempt_by_net = {attempt.net_id: attempt for attempt in candidate.net_attempts} + for net_id in candidate.routed_net_ids: + attempt = attempt_by_net.get(net_id) + result = result_by_net.get(net_id) + detail = detail_by_net.get(net_id) + if attempt is None or result is None or detail is None or not result.reached_target: + continue + hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, detail_by_net) + nets.append( + PrePairNetTrace( + net_id=net_id, + nodes_expanded=attempt.nodes_expanded, + congestion_check_calls=attempt.congestion_check_calls, + pruned_closed_set=attempt.pruned_closed_set, + pruned_cost=attempt.pruned_cost, + pruned_hard_collision=attempt.pruned_hard_collision, + guidance_seed_present=attempt.guidance_seed_present, + frontier=self._capture_single_frontier_trace(state, net_id, result, hotspot_bounds), + ) + ) + + if not nets: + return None + return PrePairFrontierTraceEntry( + iteration=candidate.iteration, + routed_net_ids=candidate.routed_net_ids, + conflict_edges=candidate.conflict_edges, + nets=tuple(nets), + ) + + def _build_iteration_reroute_plan( + self, + state: _RoutingState, + reroute_net_ids: set[str], + ) -> tuple[list[str], set[str]]: + routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids] + capped_net_ids: set[str] = set() + if len(reroute_net_ids) >= len(state.ordered_net_ids) or not state.recent_attempt_work: + return routed_net_ids, capped_net_ids + + order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)} + routed_net_ids.sort(key=lambda net_id: (state.recent_attempt_work.get(net_id, 0), order_index[net_id])) + if ( + len(routed_net_ids) == 4 + and state.best_conflict_edges <= 2 + and self._results_all_reached_target(state) + ): + heavy_net_ids = sorted( + routed_net_ids, + key=lambda net_id: (-state.recent_attempt_work.get(net_id, 0), order_index[net_id]), + )[:2] + capped_net_ids = { + net_id for net_id in heavy_net_ids if state.recent_attempt_work.get(net_id, 0) >= 200 + } + return routed_net_ids, capped_net_ids + + def _update_pre_pair_candidate( + self, + state: _RoutingState, + *, + iteration: int, + reroute_net_ids: set[str], + routed_net_ids: list[str], + attempt_traces: list[IterationNetAttemptTrace], + review: _IterationReview, + ) -> None: + if self._results_all_reached_target(state) and len(reroute_net_ids) < len(state.ordered_net_ids) and review.conflict_edges: + state.pre_pair_candidate = _PrePairCandidate( + iteration=iteration, + routed_net_ids=tuple(routed_net_ids), + conflict_edges=tuple(sorted(review.conflict_edges)), + net_attempts=tuple(attempt_traces), + ) + return + state.pre_pair_candidate = None + + def _next_reroute_net_ids( + self, + state: _RoutingState, + review: _IterationReview, + ) -> set[str]: + if self._results_all_reached_target(state) and 0 < len(review.conflict_edges) <= 3: + return set(review.conflicting_nets) + return set(state.ordered_net_ids) + + def _should_stop_for_pair_local_plateau( + self, + state: _RoutingState, + *, + improved: bool, + ) -> bool: + if improved: + state.pair_local_plateau_count = 0 + return False + if self._results_all_reached_target(state) and state.best_conflict_edges <= 2: + # Once the run is fully reached-target and already in the final <=2-edge + # basin, another non-improving negotiated iteration is just churn before + # the bounded pair-local repair. + state.pair_local_plateau_count += 1 + return state.pair_local_plateau_count >= 1 + state.pair_local_plateau_count = 0 + return False + + def _update_repeated_conflict_state( + self, + state: _RoutingState, + review: _IterationReview, + ) -> bool: + current_signature = tuple(sorted(review.conflict_edges)) + repeated = ( + bool(current_signature) + and ( + current_signature == state.last_conflict_signature + or len(current_signature) == state.last_conflict_edge_count + ) + ) + state.repeated_conflict_count = state.repeated_conflict_count + 1 if repeated else 0 + state.last_conflict_signature = current_signature + state.last_conflict_edge_count = len(current_signature) + return state.repeated_conflict_count >= 2 + + def _run_pair_local_attempt( + self, + state: _RoutingState, + incumbent_results: dict[str, RoutingResult], + pair_order: tuple[str, str], + ) -> tuple[dict[str, RoutingResult], int] | None: + local_context = self._build_pair_local_context(state, incumbent_results, pair_order) + local_results = dict(incumbent_results) + + for net_id in pair_order: + net = state.net_specs[net_id] + guidance_seed, guidance_bonus, _ = self._guidance_for_result(incumbent_results.get(net_id)) + + run_config = SearchRunConfig.from_options( + self.context.options, + return_partial=False, + skip_congestion=True, + self_collision_check=(net_id in state.needs_self_collision_check), + guidance_seed=guidance_seed, + guidance_bonus=guidance_bonus, + node_limit=self.context.options.search.node_limit, + ) + path = route_astar( + net.start, + net.target, + net.width, + context=local_context, + metrics=local_context.metrics, + net_id=net_id, + config=run_config, + ) + if not path or path[-1].end_port != net.target: + return None + + report = local_context.cost_evaluator.collision_engine.verify_path_report(net_id, path) + if not report.is_valid: + return None + local_results[net_id] = RoutingResult( + net_id=net_id, + path=tuple(path), + reached_target=True, + report=report, + ) + for component in path: + for polygon in component.physical_geometry: + local_context.cost_evaluator.collision_engine.add_static_obstacle(polygon) + local_context.clear_static_caches() + + return local_results, local_context.metrics.total_nodes_expanded + + def _apply_pair_local_candidate( + self, + state: _RoutingState, + candidate_results: dict[str, RoutingResult], + incumbent_results: dict[str, RoutingResult], + incumbent_review: _IterationReview, + ) -> tuple[bool, _IterationReview]: + self._replace_installed_paths(state, candidate_results) + candidate_results, _candidate_details_by_net, candidate_review = self._analyze_results( + state.ordered_net_ids, + candidate_results, + capture_component_conflicts=True, + count_iteration_metrics=False, + ) + if self._whole_set_is_better( + candidate_results, + candidate_review, + incumbent_results, + incumbent_review, + ): + self.metrics.total_pair_local_search_accepts += 1 + state.results = candidate_results + return True, candidate_review + + self._replace_installed_paths(state, incumbent_results) + return False, incumbent_review + + def _run_pair_local_target( + self, + state: _RoutingState, + target: _PairLocalTarget, + review: _IterationReview, + ) -> _IterationReview: + incumbent_results = dict(state.results) + incumbent_review = review + self.metrics.total_pair_local_search_pairs_considered += 1 + for pair_order in self._pair_local_attempt_orders(target): + self.metrics.total_pair_local_search_attempts += 1 + candidate = self._run_pair_local_attempt(state, incumbent_results, pair_order) + if candidate is None: + continue + candidate_results, nodes_expanded = candidate + self.metrics.total_pair_local_search_nodes_expanded += nodes_expanded + accepted, next_review = self._apply_pair_local_candidate( + state, + candidate_results, + incumbent_results, + incumbent_review, + ) + if accepted: + return next_review + + state.results = incumbent_results + self._replace_installed_paths(state, incumbent_results) + return incumbent_review + + def _run_pair_local_search(self, state: _RoutingState) -> None: + state.results, _details_by_net, review = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=True, + count_iteration_metrics=False, + ) + targets = self._collect_pair_local_targets(state, state.results, review) + if not targets: + return + + for target in targets[:2]: + review = self._run_pair_local_target(state, target, review) + + def _route_net_once( + self, + state: _RoutingState, + iteration: int, + net_id: str, + *, + node_limit_override: int | None = None, + incumbent_fallback: RoutingResult | None = None, + ) -> tuple[RoutingResult, bool]: + search = self.context.options.search + congestion = self.context.options.congestion + diagnostics = self.context.options.diagnostics + net = state.net_specs[net_id] + self.metrics.total_nets_routed += 1 + if node_limit_override is not None: + self.metrics.total_late_phase_capped_nets += 1 + self.context.cost_evaluator.collision_engine.remove_path(net_id) + guidance_seed_present = False + + 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] + else: + coll_model, _ = resolve_bend_geometry(search) + skip_congestion = False + guidance_seed, guidance_bonus, guidance_seed_present = (None, 0.0, False) + if congestion.use_tiered_strategy and iteration == 0: + skip_congestion = True + if coll_model == "arc": + coll_model = "clipped_bbox" + elif iteration > 0: + guidance_seed, guidance_bonus, guidance_seed_present = self._guidance_for_result( + state.results.get(net_id) + ) + + if node_limit_override is not None and self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) + + run_config = SearchRunConfig.from_options( + self.context.options, + bend_collision_type=coll_model, + return_partial=True, + store_expanded=diagnostics.capture_expanded, + guidance_seed=guidance_seed, + guidance_bonus=guidance_bonus, + skip_congestion=skip_congestion, + self_collision_check=(net_id in state.needs_self_collision_check), + node_limit=search.node_limit if node_limit_override is None else node_limit_override, + ) + path = route_astar( + net.start, + net.target, + net.width, + context=self.context, + metrics=self.metrics, + net_id=net_id, + config=run_config, + ) + + if diagnostics.capture_expanded and self.metrics.last_expanded_nodes: + state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes) + + if not path: + if self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) + return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present + + reached_target = path[-1].end_port == net.target + if not reached_target and self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) + if reached_target: + self.metrics.total_nets_reached_target += 1 + report = None + self._install_path(net_id, path) + if reached_target: + report = self.context.cost_evaluator.collision_engine.verify_path_report(net_id, path) + if report.self_collision_count > 0: + state.needs_self_collision_check.add(net_id) + + return ( + RoutingResult( + net_id=net_id, + path=tuple(path), + reached_target=reached_target, + report=RoutingReport() if report is None else report, + ), + guidance_seed_present, + ) + + def _run_iteration( + self, + state: _RoutingState, + iteration: int, + reroute_net_ids: set[str], + iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, + ) -> _IterationReview | None: + congestion = self.context.options.congestion + diagnostics = self.context.options.diagnostics + self.metrics.total_route_iterations += 1 + self.metrics.reset_per_route() + if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None): + iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None + random.Random(iteration_seed).shuffle(state.ordered_net_ids) + + iteration_penalty = self.context.congestion_penalty + routed_net_ids, capped_net_ids = self._build_iteration_reroute_plan(state, reroute_net_ids) + self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids) + iteration_before = {} + attempt_traces: list[IterationNetAttemptTrace] = [] + attempt_work: dict[str, int] = {} + if diagnostics.capture_iteration_trace: + iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) + + for net_id in routed_net_ids: + if time.monotonic() - state.start_time > state.timeout_s: + self.metrics.total_timeout_events += 1 + return None + + attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) + node_limit_override = None + incumbent_fallback = None + if net_id in capped_net_ids: + node_limit_override = min(self.context.options.search.node_limit, 1) + incumbent_fallback = state.results.get(net_id) + result, guidance_seed_present = self._route_net_once( + state, + iteration, + net_id, + node_limit_override=node_limit_override, + incumbent_fallback=incumbent_fallback, + ) + state.results[net_id] = result + attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) + deltas = self._metric_deltas(attempt_before, attempt_after) + attempt_work[net_id] = deltas["nodes_expanded"] + deltas["congestion_check_calls"] + attempt_traces.append( + IterationNetAttemptTrace( + net_id=net_id, + reached_target=result.reached_target, + nodes_expanded=deltas["nodes_expanded"], + congestion_check_calls=deltas["congestion_check_calls"], + pruned_closed_set=deltas["pruned_closed_set"], + pruned_cost=deltas["pruned_cost"], + pruned_hard_collision=deltas["pruned_hard_collision"], + guidance_seed_present=guidance_seed_present, + ) + ) + + state.recent_attempt_work = attempt_work + review = self._reverify_iteration_results(state) + self._update_pre_pair_candidate( + state, + iteration=iteration, + reroute_net_ids=reroute_net_ids, + routed_net_ids=routed_net_ids, + attempt_traces=attempt_traces, + review=review, + ) + if diagnostics.capture_iteration_trace: + iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) + deltas = self._metric_deltas(iteration_before, iteration_after) + self.iteration_trace.append( + IterationTraceEntry( + iteration=iteration, + congestion_penalty=iteration_penalty, + routed_net_ids=tuple(routed_net_ids), + completed_nets=len(review.completed_net_ids), + conflict_edges=len(review.conflict_edges), + total_dynamic_collisions=review.total_dynamic_collisions, + nodes_expanded=deltas["nodes_expanded"], + congestion_check_calls=deltas["congestion_check_calls"], + congestion_candidate_ids=deltas["congestion_candidate_ids"], + congestion_exact_pair_checks=deltas["congestion_exact_pair_checks"], + net_attempts=tuple(attempt_traces), + ) + ) + + if iteration_callback: + iteration_callback(iteration, state.results) + return review + + def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview: + state.results, details_by_net, review = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=self.context.options.diagnostics.capture_conflict_trace, + count_iteration_metrics=True, + ) + self._capture_conflict_trace_entry( + state, + stage="iteration", + iteration=self.metrics.total_route_iterations - 1, + results=state.results, + details_by_net=details_by_net, + review=review, + ) + return review + + def _run_iterations( + self, + state: _RoutingState, + iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, + ) -> bool: + congestion = self.context.options.congestion + reroute_net_ids = set(state.ordered_net_ids) + for iteration in range(congestion.max_iterations): + review = self._run_iteration( + state, + iteration, + reroute_net_ids, + iteration_callback, + ) + if review is None: + return True + improved = self._update_best_iteration(state, review) + if not any( + result.outcome in {"colliding", "partial", "unroutable"} + for result in state.results.values() + ): + return False + + reroute_net_ids = self._next_reroute_net_ids(state, review) + if self._should_stop_for_pair_local_plateau(state, improved=improved): + return False + + if self._update_repeated_conflict_state(state, review): + return False + self.context.congestion_penalty *= congestion.multiplier + return False + + def _refine_results(self, state: _RoutingState) -> None: + if not self.context.options.refinement.enabled or not state.results: + return + + for net_id in state.ordered_net_ids: + result = state.results.get(net_id) + if not result or not result.path or result.outcome in {"colliding", "partial", "unroutable"}: + continue + net = state.net_specs[net_id] + self.metrics.total_refine_path_calls += 1 + self.context.cost_evaluator.collision_engine.remove_path(net_id) + refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path) + self._install_path(net_id, refined_path) + # Defer full verification until _verify_results() so we do not + # verify the same refined path twice in one route_all() call. + state.results[net_id] = RoutingResult( + net_id=net_id, + path=tuple(refined_path), + reached_target=result.reached_target, + report=result.report, + ) + + def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]: + final_results: dict[str, RoutingResult] = {} + details_by_net: dict[str, PathVerificationDetail] = {} + for net in self.context.problem.nets: + result = state.results.get(net.net_id) + if not result or not result.path: + final_results[net.net_id] = RoutingResult(net_id=net.net_id, path=(), reached_target=False) + continue + detail = self.context.cost_evaluator.collision_engine.verify_path_details( + net.net_id, + result.path, + capture_component_conflicts=self.context.options.diagnostics.capture_conflict_trace, + ) + details_by_net[net.net_id] = detail + final_results[net.net_id] = RoutingResult( + net_id=net.net_id, + path=result.path, + reached_target=result.reached_target, + report=detail.report, + ) + if self.context.options.diagnostics.capture_conflict_trace: + _, _, review = self._analyze_results( + state.ordered_net_ids, + final_results, + capture_component_conflicts=True, + count_iteration_metrics=False, + ) + self._capture_conflict_trace_entry( + state, + stage="final", + iteration=None, + results=final_results, + details_by_net=details_by_net, + review=review, + ) + return final_results + + def route_all( + self, + *, + iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None, + ) -> dict[str, RoutingResult]: + self.context.congestion_penalty = self.context.options.congestion.base_penalty + self.accumulated_expanded_nodes = [] + self.conflict_trace = [] + self.frontier_trace = [] + self.pre_pair_frontier_trace = None + self.iteration_trace = [] + self.metrics.reset_totals() + self.metrics.reset_per_route() + + state = self._prepare_state() + timed_out = self._run_iterations(state, iteration_callback) + self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes) + self._restore_best_iteration(state) + self._analyze_restored_best(state) + + if timed_out: + final_results = self._verify_results(state) + self._capture_frontier_trace(state, final_results) + return final_results + + self._run_pair_local_search(state) + self._refine_results(state) + final_results = self._verify_results(state) + self._capture_frontier_trace(state, final_results) + return final_results diff --git a/inire/router/_search.py b/inire/router/_search.py new file mode 100644 index 0000000..92fc540 --- /dev/null +++ b/inire/router/_search.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import heapq +from typing import TYPE_CHECKING + +from inire.constants import TOLERANCE_LINEAR + +from ._astar_moves import expand_moves as _expand_moves +from ._astar_types import AStarContext, AStarMetrics, AStarNode as _AStarNode, SearchRunConfig + +if TYPE_CHECKING: + from inire.geometry.components import ComponentResult + from inire.geometry.primitives import Port + + +def _reconstruct_path(end_node: _AStarNode) -> list[ComponentResult]: + path = [] + curr: _AStarNode | None = end_node + while curr and curr.component_result: + path.append(curr.component_result) + curr = curr.parent + return path[::-1] + + +def route_astar( + start: Port, + target: Port, + net_width: float, + context: AStarContext, + *, + metrics: AStarMetrics | None = None, + net_id: str = "default", + config: SearchRunConfig, +) -> list[ComponentResult] | None: + if metrics is None: + metrics = AStarMetrics() + metrics.reset_per_route() + + context.ensure_static_caches_current() + context.cost_evaluator.set_target(target) + open_set: list[_AStarNode] = [] + closed_set: dict[tuple[int, int, int], float] = {} + congestion_cache: dict[tuple, int] = {} + congestion_presence_cache: dict[tuple[str, int, int, int, int], bool] = {} + congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {} + congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {} + + start_node = _AStarNode( + start, + 0.0, + context.cost_evaluator.h_manhattan(start, target, min_bend_radius=context.min_bend_radius), + seed_index=0 if config.guidance_seed else None, + ) + heapq.heappush(open_set, start_node) + best_node = start_node + nodes_expanded = 0 + + while open_set: + if nodes_expanded >= config.node_limit: + return _reconstruct_path(best_node) if config.return_partial else None + + current = heapq.heappop(open_set) + if config.max_cost is not None and current.fh_cost[0] > config.max_cost: + metrics.pruned_cost += 1 + metrics.total_pruned_cost += 1 + continue + + if current.h_cost < best_node.h_cost: + best_node = current + + state = current.port.as_tuple() + if state in closed_set and closed_set[state] <= current.g_cost + TOLERANCE_LINEAR: + continue + closed_set[state] = current.g_cost + + if config.store_expanded: + metrics.last_expanded_nodes.append(state) + + nodes_expanded += 1 + metrics.total_nodes_expanded += 1 + metrics.nodes_expanded += 1 + + if current.port == target: + return _reconstruct_path(current) + + _expand_moves( + current, + target, + net_width, + net_id, + open_set, + closed_set, + context, + metrics, + congestion_cache, + congestion_presence_cache, + congestion_candidate_precheck_cache, + congestion_net_envelope_cache, + congestion_grid_net_cache, + congestion_grid_span_cache, + config=config, + ) + + return _reconstruct_path(best_node) if config.return_partial else None diff --git a/inire/router/_seed_materialization.py b/inire/router/_seed_materialization.py new file mode 100644 index 0000000..25a76be --- /dev/null +++ b/inire/router/_seed_materialization.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from inire.model import SearchOptions, resolve_bend_geometry +from inire.seeds import Bend90Seed, PathSeed, SBendSeed, StraightSeed + +if TYPE_CHECKING: + from inire.geometry.components import ComponentResult + from inire.geometry.primitives import Port + + +def materialize_path_seed( + seed: PathSeed, + *, + start: Port, + net_width: float, + search: SearchOptions, + clearance: float, +) -> tuple[ComponentResult, ...]: + from inire.geometry.components import Bend90, SBend, Straight + + path: list[ComponentResult] = [] + current = start + dilation = clearance / 2.0 + bend_collision_type, bend_physical_geometry = resolve_bend_geometry(search) + bend_clip_margin = search.bend_clip_margin + + for segment in seed.segments: + if isinstance(segment, StraightSeed): + component = Straight.generate(current, segment.length, net_width, dilation=dilation) + elif isinstance(segment, Bend90Seed): + component = Bend90.generate( + current, + segment.radius, + net_width, + segment.direction, + collision_type=bend_collision_type, + physical_geometry_type=bend_physical_geometry, + clip_margin=bend_clip_margin, + dilation=dilation, + ) + elif isinstance(segment, SBendSeed): + component = SBend.generate( + current, + segment.offset, + segment.radius, + net_width, + collision_type=bend_collision_type, + physical_geometry_type=bend_physical_geometry, + clip_margin=bend_clip_margin, + dilation=dilation, + ) + else: + raise TypeError(f"Unsupported seed segment: {type(segment)!r}") + path.append(component) + current = component.end_port + return tuple(path) diff --git a/inire/router/_stack.py b/inire/router/_stack.py new file mode 100644 index 0000000..9bf67a2 --- /dev/null +++ b/inire/router/_stack.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from inire.geometry.collision import RoutingWorld + from inire.model import RoutingOptions, RoutingProblem + from inire.router._astar_types import AStarContext + from inire.router._router import PathFinder + from inire.router.cost import CostEvaluator + from inire.router.danger_map import DangerMap + + +@dataclass(frozen=True, slots=True) +class RoutingStack: + world: RoutingWorld + danger_map: DangerMap + evaluator: CostEvaluator + context: AStarContext + finder: PathFinder + + +def build_routing_stack(problem: RoutingProblem, options: RoutingOptions) -> RoutingStack: + from inire.geometry.collision import RoutingWorld + from inire.router._astar_types import AStarContext + from inire.router._router import PathFinder + from inire.router.cost import CostEvaluator + from inire.router.danger_map import DangerMap + + world = RoutingWorld( + clearance=problem.clearance, + safety_zone_radius=problem.safety_zone_radius, + ) + for obstacle in problem.static_obstacles: + world.add_static_obstacle(obstacle) + + danger_map = DangerMap(bounds=problem.bounds) + danger_map.precompute(list(problem.static_obstacles)) + + objective = options.objective + evaluator = CostEvaluator( + world, + danger_map, + unit_length_cost=objective.unit_length_cost, + greedy_h_weight=options.search.greedy_h_weight, + bend_penalty=objective.bend_penalty, + sbend_penalty=objective.sbend_penalty, + danger_weight=objective.danger_weight, + ) + context = AStarContext(evaluator, problem, options) + finder = PathFinder(context) + return RoutingStack( + world=world, + danger_map=danger_map, + evaluator=evaluator, + context=context, + finder=finder, + ) diff --git a/inire/router/cost.py b/inire/router/cost.py new file mode 100644 index 0000000..3bb4765 --- /dev/null +++ b/inire/router/cost.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +from time import perf_counter_ns +from typing import TYPE_CHECKING + +import numpy as np + +from inire.constants import TOLERANCE_LINEAR +from inire.model import ObjectiveWeights + +if TYPE_CHECKING: + from collections.abc import Sequence + from inire.geometry.collision import RoutingWorld + from inire.geometry.components import ComponentResult, MoveKind + from inire.geometry.primitives import Port + from inire.router.danger_map import DangerMap + + +class CostEvaluator: + __slots__ = ( + "collision_engine", + "danger_map", + "_search_weights", + "_greedy_h_weight", + "_target_x", + "_target_y", + "_target_r", + "_target_cos", + "_target_sin", + ) + + def __init__( + self, + collision_engine: RoutingWorld, + danger_map: DangerMap | None = None, + unit_length_cost: float = 1.0, + greedy_h_weight: float = 1.5, + bend_penalty: float = 250.0, + sbend_penalty: float | None = None, + danger_weight: float = 1.0, + ) -> None: + actual_sbend_penalty = 2.0 * bend_penalty if sbend_penalty is None else sbend_penalty + self.collision_engine = collision_engine + self.danger_map = danger_map + self._search_weights = ObjectiveWeights( + unit_length_cost=unit_length_cost, + bend_penalty=bend_penalty, + sbend_penalty=actual_sbend_penalty, + danger_weight=danger_weight, + ) + self._greedy_h_weight = float(greedy_h_weight) + self._target_x = 0.0 + self._target_y = 0.0 + self._target_r = 0 + self._target_cos = 1.0 + self._target_sin = 0.0 + + @property + def default_weights(self) -> ObjectiveWeights: + return self._search_weights + + @property + def greedy_h_weight(self) -> float: + return self._greedy_h_weight + + @greedy_h_weight.setter + def greedy_h_weight(self, value: float) -> None: + self._greedy_h_weight = float(value) + + def _resolve_weights(self, weights: ObjectiveWeights | None) -> ObjectiveWeights: + return self._search_weights if weights is None else weights + + def set_target(self, target: Port) -> None: + self._target_x = target.x + self._target_y = target.y + self._target_r = int(target.r) + rad = np.radians(target.r) + self._target_cos = np.cos(rad) + self._target_sin = np.sin(rad) + + def h_manhattan( + self, + current: Port, + target: Port, + *, + min_bend_radius: float = 50.0, + ) -> float: + tx, ty = target.x, target.y + if abs(tx - self._target_x) > TOLERANCE_LINEAR or abs(ty - self._target_y) > TOLERANCE_LINEAR or target.r != self._target_r: + self.set_target(target) + + dx = abs(current.x - tx) + dy = abs(current.y - ty) + dist = dx + dy + bp = self._search_weights.bend_penalty + penalty = 0.0 + + curr_r = current.r + diff = abs(curr_r - self._target_r) % 360 + if diff > 0: + penalty += 2 * bp if diff == 180 else bp + + v_dx = tx - current.x + v_dy = ty - current.y + side_proj = v_dx * self._target_cos + v_dy * self._target_sin + perp_dist = abs(v_dx * self._target_sin - v_dy * self._target_cos) + if side_proj < 0 or (side_proj < min_bend_radius and perp_dist > 0): + penalty += 2 * bp + + if curr_r == 0: + c_cos, c_sin = 1.0, 0.0 + elif curr_r == 90: + c_cos, c_sin = 0.0, 1.0 + elif curr_r == 180: + c_cos, c_sin = -1.0, 0.0 + else: + c_cos, c_sin = 0.0, -1.0 + + move_proj = v_dx * c_cos + v_dy * c_sin + if move_proj < 0: + penalty += 2 * bp + if diff == 0 and perp_dist > 0: + penalty += 2 * bp + + return self._greedy_h_weight * (dist + penalty) + + def score_component( + self, + component: ComponentResult, + *, + start_port: Port | None = None, + weights: ObjectiveWeights | None = None, + ) -> float: + metrics = self.collision_engine.metrics + if metrics is not None: + metrics.total_score_component_calls += 1 + start_ns = perf_counter_ns() + active_weights = self._resolve_weights(weights) + danger_map = self.danger_map + end_port = component.end_port + if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y): + if metrics is not None: + metrics.total_score_component_total_ns += perf_counter_ns() - start_ns + return 1e15 + + move_radius = None + if component.move_type == "bend90": + move_radius = component.length * 2.0 / np.pi if component.length > 0 else None + total_cost = component.length * active_weights.unit_length_cost + self.component_penalty( + component.move_type, + move_radius=move_radius, + weights=active_weights, + ) + + # Skip danger sampling entirely when there are no static obstacles in the KD-tree. + if danger_map is not None and active_weights.danger_weight and danger_map.tree is not None: + cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0 + cost_e = danger_map.get_cost(end_port.x, end_port.y) + if start_port: + mid_x = (start_port.x + end_port.x) / 2.0 + mid_y = (start_port.y + end_port.y) / 2.0 + cost_m = danger_map.get_cost(mid_x, mid_y) + total_cost += component.length * active_weights.danger_weight * (cost_s + cost_m + cost_e) / 3.0 + else: + total_cost += component.length * active_weights.danger_weight * cost_e + if metrics is not None: + metrics.total_score_component_total_ns += perf_counter_ns() - start_ns + return total_cost + + def component_penalty( + self, + move_type: MoveKind, + *, + move_radius: float | None = None, + weights: ObjectiveWeights | None = None, + ) -> float: + active_weights = self._resolve_weights(weights) + penalty = 0.0 + if move_type == "sbend": + penalty = active_weights.sbend_penalty + elif move_type == "bend90": + penalty = active_weights.bend_penalty + if move_radius is not None and move_radius > TOLERANCE_LINEAR and penalty > 0: + penalty *= (10.0 / move_radius) ** 0.5 + return penalty + + def path_cost( + self, + start_port: Port, + path: Sequence[ComponentResult], + *, + weights: ObjectiveWeights | None = None, + ) -> float: + metrics = self.collision_engine.metrics + if metrics is not None: + metrics.total_path_cost_calls += 1 + active_weights = self._resolve_weights(weights) + total = 0.0 + current_port = start_port + for component in path: + total += self.score_component( + component, + start_port=current_port, + weights=active_weights, + ) + current_port = component.end_port + return total diff --git a/inire/router/danger_map.py b/inire/router/danger_map.py new file mode 100644 index 0000000..c9ca9d3 --- /dev/null +++ b/inire/router/danger_map.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections import OrderedDict +from time import perf_counter_ns +from typing import TYPE_CHECKING + +import numpy +from scipy.spatial import cKDTree + +if TYPE_CHECKING: + from shapely.geometry import Polygon + from inire.router._astar_types import AStarMetrics + + +_COST_CACHE_SIZE = 100000 + + +class DangerMap: + """ + A proximity cost evaluator using a KD-Tree of obstacle boundary points. + Scales with obstacle perimeter rather than design area. + """ + __slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache', 'metrics') + + def __init__( + self, + bounds: tuple[float, float, float, float], + resolution: float = 5.0, + safety_threshold: float = 10.0, + k: float = 1.0, + ) -> None: + """ + Initialize the Danger Map. + + Args: + bounds: (minx, miny, maxx, maxy) in um. + resolution: Sampling resolution for obstacle boundaries (um). + safety_threshold: Proximity limit (um). + k: Penalty multiplier. + """ + self.minx, self.miny, self.maxx, self.maxy = bounds + self.resolution = resolution + self.safety_threshold = safety_threshold + self.k = k + self.tree: cKDTree | None = None + self._cost_cache: OrderedDict[tuple[int, int], float] = OrderedDict() + self.metrics: AStarMetrics | None = None + + def precompute(self, obstacles: list[Polygon]) -> None: + """ + Pre-compute the proximity tree by sampling obstacle boundaries. + """ + all_points = [] + for poly in obstacles: + # Sample exterior + exterior = poly.exterior + dist = 0.0 + while dist < exterior.length: + pt = exterior.interpolate(dist) + all_points.append((pt.x, pt.y)) + dist += self.resolution + # Sample interiors (holes) + for interior in poly.interiors: + dist = 0.0 + while dist < interior.length: + pt = interior.interpolate(dist) + all_points.append((pt.x, pt.y)) + dist += self.resolution + + if all_points: + self.tree = cKDTree(numpy.array(all_points)) + else: + self.tree = None + + self._cost_cache.clear() + + def is_within_bounds(self, x: float, y: float) -> bool: + """ + Check if a coordinate is within the design bounds. + """ + return self.minx <= x <= self.maxx and self.miny <= y <= self.maxy + + def get_cost(self, x: float, y: float) -> float: + """ + Get the proximity cost at a specific coordinate using the KD-Tree. + Coordinates are quantized to 1nm to improve cache performance. + """ + metrics = self.metrics + if metrics is not None: + metrics.total_danger_map_lookup_calls += 1 + start_ns = perf_counter_ns() + qx_milli = int(round(x * 1000)) + qy_milli = int(round(y * 1000)) + key = (qx_milli, qy_milli) + if key in self._cost_cache: + if metrics is not None: + metrics.total_danger_map_cache_hits += 1 + metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns + self._cost_cache.move_to_end(key) + return self._cost_cache[key] + + if metrics is not None: + metrics.total_danger_map_cache_misses += 1 + cost = self._compute_cost_quantized(qx_milli, qy_milli) + self._cost_cache[key] = cost + if len(self._cost_cache) > _COST_CACHE_SIZE: + self._cost_cache.popitem(last=False) + if metrics is not None: + metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns + return cost + + def _compute_cost_quantized(self, qx_milli: int, qy_milli: int) -> float: + qx = qx_milli / 1000.0 + qy = qy_milli / 1000.0 + if not self.is_within_bounds(qx, qy): + return 1e15 + if self.tree is None: + return 0.0 + if self.metrics is not None: + self.metrics.total_danger_map_query_calls += 1 + dist, _ = self.tree.query([qx, qy], distance_upper_bound=self.safety_threshold) + if dist >= self.safety_threshold: + return 0.0 + safe_dist = max(dist, 0.1) + return float(self.k / (safe_dist ** 2)) diff --git a/inire/router/refiner.py b/inire/router/refiner.py new file mode 100644 index 0000000..5f3a6c2 --- /dev/null +++ b/inire/router/refiner.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, Any, Literal + +from inire.geometry.component_overlap import components_overlap +from inire.geometry.components import Bend90, Straight + +if TYPE_CHECKING: + from collections.abc import Sequence + + from inire.geometry.collision import RoutingWorld + from inire.geometry.components import ComponentResult + from inire.geometry.primitives import Port + from inire.router._astar_types import AStarContext + + +def component_hits_ancestor_chain(component: ComponentResult, parent_node: Any) -> bool: + current = parent_node + while current and current.parent: + ancestor_component = current.component_result + if ancestor_component and components_overlap(component, ancestor_component): + return True + current = current.parent + return False + + +class PathRefiner: + __slots__ = ("context",) + + def __init__(self, context: AStarContext) -> None: + self.context = context + + @property + def collision_engine(self) -> RoutingWorld: + return self.context.cost_evaluator.collision_engine + + def path_cost( + self, + path: Sequence[ComponentResult], + *, + start: Port | None = None, + ) -> float: + if not path: + return 0.0 + actual_start = path[0].start_port if start is None else start + return self.score_path(actual_start, path) + + def score_path(self, start: Port, path: Sequence[ComponentResult]) -> float: + weights = self.context.options.refinement.objective or self.context.cost_evaluator.default_weights + return self.context.cost_evaluator.path_cost(start, path, weights=weights) + + def _path_ports(self, start: Port, path: Sequence[ComponentResult]) -> list[Port]: + ports = [start] + ports.extend(comp.end_port for comp in path) + return ports + + def _to_local(self, start: Port, point: Port) -> tuple[float, float]: + dx = point.x - start.x + dy = point.y - start.y + if start.r == 0: + return dx, dy + if start.r == 90: + return dy, -dx + if start.r == 180: + return -dx, -dy + return -dy, dx + + def _to_local_xy(self, start: Port, x: float, y: float) -> tuple[float, float]: + dx = float(x) - start.x + dy = float(y) - start.y + if start.r == 0: + return dx, dy + if start.r == 90: + return dy, -dx + if start.r == 180: + return -dx, -dy + return -dy, dx + + def _window_query_bounds(self, start: Port, target: Port, path: Sequence[ComponentResult], pad: float) -> tuple[float, float, float, float]: + min_x = float(min(start.x, target.x)) + min_y = float(min(start.y, target.y)) + max_x = float(max(start.x, target.x)) + max_y = float(max(start.y, target.y)) + for comp in path: + bounds = comp.total_bounds + min_x = min(min_x, bounds[0]) + min_y = min(min_y, bounds[1]) + max_x = max(max_x, bounds[2]) + max_y = max(max_y, bounds[3]) + return (min_x - pad, min_y - pad, max_x + pad, max_y + pad) + + def _candidate_side_extents( + self, + start: Port, + target: Port, + window_path: Sequence[ComponentResult], + net_width: float, + radius: float, + ) -> list[float]: + local_dx, local_dy = self._to_local(start, target) + if local_dx < 4.0 * radius - 0.01: + return [] + + local_points = [self._to_local(start, start)] + local_points.extend(self._to_local(start, comp.end_port) for comp in window_path) + min_side = float(min(point[1] for point in local_points)) + max_side = float(max(point[1] for point in local_points)) + + positive_anchors: set[float] = set() + negative_anchors: set[float] = set() + direct_extents: set[float] = set() + + if max_side > 0.01: + positive_anchors.add(max_side) + direct_extents.add(max_side) + if min_side < -0.01: + negative_anchors.add(min_side) + direct_extents.add(min_side) + if local_dy > 0: + positive_anchors.add(float(local_dy)) + elif local_dy < 0: + negative_anchors.add(float(local_dy)) + + pad = 2.0 * radius + self.collision_engine.clearance + net_width + query_bounds = self._window_query_bounds(start, target, window_path, pad) + x_min = min(0.0, float(local_dx)) - 0.01 + x_max = max(0.0, float(local_dx)) + 0.01 + + for bounds in self.collision_engine.iter_static_obstacle_bounds(query_bounds): + self.context.metrics.total_refinement_static_bounds_checked += 1 + local_corners = ( + self._to_local_xy(start, bounds[0], bounds[1]), + self._to_local_xy(start, bounds[0], bounds[3]), + self._to_local_xy(start, bounds[2], bounds[1]), + self._to_local_xy(start, bounds[2], bounds[3]), + ) + obs_min_x = min(pt[0] for pt in local_corners) + obs_max_x = max(pt[0] for pt in local_corners) + if obs_max_x < x_min or obs_min_x > x_max: + continue + obs_min_y = min(pt[1] for pt in local_corners) + obs_max_y = max(pt[1] for pt in local_corners) + positive_anchors.add(obs_max_y) + negative_anchors.add(obs_min_y) + + for bounds in self.collision_engine.iter_dynamic_path_bounds(query_bounds): + self.context.metrics.total_refinement_dynamic_bounds_checked += 1 + local_corners = ( + self._to_local_xy(start, bounds[0], bounds[1]), + self._to_local_xy(start, bounds[0], bounds[3]), + self._to_local_xy(start, bounds[2], bounds[1]), + self._to_local_xy(start, bounds[2], bounds[3]), + ) + obs_min_x = min(pt[0] for pt in local_corners) + obs_max_x = max(pt[0] for pt in local_corners) + if obs_max_x < x_min or obs_min_x > x_max: + continue + obs_min_y = min(pt[1] for pt in local_corners) + obs_max_y = max(pt[1] for pt in local_corners) + positive_anchors.add(obs_max_y) + negative_anchors.add(obs_min_y) + + for anchor in tuple(positive_anchors): + if anchor > max(0.0, float(local_dy)) - 0.01: + direct_extents.add(anchor + pad) + for anchor in tuple(negative_anchors): + if anchor < min(0.0, float(local_dy)) + 0.01: + direct_extents.add(anchor - pad) + + self.context.metrics.total_refinement_candidate_side_extents += len(direct_extents) + return sorted(direct_extents, key=lambda value: (abs(value), value)) + + def _build_same_orientation_dogleg( + self, + start: Port, + target: Port, + net_width: float, + radius: float, + side_extent: float, + ) -> list[ComponentResult] | None: + local_dx, local_dy = self._to_local(start, target) + if local_dx < 4.0 * radius - 0.01 or abs(side_extent) < 0.01: + return None + + side_abs = abs(side_extent) + first_straight = side_abs - 2.0 * radius + second_straight = side_abs - 2.0 * radius - math.copysign(float(local_dy), side_extent) + if first_straight < -0.01 or second_straight < -0.01: + return None + min_straight = self.context.options.search.min_straight_length + if 0.01 < first_straight < min_straight - 0.01: + return None + if 0.01 < second_straight < min_straight - 0.01: + return None + + forward_length = local_dx - 4.0 * radius + if forward_length < -0.01: + return None + if 0.01 < forward_length < min_straight - 0.01: + return None + + first_dir: Literal["CW", "CCW"] = "CCW" if side_extent > 0 else "CW" + second_dir: Literal["CW", "CCW"] = "CW" if side_extent > 0 else "CCW" + dilation = self.collision_engine.clearance / 2.0 + + path: list[ComponentResult] = [] + curr = start + + for direction, straight_len in ( + (first_dir, first_straight), + (second_dir, forward_length), + (second_dir, second_straight), + (first_dir, None), + ): + bend = Bend90.generate(curr, radius, net_width, direction, dilation=dilation) + path.append(bend) + curr = bend.end_port + if straight_len is None: + continue + if straight_len > 0.01: + straight = Straight.generate(curr, straight_len, net_width, dilation=dilation) + path.append(straight) + curr = straight.end_port + + if curr != target: + return None + return path + + def _iter_refinement_windows(self, start: Port, path: Sequence[ComponentResult]) -> list[tuple[int, int]]: + ports = self._path_ports(start, path) + windows: list[tuple[int, int]] = [] + min_radius = min(self.context.options.search.bend_radii, default=0.0) + + for window_size in range(len(path), 0, -1): + for start_idx in range(len(path) - window_size + 1): + end_idx = start_idx + window_size + window = path[start_idx:end_idx] + bend_count = sum(1 for comp in window if comp.move_type == "bend90") + if bend_count < 4: + continue + window_start = ports[start_idx] + window_end = ports[end_idx] + if window_start.r != window_end.r: + continue + local_dx, _ = self._to_local(window_start, window_end) + if local_dx < 4.0 * min_radius - 0.01: + continue + self.context.metrics.total_refinement_windows_considered += 1 + windows.append((start_idx, end_idx)) + return windows + + def _try_refine_window( + self, + net_id: str, + start: Port, + net_width: float, + path: list[ComponentResult], + start_idx: int, + end_idx: int, + best_cost: float, + ) -> tuple[list[ComponentResult], float] | None: + ports = self._path_ports(start, path) + window_start = ports[start_idx] + window_end = ports[end_idx] + window_path = path[start_idx:end_idx] + + best_path: list[ComponentResult] | None = None + best_candidate_cost = best_cost + + for radius in self.context.options.search.bend_radii: + side_extents = self._candidate_side_extents(window_start, window_end, window_path, net_width, radius) + for side_extent in side_extents: + replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent) + if replacement is None: + continue + self.context.metrics.total_refinement_candidates_built += 1 + candidate_path = path[:start_idx] + replacement + path[end_idx:] + self.context.metrics.total_refinement_candidates_verified += 1 + report = self.collision_engine.verify_path_report(net_id, candidate_path) + if not report.is_valid: + continue + candidate_cost = self.path_cost(candidate_path) + if candidate_cost + 1e-6 < best_candidate_cost: + self.context.metrics.total_refinement_candidates_accepted += 1 + best_candidate_cost = candidate_cost + best_path = candidate_path + + if best_path is None: + return None + return best_path, best_candidate_cost + + def refine_path( + self, + net_id: str, + start: Port, + net_width: float, + path: Sequence[ComponentResult], + ) -> list[ComponentResult]: + if not path: + return list(path) + + path = list(path) + + bend_count = sum(1 for comp in path if comp.move_type == "bend90") + if bend_count < 4: + return path + + best_path = path + best_cost = self.score_path(start, path) + + for _ in range(3): + improved = False + for start_idx, end_idx in self._iter_refinement_windows(start, best_path): + refined = self._try_refine_window(net_id, start, net_width, best_path, start_idx, end_idx, best_cost) + if refined is None: + continue + best_path, best_cost = refined + improved = True + break + if not improved: + break + + return best_path diff --git a/inire/router/results.py b/inire/router/results.py new file mode 100644 index 0000000..a9d3c1f --- /dev/null +++ b/inire/router/results.py @@ -0,0 +1,16 @@ +"""Semi-private compatibility exports for router result types. + +These deep-module imports remain accessible for advanced use, but they are +unstable and may change without notice. Prefer importing public result types +from ``inire`` or ``inire.results``. +""" + +from inire.results import RouteMetrics, RoutingOutcome, RoutingReport, RoutingResult, RoutingRunResult + +__all__ = [ + "RouteMetrics", + "RoutingOutcome", + "RoutingReport", + "RoutingResult", + "RoutingRunResult", +] diff --git a/inire/router/visibility.py b/inire/router/visibility.py new file mode 100644 index 0000000..ea4b610 --- /dev/null +++ b/inire/router/visibility.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import numpy +from typing import TYPE_CHECKING + +import rtree + +if TYPE_CHECKING: + from inire.geometry.collision import RoutingWorld + from inire.geometry.primitives import Port + +from inire.geometry.primitives import Port + + +class VisibilityManager: + """ + Manages corners of static obstacles for sparse A* / Visibility Graph jumps. + """ + __slots__ = ( + "collision_engine", + "corners", + "corner_index", + "_corner_graph", + "_point_visibility_cache", + "_corner_index_version", + "_corner_graph_version", + ) + + def __init__(self, collision_engine: RoutingWorld) -> None: + self.collision_engine = collision_engine + self.corners: list[tuple[float, float]] = [] + self.corner_index = rtree.index.Index() + self._corner_graph: dict[int, list[tuple[float, float, float]]] = {} + self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {} + self._corner_index_version = -1 + self._corner_graph_version = -1 + + def clear_cache(self) -> None: + """ + Reset all static visibility data. + """ + self.corners = [] + self.corner_index = rtree.index.Index() + self._corner_graph = {} + self._point_visibility_cache = {} + self._corner_index_version = -1 + self._corner_graph_version = -1 + + def ensure_corner_index_current(self) -> None: + if self._corner_index_version != self.collision_engine.get_static_version(): + self._build_corner_index() + + def ensure_corner_graph_current(self) -> None: + self.ensure_corner_index_current() + static_version = self.collision_engine.get_static_version() + if self._corner_graph_version != static_version: + self._build_corner_graph() + + def _ensure_current(self) -> None: + self.ensure_corner_graph_current() + + def _build_corner_index(self) -> None: + if self.collision_engine.metrics is not None: + self.collision_engine.metrics.total_visibility_corner_index_builds += 1 + self.corners = [] + self.corner_index = rtree.index.Index() + self._corner_graph = {} + self._point_visibility_cache = {} + self._corner_graph_version = -1 + self._corner_index_version = self.collision_engine.get_static_version() + raw_corners = [] + for poly in self.collision_engine.iter_static_dilated_geometries(): + coords = list(poly.exterior.coords) + if coords[0] == coords[-1]: + coords = coords[:-1] + raw_corners.extend(coords) + for ring in poly.interiors: + coords = list(ring.coords) + if coords[0] == coords[-1]: + coords = coords[:-1] + raw_corners.extend(coords) + + if not raw_corners: + return + + seen = set() + for x, y in raw_corners: + sx, sy = round(x, 3), round(y, 3) + if (sx, sy) not in seen: + seen.add((sx, sy)) + self.corners.append((sx, sy)) + + for i, (x, y) in enumerate(self.corners): + self.corner_index.insert(i, (x, y, x, y)) + + def _build_corner_graph(self) -> None: + """ + Pre-compute corner-to-corner visibility from the current corner index. + """ + self.ensure_corner_index_current() + if self.collision_engine.metrics is not None: + self.collision_engine.metrics.total_visibility_builds += 1 + self._corner_graph = {} + self._corner_graph_version = self.collision_engine.get_static_version() + + if not self.corners: + return + + # Pre-compute visibility graph between corners + num_corners = len(self.corners) + if num_corners > 200: + # Limit pre-computation if too many corners + return + + for i in range(num_corners): + self._corner_graph[i] = [] + p1 = Port(self.corners[i][0], self.corners[i][1], 0) + for j in range(num_corners): + if i == j: + continue + if self.collision_engine.metrics is not None: + self.collision_engine.metrics.total_visibility_corner_pairs_checked += 1 + cx, cy = self.corners[j] + dx, dy = cx - p1.x, cy - p1.y + dist = numpy.sqrt(dx**2 + dy**2) + angle = numpy.degrees(numpy.arctan2(dy, dx)) + reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05, caller="visibility_build") + if reach >= dist - 0.01: + self._corner_graph[i].append((cx, cy, dist)) + + def _corner_idx_at(self, origin: Port) -> int | None: + self.ensure_corner_index_current() + ox, oy = round(origin.x, 3), round(origin.y, 3) + nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001))) + for idx in nearby: + cx, cy = self.corners[idx] + if abs(cx - ox) < 1e-4 and abs(cy - oy) < 1e-4: + return idx + return None + + def get_tangent_corner_candidates( + self, + origin: Port, + *, + min_forward: float, + max_forward: float, + radii: tuple[float, ...], + tolerance: float = 2.0, + ) -> list[int]: + self.ensure_corner_index_current() + if max_forward <= min_forward or not radii or not self.corners: + return [] + + candidate_ids: set[int] = set() + x0 = float(origin.x) + y0 = float(origin.y) + + def _add_hits(bounds: tuple[float, float, float, float]) -> None: + min_x, min_y, max_x, max_y = bounds + if min_x > max_x or min_y > max_y: + return + candidate_ids.update(self.corner_index.intersection(bounds)) + + for radius in radii: + if origin.r == 0: + x_bounds = (x0 + min_forward, x0 + max_forward) + _add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance)) + _add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance)) + elif origin.r == 180: + x_bounds = (x0 - max_forward, x0 - min_forward) + _add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance)) + _add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance)) + elif origin.r == 90: + y_bounds = (y0 + min_forward, y0 + max_forward) + _add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1])) + _add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1])) + else: + y_bounds = (y0 - max_forward, y0 - min_forward) + _add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1])) + _add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1])) + + return sorted(candidate_ids) + + def get_point_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]: + """ + Find visible corners from an arbitrary point. + 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_corner_index_current() + if max_dist < 0: + return [] + + corner_idx = self._corner_idx_at(origin) + if corner_idx is not None: + self.ensure_corner_graph_current() + if corner_idx is not None and corner_idx in self._corner_graph: + return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist] + + ox, oy = round(origin.x, 3), round(origin.y, 3) + cache_key = (int(ox * 1000), int(oy * 1000), int(round(max_dist * 1000))) + 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] + 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) + candidates = list(self.corner_index.intersection(bounds)) + + visible = [] + for i in candidates: + cx, cy = self.corners[i] + dx, dy = cx - origin.x, cy - origin.y + dist = numpy.sqrt(dx**2 + dy**2) + + if dist > max_dist or dist < 1e-3: + continue + + angle = numpy.degrees(numpy.arctan2(dy, dx)) + reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05, caller="visibility_query") + if reach >= dist - 0.01: + visible.append((cx, cy, dist)) + + self._point_visibility_cache[cache_key] = visible + return visible + + def get_corner_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]: + """ + 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. + """ + if self.collision_engine.metrics is not None: + self.collision_engine.metrics.total_visibility_corner_queries_exact += 1 + self.ensure_corner_graph_current() + if max_dist < 0: + return [] + + corner_idx = self._corner_idx_at(origin) + 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_exact += 1 + return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist] + return [] diff --git a/inire/seeds.py b/inire/seeds.py new file mode 100644 index 0000000..635e489 --- /dev/null +++ b/inire/seeds.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + + +BendDirection = Literal["CW", "CCW"] + + +@dataclass(frozen=True, slots=True) +class StraightSeed: + length: float + + def __post_init__(self) -> None: + object.__setattr__(self, "length", float(self.length)) + + +@dataclass(frozen=True, slots=True) +class Bend90Seed: + radius: float + direction: BendDirection + + def __post_init__(self) -> None: + object.__setattr__(self, "radius", float(self.radius)) + + +@dataclass(frozen=True, slots=True) +class SBendSeed: + offset: float + radius: float + + def __post_init__(self) -> None: + object.__setattr__(self, "offset", float(self.offset)) + object.__setattr__(self, "radius", float(self.radius)) + + +PathSegmentSeed = StraightSeed | Bend90Seed | SBendSeed + + +@dataclass(frozen=True, slots=True) +class PathSeed: + segments: tuple[PathSegmentSeed, ...] + + def __post_init__(self) -> None: + segments = tuple(self.segments) + if any(not isinstance(segment, StraightSeed | Bend90Seed | SBendSeed) for segment in segments): + raise TypeError("PathSeed segments must be StraightSeed, Bend90Seed, or SBendSeed instances") + object.__setattr__(self, "segments", segments) diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py new file mode 100644 index 0000000..e60fa34 --- /dev/null +++ b/inire/tests/example_scenarios.py @@ -0,0 +1,688 @@ +from __future__ import annotations + +from dataclasses import dataclass +from time import perf_counter +from collections.abc import Callable + +from shapely.geometry import Polygon, box + +from inire import ( + CongestionOptions, + DiagnosticsOptions, + NetSpec, + ObjectiveWeights, + RefinementOptions, + RoutingOptions, + RoutingProblem, + RoutingResult, + RoutingRunResult, + SearchOptions, +) +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.results import RouteMetrics +from inire.router._astar_types import AStarContext, AStarMetrics +from inire.router._router import PathFinder +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + +_SEARCH_FIELDS = set(SearchOptions.__dataclass_fields__) +_CONGESTION_FIELDS = set(CongestionOptions.__dataclass_fields__) +_REFINEMENT_FIELDS = set(RefinementOptions.__dataclass_fields__) +_DIAGNOSTICS_FIELDS = set(DiagnosticsOptions.__dataclass_fields__) +_OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__) + +ScenarioOutcome = tuple[float, int, int, int] +ScenarioRun = Callable[[], ScenarioOutcome] +ScenarioSnapshotRun = Callable[[], "ScenarioSnapshot"] +TraceScenarioRun = Callable[[], RoutingRunResult] + + +@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: + return ( + duration_s, + len(results), + sum(1 for result in results.values() if result.is_valid), + sum(1 for result in results.values() if result.reached_target), + ) + + +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 _make_run_result( + results: dict[str, RoutingResult], + pathfinder: PathFinder, +) -> RoutingRunResult: + return RoutingRunResult( + results_by_net=results, + metrics=pathfinder.metrics.snapshot(), + expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes), + conflict_trace=tuple(pathfinder.conflict_trace), + frontier_trace=tuple(pathfinder.frontier_trace), + pre_pair_frontier_trace=pathfinder.pre_pair_frontier_trace, + iteration_trace=tuple(pathfinder.iteration_trace), + ) + + +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( + bounds: tuple[float, float, float, float], + *, + clearance: float = 2.0, + obstacles: list[Polygon] | None = None, + bend_penalty: float = 50.0, + sbend_penalty: float = 150.0, +) -> CostEvaluator: + static_obstacles = obstacles or [] + engine = RoutingWorld(clearance=clearance) + for obstacle in static_obstacles: + engine.add_static_obstacle(obstacle) + + danger_map = DangerMap(bounds=bounds) + danger_map.precompute(static_obstacles) + return CostEvaluator(engine, danger_map, bend_penalty=bend_penalty, sbend_penalty=sbend_penalty) + + +def _net_specs( + netlist: dict[str, tuple[Port, Port]], + widths: dict[str, float], +) -> tuple[NetSpec, ...]: + return tuple( + NetSpec(net_id=net_id, start=start, target=target, width=widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + + +def _build_options(**overrides: object) -> RoutingOptions: + search_overrides = {key: value for key, value in overrides.items() if key in _SEARCH_FIELDS} + congestion_overrides = {key: value for key, value in overrides.items() if key in _CONGESTION_FIELDS} + refinement_overrides = {key: value for key, value in overrides.items() if key in _REFINEMENT_FIELDS} + diagnostics_overrides = {key: value for key, value in overrides.items() if key in _DIAGNOSTICS_FIELDS} + objective_overrides = {key: value for key, value in overrides.items() if key in _OBJECTIVE_FIELDS} + return RoutingOptions( + search=SearchOptions(**search_overrides), + congestion=CongestionOptions(**congestion_overrides), + refinement=RefinementOptions(**refinement_overrides), + diagnostics=DiagnosticsOptions(**diagnostics_overrides), + objective=ObjectiveWeights(**objective_overrides), + ) + + +def _build_pathfinder( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + nets: tuple[NetSpec, ...], + metrics: AStarMetrics | None = None, + **request_kwargs: object, +) -> PathFinder: + resolved_metrics = AStarMetrics() if metrics is None else metrics + return PathFinder( + AStarContext( + evaluator, + RoutingProblem(bounds=bounds, nets=nets), + _build_options(**request_kwargs), + metrics=resolved_metrics, + ), + metrics=resolved_metrics, + ) + + +def _build_routing_stack( + *, + bounds: tuple[float, float, float, float], + netlist: dict[str, tuple[Port, Port]], + widths: dict[str, float], + clearance: float = 2.0, + obstacles: list[Polygon] | None = None, + evaluator_kwargs: dict[str, float] | None = None, + request_kwargs: dict[str, object] | None = None, +) -> tuple[RoutingWorld, CostEvaluator, AStarMetrics, object]: + static_obstacles = obstacles or [] + engine = RoutingWorld(clearance=clearance) + for obstacle in static_obstacles: + engine.add_static_obstacle(obstacle) + + danger_map = DangerMap(bounds=bounds) + danger_map.precompute(static_obstacles) + + evaluator = CostEvaluator(engine, danger_map, **(evaluator_kwargs or {})) + metrics = AStarMetrics() + pathfinder = _build_pathfinder( + evaluator, + bounds=bounds, + nets=_net_specs(netlist, widths), + metrics=metrics, + **(request_kwargs or {}), + ) + return engine, evaluator, metrics, pathfinder + + +def snapshot_example_01() -> ScenarioSnapshot: + netlist = {"net1": (Port(10, 50, 0), Port(90, 50, 0))} + widths = {"net1": 2.0} + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 100, 100), + netlist=netlist, + widths=widths, + request_kwargs={"bend_radii": [10.0]}, + ) + t0 = perf_counter() + results = pathfinder.route_all() + t1 = perf_counter() + return _make_snapshot("example_01_simple_route", results, t1 - t0, pathfinder.metrics.snapshot()) + + +def run_example_01() -> ScenarioOutcome: + return snapshot_example_01().as_outcome() + + +def snapshot_example_02() -> ScenarioSnapshot: + netlist = { + "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), + "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), + "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), + } + widths = dict.fromkeys(netlist, 2.0) + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 100, 100), + netlist=netlist, + widths=widths, + evaluator_kwargs={ + "greedy_h_weight": 1.5, + "bend_penalty": 50.0, + "sbend_penalty": 150.0, + }, + request_kwargs={ + "bend_radii": [10.0], + "sbend_radii": [10.0], + "base_penalty": 1000.0, + }, + ) + t0 = perf_counter() + results = pathfinder.route_all() + t1 = perf_counter() + return _make_snapshot("example_02_congestion_resolution", results, t1 - t0, pathfinder.metrics.snapshot()) + + +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))} + widths_a = {"netA": 2.0} + engine, evaluator, _, pathfinder = _build_routing_stack( + bounds=(0, -50, 100, 50), + netlist=netlist_a, + widths=widths_a, + request_kwargs={"bend_radii": [10.0]}, + ) + t0 = perf_counter() + results_a = pathfinder.route_all() + metrics_a = pathfinder.metrics.snapshot() + for polygon in results_a["netA"].locked_geometry: + engine.add_static_obstacle(polygon) + pathfinder_b = _build_pathfinder( + evaluator, + bounds=(0, -50, 100, 50), + nets=_net_specs({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0}), + bend_radii=[10.0], + ) + results_b = pathfinder_b.route_all() + t1 = perf_counter() + 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_03() -> ScenarioOutcome: + return snapshot_example_03().as_outcome() + + +def snapshot_example_04() -> ScenarioSnapshot: + netlist = { + "sbend_only": (Port(10, 50, 0), Port(60, 55, 0)), + "multi_radii": (Port(10, 10, 0), Port(90, 90, 0)), + } + widths = {"sbend_only": 2.0, "multi_radii": 2.0} + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 100, 100), + netlist=netlist, + widths=widths, + evaluator_kwargs={ + "unit_length_cost": 1.0, + "bend_penalty": 10.0, + "sbend_penalty": 20.0, + }, + request_kwargs={ + "node_limit": 50000, + "bend_radii": [10.0, 30.0], + "sbend_offsets": [5.0], + }, + ) + t0 = perf_counter() + results = pathfinder.route_all() + t1 = perf_counter() + return _make_snapshot("example_04_sbends_and_radii", results, t1 - t0, pathfinder.metrics.snapshot()) + + +def run_example_04() -> ScenarioOutcome: + return snapshot_example_04().as_outcome() + + +def snapshot_example_05() -> ScenarioSnapshot: + netlist = { + "u_turn": (Port(50, 50, 0), Port(50, 70, 180)), + "loop": (Port(100, 100, 90), Port(100, 80, 270)), + "zig_zag": (Port(20, 150, 0), Port(180, 150, 0)), + } + widths = dict.fromkeys(netlist, 2.0) + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 200, 200), + netlist=netlist, + widths=widths, + evaluator_kwargs={"bend_penalty": 50.0}, + request_kwargs={"bend_radii": [20.0]}, + ) + t0 = perf_counter() + results = pathfinder.route_all() + t1 = perf_counter() + return _make_snapshot("example_05_orientation_stress", results, t1 - t0, pathfinder.metrics.snapshot()) + + +def run_example_05() -> ScenarioOutcome: + return snapshot_example_05().as_outcome() + + +def trace_example_05() -> RoutingRunResult: + netlist = { + "u_turn": (Port(50, 50, 0), Port(50, 70, 180)), + "loop": (Port(100, 100, 90), Port(100, 80, 270)), + "zig_zag": (Port(20, 150, 0), Port(180, 150, 0)), + } + widths = dict.fromkeys(netlist, 2.0) + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 200, 200), + netlist=netlist, + widths=widths, + evaluator_kwargs={"bend_penalty": 50.0}, + request_kwargs={"bend_radii": [20.0], "capture_conflict_trace": True, "capture_frontier_trace": True}, + ) + results = pathfinder.route_all() + return _make_run_result(results, pathfinder) + + +def snapshot_example_06() -> ScenarioSnapshot: + bounds = (-20, -20, 170, 170) + obstacles = [ + box(40, 110, 60, 130), + box(40, 60, 60, 80), + box(40, 10, 60, 30), + ] + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + scenarios = [ + ( + _build_evaluator(bounds, obstacles=obstacles), + {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}, + {"arc_model": 2.0}, + {"bend_radii": [10.0], "bend_collision_type": "arc", "use_tiered_strategy": False}, + ), + ( + _build_evaluator(bounds, obstacles=obstacles), + {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}, + {"bbox_model": 2.0}, + {"bend_radii": [10.0], "bend_collision_type": "bbox", "use_tiered_strategy": False}, + ), + ( + _build_evaluator(bounds, obstacles=obstacles), + {"custom_geometry": (Port(10, 20, 0), Port(90, 40, 90))}, + {"custom_geometry": 2.0}, + { + "bend_radii": [10.0], + "bend_physical_geometry": custom_physical, + "bend_proxy_geometry": custom_physical, + "use_tiered_strategy": False, + }, + ), + ] + + t0 = perf_counter() + combined_results: dict[str, RoutingResult] = {} + route_metrics: list[RouteMetrics] = [] + for evaluator, netlist, net_widths, request_kwargs in scenarios: + pathfinder = _build_pathfinder( + evaluator, + bounds=bounds, + nets=_net_specs(netlist, net_widths), + **request_kwargs, + ) + combined_results.update(pathfinder.route_all()) + route_metrics.append(pathfinder.metrics.snapshot()) + t1 = perf_counter() + return _make_snapshot( + "example_06_bend_collision_models", + combined_results, + t1 - t0, + _sum_metrics(tuple(route_metrics)), + ) + + +def run_example_06() -> ScenarioOutcome: + return snapshot_example_06().as_outcome() + + +def snapshot_example_07() -> ScenarioSnapshot: + return _snapshot_example_07_variant( + "example_07_large_scale_routing", + warm_start_enabled=True, + ) + + +def snapshot_example_07_no_warm_start() -> ScenarioSnapshot: + return _snapshot_example_07_variant( + "example_07_large_scale_routing_no_warm_start", + warm_start_enabled=False, + ) + + +def snapshot_example_07_no_warm_start_seed43() -> ScenarioSnapshot: + return _snapshot_example_07_variant( + "example_07_large_scale_routing_no_warm_start_seed43", + warm_start_enabled=False, + seed=43, + ) + + +def trace_example_07() -> RoutingRunResult: + return _trace_example_07_variant(warm_start_enabled=True) + + +def trace_example_07_no_warm_start() -> RoutingRunResult: + return _trace_example_07_variant(warm_start_enabled=False) + + +def trace_example_07_no_warm_start_seed43() -> RoutingRunResult: + return _trace_example_07_variant(warm_start_enabled=False, seed=43) + + +def _build_example_07_variant_stack( + *, + num_nets: int, + seed: int, + warm_start_enabled: bool, + capture_conflict_trace: bool = False, + capture_frontier_trace: bool = False, + capture_iteration_trace: bool = False, + capture_pre_pair_frontier_trace: bool = False, +) -> tuple[CostEvaluator, AStarMetrics, PathFinder]: + bounds = (0, 0, 1000, 1000) + obstacles = [ + box(450, 0, 550, 400), + box(450, 600, 550, 1000), + ] + start_x = 50 + start_y_base = 500 - (num_nets * 10.0) / 2.0 + end_x = 950 + end_y_base = 100 + end_y_pitch = 800.0 / (num_nets - 1) + + netlist = {} + for index in range(num_nets): + sy = int(round(start_y_base + index * 10.0)) + ey = int(round(end_y_base + index * end_y_pitch)) + netlist[f"net_{index:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0)) + widths = dict.fromkeys(netlist, 2.0) + _, evaluator, metrics, pathfinder = _build_routing_stack( + bounds=bounds, + netlist=netlist, + widths=widths, + clearance=6.0, + obstacles=obstacles, + evaluator_kwargs={ + "greedy_h_weight": 1.5, + "unit_length_cost": 0.1, + "bend_penalty": 100.0, + "sbend_penalty": 400.0, + }, + request_kwargs={ + "node_limit": 2000000, + "bend_radii": [50.0], + "sbend_radii": [50.0], + "bend_clip_margin": 10.0, + "max_iterations": 15, + "base_penalty": 100.0, + "multiplier": 1.4, + "net_order": "shortest", + "capture_expanded": True, + "capture_conflict_trace": capture_conflict_trace, + "capture_frontier_trace": capture_frontier_trace, + "capture_iteration_trace": capture_iteration_trace, + "capture_pre_pair_frontier_trace": capture_pre_pair_frontier_trace, + "shuffle_nets": True, + "seed": seed, + "warm_start_enabled": warm_start_enabled, + }, + ) + return evaluator, metrics, pathfinder + + +def _run_example_07_variant( + *, + num_nets: int, + seed: int, + warm_start_enabled: bool, + capture_conflict_trace: bool = False, + capture_frontier_trace: bool = False, + capture_iteration_trace: bool = False, + capture_pre_pair_frontier_trace: bool = False, +) -> RoutingRunResult: + evaluator, metrics, pathfinder = _build_example_07_variant_stack( + num_nets=num_nets, + seed=seed, + warm_start_enabled=warm_start_enabled, + capture_conflict_trace=capture_conflict_trace, + capture_frontier_trace=capture_frontier_trace, + capture_iteration_trace=capture_iteration_trace, + capture_pre_pair_frontier_trace=capture_pre_pair_frontier_trace, + ) + + def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None: + _ = current_results + new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4) + evaluator.greedy_h_weight = new_greedy + metrics.reset_per_route() + + results = pathfinder.route_all(iteration_callback=iteration_callback) + return _make_run_result(results, pathfinder) + + +def _snapshot_example_07_variant( + name: str, + *, + warm_start_enabled: bool, + seed: int = 42, +) -> ScenarioSnapshot: + t0 = perf_counter() + run = _run_example_07_variant( + num_nets=10, + seed=seed, + warm_start_enabled=warm_start_enabled, + ) + t1 = perf_counter() + return _make_snapshot(name, run.results_by_net, t1 - t0, run.metrics) + + +def _trace_example_07_variant( + *, + warm_start_enabled: bool, + seed: int = 42, +) -> RoutingRunResult: + return _run_example_07_variant( + num_nets=10, + seed=seed, + warm_start_enabled=warm_start_enabled, + capture_conflict_trace=True, + capture_frontier_trace=True, + capture_iteration_trace=True, + capture_pre_pair_frontier_trace=True, + ) + + +def run_example_07() -> ScenarioOutcome: + return snapshot_example_07().as_outcome() + + +def snapshot_example_08() -> ScenarioSnapshot: + bounds = (0, 0, 150, 150) + netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))} + widths = {"standard_arc": 2.0} + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) + + t0 = perf_counter() + pathfinder_std = _build_pathfinder( + _build_evaluator(bounds), + bounds=bounds, + nets=_net_specs(netlist, widths), + bend_radii=[10.0], + sbend_radii=[], + max_iterations=1, + use_tiered_strategy=False, + metrics=AStarMetrics(), + ) + results_std = pathfinder_std.route_all() + pathfinder_custom = _build_pathfinder( + _build_evaluator(bounds), + bounds=bounds, + nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}), + bend_radii=[10.0], + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, + sbend_radii=[], + max_iterations=1, + use_tiered_strategy=False, + metrics=AStarMetrics(), + ) + results_custom = pathfinder_custom.route_all() + t1 = perf_counter() + 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_08() -> ScenarioOutcome: + return snapshot_example_08().as_outcome() + + +def snapshot_example_09() -> ScenarioSnapshot: + obstacles = [ + box(35, 35, 45, 65), + box(55, 35, 65, 65), + ] + netlist = {"budget_limited_net": (Port(10, 50, 0), Port(85, 60, 180))} + widths = {"budget_limited_net": 2.0} + _, _, _, pathfinder = _build_routing_stack( + bounds=(0, 0, 100, 100), + netlist=netlist, + widths=widths, + obstacles=obstacles, + evaluator_kwargs={"bend_penalty": 50.0, "sbend_penalty": 150.0}, + request_kwargs={"node_limit": 3, "bend_radii": [10.0], "warm_start_enabled": False, "max_iterations": 1}, + ) + t0 = perf_counter() + results = pathfinder.route_all() + t1 = perf_counter() + 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], ...] = ( + ("example_01_simple_route", run_example_01), + ("example_02_congestion_resolution", run_example_02), + ("example_03_locked_paths", run_example_03), + ("example_04_sbends_and_radii", run_example_04), + ("example_05_orientation_stress", run_example_05), + ("example_06_bend_collision_models", run_example_06), + ("example_07_large_scale_routing", run_example_07), + ("example_08_custom_bend_geometry", run_example_08), + ("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), +) + +PERFORMANCE_SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = ( + ("example_07_large_scale_routing_no_warm_start", snapshot_example_07_no_warm_start), + ("example_07_large_scale_routing_no_warm_start_seed43", snapshot_example_07_no_warm_start_seed43), +) + +TRACE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = ( + ("example_05_orientation_stress", trace_example_05), + ("example_07_large_scale_routing", trace_example_07), +) + +TRACE_PERFORMANCE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = ( + ("example_07_large_scale_routing_no_warm_start", trace_example_07_no_warm_start), + ("example_07_large_scale_routing_no_warm_start_seed43", trace_example_07_no_warm_start_seed43), +) + + +def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]: + return tuple(run() for _, run in SCENARIO_SNAPSHOTS) diff --git a/inire/tests/test_api.py b/inire/tests/test_api.py new file mode 100644 index 0000000..926859c --- /dev/null +++ b/inire/tests/test_api.py @@ -0,0 +1,805 @@ +import importlib + +import pytest +from shapely.geometry import box + +import inire.router._router as router_module +from inire import ( + CongestionOptions, + DiagnosticsOptions, + NetSpec, + ObjectiveWeights, + Port, + RefinementOptions, + RoutingOptions, + RoutingProblem, + SearchOptions, + route, +) +from inire.geometry.components import Straight +from inire.geometry.collision import RoutingWorld +from inire.results import RoutingReport, RoutingResult +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder, _IterationReview +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap +def test_root_module_exports_only_stable_surface() -> None: + import inire + + assert not hasattr(inire, "RoutingWorld") + assert not hasattr(inire, "AStarContext") + assert not hasattr(inire, "PathFinder") + assert not hasattr(inire, "CostEvaluator") + assert not hasattr(inire, "DangerMap") + + +def test_deep_raw_stack_imports_remain_accessible_but_unstable() -> None: + router_module = importlib.import_module("inire.router._router") + search_module = importlib.import_module("inire.router._search") + collision_module = importlib.import_module("inire.geometry.collision") + + assert hasattr(router_module, "PathFinder") + assert hasattr(search_module, "route_astar") + assert hasattr(collision_module, "RoutingWorld") + + +def test_route_problem_smoke() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("net1", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + + run = route(problem) + + assert set(run.results_by_net) == {"net1"} + assert run.results_by_net["net1"].is_valid + assert run.conflict_trace == () + assert run.frontier_trace == () + assert run.pre_pair_frontier_trace is None + assert run.iteration_trace == () + + +def test_route_problem_supports_configs_and_debug_data() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("net1", Port(10, 10, 0), Port(90, 90, 0), width=2.0),), + static_obstacles=(box(40, 0, 60, 70),), + ) + options = RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + node_limit=50000, + greedy_h_weight=1.2, + ), + objective=ObjectiveWeights( + bend_penalty=50.0, + sbend_penalty=150.0, + ), + congestion=CongestionOptions(warm_start_enabled=False), + refinement=RefinementOptions(enabled=True), + diagnostics=DiagnosticsOptions(capture_expanded=True), + ) + + run = route(problem, options=options) + + assert run.results_by_net["net1"].reached_target + assert run.expanded_nodes + assert run.metrics.nodes_expanded > 0 + assert run.metrics.route_iterations >= 1 + assert run.metrics.iteration_reverify_calls >= 1 + assert run.metrics.iteration_reverified_nets >= 0 + assert run.metrics.iteration_conflicting_nets >= 0 + assert run.metrics.iteration_conflict_edges >= 0 + assert run.metrics.nets_carried_forward >= 0 + 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_corner_index_builds >= 0 + assert run.metrics.visibility_builds >= 0 + assert run.metrics.guidance_match_moves >= 0 + assert run.metrics.guidance_match_moves_straight >= 0 + assert run.metrics.guidance_match_moves_bend90 >= 0 + assert run.metrics.guidance_match_moves_sbend >= 0 + assert run.metrics.guidance_bonus_applied >= 0.0 + assert run.metrics.guidance_bonus_applied_straight >= 0.0 + assert run.metrics.guidance_bonus_applied_bend90 >= 0.0 + assert run.metrics.guidance_bonus_applied_sbend >= 0.0 + assert run.metrics.congestion_grid_span_cache_hits >= 0 + assert run.metrics.congestion_grid_span_cache_misses >= 0 + assert run.metrics.congestion_presence_cache_hits >= 0 + assert run.metrics.congestion_presence_cache_misses >= 0 + assert run.metrics.congestion_presence_skips >= 0 + assert run.metrics.congestion_candidate_precheck_hits >= 0 + assert run.metrics.congestion_candidate_precheck_misses >= 0 + assert run.metrics.congestion_candidate_precheck_skips >= 0 + assert run.metrics.congestion_candidate_nets >= 0 + assert run.metrics.congestion_net_envelope_cache_hits >= 0 + assert run.metrics.congestion_net_envelope_cache_misses >= 0 + assert run.metrics.congestion_grid_net_cache_hits >= 0 + assert run.metrics.congestion_grid_net_cache_misses >= 0 + assert run.metrics.congestion_lazy_resolutions >= 0 + assert run.metrics.congestion_lazy_requeues >= 0 + assert run.metrics.congestion_candidate_ids >= 0 + assert run.metrics.verify_dynamic_candidate_nets >= 0 + assert run.metrics.verify_path_report_calls >= 0 + assert run.metrics.pair_local_search_pairs_considered >= 0 + assert run.metrics.pair_local_search_attempts >= 0 + assert run.metrics.pair_local_search_accepts >= 0 + assert run.metrics.pair_local_search_nodes_expanded >= 0 + assert run.metrics.late_phase_capped_nets >= 0 + assert run.metrics.late_phase_capped_fallbacks >= 0 + + +def test_iteration_callback_observes_reverified_conflicts() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + snapshots: list[dict[str, str]] = [] + + def callback(iteration: int, current_results: dict[str, object]) -> None: + _ = iteration + snapshots.append({net_id: result.outcome for net_id, result in current_results.items()}) + + results = pathfinder.route_all(iteration_callback=callback) + + assert snapshots == [{"horizontal": "colliding", "vertical": "colliding"}] + assert results["horizontal"].outcome == "colliding" + assert results["vertical"].outcome == "colliding" + + +def test_capture_conflict_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_conflict_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert [entry.stage for entry in run_with_trace.conflict_trace] == ["iteration", "restored_best", "final"] + + +def test_capture_iteration_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_iteration_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert len(run_with_trace.iteration_trace) == 1 + + +def test_capture_iteration_trace_records_iteration_and_attempt_deltas() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + run = route( + problem, + options=RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + diagnostics=DiagnosticsOptions(capture_iteration_trace=True), + ), + ) + + entry = run.iteration_trace[0] + assert entry.iteration == 0 + assert entry.congestion_penalty == 100.0 + assert entry.routed_net_ids == ("horizontal", "vertical") + assert entry.completed_nets == 0 + assert entry.conflict_edges == 1 + assert entry.total_dynamic_collisions >= 2 + assert entry.nodes_expanded >= 0 + assert entry.congestion_check_calls >= 0 + assert entry.congestion_candidate_ids >= 0 + assert entry.congestion_exact_pair_checks >= 0 + assert len(entry.net_attempts) == 2 + assert [attempt.net_id for attempt in entry.net_attempts] == ["horizontal", "vertical"] + assert all(attempt.nodes_expanded >= 0 for attempt in entry.net_attempts) + assert all(attempt.congestion_check_calls >= 0 for attempt in entry.net_attempts) + assert all(not attempt.guidance_seed_present for attempt in entry.net_attempts) + assert sum(attempt.nodes_expanded for attempt in entry.net_attempts) == entry.nodes_expanded + assert sum(attempt.congestion_check_calls for attempt in entry.net_attempts) == entry.congestion_check_calls + + +def test_capture_conflict_trace_records_component_pairs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + diagnostics=DiagnosticsOptions(capture_conflict_trace=True), + ) + + run = route(problem, options=options) + final_entry = run.conflict_trace[-1] + trace_by_net = {net.net_id: net for net in final_entry.nets} + + assert final_entry.stage == "final" + assert final_entry.conflict_edges == (("horizontal", "vertical"),) + assert trace_by_net["horizontal"].component_conflicts[0].other_net_id == "vertical" + assert trace_by_net["horizontal"].component_conflicts[0].self_component_index == 0 + assert trace_by_net["horizontal"].component_conflicts[0].other_component_index == 0 + assert trace_by_net["vertical"].component_conflicts[0].other_net_id == "horizontal" + assert trace_by_net["vertical"].component_conflicts[0].self_component_index == 0 + assert trace_by_net["vertical"].component_conflicts[0].other_component_index == 0 + + +def test_capture_frontier_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_frontier_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert {trace.net_id for trace in run_with_trace.frontier_trace} == {"horizontal", "vertical"} + + +def test_capture_pre_pair_frontier_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_pre_pair_frontier_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert run_with_trace.pre_pair_frontier_trace is None + + +def test_capture_frontier_trace_records_prune_reasons() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + run = route( + problem, + options=RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + diagnostics=DiagnosticsOptions(capture_frontier_trace=True), + ), + ) + + trace_by_net = {entry.net_id: entry for entry in run.frontier_trace} + assert trace_by_net["horizontal"].hotspot_bounds + assert ( + trace_by_net["horizontal"].pruned_closed_set + + trace_by_net["horizontal"].pruned_hard_collision + + trace_by_net["horizontal"].pruned_self_collision + + trace_by_net["horizontal"].pruned_cost + ) > 0 + assert trace_by_net["horizontal"].samples + + +def test_reverify_iterations_stop_early_on_stalled_conflict_graph() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=10, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run = route(problem, options=options) + + assert run.metrics.route_iterations < 10 + + +def test_reverify_iterations_limit_late_reroutes_to_conflicting_nets(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=10, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + colliding_a = RoutingResult( + net_id="netA", + path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(dynamic_collision_count=1, total_length=80.0), + ) + colliding_b = RoutingResult( + net_id="netB", + path=(Straight.generate(Port(50, 10, 90), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(dynamic_collision_count=1, total_length=80.0), + ) + completed_c = RoutingResult( + net_id="netC", + path=(Straight.generate(Port(10, 20, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(total_length=80.0), + ) + iterations_seen: list[int] = [] + reroute_sets: list[set[str]] = [] + + def fake_run_iteration(self, state, iteration, reroute_net_ids, iteration_callback): + _ = self + _ = iteration_callback + iterations_seen.append(iteration) + reroute_sets.append(set(reroute_net_ids)) + state.results = {"netA": colliding_a, "netB": colliding_b, "netC": completed_c} + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids={"netC"}, + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration) + monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results)) + monkeypatch.setattr(PathFinder, "_run_pair_local_search", lambda self, state: None) + + results = pathfinder.route_all() + + assert iterations_seen == [0, 1] + assert reroute_sets == [{"netA", "netB", "netC"}, {"netA", "netB"}] + assert results["netA"].outcome == "colliding" + assert results["netB"].outcome == "colliding" + assert results["netC"].reached_target + + +def test_run_iteration_orders_subset_reroutes_by_recent_work(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + state.recent_attempt_work = {"netA": 200, "netB": 20} + route_order: list[str] = [] + + def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None): + _ = self + _ = state + _ = iteration + route_order.append(net_id) + assert node_limit_override is None + assert incumbent_fallback is None + return RoutingResult(net_id=net_id, path=(), reached_target=False), False + + def fake_reverify(self, state): + _ = self + _ = state + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids=set(), + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once) + monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify) + + review = pathfinder._run_iteration(state, 1, {"netA", "netB"}, None) + + assert review is not None + assert route_order == ["netB", "netA"] + + +def test_run_iteration_caps_two_heaviest_late_phase_nets(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + NetSpec("netD", Port(10, 80, 0), Port(90, 80, 0), width=2.0), + NetSpec("netE", Port(10, 65, 0), Port(90, 65, 0), width=2.0), + ), + ) + options = RoutingOptions( + objective=ObjectiveWeights(bend_penalty=100.0), + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + state.results = { + net_id: RoutingResult(net_id=net_id, path=(Straight.generate(spec.start, 80.0, 2.0, dilation=1.0),), reached_target=True) + for net_id, spec in state.net_specs.items() + } + state.best_conflict_edges = 2 + state.recent_attempt_work = {"netA": 20, "netB": 40, "netC": 400, "netD": 220} + incumbents = dict(state.results) + caps_by_net: dict[str, tuple[int | None, RoutingResult | None]] = {} + + def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None): + _ = self + _ = state + _ = iteration + caps_by_net[net_id] = (node_limit_override, incumbent_fallback) + return RoutingResult(net_id=net_id, path=(), reached_target=False), False + + def fake_reverify(self, state): + _ = self + _ = state + return _IterationReview( + conflicting_nets={"netA", "netB", "netC", "netD"}, + conflict_edges={("netA", "netB"), ("netC", "netD")}, + completed_net_ids=set(), + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once) + monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify) + + review = pathfinder._run_iteration(state, 1, {"netA", "netB", "netC", "netD"}, None) + + assert review is not None + assert caps_by_net["netA"] == (None, None) + assert caps_by_net["netB"] == (None, None) + assert caps_by_net["netC"][0] == 1 + assert caps_by_net["netD"][0] == 1 + assert caps_by_net["netC"][1] is incumbents["netC"] + assert caps_by_net["netD"][1] is incumbents["netD"] + + +def test_route_net_once_skips_search_for_capped_incumbent_fallback(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + options = RoutingOptions( + objective=ObjectiveWeights(bend_penalty=100.0), + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + incumbent = RoutingResult( + net_id="netA", + path=(Straight.generate(problem.nets[0].start, 80.0, 2.0, dilation=1.0),), + reached_target=True, + ) + state.results["netA"] = incumbent + installed: list[tuple[str, tuple[object, ...]]] = [] + + def fail_route_astar(*args, **kwargs): + raise AssertionError("route_astar should not run for capped incumbent fallback") + + def record_install(self, net_id, path): + _ = self + installed.append((net_id, tuple(path))) + + monkeypatch.setattr(router_module, "route_astar", fail_route_astar) + monkeypatch.setattr(PathFinder, "_install_path", record_install) + + result, guidance_seed_present = pathfinder._route_net_once( + state, + 1, + "netA", + node_limit_override=1, + incumbent_fallback=incumbent, + ) + + assert result is incumbent + assert guidance_seed_present is True + assert installed == [("netA", incumbent.path)] + assert pathfinder.metrics.total_late_phase_capped_nets == 1 + assert pathfinder.metrics.total_late_phase_capped_fallbacks == 1 + + +def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + best_result = RoutingResult( + net_id="netA", + path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(), + ) + missing_result = RoutingResult(net_id="netA", path=(), reached_target=False) + unroutable_b = RoutingResult(net_id="netB", path=(), reached_target=False) + + def fake_run_iteration(self, state, iteration, reroute_net_ids, iteration_callback): + _ = self + _ = reroute_net_ids + _ = iteration_callback + if iteration == 0: + state.results = {"netA": best_result, "netB": unroutable_b} + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids={"netA"}, + total_dynamic_collisions=1, + ) + state.results = {"netA": missing_result, "netB": unroutable_b} + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids=set(), + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration) + monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results)) + + results = pathfinder.route_all() + + assert results["netA"].outcome == "completed" + assert results["netB"].outcome == "unroutable" + + +def test_route_all_restores_best_iteration_snapshot_on_timeout(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + best_result = RoutingResult( + net_id="netA", + path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(), + ) + worse_result = RoutingResult(net_id="netA", path=(), reached_target=False) + + def fake_run_iterations(self, state, iteration_callback): + _ = iteration_callback + _ = self + state.results = {"netA": best_result} + pathfinder._update_best_iteration( + state, + _IterationReview( + conflicting_nets=set(), + conflict_edges=set(), + completed_net_ids={"netA"}, + total_dynamic_collisions=0, + ), + ) + state.results = {"netA": worse_result} + return True + + monkeypatch.setattr(PathFinder, "_run_iterations", fake_run_iterations) + monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results)) + + results = pathfinder.route_all() + + assert results["netA"].outcome == "completed" + + +def test_capture_conflict_trace_records_restored_best_stage(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + diagnostics=DiagnosticsOptions(capture_conflict_trace=True), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + best_result = RoutingResult( + net_id="netA", + path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(), + ) + worse_result = RoutingResult(net_id="netA", path=(), reached_target=False) + + def fake_run_iteration(self, state, iteration, reroute_net_ids, iteration_callback): + _ = self + _ = reroute_net_ids + _ = iteration_callback + if iteration == 0: + state.results = {"netA": best_result} + return _IterationReview( + conflicting_nets=set(), + conflict_edges=set(), + completed_net_ids={"netA"}, + total_dynamic_collisions=0, + ) + state.results = {"netA": worse_result} + return _IterationReview( + conflicting_nets=set(), + conflict_edges=set(), + completed_net_ids=set(), + total_dynamic_collisions=0, + ) + + monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration) + + pathfinder.route_all() + + assert [entry.stage for entry in pathfinder.conflict_trace] == [ + "restored_best", + "final", + ] + restored_entry = pathfinder.conflict_trace[0] + assert restored_entry.nets[0].outcome == "completed" +def test_route_problem_locked_routes_become_static_obstacles() -> None: + locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),) + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("crossing", Port(50, 10, 90), Port(50, 90, 90), width=2.0),), + static_obstacles=tuple(polygon for component in locked for polygon in component.physical_geometry), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run = route(problem, options=options) + result = run.results_by_net["crossing"] + + assert not result.is_valid + + +def test_locked_routes_enable_incremental_requests_without_sessions() -> None: + problem_a = RoutingProblem( + bounds=(0, -50, 100, 50), + nets=(NetSpec("netA", Port(10, 0, 0), Port(90, 0, 0), width=2.0),), + ) + options = RoutingOptions(search=SearchOptions(bend_radii=(10.0,))) + results_a = route(problem_a, options=options) + assert results_a.results_by_net["netA"].is_valid + + problem_b = RoutingProblem( + bounds=(0, -50, 100, 50), + nets=(NetSpec("netB", Port(50, -20, 90), Port(50, 20, 90), width=2.0),), + static_obstacles=results_a.results_by_net["netA"].locked_geometry, + ) + results_b = route(problem_b, options=options) + + assert results_b.results_by_net["netB"].is_valid + + +def test_route_problem_rejects_untyped_initial_paths() -> None: + with pytest.raises(TypeError): + RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("net1", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + initial_paths={"net1": (object(),)}, # type: ignore[dict-item] + ) + + +def test_route_results_metrics_are_snapshots() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("net1", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + options = RoutingOptions() + run1 = route(problem, options=options) + first_metrics = run1.metrics + run2 = route(problem, options=options) + + assert first_metrics == run1.metrics + assert run1.metrics is not run2.metrics + assert first_metrics.nodes_expanded > 0 diff --git a/inire/tests/test_astar.py b/inire/tests/test_astar.py new file mode 100644 index 0000000..445991f --- /dev/null +++ b/inire/tests/test_astar.py @@ -0,0 +1,669 @@ +import math +import pytest +from shapely.geometry import Polygon + +from inire import CongestionOptions, NetSpec, RoutingProblem, RoutingOptions, RoutingResult, SearchOptions +from inire.geometry.components import Bend90, Straight +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.router._astar_types import AStarContext, AStarNode, SearchRunConfig +from inire.router._astar_admission import add_node +from inire.router._astar_moves import ( + _distance_to_bounds_in_heading, + _should_cap_straights_to_bounds, +) +from inire.router._router import PathFinder, _RoutingState +from inire.router._search import route_astar +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap +from inire.seeds import StraightSeed + +BOUNDS = (0, -50, 150, 150) + + +@pytest.fixture +def basic_evaluator() -> CostEvaluator: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=BOUNDS) + danger_map.precompute([]) + return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) + + +def _build_options(**search_overrides: object) -> RoutingOptions: + return RoutingOptions(search=SearchOptions(**search_overrides)) + + +def _build_context( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + **search_overrides: object, +) -> AStarContext: + return AStarContext( + evaluator, + RoutingProblem(bounds=bounds), + _build_options(**search_overrides), + ) + + +def _route(context: AStarContext, start: Port, target: Port, **config_overrides: object): + return route_astar( + start, + target, + net_width=2.0, + context=context, + config=SearchRunConfig.from_options(context.options, **config_overrides), + ) + + +def _validate_routing_result( + result: RoutingResult, + static_obstacles: list[Polygon], + clearance: float, + expected_start: Port | None = None, + expected_end: Port | None = None, +) -> dict[str, object]: + if not result.path: + return {"is_valid": False, "reason": "No path found"} + + connectivity_errors: list[str] = [] + if expected_start: + first_port = result.path[0].start_port + dist_to_start = math.hypot(first_port.x - expected_start.x, first_port.y - expected_start.y) + if dist_to_start > 0.005: + connectivity_errors.append(f"Initial port position mismatch: {dist_to_start*1000:.2f}nm") + if abs(first_port.r - expected_start.r) > 0.1: + connectivity_errors.append(f"Initial port orientation mismatch: {first_port.r} vs {expected_start.r}") + + if expected_end: + last_port = result.path[-1].end_port + dist_to_end = math.hypot(last_port.x - expected_end.x, last_port.y - expected_end.y) + if dist_to_end > 0.005: + connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm") + if abs(last_port.r - expected_end.r) > 0.1: + connectivity_errors.append(f"Final port orientation mismatch: {last_port.r} vs {expected_end.r}") + + engine = RoutingWorld(clearance=clearance) + for obstacle in static_obstacles: + engine.add_static_obstacle(obstacle) + report = engine.verify_path_report("validation", result.path) + is_valid = report.is_valid and not connectivity_errors + + reasons = [] + if report.static_collision_count: + reasons.append(f"Found {report.static_collision_count} obstacle collisions.") + if report.dynamic_collision_count: + reasons.append(f"Found {report.dynamic_collision_count} dynamic-net collisions.") + if report.self_collision_count: + reasons.append(f"Found {report.self_collision_count} self-intersections.") + reasons.extend(connectivity_errors) + + return { + "is_valid": is_valid, + "reason": " ".join(reasons), + "obstacle_collisions": report.static_collision_count, + "dynamic_collisions": report.dynamic_collision_count, + "self_intersections": report.self_collision_count, + "total_length": report.total_length, + "connectivity_ok": not connectivity_errors, + } + + +def test_astar_straight(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + start = Port(0, 0, 0) + target = Port(50, 0, 0) + path = _route(context, start, target) + + assert path is not None + result = RoutingResult(net_id="test", path=path, reached_target=True) + validation = _validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + assert validation["connectivity_ok"] + # Path should be exactly 50um (or slightly more if it did weird things, but here it's straight) + assert abs(validation["total_length"] - 50.0) < 1e-6 + + +def test_astar_bend(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,)) + start = Port(0, 0, 0) + # 20um right, 20um up. Needs a 10um bend and a 10um bend. + target = Port(20, 20, 0) + path = _route(context, start, target) + + assert path is not None + result = RoutingResult(net_id="test", path=path, reached_target=True) + validation = _validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + assert validation["connectivity_ok"] + + +def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None: + # Add an obstacle in the middle of a straight path + # Obstacle from x=20 to 40, y=-20 to 20 + obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)]) + basic_evaluator.collision_engine.add_static_obstacle(obstacle) + basic_evaluator.danger_map.precompute([obstacle]) + + context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,), node_limit=1000000) + start = Port(0, 0, 0) + target = Port(60, 0, 0) + path = _route(context, start, target) + + assert path is not None + result = RoutingResult(net_id="test", path=path, reached_target=True) + validation = _validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + # Path should have detoured, so length > 50 + assert validation["total_length"] > 50.0 + + +def test_astar_uses_integerized_ports(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + start = Port(0, 0, 0) + target = Port(10.1, 0, 0) + path = _route(context, start, target) + + assert path is not None + result = RoutingResult(net_id="test", path=path, reached_target=True) + assert target.x == 10 + validation = _validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + + +def test_validate_routing_result_checks_expected_start() -> None: + path = [Straight.generate(Port(100, 0, 0), 10.0, width=2.0, dilation=1.0)] + result = RoutingResult(net_id="test", path=path, reached_target=True) + + validation = _validate_routing_result( + result, + [], + clearance=2.0, + expected_start=Port(0, 0, 0), + expected_end=Port(110, 0, 0), + ) + + assert not validation["is_valid"] + assert "Initial port position mismatch" in validation["reason"] + + +def test_validate_routing_result_uses_exact_component_geometry() -> None: + bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox", dilation=1.0) + result = RoutingResult(net_id="test", path=[bend], reached_target=True) + obstacle = Polygon([(2.0, 7.0), (4.0, 7.0), (4.0, 9.0), (2.0, 9.0)]) + + validation = _validate_routing_result( + result, + [obstacle], + clearance=2.0, + expected_start=Port(0, 0, 0), + expected_end=bend.end_port, + ) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + + +def test_astar_context_keeps_evaluator_weights_separate(basic_evaluator: CostEvaluator) -> None: + basic_evaluator = CostEvaluator( + basic_evaluator.collision_engine, + basic_evaluator.danger_map, + bend_penalty=120.0, + sbend_penalty=240.0, + ) + context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(5.0,)) + + assert context.options.search.bend_radii == (5.0,) + assert basic_evaluator.h_manhattan(Port(0, 0, 0), Port(10, 10, 0)) > 0.0 + + +def test_distance_to_bounds_in_heading_is_directional() -> None: + bounds = (0, 0, 100, 200) + + assert _distance_to_bounds_in_heading(Port(20, 30, 0), bounds) == pytest.approx(80.0) + assert _distance_to_bounds_in_heading(Port(20, 30, 90), bounds) == pytest.approx(170.0) + assert _distance_to_bounds_in_heading(Port(20, 30, 180), bounds) == pytest.approx(20.0) + assert _distance_to_bounds_in_heading(Port(20, 30, 270), bounds) == pytest.approx(30.0) + + +def test_should_cap_straights_to_bounds_only_for_large_no_warm_runs(basic_evaluator: CostEvaluator) -> None: + large_context = AStarContext( + basic_evaluator, + RoutingProblem( + bounds=(0, 0, 1000, 1000), + nets=tuple( + NetSpec(f"net{i}", Port(0, i * 10, 0), Port(10, i * 10, 0), width=2.0) + for i in range(8) + ), + ), + RoutingOptions( + congestion=CongestionOptions(warm_start_enabled=False), + ), + ) + small_context = _build_context(basic_evaluator, bounds=BOUNDS) + + assert _should_cap_straights_to_bounds(large_context) + assert not _should_cap_straights_to_bounds(small_context) + + +def test_pair_local_context_clones_live_static_obstacles() -> None: + obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)]) + engine = RoutingWorld(clearance=2.0) + engine.add_static_obstacle(obstacle) + danger_map = DangerMap(bounds=BOUNDS) + danger_map.precompute([obstacle]) + evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) + finder = PathFinder( + AStarContext( + evaluator, + RoutingProblem( + bounds=BOUNDS, + nets=( + NetSpec("pair_a", Port(0, 0, 0), Port(60, 0, 0), width=2.0), + NetSpec("pair_b", Port(0, 10, 0), Port(60, 10, 0), width=2.0), + ), + ), + RoutingOptions(), + ) + ) + state = _RoutingState( + net_specs={ + "pair_a": NetSpec("pair_a", Port(0, 0, 0), Port(60, 0, 0), width=2.0), + "pair_b": NetSpec("pair_b", Port(0, 10, 0), Port(60, 10, 0), width=2.0), + }, + ordered_net_ids=["pair_a", "pair_b"], + results={}, + needs_self_collision_check=set(), + start_time=0.0, + timeout_s=1.0, + initial_paths=None, + accumulated_expanded_nodes=[], + best_results={}, + best_completed_nets=-1, + best_conflict_edges=10**9, + best_dynamic_collisions=10**9, + last_conflict_signature=(), + last_conflict_edge_count=0, + repeated_conflict_count=0, + pair_local_plateau_count=0, + recent_attempt_work={}, + pre_pair_candidate=None, + ) + + local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b")) + + assert finder.context.problem.static_obstacles == () + assert len(local_context.problem.static_obstacles) == 1 + assert len(local_context.cost_evaluator.collision_engine._static_obstacles.geometries) == 1 + assert next(iter(local_context.problem.static_obstacles)).equals(obstacle) + + +def test_route_astar_bend_collision_override_does_not_persist(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,), bend_collision_type="arc") + + route_astar( + Port(0, 0, 0), + Port(30, 10, 0), + net_width=2.0, + context=context, + config=SearchRunConfig.from_options( + context.options, + bend_collision_type="clipped_bbox", + return_partial=True, + ), + ) + + assert context.options.search.bend_collision_type == "arc" + + +def test_route_astar_returns_partial_path_when_node_limited(basic_evaluator: CostEvaluator) -> None: + obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)]) + basic_evaluator.collision_engine.add_static_obstacle(obstacle) + basic_evaluator.danger_map.precompute([obstacle]) + context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,), node_limit=2) + start = Port(0, 0, 0) + target = Port(60, 0, 0) + + partial_path = _route(context, start, target, return_partial=True) + no_partial_path = _route(context, start, target, return_partial=False) + + assert partial_path is not None + assert partial_path + assert partial_path[-1].end_port != target + assert no_partial_path is None + + +def test_route_astar_uses_single_sbend_for_same_orientation_offset(basic_evaluator: CostEvaluator) -> None: + context = _build_context( + basic_evaluator, + bounds=BOUNDS, + bend_radii=(10.0,), + sbend_radii=(10.0,), + sbend_offsets=(10.0,), + max_straight_length=150.0, + ) + start = Port(0, 0, 0) + target = Port(100, 10, 0) + + path = _route(context, start, target) + + assert path is not None + assert path[-1].end_port == target + assert sum(1 for component in path if component.move_type == "sbend") == 1 + assert not any( + first.move_type == second.move_type == "sbend" + for first, second in zip(path, path[1:], strict=False) + ) + + +@pytest.mark.parametrize("visibility_guidance", ["off", "exact_corner", "tangent_corner"]) +def test_route_astar_supports_all_visibility_guidance_modes( + basic_evaluator: CostEvaluator, + visibility_guidance: str, +) -> None: + obstacle = Polygon([(30, 10), (50, 10), (50, 40), (30, 40)]) + basic_evaluator.collision_engine.add_static_obstacle(obstacle) + basic_evaluator.danger_map.precompute([obstacle]) + context = _build_context( + basic_evaluator, + bounds=BOUNDS, + bend_radii=(10.0,), + sbend_radii=(), + max_straight_length=150.0, + visibility_guidance=visibility_guidance, + ) + start = Port(0, 0, 0) + target = Port(80, 50, 0) + + path = _route(context, start, target) + + assert path is not None + result = RoutingResult(net_id="test", path=path, reached_target=True) + validation = _validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target) + + assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" + assert validation["connectivity_ok"] + + +def test_tangent_corner_mode_avoids_exact_visibility_graph_builds(basic_evaluator: CostEvaluator) -> None: + obstacle = Polygon([(30, 10), (50, 10), (50, 40), (30, 40)]) + basic_evaluator.collision_engine.add_static_obstacle(obstacle) + basic_evaluator.danger_map.precompute([obstacle]) + context = _build_context( + basic_evaluator, + bounds=BOUNDS, + bend_radii=(10.0,), + sbend_radii=(), + max_straight_length=150.0, + visibility_guidance="tangent_corner", + ) + + path = _route(context, Port(0, 0, 0), Port(80, 50, 0)) + + assert path is not None + assert context.metrics.total_visibility_builds == 0 + assert context.metrics.total_visibility_corner_pairs_checked == 0 + assert context.metrics.total_ray_cast_calls_visibility_build == 0 + + +def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None: + context = AStarContext( + basic_evaluator, + RoutingProblem(bounds=BOUNDS), + _build_options( + min_straight_length=1.0, + max_straight_length=100.0, + ), + max_cache_size=2, + ) + start = Port(0, 0, 0) + targets = [Port(length, 0, 0) for length in range(10, 70, 10)] + + for target in targets: + path = _route(context, start, target) + assert path is not None + assert path[-1].end_port == target + + +def test_self_collision_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + root = AStarNode(Port(0, 0, 0), 0.0, 0.0) + parent_result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + parent = AStarNode(parent_result.end_port, g_cost=10.0, h_cost=0.0, parent=root, component_result=parent_result) + open_set: list[AStarNode] = [] + closed_set: dict[tuple[int, int, int], float] = {} + + add_node( + parent, + parent_result, + target=Port(20, 0, 0), + net_width=2.0, + net_id="netA", + open_set=open_set, + closed_set=closed_set, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options(context.options, self_collision_check=True), + move_type="straight", + cache_key=("overlap",), + ) + + assert not open_set + assert context.metrics.total_congestion_check_calls == 0 + assert context.metrics.total_congestion_cache_misses == 0 + + +def test_closed_set_dominance_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + root = AStarNode(Port(0, 0, 0), 0.0, 0.0) + result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + open_set: list[AStarNode] = [] + closed_set = {result.end_port.as_tuple(): context.cost_evaluator.score_component(result, start_port=root.port)} + + add_node( + root, + result, + target=Port(20, 0, 0), + net_width=2.0, + net_id="netA", + open_set=open_set, + closed_set=closed_set, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options(context.options), + move_type="straight", + cache_key=("dominated",), + ) + + assert not open_set + assert context.metrics.total_congestion_check_calls == 0 + assert context.metrics.total_congestion_cache_misses == 0 + + +def test_no_dynamic_paths_skips_congestion_check(basic_evaluator: CostEvaluator) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + root = AStarNode(Port(0, 0, 0), 0.0, 0.0) + result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + open_set: list[AStarNode] = [] + closed_set: dict[tuple[int, int, int], float] = {} + + add_node( + root, + result, + target=Port(20, 0, 0), + net_width=2.0, + net_id="netA", + open_set=open_set, + closed_set=closed_set, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options(context.options), + move_type="straight", + cache_key=("no-dynamic",), + ) + + assert open_set + assert context.metrics.total_congestion_check_calls == 0 + assert context.metrics.total_congestion_cache_misses == 0 + + +def test_guidance_seed_matching_move_reduces_cost_and_advances_seed_index( + basic_evaluator: CostEvaluator, +) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + root = AStarNode(Port(0, 0, 0), 0.0, 0.0, seed_index=0) + result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + open_set: list[AStarNode] = [] + unguided_open_set: list[AStarNode] = [] + closed_set: dict[tuple[int, int, int], float] = {} + + add_node( + root, + result, + target=Port(20, 0, 0), + net_width=2.0, + net_id="netA", + open_set=open_set, + closed_set=closed_set, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options( + context.options, + guidance_seed=(StraightSeed(length=10.0),), + guidance_bonus=5.0, + ), + move_type="straight", + cache_key=("guided",), + ) + add_node( + AStarNode(Port(0, 0, 0), 0.0, 0.0), + result, + target=Port(20, 0, 0), + net_width=2.0, + net_id="netA", + open_set=unguided_open_set, + closed_set={}, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options(context.options), + move_type="straight", + cache_key=("unguided",), + ) + + assert open_set + assert unguided_open_set + guided_node = open_set[0] + unguided_node = unguided_open_set[0] + assert guided_node.seed_index == 1 + assert guided_node.g_cost < unguided_node.g_cost + assert context.metrics.total_guidance_match_moves == 1 + assert context.metrics.total_guidance_match_moves_straight == 1 + assert context.metrics.total_guidance_match_moves_bend90 == 0 + assert context.metrics.total_guidance_match_moves_sbend == 0 + assert context.metrics.total_guidance_bonus_applied == pytest.approx(5.0) + assert context.metrics.total_guidance_bonus_applied_straight == pytest.approx(5.0) + assert context.metrics.total_guidance_bonus_applied_bend90 == pytest.approx(0.0) + assert context.metrics.total_guidance_bonus_applied_sbend == pytest.approx(0.0) + + +def test_guidance_seed_bend90_keeps_full_bonus( + basic_evaluator: CostEvaluator, +) -> None: + context = _build_context(basic_evaluator, bounds=BOUNDS) + root = AStarNode(Port(0, 0, 0), 0.0, 0.0, seed_index=0) + result = Bend90.generate(Port(0, 0, 0), 10.0, width=2.0, direction="CCW", dilation=1.0) + open_set: list[AStarNode] = [] + unguided_open_set: list[AStarNode] = [] + + add_node( + root, + result, + target=Port(10, 10, 90), + net_width=2.0, + net_id="netA", + open_set=open_set, + closed_set={}, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options( + context.options, + guidance_seed=(result.move_spec,), + guidance_bonus=5.0, + ), + move_type="bend90", + cache_key=("guided-bend90",), + ) + add_node( + AStarNode(Port(0, 0, 0), 0.0, 0.0), + result, + target=Port(10, 10, 90), + net_width=2.0, + net_id="netA", + open_set=unguided_open_set, + closed_set={}, + context=context, + metrics=context.metrics, + congestion_cache={}, + congestion_presence_cache={}, + congestion_candidate_precheck_cache={}, + congestion_net_envelope_cache={}, + congestion_grid_net_cache={}, + congestion_grid_span_cache={}, + config=SearchRunConfig.from_options(context.options), + move_type="bend90", + cache_key=("unguided-bend90",), + ) + + assert open_set + assert unguided_open_set + guided_node = open_set[0] + unguided_node = unguided_open_set[0] + assert guided_node.seed_index == 1 + assert unguided_node.g_cost - guided_node.g_cost == pytest.approx(5.0) + assert context.metrics.total_guidance_match_moves == 1 + assert context.metrics.total_guidance_match_moves_straight == 0 + assert context.metrics.total_guidance_match_moves_bend90 == 1 + assert context.metrics.total_guidance_match_moves_sbend == 0 + assert context.metrics.total_guidance_bonus_applied == pytest.approx(5.0) + assert context.metrics.total_guidance_bonus_applied_straight == pytest.approx(0.0) + assert context.metrics.total_guidance_bonus_applied_bend90 == pytest.approx(5.0) + assert context.metrics.total_guidance_bonus_applied_sbend == pytest.approx(0.0) diff --git a/inire/tests/test_clearance_precision.py b/inire/tests/test_clearance_precision.py new file mode 100644 index 0000000..2829d8c --- /dev/null +++ b/inire/tests/test_clearance_precision.py @@ -0,0 +1,132 @@ +from inire import CongestionOptions, RoutingOptions, RoutingProblem, SearchOptions +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.geometry.components import Straight +from inire.model import NetSpec +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + + +def _build_pathfinder( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], + search: SearchOptions | None = None, + congestion: CongestionOptions | None = None, +) -> PathFinder: + nets = tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + return PathFinder( + AStarContext( + evaluator, + RoutingProblem(bounds=bounds, nets=nets), + RoutingOptions( + search=SearchOptions() if search is None else search, + congestion=CongestionOptions() if congestion is None else congestion, + ), + ), + ) + +def test_clearance_thresholds(): + """ + Check that clearance is correctly calculated: + two paths slightly beyond, exactly at, and slightly violating. + """ + # Clearance = 2.0, Width = 2.0 + # Required Centerline-to-Centerline = (2+2)/2 + 2.0 = 4.0 + ce = RoutingWorld(clearance=2.0) + + # Net 1: Centerline at y=0 + p1 = Port(0, 0, 0) + res1 = Straight.generate(p1, 50.0, width=2.0, dilation=1.0) + ce.add_path("net1", res1.collision_geometry, dilated_geometry=res1.dilated_collision_geometry) + + # Net 2: Parallel to Net 1 + # 1. Beyond minimum spacing: y=5. Gap = 5 - 2 = 3 > 2. OK. + p2_ok = Port(0, 5, 0) + res2_ok = Straight.generate(p2_ok, 50.0, width=2.0, dilation=1.0) + report_ok = ce.verify_path_report("net2", [res2_ok]) + assert report_ok.is_valid, f"Gap 3 should be valid, but got {report_ok.collision_count} collisions" + + # 2. Exactly at: y=4.0. Gap = 4.0 - 2.0 = 2.0. OK. + p2_exact = Port(0, 4, 0) + res2_exact = Straight.generate(p2_exact, 50.0, width=2.0, dilation=1.0) + report_exact = ce.verify_path_report("net2", [res2_exact]) + assert report_exact.is_valid, f"Gap exactly 2.0 should be valid, but got {report_exact.collision_count} collisions" + + # 3. Slightly violating: y=3.999. Gap = 3.999 - 2.0 = 1.999 < 2.0. FAIL. + p2_fail = Port(0, 3, 0) + res2_fail = Straight.generate(p2_fail, 50.0, width=2.0, dilation=1.0) + report_fail = ce.verify_path_report("net2", [res2_fail]) + assert not report_fail.is_valid, "Gap 1.999 should be invalid" + assert report_fail.collision_count > 0 + +def test_verify_all_nets_cases(): + """ + Validate that verify_all_nets catches some common cases and doesn't flag reasonable non-failing cases. + """ + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(0, 0, 100, 100)) + danger_map.precompute([]) + evaluator = CostEvaluator(collision_engine=engine, danger_map=danger_map) + + # Case 1: Parallel paths exactly at clearance (Should be VALID) + netlist_parallel_ok = { + "net1": (Port(0, 50, 0), Port(100, 50, 0)), + "net2": (Port(0, 54, 0), Port(100, 54, 0)), + } + net_widths = {"net1": 2.0, "net2": 2.0} + + results = _build_pathfinder( + evaluator, + bounds=(0, 0, 100, 100), + netlist=netlist_parallel_ok, + net_widths=net_widths, + congestion=CongestionOptions(warm_start_enabled=False, max_iterations=1), + ).route_all() + assert results["net1"].is_valid, f"Exactly at clearance should be valid, collisions={results['net1'].collisions}" + assert results["net2"].is_valid + + # Case 2: Parallel paths slightly within clearance (Should be INVALID) + netlist_parallel_fail = { + "net3": (Port(0, 20, 0), Port(100, 20, 0)), + "net4": (Port(0, 23, 0), Port(100, 23, 0)), + } + # Reset engine + engine.remove_path("net1") + engine.remove_path("net2") + + results_p = _build_pathfinder( + evaluator, + bounds=(0, 0, 100, 100), + netlist=netlist_parallel_fail, + net_widths=net_widths, + congestion=CongestionOptions(warm_start_enabled=False, max_iterations=1), + ).route_all() + # verify_all_nets should flag both as invalid because they cross-collide + assert not results_p["net3"].is_valid + assert not results_p["net4"].is_valid + + # Case 3: Crossing paths (Should be INVALID) + netlist_cross = { + "net5": (Port(0, 75, 0), Port(100, 75, 0)), + "net6": (Port(50, 0, 90), Port(50, 100, 90)), + } + engine.remove_path("net3") + engine.remove_path("net4") + + results_c = _build_pathfinder( + evaluator, + bounds=(0, 0, 100, 100), + netlist=netlist_cross, + net_widths=net_widths, + congestion=CongestionOptions(warm_start_enabled=False, max_iterations=1), + ).route_all() + assert not results_c["net5"].is_valid + assert not results_c["net6"].is_valid diff --git a/inire/tests/test_collision.py b/inire/tests/test_collision.py new file mode 100644 index 0000000..e016061 --- /dev/null +++ b/inire/tests/test_collision.py @@ -0,0 +1,488 @@ +from shapely.geometry import box + +from inire.geometry.collision import RoutingWorld +from inire.geometry.components import ComponentResult +from inire.geometry.components import Straight +from inire.geometry.primitives import Port +from inire.router._astar_types import AStarMetrics +from inire.seeds import StraightSeed + + +def _install_static_straight( + engine: RoutingWorld, + start: Port, + length: float, + *, + width: float, + dilation: float = 0.0, +) -> None: + obstacle = Straight.generate(start, length, width=width, dilation=dilation) + for polygon in obstacle.physical_geometry: + engine.add_static_obstacle(polygon) + + +def test_collision_detection() -> None: + engine = RoutingWorld(clearance=2.0) + _install_static_straight(engine, Port(10, 15, 0), 10.0, width=10.0, dilation=1.0) + + direct_hit = Straight.generate(Port(12, 12.5, 0), 1.0, width=1.0, dilation=1.0) + assert engine.check_move_static(direct_hit, start_port=direct_hit.start_port) + + far_away = Straight.generate(Port(0, 2.5, 0), 5.0, width=5.0, dilation=1.0) + assert not engine.check_move_static(far_away, start_port=far_away.start_port) + + near_hit = Straight.generate(Port(8, 12.5, 0), 1.0, width=5.0, dilation=1.0) + assert engine.check_move_static(near_hit, start_port=near_hit.start_port) + + +def test_safety_zone() -> None: + engine = RoutingWorld(clearance=0.0) + _install_static_straight(engine, Port(10, 15, 0), 10.0, width=10.0) + + start_port = Port(10, 12, 0) + test_move = Straight.generate(start_port, 0.002, width=0.001) + assert not engine.check_move_static(test_move, start_port=start_port) + + +def test_ray_cast_width_clearance() -> None: + # Clearance = 2.0um, Width = 2.0um. + # Centerline to obstacle edge must be >= W/2 + C = 1.0 + 2.0 = 3.0um. + engine = RoutingWorld(clearance=2.0) + + # Obstacle at x=10 to 20 + _install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0) + + # 1. Parallel move at x=6. Gap = 10 - 6 = 4.0. Clearly OK. + start_ok = Port(6, 50, 90) + reach_ok = engine.ray_cast(start_ok, 90, max_dist=10.0, net_width=2.0) + assert reach_ok >= 10.0 + + # 2. Parallel move at x=8. Gap = 10 - 8 = 2.0. COLLISION. + start_fail = Port(8, 50, 90) + reach_fail = engine.ray_cast(start_fail, 90, max_dist=10.0, net_width=2.0) + assert reach_fail < 10.0 + + +def test_check_move_static_clearance() -> None: + engine = RoutingWorld(clearance=2.0) + _install_static_straight(engine, Port(10, 50, 0), 10.0, width=100.0, dilation=1.0) + + # Straight move of length 10 at x=8 (Width 2.0) + # Gap = 10 - 8 = 2.0 < 3.0. COLLISION. + start = Port(8, 0, 90) + res = Straight.generate(start, 10.0, width=2.0, dilation=1.0) # dilation = C/2 + + assert engine.check_move_static(res, start_port=start) + + # Move at x=7. Gap = 3.0 == minimum. OK. + start_ok = Port(7, 0, 90) + res_ok = Straight.generate(start_ok, 10.0, width=2.0, dilation=1.0) + assert not engine.check_move_static(res_ok, start_port=start_ok) + + # 3. Same exact-boundary case. + start_exact = Port(7, 0, 90) + res_exact = Straight.generate(start_exact, 10.0, width=2.0, dilation=1.0) + assert not engine.check_move_static(res_exact, start_port=start_exact) + + +def test_verify_path_report_preserves_long_net_id() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789" + path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + geoms = [poly for component in path for poly in component.collision_geometry] + dilated = [poly for component in path for poly in component.dilated_collision_geometry] + + engine.add_path(net_id, geoms, dilated_geometry=dilated) + report = engine.verify_path_report(net_id, path) + + assert report.dynamic_collision_count == 0 + assert engine.metrics.total_verify_dynamic_candidate_nets == 0 + + +def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_" + net_a = f"{shared_prefix}A" + net_b = f"{shared_prefix}B" + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + + engine.add_path( + net_a, + [poly for component in path_a for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry], + ) + engine.add_path( + net_b, + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + ) + + report = engine.verify_path_report(net_a, path_a) + + assert report.dynamic_collision_count == 1 + assert engine.metrics.total_verify_dynamic_candidate_nets == 1 + + +def test_verify_path_report_uses_net_envelopes_before_dynamic_object_scan() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_far = [Straight.generate(Port(100, 100, 0), 20.0, width=2.0, dilation=1.0)] + + engine.add_path( + "netB", + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + ) + engine.add_path( + "netFar", + [poly for component in path_far for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_far for poly in component.dilated_collision_geometry], + ) + + report = engine.verify_path_report("netA", path_a) + + assert report.dynamic_collision_count == 1 + assert engine.metrics.total_verify_dynamic_candidate_nets == 1 + assert engine.metrics.total_verify_dynamic_exact_pair_checks == 1 + + +def test_verify_path_details_returns_conflicting_net_ids() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + + engine.add_path( + "netB", + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + ) + + detail = engine.verify_path_details("netA", path_a) + + assert detail.report.dynamic_collision_count == 1 + assert detail.conflicting_net_ids == ("netB",) + + +def test_verify_path_details_reports_component_conflicts() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [ + Straight.generate(Port(100, 0, 0), 10.0, width=2.0, dilation=1.0), + Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0), + ] + + engine.add_path( + "netB", + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + component_indexes=[0] * len(path_b[0].collision_geometry) + [1] * len(path_b[1].collision_geometry), + ) + + detail = engine.verify_path_details("netA", path_a, capture_component_conflicts=True) + + assert detail.conflicting_net_ids == ("netB",) + assert detail.component_conflicts == ((0, "netB", 1),) + + +def test_verify_path_details_deduplicates_component_conflicts() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + query_component = ComponentResult( + start_port=Port(0, 0, 0), + collision_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)], + end_port=Port(22, 0, 0), + length=22.0, + move_type="straight", + move_spec=StraightSeed(22.0), + physical_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)], + dilated_collision_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)], + dilated_physical_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)], + ) + blocker_component = ComponentResult( + start_port=Port(0, 0, 0), + collision_geometry=[box(5, 0, 17, 10)], + end_port=Port(17, 0, 0), + length=12.0, + move_type="straight", + move_spec=StraightSeed(12.0), + physical_geometry=[box(5, 0, 17, 10)], + dilated_collision_geometry=[box(5, 0, 17, 10)], + dilated_physical_geometry=[box(5, 0, 17, 10)], + ) + + engine.add_path( + "netB", + blocker_component.collision_geometry, + dilated_geometry=blocker_component.dilated_collision_geometry, + component_indexes=[0], + ) + + detail = engine.verify_path_details("netA", [query_component], capture_component_conflicts=True) + + assert detail.conflicting_net_ids == ("netB",) + assert detail.component_conflicts == ((0, "netB", 0),) + + +def test_remove_path_clears_dynamic_path() -> None: + engine = RoutingWorld(clearance=2.0) + path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + geoms = [poly for component in path for poly in component.collision_geometry] + dilated = [poly for component in path for poly in component.dilated_collision_geometry] + + engine.add_path("netA", geoms, dilated_geometry=dilated) + assert {net_id for net_id, _ in engine._dynamic_paths.geometries.values()} == {"netA"} + + engine.remove_path("netA") + assert list(engine._dynamic_paths.geometries.values()) == [] + assert len(engine._static_obstacles.geometries) == 0 + + +def test_dynamic_grid_updates_incrementally_on_add_and_remove() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [Straight.generate(Port(0, 4, 0), 20.0, width=2.0, dilation=1.0)] + + engine.add_path( + "netA", + [poly for component in path_a for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry], + ) + engine.add_path( + "netB", + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + ) + + dynamic_paths = engine._dynamic_paths + assert dynamic_paths.net_to_obj_ids["netA"] + assert dynamic_paths.net_to_obj_ids["netB"] + assert dynamic_paths.grid + assert engine.metrics.total_dynamic_grid_rebuilds == 0 + + engine.remove_path("netA") + + assert "netA" not in dynamic_paths.net_to_obj_ids + assert "netB" in dynamic_paths.net_to_obj_ids + assert engine.metrics.total_dynamic_grid_rebuilds == 0 + assert "netA" not in dynamic_paths.grid_net_obj_ids.get((0, -1), {}) + assert "netB" in dynamic_paths.grid_net_obj_ids.get((0, 0), {}) + + +def test_dynamic_net_envelopes_update_incrementally_on_add_and_remove() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] + path_b = [Straight.generate(Port(0, 40, 0), 10.0, width=2.0, dilation=1.0)] + + engine.add_path( + "netA", + [poly for component in path_a for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry], + ) + engine.add_path( + "netB", + [poly for component in path_b for poly in component.collision_geometry], + dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry], + ) + + dynamic_paths = engine._dynamic_paths + assert set(dynamic_paths.net_envelopes) == {"netA", "netB"} + assert dynamic_paths.net_envelopes["netA"] == (-1.0, -2.0, 21.0, 2.0) + assert dynamic_paths.net_envelopes["netB"] == (-1.0, 38.0, 11.0, 42.0) + assert engine.metrics.total_dynamic_tree_rebuilds == 0 + + net_b_envelope_obj_id = dynamic_paths.net_envelope_obj_ids["netB"] + assert list(dynamic_paths.net_envelope_index.intersection((-5.0, 35.0, 15.0, 45.0))) == [net_b_envelope_obj_id] + + engine.remove_path("netA") + + assert "netA" not in dynamic_paths.net_envelopes + assert "netA" not in dynamic_paths.net_envelope_obj_ids + assert "netB" in dynamic_paths.net_envelopes + assert engine.metrics.total_dynamic_tree_rebuilds == 0 + + + +def test_congestion_query_uses_per_polygon_bounds() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + + blocker = Straight.generate(Port(40, 4, 90), 2.0, width=2.0, dilation=1.0) + engine.add_path( + "netB", + [poly for poly in blocker.collision_geometry], + dilated_geometry=[poly for poly in blocker.dilated_collision_geometry], + ) + + move = ComponentResult( + start_port=Port(0, 0, 0), + collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)], + end_port=Port(100, 0, 0), + length=100.0, + move_type="straight", + move_spec=StraightSeed(100.0), + physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)], + dilated_collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)], + dilated_physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)], + ) + + assert engine.check_move_congestion(move, "netA") == 0 + assert engine.metrics.total_congestion_candidate_nets == 0 + assert engine.metrics.total_congestion_candidate_ids == 0 + assert engine.metrics.total_congestion_exact_pair_checks == 0 + + +def test_congestion_touching_geometries_do_not_count_as_overlap() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + + existing = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + touching = Straight.generate(Port(12, 0, 0), 10.0, width=2.0, dilation=1.0) + + engine.add_path( + "netB", + [poly for poly in existing.collision_geometry], + dilated_geometry=[poly for poly in existing.dilated_collision_geometry], + ) + + assert engine.check_move_congestion(touching, "netA") == 0 + + +def test_congestion_exact_checks_only_touch_relevant_move_polygons() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + + blocker = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0) + engine.add_path( + "netB", + [poly for poly in blocker.collision_geometry], + dilated_geometry=[poly for poly in blocker.dilated_collision_geometry], + ) + + move = ComponentResult( + start_port=Port(0, 0, 0), + collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)], + end_port=Port(100, 0, 0), + length=100.0, + move_type="straight", + move_spec=StraightSeed(100.0), + physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)], + dilated_collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)], + dilated_physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)], + ) + + assert engine.check_move_congestion(move, "netA") == 1 + assert engine.metrics.total_congestion_candidate_nets == 1 + assert engine.metrics.total_congestion_candidate_ids == 1 + assert engine.metrics.total_congestion_exact_pair_checks == 1 + +def test_congestion_grid_span_cache_reuses_broad_phase_candidates() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {} + + blocker = Straight.generate(Port(15, 0, 0), 20.0, width=2.0, dilation=1.0) + engine.add_path( + "netB", + [poly for poly in blocker.collision_geometry], + dilated_geometry=[poly for poly in blocker.dilated_collision_geometry], + ) + + move_a = Straight.generate(Port(5, 0, 0), 20.0, width=2.0, dilation=1.0) + move_b = Straight.generate(Port(7, 0, 0), 20.0, width=2.0, dilation=1.0) + + assert engine.check_move_congestion( + move_a, + "netA", + net_envelope_cache=net_envelope_cache, + grid_net_cache=grid_net_cache, + broad_phase_cache=cache, + ) == 1 + assert engine.check_move_congestion( + move_b, + "netA", + net_envelope_cache=net_envelope_cache, + grid_net_cache=grid_net_cache, + broad_phase_cache=cache, + ) == 1 + assert engine.metrics.total_congestion_candidate_nets == 2 + assert engine.metrics.total_congestion_net_envelope_cache_misses == 1 + assert engine.metrics.total_congestion_net_envelope_cache_hits == 1 + assert engine.metrics.total_congestion_grid_net_cache_misses == 1 + assert engine.metrics.total_congestion_grid_net_cache_hits == 1 + assert engine.metrics.total_congestion_grid_span_cache_misses == 1 + assert engine.metrics.total_congestion_grid_span_cache_hits == 1 + + +def test_has_possible_move_congestion_uses_presence_cache_and_skips_empty_spans() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + presence_cache: dict[tuple[str, int, int, int, int], bool] = {} + + blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0) + engine.add_path( + "netB", + [poly for poly in blocker.collision_geometry], + dilated_geometry=[poly for poly in blocker.dilated_collision_geometry], + ) + empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0) + + assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache) + assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache) + assert engine.metrics.total_congestion_presence_cache_misses == 1 + assert engine.metrics.total_congestion_presence_cache_hits == 1 + + occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0) + + assert engine.has_possible_move_congestion(occupied_move, "netA") + + +def test_has_candidate_move_congestion_uses_candidate_precheck_cache() -> None: + engine = RoutingWorld(clearance=2.0) + engine.metrics = AStarMetrics() + candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {} + net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {} + + blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0) + engine.add_path( + "netB", + [poly for poly in blocker.collision_geometry], + dilated_geometry=[poly for poly in blocker.dilated_collision_geometry], + ) + empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0) + occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0) + + assert not engine.has_candidate_move_congestion( + empty_move, + "netA", + candidate_precheck_cache, + net_envelope_cache, + grid_net_cache, + ) + assert not engine.has_candidate_move_congestion( + empty_move, + "netA", + candidate_precheck_cache, + net_envelope_cache, + grid_net_cache, + ) + assert engine.has_candidate_move_congestion( + occupied_move, + "netA", + candidate_precheck_cache, + net_envelope_cache, + grid_net_cache, + ) + assert engine.metrics.total_congestion_candidate_precheck_misses >= 2 + assert engine.metrics.total_congestion_candidate_precheck_hits >= 1 diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py new file mode 100644 index 0000000..3c14326 --- /dev/null +++ b/inire/tests/test_components.py @@ -0,0 +1,251 @@ +import pytest +from dataclasses import FrozenInstanceError +from shapely.affinity import rotate as shapely_rotate +from shapely.affinity import scale as shapely_scale +from shapely.affinity import translate as shapely_translate +from shapely.geometry import Polygon + +from inire.geometry.components import Bend90, SBend, Straight +from inire.geometry.primitives import Port + + +def test_straight_generation() -> None: + start = Port(0, 0, 0) + length = 10.0 + width = 2.0 + result = Straight.generate(start, length, width) + + assert result.end_port.x == 10.0 + assert result.end_port.y == 0.0 + assert result.end_port.r == 0.0 + assert len(result.collision_geometry) == 1 + + # Bounds of the polygon + minx, miny, maxx, maxy = result.collision_geometry[0].bounds + assert minx == 0.0 + assert maxx == 10.0 + assert miny == -1.0 + assert maxy == 1.0 + assert isinstance(result.collision_geometry, tuple) + + +def test_bend90_generation() -> None: + start = Port(0, 0, 0) + radius = 10.0 + width = 2.0 + + # CW bend + result_cw = Bend90.generate(start, radius, width, direction="CW") + assert result_cw.end_port.x == 10.0 + assert result_cw.end_port.y == -10.0 + assert result_cw.end_port.r == 270.0 + + # CCW bend + result_ccw = Bend90.generate(start, radius, width, direction="CCW") + assert result_ccw.end_port.x == 10.0 + assert result_ccw.end_port.y == 10.0 + assert result_ccw.end_port.r == 90.0 + + +def test_sbend_generation() -> None: + start = Port(0, 0, 0) + offset = 5.0 + radius = 10.0 + width = 2.0 + + result = SBend.generate(start, offset, radius, width) + assert result.end_port.y == 5.0 + assert result.end_port.r == 0.0 + assert len(result.collision_geometry) == 2 # Optimization: returns individual arcs + + # Verify failure for large offset + with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"): + SBend.generate(start, 25.0, 10.0, 2.0) + + +def test_sbend_generation_negative_offset_keeps_second_arc_below_centerline() -> None: + start = Port(0, 0, 0) + offset = -5.0 + radius = 10.0 + width = 2.0 + + result = SBend.generate(start, offset, radius, width) + + assert result.end_port.y == -5.0 + second_arc_minx, second_arc_miny, second_arc_maxx, second_arc_maxy = result.collision_geometry[1].bounds + assert second_arc_maxy <= width / 2.0 + 1e-6 + assert second_arc_miny < -width / 2.0 + + +def test_bend_collision_models() -> None: + start = Port(0, 0, 0) + radius = 10.0 + width = 2.0 + + # 1. BBox model + res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox") + # Arc CCW R=10 from (0,0,0) ends at (10,10,90). + # Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10) + minx, miny, maxx, maxy = res_bbox.collision_geometry[0].bounds + assert minx <= 0.0 + 1e-6 + assert maxx >= 10.0 - 1e-6 + assert miny <= 0.0 + 1e-6 + assert maxy >= 10.0 - 1e-6 + + # 2. Clipped BBox model + res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox") + # Conservative 8-point approximation should still be tighter than the full bbox. + assert len(res_clipped.collision_geometry[0].exterior.coords) - 1 == 8 + assert res_clipped.collision_geometry[0].area < res_bbox.collision_geometry[0].area + + # It should also conservatively contain the true arc. + res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc") + assert res_clipped.collision_geometry[0].covers(res_arc.collision_geometry[0]) + + # 3. Legacy clip-margin mode should still be available when explicitly requested. + res_clipped_margin = Bend90.generate( + start, + radius, + width, + direction="CCW", + collision_type="clipped_bbox", + clip_margin=1.0, + ) + assert len(res_clipped_margin.collision_geometry[0].exterior.coords) - 1 == 4 + assert abs(res_clipped_margin.collision_geometry[0].area - 81.0) < 1e-6 + assert res_clipped_margin.collision_geometry[0].area > res_clipped.collision_geometry[0].area + + +def test_custom_bend_collision_polygon_uses_local_transform() -> None: + custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + + cases = [ + (Port(0, 0, 0), "CCW", (0.0, 10.0), 0.0, False), + (Port(0, 0, 0), "CW", (0.0, -10.0), 0.0, True), + (Port(0, 0, 90), "CCW", (-10.0, 0.0), 90.0, False), + ] + + for start, direction, center_xy, rotation_deg, mirror_y in cases: + result = Bend90.generate(start, 10.0, 2.0, direction=direction, collision_type=custom_poly) + expected = custom_poly + if mirror_y: + expected = shapely_scale(expected, xfact=1.0, yfact=-1.0, origin=(0.0, 0.0)) + if rotation_deg: + expected = shapely_rotate(expected, rotation_deg, origin=(0.0, 0.0), use_radians=False) + expected = shapely_translate(expected, center_xy[0], center_xy[1]) + + assert result.collision_geometry[0].symmetric_difference(expected).area < 1e-6 + + +def test_custom_bend_collision_polygon_is_true_geometry() -> None: + custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + result = Bend90.generate( + Port(0, 0, 0), + 10.0, + 2.0, + direction="CCW", + collision_type=custom_poly, + physical_geometry_type=custom_poly, + dilation=1.0, + ) + + assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area < 1e-6 + assert result.dilated_collision_geometry is not None + assert result.dilated_physical_geometry is not None + assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area < 1e-6 + + +def test_custom_bend_collision_polygon_can_differ_from_physical_geometry() -> None: + custom_proxy = Polygon([(0, -11), (11, -11), (11, 0), (0, 0)]) + result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_proxy, dilation=1.0) + + assert result.collision_geometry[0].symmetric_difference(result.physical_geometry[0]).area > 1e-6 + assert result.dilated_collision_geometry[0].symmetric_difference(result.dilated_physical_geometry[0]).area > 1e-6 + + +def test_sbend_collision_models() -> None: + start = Port(0, 0, 0) + offset = 5.0 + radius = 10.0 + width = 2.0 + + res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox") + # Geometry should be a list of individual bbox polygons for each arc + assert len(res_bbox.collision_geometry) == 2 + + res_arc = SBend.generate(start, offset, radius, width, collision_type="arc") + area_bbox = sum(p.area for p in res_bbox.collision_geometry) + area_arc = sum(p.area for p in res_arc.collision_geometry) + assert area_bbox > area_arc + + +def test_sbend_continuity() -> None: + # Verify SBend endpoints and continuity math + start = Port(10, 20, 90) # Starting facing up + offset = 4.0 + radius = 20.0 + width = 1.0 + + res = SBend.generate(start, offset, radius, width) + + # Target orientation should be same as start + assert abs(res.end_port.r - 90.0) < 1e-6 + + # For a port at 90 deg, +offset is a shift in -x direction + assert abs(res.end_port.x - (10.0 - offset)) < 1e-6 + + # Geometry should be a list of valid polygons + assert len(res.collision_geometry) == 2 + for p in res.collision_geometry: + assert p.is_valid + + +def test_arc_sagitta_precision() -> None: + # Verify that requested sagitta actually controls segment count + start = Port(0, 0, 0) + radius = 100.0 # Large radius to make sagitta significant + width = 2.0 + + # Coarse: 1um sagitta + res_coarse = Bend90.generate(start, radius, width, direction="CCW", sagitta=1.0) + # Fine: 0.01um (10nm) sagitta + res_fine = Bend90.generate(start, radius, width, direction="CCW", sagitta=0.01) + + # Number of segments should be significantly higher for fine + # Exterior points = (segments + 1) * 2 + pts_coarse = len(res_coarse.collision_geometry[0].exterior.coords) + pts_fine = len(res_fine.collision_geometry[0].exterior.coords) + + assert pts_fine > pts_coarse * 2 + + +def test_component_transform_invariance() -> None: + # Verify that generating at (0,0) then transforming + # is same as generating at the transformed port. + start0 = Port(0, 0, 0) + radius = 10.0 + width = 2.0 + + res0 = Bend90.generate(start0, radius, width, direction="CCW") + + # Transform: Translate (10, 10) then Rotate 90 + dx, dy = 10.0, 5.0 + angle = 90.0 + + # 1. Transform the generated geometry + p_end_transformed = res0.end_port.translate(dx, dy).rotated(angle) + + # 2. Generate at transformed start + start_transformed = start0.translate(dx, dy).rotated(angle) + res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW") + + assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6 + assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6 + assert abs(res_transformed.end_port.r - p_end_transformed.r) < 1e-6 + + +def test_component_result_is_immutable_value_type() -> None: + result = Straight.generate(Port(0, 0, 0), 10.0, 2.0) + + with pytest.raises(FrozenInstanceError): + result.length = 42.0 diff --git a/inire/tests/test_congestion.py b/inire/tests/test_congestion.py new file mode 100644 index 0000000..7d3f2eb --- /dev/null +++ b/inire/tests/test_congestion.py @@ -0,0 +1,98 @@ +import pytest + +from inire import CongestionOptions, RoutingOptions, RoutingProblem, SearchOptions +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.model import NetSpec +from inire.router._astar_types import AStarContext, SearchRunConfig +from inire.router._router import PathFinder +from inire.router._search import route_astar +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + +BOUNDS = (0, -40, 100, 40) + + +@pytest.fixture +def basic_evaluator() -> CostEvaluator: + engine = RoutingWorld(clearance=2.0) + # Wider bounds to allow going around (y from -40 to 40) + danger_map = DangerMap(bounds=BOUNDS) + danger_map.precompute([]) + return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) + + +def _build_context( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + nets: tuple[NetSpec, ...] = (), + search: SearchOptions | None = None, + congestion: CongestionOptions | None = None, +) -> AStarContext: + return AStarContext( + evaluator, + RoutingProblem(bounds=bounds, nets=nets), + RoutingOptions( + search=SearchOptions() if search is None else search, + congestion=CongestionOptions() if congestion is None else congestion, + ), + ) + + +def _build_pathfinder( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], + search: SearchOptions | None = None, + congestion: CongestionOptions | None = None, +) -> PathFinder: + nets = tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + return PathFinder( + _build_context( + evaluator, + bounds=bounds, + nets=nets, + search=search, + congestion=congestion, + ), + ) + + +def _route(context: AStarContext, start: Port, target: Port) -> object: + return route_astar( + start, + target, + net_width=2.0, + context=context, + config=SearchRunConfig.from_options(context.options), + ) + + +def test_astar_sbend(basic_evaluator: CostEvaluator) -> None: + context = _build_context( + basic_evaluator, + bounds=BOUNDS, + search=SearchOptions(sbend_offsets=(2.0, 5.0)), + ) + # Start at (0,0), target at (50, 2) -> 2um lateral offset + # This matches one of our discretized SBend offsets. + start = Port(0, 0, 0) + target = Port(50, 2, 0) + path = _route(context, start, target) + + assert path is not None + # Check if any component in the path is an SBend + found_sbend = False + for res in path: + # Check if the end port orientation is same as start + # and it's not a single straight (which would have y=0) + if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.r - start.r) < 0.1: + found_sbend = True + break + assert found_sbend diff --git a/inire/tests/test_cost.py b/inire/tests/test_cost.py new file mode 100644 index 0000000..05976a9 --- /dev/null +++ b/inire/tests/test_cost.py @@ -0,0 +1,95 @@ +from shapely.geometry import Polygon +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + + +def test_cost_calculation() -> None: + engine = RoutingWorld(clearance=2.0) + # 50x50 um area, 1um resolution + danger_map = DangerMap(bounds=(0, 0, 50, 50)) + danger_map.precompute([]) + # Use small penalties for testing + evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1, bend_penalty=10.0) + + p1 = Port(0, 0, 0) + p2 = Port(10, 10, 0) + + h = evaluator.h_manhattan(p1, p2) + # Manhattan distance = 20. + # Jog alignment penalty = 2*bp = 20. + # Side check penalty = 2*bp = 20. + # Total = 1.1 * (20 + 40) = 66.0 + assert abs(h - 66.0) < 1e-6 + + # Orientation difference + p3 = Port(10, 10, 90) + h_90 = evaluator.h_manhattan(p1, p3) + # diff = 90. penalty += 1*bp = 10. + # Side check: 2*bp = 20. (Total penalty = 30) + # Total = 1.1 * (20 + 30) = 55.0 + assert abs(h_90 - 55.0) < 1e-6 + + # Traveling away + p4 = Port(10, 10, 180) + h_away = evaluator.h_manhattan(p1, p4) + # diff = 180. penalty += 2*bp = 20. + # Side check: 2*bp = 20. + # Total = 1.1 * (20 + 40) = 66.0 + assert h_away >= h_90 + + +def test_greedy_h_weight_is_mutable() -> None: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(0, 0, 50, 50)) + danger_map.precompute([]) + evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=10.0) + + assert evaluator.greedy_h_weight == 1.5 + evaluator.greedy_h_weight = 1.2 + assert evaluator.greedy_h_weight == 1.2 + assert abs(evaluator.h_manhattan(Port(0, 0, 0), Port(10, 10, 0)) - 72.0) < 1e-6 + + +def test_danger_map_kd_tree_and_cache() -> None: + # Test that KD-Tree based danger map works and uses cache + bounds = (0, 0, 1000, 1000) + dm = DangerMap(bounds, resolution=1.0, safety_threshold=10.0) + + # Square obstacle at (100, 100) to (110, 110) + obstacle = Polygon([(100, 100), (110, 100), (110, 110), (100, 110)]) + dm.precompute([obstacle]) + + # 1. High cost near boundary + cost_near = dm.get_cost(100.5, 100.5) + assert cost_near > 1.0 + + # 2. Zero cost far away + cost_far = dm.get_cost(500, 500) + assert cost_far == 0.0 + + # 3. Check cache usage (internal detail check) + # We can check if calling it again is fast or just verify it returns same result + cost_near_2 = dm.get_cost(100.5, 100.5) + assert cost_near_2 == cost_near + assert len(dm._cost_cache) == 2 + + # 4. Out of bounds + assert dm.get_cost(-1, -1) >= 1e12 + + +def test_danger_map_cache_is_instance_local_and_cleared_on_precompute() -> None: + obstacle = Polygon([(10, 10), (20, 10), (20, 20), (10, 20)]) + dm_a = DangerMap((0, 0, 100, 100)) + dm_b = DangerMap((0, 0, 100, 100)) + + dm_a.precompute([obstacle]) + dm_b.precompute([]) + + dm_a.get_cost(15.0, 15.0) + assert len(dm_a._cost_cache) == 1 + assert len(dm_b._cost_cache) == 0 + + dm_a.precompute([]) + assert len(dm_a._cost_cache) == 0 diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py new file mode 100644 index 0000000..adf7bfb --- /dev/null +++ b/inire/tests/test_example_performance.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import os +import statistics +from typing import TYPE_CHECKING + +import pytest + +from inire.tests.example_scenarios import ( + SCENARIOS, + ScenarioOutcome, + snapshot_example_07_no_warm_start, + snapshot_example_07_no_warm_start_seed43, + trace_example_07_no_warm_start_seed43, +) + +if TYPE_CHECKING: + from collections.abc import Callable + + +RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" +PERFORMANCE_REPEATS = 3 +REGRESSION_FACTOR = 1.5 +NO_WARM_START_REGRESSION_SECONDS = 15.0 +NO_WARM_START_SEED43_REGRESSION_SECONDS = 20.0 + +# Baselines are measured from clean 6a28dcf-style runs without plotting. +BASELINE_SECONDS = { + "example_01_simple_route": 0.0035, + "example_02_congestion_resolution": 0.2666, + "example_03_locked_paths": 0.2304, + "example_04_sbends_and_radii": 1.8734, + "example_05_orientation_stress": 0.5630, + "example_06_bend_collision_models": 5.2382, + "example_07_large_scale_routing": 1.2081, + "example_08_custom_bend_geometry": 0.9848, + "example_09_unroutable_best_effort": 0.0056, +} + +EXPECTED_OUTCOMES = { + "example_01_simple_route": {"total_results": 1, "valid_results": 1, "reached_targets": 1}, + "example_02_congestion_resolution": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, + "example_03_locked_paths": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, + "example_04_sbends_and_radii": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, + "example_05_orientation_stress": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, + "example_06_bend_collision_models": {"total_results": 3, "valid_results": 3, "reached_targets": 3}, + "example_07_large_scale_routing": {"total_results": 10, "valid_results": 10, "reached_targets": 10}, + "example_08_custom_bend_geometry": {"total_results": 2, "valid_results": 2, "reached_targets": 2}, + "example_09_unroutable_best_effort": {"total_results": 1, "valid_results": 0, "reached_targets": 0}, +} + + +def _assert_expected_outcome(name: str, outcome: ScenarioOutcome) -> None: + _, total_results, valid_results, reached_targets = outcome + expected = EXPECTED_OUTCOMES[name] + assert total_results == expected["total_results"] + assert valid_results == expected["valid_results"] + assert reached_targets == expected["reached_targets"] + + +@pytest.mark.performance +@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks") +@pytest.mark.parametrize("scenario", SCENARIOS, ids=[name for name, _ in SCENARIOS]) +def test_example_like_runtime_regression(scenario: tuple[str, Callable[[], ScenarioOutcome]]) -> None: + name, run = scenario + timings = [] + for _ in range(PERFORMANCE_REPEATS): + outcome = run() + _assert_expected_outcome(name, outcome) + timings.append(outcome[0]) + + median_runtime = statistics.median(timings) + assert median_runtime <= BASELINE_SECONDS[name] * REGRESSION_FACTOR, ( + f"{name} median runtime {median_runtime:.4f}s exceeded " + f"{REGRESSION_FACTOR:.1f}x baseline {BASELINE_SECONDS[name]:.4f}s " + f"from timings {timings!r}" + ) + + +@pytest.mark.performance +@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks") +def test_example_07_no_warm_start_runtime_regression() -> None: + snapshot = snapshot_example_07_no_warm_start() + + assert snapshot.total_results == 10 + assert snapshot.valid_results == 10 + assert snapshot.reached_targets == 10 + assert snapshot.metrics.warm_start_paths_built == 0 + assert snapshot.metrics.warm_start_paths_used == 0 + assert snapshot.duration_s <= NO_WARM_START_REGRESSION_SECONDS, ( + "example_07_large_scale_routing_no_warm_start runtime " + f"{snapshot.duration_s:.4f}s exceeded guardrail " + f"{NO_WARM_START_REGRESSION_SECONDS:.1f}s" + ) + + +@pytest.mark.performance +@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks") +def test_example_07_no_warm_start_seed43_runtime_regression() -> None: + snapshot = snapshot_example_07_no_warm_start_seed43() + run = trace_example_07_no_warm_start_seed43() + + assert snapshot.total_results == 10 + assert snapshot.valid_results == 10 + assert snapshot.reached_targets == 10 + assert snapshot.metrics.warm_start_paths_built == 0 + assert snapshot.metrics.warm_start_paths_used == 0 + assert snapshot.metrics.pair_local_search_pairs_considered >= 1 + assert snapshot.metrics.pair_local_search_accepts >= 1 + assert snapshot.duration_s <= NO_WARM_START_SEED43_REGRESSION_SECONDS, ( + "example_07_large_scale_routing_no_warm_start_seed43 runtime " + f"{snapshot.duration_s:.4f}s exceeded guardrail " + f"{NO_WARM_START_SEED43_REGRESSION_SECONDS:.1f}s" + ) + + assert run.iteration_trace + assert len(run.results_by_net) == 10 + assert sum(result.is_valid for result in run.results_by_net.values()) == 10 + assert sum(result.reached_target for result in run.results_by_net.values()) == 10 + assert run.metrics.warm_start_paths_built == 0 + assert run.metrics.warm_start_paths_used == 0 + assert run.metrics.pair_local_search_pairs_considered >= 1 + assert run.metrics.pair_local_search_accepts >= 1 diff --git a/inire/tests/test_example_regressions.py b/inire/tests/test_example_regressions.py new file mode 100644 index 0000000..c7a10d9 --- /dev/null +++ b/inire/tests/test_example_regressions.py @@ -0,0 +1,303 @@ +import pytest +from shapely.geometry import Polygon, box + +from inire import ( + CongestionOptions, + DiagnosticsOptions, + NetSpec, + ObjectiveWeights, + Port, + RoutingOptions, + RoutingProblem, + SearchOptions, + route, +) +from inire.router._stack import build_routing_stack +from inire.seeds import Bend90Seed, PathSeed, StraightSeed +from inire.tests.example_scenarios import ( + SCENARIOS, + _build_evaluator, + _build_pathfinder, + _net_specs, + AStarMetrics, + snapshot_example_05, + snapshot_example_07_no_warm_start, + trace_example_07_no_warm_start, +) + + +EXPECTED_OUTCOMES = { + "example_01_simple_route": (1, 1, 1), + "example_02_congestion_resolution": (3, 3, 3), + "example_03_locked_paths": (2, 2, 2), + "example_04_sbends_and_radii": (2, 2, 2), + "example_05_orientation_stress": (3, 3, 3), + "example_06_bend_collision_models": (3, 3, 3), + "example_07_large_scale_routing": (10, 10, 10), + "example_08_custom_bend_geometry": (2, 2, 2), + "example_09_unroutable_best_effort": (1, 0, 0), +} + + +@pytest.mark.parametrize(("name", "run"), SCENARIOS, ids=[name for name, _ in SCENARIOS]) +def test_examples_match_legacy_expected_outcomes(name: str, run) -> None: + outcome = run() + assert outcome[1:] == EXPECTED_OUTCOMES[name] + + +def test_example_05_avoids_dynamic_tree_rebuilds() -> None: + snapshot = snapshot_example_05() + + assert snapshot.valid_results == 3 + assert snapshot.metrics.dynamic_tree_rebuilds == 0 + + +def test_example_07_no_warm_start_canary_improves_validity() -> None: + snapshot = snapshot_example_07_no_warm_start() + + assert snapshot.total_results == 10 + assert snapshot.reached_targets == 10 + assert snapshot.valid_results == 10 + assert snapshot.metrics.warm_start_paths_built == 0 + assert snapshot.metrics.warm_start_paths_used == 0 + assert snapshot.metrics.pair_local_search_pairs_considered >= 1 + assert snapshot.metrics.pair_local_search_accepts >= 1 + assert snapshot.metrics.pair_local_search_nodes_expanded <= 128 + assert snapshot.metrics.nodes_expanded <= 2500 + assert snapshot.metrics.congestion_check_calls <= 6000 + + +def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> None: + run = trace_example_07_no_warm_start() + + assert len(run.results_by_net) == 10 + assert sum(result.is_valid for result in run.results_by_net.values()) == 10 + assert sum(result.reached_target for result in run.results_by_net.values()) == 10 + assert run.metrics.pair_local_search_pairs_considered >= 1 + assert run.metrics.pair_local_search_accepts >= 1 + assert run.pre_pair_frontier_trace is not None + + final_entry = run.conflict_trace[-1] + assert final_entry.stage == "final" + assert len(final_entry.completed_net_ids) == 10 + assert final_entry.conflict_edges == () + + +def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None: + bounds = (-20, -20, 170, 170) + obstacles = ( + Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]), + Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]), + Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]), + ) + problem = RoutingProblem( + bounds=bounds, + nets=(NetSpec("clipped_model", Port(10, 20, 0), Port(90, 40, 90), width=2.0),), + static_obstacles=obstacles, + ) + common_kwargs = { + "objective": ObjectiveWeights(bend_penalty=50.0, sbend_penalty=150.0), + "congestion": CongestionOptions(use_tiered_strategy=False), + } + no_margin = route( + problem, + options=RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_collision_type="clipped_bbox", + ), + **common_kwargs, + ), + ).results_by_net["clipped_model"] + legacy_margin = route( + problem, + options=RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_collision_type="clipped_bbox", + bend_clip_margin=1.0, + ), + **common_kwargs, + ), + ).results_by_net["clipped_model"] + + assert no_margin.is_valid + assert legacy_margin.is_valid + assert legacy_margin.as_seed() != no_margin.as_seed() + assert legacy_margin.as_seed() == PathSeed( + ( + StraightSeed(5.0), + Bend90Seed(10.0, "CW"), + Bend90Seed(10.0, "CCW"), + StraightSeed(45.0), + Bend90Seed(10.0, "CCW"), + StraightSeed(30.0), + ) + ) + + +def test_example_07_reduced_bottleneck_uses_adaptive_greedy_callback() -> None: + bounds = (0, 0, 500, 300) + obstacles = ( + box(220, 0, 280, 100), + box(220, 200, 280, 300), + ) + netlist = { + "net_00": (Port(30, 130, 0), Port(470, 60, 0)), + "net_01": (Port(30, 140, 0), Port(470, 120, 0)), + "net_02": (Port(30, 150, 0), Port(470, 180, 0)), + "net_03": (Port(30, 160, 0), Port(470, 240, 0)), + } + problem = RoutingProblem( + bounds=bounds, + nets=tuple(NetSpec(net_id, start, target, width=2.0) for net_id, (start, target) in netlist.items()), + static_obstacles=obstacles, + clearance=6.0, + ) + options = RoutingOptions( + search=SearchOptions( + node_limit=200000, + bend_radii=(30.0,), + sbend_radii=(30.0,), + greedy_h_weight=1.5, + bend_clip_margin=10.0, + ), + objective=ObjectiveWeights( + unit_length_cost=0.1, + bend_penalty=100.0, + sbend_penalty=400.0, + ), + congestion=CongestionOptions( + max_iterations=6, + base_penalty=100.0, + multiplier=1.4, + net_order="shortest", + shuffle_nets=True, + seed=42, + ), + diagnostics=DiagnosticsOptions(capture_expanded=False), + ) + stack = build_routing_stack(problem, options) + evaluator = stack.evaluator + finder = stack.finder + weights: list[float] = [] + + def iteration_callback(iteration: int, current_results: dict[str, object]) -> None: + _ = current_results + new_greedy = max(1.1, 1.5 - ((iteration + 1) / 10.0) * 0.4) + evaluator.greedy_h_weight = new_greedy + weights.append(new_greedy) + finder.metrics.reset_per_route() + + results = finder.route_all(iteration_callback=iteration_callback) + + assert weights == [1.46] + assert evaluator.greedy_h_weight == 1.46 + assert all(result.is_valid for result in results.values()) + assert all(result.reached_target for result in results.values()) + + +def test_example_06_custom_geometry_can_be_true_physical_geometry() -> None: + bounds = (-20, -20, 170, 170) + obstacles = ( + Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]), + Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]), + Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]), + ) + custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + result = route( + RoutingProblem( + bounds=bounds, + nets=(NetSpec("custom_geometry", Port(10, 20, 0), Port(90, 40, 90), width=2.0),), + static_obstacles=obstacles, + ), + options=RoutingOptions( + search=SearchOptions( + bend_radii=(10.0,), + bend_physical_geometry=custom_poly, + bend_proxy_geometry=custom_poly, + ), + objective=ObjectiveWeights(bend_penalty=50.0, sbend_penalty=150.0), + congestion=CongestionOptions(use_tiered_strategy=False), + ), + ).results_by_net["custom_geometry"] + + assert result.is_valid + bends = [component for component in result.path if component.move_type == "bend90"] + assert bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area < 1e-6 + for component in bends + ) + + +def test_custom_proxy_without_physical_geometry_warns_and_keeps_arc_geometry() -> None: + custom_proxy = Polygon([(0, -11), (11, -11), (11, 0), (0, 0)]) + + with pytest.warns(UserWarning, match="Custom bend proxy provided without bend_physical_geometry"): + search = SearchOptions( + bend_radii=(10.0,), + sbend_radii=(), + bend_proxy_geometry=custom_proxy, + ) + + problem = RoutingProblem( + bounds=(0, 0, 150, 150), + nets=(NetSpec("proxy_only", Port(20, 20, 0), Port(100, 100, 90), width=2.0),), + ) + result = route( + problem, + options=RoutingOptions( + search=search, + congestion=CongestionOptions(max_iterations=1, use_tiered_strategy=False), + ), + ).results_by_net["proxy_only"] + + bends = [component for component in result.path if component.move_type == "bend90"] + assert bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area > 1e-6 + for component in bends + ) + + +def test_example_08_custom_geometry_runs_in_separate_sessions() -> None: + bounds = (0, 0, 150, 150) + netlist = {"standard_arc": (Port(20, 20, 0), Port(100, 100, 90))} + widths = {"standard_arc": 2.0} + custom_physical = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)]) + custom_proxy = box(0, -11, 11, 0) + + standard = _build_pathfinder( + _build_evaluator(bounds), + bounds=bounds, + nets=_net_specs(netlist, widths), + bend_radii=[10.0], + sbend_radii=[], + max_iterations=1, + use_tiered_strategy=False, + metrics=AStarMetrics(), + ).route_all() + custom = _build_pathfinder( + _build_evaluator(bounds), + bounds=bounds, + nets=_net_specs({"custom_geometry_and_proxy": netlist["standard_arc"]}, {"custom_geometry_and_proxy": 2.0}), + bend_radii=[10.0], + bend_physical_geometry=custom_physical, + bend_proxy_geometry=custom_proxy, + sbend_radii=[], + max_iterations=1, + use_tiered_strategy=False, + metrics=AStarMetrics(), + ).route_all() + + assert standard["standard_arc"].is_valid + assert standard["standard_arc"].reached_target + assert custom["custom_geometry_and_proxy"].is_valid + assert custom["custom_geometry_and_proxy"].reached_target + custom_bends = [component for component in custom["custom_geometry_and_proxy"].path if component.move_type == "bend90"] + assert custom_bends + assert all( + component.collision_geometry[0].symmetric_difference(component.physical_geometry[0]).area > 1e-6 + for component in custom_bends + ) diff --git a/inire/tests/test_failed_net_congestion.py b/inire/tests/test_failed_net_congestion.py new file mode 100644 index 0000000..db300ce --- /dev/null +++ b/inire/tests/test_failed_net_congestion.py @@ -0,0 +1,77 @@ +from inire import CongestionOptions, RoutingOptions, RoutingProblem +from inire.geometry.primitives import Port +from inire.geometry.collision import RoutingWorld +from inire.model import NetSpec +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + +def test_failed_net_visibility() -> None: + """ + Verifies that nets that fail to reach their target (return partial paths) + ARE added to the collision engine, making them visible to other nets + for negotiated congestion. + """ + # 1. Setup + engine = RoutingWorld(clearance=2.0) + + # Create a simple danger map (bounds 0-100) + # We don't strictly need obstacles in it for this test. + dm = DangerMap(bounds=(0, 0, 100, 100)) + + evaluator = CostEvaluator(engine, dm) + + # 2. Configure Router with low limit to FORCE failure + # node_limit=10 is extremely low, likely allowing only a few moves. + # Start (0,0) -> Target (100,0) is 100um away. + + # Let's add a static obstacle that blocks the direct path. + from shapely.geometry import box + + obstacle = box(40, -10, 60, 10) # Wall at x=50 + engine.add_static_obstacle(obstacle) + + # With obstacle, direct jump fails. A* must search around. + # Limit=10 should be enough to fail to find a path around. + netlist = { + "net1": (Port(0, 0, 0), Port(100, 0, 0)) + } + net_widths = {"net1": 1.0} + pf = PathFinder( + AStarContext( + evaluator, + RoutingProblem( + bounds=(0, 0, 100, 100), + nets=tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ), + ), + RoutingOptions( + search=RoutingOptions().search.__class__(node_limit=10), + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + ), + ), + ) + + # 4. Route + print("\nStarting Route...") + results = pf.route_all() + + res = results["net1"] + print(f"Result: is_valid={res.is_valid}, reached={res.reached_target}, path_len={len(res.path)}") + + # 5. Verify Failure Condition + # We expect reached_target to be False because of node_limit + obstacle + assert not res.reached_target, "Test setup failed: Net reached target despite low limit!" + assert len(res.path) > 0, "Test setup failed: No partial path returned!" + + # 6. Verify Visibility + # Check if net1 is in the collision engine + found_nets = {net_id for net_id, _ in engine._dynamic_paths.geometries.values()} + + print(f"Nets found in engine: {found_nets}") + + # The FIX Expectation: "net1" SHOULD be present + assert "net1" in found_nets, "Bug present: Net1 is invisible despite having partial path!" diff --git a/inire/tests/test_fuzz.py b/inire/tests/test_fuzz.py new file mode 100644 index 0000000..7c43251 --- /dev/null +++ b/inire/tests/test_fuzz.py @@ -0,0 +1,93 @@ +from typing import Any + +import pytest +from hypothesis import given, settings, strategies as st +from shapely.geometry import Point, Polygon + +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.model import RoutingOptions, RoutingProblem, SearchOptions +from inire.router._astar_types import AStarContext, SearchRunConfig +from inire.router._search import route_astar +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + + +@st.composite +def random_obstacle(draw: Any) -> Polygon: + x = draw(st.floats(min_value=0, max_value=20)) + y = draw(st.floats(min_value=0, max_value=20)) + w = draw(st.floats(min_value=1, max_value=5)) + h = draw(st.floats(min_value=1, max_value=5)) + return Polygon([(x, y), (x + w, y), (x + w, y + h), (x, y + h)]) + + +@st.composite +def random_port(draw: Any) -> Port: + x = draw(st.floats(min_value=0, max_value=20)) + y = draw(st.floats(min_value=0, max_value=20)) + orientation = draw(st.sampled_from([0, 90, 180, 270])) + return Port(x, y, orientation) + + +def _port_has_required_clearance(port: Port, obstacles: list[Polygon], clearance: float, net_width: float) -> bool: + point = Point(float(port.x), float(port.y)) + required_gap = (net_width / 2.0) + clearance + return all(point.distance(obstacle) >= required_gap for obstacle in obstacles) + + +def _build_context( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + **search_overrides: object, +) -> AStarContext: + return AStarContext( + evaluator, + RoutingProblem(bounds=bounds), + RoutingOptions(search=SearchOptions(**search_overrides)), + ) + + +def _route(context: AStarContext, start: Port, target: Port): + return route_astar( + start, + target, + net_width=2.0, + context=context, + config=SearchRunConfig.from_options(context.options), + ) + + +@settings(max_examples=3, deadline=None) +@given(obstacles=st.lists(random_obstacle(), min_size=0, max_size=3), start=random_port(), target=random_port()) +def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port) -> None: + net_width = 2.0 + clearance = 2.0 + engine = RoutingWorld(clearance=2.0) + for obs in obstacles: + engine.add_static_obstacle(obs) + + danger_map = DangerMap(bounds=(0, 0, 30, 30)) + danger_map.precompute(obstacles) + + evaluator = CostEvaluator(engine, danger_map) + context = _build_context(evaluator, bounds=(0, 0, 30, 30), node_limit=5000) + + # Check if start/target are inside obstacles (safety zone check) + # The router should handle this gracefully (either route or return None) + try: + path = _route(context, start, target) + + # This is a crash-smoke test rather than a full correctness proof. + # If a full path is returned, it should at least terminate at the requested target. + endpoints_are_clear = ( + _port_has_required_clearance(start, obstacles, clearance, net_width) + and _port_has_required_clearance(target, obstacles, clearance, net_width) + ) + if path and endpoints_are_clear: + assert path[-1].end_port == target + + except Exception as e: + # Unexpected exceptions are failures + pytest.fail(f"Router crashed with {type(e).__name__}: {e}") diff --git a/inire/tests/test_pathfinder.py b/inire/tests/test_pathfinder.py new file mode 100644 index 0000000..773bd56 --- /dev/null +++ b/inire/tests/test_pathfinder.py @@ -0,0 +1,231 @@ +from shapely.geometry import box + +from inire import ( + CongestionOptions, + DiagnosticsOptions, + NetSpec, + ObjectiveWeights, + RefinementOptions, + RoutingOptions, + RoutingProblem, + SearchOptions, +) +from inire.geometry.collision import RoutingWorld +from inire.geometry.components import Bend90, Straight +from inire.geometry.primitives import Port +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + +DEFAULT_BOUNDS = (0, 0, 100, 100) + +_PROBLEM_FIELDS = set(RoutingProblem.__dataclass_fields__) - {"bounds", "nets"} +_SEARCH_FIELDS = set(SearchOptions.__dataclass_fields__) +_CONGESTION_FIELDS = set(CongestionOptions.__dataclass_fields__) +_REFINEMENT_FIELDS = set(RefinementOptions.__dataclass_fields__) +_DIAGNOSTICS_FIELDS = set(DiagnosticsOptions.__dataclass_fields__) +_OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__) + +def _request_nets( + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], +) -> tuple[NetSpec, ...]: + return tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + + +def _build_options(**overrides: object) -> RoutingOptions: + search_overrides = {key: value for key, value in overrides.items() if key in _SEARCH_FIELDS} + congestion_overrides = {key: value for key, value in overrides.items() if key in _CONGESTION_FIELDS} + refinement_overrides = {key: value for key, value in overrides.items() if key in _REFINEMENT_FIELDS} + diagnostics_overrides = {key: value for key, value in overrides.items() if key in _DIAGNOSTICS_FIELDS} + objective_overrides = {key: value for key, value in overrides.items() if key in _OBJECTIVE_FIELDS} + return RoutingOptions( + search=SearchOptions(**search_overrides), + congestion=CongestionOptions(**congestion_overrides), + refinement=RefinementOptions(**refinement_overrides), + diagnostics=DiagnosticsOptions(**diagnostics_overrides), + objective=ObjectiveWeights(**objective_overrides), + ) + + +def _build_context( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + nets: tuple[NetSpec, ...] = (), + **request_overrides: object, +) -> AStarContext: + problem_overrides = {key: value for key, value in request_overrides.items() if key in _PROBLEM_FIELDS} + option_overrides = {key: value for key, value in request_overrides.items() if key not in _PROBLEM_FIELDS} + return AStarContext( + evaluator, + RoutingProblem(bounds=bounds, nets=nets, **problem_overrides), + _build_options(**option_overrides), + ) + + +def _build_pathfinder( + evaluator: CostEvaluator, + *, + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], + bounds: tuple[float, float, float, float] = DEFAULT_BOUNDS, + metrics=None, + **request_overrides: object, +) -> PathFinder: + return PathFinder( + _build_context( + evaluator, + bounds=bounds, + nets=_request_nets(netlist, net_widths), + **request_overrides, + ), + metrics=metrics, + ) + + +def _build_manual_path(start: Port, width: float, clearance: float, steps: list[tuple[str, float | str]]) -> list: + path = [] + curr = start + dilation = clearance / 2.0 + for kind, value in steps: + if kind == "B": + comp = Bend90.generate(curr, 5.0, width, value, dilation=dilation) + else: + comp = Straight.generate(curr, value, width, dilation=dilation) + path.append(comp) + curr = comp.end_port + return path +def test_route_all_refreshes_static_caches_after_static_topology_changes() -> None: + netlist = {"net": (Port(0, 0, 0), Port(10, 10, 90))} + widths = {"net": 2.0} + + def build_router() -> tuple[RoutingWorld, AStarContext, PathFinder]: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(-20, -20, 60, 60)) + danger_map.precompute([]) + evaluator = CostEvaluator(engine, danger_map) + context = _build_context( + evaluator, + bounds=(-20, -20, 60, 60), + nets=_request_nets(netlist, widths), + bend_radii=[10.0], + max_straight_length=50.0, + node_limit=50, + warm_start_enabled=False, + max_iterations=1, + enabled=False, + ) + return engine, context, PathFinder(context) + + engine_auto, _context_auto, pf_auto = build_router() + assert pf_auto.route_all()["net"].is_valid + engine_auto.add_static_obstacle(box(4, 4, 8, 12)) + auto_result = pf_auto.route_all()["net"] + + engine_manual, context_manual, pf_manual = build_router() + assert pf_manual.route_all()["net"].is_valid + engine_manual.add_static_obstacle(box(4, 4, 8, 12)) + context_manual.clear_static_caches() + manual_result = pf_manual.route_all()["net"] + + assert auto_result.reached_target == manual_result.reached_target + assert auto_result.collisions == manual_result.collisions + assert auto_result.outcome == manual_result.outcome + assert [(comp.move_type, comp.start_port.as_tuple(), comp.end_port.as_tuple()) for comp in auto_result.path] == [ + (comp.move_type, comp.start_port.as_tuple(), comp.end_port.as_tuple()) for comp in manual_result.path + ] +def test_refine_path_handles_same_orientation_lateral_offset() -> None: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(-20, -20, 120, 120)) + danger_map.precompute([]) + evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0) + pf = _build_pathfinder( + evaluator, + netlist={"net": (Port(0, 0, 0), Port(60, 15, 0))}, + net_widths={"net": 2.0}, + bounds=(-20, -20, 120, 120), + bend_radii=[5.0, 10.0], + enabled=True, + ) + + start = Port(0, 0, 0) + width = 2.0 + path = _build_manual_path( + start, + width, + engine.clearance, + [ + ("B", "CCW"), + ("S", 10.0), + ("B", "CW"), + ("S", 20.0), + ("B", "CW"), + ("S", 10.0), + ("B", "CCW"), + ("S", 10.0), + ("B", "CCW"), + ("S", 5.0), + ("B", "CW"), + ], + ) + target = path[-1].end_port + + refined = pf.refiner.refine_path("net", start, width, path) + + assert target == Port(60, 15, 0) + assert sum(1 for comp in path if comp.move_type == "bend90") == 6 + assert sum(1 for comp in refined if comp.move_type == "bend90") == 4 + assert refined[-1].end_port == target + assert pf.refiner.path_cost(refined) < pf.refiner.path_cost(path) + + +def test_refine_path_can_simplify_subpath_with_different_global_orientation() -> None: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(-20, -20, 120, 120)) + danger_map.precompute([]) + evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0) + pf = _build_pathfinder( + evaluator, + netlist={"net": (Port(0, 0, 0), Port(65, 30, 90))}, + net_widths={"net": 2.0}, + bounds=(-20, -20, 120, 120), + bend_radii=[5.0, 10.0], + enabled=True, + ) + + start = Port(0, 0, 0) + width = 2.0 + path = _build_manual_path( + start, + width, + engine.clearance, + [ + ("B", "CCW"), + ("S", 10.0), + ("B", "CW"), + ("S", 20.0), + ("B", "CW"), + ("S", 10.0), + ("B", "CCW"), + ("S", 10.0), + ("B", "CCW"), + ("S", 5.0), + ("B", "CW"), + ("B", "CCW"), + ("S", 10.0), + ], + ) + target = path[-1].end_port + + refined = pf.refiner.refine_path("net", start, width, path) + + assert target == Port(65, 30, 90) + assert sum(1 for comp in path if comp.move_type == "bend90") == 7 + assert sum(1 for comp in refined if comp.move_type == "bend90") == 5 + assert refined[-1].end_port == target + assert pf.refiner.path_cost(refined) < pf.refiner.path_cost(path) diff --git a/inire/tests/test_performance_reporting.py b/inire/tests/test_performance_reporting.py new file mode 100644 index 0000000..9ded29a --- /dev/null +++ b/inire/tests/test_performance_reporting.py @@ -0,0 +1,357 @@ +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.score_component_calls >= 0 + assert snapshot.metrics.danger_map_lookup_calls >= 0 + assert snapshot.metrics.move_cache_abs_misses >= 0 + assert snapshot.metrics.guidance_match_moves >= 0 + assert snapshot.metrics.guidance_match_moves_straight >= 0 + assert snapshot.metrics.guidance_match_moves_bend90 >= 0 + assert snapshot.metrics.guidance_match_moves_sbend >= 0 + assert snapshot.metrics.guidance_bonus_applied >= 0.0 + assert snapshot.metrics.guidance_bonus_applied_straight >= 0.0 + assert snapshot.metrics.guidance_bonus_applied_bend90 >= 0.0 + assert snapshot.metrics.guidance_bonus_applied_sbend >= 0.0 + assert snapshot.metrics.ray_cast_calls >= 0 + assert snapshot.metrics.ray_cast_calls_expand_forward >= 0 + assert snapshot.metrics.dynamic_tree_rebuilds >= 0 + assert snapshot.metrics.visibility_corner_index_builds >= 0 + assert snapshot.metrics.visibility_builds >= 0 + assert snapshot.metrics.congestion_grid_span_cache_hits >= 0 + assert snapshot.metrics.congestion_grid_span_cache_misses >= 0 + assert snapshot.metrics.congestion_candidate_nets >= 0 + assert snapshot.metrics.congestion_net_envelope_cache_hits >= 0 + assert snapshot.metrics.congestion_net_envelope_cache_misses >= 0 + assert snapshot.metrics.congestion_grid_net_cache_hits >= 0 + assert snapshot.metrics.congestion_grid_net_cache_misses >= 0 + assert snapshot.metrics.congestion_lazy_resolutions >= 0 + assert snapshot.metrics.congestion_lazy_requeues >= 0 + assert snapshot.metrics.congestion_candidate_ids >= 0 + assert snapshot.metrics.verify_dynamic_candidate_nets >= 0 + assert snapshot.metrics.refinement_candidates_verified >= 0 + assert snapshot.metrics.pair_local_search_pairs_considered >= 0 + assert snapshot.metrics.pair_local_search_attempts >= 0 + assert snapshot.metrics.pair_local_search_accepts >= 0 + assert snapshot.metrics.pair_local_search_nodes_expanded >= 0 + assert snapshot.metrics.late_phase_capped_nets >= 0 + assert snapshot.metrics.late_phase_capped_fallbacks >= 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() + + +def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + record_script = repo_root / "scripts" / "record_performance_baseline.py" + diff_script = repo_root / "scripts" / "diff_performance_baseline.py" + baseline_dir = tmp_path / "baseline" + baseline_dir.mkdir() + output_path = tmp_path / "diff.md" + + subprocess.run( + [ + sys.executable, + str(record_script), + "--output-dir", + str(baseline_dir), + "--scenario", + "example_01_simple_route", + ], + check=True, + ) + subprocess.run( + [ + sys.executable, + str(diff_script), + "--baseline", + str(baseline_dir / "performance_baseline.json"), + "--include-performance-only", + "--scenario", + "example_01_simple_route", + "--output", + str(output_path), + ], + check=True, + ) + + report = output_path.read_text() + assert "Performance Baseline Diff" in report + assert "example_01_simple_route" in report + + +def test_diff_performance_baseline_script_can_append_measurement_log(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + record_script = repo_root / "scripts" / "record_performance_baseline.py" + diff_script = repo_root / "scripts" / "diff_performance_baseline.py" + baseline_dir = tmp_path / "baseline" + baseline_dir.mkdir() + log_path = tmp_path / "optimization.md" + + subprocess.run( + [ + sys.executable, + str(record_script), + "--output-dir", + str(baseline_dir), + "--scenario", + "example_01_simple_route", + ], + check=True, + ) + subprocess.run( + [ + sys.executable, + str(diff_script), + "--baseline", + str(baseline_dir / "performance_baseline.json"), + "--include-performance-only", + "--scenario", + "example_01_simple_route", + "--metric", + "duration_s", + "--metric", + "valid_results", + "--metric", + "nodes_expanded", + "--metric", + "visibility_corner_index_builds", + "--label", + "Step 0 - Baseline", + "--notes", + "Tooling smoke test.", + "--log", + str(log_path), + ], + check=True, + ) + + report = log_path.read_text() + assert "Step 0 - Baseline" in report + assert "Tooling smoke test." in report + assert "| example_01_simple_route | duration_s |" in report + assert "| example_01_simple_route | valid_results |" in report + assert "| example_01_simple_route | visibility_corner_index_builds |" in report + + +def test_diff_performance_baseline_script_renders_current_metrics_for_added_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + record_script = repo_root / "scripts" / "record_performance_baseline.py" + diff_script = repo_root / "scripts" / "diff_performance_baseline.py" + baseline_dir = tmp_path / "baseline" + baseline_dir.mkdir() + output_path = tmp_path / "diff_added.md" + + subprocess.run( + [ + sys.executable, + str(record_script), + "--output-dir", + str(baseline_dir), + "--scenario", + "example_01_simple_route", + ], + check=True, + ) + subprocess.run( + [ + sys.executable, + str(diff_script), + "--baseline", + str(baseline_dir / "performance_baseline.json"), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + "--metric", + "duration_s", + "--metric", + "nodes_expanded", + "--output", + str(output_path), + ], + check=True, + ) + + report = output_path.read_text() + assert "| example_07_large_scale_routing_no_warm_start | duration_s | - |" in report + assert "| example_07_large_scale_routing_no_warm_start | nodes_expanded | - |" in report + + +def test_record_conflict_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_conflict_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--output-dir", + str(tmp_path), + "--scenario", + "example_05_orientation_stress", + ], + check=True, + ) + + payload = json.loads((tmp_path / "conflict_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_conflict_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_05_orientation_stress"] + assert (tmp_path / "conflict_trace.md").exists() + + +def test_record_conflict_trace_script_supports_performance_only_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_conflict_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--output-dir", + str(tmp_path), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + ], + check=True, + ) + + payload = json.loads((tmp_path / "conflict_trace.json").read_text()) + assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"] + + +def test_record_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_frontier_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--output-dir", + str(tmp_path), + "--scenario", + "example_05_orientation_stress", + ], + check=True, + ) + + payload = json.loads((tmp_path / "frontier_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_frontier_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_05_orientation_stress"] + assert (tmp_path / "frontier_trace.md").exists() + + +def test_record_iteration_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_iteration_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + "--output-dir", + str(tmp_path), + ], + check=True, + ) + + payload = json.loads((tmp_path / "iteration_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_iteration_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"] + assert (tmp_path / "iteration_trace.md").exists() + + +def test_record_pre_pair_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_pre_pair_frontier_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + "--output-dir", + str(tmp_path), + ], + check=True, + ) + + payload = json.loads((tmp_path / "pre_pair_frontier_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_pre_pair_frontier_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"] + assert payload["scenarios"][0]["pre_pair_frontier_trace"] is not None + assert (tmp_path / "pre_pair_frontier_trace.md").exists() + + +def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "characterize_pair_local_search.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--output-dir", + str(tmp_path), + "--num-nets", + "6", + "--seeds", + "41", + "--repeats", + "1", + ], + check=True, + ) + + payload = json.loads((tmp_path / "pair_local_characterization.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/characterize_pair_local_search.py" + assert payload["grid"]["num_nets"] == [6] + assert payload["grid"]["seeds"] == [41] + assert payload["grid"]["repeats"] == 1 + assert len(payload["cases"]) == 1 + assert (tmp_path / "pair_local_characterization.md").exists() diff --git a/inire/tests/test_primitives.py b/inire/tests/test_primitives.py new file mode 100644 index 0000000..7e2bd5b --- /dev/null +++ b/inire/tests/test_primitives.py @@ -0,0 +1,59 @@ +from dataclasses import FrozenInstanceError +from typing import Any + +from hypothesis import given, strategies as st +import pytest + +from inire.geometry.primitives import Port + + +@st.composite +def port_strategy(draw: Any) -> Port: + x = draw(st.floats(min_value=-1e6, max_value=1e6)) + y = draw(st.floats(min_value=-1e6, max_value=1e6)) + orientation = draw(st.sampled_from([0, 90, 180, 270])) + return Port(x, y, orientation) + + +def test_port_snapping() -> None: + p = Port(0.123456, 0.654321, 90) + assert p.x == 0 + assert p.y == 1 + + +@given(p=port_strategy()) +def test_port_transform_invariants(p: Port) -> None: + # Rotating 90 degrees 4 times should return to same orientation + p_rot = p + for _ in range(4): + p_rot = p_rot.rotated(90) + + assert abs(p_rot.x - p.x) < 1e-6 + assert abs(p_rot.y - p.y) < 1e-6 + assert (p_rot.r % 360) == (p.r % 360) + + +@given( + p=port_strategy(), + dx=st.floats(min_value=-1000, max_value=1000), + dy=st.floats(min_value=-1000, max_value=1000), +) +def test_translate_snapping(p: Port, dx: float, dy: float) -> None: + p_trans = p.translate(dx, dy) + assert isinstance(p_trans.x, int) + assert isinstance(p_trans.y, int) + + +def test_orientation_normalization() -> None: + p = Port(0, 0, 360) + assert p.r == 0 + + p2 = Port(0, 0, -90) + assert p2.r == 270 + + +def test_port_is_immutable_value_type() -> None: + p = Port(1, 2, 90) + + with pytest.raises(FrozenInstanceError): + p.x = 3 diff --git a/inire/tests/test_refinements.py b/inire/tests/test_refinements.py new file mode 100644 index 0000000..56ce7a4 --- /dev/null +++ b/inire/tests/test_refinements.py @@ -0,0 +1,97 @@ +from inire import RoutingOptions, RoutingProblem, SearchOptions +from inire.geometry.collision import RoutingWorld +from inire.geometry.components import Bend90 +from inire.geometry.primitives import Port +from inire.model import NetSpec +from inire.router._astar_types import AStarContext +from inire.router._router import PathFinder +from inire.router.cost import CostEvaluator +from inire.router.danger_map import DangerMap + + +def _build_pathfinder( + evaluator: CostEvaluator, + *, + bounds: tuple[float, float, float, float], + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], + search: SearchOptions | None = None, +) -> PathFinder: + nets = tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + return PathFinder( + AStarContext( + evaluator, + RoutingProblem(bounds=bounds, nets=nets), + RoutingOptions(search=SearchOptions() if search is None else search), + ), + ) + + +def test_arc_resolution_sagitta() -> None: + start = Port(0, 0, 0) + # R=10, 90 deg bend. + # High tolerance (0.5um) -> few segments + res_coarse = Bend90.generate(start, radius=10.0, width=2.0, direction="CCW", sagitta=0.5) + # Low tolerance (1nm) -> many segments + res_fine = Bend90.generate(start, radius=10.0, width=2.0, direction="CCW", sagitta=0.001) + + + # Check number of points in the polygon exterior + # (num_segments + 1) * 2 points usually + pts_coarse = len(res_coarse.collision_geometry[0].exterior.coords) + pts_fine = len(res_fine.collision_geometry[0].exterior.coords) + + assert pts_fine > pts_coarse + + +def test_locked_routes() -> None: + engine = RoutingWorld(clearance=2.0) + danger_map = DangerMap(bounds=(0, -50, 100, 50)) + danger_map.precompute([]) + evaluator = CostEvaluator(engine, danger_map) + + # 1. Route Net A + netlist_a = {"netA": (Port(0, 0, 0), Port(50, 0, 0))} + results_a = _build_pathfinder( + evaluator, + bounds=(0, -50, 100, 50), + netlist=netlist_a, + net_widths={"netA": 2.0}, + search=SearchOptions(bend_radii=(5.0, 10.0)), + ).route_all() + assert results_a["netA"].is_valid + + # 2. Treat Net A as locked geometry in the next run. + for polygon in results_a["netA"].locked_geometry: + engine.add_static_obstacle(polygon) + + # 3. Route Net B through the same space. It should detour or fail. + # We'll place Net B's start/target such that it MUST cross Net A's physical path. + netlist_b = {"netB": (Port(0, -5, 0), Port(50, 5, 0))} + + # Route Net B + results_b = _build_pathfinder( + evaluator, + bounds=(0, -50, 100, 50), + netlist=netlist_b, + net_widths={"netB": 2.0}, + search=SearchOptions(bend_radii=(5.0, 10.0)), + ).route_all() + + # Net B should be is_valid (it detoured) or at least not have collisions + # with Net A in the dynamic set (because netA is now static). + # Since netA is static, netB will see it as a HARD collision if it tries to cross. + + # Our A* will find a detour around the static obstacle. + assert results_b["netB"].is_valid + + # Verify geometry doesn't intersect locked netA (physical check) + poly_a = [p.physical_geometry[0] for p in results_a["netA"].path] + poly_b = [p.physical_geometry[0] for p in results_b["netB"].path] + + for pa in poly_a: + for pb in poly_b: + assert not pa.intersects(pb) diff --git a/inire/tests/test_route_behavior.py b/inire/tests/test_route_behavior.py new file mode 100644 index 0000000..8664a12 --- /dev/null +++ b/inire/tests/test_route_behavior.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +from shapely.geometry import Polygon + +from inire import ( + Bend90Seed, + CongestionOptions, + DiagnosticsOptions, + NetSpec, + ObjectiveWeights, + PathSeed, + Port, + RefinementOptions, + RoutingOptions, + RoutingProblem, + RoutingResult, + SearchOptions, + StraightSeed, + route, +) + +DEFAULT_BOUNDS = (0, 0, 100, 100) + +_PROBLEM_FIELDS = set(RoutingProblem.__dataclass_fields__) - {"bounds", "nets", "static_obstacles"} +_SEARCH_FIELDS = set(SearchOptions.__dataclass_fields__) +_CONGESTION_FIELDS = set(CongestionOptions.__dataclass_fields__) +_REFINEMENT_FIELDS = set(RefinementOptions.__dataclass_fields__) +_DIAGNOSTICS_FIELDS = set(DiagnosticsOptions.__dataclass_fields__) +_OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__) + + +def _request_nets( + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], +) -> tuple[NetSpec, ...]: + return tuple( + NetSpec(net_id=net_id, start=start, target=target, width=net_widths.get(net_id, 2.0)) + for net_id, (start, target) in netlist.items() + ) + + +def _build_options(**overrides: object) -> RoutingOptions: + search_overrides = {key: value for key, value in overrides.items() if key in _SEARCH_FIELDS} + congestion_overrides = {key: value for key, value in overrides.items() if key in _CONGESTION_FIELDS} + refinement_overrides = {key: value for key, value in overrides.items() if key in _REFINEMENT_FIELDS} + diagnostics_overrides = {key: value for key, value in overrides.items() if key in _DIAGNOSTICS_FIELDS} + objective_overrides = {key: value for key, value in overrides.items() if key in _OBJECTIVE_FIELDS} + return RoutingOptions( + search=SearchOptions(**search_overrides), + congestion=CongestionOptions(**congestion_overrides), + refinement=RefinementOptions(**refinement_overrides), + diagnostics=DiagnosticsOptions(**diagnostics_overrides), + objective=ObjectiveWeights(**objective_overrides), + ) + + +def _route_problem( + *, + netlist: dict[str, tuple[Port, Port]], + net_widths: dict[str, float], + bounds: tuple[float, float, float, float] = DEFAULT_BOUNDS, + static_obstacles: tuple[Polygon, ...] = (), + iteration_callback=None, + **overrides: object, +): + problem_overrides = {key: value for key, value in overrides.items() if key in _PROBLEM_FIELDS} + option_overrides = {key: value for key, value in overrides.items() if key not in _PROBLEM_FIELDS} + problem = RoutingProblem( + bounds=bounds, + nets=_request_nets(netlist, net_widths), + static_obstacles=static_obstacles, + **problem_overrides, + ) + return route(problem, options=_build_options(**option_overrides), iteration_callback=iteration_callback) + + +def _bend_count(result: RoutingResult) -> int: + return sum(1 for component in result.path if component.move_type == "bend90") + + +def _build_manual_seed(steps: list[tuple[str, float | str]]) -> PathSeed: + segments = [] + for kind, value in steps: + if kind == "B": + segments.append(Bend90Seed(radius=5.0, direction=value)) + else: + segments.append(StraightSeed(length=value)) + return PathSeed(tuple(segments)) + + +def test_route_parallel_nets_are_valid() -> None: + run = _route_problem( + netlist={ + "net1": (Port(0, 0, 0), Port(50, 0, 0)), + "net2": (Port(0, 10, 0), Port(50, 10, 0)), + }, + net_widths={"net1": 2.0, "net2": 2.0}, + ) + + assert len(run.results_by_net) == 2 + assert run.results_by_net["net1"].is_valid + assert run.results_by_net["net2"].is_valid + assert run.results_by_net["net1"].collisions == 0 + assert run.results_by_net["net2"].collisions == 0 + + +def test_route_reports_crossing_nets_without_congestion_resolution() -> None: + run = _route_problem( + netlist={ + "net1": (Port(0, 25, 0), Port(100, 25, 0)), + "net2": (Port(50, 0, 90), Port(50, 50, 90)), + }, + net_widths={"net1": 2.0, "net2": 2.0}, + max_iterations=1, + base_penalty=1.0, + warm_start_enabled=False, + ) + + assert not run.results_by_net["net1"].is_valid + assert not run.results_by_net["net2"].is_valid + assert run.results_by_net["net1"].collisions > 0 + assert run.results_by_net["net2"].collisions > 0 + + +def test_route_callback_respects_requested_net_order() -> None: + callback_orders: list[list[str]] = [] + + _route_problem( + netlist={ + "short": (Port(0, 0, 0), Port(10, 0, 0)), + "long": (Port(0, 0, 0), Port(40, 10, 0)), + "mid": (Port(0, 0, 0), Port(20, 0, 0)), + }, + net_widths={"short": 2.0, "long": 2.0, "mid": 2.0}, + max_iterations=1, + warm_start_enabled=False, + net_order="longest", + enabled=False, + iteration_callback=lambda iteration, results: callback_orders.append(list(results)), + ) + + assert callback_orders == [["long", "mid", "short"]] + + +def test_route_callback_receives_iteration_results() -> None: + callback_results: list[dict[str, RoutingResult]] = [] + + run = _route_problem( + netlist={ + "net1": (Port(0, 0, 0), Port(10, 0, 0)), + "net2": (Port(0, 10, 0), Port(10, 10, 0)), + }, + net_widths={"net1": 2.0, "net2": 2.0}, + iteration_callback=lambda iteration, results: callback_results.append(dict(results)), + ) + + assert len(callback_results) == 1 + assert set(callback_results[0]) == {"net1", "net2"} + assert callback_results[0]["net1"].is_valid + assert callback_results[0]["net2"].is_valid + assert run.results_by_net["net1"].reached_target + assert run.results_by_net["net2"].reached_target + + +def test_route_uses_complete_initial_paths_without_rerouting() -> None: + initial_seed = _build_manual_seed([("S", 10.0), ("B", "CCW"), ("S", 10.0), ("B", "CW")]) + run = _route_problem( + netlist={"net": (Port(0, 0, 0), Port(20, 20, 0))}, + net_widths={"net": 2.0}, + bend_radii=[5.0], + max_iterations=1, + warm_start_enabled=False, + initial_paths={"net": initial_seed}, + enabled=False, + ) + + result = run.results_by_net["net"] + assert result.is_valid + assert result.reached_target + assert result.as_seed() == initial_seed + + +def test_route_retries_partial_initial_paths_across_iterations() -> None: + iterations: list[int] = [] + partial_seed = PathSeed((StraightSeed(length=5.0),)) + run = _route_problem( + netlist={"net": (Port(0, 0, 0), Port(10, 0, 0))}, + net_widths={"net": 2.0}, + max_iterations=2, + warm_start_enabled=False, + capture_expanded=True, + initial_paths={"net": partial_seed}, + enabled=False, + iteration_callback=lambda iteration, results: iterations.append(iteration), + ) + + result = run.results_by_net["net"] + assert iterations == [0, 1] + assert result.is_valid + assert result.reached_target + assert result.outcome == "completed" + assert result.as_seed() != partial_seed + assert run.expanded_nodes + + +def test_route_negotiated_congestion_resolution() -> None: + obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) + obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)]) + run = _route_problem( + bounds=(0, -40, 100, 40), + netlist={ + "net1": (Port(0, 0, 0), Port(50, 0, 0)), + "net2": (Port(0, 10, 0), Port(50, 10, 0)), + }, + net_widths={"net1": 2.0, "net2": 2.0}, + static_obstacles=(obs_top, obs_bottom), + bend_radii=(5.0, 10.0), + max_iterations=10, + base_penalty=1000.0, + ) + + assert run.results_by_net["net1"].reached_target + assert run.results_by_net["net2"].reached_target + assert run.results_by_net["net1"].is_valid + assert run.results_by_net["net2"].is_valid + + +def test_route_refinement_reduces_locked_detour_bends() -> None: + route_a = _route_problem( + bounds=(0, -50, 100, 50), + netlist={"netA": (Port(10, 0, 0), Port(90, 0, 0))}, + net_widths={"netA": 2.0}, + bend_radii=[10.0], + enabled=False, + ) + locked_geometry = route_a.results_by_net["netA"].locked_geometry + + base_run = _route_problem( + bounds=(0, -50, 100, 50), + netlist={"netB": (Port(50, -20, 90), Port(50, 20, 90))}, + net_widths={"netB": 2.0}, + static_obstacles=locked_geometry, + bend_radii=[10.0], + enabled=False, + ) + refined_run = _route_problem( + bounds=(0, -50, 100, 50), + netlist={"netB": (Port(50, -20, 90), Port(50, 20, 90))}, + net_widths={"netB": 2.0}, + static_obstacles=locked_geometry, + bend_radii=[10.0], + enabled=True, + ) + + base_result = base_run.results_by_net["netB"] + refined_result = refined_run.results_by_net["netB"] + assert base_result.is_valid + assert refined_result.is_valid + assert _bend_count(refined_result) < _bend_count(base_result) + + +def test_route_refinement_simplifies_triple_crossing_detours() -> None: + base_run = _route_problem( + bounds=(0, 0, 100, 100), + netlist={ + "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), + "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), + "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), + }, + net_widths={"horizontal": 2.0, "vertical_up": 2.0, "vertical_down": 2.0}, + bend_radii=[10.0], + sbend_radii=[10.0], + base_penalty=1000.0, + enabled=False, + greedy_h_weight=1.5, + bend_penalty=250.0, + sbend_penalty=500.0, + ) + refined_run = _route_problem( + bounds=(0, 0, 100, 100), + netlist={ + "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), + "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), + "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), + }, + net_widths={"horizontal": 2.0, "vertical_up": 2.0, "vertical_down": 2.0}, + bend_radii=[10.0], + sbend_radii=[10.0], + base_penalty=1000.0, + enabled=True, + greedy_h_weight=1.5, + bend_penalty=250.0, + sbend_penalty=500.0, + ) + + for net_id in ("vertical_up", "vertical_down"): + base_result = base_run.results_by_net[net_id] + refined_result = refined_run.results_by_net[net_id] + assert base_result.is_valid + assert refined_result.is_valid + assert _bend_count(refined_result) < _bend_count(base_result) diff --git a/inire/tests/test_variable_grid.py b/inire/tests/test_variable_grid.py new file mode 100644 index 0000000..e0acd71 --- /dev/null +++ b/inire/tests/test_variable_grid.py @@ -0,0 +1,71 @@ +import unittest + +from inire.geometry.primitives import Port +from inire.model import RoutingOptions, RoutingProblem +from inire.router._astar_types import AStarContext, SearchRunConfig +from inire.router._search import route_astar +from inire.router.cost import CostEvaluator +from inire.geometry.collision import RoutingWorld + + +class TestIntegerPorts(unittest.TestCase): + def setUp(self): + self.ce = RoutingWorld(clearance=2.0) + self.cost = CostEvaluator(self.ce) + self.bounds = (0, 0, 100, 100) + + def _build_context(self) -> AStarContext: + return AStarContext( + self.cost, + RoutingProblem(bounds=self.bounds), + RoutingOptions(), + ) + + def _route(self, context: AStarContext, start: Port, target: Port): + return route_astar( + start, + target, + net_width=1.0, + context=context, + config=SearchRunConfig.from_options(context.options), + ) + + def test_route_reaches_integer_target(self): + context = self._build_context() + start = Port(0, 0, 0) + target = Port(12, 0, 0) + + path = self._route(context, start, target) + + self.assertIsNotNone(path) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 12) + self.assertEqual(last_port.y, 0) + self.assertEqual(last_port.r, 0) + + def test_port_constructor_rounds_to_integer_lattice(self): + context = self._build_context() + start = Port(0.0, 0.0, 0.0) + target = Port(12.3, 0.0, 0.0) + + path = self._route(context, start, target) + + self.assertIsNotNone(path) + self.assertEqual(target.x, 12) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 12) + + def test_half_step_inputs_use_integerized_targets(self): + context = self._build_context() + start = Port(0.0, 0.0, 0.0) + target = Port(7.5, 0.0, 0.0) + + path = self._route(context, start, target) + + self.assertIsNotNone(path) + self.assertEqual(target.x, 8) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 8) + +if __name__ == '__main__': + unittest.main() diff --git a/inire/tests/test_visibility.py b/inire/tests/test_visibility.py new file mode 100644 index 0000000..ff20439 --- /dev/null +++ b/inire/tests/test_visibility.py @@ -0,0 +1,129 @@ +from shapely.geometry import box + +from inire.geometry.collision import RoutingWorld +from inire.geometry.primitives import Port +from inire.router._astar_types import AStarMetrics +from inire.router.visibility import VisibilityManager + + +def test_point_visibility_cache_respects_max_distance() -> None: + engine = RoutingWorld(clearance=0.0) + engine.add_static_obstacle(box(10, 20, 20, 30)) + engine.add_static_obstacle(box(100, 20, 110, 30)) + visibility = VisibilityManager(engine) + origin = Port(0, 0, 0) + + near_corners = visibility.get_point_visibility(origin, max_dist=40.0) + far_corners = visibility.get_point_visibility(origin, max_dist=200.0) + + assert len(near_corners) == 3 + assert len(far_corners) > len(near_corners) + assert any(corner[0] >= 100.0 for corner in far_corners) + + +def test_visibility_manager_is_lazy_until_queried() -> None: + engine = RoutingWorld(clearance=0.0) + engine.metrics = AStarMetrics() + engine.add_static_obstacle(box(10, 20, 20, 30)) + + visibility = VisibilityManager(engine) + + assert visibility.corners == [] + assert engine.metrics.total_visibility_corner_index_builds == 0 + assert engine.metrics.total_visibility_builds == 0 + + visibility.ensure_corner_index_current() + + assert visibility.corners + assert engine.metrics.total_visibility_corner_index_builds == 1 + assert engine.metrics.total_visibility_builds == 0 + + +def test_exact_corner_visibility_builds_graph_once_per_static_version() -> None: + engine = RoutingWorld(clearance=0.0) + engine.metrics = AStarMetrics() + engine.add_static_obstacle(box(10, 20, 20, 30)) + visibility = VisibilityManager(engine) + origin = Port(10, 20, 0) + + first = visibility.get_corner_visibility(origin, max_dist=100.0) + second = visibility.get_corner_visibility(origin, max_dist=100.0) + + assert second == first + assert engine.metrics.total_visibility_corner_index_builds == 1 + assert engine.metrics.total_visibility_builds == 1 + + +def test_clear_cache_invalidates_without_rebuilding_and_static_change_rebuilds_lazily() -> None: + engine = RoutingWorld(clearance=0.0) + engine.metrics = AStarMetrics() + engine.add_static_obstacle(box(10, 20, 20, 30)) + visibility = VisibilityManager(engine) + + visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0) + assert engine.metrics.total_visibility_corner_index_builds == 1 + assert engine.metrics.total_visibility_builds == 1 + + visibility.clear_cache() + + assert visibility.corners == [] + assert engine.metrics.total_visibility_corner_index_builds == 1 + assert engine.metrics.total_visibility_builds == 1 + + engine.add_static_obstacle(box(40, 20, 50, 30)) + + visible = visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0) + + assert visible == [] + assert engine.metrics.total_visibility_corner_index_builds == 2 + assert engine.metrics.total_visibility_builds == 2 + + +def test_tangent_corner_candidate_query_matches_bruteforce_filter() -> None: + engine = RoutingWorld(clearance=0.0) + engine.add_static_obstacle(box(10, 20, 20, 30)) + engine.add_static_obstacle(box(-35, -15, -25, -5)) + engine.add_static_obstacle(box(35, -40, 45, -30)) + visibility = VisibilityManager(engine) + radii = (10.0, 20.0) + min_forward = 5.0 + max_forward = 60.0 + tolerance = 2.0 + + for origin in ( + Port(0, 0, 0), + Port(0, 0, 90), + Port(0, 0, 180), + Port(0, 0, 270), + ): + candidate_ids = set( + visibility.get_tangent_corner_candidates( + origin, + min_forward=min_forward, + max_forward=max_forward, + radii=radii, + tolerance=tolerance, + ) + ) + expected_ids: set[int] = set() + if origin.r == 0: + cos_v, sin_v = 1.0, 0.0 + elif origin.r == 90: + cos_v, sin_v = 0.0, 1.0 + elif origin.r == 180: + cos_v, sin_v = -1.0, 0.0 + else: + cos_v, sin_v = 0.0, -1.0 + + for idx, (cx, cy) in enumerate(visibility.corners): + dx = cx - origin.x + dy = cy - origin.y + local_x = dx * cos_v + dy * sin_v + local_y = -dx * sin_v + dy * cos_v + if local_x <= min_forward or local_x > max_forward + 0.01: + continue + nearest_radius = min(radii, key=lambda radius: abs(abs(local_y) - radius)) + if abs(abs(local_y) - nearest_radius) <= tolerance: + expected_ids.add(idx) + + assert candidate_ids == expected_ids diff --git a/inire/tests/test_visualization.py b/inire/tests/test_visualization.py new file mode 100644 index 0000000..eb139ca --- /dev/null +++ b/inire/tests/test_visualization.py @@ -0,0 +1,26 @@ +import matplotlib + +matplotlib.use("Agg") + +from inire.geometry.components import Bend90 +from inire.geometry.primitives import Port +from inire import RoutingResult +from inire.utils.visualization import plot_routing_results + + +def test_plot_routing_results_respects_show_actual() -> None: + bend = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type="bbox") + result = RoutingResult("net", [bend], reached_target=True) + + fig_actual, ax_actual = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=True) + fig_proxy, ax_proxy = plot_routing_results({"net": result}, [], (-5.0, -5.0, 20.0, 20.0), show_actual=False) + + actual_line_points = max(len(line.get_xdata()) for line in ax_actual.lines) + proxy_line_points = max(len(line.get_xdata()) for line in ax_proxy.lines) + + assert actual_line_points > proxy_line_points + assert ax_actual.get_title().endswith("Actual Geometry)") + assert ax_proxy.get_title().endswith("(Proxy Geometry)") + + fig_actual.clf() + fig_proxy.clf() diff --git a/inire/utils/visualization.py b/inire/utils/visualization.py new file mode 100644 index 0000000..d828d1e --- /dev/null +++ b/inire/utils/visualization.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast +import matplotlib.pyplot as plt +import numpy +from shapely.geometry import MultiPolygon, Polygon + +if TYPE_CHECKING: + from matplotlib.axes import Axes + from matplotlib.figure import Figure + + from inire.geometry.primitives import Port + from inire.router.danger_map import DangerMap + from inire.results import RoutingResult + + +def plot_routing_results( + results: dict[str, RoutingResult], + static_obstacles: list[Polygon], + bounds: tuple[float, float, float, float], + netlist: dict[str, tuple[Port, Port]] | None = None, + show_actual: bool = True, +) -> tuple[Figure, Axes]: + """ + Plot obstacles and routed paths using matplotlib. + + Args: + results: Dictionary of net_id to RoutingResult. + static_obstacles: List of static obstacle polygons. + bounds: Plot limits (minx, miny, maxx, maxy). + netlist: Optional original netlist for port visualization. + show_actual: If True, overlay high-fidelity geometry if available. + + Returns: + The matplotlib Figure and Axes objects. + """ + fig, ax = plt.subplots(figsize=(12, 12)) + + # Plot static obstacles (gray) + for poly in static_obstacles: + x, y = poly.exterior.xy + ax.fill(x, y, alpha=0.3, fc="gray", ec="black", zorder=1) + + # Plot paths + colors = plt.get_cmap("tab20") + for i, (net_id, res) in enumerate(results.items()): + color: str | tuple[float, ...] = colors(i % 20) + if not res.is_valid: + color = "red" + + label_added = False + for comp in res.path: + # 1. Plot Collision Geometry (Translucent fill) + for poly in comp.collision_geometry: + if isinstance(poly, MultiPolygon): + geoms = list(poly.geoms) + else: + geoms = [poly] + + for g in geoms: + if hasattr(g, "exterior"): + x, y = g.exterior.xy + ax.fill(x, y, alpha=0.15, fc=color, ec=color, linestyle='--', lw=0.5, zorder=2) + else: + # Fallback for LineString or other geometries + x, y = g.xy + ax.plot(x, y, color=color, alpha=0.15, linestyle='--', lw=0.5, zorder=2) + + actual_geoms_to_plot = comp.physical_geometry if show_actual else comp.collision_geometry + + for poly in actual_geoms_to_plot: + if isinstance(poly, MultiPolygon): + geoms = list(poly.geoms) + else: + geoms = [poly] + for g in geoms: + if hasattr(g, "exterior"): + x, y = g.exterior.xy + ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "") + else: + x, y = g.xy + ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "") + label_added = True + + # 3. Plot subtle port orientation arrow + p = comp.end_port + rad = numpy.radians(p.r) + ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black", + scale=40, width=0.002, alpha=0.2, pivot="tail", zorder=4) + + if not res.path and not res.is_valid: + # Empty failed paths are typically unroutable. + pass + + # 4. Plot main arrows for netlist ports + if netlist: + for _net_id, (start_p, target_p) in netlist.items(): + for p in [start_p, target_p]: + rad = numpy.radians(p.r) + ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black", + scale=25, width=0.004, pivot="tail", zorder=6) + + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + ax.set_aspect("equal") + if show_actual: + ax.set_title("Inire Routing Results (Dashed: Collision Proxy, Solid: Actual Geometry)") + else: + ax.set_title("Inire Routing Results (Proxy Geometry)") + + # Legend handling for many nets + if len(results) < 25: + handles, labels = ax.get_legend_handles_labels() + if labels: + ax.legend(loc='upper left', bbox_to_anchor=(1, 1), fontsize='small', ncol=2) + fig.tight_layout() + + plt.grid(True, which='both', linestyle=':', alpha=0.5) + return fig, ax + +def plot_danger_map( + danger_map: DangerMap, + ax: Axes | None = None, + resolution: float | None = None +) -> tuple[Figure, Axes]: + """ + Plot the pre-computed danger map as a heatmap. + """ + if ax is None: + fig, ax = plt.subplots(figsize=(10, 10)) + else: + fig = cast("Figure", ax.get_figure()) + + # Generate a temporary grid for visualization + res = resolution if resolution is not None else max(1.0, (danger_map.maxx - danger_map.minx) / 200.0) + x_coords = numpy.arange(danger_map.minx + res/2, danger_map.maxx, res) + y_coords = numpy.arange(danger_map.miny + res/2, danger_map.maxy, res) + xv, yv = numpy.meshgrid(x_coords, y_coords, indexing='ij') + + if danger_map.tree is not None: + points = numpy.stack([xv.ravel(), yv.ravel()], axis=1) + dists, _ = danger_map.tree.query(points, distance_upper_bound=danger_map.safety_threshold) + + # Apply cost function + safe_dists = numpy.maximum(dists, 0.1) + grid_flat = numpy.where( + dists < danger_map.safety_threshold, + danger_map.k / (safe_dists**2), + 0.0 + ) + grid = grid_flat.reshape(xv.shape) + else: + grid = numpy.zeros(xv.shape) + + # Need to transpose because grid is [x, y] and imshow expects [row, col] (y, x) + im = ax.imshow( + grid.T, + origin="lower", + extent=(danger_map.minx, danger_map.maxx, danger_map.miny, danger_map.maxy), + cmap="YlOrRd", + alpha=0.6, + ) + plt.colorbar(im, ax=ax, label="Danger Cost") + ax.set_title("Danger Map (Proximity Costs)") + return fig, ax + +def plot_expanded_nodes( + nodes: list[tuple[float, float, float]], + ax: Axes | None = None, + color: str = 'gray', + alpha: float = 0.3, +) -> tuple[Figure, Axes]: + """ + Plot A* expanded nodes for debugging. + """ + if ax is None: + fig, ax = plt.subplots(figsize=(10, 10)) + else: + fig = cast("Figure", ax.get_figure()) + + if not nodes: + return fig, ax + + x, y, _ = zip(*nodes, strict=False) + ax.scatter(x, y, s=1, c=color, alpha=alpha, zorder=0) + return fig, ax + +def plot_expansion_density( + nodes: list[tuple[float, float, float]], + bounds: tuple[float, float, float, float], + ax: Axes | None = None, + bins: int | tuple[int, int] = 50, +) -> tuple[Figure, Axes]: + """ + Plot a density heatmap (2D histogram) of expanded nodes. + + Args: + nodes: List of (x, y, orientation) tuples. + bounds: (minx, miny, maxx, maxy) for the plot range. + ax: Optional existing axes to plot on. + bins: Number of bins for the histogram (int or (nx, ny)). + + Returns: + Figure and Axes objects. + """ + if ax is None: + fig, ax = plt.subplots(figsize=(12, 12)) + else: + fig = cast("Figure", ax.get_figure()) + + if not nodes: + ax.text(0.5, 0.5, "No Expansion Data", ha='center', va='center', transform=ax.transAxes) + return fig, ax + + x, y, _ = zip(*nodes, strict=False) + + # Create 2D histogram + h, xedges, yedges = numpy.histogram2d( + x, y, + bins=bins, + range=[[bounds[0], bounds[2]], [bounds[1], bounds[3]]] + ) + + # Plot as image + im = ax.imshow( + h.T, + origin="lower", + extent=(bounds[0], bounds[2], bounds[1], bounds[3]), + cmap="plasma", + interpolation="nearest", + alpha=0.7, + ) + + plt.colorbar(im, ax=ax, label="Expansion Count") + ax.set_title("Search Expansion Density") + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig, ax diff --git a/pyproject.toml b/pyproject.toml index 869b0a4..e4031c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "inire" -description = "Wave-router" +description = "Wave-router: Auto-routing for photonic and RF integrated circuits" readme = "README.md" requires-python = ">=3.11" license = { file = "LICENSE.md" } @@ -9,22 +9,6 @@ authors = [ ] homepage = "https://mpxd.net/code/jan/inire" repository = "https://mpxd.net/code/jan/inire" -keywords = [ - "layout", - "CAD", - "EDA", - "mask", - "pattern", - "lithography", - "oas", - "gds", - "dxf", - "svg", - "OASIS", - "gdsii", - "gds2", - "stream", - ] classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", @@ -36,10 +20,17 @@ classifiers = [ "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", ] dynamic = ["version"] -dependencies = [] +dependencies = [ + "numpy", + "scipy", + "shapely", + "rtree", + "matplotlib", +] [dependency-groups] dev = [ + "hypothesis>=6.151.9", "matplotlib>=3.10.8", "pytest>=9.0.2", "ruff>=0.15.5", @@ -83,7 +74,21 @@ lint.ignore = [ "TRY003", # Long exception message ] +[tool.ruff.lint.per-file-ignores] +"inire/tests/*.py" = ["ANN", "ARG005", "PT009"] + +[tool.mypy] +python_version = "3.11" +warn_unused_configs = true +exclude = ["^examples/", "^inire/tests/"] + +[[tool.mypy.overrides]] +module = ["scipy.*"] +ignore_missing_imports = true + [tool.pytest.ini_options] addopts = "-rsXx" testpaths = ["inire"] - +markers = [ + "performance: opt-in runtime regression checks against example-like routing scenarios", +] diff --git a/scripts/characterize_pair_local_search.py b/scripts/characterize_pair_local_search.py new file mode 100644 index 0000000..5b575e3 --- /dev/null +++ b/scripts/characterize_pair_local_search.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict +from datetime import datetime +from pathlib import Path +from time import perf_counter + +from inire.tests.example_scenarios import _run_example_07_variant + + +def _parse_csv_ints(raw: str) -> tuple[int, ...]: + return tuple(int(part) for part in raw.split(",") if part.strip()) + + +def _run_case(num_nets: int, seed: int) -> dict[str, object]: + t0 = perf_counter() + run = _run_example_07_variant( + num_nets=num_nets, + seed=seed, + warm_start_enabled=False, + ) + duration_s = perf_counter() - t0 + return { + "duration_s": duration_s, + "summary": { + "total_results": len(run.results_by_net), + "valid_results": sum(1 for result in run.results_by_net.values() if result.is_valid), + "reached_targets": sum(1 for result in run.results_by_net.values() if result.reached_target), + }, + "metrics": asdict(run.metrics), + } + + +def _is_smoke_candidate(entry: dict[str, object]) -> bool: + summary = entry["summary"] + metrics = entry["metrics"] + return ( + summary["valid_results"] == summary["total_results"] + and metrics["pair_local_search_accepts"] >= 1 + and entry["duration_s"] <= 1.0 + ) + + +def _select_smoke_case(cases: list[dict[str, object]]) -> dict[str, object] | None: + grouped: dict[tuple[int, int], list[dict[str, object]]] = {} + for case in cases: + key = (case["num_nets"], case["seed"]) + grouped.setdefault(key, []).append(case) + + candidates = [] + for (num_nets, seed), repeats in grouped.items(): + if repeats and all(_is_smoke_candidate(repeat) for repeat in repeats): + candidates.append({"num_nets": num_nets, "seed": seed}) + if not candidates: + return None + candidates.sort(key=lambda item: (item["num_nets"], item["seed"])) + return candidates[0] + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Pair-Local Search Characterization", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + f"Grid: `num_nets={payload['grid']['num_nets']}`, `seed={payload['grid']['seeds']}`, repeats={payload['grid']['repeats']}.", + "", + "| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks |", + "| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |", + ] + for case in payload["cases"]: + summary = case["summary"] + metrics = case["metrics"] + lines.append( + "| " + f"{case['num_nets']} | " + f"{case['seed']} | " + f"{case['repeat']} | " + f"{case['duration_s']:.4f} | " + f"{summary['valid_results']} | " + f"{summary['reached_targets']} | " + f"{metrics['pair_local_search_pairs_considered']} | " + f"{metrics['pair_local_search_accepts']} | " + f"{metrics['pair_local_search_nodes_expanded']} | " + f"{metrics['nodes_expanded']} | " + f"{metrics['congestion_check_calls']} |" + ) + + lines.extend(["", "## Recommendation", ""]) + recommended = payload["recommended_smoke_scenario"] + if recommended is None: + lines.append( + "No smaller stable pair-local smoke scenario satisfied the rule " + "`valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats." + ) + else: + lines.append( + f"Recommended smoke scenario: `num_nets={recommended['num_nets']}`, `seed={recommended['seed']}`." + ) + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Characterize pair-local search across example_07-style no-warm runs.") + parser.add_argument( + "--num-nets", + default="6,8,10", + help="Comma-separated num_nets values to sweep. Default: 6,8,10.", + ) + parser.add_argument( + "--seeds", + default="41,42,43", + help="Comma-separated seed values to sweep. Default: 41,42,43.", + ) + parser.add_argument( + "--repeats", + type=int, + default=2, + help="Number of repeated runs per (num_nets, seed). Default: 2.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write pair_local_characterization.json and .md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + num_nets_values = _parse_csv_ints(args.num_nets) + seed_values = _parse_csv_ints(args.seeds) + + cases: list[dict[str, object]] = [] + for num_nets in num_nets_values: + for seed in seed_values: + for repeat in range(args.repeats): + case = _run_case(num_nets, seed) + case["num_nets"] = num_nets + case["seed"] = seed + case["repeat"] = repeat + cases.append(case) + + payload = { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/characterize_pair_local_search.py", + "grid": { + "num_nets": list(num_nets_values), + "seeds": list(seed_values), + "repeats": args.repeats, + }, + "cases": cases, + "recommended_smoke_scenario": _select_smoke_case(cases), + } + + json_path = output_dir / "pair_local_characterization.json" + markdown_path = output_dir / "pair_local_characterization.md" + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + 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() diff --git a/scripts/diff_performance_baseline.py b/scripts/diff_performance_baseline.py new file mode 100644 index 0000000..352022b --- /dev/null +++ b/scripts/diff_performance_baseline.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS +from inire.results import RouteMetrics + + +SUMMARY_KEYS = ( + "duration_s", + "valid_results", + "reached_targets", + "route_iterations", + "nets_routed", + "nodes_expanded", + "ray_cast_calls", + "moves_generated", + "moves_added", + "congestion_check_calls", + "verify_path_report_calls", +) + + +def _snapshot_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if not include_performance_only: + return SCENARIO_SNAPSHOTS + return SCENARIO_SNAPSHOTS + PERFORMANCE_SCENARIO_SNAPSHOTS + + +def _available_metric_names() -> tuple[str, ...]: + return ( + "duration_s", + "total_results", + "valid_results", + "reached_targets", + *RouteMetrics.__dataclass_fields__.keys(), + ) + + +def _current_snapshots( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, dict[str, object]]: + allowed = None if selected_scenarios is None else set(selected_scenarios) + snapshots: dict[str, dict[str, object]] = {} + for name, run in _snapshot_registry(include_performance_only): + if allowed is not None and name not in allowed: + continue + snapshots[name] = asdict(run()) + return snapshots + + +def _load_baseline(path: Path, selected_scenarios: tuple[str, ...] | None) -> dict[str, dict[str, object]]: + payload = json.loads(path.read_text()) + allowed = None if selected_scenarios is None else set(selected_scenarios) + return { + entry["name"]: entry + for entry in payload["scenarios"] + if allowed is None or entry["name"] in allowed + } + + +def _metric_value(snapshot: dict[str, object], key: str) -> float | None: + if key in {"duration_s", "total_results", "valid_results", "reached_targets"}: + return float(snapshot[key]) + if key not in snapshot["metrics"]: + return None + return float(snapshot["metrics"][key]) + + +def _validate_metrics(metric_names: tuple[str, ...]) -> None: + valid_names = set(_available_metric_names()) + unknown = [name for name in metric_names if name not in valid_names] + if unknown: + raise SystemExit( + f"Unknown metric name(s): {', '.join(sorted(unknown))}. " + f"Valid names are: {', '.join(_available_metric_names())}" + ) + + +def _render_report( + baseline: dict[str, dict[str, object]], + current: dict[str, dict[str, object]], + metric_names: tuple[str, ...], +) -> str: + scenario_names = sorted(set(baseline) | set(current)) + lines = [ + "# Performance Baseline Diff", + "", + "| Scenario | Metric | Baseline | Current | Delta |", + "| :-- | :-- | --: | --: | --: |", + ] + for scenario in scenario_names: + base_snapshot = baseline.get(scenario) + curr_snapshot = current.get(scenario) + if base_snapshot is None: + if curr_snapshot is None: + lines.append(f"| {scenario} | added | - | - | - |") + continue + for key in metric_names: + curr_value = _metric_value(curr_snapshot, key) + if curr_value is None: + lines.append(f"| {scenario} | {key} | - | - | - |") + continue + lines.append(f"| {scenario} | {key} | - | {curr_value:.4f} | - |") + continue + if curr_snapshot is None: + lines.append(f"| {scenario} | missing | - | - | - |") + continue + for key in metric_names: + base_value = _metric_value(base_snapshot, key) + curr_value = _metric_value(curr_snapshot, key) + if base_value is None: + lines.append( + f"| {scenario} | {key} | - | {curr_value:.4f} | - |" + ) + continue + if curr_value is None: + lines.append( + f"| {scenario} | {key} | {base_value:.4f} | - | - |" + ) + continue + lines.append( + f"| {scenario} | {key} | {base_value:.4f} | {curr_value:.4f} | {curr_value - base_value:+.4f} |" + ) + return "\n".join(lines) + "\n" + + +def _render_log_entry( + *, + baseline_path: Path, + label: str, + notes: tuple[str, ...], + report: str, +) -> str: + lines = [ + f"## {label}", + "", + f"Measured on {datetime.now().astimezone().isoformat(timespec='seconds')}.", + f"Baseline: `{baseline_path}`.", + "", + ] + if notes: + lines.extend(["Findings:", ""]) + lines.extend(f"- {note}" for note in notes) + lines.append("") + lines.append(report.rstrip()) + lines.append("") + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Diff the committed performance baseline against a fresh run.") + parser.add_argument( + "--baseline", + type=Path, + default=Path("docs/performance_baseline.json"), + help="Baseline JSON to compare against.", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Optional file to write the report to. Defaults to stdout.", + ) + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--metric", + action="append", + dest="metrics", + default=[], + help="Optional metric to include. May be passed more than once. Defaults to the summary metric set.", + ) + parser.add_argument( + "--label", + default="Measurement", + help="Section label to use when appending to a log file.", + ) + parser.add_argument( + "--notes", + action="append", + dest="notes", + default=[], + help="Optional short finding to append under the measurement section. May be passed more than once.", + ) + parser.add_argument( + "--log", + type=Path, + default=None, + help="Optional Markdown log file to append the rendered report to.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only snapshot scenarios that are excluded from the default baseline corpus.", + ) + args = parser.parse_args() + + selected = tuple(args.scenarios) if args.scenarios else None + metrics = tuple(args.metrics) if args.metrics else SUMMARY_KEYS + _validate_metrics(metrics) + baseline = _load_baseline(args.baseline, selected) + current = _current_snapshots(selected, include_performance_only=args.include_performance_only) + report = _render_report(baseline, current, metrics) + + if args.output is not None: + args.output.write_text(report) + print(f"Wrote {args.output}") + elif args.log is None: + print(report, end="") + + if args.log is not None: + entry = _render_log_entry( + baseline_path=args.baseline, + label=args.label, + notes=tuple(args.notes), + report=report, + ) + with args.log.open("a", encoding="utf-8") as handle: + handle.write(entry) + print(f"Appended {args.log}") + + +if __name__ == "__main__": + main() diff --git a/scripts/record_conflict_trace.py b/scripts/record_conflict_trace.py new file mode 100644 index 0000000..2baaf9c --- /dev/null +++ b/scripts/record_conflict_trace.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.results import RoutingRunResult +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + return (("example_07_large_scale_routing_no_warm_start", dict(TRACE_PERFORMANCE_SCENARIO_RUNS)["example_07_large_scale_routing_no_warm_start"]),) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _result_summary(run: RoutingRunResult) -> dict[str, object]: + return { + "total_results": len(run.results_by_net), + "valid_results": sum(1 for result in run.results_by_net.values() if result.is_valid), + "reached_targets": sum(1 for result in run.results_by_net.values() if result.reached_target), + "results_by_net": { + net_id: { + "outcome": result.outcome, + "reached_target": result.reached_target, + "report": asdict(result.report), + } + for net_id, result in run.results_by_net.items() + }, + } + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": _result_summary(result), + "metrics": asdict(result.metrics), + "conflict_trace": [asdict(entry) for entry in result.conflict_trace], + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_conflict_trace.py", + "scenarios": scenarios, + } + + +def _count_stage_nets(entry: dict[str, object]) -> int: + return sum( + 1 + for net in entry["nets"] + if net["report"]["dynamic_collision_count"] > 0 + ) + + +def _canonical_component_pair( + net_id: str, + self_component_index: int, + other_net_id: str, + other_component_index: int, +) -> tuple[tuple[str, int], tuple[str, int]]: + left = (net_id, self_component_index) + right = (other_net_id, other_component_index) + if left <= right: + return (left, right) + return (right, left) + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Conflict Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + "| Stage | Iteration | Conflicting Nets | Conflict Edges | Completed Nets |", + "| :-- | --: | --: | --: | --: |", + ] + ) + + net_stage_counts: Counter[str] = Counter() + edge_counts: Counter[tuple[str, str]] = Counter() + component_pair_counts: Counter[tuple[tuple[str, int], tuple[str, int]]] = Counter() + trace_entries = scenario["conflict_trace"] + for entry in trace_entries: + lines.append( + "| " + f"{entry['stage']} | " + f"{'' if entry['iteration'] is None else entry['iteration']} | " + f"{_count_stage_nets(entry)} | " + f"{len(entry['conflict_edges'])} | " + f"{len(entry['completed_net_ids'])} |" + ) + seen_component_pairs: set[tuple[tuple[str, int], tuple[str, int]]] = set() + for edge in entry["conflict_edges"]: + edge_counts[tuple(edge)] += 1 + for net in entry["nets"]: + if net["report"]["dynamic_collision_count"] > 0: + net_stage_counts[net["net_id"]] += 1 + for component_conflict in net["component_conflicts"]: + pair = _canonical_component_pair( + net["net_id"], + component_conflict["self_component_index"], + component_conflict["other_net_id"], + component_conflict["other_component_index"], + ) + seen_component_pairs.add(pair) + for pair in seen_component_pairs: + component_pair_counts[pair] += 1 + + lines.extend(["", "Top nets by traced dynamic-collision stages:", ""]) + if net_stage_counts: + for net_id, count in net_stage_counts.most_common(10): + lines.append(f"- `{net_id}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Top net pairs by frequency:", ""]) + if edge_counts: + for (left, right), count in edge_counts.most_common(10): + lines.append(f"- `{left}` <-> `{right}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Top component pairs by frequency:", ""]) + if component_pair_counts: + for pair, count in component_pair_counts.most_common(10): + (left_net, left_index), (right_net, right_index) = pair + lines.append(f"- `{left_net}[{left_index}]` <-> `{right_net}[{right_index}]`: {count}") + else: + lines.append("- None") + + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record conflict-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write conflict_trace.json and conflict_trace.md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "conflict_trace.json" + markdown_path = output_dir / "conflict_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + 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() diff --git a/scripts/record_frontier_trace.py b/scripts/record_frontier_trace.py new file mode 100644 index 0000000..e101c90 --- /dev/null +++ b/scripts/record_frontier_trace.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + default_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS) + return (("example_07_large_scale_routing_no_warm_start", default_registry["example_07_large_scale_routing_no_warm_start"]),) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": { + "total_results": len(result.results_by_net), + "valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid), + "reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target), + }, + "metrics": asdict(result.metrics), + "frontier_trace": [asdict(entry) for entry in result.frontier_trace], + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_frontier_trace.py", + "scenarios": scenarios, + } + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Frontier Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + "| Net | Hotspots | Closed-Set | Hard Collision | Self Collision | Cost | Samples |", + "| :-- | --: | --: | --: | --: | --: | --: |", + ] + ) + + reason_counts: Counter[str] = Counter() + hotspot_counts: Counter[tuple[str, int]] = Counter() + for net_trace in scenario["frontier_trace"]: + sample_count = len(net_trace["samples"]) + lines.append( + "| " + f"{net_trace['net_id']} | " + f"{len(net_trace['hotspot_bounds'])} | " + f"{net_trace['pruned_closed_set']} | " + f"{net_trace['pruned_hard_collision']} | " + f"{net_trace['pruned_self_collision']} | " + f"{net_trace['pruned_cost']} | " + f"{sample_count} |" + ) + reason_counts["closed_set"] += net_trace["pruned_closed_set"] + reason_counts["hard_collision"] += net_trace["pruned_hard_collision"] + reason_counts["self_collision"] += net_trace["pruned_self_collision"] + reason_counts["cost"] += net_trace["pruned_cost"] + for sample in net_trace["samples"]: + hotspot_counts[(net_trace["net_id"], sample["hotspot_index"])] += 1 + + lines.extend(["", "Prune totals by reason:", ""]) + if reason_counts: + for reason, count in reason_counts.most_common(): + lines.append(f"- `{reason}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Top traced hotspots by sample count:", ""]) + if hotspot_counts: + for (net_id, hotspot_index), count in hotspot_counts.most_common(10): + lines.append(f"- `{net_id}` hotspot `{hotspot_index}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Per-net sampled reason/move breakdown:", ""]) + if scenario["frontier_trace"]: + for net_trace in scenario["frontier_trace"]: + reason_move_counts: Counter[tuple[str, str]] = Counter() + hotspot_sample_counts: Counter[int] = Counter() + for sample in net_trace["samples"]: + reason_move_counts[(sample["reason"], sample["move_type"])] += 1 + hotspot_sample_counts[sample["hotspot_index"]] += 1 + lines.append(f"- `{net_trace['net_id']}`") + if reason_move_counts: + top_pairs = ", ".join( + f"`{reason}` x `{move}` = {count}" + for (reason, move), count in reason_move_counts.most_common(3) + ) + lines.append(f" sampled reasons: {top_pairs}") + else: + lines.append(" sampled reasons: none") + if hotspot_sample_counts: + top_hotspots = ", ".join( + f"`{hotspot}` = {count}" for hotspot, count in hotspot_sample_counts.most_common(3) + ) + lines.append(f" hotspot samples: {top_hotspots}") + else: + lines.append(" hotspot samples: none") + else: + lines.append("- None") + + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record frontier-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write frontier_trace.json and frontier_trace.md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "frontier_trace.json" + markdown_path = output_dir / "frontier_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + 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() diff --git a/scripts/record_iteration_trace.py b/scripts/record_iteration_trace.py new file mode 100644 index 0000000..7691558 --- /dev/null +++ b/scripts/record_iteration_trace.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS) + return ( + ( + "example_07_large_scale_routing_no_warm_start", + perf_registry["example_07_large_scale_routing_no_warm_start"], + ), + ( + "example_07_large_scale_routing_no_warm_start_seed43", + perf_registry["example_07_large_scale_routing_no_warm_start_seed43"], + ), + ) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown iteration-trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": { + "total_results": len(result.results_by_net), + "valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid), + "reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target), + }, + "metrics": asdict(result.metrics), + "iteration_trace": [asdict(entry) for entry in result.iteration_trace], + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_iteration_trace.py", + "scenarios": scenarios, + } + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Iteration Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + "| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs |", + "| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |", + ] + ) + + net_node_counts: Counter[str] = Counter() + net_check_counts: Counter[str] = Counter() + for entry in scenario["iteration_trace"]: + lines.append( + "| " + f"{entry['iteration']} | " + f"{entry['congestion_penalty']:.1f} | " + f"{len(entry['routed_net_ids'])} | " + f"{entry['completed_nets']} | " + f"{entry['conflict_edges']} | " + f"{entry['total_dynamic_collisions']} | " + f"{entry['nodes_expanded']} | " + f"{entry['congestion_check_calls']} | " + f"{entry['congestion_candidate_ids']} | " + f"{entry['congestion_exact_pair_checks']} |" + ) + for attempt in entry["net_attempts"]: + net_node_counts[attempt["net_id"]] += attempt["nodes_expanded"] + net_check_counts[attempt["net_id"]] += attempt["congestion_check_calls"] + + lines.extend(["", "Top nets by iteration-attributed nodes expanded:", ""]) + if net_node_counts: + for net_id, count in net_node_counts.most_common(10): + lines.append(f"- `{net_id}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Top nets by iteration-attributed congestion checks:", ""]) + if net_check_counts: + for net_id, count in net_check_counts.most_common(10): + lines.append(f"- `{net_id}`: {count}") + else: + lines.append("- None") + + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record iteration-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write iteration_trace.json and iteration_trace.md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "iteration_trace.json" + markdown_path = output_dir / "iteration_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + 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() diff --git a/scripts/record_performance_baseline.py b/scripts/record_performance_baseline.py new file mode 100644 index 0000000..14944bb --- /dev/null +++ b/scripts/record_performance_baseline.py @@ -0,0 +1,146 @@ +#!/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 PERFORMANCE_SCENARIO_SNAPSHOTS, 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 _snapshot_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if not include_performance_only: + return SCENARIO_SNAPSHOTS + return SCENARIO_SNAPSHOTS + PERFORMANCE_SCENARIO_SNAPSHOTS + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None = None, + *, + include_performance_only: bool = False, +) -> dict[str, object]: + allowed = None if selected_scenarios is None else set(selected_scenarios) + snapshots = [] + for name, run in _snapshot_registry(include_performance_only): + 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`.", + "Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.", + "", + "| 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.", + "These counters are currently observational only and are not enforced as CI regression gates.", + "", + "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 /docs.", + ) + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only snapshot scenarios that are excluded from the default baseline corpus.", + ) + 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, include_performance_only=args.include_performance_only) + 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() diff --git a/scripts/record_pre_pair_frontier_trace.py b/scripts/record_pre_pair_frontier_trace.py new file mode 100644 index 0000000..398fc0d --- /dev/null +++ b/scripts/record_pre_pair_frontier_trace.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS) + return ( + ( + "example_07_large_scale_routing_no_warm_start", + perf_registry["example_07_large_scale_routing_no_warm_start"], + ), + ( + "example_07_large_scale_routing_no_warm_start_seed43", + perf_registry["example_07_large_scale_routing_no_warm_start_seed43"], + ), + ) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown pre-pair frontier-trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": { + "total_results": len(result.results_by_net), + "valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid), + "reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target), + }, + "metrics": asdict(result.metrics), + "pre_pair_frontier_trace": None + if result.pre_pair_frontier_trace is None + else asdict(result.pre_pair_frontier_trace), + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_pre_pair_frontier_trace.py", + "scenarios": scenarios, + } + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Pre-Pair Frontier Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + ] + ) + trace = scenario["pre_pair_frontier_trace"] + if trace is None: + lines.extend(["No pre-pair frontier trace captured.", ""]) + continue + + lines.extend( + [ + f"Captured iteration: `{trace['iteration']}`", + "", + f"Conflict edges: `{trace['conflict_edges']}`", + "", + "| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |", + "| :-- | --: | --: | --: | --: | --: | :--: | --: |", + ] + ) + reason_counts: Counter[str] = Counter() + for net_trace in trace["nets"]: + frontier = net_trace["frontier"] + lines.append( + "| " + f"{net_trace['net_id']} | " + f"{net_trace['nodes_expanded']} | " + f"{net_trace['congestion_check_calls']} | " + f"{net_trace['pruned_closed_set']} | " + f"{net_trace['pruned_cost']} | " + f"{net_trace['pruned_hard_collision']} | " + f"{'yes' if net_trace['guidance_seed_present'] else 'no'} | " + f"{len(frontier['samples'])} |" + ) + reason_counts["closed_set"] += frontier["pruned_closed_set"] + reason_counts["hard_collision"] += frontier["pruned_hard_collision"] + reason_counts["self_collision"] += frontier["pruned_self_collision"] + reason_counts["cost"] += frontier["pruned_cost"] + + lines.extend(["", "Frontier prune totals by reason:", ""]) + if reason_counts: + for reason, count in reason_counts.most_common(): + lines.append(f"- `{reason}`: {count}") + else: + lines.append("- None") + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record pre-pair frontier-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write pre_pair_frontier_trace.json and .md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "pre_pair_frontier_trace.json" + markdown_path = output_dir / "pre_pair_frontier_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + 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() diff --git a/uv.lock b/uv.lock index d2a15c4..ca0ac7d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.13" +requires-python = ">=3.11" [[package]] name = "colorama" @@ -19,6 +19,28 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, @@ -63,6 +85,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, ] [[package]] @@ -80,6 +107,22 @@ version = "4.61.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756 } wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213 }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689 }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809 }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039 }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714 }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648 }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681 }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951 }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593 }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231 }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103 }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295 }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109 }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598 }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060 }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078 }, { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454 }, { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191 }, { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410 }, @@ -107,6 +150,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996 }, ] +[[package]] +name = "hypothesis" +version = "6.151.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/e1/ef365ff480903b929d28e057f57b76cae51a30375943e33374ec9a165d9c/hypothesis-6.151.9.tar.gz", hash = "sha256:2f284428dda6c3c48c580de0e18470ff9c7f5ef628a647ee8002f38c3f9097ca", size = 463534 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/f7/5cc291d701094754a1d327b44d80a44971e13962881d9a400235726171da/hypothesis-6.151.9-py3-none-any.whl", hash = "sha256:7b7220585c67759b1b1ef839b1e6e9e3d82ed468cfc1ece43c67184848d7edd9", size = 529307 }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -118,20 +173,35 @@ wheels = [ [[package]] name = "inire" -version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "rtree" }, + { name = "scipy" }, + { name = "shapely" }, +] [package.dev-dependencies] dev = [ + { name = "hypothesis" }, { name = "matplotlib" }, { name = "pytest" }, { name = "ruff" }, ] [package.metadata] +requires-dist = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "rtree" }, + { name = "scipy" }, + { name = "shapely" }, +] [package.metadata.requires-dev] dev = [ + { name = "hypothesis", specifier = ">=6.151.9" }, { name = "matplotlib", specifier = ">=3.10.8" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "ruff", specifier = ">=0.15.5" }, @@ -143,6 +213,32 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 } wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167 }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579 }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309 }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596 }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548 }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618 }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437 }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810 }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579 }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071 }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840 }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159 }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686 }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952 }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756 }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404 }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410 }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631 }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963 }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295 }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987 }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817 }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895 }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992 }, { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681 }, { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464 }, { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961 }, @@ -194,6 +290,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835 }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988 }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260 }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104 }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592 }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281 }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009 }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929 }, ] [[package]] @@ -213,6 +314,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 } wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215 }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625 }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614 }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997 }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825 }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090 }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377 }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453 }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321 }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944 }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099 }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040 }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717 }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751 }, { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076 }, { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794 }, { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474 }, @@ -241,6 +356,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011 }, { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801 }, { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560 }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198 }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817 }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867 }, ] [[package]] @@ -249,6 +367,28 @@ version = "2.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 } wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478 }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467 }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172 }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145 }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084 }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477 }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429 }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109 }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915 }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972 }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763 }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 }, { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 }, { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 }, { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 }, @@ -291,6 +431,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 }, { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 }, { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179 }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755 }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500 }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252 }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142 }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979 }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577 }, ] [[package]] @@ -308,6 +455,28 @@ version = "12.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 } wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084 }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866 }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148 }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007 }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418 }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590 }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655 }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286 }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663 }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448 }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651 }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 }, { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689 }, { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535 }, { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364 }, @@ -358,6 +527,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736 }, { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894 }, { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446 }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606 }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321 }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579 }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094 }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850 }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343 }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 }, ] [[package]] @@ -415,6 +591,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "rtree" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484 }, + { url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325 }, + { url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789 }, + { url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644 }, + { url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478 }, + { url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140 }, + { url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358 }, + { url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253 }, +] + [[package]] name = "ruff" version = "0.15.5" @@ -440,6 +632,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572 }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675 }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057 }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032 }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533 }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057 }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300 }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333 }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314 }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512 }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248 }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954 }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662 }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366 }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017 }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842 }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890 }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557 }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856 }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682 }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340 }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199 }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001 }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719 }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429 }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952 }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063 }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449 }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943 }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621 }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708 }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135 }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977 }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601 }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667 }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159 }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771 }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910 }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980 }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543 }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510 }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131 }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032 }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766 }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007 }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333 }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066 }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763 }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984 }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877 }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750 }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858 }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723 }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098 }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397 }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163 }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291 }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317 }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327 }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038 }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039 }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519 }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842 }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316 }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586 }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961 }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856 }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, +] + [[package]] name = "six" version = "1.17.0" @@ -448,3 +770,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68 wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +]