Limit late reroutes to conflicting nets

This commit is contained in:
Jan Petykiewicz 2026-04-02 17:10:00 -07:00
commit 2c3aa90544
9 changed files with 377 additions and 1003 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# Iteration Trace # 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 ## 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 | | 1 | 140.0 | 10 | 2 | 12 | 54 | 253 | 974 | 2378 | 1998 |
| 2 | 196.0 | 10 | 4 | 5 | 22 | 253 | 993 | 1928 | 1571 | | 2 | 196.0 | 10 | 4 | 5 | 22 | 253 | 993 | 1928 | 1571 |
| 3 | 274.4 | 10 | 6 | 2 | 10 | 100 | 437 | 852 | 698 | | 3 | 274.4 | 10 | 6 | 2 | 10 | 100 | 437 | 852 | 698 |
| 4 | 384.2 | 10 | 6 | 2 | 10 | 126 | 517 | 961 | 812 | | 4 | 384.2 | 4 | 6 | 2 | 10 | 81 | 332 | 627 | 513 |
| 5 | 537.8 | 10 | 6 | 2 | 10 | 461 | 1704 | 3805 | 3043 |
Top nets by iteration-attributed nodes expanded: Top nets by iteration-attributed nodes expanded:
- `net_03`: 383 - `net_09`: 242
- `net_06`: 292 - `net_00`: 201
- `net_09`: 260 - `net_02`: 157
- `net_00`: 210 - `net_06`: 155
- `net_02`: 190 - `net_01`: 147
- `net_08`: 168 - `net_08`: 144
- `net_01`: 162 - `net_03`: 141
- `net_07`: 61 - `net_07`: 45
- `net_04`: 19 - `net_04`: 13
- `net_05`: 19 - `net_05`: 13
Top nets by iteration-attributed congestion checks: Top nets by iteration-attributed congestion checks:
- `net_03`: 1242 - `net_06`: 569
- `net_06`: 1080 - `net_02`: 514
- `net_02`: 674 - `net_01`: 468
- `net_01`: 534 - `net_03`: 425
- `net_08`: 262 - `net_00`: 203
- `net_00`: 229 - `net_08`: 170
- `net_07`: 228 - `net_07`: 143
- `net_09`: 176 - `net_09`: 124
- `net_04`: 100 - `net_04`: 60
- `net_05`: 100 - `net_05`: 60
## example_07_large_scale_routing_no_warm_start_seed43 ## 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 | | 0 | 100.0 | 10 | 1 | 16 | 50 | 571 | 0 | 0 | 0 |
| 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 | | 1 | 140.0 | 10 | 1 | 13 | 53 | 269 | 961 | 2562 | 2032 |
| 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 | | 2 | 196.0 | 10 | 4 | 3 | 15 | 140 | 643 | 1610 | 1224 |
| 3 | 274.4 | 10 | 4 | 3 | 15 | 84 | 382 | 801 | 651 | | 3 | 274.4 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 |
| 4 | 384.2 | 10 | 6 | 2 | 10 | 170 | 673 | 1334 | 1072 | | 4 | 384.2 | 6 | 6 | 2 | 10 | 142 | 550 | 1126 | 884 |
| 5 | 537.8 | 10 | 6 | 2 | 10 | 457 | 1671 | 3718 | 2992 | | 5 | 537.8 | 4 | 6 | 2 | 10 | 406 | 1477 | 3377 | 2666 |
| 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: Top nets by iteration-attributed nodes expanded:
- `net_06`: 31604 - `net_03`: 435
- `net_03`: 27532 - `net_09`: 250
- `net_02`: 763 - `net_06`: 242
- `net_09`: 286 - `net_00`: 177
- `net_07`: 239 - `net_08`: 172
- `net_00`: 233 - `net_07`: 140
- `net_08`: 218 - `net_02`: 79
- `net_05`: 134 - `net_01`: 65
- `net_01`: 132 - `net_05`: 12
- `net_04`: 118 - `net_04`: 10
Top nets by iteration-attributed congestion checks: Top nets by iteration-attributed congestion checks:
- `net_06`: 83752 - `net_03`: 1434
- `net_03`: 75019 - `net_06`: 893
- `net_02`: 3270 - `net_07`: 454
- `net_07`: 844 - `net_08`: 328
- `net_08`: 540 - `net_02`: 290
- `net_01`: 441 - `net_09`: 178
- `net_05`: 425 - `net_01`: 135
- `net_04`: 398 - `net_00`: 82
- `net_09`: 288 - `net_05`: 47
- `net_00`: 246 - `net_04`: 40

View file

@ -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 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. - 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. - 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.

View file

@ -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`. The full machine-readable snapshot lives in `docs/performance_baseline.json`.
Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot. Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.
The default baseline table below covers the standard example corpus only. The heavier `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. 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`.
The current tracked sweep output lives 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`.
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 | | 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_01_simple_route | 0.0038 | 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_02_congestion_resolution | 0.3614 | 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_03_locked_paths | 0.1953 | 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_04_sbends_and_radii | 0.0277 | 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_05_orientation_stress | 0.2537 | 3 | 3 | 3 | 2 | 5 | 297 | 1274 | 1680 | 689 | 0 | 0 | 146 | 17 |
| example_06_bend_collision_models | 0.1998 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 12 | | 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.2005 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 40 | | 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.0176 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 8 | | 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.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 | | example_09_unroutable_best_effort | 0.0079 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
## Full Counter Set ## Full Counter Set
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters. Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.
These counters are currently observational only and are not enforced as CI regression gates. These counters are currently observational only and are not enforced as CI regression gates.
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: 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.
- `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`.
Tracked metric keys: Tracked metric keys:

View file

@ -3,7 +3,7 @@
"generator": "scripts/record_performance_baseline.py", "generator": "scripts/record_performance_baseline.py",
"scenarios": [ "scenarios": [
{ {
"duration_s": 0.003964120987802744, "duration_s": 0.003825429128482938,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -85,7 +85,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 11, "score_component_calls": 11,
"score_component_total_ns": 18064, "score_component_total_ns": 16571,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
@ -115,7 +115,7 @@
"valid_results": 1 "valid_results": 1
}, },
{ {
"duration_s": 0.3377689190674573, "duration_s": 0.36141274496912956,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -197,7 +197,7 @@
"refinement_windows_considered": 10, "refinement_windows_considered": 10,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 976, "score_component_calls": 976,
"score_component_total_ns": 1140704, "score_component_total_ns": 1143187,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
@ -227,7 +227,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.1929313091095537, "duration_s": 0.19532882701605558,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -309,7 +309,7 @@
"refinement_windows_considered": 2, "refinement_windows_considered": 2,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 504, "score_component_calls": 504,
"score_component_total_ns": 565410, "score_component_total_ns": 565663,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
@ -339,7 +339,7 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.02791503700427711, "duration_s": 0.027705274987965822,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -421,7 +421,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 90, "score_component_calls": 90,
"score_component_total_ns": 100083, "score_component_total_ns": 96756,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
@ -451,73 +451,73 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.23665715800598264, "duration_s": 0.25367443496361375,
"metrics": { "metrics": {
"congestion_cache_hits": 4, "congestion_cache_hits": 3,
"congestion_cache_misses": 149, "congestion_cache_misses": 146,
"congestion_candidate_ids": 32, "congestion_candidate_ids": 32,
"congestion_candidate_nets": 23, "congestion_candidate_nets": 23,
"congestion_candidate_precheck_hits": 131, "congestion_candidate_precheck_hits": 129,
"congestion_candidate_precheck_misses": 22, "congestion_candidate_precheck_misses": 20,
"congestion_candidate_precheck_skips": 0, "congestion_candidate_precheck_skips": 0,
"congestion_check_calls": 149, "congestion_check_calls": 146,
"congestion_exact_pair_checks": 30, "congestion_exact_pair_checks": 30,
"congestion_grid_net_cache_hits": 16, "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_hits": 15,
"congestion_grid_span_cache_misses": 7, "congestion_grid_span_cache_misses": 7,
"congestion_lazy_requeues": 0, "congestion_lazy_requeues": 0,
"congestion_lazy_resolutions": 0, "congestion_lazy_resolutions": 0,
"congestion_net_envelope_cache_hits": 128, "congestion_net_envelope_cache_hits": 127,
"congestion_net_envelope_cache_misses": 43, "congestion_net_envelope_cache_misses": 39,
"congestion_presence_cache_hits": 200, "congestion_presence_cache_hits": 196,
"congestion_presence_cache_misses": 30, "congestion_presence_cache_misses": 27,
"congestion_presence_skips": 77, "congestion_presence_skips": 74,
"danger_map_cache_hits": 0, "danger_map_cache_hits": 0,
"danger_map_cache_misses": 0, "danger_map_cache_misses": 0,
"danger_map_lookup_calls": 0, "danger_map_lookup_calls": 0,
"danger_map_query_calls": 0, "danger_map_query_calls": 0,
"danger_map_total_ns": 0, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 49, "dynamic_path_objects_added": 48,
"dynamic_path_objects_removed": 37, "dynamic_path_objects_removed": 36,
"dynamic_tree_rebuilds": 0, "dynamic_tree_rebuilds": 0,
"guidance_bonus_applied": 687.5, "guidance_bonus_applied": 562.5,
"guidance_bonus_applied_bend90": 500.0, "guidance_bonus_applied_bend90": 500.0,
"guidance_bonus_applied_sbend": 0.0, "guidance_bonus_applied_sbend": 0.0,
"guidance_bonus_applied_straight": 187.5, "guidance_bonus_applied_straight": 62.5,
"guidance_match_moves": 11, "guidance_match_moves": 9,
"guidance_match_moves_bend90": 8, "guidance_match_moves_bend90": 8,
"guidance_match_moves_sbend": 0, "guidance_match_moves_sbend": 0,
"guidance_match_moves_straight": 3, "guidance_match_moves_straight": 1,
"hard_collision_cache_hits": 0, "hard_collision_cache_hits": 0,
"iteration_conflict_edges": 1, "iteration_conflict_edges": 1,
"iteration_conflicting_nets": 2, "iteration_conflicting_nets": 2,
"iteration_reverified_nets": 6, "iteration_reverified_nets": 6,
"iteration_reverify_calls": 2, "iteration_reverify_calls": 2,
"move_cache_abs_hits": 385, "move_cache_abs_hits": 374,
"move_cache_abs_misses": 1306, "move_cache_abs_misses": 1306,
"move_cache_rel_hits": 1204, "move_cache_rel_hits": 1204,
"move_cache_rel_misses": 102, "move_cache_rel_misses": 102,
"moves_added": 696, "moves_added": 689,
"moves_generated": 1691, "moves_generated": 1680,
"nets_carried_forward": 0, "nets_carried_forward": 1,
"nets_reached_target": 6, "nets_reached_target": 5,
"nets_routed": 6, "nets_routed": 5,
"nodes_expanded": 299, "nodes_expanded": 297,
"pair_local_search_accepts": 0, "pair_local_search_accepts": 0,
"pair_local_search_attempts": 0, "pair_local_search_attempts": 0,
"pair_local_search_nodes_expanded": 0, "pair_local_search_nodes_expanded": 0,
"pair_local_search_pairs_considered": 0, "pair_local_search_pairs_considered": 0,
"path_cost_calls": 2, "path_cost_calls": 2,
"pruned_closed_set": 159, "pruned_closed_set": 159,
"pruned_cost": 537, "pruned_cost": 533,
"pruned_hard_collision": 14, "pruned_hard_collision": 14,
"ray_cast_calls": 1284, "ray_cast_calls": 1274,
"ray_cast_calls_expand_forward": 293, "ray_cast_calls_expand_forward": 292,
"ray_cast_calls_expand_snap": 3, "ray_cast_calls_expand_snap": 2,
"ray_cast_calls_other": 0, "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_build": 0,
"ray_cast_calls_visibility_query": 0, "ray_cast_calls_visibility_query": 0,
"ray_cast_calls_visibility_tangent": 9, "ray_cast_calls_visibility_tangent": 9,
@ -532,16 +532,16 @@
"refinement_static_bounds_checked": 0, "refinement_static_bounds_checked": 0,
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 1245, "score_component_calls": 1234,
"score_component_total_ns": 1260961, "score_component_total_ns": 1311211,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 9, "static_safe_cache_hits": 8,
"static_tree_rebuilds": 1, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 8, "verify_dynamic_candidate_nets": 8,
"verify_dynamic_exact_pair_checks": 12, "verify_dynamic_exact_pair_checks": 12,
"verify_path_report_calls": 18, "verify_path_report_calls": 17,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
@ -553,7 +553,7 @@
"visibility_point_queries": 0, "visibility_point_queries": 0,
"visibility_tangent_candidate_corner_checks": 70, "visibility_tangent_candidate_corner_checks": 70,
"visibility_tangent_candidate_ray_tests": 9, "visibility_tangent_candidate_ray_tests": 9,
"visibility_tangent_candidate_scans": 293, "visibility_tangent_candidate_scans": 292,
"warm_start_paths_built": 2, "warm_start_paths_built": 2,
"warm_start_paths_used": 2 "warm_start_paths_used": 2
}, },
@ -563,7 +563,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.19982667709700763, "duration_s": 0.21031348290853202,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -589,7 +589,7 @@
"danger_map_cache_misses": 731, "danger_map_cache_misses": 731,
"danger_map_lookup_calls": 1914, "danger_map_lookup_calls": 1914,
"danger_map_query_calls": 731, "danger_map_query_calls": 731,
"danger_map_total_ns": 18959782, "danger_map_total_ns": 19983976,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 54, "dynamic_path_objects_added": 54,
"dynamic_path_objects_removed": 36, "dynamic_path_objects_removed": 36,
@ -645,7 +645,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 3, "route_iterations": 3,
"score_component_calls": 842, "score_component_calls": 842,
"score_component_total_ns": 21338709, "score_component_total_ns": 22474166,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3,
"static_safe_cache_hits": 141, "static_safe_cache_hits": 141,
@ -675,7 +675,7 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.20046633295714855, "duration_s": 0.20740868314169347,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -701,7 +701,7 @@
"danger_map_cache_misses": 448, "danger_map_cache_misses": 448,
"danger_map_lookup_calls": 681, "danger_map_lookup_calls": 681,
"danger_map_query_calls": 448, "danger_map_query_calls": 448,
"danger_map_total_ns": 11017087, "danger_map_total_ns": 11224403,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 132, "dynamic_path_objects_added": 132,
"dynamic_path_objects_removed": 88, "dynamic_path_objects_removed": 88,
@ -757,7 +757,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 291, "score_component_calls": 291,
"score_component_total_ns": 11869917, "score_component_total_ns": 12117666,
"static_net_tree_rebuilds": 10, "static_net_tree_rebuilds": 10,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 6, "static_safe_cache_hits": 6,
@ -787,7 +787,7 @@
"valid_results": 10 "valid_results": 10
}, },
{ {
"duration_s": 0.01759456400759518, "duration_s": 0.018604618962854147,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -869,7 +869,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 72, "score_component_calls": 72,
"score_component_total_ns": 85864, "score_component_total_ns": 87655,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 2, "static_safe_cache_hits": 2,
@ -899,7 +899,7 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.005838233977556229, "duration_s": 0.00794802000746131,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 0, "congestion_cache_misses": 0,
@ -925,7 +925,7 @@
"danger_map_cache_misses": 20, "danger_map_cache_misses": 20,
"danger_map_lookup_calls": 30, "danger_map_lookup_calls": 30,
"danger_map_query_calls": 20, "danger_map_query_calls": 20,
"danger_map_total_ns": 523870, "danger_map_total_ns": 675454,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 2, "dynamic_path_objects_added": 2,
"dynamic_path_objects_removed": 1, "dynamic_path_objects_removed": 1,
@ -981,7 +981,7 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 14, "score_component_calls": 14,
"score_component_total_ns": 563611, "score_component_total_ns": 722637,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0, "static_safe_cache_hits": 0,

View file

@ -52,6 +52,7 @@ class _RoutingState:
last_conflict_signature: tuple[tuple[str, str], ...] last_conflict_signature: tuple[tuple[str, str], ...]
last_conflict_edge_count: int last_conflict_edge_count: int
repeated_conflict_count: int repeated_conflict_count: int
pair_local_plateau_count: int
@dataclass(slots=True) @dataclass(slots=True)
@ -217,6 +218,7 @@ class PathFinder:
last_conflict_signature=(), last_conflict_signature=(),
last_conflict_edge_count=0, last_conflict_edge_count=0,
repeated_conflict_count=0, repeated_conflict_count=0,
pair_local_plateau_count=0,
) )
if state.initial_paths is None and congestion.warm_start_enabled: if state.initial_paths is None and congestion.warm_start_enabled:
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order) state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
@ -949,22 +951,43 @@ class PathFinder:
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
) -> bool: ) -> bool:
congestion = self.context.options.congestion congestion = self.context.options.congestion
reroute_net_ids = set(state.ordered_net_ids)
for iteration in range(congestion.max_iterations): for iteration in range(congestion.max_iterations):
review = self._run_iteration( review = self._run_iteration(
state, state,
iteration, iteration,
set(state.ordered_net_ids), reroute_net_ids,
iteration_callback, iteration_callback,
) )
if review is None: if review is None:
return True return True
self._update_best_iteration(state, review) improved = self._update_best_iteration(state, review)
if not any( if not any(
result.outcome in {"colliding", "partial", "unroutable"} result.outcome in {"colliding", "partial", "unroutable"}
for result in state.results.values() for result in state.results.values()
): ):
return False 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)) current_signature = tuple(sorted(review.conflict_edges))
repeated = ( repeated = (
bool(current_signature) bool(current_signature)

View file

@ -352,6 +352,68 @@ def test_reverify_iterations_stop_early_on_stalled_conflict_graph() -> None:
assert run.metrics.route_iterations < 10 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: def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:
problem = RoutingProblem( problem = RoutingProblem(
bounds=(0, 0, 100, 100), bounds=(0, 0, 100, 100),

View file

@ -288,6 +288,7 @@ def test_pair_local_context_clones_live_static_obstacles() -> None:
last_conflict_signature=(), last_conflict_signature=(),
last_conflict_edge_count=0, last_conflict_edge_count=0,
repeated_conflict_count=0, repeated_conflict_count=0,
pair_local_plateau_count=0,
) )
local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b")) local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b"))

View file

@ -22,7 +22,7 @@ RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
PERFORMANCE_REPEATS = 3 PERFORMANCE_REPEATS = 3
REGRESSION_FACTOR = 1.5 REGRESSION_FACTOR = 1.5
NO_WARM_START_REGRESSION_SECONDS = 15.0 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. # Baselines are measured from clean 6a28dcf-style runs without plotting.
BASELINE_SECONDS = { BASELINE_SECONDS = {