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

View file

@ -1,6 +1,6 @@
# Iteration Trace # 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 ## 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 | | 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 |
| 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | | 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 |
| 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 | | 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 |
| 4 | 384.2 | 6 | 6 | 2 | 10 | 142 | 550 | 1126 | 884 | | 4 | 384.2 | 6 | 6 | 2 | 10 | 136 | 505 | 1025 | 829 |
| 5 | 537.8 | 4 | 6 | 2 | 10 | 406 | 1477 | 3377 | 2666 | | 5 | 537.8 | 4 | 6 | 2 | 10 | 33 | 171 | 419 | 287 |
Top nets by iteration-attributed nodes expanded: Top nets by iteration-attributed nodes expanded:
- `net_03`: 435
- `net_09`: 250 - `net_09`: 250
- `net_06`: 242 - `net_03`: 199
- `net_00`: 177 - `net_00`: 177
- `net_08`: 172 - `net_08`: 166
- `net_07`: 140 - `net_07`: 140
- `net_06`: 105
- `net_02`: 79 - `net_02`: 79
- `net_01`: 65 - `net_01`: 65
- `net_05`: 12 - `net_05`: 12
@ -68,11 +68,11 @@ Top nets by iteration-attributed nodes expanded:
Top nets by iteration-attributed congestion checks: Top nets by iteration-attributed congestion checks:
- `net_03`: 1434 - `net_03`: 639
- `net_06`: 893
- `net_07`: 454 - `net_07`: 454
- `net_08`: 328 - `net_06`: 382
- `net_02`: 290 - `net_02`: 290
- `net_08`: 283
- `net_09`: 178 - `net_09`: 178
- `net_01`: 135 - `net_01`: 135
- `net_00`: 82 - `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 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. - 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. - 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`. 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. 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 | | 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_01_simple_route | 0.0037 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 5 |
| example_02_congestion_resolution | 0.3614 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 38 | | example_02_congestion_resolution | 0.3361 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 41 |
| example_03_locked_paths | 0.1953 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 16 | | 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.0277 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 8 | | 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.2537 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 17 | | 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.2103 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 12 | | 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.2074 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 40 | | 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.0186 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 8 | | 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.0079 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | | example_09_unroutable_best_effort | 0.0057 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
## Full Counter Set ## Full Counter Set
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters. 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. 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: 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", "generator": "scripts/record_performance_baseline.py",
"scenarios": [ "scenarios": [
{ {
"duration_s": 0.003825429128482938, "duration_s": 0.003715757979080081,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -47,6 +47,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 1, "iteration_reverified_nets": 1,
"iteration_reverify_calls": 1, "iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1, "move_cache_abs_hits": 1,
"move_cache_abs_misses": 10, "move_cache_abs_misses": 10,
"move_cache_rel_hits": 0, "move_cache_rel_hits": 0,
@ -85,7 +87,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 11, "score_component_calls": 11,
"score_component_total_ns": 16571, "score_component_total_ns": 16864,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
@ -93,7 +95,7 @@
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0, "verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 4, "verify_path_report_calls": 5,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -115,7 +117,7 @@
"valid_results": 1 "valid_results": 1
}, },
{ {
"duration_s": 0.36141274496912956, "duration_s": 0.33605348505079746,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -159,6 +161,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 3, "iteration_reverified_nets": 3,
"iteration_reverify_calls": 1, "iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 12, "move_cache_abs_hits": 12,
"move_cache_abs_misses": 1401, "move_cache_abs_misses": 1401,
"move_cache_rel_hits": 1293, "move_cache_rel_hits": 1293,
@ -197,15 +201,15 @@
"refinement_windows_considered": 10, "refinement_windows_considered": 10,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 976, "score_component_calls": 976,
"score_component_total_ns": 1143187, "score_component_total_ns": 1109505,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 2, "static_tree_rebuilds": 2,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 88, "verify_dynamic_candidate_nets": 92,
"verify_dynamic_exact_pair_checks": 86, "verify_dynamic_exact_pair_checks": 90,
"verify_path_report_calls": 38, "verify_path_report_calls": 41,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -227,7 +231,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.19532882701605558, "duration_s": 0.18771230895072222,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -271,6 +275,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2, "iteration_reverified_nets": 2,
"iteration_reverify_calls": 2, "iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1, "move_cache_abs_hits": 1,
"move_cache_abs_misses": 903, "move_cache_abs_misses": 903,
"move_cache_rel_hits": 821, "move_cache_rel_hits": 821,
@ -309,16 +315,16 @@
"refinement_windows_considered": 2, "refinement_windows_considered": 2,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 504, "score_component_calls": 504,
"score_component_total_ns": 565663, "score_component_total_ns": 546567,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 1, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 9, "verify_dynamic_candidate_nets": 10,
"verify_dynamic_exact_pair_checks": 9, "verify_dynamic_exact_pair_checks": 10,
"verify_path_report_calls": 16, "verify_path_report_calls": 18,
"verify_static_buffer_ops": 81, "verify_static_buffer_ops": 90,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 2, "visibility_corner_index_builds": 2,
@ -339,7 +345,7 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.027705274987965822, "duration_s": 0.026945222169160843,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -383,6 +389,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2, "iteration_reverified_nets": 2,
"iteration_reverify_calls": 1, "iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 1, "move_cache_abs_hits": 1,
"move_cache_abs_misses": 122, "move_cache_abs_misses": 122,
"move_cache_rel_hits": 80, "move_cache_rel_hits": 80,
@ -421,15 +429,15 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 90, "score_component_calls": 90,
"score_component_total_ns": 96756, "score_component_total_ns": 97710,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 1, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 9, "verify_dynamic_candidate_nets": 12,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 8, "verify_path_report_calls": 10,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -451,7 +459,7 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.25367443496361375, "duration_s": 0.23108969815075397,
"metrics": { "metrics": {
"congestion_cache_hits": 3, "congestion_cache_hits": 3,
"congestion_cache_misses": 146, "congestion_cache_misses": 146,
@ -495,6 +503,8 @@
"iteration_conflicting_nets": 2, "iteration_conflicting_nets": 2,
"iteration_reverified_nets": 6, "iteration_reverified_nets": 6,
"iteration_reverify_calls": 2, "iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 374, "move_cache_abs_hits": 374,
"move_cache_abs_misses": 1306, "move_cache_abs_misses": 1306,
"move_cache_rel_hits": 1204, "move_cache_rel_hits": 1204,
@ -533,7 +543,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 1234, "score_component_calls": 1234,
"score_component_total_ns": 1311211, "score_component_total_ns": 1223569,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 8, "static_safe_cache_hits": 8,
@ -541,7 +551,7 @@
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 8, "verify_dynamic_candidate_nets": 8,
"verify_dynamic_exact_pair_checks": 12, "verify_dynamic_exact_pair_checks": 12,
"verify_path_report_calls": 17, "verify_path_report_calls": 20,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -563,7 +573,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.21031348290853202, "duration_s": 0.19879506202414632,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -589,7 +599,7 @@
"danger_map_cache_misses": 731, "danger_map_cache_misses": 731,
"danger_map_lookup_calls": 1914, "danger_map_lookup_calls": 1914,
"danger_map_query_calls": 731, "danger_map_query_calls": 731,
"danger_map_total_ns": 19983976, "danger_map_total_ns": 19050142,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 54, "dynamic_path_objects_added": 54,
"dynamic_path_objects_removed": 36, "dynamic_path_objects_removed": 36,
@ -607,6 +617,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 3, "iteration_reverified_nets": 3,
"iteration_reverify_calls": 3, "iteration_reverify_calls": 3,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 186, "move_cache_abs_hits": 186,
"move_cache_abs_misses": 840, "move_cache_abs_misses": 840,
"move_cache_rel_hits": 702, "move_cache_rel_hits": 702,
@ -645,7 +657,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 3, "route_iterations": 3,
"score_component_calls": 842, "score_component_calls": 842,
"score_component_total_ns": 22474166, "score_component_total_ns": 21353240,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3,
"static_safe_cache_hits": 141, "static_safe_cache_hits": 141,
@ -653,8 +665,8 @@
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0, "verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 12, "verify_path_report_calls": 15,
"verify_static_buffer_ops": 72, "verify_static_buffer_ops": 90,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 3, "visibility_corner_index_builds": 3,
@ -675,7 +687,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.20740868314169347, "duration_s": 0.20880168909206986,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -701,7 +713,7 @@
"danger_map_cache_misses": 448, "danger_map_cache_misses": 448,
"danger_map_lookup_calls": 681, "danger_map_lookup_calls": 681,
"danger_map_query_calls": 448, "danger_map_query_calls": 448,
"danger_map_total_ns": 11224403, "danger_map_total_ns": 11025527,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 132, "dynamic_path_objects_added": 132,
"dynamic_path_objects_removed": 88, "dynamic_path_objects_removed": 88,
@ -719,6 +731,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 10, "iteration_reverified_nets": 10,
"iteration_reverify_calls": 1, "iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 6, "move_cache_abs_hits": 6,
"move_cache_abs_misses": 366, "move_cache_abs_misses": 366,
"move_cache_rel_hits": 275, "move_cache_rel_hits": 275,
@ -757,16 +771,16 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 291, "score_component_calls": 291,
"score_component_total_ns": 12117666, "score_component_total_ns": 11875928,
"static_net_tree_rebuilds": 10, "static_net_tree_rebuilds": 10,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 6, "static_safe_cache_hits": 6,
"static_tree_rebuilds": 10, "static_tree_rebuilds": 10,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 370, "verify_dynamic_candidate_nets": 476,
"verify_dynamic_exact_pair_checks": 56, "verify_dynamic_exact_pair_checks": 72,
"verify_path_report_calls": 40, "verify_path_report_calls": 50,
"verify_static_buffer_ops": 176, "verify_static_buffer_ops": 220,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 10, "visibility_corner_index_builds": 10,
@ -787,7 +801,7 @@
"valid_results": 10 "valid_results": 10
}, },
{ {
"duration_s": 0.018604618962854147, "duration_s": 0.017696003895252943,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -831,6 +845,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 2, "iteration_reverified_nets": 2,
"iteration_reverify_calls": 2, "iteration_reverify_calls": 2,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 2, "move_cache_abs_hits": 2,
"move_cache_abs_misses": 76, "move_cache_abs_misses": 76,
"move_cache_rel_hits": 32, "move_cache_rel_hits": 32,
@ -869,7 +885,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 72, "score_component_calls": 72,
"score_component_total_ns": 87655, "score_component_total_ns": 87742,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 2, "static_safe_cache_hits": 2,
@ -877,7 +893,7 @@
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0, "verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 8, "verify_path_report_calls": 10,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -899,7 +915,7 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.00794802000746131, "duration_s": 0.005660973023623228,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -925,7 +941,7 @@
"danger_map_cache_misses": 20, "danger_map_cache_misses": 20,
"danger_map_lookup_calls": 30, "danger_map_lookup_calls": 30,
"danger_map_query_calls": 20, "danger_map_query_calls": 20,
"danger_map_total_ns": 675454, "danger_map_total_ns": 515133,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 2, "dynamic_path_objects_added": 2,
"dynamic_path_objects_removed": 1, "dynamic_path_objects_removed": 1,
@ -943,6 +959,8 @@
"iteration_conflicting_nets": 0, "iteration_conflicting_nets": 0,
"iteration_reverified_nets": 0, "iteration_reverified_nets": 0,
"iteration_reverify_calls": 1, "iteration_reverify_calls": 1,
"late_phase_capped_fallbacks": 0,
"late_phase_capped_nets": 0,
"move_cache_abs_hits": 0, "move_cache_abs_hits": 0,
"move_cache_abs_misses": 16, "move_cache_abs_misses": 16,
"move_cache_rel_hits": 2, "move_cache_rel_hits": 2,
@ -981,7 +999,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 14, "score_component_calls": 14,
"score_component_total_ns": 722637, "score_component_total_ns": 554809,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0, "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, IterationTraceEntry as IterationTraceEntry,
NetConflictTrace as NetConflictTrace, NetConflictTrace as NetConflictTrace,
NetFrontierTrace as NetFrontierTrace, NetFrontierTrace as NetFrontierTrace,
PrePairFrontierTraceEntry as PrePairFrontierTraceEntry,
PrePairNetTrace as PrePairNetTrace,
RoutingResult as RoutingResult, RoutingResult as RoutingResult,
RoutingRunResult as RoutingRunResult, RoutingRunResult as RoutingRunResult,
) )
@ -49,6 +51,7 @@ def route(
expanded_nodes=tuple(finder.accumulated_expanded_nodes), expanded_nodes=tuple(finder.accumulated_expanded_nodes),
conflict_trace=tuple(finder.conflict_trace), conflict_trace=tuple(finder.conflict_trace),
frontier_trace=tuple(finder.frontier_trace), frontier_trace=tuple(finder.frontier_trace),
pre_pair_frontier_trace=finder.pre_pair_frontier_trace,
iteration_trace=tuple(finder.iteration_trace), iteration_trace=tuple(finder.iteration_trace),
) )
@ -67,6 +70,8 @@ __all__ = [
"FrontierPruneSample", "FrontierPruneSample",
"IterationNetAttemptTrace", "IterationNetAttemptTrace",
"IterationTraceEntry", "IterationTraceEntry",
"PrePairFrontierTraceEntry",
"PrePairNetTrace",
"RefinementOptions", "RefinementOptions",
"RoutingOptions", "RoutingOptions",
"RoutingProblem", "RoutingProblem",

View file

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

View file

@ -78,6 +78,26 @@ class NetFrontierTrace:
samples: tuple[FrontierPruneSample, ...] = () 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) @dataclass(frozen=True, slots=True)
class IterationNetAttemptTrace: class IterationNetAttemptTrace:
net_id: str net_id: str
@ -210,6 +230,8 @@ class RouteMetrics:
pair_local_search_attempts: int pair_local_search_attempts: int
pair_local_search_accepts: int pair_local_search_accepts: int
pair_local_search_nodes_expanded: int pair_local_search_nodes_expanded: int
late_phase_capped_nets: int
late_phase_capped_fallbacks: int
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@ -258,4 +280,5 @@ class RoutingRunResult:
expanded_nodes: tuple[tuple[int, int, int], ...] = () expanded_nodes: tuple[tuple[int, int, int], ...] = ()
conflict_trace: tuple[ConflictTraceEntry, ...] = () conflict_trace: tuple[ConflictTraceEntry, ...] = ()
frontier_trace: tuple[NetFrontierTrace, ...] = () frontier_trace: tuple[NetFrontierTrace, ...] = ()
pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None
iteration_trace: tuple[IterationTraceEntry, ...] = () iteration_trace: tuple[IterationTraceEntry, ...] = ()

View file

@ -258,6 +258,8 @@ class AStarMetrics:
"total_pair_local_search_attempts", "total_pair_local_search_attempts",
"total_pair_local_search_accepts", "total_pair_local_search_accepts",
"total_pair_local_search_nodes_expanded", "total_pair_local_search_nodes_expanded",
"total_late_phase_capped_nets",
"total_late_phase_capped_fallbacks",
"last_expanded_nodes", "last_expanded_nodes",
"nodes_expanded", "nodes_expanded",
"moves_generated", "moves_generated",
@ -371,6 +373,8 @@ class AStarMetrics:
self.total_pair_local_search_attempts = 0 self.total_pair_local_search_attempts = 0
self.total_pair_local_search_accepts = 0 self.total_pair_local_search_accepts = 0
self.total_pair_local_search_nodes_expanded = 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.last_expanded_nodes: list[tuple[int, int, int]] = []
self.nodes_expanded = 0 self.nodes_expanded = 0
self.moves_generated = 0 self.moves_generated = 0
@ -483,6 +487,8 @@ class AStarMetrics:
self.total_pair_local_search_attempts = 0 self.total_pair_local_search_attempts = 0
self.total_pair_local_search_accepts = 0 self.total_pair_local_search_accepts = 0
self.total_pair_local_search_nodes_expanded = 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: def reset_per_route(self) -> None:
self.nodes_expanded = 0 self.nodes_expanded = 0
@ -598,6 +604,8 @@ class AStarMetrics:
pair_local_search_attempts=self.total_pair_local_search_attempts, pair_local_search_attempts=self.total_pair_local_search_attempts,
pair_local_search_accepts=self.total_pair_local_search_accepts, pair_local_search_accepts=self.total_pair_local_search_accepts,
pair_local_search_nodes_expanded=self.total_pair_local_search_nodes_expanded, 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, IterationTraceEntry,
NetConflictTrace, NetConflictTrace,
NetFrontierTrace, NetFrontierTrace,
PrePairFrontierTraceEntry,
PrePairNetTrace,
RoutingOutcome, RoutingOutcome,
RoutingReport, RoutingReport,
RoutingResult, RoutingResult,
@ -53,6 +55,8 @@ class _RoutingState:
last_conflict_edge_count: int last_conflict_edge_count: int
repeated_conflict_count: int repeated_conflict_count: int
pair_local_plateau_count: int pair_local_plateau_count: int
recent_attempt_work: dict[str, int]
pre_pair_candidate: _PrePairCandidate | None
@dataclass(slots=True) @dataclass(slots=True)
@ -68,6 +72,14 @@ class _PairLocalTarget:
net_ids: tuple[str, str] 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 = ( _ITERATION_TRACE_TOTALS = (
"nodes_expanded", "nodes_expanded",
"congestion_check_calls", "congestion_check_calls",
@ -92,6 +104,7 @@ class PathFinder:
"accumulated_expanded_nodes", "accumulated_expanded_nodes",
"conflict_trace", "conflict_trace",
"frontier_trace", "frontier_trace",
"pre_pair_frontier_trace",
"iteration_trace", "iteration_trace",
) )
@ -110,6 +123,7 @@ class PathFinder:
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
self.conflict_trace: list[ConflictTraceEntry] = [] self.conflict_trace: list[ConflictTraceEntry] = []
self.frontier_trace: list[NetFrontierTrace] = [] self.frontier_trace: list[NetFrontierTrace] = []
self.pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None
self.iteration_trace: list[IterationTraceEntry] = [] self.iteration_trace: list[IterationTraceEntry] = []
def _metric_total(self, metric_name: str) -> int: def _metric_total(self, metric_name: str) -> int:
@ -219,6 +233,8 @@ class PathFinder:
last_conflict_edge_count=0, last_conflict_edge_count=0,
repeated_conflict_count=0, repeated_conflict_count=0,
pair_local_plateau_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: 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) state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
@ -399,6 +415,93 @@ class PathFinder:
return tuple(hotspot_bounds) 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( def _analyze_results(
self, self,
ordered_net_ids: Sequence[str], ordered_net_ids: Sequence[str],
@ -477,15 +580,6 @@ class PathFinder:
capture_component_conflicts=True, capture_component_conflicts=True,
count_iteration_metrics=False, count_iteration_metrics=False,
) )
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:
for net_id in state.ordered_net_ids: for net_id in state.ordered_net_ids:
result = state.results.get(net_id) result = state.results.get(net_id)
detail = details_by_net.get(net_id) detail = details_by_net.get(net_id)
@ -498,69 +592,14 @@ class PathFinder:
if not hotspot_bounds: if not hotspot_bounds:
continue 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.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( self.frontier_trace.append(
NetFrontierTrace( self._capture_single_frontier_trace(
net_id=net_id, state,
hotspot_bounds=hotspot_bounds, net_id,
pruned_closed_set=collector.pruned_closed_set, result,
pruned_hard_collision=collector.pruned_hard_collision, hotspot_bounds,
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( def _whole_set_is_better(
self, self,
@ -673,6 +712,50 @@ class PathFinder:
metrics=AStarMetrics(), 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( def _run_pair_local_attempt(
self, self,
state: _RoutingState, state: _RoutingState,
@ -780,12 +863,17 @@ class PathFinder:
state: _RoutingState, state: _RoutingState,
iteration: int, iteration: int,
net_id: str, net_id: str,
*,
node_limit_override: int | None = None,
incumbent_fallback: RoutingResult | None = None,
) -> tuple[RoutingResult, bool]: ) -> tuple[RoutingResult, bool]:
search = self.context.options.search search = self.context.options.search
congestion = self.context.options.congestion congestion = self.context.options.congestion
diagnostics = self.context.options.diagnostics diagnostics = self.context.options.diagnostics
net = state.net_specs[net_id] net = state.net_specs[net_id]
self.metrics.total_nets_routed += 1 self.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) self.context.cost_evaluator.collision_engine.remove_path(net_id)
guidance_seed_present = False guidance_seed_present = False
@ -808,6 +896,16 @@ class PathFinder:
guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25)
guidance_seed_present = True 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( run_config = SearchRunConfig.from_options(
self.context.options, self.context.options,
bend_collision_type=coll_model, bend_collision_type=coll_model,
@ -817,7 +915,7 @@ class PathFinder:
guidance_bonus=guidance_bonus, guidance_bonus=guidance_bonus,
skip_congestion=skip_congestion, skip_congestion=skip_congestion,
self_collision_check=(net_id in state.needs_self_collision_check), 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( path = route_astar(
net.start, net.start,
@ -833,9 +931,17 @@ class PathFinder:
state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes) state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes)
if not path: 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 return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present
reached_target = path[-1].end_port == net.target 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: if reached_target:
self.metrics.total_nets_reached_target += 1 self.metrics.total_nets_reached_target += 1
report = None report = None
@ -872,9 +978,27 @@ class PathFinder:
iteration_penalty = self.context.congestion_penalty 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] 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) self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids)
iteration_before = {} iteration_before = {}
attempt_traces: list[IterationNetAttemptTrace] = [] attempt_traces: list[IterationNetAttemptTrace] = []
attempt_work: dict[str, int] = {}
if diagnostics.capture_iteration_trace: if diagnostics.capture_iteration_trace:
iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS)
@ -883,14 +1007,23 @@ class PathFinder:
self.metrics.total_timeout_events += 1 self.metrics.total_timeout_events += 1
return None return None
attempt_before = {}
if diagnostics.capture_iteration_trace:
attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
result, guidance_seed_present = self._route_net_once(state, iteration, net_id) 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 state.results[net_id] = result
if diagnostics.capture_iteration_trace:
attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS)
deltas = self._metric_deltas(attempt_before, attempt_after) deltas = self._metric_deltas(attempt_before, attempt_after)
attempt_work[net_id] = deltas["nodes_expanded"] + deltas["congestion_check_calls"]
attempt_traces.append( attempt_traces.append(
IterationNetAttemptTrace( IterationNetAttemptTrace(
net_id=net_id, net_id=net_id,
@ -904,7 +1037,21 @@ class PathFinder:
) )
) )
state.recent_attempt_work = attempt_work
review = self._reverify_iteration_results(state) 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: if diagnostics.capture_iteration_trace:
iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS)
deltas = self._metric_deltas(iteration_before, iteration_after) deltas = self._metric_deltas(iteration_before, iteration_after)
@ -1072,6 +1219,7 @@ class PathFinder:
self.accumulated_expanded_nodes = [] self.accumulated_expanded_nodes = []
self.conflict_trace = [] self.conflict_trace = []
self.frontier_trace = [] self.frontier_trace = []
self.pre_pair_frontier_trace = None
self.iteration_trace = [] self.iteration_trace = []
self.metrics.reset_totals() self.metrics.reset_totals()
self.metrics.reset_per_route() self.metrics.reset_per_route()
@ -1080,13 +1228,17 @@ class PathFinder:
timed_out = self._run_iterations(state, iteration_callback) timed_out = self._run_iterations(state, iteration_callback)
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes) self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
self._restore_best_iteration(state) self._restore_best_iteration(state)
if self.context.options.diagnostics.capture_conflict_trace: 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.results, details_by_net, review = self._analyze_results(
state.ordered_net_ids, state.ordered_net_ids,
state.results, state.results,
capture_component_conflicts=True, capture_component_conflicts=capture_component_conflicts,
count_iteration_metrics=False, count_iteration_metrics=False,
) )
if self.context.options.diagnostics.capture_conflict_trace:
self._capture_conflict_trace_entry( self._capture_conflict_trace_entry(
state, state,
stage="restored_best", stage="restored_best",
@ -1095,6 +1247,13 @@ class PathFinder:
details_by_net=details_by_net, details_by_net=details_by_net,
review=review, 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: if timed_out:
final_results = self._verify_results(state) final_results = self._verify_results(state)

View file

@ -91,6 +91,7 @@ def _make_run_result(
expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes), expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes),
conflict_trace=tuple(pathfinder.conflict_trace), conflict_trace=tuple(pathfinder.conflict_trace),
frontier_trace=tuple(pathfinder.frontier_trace), frontier_trace=tuple(pathfinder.frontier_trace),
pre_pair_frontier_trace=pathfinder.pre_pair_frontier_trace,
iteration_trace=tuple(pathfinder.iteration_trace), iteration_trace=tuple(pathfinder.iteration_trace),
) )
@ -453,6 +454,7 @@ def _build_example_07_variant_stack(
capture_conflict_trace: bool = False, capture_conflict_trace: bool = False,
capture_frontier_trace: bool = False, capture_frontier_trace: bool = False,
capture_iteration_trace: bool = False, capture_iteration_trace: bool = False,
capture_pre_pair_frontier_trace: bool = False,
) -> tuple[CostEvaluator, AStarMetrics, PathFinder]: ) -> tuple[CostEvaluator, AStarMetrics, PathFinder]:
bounds = (0, 0, 1000, 1000) bounds = (0, 0, 1000, 1000)
obstacles = [ obstacles = [
@ -496,6 +498,7 @@ def _build_example_07_variant_stack(
"capture_conflict_trace": capture_conflict_trace, "capture_conflict_trace": capture_conflict_trace,
"capture_frontier_trace": capture_frontier_trace, "capture_frontier_trace": capture_frontier_trace,
"capture_iteration_trace": capture_iteration_trace, "capture_iteration_trace": capture_iteration_trace,
"capture_pre_pair_frontier_trace": capture_pre_pair_frontier_trace,
"shuffle_nets": True, "shuffle_nets": True,
"seed": seed, "seed": seed,
"warm_start_enabled": warm_start_enabled, "warm_start_enabled": warm_start_enabled,
@ -512,6 +515,7 @@ def _run_example_07_variant(
capture_conflict_trace: bool = False, capture_conflict_trace: bool = False,
capture_frontier_trace: bool = False, capture_frontier_trace: bool = False,
capture_iteration_trace: bool = False, capture_iteration_trace: bool = False,
capture_pre_pair_frontier_trace: bool = False,
) -> RoutingRunResult: ) -> RoutingRunResult:
evaluator, metrics, pathfinder = _build_example_07_variant_stack( evaluator, metrics, pathfinder = _build_example_07_variant_stack(
num_nets=num_nets, num_nets=num_nets,
@ -520,6 +524,7 @@ def _run_example_07_variant(
capture_conflict_trace=capture_conflict_trace, capture_conflict_trace=capture_conflict_trace,
capture_frontier_trace=capture_frontier_trace, capture_frontier_trace=capture_frontier_trace,
capture_iteration_trace=capture_iteration_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: 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_conflict_trace=True,
capture_frontier_trace=True, capture_frontier_trace=True,
capture_iteration_trace=True, capture_iteration_trace=True,
capture_pre_pair_frontier_trace=True,
) )

View file

@ -3,6 +3,7 @@ import importlib
import pytest import pytest
from shapely.geometry import box from shapely.geometry import box
import inire.router._router as router_module
from inire import ( from inire import (
CongestionOptions, CongestionOptions,
DiagnosticsOptions, DiagnosticsOptions,
@ -54,6 +55,7 @@ def test_route_problem_smoke() -> None:
assert run.results_by_net["net1"].is_valid assert run.results_by_net["net1"].is_valid
assert run.conflict_trace == () assert run.conflict_trace == ()
assert run.frontier_trace == () assert run.frontier_trace == ()
assert run.pre_pair_frontier_trace is None
assert run.iteration_trace == () 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_attempts >= 0
assert run.metrics.pair_local_search_accepts >= 0 assert run.metrics.pair_local_search_accepts >= 0
assert run.metrics.pair_local_search_nodes_expanded >= 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: 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"} 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: def test_capture_frontier_trace_records_prune_reasons() -> None:
problem = RoutingProblem( problem = RoutingProblem(
bounds=(0, 0, 100, 100), 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 reroute_sets == [{"netA", "netB", "netC"}, {"netA", "netB"}]
assert results["netA"].outcome == "colliding" assert results["netA"].outcome == "colliding"
assert results["netB"].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: 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, last_conflict_edge_count=0,
repeated_conflict_count=0, repeated_conflict_count=0,
pair_local_plateau_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")) 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 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_pairs_considered >= 1
assert run.metrics.pair_local_search_accepts >= 1 assert run.metrics.pair_local_search_accepts >= 1
assert run.pre_pair_frontier_trace is not None
final_entry = run.conflict_trace[-1] final_entry = run.conflict_trace[-1]
assert final_entry.stage == "final" 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_attempts >= 0
assert snapshot.metrics.pair_local_search_accepts >= 0 assert snapshot.metrics.pair_local_search_accepts >= 0
assert snapshot.metrics.pair_local_search_nodes_expanded >= 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: 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() 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: def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None:
repo_root = Path(__file__).resolve().parents[2] repo_root = Path(__file__).resolve().parents[2]
script_path = repo_root / "scripts" / "characterize_pair_local_search.py" 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()