Add conflict tracing and pair-local repair
This commit is contained in:
parent
71e263c527
commit
42e46c67e0
27 changed files with 6981 additions and 142 deletions
76
DOCS.md
76
DOCS.md
|
|
@ -128,8 +128,65 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
|||
| Field | Default | Description |
|
||||
| :-- | :-- | :-- |
|
||||
| `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. |
|
||||
|
||||
## 7. RouteMetrics
|
||||
## 7. Conflict Trace
|
||||
|
||||
`RoutingRunResult.conflict_trace` is an immutable tuple of post-reverify conflict snapshots. It is empty unless `RoutingOptions.diagnostics.capture_conflict_trace=True`.
|
||||
|
||||
Trace types:
|
||||
|
||||
- `ConflictTraceEntry`
|
||||
- `stage`: `"iteration"`, `"restored_best"`, or `"final"`
|
||||
- `iteration`: Iteration index for `"iteration"` entries, otherwise `None`
|
||||
- `completed_net_ids`: Nets with collision-free reached-target paths at that stage
|
||||
- `conflict_edges`: Undirected dynamic-conflict net pairs seen after full reverify
|
||||
- `nets`: Per-net trace payloads in routing-order order
|
||||
- `NetConflictTrace`
|
||||
- `net_id`
|
||||
- `outcome`
|
||||
- `reached_target`
|
||||
- `report`
|
||||
- `conflicting_net_ids`: Dynamic conflicting nets for that stage
|
||||
- `component_conflicts`: Dynamic component-pair overlaps for that stage
|
||||
- `ComponentConflictTrace`
|
||||
- `other_net_id`
|
||||
- `self_component_index`
|
||||
- `other_component_index`
|
||||
|
||||
The conflict trace only records dynamic net-vs-net component overlaps. Static-obstacle and self-collision details remain count-only in `RoutingReport`.
|
||||
|
||||
Use `scripts/record_conflict_trace.py` to capture JSON and Markdown trace artifacts for the built-in trace scenarios. The default target is `example_07_large_scale_routing_no_warm_start`.
|
||||
|
||||
## 8. Frontier Trace
|
||||
|
||||
`RoutingRunResult.frontier_trace` is an immutable tuple of per-net post-run frontier analyses. It is empty unless `RoutingOptions.diagnostics.capture_frontier_trace=True`.
|
||||
|
||||
Trace types:
|
||||
|
||||
- `NetFrontierTrace`
|
||||
- `net_id`
|
||||
- `hotspot_bounds`: Buffered bounds around the net's final dynamic component-overlap hotspots
|
||||
- `pruned_closed_set`
|
||||
- `pruned_hard_collision`
|
||||
- `pruned_self_collision`
|
||||
- `pruned_cost`
|
||||
- `samples`: First traced prune events near those hotspots
|
||||
- `FrontierPruneSample`
|
||||
- `reason`: `"closed_set"`, `"hard_collision"`, `"self_collision"`, or `"cost"`
|
||||
- `move_type`
|
||||
- `hotspot_index`
|
||||
- `parent_state`
|
||||
- `end_state`
|
||||
|
||||
The frontier trace is observational only. It reruns only the final reached-but-colliding nets in analysis mode, with scratch metrics, after the routed result is already fixed.
|
||||
|
||||
Use `scripts/record_frontier_trace.py` to capture JSON and Markdown frontier-prune artifacts for the built-in trace scenarios. The default target is `example_07_large_scale_routing_no_warm_start`.
|
||||
|
||||
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
|
||||
|
||||
`RoutingRunResult.metrics` is an immutable per-run snapshot.
|
||||
|
||||
|
|
@ -158,6 +215,10 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
|||
|
||||
- `move_cache_abs_hits` / `move_cache_abs_misses`: Absolute move-geometry cache activity.
|
||||
- `move_cache_rel_hits` / `move_cache_rel_misses`: Relative move-geometry cache activity.
|
||||
- `guidance_match_moves`: Number of moves that matched the reroute guidance seed and received the guidance bonus.
|
||||
- `guidance_match_moves_straight`, `guidance_match_moves_bend90`, `guidance_match_moves_sbend`: Guidance-match counts split by move type.
|
||||
- `guidance_bonus_applied`: Total reroute-guidance bonus subtracted from move costs across the run.
|
||||
- `guidance_bonus_applied_straight`, `guidance_bonus_applied_bend90`, `guidance_bonus_applied_sbend`: Guidance bonus totals split by move type.
|
||||
- `static_safe_cache_hits`: Reuse count for the static-safe admission cache.
|
||||
- `hard_collision_cache_hits`: Reuse count for the hard-collision cache.
|
||||
- `congestion_cache_hits` / `congestion_cache_misses`: Per-search congestion-cache activity.
|
||||
|
|
@ -199,14 +260,21 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
|||
- `verify_dynamic_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during final verification.
|
||||
- `verify_dynamic_exact_pair_checks`: Number of exact geometry-pair checks performed during dynamic-path verification.
|
||||
|
||||
## 8. Internal Modules
|
||||
### Local Search Counters
|
||||
|
||||
- `pair_local_search_pairs_considered`: Number of final reached-target conflict pairs considered by the bounded post-loop pair-local-search phase.
|
||||
- `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.
|
||||
|
||||
## 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. 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 and `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces. The counter baseline is currently observational and is not enforced as a CI gate.
|
||||
|
||||
## 9. Tuning Notes
|
||||
## 11. Tuning Notes
|
||||
|
||||
### Speed vs. optimality
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ The search state is a snapped Manhattan `(x, y, r)` port. From each state the ro
|
|||
- The visibility subsystem keeps a lazy static corner index for default `tangent_corner` guidance and only builds the exact corner-to-corner graph on demand for `exact_corner` queries.
|
||||
- `use_tiered_strategy` can swap in a cheaper bend proxy on the first congestion iteration.
|
||||
- Negotiated congestion now re-verifies every reached-target path at the end of each iteration against the final installed dynamic geometry, and it stops early if the conflict graph stalls for consecutive iterations.
|
||||
- After best-snapshot restoration, the router runs a bounded pair-local scratch reroute on final two-net reached-target conflict pairs. That repair phase clones static obstacles from the live collision world, treats all outside-pair geometry as fixed blockers, tries both pair orders, and only keeps the result if whole-set reverify improves.
|
||||
- Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning.
|
||||
|
||||
## Performance Visibility
|
||||
|
|
|
|||
2533
docs/conflict_trace.json
Normal file
2533
docs/conflict_trace.json
Normal file
File diff suppressed because it is too large
Load diff
57
docs/conflict_trace.md
Normal file
57
docs/conflict_trace.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Conflict Trace
|
||||
|
||||
Generated at 2026-04-02T14:24:39-07:00 by `scripts/record_conflict_trace.py`.
|
||||
|
||||
## example_07_large_scale_routing_no_warm_start
|
||||
|
||||
Results: 10 valid / 10 reached / 10 total.
|
||||
|
||||
| Stage | Iteration | Conflicting Nets | Conflict Edges | Completed Nets |
|
||||
| :-- | --: | --: | --: | --: |
|
||||
| iteration | 0 | 9 | 16 | 1 |
|
||||
| iteration | 1 | 8 | 12 | 2 |
|
||||
| iteration | 2 | 6 | 5 | 4 |
|
||||
| iteration | 3 | 4 | 2 | 6 |
|
||||
| iteration | 4 | 4 | 2 | 6 |
|
||||
| iteration | 5 | 4 | 2 | 6 |
|
||||
| restored_best | | 4 | 2 | 6 |
|
||||
| final | | 0 | 0 | 10 |
|
||||
|
||||
Top nets by traced dynamic-collision stages:
|
||||
|
||||
- `net_06`: 7
|
||||
- `net_07`: 7
|
||||
- `net_01`: 6
|
||||
- `net_00`: 5
|
||||
- `net_02`: 5
|
||||
- `net_03`: 4
|
||||
- `net_08`: 2
|
||||
- `net_09`: 2
|
||||
- `net_05`: 1
|
||||
|
||||
Top net pairs by frequency:
|
||||
|
||||
- `net_06` <-> `net_07`: 7
|
||||
- `net_00` <-> `net_01`: 5
|
||||
- `net_01` <-> `net_02`: 4
|
||||
- `net_00` <-> `net_02`: 3
|
||||
- `net_00` <-> `net_03`: 3
|
||||
- `net_02` <-> `net_03`: 3
|
||||
- `net_01` <-> `net_03`: 2
|
||||
- `net_06` <-> `net_08`: 2
|
||||
- `net_06` <-> `net_09`: 2
|
||||
- `net_07` <-> `net_08`: 2
|
||||
|
||||
Top component pairs by frequency:
|
||||
|
||||
- `net_06[2]` <-> `net_07[2]`: 6
|
||||
- `net_06[3]` <-> `net_07[2]`: 6
|
||||
- `net_06[1]` <-> `net_07[1]`: 6
|
||||
- `net_06[2]` <-> `net_07[1]`: 5
|
||||
- `net_00[2]` <-> `net_01[3]`: 4
|
||||
- `net_01[2]` <-> `net_02[2]`: 3
|
||||
- `net_01[2]` <-> `net_02[3]`: 3
|
||||
- `net_00[2]` <-> `net_01[2]`: 3
|
||||
- `net_07[3]` <-> `net_08[2]`: 2
|
||||
- `net_02[1]` <-> `net_03[1]`: 2
|
||||
|
||||
120
docs/frontier_trace.json
Normal file
120
docs/frontier_trace.json
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"generated_at": "2026-04-02T14:24:39-07:00",
|
||||
"generator": "scripts/record_frontier_trace.py",
|
||||
"scenarios": [
|
||||
{
|
||||
"frontier_trace": [],
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 31,
|
||||
"congestion_cache_misses": 4625,
|
||||
"congestion_candidate_ids": 9924,
|
||||
"congestion_candidate_nets": 9979,
|
||||
"congestion_candidate_precheck_hits": 2562,
|
||||
"congestion_candidate_precheck_misses": 2165,
|
||||
"congestion_candidate_precheck_skips": 71,
|
||||
"congestion_check_calls": 4625,
|
||||
"congestion_exact_pair_checks": 8122,
|
||||
"congestion_grid_net_cache_hits": 2457,
|
||||
"congestion_grid_net_cache_misses": 3942,
|
||||
"congestion_grid_span_cache_hits": 2283,
|
||||
"congestion_grid_span_cache_misses": 1948,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 2673,
|
||||
"congestion_net_envelope_cache_misses": 4139,
|
||||
"congestion_presence_cache_hits": 2858,
|
||||
"congestion_presence_cache_misses": 2556,
|
||||
"congestion_presence_skips": 687,
|
||||
"danger_map_cache_hits": 16878,
|
||||
"danger_map_cache_misses": 7425,
|
||||
"danger_map_lookup_calls": 24303,
|
||||
"danger_map_query_calls": 7425,
|
||||
"danger_map_total_ns": 212814061,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 471,
|
||||
"dynamic_path_objects_removed": 423,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 11000.0,
|
||||
"guidance_bonus_applied_bend90": 3500.0,
|
||||
"guidance_bonus_applied_sbend": 625.0,
|
||||
"guidance_bonus_applied_straight": 6875.0,
|
||||
"guidance_match_moves": 176,
|
||||
"guidance_match_moves_bend90": 56,
|
||||
"guidance_match_moves_sbend": 10,
|
||||
"guidance_match_moves_straight": 110,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 39,
|
||||
"iteration_conflicting_nets": 36,
|
||||
"iteration_reverified_nets": 60,
|
||||
"iteration_reverify_calls": 6,
|
||||
"move_cache_abs_hits": 2559,
|
||||
"move_cache_abs_misses": 6494,
|
||||
"move_cache_rel_hits": 5872,
|
||||
"move_cache_rel_misses": 622,
|
||||
"moves_added": 8081,
|
||||
"moves_generated": 9053,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 60,
|
||||
"nets_routed": 60,
|
||||
"nodes_expanded": 1764,
|
||||
"pair_local_search_accepts": 2,
|
||||
"pair_local_search_attempts": 2,
|
||||
"pair_local_search_nodes_expanded": 68,
|
||||
"pair_local_search_pairs_considered": 2,
|
||||
"path_cost_calls": 0,
|
||||
"pruned_closed_set": 439,
|
||||
"pruned_cost": 533,
|
||||
"pruned_hard_collision": 0,
|
||||
"ray_cast_calls": 5477,
|
||||
"ray_cast_calls_expand_forward": 1704,
|
||||
"ray_cast_calls_expand_snap": 46,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 3721,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 6,
|
||||
"ray_cast_candidate_bounds": 305,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 10,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
"refinement_candidates_accepted": 0,
|
||||
"refinement_candidates_built": 0,
|
||||
"refinement_candidates_verified": 0,
|
||||
"refinement_dynamic_bounds_checked": 0,
|
||||
"refinement_static_bounds_checked": 0,
|
||||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 6,
|
||||
"score_component_calls": 8634,
|
||||
"score_component_total_ns": 241025335,
|
||||
"static_net_tree_rebuilds": 1,
|
||||
"static_raw_tree_rebuilds": 1,
|
||||
"static_safe_cache_hits": 2482,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 2106,
|
||||
"verify_dynamic_exact_pair_checks": 558,
|
||||
"verify_path_report_calls": 190,
|
||||
"verify_static_buffer_ops": 895,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_index_builds": 1,
|
||||
"visibility_corner_pairs_checked": 0,
|
||||
"visibility_corner_queries_exact": 0,
|
||||
"visibility_point_cache_hits": 0,
|
||||
"visibility_point_cache_misses": 0,
|
||||
"visibility_point_queries": 0,
|
||||
"visibility_tangent_candidate_corner_checks": 6,
|
||||
"visibility_tangent_candidate_ray_tests": 6,
|
||||
"visibility_tangent_candidate_scans": 1704,
|
||||
"warm_start_paths_built": 0,
|
||||
"warm_start_paths_used": 0
|
||||
},
|
||||
"name": "example_07_large_scale_routing_no_warm_start",
|
||||
"summary": {
|
||||
"reached_targets": 10,
|
||||
"total_results": 10,
|
||||
"valid_results": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
23
docs/frontier_trace.md
Normal file
23
docs/frontier_trace.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Frontier Trace
|
||||
|
||||
Generated at 2026-04-02T14:24:39-07:00 by `scripts/record_frontier_trace.py`.
|
||||
|
||||
## example_07_large_scale_routing_no_warm_start
|
||||
|
||||
Results: 10 valid / 10 reached / 10 total.
|
||||
|
||||
| Net | Hotspots | Closed-Set | Hard Collision | Self Collision | Cost | Samples |
|
||||
| :-- | --: | --: | --: | --: | --: | --: |
|
||||
|
||||
Prune totals by reason:
|
||||
|
||||
- None
|
||||
|
||||
Top traced hotspots by sample count:
|
||||
|
||||
- None
|
||||
|
||||
Per-net sampled reason/move breakdown:
|
||||
|
||||
- None
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,22 @@
|
|||
# Performance Baseline
|
||||
|
||||
Generated on 2026-04-01 by `scripts/record_performance_baseline.py`.
|
||||
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.
|
||||
|
||||
| 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.0036 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 3 |
|
||||
| example_02_congestion_resolution | 0.3297 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 35 |
|
||||
| example_03_locked_paths | 0.1832 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 14 |
|
||||
| example_04_sbends_and_radii | 0.0260 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 6 |
|
||||
| example_05_orientation_stress | 0.2348 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 0 | 0 | 155 | 15 |
|
||||
| example_06_bend_collision_models | 0.1953 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 9 |
|
||||
| example_07_large_scale_routing | 0.1945 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 30 |
|
||||
| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 6 |
|
||||
| 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 |
|
||||
|
||||
## Full Counter Set
|
||||
|
|
@ -22,6 +24,13 @@ Use `scripts/diff_performance_baseline.py` to compare a fresh run against that s
|
|||
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`
|
||||
|
||||
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, 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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"generated_on": "2026-04-01",
|
||||
"generated_on": "2026-04-02",
|
||||
"generator": "scripts/record_performance_baseline.py",
|
||||
"scenarios": [
|
||||
{
|
||||
"duration_s": 0.0035884700482711196,
|
||||
"duration_s": 0.003964120987802744,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -34,6 +34,14 @@
|
|||
"dynamic_path_objects_added": 3,
|
||||
"dynamic_path_objects_removed": 2,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -49,6 +57,10 @@
|
|||
"nets_reached_target": 1,
|
||||
"nets_routed": 1,
|
||||
"nodes_expanded": 2,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 0,
|
||||
"pruned_cost": 4,
|
||||
|
|
@ -73,7 +85,7 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 11,
|
||||
"score_component_total_ns": 16010,
|
||||
"score_component_total_ns": 18064,
|
||||
"static_net_tree_rebuilds": 1,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 1,
|
||||
|
|
@ -81,7 +93,7 @@
|
|||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 3,
|
||||
"verify_path_report_calls": 4,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
|
|
@ -103,7 +115,7 @@
|
|||
"valid_results": 1
|
||||
},
|
||||
{
|
||||
"duration_s": 0.32969290704932064,
|
||||
"duration_s": 0.3377689190674573,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -134,6 +146,14 @@
|
|||
"dynamic_path_objects_added": 49,
|
||||
"dynamic_path_objects_removed": 34,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -149,6 +169,10 @@
|
|||
"nets_reached_target": 3,
|
||||
"nets_routed": 3,
|
||||
"nodes_expanded": 366,
|
||||
"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": 14,
|
||||
"pruned_closed_set": 157,
|
||||
"pruned_cost": 208,
|
||||
|
|
@ -173,15 +197,15 @@
|
|||
"refinement_windows_considered": 10,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 976,
|
||||
"score_component_total_ns": 1091130,
|
||||
"score_component_total_ns": 1140704,
|
||||
"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": 84,
|
||||
"verify_dynamic_exact_pair_checks": 82,
|
||||
"verify_path_report_calls": 35,
|
||||
"verify_dynamic_candidate_nets": 88,
|
||||
"verify_dynamic_exact_pair_checks": 86,
|
||||
"verify_path_report_calls": 38,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
|
|
@ -203,7 +227,7 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 0.18321374501101673,
|
||||
"duration_s": 0.1929313091095537,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -234,6 +258,14 @@
|
|||
"dynamic_path_objects_added": 27,
|
||||
"dynamic_path_objects_removed": 20,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -249,6 +281,10 @@
|
|||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 191,
|
||||
"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": 9,
|
||||
"pruned_closed_set": 97,
|
||||
"pruned_cost": 140,
|
||||
|
|
@ -273,16 +309,16 @@
|
|||
"refinement_windows_considered": 2,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 504,
|
||||
"score_component_total_ns": 556716,
|
||||
"score_component_total_ns": 565410,
|
||||
"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": 8,
|
||||
"verify_dynamic_exact_pair_checks": 8,
|
||||
"verify_path_report_calls": 14,
|
||||
"verify_static_buffer_ops": 72,
|
||||
"verify_dynamic_candidate_nets": 9,
|
||||
"verify_dynamic_exact_pair_checks": 9,
|
||||
"verify_path_report_calls": 16,
|
||||
"verify_static_buffer_ops": 81,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_index_builds": 2,
|
||||
|
|
@ -303,7 +339,7 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 0.026024609920568764,
|
||||
"duration_s": 0.02791503700427711,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -334,6 +370,14 @@
|
|||
"dynamic_path_objects_added": 21,
|
||||
"dynamic_path_objects_removed": 14,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -349,6 +393,10 @@
|
|||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 15,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 2,
|
||||
"pruned_cost": 25,
|
||||
|
|
@ -373,15 +421,15 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 90,
|
||||
"score_component_total_ns": 97738,
|
||||
"score_component_total_ns": 100083,
|
||||
"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": 6,
|
||||
"verify_dynamic_candidate_nets": 9,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 6,
|
||||
"verify_path_report_calls": 8,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
|
|
@ -403,28 +451,28 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 0.23484283208381385,
|
||||
"duration_s": 0.23665715800598264,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 2,
|
||||
"congestion_cache_misses": 155,
|
||||
"congestion_candidate_ids": 19,
|
||||
"congestion_candidate_nets": 15,
|
||||
"congestion_candidate_precheck_hits": 135,
|
||||
"congestion_cache_hits": 4,
|
||||
"congestion_cache_misses": 149,
|
||||
"congestion_candidate_ids": 32,
|
||||
"congestion_candidate_nets": 23,
|
||||
"congestion_candidate_precheck_hits": 131,
|
||||
"congestion_candidate_precheck_misses": 22,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 155,
|
||||
"congestion_exact_pair_checks": 18,
|
||||
"congestion_grid_net_cache_hits": 12,
|
||||
"congestion_grid_net_cache_misses": 25,
|
||||
"congestion_grid_span_cache_hits": 11,
|
||||
"congestion_grid_span_cache_misses": 4,
|
||||
"congestion_check_calls": 149,
|
||||
"congestion_exact_pair_checks": 30,
|
||||
"congestion_grid_net_cache_hits": 16,
|
||||
"congestion_grid_net_cache_misses": 28,
|
||||
"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": 134,
|
||||
"congestion_net_envelope_cache_hits": 128,
|
||||
"congestion_net_envelope_cache_misses": 43,
|
||||
"congestion_presence_cache_hits": 185,
|
||||
"congestion_presence_cache_hits": 200,
|
||||
"congestion_presence_cache_misses": 30,
|
||||
"congestion_presence_skips": 58,
|
||||
"congestion_presence_skips": 77,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
|
|
@ -434,30 +482,42 @@
|
|||
"dynamic_path_objects_added": 49,
|
||||
"dynamic_path_objects_removed": 37,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 687.5,
|
||||
"guidance_bonus_applied_bend90": 500.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 187.5,
|
||||
"guidance_match_moves": 11,
|
||||
"guidance_match_moves_bend90": 8,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 3,
|
||||
"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": 253,
|
||||
"move_cache_abs_misses": 1371,
|
||||
"move_cache_rel_hits": 1269,
|
||||
"move_cache_abs_hits": 385,
|
||||
"move_cache_abs_misses": 1306,
|
||||
"move_cache_rel_hits": 1204,
|
||||
"move_cache_rel_misses": 102,
|
||||
"moves_added": 681,
|
||||
"moves_generated": 1624,
|
||||
"moves_added": 696,
|
||||
"moves_generated": 1691,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 6,
|
||||
"nets_routed": 6,
|
||||
"nodes_expanded": 286,
|
||||
"nodes_expanded": 299,
|
||||
"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": 139,
|
||||
"pruned_cost": 505,
|
||||
"pruned_closed_set": 159,
|
||||
"pruned_cost": 537,
|
||||
"pruned_hard_collision": 14,
|
||||
"ray_cast_calls": 1243,
|
||||
"ray_cast_calls_expand_forward": 280,
|
||||
"ray_cast_calls": 1284,
|
||||
"ray_cast_calls_expand_forward": 293,
|
||||
"ray_cast_calls_expand_snap": 3,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 951,
|
||||
"ray_cast_calls_straight_static": 979,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 9,
|
||||
|
|
@ -472,16 +532,16 @@
|
|||
"refinement_static_bounds_checked": 0,
|
||||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 1198,
|
||||
"score_component_total_ns": 1194981,
|
||||
"score_component_calls": 1245,
|
||||
"score_component_total_ns": 1260961,
|
||||
"static_net_tree_rebuilds": 3,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 3,
|
||||
"static_safe_cache_hits": 9,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 8,
|
||||
"verify_dynamic_exact_pair_checks": 12,
|
||||
"verify_path_report_calls": 15,
|
||||
"verify_path_report_calls": 18,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
|
|
@ -493,7 +553,7 @@
|
|||
"visibility_point_queries": 0,
|
||||
"visibility_tangent_candidate_corner_checks": 70,
|
||||
"visibility_tangent_candidate_ray_tests": 9,
|
||||
"visibility_tangent_candidate_scans": 280,
|
||||
"visibility_tangent_candidate_scans": 293,
|
||||
"warm_start_paths_built": 2,
|
||||
"warm_start_paths_used": 2
|
||||
},
|
||||
|
|
@ -503,7 +563,7 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 0.19533946400042623,
|
||||
"duration_s": 0.19982667709700763,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -529,11 +589,19 @@
|
|||
"danger_map_cache_misses": 731,
|
||||
"danger_map_lookup_calls": 1914,
|
||||
"danger_map_query_calls": 731,
|
||||
"danger_map_total_ns": 18697751,
|
||||
"danger_map_total_ns": 18959782,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 54,
|
||||
"dynamic_path_objects_removed": 36,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 18,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -549,6 +617,10 @@
|
|||
"nets_reached_target": 3,
|
||||
"nets_routed": 3,
|
||||
"nodes_expanded": 240,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 108,
|
||||
"pruned_cost": 204,
|
||||
|
|
@ -573,7 +645,7 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 3,
|
||||
"score_component_calls": 842,
|
||||
"score_component_total_ns": 21016472,
|
||||
"score_component_total_ns": 21338709,
|
||||
"static_net_tree_rebuilds": 3,
|
||||
"static_raw_tree_rebuilds": 3,
|
||||
"static_safe_cache_hits": 141,
|
||||
|
|
@ -581,8 +653,8 @@
|
|||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 9,
|
||||
"verify_static_buffer_ops": 54,
|
||||
"verify_path_report_calls": 12,
|
||||
"verify_static_buffer_ops": 72,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_index_builds": 3,
|
||||
|
|
@ -603,7 +675,7 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 0.19448363897390664,
|
||||
"duration_s": 0.20046633295714855,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -629,11 +701,19 @@
|
|||
"danger_map_cache_misses": 448,
|
||||
"danger_map_lookup_calls": 681,
|
||||
"danger_map_query_calls": 448,
|
||||
"danger_map_total_ns": 10973251,
|
||||
"danger_map_total_ns": 11017087,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 132,
|
||||
"dynamic_path_objects_removed": 88,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -649,6 +729,10 @@
|
|||
"nets_reached_target": 10,
|
||||
"nets_routed": 10,
|
||||
"nodes_expanded": 78,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 20,
|
||||
"pruned_cost": 64,
|
||||
|
|
@ -673,16 +757,16 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 291,
|
||||
"score_component_total_ns": 11824081,
|
||||
"score_component_total_ns": 11869917,
|
||||
"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": 264,
|
||||
"verify_dynamic_exact_pair_checks": 40,
|
||||
"verify_path_report_calls": 30,
|
||||
"verify_static_buffer_ops": 132,
|
||||
"verify_dynamic_candidate_nets": 370,
|
||||
"verify_dynamic_exact_pair_checks": 56,
|
||||
"verify_path_report_calls": 40,
|
||||
"verify_static_buffer_ops": 176,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_index_builds": 10,
|
||||
|
|
@ -703,7 +787,7 @@
|
|||
"valid_results": 10
|
||||
},
|
||||
{
|
||||
"duration_s": 0.017700672964565456,
|
||||
"duration_s": 0.01759456400759518,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -734,6 +818,14 @@
|
|||
"dynamic_path_objects_added": 18,
|
||||
"dynamic_path_objects_removed": 12,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -749,6 +841,10 @@
|
|||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 18,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 6,
|
||||
"pruned_cost": 16,
|
||||
|
|
@ -773,7 +869,7 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 72,
|
||||
"score_component_total_ns": 85969,
|
||||
"score_component_total_ns": 85864,
|
||||
"static_net_tree_rebuilds": 2,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 2,
|
||||
|
|
@ -781,7 +877,7 @@
|
|||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 6,
|
||||
"verify_path_report_calls": 8,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
|
|
@ -803,7 +899,7 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 0.005781985004432499,
|
||||
"duration_s": 0.005838233977556229,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
|
|
@ -829,11 +925,19 @@
|
|||
"danger_map_cache_misses": 20,
|
||||
"danger_map_lookup_calls": 30,
|
||||
"danger_map_query_calls": 20,
|
||||
"danger_map_total_ns": 536009,
|
||||
"danger_map_total_ns": 523870,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 2,
|
||||
"dynamic_path_objects_removed": 1,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"guidance_bonus_applied": 0.0,
|
||||
"guidance_bonus_applied_bend90": 0.0,
|
||||
"guidance_bonus_applied_sbend": 0.0,
|
||||
"guidance_bonus_applied_straight": 0.0,
|
||||
"guidance_match_moves": 0,
|
||||
"guidance_match_moves_bend90": 0,
|
||||
"guidance_match_moves_sbend": 0,
|
||||
"guidance_match_moves_straight": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
|
|
@ -849,6 +953,10 @@
|
|||
"nets_reached_target": 0,
|
||||
"nets_routed": 1,
|
||||
"nodes_expanded": 3,
|
||||
"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": 0,
|
||||
"pruned_closed_set": 0,
|
||||
"pruned_cost": 4,
|
||||
|
|
@ -873,7 +981,7 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 14,
|
||||
"score_component_total_ns": 574907,
|
||||
"score_component_total_ns": 563611,
|
||||
"static_net_tree_rebuilds": 1,
|
||||
"static_raw_tree_rebuilds": 1,
|
||||
"static_safe_cache_hits": 0,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,15 @@ from .model import (
|
|||
RoutingProblem as RoutingProblem,
|
||||
SearchOptions as SearchOptions,
|
||||
) # noqa: PLC0414
|
||||
from .results import RoutingResult as RoutingResult, RoutingRunResult as RoutingRunResult # noqa: PLC0414
|
||||
from .results import ( # noqa: PLC0414
|
||||
ComponentConflictTrace as ComponentConflictTrace,
|
||||
ConflictTraceEntry as ConflictTraceEntry,
|
||||
FrontierPruneSample as FrontierPruneSample,
|
||||
NetConflictTrace as NetConflictTrace,
|
||||
NetFrontierTrace as NetFrontierTrace,
|
||||
RoutingResult as RoutingResult,
|
||||
RoutingRunResult as RoutingRunResult,
|
||||
)
|
||||
from .seeds import Bend90Seed as Bend90Seed, PathSeed as PathSeed, SBendSeed as SBendSeed, StraightSeed as StraightSeed # noqa: PLC0414
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
|
@ -37,16 +45,23 @@ def route(
|
|||
results_by_net=results,
|
||||
metrics=finder.metrics.snapshot(),
|
||||
expanded_nodes=tuple(finder.accumulated_expanded_nodes),
|
||||
conflict_trace=tuple(finder.conflict_trace),
|
||||
frontier_trace=tuple(finder.frontier_trace),
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Bend90Seed",
|
||||
"CongestionOptions",
|
||||
"ComponentConflictTrace",
|
||||
"ConflictTraceEntry",
|
||||
"DiagnosticsOptions",
|
||||
"NetSpec",
|
||||
"NetConflictTrace",
|
||||
"NetFrontierTrace",
|
||||
"ObjectiveWeights",
|
||||
"PathSeed",
|
||||
"Port",
|
||||
"FrontierPruneSample",
|
||||
"RefinementOptions",
|
||||
"RoutingOptions",
|
||||
"RoutingProblem",
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ def _span_to_bounds(
|
|||
class PathVerificationDetail:
|
||||
report: RoutingReport
|
||||
conflicting_net_ids: tuple[str, ...] = ()
|
||||
component_conflicts: tuple[tuple[int, str, int], ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -140,8 +141,19 @@ class RoutingWorld:
|
|||
def _ensure_dynamic_grid(self) -> None:
|
||||
self._dynamic_paths.ensure_grid()
|
||||
|
||||
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
||||
self._dynamic_paths.add_path(net_id, geometry, dilated_geometry=dilated_geometry)
|
||||
def add_path(
|
||||
self,
|
||||
net_id: str,
|
||||
geometry: Sequence[Polygon],
|
||||
dilated_geometry: Sequence[Polygon],
|
||||
component_indexes: Sequence[int] | None = None,
|
||||
) -> None:
|
||||
self._dynamic_paths.add_path(
|
||||
net_id,
|
||||
geometry,
|
||||
dilated_geometry=dilated_geometry,
|
||||
component_indexes=component_indexes,
|
||||
)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
self._dynamic_paths.remove_path(net_id)
|
||||
|
|
@ -651,7 +663,13 @@ class RoutingWorld:
|
|||
frozen_net_ids=frozen_net_ids,
|
||||
)
|
||||
|
||||
def verify_path_details(self, net_id: str, components: Sequence[ComponentResult]) -> PathVerificationDetail:
|
||||
def verify_path_details(
|
||||
self,
|
||||
net_id: str,
|
||||
components: Sequence[ComponentResult],
|
||||
*,
|
||||
capture_component_conflicts: bool = False,
|
||||
) -> PathVerificationDetail:
|
||||
if self.metrics is not None:
|
||||
self.metrics.total_verify_path_report_calls += 1
|
||||
static_collision_count = 0
|
||||
|
|
@ -659,6 +677,7 @@ class RoutingWorld:
|
|||
self_collision_count = 0
|
||||
total_length = sum(component.length for component in components)
|
||||
conflicting_net_ids: set[str] = set()
|
||||
component_conflicts: set[tuple[int, str, int]] = set()
|
||||
|
||||
static_obstacles = self._static_obstacles
|
||||
dynamic_paths = self._dynamic_paths
|
||||
|
|
@ -682,7 +701,7 @@ class RoutingWorld:
|
|||
static_collision_count += 1
|
||||
|
||||
if dynamic_paths.dilated:
|
||||
for component in components:
|
||||
for component_index, component in enumerate(components):
|
||||
test_geometries = component.dilated_physical_geometry
|
||||
component_hits = []
|
||||
for new_geometry in test_geometries:
|
||||
|
|
@ -695,6 +714,14 @@ class RoutingWorld:
|
|||
tree_geometry = dynamic_paths.dilated[obj_id]
|
||||
if _has_non_touching_overlap(new_geometry, tree_geometry):
|
||||
component_hits.append(hit_net_id)
|
||||
if capture_component_conflicts:
|
||||
component_conflicts.add(
|
||||
(
|
||||
component_index,
|
||||
hit_net_id,
|
||||
dynamic_paths.component_indexes[obj_id],
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
if component_hits:
|
||||
|
|
@ -715,6 +742,7 @@ class RoutingWorld:
|
|||
total_length=total_length,
|
||||
),
|
||||
conflicting_net_ids=tuple(sorted(conflicting_net_ids)),
|
||||
component_conflicts=tuple(sorted(component_conflicts)),
|
||||
)
|
||||
|
||||
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class DynamicPathIndex:
|
|||
"engine",
|
||||
"index",
|
||||
"geometries",
|
||||
"component_indexes",
|
||||
"dilated",
|
||||
"dilated_bounds",
|
||||
"net_envelope_index",
|
||||
|
|
@ -43,6 +44,7 @@ class DynamicPathIndex:
|
|||
self.engine = engine
|
||||
self.index = rtree.index.Index()
|
||||
self.geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
self.component_indexes: dict[int, int] = {}
|
||||
self.dilated: dict[int, Polygon] = {}
|
||||
self.dilated_bounds: dict[int, tuple[float, float, float, float]] = {}
|
||||
self.net_envelope_index = rtree.index.Index()
|
||||
|
|
@ -176,7 +178,13 @@ class DynamicPathIndex:
|
|||
if not net_counts:
|
||||
self.grid_net_counts.pop(cell, None)
|
||||
|
||||
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
||||
def add_path(
|
||||
self,
|
||||
net_id: str,
|
||||
geometry: Sequence[Polygon],
|
||||
dilated_geometry: Sequence[Polygon],
|
||||
component_indexes: Sequence[int] | None = None,
|
||||
) -> None:
|
||||
if self.engine.metrics is not None:
|
||||
self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
|
||||
cell_size = self.engine.grid_cell_size
|
||||
|
|
@ -186,6 +194,7 @@ class DynamicPathIndex:
|
|||
dilated = dilated_geometry[index]
|
||||
dilated_bounds = dilated.bounds
|
||||
self.geometries[obj_id] = (net_id, polygon)
|
||||
self.component_indexes[obj_id] = index if component_indexes is None else component_indexes[index]
|
||||
self.dilated[obj_id] = dilated
|
||||
self.dilated_bounds[obj_id] = dilated_bounds
|
||||
self.index.insert(obj_id, dilated_bounds)
|
||||
|
|
@ -211,6 +220,7 @@ class DynamicPathIndex:
|
|||
self._unregister_grid_membership(obj_id, net_id)
|
||||
self.index.delete(obj_id, self.dilated_bounds[obj_id])
|
||||
del self.geometries[obj_id]
|
||||
del self.component_indexes[obj_id]
|
||||
del self.dilated[obj_id]
|
||||
del self.dilated_bounds[obj_id]
|
||||
obj_id_set = self.net_to_obj_ids.get(net_id)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ class RefinementOptions:
|
|||
@dataclass(frozen=True, slots=True)
|
||||
class DiagnosticsOptions:
|
||||
capture_expanded: bool = False
|
||||
capture_conflict_trace: bool = False
|
||||
capture_frontier_trace: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
RoutingOutcome = Literal["completed", "colliding", "partial", "unroutable"]
|
||||
ConflictTraceStage = Literal["iteration", "restored_best", "final"]
|
||||
FrontierTraceReason = Literal["closed_set", "hard_collision", "self_collision", "cost"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -30,6 +32,52 @@ class RoutingReport:
|
|||
return self.collision_count == 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ComponentConflictTrace:
|
||||
other_net_id: str
|
||||
self_component_index: int
|
||||
other_component_index: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NetConflictTrace:
|
||||
net_id: str
|
||||
outcome: RoutingOutcome
|
||||
reached_target: bool
|
||||
report: RoutingReport
|
||||
conflicting_net_ids: tuple[str, ...] = ()
|
||||
component_conflicts: tuple[ComponentConflictTrace, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConflictTraceEntry:
|
||||
stage: ConflictTraceStage
|
||||
iteration: int | None
|
||||
completed_net_ids: tuple[str, ...]
|
||||
conflict_edges: tuple[tuple[str, str], ...]
|
||||
nets: tuple[NetConflictTrace, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FrontierPruneSample:
|
||||
reason: FrontierTraceReason
|
||||
move_type: str
|
||||
hotspot_index: int
|
||||
parent_state: tuple[int, int, int]
|
||||
end_state: tuple[int, int, int]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NetFrontierTrace:
|
||||
net_id: str
|
||||
hotspot_bounds: tuple[tuple[float, float, float, float], ...]
|
||||
pruned_closed_set: int
|
||||
pruned_hard_collision: int
|
||||
pruned_self_collision: int
|
||||
pruned_cost: int
|
||||
samples: tuple[FrontierPruneSample, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RouteMetrics:
|
||||
nodes_expanded: int
|
||||
|
|
@ -62,6 +110,14 @@ class RouteMetrics:
|
|||
move_cache_abs_misses: int
|
||||
move_cache_rel_hits: int
|
||||
move_cache_rel_misses: int
|
||||
guidance_match_moves: int
|
||||
guidance_match_moves_straight: int
|
||||
guidance_match_moves_bend90: int
|
||||
guidance_match_moves_sbend: int
|
||||
guidance_bonus_applied: float
|
||||
guidance_bonus_applied_straight: float
|
||||
guidance_bonus_applied_bend90: float
|
||||
guidance_bonus_applied_sbend: float
|
||||
static_safe_cache_hits: int
|
||||
hard_collision_cache_hits: int
|
||||
congestion_cache_hits: int
|
||||
|
|
@ -123,6 +179,10 @@ class RouteMetrics:
|
|||
refinement_candidates_built: int
|
||||
refinement_candidates_verified: int
|
||||
refinement_candidates_accepted: int
|
||||
pair_local_search_pairs_considered: int
|
||||
pair_local_search_attempts: int
|
||||
pair_local_search_accepts: int
|
||||
pair_local_search_nodes_expanded: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -169,3 +229,5 @@ class RoutingRunResult:
|
|||
results_by_net: dict[str, RoutingResult]
|
||||
metrics: RouteMetrics
|
||||
expanded_nodes: tuple[tuple[int, int, int], ...] = ()
|
||||
conflict_trace: tuple[ConflictTraceEntry, ...] = ()
|
||||
frontier_trace: tuple[NetFrontierTrace, ...] = ()
|
||||
|
|
|
|||
|
|
@ -145,11 +145,14 @@ def add_node(
|
|||
move_type: MoveKind,
|
||||
cache_key: tuple,
|
||||
) -> None:
|
||||
frontier_trace = config.frontier_trace
|
||||
metrics.moves_generated += 1
|
||||
metrics.total_moves_generated += 1
|
||||
state = result.end_port.as_tuple()
|
||||
new_lower_bound_g = parent.g_cost + result.length
|
||||
if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_closed_set += 1
|
||||
metrics.total_pruned_closed_set += 1
|
||||
return
|
||||
|
|
@ -158,6 +161,8 @@ def add_node(
|
|||
end_p = result.end_port
|
||||
|
||||
if cache_key in context.hard_collision_set:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("hard_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
context.metrics.total_hard_collision_cache_hits += 1
|
||||
metrics.pruned_hard_collision += 1
|
||||
metrics.total_pruned_hard_collision += 1
|
||||
|
|
@ -174,29 +179,62 @@ def add_node(
|
|||
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p)
|
||||
if collision_found:
|
||||
context.hard_collision_set.add(cache_key)
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("hard_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_hard_collision += 1
|
||||
metrics.total_pruned_hard_collision += 1
|
||||
return
|
||||
context.static_safe_cache.add(cache_key)
|
||||
|
||||
if config.self_collision_check and component_hits_ancestor_chain(result, parent):
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("self_collision", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
return
|
||||
|
||||
move_cost = context.cost_evaluator.score_component(
|
||||
result,
|
||||
start_port=parent_p,
|
||||
)
|
||||
next_seed_index = None
|
||||
if (
|
||||
config.guidance_seed is not None
|
||||
and parent.seed_index is not None
|
||||
and parent.seed_index < len(config.guidance_seed)
|
||||
and result.move_spec == config.guidance_seed[parent.seed_index]
|
||||
):
|
||||
context.metrics.total_guidance_match_moves += 1
|
||||
if result.move_type == "straight":
|
||||
context.metrics.total_guidance_match_moves_straight += 1
|
||||
applied_bonus = config.guidance_bonus
|
||||
context.metrics.total_guidance_bonus_applied_straight += applied_bonus
|
||||
elif result.move_type == "bend90":
|
||||
context.metrics.total_guidance_match_moves_bend90 += 1
|
||||
applied_bonus = config.guidance_bonus
|
||||
context.metrics.total_guidance_bonus_applied_bend90 += applied_bonus
|
||||
else:
|
||||
context.metrics.total_guidance_match_moves_sbend += 1
|
||||
applied_bonus = config.guidance_bonus
|
||||
context.metrics.total_guidance_bonus_applied_sbend += applied_bonus
|
||||
context.metrics.total_guidance_bonus_applied += applied_bonus
|
||||
move_cost = max(0.001, move_cost - applied_bonus)
|
||||
next_seed_index = parent.seed_index + 1
|
||||
|
||||
if config.max_cost is not None and parent.g_cost + move_cost > config.max_cost:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("cost", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_cost += 1
|
||||
metrics.total_pruned_cost += 1
|
||||
return
|
||||
if move_cost > 1e12:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("cost", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_cost += 1
|
||||
metrics.total_pruned_cost += 1
|
||||
return
|
||||
|
||||
if state in closed_set and closed_set[state] <= parent.g_cost + move_cost + TOLERANCE_LINEAR:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_closed_set += 1
|
||||
metrics.total_pruned_closed_set += 1
|
||||
return
|
||||
|
|
@ -233,6 +271,8 @@ def add_node(
|
|||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
||||
if frontier_trace is not None:
|
||||
frontier_trace.record("closed_set", move_type, parent.port.as_tuple(), state, result.total_dilated_bounds)
|
||||
metrics.pruned_closed_set += 1
|
||||
metrics.total_pruned_closed_set += 1
|
||||
return
|
||||
|
|
@ -242,6 +282,16 @@ def add_node(
|
|||
target,
|
||||
min_bend_radius=context.min_bend_radius,
|
||||
)
|
||||
heapq.heappush(open_set, AStarNode(result.end_port, g_cost, h_cost, parent, result))
|
||||
heapq.heappush(
|
||||
open_set,
|
||||
AStarNode(
|
||||
result.end_port,
|
||||
g_cost,
|
||||
h_cost,
|
||||
parent,
|
||||
result,
|
||||
seed_index=next_seed_index,
|
||||
),
|
||||
)
|
||||
metrics.moves_added += 1
|
||||
metrics.total_moves_added += 1
|
||||
|
|
|
|||
|
|
@ -18,6 +18,27 @@ def _quantized_lengths(values: list[float], max_reach: float) -> list[int]:
|
|||
return sorted((v for v in out if v > 0), reverse=True)
|
||||
|
||||
|
||||
def _distance_to_bounds_in_heading(
|
||||
current: Port,
|
||||
bounds: tuple[float, float, float, float],
|
||||
) -> float:
|
||||
min_x, min_y, max_x, max_y = bounds
|
||||
if current.r == 0:
|
||||
return max(0.0, max_x - current.x)
|
||||
if current.r == 90:
|
||||
return max(0.0, max_y - current.y)
|
||||
if current.r == 180:
|
||||
return max(0.0, current.x - min_x)
|
||||
return max(0.0, current.y - min_y)
|
||||
|
||||
|
||||
def _should_cap_straights_to_bounds(context: AStarContext) -> bool:
|
||||
return (
|
||||
not context.options.congestion.warm_start_enabled
|
||||
and len(context.problem.nets) >= 8
|
||||
)
|
||||
|
||||
|
||||
def _sbend_forward_span(offset: float, radius: float) -> float | None:
|
||||
abs_offset = abs(offset)
|
||||
if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius:
|
||||
|
|
@ -211,6 +232,8 @@ def expand_moves(
|
|||
net_width=net_width,
|
||||
caller="expand_forward",
|
||||
)
|
||||
if _should_cap_straights_to_bounds(context):
|
||||
max_reach = min(max_reach, _distance_to_bounds_in_heading(cp, context.problem.bounds))
|
||||
candidate_lengths = [
|
||||
search_options.min_straight_length,
|
||||
max_reach,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from inire.model import resolve_bend_geometry
|
||||
|
|
@ -12,6 +12,51 @@ if TYPE_CHECKING:
|
|||
from inire.geometry.primitives import Port
|
||||
from inire.model import RoutingOptions, RoutingProblem
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.seeds import PathSegmentSeed
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FrontierTraceCollector:
|
||||
hotspot_bounds: tuple[tuple[float, float, float, float], ...]
|
||||
sample_limit: int = 64
|
||||
pruned_closed_set: int = 0
|
||||
pruned_hard_collision: int = 0
|
||||
pruned_self_collision: int = 0
|
||||
pruned_cost: int = 0
|
||||
samples: list[tuple[str, str, int, tuple[int, int, int], tuple[int, int, int]]] = field(default_factory=list)
|
||||
|
||||
def _matching_hotspot_index(self, bounds: tuple[float, float, float, float]) -> int | None:
|
||||
for idx, hotspot_bounds in enumerate(self.hotspot_bounds):
|
||||
if (
|
||||
bounds[0] < hotspot_bounds[2]
|
||||
and bounds[2] > hotspot_bounds[0]
|
||||
and bounds[1] < hotspot_bounds[3]
|
||||
and bounds[3] > hotspot_bounds[1]
|
||||
):
|
||||
return idx
|
||||
return None
|
||||
|
||||
def record(
|
||||
self,
|
||||
reason: str,
|
||||
move_type: str,
|
||||
parent_state: tuple[int, int, int],
|
||||
end_state: tuple[int, int, int],
|
||||
bounds: tuple[float, float, float, float],
|
||||
) -> None:
|
||||
hotspot_index = self._matching_hotspot_index(bounds)
|
||||
if hotspot_index is None:
|
||||
return
|
||||
if reason == "closed_set":
|
||||
self.pruned_closed_set += 1
|
||||
elif reason == "hard_collision":
|
||||
self.pruned_hard_collision += 1
|
||||
elif reason == "self_collision":
|
||||
self.pruned_self_collision += 1
|
||||
else:
|
||||
self.pruned_cost += 1
|
||||
if len(self.samples) < self.sample_limit:
|
||||
self.samples.append((reason, move_type, hotspot_index, parent_state, end_state))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -20,6 +65,9 @@ class SearchRunConfig:
|
|||
bend_physical_geometry: BendPhysicalGeometry
|
||||
bend_clip_margin: float | None
|
||||
node_limit: int
|
||||
guidance_seed: tuple[PathSegmentSeed, ...] | None = None
|
||||
guidance_bonus: float = 0.0
|
||||
frontier_trace: FrontierTraceCollector | None = None
|
||||
return_partial: bool = False
|
||||
store_expanded: bool = False
|
||||
skip_congestion: bool = False
|
||||
|
|
@ -33,6 +81,9 @@ class SearchRunConfig:
|
|||
*,
|
||||
bend_collision_type: BendCollisionModel | None = None,
|
||||
node_limit: int | None = None,
|
||||
guidance_seed: tuple[PathSegmentSeed, ...] | None = None,
|
||||
guidance_bonus: float = 0.0,
|
||||
frontier_trace: FrontierTraceCollector | None = None,
|
||||
return_partial: bool = False,
|
||||
store_expanded: bool = False,
|
||||
skip_congestion: bool = False,
|
||||
|
|
@ -49,6 +100,9 @@ class SearchRunConfig:
|
|||
bend_physical_geometry=bend_physical_geometry,
|
||||
bend_clip_margin=search.bend_clip_margin,
|
||||
node_limit=search.node_limit if node_limit is None else node_limit,
|
||||
guidance_seed=guidance_seed,
|
||||
guidance_bonus=float(guidance_bonus),
|
||||
frontier_trace=frontier_trace,
|
||||
return_partial=return_partial,
|
||||
store_expanded=store_expanded,
|
||||
skip_congestion=skip_congestion,
|
||||
|
|
@ -67,6 +121,7 @@ class AStarNode:
|
|||
"component_result",
|
||||
"base_move_cost",
|
||||
"cache_key",
|
||||
"seed_index",
|
||||
"congestion_resolved",
|
||||
)
|
||||
|
||||
|
|
@ -80,6 +135,7 @@ class AStarNode:
|
|||
*,
|
||||
base_move_cost: float = 0.0,
|
||||
cache_key: tuple | None = None,
|
||||
seed_index: int | None = None,
|
||||
congestion_resolved: bool = True,
|
||||
) -> None:
|
||||
self.port = port
|
||||
|
|
@ -90,6 +146,7 @@ class AStarNode:
|
|||
self.component_result = component_result
|
||||
self.base_move_cost = base_move_cost
|
||||
self.cache_key = cache_key
|
||||
self.seed_index = seed_index
|
||||
self.congestion_resolved = congestion_resolved
|
||||
|
||||
def __lt__(self, other: AStarNode) -> bool:
|
||||
|
|
@ -128,6 +185,14 @@ class AStarMetrics:
|
|||
"total_move_cache_abs_misses",
|
||||
"total_move_cache_rel_hits",
|
||||
"total_move_cache_rel_misses",
|
||||
"total_guidance_match_moves",
|
||||
"total_guidance_match_moves_straight",
|
||||
"total_guidance_match_moves_bend90",
|
||||
"total_guidance_match_moves_sbend",
|
||||
"total_guidance_bonus_applied",
|
||||
"total_guidance_bonus_applied_straight",
|
||||
"total_guidance_bonus_applied_bend90",
|
||||
"total_guidance_bonus_applied_sbend",
|
||||
"total_static_safe_cache_hits",
|
||||
"total_hard_collision_cache_hits",
|
||||
"total_congestion_cache_hits",
|
||||
|
|
@ -189,6 +254,10 @@ class AStarMetrics:
|
|||
"total_refinement_candidates_built",
|
||||
"total_refinement_candidates_verified",
|
||||
"total_refinement_candidates_accepted",
|
||||
"total_pair_local_search_pairs_considered",
|
||||
"total_pair_local_search_attempts",
|
||||
"total_pair_local_search_accepts",
|
||||
"total_pair_local_search_nodes_expanded",
|
||||
"last_expanded_nodes",
|
||||
"nodes_expanded",
|
||||
"moves_generated",
|
||||
|
|
@ -229,6 +298,14 @@ class AStarMetrics:
|
|||
self.total_move_cache_abs_misses = 0
|
||||
self.total_move_cache_rel_hits = 0
|
||||
self.total_move_cache_rel_misses = 0
|
||||
self.total_guidance_match_moves = 0
|
||||
self.total_guidance_match_moves_straight = 0
|
||||
self.total_guidance_match_moves_bend90 = 0
|
||||
self.total_guidance_match_moves_sbend = 0
|
||||
self.total_guidance_bonus_applied = 0.0
|
||||
self.total_guidance_bonus_applied_straight = 0.0
|
||||
self.total_guidance_bonus_applied_bend90 = 0.0
|
||||
self.total_guidance_bonus_applied_sbend = 0.0
|
||||
self.total_static_safe_cache_hits = 0
|
||||
self.total_hard_collision_cache_hits = 0
|
||||
self.total_congestion_cache_hits = 0
|
||||
|
|
@ -290,6 +367,10 @@ class AStarMetrics:
|
|||
self.total_refinement_candidates_built = 0
|
||||
self.total_refinement_candidates_verified = 0
|
||||
self.total_refinement_candidates_accepted = 0
|
||||
self.total_pair_local_search_pairs_considered = 0
|
||||
self.total_pair_local_search_attempts = 0
|
||||
self.total_pair_local_search_accepts = 0
|
||||
self.total_pair_local_search_nodes_expanded = 0
|
||||
self.last_expanded_nodes: list[tuple[int, int, int]] = []
|
||||
self.nodes_expanded = 0
|
||||
self.moves_generated = 0
|
||||
|
|
@ -329,6 +410,14 @@ class AStarMetrics:
|
|||
self.total_move_cache_abs_misses = 0
|
||||
self.total_move_cache_rel_hits = 0
|
||||
self.total_move_cache_rel_misses = 0
|
||||
self.total_guidance_match_moves = 0
|
||||
self.total_guidance_match_moves_straight = 0
|
||||
self.total_guidance_match_moves_bend90 = 0
|
||||
self.total_guidance_match_moves_sbend = 0
|
||||
self.total_guidance_bonus_applied = 0.0
|
||||
self.total_guidance_bonus_applied_straight = 0.0
|
||||
self.total_guidance_bonus_applied_bend90 = 0.0
|
||||
self.total_guidance_bonus_applied_sbend = 0.0
|
||||
self.total_static_safe_cache_hits = 0
|
||||
self.total_hard_collision_cache_hits = 0
|
||||
self.total_congestion_cache_hits = 0
|
||||
|
|
@ -390,6 +479,10 @@ class AStarMetrics:
|
|||
self.total_refinement_candidates_built = 0
|
||||
self.total_refinement_candidates_verified = 0
|
||||
self.total_refinement_candidates_accepted = 0
|
||||
self.total_pair_local_search_pairs_considered = 0
|
||||
self.total_pair_local_search_attempts = 0
|
||||
self.total_pair_local_search_accepts = 0
|
||||
self.total_pair_local_search_nodes_expanded = 0
|
||||
|
||||
def reset_per_route(self) -> None:
|
||||
self.nodes_expanded = 0
|
||||
|
|
@ -432,6 +525,14 @@ class AStarMetrics:
|
|||
move_cache_abs_misses=self.total_move_cache_abs_misses,
|
||||
move_cache_rel_hits=self.total_move_cache_rel_hits,
|
||||
move_cache_rel_misses=self.total_move_cache_rel_misses,
|
||||
guidance_match_moves=self.total_guidance_match_moves,
|
||||
guidance_match_moves_straight=self.total_guidance_match_moves_straight,
|
||||
guidance_match_moves_bend90=self.total_guidance_match_moves_bend90,
|
||||
guidance_match_moves_sbend=self.total_guidance_match_moves_sbend,
|
||||
guidance_bonus_applied=self.total_guidance_bonus_applied,
|
||||
guidance_bonus_applied_straight=self.total_guidance_bonus_applied_straight,
|
||||
guidance_bonus_applied_bend90=self.total_guidance_bonus_applied_bend90,
|
||||
guidance_bonus_applied_sbend=self.total_guidance_bonus_applied_sbend,
|
||||
static_safe_cache_hits=self.total_static_safe_cache_hits,
|
||||
hard_collision_cache_hits=self.total_hard_collision_cache_hits,
|
||||
congestion_cache_hits=self.total_congestion_cache_hits,
|
||||
|
|
@ -493,6 +594,10 @@ class AStarMetrics:
|
|||
refinement_candidates_built=self.total_refinement_candidates_built,
|
||||
refinement_candidates_verified=self.total_refinement_candidates_verified,
|
||||
refinement_candidates_accepted=self.total_refinement_candidates_accepted,
|
||||
pair_local_search_pairs_considered=self.total_pair_local_search_pairs_considered,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,23 @@ import time
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from inire.model import NetOrder, NetSpec, resolve_bend_geometry
|
||||
from inire.results import RoutingOutcome, RoutingReport, RoutingResult
|
||||
from inire.router._astar_types import AStarContext, AStarMetrics, SearchRunConfig
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
from inire.model import NetOrder, NetSpec, RoutingProblem, resolve_bend_geometry
|
||||
from inire.results import (
|
||||
ComponentConflictTrace,
|
||||
ConflictTraceEntry,
|
||||
FrontierPruneSample,
|
||||
NetConflictTrace,
|
||||
NetFrontierTrace,
|
||||
RoutingOutcome,
|
||||
RoutingReport,
|
||||
RoutingResult,
|
||||
)
|
||||
from inire.router._astar_types import AStarContext, AStarMetrics, FrontierTraceCollector, SearchRunConfig
|
||||
from inire.router._search import route_astar
|
||||
from inire.router._seed_materialization import materialize_path_seed
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.refiner import PathRefiner
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -17,6 +29,7 @@ if TYPE_CHECKING:
|
|||
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import PathVerificationDetail
|
||||
from inire.geometry.components import ComponentResult
|
||||
|
||||
|
||||
|
|
@ -46,12 +59,20 @@ class _IterationReview:
|
|||
completed_net_ids: set[str]
|
||||
total_dynamic_collisions: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _PairLocalTarget:
|
||||
net_ids: tuple[str, str]
|
||||
|
||||
|
||||
class PathFinder:
|
||||
__slots__ = (
|
||||
"context",
|
||||
"metrics",
|
||||
"refiner",
|
||||
"accumulated_expanded_nodes",
|
||||
"conflict_trace",
|
||||
"frontier_trace",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
|
@ -67,14 +88,23 @@ class PathFinder:
|
|||
self.context.cost_evaluator.danger_map.metrics = self.metrics
|
||||
self.refiner = PathRefiner(self.context)
|
||||
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
|
||||
self.conflict_trace: list[ConflictTraceEntry] = []
|
||||
self.frontier_trace: list[NetFrontierTrace] = []
|
||||
|
||||
def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None:
|
||||
all_geoms: list[Polygon] = []
|
||||
all_dilated: list[Polygon] = []
|
||||
for result in path:
|
||||
component_indexes: list[int] = []
|
||||
for component_index, result in enumerate(path):
|
||||
all_geoms.extend(result.collision_geometry)
|
||||
all_dilated.extend(result.dilated_collision_geometry)
|
||||
self.context.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||
component_indexes.extend([component_index] * len(result.collision_geometry))
|
||||
self.context.cost_evaluator.collision_engine.add_path(
|
||||
net_id,
|
||||
all_geoms,
|
||||
dilated_geometry=all_dilated,
|
||||
component_indexes=component_indexes,
|
||||
)
|
||||
|
||||
def _routing_order(
|
||||
self,
|
||||
|
|
@ -227,6 +257,493 @@ class PathFinder:
|
|||
state.results = dict(state.best_results)
|
||||
self._replace_installed_paths(state, state.results)
|
||||
|
||||
def _capture_conflict_trace_entry(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
*,
|
||||
stage: str,
|
||||
iteration: int | None,
|
||||
results: dict[str, RoutingResult],
|
||||
details_by_net: dict[str, PathVerificationDetail],
|
||||
review: _IterationReview,
|
||||
) -> None:
|
||||
if not self.context.options.diagnostics.capture_conflict_trace:
|
||||
return
|
||||
|
||||
nets = []
|
||||
for net_id in state.ordered_net_ids:
|
||||
result = results.get(net_id)
|
||||
if result is None:
|
||||
result = RoutingResult(net_id=net_id, path=(), reached_target=False)
|
||||
detail = details_by_net.get(net_id)
|
||||
component_conflicts = ()
|
||||
conflicting_net_ids = ()
|
||||
if detail is not None:
|
||||
conflicting_net_ids = detail.conflicting_net_ids
|
||||
component_conflicts = tuple(
|
||||
ComponentConflictTrace(
|
||||
other_net_id=other_net_id,
|
||||
self_component_index=self_component_index,
|
||||
other_component_index=other_component_index,
|
||||
)
|
||||
for self_component_index, other_net_id, other_component_index in detail.component_conflicts
|
||||
)
|
||||
nets.append(
|
||||
NetConflictTrace(
|
||||
net_id=net_id,
|
||||
outcome=result.outcome,
|
||||
reached_target=result.reached_target,
|
||||
report=result.report,
|
||||
conflicting_net_ids=tuple(conflicting_net_ids),
|
||||
component_conflicts=component_conflicts,
|
||||
)
|
||||
)
|
||||
|
||||
self.conflict_trace.append(
|
||||
ConflictTraceEntry(
|
||||
stage=stage, # type: ignore[arg-type]
|
||||
iteration=iteration,
|
||||
completed_net_ids=tuple(sorted(review.completed_net_ids)),
|
||||
conflict_edges=tuple(sorted(review.conflict_edges)),
|
||||
nets=tuple(nets),
|
||||
)
|
||||
)
|
||||
|
||||
def _build_frontier_hotspot_bounds(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
net_id: str,
|
||||
details_by_net: dict[str, PathVerificationDetail],
|
||||
) -> tuple[tuple[float, float, float, float], ...]:
|
||||
result = state.results.get(net_id)
|
||||
detail = details_by_net.get(net_id)
|
||||
if result is None or detail is None or not result.path:
|
||||
return ()
|
||||
|
||||
hotspot_bounds: list[tuple[float, float, float, float]] = []
|
||||
seen: set[tuple[float, float, float, float]] = set()
|
||||
margin = max(5.0, self.context.cost_evaluator.collision_engine.clearance * 2.0)
|
||||
|
||||
for self_component_index, other_net_id, other_component_index in detail.component_conflicts:
|
||||
other_result = state.results.get(other_net_id)
|
||||
if other_result is None or not other_result.path:
|
||||
continue
|
||||
if self_component_index >= len(result.path) or other_component_index >= len(other_result.path):
|
||||
continue
|
||||
left_component = result.path[self_component_index]
|
||||
right_component = other_result.path[other_component_index]
|
||||
overlap_found = False
|
||||
for left_poly in left_component.dilated_physical_geometry:
|
||||
for right_poly in right_component.dilated_physical_geometry:
|
||||
if not left_poly.intersects(right_poly) or left_poly.touches(right_poly):
|
||||
continue
|
||||
overlap = left_poly.intersection(right_poly)
|
||||
if overlap.is_empty:
|
||||
continue
|
||||
buffered = overlap.buffer(margin, join_style="mitre").bounds
|
||||
if buffered not in seen:
|
||||
seen.add(buffered)
|
||||
hotspot_bounds.append(buffered)
|
||||
overlap_found = True
|
||||
if overlap_found:
|
||||
continue
|
||||
|
||||
left_bounds = left_component.total_dilated_bounds
|
||||
right_bounds = right_component.total_dilated_bounds
|
||||
if (
|
||||
left_bounds[0] < right_bounds[2]
|
||||
and left_bounds[2] > right_bounds[0]
|
||||
and left_bounds[1] < right_bounds[3]
|
||||
and left_bounds[3] > right_bounds[1]
|
||||
):
|
||||
buffered = (
|
||||
max(left_bounds[0], right_bounds[0]) - margin,
|
||||
max(left_bounds[1], right_bounds[1]) - margin,
|
||||
min(left_bounds[2], right_bounds[2]) + margin,
|
||||
min(left_bounds[3], right_bounds[3]) + margin,
|
||||
)
|
||||
if buffered not in seen:
|
||||
seen.add(buffered)
|
||||
hotspot_bounds.append(buffered)
|
||||
|
||||
return tuple(hotspot_bounds)
|
||||
|
||||
def _analyze_results(
|
||||
self,
|
||||
ordered_net_ids: Sequence[str],
|
||||
results: dict[str, RoutingResult],
|
||||
*,
|
||||
capture_component_conflicts: bool,
|
||||
count_iteration_metrics: bool,
|
||||
) -> tuple[dict[str, RoutingResult], dict[str, PathVerificationDetail], _IterationReview]:
|
||||
if count_iteration_metrics:
|
||||
self.metrics.total_iteration_reverify_calls += 1
|
||||
conflict_edges: set[tuple[str, str]] = set()
|
||||
conflicting_nets: set[str] = set()
|
||||
completed_net_ids: set[str] = set()
|
||||
total_dynamic_collisions = 0
|
||||
analyzed_results = dict(results)
|
||||
details_by_net: dict[str, PathVerificationDetail] = {}
|
||||
|
||||
for net_id in ordered_net_ids:
|
||||
result = results.get(net_id)
|
||||
if not result or not result.path or not result.reached_target:
|
||||
continue
|
||||
|
||||
if count_iteration_metrics:
|
||||
self.metrics.total_iteration_reverified_nets += 1
|
||||
detail = self.context.cost_evaluator.collision_engine.verify_path_details(
|
||||
net_id,
|
||||
result.path,
|
||||
capture_component_conflicts=capture_component_conflicts,
|
||||
)
|
||||
details_by_net[net_id] = detail
|
||||
analyzed_results[net_id] = RoutingResult(
|
||||
net_id=net_id,
|
||||
path=result.path,
|
||||
reached_target=result.reached_target,
|
||||
report=detail.report,
|
||||
)
|
||||
total_dynamic_collisions += detail.report.dynamic_collision_count
|
||||
if analyzed_results[net_id].outcome == "completed":
|
||||
completed_net_ids.add(net_id)
|
||||
if not detail.conflicting_net_ids:
|
||||
continue
|
||||
conflicting_nets.add(net_id)
|
||||
for other_net_id in detail.conflicting_net_ids:
|
||||
conflicting_nets.add(other_net_id)
|
||||
if other_net_id == net_id:
|
||||
continue
|
||||
conflict_edges.add(tuple(sorted((net_id, other_net_id))))
|
||||
|
||||
if count_iteration_metrics:
|
||||
self.metrics.total_iteration_conflicting_nets += len(conflicting_nets)
|
||||
self.metrics.total_iteration_conflict_edges += len(conflict_edges)
|
||||
return (
|
||||
analyzed_results,
|
||||
details_by_net,
|
||||
_IterationReview(
|
||||
conflicting_nets=conflicting_nets,
|
||||
conflict_edges=conflict_edges,
|
||||
completed_net_ids=completed_net_ids,
|
||||
total_dynamic_collisions=total_dynamic_collisions,
|
||||
),
|
||||
)
|
||||
|
||||
def _capture_frontier_trace(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
final_results: dict[str, RoutingResult],
|
||||
) -> None:
|
||||
if not self.context.options.diagnostics.capture_frontier_trace:
|
||||
return
|
||||
|
||||
self.frontier_trace = []
|
||||
state.results = dict(final_results)
|
||||
state.results, details_by_net, _ = self._analyze_results(
|
||||
state.ordered_net_ids,
|
||||
state.results,
|
||||
capture_component_conflicts=True,
|
||||
count_iteration_metrics=False,
|
||||
)
|
||||
|
||||
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:
|
||||
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.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,
|
||||
candidate_results: dict[str, RoutingResult],
|
||||
candidate_review: _IterationReview,
|
||||
incumbent_results: dict[str, RoutingResult],
|
||||
incumbent_review: _IterationReview,
|
||||
) -> bool:
|
||||
candidate_completed = len(candidate_review.completed_net_ids)
|
||||
incumbent_completed = len(incumbent_review.completed_net_ids)
|
||||
if candidate_completed != incumbent_completed:
|
||||
return candidate_completed > incumbent_completed
|
||||
|
||||
candidate_edges = len(candidate_review.conflict_edges)
|
||||
incumbent_edges = len(incumbent_review.conflict_edges)
|
||||
if candidate_edges != incumbent_edges:
|
||||
return candidate_edges < incumbent_edges
|
||||
|
||||
if candidate_review.total_dynamic_collisions != incumbent_review.total_dynamic_collisions:
|
||||
return candidate_review.total_dynamic_collisions < incumbent_review.total_dynamic_collisions
|
||||
|
||||
candidate_length = sum(
|
||||
result.report.total_length
|
||||
for result in candidate_results.values()
|
||||
if result.reached_target
|
||||
)
|
||||
incumbent_length = sum(
|
||||
result.report.total_length
|
||||
for result in incumbent_results.values()
|
||||
if result.reached_target
|
||||
)
|
||||
if abs(candidate_length - incumbent_length) > 1e-6:
|
||||
return candidate_length < incumbent_length
|
||||
return False
|
||||
|
||||
def _collect_pair_local_targets(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
results: dict[str, RoutingResult],
|
||||
review: _IterationReview,
|
||||
) -> list[_PairLocalTarget]:
|
||||
if not review.conflict_edges:
|
||||
return []
|
||||
order_index = {net_id: idx for idx, net_id in enumerate(state.ordered_net_ids)}
|
||||
seen_net_ids: set[str] = set()
|
||||
targets: list[_PairLocalTarget] = []
|
||||
for left_net_id, right_net_id in sorted(review.conflict_edges):
|
||||
if left_net_id in seen_net_ids or right_net_id in seen_net_ids:
|
||||
return []
|
||||
left_result = results.get(left_net_id)
|
||||
right_result = results.get(right_net_id)
|
||||
if (
|
||||
left_result is None
|
||||
or right_result is None
|
||||
or not left_result.reached_target
|
||||
or not right_result.reached_target
|
||||
):
|
||||
continue
|
||||
seen_net_ids.update((left_net_id, right_net_id))
|
||||
targets.append(_PairLocalTarget(net_ids=(left_net_id, right_net_id)))
|
||||
targets.sort(key=lambda target: min(order_index[target.net_ids[0]], order_index[target.net_ids[1]]))
|
||||
return targets
|
||||
|
||||
def _build_pair_local_context(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
incumbent_results: dict[str, RoutingResult],
|
||||
pair_net_ids: tuple[str, str],
|
||||
) -> AStarContext:
|
||||
problem = self.context.problem
|
||||
objective = self.context.options.objective
|
||||
static_obstacles = tuple(self.context.cost_evaluator.collision_engine._static_obstacles.geometries.values())
|
||||
engine = RoutingWorld(
|
||||
clearance=self.context.cost_evaluator.collision_engine.clearance,
|
||||
safety_zone_radius=self.context.cost_evaluator.collision_engine.safety_zone_radius,
|
||||
)
|
||||
for obstacle in static_obstacles:
|
||||
engine.add_static_obstacle(obstacle)
|
||||
for net_id in state.ordered_net_ids:
|
||||
if net_id in pair_net_ids:
|
||||
continue
|
||||
result = incumbent_results.get(net_id)
|
||||
if result is None or not result.path:
|
||||
continue
|
||||
for component in result.path:
|
||||
for polygon in component.physical_geometry:
|
||||
engine.add_static_obstacle(polygon)
|
||||
|
||||
danger_map = DangerMap(bounds=problem.bounds)
|
||||
danger_map.precompute(list(static_obstacles))
|
||||
evaluator = CostEvaluator(
|
||||
engine,
|
||||
danger_map,
|
||||
unit_length_cost=objective.unit_length_cost,
|
||||
greedy_h_weight=self.context.cost_evaluator.greedy_h_weight,
|
||||
bend_penalty=objective.bend_penalty,
|
||||
sbend_penalty=objective.sbend_penalty,
|
||||
danger_weight=objective.danger_weight,
|
||||
)
|
||||
return AStarContext(
|
||||
evaluator,
|
||||
RoutingProblem(
|
||||
bounds=problem.bounds,
|
||||
nets=tuple(state.net_specs[net_id] for net_id in state.ordered_net_ids),
|
||||
static_obstacles=static_obstacles,
|
||||
clearance=problem.clearance,
|
||||
safety_zone_radius=problem.safety_zone_radius,
|
||||
),
|
||||
self.context.options,
|
||||
metrics=AStarMetrics(),
|
||||
)
|
||||
|
||||
def _run_pair_local_attempt(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
incumbent_results: dict[str, RoutingResult],
|
||||
pair_order: tuple[str, str],
|
||||
) -> tuple[dict[str, RoutingResult], int] | None:
|
||||
local_context = self._build_pair_local_context(state, incumbent_results, pair_order)
|
||||
local_results = dict(incumbent_results)
|
||||
|
||||
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)
|
||||
|
||||
run_config = SearchRunConfig.from_options(
|
||||
self.context.options,
|
||||
return_partial=False,
|
||||
skip_congestion=True,
|
||||
self_collision_check=(net_id in state.needs_self_collision_check),
|
||||
guidance_seed=guidance_seed,
|
||||
guidance_bonus=guidance_bonus,
|
||||
node_limit=self.context.options.search.node_limit,
|
||||
)
|
||||
path = route_astar(
|
||||
net.start,
|
||||
net.target,
|
||||
net.width,
|
||||
context=local_context,
|
||||
metrics=local_context.metrics,
|
||||
net_id=net_id,
|
||||
config=run_config,
|
||||
)
|
||||
if not path or path[-1].end_port != net.target:
|
||||
return None
|
||||
|
||||
report = local_context.cost_evaluator.collision_engine.verify_path_report(net_id, path)
|
||||
if not report.is_valid:
|
||||
return None
|
||||
local_results[net_id] = RoutingResult(
|
||||
net_id=net_id,
|
||||
path=tuple(path),
|
||||
reached_target=True,
|
||||
report=report,
|
||||
)
|
||||
for component in path:
|
||||
for polygon in component.physical_geometry:
|
||||
local_context.cost_evaluator.collision_engine.add_static_obstacle(polygon)
|
||||
local_context.clear_static_caches()
|
||||
|
||||
return local_results, local_context.metrics.total_nodes_expanded
|
||||
|
||||
def _run_pair_local_search(self, state: _RoutingState) -> None:
|
||||
state.results, _details_by_net, review = self._analyze_results(
|
||||
state.ordered_net_ids,
|
||||
state.results,
|
||||
capture_component_conflicts=True,
|
||||
count_iteration_metrics=False,
|
||||
)
|
||||
targets = self._collect_pair_local_targets(state, state.results, review)
|
||||
if not targets:
|
||||
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)
|
||||
|
||||
def _route_net_once(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
|
|
@ -246,16 +763,25 @@ class PathFinder:
|
|||
else:
|
||||
coll_model, _ = resolve_bend_geometry(search)
|
||||
skip_congestion = False
|
||||
guidance_seed = None
|
||||
guidance_bonus = 0.0
|
||||
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)
|
||||
|
||||
run_config = SearchRunConfig.from_options(
|
||||
self.context.options,
|
||||
bend_collision_type=coll_model,
|
||||
return_partial=True,
|
||||
store_expanded=diagnostics.capture_expanded,
|
||||
guidance_seed=guidance_seed,
|
||||
guidance_bonus=guidance_bonus,
|
||||
skip_congestion=skip_congestion,
|
||||
self_collision_check=(net_id in state.needs_self_collision_check),
|
||||
node_limit=search.node_limit,
|
||||
|
|
@ -303,7 +829,6 @@ class PathFinder:
|
|||
congestion = self.context.options.congestion
|
||||
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)
|
||||
|
|
@ -326,45 +851,21 @@ class PathFinder:
|
|||
return review
|
||||
|
||||
def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview:
|
||||
self.metrics.total_iteration_reverify_calls += 1
|
||||
conflict_edges: set[tuple[str, str]] = set()
|
||||
conflicting_nets: set[str] = set()
|
||||
completed_net_ids: set[str] = set()
|
||||
total_dynamic_collisions = 0
|
||||
|
||||
for net_id in state.ordered_net_ids:
|
||||
result = state.results.get(net_id)
|
||||
if not result or not result.path or not result.reached_target:
|
||||
continue
|
||||
|
||||
self.metrics.total_iteration_reverified_nets += 1
|
||||
detail = self.context.cost_evaluator.collision_engine.verify_path_details(net_id, result.path)
|
||||
state.results[net_id] = RoutingResult(
|
||||
net_id=net_id,
|
||||
path=result.path,
|
||||
reached_target=result.reached_target,
|
||||
report=detail.report,
|
||||
state.results, details_by_net, review = self._analyze_results(
|
||||
state.ordered_net_ids,
|
||||
state.results,
|
||||
capture_component_conflicts=self.context.options.diagnostics.capture_conflict_trace,
|
||||
count_iteration_metrics=True,
|
||||
)
|
||||
total_dynamic_collisions += detail.report.dynamic_collision_count
|
||||
if state.results[net_id].outcome == "completed":
|
||||
completed_net_ids.add(net_id)
|
||||
if not detail.conflicting_net_ids:
|
||||
continue
|
||||
conflicting_nets.add(net_id)
|
||||
for other_net_id in detail.conflicting_net_ids:
|
||||
conflicting_nets.add(other_net_id)
|
||||
if other_net_id == net_id:
|
||||
continue
|
||||
conflict_edges.add(tuple(sorted((net_id, other_net_id))))
|
||||
|
||||
self.metrics.total_iteration_conflicting_nets += len(conflicting_nets)
|
||||
self.metrics.total_iteration_conflict_edges += len(conflict_edges)
|
||||
return _IterationReview(
|
||||
conflicting_nets=conflicting_nets,
|
||||
conflict_edges=conflict_edges,
|
||||
completed_net_ids=completed_net_ids,
|
||||
total_dynamic_collisions=total_dynamic_collisions,
|
||||
self._capture_conflict_trace_entry(
|
||||
state,
|
||||
stage="iteration",
|
||||
iteration=self.metrics.total_route_iterations - 1,
|
||||
results=state.results,
|
||||
details_by_net=details_by_net,
|
||||
review=review,
|
||||
)
|
||||
return review
|
||||
|
||||
def _run_iterations(
|
||||
self,
|
||||
|
|
@ -428,17 +929,38 @@ class PathFinder:
|
|||
|
||||
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
|
||||
final_results: dict[str, RoutingResult] = {}
|
||||
details_by_net: dict[str, PathVerificationDetail] = {}
|
||||
for net in self.context.problem.nets:
|
||||
result = state.results.get(net.net_id)
|
||||
if not result or not result.path:
|
||||
final_results[net.net_id] = RoutingResult(net_id=net.net_id, path=(), reached_target=False)
|
||||
continue
|
||||
report = self.context.cost_evaluator.collision_engine.verify_path_report(net.net_id, result.path)
|
||||
detail = self.context.cost_evaluator.collision_engine.verify_path_details(
|
||||
net.net_id,
|
||||
result.path,
|
||||
capture_component_conflicts=self.context.options.diagnostics.capture_conflict_trace,
|
||||
)
|
||||
details_by_net[net.net_id] = detail
|
||||
final_results[net.net_id] = RoutingResult(
|
||||
net_id=net.net_id,
|
||||
path=result.path,
|
||||
reached_target=result.reached_target,
|
||||
report=report,
|
||||
report=detail.report,
|
||||
)
|
||||
if self.context.options.diagnostics.capture_conflict_trace:
|
||||
_, _, review = self._analyze_results(
|
||||
state.ordered_net_ids,
|
||||
final_results,
|
||||
capture_component_conflicts=True,
|
||||
count_iteration_metrics=False,
|
||||
)
|
||||
self._capture_conflict_trace_entry(
|
||||
state,
|
||||
stage="final",
|
||||
iteration=None,
|
||||
results=final_results,
|
||||
details_by_net=details_by_net,
|
||||
review=review,
|
||||
)
|
||||
return final_results
|
||||
|
||||
|
|
@ -449,6 +971,8 @@ class PathFinder:
|
|||
) -> dict[str, RoutingResult]:
|
||||
self.context.congestion_penalty = self.context.options.congestion.base_penalty
|
||||
self.accumulated_expanded_nodes = []
|
||||
self.conflict_trace = []
|
||||
self.frontier_trace = []
|
||||
self.metrics.reset_totals()
|
||||
self.metrics.reset_per_route()
|
||||
|
||||
|
|
@ -456,9 +980,29 @@ 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,
|
||||
)
|
||||
|
||||
if timed_out:
|
||||
return self._verify_results(state)
|
||||
final_results = self._verify_results(state)
|
||||
self._capture_frontier_trace(state, final_results)
|
||||
return final_results
|
||||
|
||||
self._run_pair_local_search(state)
|
||||
self._refine_results(state)
|
||||
return self._verify_results(state)
|
||||
final_results = self._verify_results(state)
|
||||
self._capture_frontier_trace(state, final_results)
|
||||
return final_results
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ def route_astar(
|
|||
start,
|
||||
0.0,
|
||||
context.cost_evaluator.h_manhattan(start, target, min_bend_radius=context.min_bend_radius),
|
||||
seed_index=0 if config.guidance_seed else None,
|
||||
)
|
||||
heapq.heappush(open_set, start_node)
|
||||
best_node = start_node
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from inire import (
|
|||
RoutingOptions,
|
||||
RoutingProblem,
|
||||
RoutingResult,
|
||||
RoutingRunResult,
|
||||
SearchOptions,
|
||||
)
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
|
|
@ -34,6 +35,7 @@ _OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__)
|
|||
ScenarioOutcome = tuple[float, int, int, int]
|
||||
ScenarioRun = Callable[[], ScenarioOutcome]
|
||||
ScenarioSnapshotRun = Callable[[], "ScenarioSnapshot"]
|
||||
TraceScenarioRun = Callable[[], RoutingRunResult]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -79,6 +81,19 @@ def _make_snapshot(
|
|||
)
|
||||
|
||||
|
||||
def _make_run_result(
|
||||
results: dict[str, RoutingResult],
|
||||
pathfinder: PathFinder,
|
||||
) -> RoutingRunResult:
|
||||
return RoutingRunResult(
|
||||
results_by_net=results,
|
||||
metrics=pathfinder.metrics.snapshot(),
|
||||
expanded_nodes=tuple(pathfinder.accumulated_expanded_nodes),
|
||||
conflict_trace=tuple(pathfinder.conflict_trace),
|
||||
frontier_trace=tuple(pathfinder.frontier_trace),
|
||||
)
|
||||
|
||||
|
||||
def _sum_metrics(metrics_list: tuple[RouteMetrics, ...]) -> RouteMetrics:
|
||||
metric_names = RouteMetrics.__dataclass_fields__
|
||||
return RouteMetrics(
|
||||
|
|
@ -318,6 +333,24 @@ def run_example_05() -> ScenarioOutcome:
|
|||
return snapshot_example_05().as_outcome()
|
||||
|
||||
|
||||
def trace_example_05() -> RoutingRunResult:
|
||||
netlist = {
|
||||
"u_turn": (Port(50, 50, 0), Port(50, 70, 180)),
|
||||
"loop": (Port(100, 100, 90), Port(100, 80, 270)),
|
||||
"zig_zag": (Port(20, 150, 0), Port(180, 150, 0)),
|
||||
}
|
||||
widths = dict.fromkeys(netlist, 2.0)
|
||||
_, _, _, pathfinder = _build_routing_stack(
|
||||
bounds=(0, 0, 200, 200),
|
||||
netlist=netlist,
|
||||
widths=widths,
|
||||
evaluator_kwargs={"bend_penalty": 50.0},
|
||||
request_kwargs={"bend_radii": [20.0], "capture_conflict_trace": True, "capture_frontier_trace": True},
|
||||
)
|
||||
results = pathfinder.route_all()
|
||||
return _make_run_result(results, pathfinder)
|
||||
|
||||
|
||||
def snapshot_example_06() -> ScenarioSnapshot:
|
||||
bounds = (-20, -20, 170, 170)
|
||||
obstacles = [
|
||||
|
|
@ -391,6 +424,14 @@ def snapshot_example_07_no_warm_start() -> ScenarioSnapshot:
|
|||
)
|
||||
|
||||
|
||||
def trace_example_07() -> RoutingRunResult:
|
||||
return _trace_example_07_variant(warm_start_enabled=True)
|
||||
|
||||
|
||||
def trace_example_07_no_warm_start() -> RoutingRunResult:
|
||||
return _trace_example_07_variant(warm_start_enabled=False)
|
||||
|
||||
|
||||
def _snapshot_example_07_variant(
|
||||
name: str,
|
||||
*,
|
||||
|
|
@ -454,6 +495,68 @@ def _snapshot_example_07_variant(
|
|||
return _make_snapshot(name, results, t1 - t0, pathfinder.metrics.snapshot())
|
||||
|
||||
|
||||
def _trace_example_07_variant(
|
||||
*,
|
||||
warm_start_enabled: bool,
|
||||
) -> RoutingRunResult:
|
||||
bounds = (0, 0, 1000, 1000)
|
||||
obstacles = [
|
||||
box(450, 0, 550, 400),
|
||||
box(450, 600, 550, 1000),
|
||||
]
|
||||
num_nets = 10
|
||||
start_x = 50
|
||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||
end_x = 950
|
||||
end_y_base = 100
|
||||
end_y_pitch = 800.0 / (num_nets - 1)
|
||||
|
||||
netlist = {}
|
||||
for index in range(num_nets):
|
||||
sy = int(round(start_y_base + index * 10.0))
|
||||
ey = int(round(end_y_base + index * end_y_pitch))
|
||||
netlist[f"net_{index:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
||||
widths = dict.fromkeys(netlist, 2.0)
|
||||
_, evaluator, metrics, pathfinder = _build_routing_stack(
|
||||
bounds=bounds,
|
||||
netlist=netlist,
|
||||
widths=widths,
|
||||
clearance=6.0,
|
||||
obstacles=obstacles,
|
||||
evaluator_kwargs={
|
||||
"greedy_h_weight": 1.5,
|
||||
"unit_length_cost": 0.1,
|
||||
"bend_penalty": 100.0,
|
||||
"sbend_penalty": 400.0,
|
||||
},
|
||||
request_kwargs={
|
||||
"node_limit": 2000000,
|
||||
"bend_radii": [50.0],
|
||||
"sbend_radii": [50.0],
|
||||
"bend_clip_margin": 10.0,
|
||||
"max_iterations": 15,
|
||||
"base_penalty": 100.0,
|
||||
"multiplier": 1.4,
|
||||
"net_order": "shortest",
|
||||
"capture_expanded": True,
|
||||
"capture_conflict_trace": True,
|
||||
"capture_frontier_trace": True,
|
||||
"shuffle_nets": True,
|
||||
"seed": 42,
|
||||
"warm_start_enabled": warm_start_enabled,
|
||||
},
|
||||
)
|
||||
|
||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
||||
_ = current_results
|
||||
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
||||
evaluator.greedy_h_weight = new_greedy
|
||||
metrics.reset_per_route()
|
||||
|
||||
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||
return _make_run_result(results, pathfinder)
|
||||
|
||||
|
||||
def run_example_07() -> ScenarioOutcome:
|
||||
return snapshot_example_07().as_outcome()
|
||||
|
||||
|
|
@ -557,6 +660,15 @@ PERFORMANCE_SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
|
|||
("example_07_large_scale_routing_no_warm_start", snapshot_example_07_no_warm_start),
|
||||
)
|
||||
|
||||
TRACE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = (
|
||||
("example_05_orientation_stress", trace_example_05),
|
||||
("example_07_large_scale_routing", trace_example_07),
|
||||
)
|
||||
|
||||
TRACE_PERFORMANCE_SCENARIO_RUNS: tuple[tuple[str, TraceScenarioRun], ...] = (
|
||||
("example_07_large_scale_routing_no_warm_start", trace_example_07_no_warm_start),
|
||||
)
|
||||
|
||||
|
||||
def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]:
|
||||
return tuple(run() for _, run in SCENARIO_SNAPSHOTS)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ from inire.router._astar_types import AStarContext
|
|||
from inire.router._router import PathFinder, _IterationReview
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
|
||||
|
||||
def test_root_module_exports_only_stable_surface() -> None:
|
||||
import inire
|
||||
|
||||
|
|
@ -54,6 +52,8 @@ def test_route_problem_smoke() -> None:
|
|||
|
||||
assert set(run.results_by_net) == {"net1"}
|
||||
assert run.results_by_net["net1"].is_valid
|
||||
assert run.conflict_trace == ()
|
||||
assert run.frontier_trace == ()
|
||||
|
||||
|
||||
def test_route_problem_supports_configs_and_debug_data() -> None:
|
||||
|
|
@ -94,6 +94,14 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
|
|||
assert run.metrics.dynamic_tree_rebuilds >= 0
|
||||
assert run.metrics.visibility_corner_index_builds >= 0
|
||||
assert run.metrics.visibility_builds >= 0
|
||||
assert run.metrics.guidance_match_moves >= 0
|
||||
assert run.metrics.guidance_match_moves_straight >= 0
|
||||
assert run.metrics.guidance_match_moves_bend90 >= 0
|
||||
assert run.metrics.guidance_match_moves_sbend >= 0
|
||||
assert run.metrics.guidance_bonus_applied >= 0.0
|
||||
assert run.metrics.guidance_bonus_applied_straight >= 0.0
|
||||
assert run.metrics.guidance_bonus_applied_bend90 >= 0.0
|
||||
assert run.metrics.guidance_bonus_applied_sbend >= 0.0
|
||||
assert run.metrics.congestion_grid_span_cache_hits >= 0
|
||||
assert run.metrics.congestion_grid_span_cache_misses >= 0
|
||||
assert run.metrics.congestion_presence_cache_hits >= 0
|
||||
|
|
@ -112,6 +120,10 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
|
|||
assert run.metrics.congestion_candidate_ids >= 0
|
||||
assert run.metrics.verify_dynamic_candidate_nets >= 0
|
||||
assert run.metrics.verify_path_report_calls >= 0
|
||||
assert run.metrics.pair_local_search_pairs_considered >= 0
|
||||
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
|
||||
|
||||
|
||||
def test_iteration_callback_observes_reverified_conflicts() -> None:
|
||||
|
|
@ -141,6 +153,120 @@ def test_iteration_callback_observes_reverified_conflicts() -> None:
|
|||
assert results["vertical"].outcome == "colliding"
|
||||
|
||||
|
||||
def test_capture_conflict_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_conflict_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 [entry.stage for entry in run_with_trace.conflict_trace] == ["iteration", "restored_best", "final"]
|
||||
|
||||
|
||||
def test_capture_conflict_trace_records_component_pairs() -> 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),
|
||||
),
|
||||
)
|
||||
options = RoutingOptions(
|
||||
congestion=CongestionOptions(max_iterations=1, warm_start_enabled=False),
|
||||
refinement=RefinementOptions(enabled=False),
|
||||
diagnostics=DiagnosticsOptions(capture_conflict_trace=True),
|
||||
)
|
||||
|
||||
run = route(problem, options=options)
|
||||
final_entry = run.conflict_trace[-1]
|
||||
trace_by_net = {net.net_id: net for net in final_entry.nets}
|
||||
|
||||
assert final_entry.stage == "final"
|
||||
assert final_entry.conflict_edges == (("horizontal", "vertical"),)
|
||||
assert trace_by_net["horizontal"].component_conflicts[0].other_net_id == "vertical"
|
||||
assert trace_by_net["horizontal"].component_conflicts[0].self_component_index == 0
|
||||
assert trace_by_net["horizontal"].component_conflicts[0].other_component_index == 0
|
||||
assert trace_by_net["vertical"].component_conflicts[0].other_net_id == "horizontal"
|
||||
assert trace_by_net["vertical"].component_conflicts[0].self_component_index == 0
|
||||
assert trace_by_net["vertical"].component_conflicts[0].other_component_index == 0
|
||||
|
||||
|
||||
def test_capture_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_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 {trace.net_id for trace in run_with_trace.frontier_trace} == {"horizontal", "vertical"}
|
||||
|
||||
|
||||
def test_capture_frontier_trace_records_prune_reasons() -> 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_frontier_trace=True),
|
||||
),
|
||||
)
|
||||
|
||||
trace_by_net = {entry.net_id: entry for entry in run.frontier_trace}
|
||||
assert trace_by_net["horizontal"].hotspot_bounds
|
||||
assert (
|
||||
trace_by_net["horizontal"].pruned_closed_set
|
||||
+ trace_by_net["horizontal"].pruned_hard_collision
|
||||
+ trace_by_net["horizontal"].pruned_self_collision
|
||||
+ trace_by_net["horizontal"].pruned_cost
|
||||
) > 0
|
||||
assert trace_by_net["horizontal"].samples
|
||||
|
||||
|
||||
def test_reverify_iterations_stop_early_on_stalled_conflict_graph() -> None:
|
||||
problem = RoutingProblem(
|
||||
bounds=(0, 0, 100, 100),
|
||||
|
|
@ -254,6 +380,56 @@ def test_route_all_restores_best_iteration_snapshot_on_timeout(monkeypatch: pyte
|
|||
assert results["netA"].outcome == "completed"
|
||||
|
||||
|
||||
def test_capture_conflict_trace_records_restored_best_stage(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(
|
||||
congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False),
|
||||
refinement=RefinementOptions(enabled=False),
|
||||
diagnostics=DiagnosticsOptions(capture_conflict_trace=True),
|
||||
)
|
||||
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
|
||||
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
|
||||
best_result = RoutingResult(
|
||||
net_id="netA",
|
||||
path=(Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),),
|
||||
reached_target=True,
|
||||
report=RoutingReport(),
|
||||
)
|
||||
worse_result = RoutingResult(net_id="netA", path=(), reached_target=False)
|
||||
|
||||
def fake_run_iteration(self, state, iteration, reroute_net_ids, iteration_callback):
|
||||
_ = self
|
||||
_ = reroute_net_ids
|
||||
_ = iteration_callback
|
||||
if iteration == 0:
|
||||
state.results = {"netA": best_result}
|
||||
return _IterationReview(
|
||||
conflicting_nets=set(),
|
||||
conflict_edges=set(),
|
||||
completed_net_ids={"netA"},
|
||||
total_dynamic_collisions=0,
|
||||
)
|
||||
state.results = {"netA": worse_result}
|
||||
return _IterationReview(
|
||||
conflicting_nets=set(),
|
||||
conflict_edges=set(),
|
||||
completed_net_ids=set(),
|
||||
total_dynamic_collisions=0,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration)
|
||||
|
||||
pathfinder.route_all()
|
||||
|
||||
assert [entry.stage for entry in pathfinder.conflict_trace] == [
|
||||
"restored_best",
|
||||
"final",
|
||||
]
|
||||
restored_entry = pathfinder.conflict_trace[0]
|
||||
assert restored_entry.nets[0].outcome == "completed"
|
||||
def test_route_problem_locked_routes_become_static_obstacles() -> None:
|
||||
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
|
||||
problem = RoutingProblem(
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ import math
|
|||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire import RoutingProblem, RoutingOptions, RoutingResult, SearchOptions
|
||||
from inire import CongestionOptions, NetSpec, RoutingProblem, RoutingOptions, RoutingResult, SearchOptions
|
||||
from inire.geometry.components import Bend90, Straight
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router._astar_types import AStarContext, AStarNode, SearchRunConfig
|
||||
from inire.router._astar_admission import add_node
|
||||
from inire.router._astar_moves import (
|
||||
_distance_to_bounds_in_heading,
|
||||
_should_cap_straights_to_bounds,
|
||||
)
|
||||
from inire.router._router import PathFinder, _RoutingState
|
||||
from inire.router._search import route_astar
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.seeds import StraightSeed
|
||||
|
||||
BOUNDS = (0, -50, 150, 150)
|
||||
|
||||
|
|
@ -214,6 +220,84 @@ def test_astar_context_keeps_evaluator_weights_separate(basic_evaluator: CostEva
|
|||
assert basic_evaluator.h_manhattan(Port(0, 0, 0), Port(10, 10, 0)) > 0.0
|
||||
|
||||
|
||||
def test_distance_to_bounds_in_heading_is_directional() -> None:
|
||||
bounds = (0, 0, 100, 200)
|
||||
|
||||
assert _distance_to_bounds_in_heading(Port(20, 30, 0), bounds) == pytest.approx(80.0)
|
||||
assert _distance_to_bounds_in_heading(Port(20, 30, 90), bounds) == pytest.approx(170.0)
|
||||
assert _distance_to_bounds_in_heading(Port(20, 30, 180), bounds) == pytest.approx(20.0)
|
||||
assert _distance_to_bounds_in_heading(Port(20, 30, 270), bounds) == pytest.approx(30.0)
|
||||
|
||||
|
||||
def test_should_cap_straights_to_bounds_only_for_large_no_warm_runs(basic_evaluator: CostEvaluator) -> None:
|
||||
large_context = AStarContext(
|
||||
basic_evaluator,
|
||||
RoutingProblem(
|
||||
bounds=(0, 0, 1000, 1000),
|
||||
nets=tuple(
|
||||
NetSpec(f"net{i}", Port(0, i * 10, 0), Port(10, i * 10, 0), width=2.0)
|
||||
for i in range(8)
|
||||
),
|
||||
),
|
||||
RoutingOptions(
|
||||
congestion=CongestionOptions(warm_start_enabled=False),
|
||||
),
|
||||
)
|
||||
small_context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||
|
||||
assert _should_cap_straights_to_bounds(large_context)
|
||||
assert not _should_cap_straights_to_bounds(small_context)
|
||||
|
||||
|
||||
def test_pair_local_context_clones_live_static_obstacles() -> None:
|
||||
obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)])
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
engine.add_static_obstacle(obstacle)
|
||||
danger_map = DangerMap(bounds=BOUNDS)
|
||||
danger_map.precompute([obstacle])
|
||||
evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0)
|
||||
finder = PathFinder(
|
||||
AStarContext(
|
||||
evaluator,
|
||||
RoutingProblem(
|
||||
bounds=BOUNDS,
|
||||
nets=(
|
||||
NetSpec("pair_a", Port(0, 0, 0), Port(60, 0, 0), width=2.0),
|
||||
NetSpec("pair_b", Port(0, 10, 0), Port(60, 10, 0), width=2.0),
|
||||
),
|
||||
),
|
||||
RoutingOptions(),
|
||||
)
|
||||
)
|
||||
state = _RoutingState(
|
||||
net_specs={
|
||||
"pair_a": NetSpec("pair_a", Port(0, 0, 0), Port(60, 0, 0), width=2.0),
|
||||
"pair_b": NetSpec("pair_b", Port(0, 10, 0), Port(60, 10, 0), width=2.0),
|
||||
},
|
||||
ordered_net_ids=["pair_a", "pair_b"],
|
||||
results={},
|
||||
needs_self_collision_check=set(),
|
||||
start_time=0.0,
|
||||
timeout_s=1.0,
|
||||
initial_paths=None,
|
||||
accumulated_expanded_nodes=[],
|
||||
best_results={},
|
||||
best_completed_nets=-1,
|
||||
best_conflict_edges=10**9,
|
||||
best_dynamic_collisions=10**9,
|
||||
last_conflict_signature=(),
|
||||
last_conflict_edge_count=0,
|
||||
repeated_conflict_count=0,
|
||||
)
|
||||
|
||||
local_context = finder._build_pair_local_context(state, {}, ("pair_a", "pair_b"))
|
||||
|
||||
assert finder.context.problem.static_obstacles == ()
|
||||
assert len(local_context.problem.static_obstacles) == 1
|
||||
assert len(local_context.cost_evaluator.collision_engine._static_obstacles.geometries) == 1
|
||||
assert next(iter(local_context.problem.static_obstacles)).equals(obstacle)
|
||||
|
||||
|
||||
def test_route_astar_bend_collision_override_does_not_persist(basic_evaluator: CostEvaluator) -> None:
|
||||
context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,), bend_collision_type="arc")
|
||||
|
||||
|
|
@ -439,3 +523,144 @@ def test_no_dynamic_paths_skips_congestion_check(basic_evaluator: CostEvaluator)
|
|||
assert open_set
|
||||
assert context.metrics.total_congestion_check_calls == 0
|
||||
assert context.metrics.total_congestion_cache_misses == 0
|
||||
|
||||
|
||||
def test_guidance_seed_matching_move_reduces_cost_and_advances_seed_index(
|
||||
basic_evaluator: CostEvaluator,
|
||||
) -> None:
|
||||
context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||
root = AStarNode(Port(0, 0, 0), 0.0, 0.0, seed_index=0)
|
||||
result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||
open_set: list[AStarNode] = []
|
||||
unguided_open_set: list[AStarNode] = []
|
||||
closed_set: dict[tuple[int, int, int], float] = {}
|
||||
|
||||
add_node(
|
||||
root,
|
||||
result,
|
||||
target=Port(20, 0, 0),
|
||||
net_width=2.0,
|
||||
net_id="netA",
|
||||
open_set=open_set,
|
||||
closed_set=closed_set,
|
||||
context=context,
|
||||
metrics=context.metrics,
|
||||
congestion_cache={},
|
||||
congestion_presence_cache={},
|
||||
congestion_candidate_precheck_cache={},
|
||||
congestion_net_envelope_cache={},
|
||||
congestion_grid_net_cache={},
|
||||
congestion_grid_span_cache={},
|
||||
config=SearchRunConfig.from_options(
|
||||
context.options,
|
||||
guidance_seed=(StraightSeed(length=10.0),),
|
||||
guidance_bonus=5.0,
|
||||
),
|
||||
move_type="straight",
|
||||
cache_key=("guided",),
|
||||
)
|
||||
add_node(
|
||||
AStarNode(Port(0, 0, 0), 0.0, 0.0),
|
||||
result,
|
||||
target=Port(20, 0, 0),
|
||||
net_width=2.0,
|
||||
net_id="netA",
|
||||
open_set=unguided_open_set,
|
||||
closed_set={},
|
||||
context=context,
|
||||
metrics=context.metrics,
|
||||
congestion_cache={},
|
||||
congestion_presence_cache={},
|
||||
congestion_candidate_precheck_cache={},
|
||||
congestion_net_envelope_cache={},
|
||||
congestion_grid_net_cache={},
|
||||
congestion_grid_span_cache={},
|
||||
config=SearchRunConfig.from_options(context.options),
|
||||
move_type="straight",
|
||||
cache_key=("unguided",),
|
||||
)
|
||||
|
||||
assert open_set
|
||||
assert unguided_open_set
|
||||
guided_node = open_set[0]
|
||||
unguided_node = unguided_open_set[0]
|
||||
assert guided_node.seed_index == 1
|
||||
assert guided_node.g_cost < unguided_node.g_cost
|
||||
assert context.metrics.total_guidance_match_moves == 1
|
||||
assert context.metrics.total_guidance_match_moves_straight == 1
|
||||
assert context.metrics.total_guidance_match_moves_bend90 == 0
|
||||
assert context.metrics.total_guidance_match_moves_sbend == 0
|
||||
assert context.metrics.total_guidance_bonus_applied == pytest.approx(5.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_straight == pytest.approx(5.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_bend90 == pytest.approx(0.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_sbend == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_guidance_seed_bend90_keeps_full_bonus(
|
||||
basic_evaluator: CostEvaluator,
|
||||
) -> None:
|
||||
context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||
root = AStarNode(Port(0, 0, 0), 0.0, 0.0, seed_index=0)
|
||||
result = Bend90.generate(Port(0, 0, 0), 10.0, width=2.0, direction="CCW", dilation=1.0)
|
||||
open_set: list[AStarNode] = []
|
||||
unguided_open_set: list[AStarNode] = []
|
||||
|
||||
add_node(
|
||||
root,
|
||||
result,
|
||||
target=Port(10, 10, 90),
|
||||
net_width=2.0,
|
||||
net_id="netA",
|
||||
open_set=open_set,
|
||||
closed_set={},
|
||||
context=context,
|
||||
metrics=context.metrics,
|
||||
congestion_cache={},
|
||||
congestion_presence_cache={},
|
||||
congestion_candidate_precheck_cache={},
|
||||
congestion_net_envelope_cache={},
|
||||
congestion_grid_net_cache={},
|
||||
congestion_grid_span_cache={},
|
||||
config=SearchRunConfig.from_options(
|
||||
context.options,
|
||||
guidance_seed=(result.move_spec,),
|
||||
guidance_bonus=5.0,
|
||||
),
|
||||
move_type="bend90",
|
||||
cache_key=("guided-bend90",),
|
||||
)
|
||||
add_node(
|
||||
AStarNode(Port(0, 0, 0), 0.0, 0.0),
|
||||
result,
|
||||
target=Port(10, 10, 90),
|
||||
net_width=2.0,
|
||||
net_id="netA",
|
||||
open_set=unguided_open_set,
|
||||
closed_set={},
|
||||
context=context,
|
||||
metrics=context.metrics,
|
||||
congestion_cache={},
|
||||
congestion_presence_cache={},
|
||||
congestion_candidate_precheck_cache={},
|
||||
congestion_net_envelope_cache={},
|
||||
congestion_grid_net_cache={},
|
||||
congestion_grid_span_cache={},
|
||||
config=SearchRunConfig.from_options(context.options),
|
||||
move_type="bend90",
|
||||
cache_key=("unguided-bend90",),
|
||||
)
|
||||
|
||||
assert open_set
|
||||
assert unguided_open_set
|
||||
guided_node = open_set[0]
|
||||
unguided_node = unguided_open_set[0]
|
||||
assert guided_node.seed_index == 1
|
||||
assert unguided_node.g_cost - guided_node.g_cost == pytest.approx(5.0)
|
||||
assert context.metrics.total_guidance_match_moves == 1
|
||||
assert context.metrics.total_guidance_match_moves_straight == 0
|
||||
assert context.metrics.total_guidance_match_moves_bend90 == 1
|
||||
assert context.metrics.total_guidance_match_moves_sbend == 0
|
||||
assert context.metrics.total_guidance_bonus_applied == pytest.approx(5.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_straight == pytest.approx(0.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_bend90 == pytest.approx(5.0)
|
||||
assert context.metrics.total_guidance_bonus_applied_sbend == pytest.approx(0.0)
|
||||
|
|
|
|||
|
|
@ -169,6 +169,67 @@ def test_verify_path_details_returns_conflicting_net_ids() -> None:
|
|||
assert detail.conflicting_net_ids == ("netB",)
|
||||
|
||||
|
||||
def test_verify_path_details_reports_component_conflicts() -> None:
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
engine.metrics = AStarMetrics()
|
||||
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||
path_b = [
|
||||
Straight.generate(Port(100, 0, 0), 10.0, width=2.0, dilation=1.0),
|
||||
Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0),
|
||||
]
|
||||
|
||||
engine.add_path(
|
||||
"netB",
|
||||
[poly for component in path_b for poly in component.collision_geometry],
|
||||
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
|
||||
component_indexes=[0] * len(path_b[0].collision_geometry) + [1] * len(path_b[1].collision_geometry),
|
||||
)
|
||||
|
||||
detail = engine.verify_path_details("netA", path_a, capture_component_conflicts=True)
|
||||
|
||||
assert detail.conflicting_net_ids == ("netB",)
|
||||
assert detail.component_conflicts == ((0, "netB", 1),)
|
||||
|
||||
|
||||
def test_verify_path_details_deduplicates_component_conflicts() -> None:
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
engine.metrics = AStarMetrics()
|
||||
query_component = ComponentResult(
|
||||
start_port=Port(0, 0, 0),
|
||||
collision_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)],
|
||||
end_port=Port(22, 0, 0),
|
||||
length=22.0,
|
||||
move_type="straight",
|
||||
move_spec=StraightSeed(22.0),
|
||||
physical_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)],
|
||||
dilated_collision_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)],
|
||||
dilated_physical_geometry=[box(0, 0, 10, 10), box(12, 0, 22, 10)],
|
||||
)
|
||||
blocker_component = ComponentResult(
|
||||
start_port=Port(0, 0, 0),
|
||||
collision_geometry=[box(5, 0, 17, 10)],
|
||||
end_port=Port(17, 0, 0),
|
||||
length=12.0,
|
||||
move_type="straight",
|
||||
move_spec=StraightSeed(12.0),
|
||||
physical_geometry=[box(5, 0, 17, 10)],
|
||||
dilated_collision_geometry=[box(5, 0, 17, 10)],
|
||||
dilated_physical_geometry=[box(5, 0, 17, 10)],
|
||||
)
|
||||
|
||||
engine.add_path(
|
||||
"netB",
|
||||
blocker_component.collision_geometry,
|
||||
dilated_geometry=blocker_component.dilated_collision_geometry,
|
||||
component_indexes=[0],
|
||||
)
|
||||
|
||||
detail = engine.verify_path_details("netA", [query_component], capture_component_conflicts=True)
|
||||
|
||||
assert detail.conflicting_net_ids == ("netB",)
|
||||
assert detail.component_conflicts == ((0, "netB", 0),)
|
||||
|
||||
|
||||
def test_remove_path_clears_dynamic_path() -> None:
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,15 @@ from inire import (
|
|||
)
|
||||
from inire.router._stack import build_routing_stack
|
||||
from inire.seeds import Bend90Seed, PathSeed, StraightSeed
|
||||
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics, snapshot_example_05
|
||||
from inire.tests.example_scenarios import (
|
||||
SCENARIOS,
|
||||
_build_evaluator,
|
||||
_build_pathfinder,
|
||||
_net_specs,
|
||||
AStarMetrics,
|
||||
snapshot_example_05,
|
||||
snapshot_example_07_no_warm_start,
|
||||
)
|
||||
|
||||
|
||||
EXPECTED_OUTCOMES = {
|
||||
|
|
@ -43,6 +51,14 @@ def test_example_05_avoids_dynamic_tree_rebuilds() -> None:
|
|||
assert snapshot.metrics.dynamic_tree_rebuilds == 0
|
||||
|
||||
|
||||
def test_example_07_no_warm_start_canary_improves_validity() -> None:
|
||||
snapshot = snapshot_example_07_no_warm_start()
|
||||
|
||||
assert snapshot.total_results == 10
|
||||
assert snapshot.reached_targets == 10
|
||||
assert snapshot.valid_results == 10
|
||||
|
||||
|
||||
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
|
||||
bounds = (-20, -20, 170, 170)
|
||||
obstacles = (
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ def test_snapshot_example_01_exposes_metrics() -> None:
|
|||
assert snapshot.metrics.score_component_calls >= 0
|
||||
assert snapshot.metrics.danger_map_lookup_calls >= 0
|
||||
assert snapshot.metrics.move_cache_abs_misses >= 0
|
||||
assert snapshot.metrics.guidance_match_moves >= 0
|
||||
assert snapshot.metrics.guidance_match_moves_straight >= 0
|
||||
assert snapshot.metrics.guidance_match_moves_bend90 >= 0
|
||||
assert snapshot.metrics.guidance_match_moves_sbend >= 0
|
||||
assert snapshot.metrics.guidance_bonus_applied >= 0.0
|
||||
assert snapshot.metrics.guidance_bonus_applied_straight >= 0.0
|
||||
assert snapshot.metrics.guidance_bonus_applied_bend90 >= 0.0
|
||||
assert snapshot.metrics.guidance_bonus_applied_sbend >= 0.0
|
||||
assert snapshot.metrics.ray_cast_calls >= 0
|
||||
assert snapshot.metrics.ray_cast_calls_expand_forward >= 0
|
||||
assert snapshot.metrics.dynamic_tree_rebuilds >= 0
|
||||
|
|
@ -36,6 +44,10 @@ def test_snapshot_example_01_exposes_metrics() -> None:
|
|||
assert snapshot.metrics.congestion_candidate_ids >= 0
|
||||
assert snapshot.metrics.verify_dynamic_candidate_nets >= 0
|
||||
assert snapshot.metrics.refinement_candidates_verified >= 0
|
||||
assert snapshot.metrics.pair_local_search_pairs_considered >= 0
|
||||
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
|
||||
|
||||
|
||||
def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None:
|
||||
|
|
@ -195,3 +207,70 @@ def test_diff_performance_baseline_script_renders_current_metrics_for_added_scen
|
|||
report = output_path.read_text()
|
||||
assert "| example_07_large_scale_routing_no_warm_start | duration_s | - |" in report
|
||||
assert "| example_07_large_scale_routing_no_warm_start | nodes_expanded | - |" in report
|
||||
|
||||
|
||||
def test_record_conflict_trace_script_writes_selected_scenario(tmp_path: Path) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
script_path = repo_root / "scripts" / "record_conflict_trace.py"
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--output-dir",
|
||||
str(tmp_path),
|
||||
"--scenario",
|
||||
"example_05_orientation_stress",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
payload = json.loads((tmp_path / "conflict_trace.json").read_text())
|
||||
assert payload["generated_at"]
|
||||
assert payload["generator"] == "scripts/record_conflict_trace.py"
|
||||
assert [entry["name"] for entry in payload["scenarios"]] == ["example_05_orientation_stress"]
|
||||
assert (tmp_path / "conflict_trace.md").exists()
|
||||
|
||||
|
||||
def test_record_conflict_trace_script_supports_performance_only_scenario(tmp_path: Path) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
script_path = repo_root / "scripts" / "record_conflict_trace.py"
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--output-dir",
|
||||
str(tmp_path),
|
||||
"--include-performance-only",
|
||||
"--scenario",
|
||||
"example_07_large_scale_routing_no_warm_start",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
payload = json.loads((tmp_path / "conflict_trace.json").read_text())
|
||||
assert [entry["name"] for entry in payload["scenarios"]] == ["example_07_large_scale_routing_no_warm_start"]
|
||||
|
||||
|
||||
def test_record_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
script_path = repo_root / "scripts" / "record_frontier_trace.py"
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--output-dir",
|
||||
str(tmp_path),
|
||||
"--scenario",
|
||||
"example_05_orientation_stress",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
payload = json.loads((tmp_path / "frontier_trace.json").read_text())
|
||||
assert payload["generated_at"]
|
||||
assert payload["generator"] == "scripts/record_frontier_trace.py"
|
||||
assert [entry["name"] for entry in payload["scenarios"]] == ["example_05_orientation_stress"]
|
||||
assert (tmp_path / "frontier_trace.md").exists()
|
||||
|
|
|
|||
228
scripts/record_conflict_trace.py
Normal file
228
scripts/record_conflict_trace.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#!/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.results import RoutingRunResult
|
||||
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:
|
||||
return (("example_07_large_scale_routing_no_warm_start", dict(TRACE_PERFORMANCE_SCENARIO_RUNS)["example_07_large_scale_routing_no_warm_start"]),)
|
||||
|
||||
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 trace scenario: {name}. Valid scenarios: {valid}")
|
||||
return tuple(runs)
|
||||
|
||||
|
||||
def _result_summary(run: RoutingRunResult) -> dict[str, object]:
|
||||
return {
|
||||
"total_results": len(run.results_by_net),
|
||||
"valid_results": sum(1 for result in run.results_by_net.values() if result.is_valid),
|
||||
"reached_targets": sum(1 for result in run.results_by_net.values() if result.reached_target),
|
||||
"results_by_net": {
|
||||
net_id: {
|
||||
"outcome": result.outcome,
|
||||
"reached_target": result.reached_target,
|
||||
"report": asdict(result.report),
|
||||
}
|
||||
for net_id, result in run.results_by_net.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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": _result_summary(result),
|
||||
"metrics": asdict(result.metrics),
|
||||
"conflict_trace": [asdict(entry) for entry in result.conflict_trace],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"generator": "scripts/record_conflict_trace.py",
|
||||
"scenarios": scenarios,
|
||||
}
|
||||
|
||||
|
||||
def _count_stage_nets(entry: dict[str, object]) -> int:
|
||||
return sum(
|
||||
1
|
||||
for net in entry["nets"]
|
||||
if net["report"]["dynamic_collision_count"] > 0
|
||||
)
|
||||
|
||||
|
||||
def _canonical_component_pair(
|
||||
net_id: str,
|
||||
self_component_index: int,
|
||||
other_net_id: str,
|
||||
other_component_index: int,
|
||||
) -> tuple[tuple[str, int], tuple[str, int]]:
|
||||
left = (net_id, self_component_index)
|
||||
right = (other_net_id, other_component_index)
|
||||
if left <= right:
|
||||
return (left, right)
|
||||
return (right, left)
|
||||
|
||||
|
||||
def _render_markdown(payload: dict[str, object]) -> str:
|
||||
lines = [
|
||||
"# Conflict 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.",
|
||||
"",
|
||||
"| Stage | Iteration | Conflicting Nets | Conflict Edges | Completed Nets |",
|
||||
"| :-- | --: | --: | --: | --: |",
|
||||
]
|
||||
)
|
||||
|
||||
net_stage_counts: Counter[str] = Counter()
|
||||
edge_counts: Counter[tuple[str, str]] = Counter()
|
||||
component_pair_counts: Counter[tuple[tuple[str, int], tuple[str, int]]] = Counter()
|
||||
trace_entries = scenario["conflict_trace"]
|
||||
for entry in trace_entries:
|
||||
lines.append(
|
||||
"| "
|
||||
f"{entry['stage']} | "
|
||||
f"{'' if entry['iteration'] is None else entry['iteration']} | "
|
||||
f"{_count_stage_nets(entry)} | "
|
||||
f"{len(entry['conflict_edges'])} | "
|
||||
f"{len(entry['completed_net_ids'])} |"
|
||||
)
|
||||
seen_component_pairs: set[tuple[tuple[str, int], tuple[str, int]]] = set()
|
||||
for edge in entry["conflict_edges"]:
|
||||
edge_counts[tuple(edge)] += 1
|
||||
for net in entry["nets"]:
|
||||
if net["report"]["dynamic_collision_count"] > 0:
|
||||
net_stage_counts[net["net_id"]] += 1
|
||||
for component_conflict in net["component_conflicts"]:
|
||||
pair = _canonical_component_pair(
|
||||
net["net_id"],
|
||||
component_conflict["self_component_index"],
|
||||
component_conflict["other_net_id"],
|
||||
component_conflict["other_component_index"],
|
||||
)
|
||||
seen_component_pairs.add(pair)
|
||||
for pair in seen_component_pairs:
|
||||
component_pair_counts[pair] += 1
|
||||
|
||||
lines.extend(["", "Top nets by traced dynamic-collision stages:", ""])
|
||||
if net_stage_counts:
|
||||
for net_id, count in net_stage_counts.most_common(10):
|
||||
lines.append(f"- `{net_id}`: {count}")
|
||||
else:
|
||||
lines.append("- None")
|
||||
|
||||
lines.extend(["", "Top net pairs by frequency:", ""])
|
||||
if edge_counts:
|
||||
for (left, right), count in edge_counts.most_common(10):
|
||||
lines.append(f"- `{left}` <-> `{right}`: {count}")
|
||||
else:
|
||||
lines.append("- None")
|
||||
|
||||
lines.extend(["", "Top component pairs by frequency:", ""])
|
||||
if component_pair_counts:
|
||||
for pair, count in component_pair_counts.most_common(10):
|
||||
(left_net, left_index), (right_net, right_index) = pair
|
||||
lines.append(f"- `{left_net}[{left_index}]` <-> `{right_net}[{right_index}]`: {count}")
|
||||
else:
|
||||
lines.append("- None")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Record conflict-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 conflict_trace.json and conflict_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 / "conflict_trace.json"
|
||||
markdown_path = output_dir / "conflict_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()
|
||||
205
scripts/record_frontier_trace.py
Normal file
205
scripts/record_frontier_trace.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
#!/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:
|
||||
default_registry = dict(TRACE_PERFORMANCE_SCENARIO_RUNS)
|
||||
return (("example_07_large_scale_routing_no_warm_start", default_registry["example_07_large_scale_routing_no_warm_start"]),)
|
||||
|
||||
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 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),
|
||||
"frontier_trace": [asdict(entry) for entry in result.frontier_trace],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"generator": "scripts/record_frontier_trace.py",
|
||||
"scenarios": scenarios,
|
||||
}
|
||||
|
||||
|
||||
def _render_markdown(payload: dict[str, object]) -> str:
|
||||
lines = [
|
||||
"# 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.",
|
||||
"",
|
||||
"| Net | Hotspots | Closed-Set | Hard Collision | Self Collision | Cost | Samples |",
|
||||
"| :-- | --: | --: | --: | --: | --: | --: |",
|
||||
]
|
||||
)
|
||||
|
||||
reason_counts: Counter[str] = Counter()
|
||||
hotspot_counts: Counter[tuple[str, int]] = Counter()
|
||||
for net_trace in scenario["frontier_trace"]:
|
||||
sample_count = len(net_trace["samples"])
|
||||
lines.append(
|
||||
"| "
|
||||
f"{net_trace['net_id']} | "
|
||||
f"{len(net_trace['hotspot_bounds'])} | "
|
||||
f"{net_trace['pruned_closed_set']} | "
|
||||
f"{net_trace['pruned_hard_collision']} | "
|
||||
f"{net_trace['pruned_self_collision']} | "
|
||||
f"{net_trace['pruned_cost']} | "
|
||||
f"{sample_count} |"
|
||||
)
|
||||
reason_counts["closed_set"] += net_trace["pruned_closed_set"]
|
||||
reason_counts["hard_collision"] += net_trace["pruned_hard_collision"]
|
||||
reason_counts["self_collision"] += net_trace["pruned_self_collision"]
|
||||
reason_counts["cost"] += net_trace["pruned_cost"]
|
||||
for sample in net_trace["samples"]:
|
||||
hotspot_counts[(net_trace["net_id"], sample["hotspot_index"])] += 1
|
||||
|
||||
lines.extend(["", "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.extend(["", "Top traced hotspots by sample count:", ""])
|
||||
if hotspot_counts:
|
||||
for (net_id, hotspot_index), count in hotspot_counts.most_common(10):
|
||||
lines.append(f"- `{net_id}` hotspot `{hotspot_index}`: {count}")
|
||||
else:
|
||||
lines.append("- None")
|
||||
|
||||
lines.extend(["", "Per-net sampled reason/move breakdown:", ""])
|
||||
if scenario["frontier_trace"]:
|
||||
for net_trace in scenario["frontier_trace"]:
|
||||
reason_move_counts: Counter[tuple[str, str]] = Counter()
|
||||
hotspot_sample_counts: Counter[int] = Counter()
|
||||
for sample in net_trace["samples"]:
|
||||
reason_move_counts[(sample["reason"], sample["move_type"])] += 1
|
||||
hotspot_sample_counts[sample["hotspot_index"]] += 1
|
||||
lines.append(f"- `{net_trace['net_id']}`")
|
||||
if reason_move_counts:
|
||||
top_pairs = ", ".join(
|
||||
f"`{reason}` x `{move}` = {count}"
|
||||
for (reason, move), count in reason_move_counts.most_common(3)
|
||||
)
|
||||
lines.append(f" sampled reasons: {top_pairs}")
|
||||
else:
|
||||
lines.append(" sampled reasons: none")
|
||||
if hotspot_sample_counts:
|
||||
top_hotspots = ", ".join(
|
||||
f"`{hotspot}` = {count}" for hotspot, count in hotspot_sample_counts.most_common(3)
|
||||
)
|
||||
lines.append(f" hotspot samples: {top_hotspots}")
|
||||
else:
|
||||
lines.append(" hotspot samples: none")
|
||||
else:
|
||||
lines.append("- None")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Record 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 frontier_trace.json and frontier_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 / "frontier_trace.json"
|
||||
markdown_path = output_dir / "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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue