Compare commits
4 commits
725980e694
...
2049353ee9
| Author | SHA1 | Date | |
|---|---|---|---|
| 2049353ee9 | |||
| 42e46c67e0 | |||
| 71e263c527 | |||
| e77fd6e69f |
38 changed files with 13878 additions and 346 deletions
99
DOCS.md
99
DOCS.md
|
|
@ -128,8 +128,65 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
||||||
| Field | Default | Description |
|
| Field | Default | Description |
|
||||||
| :-- | :-- | :-- |
|
| :-- | :-- | :-- |
|
||||||
| `capture_expanded` | `False` | Record expanded nodes for diagnostics and visualization. |
|
| `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.
|
`RoutingRunResult.metrics` is an immutable per-run snapshot.
|
||||||
|
|
||||||
|
|
@ -148,11 +205,20 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
||||||
- `warm_start_paths_used`: Number of routing attempts satisfied directly from an initial or warm-start path.
|
- `warm_start_paths_used`: Number of routing attempts satisfied directly from an initial or warm-start path.
|
||||||
- `refine_path_calls`: Number of completed paths passed through the post-route refiner.
|
- `refine_path_calls`: Number of completed paths passed through the post-route refiner.
|
||||||
- `timeout_events`: Number of timeout exits encountered during the run.
|
- `timeout_events`: Number of timeout exits encountered during the run.
|
||||||
|
- `iteration_reverify_calls`: Number of end-of-iteration full reverify passes against the final installed dynamic geometry.
|
||||||
|
- `iteration_reverified_nets`: Number of reached-target nets reverified at iteration boundaries.
|
||||||
|
- `iteration_conflicting_nets`: Total unique nets found in end-of-iteration dynamic conflicts.
|
||||||
|
- `iteration_conflict_edges`: Total undirected dynamic-conflict edges observed at iteration boundaries.
|
||||||
|
- `nets_carried_forward`: Number of nets retained unchanged between iterations.
|
||||||
|
|
||||||
### Cache Counters
|
### Cache Counters
|
||||||
|
|
||||||
- `move_cache_abs_hits` / `move_cache_abs_misses`: Absolute move-geometry cache activity.
|
- `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.
|
- `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.
|
- `static_safe_cache_hits`: Reuse count for the static-safe admission cache.
|
||||||
- `hard_collision_cache_hits`: Reuse count for the hard-collision cache.
|
- `hard_collision_cache_hits`: Reuse count for the hard-collision cache.
|
||||||
- `congestion_cache_hits` / `congestion_cache_misses`: Per-search congestion-cache activity.
|
- `congestion_cache_hits` / `congestion_cache_misses`: Per-search congestion-cache activity.
|
||||||
|
|
@ -165,29 +231,50 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
||||||
- `static_tree_rebuilds`: Number of static dilated-obstacle STRtree rebuilds.
|
- `static_tree_rebuilds`: Number of static dilated-obstacle STRtree rebuilds.
|
||||||
- `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification.
|
- `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification.
|
||||||
- `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds.
|
- `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds.
|
||||||
- `visibility_builds`: Number of static visibility-graph rebuilds.
|
- `visibility_corner_index_builds`: Number of lazy corner-index rebuilds.
|
||||||
- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building that graph.
|
- `visibility_builds`: Number of exact corner-visibility graph rebuilds.
|
||||||
- `visibility_corner_queries` / `visibility_corner_hits`: Precomputed-corner visibility query activity.
|
- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building the exact graph.
|
||||||
|
- `visibility_corner_queries_exact` / `visibility_corner_hits_exact`: Exact-corner visibility query activity.
|
||||||
- `visibility_point_queries`, `visibility_point_cache_hits`, `visibility_point_cache_misses`: Arbitrary-point visibility query and cache activity.
|
- `visibility_point_queries`, `visibility_point_cache_hits`, `visibility_point_cache_misses`: Arbitrary-point visibility query and cache activity.
|
||||||
- `ray_cast_calls`: Number of ray-cast queries issued against static obstacles.
|
- `ray_cast_calls`: Number of ray-cast queries issued against static obstacles.
|
||||||
- `ray_cast_candidate_bounds`: Total broad-phase candidate bounds considered by ray casts.
|
- `ray_cast_candidate_bounds`: Total broad-phase candidate bounds considered by ray casts.
|
||||||
- `ray_cast_exact_geometry_checks`: Total exact non-rectangular geometry checks performed by ray casts.
|
- `ray_cast_exact_geometry_checks`: Total exact non-rectangular geometry checks performed by ray casts.
|
||||||
- `congestion_check_calls`: Number of congestion broad-phase checks requested by search.
|
- `congestion_check_calls`: Number of congestion broad-phase checks requested by search.
|
||||||
|
- `congestion_presence_cache_hits` / `congestion_presence_cache_misses`: Reuse of cached per-span booleans indicating whether a move polygon could overlap any other routed net at all.
|
||||||
|
- `congestion_presence_skips`: Number of moves that bypassed full congestion evaluation because the presence precheck found no other routed nets in any covered dynamic-grid span.
|
||||||
|
- `congestion_candidate_precheck_hits` / `congestion_candidate_precheck_misses`: Reuse of cached conservative per-span booleans indicating whether any candidate nets survive the net-envelope and grid-net broad phases.
|
||||||
|
- `congestion_candidate_precheck_skips`: Number of moves that bypassed full congestion evaluation because the candidate-net precheck found no surviving candidate nets after those broad phases.
|
||||||
|
- `congestion_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during routing.
|
||||||
|
- `congestion_net_envelope_cache_hits` / `congestion_net_envelope_cache_misses`: Reuse of cached dynamic net-envelope candidate sets keyed by the queried grid-cell span.
|
||||||
|
- `congestion_grid_net_cache_hits` / `congestion_grid_net_cache_misses`: Reuse of cached per-span candidate net ids gathered from dynamic grid occupancy.
|
||||||
|
- `congestion_grid_span_cache_hits` / `congestion_grid_span_cache_misses`: Reuse of cached dynamic-path candidate unions keyed by the queried grid-cell span.
|
||||||
|
- `congestion_lazy_resolutions`: Number of popped nodes whose pending congestion was resolved lazily.
|
||||||
|
- `congestion_lazy_requeues`: Number of lazily resolved nodes requeued after a positive congestion penalty was applied.
|
||||||
|
- `congestion_candidate_ids`: Total dynamic-path object ids returned by the congestion broad phase before exact confirmation.
|
||||||
- `congestion_exact_pair_checks`: Number of exact geometry-pair checks performed while confirming congestion hits.
|
- `congestion_exact_pair_checks`: Number of exact geometry-pair checks performed while confirming congestion hits.
|
||||||
|
|
||||||
### Verification Counters
|
### Verification Counters
|
||||||
|
|
||||||
- `verify_path_report_calls`: Number of full path-verification passes.
|
- `verify_path_report_calls`: Number of full path-verification passes.
|
||||||
- `verify_static_buffer_ops`: Number of static-verification `buffer()` operations.
|
- `verify_static_buffer_ops`: Number of static-verification `buffer()` operations.
|
||||||
|
- `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.
|
- `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=...)`.
|
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)**.
|
The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**.
|
||||||
|
Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate.
|
||||||
|
|
||||||
## 9. Tuning Notes
|
## 11. Tuning Notes
|
||||||
|
|
||||||
### Speed vs. optimality
|
### Speed vs. optimality
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
- `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers.
|
- `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers.
|
||||||
- `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation.
|
- `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation.
|
||||||
- `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic.
|
- `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic.
|
||||||
- `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths.
|
- `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths, including dynamic per-object indices, per-net grid occupancy, congestion grid membership, and per-net dynamic envelopes.
|
||||||
- `inire/router/_search.py`, `_astar_moves.py`, `_astar_admission.py`, `_astar_types.py`: The state-lattice A* search loop and move admission pipeline.
|
- `inire/router/_search.py`, `_astar_moves.py`, `_astar_admission.py`, `_astar_types.py`: The state-lattice A* search loop and move admission pipeline.
|
||||||
- `inire/router/_router.py`: The negotiated-congestion driver and refinement orchestration.
|
- `inire/router/_router.py`: The negotiated-congestion driver and refinement orchestration.
|
||||||
- `inire/router/refiner.py`: Post-route path simplification for completed paths.
|
- `inire/router/refiner.py`: Post-route path simplification for completed paths.
|
||||||
|
|
@ -39,7 +39,10 @@ The search state is a snapped Manhattan `(x, y, r)` port. From each state the ro
|
||||||
|
|
||||||
- Static obstacles and routed paths are treated as single-layer geometry; automatic crossings are not supported.
|
- Static obstacles and routed paths are treated as single-layer geometry; automatic crossings are not supported.
|
||||||
- The danger-map implementation uses sampled obstacle-boundary points and a KD-tree, not a dense distance-transform grid.
|
- The danger-map implementation uses sampled obstacle-boundary points and a KD-tree, not a dense distance-transform grid.
|
||||||
|
- 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.
|
- `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.
|
- Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning.
|
||||||
|
|
||||||
## Performance Visibility
|
## 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
|
||||||
|
|
||||||
3631
docs/optimization_pass_01_log.md
Normal file
3631
docs/optimization_pass_01_log.md
Normal file
File diff suppressed because it is too large
Load diff
2108
docs/pair_local_characterization.json
Normal file
2108
docs/pair_local_characterization.json
Normal file
File diff suppressed because it is too large
Load diff
30
docs/pair_local_characterization.md
Normal file
30
docs/pair_local_characterization.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Pair-Local Search Characterization
|
||||||
|
|
||||||
|
Generated at 2026-04-02T15:53:29-07:00 by `scripts/characterize_pair_local_search.py`.
|
||||||
|
|
||||||
|
Grid: `num_nets=[6, 8, 10]`, `seed=[41, 42, 43]`, repeats=2.
|
||||||
|
|
||||||
|
| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks |
|
||||||
|
| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |
|
||||||
|
| 6 | 41 | 0 | 0.5462 | 1 | 6 | 0 | 0 | 0 | 500 | 674 |
|
||||||
|
| 6 | 41 | 1 | 0.5256 | 1 | 6 | 0 | 0 | 0 | 500 | 674 |
|
||||||
|
| 6 | 42 | 0 | 0.5241 | 1 | 6 | 0 | 0 | 0 | 503 | 683 |
|
||||||
|
| 6 | 42 | 1 | 0.5477 | 1 | 6 | 0 | 0 | 0 | 503 | 683 |
|
||||||
|
| 6 | 43 | 0 | 0.5199 | 1 | 6 | 0 | 0 | 0 | 493 | 654 |
|
||||||
|
| 6 | 43 | 1 | 0.5160 | 1 | 6 | 0 | 0 | 0 | 493 | 654 |
|
||||||
|
| 8 | 41 | 0 | 1.8818 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 |
|
||||||
|
| 8 | 41 | 1 | 1.8618 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 |
|
||||||
|
| 8 | 42 | 0 | 1.4850 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 |
|
||||||
|
| 8 | 42 | 1 | 1.4636 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 |
|
||||||
|
| 8 | 43 | 0 | 1.0652 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 |
|
||||||
|
| 8 | 43 | 1 | 1.0502 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 |
|
||||||
|
| 10 | 41 | 0 | 2.8617 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 |
|
||||||
|
| 10 | 41 | 1 | 2.8282 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 |
|
||||||
|
| 10 | 42 | 0 | 2.0356 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 |
|
||||||
|
| 10 | 42 | 1 | 2.0052 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 |
|
||||||
|
| 10 | 43 | 0 | 50.1863 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 |
|
||||||
|
| 10 | 43 | 1 | 50.4019 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 |
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
No smaller stable pair-local smoke scenario satisfied the rule `valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats.
|
||||||
|
|
@ -1,25 +1,41 @@
|
||||||
# Performance Baseline
|
# Performance Baseline
|
||||||
|
|
||||||
Generated on 2026-03-31 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`.
|
The full machine-readable snapshot lives in `docs/performance_baseline.json`.
|
||||||
|
Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.
|
||||||
|
|
||||||
|
The default baseline table below covers the standard example corpus only. The heavier `example_07_large_scale_routing_no_warm_start` canary remains performance-only and is tracked through targeted diffs plus the conflict/frontier trace artifacts.
|
||||||
|
|
||||||
|
Use `scripts/characterize_pair_local_search.py` when you want a small parameter sweep over example_07-style no-warm runs instead of a single canary reading.
|
||||||
|
The current tracked sweep output lives in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`.
|
||||||
|
|
||||||
| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |
|
| 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.0042 | 1 | 1 | 1 | 1 | 1 | 2 | 22 | 11 | 7 | 2 | 2 | 0 | 3 |
|
| example_01_simple_route | 0.0040 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 4 |
|
||||||
| example_02_congestion_resolution | 0.3335 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 |
|
| example_02_congestion_resolution | 0.3378 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 38 |
|
||||||
| example_03_locked_paths | 0.1810 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 |
|
| example_03_locked_paths | 0.1929 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 16 |
|
||||||
| example_04_sbends_and_radii | 2.0151 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 |
|
| 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.2438 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 |
|
| example_05_orientation_stress | 0.2367 | 3 | 3 | 3 | 2 | 6 | 299 | 1284 | 1691 | 696 | 0 | 0 | 149 | 18 |
|
||||||
| example_06_bend_collision_models | 4.1636 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 |
|
| 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 | 1.3759 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 |
|
| 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.2437 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 |
|
| 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.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 |
|
| example_09_unroutable_best_effort | 0.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
|
||||||
|
|
||||||
## Full Counter Set
|
## Full Counter Set
|
||||||
|
|
||||||
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.
|
Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.
|
||||||
|
These counters are currently observational only and are not enforced as CI regression gates.
|
||||||
|
|
||||||
|
For the current accepted branch, the most important performance-only canary is `example_07_large_scale_routing_no_warm_start`, which now finishes `10/10/10` after a bounded post-loop pair-local scratch reroute. The relevant counters for that phase are:
|
||||||
|
|
||||||
|
- `pair_local_search_pairs_considered`
|
||||||
|
- `pair_local_search_attempts`
|
||||||
|
- `pair_local_search_accepts`
|
||||||
|
- `pair_local_search_nodes_expanded`
|
||||||
|
|
||||||
|
The latest tracked characterization sweep confirms there is no smaller stable pair-local smoke case under the `<=1.0s` rule, so the 10-net no-warm-start canary remains the primary regression target for this behavior.
|
||||||
|
|
||||||
Tracked metric keys:
|
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, 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, 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_builds, visibility_corner_pairs_checked, visibility_corner_queries, visibility_corner_hits, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, ray_cast_calls, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_exact_pair_checks
|
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
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,15 @@ from .model import (
|
||||||
RoutingProblem as RoutingProblem,
|
RoutingProblem as RoutingProblem,
|
||||||
SearchOptions as SearchOptions,
|
SearchOptions as SearchOptions,
|
||||||
) # noqa: PLC0414
|
) # 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
|
from .seeds import Bend90Seed as Bend90Seed, PathSeed as PathSeed, SBendSeed as SBendSeed, StraightSeed as StraightSeed # noqa: PLC0414
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
@ -37,16 +45,23 @@ def route(
|
||||||
results_by_net=results,
|
results_by_net=results,
|
||||||
metrics=finder.metrics.snapshot(),
|
metrics=finder.metrics.snapshot(),
|
||||||
expanded_nodes=tuple(finder.accumulated_expanded_nodes),
|
expanded_nodes=tuple(finder.accumulated_expanded_nodes),
|
||||||
|
conflict_trace=tuple(finder.conflict_trace),
|
||||||
|
frontier_trace=tuple(finder.frontier_trace),
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Bend90Seed",
|
"Bend90Seed",
|
||||||
"CongestionOptions",
|
"CongestionOptions",
|
||||||
|
"ComponentConflictTrace",
|
||||||
|
"ConflictTraceEntry",
|
||||||
"DiagnosticsOptions",
|
"DiagnosticsOptions",
|
||||||
"NetSpec",
|
"NetSpec",
|
||||||
|
"NetConflictTrace",
|
||||||
|
"NetFrontierTrace",
|
||||||
"ObjectiveWeights",
|
"ObjectiveWeights",
|
||||||
"PathSeed",
|
"PathSeed",
|
||||||
"Port",
|
"Port",
|
||||||
|
"FrontierPruneSample",
|
||||||
"RefinementOptions",
|
"RefinementOptions",
|
||||||
"RoutingOptions",
|
"RoutingOptions",
|
||||||
"RoutingProblem",
|
"RoutingProblem",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -28,6 +29,50 @@ def _intersection_distance(origin: Port, geometry: BaseGeometry) -> float:
|
||||||
return float(numpy.sqrt((geometry.coords[0][0] - origin.x) ** 2 + (geometry.coords[0][1] - origin.y) ** 2))
|
return float(numpy.sqrt((geometry.coords[0][0] - origin.x) ** 2 + (geometry.coords[0][1] - origin.y) ** 2))
|
||||||
|
|
||||||
|
|
||||||
|
def _bounds_overlap(
|
||||||
|
left: tuple[float, float, float, float],
|
||||||
|
right: tuple[float, float, float, float],
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
left[0] < right[2]
|
||||||
|
and left[2] > right[0]
|
||||||
|
and left[1] < right[3]
|
||||||
|
and left[3] > right[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_non_touching_overlap(left: BaseGeometry, right: BaseGeometry) -> bool:
|
||||||
|
return left.intersects(right) and not left.touches(right)
|
||||||
|
|
||||||
|
|
||||||
|
def _span_to_bounds(
|
||||||
|
gx_min: int,
|
||||||
|
gy_min: int,
|
||||||
|
gx_max: int,
|
||||||
|
gy_max: int,
|
||||||
|
cell_size: float,
|
||||||
|
) -> tuple[float, float, float, float]:
|
||||||
|
return (
|
||||||
|
gx_min * cell_size,
|
||||||
|
gy_min * cell_size,
|
||||||
|
(gx_max + 1) * cell_size,
|
||||||
|
(gy_max + 1) * cell_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PathVerificationDetail:
|
||||||
|
report: RoutingReport
|
||||||
|
conflicting_net_ids: tuple[str, ...] = ()
|
||||||
|
component_conflicts: tuple[tuple[int, str, int], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DynamicCongestionDetail:
|
||||||
|
soft_overlap_count: int = 0
|
||||||
|
hits_frozen_net: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RoutingWorld:
|
class RoutingWorld:
|
||||||
"""
|
"""
|
||||||
Internal spatial state for collision detection, congestion, and verification.
|
Internal spatial state for collision detection, congestion, and verification.
|
||||||
|
|
@ -96,14 +141,34 @@ class RoutingWorld:
|
||||||
def _ensure_dynamic_grid(self) -> None:
|
def _ensure_dynamic_grid(self) -> None:
|
||||||
self._dynamic_paths.ensure_grid()
|
self._dynamic_paths.ensure_grid()
|
||||||
|
|
||||||
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
def add_path(
|
||||||
self._dynamic_paths.add_path(net_id, geometry, dilated_geometry=dilated_geometry)
|
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:
|
def remove_path(self, net_id: str) -> None:
|
||||||
self._dynamic_paths.remove_path(net_id)
|
self._dynamic_paths.remove_path(net_id)
|
||||||
|
|
||||||
|
def has_dynamic_paths(self) -> bool:
|
||||||
|
return bool(self._dynamic_paths.geometries)
|
||||||
|
|
||||||
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
|
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
|
||||||
reach = self.ray_cast(start_port, start_port.r, max_dist=length + 0.01, net_width=net_width)
|
reach = self.ray_cast(
|
||||||
|
start_port,
|
||||||
|
start_port.r,
|
||||||
|
max_dist=length + 0.01,
|
||||||
|
net_width=net_width,
|
||||||
|
caller="straight_static",
|
||||||
|
)
|
||||||
return reach < length - 0.001
|
return reach < length - 0.001
|
||||||
|
|
||||||
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||||
|
|
@ -229,105 +294,390 @@ class RoutingWorld:
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int:
|
def _check_real_congestion(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
candidates_by_net: dict[str, dict[int, tuple[int, ...]]],
|
||||||
|
frozen_net_ids: frozenset[str] = frozenset(),
|
||||||
|
) -> DynamicCongestionDetail:
|
||||||
|
if not candidates_by_net:
|
||||||
|
return DynamicCongestionDetail()
|
||||||
|
|
||||||
dynamic_paths = self._dynamic_paths
|
dynamic_paths = self._dynamic_paths
|
||||||
self._ensure_dynamic_tree()
|
|
||||||
if dynamic_paths.tree is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
total_bounds = result.total_dilated_bounds
|
|
||||||
dynamic_bounds = dynamic_paths.bounds_array
|
|
||||||
possible_total = (
|
|
||||||
(total_bounds[0] < dynamic_bounds[:, 2])
|
|
||||||
& (total_bounds[2] > dynamic_bounds[:, 0])
|
|
||||||
& (total_bounds[1] < dynamic_bounds[:, 3])
|
|
||||||
& (total_bounds[3] > dynamic_bounds[:, 1])
|
|
||||||
)
|
|
||||||
|
|
||||||
valid_hits_mask = dynamic_paths.net_ids_array != net_id
|
|
||||||
if not numpy.any(possible_total & valid_hits_mask):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
geometries_to_test = result.dilated_collision_geometry
|
geometries_to_test = result.dilated_collision_geometry
|
||||||
res_indices, tree_indices = dynamic_paths.tree.query(geometries_to_test, predicate="intersects")
|
|
||||||
if tree_indices.size == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
|
||||||
unique_other_nets = numpy.unique(hit_net_ids[hit_net_ids != net_id])
|
|
||||||
if unique_other_nets.size == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
tree_geometries = dynamic_paths.tree.geometries
|
|
||||||
real_hits_count = 0
|
real_hits_count = 0
|
||||||
for other_net_id in unique_other_nets:
|
for other_net_id, other_obj_ids in candidates_by_net.items():
|
||||||
other_mask = hit_net_ids == other_net_id
|
|
||||||
sub_tree_indices = tree_indices[other_mask]
|
|
||||||
sub_res_indices = res_indices[other_mask]
|
|
||||||
|
|
||||||
found_real = False
|
found_real = False
|
||||||
for index in range(len(sub_tree_indices)):
|
for obj_id, test_geometry_indexes in other_obj_ids.items():
|
||||||
|
tree_geometry = dynamic_paths.dilated[obj_id]
|
||||||
|
for test_geometry_index in test_geometry_indexes:
|
||||||
|
test_geometry = geometries_to_test[test_geometry_index]
|
||||||
if self.metrics is not None:
|
if self.metrics is not None:
|
||||||
self.metrics.total_congestion_exact_pair_checks += 1
|
self.metrics.total_congestion_exact_pair_checks += 1
|
||||||
test_geometry = geometries_to_test[sub_res_indices[index]]
|
if _has_non_touching_overlap(test_geometry, tree_geometry):
|
||||||
tree_geometry = tree_geometries[sub_tree_indices[index]]
|
|
||||||
if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7:
|
|
||||||
found_real = True
|
found_real = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if found_real:
|
if found_real:
|
||||||
|
break
|
||||||
|
if found_real:
|
||||||
|
if other_net_id in frozen_net_ids:
|
||||||
|
return DynamicCongestionDetail(
|
||||||
|
soft_overlap_count=real_hits_count,
|
||||||
|
hits_frozen_net=True,
|
||||||
|
)
|
||||||
real_hits_count += 1
|
real_hits_count += 1
|
||||||
|
|
||||||
return real_hits_count
|
return DynamicCongestionDetail(soft_overlap_count=real_hits_count)
|
||||||
|
|
||||||
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
def _collect_congestion_candidates(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
net_id: str,
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
|
||||||
|
) -> dict[str, dict[int, tuple[int, ...]]]:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
if not dynamic_paths.dilated:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
self._ensure_dynamic_grid()
|
||||||
|
if not dynamic_paths.grid:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
candidates_by_net: dict[str, dict[int, set[int]]] = {}
|
||||||
|
for test_geometry_index, test_bounds in enumerate(result.dilated_bounds):
|
||||||
|
if not self._has_possible_congestion_in_grid(test_bounds, net_id):
|
||||||
|
continue
|
||||||
|
envelope_net_ids = self._get_net_envelope_candidates(
|
||||||
|
test_bounds,
|
||||||
|
net_id,
|
||||||
|
net_envelope_cache,
|
||||||
|
)
|
||||||
|
if not envelope_net_ids:
|
||||||
|
continue
|
||||||
|
grid_net_ids = self._get_grid_span_net_candidates(
|
||||||
|
test_bounds,
|
||||||
|
net_id,
|
||||||
|
grid_net_cache,
|
||||||
|
)
|
||||||
|
if not grid_net_ids:
|
||||||
|
continue
|
||||||
|
candidate_net_ids = tuple(sorted(set(envelope_net_ids) & set(grid_net_ids)))
|
||||||
|
if not candidate_net_ids:
|
||||||
|
continue
|
||||||
|
grid_candidates = self._get_grid_span_candidates(
|
||||||
|
test_bounds,
|
||||||
|
net_id,
|
||||||
|
broad_phase_cache,
|
||||||
|
)
|
||||||
|
for other_net_id in candidate_net_ids:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_candidate_nets += 1
|
||||||
|
obj_ids = grid_candidates.get(other_net_id)
|
||||||
|
if not obj_ids:
|
||||||
|
continue
|
||||||
|
for obj_id in obj_ids:
|
||||||
|
if not _bounds_overlap(test_bounds, dynamic_paths.dilated_bounds[obj_id]):
|
||||||
|
continue
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_candidate_ids += 1
|
||||||
|
candidate_indexes = candidates_by_net.setdefault(other_net_id, {}).setdefault(obj_id, set())
|
||||||
|
candidate_indexes.add(test_geometry_index)
|
||||||
|
|
||||||
|
return {
|
||||||
|
other_net_id: {
|
||||||
|
obj_id: tuple(sorted(test_geometry_indexes))
|
||||||
|
for obj_id, test_geometry_indexes in sorted(obj_ids.items())
|
||||||
|
}
|
||||||
|
for other_net_id, obj_ids in candidates_by_net.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _has_possible_congestion_in_grid(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
net_id: str,
|
||||||
|
) -> bool:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
|
||||||
|
if gx_min == gx_max and gy_min == gy_max:
|
||||||
|
net_counts = dynamic_paths.grid_net_counts.get((gx_min, gy_min))
|
||||||
|
return bool(net_counts and (len(net_counts) > 1 or net_id not in net_counts))
|
||||||
|
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
net_counts = dynamic_paths.grid_net_counts.get((gx, gy))
|
||||||
|
if net_counts and (len(net_counts) > 1 or net_id not in net_counts):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_possible_move_congestion(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
net_id: str,
|
||||||
|
presence_cache: dict[tuple[str, int, int, int, int], bool] | None = None,
|
||||||
|
) -> bool:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
if not dynamic_paths.dilated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._ensure_dynamic_grid()
|
||||||
|
if not dynamic_paths.grid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for bounds in result.dilated_bounds:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
|
||||||
|
if presence_cache is not None and cache_key in presence_cache:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_presence_cache_hits += 1
|
||||||
|
has_possible = presence_cache[cache_key]
|
||||||
|
else:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_presence_cache_misses += 1
|
||||||
|
has_possible = self._has_possible_congestion_in_grid(bounds, net_id)
|
||||||
|
if presence_cache is not None:
|
||||||
|
presence_cache[cache_key] = has_possible
|
||||||
|
if has_possible:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_candidate_move_congestion(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
net_id: str,
|
||||||
|
candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] | None = None,
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
) -> bool:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
if not dynamic_paths.dilated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for bounds in result.dilated_bounds:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
|
||||||
|
if candidate_precheck_cache is not None and cache_key in candidate_precheck_cache:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_candidate_precheck_hits += 1
|
||||||
|
has_candidates = candidate_precheck_cache[cache_key]
|
||||||
|
else:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_candidate_precheck_misses += 1
|
||||||
|
span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size)
|
||||||
|
envelope_net_ids = self._get_net_envelope_candidates(
|
||||||
|
span_bounds,
|
||||||
|
net_id,
|
||||||
|
net_envelope_cache,
|
||||||
|
)
|
||||||
|
if not envelope_net_ids:
|
||||||
|
has_candidates = False
|
||||||
|
else:
|
||||||
|
grid_net_ids = self._get_grid_span_net_candidates(
|
||||||
|
span_bounds,
|
||||||
|
net_id,
|
||||||
|
grid_net_cache,
|
||||||
|
)
|
||||||
|
if not grid_net_ids:
|
||||||
|
has_candidates = False
|
||||||
|
else:
|
||||||
|
has_candidates = bool(set(envelope_net_ids) & set(grid_net_ids))
|
||||||
|
if candidate_precheck_cache is not None:
|
||||||
|
candidate_precheck_cache[cache_key] = has_candidates
|
||||||
|
if has_candidates:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_grid_span_candidates(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
net_id: str,
|
||||||
|
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None,
|
||||||
|
) -> dict[str, tuple[int, ...]]:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
|
||||||
|
if broad_phase_cache is not None and cache_key in broad_phase_cache:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_grid_span_cache_hits += 1
|
||||||
|
return broad_phase_cache[cache_key]
|
||||||
|
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_grid_span_cache_misses += 1
|
||||||
|
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
candidates_by_net: dict[str, set[int]] = {}
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
for other_net_id, obj_ids in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).items():
|
||||||
|
if other_net_id == net_id:
|
||||||
|
continue
|
||||||
|
candidates_by_net.setdefault(other_net_id, set()).update(obj_ids)
|
||||||
|
|
||||||
|
frozen = {
|
||||||
|
other_net_id: tuple(sorted(obj_ids))
|
||||||
|
for other_net_id, obj_ids in sorted(candidates_by_net.items())
|
||||||
|
}
|
||||||
|
if broad_phase_cache is not None:
|
||||||
|
broad_phase_cache[cache_key] = frozen
|
||||||
|
return frozen
|
||||||
|
|
||||||
|
def _get_grid_span_net_candidates(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
net_id: str,
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
|
||||||
|
if grid_net_cache is not None and cache_key in grid_net_cache:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_grid_net_cache_hits += 1
|
||||||
|
return grid_net_cache[cache_key]
|
||||||
|
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_grid_net_cache_misses += 1
|
||||||
|
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
candidate_net_ids: set[str] = set()
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
for other_net_id in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}):
|
||||||
|
if other_net_id != net_id:
|
||||||
|
candidate_net_ids.add(other_net_id)
|
||||||
|
|
||||||
|
frozen = tuple(sorted(candidate_net_ids))
|
||||||
|
if grid_net_cache is not None:
|
||||||
|
grid_net_cache[cache_key] = frozen
|
||||||
|
return frozen
|
||||||
|
|
||||||
|
def _get_net_envelope_candidates(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
net_id: str,
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
|
||||||
|
if net_envelope_cache is not None and cache_key in net_envelope_cache:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_net_envelope_cache_hits += 1
|
||||||
|
cached_net_ids = net_envelope_cache[cache_key]
|
||||||
|
else:
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_congestion_net_envelope_cache_misses += 1
|
||||||
|
span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size)
|
||||||
|
cached_net_ids = tuple(
|
||||||
|
sorted(
|
||||||
|
dynamic_paths.net_envelope_obj_to_net[obj_id]
|
||||||
|
for obj_id in dynamic_paths.net_envelope_index.intersection(span_bounds)
|
||||||
|
if dynamic_paths.net_envelope_obj_to_net[obj_id] != net_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if net_envelope_cache is not None:
|
||||||
|
net_envelope_cache[cache_key] = cached_net_ids
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
other_net_id
|
||||||
|
for other_net_id in cached_net_ids
|
||||||
|
if _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id])
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_verify_net_envelope_candidates(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
net_id: str,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
candidate_net_ids: list[str] = []
|
||||||
|
for obj_id in dynamic_paths.net_envelope_index.intersection(bounds):
|
||||||
|
other_net_id = dynamic_paths.net_envelope_obj_to_net[obj_id]
|
||||||
|
if other_net_id == net_id:
|
||||||
|
continue
|
||||||
|
if not _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id]):
|
||||||
|
continue
|
||||||
|
candidate_net_ids.append(other_net_id)
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_verify_dynamic_candidate_nets += len(candidate_net_ids)
|
||||||
|
return tuple(candidate_net_ids)
|
||||||
|
|
||||||
|
def _get_verify_grid_span_obj_ids(
|
||||||
|
self,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
other_net_id: str,
|
||||||
|
) -> tuple[int, ...]:
|
||||||
|
dynamic_paths = self._dynamic_paths
|
||||||
|
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
|
||||||
|
obj_ids: set[int] = set()
|
||||||
|
for gx in range(gx_min, gx_max + 1):
|
||||||
|
for gy in range(gy_min, gy_max + 1):
|
||||||
|
obj_ids.update(dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).get(other_net_id, ()))
|
||||||
|
return tuple(sorted(obj_ids))
|
||||||
|
|
||||||
|
def check_move_congestion(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
net_id: str,
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
|
||||||
|
) -> int:
|
||||||
|
return self.check_move_congestion_detail(
|
||||||
|
result,
|
||||||
|
net_id,
|
||||||
|
net_envelope_cache=net_envelope_cache,
|
||||||
|
grid_net_cache=grid_net_cache,
|
||||||
|
broad_phase_cache=broad_phase_cache,
|
||||||
|
).soft_overlap_count
|
||||||
|
|
||||||
|
def check_move_congestion_detail(
|
||||||
|
self,
|
||||||
|
result: ComponentResult,
|
||||||
|
net_id: str,
|
||||||
|
*,
|
||||||
|
frozen_net_ids: frozenset[str] = frozenset(),
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
|
||||||
|
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
|
||||||
|
) -> DynamicCongestionDetail:
|
||||||
if self.metrics is not None:
|
if self.metrics is not None:
|
||||||
self.metrics.total_congestion_check_calls += 1
|
self.metrics.total_congestion_check_calls += 1
|
||||||
dynamic_paths = self._dynamic_paths
|
dynamic_paths = self._dynamic_paths
|
||||||
if not dynamic_paths.geometries:
|
if not dynamic_paths.geometries:
|
||||||
return 0
|
return DynamicCongestionDetail()
|
||||||
|
|
||||||
total_bounds = result.total_dilated_bounds
|
candidates_by_net = self._collect_congestion_candidates(
|
||||||
self._ensure_dynamic_grid()
|
result,
|
||||||
dynamic_grid = dynamic_paths.grid
|
net_id,
|
||||||
if not dynamic_grid:
|
net_envelope_cache,
|
||||||
return 0
|
grid_net_cache,
|
||||||
|
broad_phase_cache,
|
||||||
|
)
|
||||||
|
if not candidates_by_net:
|
||||||
|
return DynamicCongestionDetail()
|
||||||
|
return self._check_real_congestion(
|
||||||
|
result,
|
||||||
|
candidates_by_net,
|
||||||
|
frozen_net_ids=frozen_net_ids,
|
||||||
|
)
|
||||||
|
|
||||||
gx_min, gy_min, gx_max, gy_max = grid_cell_span(total_bounds, self.grid_cell_size)
|
def verify_path_details(
|
||||||
|
self,
|
||||||
if gx_min == gx_max and gy_min == gy_max:
|
net_id: str,
|
||||||
cell = (gx_min, gy_min)
|
components: Sequence[ComponentResult],
|
||||||
if cell in dynamic_grid:
|
*,
|
||||||
for obj_id in dynamic_grid[cell]:
|
capture_component_conflicts: bool = False,
|
||||||
if dynamic_paths.geometries[obj_id][0] != net_id:
|
) -> PathVerificationDetail:
|
||||||
return self._check_real_congestion(result, net_id)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
any_possible = False
|
|
||||||
for gx in range(gx_min, gx_max + 1):
|
|
||||||
for gy in range(gy_min, gy_max + 1):
|
|
||||||
cell = (gx, gy)
|
|
||||||
if cell in dynamic_grid:
|
|
||||||
for obj_id in dynamic_grid[cell]:
|
|
||||||
if dynamic_paths.geometries[obj_id][0] != net_id:
|
|
||||||
any_possible = True
|
|
||||||
break
|
|
||||||
if any_possible:
|
|
||||||
break
|
|
||||||
if any_possible:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not any_possible:
|
|
||||||
return 0
|
|
||||||
return self._check_real_congestion(result, net_id)
|
|
||||||
|
|
||||||
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
|
|
||||||
if self.metrics is not None:
|
if self.metrics is not None:
|
||||||
self.metrics.total_verify_path_report_calls += 1
|
self.metrics.total_verify_path_report_calls += 1
|
||||||
static_collision_count = 0
|
static_collision_count = 0
|
||||||
dynamic_collision_count = 0
|
dynamic_collision_count = 0
|
||||||
self_collision_count = 0
|
self_collision_count = 0
|
||||||
total_length = sum(component.length for component in components)
|
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
|
static_obstacles = self._static_obstacles
|
||||||
dynamic_paths = self._dynamic_paths
|
dynamic_paths = self._dynamic_paths
|
||||||
|
|
@ -350,52 +700,78 @@ class RoutingWorld:
|
||||||
if not self._is_in_safety_zone(polygon, obj_id, None, None):
|
if not self._is_in_safety_zone(polygon, obj_id, None, None):
|
||||||
static_collision_count += 1
|
static_collision_count += 1
|
||||||
|
|
||||||
self._ensure_dynamic_tree()
|
if dynamic_paths.dilated:
|
||||||
if dynamic_paths.tree is not None:
|
for component_index, component in enumerate(components):
|
||||||
tree_geometries = dynamic_paths.tree.geometries
|
|
||||||
for component in components:
|
|
||||||
test_geometries = component.dilated_physical_geometry
|
test_geometries = component.dilated_physical_geometry
|
||||||
res_indices, tree_indices = dynamic_paths.tree.query(test_geometries, predicate="intersects")
|
|
||||||
if tree_indices.size == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
|
|
||||||
component_hits = []
|
component_hits = []
|
||||||
for index in range(len(tree_indices)):
|
for new_geometry in test_geometries:
|
||||||
if hit_net_ids[index] == str(net_id):
|
for hit_net_id in self._get_verify_net_envelope_candidates(new_geometry.bounds, str(net_id)):
|
||||||
|
for obj_id in self._get_verify_grid_span_obj_ids(new_geometry.bounds, hit_net_id):
|
||||||
|
if not _bounds_overlap(new_geometry.bounds, dynamic_paths.dilated_bounds[obj_id]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.metrics is not None:
|
if self.metrics is not None:
|
||||||
self.metrics.total_verify_dynamic_exact_pair_checks += 1
|
self.metrics.total_verify_dynamic_exact_pair_checks += 1
|
||||||
new_geometry = test_geometries[res_indices[index]]
|
tree_geometry = dynamic_paths.dilated[obj_id]
|
||||||
tree_geometry = tree_geometries[tree_indices[index]]
|
if _has_non_touching_overlap(new_geometry, tree_geometry):
|
||||||
if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7:
|
component_hits.append(hit_net_id)
|
||||||
component_hits.append(hit_net_ids[index])
|
if capture_component_conflicts:
|
||||||
|
component_conflicts.add(
|
||||||
|
(
|
||||||
|
component_index,
|
||||||
|
hit_net_id,
|
||||||
|
dynamic_paths.component_indexes[obj_id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
if component_hits:
|
if component_hits:
|
||||||
dynamic_collision_count += len(numpy.unique(component_hits))
|
unique_hits = tuple(sorted(set(component_hits)))
|
||||||
|
dynamic_collision_count += len(unique_hits)
|
||||||
|
conflicting_net_ids.update(unique_hits)
|
||||||
|
|
||||||
for index, component in enumerate(components):
|
for index, component in enumerate(components):
|
||||||
for other_index in range(index + 2, len(components)):
|
for other_index in range(index + 2, len(components)):
|
||||||
if components_overlap(component, components[other_index], prefer_actual=True):
|
if components_overlap(component, components[other_index], prefer_actual=True):
|
||||||
self_collision_count += 1
|
self_collision_count += 1
|
||||||
|
|
||||||
return RoutingReport(
|
return PathVerificationDetail(
|
||||||
|
report=RoutingReport(
|
||||||
static_collision_count=static_collision_count,
|
static_collision_count=static_collision_count,
|
||||||
dynamic_collision_count=dynamic_collision_count,
|
dynamic_collision_count=dynamic_collision_count,
|
||||||
self_collision_count=self_collision_count,
|
self_collision_count=self_collision_count,
|
||||||
total_length=total_length,
|
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:
|
||||||
|
return self.verify_path_details(net_id, components).report
|
||||||
|
|
||||||
def ray_cast(
|
def ray_cast(
|
||||||
self,
|
self,
|
||||||
origin: Port,
|
origin: Port,
|
||||||
angle_deg: float,
|
angle_deg: float,
|
||||||
max_dist: float = 2000.0,
|
max_dist: float = 2000.0,
|
||||||
net_width: float | None = None,
|
net_width: float | None = None,
|
||||||
|
caller: str = "other",
|
||||||
) -> float:
|
) -> float:
|
||||||
if self.metrics is not None:
|
if self.metrics is not None:
|
||||||
self.metrics.total_ray_cast_calls += 1
|
self.metrics.total_ray_cast_calls += 1
|
||||||
|
if caller == "straight_static":
|
||||||
|
self.metrics.total_ray_cast_calls_straight_static += 1
|
||||||
|
elif caller == "expand_snap":
|
||||||
|
self.metrics.total_ray_cast_calls_expand_snap += 1
|
||||||
|
elif caller == "expand_forward":
|
||||||
|
self.metrics.total_ray_cast_calls_expand_forward += 1
|
||||||
|
elif caller == "visibility_build":
|
||||||
|
self.metrics.total_ray_cast_calls_visibility_build += 1
|
||||||
|
elif caller == "visibility_query":
|
||||||
|
self.metrics.total_ray_cast_calls_visibility_query += 1
|
||||||
|
elif caller == "visibility_tangent":
|
||||||
|
self.metrics.total_ray_cast_calls_visibility_tangent += 1
|
||||||
|
else:
|
||||||
|
self.metrics.total_ray_cast_calls_other += 1
|
||||||
static_obstacles = self._static_obstacles
|
static_obstacles = self._static_obstacles
|
||||||
tree: STRtree | None
|
tree: STRtree | None
|
||||||
is_rect_array: numpy.ndarray | None
|
is_rect_array: numpy.ndarray | None
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,20 @@ class DynamicPathIndex:
|
||||||
"engine",
|
"engine",
|
||||||
"index",
|
"index",
|
||||||
"geometries",
|
"geometries",
|
||||||
|
"component_indexes",
|
||||||
"dilated",
|
"dilated",
|
||||||
|
"dilated_bounds",
|
||||||
|
"net_envelope_index",
|
||||||
|
"net_envelopes",
|
||||||
|
"net_envelope_obj_ids",
|
||||||
|
"net_envelope_obj_to_net",
|
||||||
"tree",
|
"tree",
|
||||||
"obj_ids",
|
"obj_ids",
|
||||||
"grid",
|
"grid",
|
||||||
|
"grid_net_obj_ids",
|
||||||
|
"grid_net_counts",
|
||||||
|
"obj_cells",
|
||||||
|
"net_to_obj_ids",
|
||||||
"id_counter",
|
"id_counter",
|
||||||
"net_ids_array",
|
"net_ids_array",
|
||||||
"bounds_array",
|
"bounds_array",
|
||||||
|
|
@ -34,17 +44,71 @@ class DynamicPathIndex:
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self.index = rtree.index.Index()
|
self.index = rtree.index.Index()
|
||||||
self.geometries: dict[int, tuple[str, Polygon]] = {}
|
self.geometries: dict[int, tuple[str, Polygon]] = {}
|
||||||
|
self.component_indexes: dict[int, int] = {}
|
||||||
self.dilated: dict[int, Polygon] = {}
|
self.dilated: dict[int, Polygon] = {}
|
||||||
|
self.dilated_bounds: dict[int, tuple[float, float, float, float]] = {}
|
||||||
|
self.net_envelope_index = rtree.index.Index()
|
||||||
|
self.net_envelopes: dict[str, tuple[float, float, float, float]] = {}
|
||||||
|
self.net_envelope_obj_ids: dict[str, int] = {}
|
||||||
|
self.net_envelope_obj_to_net: dict[int, str] = {}
|
||||||
self.tree: STRtree | None = None
|
self.tree: STRtree | None = None
|
||||||
self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
||||||
self.grid: dict[tuple[int, int], list[int]] = {}
|
self.grid: dict[tuple[int, int], list[int]] = {}
|
||||||
|
self.grid_net_obj_ids: dict[tuple[int, int], dict[str, set[int]]] = {}
|
||||||
|
self.grid_net_counts: dict[tuple[int, int], dict[str, int]] = {}
|
||||||
|
self.obj_cells: dict[int, tuple[tuple[int, int], ...]] = {}
|
||||||
|
self.net_to_obj_ids: dict[str, set[int]] = {}
|
||||||
self.id_counter = 0
|
self.id_counter = 0
|
||||||
self.net_ids_array = numpy.array([], dtype=object)
|
self.net_ids_array = numpy.array([], dtype=object)
|
||||||
self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
||||||
|
|
||||||
|
def _combine_net_bounds(self, obj_ids: set[int]) -> tuple[float, float, float, float]:
|
||||||
|
first_obj_id = next(iter(obj_ids))
|
||||||
|
minx, miny, maxx, maxy = self.dilated_bounds[first_obj_id]
|
||||||
|
for obj_id in obj_ids:
|
||||||
|
bounds = self.dilated_bounds[obj_id]
|
||||||
|
minx = min(minx, bounds[0])
|
||||||
|
miny = min(miny, bounds[1])
|
||||||
|
maxx = max(maxx, bounds[2])
|
||||||
|
maxy = max(maxy, bounds[3])
|
||||||
|
return (minx, miny, maxx, maxy)
|
||||||
|
|
||||||
|
def _set_net_envelope(self, net_id: str, bounds: tuple[float, float, float, float]) -> None:
|
||||||
|
old_bounds = self.net_envelopes.get(net_id)
|
||||||
|
if old_bounds is not None:
|
||||||
|
obj_id = self.net_envelope_obj_ids[net_id]
|
||||||
|
self.net_envelope_index.delete(obj_id, old_bounds)
|
||||||
|
else:
|
||||||
|
obj_id = len(self.net_envelope_obj_ids)
|
||||||
|
while obj_id in self.net_envelope_obj_to_net:
|
||||||
|
obj_id += 1
|
||||||
|
self.net_envelope_obj_ids[net_id] = obj_id
|
||||||
|
self.net_envelope_obj_to_net[obj_id] = net_id
|
||||||
|
|
||||||
|
self.net_envelopes[net_id] = bounds
|
||||||
|
self.net_envelope_index.insert(self.net_envelope_obj_ids[net_id], bounds)
|
||||||
|
|
||||||
|
def _clear_net_envelope(self, net_id: str) -> None:
|
||||||
|
old_bounds = self.net_envelopes.pop(net_id, None)
|
||||||
|
obj_id = self.net_envelope_obj_ids.pop(net_id, None)
|
||||||
|
if old_bounds is None or obj_id is None:
|
||||||
|
return
|
||||||
|
self.net_envelope_index.delete(obj_id, old_bounds)
|
||||||
|
self.net_envelope_obj_to_net.pop(obj_id, None)
|
||||||
|
|
||||||
|
def _refresh_net_envelope(self, net_id: str) -> None:
|
||||||
|
obj_ids = self.net_to_obj_ids.get(net_id)
|
||||||
|
if not obj_ids:
|
||||||
|
self._clear_net_envelope(net_id)
|
||||||
|
return
|
||||||
|
self._set_net_envelope(net_id, self._combine_net_bounds(obj_ids))
|
||||||
|
|
||||||
def invalidate_queries(self) -> None:
|
def invalidate_queries(self) -> None:
|
||||||
self.tree = None
|
self.tree = None
|
||||||
self.grid = {}
|
self.grid = {}
|
||||||
|
self.grid_net_obj_ids = {}
|
||||||
|
self.grid_net_counts = {}
|
||||||
|
self.obj_cells = {}
|
||||||
|
|
||||||
def ensure_tree(self) -> None:
|
def ensure_tree(self) -> None:
|
||||||
if self.tree is None and self.dilated:
|
if self.tree is None and self.dilated:
|
||||||
|
|
@ -65,33 +129,105 @@ class DynamicPathIndex:
|
||||||
self.engine.metrics.total_dynamic_grid_rebuilds += 1
|
self.engine.metrics.total_dynamic_grid_rebuilds += 1
|
||||||
cell_size = self.engine.grid_cell_size
|
cell_size = self.engine.grid_cell_size
|
||||||
for obj_id, polygon in self.dilated.items():
|
for obj_id, polygon in self.dilated.items():
|
||||||
for cell in iter_grid_cells(polygon.bounds, cell_size):
|
self._register_grid_membership(obj_id, self.geometries[obj_id][0], polygon.bounds, cell_size=cell_size)
|
||||||
self.grid.setdefault(cell, []).append(obj_id)
|
|
||||||
|
|
||||||
def add_path(self, net_id: str, geometry: Sequence[Polygon], dilated_geometry: Sequence[Polygon]) -> None:
|
def _register_grid_membership(
|
||||||
self.invalidate_queries()
|
self,
|
||||||
|
obj_id: int,
|
||||||
|
net_id: str,
|
||||||
|
bounds: tuple[float, float, float, float],
|
||||||
|
*,
|
||||||
|
cell_size: float,
|
||||||
|
) -> None:
|
||||||
|
cells = tuple(iter_grid_cells(bounds, cell_size))
|
||||||
|
self.obj_cells[obj_id] = cells
|
||||||
|
for cell in cells:
|
||||||
|
self.grid.setdefault(cell, []).append(obj_id)
|
||||||
|
net_obj_ids = self.grid_net_obj_ids.setdefault(cell, {})
|
||||||
|
net_obj_ids.setdefault(net_id, set()).add(obj_id)
|
||||||
|
net_counts = self.grid_net_counts.setdefault(cell, {})
|
||||||
|
net_counts[net_id] = net_counts.get(net_id, 0) + 1
|
||||||
|
|
||||||
|
def _unregister_grid_membership(self, obj_id: int, net_id: str) -> None:
|
||||||
|
cells = self.obj_cells.pop(obj_id, ())
|
||||||
|
for cell in cells:
|
||||||
|
obj_ids = self.grid.get(cell)
|
||||||
|
if obj_ids is not None:
|
||||||
|
try:
|
||||||
|
obj_ids.remove(obj_id)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if not obj_ids:
|
||||||
|
self.grid.pop(cell, None)
|
||||||
|
net_obj_ids = self.grid_net_obj_ids.get(cell)
|
||||||
|
if net_obj_ids is not None:
|
||||||
|
member_ids = net_obj_ids.get(net_id)
|
||||||
|
if member_ids is not None:
|
||||||
|
member_ids.discard(obj_id)
|
||||||
|
if not member_ids:
|
||||||
|
net_obj_ids.pop(net_id, None)
|
||||||
|
if not net_obj_ids:
|
||||||
|
self.grid_net_obj_ids.pop(cell, None)
|
||||||
|
net_counts = self.grid_net_counts.get(cell)
|
||||||
|
if net_counts is not None:
|
||||||
|
remaining = net_counts.get(net_id, 0) - 1
|
||||||
|
if remaining > 0:
|
||||||
|
net_counts[net_id] = remaining
|
||||||
|
else:
|
||||||
|
net_counts.pop(net_id, None)
|
||||||
|
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],
|
||||||
|
component_indexes: Sequence[int] | None = None,
|
||||||
|
) -> None:
|
||||||
if self.engine.metrics is not None:
|
if self.engine.metrics is not None:
|
||||||
self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
|
self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
|
||||||
|
cell_size = self.engine.grid_cell_size
|
||||||
for index, polygon in enumerate(geometry):
|
for index, polygon in enumerate(geometry):
|
||||||
obj_id = self.id_counter
|
obj_id = self.id_counter
|
||||||
self.id_counter += 1
|
self.id_counter += 1
|
||||||
dilated = dilated_geometry[index]
|
dilated = dilated_geometry[index]
|
||||||
|
dilated_bounds = dilated.bounds
|
||||||
self.geometries[obj_id] = (net_id, polygon)
|
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[obj_id] = dilated
|
||||||
self.index.insert(obj_id, dilated.bounds)
|
self.dilated_bounds[obj_id] = dilated_bounds
|
||||||
|
self.index.insert(obj_id, dilated_bounds)
|
||||||
|
self.net_to_obj_ids.setdefault(net_id, set()).add(obj_id)
|
||||||
|
self._register_grid_membership(obj_id, net_id, dilated_bounds, cell_size=cell_size)
|
||||||
|
self._refresh_net_envelope(net_id)
|
||||||
|
self.tree = None
|
||||||
|
|
||||||
def remove_path(self, net_id: str) -> None:
|
def remove_path(self, net_id: str) -> None:
|
||||||
to_remove = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id]
|
to_remove = list(self.net_to_obj_ids.get(net_id, ()))
|
||||||
self.remove_obj_ids(to_remove)
|
self.remove_obj_ids(to_remove)
|
||||||
|
|
||||||
def remove_obj_ids(self, obj_ids: list[int]) -> None:
|
def remove_obj_ids(self, obj_ids: list[int]) -> None:
|
||||||
if not obj_ids:
|
if not obj_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.invalidate_queries()
|
|
||||||
if self.engine.metrics is not None:
|
if self.engine.metrics is not None:
|
||||||
self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids)
|
self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids)
|
||||||
|
affected_nets: set[str] = set()
|
||||||
for obj_id in obj_ids:
|
for obj_id in obj_ids:
|
||||||
self.index.delete(obj_id, self.dilated[obj_id].bounds)
|
net_id, _ = self.geometries[obj_id]
|
||||||
|
affected_nets.add(net_id)
|
||||||
|
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.geometries[obj_id]
|
||||||
|
del self.component_indexes[obj_id]
|
||||||
del self.dilated[obj_id]
|
del self.dilated[obj_id]
|
||||||
|
del self.dilated_bounds[obj_id]
|
||||||
|
obj_id_set = self.net_to_obj_ids.get(net_id)
|
||||||
|
if obj_id_set is not None:
|
||||||
|
obj_id_set.discard(obj_id)
|
||||||
|
if not obj_id_set:
|
||||||
|
self.net_to_obj_ids.pop(net_id, None)
|
||||||
|
for net_id in affected_nets:
|
||||||
|
self._refresh_net_envelope(net_id)
|
||||||
|
self.tree = None
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ class RefinementOptions:
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class DiagnosticsOptions:
|
class DiagnosticsOptions:
|
||||||
capture_expanded: bool = False
|
capture_expanded: bool = False
|
||||||
|
capture_conflict_trace: bool = False
|
||||||
|
capture_frontier_trace: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
|
||||||
114
inire/results.py
114
inire/results.py
|
|
@ -12,6 +12,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
RoutingOutcome = Literal["completed", "colliding", "partial", "unroutable"]
|
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)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -30,6 +32,52 @@ class RoutingReport:
|
||||||
return self.collision_count == 0
|
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)
|
@dataclass(frozen=True, slots=True)
|
||||||
class RouteMetrics:
|
class RouteMetrics:
|
||||||
nodes_expanded: int
|
nodes_expanded: int
|
||||||
|
|
@ -45,14 +93,48 @@ class RouteMetrics:
|
||||||
warm_start_paths_used: int
|
warm_start_paths_used: int
|
||||||
refine_path_calls: int
|
refine_path_calls: int
|
||||||
timeout_events: int
|
timeout_events: int
|
||||||
|
iteration_reverify_calls: int
|
||||||
|
iteration_reverified_nets: int
|
||||||
|
iteration_conflicting_nets: int
|
||||||
|
iteration_conflict_edges: int
|
||||||
|
nets_carried_forward: int
|
||||||
|
score_component_calls: int
|
||||||
|
score_component_total_ns: int
|
||||||
|
path_cost_calls: int
|
||||||
|
danger_map_lookup_calls: int
|
||||||
|
danger_map_cache_hits: int
|
||||||
|
danger_map_cache_misses: int
|
||||||
|
danger_map_query_calls: int
|
||||||
|
danger_map_total_ns: int
|
||||||
move_cache_abs_hits: int
|
move_cache_abs_hits: int
|
||||||
move_cache_abs_misses: int
|
move_cache_abs_misses: int
|
||||||
move_cache_rel_hits: int
|
move_cache_rel_hits: int
|
||||||
move_cache_rel_misses: 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
|
static_safe_cache_hits: int
|
||||||
hard_collision_cache_hits: int
|
hard_collision_cache_hits: int
|
||||||
congestion_cache_hits: int
|
congestion_cache_hits: int
|
||||||
congestion_cache_misses: int
|
congestion_cache_misses: int
|
||||||
|
congestion_presence_cache_hits: int
|
||||||
|
congestion_presence_cache_misses: int
|
||||||
|
congestion_presence_skips: int
|
||||||
|
congestion_candidate_precheck_hits: int
|
||||||
|
congestion_candidate_precheck_misses: int
|
||||||
|
congestion_candidate_precheck_skips: int
|
||||||
|
congestion_grid_net_cache_hits: int
|
||||||
|
congestion_grid_net_cache_misses: int
|
||||||
|
congestion_grid_span_cache_hits: int
|
||||||
|
congestion_grid_span_cache_misses: int
|
||||||
|
congestion_candidate_nets: int
|
||||||
|
congestion_net_envelope_cache_hits: int
|
||||||
|
congestion_net_envelope_cache_misses: int
|
||||||
dynamic_path_objects_added: int
|
dynamic_path_objects_added: int
|
||||||
dynamic_path_objects_removed: int
|
dynamic_path_objects_removed: int
|
||||||
dynamic_tree_rebuilds: int
|
dynamic_tree_rebuilds: int
|
||||||
|
|
@ -60,21 +142,47 @@ class RouteMetrics:
|
||||||
static_tree_rebuilds: int
|
static_tree_rebuilds: int
|
||||||
static_raw_tree_rebuilds: int
|
static_raw_tree_rebuilds: int
|
||||||
static_net_tree_rebuilds: int
|
static_net_tree_rebuilds: int
|
||||||
|
visibility_corner_index_builds: int
|
||||||
visibility_builds: int
|
visibility_builds: int
|
||||||
visibility_corner_pairs_checked: int
|
visibility_corner_pairs_checked: int
|
||||||
visibility_corner_queries: int
|
visibility_corner_queries_exact: int
|
||||||
visibility_corner_hits: int
|
visibility_corner_hits_exact: int
|
||||||
visibility_point_queries: int
|
visibility_point_queries: int
|
||||||
visibility_point_cache_hits: int
|
visibility_point_cache_hits: int
|
||||||
visibility_point_cache_misses: int
|
visibility_point_cache_misses: int
|
||||||
|
visibility_tangent_candidate_scans: int
|
||||||
|
visibility_tangent_candidate_corner_checks: int
|
||||||
|
visibility_tangent_candidate_ray_tests: int
|
||||||
ray_cast_calls: int
|
ray_cast_calls: int
|
||||||
|
ray_cast_calls_straight_static: int
|
||||||
|
ray_cast_calls_expand_snap: int
|
||||||
|
ray_cast_calls_expand_forward: int
|
||||||
|
ray_cast_calls_visibility_build: int
|
||||||
|
ray_cast_calls_visibility_query: int
|
||||||
|
ray_cast_calls_visibility_tangent: int
|
||||||
|
ray_cast_calls_other: int
|
||||||
ray_cast_candidate_bounds: int
|
ray_cast_candidate_bounds: int
|
||||||
ray_cast_exact_geometry_checks: int
|
ray_cast_exact_geometry_checks: int
|
||||||
congestion_check_calls: int
|
congestion_check_calls: int
|
||||||
|
congestion_lazy_resolutions: int
|
||||||
|
congestion_lazy_requeues: int
|
||||||
|
congestion_candidate_ids: int
|
||||||
congestion_exact_pair_checks: int
|
congestion_exact_pair_checks: int
|
||||||
verify_path_report_calls: int
|
verify_path_report_calls: int
|
||||||
verify_static_buffer_ops: int
|
verify_static_buffer_ops: int
|
||||||
|
verify_dynamic_candidate_nets: int
|
||||||
verify_dynamic_exact_pair_checks: int
|
verify_dynamic_exact_pair_checks: int
|
||||||
|
refinement_windows_considered: int
|
||||||
|
refinement_static_bounds_checked: int
|
||||||
|
refinement_dynamic_bounds_checked: int
|
||||||
|
refinement_candidate_side_extents: int
|
||||||
|
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)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -121,3 +229,5 @@ class RoutingRunResult:
|
||||||
results_by_net: dict[str, RoutingResult]
|
results_by_net: dict[str, RoutingResult]
|
||||||
metrics: RouteMetrics
|
metrics: RouteMetrics
|
||||||
expanded_nodes: tuple[tuple[int, int, int], ...] = ()
|
expanded_nodes: tuple[tuple[int, int, int], ...] = ()
|
||||||
|
conflict_trace: tuple[ConflictTraceEntry, ...] = ()
|
||||||
|
frontier_trace: tuple[NetFrontierTrace, ...] = ()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ def process_move(
|
||||||
context: AStarContext,
|
context: AStarContext,
|
||||||
metrics: AStarMetrics,
|
metrics: AStarMetrics,
|
||||||
congestion_cache: dict[tuple, int],
|
congestion_cache: dict[tuple, int],
|
||||||
|
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
|
||||||
config: SearchRunConfig,
|
config: SearchRunConfig,
|
||||||
move_class: MoveKind,
|
move_class: MoveKind,
|
||||||
params: tuple,
|
params: tuple,
|
||||||
|
|
@ -109,6 +114,11 @@ def process_move(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config,
|
config,
|
||||||
move_class,
|
move_class,
|
||||||
abs_key,
|
abs_key,
|
||||||
|
|
@ -126,15 +136,23 @@ def add_node(
|
||||||
context: AStarContext,
|
context: AStarContext,
|
||||||
metrics: AStarMetrics,
|
metrics: AStarMetrics,
|
||||||
congestion_cache: dict[tuple, int],
|
congestion_cache: dict[tuple, int],
|
||||||
|
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
|
||||||
config: SearchRunConfig,
|
config: SearchRunConfig,
|
||||||
move_type: MoveKind,
|
move_type: MoveKind,
|
||||||
cache_key: tuple,
|
cache_key: tuple,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
frontier_trace = config.frontier_trace
|
||||||
metrics.moves_generated += 1
|
metrics.moves_generated += 1
|
||||||
metrics.total_moves_generated += 1
|
metrics.total_moves_generated += 1
|
||||||
state = result.end_port.as_tuple()
|
state = result.end_port.as_tuple()
|
||||||
new_lower_bound_g = parent.g_cost + result.length
|
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 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.pruned_closed_set += 1
|
||||||
metrics.total_pruned_closed_set += 1
|
metrics.total_pruned_closed_set += 1
|
||||||
return
|
return
|
||||||
|
|
@ -143,6 +161,8 @@ def add_node(
|
||||||
end_p = result.end_port
|
end_p = result.end_port
|
||||||
|
|
||||||
if cache_key in context.hard_collision_set:
|
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
|
context.metrics.total_hard_collision_cache_hits += 1
|
||||||
metrics.pruned_hard_collision += 1
|
metrics.pruned_hard_collision += 1
|
||||||
metrics.total_pruned_hard_collision += 1
|
metrics.total_pruned_hard_collision += 1
|
||||||
|
|
@ -159,41 +179,100 @@ def add_node(
|
||||||
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p)
|
collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p)
|
||||||
if collision_found:
|
if collision_found:
|
||||||
context.hard_collision_set.add(cache_key)
|
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.pruned_hard_collision += 1
|
||||||
metrics.total_pruned_hard_collision += 1
|
metrics.total_pruned_hard_collision += 1
|
||||||
return
|
return
|
||||||
context.static_safe_cache.add(cache_key)
|
context.static_safe_cache.add(cache_key)
|
||||||
|
|
||||||
total_overlaps = 0
|
|
||||||
if not config.skip_congestion:
|
|
||||||
if cache_key in congestion_cache:
|
|
||||||
context.metrics.total_congestion_cache_hits += 1
|
|
||||||
total_overlaps = congestion_cache[cache_key]
|
|
||||||
else:
|
|
||||||
context.metrics.total_congestion_cache_misses += 1
|
|
||||||
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
|
||||||
congestion_cache[cache_key] = total_overlaps
|
|
||||||
|
|
||||||
if config.self_collision_check and component_hits_ancestor_chain(result, parent):
|
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
|
return
|
||||||
|
|
||||||
move_cost = context.cost_evaluator.score_component(
|
move_cost = context.cost_evaluator.score_component(
|
||||||
result,
|
result,
|
||||||
start_port=parent_p,
|
start_port=parent_p,
|
||||||
)
|
)
|
||||||
move_cost += total_overlaps * context.congestion_penalty
|
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 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.pruned_cost += 1
|
||||||
metrics.total_pruned_cost += 1
|
metrics.total_pruned_cost += 1
|
||||||
return
|
return
|
||||||
if move_cost > 1e12:
|
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.pruned_cost += 1
|
||||||
metrics.total_pruned_cost += 1
|
metrics.total_pruned_cost += 1
|
||||||
return
|
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
|
||||||
|
|
||||||
|
total_overlaps = 0
|
||||||
|
if not config.skip_congestion and context.cost_evaluator.collision_engine.has_dynamic_paths():
|
||||||
|
ce = context.cost_evaluator.collision_engine
|
||||||
|
if ce.has_possible_move_congestion(result, net_id, congestion_presence_cache):
|
||||||
|
if ce.has_candidate_move_congestion(
|
||||||
|
result,
|
||||||
|
net_id,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
):
|
||||||
|
if cache_key in congestion_cache:
|
||||||
|
context.metrics.total_congestion_cache_hits += 1
|
||||||
|
total_overlaps = congestion_cache[cache_key]
|
||||||
|
else:
|
||||||
|
context.metrics.total_congestion_cache_misses += 1
|
||||||
|
total_overlaps = ce.check_move_congestion(
|
||||||
|
result,
|
||||||
|
net_id,
|
||||||
|
net_envelope_cache=congestion_net_envelope_cache,
|
||||||
|
grid_net_cache=congestion_grid_net_cache,
|
||||||
|
broad_phase_cache=congestion_grid_span_cache,
|
||||||
|
)
|
||||||
|
congestion_cache[cache_key] = total_overlaps
|
||||||
|
else:
|
||||||
|
context.metrics.total_congestion_candidate_precheck_skips += 1
|
||||||
|
else:
|
||||||
|
context.metrics.total_congestion_presence_skips += 1
|
||||||
|
move_cost += total_overlaps * context.congestion_penalty
|
||||||
|
|
||||||
g_cost = parent.g_cost + move_cost
|
g_cost = parent.g_cost + move_cost
|
||||||
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
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.pruned_closed_set += 1
|
||||||
metrics.total_pruned_closed_set += 1
|
metrics.total_pruned_closed_set += 1
|
||||||
return
|
return
|
||||||
|
|
@ -203,6 +282,16 @@ def add_node(
|
||||||
target,
|
target,
|
||||||
min_bend_radius=context.min_bend_radius,
|
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.moves_added += 1
|
||||||
metrics.total_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)
|
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:
|
def _sbend_forward_span(offset: float, radius: float) -> float | None:
|
||||||
abs_offset = abs(offset)
|
abs_offset = abs(offset)
|
||||||
if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius:
|
if abs_offset <= TOLERANCE_LINEAR or radius <= 0 or abs_offset >= 2.0 * radius:
|
||||||
|
|
@ -63,19 +84,25 @@ def _visible_straight_candidates(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
visibility_manager = context.visibility_manager
|
visibility_manager = context.visibility_manager
|
||||||
visibility_manager._ensure_current()
|
visibility_manager.ensure_corner_index_current()
|
||||||
|
context.metrics.total_visibility_tangent_candidate_scans += 1
|
||||||
max_bend_radius = max(search_options.bend_radii, default=0.0)
|
max_bend_radius = max(search_options.bend_radii, default=0.0)
|
||||||
if max_bend_radius <= 0 or not visibility_manager.corners:
|
if max_bend_radius <= 0 or not visibility_manager.corners:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
reach = max_reach + max_bend_radius
|
reach = max_reach + max_bend_radius
|
||||||
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach)
|
candidate_ids = visibility_manager.get_tangent_corner_candidates(
|
||||||
candidate_ids = list(visibility_manager.corner_index.intersection(bounds))
|
current,
|
||||||
|
min_forward=search_options.min_straight_length,
|
||||||
|
max_forward=reach,
|
||||||
|
radii=search_options.bend_radii,
|
||||||
|
)
|
||||||
if not candidate_ids:
|
if not candidate_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
scored: list[tuple[float, float, float, float, float]] = []
|
scored: list[tuple[float, float, float, float, float]] = []
|
||||||
for idx in candidate_ids:
|
for idx in candidate_ids:
|
||||||
|
context.metrics.total_visibility_tangent_candidate_corner_checks += 1
|
||||||
cx, cy = visibility_manager.corners[idx]
|
cx, cy = visibility_manager.corners[idx]
|
||||||
dx = cx - current.x
|
dx = cx - current.x
|
||||||
dy = cy - current.y
|
dy = cy - current.y
|
||||||
|
|
@ -101,8 +128,15 @@ def _visible_straight_candidates(
|
||||||
collision_engine = context.cost_evaluator.collision_engine
|
collision_engine = context.cost_evaluator.collision_engine
|
||||||
tangent_candidates: set[int] = set()
|
tangent_candidates: set[int] = set()
|
||||||
for _, dist, length, dx, dy in sorted(scored)[:4]:
|
for _, dist, length, dx, dy in sorted(scored)[:4]:
|
||||||
|
context.metrics.total_visibility_tangent_candidate_ray_tests += 1
|
||||||
angle = math.degrees(math.atan2(dy, dx))
|
angle = math.degrees(math.atan2(dy, dx))
|
||||||
corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width)
|
corner_reach = collision_engine.ray_cast(
|
||||||
|
current,
|
||||||
|
angle,
|
||||||
|
max_dist=dist + 0.05,
|
||||||
|
net_width=net_width,
|
||||||
|
caller="visibility_tangent",
|
||||||
|
)
|
||||||
if corner_reach < dist - 0.01:
|
if corner_reach < dist - 0.01:
|
||||||
continue
|
continue
|
||||||
qlen = int(round(length))
|
qlen = int(round(length))
|
||||||
|
|
@ -132,6 +166,11 @@ def expand_moves(
|
||||||
context: AStarContext,
|
context: AStarContext,
|
||||||
metrics: AStarMetrics,
|
metrics: AStarMetrics,
|
||||||
congestion_cache: dict[tuple, int],
|
congestion_cache: dict[tuple, int],
|
||||||
|
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
|
||||||
|
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
|
||||||
|
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
|
||||||
config: SearchRunConfig,
|
config: SearchRunConfig,
|
||||||
) -> None:
|
) -> None:
|
||||||
search_options = context.options.search
|
search_options = context.options.search
|
||||||
|
|
@ -156,7 +195,13 @@ def expand_moves(
|
||||||
dy_local = perp_t
|
dy_local = perp_t
|
||||||
|
|
||||||
if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r:
|
if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r:
|
||||||
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, proj_t + 1.0, net_width=net_width)
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(
|
||||||
|
cp,
|
||||||
|
cp.r,
|
||||||
|
proj_t + 1.0,
|
||||||
|
net_width=net_width,
|
||||||
|
caller="expand_snap",
|
||||||
|
)
|
||||||
if max_reach >= proj_t - 0.01 and (
|
if max_reach >= proj_t - 0.01 and (
|
||||||
prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR
|
prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR
|
||||||
):
|
):
|
||||||
|
|
@ -170,12 +215,25 @@ def expand_moves(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config,
|
config,
|
||||||
"straight",
|
"straight",
|
||||||
(int(round(proj_t)),),
|
(int(round(proj_t)),),
|
||||||
)
|
)
|
||||||
|
|
||||||
max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, search_options.max_straight_length, net_width=net_width)
|
max_reach = context.cost_evaluator.collision_engine.ray_cast(
|
||||||
|
cp,
|
||||||
|
cp.r,
|
||||||
|
search_options.max_straight_length,
|
||||||
|
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 = [
|
candidate_lengths = [
|
||||||
search_options.min_straight_length,
|
search_options.min_straight_length,
|
||||||
max_reach,
|
max_reach,
|
||||||
|
|
@ -221,6 +279,11 @@ def expand_moves(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config,
|
config,
|
||||||
"straight",
|
"straight",
|
||||||
(length,),
|
(length,),
|
||||||
|
|
@ -249,6 +312,11 @@ def expand_moves(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config,
|
config,
|
||||||
"bend90",
|
"bend90",
|
||||||
(radius, direction),
|
(radius, direction),
|
||||||
|
|
@ -283,6 +351,11 @@ def expand_moves(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config,
|
config,
|
||||||
"sbend",
|
"sbend",
|
||||||
(offset, radius),
|
(offset, radius),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from inire.model import resolve_bend_geometry
|
from inire.model import resolve_bend_geometry
|
||||||
|
|
@ -12,6 +12,51 @@ if TYPE_CHECKING:
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.model import RoutingOptions, RoutingProblem
|
from inire.model import RoutingOptions, RoutingProblem
|
||||||
from inire.router.cost import CostEvaluator
|
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)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -20,6 +65,9 @@ class SearchRunConfig:
|
||||||
bend_physical_geometry: BendPhysicalGeometry
|
bend_physical_geometry: BendPhysicalGeometry
|
||||||
bend_clip_margin: float | None
|
bend_clip_margin: float | None
|
||||||
node_limit: int
|
node_limit: int
|
||||||
|
guidance_seed: tuple[PathSegmentSeed, ...] | None = None
|
||||||
|
guidance_bonus: float = 0.0
|
||||||
|
frontier_trace: FrontierTraceCollector | None = None
|
||||||
return_partial: bool = False
|
return_partial: bool = False
|
||||||
store_expanded: bool = False
|
store_expanded: bool = False
|
||||||
skip_congestion: bool = False
|
skip_congestion: bool = False
|
||||||
|
|
@ -33,6 +81,9 @@ class SearchRunConfig:
|
||||||
*,
|
*,
|
||||||
bend_collision_type: BendCollisionModel | None = None,
|
bend_collision_type: BendCollisionModel | None = None,
|
||||||
node_limit: int | 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,
|
return_partial: bool = False,
|
||||||
store_expanded: bool = False,
|
store_expanded: bool = False,
|
||||||
skip_congestion: bool = False,
|
skip_congestion: bool = False,
|
||||||
|
|
@ -49,6 +100,9 @@ class SearchRunConfig:
|
||||||
bend_physical_geometry=bend_physical_geometry,
|
bend_physical_geometry=bend_physical_geometry,
|
||||||
bend_clip_margin=search.bend_clip_margin,
|
bend_clip_margin=search.bend_clip_margin,
|
||||||
node_limit=search.node_limit if node_limit is None else node_limit,
|
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,
|
return_partial=return_partial,
|
||||||
store_expanded=store_expanded,
|
store_expanded=store_expanded,
|
||||||
skip_congestion=skip_congestion,
|
skip_congestion=skip_congestion,
|
||||||
|
|
@ -58,7 +112,18 @@ class SearchRunConfig:
|
||||||
|
|
||||||
|
|
||||||
class AStarNode:
|
class AStarNode:
|
||||||
__slots__ = ("port", "g_cost", "h_cost", "fh_cost", "parent", "component_result")
|
__slots__ = (
|
||||||
|
"port",
|
||||||
|
"g_cost",
|
||||||
|
"h_cost",
|
||||||
|
"fh_cost",
|
||||||
|
"parent",
|
||||||
|
"component_result",
|
||||||
|
"base_move_cost",
|
||||||
|
"cache_key",
|
||||||
|
"seed_index",
|
||||||
|
"congestion_resolved",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -67,6 +132,11 @@ class AStarNode:
|
||||||
h_cost: float,
|
h_cost: float,
|
||||||
parent: AStarNode | None = None,
|
parent: AStarNode | None = None,
|
||||||
component_result: ComponentResult | None = None,
|
component_result: ComponentResult | None = None,
|
||||||
|
*,
|
||||||
|
base_move_cost: float = 0.0,
|
||||||
|
cache_key: tuple | None = None,
|
||||||
|
seed_index: int | None = None,
|
||||||
|
congestion_resolved: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.port = port
|
self.port = port
|
||||||
self.g_cost = g_cost
|
self.g_cost = g_cost
|
||||||
|
|
@ -74,6 +144,10 @@ class AStarNode:
|
||||||
self.fh_cost = (g_cost + h_cost, h_cost)
|
self.fh_cost = (g_cost + h_cost, h_cost)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.component_result = component_result
|
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:
|
def __lt__(self, other: AStarNode) -> bool:
|
||||||
return self.fh_cost < other.fh_cost
|
return self.fh_cost < other.fh_cost
|
||||||
|
|
@ -94,14 +168,48 @@ class AStarMetrics:
|
||||||
"total_warm_start_paths_used",
|
"total_warm_start_paths_used",
|
||||||
"total_refine_path_calls",
|
"total_refine_path_calls",
|
||||||
"total_timeout_events",
|
"total_timeout_events",
|
||||||
|
"total_iteration_reverify_calls",
|
||||||
|
"total_iteration_reverified_nets",
|
||||||
|
"total_iteration_conflicting_nets",
|
||||||
|
"total_iteration_conflict_edges",
|
||||||
|
"total_nets_carried_forward",
|
||||||
|
"total_score_component_calls",
|
||||||
|
"total_score_component_total_ns",
|
||||||
|
"total_path_cost_calls",
|
||||||
|
"total_danger_map_lookup_calls",
|
||||||
|
"total_danger_map_cache_hits",
|
||||||
|
"total_danger_map_cache_misses",
|
||||||
|
"total_danger_map_query_calls",
|
||||||
|
"total_danger_map_total_ns",
|
||||||
"total_move_cache_abs_hits",
|
"total_move_cache_abs_hits",
|
||||||
"total_move_cache_abs_misses",
|
"total_move_cache_abs_misses",
|
||||||
"total_move_cache_rel_hits",
|
"total_move_cache_rel_hits",
|
||||||
"total_move_cache_rel_misses",
|
"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_static_safe_cache_hits",
|
||||||
"total_hard_collision_cache_hits",
|
"total_hard_collision_cache_hits",
|
||||||
"total_congestion_cache_hits",
|
"total_congestion_cache_hits",
|
||||||
"total_congestion_cache_misses",
|
"total_congestion_cache_misses",
|
||||||
|
"total_congestion_presence_cache_hits",
|
||||||
|
"total_congestion_presence_cache_misses",
|
||||||
|
"total_congestion_presence_skips",
|
||||||
|
"total_congestion_candidate_precheck_hits",
|
||||||
|
"total_congestion_candidate_precheck_misses",
|
||||||
|
"total_congestion_candidate_precheck_skips",
|
||||||
|
"total_congestion_grid_net_cache_hits",
|
||||||
|
"total_congestion_grid_net_cache_misses",
|
||||||
|
"total_congestion_grid_span_cache_hits",
|
||||||
|
"total_congestion_grid_span_cache_misses",
|
||||||
|
"total_congestion_candidate_nets",
|
||||||
|
"total_congestion_net_envelope_cache_hits",
|
||||||
|
"total_congestion_net_envelope_cache_misses",
|
||||||
"total_dynamic_path_objects_added",
|
"total_dynamic_path_objects_added",
|
||||||
"total_dynamic_path_objects_removed",
|
"total_dynamic_path_objects_removed",
|
||||||
"total_dynamic_tree_rebuilds",
|
"total_dynamic_tree_rebuilds",
|
||||||
|
|
@ -109,21 +217,47 @@ class AStarMetrics:
|
||||||
"total_static_tree_rebuilds",
|
"total_static_tree_rebuilds",
|
||||||
"total_static_raw_tree_rebuilds",
|
"total_static_raw_tree_rebuilds",
|
||||||
"total_static_net_tree_rebuilds",
|
"total_static_net_tree_rebuilds",
|
||||||
|
"total_visibility_corner_index_builds",
|
||||||
"total_visibility_builds",
|
"total_visibility_builds",
|
||||||
"total_visibility_corner_pairs_checked",
|
"total_visibility_corner_pairs_checked",
|
||||||
"total_visibility_corner_queries",
|
"total_visibility_corner_queries_exact",
|
||||||
"total_visibility_corner_hits",
|
"total_visibility_corner_hits_exact",
|
||||||
"total_visibility_point_queries",
|
"total_visibility_point_queries",
|
||||||
"total_visibility_point_cache_hits",
|
"total_visibility_point_cache_hits",
|
||||||
"total_visibility_point_cache_misses",
|
"total_visibility_point_cache_misses",
|
||||||
|
"total_visibility_tangent_candidate_scans",
|
||||||
|
"total_visibility_tangent_candidate_corner_checks",
|
||||||
|
"total_visibility_tangent_candidate_ray_tests",
|
||||||
"total_ray_cast_calls",
|
"total_ray_cast_calls",
|
||||||
|
"total_ray_cast_calls_straight_static",
|
||||||
|
"total_ray_cast_calls_expand_snap",
|
||||||
|
"total_ray_cast_calls_expand_forward",
|
||||||
|
"total_ray_cast_calls_visibility_build",
|
||||||
|
"total_ray_cast_calls_visibility_query",
|
||||||
|
"total_ray_cast_calls_visibility_tangent",
|
||||||
|
"total_ray_cast_calls_other",
|
||||||
"total_ray_cast_candidate_bounds",
|
"total_ray_cast_candidate_bounds",
|
||||||
"total_ray_cast_exact_geometry_checks",
|
"total_ray_cast_exact_geometry_checks",
|
||||||
"total_congestion_check_calls",
|
"total_congestion_check_calls",
|
||||||
|
"total_congestion_lazy_resolutions",
|
||||||
|
"total_congestion_lazy_requeues",
|
||||||
|
"total_congestion_candidate_ids",
|
||||||
"total_congestion_exact_pair_checks",
|
"total_congestion_exact_pair_checks",
|
||||||
"total_verify_path_report_calls",
|
"total_verify_path_report_calls",
|
||||||
"total_verify_static_buffer_ops",
|
"total_verify_static_buffer_ops",
|
||||||
|
"total_verify_dynamic_candidate_nets",
|
||||||
"total_verify_dynamic_exact_pair_checks",
|
"total_verify_dynamic_exact_pair_checks",
|
||||||
|
"total_refinement_windows_considered",
|
||||||
|
"total_refinement_static_bounds_checked",
|
||||||
|
"total_refinement_dynamic_bounds_checked",
|
||||||
|
"total_refinement_candidate_side_extents",
|
||||||
|
"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",
|
"last_expanded_nodes",
|
||||||
"nodes_expanded",
|
"nodes_expanded",
|
||||||
"moves_generated",
|
"moves_generated",
|
||||||
|
|
@ -147,14 +281,48 @@ class AStarMetrics:
|
||||||
self.total_warm_start_paths_used = 0
|
self.total_warm_start_paths_used = 0
|
||||||
self.total_refine_path_calls = 0
|
self.total_refine_path_calls = 0
|
||||||
self.total_timeout_events = 0
|
self.total_timeout_events = 0
|
||||||
|
self.total_iteration_reverify_calls = 0
|
||||||
|
self.total_iteration_reverified_nets = 0
|
||||||
|
self.total_iteration_conflicting_nets = 0
|
||||||
|
self.total_iteration_conflict_edges = 0
|
||||||
|
self.total_nets_carried_forward = 0
|
||||||
|
self.total_score_component_calls = 0
|
||||||
|
self.total_score_component_total_ns = 0
|
||||||
|
self.total_path_cost_calls = 0
|
||||||
|
self.total_danger_map_lookup_calls = 0
|
||||||
|
self.total_danger_map_cache_hits = 0
|
||||||
|
self.total_danger_map_cache_misses = 0
|
||||||
|
self.total_danger_map_query_calls = 0
|
||||||
|
self.total_danger_map_total_ns = 0
|
||||||
self.total_move_cache_abs_hits = 0
|
self.total_move_cache_abs_hits = 0
|
||||||
self.total_move_cache_abs_misses = 0
|
self.total_move_cache_abs_misses = 0
|
||||||
self.total_move_cache_rel_hits = 0
|
self.total_move_cache_rel_hits = 0
|
||||||
self.total_move_cache_rel_misses = 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_static_safe_cache_hits = 0
|
||||||
self.total_hard_collision_cache_hits = 0
|
self.total_hard_collision_cache_hits = 0
|
||||||
self.total_congestion_cache_hits = 0
|
self.total_congestion_cache_hits = 0
|
||||||
self.total_congestion_cache_misses = 0
|
self.total_congestion_cache_misses = 0
|
||||||
|
self.total_congestion_presence_cache_hits = 0
|
||||||
|
self.total_congestion_presence_cache_misses = 0
|
||||||
|
self.total_congestion_presence_skips = 0
|
||||||
|
self.total_congestion_candidate_precheck_hits = 0
|
||||||
|
self.total_congestion_candidate_precheck_misses = 0
|
||||||
|
self.total_congestion_candidate_precheck_skips = 0
|
||||||
|
self.total_congestion_grid_net_cache_hits = 0
|
||||||
|
self.total_congestion_grid_net_cache_misses = 0
|
||||||
|
self.total_congestion_grid_span_cache_hits = 0
|
||||||
|
self.total_congestion_grid_span_cache_misses = 0
|
||||||
|
self.total_congestion_candidate_nets = 0
|
||||||
|
self.total_congestion_net_envelope_cache_hits = 0
|
||||||
|
self.total_congestion_net_envelope_cache_misses = 0
|
||||||
self.total_dynamic_path_objects_added = 0
|
self.total_dynamic_path_objects_added = 0
|
||||||
self.total_dynamic_path_objects_removed = 0
|
self.total_dynamic_path_objects_removed = 0
|
||||||
self.total_dynamic_tree_rebuilds = 0
|
self.total_dynamic_tree_rebuilds = 0
|
||||||
|
|
@ -162,21 +330,47 @@ class AStarMetrics:
|
||||||
self.total_static_tree_rebuilds = 0
|
self.total_static_tree_rebuilds = 0
|
||||||
self.total_static_raw_tree_rebuilds = 0
|
self.total_static_raw_tree_rebuilds = 0
|
||||||
self.total_static_net_tree_rebuilds = 0
|
self.total_static_net_tree_rebuilds = 0
|
||||||
|
self.total_visibility_corner_index_builds = 0
|
||||||
self.total_visibility_builds = 0
|
self.total_visibility_builds = 0
|
||||||
self.total_visibility_corner_pairs_checked = 0
|
self.total_visibility_corner_pairs_checked = 0
|
||||||
self.total_visibility_corner_queries = 0
|
self.total_visibility_corner_queries_exact = 0
|
||||||
self.total_visibility_corner_hits = 0
|
self.total_visibility_corner_hits_exact = 0
|
||||||
self.total_visibility_point_queries = 0
|
self.total_visibility_point_queries = 0
|
||||||
self.total_visibility_point_cache_hits = 0
|
self.total_visibility_point_cache_hits = 0
|
||||||
self.total_visibility_point_cache_misses = 0
|
self.total_visibility_point_cache_misses = 0
|
||||||
|
self.total_visibility_tangent_candidate_scans = 0
|
||||||
|
self.total_visibility_tangent_candidate_corner_checks = 0
|
||||||
|
self.total_visibility_tangent_candidate_ray_tests = 0
|
||||||
self.total_ray_cast_calls = 0
|
self.total_ray_cast_calls = 0
|
||||||
|
self.total_ray_cast_calls_straight_static = 0
|
||||||
|
self.total_ray_cast_calls_expand_snap = 0
|
||||||
|
self.total_ray_cast_calls_expand_forward = 0
|
||||||
|
self.total_ray_cast_calls_visibility_build = 0
|
||||||
|
self.total_ray_cast_calls_visibility_query = 0
|
||||||
|
self.total_ray_cast_calls_visibility_tangent = 0
|
||||||
|
self.total_ray_cast_calls_other = 0
|
||||||
self.total_ray_cast_candidate_bounds = 0
|
self.total_ray_cast_candidate_bounds = 0
|
||||||
self.total_ray_cast_exact_geometry_checks = 0
|
self.total_ray_cast_exact_geometry_checks = 0
|
||||||
self.total_congestion_check_calls = 0
|
self.total_congestion_check_calls = 0
|
||||||
|
self.total_congestion_lazy_resolutions = 0
|
||||||
|
self.total_congestion_lazy_requeues = 0
|
||||||
|
self.total_congestion_candidate_ids = 0
|
||||||
self.total_congestion_exact_pair_checks = 0
|
self.total_congestion_exact_pair_checks = 0
|
||||||
self.total_verify_path_report_calls = 0
|
self.total_verify_path_report_calls = 0
|
||||||
self.total_verify_static_buffer_ops = 0
|
self.total_verify_static_buffer_ops = 0
|
||||||
|
self.total_verify_dynamic_candidate_nets = 0
|
||||||
self.total_verify_dynamic_exact_pair_checks = 0
|
self.total_verify_dynamic_exact_pair_checks = 0
|
||||||
|
self.total_refinement_windows_considered = 0
|
||||||
|
self.total_refinement_static_bounds_checked = 0
|
||||||
|
self.total_refinement_dynamic_bounds_checked = 0
|
||||||
|
self.total_refinement_candidate_side_extents = 0
|
||||||
|
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.last_expanded_nodes: list[tuple[int, int, int]] = []
|
||||||
self.nodes_expanded = 0
|
self.nodes_expanded = 0
|
||||||
self.moves_generated = 0
|
self.moves_generated = 0
|
||||||
|
|
@ -199,14 +393,48 @@ class AStarMetrics:
|
||||||
self.total_warm_start_paths_used = 0
|
self.total_warm_start_paths_used = 0
|
||||||
self.total_refine_path_calls = 0
|
self.total_refine_path_calls = 0
|
||||||
self.total_timeout_events = 0
|
self.total_timeout_events = 0
|
||||||
|
self.total_iteration_reverify_calls = 0
|
||||||
|
self.total_iteration_reverified_nets = 0
|
||||||
|
self.total_iteration_conflicting_nets = 0
|
||||||
|
self.total_iteration_conflict_edges = 0
|
||||||
|
self.total_nets_carried_forward = 0
|
||||||
|
self.total_score_component_calls = 0
|
||||||
|
self.total_score_component_total_ns = 0
|
||||||
|
self.total_path_cost_calls = 0
|
||||||
|
self.total_danger_map_lookup_calls = 0
|
||||||
|
self.total_danger_map_cache_hits = 0
|
||||||
|
self.total_danger_map_cache_misses = 0
|
||||||
|
self.total_danger_map_query_calls = 0
|
||||||
|
self.total_danger_map_total_ns = 0
|
||||||
self.total_move_cache_abs_hits = 0
|
self.total_move_cache_abs_hits = 0
|
||||||
self.total_move_cache_abs_misses = 0
|
self.total_move_cache_abs_misses = 0
|
||||||
self.total_move_cache_rel_hits = 0
|
self.total_move_cache_rel_hits = 0
|
||||||
self.total_move_cache_rel_misses = 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_static_safe_cache_hits = 0
|
||||||
self.total_hard_collision_cache_hits = 0
|
self.total_hard_collision_cache_hits = 0
|
||||||
self.total_congestion_cache_hits = 0
|
self.total_congestion_cache_hits = 0
|
||||||
self.total_congestion_cache_misses = 0
|
self.total_congestion_cache_misses = 0
|
||||||
|
self.total_congestion_presence_cache_hits = 0
|
||||||
|
self.total_congestion_presence_cache_misses = 0
|
||||||
|
self.total_congestion_presence_skips = 0
|
||||||
|
self.total_congestion_candidate_precheck_hits = 0
|
||||||
|
self.total_congestion_candidate_precheck_misses = 0
|
||||||
|
self.total_congestion_candidate_precheck_skips = 0
|
||||||
|
self.total_congestion_grid_net_cache_hits = 0
|
||||||
|
self.total_congestion_grid_net_cache_misses = 0
|
||||||
|
self.total_congestion_grid_span_cache_hits = 0
|
||||||
|
self.total_congestion_grid_span_cache_misses = 0
|
||||||
|
self.total_congestion_candidate_nets = 0
|
||||||
|
self.total_congestion_net_envelope_cache_hits = 0
|
||||||
|
self.total_congestion_net_envelope_cache_misses = 0
|
||||||
self.total_dynamic_path_objects_added = 0
|
self.total_dynamic_path_objects_added = 0
|
||||||
self.total_dynamic_path_objects_removed = 0
|
self.total_dynamic_path_objects_removed = 0
|
||||||
self.total_dynamic_tree_rebuilds = 0
|
self.total_dynamic_tree_rebuilds = 0
|
||||||
|
|
@ -214,21 +442,47 @@ class AStarMetrics:
|
||||||
self.total_static_tree_rebuilds = 0
|
self.total_static_tree_rebuilds = 0
|
||||||
self.total_static_raw_tree_rebuilds = 0
|
self.total_static_raw_tree_rebuilds = 0
|
||||||
self.total_static_net_tree_rebuilds = 0
|
self.total_static_net_tree_rebuilds = 0
|
||||||
|
self.total_visibility_corner_index_builds = 0
|
||||||
self.total_visibility_builds = 0
|
self.total_visibility_builds = 0
|
||||||
self.total_visibility_corner_pairs_checked = 0
|
self.total_visibility_corner_pairs_checked = 0
|
||||||
self.total_visibility_corner_queries = 0
|
self.total_visibility_corner_queries_exact = 0
|
||||||
self.total_visibility_corner_hits = 0
|
self.total_visibility_corner_hits_exact = 0
|
||||||
self.total_visibility_point_queries = 0
|
self.total_visibility_point_queries = 0
|
||||||
self.total_visibility_point_cache_hits = 0
|
self.total_visibility_point_cache_hits = 0
|
||||||
self.total_visibility_point_cache_misses = 0
|
self.total_visibility_point_cache_misses = 0
|
||||||
|
self.total_visibility_tangent_candidate_scans = 0
|
||||||
|
self.total_visibility_tangent_candidate_corner_checks = 0
|
||||||
|
self.total_visibility_tangent_candidate_ray_tests = 0
|
||||||
self.total_ray_cast_calls = 0
|
self.total_ray_cast_calls = 0
|
||||||
|
self.total_ray_cast_calls_straight_static = 0
|
||||||
|
self.total_ray_cast_calls_expand_snap = 0
|
||||||
|
self.total_ray_cast_calls_expand_forward = 0
|
||||||
|
self.total_ray_cast_calls_visibility_build = 0
|
||||||
|
self.total_ray_cast_calls_visibility_query = 0
|
||||||
|
self.total_ray_cast_calls_visibility_tangent = 0
|
||||||
|
self.total_ray_cast_calls_other = 0
|
||||||
self.total_ray_cast_candidate_bounds = 0
|
self.total_ray_cast_candidate_bounds = 0
|
||||||
self.total_ray_cast_exact_geometry_checks = 0
|
self.total_ray_cast_exact_geometry_checks = 0
|
||||||
self.total_congestion_check_calls = 0
|
self.total_congestion_check_calls = 0
|
||||||
|
self.total_congestion_lazy_resolutions = 0
|
||||||
|
self.total_congestion_lazy_requeues = 0
|
||||||
|
self.total_congestion_candidate_ids = 0
|
||||||
self.total_congestion_exact_pair_checks = 0
|
self.total_congestion_exact_pair_checks = 0
|
||||||
self.total_verify_path_report_calls = 0
|
self.total_verify_path_report_calls = 0
|
||||||
self.total_verify_static_buffer_ops = 0
|
self.total_verify_static_buffer_ops = 0
|
||||||
|
self.total_verify_dynamic_candidate_nets = 0
|
||||||
self.total_verify_dynamic_exact_pair_checks = 0
|
self.total_verify_dynamic_exact_pair_checks = 0
|
||||||
|
self.total_refinement_windows_considered = 0
|
||||||
|
self.total_refinement_static_bounds_checked = 0
|
||||||
|
self.total_refinement_dynamic_bounds_checked = 0
|
||||||
|
self.total_refinement_candidate_side_extents = 0
|
||||||
|
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:
|
def reset_per_route(self) -> None:
|
||||||
self.nodes_expanded = 0
|
self.nodes_expanded = 0
|
||||||
|
|
@ -254,14 +508,48 @@ class AStarMetrics:
|
||||||
warm_start_paths_used=self.total_warm_start_paths_used,
|
warm_start_paths_used=self.total_warm_start_paths_used,
|
||||||
refine_path_calls=self.total_refine_path_calls,
|
refine_path_calls=self.total_refine_path_calls,
|
||||||
timeout_events=self.total_timeout_events,
|
timeout_events=self.total_timeout_events,
|
||||||
|
iteration_reverify_calls=self.total_iteration_reverify_calls,
|
||||||
|
iteration_reverified_nets=self.total_iteration_reverified_nets,
|
||||||
|
iteration_conflicting_nets=self.total_iteration_conflicting_nets,
|
||||||
|
iteration_conflict_edges=self.total_iteration_conflict_edges,
|
||||||
|
nets_carried_forward=self.total_nets_carried_forward,
|
||||||
|
score_component_calls=self.total_score_component_calls,
|
||||||
|
score_component_total_ns=self.total_score_component_total_ns,
|
||||||
|
path_cost_calls=self.total_path_cost_calls,
|
||||||
|
danger_map_lookup_calls=self.total_danger_map_lookup_calls,
|
||||||
|
danger_map_cache_hits=self.total_danger_map_cache_hits,
|
||||||
|
danger_map_cache_misses=self.total_danger_map_cache_misses,
|
||||||
|
danger_map_query_calls=self.total_danger_map_query_calls,
|
||||||
|
danger_map_total_ns=self.total_danger_map_total_ns,
|
||||||
move_cache_abs_hits=self.total_move_cache_abs_hits,
|
move_cache_abs_hits=self.total_move_cache_abs_hits,
|
||||||
move_cache_abs_misses=self.total_move_cache_abs_misses,
|
move_cache_abs_misses=self.total_move_cache_abs_misses,
|
||||||
move_cache_rel_hits=self.total_move_cache_rel_hits,
|
move_cache_rel_hits=self.total_move_cache_rel_hits,
|
||||||
move_cache_rel_misses=self.total_move_cache_rel_misses,
|
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,
|
static_safe_cache_hits=self.total_static_safe_cache_hits,
|
||||||
hard_collision_cache_hits=self.total_hard_collision_cache_hits,
|
hard_collision_cache_hits=self.total_hard_collision_cache_hits,
|
||||||
congestion_cache_hits=self.total_congestion_cache_hits,
|
congestion_cache_hits=self.total_congestion_cache_hits,
|
||||||
congestion_cache_misses=self.total_congestion_cache_misses,
|
congestion_cache_misses=self.total_congestion_cache_misses,
|
||||||
|
congestion_presence_cache_hits=self.total_congestion_presence_cache_hits,
|
||||||
|
congestion_presence_cache_misses=self.total_congestion_presence_cache_misses,
|
||||||
|
congestion_presence_skips=self.total_congestion_presence_skips,
|
||||||
|
congestion_candidate_precheck_hits=self.total_congestion_candidate_precheck_hits,
|
||||||
|
congestion_candidate_precheck_misses=self.total_congestion_candidate_precheck_misses,
|
||||||
|
congestion_candidate_precheck_skips=self.total_congestion_candidate_precheck_skips,
|
||||||
|
congestion_grid_net_cache_hits=self.total_congestion_grid_net_cache_hits,
|
||||||
|
congestion_grid_net_cache_misses=self.total_congestion_grid_net_cache_misses,
|
||||||
|
congestion_grid_span_cache_hits=self.total_congestion_grid_span_cache_hits,
|
||||||
|
congestion_grid_span_cache_misses=self.total_congestion_grid_span_cache_misses,
|
||||||
|
congestion_candidate_nets=self.total_congestion_candidate_nets,
|
||||||
|
congestion_net_envelope_cache_hits=self.total_congestion_net_envelope_cache_hits,
|
||||||
|
congestion_net_envelope_cache_misses=self.total_congestion_net_envelope_cache_misses,
|
||||||
dynamic_path_objects_added=self.total_dynamic_path_objects_added,
|
dynamic_path_objects_added=self.total_dynamic_path_objects_added,
|
||||||
dynamic_path_objects_removed=self.total_dynamic_path_objects_removed,
|
dynamic_path_objects_removed=self.total_dynamic_path_objects_removed,
|
||||||
dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds,
|
dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds,
|
||||||
|
|
@ -269,21 +557,47 @@ class AStarMetrics:
|
||||||
static_tree_rebuilds=self.total_static_tree_rebuilds,
|
static_tree_rebuilds=self.total_static_tree_rebuilds,
|
||||||
static_raw_tree_rebuilds=self.total_static_raw_tree_rebuilds,
|
static_raw_tree_rebuilds=self.total_static_raw_tree_rebuilds,
|
||||||
static_net_tree_rebuilds=self.total_static_net_tree_rebuilds,
|
static_net_tree_rebuilds=self.total_static_net_tree_rebuilds,
|
||||||
|
visibility_corner_index_builds=self.total_visibility_corner_index_builds,
|
||||||
visibility_builds=self.total_visibility_builds,
|
visibility_builds=self.total_visibility_builds,
|
||||||
visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked,
|
visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked,
|
||||||
visibility_corner_queries=self.total_visibility_corner_queries,
|
visibility_corner_queries_exact=self.total_visibility_corner_queries_exact,
|
||||||
visibility_corner_hits=self.total_visibility_corner_hits,
|
visibility_corner_hits_exact=self.total_visibility_corner_hits_exact,
|
||||||
visibility_point_queries=self.total_visibility_point_queries,
|
visibility_point_queries=self.total_visibility_point_queries,
|
||||||
visibility_point_cache_hits=self.total_visibility_point_cache_hits,
|
visibility_point_cache_hits=self.total_visibility_point_cache_hits,
|
||||||
visibility_point_cache_misses=self.total_visibility_point_cache_misses,
|
visibility_point_cache_misses=self.total_visibility_point_cache_misses,
|
||||||
|
visibility_tangent_candidate_scans=self.total_visibility_tangent_candidate_scans,
|
||||||
|
visibility_tangent_candidate_corner_checks=self.total_visibility_tangent_candidate_corner_checks,
|
||||||
|
visibility_tangent_candidate_ray_tests=self.total_visibility_tangent_candidate_ray_tests,
|
||||||
ray_cast_calls=self.total_ray_cast_calls,
|
ray_cast_calls=self.total_ray_cast_calls,
|
||||||
|
ray_cast_calls_straight_static=self.total_ray_cast_calls_straight_static,
|
||||||
|
ray_cast_calls_expand_snap=self.total_ray_cast_calls_expand_snap,
|
||||||
|
ray_cast_calls_expand_forward=self.total_ray_cast_calls_expand_forward,
|
||||||
|
ray_cast_calls_visibility_build=self.total_ray_cast_calls_visibility_build,
|
||||||
|
ray_cast_calls_visibility_query=self.total_ray_cast_calls_visibility_query,
|
||||||
|
ray_cast_calls_visibility_tangent=self.total_ray_cast_calls_visibility_tangent,
|
||||||
|
ray_cast_calls_other=self.total_ray_cast_calls_other,
|
||||||
ray_cast_candidate_bounds=self.total_ray_cast_candidate_bounds,
|
ray_cast_candidate_bounds=self.total_ray_cast_candidate_bounds,
|
||||||
ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks,
|
ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks,
|
||||||
congestion_check_calls=self.total_congestion_check_calls,
|
congestion_check_calls=self.total_congestion_check_calls,
|
||||||
|
congestion_lazy_resolutions=self.total_congestion_lazy_resolutions,
|
||||||
|
congestion_lazy_requeues=self.total_congestion_lazy_requeues,
|
||||||
|
congestion_candidate_ids=self.total_congestion_candidate_ids,
|
||||||
congestion_exact_pair_checks=self.total_congestion_exact_pair_checks,
|
congestion_exact_pair_checks=self.total_congestion_exact_pair_checks,
|
||||||
verify_path_report_calls=self.total_verify_path_report_calls,
|
verify_path_report_calls=self.total_verify_path_report_calls,
|
||||||
verify_static_buffer_ops=self.total_verify_static_buffer_ops,
|
verify_static_buffer_ops=self.total_verify_static_buffer_ops,
|
||||||
|
verify_dynamic_candidate_nets=self.total_verify_dynamic_candidate_nets,
|
||||||
verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks,
|
verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks,
|
||||||
|
refinement_windows_considered=self.total_refinement_windows_considered,
|
||||||
|
refinement_static_bounds_checked=self.total_refinement_static_bounds_checked,
|
||||||
|
refinement_dynamic_bounds_checked=self.total_refinement_dynamic_bounds_checked,
|
||||||
|
refinement_candidate_side_extents=self.total_refinement_candidate_side_extents,
|
||||||
|
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 dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from inire.model import NetOrder, NetSpec, resolve_bend_geometry
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.results import RoutingOutcome, RoutingReport, RoutingResult
|
from inire.model import NetOrder, NetSpec, RoutingProblem, resolve_bend_geometry
|
||||||
from inire.router._astar_types import AStarContext, AStarMetrics, SearchRunConfig
|
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._search import route_astar
|
||||||
from inire.router._seed_materialization import materialize_path_seed
|
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
|
from inire.router.refiner import PathRefiner
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -17,6 +29,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import PathVerificationDetail
|
||||||
from inire.geometry.components import ComponentResult
|
from inire.geometry.components import ComponentResult
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,6 +43,27 @@ class _RoutingState:
|
||||||
timeout_s: float
|
timeout_s: float
|
||||||
initial_paths: dict[str, tuple[ComponentResult, ...]] | None
|
initial_paths: dict[str, tuple[ComponentResult, ...]] | None
|
||||||
accumulated_expanded_nodes: list[tuple[int, int, int]]
|
accumulated_expanded_nodes: list[tuple[int, int, int]]
|
||||||
|
best_results: dict[str, RoutingResult]
|
||||||
|
best_completed_nets: int
|
||||||
|
best_conflict_edges: int
|
||||||
|
best_dynamic_collisions: int
|
||||||
|
last_conflict_signature: tuple[tuple[str, str], ...]
|
||||||
|
last_conflict_edge_count: int
|
||||||
|
repeated_conflict_count: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class _IterationReview:
|
||||||
|
conflicting_nets: set[str]
|
||||||
|
conflict_edges: set[tuple[str, str]]
|
||||||
|
completed_net_ids: set[str]
|
||||||
|
total_dynamic_collisions: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class _PairLocalTarget:
|
||||||
|
net_ids: tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
class PathFinder:
|
class PathFinder:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
|
@ -37,6 +71,8 @@ class PathFinder:
|
||||||
"metrics",
|
"metrics",
|
||||||
"refiner",
|
"refiner",
|
||||||
"accumulated_expanded_nodes",
|
"accumulated_expanded_nodes",
|
||||||
|
"conflict_trace",
|
||||||
|
"frontier_trace",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -48,16 +84,27 @@ class PathFinder:
|
||||||
self.metrics = self.context.metrics if metrics is None else metrics
|
self.metrics = self.context.metrics if metrics is None else metrics
|
||||||
self.context.metrics = self.metrics
|
self.context.metrics = self.metrics
|
||||||
self.context.cost_evaluator.collision_engine.metrics = self.metrics
|
self.context.cost_evaluator.collision_engine.metrics = self.metrics
|
||||||
|
if self.context.cost_evaluator.danger_map is not None:
|
||||||
|
self.context.cost_evaluator.danger_map.metrics = self.metrics
|
||||||
self.refiner = PathRefiner(self.context)
|
self.refiner = PathRefiner(self.context)
|
||||||
self.accumulated_expanded_nodes: list[tuple[int, int, int]] = []
|
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:
|
def _install_path(self, net_id: str, path: Sequence[ComponentResult]) -> None:
|
||||||
all_geoms: list[Polygon] = []
|
all_geoms: list[Polygon] = []
|
||||||
all_dilated: 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_geoms.extend(result.collision_geometry)
|
||||||
all_dilated.extend(result.dilated_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(
|
def _routing_order(
|
||||||
self,
|
self,
|
||||||
|
|
@ -134,6 +181,13 @@ class PathFinder:
|
||||||
timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations),
|
timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations),
|
||||||
initial_paths=initial_paths,
|
initial_paths=initial_paths,
|
||||||
accumulated_expanded_nodes=[],
|
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,
|
||||||
)
|
)
|
||||||
if state.initial_paths is None and congestion.warm_start_enabled:
|
if state.initial_paths is None and congestion.warm_start_enabled:
|
||||||
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
|
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
|
||||||
|
|
@ -163,6 +217,533 @@ class PathFinder:
|
||||||
)
|
)
|
||||||
return initial_paths
|
return initial_paths
|
||||||
|
|
||||||
|
def _replace_installed_paths(self, state: _RoutingState, results: dict[str, RoutingResult]) -> None:
|
||||||
|
for net_id in state.ordered_net_ids:
|
||||||
|
self.context.cost_evaluator.collision_engine.remove_path(net_id)
|
||||||
|
for net_id in state.ordered_net_ids:
|
||||||
|
result = results.get(net_id)
|
||||||
|
if result and result.path:
|
||||||
|
self._install_path(net_id, result.path)
|
||||||
|
|
||||||
|
def _update_best_iteration(self, state: _RoutingState, review: _IterationReview) -> bool:
|
||||||
|
completed_nets = len(review.completed_net_ids)
|
||||||
|
conflict_edges = len(review.conflict_edges)
|
||||||
|
dynamic_collisions = review.total_dynamic_collisions
|
||||||
|
is_better = (
|
||||||
|
completed_nets > state.best_completed_nets
|
||||||
|
or (
|
||||||
|
completed_nets == state.best_completed_nets
|
||||||
|
and (
|
||||||
|
conflict_edges < state.best_conflict_edges
|
||||||
|
or (
|
||||||
|
conflict_edges == state.best_conflict_edges
|
||||||
|
and dynamic_collisions < state.best_dynamic_collisions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not is_better:
|
||||||
|
return False
|
||||||
|
|
||||||
|
state.best_results = dict(state.results)
|
||||||
|
state.best_completed_nets = completed_nets
|
||||||
|
state.best_conflict_edges = conflict_edges
|
||||||
|
state.best_dynamic_collisions = dynamic_collisions
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _restore_best_iteration(self, state: _RoutingState) -> None:
|
||||||
|
if not state.best_results:
|
||||||
|
return
|
||||||
|
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(
|
def _route_net_once(
|
||||||
self,
|
self,
|
||||||
state: _RoutingState,
|
state: _RoutingState,
|
||||||
|
|
@ -182,16 +763,25 @@ class PathFinder:
|
||||||
else:
|
else:
|
||||||
coll_model, _ = resolve_bend_geometry(search)
|
coll_model, _ = resolve_bend_geometry(search)
|
||||||
skip_congestion = False
|
skip_congestion = False
|
||||||
|
guidance_seed = None
|
||||||
|
guidance_bonus = 0.0
|
||||||
if congestion.use_tiered_strategy and iteration == 0:
|
if congestion.use_tiered_strategy and iteration == 0:
|
||||||
skip_congestion = True
|
skip_congestion = True
|
||||||
if coll_model == "arc":
|
if coll_model == "arc":
|
||||||
coll_model = "clipped_bbox"
|
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(
|
run_config = SearchRunConfig.from_options(
|
||||||
self.context.options,
|
self.context.options,
|
||||||
bend_collision_type=coll_model,
|
bend_collision_type=coll_model,
|
||||||
return_partial=True,
|
return_partial=True,
|
||||||
store_expanded=diagnostics.capture_expanded,
|
store_expanded=diagnostics.capture_expanded,
|
||||||
|
guidance_seed=guidance_seed,
|
||||||
|
guidance_bonus=guidance_bonus,
|
||||||
skip_congestion=skip_congestion,
|
skip_congestion=skip_congestion,
|
||||||
self_collision_check=(net_id in state.needs_self_collision_check),
|
self_collision_check=(net_id in state.needs_self_collision_check),
|
||||||
node_limit=search.node_limit,
|
node_limit=search.node_limit,
|
||||||
|
|
@ -233,29 +823,49 @@ class PathFinder:
|
||||||
self,
|
self,
|
||||||
state: _RoutingState,
|
state: _RoutingState,
|
||||||
iteration: int,
|
iteration: int,
|
||||||
|
reroute_net_ids: set[str],
|
||||||
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
||||||
) -> dict[str, RoutingOutcome] | None:
|
) -> _IterationReview | None:
|
||||||
outcomes: dict[str, RoutingOutcome] = {}
|
|
||||||
congestion = self.context.options.congestion
|
congestion = self.context.options.congestion
|
||||||
self.metrics.total_route_iterations += 1
|
self.metrics.total_route_iterations += 1
|
||||||
self.metrics.reset_per_route()
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
if congestion.shuffle_nets and (iteration > 0 or state.initial_paths is None):
|
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
|
iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None
|
||||||
random.Random(iteration_seed).shuffle(state.ordered_net_ids)
|
random.Random(iteration_seed).shuffle(state.ordered_net_ids)
|
||||||
|
|
||||||
for net_id in state.ordered_net_ids:
|
routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids]
|
||||||
|
self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids)
|
||||||
|
|
||||||
|
for net_id in routed_net_ids:
|
||||||
if time.monotonic() - state.start_time > state.timeout_s:
|
if time.monotonic() - state.start_time > state.timeout_s:
|
||||||
self.metrics.total_timeout_events += 1
|
self.metrics.total_timeout_events += 1
|
||||||
return None
|
return None
|
||||||
|
|
||||||
result = self._route_net_once(state, iteration, net_id)
|
result = self._route_net_once(state, iteration, net_id)
|
||||||
state.results[net_id] = result
|
state.results[net_id] = result
|
||||||
outcomes[net_id] = result.outcome
|
|
||||||
|
review = self._reverify_iteration_results(state)
|
||||||
|
|
||||||
if iteration_callback:
|
if iteration_callback:
|
||||||
iteration_callback(iteration, state.results)
|
iteration_callback(iteration, state.results)
|
||||||
return outcomes
|
return review
|
||||||
|
|
||||||
|
def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
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(
|
def _run_iterations(
|
||||||
self,
|
self,
|
||||||
|
|
@ -264,10 +874,33 @@ class PathFinder:
|
||||||
) -> bool:
|
) -> bool:
|
||||||
congestion = self.context.options.congestion
|
congestion = self.context.options.congestion
|
||||||
for iteration in range(congestion.max_iterations):
|
for iteration in range(congestion.max_iterations):
|
||||||
outcomes = self._run_iteration(state, iteration, iteration_callback)
|
review = self._run_iteration(
|
||||||
if outcomes is None:
|
state,
|
||||||
|
iteration,
|
||||||
|
set(state.ordered_net_ids),
|
||||||
|
iteration_callback,
|
||||||
|
)
|
||||||
|
if review is None:
|
||||||
return True
|
return True
|
||||||
if not any(outcome in {"colliding", "partial", "unroutable"} for outcome in outcomes.values()):
|
self._update_best_iteration(state, review)
|
||||||
|
if not any(
|
||||||
|
result.outcome in {"colliding", "partial", "unroutable"}
|
||||||
|
for result in state.results.values()
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_signature = tuple(sorted(review.conflict_edges))
|
||||||
|
repeated = (
|
||||||
|
bool(current_signature)
|
||||||
|
and (
|
||||||
|
current_signature == state.last_conflict_signature
|
||||||
|
or len(current_signature) == state.last_conflict_edge_count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.repeated_conflict_count = state.repeated_conflict_count + 1 if repeated else 0
|
||||||
|
state.last_conflict_signature = current_signature
|
||||||
|
state.last_conflict_edge_count = len(current_signature)
|
||||||
|
if state.repeated_conflict_count >= 2:
|
||||||
return False
|
return False
|
||||||
self.context.congestion_penalty *= congestion.multiplier
|
self.context.congestion_penalty *= congestion.multiplier
|
||||||
return False
|
return False
|
||||||
|
|
@ -285,27 +918,49 @@ class PathFinder:
|
||||||
self.context.cost_evaluator.collision_engine.remove_path(net_id)
|
self.context.cost_evaluator.collision_engine.remove_path(net_id)
|
||||||
refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path)
|
refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path)
|
||||||
self._install_path(net_id, refined_path)
|
self._install_path(net_id, refined_path)
|
||||||
report = self.context.cost_evaluator.collision_engine.verify_path_report(net_id, refined_path)
|
# Defer full verification until _verify_results() so we do not
|
||||||
|
# verify the same refined path twice in one route_all() call.
|
||||||
state.results[net_id] = RoutingResult(
|
state.results[net_id] = RoutingResult(
|
||||||
net_id=net_id,
|
net_id=net_id,
|
||||||
path=tuple(refined_path),
|
path=tuple(refined_path),
|
||||||
reached_target=result.reached_target,
|
reached_target=result.reached_target,
|
||||||
report=report,
|
report=result.report,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
|
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
|
||||||
final_results: dict[str, RoutingResult] = {}
|
final_results: dict[str, RoutingResult] = {}
|
||||||
|
details_by_net: dict[str, PathVerificationDetail] = {}
|
||||||
for net in self.context.problem.nets:
|
for net in self.context.problem.nets:
|
||||||
result = state.results.get(net.net_id)
|
result = state.results.get(net.net_id)
|
||||||
if not result or not result.path:
|
if not result or not result.path:
|
||||||
final_results[net.net_id] = RoutingResult(net_id=net.net_id, path=(), reached_target=False)
|
final_results[net.net_id] = RoutingResult(net_id=net.net_id, path=(), reached_target=False)
|
||||||
continue
|
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(
|
final_results[net.net_id] = RoutingResult(
|
||||||
net_id=net.net_id,
|
net_id=net.net_id,
|
||||||
path=result.path,
|
path=result.path,
|
||||||
reached_target=result.reached_target,
|
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
|
return final_results
|
||||||
|
|
||||||
|
|
@ -316,15 +971,38 @@ class PathFinder:
|
||||||
) -> dict[str, RoutingResult]:
|
) -> dict[str, RoutingResult]:
|
||||||
self.context.congestion_penalty = self.context.options.congestion.base_penalty
|
self.context.congestion_penalty = self.context.options.congestion.base_penalty
|
||||||
self.accumulated_expanded_nodes = []
|
self.accumulated_expanded_nodes = []
|
||||||
|
self.conflict_trace = []
|
||||||
|
self.frontier_trace = []
|
||||||
self.metrics.reset_totals()
|
self.metrics.reset_totals()
|
||||||
self.metrics.reset_per_route()
|
self.metrics.reset_per_route()
|
||||||
|
|
||||||
state = self._prepare_state()
|
state = self._prepare_state()
|
||||||
timed_out = self._run_iterations(state, iteration_callback)
|
timed_out = self._run_iterations(state, iteration_callback)
|
||||||
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
|
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:
|
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)
|
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
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,17 @@ def route_astar(
|
||||||
open_set: list[_AStarNode] = []
|
open_set: list[_AStarNode] = []
|
||||||
closed_set: dict[tuple[int, int, int], float] = {}
|
closed_set: dict[tuple[int, int, int], float] = {}
|
||||||
congestion_cache: dict[tuple, int] = {}
|
congestion_cache: dict[tuple, int] = {}
|
||||||
|
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool] = {}
|
||||||
|
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {}
|
||||||
|
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {}
|
||||||
|
|
||||||
start_node = _AStarNode(
|
start_node = _AStarNode(
|
||||||
start,
|
start,
|
||||||
0.0,
|
0.0,
|
||||||
context.cost_evaluator.h_manhattan(start, target, min_bend_radius=context.min_bend_radius),
|
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)
|
heapq.heappush(open_set, start_node)
|
||||||
best_node = start_node
|
best_node = start_node
|
||||||
|
|
@ -89,6 +95,11 @@ def route_astar(
|
||||||
context,
|
context,
|
||||||
metrics,
|
metrics,
|
||||||
congestion_cache,
|
congestion_cache,
|
||||||
|
congestion_presence_cache,
|
||||||
|
congestion_candidate_precheck_cache,
|
||||||
|
congestion_net_envelope_cache,
|
||||||
|
congestion_grid_net_cache,
|
||||||
|
congestion_grid_span_cache,
|
||||||
config=config,
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from time import perf_counter_ns
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -130,10 +131,16 @@ class CostEvaluator:
|
||||||
start_port: Port | None = None,
|
start_port: Port | None = None,
|
||||||
weights: ObjectiveWeights | None = None,
|
weights: ObjectiveWeights | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
|
metrics = self.collision_engine.metrics
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_score_component_calls += 1
|
||||||
|
start_ns = perf_counter_ns()
|
||||||
active_weights = self._resolve_weights(weights)
|
active_weights = self._resolve_weights(weights)
|
||||||
danger_map = self.danger_map
|
danger_map = self.danger_map
|
||||||
end_port = component.end_port
|
end_port = component.end_port
|
||||||
if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y):
|
if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_score_component_total_ns += perf_counter_ns() - start_ns
|
||||||
return 1e15
|
return 1e15
|
||||||
|
|
||||||
move_radius = None
|
move_radius = None
|
||||||
|
|
@ -145,7 +152,8 @@ class CostEvaluator:
|
||||||
weights=active_weights,
|
weights=active_weights,
|
||||||
)
|
)
|
||||||
|
|
||||||
if danger_map is not None and active_weights.danger_weight:
|
# Skip danger sampling entirely when there are no static obstacles in the KD-tree.
|
||||||
|
if danger_map is not None and active_weights.danger_weight and danger_map.tree is not None:
|
||||||
cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0
|
cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0
|
||||||
cost_e = danger_map.get_cost(end_port.x, end_port.y)
|
cost_e = danger_map.get_cost(end_port.x, end_port.y)
|
||||||
if start_port:
|
if start_port:
|
||||||
|
|
@ -155,6 +163,8 @@ class CostEvaluator:
|
||||||
total_cost += component.length * active_weights.danger_weight * (cost_s + cost_m + cost_e) / 3.0
|
total_cost += component.length * active_weights.danger_weight * (cost_s + cost_m + cost_e) / 3.0
|
||||||
else:
|
else:
|
||||||
total_cost += component.length * active_weights.danger_weight * cost_e
|
total_cost += component.length * active_weights.danger_weight * cost_e
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_score_component_total_ns += perf_counter_ns() - start_ns
|
||||||
return total_cost
|
return total_cost
|
||||||
|
|
||||||
def component_penalty(
|
def component_penalty(
|
||||||
|
|
@ -181,6 +191,9 @@ class CostEvaluator:
|
||||||
*,
|
*,
|
||||||
weights: ObjectiveWeights | None = None,
|
weights: ObjectiveWeights | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
|
metrics = self.collision_engine.metrics
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_path_cost_calls += 1
|
||||||
active_weights = self._resolve_weights(weights)
|
active_weights = self._resolve_weights(weights)
|
||||||
total = 0.0
|
total = 0.0
|
||||||
current_port = start_port
|
current_port = start_port
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from time import perf_counter_ns
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -8,6 +9,7 @@ from scipy.spatial import cKDTree
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
from inire.router._astar_types import AStarMetrics
|
||||||
|
|
||||||
|
|
||||||
_COST_CACHE_SIZE = 100000
|
_COST_CACHE_SIZE = 100000
|
||||||
|
|
@ -18,7 +20,7 @@ class DangerMap:
|
||||||
A proximity cost evaluator using a KD-Tree of obstacle boundary points.
|
A proximity cost evaluator using a KD-Tree of obstacle boundary points.
|
||||||
Scales with obstacle perimeter rather than design area.
|
Scales with obstacle perimeter rather than design area.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache')
|
__slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache', 'metrics')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -42,6 +44,7 @@ class DangerMap:
|
||||||
self.k = k
|
self.k = k
|
||||||
self.tree: cKDTree | None = None
|
self.tree: cKDTree | None = None
|
||||||
self._cost_cache: OrderedDict[tuple[int, int], float] = OrderedDict()
|
self._cost_cache: OrderedDict[tuple[int, int], float] = OrderedDict()
|
||||||
|
self.metrics: AStarMetrics | None = None
|
||||||
|
|
||||||
def precompute(self, obstacles: list[Polygon]) -> None:
|
def precompute(self, obstacles: list[Polygon]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -82,17 +85,28 @@ class DangerMap:
|
||||||
Get the proximity cost at a specific coordinate using the KD-Tree.
|
Get the proximity cost at a specific coordinate using the KD-Tree.
|
||||||
Coordinates are quantized to 1nm to improve cache performance.
|
Coordinates are quantized to 1nm to improve cache performance.
|
||||||
"""
|
"""
|
||||||
|
metrics = self.metrics
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_danger_map_lookup_calls += 1
|
||||||
|
start_ns = perf_counter_ns()
|
||||||
qx_milli = int(round(x * 1000))
|
qx_milli = int(round(x * 1000))
|
||||||
qy_milli = int(round(y * 1000))
|
qy_milli = int(round(y * 1000))
|
||||||
key = (qx_milli, qy_milli)
|
key = (qx_milli, qy_milli)
|
||||||
if key in self._cost_cache:
|
if key in self._cost_cache:
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_danger_map_cache_hits += 1
|
||||||
|
metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns
|
||||||
self._cost_cache.move_to_end(key)
|
self._cost_cache.move_to_end(key)
|
||||||
return self._cost_cache[key]
|
return self._cost_cache[key]
|
||||||
|
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_danger_map_cache_misses += 1
|
||||||
cost = self._compute_cost_quantized(qx_milli, qy_milli)
|
cost = self._compute_cost_quantized(qx_milli, qy_milli)
|
||||||
self._cost_cache[key] = cost
|
self._cost_cache[key] = cost
|
||||||
if len(self._cost_cache) > _COST_CACHE_SIZE:
|
if len(self._cost_cache) > _COST_CACHE_SIZE:
|
||||||
self._cost_cache.popitem(last=False)
|
self._cost_cache.popitem(last=False)
|
||||||
|
if metrics is not None:
|
||||||
|
metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
def _compute_cost_quantized(self, qx_milli: int, qy_milli: int) -> float:
|
def _compute_cost_quantized(self, qx_milli: int, qy_milli: int) -> float:
|
||||||
|
|
@ -102,6 +116,8 @@ class DangerMap:
|
||||||
return 1e15
|
return 1e15
|
||||||
if self.tree is None:
|
if self.tree is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
if self.metrics is not None:
|
||||||
|
self.metrics.total_danger_map_query_calls += 1
|
||||||
dist, _ = self.tree.query([qx, qy], distance_upper_bound=self.safety_threshold)
|
dist, _ = self.tree.query([qx, qy], distance_upper_bound=self.safety_threshold)
|
||||||
if dist >= self.safety_threshold:
|
if dist >= self.safety_threshold:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ class PathRefiner:
|
||||||
x_max = max(0.0, float(local_dx)) + 0.01
|
x_max = max(0.0, float(local_dx)) + 0.01
|
||||||
|
|
||||||
for bounds in self.collision_engine.iter_static_obstacle_bounds(query_bounds):
|
for bounds in self.collision_engine.iter_static_obstacle_bounds(query_bounds):
|
||||||
|
self.context.metrics.total_refinement_static_bounds_checked += 1
|
||||||
local_corners = (
|
local_corners = (
|
||||||
self._to_local_xy(start, bounds[0], bounds[1]),
|
self._to_local_xy(start, bounds[0], bounds[1]),
|
||||||
self._to_local_xy(start, bounds[0], bounds[3]),
|
self._to_local_xy(start, bounds[0], bounds[3]),
|
||||||
|
|
@ -144,6 +145,7 @@ class PathRefiner:
|
||||||
negative_anchors.add(obs_min_y)
|
negative_anchors.add(obs_min_y)
|
||||||
|
|
||||||
for bounds in self.collision_engine.iter_dynamic_path_bounds(query_bounds):
|
for bounds in self.collision_engine.iter_dynamic_path_bounds(query_bounds):
|
||||||
|
self.context.metrics.total_refinement_dynamic_bounds_checked += 1
|
||||||
local_corners = (
|
local_corners = (
|
||||||
self._to_local_xy(start, bounds[0], bounds[1]),
|
self._to_local_xy(start, bounds[0], bounds[1]),
|
||||||
self._to_local_xy(start, bounds[0], bounds[3]),
|
self._to_local_xy(start, bounds[0], bounds[3]),
|
||||||
|
|
@ -166,6 +168,7 @@ class PathRefiner:
|
||||||
if anchor < min(0.0, float(local_dy)) + 0.01:
|
if anchor < min(0.0, float(local_dy)) + 0.01:
|
||||||
direct_extents.add(anchor - pad)
|
direct_extents.add(anchor - pad)
|
||||||
|
|
||||||
|
self.context.metrics.total_refinement_candidate_side_extents += len(direct_extents)
|
||||||
return sorted(direct_extents, key=lambda value: (abs(value), value))
|
return sorted(direct_extents, key=lambda value: (abs(value), value))
|
||||||
|
|
||||||
def _build_same_orientation_dogleg(
|
def _build_same_orientation_dogleg(
|
||||||
|
|
@ -243,6 +246,7 @@ class PathRefiner:
|
||||||
local_dx, _ = self._to_local(window_start, window_end)
|
local_dx, _ = self._to_local(window_start, window_end)
|
||||||
if local_dx < 4.0 * min_radius - 0.01:
|
if local_dx < 4.0 * min_radius - 0.01:
|
||||||
continue
|
continue
|
||||||
|
self.context.metrics.total_refinement_windows_considered += 1
|
||||||
windows.append((start_idx, end_idx))
|
windows.append((start_idx, end_idx))
|
||||||
return windows
|
return windows
|
||||||
|
|
||||||
|
|
@ -270,12 +274,15 @@ class PathRefiner:
|
||||||
replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent)
|
replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent)
|
||||||
if replacement is None:
|
if replacement is None:
|
||||||
continue
|
continue
|
||||||
|
self.context.metrics.total_refinement_candidates_built += 1
|
||||||
candidate_path = path[:start_idx] + replacement + path[end_idx:]
|
candidate_path = path[:start_idx] + replacement + path[end_idx:]
|
||||||
|
self.context.metrics.total_refinement_candidates_verified += 1
|
||||||
report = self.collision_engine.verify_path_report(net_id, candidate_path)
|
report = self.collision_engine.verify_path_report(net_id, candidate_path)
|
||||||
if not report.is_valid:
|
if not report.is_valid:
|
||||||
continue
|
continue
|
||||||
candidate_cost = self.path_cost(candidate_path)
|
candidate_cost = self.path_cost(candidate_path)
|
||||||
if candidate_cost + 1e-6 < best_candidate_cost:
|
if candidate_cost + 1e-6 < best_candidate_cost:
|
||||||
|
self.context.metrics.total_refinement_candidates_accepted += 1
|
||||||
best_candidate_cost = candidate_cost
|
best_candidate_cost = candidate_cost
|
||||||
best_path = candidate_path
|
best_path = candidate_path
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,15 @@ class VisibilityManager:
|
||||||
"""
|
"""
|
||||||
Manages corners of static obstacles for sparse A* / Visibility Graph jumps.
|
Manages corners of static obstacles for sparse A* / Visibility Graph jumps.
|
||||||
"""
|
"""
|
||||||
__slots__ = ("collision_engine", "corners", "corner_index", "_corner_graph", "_point_visibility_cache", "_built_static_version")
|
__slots__ = (
|
||||||
|
"collision_engine",
|
||||||
|
"corners",
|
||||||
|
"corner_index",
|
||||||
|
"_corner_graph",
|
||||||
|
"_point_visibility_cache",
|
||||||
|
"_corner_index_version",
|
||||||
|
"_corner_graph_version",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, collision_engine: RoutingWorld) -> None:
|
def __init__(self, collision_engine: RoutingWorld) -> None:
|
||||||
self.collision_engine = collision_engine
|
self.collision_engine = collision_engine
|
||||||
|
|
@ -24,8 +32,8 @@ class VisibilityManager:
|
||||||
self.corner_index = rtree.index.Index()
|
self.corner_index = rtree.index.Index()
|
||||||
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
|
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
|
||||||
self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {}
|
self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {}
|
||||||
self._built_static_version = -1
|
self._corner_index_version = -1
|
||||||
self._build()
|
self._corner_graph_version = -1
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
def clear_cache(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -35,19 +43,31 @@ class VisibilityManager:
|
||||||
self.corner_index = rtree.index.Index()
|
self.corner_index = rtree.index.Index()
|
||||||
self._corner_graph = {}
|
self._corner_graph = {}
|
||||||
self._point_visibility_cache = {}
|
self._point_visibility_cache = {}
|
||||||
self._build()
|
self._corner_index_version = -1
|
||||||
|
self._corner_graph_version = -1
|
||||||
|
|
||||||
|
def ensure_corner_index_current(self) -> None:
|
||||||
|
if self._corner_index_version != self.collision_engine.get_static_version():
|
||||||
|
self._build_corner_index()
|
||||||
|
|
||||||
|
def ensure_corner_graph_current(self) -> None:
|
||||||
|
self.ensure_corner_index_current()
|
||||||
|
static_version = self.collision_engine.get_static_version()
|
||||||
|
if self._corner_graph_version != static_version:
|
||||||
|
self._build_corner_graph()
|
||||||
|
|
||||||
def _ensure_current(self) -> None:
|
def _ensure_current(self) -> None:
|
||||||
if self._built_static_version != self.collision_engine.get_static_version():
|
self.ensure_corner_graph_current()
|
||||||
self.clear_cache()
|
|
||||||
|
|
||||||
def _build(self) -> None:
|
def _build_corner_index(self) -> None:
|
||||||
"""
|
|
||||||
Extract corners and pre-compute corner-to-corner visibility.
|
|
||||||
"""
|
|
||||||
if self.collision_engine.metrics is not None:
|
if self.collision_engine.metrics is not None:
|
||||||
self.collision_engine.metrics.total_visibility_builds += 1
|
self.collision_engine.metrics.total_visibility_corner_index_builds += 1
|
||||||
self._built_static_version = self.collision_engine.get_static_version()
|
self.corners = []
|
||||||
|
self.corner_index = rtree.index.Index()
|
||||||
|
self._corner_graph = {}
|
||||||
|
self._point_visibility_cache = {}
|
||||||
|
self._corner_graph_version = -1
|
||||||
|
self._corner_index_version = self.collision_engine.get_static_version()
|
||||||
raw_corners = []
|
raw_corners = []
|
||||||
for poly in self.collision_engine.iter_static_dilated_geometries():
|
for poly in self.collision_engine.iter_static_dilated_geometries():
|
||||||
coords = list(poly.exterior.coords)
|
coords = list(poly.exterior.coords)
|
||||||
|
|
@ -63,7 +83,6 @@ class VisibilityManager:
|
||||||
if not raw_corners:
|
if not raw_corners:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deduplicate repeated corner coordinates
|
|
||||||
seen = set()
|
seen = set()
|
||||||
for x, y in raw_corners:
|
for x, y in raw_corners:
|
||||||
sx, sy = round(x, 3), round(y, 3)
|
sx, sy = round(x, 3), round(y, 3)
|
||||||
|
|
@ -71,10 +90,22 @@ class VisibilityManager:
|
||||||
seen.add((sx, sy))
|
seen.add((sx, sy))
|
||||||
self.corners.append((sx, sy))
|
self.corners.append((sx, sy))
|
||||||
|
|
||||||
# Build spatial index for corners
|
|
||||||
for i, (x, y) in enumerate(self.corners):
|
for i, (x, y) in enumerate(self.corners):
|
||||||
self.corner_index.insert(i, (x, y, x, y))
|
self.corner_index.insert(i, (x, y, x, y))
|
||||||
|
|
||||||
|
def _build_corner_graph(self) -> None:
|
||||||
|
"""
|
||||||
|
Pre-compute corner-to-corner visibility from the current corner index.
|
||||||
|
"""
|
||||||
|
self.ensure_corner_index_current()
|
||||||
|
if self.collision_engine.metrics is not None:
|
||||||
|
self.collision_engine.metrics.total_visibility_builds += 1
|
||||||
|
self._corner_graph = {}
|
||||||
|
self._corner_graph_version = self.collision_engine.get_static_version()
|
||||||
|
|
||||||
|
if not self.corners:
|
||||||
|
return
|
||||||
|
|
||||||
# Pre-compute visibility graph between corners
|
# Pre-compute visibility graph between corners
|
||||||
num_corners = len(self.corners)
|
num_corners = len(self.corners)
|
||||||
if num_corners > 200:
|
if num_corners > 200:
|
||||||
|
|
@ -93,11 +124,12 @@ class VisibilityManager:
|
||||||
dx, dy = cx - p1.x, cy - p1.y
|
dx, dy = cx - p1.x, cy - p1.y
|
||||||
dist = numpy.sqrt(dx**2 + dy**2)
|
dist = numpy.sqrt(dx**2 + dy**2)
|
||||||
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||||
reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05)
|
reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05, caller="visibility_build")
|
||||||
if reach >= dist - 0.01:
|
if reach >= dist - 0.01:
|
||||||
self._corner_graph[i].append((cx, cy, dist))
|
self._corner_graph[i].append((cx, cy, dist))
|
||||||
|
|
||||||
def _corner_idx_at(self, origin: Port) -> int | None:
|
def _corner_idx_at(self, origin: Port) -> int | None:
|
||||||
|
self.ensure_corner_index_current()
|
||||||
ox, oy = round(origin.x, 3), round(origin.y, 3)
|
ox, oy = round(origin.x, 3), round(origin.y, 3)
|
||||||
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
|
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
|
||||||
for idx in nearby:
|
for idx in nearby:
|
||||||
|
|
@ -106,6 +138,49 @@ class VisibilityManager:
|
||||||
return idx
|
return idx
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_tangent_corner_candidates(
|
||||||
|
self,
|
||||||
|
origin: Port,
|
||||||
|
*,
|
||||||
|
min_forward: float,
|
||||||
|
max_forward: float,
|
||||||
|
radii: tuple[float, ...],
|
||||||
|
tolerance: float = 2.0,
|
||||||
|
) -> list[int]:
|
||||||
|
self.ensure_corner_index_current()
|
||||||
|
if max_forward <= min_forward or not radii or not self.corners:
|
||||||
|
return []
|
||||||
|
|
||||||
|
candidate_ids: set[int] = set()
|
||||||
|
x0 = float(origin.x)
|
||||||
|
y0 = float(origin.y)
|
||||||
|
|
||||||
|
def _add_hits(bounds: tuple[float, float, float, float]) -> None:
|
||||||
|
min_x, min_y, max_x, max_y = bounds
|
||||||
|
if min_x > max_x or min_y > max_y:
|
||||||
|
return
|
||||||
|
candidate_ids.update(self.corner_index.intersection(bounds))
|
||||||
|
|
||||||
|
for radius in radii:
|
||||||
|
if origin.r == 0:
|
||||||
|
x_bounds = (x0 + min_forward, x0 + max_forward)
|
||||||
|
_add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance))
|
||||||
|
_add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance))
|
||||||
|
elif origin.r == 180:
|
||||||
|
x_bounds = (x0 - max_forward, x0 - min_forward)
|
||||||
|
_add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance))
|
||||||
|
_add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance))
|
||||||
|
elif origin.r == 90:
|
||||||
|
y_bounds = (y0 + min_forward, y0 + max_forward)
|
||||||
|
_add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1]))
|
||||||
|
_add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1]))
|
||||||
|
else:
|
||||||
|
y_bounds = (y0 - max_forward, y0 - min_forward)
|
||||||
|
_add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1]))
|
||||||
|
_add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1]))
|
||||||
|
|
||||||
|
return sorted(candidate_ids)
|
||||||
|
|
||||||
def get_point_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
|
def get_point_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
|
||||||
"""
|
"""
|
||||||
Find visible corners from an arbitrary point.
|
Find visible corners from an arbitrary point.
|
||||||
|
|
@ -113,11 +188,13 @@ class VisibilityManager:
|
||||||
"""
|
"""
|
||||||
if self.collision_engine.metrics is not None:
|
if self.collision_engine.metrics is not None:
|
||||||
self.collision_engine.metrics.total_visibility_point_queries += 1
|
self.collision_engine.metrics.total_visibility_point_queries += 1
|
||||||
self._ensure_current()
|
self.ensure_corner_index_current()
|
||||||
if max_dist < 0:
|
if max_dist < 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
corner_idx = self._corner_idx_at(origin)
|
corner_idx = self._corner_idx_at(origin)
|
||||||
|
if corner_idx is not None:
|
||||||
|
self.ensure_corner_graph_current()
|
||||||
if corner_idx is not None and corner_idx in self._corner_graph:
|
if corner_idx is not None and corner_idx in self._corner_graph:
|
||||||
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
||||||
|
|
||||||
|
|
@ -143,7 +220,7 @@ class VisibilityManager:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
angle = numpy.degrees(numpy.arctan2(dy, dx))
|
||||||
reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05)
|
reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05, caller="visibility_query")
|
||||||
if reach >= dist - 0.01:
|
if reach >= dist - 0.01:
|
||||||
visible.append((cx, cy, dist))
|
visible.append((cx, cy, dist))
|
||||||
|
|
||||||
|
|
@ -156,14 +233,14 @@ class VisibilityManager:
|
||||||
This avoids the expensive arbitrary-point visibility scan in hot search paths.
|
This avoids the expensive arbitrary-point visibility scan in hot search paths.
|
||||||
"""
|
"""
|
||||||
if self.collision_engine.metrics is not None:
|
if self.collision_engine.metrics is not None:
|
||||||
self.collision_engine.metrics.total_visibility_corner_queries += 1
|
self.collision_engine.metrics.total_visibility_corner_queries_exact += 1
|
||||||
self._ensure_current()
|
self.ensure_corner_graph_current()
|
||||||
if max_dist < 0:
|
if max_dist < 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
corner_idx = self._corner_idx_at(origin)
|
corner_idx = self._corner_idx_at(origin)
|
||||||
if corner_idx is not None and corner_idx in self._corner_graph:
|
if corner_idx is not None and corner_idx in self._corner_graph:
|
||||||
if self.collision_engine.metrics is not None:
|
if self.collision_engine.metrics is not None:
|
||||||
self.collision_engine.metrics.total_visibility_corner_hits += 1
|
self.collision_engine.metrics.total_visibility_corner_hits_exact += 1
|
||||||
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from inire import (
|
||||||
RoutingOptions,
|
RoutingOptions,
|
||||||
RoutingProblem,
|
RoutingProblem,
|
||||||
RoutingResult,
|
RoutingResult,
|
||||||
|
RoutingRunResult,
|
||||||
SearchOptions,
|
SearchOptions,
|
||||||
)
|
)
|
||||||
from inire.geometry.collision import RoutingWorld
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
|
@ -34,6 +35,7 @@ _OBJECTIVE_FIELDS = set(ObjectiveWeights.__dataclass_fields__)
|
||||||
ScenarioOutcome = tuple[float, int, int, int]
|
ScenarioOutcome = tuple[float, int, int, int]
|
||||||
ScenarioRun = Callable[[], ScenarioOutcome]
|
ScenarioRun = Callable[[], ScenarioOutcome]
|
||||||
ScenarioSnapshotRun = Callable[[], "ScenarioSnapshot"]
|
ScenarioSnapshotRun = Callable[[], "ScenarioSnapshot"]
|
||||||
|
TraceScenarioRun = Callable[[], RoutingRunResult]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@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:
|
def _sum_metrics(metrics_list: tuple[RouteMetrics, ...]) -> RouteMetrics:
|
||||||
metric_names = RouteMetrics.__dataclass_fields__
|
metric_names = RouteMetrics.__dataclass_fields__
|
||||||
return RouteMetrics(
|
return RouteMetrics(
|
||||||
|
|
@ -318,6 +333,24 @@ def run_example_05() -> ScenarioOutcome:
|
||||||
return snapshot_example_05().as_outcome()
|
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:
|
def snapshot_example_06() -> ScenarioSnapshot:
|
||||||
bounds = (-20, -20, 170, 170)
|
bounds = (-20, -20, 170, 170)
|
||||||
obstacles = [
|
obstacles = [
|
||||||
|
|
@ -378,12 +411,40 @@ def run_example_06() -> ScenarioOutcome:
|
||||||
|
|
||||||
|
|
||||||
def snapshot_example_07() -> ScenarioSnapshot:
|
def snapshot_example_07() -> ScenarioSnapshot:
|
||||||
|
return _snapshot_example_07_variant(
|
||||||
|
"example_07_large_scale_routing",
|
||||||
|
warm_start_enabled=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_example_07_no_warm_start() -> ScenarioSnapshot:
|
||||||
|
return _snapshot_example_07_variant(
|
||||||
|
"example_07_large_scale_routing_no_warm_start",
|
||||||
|
warm_start_enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 _build_example_07_variant_stack(
|
||||||
|
*,
|
||||||
|
num_nets: int,
|
||||||
|
seed: int,
|
||||||
|
warm_start_enabled: bool,
|
||||||
|
capture_conflict_trace: bool = False,
|
||||||
|
capture_frontier_trace: bool = False,
|
||||||
|
) -> tuple[CostEvaluator, AStarMetrics, PathFinder]:
|
||||||
bounds = (0, 0, 1000, 1000)
|
bounds = (0, 0, 1000, 1000)
|
||||||
obstacles = [
|
obstacles = [
|
||||||
box(450, 0, 550, 400),
|
box(450, 0, 550, 400),
|
||||||
box(450, 600, 550, 1000),
|
box(450, 600, 550, 1000),
|
||||||
]
|
]
|
||||||
num_nets = 10
|
|
||||||
start_x = 50
|
start_x = 50
|
||||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||||
end_x = 950
|
end_x = 950
|
||||||
|
|
@ -418,10 +479,31 @@ def snapshot_example_07() -> ScenarioSnapshot:
|
||||||
"multiplier": 1.4,
|
"multiplier": 1.4,
|
||||||
"net_order": "shortest",
|
"net_order": "shortest",
|
||||||
"capture_expanded": True,
|
"capture_expanded": True,
|
||||||
|
"capture_conflict_trace": capture_conflict_trace,
|
||||||
|
"capture_frontier_trace": capture_frontier_trace,
|
||||||
"shuffle_nets": True,
|
"shuffle_nets": True,
|
||||||
"seed": 42,
|
"seed": seed,
|
||||||
|
"warm_start_enabled": warm_start_enabled,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
return evaluator, metrics, pathfinder
|
||||||
|
|
||||||
|
|
||||||
|
def _run_example_07_variant(
|
||||||
|
*,
|
||||||
|
num_nets: int,
|
||||||
|
seed: int,
|
||||||
|
warm_start_enabled: bool,
|
||||||
|
capture_conflict_trace: bool = False,
|
||||||
|
capture_frontier_trace: bool = False,
|
||||||
|
) -> RoutingRunResult:
|
||||||
|
evaluator, metrics, pathfinder = _build_example_07_variant_stack(
|
||||||
|
num_nets=num_nets,
|
||||||
|
seed=seed,
|
||||||
|
warm_start_enabled=warm_start_enabled,
|
||||||
|
capture_conflict_trace=capture_conflict_trace,
|
||||||
|
capture_frontier_trace=capture_frontier_trace,
|
||||||
|
)
|
||||||
|
|
||||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
||||||
_ = current_results
|
_ = current_results
|
||||||
|
|
@ -429,10 +511,36 @@ def snapshot_example_07() -> ScenarioSnapshot:
|
||||||
evaluator.greedy_h_weight = new_greedy
|
evaluator.greedy_h_weight = new_greedy
|
||||||
metrics.reset_per_route()
|
metrics.reset_per_route()
|
||||||
|
|
||||||
t0 = perf_counter()
|
|
||||||
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||||
|
return _make_run_result(results, pathfinder)
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_example_07_variant(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
warm_start_enabled: bool,
|
||||||
|
) -> ScenarioSnapshot:
|
||||||
|
t0 = perf_counter()
|
||||||
|
run = _run_example_07_variant(
|
||||||
|
num_nets=10,
|
||||||
|
seed=42,
|
||||||
|
warm_start_enabled=warm_start_enabled,
|
||||||
|
)
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
return _make_snapshot("example_07_large_scale_routing", results, t1 - t0, pathfinder.metrics.snapshot())
|
return _make_snapshot(name, run.results_by_net, t1 - t0, run.metrics)
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_example_07_variant(
|
||||||
|
*,
|
||||||
|
warm_start_enabled: bool,
|
||||||
|
) -> RoutingRunResult:
|
||||||
|
return _run_example_07_variant(
|
||||||
|
num_nets=10,
|
||||||
|
seed=42,
|
||||||
|
warm_start_enabled=warm_start_enabled,
|
||||||
|
capture_conflict_trace=True,
|
||||||
|
capture_frontier_trace=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_example_07() -> ScenarioOutcome:
|
def run_example_07() -> ScenarioOutcome:
|
||||||
|
|
@ -534,6 +642,19 @@ SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
|
||||||
("example_09_unroutable_best_effort", snapshot_example_09),
|
("example_09_unroutable_best_effort", snapshot_example_09),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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, ...]:
|
def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]:
|
||||||
return tuple(run() for _, run in SCENARIO_SNAPSHOTS)
|
return tuple(run() for _, run in SCENARIO_SNAPSHOTS)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,12 @@ from inire import (
|
||||||
route,
|
route,
|
||||||
)
|
)
|
||||||
from inire.geometry.components import Straight
|
from inire.geometry.components import Straight
|
||||||
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
from inire.results import RoutingReport, RoutingResult
|
||||||
|
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:
|
def test_root_module_exports_only_stable_surface() -> None:
|
||||||
import inire
|
import inire
|
||||||
|
|
||||||
|
|
@ -48,6 +52,8 @@ def test_route_problem_smoke() -> None:
|
||||||
|
|
||||||
assert set(run.results_by_net) == {"net1"}
|
assert set(run.results_by_net) == {"net1"}
|
||||||
assert run.results_by_net["net1"].is_valid
|
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:
|
def test_route_problem_supports_configs_and_debug_data() -> None:
|
||||||
|
|
@ -77,14 +83,353 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
|
||||||
assert run.expanded_nodes
|
assert run.expanded_nodes
|
||||||
assert run.metrics.nodes_expanded > 0
|
assert run.metrics.nodes_expanded > 0
|
||||||
assert run.metrics.route_iterations >= 1
|
assert run.metrics.route_iterations >= 1
|
||||||
|
assert run.metrics.iteration_reverify_calls >= 1
|
||||||
|
assert run.metrics.iteration_reverified_nets >= 0
|
||||||
|
assert run.metrics.iteration_conflicting_nets >= 0
|
||||||
|
assert run.metrics.iteration_conflict_edges >= 0
|
||||||
|
assert run.metrics.nets_carried_forward >= 0
|
||||||
assert run.metrics.nets_routed >= 1
|
assert run.metrics.nets_routed >= 1
|
||||||
assert run.metrics.move_cache_abs_misses >= 0
|
assert run.metrics.move_cache_abs_misses >= 0
|
||||||
assert run.metrics.ray_cast_calls >= 0
|
assert run.metrics.ray_cast_calls >= 0
|
||||||
assert run.metrics.dynamic_tree_rebuilds >= 0
|
assert run.metrics.dynamic_tree_rebuilds >= 0
|
||||||
|
assert run.metrics.visibility_corner_index_builds >= 0
|
||||||
assert run.metrics.visibility_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
|
||||||
|
assert run.metrics.congestion_presence_cache_misses >= 0
|
||||||
|
assert run.metrics.congestion_presence_skips >= 0
|
||||||
|
assert run.metrics.congestion_candidate_precheck_hits >= 0
|
||||||
|
assert run.metrics.congestion_candidate_precheck_misses >= 0
|
||||||
|
assert run.metrics.congestion_candidate_precheck_skips >= 0
|
||||||
|
assert run.metrics.congestion_candidate_nets >= 0
|
||||||
|
assert run.metrics.congestion_net_envelope_cache_hits >= 0
|
||||||
|
assert run.metrics.congestion_net_envelope_cache_misses >= 0
|
||||||
|
assert run.metrics.congestion_grid_net_cache_hits >= 0
|
||||||
|
assert run.metrics.congestion_grid_net_cache_misses >= 0
|
||||||
|
assert run.metrics.congestion_lazy_resolutions >= 0
|
||||||
|
assert run.metrics.congestion_lazy_requeues >= 0
|
||||||
|
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.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:
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
|
||||||
|
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
|
||||||
|
snapshots: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
def callback(iteration: int, current_results: dict[str, object]) -> None:
|
||||||
|
_ = iteration
|
||||||
|
snapshots.append({net_id: result.outcome for net_id, result in current_results.items()})
|
||||||
|
|
||||||
|
results = pathfinder.route_all(iteration_callback=callback)
|
||||||
|
|
||||||
|
assert snapshots == [{"horizontal": "colliding", "vertical": "colliding"}]
|
||||||
|
assert results["horizontal"].outcome == "colliding"
|
||||||
|
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),
|
||||||
|
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=10, warm_start_enabled=False),
|
||||||
|
refinement=RefinementOptions(enabled=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
run = route(problem, options=options)
|
||||||
|
|
||||||
|
assert run.metrics.route_iterations < 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_all_restores_best_iteration_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
problem = RoutingProblem(
|
||||||
|
bounds=(0, 0, 100, 100),
|
||||||
|
nets=(
|
||||||
|
NetSpec("netA", Port(10, 50, 0), Port(90, 50, 0), width=2.0),
|
||||||
|
NetSpec("netB", Port(50, 10, 90), Port(50, 90, 90), width=2.0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
options = RoutingOptions(
|
||||||
|
congestion=CongestionOptions(max_iterations=2, warm_start_enabled=False),
|
||||||
|
refinement=RefinementOptions(enabled=False),
|
||||||
|
)
|
||||||
|
evaluator = CostEvaluator(RoutingWorld(clearance=2.0), DangerMap(bounds=problem.bounds))
|
||||||
|
pathfinder = PathFinder(AStarContext(evaluator, problem, options))
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
missing_result = RoutingResult(net_id="netA", path=(), reached_target=False)
|
||||||
|
unroutable_b = RoutingResult(net_id="netB", 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, "netB": unroutable_b}
|
||||||
|
return _IterationReview(
|
||||||
|
conflicting_nets={"netA", "netB"},
|
||||||
|
conflict_edges={("netA", "netB")},
|
||||||
|
completed_net_ids={"netA"},
|
||||||
|
total_dynamic_collisions=1,
|
||||||
|
)
|
||||||
|
state.results = {"netA": missing_result, "netB": unroutable_b}
|
||||||
|
return _IterationReview(
|
||||||
|
conflicting_nets={"netA", "netB"},
|
||||||
|
conflict_edges={("netA", "netB")},
|
||||||
|
completed_net_ids=set(),
|
||||||
|
total_dynamic_collisions=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(PathFinder, "_run_iteration", fake_run_iteration)
|
||||||
|
monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results))
|
||||||
|
|
||||||
|
results = pathfinder.route_all()
|
||||||
|
|
||||||
|
assert results["netA"].outcome == "completed"
|
||||||
|
assert results["netB"].outcome == "unroutable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_all_restores_best_iteration_snapshot_on_timeout(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),
|
||||||
|
)
|
||||||
|
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_iterations(self, state, iteration_callback):
|
||||||
|
_ = iteration_callback
|
||||||
|
_ = self
|
||||||
|
state.results = {"netA": best_result}
|
||||||
|
pathfinder._update_best_iteration(
|
||||||
|
state,
|
||||||
|
_IterationReview(
|
||||||
|
conflicting_nets=set(),
|
||||||
|
conflict_edges=set(),
|
||||||
|
completed_net_ids={"netA"},
|
||||||
|
total_dynamic_collisions=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
state.results = {"netA": worse_result}
|
||||||
|
return True
|
||||||
|
|
||||||
|
monkeypatch.setattr(PathFinder, "_run_iterations", fake_run_iterations)
|
||||||
|
monkeypatch.setattr(PathFinder, "_verify_results", lambda self, state: dict(state.results))
|
||||||
|
|
||||||
|
results = pathfinder.route_all()
|
||||||
|
|
||||||
|
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:
|
def test_route_problem_locked_routes_become_static_obstacles() -> None:
|
||||||
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
|
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
|
||||||
problem = RoutingProblem(
|
problem = RoutingProblem(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
import math
|
import math
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from shapely.geometry import Polygon
|
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.components import Bend90, Straight
|
||||||
from inire.geometry.collision import RoutingWorld
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router._astar_types import AStarContext, SearchRunConfig
|
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._search import route_astar
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.seeds import StraightSeed
|
||||||
|
|
||||||
BOUNDS = (0, -50, 150, 150)
|
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
|
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:
|
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")
|
context = _build_context(basic_evaluator, bounds=BOUNDS, bend_radii=(10.0,), bend_collision_type="arc")
|
||||||
|
|
||||||
|
|
@ -301,6 +385,27 @@ def test_route_astar_supports_all_visibility_guidance_modes(
|
||||||
assert validation["connectivity_ok"]
|
assert validation["connectivity_ok"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tangent_corner_mode_avoids_exact_visibility_graph_builds(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
obstacle = Polygon([(30, 10), (50, 10), (50, 40), (30, 40)])
|
||||||
|
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||||
|
basic_evaluator.danger_map.precompute([obstacle])
|
||||||
|
context = _build_context(
|
||||||
|
basic_evaluator,
|
||||||
|
bounds=BOUNDS,
|
||||||
|
bend_radii=(10.0,),
|
||||||
|
sbend_radii=(),
|
||||||
|
max_straight_length=150.0,
|
||||||
|
visibility_guidance="tangent_corner",
|
||||||
|
)
|
||||||
|
|
||||||
|
path = _route(context, Port(0, 0, 0), Port(80, 50, 0))
|
||||||
|
|
||||||
|
assert path is not None
|
||||||
|
assert context.metrics.total_visibility_builds == 0
|
||||||
|
assert context.metrics.total_visibility_corner_pairs_checked == 0
|
||||||
|
assert context.metrics.total_ray_cast_calls_visibility_build == 0
|
||||||
|
|
||||||
|
|
||||||
def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None:
|
def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None:
|
||||||
context = AStarContext(
|
context = AStarContext(
|
||||||
basic_evaluator,
|
basic_evaluator,
|
||||||
|
|
@ -318,3 +423,244 @@ def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_eval
|
||||||
path = _route(context, start, target)
|
path = _route(context, start, target)
|
||||||
assert path is not None
|
assert path is not None
|
||||||
assert path[-1].end_port == target
|
assert path[-1].end_port == target
|
||||||
|
|
||||||
|
|
||||||
|
def test_self_collision_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||||
|
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
|
||||||
|
parent_result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
parent = AStarNode(parent_result.end_port, g_cost=10.0, h_cost=0.0, parent=root, component_result=parent_result)
|
||||||
|
open_set: list[AStarNode] = []
|
||||||
|
closed_set: dict[tuple[int, int, int], float] = {}
|
||||||
|
|
||||||
|
add_node(
|
||||||
|
parent,
|
||||||
|
parent_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, self_collision_check=True),
|
||||||
|
move_type="straight",
|
||||||
|
cache_key=("overlap",),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not open_set
|
||||||
|
assert context.metrics.total_congestion_check_calls == 0
|
||||||
|
assert context.metrics.total_congestion_cache_misses == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_closed_set_dominance_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||||
|
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
|
||||||
|
result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
open_set: list[AStarNode] = []
|
||||||
|
closed_set = {result.end_port.as_tuple(): context.cost_evaluator.score_component(result, start_port=root.port)}
|
||||||
|
|
||||||
|
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),
|
||||||
|
move_type="straight",
|
||||||
|
cache_key=("dominated",),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not open_set
|
||||||
|
assert context.metrics.total_congestion_check_calls == 0
|
||||||
|
assert context.metrics.total_congestion_cache_misses == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_dynamic_paths_skips_congestion_check(basic_evaluator: CostEvaluator) -> None:
|
||||||
|
context = _build_context(basic_evaluator, bounds=BOUNDS)
|
||||||
|
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
|
||||||
|
result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
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),
|
||||||
|
move_type="straight",
|
||||||
|
cache_key=("no-dynamic",),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
from inire.geometry.collision import RoutingWorld
|
from inire.geometry.collision import RoutingWorld
|
||||||
|
from inire.geometry.components import ComponentResult
|
||||||
from inire.geometry.components import Straight
|
from inire.geometry.components import Straight
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router._astar_types import AStarMetrics
|
||||||
|
from inire.seeds import StraightSeed
|
||||||
|
|
||||||
|
|
||||||
def _install_static_straight(
|
def _install_static_straight(
|
||||||
|
|
@ -82,6 +87,7 @@ def test_check_move_static_clearance() -> None:
|
||||||
|
|
||||||
def test_verify_path_report_preserves_long_net_id() -> None:
|
def test_verify_path_report_preserves_long_net_id() -> None:
|
||||||
engine = RoutingWorld(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789"
|
net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789"
|
||||||
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
geoms = [poly for component in path for poly in component.collision_geometry]
|
geoms = [poly for component in path for poly in component.collision_geometry]
|
||||||
|
|
@ -91,10 +97,12 @@ def test_verify_path_report_preserves_long_net_id() -> None:
|
||||||
report = engine.verify_path_report(net_id, path)
|
report = engine.verify_path_report(net_id, path)
|
||||||
|
|
||||||
assert report.dynamic_collision_count == 0
|
assert report.dynamic_collision_count == 0
|
||||||
|
assert engine.metrics.total_verify_dynamic_candidate_nets == 0
|
||||||
|
|
||||||
|
|
||||||
def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None:
|
def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None:
|
||||||
engine = RoutingWorld(clearance=2.0)
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_"
|
shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_"
|
||||||
net_a = f"{shared_prefix}A"
|
net_a = f"{shared_prefix}A"
|
||||||
net_b = f"{shared_prefix}B"
|
net_b = f"{shared_prefix}B"
|
||||||
|
|
@ -115,6 +123,111 @@ def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> N
|
||||||
report = engine.verify_path_report(net_a, path_a)
|
report = engine.verify_path_report(net_a, path_a)
|
||||||
|
|
||||||
assert report.dynamic_collision_count == 1
|
assert report.dynamic_collision_count == 1
|
||||||
|
assert engine.metrics.total_verify_dynamic_candidate_nets == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_path_report_uses_net_envelopes_before_dynamic_object_scan() -> 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(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
path_far = [Straight.generate(Port(100, 100, 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],
|
||||||
|
)
|
||||||
|
engine.add_path(
|
||||||
|
"netFar",
|
||||||
|
[poly for component in path_far for poly in component.collision_geometry],
|
||||||
|
dilated_geometry=[poly for component in path_far for poly in component.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
report = engine.verify_path_report("netA", path_a)
|
||||||
|
|
||||||
|
assert report.dynamic_collision_count == 1
|
||||||
|
assert engine.metrics.total_verify_dynamic_candidate_nets == 1
|
||||||
|
assert engine.metrics.total_verify_dynamic_exact_pair_checks == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_path_details_returns_conflicting_net_ids() -> 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(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],
|
||||||
|
)
|
||||||
|
|
||||||
|
detail = engine.verify_path_details("netA", path_a)
|
||||||
|
|
||||||
|
assert detail.report.dynamic_collision_count == 1
|
||||||
|
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:
|
def test_remove_path_clears_dynamic_path() -> None:
|
||||||
|
|
@ -129,3 +242,247 @@ def test_remove_path_clears_dynamic_path() -> None:
|
||||||
engine.remove_path("netA")
|
engine.remove_path("netA")
|
||||||
assert list(engine._dynamic_paths.geometries.values()) == []
|
assert list(engine._dynamic_paths.geometries.values()) == []
|
||||||
assert len(engine._static_obstacles.geometries) == 0
|
assert len(engine._static_obstacles.geometries) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_grid_updates_incrementally_on_add_and_remove() -> 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(0, 4, 0), 20.0, width=2.0, dilation=1.0)]
|
||||||
|
|
||||||
|
engine.add_path(
|
||||||
|
"netA",
|
||||||
|
[poly for component in path_a for poly in component.collision_geometry],
|
||||||
|
dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamic_paths = engine._dynamic_paths
|
||||||
|
assert dynamic_paths.net_to_obj_ids["netA"]
|
||||||
|
assert dynamic_paths.net_to_obj_ids["netB"]
|
||||||
|
assert dynamic_paths.grid
|
||||||
|
assert engine.metrics.total_dynamic_grid_rebuilds == 0
|
||||||
|
|
||||||
|
engine.remove_path("netA")
|
||||||
|
|
||||||
|
assert "netA" not in dynamic_paths.net_to_obj_ids
|
||||||
|
assert "netB" in dynamic_paths.net_to_obj_ids
|
||||||
|
assert engine.metrics.total_dynamic_grid_rebuilds == 0
|
||||||
|
assert "netA" not in dynamic_paths.grid_net_obj_ids.get((0, -1), {})
|
||||||
|
assert "netB" in dynamic_paths.grid_net_obj_ids.get((0, 0), {})
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_net_envelopes_update_incrementally_on_add_and_remove() -> 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(0, 40, 0), 10.0, width=2.0, dilation=1.0)]
|
||||||
|
|
||||||
|
engine.add_path(
|
||||||
|
"netA",
|
||||||
|
[poly for component in path_a for poly in component.collision_geometry],
|
||||||
|
dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamic_paths = engine._dynamic_paths
|
||||||
|
assert set(dynamic_paths.net_envelopes) == {"netA", "netB"}
|
||||||
|
assert dynamic_paths.net_envelopes["netA"] == (-1.0, -2.0, 21.0, 2.0)
|
||||||
|
assert dynamic_paths.net_envelopes["netB"] == (-1.0, 38.0, 11.0, 42.0)
|
||||||
|
assert engine.metrics.total_dynamic_tree_rebuilds == 0
|
||||||
|
|
||||||
|
net_b_envelope_obj_id = dynamic_paths.net_envelope_obj_ids["netB"]
|
||||||
|
assert list(dynamic_paths.net_envelope_index.intersection((-5.0, 35.0, 15.0, 45.0))) == [net_b_envelope_obj_id]
|
||||||
|
|
||||||
|
engine.remove_path("netA")
|
||||||
|
|
||||||
|
assert "netA" not in dynamic_paths.net_envelopes
|
||||||
|
assert "netA" not in dynamic_paths.net_envelope_obj_ids
|
||||||
|
assert "netB" in dynamic_paths.net_envelopes
|
||||||
|
assert engine.metrics.total_dynamic_tree_rebuilds == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_congestion_query_uses_per_polygon_bounds() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
|
||||||
|
blocker = Straight.generate(Port(40, 4, 90), 2.0, width=2.0, dilation=1.0)
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in blocker.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
move = ComponentResult(
|
||||||
|
start_port=Port(0, 0, 0),
|
||||||
|
collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
|
||||||
|
end_port=Port(100, 0, 0),
|
||||||
|
length=100.0,
|
||||||
|
move_type="straight",
|
||||||
|
move_spec=StraightSeed(100.0),
|
||||||
|
physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
|
||||||
|
dilated_collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
|
||||||
|
dilated_physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert engine.check_move_congestion(move, "netA") == 0
|
||||||
|
assert engine.metrics.total_congestion_candidate_nets == 0
|
||||||
|
assert engine.metrics.total_congestion_candidate_ids == 0
|
||||||
|
assert engine.metrics.total_congestion_exact_pair_checks == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_congestion_touching_geometries_do_not_count_as_overlap() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
|
||||||
|
existing = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
touching = Straight.generate(Port(12, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in existing.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in existing.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert engine.check_move_congestion(touching, "netA") == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_congestion_exact_checks_only_touch_relevant_move_polygons() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
|
||||||
|
blocker = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in blocker.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
move = ComponentResult(
|
||||||
|
start_port=Port(0, 0, 0),
|
||||||
|
collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
|
||||||
|
end_port=Port(100, 0, 0),
|
||||||
|
length=100.0,
|
||||||
|
move_type="straight",
|
||||||
|
move_spec=StraightSeed(100.0),
|
||||||
|
physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
|
||||||
|
dilated_collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
|
||||||
|
dilated_physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert engine.check_move_congestion(move, "netA") == 1
|
||||||
|
assert engine.metrics.total_congestion_candidate_nets == 1
|
||||||
|
assert engine.metrics.total_congestion_candidate_ids == 1
|
||||||
|
assert engine.metrics.total_congestion_exact_pair_checks == 1
|
||||||
|
|
||||||
|
def test_congestion_grid_span_cache_reuses_broad_phase_candidates() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {}
|
||||||
|
|
||||||
|
blocker = Straight.generate(Port(15, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in blocker.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
|
||||||
|
move_a = Straight.generate(Port(5, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
move_b = Straight.generate(Port(7, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
|
||||||
|
assert engine.check_move_congestion(
|
||||||
|
move_a,
|
||||||
|
"netA",
|
||||||
|
net_envelope_cache=net_envelope_cache,
|
||||||
|
grid_net_cache=grid_net_cache,
|
||||||
|
broad_phase_cache=cache,
|
||||||
|
) == 1
|
||||||
|
assert engine.check_move_congestion(
|
||||||
|
move_b,
|
||||||
|
"netA",
|
||||||
|
net_envelope_cache=net_envelope_cache,
|
||||||
|
grid_net_cache=grid_net_cache,
|
||||||
|
broad_phase_cache=cache,
|
||||||
|
) == 1
|
||||||
|
assert engine.metrics.total_congestion_candidate_nets == 2
|
||||||
|
assert engine.metrics.total_congestion_net_envelope_cache_misses == 1
|
||||||
|
assert engine.metrics.total_congestion_net_envelope_cache_hits == 1
|
||||||
|
assert engine.metrics.total_congestion_grid_net_cache_misses == 1
|
||||||
|
assert engine.metrics.total_congestion_grid_net_cache_hits == 1
|
||||||
|
assert engine.metrics.total_congestion_grid_span_cache_misses == 1
|
||||||
|
assert engine.metrics.total_congestion_grid_span_cache_hits == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_possible_move_congestion_uses_presence_cache_and_skips_empty_spans() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
presence_cache: dict[tuple[str, int, int, int, int], bool] = {}
|
||||||
|
|
||||||
|
blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in blocker.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
|
||||||
|
assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache)
|
||||||
|
assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache)
|
||||||
|
assert engine.metrics.total_congestion_presence_cache_misses == 1
|
||||||
|
assert engine.metrics.total_congestion_presence_cache_hits == 1
|
||||||
|
|
||||||
|
occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
|
||||||
|
assert engine.has_possible_move_congestion(occupied_move, "netA")
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_candidate_move_congestion_uses_candidate_precheck_cache() -> None:
|
||||||
|
engine = RoutingWorld(clearance=2.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {}
|
||||||
|
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
|
||||||
|
|
||||||
|
blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
engine.add_path(
|
||||||
|
"netB",
|
||||||
|
[poly for poly in blocker.collision_geometry],
|
||||||
|
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
|
||||||
|
)
|
||||||
|
empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
|
||||||
|
|
||||||
|
assert not engine.has_candidate_move_congestion(
|
||||||
|
empty_move,
|
||||||
|
"netA",
|
||||||
|
candidate_precheck_cache,
|
||||||
|
net_envelope_cache,
|
||||||
|
grid_net_cache,
|
||||||
|
)
|
||||||
|
assert not engine.has_candidate_move_congestion(
|
||||||
|
empty_move,
|
||||||
|
"netA",
|
||||||
|
candidate_precheck_cache,
|
||||||
|
net_envelope_cache,
|
||||||
|
grid_net_cache,
|
||||||
|
)
|
||||||
|
assert engine.has_candidate_move_congestion(
|
||||||
|
occupied_move,
|
||||||
|
"netA",
|
||||||
|
candidate_precheck_cache,
|
||||||
|
net_envelope_cache,
|
||||||
|
grid_net_cache,
|
||||||
|
)
|
||||||
|
assert engine.metrics.total_congestion_candidate_precheck_misses >= 2
|
||||||
|
assert engine.metrics.total_congestion_candidate_precheck_hits >= 1
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome
|
from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome, snapshot_example_07_no_warm_start
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
||||||
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
|
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
|
||||||
PERFORMANCE_REPEATS = 3
|
PERFORMANCE_REPEATS = 3
|
||||||
REGRESSION_FACTOR = 1.5
|
REGRESSION_FACTOR = 1.5
|
||||||
|
NO_WARM_START_REGRESSION_SECONDS = 15.0
|
||||||
|
|
||||||
# Baselines are measured from clean 6a28dcf-style runs without plotting.
|
# Baselines are measured from clean 6a28dcf-style runs without plotting.
|
||||||
BASELINE_SECONDS = {
|
BASELINE_SECONDS = {
|
||||||
|
|
@ -67,3 +68,20 @@ def test_example_like_runtime_regression(scenario: tuple[str, Callable[[], Scena
|
||||||
f"{REGRESSION_FACTOR:.1f}x baseline {BASELINE_SECONDS[name]:.4f}s "
|
f"{REGRESSION_FACTOR:.1f}x baseline {BASELINE_SECONDS[name]:.4f}s "
|
||||||
f"from timings {timings!r}"
|
f"from timings {timings!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.performance
|
||||||
|
@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks")
|
||||||
|
def test_example_07_no_warm_start_runtime_regression() -> None:
|
||||||
|
snapshot = snapshot_example_07_no_warm_start()
|
||||||
|
|
||||||
|
assert snapshot.total_results == 10
|
||||||
|
assert snapshot.valid_results == 10
|
||||||
|
assert snapshot.reached_targets == 10
|
||||||
|
assert snapshot.metrics.warm_start_paths_built == 0
|
||||||
|
assert snapshot.metrics.warm_start_paths_used == 0
|
||||||
|
assert snapshot.duration_s <= NO_WARM_START_REGRESSION_SECONDS, (
|
||||||
|
"example_07_large_scale_routing_no_warm_start runtime "
|
||||||
|
f"{snapshot.duration_s:.4f}s exceeded guardrail "
|
||||||
|
f"{NO_WARM_START_REGRESSION_SECONDS:.1f}s"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,16 @@ from inire import (
|
||||||
)
|
)
|
||||||
from inire.router._stack import build_routing_stack
|
from inire.router._stack import build_routing_stack
|
||||||
from inire.seeds import Bend90Seed, PathSeed, StraightSeed
|
from inire.seeds import Bend90Seed, PathSeed, StraightSeed
|
||||||
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics
|
from inire.tests.example_scenarios import (
|
||||||
|
SCENARIOS,
|
||||||
|
_build_evaluator,
|
||||||
|
_build_pathfinder,
|
||||||
|
_net_specs,
|
||||||
|
AStarMetrics,
|
||||||
|
snapshot_example_05,
|
||||||
|
snapshot_example_07_no_warm_start,
|
||||||
|
trace_example_07_no_warm_start,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
EXPECTED_OUTCOMES = {
|
EXPECTED_OUTCOMES = {
|
||||||
|
|
@ -36,6 +45,43 @@ def test_examples_match_legacy_expected_outcomes(name: str, run) -> None:
|
||||||
assert outcome[1:] == EXPECTED_OUTCOMES[name]
|
assert outcome[1:] == EXPECTED_OUTCOMES[name]
|
||||||
|
|
||||||
|
|
||||||
|
def test_example_05_avoids_dynamic_tree_rebuilds() -> None:
|
||||||
|
snapshot = snapshot_example_05()
|
||||||
|
|
||||||
|
assert snapshot.valid_results == 3
|
||||||
|
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
|
||||||
|
assert snapshot.metrics.warm_start_paths_built == 0
|
||||||
|
assert snapshot.metrics.warm_start_paths_used == 0
|
||||||
|
assert snapshot.metrics.pair_local_search_pairs_considered >= 1
|
||||||
|
assert snapshot.metrics.pair_local_search_accepts >= 1
|
||||||
|
assert snapshot.metrics.pair_local_search_nodes_expanded <= 128
|
||||||
|
assert snapshot.metrics.nodes_expanded <= 2500
|
||||||
|
assert snapshot.metrics.congestion_check_calls <= 6000
|
||||||
|
|
||||||
|
|
||||||
|
def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> None:
|
||||||
|
run = trace_example_07_no_warm_start()
|
||||||
|
|
||||||
|
assert len(run.results_by_net) == 10
|
||||||
|
assert sum(result.is_valid for result in run.results_by_net.values()) == 10
|
||||||
|
assert sum(result.reached_target for result in run.results_by_net.values()) == 10
|
||||||
|
assert run.metrics.pair_local_search_pairs_considered >= 1
|
||||||
|
assert run.metrics.pair_local_search_accepts >= 1
|
||||||
|
|
||||||
|
final_entry = run.conflict_trace[-1]
|
||||||
|
assert final_entry.stage == "final"
|
||||||
|
assert len(final_entry.completed_net_ids) == 10
|
||||||
|
assert final_entry.conflict_edges == ()
|
||||||
|
|
||||||
|
|
||||||
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
|
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
|
||||||
bounds = (-20, -20, 170, 170)
|
bounds = (-20, -20, 170, 170)
|
||||||
obstacles = (
|
obstacles = (
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,38 @@ def test_snapshot_example_01_exposes_metrics() -> None:
|
||||||
assert snapshot.metrics.route_iterations >= 1
|
assert snapshot.metrics.route_iterations >= 1
|
||||||
assert snapshot.metrics.nets_routed >= 1
|
assert snapshot.metrics.nets_routed >= 1
|
||||||
assert snapshot.metrics.nodes_expanded > 0
|
assert snapshot.metrics.nodes_expanded > 0
|
||||||
|
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.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 >= 0
|
||||||
|
assert snapshot.metrics.ray_cast_calls_expand_forward >= 0
|
||||||
assert snapshot.metrics.dynamic_tree_rebuilds >= 0
|
assert snapshot.metrics.dynamic_tree_rebuilds >= 0
|
||||||
|
assert snapshot.metrics.visibility_corner_index_builds >= 0
|
||||||
assert snapshot.metrics.visibility_builds >= 0
|
assert snapshot.metrics.visibility_builds >= 0
|
||||||
|
assert snapshot.metrics.congestion_grid_span_cache_hits >= 0
|
||||||
|
assert snapshot.metrics.congestion_grid_span_cache_misses >= 0
|
||||||
|
assert snapshot.metrics.congestion_candidate_nets >= 0
|
||||||
|
assert snapshot.metrics.congestion_net_envelope_cache_hits >= 0
|
||||||
|
assert snapshot.metrics.congestion_net_envelope_cache_misses >= 0
|
||||||
|
assert snapshot.metrics.congestion_grid_net_cache_hits >= 0
|
||||||
|
assert snapshot.metrics.congestion_grid_net_cache_misses >= 0
|
||||||
|
assert snapshot.metrics.congestion_lazy_resolutions >= 0
|
||||||
|
assert snapshot.metrics.congestion_lazy_requeues >= 0
|
||||||
|
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:
|
def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None:
|
||||||
|
|
@ -43,3 +71,236 @@ def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: P
|
||||||
assert payload["generator"] == "scripts/record_performance_baseline.py"
|
assert payload["generator"] == "scripts/record_performance_baseline.py"
|
||||||
assert [entry["name"] for entry in payload["scenarios"]] == ["example_01_simple_route"]
|
assert [entry["name"] for entry in payload["scenarios"]] == ["example_01_simple_route"]
|
||||||
assert (tmp_path / "performance.md").exists()
|
assert (tmp_path / "performance.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
record_script = repo_root / "scripts" / "record_performance_baseline.py"
|
||||||
|
diff_script = repo_root / "scripts" / "diff_performance_baseline.py"
|
||||||
|
baseline_dir = tmp_path / "baseline"
|
||||||
|
baseline_dir.mkdir()
|
||||||
|
output_path = tmp_path / "diff.md"
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(record_script),
|
||||||
|
"--output-dir",
|
||||||
|
str(baseline_dir),
|
||||||
|
"--scenario",
|
||||||
|
"example_01_simple_route",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(diff_script),
|
||||||
|
"--baseline",
|
||||||
|
str(baseline_dir / "performance_baseline.json"),
|
||||||
|
"--include-performance-only",
|
||||||
|
"--scenario",
|
||||||
|
"example_01_simple_route",
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
report = output_path.read_text()
|
||||||
|
assert "Performance Baseline Diff" in report
|
||||||
|
assert "example_01_simple_route" in report
|
||||||
|
|
||||||
|
|
||||||
|
def test_diff_performance_baseline_script_can_append_measurement_log(tmp_path: Path) -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
record_script = repo_root / "scripts" / "record_performance_baseline.py"
|
||||||
|
diff_script = repo_root / "scripts" / "diff_performance_baseline.py"
|
||||||
|
baseline_dir = tmp_path / "baseline"
|
||||||
|
baseline_dir.mkdir()
|
||||||
|
log_path = tmp_path / "optimization.md"
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(record_script),
|
||||||
|
"--output-dir",
|
||||||
|
str(baseline_dir),
|
||||||
|
"--scenario",
|
||||||
|
"example_01_simple_route",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(diff_script),
|
||||||
|
"--baseline",
|
||||||
|
str(baseline_dir / "performance_baseline.json"),
|
||||||
|
"--include-performance-only",
|
||||||
|
"--scenario",
|
||||||
|
"example_01_simple_route",
|
||||||
|
"--metric",
|
||||||
|
"duration_s",
|
||||||
|
"--metric",
|
||||||
|
"valid_results",
|
||||||
|
"--metric",
|
||||||
|
"nodes_expanded",
|
||||||
|
"--metric",
|
||||||
|
"visibility_corner_index_builds",
|
||||||
|
"--label",
|
||||||
|
"Step 0 - Baseline",
|
||||||
|
"--notes",
|
||||||
|
"Tooling smoke test.",
|
||||||
|
"--log",
|
||||||
|
str(log_path),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
report = log_path.read_text()
|
||||||
|
assert "Step 0 - Baseline" in report
|
||||||
|
assert "Tooling smoke test." in report
|
||||||
|
assert "| example_01_simple_route | duration_s |" in report
|
||||||
|
assert "| example_01_simple_route | valid_results |" in report
|
||||||
|
assert "| example_01_simple_route | visibility_corner_index_builds |" in report
|
||||||
|
|
||||||
|
|
||||||
|
def test_diff_performance_baseline_script_renders_current_metrics_for_added_scenario(tmp_path: Path) -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
record_script = repo_root / "scripts" / "record_performance_baseline.py"
|
||||||
|
diff_script = repo_root / "scripts" / "diff_performance_baseline.py"
|
||||||
|
baseline_dir = tmp_path / "baseline"
|
||||||
|
baseline_dir.mkdir()
|
||||||
|
output_path = tmp_path / "diff_added.md"
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(record_script),
|
||||||
|
"--output-dir",
|
||||||
|
str(baseline_dir),
|
||||||
|
"--scenario",
|
||||||
|
"example_01_simple_route",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(diff_script),
|
||||||
|
"--baseline",
|
||||||
|
str(baseline_dir / "performance_baseline.json"),
|
||||||
|
"--include-performance-only",
|
||||||
|
"--scenario",
|
||||||
|
"example_07_large_scale_routing_no_warm_start",
|
||||||
|
"--metric",
|
||||||
|
"duration_s",
|
||||||
|
"--metric",
|
||||||
|
"nodes_expanded",
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
script_path = repo_root / "scripts" / "characterize_pair_local_search.py"
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(script_path),
|
||||||
|
"--output-dir",
|
||||||
|
str(tmp_path),
|
||||||
|
"--num-nets",
|
||||||
|
"6",
|
||||||
|
"--seeds",
|
||||||
|
"41",
|
||||||
|
"--repeats",
|
||||||
|
"1",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = json.loads((tmp_path / "pair_local_characterization.json").read_text())
|
||||||
|
assert payload["generated_at"]
|
||||||
|
assert payload["generator"] == "scripts/characterize_pair_local_search.py"
|
||||||
|
assert payload["grid"]["num_nets"] == [6]
|
||||||
|
assert payload["grid"]["seeds"] == [41]
|
||||||
|
assert payload["grid"]["repeats"] == 1
|
||||||
|
assert len(payload["cases"]) == 1
|
||||||
|
assert (tmp_path / "pair_local_characterization.md").exists()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from shapely.geometry import box
|
||||||
|
|
||||||
from inire.geometry.collision import RoutingWorld
|
from inire.geometry.collision import RoutingWorld
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router._astar_types import AStarMetrics
|
||||||
from inire.router.visibility import VisibilityManager
|
from inire.router.visibility import VisibilityManager
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,3 +19,111 @@ def test_point_visibility_cache_respects_max_distance() -> None:
|
||||||
assert len(near_corners) == 3
|
assert len(near_corners) == 3
|
||||||
assert len(far_corners) > len(near_corners)
|
assert len(far_corners) > len(near_corners)
|
||||||
assert any(corner[0] >= 100.0 for corner in far_corners)
|
assert any(corner[0] >= 100.0 for corner in far_corners)
|
||||||
|
|
||||||
|
|
||||||
|
def test_visibility_manager_is_lazy_until_queried() -> None:
|
||||||
|
engine = RoutingWorld(clearance=0.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
engine.add_static_obstacle(box(10, 20, 20, 30))
|
||||||
|
|
||||||
|
visibility = VisibilityManager(engine)
|
||||||
|
|
||||||
|
assert visibility.corners == []
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 0
|
||||||
|
assert engine.metrics.total_visibility_builds == 0
|
||||||
|
|
||||||
|
visibility.ensure_corner_index_current()
|
||||||
|
|
||||||
|
assert visibility.corners
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 1
|
||||||
|
assert engine.metrics.total_visibility_builds == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_corner_visibility_builds_graph_once_per_static_version() -> None:
|
||||||
|
engine = RoutingWorld(clearance=0.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
engine.add_static_obstacle(box(10, 20, 20, 30))
|
||||||
|
visibility = VisibilityManager(engine)
|
||||||
|
origin = Port(10, 20, 0)
|
||||||
|
|
||||||
|
first = visibility.get_corner_visibility(origin, max_dist=100.0)
|
||||||
|
second = visibility.get_corner_visibility(origin, max_dist=100.0)
|
||||||
|
|
||||||
|
assert second == first
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 1
|
||||||
|
assert engine.metrics.total_visibility_builds == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_cache_invalidates_without_rebuilding_and_static_change_rebuilds_lazily() -> None:
|
||||||
|
engine = RoutingWorld(clearance=0.0)
|
||||||
|
engine.metrics = AStarMetrics()
|
||||||
|
engine.add_static_obstacle(box(10, 20, 20, 30))
|
||||||
|
visibility = VisibilityManager(engine)
|
||||||
|
|
||||||
|
visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0)
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 1
|
||||||
|
assert engine.metrics.total_visibility_builds == 1
|
||||||
|
|
||||||
|
visibility.clear_cache()
|
||||||
|
|
||||||
|
assert visibility.corners == []
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 1
|
||||||
|
assert engine.metrics.total_visibility_builds == 1
|
||||||
|
|
||||||
|
engine.add_static_obstacle(box(40, 20, 50, 30))
|
||||||
|
|
||||||
|
visible = visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0)
|
||||||
|
|
||||||
|
assert visible == []
|
||||||
|
assert engine.metrics.total_visibility_corner_index_builds == 2
|
||||||
|
assert engine.metrics.total_visibility_builds == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_tangent_corner_candidate_query_matches_bruteforce_filter() -> None:
|
||||||
|
engine = RoutingWorld(clearance=0.0)
|
||||||
|
engine.add_static_obstacle(box(10, 20, 20, 30))
|
||||||
|
engine.add_static_obstacle(box(-35, -15, -25, -5))
|
||||||
|
engine.add_static_obstacle(box(35, -40, 45, -30))
|
||||||
|
visibility = VisibilityManager(engine)
|
||||||
|
radii = (10.0, 20.0)
|
||||||
|
min_forward = 5.0
|
||||||
|
max_forward = 60.0
|
||||||
|
tolerance = 2.0
|
||||||
|
|
||||||
|
for origin in (
|
||||||
|
Port(0, 0, 0),
|
||||||
|
Port(0, 0, 90),
|
||||||
|
Port(0, 0, 180),
|
||||||
|
Port(0, 0, 270),
|
||||||
|
):
|
||||||
|
candidate_ids = set(
|
||||||
|
visibility.get_tangent_corner_candidates(
|
||||||
|
origin,
|
||||||
|
min_forward=min_forward,
|
||||||
|
max_forward=max_forward,
|
||||||
|
radii=radii,
|
||||||
|
tolerance=tolerance,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_ids: set[int] = set()
|
||||||
|
if origin.r == 0:
|
||||||
|
cos_v, sin_v = 1.0, 0.0
|
||||||
|
elif origin.r == 90:
|
||||||
|
cos_v, sin_v = 0.0, 1.0
|
||||||
|
elif origin.r == 180:
|
||||||
|
cos_v, sin_v = -1.0, 0.0
|
||||||
|
else:
|
||||||
|
cos_v, sin_v = 0.0, -1.0
|
||||||
|
|
||||||
|
for idx, (cx, cy) in enumerate(visibility.corners):
|
||||||
|
dx = cx - origin.x
|
||||||
|
dy = cy - origin.y
|
||||||
|
local_x = dx * cos_v + dy * sin_v
|
||||||
|
local_y = -dx * sin_v + dy * cos_v
|
||||||
|
if local_x <= min_forward or local_x > max_forward + 0.01:
|
||||||
|
continue
|
||||||
|
nearest_radius = min(radii, key=lambda radius: abs(abs(local_y) - radius))
|
||||||
|
if abs(abs(local_y) - nearest_radius) <= tolerance:
|
||||||
|
expected_ids.add(idx)
|
||||||
|
|
||||||
|
assert candidate_ids == expected_ids
|
||||||
|
|
|
||||||
177
scripts/characterize_pair_local_search.py
Normal file
177
scripts/characterize_pair_local_search.py
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from time import perf_counter
|
||||||
|
|
||||||
|
from inire.tests.example_scenarios import _run_example_07_variant
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_csv_ints(raw: str) -> tuple[int, ...]:
|
||||||
|
return tuple(int(part) for part in raw.split(",") if part.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _run_case(num_nets: int, seed: int) -> dict[str, object]:
|
||||||
|
t0 = perf_counter()
|
||||||
|
run = _run_example_07_variant(
|
||||||
|
num_nets=num_nets,
|
||||||
|
seed=seed,
|
||||||
|
warm_start_enabled=False,
|
||||||
|
)
|
||||||
|
duration_s = perf_counter() - t0
|
||||||
|
return {
|
||||||
|
"duration_s": duration_s,
|
||||||
|
"summary": {
|
||||||
|
"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),
|
||||||
|
},
|
||||||
|
"metrics": asdict(run.metrics),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_smoke_candidate(entry: dict[str, object]) -> bool:
|
||||||
|
summary = entry["summary"]
|
||||||
|
metrics = entry["metrics"]
|
||||||
|
return (
|
||||||
|
summary["valid_results"] == summary["total_results"]
|
||||||
|
and metrics["pair_local_search_accepts"] >= 1
|
||||||
|
and entry["duration_s"] <= 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _select_smoke_case(cases: list[dict[str, object]]) -> dict[str, object] | None:
|
||||||
|
grouped: dict[tuple[int, int], list[dict[str, object]]] = {}
|
||||||
|
for case in cases:
|
||||||
|
key = (case["num_nets"], case["seed"])
|
||||||
|
grouped.setdefault(key, []).append(case)
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for (num_nets, seed), repeats in grouped.items():
|
||||||
|
if repeats and all(_is_smoke_candidate(repeat) for repeat in repeats):
|
||||||
|
candidates.append({"num_nets": num_nets, "seed": seed})
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
candidates.sort(key=lambda item: (item["num_nets"], item["seed"]))
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _render_markdown(payload: dict[str, object]) -> str:
|
||||||
|
lines = [
|
||||||
|
"# Pair-Local Search Characterization",
|
||||||
|
"",
|
||||||
|
f"Generated at {payload['generated_at']} by `{payload['generator']}`.",
|
||||||
|
"",
|
||||||
|
f"Grid: `num_nets={payload['grid']['num_nets']}`, `seed={payload['grid']['seeds']}`, repeats={payload['grid']['repeats']}.",
|
||||||
|
"",
|
||||||
|
"| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks |",
|
||||||
|
"| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |",
|
||||||
|
]
|
||||||
|
for case in payload["cases"]:
|
||||||
|
summary = case["summary"]
|
||||||
|
metrics = case["metrics"]
|
||||||
|
lines.append(
|
||||||
|
"| "
|
||||||
|
f"{case['num_nets']} | "
|
||||||
|
f"{case['seed']} | "
|
||||||
|
f"{case['repeat']} | "
|
||||||
|
f"{case['duration_s']:.4f} | "
|
||||||
|
f"{summary['valid_results']} | "
|
||||||
|
f"{summary['reached_targets']} | "
|
||||||
|
f"{metrics['pair_local_search_pairs_considered']} | "
|
||||||
|
f"{metrics['pair_local_search_accepts']} | "
|
||||||
|
f"{metrics['pair_local_search_nodes_expanded']} | "
|
||||||
|
f"{metrics['nodes_expanded']} | "
|
||||||
|
f"{metrics['congestion_check_calls']} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend(["", "## Recommendation", ""])
|
||||||
|
recommended = payload["recommended_smoke_scenario"]
|
||||||
|
if recommended is None:
|
||||||
|
lines.append(
|
||||||
|
"No smaller stable pair-local smoke scenario satisfied the rule "
|
||||||
|
"`valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"Recommended smoke scenario: `num_nets={recommended['num_nets']}`, `seed={recommended['seed']}`."
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Characterize pair-local search across example_07-style no-warm runs.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--num-nets",
|
||||||
|
default="6,8,10",
|
||||||
|
help="Comma-separated num_nets values to sweep. Default: 6,8,10.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--seeds",
|
||||||
|
default="41,42,43",
|
||||||
|
help="Comma-separated seed values to sweep. Default: 41,42,43.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repeats",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help="Number of repeated runs per (num_nets, seed). Default: 2.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help="Directory to write pair_local_characterization.json and .md into. Defaults to <repo>/docs.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
num_nets_values = _parse_csv_ints(args.num_nets)
|
||||||
|
seed_values = _parse_csv_ints(args.seeds)
|
||||||
|
|
||||||
|
cases: list[dict[str, object]] = []
|
||||||
|
for num_nets in num_nets_values:
|
||||||
|
for seed in seed_values:
|
||||||
|
for repeat in range(args.repeats):
|
||||||
|
case = _run_case(num_nets, seed)
|
||||||
|
case["num_nets"] = num_nets
|
||||||
|
case["seed"] = seed
|
||||||
|
case["repeat"] = repeat
|
||||||
|
cases.append(case)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||||
|
"generator": "scripts/characterize_pair_local_search.py",
|
||||||
|
"grid": {
|
||||||
|
"num_nets": list(num_nets_values),
|
||||||
|
"seeds": list(seed_values),
|
||||||
|
"repeats": args.repeats,
|
||||||
|
},
|
||||||
|
"cases": cases,
|
||||||
|
"recommended_smoke_scenario": _select_smoke_case(cases),
|
||||||
|
}
|
||||||
|
|
||||||
|
json_path = output_dir / "pair_local_characterization.json"
|
||||||
|
markdown_path = output_dir / "pair_local_characterization.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()
|
||||||
237
scripts/diff_performance_baseline.py
Normal file
237
scripts/diff_performance_baseline.py
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS
|
||||||
|
from inire.results import RouteMetrics
|
||||||
|
|
||||||
|
|
||||||
|
SUMMARY_KEYS = (
|
||||||
|
"duration_s",
|
||||||
|
"valid_results",
|
||||||
|
"reached_targets",
|
||||||
|
"route_iterations",
|
||||||
|
"nets_routed",
|
||||||
|
"nodes_expanded",
|
||||||
|
"ray_cast_calls",
|
||||||
|
"moves_generated",
|
||||||
|
"moves_added",
|
||||||
|
"congestion_check_calls",
|
||||||
|
"verify_path_report_calls",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]:
|
||||||
|
if not include_performance_only:
|
||||||
|
return SCENARIO_SNAPSHOTS
|
||||||
|
return SCENARIO_SNAPSHOTS + PERFORMANCE_SCENARIO_SNAPSHOTS
|
||||||
|
|
||||||
|
|
||||||
|
def _available_metric_names() -> tuple[str, ...]:
|
||||||
|
return (
|
||||||
|
"duration_s",
|
||||||
|
"total_results",
|
||||||
|
"valid_results",
|
||||||
|
"reached_targets",
|
||||||
|
*RouteMetrics.__dataclass_fields__.keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _current_snapshots(
|
||||||
|
selected_scenarios: tuple[str, ...] | None,
|
||||||
|
*,
|
||||||
|
include_performance_only: bool,
|
||||||
|
) -> dict[str, dict[str, object]]:
|
||||||
|
allowed = None if selected_scenarios is None else set(selected_scenarios)
|
||||||
|
snapshots: dict[str, dict[str, object]] = {}
|
||||||
|
for name, run in _snapshot_registry(include_performance_only):
|
||||||
|
if allowed is not None and name not in allowed:
|
||||||
|
continue
|
||||||
|
snapshots[name] = asdict(run())
|
||||||
|
return snapshots
|
||||||
|
|
||||||
|
|
||||||
|
def _load_baseline(path: Path, selected_scenarios: tuple[str, ...] | None) -> dict[str, dict[str, object]]:
|
||||||
|
payload = json.loads(path.read_text())
|
||||||
|
allowed = None if selected_scenarios is None else set(selected_scenarios)
|
||||||
|
return {
|
||||||
|
entry["name"]: entry
|
||||||
|
for entry in payload["scenarios"]
|
||||||
|
if allowed is None or entry["name"] in allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _metric_value(snapshot: dict[str, object], key: str) -> float | None:
|
||||||
|
if key in {"duration_s", "total_results", "valid_results", "reached_targets"}:
|
||||||
|
return float(snapshot[key])
|
||||||
|
if key not in snapshot["metrics"]:
|
||||||
|
return None
|
||||||
|
return float(snapshot["metrics"][key])
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_metrics(metric_names: tuple[str, ...]) -> None:
|
||||||
|
valid_names = set(_available_metric_names())
|
||||||
|
unknown = [name for name in metric_names if name not in valid_names]
|
||||||
|
if unknown:
|
||||||
|
raise SystemExit(
|
||||||
|
f"Unknown metric name(s): {', '.join(sorted(unknown))}. "
|
||||||
|
f"Valid names are: {', '.join(_available_metric_names())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_report(
|
||||||
|
baseline: dict[str, dict[str, object]],
|
||||||
|
current: dict[str, dict[str, object]],
|
||||||
|
metric_names: tuple[str, ...],
|
||||||
|
) -> str:
|
||||||
|
scenario_names = sorted(set(baseline) | set(current))
|
||||||
|
lines = [
|
||||||
|
"# Performance Baseline Diff",
|
||||||
|
"",
|
||||||
|
"| Scenario | Metric | Baseline | Current | Delta |",
|
||||||
|
"| :-- | :-- | --: | --: | --: |",
|
||||||
|
]
|
||||||
|
for scenario in scenario_names:
|
||||||
|
base_snapshot = baseline.get(scenario)
|
||||||
|
curr_snapshot = current.get(scenario)
|
||||||
|
if base_snapshot is None:
|
||||||
|
if curr_snapshot is None:
|
||||||
|
lines.append(f"| {scenario} | added | - | - | - |")
|
||||||
|
continue
|
||||||
|
for key in metric_names:
|
||||||
|
curr_value = _metric_value(curr_snapshot, key)
|
||||||
|
if curr_value is None:
|
||||||
|
lines.append(f"| {scenario} | {key} | - | - | - |")
|
||||||
|
continue
|
||||||
|
lines.append(f"| {scenario} | {key} | - | {curr_value:.4f} | - |")
|
||||||
|
continue
|
||||||
|
if curr_snapshot is None:
|
||||||
|
lines.append(f"| {scenario} | missing | - | - | - |")
|
||||||
|
continue
|
||||||
|
for key in metric_names:
|
||||||
|
base_value = _metric_value(base_snapshot, key)
|
||||||
|
curr_value = _metric_value(curr_snapshot, key)
|
||||||
|
if base_value is None:
|
||||||
|
lines.append(
|
||||||
|
f"| {scenario} | {key} | - | {curr_value:.4f} | - |"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if curr_value is None:
|
||||||
|
lines.append(
|
||||||
|
f"| {scenario} | {key} | {base_value:.4f} | - | - |"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
lines.append(
|
||||||
|
f"| {scenario} | {key} | {base_value:.4f} | {curr_value:.4f} | {curr_value - base_value:+.4f} |"
|
||||||
|
)
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_log_entry(
|
||||||
|
*,
|
||||||
|
baseline_path: Path,
|
||||||
|
label: str,
|
||||||
|
notes: tuple[str, ...],
|
||||||
|
report: str,
|
||||||
|
) -> str:
|
||||||
|
lines = [
|
||||||
|
f"## {label}",
|
||||||
|
"",
|
||||||
|
f"Measured on {datetime.now().astimezone().isoformat(timespec='seconds')}.",
|
||||||
|
f"Baseline: `{baseline_path}`.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
if notes:
|
||||||
|
lines.extend(["Findings:", ""])
|
||||||
|
lines.extend(f"- {note}" for note in notes)
|
||||||
|
lines.append("")
|
||||||
|
lines.append(report.rstrip())
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Diff the committed performance baseline against a fresh run.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--baseline",
|
||||||
|
type=Path,
|
||||||
|
default=Path("docs/performance_baseline.json"),
|
||||||
|
help="Baseline JSON to compare against.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help="Optional file to write the report to. Defaults to stdout.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--scenario",
|
||||||
|
action="append",
|
||||||
|
dest="scenarios",
|
||||||
|
default=[],
|
||||||
|
help="Optional scenario name to include. May be passed more than once.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--metric",
|
||||||
|
action="append",
|
||||||
|
dest="metrics",
|
||||||
|
default=[],
|
||||||
|
help="Optional metric to include. May be passed more than once. Defaults to the summary metric set.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--label",
|
||||||
|
default="Measurement",
|
||||||
|
help="Section label to use when appending to a log file.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--notes",
|
||||||
|
action="append",
|
||||||
|
dest="notes",
|
||||||
|
default=[],
|
||||||
|
help="Optional short finding to append under the measurement section. May be passed more than once.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help="Optional Markdown log file to append the rendered report to.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-performance-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Include performance-only snapshot scenarios that are excluded from the default baseline corpus.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
selected = tuple(args.scenarios) if args.scenarios else None
|
||||||
|
metrics = tuple(args.metrics) if args.metrics else SUMMARY_KEYS
|
||||||
|
_validate_metrics(metrics)
|
||||||
|
baseline = _load_baseline(args.baseline, selected)
|
||||||
|
current = _current_snapshots(selected, include_performance_only=args.include_performance_only)
|
||||||
|
report = _render_report(baseline, current, metrics)
|
||||||
|
|
||||||
|
if args.output is not None:
|
||||||
|
args.output.write_text(report)
|
||||||
|
print(f"Wrote {args.output}")
|
||||||
|
elif args.log is None:
|
||||||
|
print(report, end="")
|
||||||
|
|
||||||
|
if args.log is not None:
|
||||||
|
entry = _render_log_entry(
|
||||||
|
baseline_path=args.baseline,
|
||||||
|
label=args.label,
|
||||||
|
notes=tuple(args.notes),
|
||||||
|
report=report,
|
||||||
|
)
|
||||||
|
with args.log.open("a", encoding="utf-8") as handle:
|
||||||
|
handle.write(entry)
|
||||||
|
print(f"Appended {args.log}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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()
|
||||||
|
|
@ -7,7 +7,7 @@ from dataclasses import asdict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS
|
from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS
|
||||||
|
|
||||||
|
|
||||||
SUMMARY_METRICS = (
|
SUMMARY_METRICS = (
|
||||||
|
|
@ -24,10 +24,20 @@ SUMMARY_METRICS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_payload(selected_scenarios: tuple[str, ...] | None = None) -> dict[str, object]:
|
def _snapshot_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]:
|
||||||
|
if not include_performance_only:
|
||||||
|
return SCENARIO_SNAPSHOTS
|
||||||
|
return SCENARIO_SNAPSHOTS + PERFORMANCE_SCENARIO_SNAPSHOTS
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(
|
||||||
|
selected_scenarios: tuple[str, ...] | None = None,
|
||||||
|
*,
|
||||||
|
include_performance_only: bool = False,
|
||||||
|
) -> dict[str, object]:
|
||||||
allowed = None if selected_scenarios is None else set(selected_scenarios)
|
allowed = None if selected_scenarios is None else set(selected_scenarios)
|
||||||
snapshots = []
|
snapshots = []
|
||||||
for name, run in SCENARIO_SNAPSHOTS:
|
for name, run in _snapshot_registry(include_performance_only):
|
||||||
if allowed is not None and name not in allowed:
|
if allowed is not None and name not in allowed:
|
||||||
continue
|
continue
|
||||||
snapshots.append(run())
|
snapshots.append(run())
|
||||||
|
|
@ -46,6 +56,7 @@ def _render_markdown(payload: dict[str, object]) -> str:
|
||||||
f"Generated on {payload['generated_on']} by `{payload['generator']}`.",
|
f"Generated on {payload['generated_on']} by `{payload['generator']}`.",
|
||||||
"",
|
"",
|
||||||
"The full machine-readable snapshot lives in `docs/performance_baseline.json`.",
|
"The full machine-readable snapshot lives in `docs/performance_baseline.json`.",
|
||||||
|
"Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.",
|
||||||
"",
|
"",
|
||||||
"| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |",
|
"| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |",
|
||||||
"| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |",
|
"| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |",
|
||||||
|
|
@ -77,6 +88,7 @@ def _render_markdown(payload: dict[str, object]) -> str:
|
||||||
"## Full Counter Set",
|
"## Full Counter Set",
|
||||||
"",
|
"",
|
||||||
"Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.",
|
"Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.",
|
||||||
|
"These counters are currently observational only and are not enforced as CI regression gates.",
|
||||||
"",
|
"",
|
||||||
"Tracked metric keys:",
|
"Tracked metric keys:",
|
||||||
"",
|
"",
|
||||||
|
|
@ -101,6 +113,11 @@ def main() -> None:
|
||||||
default=[],
|
default=[],
|
||||||
help="Optional scenario name to include. May be passed more than once.",
|
help="Optional scenario name to include. May be passed more than once.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-performance-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Include performance-only snapshot scenarios that are excluded from the default baseline corpus.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = Path(__file__).resolve().parents[1]
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
|
@ -108,7 +125,7 @@ def main() -> None:
|
||||||
docs_dir.mkdir(exist_ok=True)
|
docs_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
selected = tuple(args.scenarios) if args.scenarios else None
|
selected = tuple(args.scenarios) if args.scenarios else None
|
||||||
payload = _build_payload(selected)
|
payload = _build_payload(selected, include_performance_only=args.include_performance_only)
|
||||||
json_path = docs_dir / "performance_baseline.json"
|
json_path = docs_dir / "performance_baseline.json"
|
||||||
markdown_path = docs_dir / "performance.md"
|
markdown_path = docs_dir / "performance.md"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue