Optimize late no-warm reroutes

This commit is contained in:
Jan Petykiewicz 2026-04-02 18:57:34 -07:00
commit 46e7e13059
19 changed files with 2086 additions and 307 deletions

32
DOCS.md
View file

@ -131,6 +131,7 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
| `capture_conflict_trace` | `False` | Capture authoritative post-reverify conflict trace entries for debugging negotiated-congestion failures. |
| `capture_frontier_trace` | `False` | Run an analysis-only reroute for reached-but-colliding nets and capture prune causes near their final conflict hotspots. |
| `capture_iteration_trace` | `False` | Capture per-iteration and per-net route-attempt attribution for negotiated-congestion diagnosis. |
| `capture_pre_pair_frontier_trace` | `False` | Capture the final unresolved pre-pair-local subset iteration plus hotspot-adjacent frontier prunes for the routed nets in that basin. |
## 7. Conflict Trace
@ -187,7 +188,30 @@ Use `scripts/record_frontier_trace.py` to capture JSON and Markdown frontier-pru
Separately from the observational trace tooling, the router may run a bounded post-loop pair-local scratch reroute before refinement when the restored best snapshot ends with final two-net reached-target dynamic conflicts. That repair phase is part of normal routing behavior and is reported through the `pair_local_search_*` counters below.
## 9. Iteration Trace
## 9. Pre-Pair Frontier Trace
`RoutingRunResult.pre_pair_frontier_trace` is either a single immutable trace entry or `None`. It is populated only when `RoutingOptions.diagnostics.capture_pre_pair_frontier_trace=True`.
Trace types:
- `PrePairFrontierTraceEntry`
- `iteration`: The final unresolved subset-reroute iteration immediately before pair-local handoff
- `routed_net_ids`: Nets rerouted in that iteration, in routing order
- `conflict_edges`: Dynamic conflict edges reported for that unresolved basin
- `nets`: Per-net attempt attribution plus hotspot-adjacent frontier rerun data
- `PrePairNetTrace`
- `net_id`
- `nodes_expanded`
- `congestion_check_calls`
- `pruned_closed_set`
- `pruned_cost`
- `pruned_hard_collision`
- `guidance_seed_present`
- `frontier`: A `NetFrontierTrace` captured against the restored best unresolved state
Use `scripts/record_pre_pair_frontier_trace.py` to capture JSON and Markdown artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the heavier seed-43 no-warm canary.
## 10. Iteration Trace
`RoutingRunResult.iteration_trace` is an immutable tuple of negotiated-congestion iteration summaries. It is empty unless `RoutingOptions.diagnostics.capture_iteration_trace=True`.
@ -217,7 +241,7 @@ Trace types:
Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-attribution artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the pathological seed-43 no-warm canary.
## 10. RouteMetrics
## 11. RouteMetrics
`RoutingRunResult.metrics` is an immutable per-run snapshot.
@ -297,13 +321,15 @@ Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-a
- `pair_local_search_attempts`: Number of pair-local-search reroute attempts executed across all considered pairs.
- `pair_local_search_accepts`: Number of pair-local-search attempts accepted into the whole routed result set.
- `pair_local_search_nodes_expanded`: Total A* node expansions spent inside pair-local-search attempts.
- `late_phase_capped_nets`: Number of late all-reached heavy-net reroutes run under the bounded node-limit cap before pair-local handoff.
- `late_phase_capped_fallbacks`: Number of those capped late-phase reroutes that fell back to the incumbent reached-target path instead of replacing it.
## 10. Internal Modules
Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`.
The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**.
Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, `scripts/record_iteration_trace.py` for per-iteration negotiated-congestion attribution, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate.
Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, `scripts/record_pre_pair_frontier_trace.py` for the final unresolved pre-pair basin, `scripts/record_iteration_trace.py` for per-iteration negotiated-congestion attribution, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate.
## 11. Tuning Notes

View file

@ -1,5 +1,5 @@
{
"generated_at": "2026-04-02T16:46:00-07:00",
"generated_at": "2026-04-02T18:51:01-07:00",
"generator": "scripts/record_iteration_trace.py",
"scenarios": [
{
@ -514,10 +514,10 @@
"iteration": 4,
"net_attempts": [
{
"congestion_check_calls": 43,
"congestion_check_calls": 30,
"guidance_seed_present": true,
"net_id": "net_00",
"nodes_expanded": 10,
"net_id": "net_07",
"nodes_expanded": 7,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
@ -534,10 +534,10 @@
"reached_target": true
},
{
"congestion_check_calls": 30,
"congestion_check_calls": 43,
"guidance_seed_present": true,
"net_id": "net_07",
"nodes_expanded": 7,
"net_id": "net_00",
"nodes_expanded": 10,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
@ -556,9 +556,9 @@
],
"nodes_expanded": 81,
"routed_net_ids": [
"net_00",
"net_06",
"net_07",
"net_06",
"net_00",
"net_01"
],
"total_dynamic_collisions": 10
@ -589,7 +589,7 @@
"danger_map_cache_misses": 6063,
"danger_map_lookup_calls": 17610,
"danger_map_query_calls": 6063,
"danger_map_total_ns": 174709728,
"danger_map_total_ns": 171226180,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 399,
"dynamic_path_objects_removed": 351,
@ -607,6 +607,8 @@
"iteration_conflicting_nets": 32,
"iteration_reverified_nets": 50,
"iteration_reverify_calls": 5,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1200,
"move_cache_abs_misses": 5338,
"move_cache_rel_hits": 4768,
@ -645,7 +647,7 @@
"refinement_windows_considered": 0,
"route_iterations": 5,
"score_component_calls": 6181,
"score_component_total_ns": 195118641,
"score_component_total_ns": 191650546,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1170,
@ -1062,26 +1064,6 @@
"congestion_penalty": 274.4,
"iteration": 3,
"net_attempts": [
{
"congestion_check_calls": 35,
"guidance_seed_present": true,
"net_id": "net_06",
"nodes_expanded": 8,
"pruned_closed_set": 0,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 49,
"guidance_seed_present": true,
"net_id": "net_09",
"nodes_expanded": 11,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 36,
"guidance_seed_present": true,
@ -1092,6 +1074,16 @@
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 30,
"guidance_seed_present": true,
"net_id": "net_07",
"nodes_expanded": 7,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 30,
"guidance_seed_present": true,
@ -1102,6 +1094,16 @@
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 49,
"guidance_seed_present": true,
"net_id": "net_09",
"nodes_expanded": 11,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 70,
"guidance_seed_present": true,
@ -1113,11 +1115,11 @@
"reached_target": true
},
{
"congestion_check_calls": 30,
"congestion_check_calls": 35,
"guidance_seed_present": true,
"net_id": "net_07",
"nodes_expanded": 7,
"pruned_closed_set": 1,
"net_id": "net_06",
"nodes_expanded": 8,
"pruned_closed_set": 0,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
@ -1125,34 +1127,24 @@
],
"nodes_expanded": 54,
"routed_net_ids": [
"net_06",
"net_09",
"net_03",
"net_07",
"net_02",
"net_09",
"net_08",
"net_07"
"net_06"
],
"total_dynamic_collisions": 15
},
{
"completed_nets": 6,
"conflict_edges": 2,
"congestion_candidate_ids": 1126,
"congestion_check_calls": 550,
"congestion_exact_pair_checks": 884,
"congestion_candidate_ids": 1025,
"congestion_check_calls": 505,
"congestion_exact_pair_checks": 829,
"congestion_penalty": 384.15999999999997,
"iteration": 4,
"net_attempts": [
{
"congestion_check_calls": 179,
"guidance_seed_present": true,
"net_id": "net_06",
"nodes_expanded": 46,
"pruned_closed_set": 7,
"pruned_cost": 15,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 30,
"guidance_seed_present": true,
@ -1163,26 +1155,6 @@
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 91,
"guidance_seed_present": true,
"net_id": "net_08",
"nodes_expanded": 18,
"pruned_closed_set": 8,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 26,
"guidance_seed_present": true,
"net_id": "net_09",
"nodes_expanded": 9,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 32,
"guidance_seed_present": true,
@ -1193,6 +1165,16 @@
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 179,
"guidance_seed_present": true,
"net_id": "net_06",
"nodes_expanded": 46,
"pruned_closed_set": 7,
"pruned_cost": 15,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 192,
"guidance_seed_present": true,
@ -1202,25 +1184,45 @@
"pruned_cost": 21,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 26,
"guidance_seed_present": true,
"net_id": "net_09",
"nodes_expanded": 9,
"pruned_closed_set": 1,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 46,
"guidance_seed_present": true,
"net_id": "net_08",
"nodes_expanded": 12,
"pruned_closed_set": 4,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
}
],
"nodes_expanded": 142,
"nodes_expanded": 136,
"routed_net_ids": [
"net_06",
"net_07",
"net_08",
"net_09",
"net_02",
"net_03"
"net_06",
"net_03",
"net_09",
"net_08"
],
"total_dynamic_collisions": 10
},
{
"completed_nets": 6,
"conflict_edges": 2,
"congestion_candidate_ids": 3377,
"congestion_check_calls": 1477,
"congestion_exact_pair_checks": 2666,
"congestion_candidate_ids": 419,
"congestion_check_calls": 171,
"congestion_exact_pair_checks": 287,
"congestion_penalty": 537.824,
"iteration": 5,
"net_attempts": [
@ -1234,16 +1236,6 @@
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 511,
"guidance_seed_present": true,
"net_id": "net_06",
"nodes_expanded": 137,
"pruned_closed_set": 13,
"pruned_cost": 73,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 86,
"guidance_seed_present": true,
@ -1255,21 +1247,31 @@
"reached_target": true
},
{
"congestion_check_calls": 795,
"congestion_check_calls": 0,
"guidance_seed_present": true,
"net_id": "net_06",
"nodes_expanded": 0,
"pruned_closed_set": 0,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
},
{
"congestion_check_calls": 0,
"guidance_seed_present": true,
"net_id": "net_03",
"nodes_expanded": 236,
"pruned_closed_set": 28,
"pruned_cost": 149,
"nodes_expanded": 0,
"pruned_closed_set": 0,
"pruned_cost": 0,
"pruned_hard_collision": 0,
"reached_target": true
}
],
"nodes_expanded": 406,
"nodes_expanded": 33,
"routed_net_ids": [
"net_07",
"net_06",
"net_02",
"net_06",
"net_03"
],
"total_dynamic_collisions": 10
@ -1277,74 +1279,76 @@
],
"metrics": {
"congestion_cache_hits": 8,
"congestion_cache_misses": 3881,
"congestion_candidate_ids": 9232,
"congestion_candidate_nets": 8483,
"congestion_candidate_precheck_hits": 2207,
"congestion_candidate_precheck_misses": 1793,
"congestion_candidate_precheck_skips": 111,
"congestion_check_calls": 3881,
"congestion_exact_pair_checks": 7234,
"congestion_grid_net_cache_hits": 2169,
"congestion_grid_net_cache_misses": 3238,
"congestion_grid_span_cache_hits": 1997,
"congestion_grid_span_cache_misses": 1628,
"congestion_cache_misses": 2530,
"congestion_candidate_ids": 6173,
"congestion_candidate_nets": 5869,
"congestion_candidate_precheck_hits": 1152,
"congestion_candidate_precheck_misses": 1460,
"congestion_candidate_precheck_skips": 74,
"congestion_check_calls": 2530,
"congestion_exact_pair_checks": 4800,
"congestion_grid_net_cache_hits": 1192,
"congestion_grid_net_cache_misses": 2676,
"congestion_grid_span_cache_hits": 1065,
"congestion_grid_span_cache_misses": 1366,
"congestion_lazy_requeues": 0,
"congestion_lazy_resolutions": 0,
"congestion_net_envelope_cache_hits": 2311,
"congestion_net_envelope_cache_misses": 3376,
"congestion_presence_cache_hits": 2443,
"congestion_presence_cache_misses": 2009,
"congestion_presence_skips": 452,
"danger_map_cache_hits": 14603,
"danger_map_cache_misses": 6814,
"danger_map_lookup_calls": 21417,
"danger_map_query_calls": 6814,
"danger_map_total_ns": 181736341,
"congestion_net_envelope_cache_hits": 1234,
"congestion_net_envelope_cache_misses": 2769,
"congestion_presence_cache_hits": 1302,
"congestion_presence_cache_misses": 1664,
"congestion_presence_skips": 354,
"danger_map_cache_hits": 11485,
"danger_map_cache_misses": 5474,
"danger_map_lookup_calls": 16959,
"danger_map_query_calls": 5474,
"danger_map_total_ns": 145721703,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 397,
"dynamic_path_objects_removed": 350,
"dynamic_tree_rebuilds": 0,
"guidance_bonus_applied": 8062.5,
"guidance_bonus_applied_bend90": 3187.5,
"guidance_bonus_applied": 7562.5,
"guidance_bonus_applied_bend90": 2937.5,
"guidance_bonus_applied_sbend": 250.0,
"guidance_bonus_applied_straight": 4625.0,
"guidance_match_moves": 129,
"guidance_match_moves_bend90": 51,
"guidance_bonus_applied_straight": 4375.0,
"guidance_match_moves": 121,
"guidance_match_moves_bend90": 47,
"guidance_match_moves_sbend": 4,
"guidance_match_moves_straight": 74,
"guidance_match_moves_straight": 70,
"hard_collision_cache_hits": 0,
"iteration_conflict_edges": 39,
"iteration_conflicting_nets": 39,
"iteration_reverified_nets": 60,
"iteration_reverify_calls": 6,
"move_cache_abs_hits": 1915,
"move_cache_abs_misses": 6136,
"move_cache_rel_hits": 5505,
"move_cache_rel_misses": 631,
"moves_added": 7121,
"moves_generated": 8051,
"late_phase_capped_fallbacks": 2,
"late_phase_capped_nets": 2,
"move_cache_abs_hits": 1304,
"move_cache_abs_misses": 4997,
"move_cache_rel_hits": 4419,
"move_cache_rel_misses": 578,
"moves_added": 5638,
"moves_generated": 6301,
"nets_carried_forward": 14,
"nets_reached_target": 46,
"nets_reached_target": 44,
"nets_routed": 46,
"nodes_expanded": 1582,
"nodes_expanded": 1203,
"pair_local_search_accepts": 2,
"pair_local_search_attempts": 3,
"pair_local_search_nodes_expanded": 39,
"pair_local_search_pairs_considered": 2,
"path_cost_calls": 0,
"pruned_closed_set": 399,
"pruned_cost": 531,
"pruned_closed_set": 354,
"pruned_cost": 309,
"pruned_hard_collision": 0,
"ray_cast_calls": 5077,
"ray_cast_calls_expand_forward": 1536,
"ray_cast_calls": 4059,
"ray_cast_calls_expand_forward": 1159,
"ray_cast_calls_expand_snap": 13,
"ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 3522,
"ray_cast_calls_straight_static": 2881,
"ray_cast_calls_visibility_build": 0,
"ray_cast_calls_visibility_query": 0,
"ray_cast_calls_visibility_tangent": 6,
"ray_cast_candidate_bounds": 316,
"ray_cast_candidate_bounds": 170,
"ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 10,
"refinement_candidate_side_extents": 0,
@ -1355,17 +1359,17 @@
"refinement_static_bounds_checked": 0,
"refinement_windows_considered": 0,
"route_iterations": 6,
"score_component_calls": 7670,
"score_component_total_ns": 205617403,
"score_component_calls": 5962,
"score_component_total_ns": 164785883,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1869,
"static_safe_cache_hits": 1276,
"static_tree_rebuilds": 1,
"timeout_events": 0,
"verify_dynamic_candidate_nets": 1906,
"verify_dynamic_exact_pair_checks": 571,
"verify_path_report_calls": 176,
"verify_static_buffer_ops": 813,
"verify_dynamic_candidate_nets": 1884,
"verify_dynamic_exact_pair_checks": 557,
"verify_path_report_calls": 174,
"verify_static_buffer_ops": 805,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 1,
@ -1376,7 +1380,7 @@
"visibility_point_queries": 0,
"visibility_tangent_candidate_corner_checks": 6,
"visibility_tangent_candidate_ray_tests": 6,
"visibility_tangent_candidate_scans": 1536,
"visibility_tangent_candidate_scans": 1159,
"warm_start_paths_built": 0,
"warm_start_paths_used": 0
},

View file

@ -1,6 +1,6 @@
# Iteration Trace
Generated at 2026-04-02T16:46:00-07:00 by `scripts/record_iteration_trace.py`.
Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_iteration_trace.py`.
## example_07_large_scale_routing_no_warm_start
@ -50,17 +50,17 @@ Results: 10 valid / 10 reached / 10 total.
| 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 |
| 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 |
| 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 |
| 4 | 384.2 | 6 | 6 | 2 | 10 | 142 | 550 | 1126 | 884 |
| 5 | 537.8 | 4 | 6 | 2 | 10 | 406 | 1477 | 3377 | 2666 |
| 4 | 384.2 | 6 | 6 | 2 | 10 | 136 | 505 | 1025 | 829 |
| 5 | 537.8 | 4 | 6 | 2 | 10 | 33 | 171 | 419 | 287 |
Top nets by iteration-attributed nodes expanded:
- `net_03`: 435
- `net_09`: 250
- `net_06`: 242
- `net_03`: 199
- `net_00`: 177
- `net_08`: 172
- `net_08`: 166
- `net_07`: 140
- `net_06`: 105
- `net_02`: 79
- `net_01`: 65
- `net_05`: 12
@ -68,11 +68,11 @@ Top nets by iteration-attributed nodes expanded:
Top nets by iteration-attributed congestion checks:
- `net_03`: 1434
- `net_06`: 893
- `net_03`: 639
- `net_07`: 454
- `net_08`: 328
- `net_06`: 382
- `net_02`: 290
- `net_08`: 283
- `net_09`: 178
- `net_01`: 135
- `net_00`: 82

View file

@ -3662,3 +3662,80 @@ Findings:
- The solved seed-42 no-warm canary stayed `10/10/10` and improved from `50` routed nets / `1303` nodes / `2921` congestion checks to `44` routed nets / `1258` nodes / `2736` congestion checks.
- The seed-43 no-warm canary stayed `10/10/10` and improved from `60` routed nets / `1691` nodes / `4330` congestion checks to `46` routed nets / `1582` nodes / `3881` congestion checks.
- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` while trimming slightly to `5` routed nets, `297` nodes, and `146` congestion checks.
## Step 67 route lighter late conflict nets first
Measured on 2026-04-02T17:16:54-07:00.
Findings:
- Kept the late all-reached conflict-set reroute, but now order those subset reroutes by the previous iteration's attributed work ascending so the lighter partner nets settle first and the heavier nets route later against a more stable late-phase context.
- The solved seed-42 no-warm canary stayed effectively flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks.
- The seed-43 no-warm canary stayed `10/10/10` and improved slightly from `1582` nodes / `3881` congestion checks to `1576` nodes / `3836` congestion checks.
- The remaining late-phase hotspot is still concentrated in `net_03` and `net_06`, especially the final 4-net iteration in the seed-43 trace.
## Step 68 pre-pair frontier diagnostics kept, scratch reroute rejected
Measured on 2026-04-02T17:57:48-07:00.
Findings:
- Kept a new opt-in `capture_pre_pair_frontier_trace` surface plus `scripts/record_pre_pair_frontier_trace.py`, and tracked the first seed-42 vs seed-43 artifacts in `docs/pre_pair_frontier_trace.json` and `docs/pre_pair_frontier_trace.md`.
- The final unresolved subset iteration is now explicit: seed `42` captures iteration `4` with routed nets `net_07`, `net_06`, `net_00`, `net_01`; seed `43` captures iteration `5` with routed nets `net_07`, `net_02`, `net_06`, `net_03`.
- The seed-43 heavy-net concentration is confirmed by the new trace: `net_03` and `net_06` account for most of the last unresolved iteration's work, and the hotspot-adjacent sampled prunes in that basin are closed-set dominated rather than hard-collision dominated.
- I also measured a bounded pre-pair scratch reroute for the two heaviest traced nets, but rejected it: it added runtime, produced `0` accepted repairs, and left the solved canaries at the same `1258 / 2736` and `1576 / 3836` node/check totals after revert.
## Step 69 cap heavy late-phase reroutes with incumbent fallback
Measured on 2026-04-02T18:20:00-07:00.
Findings:
- In the final all-reached 4-net subset iteration, the router now caps only the heavy reroute endpoints whose previous-iteration attributed work is already pathological, and falls back to their incumbent reached-target paths if the capped reroute does not finish cleanly.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves from `1576` nodes / `3836` congestion checks to `1459` nodes / `3455` congestion checks, with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity.
## Step 70 tighten late-phase cap from 128 to 64
Measured on 2026-04-02T18:33:00-07:00.
Findings:
- Tightened the bounded heavy-net late-phase reroute cap from `128` nodes to `64`, keeping the same incumbent fallback behavior and the same heavy-net selection rule.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1459` nodes / `3455` congestion checks to `1331` nodes / `3012` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity.
## Step 71 tighten late-phase cap from 64 to 32 after cap sweep
Measured on 2026-04-02T18:43:00-07:00.
Findings:
- Ran a cap sweep across `32`, `48`, `64`, `96`, `128`, and uncapped behavior for the two no-warm seeds. The winner was `32`: it preserved both `10/10/10` canaries and gave the best seed-43 node/check totals while leaving seed-42 flat.
- Landed that tighter cap in the router.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1331` nodes / `3012` congestion checks to `1267` nodes / `2813` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
## Step 72 tighten late-phase cap from 32 to 1 after floor sweep
Measured on 2026-04-02T18:45:00-07:00.
Findings:
- Extended the cap sweep below `32` and found the same pattern continued all the way down to `1`: seed-42 stayed flat because the cap never fires there, while seed-43 kept getting cheaper and still converged through the same incumbent-fallback path.
- Landed the tightest safe setting, `1`, so late pathological reroutes now act as a minimal probe before immediately falling back to the incumbent reached-target path if they do not finish cleanly.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1267` nodes / `2813` congestion checks to `1205` nodes / `2548` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
## Step 73 skip capped late-phase reroutes and carry the incumbent directly
Measured on 2026-04-02T18:52:00-07:00.
Findings:
- Characterization showed the two capped late seed-43 reroutes were pure churn even at a `1`-node cap: they always fell back to the incumbent reached-target path and pair-local repair still resolved the final pairs.
- Moved that behavior into `_route_net_once()` directly: when a late heavy reroute is already capped and has a reached-target incumbent fallback, the router now reinstalls the incumbent immediately instead of calling `route_astar()` for a doomed probe.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1205` nodes / `2548` congestion checks to `1203` nodes / `2530` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.

View file

@ -5,30 +5,23 @@ Generated on 2026-04-02 by `scripts/record_performance_baseline.py`.
The full machine-readable snapshot lives in `docs/performance_baseline.json`.
Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.
The default baseline table below covers the standard example corpus only. The heavier no-warm `example_07` canaries remain performance-only and are tracked through targeted diffs plus the trace artifacts.
Use `scripts/characterize_pair_local_search.py` for the tracked no-warm sweep in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`.
Use `scripts/record_iteration_trace.py` for the current seed-42 vs seed-43 negotiated-congestion attribution in `docs/iteration_trace.json` and `docs/iteration_trace.md`.
| 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.0038 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 4 |
| example_02_congestion_resolution | 0.3614 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 38 |
| example_03_locked_paths | 0.1953 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 16 |
| example_04_sbends_and_radii | 0.0277 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 8 |
| example_05_orientation_stress | 0.2537 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 17 |
| example_06_bend_collision_models | 0.2103 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 12 |
| example_07_large_scale_routing | 0.2074 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 40 |
| example_08_custom_bend_geometry | 0.0186 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 8 |
| example_09_unroutable_best_effort | 0.0079 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
| example_01_simple_route | 0.0037 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 5 |
| example_02_congestion_resolution | 0.3361 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 41 |
| example_03_locked_paths | 0.1877 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 18 |
| example_04_sbends_and_radii | 0.0269 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 10 |
| example_05_orientation_stress | 0.2311 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 20 |
| example_06_bend_collision_models | 0.1988 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 15 |
| example_07_large_scale_routing | 0.2088 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 50 |
| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 10 |
| example_09_unroutable_best_effort | 0.0057 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
## Full Counter Set
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.
These counters are currently observational only and are not enforced as CI regression gates.
The current accepted branch keeps the post-loop pair-local scratch reroute and now also narrows late negotiated reroutes to the current conflict set once all nets already reach target and only a few conflict edges remain. That keeps both no-warm `example_07` seeds at `10/10/10` while reducing main-loop work before pair-local repair.
Tracked metric keys:
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded, late_phase_capped_nets, late_phase_capped_fallbacks

View file

@ -3,7 +3,7 @@
"generator": "scripts/record_performance_baseline.py",
"scenarios": [
{
"duration_s": 0.003825429128482938,
"duration_s": 0.003715757979080081,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -47,6 +47,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 1,
"iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 10,
"move_cache_rel_hits": 0,
@ -85,7 +87,7 @@
"refinement_windows_considered": 0,
"route_iterations": 1,
"score_component_calls": 11,
"score_component_total_ns": 16571,
"score_component_total_ns": 16864,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
@ -93,7 +95,7 @@
"timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 4,
"verify_path_report_calls": 5,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -115,7 +117,7 @@
"valid_results": 1
},
{
"duration_s": 0.36141274496912956,
"duration_s": 0.33605348505079746,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -159,6 +161,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 3,
"iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 12,
"move_cache_abs_misses": 1401,
"move_cache_rel_hits": 1293,
@ -197,15 +201,15 @@
"refinement_windows_considered": 10,
"route_iterations": 1,
"score_component_calls": 976,
"score_component_total_ns": 1143187,
"score_component_total_ns": 1109505,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 2,
"timeout_events": 0,
"verify_dynamic_candidate_nets": 88,
"verify_dynamic_exact_pair_checks": 86,
"verify_path_report_calls": 38,
"verify_dynamic_candidate_nets": 92,
"verify_dynamic_exact_pair_checks": 90,
"verify_path_report_calls": 41,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -227,7 +231,7 @@
"valid_results": 3
},
{
"duration_s": 0.19532882701605558,
"duration_s": 0.18771230895072222,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -271,6 +275,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2,
"iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 903,
"move_cache_rel_hits": 821,
@ -309,16 +315,16 @@
"refinement_windows_considered": 2,
"route_iterations": 2,
"score_component_calls": 504,
"score_component_total_ns": 565663,
"score_component_total_ns": 546567,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 1,
"timeout_events": 0,
"verify_dynamic_candidate_nets": 9,
"verify_dynamic_exact_pair_checks": 9,
"verify_path_report_calls": 16,
"verify_static_buffer_ops": 81,
"verify_dynamic_candidate_nets": 10,
"verify_dynamic_exact_pair_checks": 10,
"verify_path_report_calls": 18,
"verify_static_buffer_ops": 90,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 2,
@ -339,7 +345,7 @@
"valid_results": 2
},
{
"duration_s": 0.027705274987965822,
"duration_s": 0.026945222169160843,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -383,6 +389,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2,
"iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1,
"move_cache_abs_misses": 122,
"move_cache_rel_hits": 80,
@ -421,15 +429,15 @@
"refinement_windows_considered": 0,
"route_iterations": 1,
"score_component_calls": 90,
"score_component_total_ns": 96756,
"score_component_total_ns": 97710,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1,
"static_tree_rebuilds": 1,
"timeout_events": 0,
"verify_dynamic_candidate_nets": 9,
"verify_dynamic_candidate_nets": 12,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 8,
"verify_path_report_calls": 10,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -451,7 +459,7 @@
"valid_results": 2
},
{
"duration_s": 0.25367443496361375,
"duration_s": 0.23108969815075397,
"metrics": {
"congestion_cache_hits": 3,
"congestion_cache_misses": 146,
@ -495,6 +503,8 @@
"iteration_conflicting_nets": 2,
"iteration_reverified_nets": 6,
"iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 374,
"move_cache_abs_misses": 1306,
"move_cache_rel_hits": 1204,
@ -533,7 +543,7 @@
"refinement_windows_considered": 0,
"route_iterations": 2,
"score_component_calls": 1234,
"score_component_total_ns": 1311211,
"score_component_total_ns": 1223569,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 8,
@ -541,7 +551,7 @@
"timeout_events": 0,
"verify_dynamic_candidate_nets": 8,
"verify_dynamic_exact_pair_checks": 12,
"verify_path_report_calls": 17,
"verify_path_report_calls": 20,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -563,7 +573,7 @@
"valid_results": 3
},
{
"duration_s": 0.21031348290853202,
"duration_s": 0.19879506202414632,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -589,7 +599,7 @@
"danger_map_cache_misses": 731,
"danger_map_lookup_calls": 1914,
"danger_map_query_calls": 731,
"danger_map_total_ns": 19983976,
"danger_map_total_ns": 19050142,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 54,
"dynamic_path_objects_removed": 36,
@ -607,6 +617,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 3,
"iteration_reverify_calls": 3,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 186,
"move_cache_abs_misses": 840,
"move_cache_rel_hits": 702,
@ -645,7 +657,7 @@
"refinement_windows_considered": 0,
"route_iterations": 3,
"score_component_calls": 842,
"score_component_total_ns": 22474166,
"score_component_total_ns": 21353240,
"static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 3,
"static_safe_cache_hits": 141,
@ -653,8 +665,8 @@
"timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 12,
"verify_static_buffer_ops": 72,
"verify_path_report_calls": 15,
"verify_static_buffer_ops": 90,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 3,
@ -675,7 +687,7 @@
"valid_results": 3
},
{
"duration_s": 0.20740868314169347,
"duration_s": 0.20880168909206986,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -701,7 +713,7 @@
"danger_map_cache_misses": 448,
"danger_map_lookup_calls": 681,
"danger_map_query_calls": 448,
"danger_map_total_ns": 11224403,
"danger_map_total_ns": 11025527,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 132,
"dynamic_path_objects_removed": 88,
@ -719,6 +731,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 10,
"iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 6,
"move_cache_abs_misses": 366,
"move_cache_rel_hits": 275,
@ -757,16 +771,16 @@
"refinement_windows_considered": 0,
"route_iterations": 1,
"score_component_calls": 291,
"score_component_total_ns": 12117666,
"score_component_total_ns": 11875928,
"static_net_tree_rebuilds": 10,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 6,
"static_tree_rebuilds": 10,
"timeout_events": 0,
"verify_dynamic_candidate_nets": 370,
"verify_dynamic_exact_pair_checks": 56,
"verify_path_report_calls": 40,
"verify_static_buffer_ops": 176,
"verify_dynamic_candidate_nets": 476,
"verify_dynamic_exact_pair_checks": 72,
"verify_path_report_calls": 50,
"verify_static_buffer_ops": 220,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 10,
@ -787,7 +801,7 @@
"valid_results": 10
},
{
"duration_s": 0.018604618962854147,
"duration_s": 0.017696003895252943,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -831,6 +845,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2,
"iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 2,
"move_cache_abs_misses": 76,
"move_cache_rel_hits": 32,
@ -869,7 +885,7 @@
"refinement_windows_considered": 0,
"route_iterations": 2,
"score_component_calls": 72,
"score_component_total_ns": 87655,
"score_component_total_ns": 87742,
"static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 2,
@ -877,7 +893,7 @@
"timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 8,
"verify_path_report_calls": 10,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -899,7 +915,7 @@
"valid_results": 2
},
{
"duration_s": 0.00794802000746131,
"duration_s": 0.005660973023623228,
"metrics": {
"congestion_cache_hits": 0,
"congestion_cache_misses": 0,
@ -925,7 +941,7 @@
"danger_map_cache_misses": 20,
"danger_map_lookup_calls": 30,
"danger_map_query_calls": 20,
"danger_map_total_ns": 675454,
"danger_map_total_ns": 515133,
"dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 2,
"dynamic_path_objects_removed": 1,
@ -943,6 +959,8 @@
"iteration_conflicting_nets": 0,
"iteration_reverified_nets": 0,
"iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 0,
"move_cache_abs_misses": 16,
"move_cache_rel_hits": 2,
@ -981,7 +999,7 @@
"refinement_windows_considered": 0,
"route_iterations": 1,
"score_component_calls": 14,
"score_component_total_ns": 722637,
"score_component_total_ns": 554809,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
# Pre-Pair Frontier Trace
Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_pre_pair_frontier_trace.py`.
## example_07_large_scale_routing_no_warm_start
Results: 10 valid / 10 reached / 10 total.
Captured iteration: `4`
Conflict edges: `(('net_01', 'net_02'), ('net_06', 'net_07'))`
| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |
| :-- | --: | --: | --: | --: | --: | :--: | --: |
| net_07 | 7 | 30 | 1 | 0 | 0 | yes | 0 |
| net_06 | 46 | 179 | 7 | 15 | 0 | yes | 5 |
| net_00 | 10 | 43 | 1 | 0 | 0 | yes | 0 |
| net_01 | 18 | 80 | 3 | 0 | 0 | yes | 0 |
Frontier prune totals by reason:
- `closed_set`: 5
- `hard_collision`: 0
- `self_collision`: 0
- `cost`: 0
## example_07_large_scale_routing_no_warm_start_seed43
Results: 10 valid / 10 reached / 10 total.
Captured iteration: `5`
Conflict edges: `(('net_02', 'net_03'), ('net_06', 'net_07'))`
| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |
| :-- | --: | --: | --: | --: | --: | :--: | --: |
| net_07 | 16 | 85 | 3 | 0 | 0 | yes | 2 |
| net_02 | 17 | 86 | 4 | 0 | 0 | yes | 3 |
| net_06 | 0 | 0 | 0 | 0 | 0 | yes | 8 |
| net_03 | 0 | 0 | 0 | 0 | 0 | yes | 12 |
Frontier prune totals by reason:
- `closed_set`: 25
- `hard_collision`: 0
- `self_collision`: 0
- `cost`: 0

View file

@ -22,6 +22,8 @@ from .results import ( # noqa: PLC0414
IterationTraceEntry as IterationTraceEntry,
NetConflictTrace as NetConflictTrace,
NetFrontierTrace as NetFrontierTrace,
PrePairFrontierTraceEntry as PrePairFrontierTraceEntry,
PrePairNetTrace as PrePairNetTrace,
RoutingResult as RoutingResult,
RoutingRunResult as RoutingRunResult,
)
@ -49,6 +51,7 @@ def route(
expanded_nodes=tuple(finder.accumulated_expanded_nodes),
conflict_trace=tuple(finder.conflict_trace),
frontier_trace=tuple(finder.frontier_trace),
pre_pair_frontier_trace=finder.pre_pair_frontier_trace,
iteration_trace=tuple(finder.iteration_trace),
)
@ -67,6 +70,8 @@ __all__ = [
"FrontierPruneSample",
"IterationNetAttemptTrace",
"IterationTraceEntry",
"PrePairFrontierTraceEntry",
"PrePairNetTrace",
"RefinementOptions",
"RoutingOptions",
"RoutingProblem",

View file

@ -108,6 +108,7 @@ class DiagnosticsOptions:
capture_conflict_trace: bool = False
capture_frontier_trace: bool = False
capture_iteration_trace: bool = False
capture_pre_pair_frontier_trace: bool = False
@dataclass(frozen=True, slots=True)

View file

@ -78,6 +78,26 @@ class NetFrontierTrace:
samples: tuple[FrontierPruneSample, ...] = ()
@dataclass(frozen=True, slots=True)
class PrePairNetTrace:
net_id: str
nodes_expanded: int
congestion_check_calls: int
pruned_closed_set: int
pruned_cost: int
pruned_hard_collision: int
guidance_seed_present: bool
frontier: NetFrontierTrace
@dataclass(frozen=True, slots=True)
class PrePairFrontierTraceEntry:
iteration: int
routed_net_ids: tuple[str, ...]
conflict_edges: tuple[tuple[str, str], ...]
nets: tuple[PrePairNetTrace, ...]
@dataclass(frozen=True, slots=True)
class IterationNetAttemptTrace:
net_id: str
@ -210,6 +230,8 @@ class RouteMetrics:
pair_local_search_attempts: int
pair_local_search_accepts: int
pair_local_search_nodes_expanded: int
late_phase_capped_nets: int
late_phase_capped_fallbacks: int
@dataclass(frozen=True, slots=True)
@ -258,4 +280,5 @@ class RoutingRunResult:
expanded_nodes: tuple[tuple[int, int, int], ...] = ()
conflict_trace: tuple[ConflictTraceEntry, ...] = ()
frontier_trace: tuple[NetFrontierTrace, ...] = ()
pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None
iteration_trace: tuple[IterationTraceEntry, ...] = ()

View file

@ -258,6 +258,8 @@ class AStarMetrics:
"total_pair_local_search_attempts",
"total_pair_local_search_accepts",
"total_pair_local_search_nodes_expanded",
"total_late_phase_capped_nets",
"total_late_phase_capped_fallbacks",
"last_expanded_nodes",
"nodes_expanded",
"moves_generated",
@ -371,6 +373,8 @@ class AStarMetrics:
self.total_pair_local_search_attempts = 0
self.total_pair_local_search_accepts = 0
self.total_pair_local_search_nodes_expanded = 0
self.total_late_phase_capped_nets = 0
self.total_late_phase_capped_fallbacks = 0
self.last_expanded_nodes: list[tuple[int, int, int]] = []
self.nodes_expanded = 0
self.moves_generated = 0
@ -483,6 +487,8 @@ class AStarMetrics:
self.total_pair_local_search_attempts = 0
self.total_pair_local_search_accepts = 0
self.total_pair_local_search_nodes_expanded = 0
self.total_late_phase_capped_nets = 0
self.total_late_phase_capped_fallbacks = 0
def reset_per_route(self) -> None:
self.nodes_expanded = 0
@ -598,6 +604,8 @@ class AStarMetrics:
pair_local_search_attempts=self.total_pair_local_search_attempts,
pair_local_search_accepts=self.total_pair_local_search_accepts,
pair_local_search_nodes_expanded=self.total_pair_local_search_nodes_expanded,
late_phase_capped_nets=self.total_late_phase_capped_nets,
late_phase_capped_fallbacks=self.total_late_phase_capped_fallbacks,
)

View file

@ -15,6 +15,8 @@ from inire.results import (
IterationTraceEntry,
NetConflictTrace,
NetFrontierTrace,
PrePairFrontierTraceEntry,
PrePairNetTrace,
RoutingOutcome,
RoutingReport,
RoutingResult,
@ -53,6 +55,8 @@ class _RoutingState:
last_conflict_edge_count: int
repeated_conflict_count: int
pair_local_plateau_count: int
recent_attempt_work: dict[str, int]
pre_pair_candidate: _PrePairCandidate | None
@dataclass(slots=True)
@ -68,6 +72,14 @@ class _PairLocalTarget:
net_ids: tuple[str, str]
@dataclass(frozen=True, slots=True)
class _PrePairCandidate:
iteration: int
routed_net_ids: tuple[str, ...]
conflict_edges: tuple[tuple[str, str], ...]
net_attempts: tuple[IterationNetAttemptTrace, ...]
_ITERATION_TRACE_TOTALS = (
"nodes_expanded",
"congestion_check_calls",
@ -92,6 +104,7 @@ class PathFinder:
"accumulated_expanded_nodes",
"conflict_trace",
"frontier_trace",
"pre_pair_frontier_trace",
"iteration_trace",
)
@ -110,6 +123,7 @@ class PathFinder:
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
self.conflict_trace: list[ConflictTraceEntry] = []
self.frontier_trace: list[NetFrontierTrace] = []
self.pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None
self.iteration_trace: list[IterationTraceEntry] = []
def _metric_total(self, metric_name: str) -> int:
@ -219,6 +233,8 @@ class PathFinder:
last_conflict_edge_count=0,
repeated_conflict_count=0,
pair_local_plateau_count=0,
recent_attempt_work={},
pre_pair_candidate=None,
)
if state.initial_paths is None and congestion.warm_start_enabled:
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
@ -399,6 +415,93 @@ class PathFinder:
return tuple(hotspot_bounds)
def _capture_single_frontier_trace(
self,
state: _RoutingState,
net_id: str,
result: RoutingResult,
hotspot_bounds: tuple[tuple[float, float, float, float], ...],
) -> NetFrontierTrace:
if not hotspot_bounds:
return NetFrontierTrace(
net_id=net_id,
hotspot_bounds=(),
pruned_closed_set=0,
pruned_hard_collision=0,
pruned_self_collision=0,
pruned_cost=0,
)
original_metrics = self.metrics
original_context_metrics = self.context.metrics
original_engine_metrics = self.context.cost_evaluator.collision_engine.metrics
original_danger_metrics = None
if self.context.cost_evaluator.danger_map is not None:
original_danger_metrics = self.context.cost_evaluator.danger_map.metrics
try:
scratch_metrics = AStarMetrics()
self.context.metrics = scratch_metrics
self.context.cost_evaluator.collision_engine.metrics = scratch_metrics
if self.context.cost_evaluator.danger_map is not None:
self.context.cost_evaluator.danger_map.metrics = scratch_metrics
guidance_seed = result.as_seed().segments if result.path else None
guidance_bonus = 0.0
if guidance_seed:
guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25)
collector = FrontierTraceCollector(hotspot_bounds=hotspot_bounds)
run_config = SearchRunConfig.from_options(
self.context.options,
return_partial=True,
store_expanded=False,
guidance_seed=guidance_seed,
guidance_bonus=guidance_bonus,
frontier_trace=collector,
self_collision_check=(net_id in state.needs_self_collision_check),
node_limit=self.context.options.search.node_limit,
)
self.context.cost_evaluator.collision_engine.remove_path(net_id)
try:
route_astar(
state.net_specs[net_id].start,
state.net_specs[net_id].target,
state.net_specs[net_id].width,
context=self.context,
metrics=scratch_metrics,
net_id=net_id,
config=run_config,
)
finally:
if result.path:
self._install_path(net_id, result.path)
return NetFrontierTrace(
net_id=net_id,
hotspot_bounds=hotspot_bounds,
pruned_closed_set=collector.pruned_closed_set,
pruned_hard_collision=collector.pruned_hard_collision,
pruned_self_collision=collector.pruned_self_collision,
pruned_cost=collector.pruned_cost,
samples=tuple(
FrontierPruneSample(
reason=reason, # type: ignore[arg-type]
move_type=move_type,
hotspot_index=hotspot_index,
parent_state=parent_state,
end_state=end_state,
)
for reason, move_type, hotspot_index, parent_state, end_state in collector.samples
),
)
finally:
self.metrics = original_metrics
self.context.metrics = original_context_metrics
self.context.cost_evaluator.collision_engine.metrics = original_engine_metrics
if self.context.cost_evaluator.danger_map is not None:
self.context.cost_evaluator.danger_map.metrics = original_danger_metrics
def _analyze_results(
self,
ordered_net_ids: Sequence[str],
@ -477,90 +580,26 @@ class PathFinder:
capture_component_conflicts=True,
count_iteration_metrics=False,
)
for net_id in state.ordered_net_ids:
result = state.results.get(net_id)
detail = details_by_net.get(net_id)
if result is None or detail is None or not result.reached_target:
continue
if detail.report.dynamic_collision_count == 0 or not detail.component_conflicts:
continue
original_metrics = self.metrics
original_context_metrics = self.context.metrics
original_engine_metrics = self.context.cost_evaluator.collision_engine.metrics
original_danger_metrics = None
if self.context.cost_evaluator.danger_map is not None:
original_danger_metrics = self.context.cost_evaluator.danger_map.metrics
hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, details_by_net)
if not hotspot_bounds:
continue
try:
for net_id in state.ordered_net_ids:
result = state.results.get(net_id)
detail = details_by_net.get(net_id)
if result is None or detail is None or not result.reached_target:
continue
if detail.report.dynamic_collision_count == 0 or not detail.component_conflicts:
continue
hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, details_by_net)
if not hotspot_bounds:
continue
scratch_metrics = AStarMetrics()
self.context.metrics = scratch_metrics
self.context.cost_evaluator.collision_engine.metrics = scratch_metrics
if self.context.cost_evaluator.danger_map is not None:
self.context.cost_evaluator.danger_map.metrics = scratch_metrics
guidance_seed = result.as_seed().segments if result.path else None
guidance_bonus = 0.0
if guidance_seed:
guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25)
collector = FrontierTraceCollector(hotspot_bounds=hotspot_bounds)
run_config = SearchRunConfig.from_options(
self.context.options,
return_partial=True,
store_expanded=False,
guidance_seed=guidance_seed,
guidance_bonus=guidance_bonus,
frontier_trace=collector,
self_collision_check=(net_id in state.needs_self_collision_check),
node_limit=self.context.options.search.node_limit,
self.frontier_trace.append(
self._capture_single_frontier_trace(
state,
net_id,
result,
hotspot_bounds,
)
self.context.cost_evaluator.collision_engine.remove_path(net_id)
try:
route_astar(
state.net_specs[net_id].start,
state.net_specs[net_id].target,
state.net_specs[net_id].width,
context=self.context,
metrics=scratch_metrics,
net_id=net_id,
config=run_config,
)
finally:
if result.path:
self._install_path(net_id, result.path)
self.frontier_trace.append(
NetFrontierTrace(
net_id=net_id,
hotspot_bounds=hotspot_bounds,
pruned_closed_set=collector.pruned_closed_set,
pruned_hard_collision=collector.pruned_hard_collision,
pruned_self_collision=collector.pruned_self_collision,
pruned_cost=collector.pruned_cost,
samples=tuple(
FrontierPruneSample(
reason=reason, # type: ignore[arg-type]
move_type=move_type,
hotspot_index=hotspot_index,
parent_state=parent_state,
end_state=end_state,
)
for reason, move_type, hotspot_index, parent_state, end_state in collector.samples
),
)
)
finally:
self.metrics = original_metrics
self.context.metrics = original_context_metrics
self.context.cost_evaluator.collision_engine.metrics = original_engine_metrics
if self.context.cost_evaluator.danger_map is not None:
self.context.cost_evaluator.danger_map.metrics = original_danger_metrics
)
def _whole_set_is_better(
self,
@ -673,6 +712,50 @@ class PathFinder:
metrics=AStarMetrics(),
)
def _materialize_pre_pair_frontier_trace(
self,
state: _RoutingState,
results: dict[str, RoutingResult],
details_by_net: dict[str, PathVerificationDetail],
review: _IterationReview,
) -> PrePairFrontierTraceEntry | None:
candidate = state.pre_pair_candidate
if candidate is None:
return None
result_by_net = dict(results)
detail_by_net = dict(details_by_net)
nets: list[PrePairNetTrace] = []
attempt_by_net = {attempt.net_id: attempt for attempt in candidate.net_attempts}
for net_id in candidate.routed_net_ids:
attempt = attempt_by_net.get(net_id)
result = result_by_net.get(net_id)
detail = detail_by_net.get(net_id)
if attempt is None or result is None or detail is None or not result.reached_target:
continue
hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, detail_by_net)
nets.append(
PrePairNetTrace(
net_id=net_id,
nodes_expanded=attempt.nodes_expanded,
congestion_check_calls=attempt.congestion_check_calls,
pruned_closed_set=attempt.pruned_closed_set,
pruned_cost=attempt.pruned_cost,
pruned_hard_collision=attempt.pruned_hard_collision,
guidance_seed_present=attempt.guidance_seed_present,
frontier=self._capture_single_frontier_trace(state, net_id, result, hotspot_bounds),
)
)
if not nets:
return None
return PrePairFrontierTraceEntry(
iteration=candidate.iteration,
routed_net_ids=candidate.routed_net_ids,
conflict_edges=candidate.conflict_edges,
nets=tuple(nets),
)
def _run_pair_local_attempt(
self,
state: _RoutingState,
@ -780,12 +863,17 @@ class PathFinder:
state: _RoutingState,
iteration: int,
net_id: str,
*,
node_limit_override: int | None = None,
incumbent_fallback: RoutingResult | None = None,
) -> tuple[RoutingResult, bool]:
search = self.context.options.search
congestion = self.context.options.congestion
diagnostics = self.context.options.diagnostics
net = state.net_specs[net_id]
self.metrics.total_nets_routed += 1
if node_limit_override is not None:
self.metrics.total_late_phase_capped_nets += 1
self.context.cost_evaluator.collision_engine.remove_path(net_id)
guidance_seed_present = False
@ -808,6 +896,16 @@ class PathFinder:
guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25)
guidance_seed_present = True
if (
node_limit_override is not None
and incumbent_fallback is not None
and incumbent_fallback.reached_target
and incumbent_fallback.path
):
self.metrics.total_late_phase_capped_fallbacks += 1
self._install_path(net_id, incumbent_fallback.path)
return incumbent_fallback, guidance_seed_present
run_config = SearchRunConfig.from_options(
self.context.options,
bend_collision_type=coll_model,
@ -817,7 +915,7 @@ class PathFinder:
guidance_bonus=guidance_bonus,
skip_congestion=skip_congestion,
self_collision_check=(net_id in state.needs_self_collision_check),
node_limit=search.node_limit,
node_limit=search.node_limit if node_limit_override is None else node_limit_override,
)
path = route_astar(
net.start,
@ -833,9 +931,17 @@ class PathFinder:
state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
if not path:
if incumbent_fallback is not None and incumbent_fallback.reached_target and incumbent_fallback.path:
self.metrics.total_late_phase_capped_fallbacks += 1
self._install_path(net_id, incumbent_fallback.path)
return incumbent_fallback, guidance_seed_present
return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present
reached_target = path[-1].end_port == net.target
if not reached_target and incumbent_fallback is not None and incumbent_fallback.reached_target and incumbent_fallback.path:
self.metrics.total_late_phase_capped_fallbacks += 1
self._install_path(net_id, incumbent_fallback.path)
return incumbent_fallback, guidance_seed_present
if reached_target:
self.metrics.total_nets_reached_target += 1
report = None
@ -872,9 +978,27 @@ class PathFinder:
iteration_penalty = self.context.congestion_penalty
routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids]
capped_net_ids: set[str] = set()
if len(reroute_net_ids) < len(state.ordered_net_ids) and state.recent_attempt_work:
order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)}
routed_net_ids.sort(key=lambda net_id: (state.recent_attempt_work.get(net_id, 0), order_index[net_id]))
if (
len(routed_net_ids) == 4
and state.best_conflict_edges <= 2
and len(state.results) == len(state.ordered_net_ids)
and all(result.reached_target for result in state.results.values())
):
heavy_net_ids = sorted(
routed_net_ids,
key=lambda net_id: (-state.recent_attempt_work.get(net_id, 0), order_index[net_id]),
)[:2]
capped_net_ids = {
net_id for net_id in heavy_net_ids if state.recent_attempt_work.get(net_id, 0) >= 200
}
self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids)
iteration_before = {}
attempt_traces: list[IterationNetAttemptTrace] = []
attempt_work: dict[str, int] = {}
if diagnostics.capture_iteration_trace:
iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS)
@ -883,28 +1007,51 @@ class PathFinder:
self.metrics.total_timeout_events += 1
return None
attempt_before = {}
if diagnostics.capture_iteration_trace:
attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
result, guidance_seed_present = self._route_net_once(state, iteration, net_id)
attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
node_limit_override = None
incumbent_fallback = None
if net_id in capped_net_ids:
node_limit_override = min(self.context.options.search.node_limit, 1)
incumbent_fallback = state.results.get(net_id)
result, guidance_seed_present = self._route_net_once(
state,
iteration,
net_id,
node_limit_override=node_limit_override,
incumbent_fallback=incumbent_fallback,
)
state.results[net_id] = result
if diagnostics.capture_iteration_trace:
attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
deltas = self._metric_deltas(attempt_before, attempt_after)
attempt_traces.append(
IterationNetAttemptTrace(
net_id=net_id,
reached_target=result.reached_target,
nodes_expanded=deltas["nodes_expanded"],
congestion_check_calls=deltas["congestion_check_calls"],
pruned_closed_set=deltas["pruned_closed_set"],
pruned_cost=deltas["pruned_cost"],
pruned_hard_collision=deltas["pruned_hard_collision"],
guidance_seed_present=guidance_seed_present,
)
attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
deltas = self._metric_deltas(attempt_before, attempt_after)
attempt_work[net_id] = deltas["nodes_expanded"] + deltas["congestion_check_calls"]
attempt_traces.append(
IterationNetAttemptTrace(
net_id=net_id,
reached_target=result.reached_target,
nodes_expanded=deltas["nodes_expanded"],
congestion_check_calls=deltas["congestion_check_calls"],
pruned_closed_set=deltas["pruned_closed_set"],
pruned_cost=deltas["pruned_cost"],
pruned_hard_collision=deltas["pruned_hard_collision"],
guidance_seed_present=guidance_seed_present,
)
)
state.recent_attempt_work = attempt_work
review = self._reverify_iteration_results(state)
all_reached_target = (
len(state.results) == len(state.ordered_net_ids)
and all(result.reached_target for result in state.results.values())
)
if all_reached_target and len(reroute_net_ids) < len(state.ordered_net_ids) and review.conflict_edges:
state.pre_pair_candidate = _PrePairCandidate(
iteration=iteration,
routed_net_ids=tuple(routed_net_ids),
conflict_edges=tuple(sorted(review.conflict_edges)),
net_attempts=tuple(attempt_traces),
)
else:
state.pre_pair_candidate = None
if diagnostics.capture_iteration_trace:
iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS)
deltas = self._metric_deltas(iteration_before, iteration_after)
@ -1072,6 +1219,7 @@ class PathFinder:
self.accumulated_expanded_nodes = []
self.conflict_trace = []
self.frontier_trace = []
self.pre_pair_frontier_trace = None
self.iteration_trace = []
self.metrics.reset_totals()
self.metrics.reset_per_route()
@ -1080,13 +1228,17 @@ class PathFinder:
timed_out = self._run_iterations(state, iteration_callback)
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
self._restore_best_iteration(state)
capture_component_conflicts = (
self.context.options.diagnostics.capture_conflict_trace
or self.context.options.diagnostics.capture_pre_pair_frontier_trace
)
state.results, details_by_net, review = self._analyze_results(
state.ordered_net_ids,
state.results,
capture_component_conflicts=capture_component_conflicts,
count_iteration_metrics=False,
)
if self.context.options.diagnostics.capture_conflict_trace:
state.results, details_by_net, review = self._analyze_results(
state.ordered_net_ids,
state.results,
capture_component_conflicts=True,
count_iteration_metrics=False,
)
self._capture_conflict_trace_entry(
state,
stage="restored_best",
@ -1095,6 +1247,13 @@ class PathFinder:
details_by_net=details_by_net,
review=review,
)
if self.context.options.diagnostics.capture_pre_pair_frontier_trace:
self.pre_pair_frontier_trace = self._materialize_pre_pair_frontier_trace(
state,
state.results,
details_by_net,
review,
)
if timed_out:
final_results = self._verify_results(state)

View file

@ -91,6 +91,7 @@ def _make_run_result(
expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes),
conflict_trace=tuple(pathfinder.conflict_trace),
frontier_trace=tuple(pathfinder.frontier_trace),
pre_pair_frontier_trace=pathfinder.pre_pair_frontier_trace,
iteration_trace=tuple(pathfinder.iteration_trace),
)
@ -453,6 +454,7 @@ def _build_example_07_variant_stack(
capture_conflict_trace: bool = False,
capture_frontier_trace: bool = False,
capture_iteration_trace: bool = False,
capture_pre_pair_frontier_trace: bool = False,
) -> tuple[CostEvaluator, AStarMetrics, PathFinder]:
bounds = (0, 0, 1000, 1000)
obstacles = [
@ -496,6 +498,7 @@ def _build_example_07_variant_stack(
"capture_conflict_trace": capture_conflict_trace,
"capture_frontier_trace": capture_frontier_trace,
"capture_iteration_trace": capture_iteration_trace,
"capture_pre_pair_frontier_trace": capture_pre_pair_frontier_trace,
"shuffle_nets": True,
"seed": seed,
"warm_start_enabled": warm_start_enabled,
@ -512,6 +515,7 @@ def _run_example_07_variant(
capture_conflict_trace: bool = False,
capture_frontier_trace: bool = False,
capture_iteration_trace: bool = False,
capture_pre_pair_frontier_trace: bool = False,
) -> RoutingRunResult:
evaluator, metrics, pathfinder = _build_example_07_variant_stack(
num_nets=num_nets,
@ -520,6 +524,7 @@ def _run_example_07_variant(
capture_conflict_trace=capture_conflict_trace,
capture_frontier_trace=capture_frontier_trace,
capture_iteration_trace=capture_iteration_trace,
capture_pre_pair_frontier_trace=capture_pre_pair_frontier_trace,
)
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
@ -560,6 +565,7 @@ def _trace_example_07_variant(
capture_conflict_trace=True,
capture_frontier_trace=True,
capture_iteration_trace=True,
capture_pre_pair_frontier_trace=True,
)

View file

@ -3,6 +3,7 @@ import importlib
import pytest
from shapely.geometry import box
import inire.router._router as router_module
from inire import (
CongestionOptions,
DiagnosticsOptions,
@ -54,6 +55,7 @@ def test_route_problem_smoke() -> None:
assert run.results_by_net["net1"].is_valid
assert run.conflict_trace == ()
assert run.frontier_trace == ()
assert run.pre_pair_frontier_trace is None
assert run.iteration_trace == ()
@ -125,6 +127,8 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
assert run.metrics.pair_local_search_attempts >= 0
assert run.metrics.pair_local_search_accepts >= 0
assert run.metrics.pair_local_search_nodes_expanded >= 0
assert run.metrics.late_phase_capped_nets >= 0
assert run.metrics.late_phase_capped_fallbacks >= 0
def test_iteration_callback_observes_reverified_conflicts() -> None:
@ -306,6 +310,35 @@ def test_capture_frontier_trace_preserves_route_outputs() -> None:
assert {trace.net_id for trace in run_with_trace.frontier_trace} == {"horizontal", "vertical"}
def test_capture_pre_pair_frontier_trace_preserves_route_outputs() -> None:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),
nets=(
NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0),
NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0),
),
)
base_options = RoutingOptions(
congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False),
refinement=RefinementOptions(enabled=False),
)
run_without_trace = route(problem, options=base_options)
run_with_trace = route(
problem,
options=RoutingOptions(
congestion=base_options.congestion,
refinement=base_options.refinement,
diagnostics=DiagnosticsOptions(capture_pre_pair_frontier_trace=True),
),
)
assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == {
net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items()
}
assert run_with_trace.pre_pair_frontier_trace is None
def test_capture_frontier_trace_records_prune_reasons() -> None:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),
@ -411,7 +444,159 @@ def test_reverify_iterations_limit_late_reroutes_to_conflicting_nets(monkeypatch
assert reroute_sets == [{"netA", "netB", "netC"}, {"netA", "netB"}]
assert results["netA"].outcome == "colliding"
assert results["netB"].outcome == "colliding"
assert results["netC"].outcome == "completed"
assert results["netC"].reached_target
def test_run_iteration_orders_subset_reroutes_by_recent_work(monkeypatch: pytest.MonkeyPatch) -> None:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),
nets=(
NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),
NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0),
NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0),
),
)
options = RoutingOptions(
congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False),
refinement=RefinementOptions(enabled=False),
)
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
state = pathfinder._prepare_state()
state.recent_attempt_work = {"netA": 200, "netB": 20}
route_order: list[str] = []
def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None):
_ = self
_ = state
_ = iteration
route_order.append(net_id)
assert node_limit_override is None
assert incumbent_fallback is None
return RoutingResult(net_id=net_id, path=(), reached_target=False), False
def fake_reverify(self, state):
_ = self
_ = state
return _IterationReview(
conflicting_nets={"netA", "netB"},
conflict_edges={("netA", "netB")},
completed_net_ids=set(),
total_dynamic_collisions=2,
)
monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once)
monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify)
review = pathfinder._run_iteration(state, 1, {"netA", "netB"}, None)
assert review is not None
assert route_order == ["netB", "netA"]
def test_run_iteration_caps_two_heaviest_late_phase_nets(monkeypatch: pytest.MonkeyPatch) -> None:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),
nets=(
NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),
NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0),
NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0),
NetSpec("netD", Port(10, 80, 0), Port(90, 80, 0), width=2.0),
NetSpec("netE", Port(10, 65, 0), Port(90, 65, 0), width=2.0),
),
)
options = RoutingOptions(
objective=ObjectiveWeights(bend_penalty=100.0),
congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False),
refinement=RefinementOptions(enabled=False),
)
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
state = pathfinder._prepare_state()
state.results = {
net_id: RoutingResult(net_id=net_id, path=(Straight.generate(spec.start, 80.0, 2.0, dilation=1.0),), reached_target=True)
for net_id, spec in state.net_specs.items()
}
state.best_conflict_edges = 2
state.recent_attempt_work = {"netA": 20, "netB": 40, "netC": 400, "netD": 220}
incumbents = dict(state.results)
caps_by_net: dict[str, tuple[int | None, RoutingResult | None]] = {}
def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None):
_ = self
_ = state
_ = iteration
caps_by_net[net_id] = (node_limit_override, incumbent_fallback)
return RoutingResult(net_id=net_id, path=(), reached_target=False), False
def fake_reverify(self, state):
_ = self
_ = state
return _IterationReview(
conflicting_nets={"netA", "netB", "netC", "netD"},
conflict_edges={("netA", "netB"), ("netC", "netD")},
completed_net_ids=set(),
total_dynamic_collisions=2,
)
monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once)
monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify)
review = pathfinder._run_iteration(state, 1, {"netA", "netB", "netC", "netD"}, None)
assert review is not None
assert caps_by_net["netA"] == (None, None)
assert caps_by_net["netB"] == (None, None)
assert caps_by_net["netC"][0] == 1
assert caps_by_net["netD"][0] == 1
assert caps_by_net["netC"][1] is incumbents["netC"]
assert caps_by_net["netD"][1] is incumbents["netD"]
def test_route_net_once_skips_search_for_capped_incumbent_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),
nets=(NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),),
)
options = RoutingOptions(
objective=ObjectiveWeights(bend_penalty=100.0),
congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False),
refinement=RefinementOptions(enabled=False),
)
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
state = pathfinder._prepare_state()
incumbent = RoutingResult(
net_id="netA",
path=(Straight.generate(problem.nets[0].start, 80.0, 2.0, dilation=1.0),),
reached_target=True,
)
state.results["netA"] = incumbent
installed: list[tuple[str, tuple[object, ...]]] = []
def fail_route_astar(*args, **kwargs):
raise AssertionError("route_astar should not run for capped incumbent fallback")
def record_install(self, net_id, path):
_ = self
installed.append((net_id, tuple(path)))
monkeypatch.setattr(router_module, "route_astar", fail_route_astar)
monkeypatch.setattr(PathFinder, "_install_path", record_install)
result, guidance_seed_present = pathfinder._route_net_once(
state,
1,
"netA",
node_limit_override=1,
incumbent_fallback=incumbent,
)
assert result is incumbent
assert guidance_seed_present is True
assert installed == [("netA", incumbent.path)]
assert pathfinder.metrics.total_late_phase_capped_nets == 1
assert pathfinder.metrics.total_late_phase_capped_fallbacks == 1
def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:

View file

@ -289,6 +289,8 @@ def test_pair_local_context_clones_live_static_obstacles() -> None:
last_conflict_edge_count=0,
repeated_conflict_count=0,
pair_local_plateau_count=0,
recent_attempt_work={},
pre_pair_candidate=None,
)
local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b"))

View file

@ -75,6 +75,7 @@ def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> Non
assert sum(result.reached_target for result in run.results_by_net.values()) == 10
assert run.metrics.pair_local_search_pairs_considered >= 1
assert run.metrics.pair_local_search_accepts >= 1
assert run.pre_pair_frontier_trace is not None
final_entry = run.conflict_trace[-1]
assert final_entry.stage == "final"

View file

@ -48,6 +48,8 @@ def test_snapshot_example_01_exposes_metrics() -> None:
assert snapshot.metrics.pair_local_search_attempts >= 0
assert snapshot.metrics.pair_local_search_accepts >= 0
assert snapshot.metrics.pair_local_search_nodes_expanded >= 0
assert snapshot.metrics.late_phase_capped_nets >= 0
assert snapshot.metrics.late_phase_capped_fallbacks >= 0
def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None:
@ -300,6 +302,31 @@ def test_record_iteration_trace_script_writes_selected_scenario(tmp_path: Path)
assert (tmp_path / "iteration_trace.md").exists()
def test_record_pre_pair_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -> None:
repo_root = Path(__file__).resolve().parents[2]
script_path = repo_root / "scripts" / "record_pre_pair_frontier_trace.py"
subprocess.run(
[
sys.executable,
str(script_path),
"--include-performance-only",
"--scenario",
"example_07_large_scale_routing_no_warm_start",
"--output-dir",
str(tmp_path),
],
check=True,
)
payload = json.loads((tmp_path / "pre_pair_frontier_trace.json").read_text())
assert payload["generated_at"]
assert payload["generator"] == "scripts/record_pre_pair_frontier_trace.py"
assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"]
assert payload["scenarios"][0]["pre_pair_frontier_trace"] is not None
assert (tmp_path / "pre_pair_frontier_trace.md").exists()
def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None:
repo_root = Path(__file__).resolve().parents[2]
script_path = repo_root / "scripts" / "characterize_pair_local_search.py"

View file

@ -0,0 +1,191 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from collections import Counter
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS
def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]:
if include_performance_only:
return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS
return TRACE_SCENARIO_RUNS
def _selected_runs(
selected_scenarios: tuple[str, ...] | None,
*,
include_performance_only: bool,
) -> tuple[tuple[str, object], ...]:
if selected_scenarios is None:
perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS)
return (
(
"example_07_large_scale_routing_no_warm_start",
perf_registry["example_07_large_scale_routing_no_warm_start"],
),
(
"example_07_large_scale_routing_no_warm_start_seed43",
perf_registry["example_07_large_scale_routing_no_warm_start_seed43"],
),
)
registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS)
allowed_standard = dict(_trace_registry(include_performance_only))
runs = []
for name in selected_scenarios:
if name in allowed_standard:
runs.append((name, allowed_standard[name]))
continue
if name in registry:
runs.append((name, registry[name]))
continue
valid = ", ".join(sorted(registry))
raise SystemExit(f"Unknown pre-pair frontier-trace scenario: {name}. Valid scenarios: {valid}")
return tuple(runs)
def _build_payload(
selected_scenarios: tuple[str, ...] | None,
*,
include_performance_only: bool,
) -> dict[str, object]:
scenarios = []
for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only):
result = run()
scenarios.append(
{
"name": name,
"summary": {
"total_results": len(result.results_by_net),
"valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid),
"reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target),
},
"metrics": asdict(result.metrics),
"pre_pair_frontier_trace": None
if result.pre_pair_frontier_trace is None
else asdict(result.pre_pair_frontier_trace),
}
)
return {
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
"generator": "scripts/record_pre_pair_frontier_trace.py",
"scenarios": scenarios,
}
def _render_markdown(payload: dict[str, object]) -> str:
lines = [
"# Pre-Pair Frontier Trace",
"",
f"Generated at {payload['generated_at']} by `{payload['generator']}`.",
"",
]
for scenario in payload["scenarios"]:
lines.extend(
[
f"## {scenario['name']}",
"",
f"Results: {scenario['summary']['valid_results']} valid / "
f"{scenario['summary']['reached_targets']} reached / "
f"{scenario['summary']['total_results']} total.",
"",
]
)
trace = scenario["pre_pair_frontier_trace"]
if trace is None:
lines.extend(["No pre-pair frontier trace captured.", ""])
continue
lines.extend(
[
f"Captured iteration: `{trace['iteration']}`",
"",
f"Conflict edges: `{trace['conflict_edges']}`",
"",
"| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |",
"| :-- | --: | --: | --: | --: | --: | :--: | --: |",
]
)
reason_counts: Counter[str] = Counter()
for net_trace in trace["nets"]:
frontier = net_trace["frontier"]
lines.append(
"| "
f"{net_trace['net_id']} | "
f"{net_trace['nodes_expanded']} | "
f"{net_trace['congestion_check_calls']} | "
f"{net_trace['pruned_closed_set']} | "
f"{net_trace['pruned_cost']} | "
f"{net_trace['pruned_hard_collision']} | "
f"{'yes' if net_trace['guidance_seed_present'] else 'no'} | "
f"{len(frontier['samples'])} |"
)
reason_counts["closed_set"] += frontier["pruned_closed_set"]
reason_counts["hard_collision"] += frontier["pruned_hard_collision"]
reason_counts["self_collision"] += frontier["pruned_self_collision"]
reason_counts["cost"] += frontier["pruned_cost"]
lines.extend(["", "Frontier prune totals by reason:", ""])
if reason_counts:
for reason, count in reason_counts.most_common():
lines.append(f"- `{reason}`: {count}")
else:
lines.append("- None")
lines.append("")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Record pre-pair frontier-trace artifacts for selected trace scenarios.")
parser.add_argument(
"--scenario",
action="append",
dest="scenarios",
default=[],
help="Optional trace scenario name to include. May be passed more than once.",
)
parser.add_argument(
"--include-performance-only",
action="store_true",
help="Include performance-only trace scenarios when selecting from the standard registry.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=None,
help="Directory to write pre_pair_frontier_trace.json and .md into. Defaults to <repo>/docs.",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1]
output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
output_dir.mkdir(exist_ok=True)
selected = tuple(args.scenarios) if args.scenarios else None
payload = _build_payload(selected, include_performance_only=args.include_performance_only)
json_path = output_dir / "pre_pair_frontier_trace.json"
markdown_path = output_dir / "pre_pair_frontier_trace.md"
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
markdown_path.write_text(_render_markdown(payload) + "\n")
if json_path.is_relative_to(repo_root):
print(f"Wrote {json_path.relative_to(repo_root)}")
else:
print(f"Wrote {json_path}")
if markdown_path.is_relative_to(repo_root):
print(f"Wrote {markdown_path.relative_to(repo_root)}")
else:
print(f"Wrote {markdown_path}")
if __name__ == "__main__":
main()