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

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

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`.
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:

View file

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

View file

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

View file

@ -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),

View file

@ -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"))

View file

@ -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 = {