From 46e7e13059b0363c1fb5e76191426bd0a6d9e54f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 18:57:34 -0700 Subject: [PATCH] Optimize late no-warm reroutes --- DOCS.md | 32 +- docs/iteration_trace.json | 294 +++--- docs/iteration_trace.md | 18 +- docs/optimization_pass_01_log.md | 77 ++ docs/performance.md | 27 +- docs/performance_baseline.json | 96 +- docs/pre_pair_frontier_trace.json | 1005 +++++++++++++++++++++ docs/pre_pair_frontier_trace.md | 48 + inire/__init__.py | 5 + inire/model.py | 1 + inire/results.py | 23 + inire/router/_astar_types.py | 8 + inire/router/_router.py | 371 +++++--- inire/tests/example_scenarios.py | 6 + inire/tests/test_api.py | 187 +++- inire/tests/test_astar.py | 2 + inire/tests/test_example_regressions.py | 1 + inire/tests/test_performance_reporting.py | 27 + scripts/record_pre_pair_frontier_trace.py | 191 ++++ 19 files changed, 2099 insertions(+), 320 deletions(-) create mode 100644 docs/pre_pair_frontier_trace.json create mode 100644 docs/pre_pair_frontier_trace.md create mode 100644 scripts/record_pre_pair_frontier_trace.py diff --git a/DOCS.md b/DOCS.md index 1b03ea4..75a7cba 100644 --- a/DOCS.md +++ b/DOCS.md @@ -131,6 +131,7 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are | `capture_conflict_trace` | `False` | Capture authoritative post-reverify conflict trace entries for debugging negotiated-congestion failures. | | `capture_frontier_trace` | `False` | Run an analysis-only reroute for reached-but-colliding nets and capture prune causes near their final conflict hotspots. | | `capture_iteration_trace` | `False` | Capture per-iteration and per-net route-attempt attribution for negotiated-congestion diagnosis. | +| `capture_pre_pair_frontier_trace` | `False` | Capture the final unresolved pre-pair-local subset iteration plus hotspot-adjacent frontier prunes for the routed nets in that basin. | ## 7. Conflict Trace @@ -187,7 +188,30 @@ Use `scripts/record_frontier_trace.py` to capture JSON and Markdown frontier-pru Separately from the observational trace tooling, the router may run a bounded post-loop pair-local scratch reroute before refinement when the restored best snapshot ends with final two-net reached-target dynamic conflicts. That repair phase is part of normal routing behavior and is reported through the `pair_local_search_*` counters below. -## 9. Iteration Trace +## 9. Pre-Pair Frontier Trace + +`RoutingRunResult.pre_pair_frontier_trace` is either a single immutable trace entry or `None`. It is populated only when `RoutingOptions.diagnostics.capture_pre_pair_frontier_trace=True`. + +Trace types: + +- `PrePairFrontierTraceEntry` + - `iteration`: The final unresolved subset-reroute iteration immediately before pair-local handoff + - `routed_net_ids`: Nets rerouted in that iteration, in routing order + - `conflict_edges`: Dynamic conflict edges reported for that unresolved basin + - `nets`: Per-net attempt attribution plus hotspot-adjacent frontier rerun data +- `PrePairNetTrace` + - `net_id` + - `nodes_expanded` + - `congestion_check_calls` + - `pruned_closed_set` + - `pruned_cost` + - `pruned_hard_collision` + - `guidance_seed_present` + - `frontier`: A `NetFrontierTrace` captured against the restored best unresolved state + +Use `scripts/record_pre_pair_frontier_trace.py` to capture JSON and Markdown artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the heavier seed-43 no-warm canary. + +## 10. Iteration Trace `RoutingRunResult.iteration_trace` is an immutable tuple of negotiated-congestion iteration summaries. It is empty unless `RoutingOptions.diagnostics.capture_iteration_trace=True`. @@ -217,7 +241,7 @@ Trace types: Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-attribution artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the pathological seed-43 no-warm canary. -## 10. RouteMetrics +## 11. RouteMetrics `RoutingRunResult.metrics` is an immutable per-run snapshot. @@ -297,13 +321,15 @@ Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-a - `pair_local_search_attempts`: Number of pair-local-search reroute attempts executed across all considered pairs. - `pair_local_search_accepts`: Number of pair-local-search attempts accepted into the whole routed result set. - `pair_local_search_nodes_expanded`: Total A* node expansions spent inside pair-local-search attempts. +- `late_phase_capped_nets`: Number of late all-reached heavy-net reroutes run under the bounded node-limit cap before pair-local handoff. +- `late_phase_capped_fallbacks`: Number of those capped late-phase reroutes that fell back to the incumbent reached-target path instead of replacing it. ## 10. Internal Modules Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`. The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**. -Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, `scripts/record_iteration_trace.py` for per-iteration negotiated-congestion attribution, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate. +Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, `scripts/record_pre_pair_frontier_trace.py` for the final unresolved pre-pair basin, `scripts/record_iteration_trace.py` for per-iteration negotiated-congestion attribution, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate. ## 11. Tuning Notes diff --git a/docs/iteration_trace.json b/docs/iteration_trace.json index 72295bc..2bebaa6 100644 --- a/docs/iteration_trace.json +++ b/docs/iteration_trace.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-04-02T16:46:00-07:00", + "generated_at": "2026-04-02T18:51:01-07:00", "generator": "scripts/record_iteration_trace.py", "scenarios": [ { @@ -514,10 +514,10 @@ "iteration": 4, "net_attempts": [ { - "congestion_check_calls": 43, + "congestion_check_calls": 30, "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 10, + "net_id": "net_07", + "nodes_expanded": 7, "pruned_closed_set": 1, "pruned_cost": 0, "pruned_hard_collision": 0, @@ -534,10 +534,10 @@ "reached_target": true }, { - "congestion_check_calls": 30, + "congestion_check_calls": 43, "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 7, + "net_id": "net_00", + "nodes_expanded": 10, "pruned_closed_set": 1, "pruned_cost": 0, "pruned_hard_collision": 0, @@ -556,9 +556,9 @@ ], "nodes_expanded": 81, "routed_net_ids": [ - "net_00", - "net_06", "net_07", + "net_06", + "net_00", "net_01" ], "total_dynamic_collisions": 10 @@ -589,7 +589,7 @@ "danger_map_cache_misses": 6063, "danger_map_lookup_calls": 17610, "danger_map_query_calls": 6063, - "danger_map_total_ns": 174709728, + "danger_map_total_ns": 171226180, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 399, "dynamic_path_objects_removed": 351, @@ -607,6 +607,8 @@ "iteration_conflicting_nets": 32, "iteration_reverified_nets": 50, "iteration_reverify_calls": 5, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 1200, "move_cache_abs_misses": 5338, "move_cache_rel_hits": 4768, @@ -645,7 +647,7 @@ "refinement_windows_considered": 0, "route_iterations": 5, "score_component_calls": 6181, - "score_component_total_ns": 195118641, + "score_component_total_ns": 191650546, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 1170, @@ -1062,26 +1064,6 @@ "congestion_penalty": 274.4, "iteration": 3, "net_attempts": [ - { - "congestion_check_calls": 35, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 8, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 49, - "guidance_seed_present": true, - "net_id": "net_09", - "nodes_expanded": 11, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 36, "guidance_seed_present": true, @@ -1092,6 +1074,16 @@ "pruned_hard_collision": 0, "reached_target": true }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, { "congestion_check_calls": 30, "guidance_seed_present": true, @@ -1102,6 +1094,16 @@ "pruned_hard_collision": 0, "reached_target": true }, + { + "congestion_check_calls": 49, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, { "congestion_check_calls": 70, "guidance_seed_present": true, @@ -1113,11 +1115,11 @@ "reached_target": true }, { - "congestion_check_calls": 30, + "congestion_check_calls": 35, "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 7, - "pruned_closed_set": 1, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, "pruned_cost": 0, "pruned_hard_collision": 0, "reached_target": true @@ -1125,34 +1127,24 @@ ], "nodes_expanded": 54, "routed_net_ids": [ - "net_06", - "net_09", "net_03", + "net_07", "net_02", + "net_09", "net_08", - "net_07" + "net_06" ], "total_dynamic_collisions": 15 }, { "completed_nets": 6, "conflict_edges": 2, - "congestion_candidate_ids": 1126, - "congestion_check_calls": 550, - "congestion_exact_pair_checks": 884, + "congestion_candidate_ids": 1025, + "congestion_check_calls": 505, + "congestion_exact_pair_checks": 829, "congestion_penalty": 384.15999999999997, "iteration": 4, "net_attempts": [ - { - "congestion_check_calls": 179, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 46, - "pruned_closed_set": 7, - "pruned_cost": 15, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 30, "guidance_seed_present": true, @@ -1163,26 +1155,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 91, - "guidance_seed_present": true, - "net_id": "net_08", - "nodes_expanded": 18, - "pruned_closed_set": 8, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 26, - "guidance_seed_present": true, - "net_id": "net_09", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 32, "guidance_seed_present": true, @@ -1193,6 +1165,16 @@ "pruned_hard_collision": 0, "reached_target": true }, + { + "congestion_check_calls": 179, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 46, + "pruned_closed_set": 7, + "pruned_cost": 15, + "pruned_hard_collision": 0, + "reached_target": true + }, { "congestion_check_calls": 192, "guidance_seed_present": true, @@ -1202,25 +1184,45 @@ "pruned_cost": 21, "pruned_hard_collision": 0, "reached_target": true + }, + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 12, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true } ], - "nodes_expanded": 142, + "nodes_expanded": 136, "routed_net_ids": [ - "net_06", "net_07", - "net_08", - "net_09", "net_02", - "net_03" + "net_06", + "net_03", + "net_09", + "net_08" ], "total_dynamic_collisions": 10 }, { "completed_nets": 6, "conflict_edges": 2, - "congestion_candidate_ids": 3377, - "congestion_check_calls": 1477, - "congestion_exact_pair_checks": 2666, + "congestion_candidate_ids": 419, + "congestion_check_calls": 171, + "congestion_exact_pair_checks": 287, "congestion_penalty": 537.824, "iteration": 5, "net_attempts": [ @@ -1234,16 +1236,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 511, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 137, - "pruned_closed_set": 13, - "pruned_cost": 73, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 86, "guidance_seed_present": true, @@ -1255,21 +1247,31 @@ "reached_target": true }, { - "congestion_check_calls": 795, + "congestion_check_calls": 0, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, "guidance_seed_present": true, "net_id": "net_03", - "nodes_expanded": 236, - "pruned_closed_set": 28, - "pruned_cost": 149, + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, "pruned_hard_collision": 0, "reached_target": true } ], - "nodes_expanded": 406, + "nodes_expanded": 33, "routed_net_ids": [ "net_07", - "net_06", "net_02", + "net_06", "net_03" ], "total_dynamic_collisions": 10 @@ -1277,74 +1279,76 @@ ], "metrics": { "congestion_cache_hits": 8, - "congestion_cache_misses": 3881, - "congestion_candidate_ids": 9232, - "congestion_candidate_nets": 8483, - "congestion_candidate_precheck_hits": 2207, - "congestion_candidate_precheck_misses": 1793, - "congestion_candidate_precheck_skips": 111, - "congestion_check_calls": 3881, - "congestion_exact_pair_checks": 7234, - "congestion_grid_net_cache_hits": 2169, - "congestion_grid_net_cache_misses": 3238, - "congestion_grid_span_cache_hits": 1997, - "congestion_grid_span_cache_misses": 1628, + "congestion_cache_misses": 2530, + "congestion_candidate_ids": 6173, + "congestion_candidate_nets": 5869, + "congestion_candidate_precheck_hits": 1152, + "congestion_candidate_precheck_misses": 1460, + "congestion_candidate_precheck_skips": 74, + "congestion_check_calls": 2530, + "congestion_exact_pair_checks": 4800, + "congestion_grid_net_cache_hits": 1192, + "congestion_grid_net_cache_misses": 2676, + "congestion_grid_span_cache_hits": 1065, + "congestion_grid_span_cache_misses": 1366, "congestion_lazy_requeues": 0, "congestion_lazy_resolutions": 0, - "congestion_net_envelope_cache_hits": 2311, - "congestion_net_envelope_cache_misses": 3376, - "congestion_presence_cache_hits": 2443, - "congestion_presence_cache_misses": 2009, - "congestion_presence_skips": 452, - "danger_map_cache_hits": 14603, - "danger_map_cache_misses": 6814, - "danger_map_lookup_calls": 21417, - "danger_map_query_calls": 6814, - "danger_map_total_ns": 181736341, + "congestion_net_envelope_cache_hits": 1234, + "congestion_net_envelope_cache_misses": 2769, + "congestion_presence_cache_hits": 1302, + "congestion_presence_cache_misses": 1664, + "congestion_presence_skips": 354, + "danger_map_cache_hits": 11485, + "danger_map_cache_misses": 5474, + "danger_map_lookup_calls": 16959, + "danger_map_query_calls": 5474, + "danger_map_total_ns": 145721703, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 397, "dynamic_path_objects_removed": 350, "dynamic_tree_rebuilds": 0, - "guidance_bonus_applied": 8062.5, - "guidance_bonus_applied_bend90": 3187.5, + "guidance_bonus_applied": 7562.5, + "guidance_bonus_applied_bend90": 2937.5, "guidance_bonus_applied_sbend": 250.0, - "guidance_bonus_applied_straight": 4625.0, - "guidance_match_moves": 129, - "guidance_match_moves_bend90": 51, + "guidance_bonus_applied_straight": 4375.0, + "guidance_match_moves": 121, + "guidance_match_moves_bend90": 47, "guidance_match_moves_sbend": 4, - "guidance_match_moves_straight": 74, + "guidance_match_moves_straight": 70, "hard_collision_cache_hits": 0, "iteration_conflict_edges": 39, "iteration_conflicting_nets": 39, "iteration_reverified_nets": 60, "iteration_reverify_calls": 6, - "move_cache_abs_hits": 1915, - "move_cache_abs_misses": 6136, - "move_cache_rel_hits": 5505, - "move_cache_rel_misses": 631, - "moves_added": 7121, - "moves_generated": 8051, + "late_phase_capped_fallbacks": 2, + "late_phase_capped_nets": 2, + "move_cache_abs_hits": 1304, + "move_cache_abs_misses": 4997, + "move_cache_rel_hits": 4419, + "move_cache_rel_misses": 578, + "moves_added": 5638, + "moves_generated": 6301, "nets_carried_forward": 14, - "nets_reached_target": 46, + "nets_reached_target": 44, "nets_routed": 46, - "nodes_expanded": 1582, + "nodes_expanded": 1203, "pair_local_search_accepts": 2, "pair_local_search_attempts": 3, "pair_local_search_nodes_expanded": 39, "pair_local_search_pairs_considered": 2, "path_cost_calls": 0, - "pruned_closed_set": 399, - "pruned_cost": 531, + "pruned_closed_set": 354, + "pruned_cost": 309, "pruned_hard_collision": 0, - "ray_cast_calls": 5077, - "ray_cast_calls_expand_forward": 1536, + "ray_cast_calls": 4059, + "ray_cast_calls_expand_forward": 1159, "ray_cast_calls_expand_snap": 13, "ray_cast_calls_other": 0, - "ray_cast_calls_straight_static": 3522, + "ray_cast_calls_straight_static": 2881, "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 6, - "ray_cast_candidate_bounds": 316, + "ray_cast_candidate_bounds": 170, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 10, "refinement_candidate_side_extents": 0, @@ -1355,17 +1359,17 @@ "refinement_static_bounds_checked": 0, "refinement_windows_considered": 0, "route_iterations": 6, - "score_component_calls": 7670, - "score_component_total_ns": 205617403, + "score_component_calls": 5962, + "score_component_total_ns": 164785883, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, - "static_safe_cache_hits": 1869, + "static_safe_cache_hits": 1276, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_candidate_nets": 1906, - "verify_dynamic_exact_pair_checks": 571, - "verify_path_report_calls": 176, - "verify_static_buffer_ops": 813, + "verify_dynamic_candidate_nets": 1884, + "verify_dynamic_exact_pair_checks": 557, + "verify_path_report_calls": 174, + "verify_static_buffer_ops": 805, "visibility_builds": 0, "visibility_corner_hits_exact": 0, "visibility_corner_index_builds": 1, @@ -1376,7 +1380,7 @@ "visibility_point_queries": 0, "visibility_tangent_candidate_corner_checks": 6, "visibility_tangent_candidate_ray_tests": 6, - "visibility_tangent_candidate_scans": 1536, + "visibility_tangent_candidate_scans": 1159, "warm_start_paths_built": 0, "warm_start_paths_used": 0 }, diff --git a/docs/iteration_trace.md b/docs/iteration_trace.md index febbce1..54e4967 100644 --- a/docs/iteration_trace.md +++ b/docs/iteration_trace.md @@ -1,6 +1,6 @@ # Iteration Trace -Generated at 2026-04-02T16:46:00-07:00 by `scripts/record_iteration_trace.py`. +Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_iteration_trace.py`. ## example_07_large_scale_routing_no_warm_start @@ -50,17 +50,17 @@ Results: 10 valid / 10 reached / 10 total. | 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 | | 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | | 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 | -| 4 | 384.2 | 6 | 6 | 2 | 10 | 142 | 550 | 1126 | 884 | -| 5 | 537.8 | 4 | 6 | 2 | 10 | 406 | 1477 | 3377 | 2666 | +| 4 | 384.2 | 6 | 6 | 2 | 10 | 136 | 505 | 1025 | 829 | +| 5 | 537.8 | 4 | 6 | 2 | 10 | 33 | 171 | 419 | 287 | Top nets by iteration-attributed nodes expanded: -- `net_03`: 435 - `net_09`: 250 -- `net_06`: 242 +- `net_03`: 199 - `net_00`: 177 -- `net_08`: 172 +- `net_08`: 166 - `net_07`: 140 +- `net_06`: 105 - `net_02`: 79 - `net_01`: 65 - `net_05`: 12 @@ -68,11 +68,11 @@ Top nets by iteration-attributed nodes expanded: Top nets by iteration-attributed congestion checks: -- `net_03`: 1434 -- `net_06`: 893 +- `net_03`: 639 - `net_07`: 454 -- `net_08`: 328 +- `net_06`: 382 - `net_02`: 290 +- `net_08`: 283 - `net_09`: 178 - `net_01`: 135 - `net_00`: 82 diff --git a/docs/optimization_pass_01_log.md b/docs/optimization_pass_01_log.md index 71badd0..12ffdb6 100644 --- a/docs/optimization_pass_01_log.md +++ b/docs/optimization_pass_01_log.md @@ -3662,3 +3662,80 @@ Findings: - The solved seed-42 no-warm canary stayed `10/10/10` and improved from `50` routed nets / `1303` nodes / `2921` congestion checks to `44` routed nets / `1258` nodes / `2736` congestion checks. - The seed-43 no-warm canary stayed `10/10/10` and improved from `60` routed nets / `1691` nodes / `4330` congestion checks to `46` routed nets / `1582` nodes / `3881` congestion checks. - Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` while trimming slightly to `5` routed nets, `297` nodes, and `146` congestion checks. + +## Step 67 route lighter late conflict nets first + +Measured on 2026-04-02T17:16:54-07:00. + +Findings: + +- Kept the late all-reached conflict-set reroute, but now order those subset reroutes by the previous iteration's attributed work ascending so the lighter partner nets settle first and the heavier nets route later against a more stable late-phase context. +- The solved seed-42 no-warm canary stayed effectively flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks. +- The seed-43 no-warm canary stayed `10/10/10` and improved slightly from `1582` nodes / `3881` congestion checks to `1576` nodes / `3836` congestion checks. +- The remaining late-phase hotspot is still concentrated in `net_03` and `net_06`, especially the final 4-net iteration in the seed-43 trace. + +## Step 68 pre-pair frontier diagnostics kept, scratch reroute rejected + +Measured on 2026-04-02T17:57:48-07:00. + +Findings: + +- Kept a new opt-in `capture_pre_pair_frontier_trace` surface plus `scripts/record_pre_pair_frontier_trace.py`, and tracked the first seed-42 vs seed-43 artifacts in `docs/pre_pair_frontier_trace.json` and `docs/pre_pair_frontier_trace.md`. +- The final unresolved subset iteration is now explicit: seed `42` captures iteration `4` with routed nets `net_07`, `net_06`, `net_00`, `net_01`; seed `43` captures iteration `5` with routed nets `net_07`, `net_02`, `net_06`, `net_03`. +- The seed-43 heavy-net concentration is confirmed by the new trace: `net_03` and `net_06` account for most of the last unresolved iteration's work, and the hotspot-adjacent sampled prunes in that basin are closed-set dominated rather than hard-collision dominated. +- I also measured a bounded pre-pair scratch reroute for the two heaviest traced nets, but rejected it: it added runtime, produced `0` accepted repairs, and left the solved canaries at the same `1258 / 2736` and `1576 / 3836` node/check totals after revert. + +## Step 69 cap heavy late-phase reroutes with incumbent fallback + +Measured on 2026-04-02T18:20:00-07:00. + +Findings: + +- In the final all-reached 4-net subset iteration, the router now caps only the heavy reroute endpoints whose previous-iteration attributed work is already pathological, and falls back to their incumbent reached-target paths if the capped reroute does not finish cleanly. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves from `1576` nodes / `3836` congestion checks to `1459` nodes / `3455` congestion checks, with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity. + +## Step 70 tighten late-phase cap from 128 to 64 + +Measured on 2026-04-02T18:33:00-07:00. + +Findings: + +- Tightened the bounded heavy-net late-phase reroute cap from `128` nodes to `64`, keeping the same incumbent fallback behavior and the same heavy-net selection rule. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1459` nodes / `3455` congestion checks to `1331` nodes / `3012` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity. + +## Step 71 tighten late-phase cap from 64 to 32 after cap sweep + +Measured on 2026-04-02T18:43:00-07:00. + +Findings: + +- Ran a cap sweep across `32`, `48`, `64`, `96`, `128`, and uncapped behavior for the two no-warm seeds. The winner was `32`: it preserved both `10/10/10` canaries and gave the best seed-43 node/check totals while leaving seed-42 flat. +- Landed that tighter cap in the router. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1331` nodes / `3012` congestion checks to `1267` nodes / `2813` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. + +## Step 72 tighten late-phase cap from 32 to 1 after floor sweep + +Measured on 2026-04-02T18:45:00-07:00. + +Findings: + +- Extended the cap sweep below `32` and found the same pattern continued all the way down to `1`: seed-42 stayed flat because the cap never fires there, while seed-43 kept getting cheaper and still converged through the same incumbent-fallback path. +- Landed the tightest safe setting, `1`, so late pathological reroutes now act as a minimal probe before immediately falling back to the incumbent reached-target path if they do not finish cleanly. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1267` nodes / `2813` congestion checks to `1205` nodes / `2548` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. + +## Step 73 skip capped late-phase reroutes and carry the incumbent directly + +Measured on 2026-04-02T18:52:00-07:00. + +Findings: + +- Characterization showed the two capped late seed-43 reroutes were pure churn even at a `1`-node cap: they always fell back to the incumbent reached-target path and pair-local repair still resolved the final pairs. +- Moved that behavior into `_route_net_once()` directly: when a late heavy reroute is already capped and has a reached-target incumbent fallback, the router now reinstalls the incumbent immediately instead of calling `route_astar()` for a doomed probe. +- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`. +- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1205` nodes / `2548` congestion checks to `1203` nodes / `2530` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`. diff --git a/docs/performance.md b/docs/performance.md index 57d9f76..3e801d9 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -5,30 +5,23 @@ Generated on 2026-04-02 by `scripts/record_performance_baseline.py`. The full machine-readable snapshot lives in `docs/performance_baseline.json`. Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot. -The default baseline table below covers the standard example corpus only. The heavier no-warm `example_07` canaries remain performance-only and are tracked through targeted diffs plus the trace artifacts. - -Use `scripts/characterize_pair_local_search.py` for the tracked no-warm sweep in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`. -Use `scripts/record_iteration_trace.py` for the current seed-42 vs seed-43 negotiated-congestion attribution in `docs/iteration_trace.json` and `docs/iteration_trace.md`. - | Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls | | :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | -| example_01_simple_route | 0.0038 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 4 | -| example_02_congestion_resolution | 0.3614 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 38 | -| example_03_locked_paths | 0.1953 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 16 | -| example_04_sbends_and_radii | 0.0277 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 8 | -| example_05_orientation_stress | 0.2537 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 17 | -| example_06_bend_collision_models | 0.2103 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 12 | -| example_07_large_scale_routing | 0.2074 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 40 | -| example_08_custom_bend_geometry | 0.0186 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 8 | -| example_09_unroutable_best_effort | 0.0079 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | +| example_01_simple_route | 0.0037 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 5 | +| example_02_congestion_resolution | 0.3361 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 41 | +| example_03_locked_paths | 0.1877 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 18 | +| example_04_sbends_and_radii | 0.0269 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 10 | +| example_05_orientation_stress | 0.2311 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 20 | +| example_06_bend_collision_models | 0.1988 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 15 | +| example_07_large_scale_routing | 0.2088 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 50 | +| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 10 | +| example_09_unroutable_best_effort | 0.0057 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | ## Full Counter Set Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters. These counters are currently observational only and are not enforced as CI regression gates. -The current accepted branch keeps the post-loop pair-local scratch reroute and now also narrows late negotiated reroutes to the current conflict set once all nets already reach target and only a few conflict edges remain. That keeps both no-warm `example_07` seeds at `10/10/10` while reducing main-loop work before pair-local repair. - Tracked metric keys: -nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded +nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded, late_phase_capped_nets, late_phase_capped_fallbacks diff --git a/docs/performance_baseline.json b/docs/performance_baseline.json index 6dd6ae2..952600d 100644 --- a/docs/performance_baseline.json +++ b/docs/performance_baseline.json @@ -3,7 +3,7 @@ "generator": "scripts/record_performance_baseline.py", "scenarios": [ { - "duration_s": 0.003825429128482938, + "duration_s": 0.003715757979080081, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -47,6 +47,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 1, "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 1, "move_cache_abs_misses": 10, "move_cache_rel_hits": 0, @@ -85,7 +87,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 11, - "score_component_total_ns": 16571, + "score_component_total_ns": 16864, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -93,7 +95,7 @@ "timeout_events": 0, "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, - "verify_path_report_calls": 4, + "verify_path_report_calls": 5, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -115,7 +117,7 @@ "valid_results": 1 }, { - "duration_s": 0.36141274496912956, + "duration_s": 0.33605348505079746, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -159,6 +161,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 3, "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 12, "move_cache_abs_misses": 1401, "move_cache_rel_hits": 1293, @@ -197,15 +201,15 @@ "refinement_windows_considered": 10, "route_iterations": 1, "score_component_calls": 976, - "score_component_total_ns": 1143187, + "score_component_total_ns": 1109505, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, "static_tree_rebuilds": 2, "timeout_events": 0, - "verify_dynamic_candidate_nets": 88, - "verify_dynamic_exact_pair_checks": 86, - "verify_path_report_calls": 38, + "verify_dynamic_candidate_nets": 92, + "verify_dynamic_exact_pair_checks": 90, + "verify_path_report_calls": 41, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -227,7 +231,7 @@ "valid_results": 3 }, { - "duration_s": 0.19532882701605558, + "duration_s": 0.18771230895072222, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -271,6 +275,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 2, "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 1, "move_cache_abs_misses": 903, "move_cache_rel_hits": 821, @@ -309,16 +315,16 @@ "refinement_windows_considered": 2, "route_iterations": 2, "score_component_calls": 504, - "score_component_total_ns": 565663, + "score_component_total_ns": 546567, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 1, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_candidate_nets": 9, - "verify_dynamic_exact_pair_checks": 9, - "verify_path_report_calls": 16, - "verify_static_buffer_ops": 81, + "verify_dynamic_candidate_nets": 10, + "verify_dynamic_exact_pair_checks": 10, + "verify_path_report_calls": 18, + "verify_static_buffer_ops": 90, "visibility_builds": 0, "visibility_corner_hits_exact": 0, "visibility_corner_index_builds": 2, @@ -339,7 +345,7 @@ "valid_results": 2 }, { - "duration_s": 0.027705274987965822, + "duration_s": 0.026945222169160843, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -383,6 +389,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 2, "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 1, "move_cache_abs_misses": 122, "move_cache_rel_hits": 80, @@ -421,15 +429,15 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 90, - "score_component_total_ns": 96756, + "score_component_total_ns": 97710, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_candidate_nets": 9, + "verify_dynamic_candidate_nets": 12, "verify_dynamic_exact_pair_checks": 0, - "verify_path_report_calls": 8, + "verify_path_report_calls": 10, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -451,7 +459,7 @@ "valid_results": 2 }, { - "duration_s": 0.25367443496361375, + "duration_s": 0.23108969815075397, "metrics": { "congestion_cache_hits": 3, "congestion_cache_misses": 146, @@ -495,6 +503,8 @@ "iteration_conflicting_nets": 2, "iteration_reverified_nets": 6, "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 374, "move_cache_abs_misses": 1306, "move_cache_rel_hits": 1204, @@ -533,7 +543,7 @@ "refinement_windows_considered": 0, "route_iterations": 2, "score_component_calls": 1234, - "score_component_total_ns": 1311211, + "score_component_total_ns": 1223569, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 8, @@ -541,7 +551,7 @@ "timeout_events": 0, "verify_dynamic_candidate_nets": 8, "verify_dynamic_exact_pair_checks": 12, - "verify_path_report_calls": 17, + "verify_path_report_calls": 20, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -563,7 +573,7 @@ "valid_results": 3 }, { - "duration_s": 0.21031348290853202, + "duration_s": 0.19879506202414632, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -589,7 +599,7 @@ "danger_map_cache_misses": 731, "danger_map_lookup_calls": 1914, "danger_map_query_calls": 731, - "danger_map_total_ns": 19983976, + "danger_map_total_ns": 19050142, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 54, "dynamic_path_objects_removed": 36, @@ -607,6 +617,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 3, "iteration_reverify_calls": 3, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 186, "move_cache_abs_misses": 840, "move_cache_rel_hits": 702, @@ -645,7 +657,7 @@ "refinement_windows_considered": 0, "route_iterations": 3, "score_component_calls": 842, - "score_component_total_ns": 22474166, + "score_component_total_ns": 21353240, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3, "static_safe_cache_hits": 141, @@ -653,8 +665,8 @@ "timeout_events": 0, "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, - "verify_path_report_calls": 12, - "verify_static_buffer_ops": 72, + "verify_path_report_calls": 15, + "verify_static_buffer_ops": 90, "visibility_builds": 0, "visibility_corner_hits_exact": 0, "visibility_corner_index_builds": 3, @@ -675,7 +687,7 @@ "valid_results": 3 }, { - "duration_s": 0.20740868314169347, + "duration_s": 0.20880168909206986, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -701,7 +713,7 @@ "danger_map_cache_misses": 448, "danger_map_lookup_calls": 681, "danger_map_query_calls": 448, - "danger_map_total_ns": 11224403, + "danger_map_total_ns": 11025527, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 132, "dynamic_path_objects_removed": 88, @@ -719,6 +731,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 10, "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 6, "move_cache_abs_misses": 366, "move_cache_rel_hits": 275, @@ -757,16 +771,16 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 291, - "score_component_total_ns": 12117666, + "score_component_total_ns": 11875928, "static_net_tree_rebuilds": 10, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 6, "static_tree_rebuilds": 10, "timeout_events": 0, - "verify_dynamic_candidate_nets": 370, - "verify_dynamic_exact_pair_checks": 56, - "verify_path_report_calls": 40, - "verify_static_buffer_ops": 176, + "verify_dynamic_candidate_nets": 476, + "verify_dynamic_exact_pair_checks": 72, + "verify_path_report_calls": 50, + "verify_static_buffer_ops": 220, "visibility_builds": 0, "visibility_corner_hits_exact": 0, "visibility_corner_index_builds": 10, @@ -787,7 +801,7 @@ "valid_results": 10 }, { - "duration_s": 0.018604618962854147, + "duration_s": 0.017696003895252943, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -831,6 +845,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 2, "iteration_reverify_calls": 2, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 2, "move_cache_abs_misses": 76, "move_cache_rel_hits": 32, @@ -869,7 +885,7 @@ "refinement_windows_considered": 0, "route_iterations": 2, "score_component_calls": 72, - "score_component_total_ns": 87655, + "score_component_total_ns": 87742, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 2, @@ -877,7 +893,7 @@ "timeout_events": 0, "verify_dynamic_candidate_nets": 0, "verify_dynamic_exact_pair_checks": 0, - "verify_path_report_calls": 8, + "verify_path_report_calls": 10, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -899,7 +915,7 @@ "valid_results": 2 }, { - "duration_s": 0.00794802000746131, + "duration_s": 0.005660973023623228, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -925,7 +941,7 @@ "danger_map_cache_misses": 20, "danger_map_lookup_calls": 30, "danger_map_query_calls": 20, - "danger_map_total_ns": 675454, + "danger_map_total_ns": 515133, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 2, "dynamic_path_objects_removed": 1, @@ -943,6 +959,8 @@ "iteration_conflicting_nets": 0, "iteration_reverified_nets": 0, "iteration_reverify_calls": 1, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, "move_cache_abs_hits": 0, "move_cache_abs_misses": 16, "move_cache_rel_hits": 2, @@ -981,7 +999,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 14, - "score_component_total_ns": 722637, + "score_component_total_ns": 554809, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 0, diff --git a/docs/pre_pair_frontier_trace.json b/docs/pre_pair_frontier_trace.json new file mode 100644 index 0000000..42d83c6 --- /dev/null +++ b/docs/pre_pair_frontier_trace.json @@ -0,0 +1,1005 @@ +{ + "generated_at": "2026-04-02T18:51:01-07:00", + "generator": "scripts/record_pre_pair_frontier_trace.py", + "scenarios": [ + { + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 2736, + "congestion_candidate_ids": 5785, + "congestion_candidate_nets": 6163, + "congestion_candidate_precheck_hits": 1383, + "congestion_candidate_precheck_misses": 1418, + "congestion_candidate_precheck_skips": 34, + "congestion_check_calls": 2736, + "congestion_exact_pair_checks": 4780, + "congestion_grid_net_cache_hits": 1356, + "congestion_grid_net_cache_misses": 2608, + "congestion_grid_span_cache_hits": 1247, + "congestion_grid_span_cache_misses": 1308, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1452, + "congestion_net_envelope_cache_misses": 2720, + "congestion_presence_cache_hits": 1541, + "congestion_presence_cache_misses": 1642, + "congestion_presence_skips": 382, + "danger_map_cache_hits": 11547, + "danger_map_cache_misses": 6063, + "danger_map_lookup_calls": 17610, + "danger_map_query_calls": 6063, + "danger_map_total_ns": 171779571, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 399, + "dynamic_path_objects_removed": 351, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 6750.0, + "guidance_bonus_applied_bend90": 2250.0, + "guidance_bonus_applied_sbend": 375.0, + "guidance_bonus_applied_straight": 4125.0, + "guidance_match_moves": 108, + "guidance_match_moves_bend90": 36, + "guidance_match_moves_sbend": 6, + "guidance_match_moves_straight": 66, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 37, + "iteration_conflicting_nets": 32, + "iteration_reverified_nets": 50, + "iteration_reverify_calls": 5, + "late_phase_capped_fallbacks": 0, + "late_phase_capped_nets": 0, + "move_cache_abs_hits": 1200, + "move_cache_abs_misses": 5338, + "move_cache_rel_hits": 4768, + "move_cache_rel_misses": 570, + "moves_added": 5853, + "moves_generated": 6538, + "nets_carried_forward": 6, + "nets_reached_target": 44, + "nets_routed": 44, + "nodes_expanded": 1258, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 374, + "pruned_cost": 311, + "pruned_hard_collision": 0, + "ray_cast_calls": 4310, + "ray_cast_calls_expand_forward": 1214, + "ray_cast_calls_expand_snap": 39, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3051, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 159, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 5, + "score_component_calls": 6181, + "score_component_total_ns": 192508906, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1170, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1822, + "verify_dynamic_exact_pair_checks": 504, + "verify_path_report_calls": 164, + "verify_static_buffer_ops": 779, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1214, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "pre_pair_frontier_trace": { + "conflict_edges": [ + [ + "net_01", + "net_02" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 4, + "nets": [ + { + "congestion_check_calls": 30, + "frontier": { + "hotspot_bounds": [ + [ + 827.6047906391041, + 482.9684848604278, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 555.0, + 916.0, + 598.0 + ] + ], + "net_id": "net_07", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 179, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947606, + 482.8851198636423, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 545.0, + 916.2379632934325, + 582.0 + ], + [ + 883.7620367065675, + 571.0, + 916.0, + 652.2850117535756 + ] + ], + "net_id": "net_06", + "pruned_closed_set": 5, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 510, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 46, + "pruned_closed_set": 7, + "pruned_cost": 15, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 43, + "frontier": { + "hotspot_bounds": [ + [ + 506.3396407947511, + 398.0, + 597.3906878342618, + 487.1148801363561 + ], + [ + 564.0, + 224.0, + 596.0, + 415.0 + ] + ], + "net_id": "net_00", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 80, + "frontier": { + "hotspot_bounds": [ + [ + 506.3396407947511, + 398.0, + 597.3906878342618, + 487.1148801363561 + ], + [ + 564.0, + 388.0, + 596.2379632934325, + 425.0 + ], + [ + 563.7620367065675, + 169.71498824645388, + 596.0, + 251.0 + ] + ], + "net_id": "net_01", + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [] + }, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 18, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0 + } + ], + "routed_net_ids": [ + "net_07", + "net_06", + "net_00", + "net_01" + ] + }, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "metrics": { + "congestion_cache_hits": 8, + "congestion_cache_misses": 2530, + "congestion_candidate_ids": 6173, + "congestion_candidate_nets": 5869, + "congestion_candidate_precheck_hits": 1152, + "congestion_candidate_precheck_misses": 1460, + "congestion_candidate_precheck_skips": 74, + "congestion_check_calls": 2530, + "congestion_exact_pair_checks": 4800, + "congestion_grid_net_cache_hits": 1192, + "congestion_grid_net_cache_misses": 2676, + "congestion_grid_span_cache_hits": 1065, + "congestion_grid_span_cache_misses": 1366, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 1234, + "congestion_net_envelope_cache_misses": 2769, + "congestion_presence_cache_hits": 1302, + "congestion_presence_cache_misses": 1664, + "congestion_presence_skips": 354, + "danger_map_cache_hits": 11485, + "danger_map_cache_misses": 5474, + "danger_map_lookup_calls": 16959, + "danger_map_query_calls": 5474, + "danger_map_total_ns": 143896014, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 397, + "dynamic_path_objects_removed": 350, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 7562.5, + "guidance_bonus_applied_bend90": 2937.5, + "guidance_bonus_applied_sbend": 250.0, + "guidance_bonus_applied_straight": 4375.0, + "guidance_match_moves": 121, + "guidance_match_moves_bend90": 47, + "guidance_match_moves_sbend": 4, + "guidance_match_moves_straight": 70, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 39, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "late_phase_capped_fallbacks": 2, + "late_phase_capped_nets": 2, + "move_cache_abs_hits": 1304, + "move_cache_abs_misses": 4997, + "move_cache_rel_hits": 4419, + "move_cache_rel_misses": 578, + "moves_added": 5638, + "moves_generated": 6301, + "nets_carried_forward": 14, + "nets_reached_target": 44, + "nets_routed": 46, + "nodes_expanded": 1203, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 39, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 354, + "pruned_cost": 309, + "pruned_hard_collision": 0, + "ray_cast_calls": 4059, + "ray_cast_calls_expand_forward": 1159, + "ray_cast_calls_expand_snap": 13, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 2881, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 170, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 5962, + "score_component_total_ns": 163113517, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 1276, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 1884, + "verify_dynamic_exact_pair_checks": 557, + "verify_path_report_calls": 174, + "verify_static_buffer_ops": 805, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1159, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start_seed43", + "pre_pair_frontier_trace": { + "conflict_edges": [ + [ + "net_02", + "net_03" + ], + [ + "net_06", + "net_07" + ] + ], + "iteration": 5, + "nets": [ + { + "congestion_check_calls": 85, + "frontier": { + "hotspot_bounds": [ + [ + 827.6047906391041, + 482.9684848604278, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 555.0, + 916.0, + 598.0 + ] + ], + "net_id": "net_07", + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 520, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 520, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 520, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 520, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 16, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 86, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947511, + 418.0, + 917.3906878342618, + 507.1148801363561 + ], + [ + 884.0, + 402.0, + 916.0, + 435.0 + ] + ], + "net_id": "net_02", + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 470, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 470, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 470, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 470, + 0 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 17, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 0, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947606, + 482.8851198636423, + 917.390687834262, + 572.0 + ], + [ + 884.0, + 545.0, + 916.2379632934325, + 582.0 + ], + [ + 883.7620367065675, + 571.0, + 916.0, + 652.2850117535756 + ] + ], + "net_id": "net_06", + "pruned_closed_set": 8, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 510, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 881, + 510, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 895, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 895, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 880, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 880, + 638, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 827, + 270 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 633, + 270 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 832, + 270 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0 + }, + { + "congestion_check_calls": 0, + "frontier": { + "hotspot_bounds": [ + [ + 826.3396407947511, + 418.0, + 917.3906878342618, + 507.1148801363561 + ], + [ + 884.0, + 408.0, + 916.2379632934325, + 445.0 + ], + [ + 883.7620367065675, + 347.71498824645397, + 916.0, + 429.0 + ] + ], + "net_id": "net_03", + "pruned_closed_set": 12, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "pruned_self_collision": 0, + "samples": [ + { + "end_state": [ + 850, + 480, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 525, + 480, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 850, + 480, + 0 + ], + "hotspot_index": 0, + "move_type": "straight", + "parent_state": [ + 763, + 480, + 0 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 895, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 895, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 880, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 880, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 896, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 896, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 891, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 891, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 886, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 886, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 882, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 882, + 362, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 163, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 158, + 90 + ], + "reason": "closed_set" + }, + { + "end_state": [ + 900, + 367, + 90 + ], + "hotspot_index": 2, + "move_type": "straight", + "parent_state": [ + 900, + 267, + 90 + ], + "reason": "closed_set" + } + ] + }, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 0, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0 + } + ], + "routed_net_ids": [ + "net_07", + "net_02", + "net_06", + "net_03" + ] + }, + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/pre_pair_frontier_trace.md b/docs/pre_pair_frontier_trace.md new file mode 100644 index 0000000..a760f82 --- /dev/null +++ b/docs/pre_pair_frontier_trace.md @@ -0,0 +1,48 @@ +# Pre-Pair Frontier Trace + +Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_pre_pair_frontier_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +Captured iteration: `4` + +Conflict edges: `(('net_01', 'net_02'), ('net_06', 'net_07'))` + +| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples | +| :-- | --: | --: | --: | --: | --: | :--: | --: | +| net_07 | 7 | 30 | 1 | 0 | 0 | yes | 0 | +| net_06 | 46 | 179 | 7 | 15 | 0 | yes | 5 | +| net_00 | 10 | 43 | 1 | 0 | 0 | yes | 0 | +| net_01 | 18 | 80 | 3 | 0 | 0 | yes | 0 | + +Frontier prune totals by reason: + +- `closed_set`: 5 +- `hard_collision`: 0 +- `self_collision`: 0 +- `cost`: 0 + +## example_07_large_scale_routing_no_warm_start_seed43 + +Results: 10 valid / 10 reached / 10 total. + +Captured iteration: `5` + +Conflict edges: `(('net_02', 'net_03'), ('net_06', 'net_07'))` + +| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples | +| :-- | --: | --: | --: | --: | --: | :--: | --: | +| net_07 | 16 | 85 | 3 | 0 | 0 | yes | 2 | +| net_02 | 17 | 86 | 4 | 0 | 0 | yes | 3 | +| net_06 | 0 | 0 | 0 | 0 | 0 | yes | 8 | +| net_03 | 0 | 0 | 0 | 0 | 0 | yes | 12 | + +Frontier prune totals by reason: + +- `closed_set`: 25 +- `hard_collision`: 0 +- `self_collision`: 0 +- `cost`: 0 + diff --git a/inire/__init__.py b/inire/__init__.py index 72be25f..a53b46a 100644 --- a/inire/__init__.py +++ b/inire/__init__.py @@ -22,6 +22,8 @@ from .results import ( # noqa: PLC0414 IterationTraceEntry as IterationTraceEntry, NetConflictTrace as NetConflictTrace, NetFrontierTrace as NetFrontierTrace, + PrePairFrontierTraceEntry as PrePairFrontierTraceEntry, + PrePairNetTrace as PrePairNetTrace, RoutingResult as RoutingResult, RoutingRunResult as RoutingRunResult, ) @@ -49,6 +51,7 @@ def route( expanded_nodes=tuple(finder.accumulated_expanded_nodes), conflict_trace=tuple(finder.conflict_trace), frontier_trace=tuple(finder.frontier_trace), + pre_pair_frontier_trace=finder.pre_pair_frontier_trace, iteration_trace=tuple(finder.iteration_trace), ) @@ -67,6 +70,8 @@ __all__ = [ "FrontierPruneSample", "IterationNetAttemptTrace", "IterationTraceEntry", + "PrePairFrontierTraceEntry", + "PrePairNetTrace", "RefinementOptions", "RoutingOptions", "RoutingProblem", diff --git a/inire/model.py b/inire/model.py index 2e98941..f9c8020 100644 --- a/inire/model.py +++ b/inire/model.py @@ -108,6 +108,7 @@ class DiagnosticsOptions: capture_conflict_trace: bool = False capture_frontier_trace: bool = False capture_iteration_trace: bool = False + capture_pre_pair_frontier_trace: bool = False @dataclass(frozen=True, slots=True) diff --git a/inire/results.py b/inire/results.py index 3898e62..d273756 100644 --- a/inire/results.py +++ b/inire/results.py @@ -78,6 +78,26 @@ class NetFrontierTrace: samples: tuple[FrontierPruneSample, ...] = () +@dataclass(frozen=True, slots=True) +class PrePairNetTrace: + net_id: str + nodes_expanded: int + congestion_check_calls: int + pruned_closed_set: int + pruned_cost: int + pruned_hard_collision: int + guidance_seed_present: bool + frontier: NetFrontierTrace + + +@dataclass(frozen=True, slots=True) +class PrePairFrontierTraceEntry: + iteration: int + routed_net_ids: tuple[str, ...] + conflict_edges: tuple[tuple[str, str], ...] + nets: tuple[PrePairNetTrace, ...] + + @dataclass(frozen=True, slots=True) class IterationNetAttemptTrace: net_id: str @@ -210,6 +230,8 @@ class RouteMetrics: pair_local_search_attempts: int pair_local_search_accepts: int pair_local_search_nodes_expanded: int + late_phase_capped_nets: int + late_phase_capped_fallbacks: int @dataclass(frozen=True, slots=True) @@ -258,4 +280,5 @@ class RoutingRunResult: expanded_nodes: tuple[tuple[int, int, int], ...] = () conflict_trace: tuple[ConflictTraceEntry, ...] = () frontier_trace: tuple[NetFrontierTrace, ...] = () + pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None iteration_trace: tuple[IterationTraceEntry, ...] = () diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py index 83fa899..1c3b7a1 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -258,6 +258,8 @@ class AStarMetrics: "total_pair_local_search_attempts", "total_pair_local_search_accepts", "total_pair_local_search_nodes_expanded", + "total_late_phase_capped_nets", + "total_late_phase_capped_fallbacks", "last_expanded_nodes", "nodes_expanded", "moves_generated", @@ -371,6 +373,8 @@ class AStarMetrics: self.total_pair_local_search_attempts = 0 self.total_pair_local_search_accepts = 0 self.total_pair_local_search_nodes_expanded = 0 + self.total_late_phase_capped_nets = 0 + self.total_late_phase_capped_fallbacks = 0 self.last_expanded_nodes: list[tuple[int, int, int]] = [] self.nodes_expanded = 0 self.moves_generated = 0 @@ -483,6 +487,8 @@ class AStarMetrics: self.total_pair_local_search_attempts = 0 self.total_pair_local_search_accepts = 0 self.total_pair_local_search_nodes_expanded = 0 + self.total_late_phase_capped_nets = 0 + self.total_late_phase_capped_fallbacks = 0 def reset_per_route(self) -> None: self.nodes_expanded = 0 @@ -598,6 +604,8 @@ class AStarMetrics: pair_local_search_attempts=self.total_pair_local_search_attempts, pair_local_search_accepts=self.total_pair_local_search_accepts, pair_local_search_nodes_expanded=self.total_pair_local_search_nodes_expanded, + late_phase_capped_nets=self.total_late_phase_capped_nets, + late_phase_capped_fallbacks=self.total_late_phase_capped_fallbacks, ) diff --git a/inire/router/_router.py b/inire/router/_router.py index 920d6b6..9c19fb0 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -15,6 +15,8 @@ from inire.results import ( IterationTraceEntry, NetConflictTrace, NetFrontierTrace, + PrePairFrontierTraceEntry, + PrePairNetTrace, RoutingOutcome, RoutingReport, RoutingResult, @@ -53,6 +55,8 @@ class _RoutingState: last_conflict_edge_count: int repeated_conflict_count: int pair_local_plateau_count: int + recent_attempt_work: dict[str, int] + pre_pair_candidate: _PrePairCandidate | None @dataclass(slots=True) @@ -68,6 +72,14 @@ class _PairLocalTarget: net_ids: tuple[str, str] +@dataclass(frozen=True, slots=True) +class _PrePairCandidate: + iteration: int + routed_net_ids: tuple[str, ...] + conflict_edges: tuple[tuple[str, str], ...] + net_attempts: tuple[IterationNetAttemptTrace, ...] + + _ITERATION_TRACE_TOTALS = ( "nodes_expanded", "congestion_check_calls", @@ -92,6 +104,7 @@ class PathFinder: "accumulated_expanded_nodes", "conflict_trace", "frontier_trace", + "pre_pair_frontier_trace", "iteration_trace", ) @@ -110,6 +123,7 @@ class PathFinder: self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.conflict_trace: list[ConflictTraceEntry] = [] self.frontier_trace: list[NetFrontierTrace] = [] + self.pre_pair_frontier_trace: PrePairFrontierTraceEntry | None = None self.iteration_trace: list[IterationTraceEntry] = [] def _metric_total(self, metric_name: str) -> int: @@ -219,6 +233,8 @@ class PathFinder: last_conflict_edge_count=0, repeated_conflict_count=0, pair_local_plateau_count=0, + recent_attempt_work={}, + pre_pair_candidate=None, ) if state.initial_paths is None and congestion.warm_start_enabled: state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order) @@ -399,6 +415,93 @@ class PathFinder: return tuple(hotspot_bounds) + def _capture_single_frontier_trace( + self, + state: _RoutingState, + net_id: str, + result: RoutingResult, + hotspot_bounds: tuple[tuple[float, float, float, float], ...], + ) -> NetFrontierTrace: + if not hotspot_bounds: + return NetFrontierTrace( + net_id=net_id, + hotspot_bounds=(), + pruned_closed_set=0, + pruned_hard_collision=0, + pruned_self_collision=0, + pruned_cost=0, + ) + + original_metrics = self.metrics + original_context_metrics = self.context.metrics + original_engine_metrics = self.context.cost_evaluator.collision_engine.metrics + original_danger_metrics = None + if self.context.cost_evaluator.danger_map is not None: + original_danger_metrics = self.context.cost_evaluator.danger_map.metrics + + try: + scratch_metrics = AStarMetrics() + self.context.metrics = scratch_metrics + self.context.cost_evaluator.collision_engine.metrics = scratch_metrics + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = scratch_metrics + + guidance_seed = result.as_seed().segments if result.path else None + guidance_bonus = 0.0 + if guidance_seed: + guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) + collector = FrontierTraceCollector(hotspot_bounds=hotspot_bounds) + run_config = SearchRunConfig.from_options( + self.context.options, + return_partial=True, + store_expanded=False, + guidance_seed=guidance_seed, + guidance_bonus=guidance_bonus, + frontier_trace=collector, + self_collision_check=(net_id in state.needs_self_collision_check), + node_limit=self.context.options.search.node_limit, + ) + + self.context.cost_evaluator.collision_engine.remove_path(net_id) + try: + route_astar( + state.net_specs[net_id].start, + state.net_specs[net_id].target, + state.net_specs[net_id].width, + context=self.context, + metrics=scratch_metrics, + net_id=net_id, + config=run_config, + ) + finally: + if result.path: + self._install_path(net_id, result.path) + + return NetFrontierTrace( + net_id=net_id, + hotspot_bounds=hotspot_bounds, + pruned_closed_set=collector.pruned_closed_set, + pruned_hard_collision=collector.pruned_hard_collision, + pruned_self_collision=collector.pruned_self_collision, + pruned_cost=collector.pruned_cost, + samples=tuple( + FrontierPruneSample( + reason=reason, # type: ignore[arg-type] + move_type=move_type, + hotspot_index=hotspot_index, + parent_state=parent_state, + end_state=end_state, + ) + for reason, move_type, hotspot_index, parent_state, end_state in collector.samples + ), + ) + finally: + self.metrics = original_metrics + self.context.metrics = original_context_metrics + self.context.cost_evaluator.collision_engine.metrics = original_engine_metrics + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = original_danger_metrics + def _analyze_results( self, ordered_net_ids: Sequence[str], @@ -477,90 +580,26 @@ class PathFinder: capture_component_conflicts=True, count_iteration_metrics=False, ) + for net_id in state.ordered_net_ids: + result = state.results.get(net_id) + detail = details_by_net.get(net_id) + if result is None or detail is None or not result.reached_target: + continue + if detail.report.dynamic_collision_count == 0 or not detail.component_conflicts: + continue - original_metrics = self.metrics - original_context_metrics = self.context.metrics - original_engine_metrics = self.context.cost_evaluator.collision_engine.metrics - original_danger_metrics = None - if self.context.cost_evaluator.danger_map is not None: - original_danger_metrics = self.context.cost_evaluator.danger_map.metrics + hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, details_by_net) + if not hotspot_bounds: + continue - try: - for net_id in state.ordered_net_ids: - result = state.results.get(net_id) - detail = details_by_net.get(net_id) - if result is None or detail is None or not result.reached_target: - continue - if detail.report.dynamic_collision_count == 0 or not detail.component_conflicts: - continue - - hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, details_by_net) - if not hotspot_bounds: - continue - - scratch_metrics = AStarMetrics() - self.context.metrics = scratch_metrics - self.context.cost_evaluator.collision_engine.metrics = scratch_metrics - if self.context.cost_evaluator.danger_map is not None: - self.context.cost_evaluator.danger_map.metrics = scratch_metrics - - guidance_seed = result.as_seed().segments if result.path else None - guidance_bonus = 0.0 - if guidance_seed: - guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) - collector = FrontierTraceCollector(hotspot_bounds=hotspot_bounds) - run_config = SearchRunConfig.from_options( - self.context.options, - return_partial=True, - store_expanded=False, - guidance_seed=guidance_seed, - guidance_bonus=guidance_bonus, - frontier_trace=collector, - self_collision_check=(net_id in state.needs_self_collision_check), - node_limit=self.context.options.search.node_limit, + self.frontier_trace.append( + self._capture_single_frontier_trace( + state, + net_id, + result, + hotspot_bounds, ) - - self.context.cost_evaluator.collision_engine.remove_path(net_id) - try: - route_astar( - state.net_specs[net_id].start, - state.net_specs[net_id].target, - state.net_specs[net_id].width, - context=self.context, - metrics=scratch_metrics, - net_id=net_id, - config=run_config, - ) - finally: - if result.path: - self._install_path(net_id, result.path) - - self.frontier_trace.append( - NetFrontierTrace( - net_id=net_id, - hotspot_bounds=hotspot_bounds, - pruned_closed_set=collector.pruned_closed_set, - pruned_hard_collision=collector.pruned_hard_collision, - pruned_self_collision=collector.pruned_self_collision, - pruned_cost=collector.pruned_cost, - samples=tuple( - FrontierPruneSample( - reason=reason, # type: ignore[arg-type] - move_type=move_type, - hotspot_index=hotspot_index, - parent_state=parent_state, - end_state=end_state, - ) - for reason, move_type, hotspot_index, parent_state, end_state in collector.samples - ), - ) - ) - finally: - self.metrics = original_metrics - self.context.metrics = original_context_metrics - self.context.cost_evaluator.collision_engine.metrics = original_engine_metrics - if self.context.cost_evaluator.danger_map is not None: - self.context.cost_evaluator.danger_map.metrics = original_danger_metrics + ) def _whole_set_is_better( self, @@ -673,6 +712,50 @@ class PathFinder: metrics=AStarMetrics(), ) + def _materialize_pre_pair_frontier_trace( + self, + state: _RoutingState, + results: dict[str, RoutingResult], + details_by_net: dict[str, PathVerificationDetail], + review: _IterationReview, + ) -> PrePairFrontierTraceEntry | None: + candidate = state.pre_pair_candidate + if candidate is None: + return None + + result_by_net = dict(results) + detail_by_net = dict(details_by_net) + nets: list[PrePairNetTrace] = [] + attempt_by_net = {attempt.net_id: attempt for attempt in candidate.net_attempts} + for net_id in candidate.routed_net_ids: + attempt = attempt_by_net.get(net_id) + result = result_by_net.get(net_id) + detail = detail_by_net.get(net_id) + if attempt is None or result is None or detail is None or not result.reached_target: + continue + hotspot_bounds = self._build_frontier_hotspot_bounds(state, net_id, detail_by_net) + nets.append( + PrePairNetTrace( + net_id=net_id, + nodes_expanded=attempt.nodes_expanded, + congestion_check_calls=attempt.congestion_check_calls, + pruned_closed_set=attempt.pruned_closed_set, + pruned_cost=attempt.pruned_cost, + pruned_hard_collision=attempt.pruned_hard_collision, + guidance_seed_present=attempt.guidance_seed_present, + frontier=self._capture_single_frontier_trace(state, net_id, result, hotspot_bounds), + ) + ) + + if not nets: + return None + return PrePairFrontierTraceEntry( + iteration=candidate.iteration, + routed_net_ids=candidate.routed_net_ids, + conflict_edges=candidate.conflict_edges, + nets=tuple(nets), + ) + def _run_pair_local_attempt( self, state: _RoutingState, @@ -780,12 +863,17 @@ class PathFinder: state: _RoutingState, iteration: int, net_id: str, + *, + node_limit_override: int | None = None, + incumbent_fallback: RoutingResult | None = None, ) -> tuple[RoutingResult, bool]: search = self.context.options.search congestion = self.context.options.congestion diagnostics = self.context.options.diagnostics net = state.net_specs[net_id] self.metrics.total_nets_routed += 1 + if node_limit_override is not None: + self.metrics.total_late_phase_capped_nets += 1 self.context.cost_evaluator.collision_engine.remove_path(net_id) guidance_seed_present = False @@ -808,6 +896,16 @@ class PathFinder: guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) guidance_seed_present = True + if ( + node_limit_override is not None + and incumbent_fallback is not None + and incumbent_fallback.reached_target + and incumbent_fallback.path + ): + self.metrics.total_late_phase_capped_fallbacks += 1 + self._install_path(net_id, incumbent_fallback.path) + return incumbent_fallback, guidance_seed_present + run_config = SearchRunConfig.from_options( self.context.options, bend_collision_type=coll_model, @@ -817,7 +915,7 @@ class PathFinder: guidance_bonus=guidance_bonus, skip_congestion=skip_congestion, self_collision_check=(net_id in state.needs_self_collision_check), - node_limit=search.node_limit, + node_limit=search.node_limit if node_limit_override is None else node_limit_override, ) path = route_astar( net.start, @@ -833,9 +931,17 @@ class PathFinder: state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes) if not path: + if incumbent_fallback is not None and incumbent_fallback.reached_target and incumbent_fallback.path: + self.metrics.total_late_phase_capped_fallbacks += 1 + self._install_path(net_id, incumbent_fallback.path) + return incumbent_fallback, guidance_seed_present return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present reached_target = path[-1].end_port == net.target + if not reached_target and incumbent_fallback is not None and incumbent_fallback.reached_target and incumbent_fallback.path: + self.metrics.total_late_phase_capped_fallbacks += 1 + self._install_path(net_id, incumbent_fallback.path) + return incumbent_fallback, guidance_seed_present if reached_target: self.metrics.total_nets_reached_target += 1 report = None @@ -872,9 +978,27 @@ class PathFinder: iteration_penalty = self.context.congestion_penalty routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids] + capped_net_ids: set[str] = set() + if len(reroute_net_ids) < len(state.ordered_net_ids) and state.recent_attempt_work: + order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)} + routed_net_ids.sort(key=lambda net_id: (state.recent_attempt_work.get(net_id, 0), order_index[net_id])) + if ( + len(routed_net_ids) == 4 + and state.best_conflict_edges <= 2 + and len(state.results) == len(state.ordered_net_ids) + and all(result.reached_target for result in state.results.values()) + ): + heavy_net_ids = sorted( + routed_net_ids, + key=lambda net_id: (-state.recent_attempt_work.get(net_id, 0), order_index[net_id]), + )[:2] + capped_net_ids = { + net_id for net_id in heavy_net_ids if state.recent_attempt_work.get(net_id, 0) >= 200 + } self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids) iteration_before = {} attempt_traces: list[IterationNetAttemptTrace] = [] + attempt_work: dict[str, int] = {} if diagnostics.capture_iteration_trace: iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) @@ -883,28 +1007,51 @@ class PathFinder: self.metrics.total_timeout_events += 1 return None - attempt_before = {} - if diagnostics.capture_iteration_trace: - attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) - result, guidance_seed_present = self._route_net_once(state, iteration, net_id) + attempt_before = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) + node_limit_override = None + incumbent_fallback = None + if net_id in capped_net_ids: + node_limit_override = min(self.context.options.search.node_limit, 1) + incumbent_fallback = state.results.get(net_id) + result, guidance_seed_present = self._route_net_once( + state, + iteration, + net_id, + node_limit_override=node_limit_override, + incumbent_fallback=incumbent_fallback, + ) state.results[net_id] = result - if diagnostics.capture_iteration_trace: - attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) - deltas = self._metric_deltas(attempt_before, attempt_after) - attempt_traces.append( - IterationNetAttemptTrace( - net_id=net_id, - reached_target=result.reached_target, - nodes_expanded=deltas["nodes_expanded"], - congestion_check_calls=deltas["congestion_check_calls"], - pruned_closed_set=deltas["pruned_closed_set"], - pruned_cost=deltas["pruned_cost"], - pruned_hard_collision=deltas["pruned_hard_collision"], - guidance_seed_present=guidance_seed_present, - ) + attempt_after = self._capture_metric_totals(_ATTEMPT_TRACE_TOTALS) + deltas = self._metric_deltas(attempt_before, attempt_after) + attempt_work[net_id] = deltas["nodes_expanded"] + deltas["congestion_check_calls"] + attempt_traces.append( + IterationNetAttemptTrace( + net_id=net_id, + reached_target=result.reached_target, + nodes_expanded=deltas["nodes_expanded"], + congestion_check_calls=deltas["congestion_check_calls"], + pruned_closed_set=deltas["pruned_closed_set"], + pruned_cost=deltas["pruned_cost"], + pruned_hard_collision=deltas["pruned_hard_collision"], + guidance_seed_present=guidance_seed_present, ) + ) + state.recent_attempt_work = attempt_work review = self._reverify_iteration_results(state) + all_reached_target = ( + len(state.results) == len(state.ordered_net_ids) + and all(result.reached_target for result in state.results.values()) + ) + if all_reached_target and len(reroute_net_ids) < len(state.ordered_net_ids) and review.conflict_edges: + state.pre_pair_candidate = _PrePairCandidate( + iteration=iteration, + routed_net_ids=tuple(routed_net_ids), + conflict_edges=tuple(sorted(review.conflict_edges)), + net_attempts=tuple(attempt_traces), + ) + else: + state.pre_pair_candidate = None if diagnostics.capture_iteration_trace: iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) deltas = self._metric_deltas(iteration_before, iteration_after) @@ -1072,6 +1219,7 @@ class PathFinder: self.accumulated_expanded_nodes = [] self.conflict_trace = [] self.frontier_trace = [] + self.pre_pair_frontier_trace = None self.iteration_trace = [] self.metrics.reset_totals() self.metrics.reset_per_route() @@ -1080,13 +1228,17 @@ class PathFinder: timed_out = self._run_iterations(state, iteration_callback) self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes) self._restore_best_iteration(state) + capture_component_conflicts = ( + self.context.options.diagnostics.capture_conflict_trace + or self.context.options.diagnostics.capture_pre_pair_frontier_trace + ) + state.results, details_by_net, review = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=capture_component_conflicts, + count_iteration_metrics=False, + ) if self.context.options.diagnostics.capture_conflict_trace: - state.results, details_by_net, review = self._analyze_results( - state.ordered_net_ids, - state.results, - capture_component_conflicts=True, - count_iteration_metrics=False, - ) self._capture_conflict_trace_entry( state, stage="restored_best", @@ -1095,6 +1247,13 @@ class PathFinder: details_by_net=details_by_net, review=review, ) + if self.context.options.diagnostics.capture_pre_pair_frontier_trace: + self.pre_pair_frontier_trace = self._materialize_pre_pair_frontier_trace( + state, + state.results, + details_by_net, + review, + ) if timed_out: final_results = self._verify_results(state) diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index 8f96bae..e60fa34 100644 --- a/inire/tests/example_scenarios.py +++ b/inire/tests/example_scenarios.py @@ -91,6 +91,7 @@ def _make_run_result( expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes), conflict_trace=tuple(pathfinder.conflict_trace), frontier_trace=tuple(pathfinder.frontier_trace), + pre_pair_frontier_trace=pathfinder.pre_pair_frontier_trace, iteration_trace=tuple(pathfinder.iteration_trace), ) @@ -453,6 +454,7 @@ def _build_example_07_variant_stack( capture_conflict_trace: bool = False, capture_frontier_trace: bool = False, capture_iteration_trace: bool = False, + capture_pre_pair_frontier_trace: bool = False, ) -> tuple[CostEvaluator, AStarMetrics, PathFinder]: bounds = (0, 0, 1000, 1000) obstacles = [ @@ -496,6 +498,7 @@ def _build_example_07_variant_stack( "capture_conflict_trace": capture_conflict_trace, "capture_frontier_trace": capture_frontier_trace, "capture_iteration_trace": capture_iteration_trace, + "capture_pre_pair_frontier_trace": capture_pre_pair_frontier_trace, "shuffle_nets": True, "seed": seed, "warm_start_enabled": warm_start_enabled, @@ -512,6 +515,7 @@ def _run_example_07_variant( capture_conflict_trace: bool = False, capture_frontier_trace: bool = False, capture_iteration_trace: bool = False, + capture_pre_pair_frontier_trace: bool = False, ) -> RoutingRunResult: evaluator, metrics, pathfinder = _build_example_07_variant_stack( num_nets=num_nets, @@ -520,6 +524,7 @@ def _run_example_07_variant( capture_conflict_trace=capture_conflict_trace, capture_frontier_trace=capture_frontier_trace, capture_iteration_trace=capture_iteration_trace, + capture_pre_pair_frontier_trace=capture_pre_pair_frontier_trace, ) def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None: @@ -560,6 +565,7 @@ def _trace_example_07_variant( capture_conflict_trace=True, capture_frontier_trace=True, capture_iteration_trace=True, + capture_pre_pair_frontier_trace=True, ) diff --git a/inire/tests/test_api.py b/inire/tests/test_api.py index a724e90..926859c 100644 --- a/inire/tests/test_api.py +++ b/inire/tests/test_api.py @@ -3,6 +3,7 @@ import importlib import pytest from shapely.geometry import box +import inire.router._router as router_module from inire import ( CongestionOptions, DiagnosticsOptions, @@ -54,6 +55,7 @@ def test_route_problem_smoke() -> None: assert run.results_by_net["net1"].is_valid assert run.conflict_trace == () assert run.frontier_trace == () + assert run.pre_pair_frontier_trace is None assert run.iteration_trace == () @@ -125,6 +127,8 @@ def test_route_problem_supports_configs_and_debug_data() -> None: assert run.metrics.pair_local_search_attempts >= 0 assert run.metrics.pair_local_search_accepts >= 0 assert run.metrics.pair_local_search_nodes_expanded >= 0 + assert run.metrics.late_phase_capped_nets >= 0 + assert run.metrics.late_phase_capped_fallbacks >= 0 def test_iteration_callback_observes_reverified_conflicts() -> None: @@ -306,6 +310,35 @@ def test_capture_frontier_trace_preserves_route_outputs() -> None: assert {trace.net_id for trace in run_with_trace.frontier_trace} == {"horizontal", "vertical"} +def test_capture_pre_pair_frontier_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_pre_pair_frontier_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert run_with_trace.pre_pair_frontier_trace is None + + def test_capture_frontier_trace_records_prune_reasons() -> None: problem = RoutingProblem( bounds=(0, 0, 100, 100), @@ -411,7 +444,159 @@ def test_reverify_iterations_limit_late_reroutes_to_conflicting_nets(monkeypatch assert reroute_sets == [{"netA", "netB", "netC"}, {"netA", "netB"}] assert results["netA"].outcome == "colliding" assert results["netB"].outcome == "colliding" - assert results["netC"].outcome == "completed" + assert results["netC"].reached_target + + +def test_run_iteration_orders_subset_reroutes_by_recent_work(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + state.recent_attempt_work = {"netA": 200, "netB": 20} + route_order: list[str] = [] + + def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None): + _ = self + _ = state + _ = iteration + route_order.append(net_id) + assert node_limit_override is None + assert incumbent_fallback is None + return RoutingResult(net_id=net_id, path=(), reached_target=False), False + + def fake_reverify(self, state): + _ = self + _ = state + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids=set(), + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once) + monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify) + + review = pathfinder._run_iteration(state, 1, {"netA", "netB"}, None) + + assert review is not None + assert route_order == ["netB", "netA"] + + +def test_run_iteration_caps_two_heaviest_late_phase_nets(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + NetSpec("netD", Port(10, 80, 0), Port(90, 80, 0), width=2.0), + NetSpec("netE", Port(10, 65, 0), Port(90, 65, 0), width=2.0), + ), + ) + options = RoutingOptions( + objective=ObjectiveWeights(bend_penalty=100.0), + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False, shuffle_nets=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + state.results = { + net_id: RoutingResult(net_id=net_id, path=(Straight.generate(spec.start, 80.0, 2.0, dilation=1.0),), reached_target=True) + for net_id, spec in state.net_specs.items() + } + state.best_conflict_edges = 2 + state.recent_attempt_work = {"netA": 20, "netB": 40, "netC": 400, "netD": 220} + incumbents = dict(state.results) + caps_by_net: dict[str, tuple[int | None, RoutingResult | None]] = {} + + def fake_route_net_once(self, state, iteration, net_id, *, node_limit_override=None, incumbent_fallback=None): + _ = self + _ = state + _ = iteration + caps_by_net[net_id] = (node_limit_override, incumbent_fallback) + return RoutingResult(net_id=net_id, path=(), reached_target=False), False + + def fake_reverify(self, state): + _ = self + _ = state + return _IterationReview( + conflicting_nets={"netA", "netB", "netC", "netD"}, + conflict_edges={("netA", "netB"), ("netC", "netD")}, + completed_net_ids=set(), + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_route_net_once", fake_route_net_once) + monkeypatch.setattr(PathFinder, "_reverify_iteration_results", fake_reverify) + + review = pathfinder._run_iteration(state, 1, {"netA", "netB", "netC", "netD"}, None) + + assert review is not None + assert caps_by_net["netA"] == (None, None) + assert caps_by_net["netB"] == (None, None) + assert caps_by_net["netC"][0] == 1 + assert caps_by_net["netD"][0] == 1 + assert caps_by_net["netC"][1] is incumbents["netC"] + assert caps_by_net["netD"][1] is incumbents["netD"] + + +def test_route_net_once_skips_search_for_capped_incumbent_fallback(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=(NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),), + ) + options = RoutingOptions( + objective=ObjectiveWeights(bend_penalty=100.0), + congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + state = pathfinder._prepare_state() + incumbent = RoutingResult( + net_id="netA", + path=(Straight.generate(problem.nets[0].start, 80.0, 2.0, dilation=1.0),), + reached_target=True, + ) + state.results["netA"] = incumbent + installed: list[tuple[str, tuple[object, ...]]] = [] + + def fail_route_astar(*args, **kwargs): + raise AssertionError("route_astar should not run for capped incumbent fallback") + + def record_install(self, net_id, path): + _ = self + installed.append((net_id, tuple(path))) + + monkeypatch.setattr(router_module, "route_astar", fail_route_astar) + monkeypatch.setattr(PathFinder, "_install_path", record_install) + + result, guidance_seed_present = pathfinder._route_net_once( + state, + 1, + "netA", + node_limit_override=1, + incumbent_fallback=incumbent, + ) + + assert result is incumbent + assert guidance_seed_present is True + assert installed == [("netA", incumbent.path)] + assert pathfinder.metrics.total_late_phase_capped_nets == 1 + assert pathfinder.metrics.total_late_phase_capped_fallbacks == 1 def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/inire/tests/test_astar.py b/inire/tests/test_astar.py index 0fc106f..445991f 100644 --- a/inire/tests/test_astar.py +++ b/inire/tests/test_astar.py @@ -289,6 +289,8 @@ def test_pair_local_context_clones_live_static_obstacles() -> None: last_conflict_edge_count=0, repeated_conflict_count=0, pair_local_plateau_count=0, + recent_attempt_work={}, + pre_pair_candidate=None, ) local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b")) diff --git a/inire/tests/test_example_regressions.py b/inire/tests/test_example_regressions.py index a7c3b73..c7a10d9 100644 --- a/inire/tests/test_example_regressions.py +++ b/inire/tests/test_example_regressions.py @@ -75,6 +75,7 @@ def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> Non assert sum(result.reached_target for result in run.results_by_net.values()) == 10 assert run.metrics.pair_local_search_pairs_considered >= 1 assert run.metrics.pair_local_search_accepts >= 1 + assert run.pre_pair_frontier_trace is not None final_entry = run.conflict_trace[-1] assert final_entry.stage == "final" diff --git a/inire/tests/test_performance_reporting.py b/inire/tests/test_performance_reporting.py index 7a352fd..9ded29a 100644 --- a/inire/tests/test_performance_reporting.py +++ b/inire/tests/test_performance_reporting.py @@ -48,6 +48,8 @@ def test_snapshot_example_01_exposes_metrics() -> None: assert snapshot.metrics.pair_local_search_attempts >= 0 assert snapshot.metrics.pair_local_search_accepts >= 0 assert snapshot.metrics.pair_local_search_nodes_expanded >= 0 + assert snapshot.metrics.late_phase_capped_nets >= 0 + assert snapshot.metrics.late_phase_capped_fallbacks >= 0 def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None: @@ -300,6 +302,31 @@ def test_record_iteration_trace_script_writes_selected_scenario(tmp_path: Path) assert (tmp_path / "iteration_trace.md").exists() +def test_record_pre_pair_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_pre_pair_frontier_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + "--output-dir", + str(tmp_path), + ], + check=True, + ) + + payload = json.loads((tmp_path / "pre_pair_frontier_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_pre_pair_frontier_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"] + assert payload["scenarios"][0]["pre_pair_frontier_trace"] is not None + assert (tmp_path / "pre_pair_frontier_trace.md").exists() + + def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None: repo_root = Path(__file__).resolve().parents[2] script_path = repo_root / "scripts" / "characterize_pair_local_search.py" diff --git a/scripts/record_pre_pair_frontier_trace.py b/scripts/record_pre_pair_frontier_trace.py new file mode 100644 index 0000000..398fc0d --- /dev/null +++ b/scripts/record_pre_pair_frontier_trace.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS) + return ( + ( + "example_07_large_scale_routing_no_warm_start", + perf_registry["example_07_large_scale_routing_no_warm_start"], + ), + ( + "example_07_large_scale_routing_no_warm_start_seed43", + perf_registry["example_07_large_scale_routing_no_warm_start_seed43"], + ), + ) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown pre-pair frontier-trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": { + "total_results": len(result.results_by_net), + "valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid), + "reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target), + }, + "metrics": asdict(result.metrics), + "pre_pair_frontier_trace": None + if result.pre_pair_frontier_trace is None + else asdict(result.pre_pair_frontier_trace), + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_pre_pair_frontier_trace.py", + "scenarios": scenarios, + } + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Pre-Pair Frontier Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + ] + ) + trace = scenario["pre_pair_frontier_trace"] + if trace is None: + lines.extend(["No pre-pair frontier trace captured.", ""]) + continue + + lines.extend( + [ + f"Captured iteration: `{trace['iteration']}`", + "", + f"Conflict edges: `{trace['conflict_edges']}`", + "", + "| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |", + "| :-- | --: | --: | --: | --: | --: | :--: | --: |", + ] + ) + reason_counts: Counter[str] = Counter() + for net_trace in trace["nets"]: + frontier = net_trace["frontier"] + lines.append( + "| " + f"{net_trace['net_id']} | " + f"{net_trace['nodes_expanded']} | " + f"{net_trace['congestion_check_calls']} | " + f"{net_trace['pruned_closed_set']} | " + f"{net_trace['pruned_cost']} | " + f"{net_trace['pruned_hard_collision']} | " + f"{'yes' if net_trace['guidance_seed_present'] else 'no'} | " + f"{len(frontier['samples'])} |" + ) + reason_counts["closed_set"] += frontier["pruned_closed_set"] + reason_counts["hard_collision"] += frontier["pruned_hard_collision"] + reason_counts["self_collision"] += frontier["pruned_self_collision"] + reason_counts["cost"] += frontier["pruned_cost"] + + lines.extend(["", "Frontier prune totals by reason:", ""]) + if reason_counts: + for reason, count in reason_counts.most_common(): + lines.append(f"- `{reason}`: {count}") + else: + lines.append("- None") + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record pre-pair frontier-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write pre_pair_frontier_trace.json and .md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "pre_pair_frontier_trace.json" + markdown_path = output_dir / "pre_pair_frontier_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + if json_path.is_relative_to(repo_root): + print(f"Wrote {json_path.relative_to(repo_root)}") + else: + print(f"Wrote {json_path}") + if markdown_path.is_relative_to(repo_root): + print(f"Wrote {markdown_path.relative_to(repo_root)}") + else: + print(f"Wrote {markdown_path}") + + +if __name__ == "__main__": + main()