From 3b7568d6c97670ded695f913da9089fe35991cfc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 16:15:02 -0700 Subject: [PATCH 1/4] Add iteration trace diagnostics for seed43 --- DOCS.md | 35 +- docs/iteration_trace.json | 2111 +++++++++++++++++++++ docs/iteration_trace.md | 85 + docs/optimization_pass_01_log.md | 11 + docs/performance.md | 3 + inire/__init__.py | 5 + inire/model.py | 1 + inire/results.py | 28 + inire/router/_router.py | 93 +- inire/tests/example_scenarios.py | 26 +- inire/tests/test_api.py | 67 + inire/tests/test_example_performance.py | 38 +- inire/tests/test_performance_reporting.py | 24 + scripts/record_iteration_trace.py | 186 ++ 14 files changed, 2700 insertions(+), 13 deletions(-) create mode 100644 docs/iteration_trace.json create mode 100644 docs/iteration_trace.md create mode 100644 scripts/record_iteration_trace.py diff --git a/DOCS.md b/DOCS.md index d70cd8c..1b03ea4 100644 --- a/DOCS.md +++ b/DOCS.md @@ -130,6 +130,7 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are | `capture_expanded` | `False` | Record expanded nodes for diagnostics and visualization. | | `capture_conflict_trace` | `False` | Capture authoritative post-reverify conflict trace entries for debugging negotiated-congestion failures. | | `capture_frontier_trace` | `False` | Run an analysis-only reroute for reached-but-colliding nets and capture prune causes near their final conflict hotspots. | +| `capture_iteration_trace` | `False` | Capture per-iteration and per-net route-attempt attribution for negotiated-congestion diagnosis. | ## 7. Conflict Trace @@ -186,7 +187,37 @@ 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. RouteMetrics +## 9. Iteration Trace + +`RoutingRunResult.iteration_trace` is an immutable tuple of negotiated-congestion iteration summaries. It is empty unless `RoutingOptions.diagnostics.capture_iteration_trace=True`. + +Trace types: + +- `IterationTraceEntry` + - `iteration` + - `congestion_penalty`: Penalty in effect for that iteration + - `routed_net_ids`: Nets rerouted during that iteration, in routing order + - `completed_nets` + - `conflict_edges` + - `total_dynamic_collisions` + - `nodes_expanded` + - `congestion_check_calls` + - `congestion_candidate_ids` + - `congestion_exact_pair_checks` + - `net_attempts`: Per-net attribution for that iteration +- `IterationNetAttemptTrace` + - `net_id` + - `reached_target` + - `nodes_expanded` + - `congestion_check_calls` + - `pruned_closed_set` + - `pruned_cost` + - `pruned_hard_collision` + - `guidance_seed_present` + +Use `scripts/record_iteration_trace.py` to capture JSON and Markdown iteration-attribution artifacts. Its default comparison target is the solved seed-42 no-warm canary versus the pathological seed-43 no-warm canary. + +## 10. RouteMetrics `RoutingRunResult.metrics` is an immutable per-run snapshot. @@ -272,7 +303,7 @@ Separately from the observational trace tooling, the router may run a bounded po 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, 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_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 new file mode 100644 index 0000000..ef7f3a2 --- /dev/null +++ b/docs/iteration_trace.json @@ -0,0 +1,2111 @@ +{ + "generated_at": "2026-04-02T16:11:39-07:00", + "generator": "scripts/record_iteration_trace.py", + "scenarios": [ + { + "iteration_trace": [ + { + "completed_nets": 1, + "conflict_edges": 16, + "congestion_candidate_ids": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_penalty": 100.0, + "iteration": 0, + "net_attempts": [ + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_08", + "nodes_expanded": 106, + "pruned_closed_set": 58, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_06", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_03", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_00", + "nodes_expanded": 158, + "pruned_closed_set": 72, + "pruned_cost": 74, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_07", + "nodes_expanded": 14, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_01", + "nodes_expanded": 38, + "pruned_closed_set": 14, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_09", + "nodes_expanded": 209, + "pruned_closed_set": 93, + "pruned_cost": 99, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_02", + "nodes_expanded": 14, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_04", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_05", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 571, + "routed_net_ids": [ + "net_08", + "net_06", + "net_03", + "net_00", + "net_07", + "net_01", + "net_09", + "net_02", + "net_04", + "net_05" + ], + "total_dynamic_collisions": 50 + }, + { + "completed_nets": 2, + "conflict_edges": 12, + "congestion_candidate_ids": 2378, + "congestion_check_calls": 974, + "congestion_exact_pair_checks": 1998, + "congestion_penalty": 140.0, + "iteration": 1, + "net_attempts": [ + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 317, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 81, + "pruned_closed_set": 14, + "pruned_cost": 22, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 45, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 8, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 36, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 8, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 33, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 372, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 117, + "pruned_closed_set": 19, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 54, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 253, + "routed_net_ids": [ + "net_04", + "net_06", + "net_01", + "net_09", + "net_05", + "net_02", + "net_00", + "net_03", + "net_07", + "net_08" + ], + "total_dynamic_collisions": 54 + }, + { + "completed_nets": 4, + "conflict_edges": 5, + "congestion_candidate_ids": 1928, + "congestion_check_calls": 993, + "congestion_exact_pair_checks": 1571, + "congestion_penalty": 196.0, + "iteration": 2, + "net_attempts": [ + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 399, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 120, + "pruned_closed_set": 31, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 275, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 63, + "pruned_closed_set": 15, + "pruned_cost": 3, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 68, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 16, + "pruned_closed_set": 6, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 59, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 16, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 6, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 38, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 29, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 54, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 12, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 253, + "routed_net_ids": [ + "net_05", + "net_02", + "net_04", + "net_01", + "net_08", + "net_09", + "net_03", + "net_06", + "net_07", + "net_00" + ], + "total_dynamic_collisions": 22 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 852, + "congestion_check_calls": 437, + "congestion_exact_pair_checks": 698, + "congestion_penalty": 274.4, + "iteration": 3, + "net_attempts": [ + { + "congestion_check_calls": 79, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 15, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 68, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 20, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 34, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 73, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 14, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 35, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 22, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 6, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 56, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 13, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 100, + "routed_net_ids": [ + "net_02", + "net_01", + "net_09", + "net_00", + "net_07", + "net_05", + "net_04", + "net_06", + "net_03", + "net_08" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 961, + "congestion_check_calls": 517, + "congestion_exact_pair_checks": 812, + "congestion_penalty": 384.15999999999997, + "iteration": 4, + "net_attempts": [ + { + "congestion_check_calls": 43, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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": 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": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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 + }, + { + "congestion_check_calls": 22, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 6, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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": 51, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 12, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 80, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 18, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 126, + "routed_net_ids": [ + "net_00", + "net_06", + "net_09", + "net_05", + "net_08", + "net_03", + "net_07", + "net_02", + "net_04", + "net_01" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 3805, + "congestion_check_calls": 1704, + "congestion_exact_pair_checks": 3043, + "congestion_penalty": 537.824, + "iteration": 5, + "net_attempts": [ + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 66, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 15, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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": 109, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 21, + "pruned_closed_set": 5, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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 + }, + { + "congestion_check_calls": 85, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 16, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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": 795, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 236, + "pruned_closed_set": 28, + "pruned_cost": 149, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 461, + "routed_net_ids": [ + "net_00", + "net_01", + "net_04", + "net_09", + "net_02", + "net_05", + "net_08", + "net_07", + "net_06", + "net_03" + ], + "total_dynamic_collisions": 10 + } + ], + "metrics": { + "congestion_cache_hits": 31, + "congestion_cache_misses": 4625, + "congestion_candidate_ids": 9924, + "congestion_candidate_nets": 9979, + "congestion_candidate_precheck_hits": 2562, + "congestion_candidate_precheck_misses": 2165, + "congestion_candidate_precheck_skips": 71, + "congestion_check_calls": 4625, + "congestion_exact_pair_checks": 8122, + "congestion_grid_net_cache_hits": 2457, + "congestion_grid_net_cache_misses": 3942, + "congestion_grid_span_cache_hits": 2283, + "congestion_grid_span_cache_misses": 1948, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 2673, + "congestion_net_envelope_cache_misses": 4139, + "congestion_presence_cache_hits": 2858, + "congestion_presence_cache_misses": 2556, + "congestion_presence_skips": 687, + "danger_map_cache_hits": 16878, + "danger_map_cache_misses": 7425, + "danger_map_lookup_calls": 24303, + "danger_map_query_calls": 7425, + "danger_map_total_ns": 205681291, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 471, + "dynamic_path_objects_removed": 423, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 11000.0, + "guidance_bonus_applied_bend90": 3500.0, + "guidance_bonus_applied_sbend": 625.0, + "guidance_bonus_applied_straight": 6875.0, + "guidance_match_moves": 176, + "guidance_match_moves_bend90": 56, + "guidance_match_moves_sbend": 10, + "guidance_match_moves_straight": 110, + "hard_collision_cache_hits": 0, + "iteration_conflict_edges": 39, + "iteration_conflicting_nets": 36, + "iteration_reverified_nets": 60, + "iteration_reverify_calls": 6, + "move_cache_abs_hits": 2559, + "move_cache_abs_misses": 6494, + "move_cache_rel_hits": 5872, + "move_cache_rel_misses": 622, + "moves_added": 8081, + "moves_generated": 9053, + "nets_carried_forward": 0, + "nets_reached_target": 60, + "nets_routed": 60, + "nodes_expanded": 1764, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 2, + "pair_local_search_nodes_expanded": 68, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 439, + "pruned_cost": 533, + "pruned_hard_collision": 0, + "ray_cast_calls": 5477, + "ray_cast_calls_expand_forward": 1704, + "ray_cast_calls_expand_snap": 46, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 3721, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 305, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 6, + "score_component_calls": 8634, + "score_component_total_ns": 232913892, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 2482, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2106, + "verify_dynamic_exact_pair_checks": 558, + "verify_path_report_calls": 190, + "verify_static_buffer_ops": 895, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1704, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start", + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + }, + { + "iteration_trace": [ + { + "completed_nets": 1, + "conflict_edges": 16, + "congestion_candidate_ids": 0, + "congestion_check_calls": 0, + "congestion_exact_pair_checks": 0, + "congestion_penalty": 100.0, + "iteration": 0, + "net_attempts": [ + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_00", + "nodes_expanded": 158, + "pruned_closed_set": 72, + "pruned_cost": 74, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_05", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_07", + "nodes_expanded": 14, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_01", + "nodes_expanded": 38, + "pruned_closed_set": 14, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_09", + "nodes_expanded": 209, + "pruned_closed_set": 93, + "pruned_cost": 99, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_08", + "nodes_expanded": 106, + "pruned_closed_set": 58, + "pruned_cost": 2, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_06", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_03", + "nodes_expanded": 12, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_02", + "nodes_expanded": 14, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 0, + "guidance_seed_present": false, + "net_id": "net_04", + "nodes_expanded": 4, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 571, + "routed_net_ids": [ + "net_00", + "net_05", + "net_07", + "net_01", + "net_09", + "net_08", + "net_06", + "net_03", + "net_02", + "net_04" + ], + "total_dynamic_collisions": 50 + }, + { + "completed_nets": 1, + "conflict_edges": 13, + "congestion_candidate_ids": 2562, + "congestion_check_calls": 961, + "congestion_exact_pair_checks": 2032, + "congestion_penalty": 140.0, + "iteration": 1, + "net_attempts": [ + { + "congestion_check_calls": 31, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 46, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 32, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 6, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 259, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 86, + "pruned_closed_set": 19, + "pruned_cost": 44, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 43, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 7, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 372, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 117, + "pruned_closed_set": 19, + "pruned_cost": 47, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 27, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 5, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 84, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 20, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 47, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 10, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 269, + "routed_net_ids": [ + "net_09", + "net_08", + "net_00", + "net_07", + "net_04", + "net_01", + "net_03", + "net_05", + "net_02", + "net_06" + ], + "total_dynamic_collisions": 53 + }, + { + "completed_nets": 4, + "conflict_edges": 3, + "congestion_candidate_ids": 1610, + "congestion_check_calls": 643, + "congestion_exact_pair_checks": 1224, + "congestion_penalty": 196.0, + "iteration": 2, + "net_attempts": [ + { + "congestion_check_calls": 121, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 26, + "pruned_closed_set": 9, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 50, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 10, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 92, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 20, + "pruned_closed_set": 7, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 121, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 29, + "pruned_closed_set": 0, + "pruned_cost": 5, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 58, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 13, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 72, + "guidance_seed_present": true, + "net_id": "net_09", + "nodes_expanded": 15, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 50, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 13, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 39, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 140, + "routed_net_ids": [ + "net_08", + "net_07", + "net_01", + "net_06", + "net_02", + "net_09", + "net_00", + "net_05", + "net_03", + "net_04" + ], + "total_dynamic_collisions": 15 + }, + { + "completed_nets": 4, + "conflict_edges": 3, + "congestion_candidate_ids": 801, + "congestion_check_calls": 382, + "congestion_exact_pair_checks": 651, + "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": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 58, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 13, + "pruned_closed_set": 4, + "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": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 36, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 8, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 70, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 13, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 34, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 11, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 30, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 84, + "routed_net_ids": [ + "net_06", + "net_05", + "net_01", + "net_09", + "net_04", + "net_03", + "net_02", + "net_08", + "net_00", + "net_07" + ], + "total_dynamic_collisions": 15 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 1334, + "congestion_check_calls": 673, + "congestion_exact_pair_checks": 1072, + "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, + "net_id": "net_07", + "nodes_expanded": 7, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 48, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 12, + "pruned_closed_set": 4, + "pruned_cost": 0, + "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": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 32, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 8, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 201, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 55, + "pruned_closed_set": 7, + "pruned_cost": 21, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 170, + "routed_net_ids": [ + "net_06", + "net_07", + "net_00", + "net_01", + "net_08", + "net_09", + "net_04", + "net_02", + "net_05", + "net_03" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 6, + "conflict_edges": 2, + "congestion_candidate_ids": 3718, + "congestion_check_calls": 1671, + "congestion_exact_pair_checks": 2992, + "congestion_penalty": 537.824, + "iteration": 5, + "net_attempts": [ + { + "congestion_check_calls": 85, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 16, + "pruned_closed_set": 3, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 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": 48, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 12, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 86, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 17, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 803, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 239, + "pruned_closed_set": 28, + "pruned_cost": 149, + "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 + }, + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "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": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 457, + "routed_net_ids": [ + "net_07", + "net_06", + "net_01", + "net_02", + "net_04", + "net_03", + "net_08", + "net_00", + "net_09", + "net_05" + ], + "total_dynamic_collisions": 10 + }, + { + "completed_nets": 4, + "conflict_edges": 4, + "congestion_candidate_ids": 218513, + "congestion_check_calls": 89671, + "congestion_exact_pair_checks": 171925, + "congestion_penalty": 752.9535999999999, + "iteration": 6, + "net_attempts": [ + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 97, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 19, + "pruned_closed_set": 4, + "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": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 20, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 3, + "pruned_closed_set": 0, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 48, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 12, + "pruned_closed_set": 4, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 96, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 18, + "pruned_closed_set": 3, + "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 + }, + { + "congestion_check_calls": 45308, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 11409, + "pruned_closed_set": 12159, + "pruned_cost": 1513, + "pruned_hard_collision": 49, + "reached_target": true + }, + { + "congestion_check_calls": 43984, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 10794, + "pruned_closed_set": 11293, + "pruned_cost": 1256, + "pruned_hard_collision": 54, + "reached_target": true + } + ], + "nodes_expanded": 22288, + "routed_net_ids": [ + "net_05", + "net_02", + "net_09", + "net_00", + "net_04", + "net_01", + "net_07", + "net_08", + "net_03", + "net_06" + ], + "total_dynamic_collisions": 8 + }, + { + "completed_nets": 4, + "conflict_edges": 4, + "congestion_candidate_ids": 34309, + "congestion_check_calls": 29419, + "congestion_exact_pair_checks": 28603, + "congestion_penalty": 1054.13504, + "iteration": 7, + "net_attempts": [ + { + "congestion_check_calls": 13979, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 7675, + "pruned_closed_set": 5308, + "pruned_cost": 423, + "pruned_hard_collision": 214, + "reached_target": true + }, + { + "congestion_check_calls": 146, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 40, + "pruned_closed_set": 5, + "pruned_cost": 24, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 149, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 55, + "pruned_closed_set": 37, + "pruned_cost": 64, + "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": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 1269, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 283, + "pruned_closed_set": 117, + "pruned_cost": 49, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 13573, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 7598, + "pruned_closed_set": 4816, + "pruned_cost": 509, + "pruned_hard_collision": 705, + "reached_target": true + }, + { + "congestion_check_calls": 52, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 139, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 48, + "pruned_closed_set": 29, + "pruned_cost": 53, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 60, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 11, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + } + ], + "nodes_expanded": 15737, + "routed_net_ids": [ + "net_03", + "net_07", + "net_05", + "net_09", + "net_00", + "net_02", + "net_06", + "net_01", + "net_04", + "net_08" + ], + "total_dynamic_collisions": 8 + }, + { + "completed_nets": 4, + "conflict_edges": 4, + "congestion_candidate_ids": 49314, + "congestion_check_calls": 41803, + "congestion_exact_pair_checks": 41198, + "congestion_penalty": 1475.7890559999998, + "iteration": 8, + "net_attempts": [ + { + "congestion_check_calls": 14281, + "guidance_seed_present": true, + "net_id": "net_03", + "nodes_expanded": 8009, + "pruned_closed_set": 5594, + "pruned_cost": 454, + "pruned_hard_collision": 506, + "reached_target": true + }, + { + "congestion_check_calls": 60, + "guidance_seed_present": true, + "net_id": "net_08", + "nodes_expanded": 11, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 1614, + "guidance_seed_present": true, + "net_id": "net_02", + "nodes_expanded": 382, + "pruned_closed_set": 131, + "pruned_cost": 69, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 52, + "guidance_seed_present": true, + "net_id": "net_01", + "nodes_expanded": 9, + "pruned_closed_set": 2, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 26, + "guidance_seed_present": true, + "net_id": "net_00", + "nodes_expanded": 9, + "pruned_closed_set": 1, + "pruned_cost": 0, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 25302, + "guidance_seed_present": true, + "net_id": "net_06", + "nodes_expanded": 12970, + "pruned_closed_set": 9624, + "pruned_cost": 416, + "pruned_hard_collision": 595, + "reached_target": true + }, + { + "congestion_check_calls": 148, + "guidance_seed_present": true, + "net_id": "net_07", + "nodes_expanded": 41, + "pruned_closed_set": 5, + "pruned_cost": 24, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 149, + "guidance_seed_present": true, + "net_id": "net_05", + "nodes_expanded": 55, + "pruned_closed_set": 37, + "pruned_cost": 64, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 139, + "guidance_seed_present": true, + "net_id": "net_04", + "nodes_expanded": 48, + "pruned_closed_set": 29, + "pruned_cost": 53, + "pruned_hard_collision": 0, + "reached_target": true + }, + { + "congestion_check_calls": 32, + "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 + } + ], + "nodes_expanded": 21543, + "routed_net_ids": [ + "net_03", + "net_08", + "net_02", + "net_01", + "net_00", + "net_06", + "net_07", + "net_05", + "net_04", + "net_09" + ], + "total_dynamic_collisions": 8 + } + ], + "metrics": { + "congestion_cache_hits": 1713, + "congestion_cache_misses": 165223, + "congestion_candidate_ids": 312161, + "congestion_candidate_nets": 300960, + "congestion_candidate_precheck_hits": 159843, + "congestion_candidate_precheck_misses": 7810, + "congestion_candidate_precheck_skips": 717, + "congestion_check_calls": 165223, + "congestion_exact_pair_checks": 249697, + "congestion_grid_net_cache_hits": 152369, + "congestion_grid_net_cache_misses": 13481, + "congestion_grid_span_cache_hits": 144618, + "congestion_grid_span_cache_misses": 6885, + "congestion_lazy_requeues": 0, + "congestion_lazy_resolutions": 0, + "congestion_net_envelope_cache_hits": 160549, + "congestion_net_envelope_cache_misses": 14205, + "congestion_presence_cache_hits": 186369, + "congestion_presence_cache_misses": 9324, + "congestion_presence_skips": 27588, + "danger_map_cache_hits": 453553, + "danger_map_cache_misses": 140744, + "danger_map_lookup_calls": 594297, + "danger_map_query_calls": 140744, + "danger_map_total_ns": 4580111255, + "dynamic_grid_rebuilds": 0, + "dynamic_path_objects_added": 683, + "dynamic_path_objects_removed": 637, + "dynamic_tree_rebuilds": 0, + "guidance_bonus_applied": 23437.5, + "guidance_bonus_applied_bend90": 8437.5, + "guidance_bonus_applied_sbend": 1000.0, + "guidance_bonus_applied_straight": 14000.0, + "guidance_match_moves": 375, + "guidance_match_moves_bend90": 135, + "guidance_match_moves_sbend": 16, + "guidance_match_moves_straight": 224, + "hard_collision_cache_hits": 859, + "iteration_conflict_edges": 51, + "iteration_conflicting_nets": 57, + "iteration_reverified_nets": 90, + "iteration_reverify_calls": 9, + "move_cache_abs_hits": 140734, + "move_cache_abs_misses": 183345, + "move_cache_rel_hits": 180943, + "move_cache_rel_misses": 2402, + "moves_added": 197062, + "moves_generated": 324079, + "nets_carried_forward": 0, + "nets_reached_target": 90, + "nets_routed": 90, + "nodes_expanded": 61259, + "pair_local_search_accepts": 2, + "pair_local_search_attempts": 3, + "pair_local_search_nodes_expanded": 38, + "pair_local_search_pairs_considered": 2, + "path_cost_calls": 0, + "pruned_closed_set": 49632, + "pruned_cost": 5502, + "pruned_hard_collision": 2123, + "ray_cast_calls": 166977, + "ray_cast_calls_expand_forward": 61169, + "ray_cast_calls_expand_snap": 735, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 103738, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 1335, + "ray_cast_candidate_bounds": 11494, + "ray_cast_exact_geometry_checks": 0, + "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, + "route_iterations": 9, + "score_component_calls": 203601, + "score_component_total_ns": 5292565462, + "static_net_tree_rebuilds": 1, + "static_raw_tree_rebuilds": 1, + "static_safe_cache_hits": 120231, + "static_tree_rebuilds": 1, + "timeout_events": 0, + "verify_dynamic_candidate_nets": 2985, + "verify_dynamic_exact_pair_checks": 760, + "verify_path_report_calls": 250, + "verify_static_buffer_ops": 1321, + "visibility_builds": 0, + "visibility_corner_hits_exact": 0, + "visibility_corner_index_builds": 1, + "visibility_corner_pairs_checked": 0, + "visibility_corner_queries_exact": 0, + "visibility_point_cache_hits": 0, + "visibility_point_cache_misses": 0, + "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 1405, + "visibility_tangent_candidate_ray_tests": 1335, + "visibility_tangent_candidate_scans": 61169, + "warm_start_paths_built": 0, + "warm_start_paths_used": 0 + }, + "name": "example_07_large_scale_routing_no_warm_start_seed43", + "summary": { + "reached_targets": 10, + "total_results": 10, + "valid_results": 10 + } + } + ] +} diff --git a/docs/iteration_trace.md b/docs/iteration_trace.md new file mode 100644 index 0000000..ab4bbad --- /dev/null +++ b/docs/iteration_trace.md @@ -0,0 +1,85 @@ +# Iteration Trace + +Generated at 2026-04-02T16:11:39-07:00 by `scripts/record_iteration_trace.py`. + +## example_07_large_scale_routing_no_warm_start + +Results: 10 valid / 10 reached / 10 total. + +| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs | +| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | +| 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 | +| 1 | 140.0 | 10 | 2 | 12 | 54 | 253 | 974 | 2378 | 1998 | +| 2 | 196.0 | 10 | 4 | 5 | 22 | 253 | 993 | 1928 | 1571 | +| 3 | 274.4 | 10 | 6 | 2 | 10 | 100 | 437 | 852 | 698 | +| 4 | 384.2 | 10 | 6 | 2 | 10 | 126 | 517 | 961 | 812 | +| 5 | 537.8 | 10 | 6 | 2 | 10 | 461 | 1704 | 3805 | 3043 | + +Top nets by iteration-attributed nodes expanded: + +- `net_03`: 383 +- `net_06`: 292 +- `net_09`: 260 +- `net_00`: 210 +- `net_02`: 190 +- `net_08`: 168 +- `net_01`: 162 +- `net_07`: 61 +- `net_04`: 19 +- `net_05`: 19 + +Top nets by iteration-attributed congestion checks: + +- `net_03`: 1242 +- `net_06`: 1080 +- `net_02`: 674 +- `net_01`: 534 +- `net_08`: 262 +- `net_00`: 229 +- `net_07`: 228 +- `net_09`: 176 +- `net_04`: 100 +- `net_05`: 100 + +## example_07_large_scale_routing_no_warm_start_seed43 + +Results: 10 valid / 10 reached / 10 total. + +| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs | +| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | +| 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 | +| 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 | +| 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | +| 3 | 274.4 | 10 | 4 | 3 | 15 | 84 | 382 | 801 | 651 | +| 4 | 384.2 | 10 | 6 | 2 | 10 | 170 | 673 | 1334 | 1072 | +| 5 | 537.8 | 10 | 6 | 2 | 10 | 457 | 1671 | 3718 | 2992 | +| 6 | 753.0 | 10 | 4 | 4 | 8 | 22288 | 89671 | 218513 | 171925 | +| 7 | 1054.1 | 10 | 4 | 4 | 8 | 15737 | 29419 | 34309 | 28603 | +| 8 | 1475.8 | 10 | 4 | 4 | 8 | 21543 | 41803 | 49314 | 41198 | + +Top nets by iteration-attributed nodes expanded: + +- `net_06`: 31604 +- `net_03`: 27532 +- `net_02`: 763 +- `net_09`: 286 +- `net_07`: 239 +- `net_00`: 233 +- `net_08`: 218 +- `net_05`: 134 +- `net_01`: 132 +- `net_04`: 118 + +Top nets by iteration-attributed congestion checks: + +- `net_06`: 83752 +- `net_03`: 75019 +- `net_02`: 3270 +- `net_07`: 844 +- `net_08`: 540 +- `net_01`: 441 +- `net_05`: 425 +- `net_04`: 398 +- `net_09`: 288 +- `net_00`: 246 + diff --git a/docs/optimization_pass_01_log.md b/docs/optimization_pass_01_log.md index 2f5c5e7..c8fab8f 100644 --- a/docs/optimization_pass_01_log.md +++ b/docs/optimization_pass_01_log.md @@ -3629,3 +3629,14 @@ Findings: | example_07_large_scale_routing_no_warm_start | pair_local_search_attempts | - | 2.0000 | - | | example_07_large_scale_routing_no_warm_start | pair_local_search_accepts | - | 2.0000 | - | | example_07_large_scale_routing_no_warm_start | pair_local_search_nodes_expanded | - | 68.0000 | - | + +## Step 64 seed-43 iteration-trace diagnosis + +Measured on 2026-04-02T16:11:39-07:00. + +Findings: + +- Added `capture_iteration_trace` plus `scripts/record_iteration_trace.py` and tracked the first `seed 42` vs `seed 43` no-warm comparison in `docs/iteration_trace.json` and `docs/iteration_trace.md`. +- The pathological `seed 43` basin is not front-loaded. It matches the solved `seed 42` path through iteration `5`, then falls into three extra iterations with only `4` completed nets and `4` conflict edges. +- The late blowup is concentrated in two nets, not the whole routing set: `net_06` contributes `31604` attributed nodes and `83752` congestion checks, while `net_03` contributes `27532` nodes and `75019` congestion checks. +- This points the next optimization work at late-iteration reroute behavior for a small subset of nets rather than another global congestion or pair-local-search change. diff --git a/docs/performance.md b/docs/performance.md index 7085214..9159243 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -9,6 +9,7 @@ The default baseline table below covers the standard example corpus only. The he Use `scripts/characterize_pair_local_search.py` when you want a small parameter sweep over example_07-style no-warm runs instead of a single canary reading. The current tracked sweep output lives in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`. +Use `scripts/record_iteration_trace.py` when you want a seed-42 vs seed-43 negotiated-congestion attribution run; the current tracked output lives 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 | | :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | @@ -36,6 +37,8 @@ For the current accepted branch, the most important performance-only canary is ` The latest tracked characterization sweep confirms there is no smaller stable pair-local smoke case under the `<=1.0s` rule, so the 10-net no-warm-start canary remains the primary regression target for this behavior. +The tracked iteration trace adds one more diagnosis target: `example_07_large_scale_routing_no_warm_start_seed43`. That seed now remains performance-only, and its blowup is concentrated in late iterations rather than the initial basin. In the current trace, seed `43` matches seed `42` through iteration `5`, then spends three extra iterations with `4` completed nets while `net_03` and `net_06` dominate both `nodes_expanded` and `congestion_check_calls`. + 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 diff --git a/inire/__init__.py b/inire/__init__.py index 9c63046..72be25f 100644 --- a/inire/__init__.py +++ b/inire/__init__.py @@ -18,6 +18,8 @@ from .results import ( # noqa: PLC0414 ComponentConflictTrace as ComponentConflictTrace, ConflictTraceEntry as ConflictTraceEntry, FrontierPruneSample as FrontierPruneSample, + IterationNetAttemptTrace as IterationNetAttemptTrace, + IterationTraceEntry as IterationTraceEntry, NetConflictTrace as NetConflictTrace, NetFrontierTrace as NetFrontierTrace, RoutingResult as RoutingResult, @@ -47,6 +49,7 @@ def route( expanded_nodes=tuple(finder.accumulated_expanded_nodes), conflict_trace=tuple(finder.conflict_trace), frontier_trace=tuple(finder.frontier_trace), + iteration_trace=tuple(finder.iteration_trace), ) __all__ = [ @@ -62,6 +65,8 @@ __all__ = [ "PathSeed", "Port", "FrontierPruneSample", + "IterationNetAttemptTrace", + "IterationTraceEntry", "RefinementOptions", "RoutingOptions", "RoutingProblem", diff --git a/inire/model.py b/inire/model.py index 5860102..2e98941 100644 --- a/inire/model.py +++ b/inire/model.py @@ -107,6 +107,7 @@ class DiagnosticsOptions: capture_expanded: bool = False capture_conflict_trace: bool = False capture_frontier_trace: bool = False + capture_iteration_trace: bool = False @dataclass(frozen=True, slots=True) diff --git a/inire/results.py b/inire/results.py index 88dd3a3..3898e62 100644 --- a/inire/results.py +++ b/inire/results.py @@ -78,6 +78,33 @@ class NetFrontierTrace: samples: tuple[FrontierPruneSample, ...] = () +@dataclass(frozen=True, slots=True) +class IterationNetAttemptTrace: + net_id: str + reached_target: bool + nodes_expanded: int + congestion_check_calls: int + pruned_closed_set: int + pruned_cost: int + pruned_hard_collision: int + guidance_seed_present: bool + + +@dataclass(frozen=True, slots=True) +class IterationTraceEntry: + iteration: int + congestion_penalty: float + routed_net_ids: tuple[str, ...] + completed_nets: int + conflict_edges: int + total_dynamic_collisions: int + nodes_expanded: int + congestion_check_calls: int + congestion_candidate_ids: int + congestion_exact_pair_checks: int + net_attempts: tuple[IterationNetAttemptTrace, ...] = () + + @dataclass(frozen=True, slots=True) class RouteMetrics: nodes_expanded: int @@ -231,3 +258,4 @@ class RoutingRunResult: expanded_nodes: tuple[tuple[int, int, int], ...] = () conflict_trace: tuple[ConflictTraceEntry, ...] = () frontier_trace: tuple[NetFrontierTrace, ...] = () + iteration_trace: tuple[IterationTraceEntry, ...] = () diff --git a/inire/router/_router.py b/inire/router/_router.py index 367e721..3f1f400 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -11,6 +11,8 @@ from inire.results import ( ComponentConflictTrace, ConflictTraceEntry, FrontierPruneSample, + IterationNetAttemptTrace, + IterationTraceEntry, NetConflictTrace, NetFrontierTrace, RoutingOutcome, @@ -65,6 +67,22 @@ class _PairLocalTarget: net_ids: tuple[str, str] +_ITERATION_TRACE_TOTALS = ( + "nodes_expanded", + "congestion_check_calls", + "congestion_candidate_ids", + "congestion_exact_pair_checks", +) + +_ATTEMPT_TRACE_TOTALS = ( + "nodes_expanded", + "congestion_check_calls", + "pruned_closed_set", + "pruned_cost", + "pruned_hard_collision", +) + + class PathFinder: __slots__ = ( "context", @@ -73,6 +91,7 @@ class PathFinder: "accumulated_expanded_nodes", "conflict_trace", "frontier_trace", + "iteration_trace", ) def __init__( @@ -90,6 +109,16 @@ class PathFinder: self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] self.conflict_trace: list[ConflictTraceEntry] = [] self.frontier_trace: list[NetFrontierTrace] = [] + self.iteration_trace: list[IterationTraceEntry] = [] + + def _metric_total(self, metric_name: str) -> int: + return int(getattr(self.metrics, f"total_{metric_name}")) + + def _capture_metric_totals(self, metric_names: tuple[str, ...]) -> dict[str, int]: + return {metric_name: self._metric_total(metric_name) for metric_name in metric_names} + + def _metric_deltas(self, before: dict[str, int], after: dict[str, int]) -> dict[str, int]: + return {metric_name: after[metric_name] - before[metric_name] for metric_name in before} def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None: all_geoms: list[Polygon] = [] @@ -749,13 +778,14 @@ class PathFinder: state: _RoutingState, iteration: int, net_id: str, - ) -> RoutingResult: + ) -> 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 self.context.cost_evaluator.collision_engine.remove_path(net_id) + guidance_seed_present = False if iteration == 0 and state.initial_paths and net_id in state.initial_paths: self.metrics.total_warm_start_paths_used += 1 @@ -774,6 +804,7 @@ class PathFinder: if guidance_result and guidance_result.reached_target and guidance_result.path: guidance_seed = guidance_result.as_seed().segments guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) + guidance_seed_present = True run_config = SearchRunConfig.from_options( self.context.options, @@ -800,7 +831,7 @@ class PathFinder: state.accumulated_expanded_nodes.extend(self.metrics.last_expanded_nodes) if not path: - return RoutingResult(net_id=net_id, path=(), reached_target=False) + return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present reached_target = path[-1].end_port == net.target if reached_target: @@ -812,11 +843,14 @@ class PathFinder: if report.self_collision_count > 0: state.needs_self_collision_check.add(net_id) - return RoutingResult( - net_id=net_id, - path=tuple(path), - reached_target=reached_target, - report=RoutingReport() if report is None else report, + return ( + RoutingResult( + net_id=net_id, + path=tuple(path), + reached_target=reached_target, + report=RoutingReport() if report is None else report, + ), + guidance_seed_present, ) def _run_iteration( @@ -827,24 +861,66 @@ class PathFinder: iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, ) -> _IterationReview | None: congestion = self.context.options.congestion + diagnostics = self.context.options.diagnostics self.metrics.total_route_iterations += 1 self.metrics.reset_per_route() if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None): iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None random.Random(iteration_seed).shuffle(state.ordered_net_ids) + iteration_penalty = self.context.congestion_penalty routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids] self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids) + iteration_before = {} + attempt_traces: list[IterationNetAttemptTrace] = [] + if diagnostics.capture_iteration_trace: + iteration_before = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) for net_id in routed_net_ids: if time.monotonic() - state.start_time > state.timeout_s: self.metrics.total_timeout_events += 1 return None - result = self._route_net_once(state, iteration, net_id) + 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) 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, + ) + ) review = self._reverify_iteration_results(state) + if diagnostics.capture_iteration_trace: + iteration_after = self._capture_metric_totals(_ITERATION_TRACE_TOTALS) + deltas = self._metric_deltas(iteration_before, iteration_after) + self.iteration_trace.append( + IterationTraceEntry( + iteration=iteration, + congestion_penalty=iteration_penalty, + routed_net_ids=tuple(routed_net_ids), + completed_nets=len(review.completed_net_ids), + conflict_edges=len(review.conflict_edges), + total_dynamic_collisions=review.total_dynamic_collisions, + nodes_expanded=deltas["nodes_expanded"], + congestion_check_calls=deltas["congestion_check_calls"], + congestion_candidate_ids=deltas["congestion_candidate_ids"], + congestion_exact_pair_checks=deltas["congestion_exact_pair_checks"], + net_attempts=tuple(attempt_traces), + ) + ) if iteration_callback: iteration_callback(iteration, state.results) @@ -973,6 +1049,7 @@ class PathFinder: self.accumulated_expanded_nodes = [] self.conflict_trace = [] self.frontier_trace = [] + self.iteration_trace = [] self.metrics.reset_totals() self.metrics.reset_per_route() diff --git a/inire/tests/example_scenarios.py b/inire/tests/example_scenarios.py index c7feb84..8f96bae 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), + iteration_trace=tuple(pathfinder.iteration_trace), ) @@ -424,6 +425,14 @@ def snapshot_example_07_no_warm_start() -> ScenarioSnapshot: ) +def snapshot_example_07_no_warm_start_seed43() -> ScenarioSnapshot: + return _snapshot_example_07_variant( + "example_07_large_scale_routing_no_warm_start_seed43", + warm_start_enabled=False, + seed=43, + ) + + def trace_example_07() -> RoutingRunResult: return _trace_example_07_variant(warm_start_enabled=True) @@ -432,6 +441,10 @@ def trace_example_07_no_warm_start() -> RoutingRunResult: return _trace_example_07_variant(warm_start_enabled=False) +def trace_example_07_no_warm_start_seed43() -> RoutingRunResult: + return _trace_example_07_variant(warm_start_enabled=False, seed=43) + + def _build_example_07_variant_stack( *, num_nets: int, @@ -439,6 +452,7 @@ def _build_example_07_variant_stack( warm_start_enabled: bool, capture_conflict_trace: bool = False, capture_frontier_trace: bool = False, + capture_iteration_trace: bool = False, ) -> tuple[CostEvaluator, AStarMetrics, PathFinder]: bounds = (0, 0, 1000, 1000) obstacles = [ @@ -481,6 +495,7 @@ def _build_example_07_variant_stack( "capture_expanded": True, "capture_conflict_trace": capture_conflict_trace, "capture_frontier_trace": capture_frontier_trace, + "capture_iteration_trace": capture_iteration_trace, "shuffle_nets": True, "seed": seed, "warm_start_enabled": warm_start_enabled, @@ -496,6 +511,7 @@ def _run_example_07_variant( warm_start_enabled: bool, capture_conflict_trace: bool = False, capture_frontier_trace: bool = False, + capture_iteration_trace: bool = False, ) -> RoutingRunResult: evaluator, metrics, pathfinder = _build_example_07_variant_stack( num_nets=num_nets, @@ -503,6 +519,7 @@ def _run_example_07_variant( warm_start_enabled=warm_start_enabled, capture_conflict_trace=capture_conflict_trace, capture_frontier_trace=capture_frontier_trace, + capture_iteration_trace=capture_iteration_trace, ) def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None: @@ -519,11 +536,12 @@ def _snapshot_example_07_variant( name: str, *, warm_start_enabled: bool, + seed: int = 42, ) -> ScenarioSnapshot: t0 = perf_counter() run = _run_example_07_variant( num_nets=10, - seed=42, + seed=seed, warm_start_enabled=warm_start_enabled, ) t1 = perf_counter() @@ -533,13 +551,15 @@ def _snapshot_example_07_variant( def _trace_example_07_variant( *, warm_start_enabled: bool, + seed: int = 42, ) -> RoutingRunResult: return _run_example_07_variant( num_nets=10, - seed=42, + seed=seed, warm_start_enabled=warm_start_enabled, capture_conflict_trace=True, capture_frontier_trace=True, + capture_iteration_trace=True, ) @@ -644,6 +664,7 @@ SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = ( PERFORMANCE_SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = ( ("example_07_large_scale_routing_no_warm_start", snapshot_example_07_no_warm_start), + ("example_07_large_scale_routing_no_warm_start_seed43", snapshot_example_07_no_warm_start_seed43), ) TRACE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = ( @@ -653,6 +674,7 @@ TRACE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = ( TRACE_PERFORMANCE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = ( ("example_07_large_scale_routing_no_warm_start", trace_example_07_no_warm_start), + ("example_07_large_scale_routing_no_warm_start_seed43", trace_example_07_no_warm_start_seed43), ) diff --git a/inire/tests/test_api.py b/inire/tests/test_api.py index 0e9fdb3..b58bce1 100644 --- a/inire/tests/test_api.py +++ b/inire/tests/test_api.py @@ -54,6 +54,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.iteration_trace == () def test_route_problem_supports_configs_and_debug_data() -> None: @@ -182,6 +183,72 @@ def test_capture_conflict_trace_preserves_route_outputs() -> None: assert [entry.stage for entry in run_with_trace.conflict_trace] == ["iteration", "restored_best", "final"] +def test_capture_iteration_trace_preserves_route_outputs() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + base_options = RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + + run_without_trace = route(problem, options=base_options) + run_with_trace = route( + problem, + options=RoutingOptions( + congestion=base_options.congestion, + refinement=base_options.refinement, + diagnostics=DiagnosticsOptions(capture_iteration_trace=True), + ), + ) + + assert {net_id: result.outcome for net_id, result in run_without_trace.results_by_net.items()} == { + net_id: result.outcome for net_id, result in run_with_trace.results_by_net.items() + } + assert len(run_with_trace.iteration_trace) == 1 + + +def test_capture_iteration_trace_records_iteration_and_attempt_deltas() -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("horizontal", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("vertical", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + ), + ) + run = route( + problem, + options=RoutingOptions( + congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + diagnostics=DiagnosticsOptions(capture_iteration_trace=True), + ), + ) + + entry = run.iteration_trace[0] + assert entry.iteration == 0 + assert entry.congestion_penalty == 100.0 + assert entry.routed_net_ids == ("horizontal", "vertical") + assert entry.completed_nets == 0 + assert entry.conflict_edges == 1 + assert entry.total_dynamic_collisions >= 2 + assert entry.nodes_expanded >= 0 + assert entry.congestion_check_calls >= 0 + assert entry.congestion_candidate_ids >= 0 + assert entry.congestion_exact_pair_checks >= 0 + assert len(entry.net_attempts) == 2 + assert [attempt.net_id for attempt in entry.net_attempts] == ["horizontal", "vertical"] + assert all(attempt.nodes_expanded >= 0 for attempt in entry.net_attempts) + assert all(attempt.congestion_check_calls >= 0 for attempt in entry.net_attempts) + assert all(not attempt.guidance_seed_present for attempt in entry.net_attempts) + assert sum(attempt.nodes_expanded for attempt in entry.net_attempts) == entry.nodes_expanded + assert sum(attempt.congestion_check_calls for attempt in entry.net_attempts) == entry.congestion_check_calls + + def test_capture_conflict_trace_records_component_pairs() -> None: problem = RoutingProblem( bounds=(0, 0, 100, 100), diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py index d572228..26d5504 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -6,7 +6,13 @@ from typing import TYPE_CHECKING import pytest -from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome, snapshot_example_07_no_warm_start +from inire.tests.example_scenarios import ( + SCENARIOS, + ScenarioOutcome, + snapshot_example_07_no_warm_start, + snapshot_example_07_no_warm_start_seed43, + trace_example_07_no_warm_start_seed43, +) if TYPE_CHECKING: from collections.abc import Callable @@ -16,6 +22,7 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" PERFORMANCE_REPEATS = 3 REGRESSION_FACTOR = 1.5 NO_WARM_START_REGRESSION_SECONDS = 15.0 +NO_WARM_START_SEED43_REGRESSION_SECONDS = 120.0 # Baselines are measured from clean 6a28dcf-style runs without plotting. BASELINE_SECONDS = { @@ -85,3 +92,32 @@ def test_example_07_no_warm_start_runtime_regression() -> None: f"{snapshot.duration_s:.4f}s exceeded guardrail " f"{NO_WARM_START_REGRESSION_SECONDS:.1f}s" ) + + +@pytest.mark.performance +@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks") +def test_example_07_no_warm_start_seed43_runtime_regression() -> None: + snapshot = snapshot_example_07_no_warm_start_seed43() + run = trace_example_07_no_warm_start_seed43() + + assert snapshot.total_results == 10 + assert snapshot.valid_results == 10 + assert snapshot.reached_targets == 10 + assert snapshot.metrics.warm_start_paths_built == 0 + assert snapshot.metrics.warm_start_paths_used == 0 + assert snapshot.metrics.pair_local_search_pairs_considered >= 1 + assert snapshot.metrics.pair_local_search_accepts >= 1 + assert snapshot.duration_s <= NO_WARM_START_SEED43_REGRESSION_SECONDS, ( + "example_07_large_scale_routing_no_warm_start_seed43 runtime " + f"{snapshot.duration_s:.4f}s exceeded guardrail " + f"{NO_WARM_START_SEED43_REGRESSION_SECONDS:.1f}s" + ) + + assert run.iteration_trace + assert len(run.results_by_net) == 10 + assert sum(result.is_valid for result in run.results_by_net.values()) == 10 + assert sum(result.reached_target for result in run.results_by_net.values()) == 10 + assert run.metrics.warm_start_paths_built == 0 + assert run.metrics.warm_start_paths_used == 0 + assert run.metrics.pair_local_search_pairs_considered >= 1 + assert run.metrics.pair_local_search_accepts >= 1 diff --git a/inire/tests/test_performance_reporting.py b/inire/tests/test_performance_reporting.py index b594308..7a352fd 100644 --- a/inire/tests/test_performance_reporting.py +++ b/inire/tests/test_performance_reporting.py @@ -276,6 +276,30 @@ def test_record_frontier_trace_script_writes_selected_scenario(tmp_path: Path) - assert (tmp_path / "frontier_trace.md").exists() +def test_record_iteration_trace_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + script_path = repo_root / "scripts" / "record_iteration_trace.py" + + subprocess.run( + [ + sys.executable, + str(script_path), + "--include-performance-only", + "--scenario", + "example_07_large_scale_routing_no_warm_start", + "--output-dir", + str(tmp_path), + ], + check=True, + ) + + payload = json.loads((tmp_path / "iteration_trace.json").read_text()) + assert payload["generated_at"] + assert payload["generator"] == "scripts/record_iteration_trace.py" + assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"] + assert (tmp_path / "iteration_trace.md").exists() + + def test_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_iteration_trace.py b/scripts/record_iteration_trace.py new file mode 100644 index 0000000..7691558 --- /dev/null +++ b/scripts/record_iteration_trace.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict +from datetime import datetime +from pathlib import Path + +from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS + + +def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]: + if include_performance_only: + return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS + return TRACE_SCENARIO_RUNS + + +def _selected_runs( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> tuple[tuple[str, object], ...]: + if selected_scenarios is None: + perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS) + return ( + ( + "example_07_large_scale_routing_no_warm_start", + perf_registry["example_07_large_scale_routing_no_warm_start"], + ), + ( + "example_07_large_scale_routing_no_warm_start_seed43", + perf_registry["example_07_large_scale_routing_no_warm_start_seed43"], + ), + ) + + registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS) + allowed_standard = dict(_trace_registry(include_performance_only)) + runs = [] + for name in selected_scenarios: + if name in allowed_standard: + runs.append((name, allowed_standard[name])) + continue + if name in registry: + runs.append((name, registry[name])) + continue + valid = ", ".join(sorted(registry)) + raise SystemExit(f"Unknown iteration-trace scenario: {name}. Valid scenarios: {valid}") + return tuple(runs) + + +def _build_payload( + selected_scenarios: tuple[str, ...] | None, + *, + include_performance_only: bool, +) -> dict[str, object]: + scenarios = [] + for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only): + result = run() + scenarios.append( + { + "name": name, + "summary": { + "total_results": len(result.results_by_net), + "valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid), + "reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target), + }, + "metrics": asdict(result.metrics), + "iteration_trace": [asdict(entry) for entry in result.iteration_trace], + } + ) + return { + "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "generator": "scripts/record_iteration_trace.py", + "scenarios": scenarios, + } + + +def _render_markdown(payload: dict[str, object]) -> str: + lines = [ + "# Iteration Trace", + "", + f"Generated at {payload['generated_at']} by `{payload['generator']}`.", + "", + ] + + for scenario in payload["scenarios"]: + lines.extend( + [ + f"## {scenario['name']}", + "", + f"Results: {scenario['summary']['valid_results']} valid / " + f"{scenario['summary']['reached_targets']} reached / " + f"{scenario['summary']['total_results']} total.", + "", + "| Iteration | Penalty | Routed Nets | Completed | Conflict Edges | Dynamic Collisions | Nodes | Congestion Checks | Candidate Ids | Exact Pairs |", + "| --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |", + ] + ) + + net_node_counts: Counter[str] = Counter() + net_check_counts: Counter[str] = Counter() + for entry in scenario["iteration_trace"]: + lines.append( + "| " + f"{entry['iteration']} | " + f"{entry['congestion_penalty']:.1f} | " + f"{len(entry['routed_net_ids'])} | " + f"{entry['completed_nets']} | " + f"{entry['conflict_edges']} | " + f"{entry['total_dynamic_collisions']} | " + f"{entry['nodes_expanded']} | " + f"{entry['congestion_check_calls']} | " + f"{entry['congestion_candidate_ids']} | " + f"{entry['congestion_exact_pair_checks']} |" + ) + for attempt in entry["net_attempts"]: + net_node_counts[attempt["net_id"]] += attempt["nodes_expanded"] + net_check_counts[attempt["net_id"]] += attempt["congestion_check_calls"] + + lines.extend(["", "Top nets by iteration-attributed nodes expanded:", ""]) + if net_node_counts: + for net_id, count in net_node_counts.most_common(10): + lines.append(f"- `{net_id}`: {count}") + else: + lines.append("- None") + + lines.extend(["", "Top nets by iteration-attributed congestion checks:", ""]) + if net_check_counts: + for net_id, count in net_check_counts.most_common(10): + lines.append(f"- `{net_id}`: {count}") + else: + lines.append("- None") + + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record iteration-trace artifacts for selected trace scenarios.") + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional trace scenario name to include. May be passed more than once.", + ) + parser.add_argument( + "--include-performance-only", + action="store_true", + help="Include performance-only trace scenarios when selecting from the standard registry.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory to write iteration_trace.json and iteration_trace.md into. Defaults to /docs.", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve() + output_dir.mkdir(exist_ok=True) + + selected = tuple(args.scenarios) if args.scenarios else None + payload = _build_payload(selected, include_performance_only=args.include_performance_only) + json_path = output_dir / "iteration_trace.json" + markdown_path = output_dir / "iteration_trace.md" + + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + markdown_path.write_text(_render_markdown(payload) + "\n") + + if json_path.is_relative_to(repo_root): + print(f"Wrote {json_path.relative_to(repo_root)}") + else: + print(f"Wrote {json_path}") + if markdown_path.is_relative_to(repo_root): + print(f"Wrote {markdown_path.relative_to(repo_root)}") + else: + print(f"Wrote {markdown_path}") + + +if __name__ == "__main__": + main() From 2c3aa90544e283160835486ffb5c82f4e68ccaa4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 17:10:00 -0700 Subject: [PATCH 2/4] Limit late reroutes to conflicting nets --- docs/iteration_trace.json | 1026 ++++------------------- docs/iteration_trace.md | 94 +-- docs/optimization_pass_01_log.md | 22 + docs/performance.md | 36 +- docs/performance_baseline.json | 108 +-- inire/router/_router.py | 27 +- inire/tests/test_api.py | 62 ++ inire/tests/test_astar.py | 1 + inire/tests/test_example_performance.py | 2 +- 9 files changed, 376 insertions(+), 1002 deletions(-) diff --git a/docs/iteration_trace.json b/docs/iteration_trace.json index ef7f3a2..72295bc 100644 --- a/docs/iteration_trace.json +++ b/docs/iteration_trace.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-04-02T16:11:39-07:00", + "generated_at": "2026-04-02T16:46:00-07:00", "generator": "scripts/record_iteration_trace.py", "scenarios": [ { @@ -507,9 +507,9 @@ { "completed_nets": 6, "conflict_edges": 2, - "congestion_candidate_ids": 961, - "congestion_check_calls": 517, - "congestion_exact_pair_checks": 812, + "congestion_candidate_ids": 627, + "congestion_check_calls": 332, + "congestion_exact_pair_checks": 513, "congestion_penalty": 384.15999999999997, "iteration": 4, "net_attempts": [ @@ -533,46 +533,6 @@ "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": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 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 - }, - { - "congestion_check_calls": 22, - "guidance_seed_present": true, - "net_id": "net_03", - "nodes_expanded": 6, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 30, "guidance_seed_present": true, @@ -583,26 +543,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 51, - "guidance_seed_present": true, - "net_id": "net_02", - "nodes_expanded": 12, - "pruned_closed_set": 3, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 80, "guidance_seed_present": true, @@ -614,217 +554,86 @@ "reached_target": true } ], - "nodes_expanded": 126, + "nodes_expanded": 81, "routed_net_ids": [ "net_00", "net_06", - "net_09", - "net_05", - "net_08", - "net_03", "net_07", - "net_02", - "net_04", "net_01" ], "total_dynamic_collisions": 10 - }, - { - "completed_nets": 6, - "conflict_edges": 2, - "congestion_candidate_ids": 3805, - "congestion_check_calls": 1704, - "congestion_exact_pair_checks": 3043, - "congestion_penalty": 537.824, - "iteration": 5, - "net_attempts": [ - { - "congestion_check_calls": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 66, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 15, - "pruned_closed_set": 2, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 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": 109, - "guidance_seed_present": true, - "net_id": "net_02", - "nodes_expanded": 21, - "pruned_closed_set": 5, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 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 - }, - { - "congestion_check_calls": 85, - "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 16, - "pruned_closed_set": 3, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 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": 795, - "guidance_seed_present": true, - "net_id": "net_03", - "nodes_expanded": 236, - "pruned_closed_set": 28, - "pruned_cost": 149, - "pruned_hard_collision": 0, - "reached_target": true - } - ], - "nodes_expanded": 461, - "routed_net_ids": [ - "net_00", - "net_01", - "net_04", - "net_09", - "net_02", - "net_05", - "net_08", - "net_07", - "net_06", - "net_03" - ], - "total_dynamic_collisions": 10 } ], "metrics": { "congestion_cache_hits": 31, - "congestion_cache_misses": 4625, - "congestion_candidate_ids": 9924, - "congestion_candidate_nets": 9979, - "congestion_candidate_precheck_hits": 2562, - "congestion_candidate_precheck_misses": 2165, - "congestion_candidate_precheck_skips": 71, - "congestion_check_calls": 4625, - "congestion_exact_pair_checks": 8122, - "congestion_grid_net_cache_hits": 2457, - "congestion_grid_net_cache_misses": 3942, - "congestion_grid_span_cache_hits": 2283, - "congestion_grid_span_cache_misses": 1948, + "congestion_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": 2673, - "congestion_net_envelope_cache_misses": 4139, - "congestion_presence_cache_hits": 2858, - "congestion_presence_cache_misses": 2556, - "congestion_presence_skips": 687, - "danger_map_cache_hits": 16878, - "danger_map_cache_misses": 7425, - "danger_map_lookup_calls": 24303, - "danger_map_query_calls": 7425, - "danger_map_total_ns": 205681291, + "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": 174709728, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 471, - "dynamic_path_objects_removed": 423, + "dynamic_path_objects_added": 399, + "dynamic_path_objects_removed": 351, "dynamic_tree_rebuilds": 0, - "guidance_bonus_applied": 11000.0, - "guidance_bonus_applied_bend90": 3500.0, - "guidance_bonus_applied_sbend": 625.0, - "guidance_bonus_applied_straight": 6875.0, - "guidance_match_moves": 176, - "guidance_match_moves_bend90": 56, - "guidance_match_moves_sbend": 10, - "guidance_match_moves_straight": 110, + "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": 39, - "iteration_conflicting_nets": 36, - "iteration_reverified_nets": 60, - "iteration_reverify_calls": 6, - "move_cache_abs_hits": 2559, - "move_cache_abs_misses": 6494, - "move_cache_rel_hits": 5872, - "move_cache_rel_misses": 622, - "moves_added": 8081, - "moves_generated": 9053, - "nets_carried_forward": 0, - "nets_reached_target": 60, - "nets_routed": 60, - "nodes_expanded": 1764, + "iteration_conflict_edges": 37, + "iteration_conflicting_nets": 32, + "iteration_reverified_nets": 50, + "iteration_reverify_calls": 5, + "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": 439, - "pruned_cost": 533, + "pruned_closed_set": 374, + "pruned_cost": 311, "pruned_hard_collision": 0, - "ray_cast_calls": 5477, - "ray_cast_calls_expand_forward": 1704, - "ray_cast_calls_expand_snap": 46, + "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": 3721, + "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": 305, + "ray_cast_candidate_bounds": 159, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 10, "refinement_candidate_side_extents": 0, @@ -834,18 +643,18 @@ "refinement_dynamic_bounds_checked": 0, "refinement_static_bounds_checked": 0, "refinement_windows_considered": 0, - "route_iterations": 6, - "score_component_calls": 8634, - "score_component_total_ns": 232913892, + "route_iterations": 5, + "score_component_calls": 6181, + "score_component_total_ns": 195118641, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, - "static_safe_cache_hits": 2482, + "static_safe_cache_hits": 1170, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_candidate_nets": 2106, - "verify_dynamic_exact_pair_checks": 558, - "verify_path_report_calls": 190, - "verify_static_buffer_ops": 895, + "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, @@ -856,7 +665,7 @@ "visibility_point_queries": 0, "visibility_tangent_candidate_corner_checks": 6, "visibility_tangent_candidate_ray_tests": 6, - "visibility_tangent_candidate_scans": 1704, + "visibility_tangent_candidate_scans": 1214, "warm_start_paths_built": 0, "warm_start_paths_used": 0 }, @@ -1247,9 +1056,9 @@ { "completed_nets": 4, "conflict_edges": 3, - "congestion_candidate_ids": 801, - "congestion_check_calls": 382, - "congestion_exact_pair_checks": 651, + "congestion_candidate_ids": 557, + "congestion_check_calls": 250, + "congestion_exact_pair_checks": 428, "congestion_penalty": 274.4, "iteration": 3, "net_attempts": [ @@ -1263,26 +1072,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 58, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 13, - "pruned_closed_set": 4, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 49, "guidance_seed_present": true, @@ -1293,16 +1082,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 36, "guidance_seed_present": true, @@ -1333,16 +1112,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 34, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 11, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 30, "guidance_seed_present": true, @@ -1354,17 +1123,13 @@ "reached_target": true } ], - "nodes_expanded": 84, + "nodes_expanded": 54, "routed_net_ids": [ "net_06", - "net_05", - "net_01", "net_09", - "net_04", "net_03", "net_02", "net_08", - "net_00", "net_07" ], "total_dynamic_collisions": 15 @@ -1372,9 +1137,9 @@ { "completed_nets": 6, "conflict_edges": 2, - "congestion_candidate_ids": 1334, - "congestion_check_calls": 673, - "congestion_exact_pair_checks": 1072, + "congestion_candidate_ids": 1126, + "congestion_check_calls": 550, + "congestion_exact_pair_checks": 884, "congestion_penalty": 384.15999999999997, "iteration": 4, "net_attempts": [ @@ -1398,26 +1163,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 48, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 12, - "pruned_closed_set": 4, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 91, "guidance_seed_present": true, @@ -1438,16 +1183,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 32, "guidance_seed_present": true, @@ -1459,37 +1194,23 @@ "reached_target": true }, { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 201, + "congestion_check_calls": 192, "guidance_seed_present": true, "net_id": "net_03", - "nodes_expanded": 55, + "nodes_expanded": 54, "pruned_closed_set": 7, "pruned_cost": 21, "pruned_hard_collision": 0, "reached_target": true } ], - "nodes_expanded": 170, + "nodes_expanded": 142, "routed_net_ids": [ "net_06", "net_07", - "net_00", - "net_01", "net_08", "net_09", - "net_04", "net_02", - "net_05", "net_03" ], "total_dynamic_collisions": 10 @@ -1497,9 +1218,9 @@ { "completed_nets": 6, "conflict_edges": 2, - "congestion_candidate_ids": 3718, - "congestion_check_calls": 1671, - "congestion_exact_pair_checks": 2992, + "congestion_candidate_ids": 3377, + "congestion_check_calls": 1477, + "congestion_exact_pair_checks": 2666, "congestion_penalty": 537.824, "iteration": 5, "net_attempts": [ @@ -1523,16 +1244,6 @@ "pruned_hard_collision": 0, "reached_target": true }, - { - "congestion_check_calls": 48, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 12, - "pruned_closed_set": 4, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, { "congestion_check_calls": 86, "guidance_seed_present": true, @@ -1544,527 +1255,96 @@ "reached_target": true }, { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 803, + "congestion_check_calls": 795, "guidance_seed_present": true, "net_id": "net_03", - "nodes_expanded": 239, + "nodes_expanded": 236, "pruned_closed_set": 28, "pruned_cost": 149, "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 - }, - { - "congestion_check_calls": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "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": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true } ], - "nodes_expanded": 457, + "nodes_expanded": 406, "routed_net_ids": [ "net_07", "net_06", - "net_01", "net_02", - "net_04", - "net_03", - "net_08", - "net_00", - "net_09", - "net_05" + "net_03" ], "total_dynamic_collisions": 10 - }, - { - "completed_nets": 4, - "conflict_edges": 4, - "congestion_candidate_ids": 218513, - "congestion_check_calls": 89671, - "congestion_exact_pair_checks": 171925, - "congestion_penalty": 752.9535999999999, - "iteration": 6, - "net_attempts": [ - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 97, - "guidance_seed_present": true, - "net_id": "net_02", - "nodes_expanded": 19, - "pruned_closed_set": 4, - "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": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 20, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 3, - "pruned_closed_set": 0, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 48, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 12, - "pruned_closed_set": 4, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 96, - "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 18, - "pruned_closed_set": 3, - "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 - }, - { - "congestion_check_calls": 45308, - "guidance_seed_present": true, - "net_id": "net_03", - "nodes_expanded": 11409, - "pruned_closed_set": 12159, - "pruned_cost": 1513, - "pruned_hard_collision": 49, - "reached_target": true - }, - { - "congestion_check_calls": 43984, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 10794, - "pruned_closed_set": 11293, - "pruned_cost": 1256, - "pruned_hard_collision": 54, - "reached_target": true - } - ], - "nodes_expanded": 22288, - "routed_net_ids": [ - "net_05", - "net_02", - "net_09", - "net_00", - "net_04", - "net_01", - "net_07", - "net_08", - "net_03", - "net_06" - ], - "total_dynamic_collisions": 8 - }, - { - "completed_nets": 4, - "conflict_edges": 4, - "congestion_candidate_ids": 34309, - "congestion_check_calls": 29419, - "congestion_exact_pair_checks": 28603, - "congestion_penalty": 1054.13504, - "iteration": 7, - "net_attempts": [ - { - "congestion_check_calls": 13979, - "guidance_seed_present": true, - "net_id": "net_03", - "nodes_expanded": 7675, - "pruned_closed_set": 5308, - "pruned_cost": 423, - "pruned_hard_collision": 214, - "reached_target": true - }, - { - "congestion_check_calls": 146, - "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 40, - "pruned_closed_set": 5, - "pruned_cost": 24, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 149, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 55, - "pruned_closed_set": 37, - "pruned_cost": 64, - "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": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 1269, - "guidance_seed_present": true, - "net_id": "net_02", - "nodes_expanded": 283, - "pruned_closed_set": 117, - "pruned_cost": 49, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 13573, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 7598, - "pruned_closed_set": 4816, - "pruned_cost": 509, - "pruned_hard_collision": 705, - "reached_target": true - }, - { - "congestion_check_calls": 52, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 9, - "pruned_closed_set": 2, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 139, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 48, - "pruned_closed_set": 29, - "pruned_cost": 53, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 60, - "guidance_seed_present": true, - "net_id": "net_08", - "nodes_expanded": 11, - "pruned_closed_set": 2, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - } - ], - "nodes_expanded": 15737, - "routed_net_ids": [ - "net_03", - "net_07", - "net_05", - "net_09", - "net_00", - "net_02", - "net_06", - "net_01", - "net_04", - "net_08" - ], - "total_dynamic_collisions": 8 - }, - { - "completed_nets": 4, - "conflict_edges": 4, - "congestion_candidate_ids": 49314, - "congestion_check_calls": 41803, - "congestion_exact_pair_checks": 41198, - "congestion_penalty": 1475.7890559999998, - "iteration": 8, - "net_attempts": [ - { - "congestion_check_calls": 14281, - "guidance_seed_present": true, - "net_id": "net_03", - "nodes_expanded": 8009, - "pruned_closed_set": 5594, - "pruned_cost": 454, - "pruned_hard_collision": 506, - "reached_target": true - }, - { - "congestion_check_calls": 60, - "guidance_seed_present": true, - "net_id": "net_08", - "nodes_expanded": 11, - "pruned_closed_set": 2, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 1614, - "guidance_seed_present": true, - "net_id": "net_02", - "nodes_expanded": 382, - "pruned_closed_set": 131, - "pruned_cost": 69, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 52, - "guidance_seed_present": true, - "net_id": "net_01", - "nodes_expanded": 9, - "pruned_closed_set": 2, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 26, - "guidance_seed_present": true, - "net_id": "net_00", - "nodes_expanded": 9, - "pruned_closed_set": 1, - "pruned_cost": 0, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 25302, - "guidance_seed_present": true, - "net_id": "net_06", - "nodes_expanded": 12970, - "pruned_closed_set": 9624, - "pruned_cost": 416, - "pruned_hard_collision": 595, - "reached_target": true - }, - { - "congestion_check_calls": 148, - "guidance_seed_present": true, - "net_id": "net_07", - "nodes_expanded": 41, - "pruned_closed_set": 5, - "pruned_cost": 24, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 149, - "guidance_seed_present": true, - "net_id": "net_05", - "nodes_expanded": 55, - "pruned_closed_set": 37, - "pruned_cost": 64, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 139, - "guidance_seed_present": true, - "net_id": "net_04", - "nodes_expanded": 48, - "pruned_closed_set": 29, - "pruned_cost": 53, - "pruned_hard_collision": 0, - "reached_target": true - }, - { - "congestion_check_calls": 32, - "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 - } - ], - "nodes_expanded": 21543, - "routed_net_ids": [ - "net_03", - "net_08", - "net_02", - "net_01", - "net_00", - "net_06", - "net_07", - "net_05", - "net_04", - "net_09" - ], - "total_dynamic_collisions": 8 } ], "metrics": { - "congestion_cache_hits": 1713, - "congestion_cache_misses": 165223, - "congestion_candidate_ids": 312161, - "congestion_candidate_nets": 300960, - "congestion_candidate_precheck_hits": 159843, - "congestion_candidate_precheck_misses": 7810, - "congestion_candidate_precheck_skips": 717, - "congestion_check_calls": 165223, - "congestion_exact_pair_checks": 249697, - "congestion_grid_net_cache_hits": 152369, - "congestion_grid_net_cache_misses": 13481, - "congestion_grid_span_cache_hits": 144618, - "congestion_grid_span_cache_misses": 6885, + "congestion_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_lazy_requeues": 0, "congestion_lazy_resolutions": 0, - "congestion_net_envelope_cache_hits": 160549, - "congestion_net_envelope_cache_misses": 14205, - "congestion_presence_cache_hits": 186369, - "congestion_presence_cache_misses": 9324, - "congestion_presence_skips": 27588, - "danger_map_cache_hits": 453553, - "danger_map_cache_misses": 140744, - "danger_map_lookup_calls": 594297, - "danger_map_query_calls": 140744, - "danger_map_total_ns": 4580111255, + "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, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 683, - "dynamic_path_objects_removed": 637, + "dynamic_path_objects_added": 397, + "dynamic_path_objects_removed": 350, "dynamic_tree_rebuilds": 0, - "guidance_bonus_applied": 23437.5, - "guidance_bonus_applied_bend90": 8437.5, - "guidance_bonus_applied_sbend": 1000.0, - "guidance_bonus_applied_straight": 14000.0, - "guidance_match_moves": 375, - "guidance_match_moves_bend90": 135, - "guidance_match_moves_sbend": 16, - "guidance_match_moves_straight": 224, - "hard_collision_cache_hits": 859, - "iteration_conflict_edges": 51, - "iteration_conflicting_nets": 57, - "iteration_reverified_nets": 90, - "iteration_reverify_calls": 9, - "move_cache_abs_hits": 140734, - "move_cache_abs_misses": 183345, - "move_cache_rel_hits": 180943, - "move_cache_rel_misses": 2402, - "moves_added": 197062, - "moves_generated": 324079, - "nets_carried_forward": 0, - "nets_reached_target": 90, - "nets_routed": 90, - "nodes_expanded": 61259, + "guidance_bonus_applied": 8062.5, + "guidance_bonus_applied_bend90": 3187.5, + "guidance_bonus_applied_sbend": 250.0, + "guidance_bonus_applied_straight": 4625.0, + "guidance_match_moves": 129, + "guidance_match_moves_bend90": 51, + "guidance_match_moves_sbend": 4, + "guidance_match_moves_straight": 74, + "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, + "nets_carried_forward": 14, + "nets_reached_target": 46, + "nets_routed": 46, + "nodes_expanded": 1582, "pair_local_search_accepts": 2, "pair_local_search_attempts": 3, - "pair_local_search_nodes_expanded": 38, + "pair_local_search_nodes_expanded": 39, "pair_local_search_pairs_considered": 2, "path_cost_calls": 0, - "pruned_closed_set": 49632, - "pruned_cost": 5502, - "pruned_hard_collision": 2123, - "ray_cast_calls": 166977, - "ray_cast_calls_expand_forward": 61169, - "ray_cast_calls_expand_snap": 735, + "pruned_closed_set": 399, + "pruned_cost": 531, + "pruned_hard_collision": 0, + "ray_cast_calls": 5077, + "ray_cast_calls_expand_forward": 1536, + "ray_cast_calls_expand_snap": 13, "ray_cast_calls_other": 0, - "ray_cast_calls_straight_static": 103738, + "ray_cast_calls_straight_static": 3522, "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, - "ray_cast_calls_visibility_tangent": 1335, - "ray_cast_candidate_bounds": 11494, + "ray_cast_calls_visibility_tangent": 6, + "ray_cast_candidate_bounds": 316, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 10, "refinement_candidate_side_extents": 0, @@ -2074,18 +1354,18 @@ "refinement_dynamic_bounds_checked": 0, "refinement_static_bounds_checked": 0, "refinement_windows_considered": 0, - "route_iterations": 9, - "score_component_calls": 203601, - "score_component_total_ns": 5292565462, + "route_iterations": 6, + "score_component_calls": 7670, + "score_component_total_ns": 205617403, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, - "static_safe_cache_hits": 120231, + "static_safe_cache_hits": 1869, "static_tree_rebuilds": 1, "timeout_events": 0, - "verify_dynamic_candidate_nets": 2985, - "verify_dynamic_exact_pair_checks": 760, - "verify_path_report_calls": 250, - "verify_static_buffer_ops": 1321, + "verify_dynamic_candidate_nets": 1906, + "verify_dynamic_exact_pair_checks": 571, + "verify_path_report_calls": 176, + "verify_static_buffer_ops": 813, "visibility_builds": 0, "visibility_corner_hits_exact": 0, "visibility_corner_index_builds": 1, @@ -2094,9 +1374,9 @@ "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, - "visibility_tangent_candidate_corner_checks": 1405, - "visibility_tangent_candidate_ray_tests": 1335, - "visibility_tangent_candidate_scans": 61169, + "visibility_tangent_candidate_corner_checks": 6, + "visibility_tangent_candidate_ray_tests": 6, + "visibility_tangent_candidate_scans": 1536, "warm_start_paths_built": 0, "warm_start_paths_used": 0 }, diff --git a/docs/iteration_trace.md b/docs/iteration_trace.md index ab4bbad..febbce1 100644 --- a/docs/iteration_trace.md +++ b/docs/iteration_trace.md @@ -1,6 +1,6 @@ # Iteration Trace -Generated at 2026-04-02T16:11:39-07:00 by `scripts/record_iteration_trace.py`. +Generated at 2026-04-02T16:46:00-07:00 by `scripts/record_iteration_trace.py`. ## example_07_large_scale_routing_no_warm_start @@ -12,34 +12,33 @@ Results: 10 valid / 10 reached / 10 total. | 1 | 140.0 | 10 | 2 | 12 | 54 | 253 | 974 | 2378 | 1998 | | 2 | 196.0 | 10 | 4 | 5 | 22 | 253 | 993 | 1928 | 1571 | | 3 | 274.4 | 10 | 6 | 2 | 10 | 100 | 437 | 852 | 698 | -| 4 | 384.2 | 10 | 6 | 2 | 10 | 126 | 517 | 961 | 812 | -| 5 | 537.8 | 10 | 6 | 2 | 10 | 461 | 1704 | 3805 | 3043 | +| 4 | 384.2 | 4 | 6 | 2 | 10 | 81 | 332 | 627 | 513 | Top nets by iteration-attributed nodes expanded: -- `net_03`: 383 -- `net_06`: 292 -- `net_09`: 260 -- `net_00`: 210 -- `net_02`: 190 -- `net_08`: 168 -- `net_01`: 162 -- `net_07`: 61 -- `net_04`: 19 -- `net_05`: 19 +- `net_09`: 242 +- `net_00`: 201 +- `net_02`: 157 +- `net_06`: 155 +- `net_01`: 147 +- `net_08`: 144 +- `net_03`: 141 +- `net_07`: 45 +- `net_04`: 13 +- `net_05`: 13 Top nets by iteration-attributed congestion checks: -- `net_03`: 1242 -- `net_06`: 1080 -- `net_02`: 674 -- `net_01`: 534 -- `net_08`: 262 -- `net_00`: 229 -- `net_07`: 228 -- `net_09`: 176 -- `net_04`: 100 -- `net_05`: 100 +- `net_06`: 569 +- `net_02`: 514 +- `net_01`: 468 +- `net_03`: 425 +- `net_00`: 203 +- `net_08`: 170 +- `net_07`: 143 +- `net_09`: 124 +- `net_04`: 60 +- `net_05`: 60 ## example_07_large_scale_routing_no_warm_start_seed43 @@ -50,36 +49,33 @@ Results: 10 valid / 10 reached / 10 total. | 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 | | 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 | | 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | -| 3 | 274.4 | 10 | 4 | 3 | 15 | 84 | 382 | 801 | 651 | -| 4 | 384.2 | 10 | 6 | 2 | 10 | 170 | 673 | 1334 | 1072 | -| 5 | 537.8 | 10 | 6 | 2 | 10 | 457 | 1671 | 3718 | 2992 | -| 6 | 753.0 | 10 | 4 | 4 | 8 | 22288 | 89671 | 218513 | 171925 | -| 7 | 1054.1 | 10 | 4 | 4 | 8 | 15737 | 29419 | 34309 | 28603 | -| 8 | 1475.8 | 10 | 4 | 4 | 8 | 21543 | 41803 | 49314 | 41198 | +| 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 | Top nets by iteration-attributed nodes expanded: -- `net_06`: 31604 -- `net_03`: 27532 -- `net_02`: 763 -- `net_09`: 286 -- `net_07`: 239 -- `net_00`: 233 -- `net_08`: 218 -- `net_05`: 134 -- `net_01`: 132 -- `net_04`: 118 +- `net_03`: 435 +- `net_09`: 250 +- `net_06`: 242 +- `net_00`: 177 +- `net_08`: 172 +- `net_07`: 140 +- `net_02`: 79 +- `net_01`: 65 +- `net_05`: 12 +- `net_04`: 10 Top nets by iteration-attributed congestion checks: -- `net_06`: 83752 -- `net_03`: 75019 -- `net_02`: 3270 -- `net_07`: 844 -- `net_08`: 540 -- `net_01`: 441 -- `net_05`: 425 -- `net_04`: 398 -- `net_09`: 288 -- `net_00`: 246 +- `net_03`: 1434 +- `net_06`: 893 +- `net_07`: 454 +- `net_08`: 328 +- `net_02`: 290 +- `net_09`: 178 +- `net_01`: 135 +- `net_00`: 82 +- `net_05`: 47 +- `net_04`: 40 diff --git a/docs/optimization_pass_01_log.md b/docs/optimization_pass_01_log.md index c8fab8f..71badd0 100644 --- a/docs/optimization_pass_01_log.md +++ b/docs/optimization_pass_01_log.md @@ -3640,3 +3640,25 @@ Findings: - The pathological `seed 43` basin is not front-loaded. It matches the solved `seed 42` path through iteration `5`, then falls into three extra iterations with only `4` completed nets and `4` conflict edges. - The late blowup is concentrated in two nets, not the whole routing set: `net_06` contributes `31604` attributed nodes and `83752` congestion checks, while `net_03` contributes `27532` nodes and `75019` congestion checks. - This points the next optimization work at late-iteration reroute behavior for a small subset of nets rather than another global congestion or pair-local-search change. + +## Step 65 stop after fully reached two-edge plateau + +Measured on 2026-04-02T16:21:02-07:00. + +Findings: + +- Added a narrow late-iteration stop rule: once every net already reaches target and the best snapshot is down to the final `<=2` dynamic-conflict-edge basin, stop after the first no-improvement iteration and hand off to bounded pair-local repair. +- The solved seed-42 no-warm canary improved from `6` to `5` negotiated-congestion iterations and dropped from about `1764` to `1303` nodes and from `4625` to `2921` congestion checks, while staying `10/10/10`. +- The former seed-43 pathological basin collapsed from about `50s`, `61259` nodes, and `165223` congestion checks to about `2.53s`, `1691` nodes, and `4330` congestion checks, still finishing `10/10/10`. +- Guardrails held unchanged: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3`. + +## Step 66 reroute only current conflict nets in late all-reached phase + +Measured on 2026-04-02T16:46:00-07:00. + +Findings: + +- Once all nets already reach target and the live conflict graph is down to `<=3` edges, the next negotiated iteration now reroutes only the currently conflicting nets instead of all nets. +- The solved seed-42 no-warm canary stayed `10/10/10` and improved from `50` routed nets / `1303` nodes / `2921` congestion checks to `44` routed nets / `1258` nodes / `2736` congestion checks. +- The seed-43 no-warm canary stayed `10/10/10` and improved from `60` routed nets / `1691` nodes / `4330` congestion checks to `46` routed nets / `1582` nodes / `3881` congestion checks. +- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` while trimming slightly to `5` routed nets, `297` nodes, and `146` congestion checks. diff --git a/docs/performance.md b/docs/performance.md index 9159243..57d9f76 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -5,39 +5,29 @@ 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 `example_07_large_scale_routing_no_warm_start` canary remains performance-only and is tracked through targeted diffs plus the conflict/frontier trace artifacts. +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` when you want a small parameter sweep over example_07-style no-warm runs instead of a single canary reading. -The current tracked sweep output lives in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`. -Use `scripts/record_iteration_trace.py` when you want a seed-42 vs seed-43 negotiated-congestion attribution run; the current tracked output lives in `docs/iteration_trace.json` and `docs/iteration_trace.md`. +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.0040 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 4 | -| example_02_congestion_resolution | 0.3378 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 38 | -| example_03_locked_paths | 0.1929 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 16 | -| example_04_sbends_and_radii | 0.0279 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 8 | -| example_05_orientation_stress | 0.2367 | 3 | 3 | 3 | 2 | 6 | 299 | 1284 | 1691 | 696 | 0 | 0 | 149 | 18 | -| example_06_bend_collision_models | 0.1998 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 12 | -| example_07_large_scale_routing | 0.2005 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 40 | -| example_08_custom_bend_geometry | 0.0176 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 8 | -| example_09_unroutable_best_effort | 0.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | +| 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 | ## 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. -For the current accepted branch, the most important performance-only canary is `example_07_large_scale_routing_no_warm_start`, which now finishes `10/10/10` after a bounded post-loop pair-local scratch reroute. The relevant counters for that phase are: - -- `pair_local_search_pairs_considered` -- `pair_local_search_attempts` -- `pair_local_search_accepts` -- `pair_local_search_nodes_expanded` - -The latest tracked characterization sweep confirms there is no smaller stable pair-local smoke case under the `<=1.0s` rule, so the 10-net no-warm-start canary remains the primary regression target for this behavior. - -The tracked iteration trace adds one more diagnosis target: `example_07_large_scale_routing_no_warm_start_seed43`. That seed now remains performance-only, and its blowup is concentrated in late iterations rather than the initial basin. In the current trace, seed `43` matches seed `42` through iteration `5`, then spends three extra iterations with `4` completed nets while `net_03` and `net_06` dominate both `nodes_expanded` and `congestion_check_calls`. +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: diff --git a/docs/performance_baseline.json b/docs/performance_baseline.json index a0477d3..6dd6ae2 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.003964120987802744, + "duration_s": 0.003825429128482938, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -85,7 +85,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 11, - "score_component_total_ns": 18064, + "score_component_total_ns": 16571, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -115,7 +115,7 @@ "valid_results": 1 }, { - "duration_s": 0.3377689190674573, + "duration_s": 0.36141274496912956, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -197,7 +197,7 @@ "refinement_windows_considered": 10, "route_iterations": 1, "score_component_calls": 976, - "score_component_total_ns": 1140704, + "score_component_total_ns": 1143187, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -227,7 +227,7 @@ "valid_results": 3 }, { - "duration_s": 0.1929313091095537, + "duration_s": 0.19532882701605558, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -309,7 +309,7 @@ "refinement_windows_considered": 2, "route_iterations": 2, "score_component_calls": 504, - "score_component_total_ns": 565410, + "score_component_total_ns": 565663, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 1, @@ -339,7 +339,7 @@ "valid_results": 2 }, { - "duration_s": 0.02791503700427711, + "duration_s": 0.027705274987965822, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -421,7 +421,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 90, - "score_component_total_ns": 100083, + "score_component_total_ns": 96756, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -451,73 +451,73 @@ "valid_results": 2 }, { - "duration_s": 0.23665715800598264, + "duration_s": 0.25367443496361375, "metrics": { - "congestion_cache_hits": 4, - "congestion_cache_misses": 149, + "congestion_cache_hits": 3, + "congestion_cache_misses": 146, "congestion_candidate_ids": 32, "congestion_candidate_nets": 23, - "congestion_candidate_precheck_hits": 131, - "congestion_candidate_precheck_misses": 22, + "congestion_candidate_precheck_hits": 129, + "congestion_candidate_precheck_misses": 20, "congestion_candidate_precheck_skips": 0, - "congestion_check_calls": 149, + "congestion_check_calls": 146, "congestion_exact_pair_checks": 30, "congestion_grid_net_cache_hits": 16, - "congestion_grid_net_cache_misses": 28, + "congestion_grid_net_cache_misses": 26, "congestion_grid_span_cache_hits": 15, "congestion_grid_span_cache_misses": 7, "congestion_lazy_requeues": 0, "congestion_lazy_resolutions": 0, - "congestion_net_envelope_cache_hits": 128, - "congestion_net_envelope_cache_misses": 43, - "congestion_presence_cache_hits": 200, - "congestion_presence_cache_misses": 30, - "congestion_presence_skips": 77, + "congestion_net_envelope_cache_hits": 127, + "congestion_net_envelope_cache_misses": 39, + "congestion_presence_cache_hits": 196, + "congestion_presence_cache_misses": 27, + "congestion_presence_skips": 74, "danger_map_cache_hits": 0, "danger_map_cache_misses": 0, "danger_map_lookup_calls": 0, "danger_map_query_calls": 0, "danger_map_total_ns": 0, "dynamic_grid_rebuilds": 0, - "dynamic_path_objects_added": 49, - "dynamic_path_objects_removed": 37, + "dynamic_path_objects_added": 48, + "dynamic_path_objects_removed": 36, "dynamic_tree_rebuilds": 0, - "guidance_bonus_applied": 687.5, + "guidance_bonus_applied": 562.5, "guidance_bonus_applied_bend90": 500.0, "guidance_bonus_applied_sbend": 0.0, - "guidance_bonus_applied_straight": 187.5, - "guidance_match_moves": 11, + "guidance_bonus_applied_straight": 62.5, + "guidance_match_moves": 9, "guidance_match_moves_bend90": 8, "guidance_match_moves_sbend": 0, - "guidance_match_moves_straight": 3, + "guidance_match_moves_straight": 1, "hard_collision_cache_hits": 0, "iteration_conflict_edges": 1, "iteration_conflicting_nets": 2, "iteration_reverified_nets": 6, "iteration_reverify_calls": 2, - "move_cache_abs_hits": 385, + "move_cache_abs_hits": 374, "move_cache_abs_misses": 1306, "move_cache_rel_hits": 1204, "move_cache_rel_misses": 102, - "moves_added": 696, - "moves_generated": 1691, - "nets_carried_forward": 0, - "nets_reached_target": 6, - "nets_routed": 6, - "nodes_expanded": 299, + "moves_added": 689, + "moves_generated": 1680, + "nets_carried_forward": 1, + "nets_reached_target": 5, + "nets_routed": 5, + "nodes_expanded": 297, "pair_local_search_accepts": 0, "pair_local_search_attempts": 0, "pair_local_search_nodes_expanded": 0, "pair_local_search_pairs_considered": 0, "path_cost_calls": 2, "pruned_closed_set": 159, - "pruned_cost": 537, + "pruned_cost": 533, "pruned_hard_collision": 14, - "ray_cast_calls": 1284, - "ray_cast_calls_expand_forward": 293, - "ray_cast_calls_expand_snap": 3, + "ray_cast_calls": 1274, + "ray_cast_calls_expand_forward": 292, + "ray_cast_calls_expand_snap": 2, "ray_cast_calls_other": 0, - "ray_cast_calls_straight_static": 979, + "ray_cast_calls_straight_static": 971, "ray_cast_calls_visibility_build": 0, "ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_tangent": 9, @@ -532,16 +532,16 @@ "refinement_static_bounds_checked": 0, "refinement_windows_considered": 0, "route_iterations": 2, - "score_component_calls": 1245, - "score_component_total_ns": 1260961, + "score_component_calls": 1234, + "score_component_total_ns": 1311211, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, - "static_safe_cache_hits": 9, + "static_safe_cache_hits": 8, "static_tree_rebuilds": 1, "timeout_events": 0, "verify_dynamic_candidate_nets": 8, "verify_dynamic_exact_pair_checks": 12, - "verify_path_report_calls": 18, + "verify_path_report_calls": 17, "verify_static_buffer_ops": 0, "visibility_builds": 0, "visibility_corner_hits_exact": 0, @@ -553,7 +553,7 @@ "visibility_point_queries": 0, "visibility_tangent_candidate_corner_checks": 70, "visibility_tangent_candidate_ray_tests": 9, - "visibility_tangent_candidate_scans": 293, + "visibility_tangent_candidate_scans": 292, "warm_start_paths_built": 2, "warm_start_paths_used": 2 }, @@ -563,7 +563,7 @@ "valid_results": 3 }, { - "duration_s": 0.19982667709700763, + "duration_s": 0.21031348290853202, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -589,7 +589,7 @@ "danger_map_cache_misses": 731, "danger_map_lookup_calls": 1914, "danger_map_query_calls": 731, - "danger_map_total_ns": 18959782, + "danger_map_total_ns": 19983976, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 54, "dynamic_path_objects_removed": 36, @@ -645,7 +645,7 @@ "refinement_windows_considered": 0, "route_iterations": 3, "score_component_calls": 842, - "score_component_total_ns": 21338709, + "score_component_total_ns": 22474166, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3, "static_safe_cache_hits": 141, @@ -675,7 +675,7 @@ "valid_results": 3 }, { - "duration_s": 0.20046633295714855, + "duration_s": 0.20740868314169347, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -701,7 +701,7 @@ "danger_map_cache_misses": 448, "danger_map_lookup_calls": 681, "danger_map_query_calls": 448, - "danger_map_total_ns": 11017087, + "danger_map_total_ns": 11224403, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 132, "dynamic_path_objects_removed": 88, @@ -757,7 +757,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 291, - "score_component_total_ns": 11869917, + "score_component_total_ns": 12117666, "static_net_tree_rebuilds": 10, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 6, @@ -787,7 +787,7 @@ "valid_results": 10 }, { - "duration_s": 0.01759456400759518, + "duration_s": 0.018604618962854147, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -869,7 +869,7 @@ "refinement_windows_considered": 0, "route_iterations": 2, "score_component_calls": 72, - "score_component_total_ns": 85864, + "score_component_total_ns": 87655, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 2, @@ -899,7 +899,7 @@ "valid_results": 2 }, { - "duration_s": 0.005838233977556229, + "duration_s": 0.00794802000746131, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, @@ -925,7 +925,7 @@ "danger_map_cache_misses": 20, "danger_map_lookup_calls": 30, "danger_map_query_calls": 20, - "danger_map_total_ns": 523870, + "danger_map_total_ns": 675454, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 2, "dynamic_path_objects_removed": 1, @@ -981,7 +981,7 @@ "refinement_windows_considered": 0, "route_iterations": 1, "score_component_calls": 14, - "score_component_total_ns": 563611, + "score_component_total_ns": 722637, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 0, diff --git a/inire/router/_router.py b/inire/router/_router.py index 3f1f400..920d6b6 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -52,6 +52,7 @@ class _RoutingState: last_conflict_signature: tuple[tuple[str, str], ...] last_conflict_edge_count: int repeated_conflict_count: int + pair_local_plateau_count: int @dataclass(slots=True) @@ -217,6 +218,7 @@ class PathFinder: last_conflict_signature=(), last_conflict_edge_count=0, repeated_conflict_count=0, + pair_local_plateau_count=0, ) if state.initial_paths is None and congestion.warm_start_enabled: state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order) @@ -949,22 +951,43 @@ class PathFinder: iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, ) -> bool: congestion = self.context.options.congestion + reroute_net_ids = set(state.ordered_net_ids) for iteration in range(congestion.max_iterations): review = self._run_iteration( state, iteration, - set(state.ordered_net_ids), + reroute_net_ids, iteration_callback, ) if review is None: return True - self._update_best_iteration(state, review) + improved = self._update_best_iteration(state, review) if not any( result.outcome in {"colliding", "partial", "unroutable"} for result in state.results.values() ): return False + all_reached_target = ( + len(state.results) == len(state.ordered_net_ids) + and all(result.reached_target for result in state.results.values()) + ) + reroute_net_ids = set(state.ordered_net_ids) + if all_reached_target and 0 < len(review.conflict_edges) <= 3: + reroute_net_ids = set(review.conflicting_nets) + if improved: + state.pair_local_plateau_count = 0 + elif all_reached_target and state.best_conflict_edges <= 2: + # Once all nets reach target and the best snapshot is already in the + # final <=2-edge basin, later negotiated reroutes tend to churn. + # Hand off to the bounded pair-local repair instead of exploring + # additional late iterations that are not improving the best state. + state.pair_local_plateau_count += 1 + if state.pair_local_plateau_count >= 1: + return False + else: + state.pair_local_plateau_count = 0 + current_signature = tuple(sorted(review.conflict_edges)) repeated = ( bool(current_signature) diff --git a/inire/tests/test_api.py b/inire/tests/test_api.py index b58bce1..a724e90 100644 --- a/inire/tests/test_api.py +++ b/inire/tests/test_api.py @@ -352,6 +352,68 @@ def test_reverify_iterations_stop_early_on_stalled_conflict_graph() -> None: assert run.metrics.route_iterations < 10 +def test_reverify_iterations_limit_late_reroutes_to_conflicting_nets(monkeypatch: pytest.MonkeyPatch) -> None: + problem = RoutingProblem( + bounds=(0, 0, 100, 100), + nets=( + NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0), + NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0), + NetSpec("netC", Port(10, 20, 0), Port(90, 20, 0), width=2.0), + ), + ) + options = RoutingOptions( + congestion=CongestionOptions(max_iterations=10, warm_start_enabled=False), + refinement=RefinementOptions(enabled=False), + ) + evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds)) + pathfinder = PathFinder(AStarContext(evaluator, problem, options)) + colliding_a = RoutingResult( + net_id="netA", + path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(dynamic_collision_count=1, total_length=80.0), + ) + colliding_b = RoutingResult( + net_id="netB", + path=(Straight.generate(Port(50, 10, 90), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(dynamic_collision_count=1, total_length=80.0), + ) + completed_c = RoutingResult( + net_id="netC", + path=(Straight.generate(Port(10, 20, 0), 80.0, 2.0, dilation=1.0),), + reached_target=True, + report=RoutingReport(total_length=80.0), + ) + iterations_seen: list[int] = [] + reroute_sets: list[set[str]] = [] + + def fake_run_iteration(self, state, iteration, reroute_net_ids, iteration_callback): + _ = self + _ = iteration_callback + iterations_seen.append(iteration) + reroute_sets.append(set(reroute_net_ids)) + state.results = {"netA": colliding_a, "netB": colliding_b, "netC": completed_c} + return _IterationReview( + conflicting_nets={"netA", "netB"}, + conflict_edges={("netA", "netB")}, + completed_net_ids={"netC"}, + total_dynamic_collisions=2, + ) + + monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration) + monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results)) + monkeypatch.setattr(PathFinder, "_run_pair_local_search", lambda self, state: None) + + results = pathfinder.route_all() + + assert iterations_seen == [0, 1] + assert reroute_sets == [{"netA", "netB", "netC"}, {"netA", "netB"}] + assert results["netA"].outcome == "colliding" + assert results["netB"].outcome == "colliding" + assert results["netC"].outcome == "completed" + + def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None: problem = RoutingProblem( bounds=(0, 0, 100, 100), diff --git a/inire/tests/test_astar.py b/inire/tests/test_astar.py index 3a0382c..0fc106f 100644 --- a/inire/tests/test_astar.py +++ b/inire/tests/test_astar.py @@ -288,6 +288,7 @@ def test_pair_local_context_clones_live_static_obstacles() -> None: last_conflict_signature=(), last_conflict_edge_count=0, repeated_conflict_count=0, + pair_local_plateau_count=0, ) local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b")) diff --git a/inire/tests/test_example_performance.py b/inire/tests/test_example_performance.py index 26d5504..adf7bfb 100644 --- a/inire/tests/test_example_performance.py +++ b/inire/tests/test_example_performance.py @@ -22,7 +22,7 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" PERFORMANCE_REPEATS = 3 REGRESSION_FACTOR = 1.5 NO_WARM_START_REGRESSION_SECONDS = 15.0 -NO_WARM_START_SEED43_REGRESSION_SECONDS = 120.0 +NO_WARM_START_SEED43_REGRESSION_SECONDS = 20.0 # Baselines are measured from clean 6a28dcf-style runs without plotting. BASELINE_SECONDS = { From 46e7e13059b0363c1fb5e76191426bd0a6d9e54f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 18:57:34 -0700 Subject: [PATCH 3/4] 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() From 7e0d96f9876575d94c0497f6c21a984f8eb565e1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 2 Apr 2026 20:17:03 -0700 Subject: [PATCH 4/4] Refactor router late-phase control flow --- inire/router/_router.py | 389 ++++++++++++++++++++++++---------------- 1 file changed, 238 insertions(+), 151 deletions(-) diff --git a/inire/router/_router.py b/inire/router/_router.py index 9c19fb0..bc8aa97 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -135,6 +135,37 @@ class PathFinder: def _metric_deltas(self, before: dict[str, int], after: dict[str, int]) -> dict[str, int]: return {metric_name: after[metric_name] - before[metric_name] for metric_name in before} + def _results_all_reached_target(self, state: _RoutingState) -> bool: + return ( + len(state.results) == len(state.ordered_net_ids) + and all(result.reached_target for result in state.results.values()) + ) + + def _has_incumbent_fallback(self, result: RoutingResult | None) -> bool: + return bool(result and result.reached_target and result.path) + + def _restore_incumbent_fallback( + self, + net_id: str, + result: RoutingResult, + guidance_seed_present: bool, + ) -> tuple[RoutingResult, bool]: + self.metrics.total_late_phase_capped_fallbacks += 1 + self._install_path(net_id, result.path) + return result, guidance_seed_present + + def _guidance_for_result( + self, + result: RoutingResult | None, + ) -> tuple[Sequence[ComponentResult] | None, float, bool]: + if result is None or not result.reached_target or not result.path: + return None, 0.0, False + return ( + result.as_seed().segments, + max(10.0, self.context.options.objective.bend_penalty * 0.25), + True, + ) + def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None: all_geoms: list[Polygon] = [] all_dilated: list[Polygon] = [] @@ -272,6 +303,38 @@ class PathFinder: if result and result.path: self._install_path(net_id, result.path) + def _analyze_restored_best( + self, + state: _RoutingState, + ) -> tuple[dict[str, PathVerificationDetail], _IterationReview]: + capture_component_conflicts = ( + self.context.options.diagnostics.capture_conflict_trace + or self.context.options.diagnostics.capture_pre_pair_frontier_trace + ) + state.results, details_by_net, review = self._analyze_results( + state.ordered_net_ids, + state.results, + capture_component_conflicts=capture_component_conflicts, + count_iteration_metrics=False, + ) + if self.context.options.diagnostics.capture_conflict_trace: + self._capture_conflict_trace_entry( + state, + stage="restored_best", + iteration=None, + results=state.results, + details_by_net=details_by_net, + review=review, + ) + if self.context.options.diagnostics.capture_pre_pair_frontier_trace: + self.pre_pair_frontier_trace = self._materialize_pre_pair_frontier_trace( + state, + state.results, + details_by_net, + review, + ) + return details_by_net, review + def _update_best_iteration(self, state: _RoutingState, review: _IterationReview) -> bool: completed_nets = len(review.completed_net_ids) conflict_edges = len(review.conflict_edges) @@ -635,6 +698,9 @@ class PathFinder: return candidate_length < incumbent_length return False + def _pair_local_attempt_orders(self, target: _PairLocalTarget) -> tuple[tuple[str, str], tuple[str, str]]: + return target.net_ids, target.net_ids[::-1] + def _collect_pair_local_targets( self, state: _RoutingState, @@ -756,6 +822,97 @@ class PathFinder: nets=tuple(nets), ) + def _build_iteration_reroute_plan( + self, + state: _RoutingState, + reroute_net_ids: set[str], + ) -> tuple[list[str], set[str]]: + routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids] + capped_net_ids: set[str] = set() + if len(reroute_net_ids) >= len(state.ordered_net_ids) or not state.recent_attempt_work: + return routed_net_ids, capped_net_ids + + order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)} + routed_net_ids.sort(key=lambda net_id: (state.recent_attempt_work.get(net_id, 0), order_index[net_id])) + if ( + len(routed_net_ids) == 4 + and state.best_conflict_edges <= 2 + and self._results_all_reached_target(state) + ): + heavy_net_ids = sorted( + routed_net_ids, + key=lambda net_id: (-state.recent_attempt_work.get(net_id, 0), order_index[net_id]), + )[:2] + capped_net_ids = { + net_id for net_id in heavy_net_ids if state.recent_attempt_work.get(net_id, 0) >= 200 + } + return routed_net_ids, capped_net_ids + + def _update_pre_pair_candidate( + self, + state: _RoutingState, + *, + iteration: int, + reroute_net_ids: set[str], + routed_net_ids: list[str], + attempt_traces: list[IterationNetAttemptTrace], + review: _IterationReview, + ) -> None: + if self._results_all_reached_target(state) and len(reroute_net_ids) < len(state.ordered_net_ids) and review.conflict_edges: + state.pre_pair_candidate = _PrePairCandidate( + iteration=iteration, + routed_net_ids=tuple(routed_net_ids), + conflict_edges=tuple(sorted(review.conflict_edges)), + net_attempts=tuple(attempt_traces), + ) + return + state.pre_pair_candidate = None + + def _next_reroute_net_ids( + self, + state: _RoutingState, + review: _IterationReview, + ) -> set[str]: + if self._results_all_reached_target(state) and 0 < len(review.conflict_edges) <= 3: + return set(review.conflicting_nets) + return set(state.ordered_net_ids) + + def _should_stop_for_pair_local_plateau( + self, + state: _RoutingState, + *, + improved: bool, + ) -> bool: + if improved: + state.pair_local_plateau_count = 0 + return False + if self._results_all_reached_target(state) and state.best_conflict_edges <= 2: + # Once the run is fully reached-target and already in the final <=2-edge + # basin, another non-improving negotiated iteration is just churn before + # the bounded pair-local repair. + state.pair_local_plateau_count += 1 + return state.pair_local_plateau_count >= 1 + state.pair_local_plateau_count = 0 + return False + + def _update_repeated_conflict_state( + self, + state: _RoutingState, + review: _IterationReview, + ) -> bool: + current_signature = tuple(sorted(review.conflict_edges)) + repeated = ( + bool(current_signature) + and ( + current_signature == state.last_conflict_signature + or len(current_signature) == state.last_conflict_edge_count + ) + ) + state.repeated_conflict_count = state.repeated_conflict_count + 1 if repeated else 0 + state.last_conflict_signature = current_signature + state.last_conflict_edge_count = len(current_signature) + return state.repeated_conflict_count >= 2 + def _run_pair_local_attempt( self, state: _RoutingState, @@ -767,12 +924,7 @@ class PathFinder: for net_id in pair_order: net = state.net_specs[net_id] - guidance_result = incumbent_results.get(net_id) - guidance_seed = None - guidance_bonus = 0.0 - if guidance_result and guidance_result.reached_target and guidance_result.path: - guidance_seed = guidance_result.as_seed().segments - guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) + guidance_seed, guidance_bonus, _ = self._guidance_for_result(incumbent_results.get(net_id)) run_config = SearchRunConfig.from_options( self.context.options, @@ -811,6 +963,62 @@ class PathFinder: return local_results, local_context.metrics.total_nodes_expanded + def _apply_pair_local_candidate( + self, + state: _RoutingState, + candidate_results: dict[str, RoutingResult], + incumbent_results: dict[str, RoutingResult], + incumbent_review: _IterationReview, + ) -> tuple[bool, _IterationReview]: + self._replace_installed_paths(state, candidate_results) + candidate_results, _candidate_details_by_net, candidate_review = self._analyze_results( + state.ordered_net_ids, + candidate_results, + capture_component_conflicts=True, + count_iteration_metrics=False, + ) + if self._whole_set_is_better( + candidate_results, + candidate_review, + incumbent_results, + incumbent_review, + ): + self.metrics.total_pair_local_search_accepts += 1 + state.results = candidate_results + return True, candidate_review + + self._replace_installed_paths(state, incumbent_results) + return False, incumbent_review + + def _run_pair_local_target( + self, + state: _RoutingState, + target: _PairLocalTarget, + review: _IterationReview, + ) -> _IterationReview: + incumbent_results = dict(state.results) + incumbent_review = review + self.metrics.total_pair_local_search_pairs_considered += 1 + for pair_order in self._pair_local_attempt_orders(target): + self.metrics.total_pair_local_search_attempts += 1 + candidate = self._run_pair_local_attempt(state, incumbent_results, pair_order) + if candidate is None: + continue + candidate_results, nodes_expanded = candidate + self.metrics.total_pair_local_search_nodes_expanded += nodes_expanded + accepted, next_review = self._apply_pair_local_candidate( + state, + candidate_results, + incumbent_results, + incumbent_review, + ) + if accepted: + return next_review + + state.results = incumbent_results + self._replace_installed_paths(state, incumbent_results) + return incumbent_review + def _run_pair_local_search(self, state: _RoutingState) -> None: state.results, _details_by_net, review = self._analyze_results( state.ordered_net_ids, @@ -823,40 +1031,7 @@ class PathFinder: return for target in targets[:2]: - self.metrics.total_pair_local_search_pairs_considered += 1 - incumbent_results = dict(state.results) - incumbent_review = review - accepted = False - for pair_order in (target.net_ids, target.net_ids[::-1]): - self.metrics.total_pair_local_search_attempts += 1 - candidate = self._run_pair_local_attempt(state, incumbent_results, pair_order) - if candidate is None: - continue - candidate_results, nodes_expanded = candidate - self.metrics.total_pair_local_search_nodes_expanded += nodes_expanded - self._replace_installed_paths(state, candidate_results) - candidate_results, _candidate_details_by_net, candidate_review = self._analyze_results( - state.ordered_net_ids, - candidate_results, - capture_component_conflicts=True, - count_iteration_metrics=False, - ) - if self._whole_set_is_better( - candidate_results, - candidate_review, - incumbent_results, - incumbent_review, - ): - self.metrics.total_pair_local_search_accepts += 1 - state.results = candidate_results - review = candidate_review - accepted = True - break - self._replace_installed_paths(state, incumbent_results) - - if not accepted: - state.results = incumbent_results - self._replace_installed_paths(state, incumbent_results) + review = self._run_pair_local_target(state, target, review) def _route_net_once( self, @@ -883,28 +1058,18 @@ class PathFinder: else: coll_model, _ = resolve_bend_geometry(search) skip_congestion = False - guidance_seed = None - guidance_bonus = 0.0 + guidance_seed, guidance_bonus, guidance_seed_present = (None, 0.0, False) if congestion.use_tiered_strategy and iteration == 0: skip_congestion = True if coll_model == "arc": coll_model = "clipped_bbox" elif iteration > 0: - guidance_result = state.results.get(net_id) - if guidance_result and guidance_result.reached_target and guidance_result.path: - guidance_seed = guidance_result.as_seed().segments - guidance_bonus = max(10.0, self.context.options.objective.bend_penalty * 0.25) - guidance_seed_present = True + guidance_seed, guidance_bonus, guidance_seed_present = self._guidance_for_result( + state.results.get(net_id) + ) - 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 + if node_limit_override is not None and self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) run_config = SearchRunConfig.from_options( self.context.options, @@ -931,17 +1096,13 @@ 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 + if self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) return RoutingResult(net_id=net_id, path=(), reached_target=False), guidance_seed_present reached_target = path[-1].end_port == net.target - if not reached_target and 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 not reached_target and self._has_incumbent_fallback(incumbent_fallback): + return self._restore_incumbent_fallback(net_id, incumbent_fallback, guidance_seed_present) if reached_target: self.metrics.total_nets_reached_target += 1 report = None @@ -977,24 +1138,7 @@ class PathFinder: random.Random(iteration_seed).shuffle(state.ordered_net_ids) 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 - } + routed_net_ids, capped_net_ids = self._build_iteration_reroute_plan(state, reroute_net_ids) self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids) iteration_before = {} attempt_traces: list[IterationNetAttemptTrace] = [] @@ -1039,19 +1183,14 @@ class PathFinder: 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()) + self._update_pre_pair_candidate( + state, + iteration=iteration, + reroute_net_ids=reroute_net_ids, + routed_net_ids=routed_net_ids, + attempt_traces=attempt_traces, + review=review, ) - if 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) @@ -1115,38 +1254,11 @@ class PathFinder: ): return False - all_reached_target = ( - len(state.results) == len(state.ordered_net_ids) - and all(result.reached_target for result in state.results.values()) - ) - reroute_net_ids = set(state.ordered_net_ids) - if all_reached_target and 0 < len(review.conflict_edges) <= 3: - reroute_net_ids = set(review.conflicting_nets) - if improved: - state.pair_local_plateau_count = 0 - elif all_reached_target and state.best_conflict_edges <= 2: - # Once all nets reach target and the best snapshot is already in the - # final <=2-edge basin, later negotiated reroutes tend to churn. - # Hand off to the bounded pair-local repair instead of exploring - # additional late iterations that are not improving the best state. - state.pair_local_plateau_count += 1 - if state.pair_local_plateau_count >= 1: - return False - else: - state.pair_local_plateau_count = 0 + reroute_net_ids = self._next_reroute_net_ids(state, review) + if self._should_stop_for_pair_local_plateau(state, improved=improved): + return False - current_signature = tuple(sorted(review.conflict_edges)) - repeated = ( - bool(current_signature) - and ( - current_signature == state.last_conflict_signature - or len(current_signature) == state.last_conflict_edge_count - ) - ) - state.repeated_conflict_count = state.repeated_conflict_count + 1 if repeated else 0 - state.last_conflict_signature = current_signature - state.last_conflict_edge_count = len(current_signature) - if state.repeated_conflict_count >= 2: + if self._update_repeated_conflict_state(state, review): return False self.context.congestion_penalty *= congestion.multiplier return False @@ -1228,32 +1340,7 @@ 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: - self._capture_conflict_trace_entry( - state, - stage="restored_best", - iteration=None, - results=state.results, - details_by_net=details_by_net, - review=review, - ) - if self.context.options.diagnostics.capture_pre_pair_frontier_trace: - self.pre_pair_frontier_trace = self._materialize_pre_pair_frontier_trace( - state, - state.results, - details_by_net, - review, - ) + self._analyze_restored_best(state) if timed_out: final_results = self._verify_results(state)