From 71e263c5271ccb3475142d8934e83401f69e621c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 1 Apr 2026 21:29:23 -0700 Subject: [PATCH] various performance work (wip) --- DOCS.md | 24 +- docs/architecture.md | 4 +- docs/optimization_pass_01_log.md | 1653 +++++++++++++++++++++ docs/performance.md | 22 +- docs/performance_baseline.json | 479 ++++-- inire/geometry/collision.py | 529 +++++-- inire/geometry/dynamic_path_index.py | 140 +- inire/results.py | 23 + inire/router/_astar_admission.py | 61 +- inire/router/_astar_moves.py | 41 +- inire/router/_astar_types.py | 111 +- inire/router/_router.py | 154 +- inire/router/_search.py | 10 + inire/router/cost.py | 3 +- inire/router/visibility.py | 113 +- inire/tests/example_scenarios.py | 25 +- inire/tests/test_api.py | 169 +++ inire/tests/test_astar.py | 125 +- inire/tests/test_collision.py | 296 ++++ inire/tests/test_example_performance.py | 20 +- inire/tests/test_example_regressions.py | 9 +- inire/tests/test_performance_reporting.py | 110 ++ inire/tests/test_visibility.py | 109 ++ scripts/diff_performance_baseline.py | 152 +- scripts/record_performance_baseline.py | 23 +- 25 files changed, 4075 insertions(+), 330 deletions(-) create mode 100644 docs/optimization_pass_01_log.md diff --git a/DOCS.md b/DOCS.md index 3aa2688..aa45f38 100644 --- a/DOCS.md +++ b/DOCS.md @@ -148,6 +148,11 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are - `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 @@ -165,20 +170,33 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are - `static_tree_rebuilds`: Number of static dilated-obstacle STRtree rebuilds. - `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification. - `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds. -- `visibility_builds`: Number of static visibility-graph rebuilds. -- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building that graph. -- `visibility_corner_queries` / `visibility_corner_hits`: Precomputed-corner visibility query activity. +- `visibility_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. ## 8. Internal Modules diff --git a/docs/architecture.md b/docs/architecture.md index 3855199..dff4583 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,7 @@ - `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers. - `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation. - `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic. -- `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths. +- `inire/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. @@ -39,7 +39,9 @@ The search state is a snapped Manhattan `(x, y, r)` port. From each state the ro - 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. - Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning. ## Performance Visibility diff --git a/docs/optimization_pass_01_log.md b/docs/optimization_pass_01_log.md new file mode 100644 index 0000000..9e71486 --- /dev/null +++ b/docs/optimization_pass_01_log.md @@ -0,0 +1,1653 @@ +# 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 | - | diff --git a/docs/performance.md b/docs/performance.md index 713c986..5f083a4 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,21 +1,21 @@ # Performance Baseline -Generated on 2026-03-31 by `scripts/record_performance_baseline.py`. +Generated on 2026-04-01 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.0042 | 1 | 1 | 1 | 1 | 1 | 2 | 22 | 11 | 7 | 2 | 2 | 0 | 3 | -| example_02_congestion_resolution | 0.3418 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 | -| example_03_locked_paths | 0.1827 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 | -| example_04_sbends_and_radii | 1.9938 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 | -| example_05_orientation_stress | 0.2458 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 | -| example_06_bend_collision_models | 4.1186 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 | -| example_07_large_scale_routing | 1.3734 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 | -| example_08_custom_bend_geometry | 0.2410 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 | -| example_09_unroutable_best_effort | 0.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 | +| example_01_simple_route | 0.0036 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 3 | +| example_02_congestion_resolution | 0.3297 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 35 | +| example_03_locked_paths | 0.1832 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 14 | +| example_04_sbends_and_radii | 0.0260 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 6 | +| example_05_orientation_stress | 0.2348 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 0 | 0 | 155 | 15 | +| example_06_bend_collision_models | 0.1953 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 9 | +| example_07_large_scale_routing | 0.1945 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 30 | +| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 6 | +| example_09_unroutable_best_effort | 0.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | ## Full Counter Set @@ -24,4 +24,4 @@ These counters are currently observational only and are not enforced as CI regre 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, 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, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_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_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, 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 +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, 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 diff --git a/docs/performance_baseline.json b/docs/performance_baseline.json index cfb369b..c95def6 100644 --- a/docs/performance_baseline.json +++ b/docs/performance_baseline.json @@ -1,30 +1,51 @@ { - "generated_on": "2026-03-31", + "generated_on": "2026-04-01", "generator": "scripts/record_performance_baseline.py", "scenarios": [ { - "duration_s": 0.00415895797777921, + "duration_s": 0.0035884700482711196, "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, - "danger_map_cache_hits": 8, - "danger_map_cache_misses": 13, - "danger_map_lookup_calls": 21, + "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": 27079, + "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 2, - "dynamic_path_objects_removed": 1, - "dynamic_tree_rebuilds": 2, + "dynamic_path_objects_added": 3, + "dynamic_path_objects_removed": 2, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 1, + "iteration_reverify_calls": 1, "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, @@ -32,15 +53,15 @@ "pruned_closed_set": 0, "pruned_cost": 4, "pruned_hard_collision": 0, - "ray_cast_calls": 22, + "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": 12, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 0, - "ray_cast_candidate_bounds": 12, + "ray_cast_candidate_bounds": 0, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 1, "refinement_candidate_side_extents": 0, @@ -52,18 +73,20 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 11, - "score_component_total_ns": 59404, + "score_component_total_ns": 16010, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, - "static_tree_rebuilds": 1, + "static_tree_rebuilds": 0, "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, "verify_path_report_calls": 3, "verify_static_buffer_ops": 0, - "visibility_builds": 2, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 12, + "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, @@ -80,28 +103,49 @@ "valid_results": 1 }, { - "duration_s": 0.34182924893684685, + "duration_s": 0.32969290704932064, "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, - "danger_map_cache_hits": 1433, - "danger_map_cache_misses": 775, - "danger_map_lookup_calls": 2208, + "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": 2165333, + "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 32, - "dynamic_path_objects_removed": 17, - "dynamic_tree_rebuilds": 8, + "dynamic_path_objects_added": 49, + "dynamic_path_objects_removed": 34, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 3, + "iteration_reverify_calls": 1, "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, @@ -109,15 +153,15 @@ "pruned_closed_set": 157, "pruned_cost": 208, "pruned_hard_collision": 380, - "ray_cast_calls": 1176, + "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": 12, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 253, - "ray_cast_candidate_bounds": 925, + "ray_cast_candidate_bounds": 913, "ray_cast_exact_geometry_checks": 136, "refine_path_calls": 3, "refinement_candidate_side_extents": 26, @@ -129,23 +173,25 @@ "refinement_windows_considered": 10, "route_iterations": 1, "score_component_calls": 976, - "score_component_total_ns": 4650167, + "score_component_total_ns": 1091130, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, "static_tree_rebuilds": 2, "timeout_events": 0, - "verify_dynamic_exact_pair_checks": 90, + "verify_dynamic_candidate_nets": 84, + "verify_dynamic_exact_pair_checks": 82, "verify_path_report_calls": 35, "verify_static_buffer_ops": 0, - "visibility_builds": 4, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 12, + "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": 18991, + "visibility_tangent_candidate_corner_checks": 873, "visibility_tangent_candidate_ray_tests": 253, "visibility_tangent_candidate_scans": 363, "warm_start_paths_built": 3, @@ -157,28 +203,49 @@ "valid_results": 3 }, { - "duration_s": 0.18274989898782223, + "duration_s": 0.18321374501101673, "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, - "danger_map_cache_hits": 624, - "danger_map_cache_misses": 414, - "danger_map_lookup_calls": 1038, + "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": 1001517, + "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 17, - "dynamic_path_objects_removed": 10, - "dynamic_tree_rebuilds": 5, + "dynamic_path_objects_added": 27, + "dynamic_path_objects_removed": 20, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 2, "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, @@ -186,15 +253,15 @@ "pruned_closed_set": 97, "pruned_cost": 140, "pruned_hard_collision": 181, - "ray_cast_calls": 681, + "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": 24, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 53, - "ray_cast_candidate_bounds": 179, + "ray_cast_candidate_bounds": 155, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 2, "refinement_candidate_side_extents": 8, @@ -206,23 +273,25 @@ "refinement_windows_considered": 2, "route_iterations": 2, "score_component_calls": 504, - "score_component_total_ns": 2184569, + "score_component_total_ns": 556716, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 1, - "static_tree_rebuilds": 2, + "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_exact_pair_checks": 0, + "verify_dynamic_candidate_nets": 8, + "verify_dynamic_exact_pair_checks": 8, "verify_path_report_calls": 14, - "verify_static_buffer_ops": 69, - "visibility_builds": 4, + "verify_static_buffer_ops": 72, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 24, + "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": 476, + "visibility_tangent_candidate_corner_checks": 56, "visibility_tangent_candidate_ray_tests": 53, "visibility_tangent_candidate_scans": 189, "warm_start_paths_built": 2, @@ -234,28 +303,49 @@ "valid_results": 2 }, { - "duration_s": 1.993830946041271, + "duration_s": 0.026024609920568764, "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, - "danger_map_cache_hits": 75, - "danger_map_cache_misses": 120, - "danger_map_lookup_calls": 195, + "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": 207556, + "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 14, - "dynamic_path_objects_removed": 7, - "dynamic_tree_rebuilds": 4, + "dynamic_path_objects_added": 21, + "dynamic_path_objects_removed": 14, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 1, "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, @@ -263,16 +353,16 @@ "pruned_closed_set": 2, "pruned_cost": 25, "pruned_hard_collision": 16, - "ray_cast_calls": 18218, + "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": 18148, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 0, - "ray_cast_candidate_bounds": 50717, - "ray_cast_exact_geometry_checks": 21265, + "ray_cast_candidate_bounds": 4, + "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 2, "refinement_candidate_side_extents": 0, "refinement_candidates_accepted": 0, @@ -283,23 +373,25 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 90, - "score_component_total_ns": 410130, + "score_component_total_ns": 97738, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, - "static_tree_rebuilds": 2, + "static_tree_rebuilds": 1, "timeout_events": 0, + "verify_dynamic_candidate_nets": 6, "verify_dynamic_exact_pair_checks": 0, "verify_path_report_calls": 6, "verify_static_buffer_ops": 0, - "visibility_builds": 3, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 18148, + "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": 394, + "visibility_tangent_candidate_corner_checks": 50, "visibility_tangent_candidate_ray_tests": 0, "visibility_tangent_candidate_scans": 13, "warm_start_paths_built": 2, @@ -311,28 +403,49 @@ "valid_results": 2 }, { - "duration_s": 0.24581307696644217, + "duration_s": 0.23484283208381385, "metrics": { "congestion_cache_hits": 2, - "congestion_cache_misses": 412, - "congestion_check_calls": 412, - "congestion_exact_pair_checks": 66, - "danger_map_cache_hits": 1386, - "danger_map_cache_misses": 693, - "danger_map_lookup_calls": 2079, + "congestion_cache_misses": 155, + "congestion_candidate_ids": 19, + "congestion_candidate_nets": 15, + "congestion_candidate_precheck_hits": 135, + "congestion_candidate_precheck_misses": 22, + "congestion_candidate_precheck_skips": 0, + "congestion_check_calls": 155, + "congestion_exact_pair_checks": 18, + "congestion_grid_net_cache_hits": 12, + "congestion_grid_net_cache_misses": 25, + "congestion_grid_span_cache_hits": 11, + "congestion_grid_span_cache_misses": 4, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 134, + "congestion_net_envelope_cache_misses": 43, + "congestion_presence_cache_hits": 185, + "congestion_presence_cache_misses": 30, + "congestion_presence_skips": 58, + "danger_map_cache_hits": 0, + "danger_map_cache_misses": 0, + "danger_map_lookup_calls": 0, "danger_map_query_calls": 0, - "danger_map_total_ns": 1805113, - "dynamic_grid_rebuilds": 3, - "dynamic_path_objects_added": 37, - "dynamic_path_objects_removed": 25, - "dynamic_tree_rebuilds": 12, + "danger_map_total_ns": 0, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 49, + "dynamic_path_objects_removed": 37, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 1, + "iteration_conflicting_nets": 2, + "iteration_reverified_nets": 6, + "iteration_reverify_calls": 2, "move_cache_abs_hits": 253, "move_cache_abs_misses": 1371, "move_cache_rel_hits": 1269, "move_cache_rel_misses": 102, "moves_added": 681, "moves_generated": 1624, + "nets_carried_forward": 0, "nets_reached_target": 6, "nets_routed": 6, "nodes_expanded": 286, @@ -360,23 +473,25 @@ "refinement_windows_considered": 0, "route_iterations": 2, "score_component_calls": 1198, - "score_component_total_ns": 4292875, + "score_component_total_ns": 1194981, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 3, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_exact_pair_checks": 2, - "verify_path_report_calls": 12, + "verify_dynamic_candidate_nets": 8, + "verify_dynamic_exact_pair_checks": 12, + "verify_path_report_calls": 15, "verify_static_buffer_ops": 0, - "visibility_builds": 3, + "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": 1483, + "visibility_tangent_candidate_corner_checks": 70, "visibility_tangent_candidate_ray_tests": 9, "visibility_tangent_candidate_scans": 280, "warm_start_paths_built": 2, @@ -388,28 +503,49 @@ "valid_results": 3 }, { - "duration_s": 4.1186372829834, + "duration_s": 0.19533946400042623, "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": 18374289, + "danger_map_total_ns": 18697751, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 36, - "dynamic_path_objects_removed": 18, - "dynamic_tree_rebuilds": 6, + "dynamic_path_objects_added": 54, + "dynamic_path_objects_removed": 36, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 18, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 3, + "iteration_reverify_calls": 3, "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, @@ -417,16 +553,16 @@ "pruned_closed_set": 108, "pruned_cost": 204, "pruned_hard_collision": 85, - "ray_cast_calls": 40530, + "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": 39848, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 34, - "ray_cast_candidate_bounds": 121732, - "ray_cast_exact_geometry_checks": 36858, + "ray_cast_candidate_bounds": 97, + "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 3, "refinement_candidate_side_extents": 0, "refinement_candidates_accepted": 0, @@ -437,23 +573,25 @@ "refinement_windows_considered": 0, "route_iterations": 3, "score_component_calls": 842, - "score_component_total_ns": 20652599, + "score_component_total_ns": 21016472, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3, "static_safe_cache_hits": 141, - "static_tree_rebuilds": 6, + "static_tree_rebuilds": 3, "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, "verify_path_report_calls": 9, "verify_static_buffer_ops": 54, - "visibility_builds": 6, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 39848, + "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": 2400, + "visibility_tangent_candidate_corner_checks": 84, "visibility_tangent_candidate_ray_tests": 34, "visibility_tangent_candidate_scans": 237, "warm_start_paths_built": 3, @@ -465,28 +603,49 @@ "valid_results": 3 }, { - "duration_s": 1.373430646955967, + "duration_s": 0.19448363897390664, "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": 10728422, + "danger_map_total_ns": 10973251, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 88, - "dynamic_path_objects_removed": 44, - "dynamic_tree_rebuilds": 20, + "dynamic_path_objects_added": 132, + "dynamic_path_objects_removed": 88, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 10, + "iteration_reverify_calls": 1, "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, @@ -494,16 +653,16 @@ "pruned_closed_set": 20, "pruned_cost": 64, "pruned_hard_collision": 61, - "ray_cast_calls": 11151, + "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": 10768, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 77, - "ray_cast_candidate_bounds": 21198, - "ray_cast_exact_geometry_checks": 11651, + "ray_cast_candidate_bounds": 683, + "ray_cast_exact_geometry_checks": 150, "refine_path_calls": 10, "refinement_candidate_side_extents": 0, "refinement_candidates_accepted": 0, @@ -514,23 +673,25 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 291, - "score_component_total_ns": 11574800, + "score_component_total_ns": 11824081, "static_net_tree_rebuilds": 10, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 6, "static_tree_rebuilds": 10, "timeout_events": 0, - "verify_dynamic_exact_pair_checks": 0, + "verify_dynamic_candidate_nets": 264, + "verify_dynamic_exact_pair_checks": 40, "verify_path_report_calls": 30, "verify_static_buffer_ops": 132, - "visibility_builds": 11, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 10768, + "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": 34735, + "visibility_tangent_candidate_corner_checks": 321, "visibility_tangent_candidate_ray_tests": 77, "visibility_tangent_candidate_scans": 68, "warm_start_paths_built": 10, @@ -542,28 +703,49 @@ "valid_results": 10 }, { - "duration_s": 0.2410298540489748, + "duration_s": 0.017700672964565456, "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, - "danger_map_cache_hits": 58, - "danger_map_cache_misses": 110, - "danger_map_lookup_calls": 168, + "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": 178104, + "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 12, - "dynamic_path_objects_removed": 6, - "dynamic_tree_rebuilds": 4, + "dynamic_path_objects_added": 18, + "dynamic_path_objects_removed": 12, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 2, + "iteration_reverify_calls": 2, "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, @@ -571,16 +753,16 @@ "pruned_closed_set": 6, "pruned_cost": 16, "pruned_hard_collision": 0, - "ray_cast_calls": 2308, + "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": 2252, + "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 0, - "ray_cast_candidate_bounds": 3802, - "ray_cast_exact_geometry_checks": 1904, + "ray_cast_candidate_bounds": 0, + "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 2, "refinement_candidate_side_extents": 0, "refinement_candidates_accepted": 0, @@ -591,18 +773,20 @@ "refinement_windows_considered": 0, "route_iterations": 2, "score_component_calls": 72, - "score_component_total_ns": 352865, + "score_component_total_ns": 85969, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 2, - "static_tree_rebuilds": 2, + "static_tree_rebuilds": 0, "timeout_events": 0, + "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, "verify_path_report_calls": 6, "verify_static_buffer_ops": 0, - "visibility_builds": 4, + "visibility_builds": 0, "visibility_corner_hits_exact": 0, - "visibility_corner_pairs_checked": 2252, + "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, @@ -619,28 +803,49 @@ "valid_results": 2 }, { - "duration_s": 0.0052388140466064215, + "duration_s": 0.005781985004432499, "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": 502052, + "danger_map_total_ns": 536009, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 1, - "dynamic_path_objects_removed": 0, - "dynamic_tree_rebuilds": 1, + "dynamic_path_objects_added": 2, + "dynamic_path_objects_removed": 1, + "dynamic_tree_rebuilds": 0, "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 0, + "iteration_conflicting_nets": 0, + "iteration_reverified_nets": 0, + "iteration_reverify_calls": 1, "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, @@ -668,23 +873,25 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 14, - "score_component_total_ns": 538947, + "score_component_total_ns": 574907, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 0, - "static_tree_rebuilds": 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": 10, + "visibility_tangent_candidate_corner_checks": 0, "visibility_tangent_candidate_ray_tests": 0, "visibility_tangent_candidate_scans": 3, "warm_start_paths_built": 0, diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py index 01367b4..c877cfa 100644 --- a/inire/geometry/collision.py +++ b/inire/geometry/collision.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING import numpy @@ -28,6 +29,49 @@ def _intersection_distance(origin: Port, geometry: BaseGeometry) -> float: 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, ...] = () + + +@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. @@ -102,6 +146,9 @@ class RoutingWorld: 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, @@ -235,105 +282,383 @@ class RoutingWorld: return False - def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int: + 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 - self._ensure_dynamic_tree() - if dynamic_paths.tree is None: - return 0 - - total_bounds = result.total_dilated_bounds - dynamic_bounds = dynamic_paths.bounds_array - possible_total = ( - (total_bounds[0] < dynamic_bounds[:, 2]) - & (total_bounds[2] > dynamic_bounds[:, 0]) - & (total_bounds[1] < dynamic_bounds[:, 3]) - & (total_bounds[3] > dynamic_bounds[:, 1]) - ) - - valid_hits_mask = dynamic_paths.net_ids_array != net_id - if not numpy.any(possible_total & valid_hits_mask): - return 0 - geometries_to_test = result.dilated_collision_geometry - res_indices, tree_indices = dynamic_paths.tree.query(geometries_to_test, predicate="intersects") - if tree_indices.size == 0: - return 0 - hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices) - unique_other_nets = numpy.unique(hit_net_ids[hit_net_ids != net_id]) - if unique_other_nets.size == 0: - return 0 - - tree_geometries = dynamic_paths.tree.geometries real_hits_count = 0 - for other_net_id in unique_other_nets: - other_mask = hit_net_ids == other_net_id - sub_tree_indices = tree_indices[other_mask] - sub_res_indices = res_indices[other_mask] - + for other_net_id, other_obj_ids in candidates_by_net.items(): found_real = False - for index in range(len(sub_tree_indices)): - if self.metrics is not None: - self.metrics.total_congestion_exact_pair_checks += 1 - test_geometry = geometries_to_test[sub_res_indices[index]] - tree_geometry = tree_geometries[sub_tree_indices[index]] - if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7: - found_real = True + 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 real_hits_count + return DynamicCongestionDetail(soft_overlap_count=real_hits_count) - def check_move_congestion(self, result: ComponentResult, net_id: str) -> int: + 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 0 + return DynamicCongestionDetail() - total_bounds = result.total_dilated_bounds - self._ensure_dynamic_grid() - dynamic_grid = dynamic_paths.grid - if not dynamic_grid: - return 0 + 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, + ) - gx_min, gy_min, gx_max, gy_max = grid_cell_span(total_bounds, self.grid_cell_size) - - if gx_min == gx_max and gy_min == gy_max: - cell = (gx_min, gy_min) - if cell in dynamic_grid: - for obj_id in dynamic_grid[cell]: - if dynamic_paths.geometries[obj_id][0] != net_id: - return self._check_real_congestion(result, net_id) - return 0 - - any_possible = False - for gx in range(gx_min, gx_max + 1): - for gy in range(gy_min, gy_max + 1): - cell = (gx, gy) - if cell in dynamic_grid: - for obj_id in dynamic_grid[cell]: - if dynamic_paths.geometries[obj_id][0] != net_id: - any_possible = True - break - if any_possible: - break - if any_possible: - break - - if not any_possible: - return 0 - return self._check_real_congestion(result, net_id) - - def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport: + def verify_path_details(self, net_id: str, components: Sequence[ComponentResult]) -> 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() static_obstacles = self._static_obstacles dynamic_paths = self._dynamic_paths @@ -356,43 +681,45 @@ class RoutingWorld: if not self._is_in_safety_zone(polygon, obj_id, None, None): static_collision_count += 1 - self._ensure_dynamic_tree() - if dynamic_paths.tree is not None: - tree_geometries = dynamic_paths.tree.geometries + if dynamic_paths.dilated: for component in components: test_geometries = component.dilated_physical_geometry - res_indices, tree_indices = dynamic_paths.tree.query(test_geometries, predicate="intersects") - if tree_indices.size == 0: - continue - - hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices) component_hits = [] - for index in range(len(tree_indices)): - if hit_net_ids[index] == str(net_id): - continue - - if self.metrics is not None: - self.metrics.total_verify_dynamic_exact_pair_checks += 1 - new_geometry = test_geometries[res_indices[index]] - tree_geometry = tree_geometries[tree_indices[index]] - if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7: - component_hits.append(hit_net_ids[index]) + 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) + break if component_hits: - dynamic_collision_count += len(numpy.unique(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 RoutingReport( - static_collision_count=static_collision_count, - dynamic_collision_count=dynamic_collision_count, - self_collision_count=self_collision_count, - total_length=total_length, + 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)), ) + 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, diff --git a/inire/geometry/dynamic_path_index.py b/inire/geometry/dynamic_path_index.py index 0985f53..78b3f22 100644 --- a/inire/geometry/dynamic_path_index.py +++ b/inire/geometry/dynamic_path_index.py @@ -22,9 +22,18 @@ class DynamicPathIndex: "index", "geometries", "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", @@ -35,16 +44,69 @@ class DynamicPathIndex: self.index = rtree.index.Index() self.geometries: dict[int, tuple[str, Polygon]] = {} 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: @@ -65,33 +127,97 @@ class DynamicPathIndex: self.engine.metrics.total_dynamic_grid_rebuilds += 1 cell_size = self.engine.grid_cell_size for obj_id, polygon in self.dilated.items(): - for cell in iter_grid_cells(polygon.bounds, cell_size): - self.grid.setdefault(cell, []).append(obj_id) + 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]) -> None: - self.invalidate_queries() 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.dilated[obj_id] = dilated - self.index.insert(obj_id, dilated.bounds) + 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 = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id] + 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 - self.invalidate_queries() 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: - self.index.delete(obj_id, self.dilated[obj_id].bounds) + 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.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/results.py b/inire/results.py index b405117..673ca80 100644 --- a/inire/results.py +++ b/inire/results.py @@ -45,6 +45,11 @@ class RouteMetrics: 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 @@ -61,6 +66,19 @@ class RouteMetrics: 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 @@ -68,6 +86,7 @@ class RouteMetrics: 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 @@ -89,9 +108,13 @@ class RouteMetrics: 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 diff --git a/inire/router/_astar_admission.py b/inire/router/_astar_admission.py index 088b526..19fbeda 100644 --- a/inire/router/_astar_admission.py +++ b/inire/router/_astar_admission.py @@ -26,6 +26,11 @@ def process_move( 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, @@ -109,6 +114,11 @@ def process_move( 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, @@ -126,6 +136,11 @@ def add_node( 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, @@ -164,16 +179,6 @@ def add_node( return context.static_safe_cache.add(cache_key) - total_overlaps = 0 - if not config.skip_congestion: - 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 = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id) - congestion_cache[cache_key] = total_overlaps - if config.self_collision_check and component_hits_ancestor_chain(result, parent): return @@ -181,7 +186,6 @@ def add_node( result, start_port=parent_p, ) - move_cost += total_overlaps * context.congestion_penalty if config.max_cost is not None and parent.g_cost + move_cost > config.max_cost: metrics.pruned_cost += 1 @@ -192,6 +196,41 @@ def add_node( metrics.total_pruned_cost += 1 return + if state in closed_set and closed_set[state] <= parent.g_cost + move_cost + TOLERANCE_LINEAR: + 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: metrics.pruned_closed_set += 1 diff --git a/inire/router/_astar_moves.py b/inire/router/_astar_moves.py index a921aab..5eb382a 100644 --- a/inire/router/_astar_moves.py +++ b/inire/router/_astar_moves.py @@ -63,15 +63,19 @@ def _visible_straight_candidates( return [] visibility_manager = context.visibility_manager - visibility_manager._ensure_current() + 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 - bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach) - candidate_ids = list(visibility_manager.corner_index.intersection(bounds)) + 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 [] @@ -141,6 +145,11 @@ def expand_moves( 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 @@ -185,6 +194,11 @@ def expand_moves( 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)),), @@ -242,6 +256,11 @@ def expand_moves( 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,), @@ -270,6 +289,11 @@ def expand_moves( 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), @@ -298,12 +322,17 @@ def expand_moves( current, target, net_width, - net_id, - open_set, - closed_set, + 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 index 14cc7bc..e6a1d13 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -58,7 +58,17 @@ class SearchRunConfig: class AStarNode: - __slots__ = ("port", "g_cost", "h_cost", "fh_cost", "parent", "component_result") + __slots__ = ( + "port", + "g_cost", + "h_cost", + "fh_cost", + "parent", + "component_result", + "base_move_cost", + "cache_key", + "congestion_resolved", + ) def __init__( self, @@ -67,6 +77,10 @@ class AStarNode: h_cost: float, parent: AStarNode | None = None, component_result: ComponentResult | None = None, + *, + base_move_cost: float = 0.0, + cache_key: tuple | None = None, + congestion_resolved: bool = True, ) -> None: self.port = port self.g_cost = g_cost @@ -74,6 +88,9 @@ class AStarNode: 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.congestion_resolved = congestion_resolved def __lt__(self, other: AStarNode) -> bool: return self.fh_cost < other.fh_cost @@ -94,6 +111,11 @@ class AStarMetrics: "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", @@ -110,6 +132,19 @@ class AStarMetrics: "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", @@ -117,6 +152,7 @@ class AStarMetrics: "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", @@ -138,9 +174,13 @@ class AStarMetrics: "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", @@ -172,6 +212,11 @@ class AStarMetrics: 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 @@ -188,6 +233,19 @@ class AStarMetrics: 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 @@ -195,6 +253,7 @@ class AStarMetrics: 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 @@ -216,9 +275,13 @@ class AStarMetrics: 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 @@ -249,6 +312,11 @@ class AStarMetrics: 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 @@ -265,6 +333,19 @@ class AStarMetrics: 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 @@ -272,6 +353,7 @@ class AStarMetrics: 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 @@ -293,9 +375,13 @@ class AStarMetrics: 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 @@ -329,6 +415,11 @@ class AStarMetrics: 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, @@ -345,6 +436,19 @@ class AStarMetrics: 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, @@ -352,6 +456,7 @@ class AStarMetrics: 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, @@ -373,9 +478,13 @@ class AStarMetrics: 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, diff --git a/inire/router/_router.py b/inire/router/_router.py index ed71cce..a641c53 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -30,6 +30,21 @@ class _RoutingState: 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 + + +@dataclass(slots=True) +class _IterationReview: + conflicting_nets: set[str] + conflict_edges: set[tuple[str, str]] + completed_net_ids: set[str] + total_dynamic_collisions: int class PathFinder: __slots__ = ( @@ -136,6 +151,13 @@ class PathFinder: 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, ) 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) @@ -162,9 +184,49 @@ class PathFinder: 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 _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 _route_net_once( self, state: _RoutingState, @@ -235,9 +297,9 @@ class PathFinder: self, state: _RoutingState, iteration: int, + reroute_net_ids: set[str], iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, - ) -> dict[str, RoutingOutcome] | None: - outcomes: dict[str, RoutingOutcome] = {} + ) -> _IterationReview | None: congestion = self.context.options.congestion self.metrics.total_route_iterations += 1 self.metrics.reset_per_route() @@ -246,18 +308,63 @@ class PathFinder: iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None random.Random(iteration_seed).shuffle(state.ordered_net_ids) - for net_id in state.ordered_net_ids: + routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids] + self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids) + + for net_id in routed_net_ids: if time.monotonic() - state.start_time > state.timeout_s: self.metrics.total_timeout_events += 1 return None result = self._route_net_once(state, iteration, net_id) state.results[net_id] = result - outcomes[net_id] = result.outcome + + review = self._reverify_iteration_results(state) if iteration_callback: iteration_callback(iteration, state.results) - return outcomes + return review + + def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview: + 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 + + for net_id in state.ordered_net_ids: + result = state.results.get(net_id) + if not result or not result.path or not result.reached_target: + continue + + self.metrics.total_iteration_reverified_nets += 1 + detail = self.context.cost_evaluator.collision_engine.verify_path_details(net_id, result.path) + state.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 state.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)))) + + self.metrics.total_iteration_conflicting_nets += len(conflicting_nets) + self.metrics.total_iteration_conflict_edges += len(conflict_edges) + return _IterationReview( + conflicting_nets=conflicting_nets, + conflict_edges=conflict_edges, + completed_net_ids=completed_net_ids, + total_dynamic_collisions=total_dynamic_collisions, + ) def _run_iterations( self, @@ -266,10 +373,33 @@ class PathFinder: ) -> bool: congestion = self.context.options.congestion for iteration in range(congestion.max_iterations): - outcomes = self._run_iteration(state, iteration, iteration_callback) - if outcomes is None: + review = self._run_iteration( + state, + iteration, + set(state.ordered_net_ids), + iteration_callback, + ) + if review is None: return True - if not any(outcome in {"colliding", "partial", "unroutable"} for outcome in outcomes.values()): + self._update_best_iteration(state, review) + if not any( + result.outcome in {"colliding", "partial", "unroutable"} + for result in state.results.values() + ): + return False + + 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) + if state.repeated_conflict_count >= 2: return False self.context.congestion_penalty *= congestion.multiplier return False @@ -287,12 +417,13 @@ class PathFinder: 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) - report = self.context.cost_evaluator.collision_engine.verify_path_report(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=report, + report=result.report, ) def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]: @@ -324,6 +455,7 @@ class PathFinder: 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) if timed_out: return self._verify_results(state) diff --git a/inire/router/_search.py b/inire/router/_search.py index 8e7a0ab..e6de987 100644 --- a/inire/router/_search.py +++ b/inire/router/_search.py @@ -41,6 +41,11 @@ def route_astar( 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, @@ -89,6 +94,11 @@ def route_astar( 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, ) diff --git a/inire/router/cost.py b/inire/router/cost.py index dc36447..3bb4765 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -152,7 +152,8 @@ class CostEvaluator: weights=active_weights, ) - if danger_map is not None and active_weights.danger_weight: + # 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: diff --git a/inire/router/visibility.py b/inire/router/visibility.py index 00f9668..ea4b610 100644 --- a/inire/router/visibility.py +++ b/inire/router/visibility.py @@ -16,7 +16,15 @@ class VisibilityManager: """ Manages corners of static obstacles for sparse A* / Visibility Graph jumps. """ - __slots__ = ("collision_engine", "corners", "corner_index", "_corner_graph", "_point_visibility_cache", "_built_static_version") + __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 @@ -24,8 +32,8 @@ class VisibilityManager: 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._built_static_version = -1 - self._build() + self._corner_index_version = -1 + self._corner_graph_version = -1 def clear_cache(self) -> None: """ @@ -35,19 +43,31 @@ class VisibilityManager: self.corner_index = rtree.index.Index() self._corner_graph = {} self._point_visibility_cache = {} - self._build() + 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: - if self._built_static_version != self.collision_engine.get_static_version(): - self.clear_cache() + self.ensure_corner_graph_current() - def _build(self) -> None: - """ - Extract corners and pre-compute corner-to-corner visibility. - """ + def _build_corner_index(self) -> None: if self.collision_engine.metrics is not None: - self.collision_engine.metrics.total_visibility_builds += 1 - self._built_static_version = self.collision_engine.get_static_version() + self.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) @@ -63,7 +83,6 @@ class VisibilityManager: if not raw_corners: return - # Deduplicate repeated corner coordinates seen = set() for x, y in raw_corners: sx, sy = round(x, 3), round(y, 3) @@ -71,15 +90,27 @@ class VisibilityManager: seen.add((sx, sy)) self.corners.append((sx, sy)) - # Build spatial index for corners for i, (x, y) in enumerate(self.corners): self.corner_index.insert(i, (x, y, x, y)) + 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 + # Limit pre-computation if too many corners + return for i in range(num_corners): self._corner_graph[i] = [] @@ -98,6 +129,7 @@ class VisibilityManager: 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: @@ -106,6 +138,49 @@ class VisibilityManager: 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. @@ -113,11 +188,13 @@ class VisibilityManager: """ if self.collision_engine.metrics is not None: self.collision_engine.metrics.total_visibility_point_queries += 1 - self._ensure_current() + 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] @@ -157,7 +234,7 @@ class VisibilityManager: """ if self.collision_engine.metrics is not None: self.collision_engine.metrics.total_visibility_corner_queries_exact += 1 - self._ensure_current() + self.ensure_corner_graph_current() if max_dist < 0: return [] diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index 02cab01..9ba7a3a 100644 --- a/inire/tests/example_scenarios.py +++ b/inire/tests/example_scenarios.py @@ -378,6 +378,24 @@ def run_example_06() -> ScenarioOutcome: 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_variant( + name: str, + *, + warm_start_enabled: bool, +) -> ScenarioSnapshot: bounds = (0, 0, 1000, 1000) obstacles = [ box(450, 0, 550, 400), @@ -420,6 +438,7 @@ def snapshot_example_07() -> ScenarioSnapshot: "capture_expanded": True, "shuffle_nets": True, "seed": 42, + "warm_start_enabled": warm_start_enabled, }, ) @@ -432,7 +451,7 @@ def snapshot_example_07() -> ScenarioSnapshot: t0 = perf_counter() results = pathfinder.route_all(iteration_callback=iteration_callback) t1 = perf_counter() - return _make_snapshot("example_07_large_scale_routing", results, t1 - t0, pathfinder.metrics.snapshot()) + return _make_snapshot(name, results, t1 - t0, pathfinder.metrics.snapshot()) def run_example_07() -> ScenarioOutcome: @@ -534,6 +553,10 @@ SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = ( ("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), +) + 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 index d25df09..024a569 100644 --- a/inire/tests/test_api.py +++ b/inire/tests/test_api.py @@ -16,6 +16,12 @@ from inire import ( 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: @@ -77,14 +83,177 @@ def test_route_problem_supports_configs_and_debug_data() -> None: 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.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 +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_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_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_route_problem_locked_routes_become_static_obstacles() -> None: locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),) problem = RoutingProblem( diff --git a/inire/tests/test_astar.py b/inire/tests/test_astar.py index 3d637b9..54362b9 100644 --- a/inire/tests/test_astar.py +++ b/inire/tests/test_astar.py @@ -1,5 +1,4 @@ import math - import pytest from shapely.geometry import Polygon @@ -7,7 +6,8 @@ from inire import 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, SearchRunConfig +from inire.router._astar_types import AStarContext, AStarNode, SearchRunConfig +from inire.router._astar_admission import add_node from inire.router._search import route_astar from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap @@ -301,6 +301,27 @@ def test_route_astar_supports_all_visibility_guidance_modes( 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, @@ -318,3 +339,103 @@ def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_eval 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 diff --git a/inire/tests/test_collision.py b/inire/tests/test_collision.py index 284055d..4fd4033 100644 --- a/inire/tests/test_collision.py +++ b/inire/tests/test_collision.py @@ -1,6 +1,11 @@ +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( @@ -82,6 +87,7 @@ def test_check_move_static_clearance() -> None: 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] @@ -91,10 +97,12 @@ def test_verify_path_report_preserves_long_net_id() -> None: 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" @@ -115,6 +123,50 @@ def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> N 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_remove_path_clears_dynamic_path() -> None: @@ -129,3 +181,247 @@ def test_remove_path_clears_dynamic_path() -> None: 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_example_performance.py b/inire/tests/test_example_performance.py index 2d44d11..5b594a5 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING import pytest -from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome +from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome, snapshot_example_07_no_warm_start if TYPE_CHECKING: from collections.abc import Callable @@ -15,6 +15,7 @@ if TYPE_CHECKING: RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" PERFORMANCE_REPEATS = 3 REGRESSION_FACTOR = 1.5 +NO_WARM_START_REGRESSION_SECONDS = 180.0 # Baselines are measured from clean 6a28dcf-style runs without plotting. BASELINE_SECONDS = { @@ -67,3 +68,20 @@ def test_example_like_runtime_regression(scenario: tuple[str, Callable[[], Scena 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" + ) diff --git a/inire/tests/test_example_regressions.py b/inire/tests/test_example_regressions.py index e78fcab..e6dd3b6 100644 --- a/inire/tests/test_example_regressions.py +++ b/inire/tests/test_example_regressions.py @@ -14,7 +14,7 @@ from inire import ( ) 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 +from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics, snapshot_example_05 EXPECTED_OUTCOMES = { @@ -36,6 +36,13 @@ def test_examples_match_legacy_expected_outcomes(name: str, run) -> None: 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_06_clipped_bbox_margin_restores_legacy_seed() -> None: bounds = (-20, -20, 170, 170) obstacles = ( diff --git a/inire/tests/test_performance_reporting.py b/inire/tests/test_performance_reporting.py index 58954de..4a2d1be 100644 --- a/inire/tests/test_performance_reporting.py +++ b/inire/tests/test_performance_reporting.py @@ -22,7 +22,19 @@ def test_snapshot_example_01_exposes_metrics() -> None: 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 @@ -74,6 +86,7 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat str(diff_script), "--baseline", str(baseline_dir / "performance_baseline.json"), + "--include-performance-only", "--scenario", "example_01_simple_route", "--output", @@ -85,3 +98,100 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat 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 diff --git a/inire/tests/test_visibility.py b/inire/tests/test_visibility.py index 0e2100f..ff20439 100644 --- a/inire/tests/test_visibility.py +++ b/inire/tests/test_visibility.py @@ -2,6 +2,7 @@ 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 @@ -18,3 +19,111 @@ def test_point_visibility_cache_respects_max_distance() -> None: 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/scripts/diff_performance_baseline.py b/scripts/diff_performance_baseline.py index 89a0a47..352022b 100644 --- a/scripts/diff_performance_baseline.py +++ b/scripts/diff_performance_baseline.py @@ -4,13 +4,17 @@ 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 SCENARIO_SNAPSHOTS +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", @@ -22,10 +26,30 @@ SUMMARY_KEYS = ( ) -def _current_snapshots(selected_scenarios: tuple[str, ...] | None) -> dict[str, dict[str, object]]: +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 SCENARIO_SNAPSHOTS: + for name, run in _snapshot_registry(include_performance_only): if allowed is not None and name not in allowed: continue snapshots[name] = asdict(run()) @@ -42,13 +66,29 @@ def _load_baseline(path: Path, selected_scenarios: tuple[str, ...] | None) -> di } -def _metric_value(snapshot: dict[str, object], key: str) -> float: - if key == "duration_s": - return float(snapshot["duration_s"]) +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 _render_report(baseline: dict[str, dict[str, object]], current: dict[str, dict[str, object]]) -> str: +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", @@ -60,20 +100,61 @@ def _render_report(baseline: dict[str, dict[str, object]], current: dict[str, di base_snapshot = baseline.get(scenario) curr_snapshot = current.get(scenario) if base_snapshot is None: - lines.append(f"| {scenario} | added | - | - | - |") + 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 SUMMARY_KEYS: + 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( @@ -95,18 +176,61 @@ def main() -> None: 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) - report = _render_report(baseline, current) + current = _current_snapshots(selected, include_performance_only=args.include_performance_only) + report = _render_report(baseline, current, metrics) - if args.output is None: - print(report, end="") - else: + 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__": diff --git a/scripts/record_performance_baseline.py b/scripts/record_performance_baseline.py index e254da2..14944bb 100644 --- a/scripts/record_performance_baseline.py +++ b/scripts/record_performance_baseline.py @@ -7,7 +7,7 @@ from dataclasses import asdict from datetime import date from pathlib import Path -from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS +from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS SUMMARY_METRICS = ( @@ -24,10 +24,20 @@ SUMMARY_METRICS = ( ) -def _build_payload(selected_scenarios: tuple[str, ...] | None = None) -> dict[str, object]: +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 SCENARIO_SNAPSHOTS: + for name, run in _snapshot_registry(include_performance_only): if allowed is not None and name not in allowed: continue snapshots.append(run()) @@ -103,6 +113,11 @@ def main() -> None: 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] @@ -110,7 +125,7 @@ def main() -> None: docs_dir.mkdir(exist_ok=True) selected = tuple(args.scenarios) if args.scenarios else None - payload = _build_payload(selected) + payload = _build_payload(selected, include_performance_only=args.include_performance_only) json_path = docs_dir / "performance_baseline.json" markdown_path = docs_dir / "performance.md"