various performance work (wip)
This commit is contained in:
parent
e77fd6e69f
commit
71e263c527
25 changed files with 4071 additions and 326 deletions
24
DOCS.md
24
DOCS.md
|
|
@ -148,6 +148,11 @@ 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.
|
||||
- `refine_path_calls`: Number of completed paths passed through the post-route refiner.
|
||||
- `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
|
||||
|
||||
|
|
@ -165,20 +170,33 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are
|
|||
- `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_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds.
|
||||
- `visibility_builds`: Number of static visibility-graph rebuilds.
|
||||
- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building that graph.
|
||||
- `visibility_corner_queries` / `visibility_corner_hits`: Precomputed-corner visibility query activity.
|
||||
- `visibility_corner_index_builds`: Number of lazy corner-index rebuilds.
|
||||
- `visibility_builds`: Number of exact corner-visibility graph rebuilds.
|
||||
- `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.
|
||||
- `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_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_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.
|
||||
|
||||
### Verification Counters
|
||||
|
||||
- `verify_path_report_calls`: Number of full path-verification passes.
|
||||
- `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.
|
||||
|
||||
## 8. Internal Modules
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
- `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers.
|
||||
- `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/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/_router.py`: The negotiated-congestion driver and refinement orchestration.
|
||||
- `inire/router/refiner.py`: Post-route path simplification for completed paths.
|
||||
|
|
@ -39,7 +39,9 @@ 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning.
|
||||
|
||||
## Performance Visibility
|
||||
|
|
|
|||
1653
docs/optimization_pass_01_log.md
Normal file
1653
docs/optimization_pass_01_log.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,21 +1,21 @@
|
|||
# Performance Baseline
|
||||
|
||||
Generated on 2026-03-31 by `scripts/record_performance_baseline.py`.
|
||||
Generated on 2026-04-01 by `scripts/record_performance_baseline.py`.
|
||||
|
||||
The full machine-readable snapshot lives in `docs/performance_baseline.json`.
|
||||
Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.
|
||||
|
||||
| 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_02_congestion_resolution | 0.3418 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 |
|
||||
| example_03_locked_paths | 0.1827 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 |
|
||||
| example_04_sbends_and_radii | 1.9938 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 |
|
||||
| example_05_orientation_stress | 0.2458 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 |
|
||||
| example_06_bend_collision_models | 4.1186 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 |
|
||||
| example_07_large_scale_routing | 1.3734 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 |
|
||||
| example_08_custom_bend_geometry | 0.2410 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 |
|
||||
| example_09_unroutable_best_effort | 0.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 |
|
||||
| example_01_simple_route | 0.0036 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 3 |
|
||||
| example_02_congestion_resolution | 0.3297 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 35 |
|
||||
| example_03_locked_paths | 0.1832 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 14 |
|
||||
| example_04_sbends_and_radii | 0.0260 | 2 | 2 | 2 | 1 | 2 | 15 | 70 | 123 | 65 | 0 | 0 | 0 | 6 |
|
||||
| example_05_orientation_stress | 0.2348 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 0 | 0 | 155 | 15 |
|
||||
| example_06_bend_collision_models | 0.1953 | 3 | 3 | 3 | 3 | 3 | 240 | 682 | 1026 | 629 | 0 | 0 | 0 | 9 |
|
||||
| example_07_large_scale_routing | 0.1945 | 10 | 10 | 10 | 1 | 10 | 78 | 383 | 372 | 227 | 0 | 0 | 0 | 30 |
|
||||
| example_08_custom_bend_geometry | 0.0177 | 2 | 2 | 2 | 2 | 2 | 18 | 56 | 78 | 56 | 0 | 0 | 0 | 6 |
|
||||
| example_09_unroutable_best_effort | 0.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
|
||||
|
||||
## Full Counter Set
|
||||
|
||||
|
|
@ -24,4 +24,4 @@ These counters are currently observational only and are not enforced as CI regre
|
|||
|
||||
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, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, 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_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_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted
|
||||
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, 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
|
||||
|
|
|
|||
|
|
@ -1,30 +1,51 @@
|
|||
{
|
||||
"generated_on": "2026-03-31",
|
||||
"generated_on": "2026-04-01",
|
||||
"generator": "scripts/record_performance_baseline.py",
|
||||
"scenarios": [
|
||||
{
|
||||
"duration_s": 0.00415895797777921,
|
||||
"duration_s": 0.0035884700482711196,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"danger_map_cache_hits": 8,
|
||||
"danger_map_cache_misses": 13,
|
||||
"danger_map_lookup_calls": 21,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 27079,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 2,
|
||||
"dynamic_path_objects_removed": 1,
|
||||
"dynamic_tree_rebuilds": 2,
|
||||
"dynamic_path_objects_added": 3,
|
||||
"dynamic_path_objects_removed": 2,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 1,
|
||||
"iteration_reverify_calls": 1,
|
||||
"move_cache_abs_hits": 1,
|
||||
"move_cache_abs_misses": 10,
|
||||
"move_cache_rel_hits": 0,
|
||||
"move_cache_rel_misses": 10,
|
||||
"moves_added": 7,
|
||||
"moves_generated": 11,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 1,
|
||||
"nets_routed": 1,
|
||||
"nodes_expanded": 2,
|
||||
|
|
@ -32,15 +53,15 @@
|
|||
"pruned_closed_set": 0,
|
||||
"pruned_cost": 4,
|
||||
"pruned_hard_collision": 0,
|
||||
"ray_cast_calls": 22,
|
||||
"ray_cast_calls": 10,
|
||||
"ray_cast_calls_expand_forward": 1,
|
||||
"ray_cast_calls_expand_snap": 1,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 8,
|
||||
"ray_cast_calls_visibility_build": 12,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 0,
|
||||
"ray_cast_candidate_bounds": 12,
|
||||
"ray_cast_candidate_bounds": 0,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 1,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
|
|
@ -52,18 +73,20 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 11,
|
||||
"score_component_total_ns": 59404,
|
||||
"score_component_total_ns": 16010,
|
||||
"static_net_tree_rebuilds": 1,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 1,
|
||||
"static_tree_rebuilds": 1,
|
||||
"static_tree_rebuilds": 0,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 3,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 2,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 12,
|
||||
"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,
|
||||
|
|
@ -80,28 +103,49 @@
|
|||
"valid_results": 1
|
||||
},
|
||||
{
|
||||
"duration_s": 0.34182924893684685,
|
||||
"duration_s": 0.32969290704932064,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"danger_map_cache_hits": 1433,
|
||||
"danger_map_cache_misses": 775,
|
||||
"danger_map_lookup_calls": 2208,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 2165333,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 32,
|
||||
"dynamic_path_objects_removed": 17,
|
||||
"dynamic_tree_rebuilds": 8,
|
||||
"dynamic_path_objects_added": 49,
|
||||
"dynamic_path_objects_removed": 34,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 3,
|
||||
"iteration_reverify_calls": 1,
|
||||
"move_cache_abs_hits": 12,
|
||||
"move_cache_abs_misses": 1401,
|
||||
"move_cache_rel_hits": 1293,
|
||||
"move_cache_rel_misses": 108,
|
||||
"moves_added": 668,
|
||||
"moves_generated": 1413,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 3,
|
||||
"nets_routed": 3,
|
||||
"nodes_expanded": 366,
|
||||
|
|
@ -109,15 +153,15 @@
|
|||
"pruned_closed_set": 157,
|
||||
"pruned_cost": 208,
|
||||
"pruned_hard_collision": 380,
|
||||
"ray_cast_calls": 1176,
|
||||
"ray_cast_calls": 1164,
|
||||
"ray_cast_calls_expand_forward": 363,
|
||||
"ray_cast_calls_expand_snap": 19,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 529,
|
||||
"ray_cast_calls_visibility_build": 12,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 253,
|
||||
"ray_cast_candidate_bounds": 925,
|
||||
"ray_cast_candidate_bounds": 913,
|
||||
"ray_cast_exact_geometry_checks": 136,
|
||||
"refine_path_calls": 3,
|
||||
"refinement_candidate_side_extents": 26,
|
||||
|
|
@ -129,23 +173,25 @@
|
|||
"refinement_windows_considered": 10,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 976,
|
||||
"score_component_total_ns": 4650167,
|
||||
"score_component_total_ns": 1091130,
|
||||
"static_net_tree_rebuilds": 3,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 1,
|
||||
"static_tree_rebuilds": 2,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_exact_pair_checks": 90,
|
||||
"verify_dynamic_candidate_nets": 84,
|
||||
"verify_dynamic_exact_pair_checks": 82,
|
||||
"verify_path_report_calls": 35,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 4,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 12,
|
||||
"visibility_corner_index_builds": 3,
|
||||
"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": 18991,
|
||||
"visibility_tangent_candidate_corner_checks": 873,
|
||||
"visibility_tangent_candidate_ray_tests": 253,
|
||||
"visibility_tangent_candidate_scans": 363,
|
||||
"warm_start_paths_built": 3,
|
||||
|
|
@ -157,28 +203,49 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 0.18274989898782223,
|
||||
"duration_s": 0.18321374501101673,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"danger_map_cache_hits": 624,
|
||||
"danger_map_cache_misses": 414,
|
||||
"danger_map_lookup_calls": 1038,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 1001517,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 17,
|
||||
"dynamic_path_objects_removed": 10,
|
||||
"dynamic_tree_rebuilds": 5,
|
||||
"dynamic_path_objects_added": 27,
|
||||
"dynamic_path_objects_removed": 20,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 2,
|
||||
"iteration_reverify_calls": 2,
|
||||
"move_cache_abs_hits": 1,
|
||||
"move_cache_abs_misses": 903,
|
||||
"move_cache_rel_hits": 821,
|
||||
"move_cache_rel_misses": 82,
|
||||
"moves_added": 307,
|
||||
"moves_generated": 904,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 191,
|
||||
|
|
@ -186,15 +253,15 @@
|
|||
"pruned_closed_set": 97,
|
||||
"pruned_cost": 140,
|
||||
"pruned_hard_collision": 181,
|
||||
"ray_cast_calls": 681,
|
||||
"ray_cast_calls": 657,
|
||||
"ray_cast_calls_expand_forward": 189,
|
||||
"ray_cast_calls_expand_snap": 8,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 407,
|
||||
"ray_cast_calls_visibility_build": 24,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 53,
|
||||
"ray_cast_candidate_bounds": 179,
|
||||
"ray_cast_candidate_bounds": 155,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 2,
|
||||
"refinement_candidate_side_extents": 8,
|
||||
|
|
@ -206,23 +273,25 @@
|
|||
"refinement_windows_considered": 2,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 504,
|
||||
"score_component_total_ns": 2184569,
|
||||
"score_component_total_ns": 556716,
|
||||
"static_net_tree_rebuilds": 2,
|
||||
"static_raw_tree_rebuilds": 1,
|
||||
"static_safe_cache_hits": 1,
|
||||
"static_tree_rebuilds": 2,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_dynamic_candidate_nets": 8,
|
||||
"verify_dynamic_exact_pair_checks": 8,
|
||||
"verify_path_report_calls": 14,
|
||||
"verify_static_buffer_ops": 69,
|
||||
"visibility_builds": 4,
|
||||
"verify_static_buffer_ops": 72,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 24,
|
||||
"visibility_corner_index_builds": 2,
|
||||
"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": 476,
|
||||
"visibility_tangent_candidate_corner_checks": 56,
|
||||
"visibility_tangent_candidate_ray_tests": 53,
|
||||
"visibility_tangent_candidate_scans": 189,
|
||||
"warm_start_paths_built": 2,
|
||||
|
|
@ -234,28 +303,49 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 1.993830946041271,
|
||||
"duration_s": 0.026024609920568764,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"danger_map_cache_hits": 75,
|
||||
"danger_map_cache_misses": 120,
|
||||
"danger_map_lookup_calls": 195,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 207556,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 14,
|
||||
"dynamic_path_objects_removed": 7,
|
||||
"dynamic_tree_rebuilds": 4,
|
||||
"dynamic_path_objects_added": 21,
|
||||
"dynamic_path_objects_removed": 14,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 2,
|
||||
"iteration_reverify_calls": 1,
|
||||
"move_cache_abs_hits": 1,
|
||||
"move_cache_abs_misses": 122,
|
||||
"move_cache_rel_hits": 80,
|
||||
"move_cache_rel_misses": 42,
|
||||
"moves_added": 65,
|
||||
"moves_generated": 123,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 15,
|
||||
|
|
@ -263,16 +353,16 @@
|
|||
"pruned_closed_set": 2,
|
||||
"pruned_cost": 25,
|
||||
"pruned_hard_collision": 16,
|
||||
"ray_cast_calls": 18218,
|
||||
"ray_cast_calls": 70,
|
||||
"ray_cast_calls_expand_forward": 13,
|
||||
"ray_cast_calls_expand_snap": 1,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 56,
|
||||
"ray_cast_calls_visibility_build": 18148,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 0,
|
||||
"ray_cast_candidate_bounds": 50717,
|
||||
"ray_cast_exact_geometry_checks": 21265,
|
||||
"ray_cast_candidate_bounds": 4,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 2,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
"refinement_candidates_accepted": 0,
|
||||
|
|
@ -283,23 +373,25 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 90,
|
||||
"score_component_total_ns": 410130,
|
||||
"score_component_total_ns": 97738,
|
||||
"static_net_tree_rebuilds": 2,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 1,
|
||||
"static_tree_rebuilds": 2,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 6,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 6,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 3,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 18148,
|
||||
"visibility_corner_index_builds": 2,
|
||||
"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": 394,
|
||||
"visibility_tangent_candidate_corner_checks": 50,
|
||||
"visibility_tangent_candidate_ray_tests": 0,
|
||||
"visibility_tangent_candidate_scans": 13,
|
||||
"warm_start_paths_built": 2,
|
||||
|
|
@ -311,28 +403,49 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 0.24581307696644217,
|
||||
"duration_s": 0.23484283208381385,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 2,
|
||||
"congestion_cache_misses": 412,
|
||||
"congestion_check_calls": 412,
|
||||
"congestion_exact_pair_checks": 66,
|
||||
"danger_map_cache_hits": 1386,
|
||||
"danger_map_cache_misses": 693,
|
||||
"danger_map_lookup_calls": 2079,
|
||||
"congestion_cache_misses": 155,
|
||||
"congestion_candidate_ids": 19,
|
||||
"congestion_candidate_nets": 15,
|
||||
"congestion_candidate_precheck_hits": 135,
|
||||
"congestion_candidate_precheck_misses": 22,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 155,
|
||||
"congestion_exact_pair_checks": 18,
|
||||
"congestion_grid_net_cache_hits": 12,
|
||||
"congestion_grid_net_cache_misses": 25,
|
||||
"congestion_grid_span_cache_hits": 11,
|
||||
"congestion_grid_span_cache_misses": 4,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 134,
|
||||
"congestion_net_envelope_cache_misses": 43,
|
||||
"congestion_presence_cache_hits": 185,
|
||||
"congestion_presence_cache_misses": 30,
|
||||
"congestion_presence_skips": 58,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 1805113,
|
||||
"dynamic_grid_rebuilds": 3,
|
||||
"dynamic_path_objects_added": 37,
|
||||
"dynamic_path_objects_removed": 25,
|
||||
"dynamic_tree_rebuilds": 12,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 49,
|
||||
"dynamic_path_objects_removed": 37,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 1,
|
||||
"iteration_conflicting_nets": 2,
|
||||
"iteration_reverified_nets": 6,
|
||||
"iteration_reverify_calls": 2,
|
||||
"move_cache_abs_hits": 253,
|
||||
"move_cache_abs_misses": 1371,
|
||||
"move_cache_rel_hits": 1269,
|
||||
"move_cache_rel_misses": 102,
|
||||
"moves_added": 681,
|
||||
"moves_generated": 1624,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 6,
|
||||
"nets_routed": 6,
|
||||
"nodes_expanded": 286,
|
||||
|
|
@ -360,23 +473,25 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 1198,
|
||||
"score_component_total_ns": 4292875,
|
||||
"score_component_total_ns": 1194981,
|
||||
"static_net_tree_rebuilds": 3,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 3,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_exact_pair_checks": 2,
|
||||
"verify_path_report_calls": 12,
|
||||
"verify_dynamic_candidate_nets": 8,
|
||||
"verify_dynamic_exact_pair_checks": 12,
|
||||
"verify_path_report_calls": 15,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 3,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_index_builds": 3,
|
||||
"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": 1483,
|
||||
"visibility_tangent_candidate_corner_checks": 70,
|
||||
"visibility_tangent_candidate_ray_tests": 9,
|
||||
"visibility_tangent_candidate_scans": 280,
|
||||
"warm_start_paths_built": 2,
|
||||
|
|
@ -388,28 +503,49 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 4.1186372829834,
|
||||
"duration_s": 0.19533946400042623,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 1183,
|
||||
"danger_map_cache_misses": 731,
|
||||
"danger_map_lookup_calls": 1914,
|
||||
"danger_map_query_calls": 731,
|
||||
"danger_map_total_ns": 18374289,
|
||||
"danger_map_total_ns": 18697751,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 36,
|
||||
"dynamic_path_objects_removed": 18,
|
||||
"dynamic_tree_rebuilds": 6,
|
||||
"dynamic_path_objects_added": 54,
|
||||
"dynamic_path_objects_removed": 36,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 18,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 3,
|
||||
"iteration_reverify_calls": 3,
|
||||
"move_cache_abs_hits": 186,
|
||||
"move_cache_abs_misses": 840,
|
||||
"move_cache_rel_hits": 702,
|
||||
"move_cache_rel_misses": 138,
|
||||
"moves_added": 629,
|
||||
"moves_generated": 1026,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 3,
|
||||
"nets_routed": 3,
|
||||
"nodes_expanded": 240,
|
||||
|
|
@ -417,16 +553,16 @@
|
|||
"pruned_closed_set": 108,
|
||||
"pruned_cost": 204,
|
||||
"pruned_hard_collision": 85,
|
||||
"ray_cast_calls": 40530,
|
||||
"ray_cast_calls": 682,
|
||||
"ray_cast_calls_expand_forward": 237,
|
||||
"ray_cast_calls_expand_snap": 3,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 408,
|
||||
"ray_cast_calls_visibility_build": 39848,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 34,
|
||||
"ray_cast_candidate_bounds": 121732,
|
||||
"ray_cast_exact_geometry_checks": 36858,
|
||||
"ray_cast_candidate_bounds": 97,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 3,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
"refinement_candidates_accepted": 0,
|
||||
|
|
@ -437,23 +573,25 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 3,
|
||||
"score_component_calls": 842,
|
||||
"score_component_total_ns": 20652599,
|
||||
"score_component_total_ns": 21016472,
|
||||
"static_net_tree_rebuilds": 3,
|
||||
"static_raw_tree_rebuilds": 3,
|
||||
"static_safe_cache_hits": 141,
|
||||
"static_tree_rebuilds": 6,
|
||||
"static_tree_rebuilds": 3,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 9,
|
||||
"verify_static_buffer_ops": 54,
|
||||
"visibility_builds": 6,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 39848,
|
||||
"visibility_corner_index_builds": 3,
|
||||
"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": 2400,
|
||||
"visibility_tangent_candidate_corner_checks": 84,
|
||||
"visibility_tangent_candidate_ray_tests": 34,
|
||||
"visibility_tangent_candidate_scans": 237,
|
||||
"warm_start_paths_built": 3,
|
||||
|
|
@ -465,28 +603,49 @@
|
|||
"valid_results": 3
|
||||
},
|
||||
{
|
||||
"duration_s": 1.373430646955967,
|
||||
"duration_s": 0.19448363897390664,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 233,
|
||||
"danger_map_cache_misses": 448,
|
||||
"danger_map_lookup_calls": 681,
|
||||
"danger_map_query_calls": 448,
|
||||
"danger_map_total_ns": 10728422,
|
||||
"danger_map_total_ns": 10973251,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 88,
|
||||
"dynamic_path_objects_removed": 44,
|
||||
"dynamic_tree_rebuilds": 20,
|
||||
"dynamic_path_objects_added": 132,
|
||||
"dynamic_path_objects_removed": 88,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 10,
|
||||
"iteration_reverify_calls": 1,
|
||||
"move_cache_abs_hits": 6,
|
||||
"move_cache_abs_misses": 366,
|
||||
"move_cache_rel_hits": 275,
|
||||
"move_cache_rel_misses": 91,
|
||||
"moves_added": 227,
|
||||
"moves_generated": 372,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 10,
|
||||
"nets_routed": 10,
|
||||
"nodes_expanded": 78,
|
||||
|
|
@ -494,16 +653,16 @@
|
|||
"pruned_closed_set": 20,
|
||||
"pruned_cost": 64,
|
||||
"pruned_hard_collision": 61,
|
||||
"ray_cast_calls": 11151,
|
||||
"ray_cast_calls": 383,
|
||||
"ray_cast_calls_expand_forward": 68,
|
||||
"ray_cast_calls_expand_snap": 6,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 232,
|
||||
"ray_cast_calls_visibility_build": 10768,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 77,
|
||||
"ray_cast_candidate_bounds": 21198,
|
||||
"ray_cast_exact_geometry_checks": 11651,
|
||||
"ray_cast_candidate_bounds": 683,
|
||||
"ray_cast_exact_geometry_checks": 150,
|
||||
"refine_path_calls": 10,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
"refinement_candidates_accepted": 0,
|
||||
|
|
@ -514,23 +673,25 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 291,
|
||||
"score_component_total_ns": 11574800,
|
||||
"score_component_total_ns": 11824081,
|
||||
"static_net_tree_rebuilds": 10,
|
||||
"static_raw_tree_rebuilds": 1,
|
||||
"static_safe_cache_hits": 6,
|
||||
"static_tree_rebuilds": 10,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_dynamic_candidate_nets": 264,
|
||||
"verify_dynamic_exact_pair_checks": 40,
|
||||
"verify_path_report_calls": 30,
|
||||
"verify_static_buffer_ops": 132,
|
||||
"visibility_builds": 11,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 10768,
|
||||
"visibility_corner_index_builds": 10,
|
||||
"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": 34735,
|
||||
"visibility_tangent_candidate_corner_checks": 321,
|
||||
"visibility_tangent_candidate_ray_tests": 77,
|
||||
"visibility_tangent_candidate_scans": 68,
|
||||
"warm_start_paths_built": 10,
|
||||
|
|
@ -542,28 +703,49 @@
|
|||
"valid_results": 10
|
||||
},
|
||||
{
|
||||
"duration_s": 0.2410298540489748,
|
||||
"duration_s": 0.017700672964565456,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"danger_map_cache_hits": 58,
|
||||
"danger_map_cache_misses": 110,
|
||||
"danger_map_lookup_calls": 168,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 0,
|
||||
"danger_map_cache_misses": 0,
|
||||
"danger_map_lookup_calls": 0,
|
||||
"danger_map_query_calls": 0,
|
||||
"danger_map_total_ns": 178104,
|
||||
"danger_map_total_ns": 0,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 12,
|
||||
"dynamic_path_objects_removed": 6,
|
||||
"dynamic_tree_rebuilds": 4,
|
||||
"dynamic_path_objects_added": 18,
|
||||
"dynamic_path_objects_removed": 12,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 2,
|
||||
"iteration_reverify_calls": 2,
|
||||
"move_cache_abs_hits": 2,
|
||||
"move_cache_abs_misses": 76,
|
||||
"move_cache_rel_hits": 32,
|
||||
"move_cache_rel_misses": 44,
|
||||
"moves_added": 56,
|
||||
"moves_generated": 78,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 2,
|
||||
"nets_routed": 2,
|
||||
"nodes_expanded": 18,
|
||||
|
|
@ -571,16 +753,16 @@
|
|||
"pruned_closed_set": 6,
|
||||
"pruned_cost": 16,
|
||||
"pruned_hard_collision": 0,
|
||||
"ray_cast_calls": 2308,
|
||||
"ray_cast_calls": 56,
|
||||
"ray_cast_calls_expand_forward": 16,
|
||||
"ray_cast_calls_expand_snap": 2,
|
||||
"ray_cast_calls_other": 0,
|
||||
"ray_cast_calls_straight_static": 38,
|
||||
"ray_cast_calls_visibility_build": 2252,
|
||||
"ray_cast_calls_visibility_build": 0,
|
||||
"ray_cast_calls_visibility_query": 0,
|
||||
"ray_cast_calls_visibility_tangent": 0,
|
||||
"ray_cast_candidate_bounds": 3802,
|
||||
"ray_cast_exact_geometry_checks": 1904,
|
||||
"ray_cast_candidate_bounds": 0,
|
||||
"ray_cast_exact_geometry_checks": 0,
|
||||
"refine_path_calls": 2,
|
||||
"refinement_candidate_side_extents": 0,
|
||||
"refinement_candidates_accepted": 0,
|
||||
|
|
@ -591,18 +773,20 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 2,
|
||||
"score_component_calls": 72,
|
||||
"score_component_total_ns": 352865,
|
||||
"score_component_total_ns": 85969,
|
||||
"static_net_tree_rebuilds": 2,
|
||||
"static_raw_tree_rebuilds": 0,
|
||||
"static_safe_cache_hits": 2,
|
||||
"static_tree_rebuilds": 2,
|
||||
"static_tree_rebuilds": 0,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 6,
|
||||
"verify_static_buffer_ops": 0,
|
||||
"visibility_builds": 4,
|
||||
"visibility_builds": 0,
|
||||
"visibility_corner_hits_exact": 0,
|
||||
"visibility_corner_pairs_checked": 2252,
|
||||
"visibility_corner_index_builds": 2,
|
||||
"visibility_corner_pairs_checked": 0,
|
||||
"visibility_corner_queries_exact": 0,
|
||||
"visibility_point_cache_hits": 0,
|
||||
"visibility_point_cache_misses": 0,
|
||||
|
|
@ -619,28 +803,49 @@
|
|||
"valid_results": 2
|
||||
},
|
||||
{
|
||||
"duration_s": 0.0052388140466064215,
|
||||
"duration_s": 0.005781985004432499,
|
||||
"metrics": {
|
||||
"congestion_cache_hits": 0,
|
||||
"congestion_cache_misses": 0,
|
||||
"congestion_candidate_ids": 0,
|
||||
"congestion_candidate_nets": 0,
|
||||
"congestion_candidate_precheck_hits": 0,
|
||||
"congestion_candidate_precheck_misses": 0,
|
||||
"congestion_candidate_precheck_skips": 0,
|
||||
"congestion_check_calls": 0,
|
||||
"congestion_exact_pair_checks": 0,
|
||||
"congestion_grid_net_cache_hits": 0,
|
||||
"congestion_grid_net_cache_misses": 0,
|
||||
"congestion_grid_span_cache_hits": 0,
|
||||
"congestion_grid_span_cache_misses": 0,
|
||||
"congestion_lazy_requeues": 0,
|
||||
"congestion_lazy_resolutions": 0,
|
||||
"congestion_net_envelope_cache_hits": 0,
|
||||
"congestion_net_envelope_cache_misses": 0,
|
||||
"congestion_presence_cache_hits": 0,
|
||||
"congestion_presence_cache_misses": 0,
|
||||
"congestion_presence_skips": 0,
|
||||
"danger_map_cache_hits": 10,
|
||||
"danger_map_cache_misses": 20,
|
||||
"danger_map_lookup_calls": 30,
|
||||
"danger_map_query_calls": 20,
|
||||
"danger_map_total_ns": 502052,
|
||||
"danger_map_total_ns": 536009,
|
||||
"dynamic_grid_rebuilds": 0,
|
||||
"dynamic_path_objects_added": 1,
|
||||
"dynamic_path_objects_removed": 0,
|
||||
"dynamic_tree_rebuilds": 1,
|
||||
"dynamic_path_objects_added": 2,
|
||||
"dynamic_path_objects_removed": 1,
|
||||
"dynamic_tree_rebuilds": 0,
|
||||
"hard_collision_cache_hits": 0,
|
||||
"iteration_conflict_edges": 0,
|
||||
"iteration_conflicting_nets": 0,
|
||||
"iteration_reverified_nets": 0,
|
||||
"iteration_reverify_calls": 1,
|
||||
"move_cache_abs_hits": 0,
|
||||
"move_cache_abs_misses": 16,
|
||||
"move_cache_rel_hits": 2,
|
||||
"move_cache_rel_misses": 14,
|
||||
"moves_added": 10,
|
||||
"moves_generated": 16,
|
||||
"nets_carried_forward": 0,
|
||||
"nets_reached_target": 0,
|
||||
"nets_routed": 1,
|
||||
"nodes_expanded": 3,
|
||||
|
|
@ -668,23 +873,25 @@
|
|||
"refinement_windows_considered": 0,
|
||||
"route_iterations": 1,
|
||||
"score_component_calls": 14,
|
||||
"score_component_total_ns": 538947,
|
||||
"score_component_total_ns": 574907,
|
||||
"static_net_tree_rebuilds": 1,
|
||||
"static_raw_tree_rebuilds": 1,
|
||||
"static_safe_cache_hits": 0,
|
||||
"static_tree_rebuilds": 0,
|
||||
"static_tree_rebuilds": 1,
|
||||
"timeout_events": 0,
|
||||
"verify_dynamic_candidate_nets": 0,
|
||||
"verify_dynamic_exact_pair_checks": 0,
|
||||
"verify_path_report_calls": 1,
|
||||
"verify_static_buffer_ops": 1,
|
||||
"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": 10,
|
||||
"visibility_tangent_candidate_corner_checks": 0,
|
||||
"visibility_tangent_candidate_ray_tests": 0,
|
||||
"visibility_tangent_candidate_scans": 3,
|
||||
"warm_start_paths_built": 0,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy
|
||||
|
|
@ -28,6 +29,49 @@ 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))
|
||||
|
||||
|
||||
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, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DynamicCongestionDetail:
|
||||
soft_overlap_count: int = 0
|
||||
hits_frozen_net: bool = False
|
||||
|
||||
|
||||
class RoutingWorld:
|
||||
"""
|
||||
Internal spatial state for collision detection, congestion, and verification.
|
||||
|
|
@ -102,6 +146,9 @@ class RoutingWorld:
|
|||
def remove_path(self, net_id: str) -> None:
|
||||
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:
|
||||
reach = self.ray_cast(
|
||||
start_port,
|
||||
|
|
@ -235,105 +282,383 @@ class RoutingWorld:
|
|||
|
||||
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
|
||||
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
|
||||
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
|
||||
for other_net_id in unique_other_nets:
|
||||
other_mask = hit_net_ids == other_net_id
|
||||
sub_tree_indices = tree_indices[other_mask]
|
||||
sub_res_indices = res_indices[other_mask]
|
||||
|
||||
for other_net_id, other_obj_ids in candidates_by_net.items():
|
||||
found_real = False
|
||||
for index in range(len(sub_tree_indices)):
|
||||
if self.metrics is not None:
|
||||
self.metrics.total_congestion_exact_pair_checks += 1
|
||||
test_geometry = geometries_to_test[sub_res_indices[index]]
|
||||
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
|
||||
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:
|
||||
self.metrics.total_congestion_exact_pair_checks += 1
|
||||
if _has_non_touching_overlap(test_geometry, tree_geometry):
|
||||
found_real = True
|
||||
break
|
||||
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
|
||||
|
||||
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:
|
||||
self.metrics.total_congestion_check_calls += 1
|
||||
dynamic_paths = self._dynamic_paths
|
||||
if not dynamic_paths.geometries:
|
||||
return 0
|
||||
return DynamicCongestionDetail()
|
||||
|
||||
total_bounds = result.total_dilated_bounds
|
||||
self._ensure_dynamic_grid()
|
||||
dynamic_grid = dynamic_paths.grid
|
||||
if not dynamic_grid:
|
||||
return 0
|
||||
candidates_by_net = self._collect_congestion_candidates(
|
||||
result,
|
||||
net_id,
|
||||
net_envelope_cache,
|
||||
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)
|
||||
|
||||
if gx_min == gx_max and gy_min == gy_max:
|
||||
cell = (gx_min, gy_min)
|
||||
if cell in dynamic_grid:
|
||||
for obj_id in dynamic_grid[cell]:
|
||||
if dynamic_paths.geometries[obj_id][0] != net_id:
|
||||
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:
|
||||
def verify_path_details(self, net_id: str, components: Sequence[ComponentResult]) -> PathVerificationDetail:
|
||||
if self.metrics is not None:
|
||||
self.metrics.total_verify_path_report_calls += 1
|
||||
static_collision_count = 0
|
||||
dynamic_collision_count = 0
|
||||
self_collision_count = 0
|
||||
total_length = sum(component.length for component in components)
|
||||
conflicting_net_ids: set[str] = set()
|
||||
|
||||
static_obstacles = self._static_obstacles
|
||||
dynamic_paths = self._dynamic_paths
|
||||
|
|
@ -356,43 +681,45 @@ class RoutingWorld:
|
|||
if not self._is_in_safety_zone(polygon, obj_id, None, None):
|
||||
static_collision_count += 1
|
||||
|
||||
self._ensure_dynamic_tree()
|
||||
if dynamic_paths.tree is not None:
|
||||
tree_geometries = dynamic_paths.tree.geometries
|
||||
if dynamic_paths.dilated:
|
||||
for component in components:
|
||||
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 = []
|
||||
for index in range(len(tree_indices)):
|
||||
if hit_net_ids[index] == str(net_id):
|
||||
continue
|
||||
|
||||
if self.metrics is not None:
|
||||
self.metrics.total_verify_dynamic_exact_pair_checks += 1
|
||||
new_geometry = test_geometries[res_indices[index]]
|
||||
tree_geometry = tree_geometries[tree_indices[index]]
|
||||
if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7:
|
||||
component_hits.append(hit_net_ids[index])
|
||||
for new_geometry in test_geometries:
|
||||
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
|
||||
if self.metrics is not None:
|
||||
self.metrics.total_verify_dynamic_exact_pair_checks += 1
|
||||
tree_geometry = dynamic_paths.dilated[obj_id]
|
||||
if _has_non_touching_overlap(new_geometry, tree_geometry):
|
||||
component_hits.append(hit_net_id)
|
||||
break
|
||||
|
||||
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 other_index in range(index + 2, len(components)):
|
||||
if components_overlap(component, components[other_index], prefer_actual=True):
|
||||
self_collision_count += 1
|
||||
|
||||
return RoutingReport(
|
||||
static_collision_count=static_collision_count,
|
||||
dynamic_collision_count=dynamic_collision_count,
|
||||
self_collision_count=self_collision_count,
|
||||
total_length=total_length,
|
||||
return PathVerificationDetail(
|
||||
report=RoutingReport(
|
||||
static_collision_count=static_collision_count,
|
||||
dynamic_collision_count=dynamic_collision_count,
|
||||
self_collision_count=self_collision_count,
|
||||
total_length=total_length,
|
||||
),
|
||||
conflicting_net_ids=tuple(sorted(conflicting_net_ids)),
|
||||
)
|
||||
|
||||
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
|
||||
return self.verify_path_details(net_id, components).report
|
||||
|
||||
def ray_cast(
|
||||
self,
|
||||
origin: Port,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,18 @@ class DynamicPathIndex:
|
|||
"index",
|
||||
"geometries",
|
||||
"dilated",
|
||||
"dilated_bounds",
|
||||
"net_envelope_index",
|
||||
"net_envelopes",
|
||||
"net_envelope_obj_ids",
|
||||
"net_envelope_obj_to_net",
|
||||
"tree",
|
||||
"obj_ids",
|
||||
"grid",
|
||||
"grid_net_obj_ids",
|
||||
"grid_net_counts",
|
||||
"obj_cells",
|
||||
"net_to_obj_ids",
|
||||
"id_counter",
|
||||
"net_ids_array",
|
||||
"bounds_array",
|
||||
|
|
@ -35,16 +44,69 @@ class DynamicPathIndex:
|
|||
self.index = rtree.index.Index()
|
||||
self.geometries: dict[int, tuple[str, 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.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
|
||||
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.net_ids_array = numpy.array([], dtype=object)
|
||||
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:
|
||||
self.tree = None
|
||||
self.grid = {}
|
||||
self.grid_net_obj_ids = {}
|
||||
self.grid_net_counts = {}
|
||||
self.obj_cells = {}
|
||||
|
||||
def ensure_tree(self) -> None:
|
||||
if self.tree is None and self.dilated:
|
||||
|
|
@ -65,33 +127,97 @@ class DynamicPathIndex:
|
|||
self.engine.metrics.total_dynamic_grid_rebuilds += 1
|
||||
cell_size = self.engine.grid_cell_size
|
||||
for obj_id, polygon in self.dilated.items():
|
||||
for cell in iter_grid_cells(polygon.bounds, cell_size):
|
||||
self.grid.setdefault(cell, []).append(obj_id)
|
||||
self._register_grid_membership(obj_id, self.geometries[obj_id][0], polygon.bounds, cell_size=cell_size)
|
||||
|
||||
def _register_grid_membership(
|
||||
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]) -> None:
|
||||
self.invalidate_queries()
|
||||
if self.engine.metrics is not None:
|
||||
self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
|
||||
cell_size = self.engine.grid_cell_size
|
||||
for index, polygon in enumerate(geometry):
|
||||
obj_id = self.id_counter
|
||||
self.id_counter += 1
|
||||
dilated = dilated_geometry[index]
|
||||
dilated_bounds = dilated.bounds
|
||||
self.geometries[obj_id] = (net_id, polygon)
|
||||
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:
|
||||
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)
|
||||
|
||||
def remove_obj_ids(self, obj_ids: list[int]) -> None:
|
||||
if not obj_ids:
|
||||
return
|
||||
|
||||
self.invalidate_queries()
|
||||
if self.engine.metrics is not None:
|
||||
self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids)
|
||||
affected_nets: set[str] = set()
|
||||
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.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
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ class RouteMetrics:
|
|||
warm_start_paths_used: int
|
||||
refine_path_calls: 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
|
||||
|
|
@ -61,6 +66,19 @@ class RouteMetrics:
|
|||
hard_collision_cache_hits: int
|
||||
congestion_cache_hits: 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_removed: int
|
||||
dynamic_tree_rebuilds: int
|
||||
|
|
@ -68,6 +86,7 @@ class RouteMetrics:
|
|||
static_tree_rebuilds: int
|
||||
static_raw_tree_rebuilds: int
|
||||
static_net_tree_rebuilds: int
|
||||
visibility_corner_index_builds: int
|
||||
visibility_builds: int
|
||||
visibility_corner_pairs_checked: int
|
||||
visibility_corner_queries_exact: int
|
||||
|
|
@ -89,9 +108,13 @@ class RouteMetrics:
|
|||
ray_cast_candidate_bounds: int
|
||||
ray_cast_exact_geometry_checks: int
|
||||
congestion_check_calls: int
|
||||
congestion_lazy_resolutions: int
|
||||
congestion_lazy_requeues: int
|
||||
congestion_candidate_ids: int
|
||||
congestion_exact_pair_checks: int
|
||||
verify_path_report_calls: int
|
||||
verify_static_buffer_ops: int
|
||||
verify_dynamic_candidate_nets: int
|
||||
verify_dynamic_exact_pair_checks: int
|
||||
refinement_windows_considered: int
|
||||
refinement_static_bounds_checked: int
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ def process_move(
|
|||
context: AStarContext,
|
||||
metrics: AStarMetrics,
|
||||
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,
|
||||
move_class: MoveKind,
|
||||
params: tuple,
|
||||
|
|
@ -109,6 +114,11 @@ def process_move(
|
|||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config,
|
||||
move_class,
|
||||
abs_key,
|
||||
|
|
@ -126,6 +136,11 @@ def add_node(
|
|||
context: AStarContext,
|
||||
metrics: AStarMetrics,
|
||||
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,
|
||||
move_type: MoveKind,
|
||||
cache_key: tuple,
|
||||
|
|
@ -164,16 +179,6 @@ def add_node(
|
|||
return
|
||||
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):
|
||||
return
|
||||
|
||||
|
|
@ -181,7 +186,6 @@ def add_node(
|
|||
result,
|
||||
start_port=parent_p,
|
||||
)
|
||||
move_cost += total_overlaps * context.congestion_penalty
|
||||
|
||||
if config.max_cost is not None and parent.g_cost + move_cost > config.max_cost:
|
||||
metrics.pruned_cost += 1
|
||||
|
|
@ -192,6 +196,41 @@ def add_node(
|
|||
metrics.total_pruned_cost += 1
|
||||
return
|
||||
|
||||
if state in closed_set and closed_set[state] <= parent.g_cost + move_cost + TOLERANCE_LINEAR:
|
||||
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
|
||||
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
|
||||
metrics.pruned_closed_set += 1
|
||||
|
|
|
|||
|
|
@ -63,15 +63,19 @@ def _visible_straight_candidates(
|
|||
return []
|
||||
|
||||
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)
|
||||
if max_bend_radius <= 0 or not visibility_manager.corners:
|
||||
return []
|
||||
|
||||
reach = max_reach + max_bend_radius
|
||||
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach)
|
||||
candidate_ids = list(visibility_manager.corner_index.intersection(bounds))
|
||||
candidate_ids = visibility_manager.get_tangent_corner_candidates(
|
||||
current,
|
||||
min_forward=search_options.min_straight_length,
|
||||
max_forward=reach,
|
||||
radii=search_options.bend_radii,
|
||||
)
|
||||
if not candidate_ids:
|
||||
return []
|
||||
|
||||
|
|
@ -141,6 +145,11 @@ def expand_moves(
|
|||
context: AStarContext,
|
||||
metrics: AStarMetrics,
|
||||
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,
|
||||
) -> None:
|
||||
search_options = context.options.search
|
||||
|
|
@ -185,6 +194,11 @@ def expand_moves(
|
|||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config,
|
||||
"straight",
|
||||
(int(round(proj_t)),),
|
||||
|
|
@ -242,6 +256,11 @@ def expand_moves(
|
|||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config,
|
||||
"straight",
|
||||
(length,),
|
||||
|
|
@ -270,6 +289,11 @@ def expand_moves(
|
|||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config,
|
||||
"bend90",
|
||||
(radius, direction),
|
||||
|
|
@ -298,12 +322,17 @@ def expand_moves(
|
|||
current,
|
||||
target,
|
||||
net_width,
|
||||
net_id,
|
||||
open_set,
|
||||
closed_set,
|
||||
net_id,
|
||||
open_set,
|
||||
closed_set,
|
||||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config,
|
||||
"sbend",
|
||||
(offset, radius),
|
||||
|
|
|
|||
|
|
@ -58,7 +58,17 @@ class SearchRunConfig:
|
|||
|
||||
|
||||
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",
|
||||
"congestion_resolved",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -67,6 +77,10 @@ class AStarNode:
|
|||
h_cost: float,
|
||||
parent: AStarNode | None = None,
|
||||
component_result: ComponentResult | None = None,
|
||||
*,
|
||||
base_move_cost: float = 0.0,
|
||||
cache_key: tuple | None = None,
|
||||
congestion_resolved: bool = True,
|
||||
) -> None:
|
||||
self.port = port
|
||||
self.g_cost = g_cost
|
||||
|
|
@ -74,6 +88,9 @@ class AStarNode:
|
|||
self.fh_cost = (g_cost + h_cost, h_cost)
|
||||
self.parent = parent
|
||||
self.component_result = component_result
|
||||
self.base_move_cost = base_move_cost
|
||||
self.cache_key = cache_key
|
||||
self.congestion_resolved = congestion_resolved
|
||||
|
||||
def __lt__(self, other: AStarNode) -> bool:
|
||||
return self.fh_cost < other.fh_cost
|
||||
|
|
@ -94,6 +111,11 @@ class AStarMetrics:
|
|||
"total_warm_start_paths_used",
|
||||
"total_refine_path_calls",
|
||||
"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",
|
||||
|
|
@ -110,6 +132,19 @@ class AStarMetrics:
|
|||
"total_hard_collision_cache_hits",
|
||||
"total_congestion_cache_hits",
|
||||
"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_removed",
|
||||
"total_dynamic_tree_rebuilds",
|
||||
|
|
@ -117,6 +152,7 @@ class AStarMetrics:
|
|||
"total_static_tree_rebuilds",
|
||||
"total_static_raw_tree_rebuilds",
|
||||
"total_static_net_tree_rebuilds",
|
||||
"total_visibility_corner_index_builds",
|
||||
"total_visibility_builds",
|
||||
"total_visibility_corner_pairs_checked",
|
||||
"total_visibility_corner_queries_exact",
|
||||
|
|
@ -138,9 +174,13 @@ class AStarMetrics:
|
|||
"total_ray_cast_candidate_bounds",
|
||||
"total_ray_cast_exact_geometry_checks",
|
||||
"total_congestion_check_calls",
|
||||
"total_congestion_lazy_resolutions",
|
||||
"total_congestion_lazy_requeues",
|
||||
"total_congestion_candidate_ids",
|
||||
"total_congestion_exact_pair_checks",
|
||||
"total_verify_path_report_calls",
|
||||
"total_verify_static_buffer_ops",
|
||||
"total_verify_dynamic_candidate_nets",
|
||||
"total_verify_dynamic_exact_pair_checks",
|
||||
"total_refinement_windows_considered",
|
||||
"total_refinement_static_bounds_checked",
|
||||
|
|
@ -172,6 +212,11 @@ class AStarMetrics:
|
|||
self.total_warm_start_paths_used = 0
|
||||
self.total_refine_path_calls = 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
|
||||
|
|
@ -188,6 +233,19 @@ class AStarMetrics:
|
|||
self.total_hard_collision_cache_hits = 0
|
||||
self.total_congestion_cache_hits = 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_removed = 0
|
||||
self.total_dynamic_tree_rebuilds = 0
|
||||
|
|
@ -195,6 +253,7 @@ class AStarMetrics:
|
|||
self.total_static_tree_rebuilds = 0
|
||||
self.total_static_raw_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_corner_pairs_checked = 0
|
||||
self.total_visibility_corner_queries_exact = 0
|
||||
|
|
@ -216,9 +275,13 @@ class AStarMetrics:
|
|||
self.total_ray_cast_candidate_bounds = 0
|
||||
self.total_ray_cast_exact_geometry_checks = 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_verify_path_report_calls = 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_refinement_windows_considered = 0
|
||||
self.total_refinement_static_bounds_checked = 0
|
||||
|
|
@ -249,6 +312,11 @@ class AStarMetrics:
|
|||
self.total_warm_start_paths_used = 0
|
||||
self.total_refine_path_calls = 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
|
||||
|
|
@ -265,6 +333,19 @@ class AStarMetrics:
|
|||
self.total_hard_collision_cache_hits = 0
|
||||
self.total_congestion_cache_hits = 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_removed = 0
|
||||
self.total_dynamic_tree_rebuilds = 0
|
||||
|
|
@ -272,6 +353,7 @@ class AStarMetrics:
|
|||
self.total_static_tree_rebuilds = 0
|
||||
self.total_static_raw_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_corner_pairs_checked = 0
|
||||
self.total_visibility_corner_queries_exact = 0
|
||||
|
|
@ -293,9 +375,13 @@ class AStarMetrics:
|
|||
self.total_ray_cast_candidate_bounds = 0
|
||||
self.total_ray_cast_exact_geometry_checks = 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_verify_path_report_calls = 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_refinement_windows_considered = 0
|
||||
self.total_refinement_static_bounds_checked = 0
|
||||
|
|
@ -329,6 +415,11 @@ class AStarMetrics:
|
|||
warm_start_paths_used=self.total_warm_start_paths_used,
|
||||
refine_path_calls=self.total_refine_path_calls,
|
||||
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,
|
||||
|
|
@ -345,6 +436,19 @@ class AStarMetrics:
|
|||
hard_collision_cache_hits=self.total_hard_collision_cache_hits,
|
||||
congestion_cache_hits=self.total_congestion_cache_hits,
|
||||
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_removed=self.total_dynamic_path_objects_removed,
|
||||
dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds,
|
||||
|
|
@ -352,6 +456,7 @@ class AStarMetrics:
|
|||
static_tree_rebuilds=self.total_static_tree_rebuilds,
|
||||
static_raw_tree_rebuilds=self.total_static_raw_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_corner_pairs_checked=self.total_visibility_corner_pairs_checked,
|
||||
visibility_corner_queries_exact=self.total_visibility_corner_queries_exact,
|
||||
|
|
@ -373,9 +478,13 @@ class AStarMetrics:
|
|||
ray_cast_candidate_bounds=self.total_ray_cast_candidate_bounds,
|
||||
ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks,
|
||||
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,
|
||||
verify_path_report_calls=self.total_verify_path_report_calls,
|
||||
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,
|
||||
refinement_windows_considered=self.total_refinement_windows_considered,
|
||||
refinement_static_bounds_checked=self.total_refinement_static_bounds_checked,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,21 @@ class _RoutingState:
|
|||
timeout_s: float
|
||||
initial_paths: dict[str, tuple[ComponentResult, ...]] | None
|
||||
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
|
||||
|
||||
class PathFinder:
|
||||
__slots__ = (
|
||||
|
|
@ -136,6 +151,13 @@ class PathFinder:
|
|||
timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations),
|
||||
initial_paths=initial_paths,
|
||||
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:
|
||||
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
|
||||
|
|
@ -162,9 +184,49 @@ class PathFinder:
|
|||
net_width=net.width,
|
||||
search=search,
|
||||
clearance=self.context.cost_evaluator.collision_engine.clearance,
|
||||
)
|
||||
)
|
||||
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 _route_net_once(
|
||||
self,
|
||||
state: _RoutingState,
|
||||
|
|
@ -235,9 +297,9 @@ class PathFinder:
|
|||
self,
|
||||
state: _RoutingState,
|
||||
iteration: int,
|
||||
reroute_net_ids: set[str],
|
||||
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
|
||||
) -> dict[str, RoutingOutcome] | None:
|
||||
outcomes: dict[str, RoutingOutcome] = {}
|
||||
) -> _IterationReview | None:
|
||||
congestion = self.context.options.congestion
|
||||
self.metrics.total_route_iterations += 1
|
||||
self.metrics.reset_per_route()
|
||||
|
|
@ -246,18 +308,63 @@ class PathFinder:
|
|||
iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None
|
||||
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:
|
||||
self.metrics.total_timeout_events += 1
|
||||
return None
|
||||
|
||||
result = self._route_net_once(state, iteration, net_id)
|
||||
state.results[net_id] = result
|
||||
outcomes[net_id] = result.outcome
|
||||
|
||||
review = self._reverify_iteration_results(state)
|
||||
|
||||
if iteration_callback:
|
||||
iteration_callback(iteration, state.results)
|
||||
return outcomes
|
||||
return review
|
||||
|
||||
def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview:
|
||||
self.metrics.total_iteration_reverify_calls += 1
|
||||
conflict_edges: set[tuple[str, str]] = set()
|
||||
conflicting_nets: set[str] = set()
|
||||
completed_net_ids: set[str] = set()
|
||||
total_dynamic_collisions = 0
|
||||
|
||||
for net_id in state.ordered_net_ids:
|
||||
result = state.results.get(net_id)
|
||||
if not result or not result.path or not result.reached_target:
|
||||
continue
|
||||
|
||||
self.metrics.total_iteration_reverified_nets += 1
|
||||
detail = self.context.cost_evaluator.collision_engine.verify_path_details(net_id, result.path)
|
||||
state.results[net_id] = RoutingResult(
|
||||
net_id=net_id,
|
||||
path=result.path,
|
||||
reached_target=result.reached_target,
|
||||
report=detail.report,
|
||||
)
|
||||
total_dynamic_collisions += detail.report.dynamic_collision_count
|
||||
if state.results[net_id].outcome == "completed":
|
||||
completed_net_ids.add(net_id)
|
||||
if not detail.conflicting_net_ids:
|
||||
continue
|
||||
conflicting_nets.add(net_id)
|
||||
for other_net_id in detail.conflicting_net_ids:
|
||||
conflicting_nets.add(other_net_id)
|
||||
if other_net_id == net_id:
|
||||
continue
|
||||
conflict_edges.add(tuple(sorted((net_id, other_net_id))))
|
||||
|
||||
self.metrics.total_iteration_conflicting_nets += len(conflicting_nets)
|
||||
self.metrics.total_iteration_conflict_edges += len(conflict_edges)
|
||||
return _IterationReview(
|
||||
conflicting_nets=conflicting_nets,
|
||||
conflict_edges=conflict_edges,
|
||||
completed_net_ids=completed_net_ids,
|
||||
total_dynamic_collisions=total_dynamic_collisions,
|
||||
)
|
||||
|
||||
def _run_iterations(
|
||||
self,
|
||||
|
|
@ -266,10 +373,33 @@ class PathFinder:
|
|||
) -> bool:
|
||||
congestion = self.context.options.congestion
|
||||
for iteration in range(congestion.max_iterations):
|
||||
outcomes = self._run_iteration(state, iteration, iteration_callback)
|
||||
if outcomes is None:
|
||||
review = self._run_iteration(
|
||||
state,
|
||||
iteration,
|
||||
set(state.ordered_net_ids),
|
||||
iteration_callback,
|
||||
)
|
||||
if review is None:
|
||||
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
|
||||
self.context.congestion_penalty *= congestion.multiplier
|
||||
return False
|
||||
|
|
@ -287,12 +417,13 @@ class PathFinder:
|
|||
self.context.cost_evaluator.collision_engine.remove_path(net_id)
|
||||
refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.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(
|
||||
net_id=net_id,
|
||||
path=tuple(refined_path),
|
||||
reached_target=result.reached_target,
|
||||
report=report,
|
||||
report=result.report,
|
||||
)
|
||||
|
||||
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
|
||||
|
|
@ -324,6 +455,7 @@ class PathFinder:
|
|||
state = self._prepare_state()
|
||||
timed_out = self._run_iterations(state, iteration_callback)
|
||||
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
|
||||
self._restore_best_iteration(state)
|
||||
|
||||
if timed_out:
|
||||
return self._verify_results(state)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ def route_astar(
|
|||
open_set: list[_AStarNode] = []
|
||||
closed_set: dict[tuple[int, int, int], float] = {}
|
||||
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,
|
||||
|
|
@ -89,6 +94,11 @@ def route_astar(
|
|||
context,
|
||||
metrics,
|
||||
congestion_cache,
|
||||
congestion_presence_cache,
|
||||
congestion_candidate_precheck_cache,
|
||||
congestion_net_envelope_cache,
|
||||
congestion_grid_net_cache,
|
||||
congestion_grid_span_cache,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ class CostEvaluator:
|
|||
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_e = danger_map.get_cost(end_port.x, end_port.y)
|
||||
if start_port:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ class VisibilityManager:
|
|||
"""
|
||||
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:
|
||||
self.collision_engine = collision_engine
|
||||
|
|
@ -24,8 +32,8 @@ class VisibilityManager:
|
|||
self.corner_index = rtree.index.Index()
|
||||
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._built_static_version = -1
|
||||
self._build()
|
||||
self._corner_index_version = -1
|
||||
self._corner_graph_version = -1
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
|
|
@ -35,19 +43,31 @@ class VisibilityManager:
|
|||
self.corner_index = rtree.index.Index()
|
||||
self._corner_graph = {}
|
||||
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:
|
||||
if self._built_static_version != self.collision_engine.get_static_version():
|
||||
self.clear_cache()
|
||||
self.ensure_corner_graph_current()
|
||||
|
||||
def _build(self) -> None:
|
||||
"""
|
||||
Extract corners and pre-compute corner-to-corner visibility.
|
||||
"""
|
||||
def _build_corner_index(self) -> None:
|
||||
if self.collision_engine.metrics is not None:
|
||||
self.collision_engine.metrics.total_visibility_builds += 1
|
||||
self._built_static_version = self.collision_engine.get_static_version()
|
||||
self.collision_engine.metrics.total_visibility_corner_index_builds += 1
|
||||
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 = []
|
||||
for poly in self.collision_engine.iter_static_dilated_geometries():
|
||||
coords = list(poly.exterior.coords)
|
||||
|
|
@ -63,7 +83,6 @@ class VisibilityManager:
|
|||
if not raw_corners:
|
||||
return
|
||||
|
||||
# Deduplicate repeated corner coordinates
|
||||
seen = set()
|
||||
for x, y in raw_corners:
|
||||
sx, sy = round(x, 3), round(y, 3)
|
||||
|
|
@ -71,15 +90,27 @@ class VisibilityManager:
|
|||
seen.add((sx, sy))
|
||||
self.corners.append((sx, sy))
|
||||
|
||||
# Build spatial index for corners
|
||||
for i, (x, y) in enumerate(self.corners):
|
||||
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
|
||||
num_corners = len(self.corners)
|
||||
if num_corners > 200:
|
||||
# Limit pre-computation if too many corners
|
||||
return
|
||||
# Limit pre-computation if too many corners
|
||||
return
|
||||
|
||||
for i in range(num_corners):
|
||||
self._corner_graph[i] = []
|
||||
|
|
@ -98,6 +129,7 @@ class VisibilityManager:
|
|||
self._corner_graph[i].append((cx, cy, dist))
|
||||
|
||||
def _corner_idx_at(self, origin: Port) -> int | None:
|
||||
self.ensure_corner_index_current()
|
||||
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)))
|
||||
for idx in nearby:
|
||||
|
|
@ -106,6 +138,49 @@ class VisibilityManager:
|
|||
return idx
|
||||
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]]:
|
||||
"""
|
||||
Find visible corners from an arbitrary point.
|
||||
|
|
@ -113,11 +188,13 @@ class VisibilityManager:
|
|||
"""
|
||||
if self.collision_engine.metrics is not None:
|
||||
self.collision_engine.metrics.total_visibility_point_queries += 1
|
||||
self._ensure_current()
|
||||
self.ensure_corner_index_current()
|
||||
if max_dist < 0:
|
||||
return []
|
||||
|
||||
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:
|
||||
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
|
||||
|
||||
|
|
@ -157,7 +234,7 @@ class VisibilityManager:
|
|||
"""
|
||||
if self.collision_engine.metrics is not None:
|
||||
self.collision_engine.metrics.total_visibility_corner_queries_exact += 1
|
||||
self._ensure_current()
|
||||
self.ensure_corner_graph_current()
|
||||
if max_dist < 0:
|
||||
return []
|
||||
|
||||
|
|
|
|||
|
|
@ -378,6 +378,24 @@ def run_example_06() -> ScenarioOutcome:
|
|||
|
||||
|
||||
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 _snapshot_example_07_variant(
|
||||
name: str,
|
||||
*,
|
||||
warm_start_enabled: bool,
|
||||
) -> ScenarioSnapshot:
|
||||
bounds = (0, 0, 1000, 1000)
|
||||
obstacles = [
|
||||
box(450, 0, 550, 400),
|
||||
|
|
@ -420,6 +438,7 @@ def snapshot_example_07() -> ScenarioSnapshot:
|
|||
"capture_expanded": True,
|
||||
"shuffle_nets": True,
|
||||
"seed": 42,
|
||||
"warm_start_enabled": warm_start_enabled,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -432,7 +451,7 @@ def snapshot_example_07() -> ScenarioSnapshot:
|
|||
t0 = perf_counter()
|
||||
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||
t1 = perf_counter()
|
||||
return _make_snapshot("example_07_large_scale_routing", results, t1 - t0, pathfinder.metrics.snapshot())
|
||||
return _make_snapshot(name, results, t1 - t0, pathfinder.metrics.snapshot())
|
||||
|
||||
|
||||
def run_example_07() -> ScenarioOutcome:
|
||||
|
|
@ -534,6 +553,10 @@ SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
|
|||
("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),
|
||||
)
|
||||
|
||||
|
||||
def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]:
|
||||
return tuple(run() for _, run in SCENARIO_SNAPSHOTS)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ from inire import (
|
|||
route,
|
||||
)
|
||||
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:
|
||||
|
|
@ -77,14 +83,177 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
|
|||
assert run.expanded_nodes
|
||||
assert run.metrics.nodes_expanded > 0
|
||||
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.move_cache_abs_misses >= 0
|
||||
assert run.metrics.ray_cast_calls >= 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.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
|
||||
|
||||
|
||||
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_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_route_problem_locked_routes_become_static_obstacles() -> None:
|
||||
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
|
||||
problem = RoutingProblem(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import math
|
||||
|
||||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
|
@ -7,7 +6,8 @@ from inire import RoutingProblem, RoutingOptions, RoutingResult, SearchOptions
|
|||
from inire.geometry.components import Bend90, Straight
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router._astar_types import AStarContext, SearchRunConfig
|
||||
from inire.router._astar_types import AStarContext, AStarNode, SearchRunConfig
|
||||
from inire.router._astar_admission import add_node
|
||||
from inire.router._search import route_astar
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
|
|
@ -301,6 +301,27 @@ def test_route_astar_supports_all_visibility_guidance_modes(
|
|||
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:
|
||||
context = AStarContext(
|
||||
basic_evaluator,
|
||||
|
|
@ -318,3 +339,103 @@ def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_eval
|
|||
path = _route(context, start, target)
|
||||
assert path is not None
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
from shapely.geometry import box
|
||||
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
from inire.geometry.components import ComponentResult
|
||||
from inire.geometry.components import Straight
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router._astar_types import AStarMetrics
|
||||
from inire.seeds import StraightSeed
|
||||
|
||||
|
||||
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:
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
engine.metrics = AStarMetrics()
|
||||
net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789"
|
||||
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]
|
||||
|
|
@ -91,10 +97,12 @@ def test_verify_path_report_preserves_long_net_id() -> None:
|
|||
report = engine.verify_path_report(net_id, path)
|
||||
|
||||
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:
|
||||
engine = RoutingWorld(clearance=2.0)
|
||||
engine.metrics = AStarMetrics()
|
||||
shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_"
|
||||
net_a = f"{shared_prefix}A"
|
||||
net_b = f"{shared_prefix}B"
|
||||
|
|
@ -115,6 +123,50 @@ def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> N
|
|||
report = engine.verify_path_report(net_a, path_a)
|
||||
|
||||
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_remove_path_clears_dynamic_path() -> None:
|
||||
|
|
@ -129,3 +181,247 @@ def test_remove_path_clears_dynamic_path() -> None:
|
|||
engine.remove_path("netA")
|
||||
assert list(engine._dynamic_paths.geometries.values()) == []
|
||||
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
|
||||
|
||||
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:
|
||||
from collections.abc import Callable
|
||||
|
|
@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|||
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
|
||||
PERFORMANCE_REPEATS = 3
|
||||
REGRESSION_FACTOR = 1.5
|
||||
NO_WARM_START_REGRESSION_SECONDS = 180.0
|
||||
|
||||
# Baselines are measured from clean 6a28dcf-style runs without plotting.
|
||||
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"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,7 @@ from inire import (
|
|||
)
|
||||
from inire.router._stack import build_routing_stack
|
||||
from inire.seeds import Bend90Seed, PathSeed, StraightSeed
|
||||
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics
|
||||
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics, snapshot_example_05
|
||||
|
||||
|
||||
EXPECTED_OUTCOMES = {
|
||||
|
|
@ -36,6 +36,13 @@ def test_examples_match_legacy_expected_outcomes(name: str, run) -> None:
|
|||
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_06_clipped_bbox_margin_restores_legacy_seed() -> None:
|
||||
bounds = (-20, -20, 170, 170)
|
||||
obstacles = (
|
||||
|
|
|
|||
|
|
@ -22,7 +22,19 @@ def test_snapshot_example_01_exposes_metrics() -> None:
|
|||
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.visibility_corner_index_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
|
||||
|
||||
|
||||
|
|
@ -74,6 +86,7 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat
|
|||
str(diff_script),
|
||||
"--baseline",
|
||||
str(baseline_dir / "performance_baseline.json"),
|
||||
"--include-performance-only",
|
||||
"--scenario",
|
||||
"example_01_simple_route",
|
||||
"--output",
|
||||
|
|
@ -85,3 +98,100 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat
|
|||
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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from shapely.geometry import box
|
|||
|
||||
from inire.geometry.collision import RoutingWorld
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router._astar_types import AStarMetrics
|
||||
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(far_corners) > len(near_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
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@ 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 SCENARIO_SNAPSHOTS
|
||||
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",
|
||||
|
|
@ -22,10 +26,30 @@ SUMMARY_KEYS = (
|
|||
)
|
||||
|
||||
|
||||
def _current_snapshots(selected_scenarios: tuple[str, ...] | None) -> dict[str, 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 _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 SCENARIO_SNAPSHOTS:
|
||||
for name, run in _snapshot_registry(include_performance_only):
|
||||
if allowed is not None and name not in allowed:
|
||||
continue
|
||||
snapshots[name] = asdict(run())
|
||||
|
|
@ -42,13 +66,29 @@ def _load_baseline(path: Path, selected_scenarios: tuple[str, ...] | None) -> di
|
|||
}
|
||||
|
||||
|
||||
def _metric_value(snapshot: dict[str, object], key: str) -> float:
|
||||
if key == "duration_s":
|
||||
return float(snapshot["duration_s"])
|
||||
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 _render_report(baseline: dict[str, dict[str, object]], current: dict[str, dict[str, object]]) -> str:
|
||||
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",
|
||||
|
|
@ -60,20 +100,61 @@ def _render_report(baseline: dict[str, dict[str, object]], current: dict[str, di
|
|||
base_snapshot = baseline.get(scenario)
|
||||
curr_snapshot = current.get(scenario)
|
||||
if base_snapshot is None:
|
||||
lines.append(f"| {scenario} | added | - | - | - |")
|
||||
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 SUMMARY_KEYS:
|
||||
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(
|
||||
|
|
@ -95,18 +176,61 @@ def main() -> None:
|
|||
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)
|
||||
report = _render_report(baseline, current)
|
||||
current = _current_snapshots(selected, include_performance_only=args.include_performance_only)
|
||||
report = _render_report(baseline, current, metrics)
|
||||
|
||||
if args.output is None:
|
||||
print(report, end="")
|
||||
else:
|
||||
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__":
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from dataclasses import asdict
|
|||
from datetime import date
|
||||
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 = (
|
||||
|
|
@ -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)
|
||||
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:
|
||||
continue
|
||||
snapshots.append(run())
|
||||
|
|
@ -103,6 +113,11 @@ def main() -> None:
|
|||
default=[],
|
||||
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()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
|
|
@ -110,7 +125,7 @@ def main() -> None:
|
|||
docs_dir.mkdir(exist_ok=True)
|
||||
|
||||
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"
|
||||
markdown_path = docs_dir / "performance.md"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue