Compare commits

...

4 commits

21 changed files with 4193 additions and 266 deletions

61
DOCS.md
View file

@ -130,6 +130,8 @@ 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. |
| `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
@ -186,7 +188,60 @@ 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. 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`.
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.
## 11. RouteMetrics
`RoutingRunResult.metrics` is an immutable per-run snapshot.
@ -266,13 +321,15 @@ Separately from the observational trace tooling, the router may run a bounded po
- `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, 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

1395
docs/iteration_trace.json Normal file

File diff suppressed because it is too large Load diff

81
docs/iteration_trace.md Normal file
View file

@ -0,0 +1,81 @@
# Iteration Trace
Generated at 2026-04-02T18:51:01-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 | 4 | 6 | 2 | 10 | 81 | 332 | 627 | 513 |
Top nets by iteration-attributed nodes expanded:
- `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_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
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 | 6 | 4 | 3 | 15 | 54 | 250 | 557 | 428 |
| 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_09`: 250
- `net_03`: 199
- `net_00`: 177
- `net_08`: 166
- `net_07`: 140
- `net_06`: 105
- `net_02`: 79
- `net_01`: 65
- `net_05`: 12
- `net_04`: 10
Top nets by iteration-attributed congestion checks:
- `net_03`: 639
- `net_07`: 454
- `net_06`: 382
- `net_02`: 290
- `net_08`: 283
- `net_09`: 178
- `net_01`: 135
- `net_00`: 82
- `net_05`: 47
- `net_04`: 40

View file

@ -3629,3 +3629,113 @@ 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.
## 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.
## Step 67 route lighter late conflict nets first
Measured on 2026-04-02T17:16:54-07:00.
Findings:
- Kept the late all-reached conflict-set reroute, but now order those subset reroutes by the previous iteration's attributed work ascending so the lighter partner nets settle first and the heavier nets route later against a more stable late-phase context.
- The solved seed-42 no-warm canary stayed effectively flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks.
- The seed-43 no-warm canary stayed `10/10/10` and improved slightly from `1582` nodes / `3881` congestion checks to `1576` nodes / `3836` congestion checks.
- The remaining late-phase hotspot is still concentrated in `net_03` and `net_06`, especially the final 4-net iteration in the seed-43 trace.
## Step 68 pre-pair frontier diagnostics kept, scratch reroute rejected
Measured on 2026-04-02T17:57:48-07:00.
Findings:
- Kept a new opt-in `capture_pre_pair_frontier_trace` surface plus `scripts/record_pre_pair_frontier_trace.py`, and tracked the first seed-42 vs seed-43 artifacts in `docs/pre_pair_frontier_trace.json` and `docs/pre_pair_frontier_trace.md`.
- The final unresolved subset iteration is now explicit: seed `42` captures iteration `4` with routed nets `net_07`, `net_06`, `net_00`, `net_01`; seed `43` captures iteration `5` with routed nets `net_07`, `net_02`, `net_06`, `net_03`.
- The seed-43 heavy-net concentration is confirmed by the new trace: `net_03` and `net_06` account for most of the last unresolved iteration's work, and the hotspot-adjacent sampled prunes in that basin are closed-set dominated rather than hard-collision dominated.
- I also measured a bounded pre-pair scratch reroute for the two heaviest traced nets, but rejected it: it added runtime, produced `0` accepted repairs, and left the solved canaries at the same `1258 / 2736` and `1576 / 3836` node/check totals after revert.
## Step 69 cap heavy late-phase reroutes with incumbent fallback
Measured on 2026-04-02T18:20:00-07:00.
Findings:
- In the final all-reached 4-net subset iteration, the router now caps only the heavy reroute endpoints whose previous-iteration attributed work is already pathological, and falls back to their incumbent reached-target paths if the capped reroute does not finish cleanly.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves from `1576` nodes / `3836` congestion checks to `1459` nodes / `3455` congestion checks, with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity.
## Step 70 tighten late-phase cap from 128 to 64
Measured on 2026-04-02T18:33:00-07:00.
Findings:
- Tightened the bounded heavy-net late-phase reroute cap from `128` nodes to `64`, keeping the same incumbent fallback behavior and the same heavy-net selection rule.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1459` nodes / `3455` congestion checks to `1331` nodes / `3012` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
- Guardrails held: warmed `example_07` stayed `10/10/10`, and `example_05_orientation_stress` stayed `3/3/3` with no late-phase capping activity.
## Step 71 tighten late-phase cap from 64 to 32 after cap sweep
Measured on 2026-04-02T18:43:00-07:00.
Findings:
- Ran a cap sweep across `32`, `48`, `64`, `96`, `128`, and uncapped behavior for the two no-warm seeds. The winner was `32`: it preserved both `10/10/10` canaries and gave the best seed-43 node/check totals while leaving seed-42 flat.
- Landed that tighter cap in the router.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1331` nodes / `3012` congestion checks to `1267` nodes / `2813` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
## Step 72 tighten late-phase cap from 32 to 1 after floor sweep
Measured on 2026-04-02T18:45:00-07:00.
Findings:
- Extended the cap sweep below `32` and found the same pattern continued all the way down to `1`: seed-42 stayed flat because the cap never fires there, while seed-43 kept getting cheaper and still converged through the same incumbent-fallback path.
- Landed the tightest safe setting, `1`, so late pathological reroutes now act as a minimal probe before immediately falling back to the incumbent reached-target path if they do not finish cleanly.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1267` nodes / `2813` congestion checks to `1205` nodes / `2548` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.
## Step 73 skip capped late-phase reroutes and carry the incumbent directly
Measured on 2026-04-02T18:52:00-07:00.
Findings:
- Characterization showed the two capped late seed-43 reroutes were pure churn even at a `1`-node cap: they always fell back to the incumbent reached-target path and pair-local repair still resolved the final pairs.
- Moved that behavior into `_route_net_once()` directly: when a late heavy reroute is already capped and has a reached-target incumbent fallback, the router now reinstalls the incumbent immediately instead of calling `route_astar()` for a doomed probe.
- The solved seed-42 no-warm canary stays flat at `10/10/10`, `44` routed nets, `1258` nodes, and `2736` congestion checks, with `late_phase_capped_nets=0`.
- The heavier seed-43 no-warm canary stays `10/10/10` and improves again from `1205` nodes / `2548` congestion checks to `1203` nodes / `2530` congestion checks, still with `late_phase_capped_nets=2` and `late_phase_capped_fallbacks=2`.

View file

@ -5,37 +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 `example_07_large_scale_routing_no_warm_start` canary remains performance-only and is tracked through targeted diffs plus the conflict/frontier 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`.
| 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.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.
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.
Tracked metric keys:
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded, late_phase_capped_nets, late_phase_capped_fallbacks

View file

@ -3,7 +3,7 @@
"generator": "scripts/record_performance_baseline.py",
"scenarios": [
{
"duration_s": 0.003964120987802744,
"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": 18064,
"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.3377689190674573,
"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": 1140704,
"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.1929313091095537,
"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": 565410,
"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.02791503700427711,
"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": 100083,
"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,73 +459,75 @@
"valid_results": 2
},
{
"duration_s": 0.23665715800598264,
"duration_s": 0.23108969815075397,
"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,
"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,
"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 +542,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": 1223569,
"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": 20,
"verify_static_buffer_ops": 0,
"visibility_builds": 0,
"visibility_corner_hits_exact": 0,
@ -553,7 +563,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 +573,7 @@
"valid_results": 3
},
{
"duration_s": 0.19982667709700763,
"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": 18959782,
"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": 21338709,
"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.20046633295714855,
"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": 11017087,
"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": 11869917,
"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.01759456400759518,
"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": 85864,
"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.005838233977556229,
"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": 523870,
"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": 563611,
"score_component_total_ns": 554809,
"static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
# Pre-Pair Frontier Trace
Generated at 2026-04-02T18:51:01-07:00 by `scripts/record_pre_pair_frontier_trace.py`.
## example_07_large_scale_routing_no_warm_start
Results: 10 valid / 10 reached / 10 total.
Captured iteration: `4`
Conflict edges: `(('net_01', 'net_02'), ('net_06', 'net_07'))`
| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |
| :-- | --: | --: | --: | --: | --: | :--: | --: |
| net_07 | 7 | 30 | 1 | 0 | 0 | yes | 0 |
| net_06 | 46 | 179 | 7 | 15 | 0 | yes | 5 |
| net_00 | 10 | 43 | 1 | 0 | 0 | yes | 0 |
| net_01 | 18 | 80 | 3 | 0 | 0 | yes | 0 |
Frontier prune totals by reason:
- `closed_set`: 5
- `hard_collision`: 0
- `self_collision`: 0
- `cost`: 0
## example_07_large_scale_routing_no_warm_start_seed43
Results: 10 valid / 10 reached / 10 total.
Captured iteration: `5`
Conflict edges: `(('net_02', 'net_03'), ('net_06', 'net_07'))`
| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |
| :-- | --: | --: | --: | --: | --: | :--: | --: |
| net_07 | 16 | 85 | 3 | 0 | 0 | yes | 2 |
| net_02 | 17 | 86 | 4 | 0 | 0 | yes | 3 |
| net_06 | 0 | 0 | 0 | 0 | 0 | yes | 8 |
| net_03 | 0 | 0 | 0 | 0 | 0 | yes | 12 |
Frontier prune totals by reason:
- `closed_set`: 25
- `hard_collision`: 0
- `self_collision`: 0
- `cost`: 0

View file

@ -18,8 +18,12 @@ 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,
PrePairFrontierTraceEntry as PrePairFrontierTraceEntry,
PrePairNetTrace as PrePairNetTrace,
RoutingResult as RoutingResult,
RoutingRunResult as RoutingRunResult,
)
@ -47,6 +51,8 @@ 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),
)
__all__ = [
@ -62,6 +68,10 @@ __all__ = [
"PathSeed",
"Port",
"FrontierPruneSample",
"IterationNetAttemptTrace",
"IterationTraceEntry",
"PrePairFrontierTraceEntry",
"PrePairNetTrace",
"RefinementOptions",
"RoutingOptions",
"RoutingProblem",

View file

@ -107,6 +107,8 @@ class DiagnosticsOptions:
capture_expanded: bool = False
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)

View file

@ -78,6 +78,53 @@ 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
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
@ -183,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)
@ -231,3 +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, ...] = ()

View file

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

View file

@ -11,8 +11,12 @@ from inire.results import (
ComponentConflictTrace,
ConflictTraceEntry,
FrontierPruneSample,
IterationNetAttemptTrace,
IterationTraceEntry,
NetConflictTrace,
NetFrontierTrace,
PrePairFrontierTraceEntry,
PrePairNetTrace,
RoutingOutcome,
RoutingReport,
RoutingResult,
@ -50,6 +54,9 @@ class _RoutingState:
last_conflict_signature: tuple[tuple[str, str], ...]
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)
@ -65,6 +72,30 @@ 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",
"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 +104,8 @@ class PathFinder:
"accumulated_expanded_nodes",
"conflict_trace",
"frontier_trace",
"pre_pair_frontier_trace",
"iteration_trace",
)
def __init__(
@ -90,6 +123,48 @@ 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:
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 _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] = []
@ -188,6 +263,9 @@ class PathFinder:
last_conflict_signature=(),
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)
@ -225,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)
@ -368,6 +478,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],
@ -446,90 +643,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,
@ -565,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,
@ -642,6 +778,141 @@ 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 _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,
@ -653,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,
@ -697,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,
@ -709,53 +1031,26 @@ 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,
state: _RoutingState,
iteration: int,
net_id: str,
) -> RoutingResult:
*,
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
if iteration == 0 and state.initial_paths and net_id in state.initial_paths:
self.metrics.total_warm_start_paths_used += 1
@ -763,17 +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, guidance_bonus, guidance_seed_present = self._guidance_for_result(
state.results.get(net_id)
)
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,
@ -784,7 +1080,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,
@ -800,9 +1096,13 @@ 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)
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 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
@ -812,11 +1112,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 +1130,85 @@ 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)
routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids]
iteration_penalty = self.context.congestion_penalty
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] = []
attempt_work: dict[str, int] = {}
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 = 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
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)
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 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)
@ -873,34 +1237,28 @@ 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
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:
reroute_net_ids = self._next_reroute_net_ids(state, review)
if self._should_stop_for_pair_local_plateau(state, improved=improved):
return False
if self._update_repeated_conflict_state(state, review):
return False
self.context.congestion_penalty *= congestion.multiplier
return False
@ -973,6 +1331,8 @@ 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()
@ -980,21 +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)
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",
iteration=None,
results=state.results,
details_by_net=details_by_net,
review=review,
)
self._analyze_restored_best(state)
if timed_out:
final_results = self._verify_results(state)

View file

@ -91,6 +91,8 @@ 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),
)
@ -424,6 +426,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 +442,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 +453,8 @@ 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,
capture_pre_pair_frontier_trace: bool = False,
) -> tuple[CostEvaluator, AStarMetrics, PathFinder]:
bounds = (0, 0, 1000, 1000)
obstacles = [
@ -481,6 +497,8 @@ 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,
"capture_pre_pair_frontier_trace": capture_pre_pair_frontier_trace,
"shuffle_nets": True,
"seed": seed,
"warm_start_enabled": warm_start_enabled,
@ -496,6 +514,8 @@ def _run_example_07_variant(
warm_start_enabled: bool,
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,
@ -503,6 +523,8 @@ 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,
capture_pre_pair_frontier_trace=capture_pre_pair_frontier_trace,
)
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
@ -519,11 +541,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 +556,16 @@ 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,
capture_pre_pair_frontier_trace=True,
)
@ -644,6 +670,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 +680,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),
)

View file

@ -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,8 @@ 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 == ()
def test_route_problem_supports_configs_and_debug_data() -> None:
@ -124,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:
@ -182,6 +187,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),
@ -239,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),
@ -285,6 +385,220 @@ 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"].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:
problem = RoutingProblem(
bounds=(0, 0, 100, 100),

View file

@ -288,6 +288,9 @@ 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,
recent_attempt_work={},
pre_pair_candidate=None,
)
local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b"))

View file

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

View file

@ -75,6 +75,7 @@ def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> Non
assert sum(result.reached_target for result in run.results_by_net.values()) == 10
assert 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"

View file

@ -48,6 +48,8 @@ def test_snapshot_example_01_exposes_metrics() -> None:
assert snapshot.metrics.pair_local_search_attempts >= 0
assert snapshot.metrics.pair_local_search_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:
@ -276,6 +278,55 @@ 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_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"

View file

@ -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 <repo>/docs.",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1]
output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
output_dir.mkdir(exist_ok=True)
selected = tuple(args.scenarios) if args.scenarios else None
payload = _build_payload(selected, include_performance_only=args.include_performance_only)
json_path = output_dir / "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()

View file

@ -0,0 +1,191 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from collections import Counter
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from inire.tests.example_scenarios import TRACE_PERFORMANCE_SCENARIO_RUNS, TRACE_SCENARIO_RUNS
def _trace_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]:
if include_performance_only:
return TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS
return TRACE_SCENARIO_RUNS
def _selected_runs(
selected_scenarios: tuple[str, ...] | None,
*,
include_performance_only: bool,
) -> tuple[tuple[str, object], ...]:
if selected_scenarios is None:
perf_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS)
return (
(
"example_07_large_scale_routing_no_warm_start",
perf_registry["example_07_large_scale_routing_no_warm_start"],
),
(
"example_07_large_scale_routing_no_warm_start_seed43",
perf_registry["example_07_large_scale_routing_no_warm_start_seed43"],
),
)
registry = dict(TRACE_SCENARIO_RUNS + TRACE_PERFORMANCE_SCENARIO_RUNS)
allowed_standard = dict(_trace_registry(include_performance_only))
runs = []
for name in selected_scenarios:
if name in allowed_standard:
runs.append((name, allowed_standard[name]))
continue
if name in registry:
runs.append((name, registry[name]))
continue
valid = ", ".join(sorted(registry))
raise SystemExit(f"Unknown pre-pair frontier-trace scenario: {name}. Valid scenarios: {valid}")
return tuple(runs)
def _build_payload(
selected_scenarios: tuple[str, ...] | None,
*,
include_performance_only: bool,
) -> dict[str, object]:
scenarios = []
for name, run in _selected_runs(selected_scenarios, include_performance_only=include_performance_only):
result = run()
scenarios.append(
{
"name": name,
"summary": {
"total_results": len(result.results_by_net),
"valid_results": sum(1 for entry in result.results_by_net.values() if entry.is_valid),
"reached_targets": sum(1 for entry in result.results_by_net.values() if entry.reached_target),
},
"metrics": asdict(result.metrics),
"pre_pair_frontier_trace": None
if result.pre_pair_frontier_trace is None
else asdict(result.pre_pair_frontier_trace),
}
)
return {
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
"generator": "scripts/record_pre_pair_frontier_trace.py",
"scenarios": scenarios,
}
def _render_markdown(payload: dict[str, object]) -> str:
lines = [
"# Pre-Pair Frontier Trace",
"",
f"Generated at {payload['generated_at']} by `{payload['generator']}`.",
"",
]
for scenario in payload["scenarios"]:
lines.extend(
[
f"## {scenario['name']}",
"",
f"Results: {scenario['summary']['valid_results']} valid / "
f"{scenario['summary']['reached_targets']} reached / "
f"{scenario['summary']['total_results']} total.",
"",
]
)
trace = scenario["pre_pair_frontier_trace"]
if trace is None:
lines.extend(["No pre-pair frontier trace captured.", ""])
continue
lines.extend(
[
f"Captured iteration: `{trace['iteration']}`",
"",
f"Conflict edges: `{trace['conflict_edges']}`",
"",
"| Net | Nodes | Checks | Closed-Set | Cost | Hard Collision | Guidance Seed | Frontier Samples |",
"| :-- | --: | --: | --: | --: | --: | :--: | --: |",
]
)
reason_counts: Counter[str] = Counter()
for net_trace in trace["nets"]:
frontier = net_trace["frontier"]
lines.append(
"| "
f"{net_trace['net_id']} | "
f"{net_trace['nodes_expanded']} | "
f"{net_trace['congestion_check_calls']} | "
f"{net_trace['pruned_closed_set']} | "
f"{net_trace['pruned_cost']} | "
f"{net_trace['pruned_hard_collision']} | "
f"{'yes' if net_trace['guidance_seed_present'] else 'no'} | "
f"{len(frontier['samples'])} |"
)
reason_counts["closed_set"] += frontier["pruned_closed_set"]
reason_counts["hard_collision"] += frontier["pruned_hard_collision"]
reason_counts["self_collision"] += frontier["pruned_self_collision"]
reason_counts["cost"] += frontier["pruned_cost"]
lines.extend(["", "Frontier prune totals by reason:", ""])
if reason_counts:
for reason, count in reason_counts.most_common():
lines.append(f"- `{reason}`: {count}")
else:
lines.append("- None")
lines.append("")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Record pre-pair frontier-trace artifacts for selected trace scenarios.")
parser.add_argument(
"--scenario",
action="append",
dest="scenarios",
default=[],
help="Optional trace scenario name to include. May be passed more than once.",
)
parser.add_argument(
"--include-performance-only",
action="store_true",
help="Include performance-only trace scenarios when selecting from the standard registry.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=None,
help="Directory to write pre_pair_frontier_trace.json and .md into. Defaults to <repo>/docs.",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1]
output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
output_dir.mkdir(exist_ok=True)
selected = tuple(args.scenarios) if args.scenarios else None
payload = _build_payload(selected, include_performance_only=args.include_performance_only)
json_path = output_dir / "pre_pair_frontier_trace.json"
markdown_path = output_dir / "pre_pair_frontier_trace.md"
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
markdown_path.write_text(_render_markdown(payload) + "\n")
if json_path.is_relative_to(repo_root):
print(f"Wrote {json_path.relative_to(repo_root)}")
else:
print(f"Wrote {json_path}")
if markdown_path.is_relative_to(repo_root):
print(f"Wrote {markdown_path.relative_to(repo_root)}")
else:
print(f"Wrote {markdown_path}")
if __name__ == "__main__":
main()