various performance work (wip)

This commit is contained in:
Jan Petykiewicz 2026-04-01 21:29:23 -07:00
commit 71e263c527
25 changed files with 4071 additions and 326 deletions

24
DOCS.md
View file

@ -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. - `warm_start_paths_used`: Number of routing attempts satisfied directly from an initial or warm-start path.
- `refine_path_calls`: Number of completed paths passed through the post-route refiner. - `refine_path_calls`: Number of completed paths passed through the post-route refiner.
- `timeout_events`: Number of timeout exits encountered during the run. - `timeout_events`: Number of timeout exits encountered during the run.
- `iteration_reverify_calls`: Number of end-of-iteration full reverify passes against the final installed dynamic geometry.
- `iteration_reverified_nets`: Number of reached-target nets reverified at iteration boundaries.
- `iteration_conflicting_nets`: Total unique nets found in end-of-iteration dynamic conflicts.
- `iteration_conflict_edges`: Total undirected dynamic-conflict edges observed at iteration boundaries.
- `nets_carried_forward`: Number of nets retained unchanged between iterations.
### Cache Counters ### Cache Counters
@ -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_tree_rebuilds`: Number of static dilated-obstacle STRtree rebuilds.
- `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification. - `static_raw_tree_rebuilds`: Number of raw static-obstacle STRtree rebuilds used for verification.
- `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds. - `static_net_tree_rebuilds`: Number of net-width-specific static STRtree rebuilds.
- `visibility_builds`: Number of static visibility-graph rebuilds. - `visibility_corner_index_builds`: Number of lazy corner-index rebuilds.
- `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building that graph. - `visibility_builds`: Number of exact corner-visibility graph rebuilds.
- `visibility_corner_queries` / `visibility_corner_hits`: Precomputed-corner visibility query activity. - `visibility_corner_pairs_checked`: Number of corner-pair visibility probes considered while building the exact graph.
- `visibility_corner_queries_exact` / `visibility_corner_hits_exact`: Exact-corner visibility query activity.
- `visibility_point_queries`, `visibility_point_cache_hits`, `visibility_point_cache_misses`: Arbitrary-point visibility query and cache activity. - `visibility_point_queries`, `visibility_point_cache_hits`, `visibility_point_cache_misses`: Arbitrary-point visibility query and cache activity.
- `ray_cast_calls`: Number of ray-cast queries issued against static obstacles. - `ray_cast_calls`: Number of ray-cast queries issued against static obstacles.
- `ray_cast_candidate_bounds`: Total broad-phase candidate bounds considered by ray casts. - `ray_cast_candidate_bounds`: Total broad-phase candidate bounds considered by ray casts.
- `ray_cast_exact_geometry_checks`: Total exact non-rectangular geometry checks performed by ray casts. - `ray_cast_exact_geometry_checks`: Total exact non-rectangular geometry checks performed by ray casts.
- `congestion_check_calls`: Number of congestion broad-phase checks requested by search. - `congestion_check_calls`: Number of congestion broad-phase checks requested by search.
- `congestion_presence_cache_hits` / `congestion_presence_cache_misses`: Reuse of cached per-span booleans indicating whether a move polygon could overlap any other routed net at all.
- `congestion_presence_skips`: Number of moves that bypassed full congestion evaluation because the presence precheck found no other routed nets in any covered dynamic-grid span.
- `congestion_candidate_precheck_hits` / `congestion_candidate_precheck_misses`: Reuse of cached conservative per-span booleans indicating whether any candidate nets survive the net-envelope and grid-net broad phases.
- `congestion_candidate_precheck_skips`: Number of moves that bypassed full congestion evaluation because the candidate-net precheck found no surviving candidate nets after those broad phases.
- `congestion_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during routing.
- `congestion_net_envelope_cache_hits` / `congestion_net_envelope_cache_misses`: Reuse of cached dynamic net-envelope candidate sets keyed by the queried grid-cell span.
- `congestion_grid_net_cache_hits` / `congestion_grid_net_cache_misses`: Reuse of cached per-span candidate net ids gathered from dynamic grid occupancy.
- `congestion_grid_span_cache_hits` / `congestion_grid_span_cache_misses`: Reuse of cached dynamic-path candidate unions keyed by the queried grid-cell span.
- `congestion_lazy_resolutions`: Number of popped nodes whose pending congestion was resolved lazily.
- `congestion_lazy_requeues`: Number of lazily resolved nodes requeued after a positive congestion penalty was applied.
- `congestion_candidate_ids`: Total dynamic-path object ids returned by the congestion broad phase before exact confirmation.
- `congestion_exact_pair_checks`: Number of exact geometry-pair checks performed while confirming congestion hits. - `congestion_exact_pair_checks`: Number of exact geometry-pair checks performed while confirming congestion hits.
### Verification Counters ### Verification Counters
- `verify_path_report_calls`: Number of full path-verification passes. - `verify_path_report_calls`: Number of full path-verification passes.
- `verify_static_buffer_ops`: Number of static-verification `buffer()` operations. - `verify_static_buffer_ops`: Number of static-verification `buffer()` operations.
- `verify_dynamic_candidate_nets`: Total candidate net ids returned by the dynamic net-envelope broad phase during final verification.
- `verify_dynamic_exact_pair_checks`: Number of exact geometry-pair checks performed during dynamic-path verification. - `verify_dynamic_exact_pair_checks`: Number of exact geometry-pair checks performed during dynamic-path verification.
## 8. Internal Modules ## 8. Internal Modules

View file

@ -16,7 +16,7 @@
- `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers. - `inire/geometry/primitives.py`: Integer Manhattan ports and small transform helpers.
- `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation. - `inire/geometry/components.py`: `Straight`, `Bend90`, and `SBend` geometry generation.
- `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic. - `inire/geometry/collision.py`: Routing-world collision, congestion, ray-cast, and path-verification logic.
- `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths. - `inire/geometry/static_obstacle_index.py` and `inire/geometry/dynamic_path_index.py`: Spatial-index management for static obstacles and routed paths, including dynamic per-object indices, per-net grid occupancy, congestion grid membership, and per-net dynamic envelopes.
- `inire/router/_search.py`, `_astar_moves.py`, `_astar_admission.py`, `_astar_types.py`: The state-lattice A* search loop and move admission pipeline. - `inire/router/_search.py`, `_astar_moves.py`, `_astar_admission.py`, `_astar_types.py`: The state-lattice A* search loop and move admission pipeline.
- `inire/router/_router.py`: The negotiated-congestion driver and refinement orchestration. - `inire/router/_router.py`: The negotiated-congestion driver and refinement orchestration.
- `inire/router/refiner.py`: Post-route path simplification for completed paths. - `inire/router/refiner.py`: Post-route path simplification for completed paths.
@ -39,7 +39,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. - Static obstacles and routed paths are treated as single-layer geometry; automatic crossings are not supported.
- The danger-map implementation uses sampled obstacle-boundary points and a KD-tree, not a dense distance-transform grid. - The danger-map implementation uses sampled obstacle-boundary points and a KD-tree, not a dense distance-transform grid.
- The visibility subsystem keeps a lazy static corner index for default `tangent_corner` guidance and only builds the exact corner-to-corner graph on demand for `exact_corner` queries.
- `use_tiered_strategy` can swap in a cheaper bend proxy on the first congestion iteration. - `use_tiered_strategy` can swap in a cheaper bend proxy on the first congestion iteration.
- Negotiated congestion now re-verifies every reached-target path at the end of each iteration against the final installed dynamic geometry, and it stops early if the conflict graph stalls for consecutive iterations.
- Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning. - Final `RoutingResult` validity is determined by explicit post-route verification, not only by search-time pruning.
## Performance Visibility ## Performance Visibility

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,21 @@
# Performance Baseline # 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`. 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. Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.
| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls | | Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |
| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | | :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |
| example_01_simple_route | 0.0042 | 1 | 1 | 1 | 1 | 1 | 2 | 22 | 11 | 7 | 2 | 2 | 0 | 3 | | example_01_simple_route | 0.0036 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 3 |
| example_02_congestion_resolution | 0.3418 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 | | example_02_congestion_resolution | 0.3297 | 3 | 3 | 3 | 1 | 3 | 366 | 1164 | 1413 | 668 | 0 | 0 | 0 | 35 |
| example_03_locked_paths | 0.1827 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 | | example_03_locked_paths | 0.1832 | 2 | 2 | 2 | 2 | 2 | 191 | 657 | 904 | 307 | 0 | 0 | 0 | 14 |
| example_04_sbends_and_radii | 1.9938 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 | | 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.2458 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 | | example_05_orientation_stress | 0.2348 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 0 | 0 | 155 | 15 |
| example_06_bend_collision_models | 4.1186 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 | | 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 | 1.3734 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 | | 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.2410 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 | | 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.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 | | example_09_unroutable_best_effort | 0.0058 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 0 | 0 | 0 | 1 |
## Full Counter Set ## Full Counter Set
@ -24,4 +24,4 @@ These counters are currently observational only and are not enforced as CI regre
Tracked metric keys: Tracked metric keys:
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, 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

View file

@ -1,30 +1,51 @@
{ {
"generated_on": "2026-03-31", "generated_on": "2026-04-01",
"generator": "scripts/record_performance_baseline.py", "generator": "scripts/record_performance_baseline.py",
"scenarios": [ "scenarios": [
{ {
"duration_s": 0.00415895797777921, "duration_s": 0.0035884700482711196,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 0, "congestion_exact_pair_checks": 0,
"danger_map_cache_hits": 8, "congestion_grid_net_cache_hits": 0,
"danger_map_cache_misses": 13, "congestion_grid_net_cache_misses": 0,
"danger_map_lookup_calls": 21, "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_query_calls": 0,
"danger_map_total_ns": 27079, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 2, "dynamic_path_objects_added": 3,
"dynamic_path_objects_removed": 1, "dynamic_path_objects_removed": 2,
"dynamic_tree_rebuilds": 2, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 1,
"move_cache_abs_misses": 10, "move_cache_abs_misses": 10,
"move_cache_rel_hits": 0, "move_cache_rel_hits": 0,
"move_cache_rel_misses": 10, "move_cache_rel_misses": 10,
"moves_added": 7, "moves_added": 7,
"moves_generated": 11, "moves_generated": 11,
"nets_carried_forward": 0,
"nets_reached_target": 1, "nets_reached_target": 1,
"nets_routed": 1, "nets_routed": 1,
"nodes_expanded": 2, "nodes_expanded": 2,
@ -32,15 +53,15 @@
"pruned_closed_set": 0, "pruned_closed_set": 0,
"pruned_cost": 4, "pruned_cost": 4,
"pruned_hard_collision": 0, "pruned_hard_collision": 0,
"ray_cast_calls": 22, "ray_cast_calls": 10,
"ray_cast_calls_expand_forward": 1, "ray_cast_calls_expand_forward": 1,
"ray_cast_calls_expand_snap": 1, "ray_cast_calls_expand_snap": 1,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 8, "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_query": 0,
"ray_cast_calls_visibility_tangent": 0, "ray_cast_calls_visibility_tangent": 0,
"ray_cast_candidate_bounds": 12, "ray_cast_candidate_bounds": 0,
"ray_cast_exact_geometry_checks": 0, "ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 1, "refine_path_calls": 1,
"refinement_candidate_side_extents": 0, "refinement_candidate_side_extents": 0,
@ -52,18 +73,20 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 11, "score_component_calls": 11,
"score_component_total_ns": 59404, "score_component_total_ns": 16010,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 1, "static_tree_rebuilds": 0,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 3, "verify_path_report_calls": 3,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 2, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
@ -80,28 +103,49 @@
"valid_results": 1 "valid_results": 1
}, },
{ {
"duration_s": 0.34182924893684685, "duration_s": 0.32969290704932064,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 0, "congestion_exact_pair_checks": 0,
"danger_map_cache_hits": 1433, "congestion_grid_net_cache_hits": 0,
"danger_map_cache_misses": 775, "congestion_grid_net_cache_misses": 0,
"danger_map_lookup_calls": 2208, "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_query_calls": 0,
"danger_map_total_ns": 2165333, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 32, "dynamic_path_objects_added": 49,
"dynamic_path_objects_removed": 17, "dynamic_path_objects_removed": 34,
"dynamic_tree_rebuilds": 8, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 12,
"move_cache_abs_misses": 1401, "move_cache_abs_misses": 1401,
"move_cache_rel_hits": 1293, "move_cache_rel_hits": 1293,
"move_cache_rel_misses": 108, "move_cache_rel_misses": 108,
"moves_added": 668, "moves_added": 668,
"moves_generated": 1413, "moves_generated": 1413,
"nets_carried_forward": 0,
"nets_reached_target": 3, "nets_reached_target": 3,
"nets_routed": 3, "nets_routed": 3,
"nodes_expanded": 366, "nodes_expanded": 366,
@ -109,15 +153,15 @@
"pruned_closed_set": 157, "pruned_closed_set": 157,
"pruned_cost": 208, "pruned_cost": 208,
"pruned_hard_collision": 380, "pruned_hard_collision": 380,
"ray_cast_calls": 1176, "ray_cast_calls": 1164,
"ray_cast_calls_expand_forward": 363, "ray_cast_calls_expand_forward": 363,
"ray_cast_calls_expand_snap": 19, "ray_cast_calls_expand_snap": 19,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 529, "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_query": 0,
"ray_cast_calls_visibility_tangent": 253, "ray_cast_calls_visibility_tangent": 253,
"ray_cast_candidate_bounds": 925, "ray_cast_candidate_bounds": 913,
"ray_cast_exact_geometry_checks": 136, "ray_cast_exact_geometry_checks": 136,
"refine_path_calls": 3, "refine_path_calls": 3,
"refinement_candidate_side_extents": 26, "refinement_candidate_side_extents": 26,
@ -129,23 +173,25 @@
"refinement_windows_considered": 10, "refinement_windows_considered": 10,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 976, "score_component_calls": 976,
"score_component_total_ns": 4650167, "score_component_total_ns": 1091130,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 2, "static_tree_rebuilds": 2,
"timeout_events": 0, "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_path_report_calls": 35,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 4, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 253,
"visibility_tangent_candidate_scans": 363, "visibility_tangent_candidate_scans": 363,
"warm_start_paths_built": 3, "warm_start_paths_built": 3,
@ -157,28 +203,49 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 0.18274989898782223, "duration_s": 0.18321374501101673,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 0, "congestion_exact_pair_checks": 0,
"danger_map_cache_hits": 624, "congestion_grid_net_cache_hits": 0,
"danger_map_cache_misses": 414, "congestion_grid_net_cache_misses": 0,
"danger_map_lookup_calls": 1038, "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_query_calls": 0,
"danger_map_total_ns": 1001517, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 17, "dynamic_path_objects_added": 27,
"dynamic_path_objects_removed": 10, "dynamic_path_objects_removed": 20,
"dynamic_tree_rebuilds": 5, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 1,
"move_cache_abs_misses": 903, "move_cache_abs_misses": 903,
"move_cache_rel_hits": 821, "move_cache_rel_hits": 821,
"move_cache_rel_misses": 82, "move_cache_rel_misses": 82,
"moves_added": 307, "moves_added": 307,
"moves_generated": 904, "moves_generated": 904,
"nets_carried_forward": 0,
"nets_reached_target": 2, "nets_reached_target": 2,
"nets_routed": 2, "nets_routed": 2,
"nodes_expanded": 191, "nodes_expanded": 191,
@ -186,15 +253,15 @@
"pruned_closed_set": 97, "pruned_closed_set": 97,
"pruned_cost": 140, "pruned_cost": 140,
"pruned_hard_collision": 181, "pruned_hard_collision": 181,
"ray_cast_calls": 681, "ray_cast_calls": 657,
"ray_cast_calls_expand_forward": 189, "ray_cast_calls_expand_forward": 189,
"ray_cast_calls_expand_snap": 8, "ray_cast_calls_expand_snap": 8,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 407, "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_query": 0,
"ray_cast_calls_visibility_tangent": 53, "ray_cast_calls_visibility_tangent": 53,
"ray_cast_candidate_bounds": 179, "ray_cast_candidate_bounds": 155,
"ray_cast_exact_geometry_checks": 0, "ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 2, "refine_path_calls": 2,
"refinement_candidate_side_extents": 8, "refinement_candidate_side_extents": 8,
@ -206,23 +273,25 @@
"refinement_windows_considered": 2, "refinement_windows_considered": 2,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 504, "score_component_calls": 504,
"score_component_total_ns": 2184569, "score_component_total_ns": 556716,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 2, "static_tree_rebuilds": 1,
"timeout_events": 0, "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_path_report_calls": 14,
"verify_static_buffer_ops": 69, "verify_static_buffer_ops": 72,
"visibility_builds": 4, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 53,
"visibility_tangent_candidate_scans": 189, "visibility_tangent_candidate_scans": 189,
"warm_start_paths_built": 2, "warm_start_paths_built": 2,
@ -234,28 +303,49 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 1.993830946041271, "duration_s": 0.026024609920568764,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 0, "congestion_exact_pair_checks": 0,
"danger_map_cache_hits": 75, "congestion_grid_net_cache_hits": 0,
"danger_map_cache_misses": 120, "congestion_grid_net_cache_misses": 0,
"danger_map_lookup_calls": 195, "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_query_calls": 0,
"danger_map_total_ns": 207556, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 14, "dynamic_path_objects_added": 21,
"dynamic_path_objects_removed": 7, "dynamic_path_objects_removed": 14,
"dynamic_tree_rebuilds": 4, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 1,
"move_cache_abs_misses": 122, "move_cache_abs_misses": 122,
"move_cache_rel_hits": 80, "move_cache_rel_hits": 80,
"move_cache_rel_misses": 42, "move_cache_rel_misses": 42,
"moves_added": 65, "moves_added": 65,
"moves_generated": 123, "moves_generated": 123,
"nets_carried_forward": 0,
"nets_reached_target": 2, "nets_reached_target": 2,
"nets_routed": 2, "nets_routed": 2,
"nodes_expanded": 15, "nodes_expanded": 15,
@ -263,16 +353,16 @@
"pruned_closed_set": 2, "pruned_closed_set": 2,
"pruned_cost": 25, "pruned_cost": 25,
"pruned_hard_collision": 16, "pruned_hard_collision": 16,
"ray_cast_calls": 18218, "ray_cast_calls": 70,
"ray_cast_calls_expand_forward": 13, "ray_cast_calls_expand_forward": 13,
"ray_cast_calls_expand_snap": 1, "ray_cast_calls_expand_snap": 1,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 56, "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_query": 0,
"ray_cast_calls_visibility_tangent": 0, "ray_cast_calls_visibility_tangent": 0,
"ray_cast_candidate_bounds": 50717, "ray_cast_candidate_bounds": 4,
"ray_cast_exact_geometry_checks": 21265, "ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 2, "refine_path_calls": 2,
"refinement_candidate_side_extents": 0, "refinement_candidate_side_extents": 0,
"refinement_candidates_accepted": 0, "refinement_candidates_accepted": 0,
@ -283,23 +373,25 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 90, "score_component_calls": 90,
"score_component_total_ns": 410130, "score_component_total_ns": 97738,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 1, "static_safe_cache_hits": 1,
"static_tree_rebuilds": 2, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 6,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 6, "verify_path_report_calls": 6,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 3, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 0,
"visibility_tangent_candidate_scans": 13, "visibility_tangent_candidate_scans": 13,
"warm_start_paths_built": 2, "warm_start_paths_built": 2,
@ -311,28 +403,49 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.24581307696644217, "duration_s": 0.23484283208381385,
"metrics": { "metrics": {
"congestion_cache_hits": 2, "congestion_cache_hits": 2,
"congestion_cache_misses": 412, "congestion_cache_misses": 155,
"congestion_check_calls": 412, "congestion_candidate_ids": 19,
"congestion_exact_pair_checks": 66, "congestion_candidate_nets": 15,
"danger_map_cache_hits": 1386, "congestion_candidate_precheck_hits": 135,
"danger_map_cache_misses": 693, "congestion_candidate_precheck_misses": 22,
"danger_map_lookup_calls": 2079, "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_query_calls": 0,
"danger_map_total_ns": 1805113, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 3, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 37, "dynamic_path_objects_added": 49,
"dynamic_path_objects_removed": 25, "dynamic_path_objects_removed": 37,
"dynamic_tree_rebuilds": 12, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 253,
"move_cache_abs_misses": 1371, "move_cache_abs_misses": 1371,
"move_cache_rel_hits": 1269, "move_cache_rel_hits": 1269,
"move_cache_rel_misses": 102, "move_cache_rel_misses": 102,
"moves_added": 681, "moves_added": 681,
"moves_generated": 1624, "moves_generated": 1624,
"nets_carried_forward": 0,
"nets_reached_target": 6, "nets_reached_target": 6,
"nets_routed": 6, "nets_routed": 6,
"nodes_expanded": 286, "nodes_expanded": 286,
@ -360,23 +473,25 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 1198, "score_component_calls": 1198,
"score_component_total_ns": 4292875, "score_component_total_ns": 1194981,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 3, "static_safe_cache_hits": 3,
"static_tree_rebuilds": 1, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_exact_pair_checks": 2, "verify_dynamic_candidate_nets": 8,
"verify_path_report_calls": 12, "verify_dynamic_exact_pair_checks": 12,
"verify_path_report_calls": 15,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 3, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 3,
"visibility_corner_pairs_checked": 0, "visibility_corner_pairs_checked": 0,
"visibility_corner_queries_exact": 0, "visibility_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 9,
"visibility_tangent_candidate_scans": 280, "visibility_tangent_candidate_scans": 280,
"warm_start_paths_built": 2, "warm_start_paths_built": 2,
@ -388,28 +503,49 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 4.1186372829834, "duration_s": 0.19533946400042623,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 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_hits": 1183,
"danger_map_cache_misses": 731, "danger_map_cache_misses": 731,
"danger_map_lookup_calls": 1914, "danger_map_lookup_calls": 1914,
"danger_map_query_calls": 731, "danger_map_query_calls": 731,
"danger_map_total_ns": 18374289, "danger_map_total_ns": 18697751,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 36, "dynamic_path_objects_added": 54,
"dynamic_path_objects_removed": 18, "dynamic_path_objects_removed": 36,
"dynamic_tree_rebuilds": 6, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 18, "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_hits": 186,
"move_cache_abs_misses": 840, "move_cache_abs_misses": 840,
"move_cache_rel_hits": 702, "move_cache_rel_hits": 702,
"move_cache_rel_misses": 138, "move_cache_rel_misses": 138,
"moves_added": 629, "moves_added": 629,
"moves_generated": 1026, "moves_generated": 1026,
"nets_carried_forward": 0,
"nets_reached_target": 3, "nets_reached_target": 3,
"nets_routed": 3, "nets_routed": 3,
"nodes_expanded": 240, "nodes_expanded": 240,
@ -417,16 +553,16 @@
"pruned_closed_set": 108, "pruned_closed_set": 108,
"pruned_cost": 204, "pruned_cost": 204,
"pruned_hard_collision": 85, "pruned_hard_collision": 85,
"ray_cast_calls": 40530, "ray_cast_calls": 682,
"ray_cast_calls_expand_forward": 237, "ray_cast_calls_expand_forward": 237,
"ray_cast_calls_expand_snap": 3, "ray_cast_calls_expand_snap": 3,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 408, "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_query": 0,
"ray_cast_calls_visibility_tangent": 34, "ray_cast_calls_visibility_tangent": 34,
"ray_cast_candidate_bounds": 121732, "ray_cast_candidate_bounds": 97,
"ray_cast_exact_geometry_checks": 36858, "ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 3, "refine_path_calls": 3,
"refinement_candidate_side_extents": 0, "refinement_candidate_side_extents": 0,
"refinement_candidates_accepted": 0, "refinement_candidates_accepted": 0,
@ -437,23 +573,25 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 3, "route_iterations": 3,
"score_component_calls": 842, "score_component_calls": 842,
"score_component_total_ns": 20652599, "score_component_total_ns": 21016472,
"static_net_tree_rebuilds": 3, "static_net_tree_rebuilds": 3,
"static_raw_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3,
"static_safe_cache_hits": 141, "static_safe_cache_hits": 141,
"static_tree_rebuilds": 6, "static_tree_rebuilds": 3,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 9, "verify_path_report_calls": 9,
"verify_static_buffer_ops": 54, "verify_static_buffer_ops": 54,
"visibility_builds": 6, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 34,
"visibility_tangent_candidate_scans": 237, "visibility_tangent_candidate_scans": 237,
"warm_start_paths_built": 3, "warm_start_paths_built": 3,
@ -465,28 +603,49 @@
"valid_results": 3 "valid_results": 3
}, },
{ {
"duration_s": 1.373430646955967, "duration_s": 0.19448363897390664,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 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_hits": 233,
"danger_map_cache_misses": 448, "danger_map_cache_misses": 448,
"danger_map_lookup_calls": 681, "danger_map_lookup_calls": 681,
"danger_map_query_calls": 448, "danger_map_query_calls": 448,
"danger_map_total_ns": 10728422, "danger_map_total_ns": 10973251,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 88, "dynamic_path_objects_added": 132,
"dynamic_path_objects_removed": 44, "dynamic_path_objects_removed": 88,
"dynamic_tree_rebuilds": 20, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 6,
"move_cache_abs_misses": 366, "move_cache_abs_misses": 366,
"move_cache_rel_hits": 275, "move_cache_rel_hits": 275,
"move_cache_rel_misses": 91, "move_cache_rel_misses": 91,
"moves_added": 227, "moves_added": 227,
"moves_generated": 372, "moves_generated": 372,
"nets_carried_forward": 0,
"nets_reached_target": 10, "nets_reached_target": 10,
"nets_routed": 10, "nets_routed": 10,
"nodes_expanded": 78, "nodes_expanded": 78,
@ -494,16 +653,16 @@
"pruned_closed_set": 20, "pruned_closed_set": 20,
"pruned_cost": 64, "pruned_cost": 64,
"pruned_hard_collision": 61, "pruned_hard_collision": 61,
"ray_cast_calls": 11151, "ray_cast_calls": 383,
"ray_cast_calls_expand_forward": 68, "ray_cast_calls_expand_forward": 68,
"ray_cast_calls_expand_snap": 6, "ray_cast_calls_expand_snap": 6,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 232, "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_query": 0,
"ray_cast_calls_visibility_tangent": 77, "ray_cast_calls_visibility_tangent": 77,
"ray_cast_candidate_bounds": 21198, "ray_cast_candidate_bounds": 683,
"ray_cast_exact_geometry_checks": 11651, "ray_cast_exact_geometry_checks": 150,
"refine_path_calls": 10, "refine_path_calls": 10,
"refinement_candidate_side_extents": 0, "refinement_candidate_side_extents": 0,
"refinement_candidates_accepted": 0, "refinement_candidates_accepted": 0,
@ -514,23 +673,25 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 291, "score_component_calls": 291,
"score_component_total_ns": 11574800, "score_component_total_ns": 11824081,
"static_net_tree_rebuilds": 10, "static_net_tree_rebuilds": 10,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 6, "static_safe_cache_hits": 6,
"static_tree_rebuilds": 10, "static_tree_rebuilds": 10,
"timeout_events": 0, "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_path_report_calls": 30,
"verify_static_buffer_ops": 132, "verify_static_buffer_ops": 132,
"visibility_builds": 11, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 77,
"visibility_tangent_candidate_scans": 68, "visibility_tangent_candidate_scans": 68,
"warm_start_paths_built": 10, "warm_start_paths_built": 10,
@ -542,28 +703,49 @@
"valid_results": 10 "valid_results": 10
}, },
{ {
"duration_s": 0.2410298540489748, "duration_s": 0.017700672964565456,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 0, "congestion_exact_pair_checks": 0,
"danger_map_cache_hits": 58, "congestion_grid_net_cache_hits": 0,
"danger_map_cache_misses": 110, "congestion_grid_net_cache_misses": 0,
"danger_map_lookup_calls": 168, "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_query_calls": 0,
"danger_map_total_ns": 178104, "danger_map_total_ns": 0,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 12, "dynamic_path_objects_added": 18,
"dynamic_path_objects_removed": 6, "dynamic_path_objects_removed": 12,
"dynamic_tree_rebuilds": 4, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 2,
"move_cache_abs_misses": 76, "move_cache_abs_misses": 76,
"move_cache_rel_hits": 32, "move_cache_rel_hits": 32,
"move_cache_rel_misses": 44, "move_cache_rel_misses": 44,
"moves_added": 56, "moves_added": 56,
"moves_generated": 78, "moves_generated": 78,
"nets_carried_forward": 0,
"nets_reached_target": 2, "nets_reached_target": 2,
"nets_routed": 2, "nets_routed": 2,
"nodes_expanded": 18, "nodes_expanded": 18,
@ -571,16 +753,16 @@
"pruned_closed_set": 6, "pruned_closed_set": 6,
"pruned_cost": 16, "pruned_cost": 16,
"pruned_hard_collision": 0, "pruned_hard_collision": 0,
"ray_cast_calls": 2308, "ray_cast_calls": 56,
"ray_cast_calls_expand_forward": 16, "ray_cast_calls_expand_forward": 16,
"ray_cast_calls_expand_snap": 2, "ray_cast_calls_expand_snap": 2,
"ray_cast_calls_other": 0, "ray_cast_calls_other": 0,
"ray_cast_calls_straight_static": 38, "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_query": 0,
"ray_cast_calls_visibility_tangent": 0, "ray_cast_calls_visibility_tangent": 0,
"ray_cast_candidate_bounds": 3802, "ray_cast_candidate_bounds": 0,
"ray_cast_exact_geometry_checks": 1904, "ray_cast_exact_geometry_checks": 0,
"refine_path_calls": 2, "refine_path_calls": 2,
"refinement_candidate_side_extents": 0, "refinement_candidate_side_extents": 0,
"refinement_candidates_accepted": 0, "refinement_candidates_accepted": 0,
@ -591,18 +773,20 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 2, "route_iterations": 2,
"score_component_calls": 72, "score_component_calls": 72,
"score_component_total_ns": 352865, "score_component_total_ns": 85969,
"static_net_tree_rebuilds": 2, "static_net_tree_rebuilds": 2,
"static_raw_tree_rebuilds": 0, "static_raw_tree_rebuilds": 0,
"static_safe_cache_hits": 2, "static_safe_cache_hits": 2,
"static_tree_rebuilds": 2, "static_tree_rebuilds": 0,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 6, "verify_path_report_calls": 6,
"verify_static_buffer_ops": 0, "verify_static_buffer_ops": 0,
"visibility_builds": 4, "visibility_builds": 0,
"visibility_corner_hits_exact": 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_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
@ -619,28 +803,49 @@
"valid_results": 2 "valid_results": 2
}, },
{ {
"duration_s": 0.0052388140466064215, "duration_s": 0.005781985004432499,
"metrics": { "metrics": {
"congestion_cache_hits": 0, "congestion_cache_hits": 0,
"congestion_cache_misses": 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_check_calls": 0,
"congestion_exact_pair_checks": 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_hits": 10,
"danger_map_cache_misses": 20, "danger_map_cache_misses": 20,
"danger_map_lookup_calls": 30, "danger_map_lookup_calls": 30,
"danger_map_query_calls": 20, "danger_map_query_calls": 20,
"danger_map_total_ns": 502052, "danger_map_total_ns": 536009,
"dynamic_grid_rebuilds": 0, "dynamic_grid_rebuilds": 0,
"dynamic_path_objects_added": 1, "dynamic_path_objects_added": 2,
"dynamic_path_objects_removed": 0, "dynamic_path_objects_removed": 1,
"dynamic_tree_rebuilds": 1, "dynamic_tree_rebuilds": 0,
"hard_collision_cache_hits": 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_hits": 0,
"move_cache_abs_misses": 16, "move_cache_abs_misses": 16,
"move_cache_rel_hits": 2, "move_cache_rel_hits": 2,
"move_cache_rel_misses": 14, "move_cache_rel_misses": 14,
"moves_added": 10, "moves_added": 10,
"moves_generated": 16, "moves_generated": 16,
"nets_carried_forward": 0,
"nets_reached_target": 0, "nets_reached_target": 0,
"nets_routed": 1, "nets_routed": 1,
"nodes_expanded": 3, "nodes_expanded": 3,
@ -668,23 +873,25 @@
"refinement_windows_considered": 0, "refinement_windows_considered": 0,
"route_iterations": 1, "route_iterations": 1,
"score_component_calls": 14, "score_component_calls": 14,
"score_component_total_ns": 538947, "score_component_total_ns": 574907,
"static_net_tree_rebuilds": 1, "static_net_tree_rebuilds": 1,
"static_raw_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1,
"static_safe_cache_hits": 0, "static_safe_cache_hits": 0,
"static_tree_rebuilds": 0, "static_tree_rebuilds": 1,
"timeout_events": 0, "timeout_events": 0,
"verify_dynamic_candidate_nets": 0,
"verify_dynamic_exact_pair_checks": 0, "verify_dynamic_exact_pair_checks": 0,
"verify_path_report_calls": 1, "verify_path_report_calls": 1,
"verify_static_buffer_ops": 1, "verify_static_buffer_ops": 1,
"visibility_builds": 0, "visibility_builds": 0,
"visibility_corner_hits_exact": 0, "visibility_corner_hits_exact": 0,
"visibility_corner_index_builds": 1,
"visibility_corner_pairs_checked": 0, "visibility_corner_pairs_checked": 0,
"visibility_corner_queries_exact": 0, "visibility_corner_queries_exact": 0,
"visibility_point_cache_hits": 0, "visibility_point_cache_hits": 0,
"visibility_point_cache_misses": 0, "visibility_point_cache_misses": 0,
"visibility_point_queries": 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_ray_tests": 0,
"visibility_tangent_candidate_scans": 3, "visibility_tangent_candidate_scans": 3,
"warm_start_paths_built": 0, "warm_start_paths_built": 0,

View file

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import numpy import numpy
@ -28,6 +29,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)) 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: class RoutingWorld:
""" """
Internal spatial state for collision detection, congestion, and verification. Internal spatial state for collision detection, congestion, and verification.
@ -102,6 +146,9 @@ class RoutingWorld:
def remove_path(self, net_id: str) -> None: def remove_path(self, net_id: str) -> None:
self._dynamic_paths.remove_path(net_id) self._dynamic_paths.remove_path(net_id)
def has_dynamic_paths(self) -> bool:
return bool(self._dynamic_paths.geometries)
def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool: def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool:
reach = self.ray_cast( reach = self.ray_cast(
start_port, start_port,
@ -235,105 +282,383 @@ class RoutingWorld:
return False return False
def _check_real_congestion(self, result: ComponentResult, net_id: str) -> int: def _check_real_congestion(
self,
result: ComponentResult,
candidates_by_net: dict[str, dict[int, tuple[int, ...]]],
frozen_net_ids: frozenset[str] = frozenset(),
) -> DynamicCongestionDetail:
if not candidates_by_net:
return DynamicCongestionDetail()
dynamic_paths = self._dynamic_paths dynamic_paths = self._dynamic_paths
self._ensure_dynamic_tree()
if dynamic_paths.tree is None:
return 0
total_bounds = result.total_dilated_bounds
dynamic_bounds = dynamic_paths.bounds_array
possible_total = (
(total_bounds[0] < dynamic_bounds[:, 2])
& (total_bounds[2] > dynamic_bounds[:, 0])
& (total_bounds[1] < dynamic_bounds[:, 3])
& (total_bounds[3] > dynamic_bounds[:, 1])
)
valid_hits_mask = dynamic_paths.net_ids_array != net_id
if not numpy.any(possible_total & valid_hits_mask):
return 0
geometries_to_test = result.dilated_collision_geometry geometries_to_test = result.dilated_collision_geometry
res_indices, tree_indices = dynamic_paths.tree.query(geometries_to_test, predicate="intersects")
if tree_indices.size == 0:
return 0
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
unique_other_nets = numpy.unique(hit_net_ids[hit_net_ids != net_id])
if unique_other_nets.size == 0:
return 0
tree_geometries = dynamic_paths.tree.geometries
real_hits_count = 0 real_hits_count = 0
for other_net_id in unique_other_nets: for other_net_id, other_obj_ids in candidates_by_net.items():
other_mask = hit_net_ids == other_net_id
sub_tree_indices = tree_indices[other_mask]
sub_res_indices = res_indices[other_mask]
found_real = False found_real = False
for index in range(len(sub_tree_indices)): for obj_id, test_geometry_indexes in other_obj_ids.items():
tree_geometry = dynamic_paths.dilated[obj_id]
for test_geometry_index in test_geometry_indexes:
test_geometry = geometries_to_test[test_geometry_index]
if self.metrics is not None: if self.metrics is not None:
self.metrics.total_congestion_exact_pair_checks += 1 self.metrics.total_congestion_exact_pair_checks += 1
test_geometry = geometries_to_test[sub_res_indices[index]] if _has_non_touching_overlap(test_geometry, tree_geometry):
tree_geometry = tree_geometries[sub_tree_indices[index]]
if not test_geometry.touches(tree_geometry) and test_geometry.intersection(tree_geometry).area > 1e-7:
found_real = True found_real = True
break break
if found_real: if found_real:
break
if found_real:
if other_net_id in frozen_net_ids:
return DynamicCongestionDetail(
soft_overlap_count=real_hits_count,
hits_frozen_net=True,
)
real_hits_count += 1 real_hits_count += 1
return real_hits_count return DynamicCongestionDetail(soft_overlap_count=real_hits_count)
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int: def _collect_congestion_candidates(
self,
result: ComponentResult,
net_id: str,
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
) -> dict[str, dict[int, tuple[int, ...]]]:
dynamic_paths = self._dynamic_paths
if not dynamic_paths.dilated:
return {}
self._ensure_dynamic_grid()
if not dynamic_paths.grid:
return {}
candidates_by_net: dict[str, dict[int, set[int]]] = {}
for test_geometry_index, test_bounds in enumerate(result.dilated_bounds):
if not self._has_possible_congestion_in_grid(test_bounds, net_id):
continue
envelope_net_ids = self._get_net_envelope_candidates(
test_bounds,
net_id,
net_envelope_cache,
)
if not envelope_net_ids:
continue
grid_net_ids = self._get_grid_span_net_candidates(
test_bounds,
net_id,
grid_net_cache,
)
if not grid_net_ids:
continue
candidate_net_ids = tuple(sorted(set(envelope_net_ids) & set(grid_net_ids)))
if not candidate_net_ids:
continue
grid_candidates = self._get_grid_span_candidates(
test_bounds,
net_id,
broad_phase_cache,
)
for other_net_id in candidate_net_ids:
if self.metrics is not None:
self.metrics.total_congestion_candidate_nets += 1
obj_ids = grid_candidates.get(other_net_id)
if not obj_ids:
continue
for obj_id in obj_ids:
if not _bounds_overlap(test_bounds, dynamic_paths.dilated_bounds[obj_id]):
continue
if self.metrics is not None:
self.metrics.total_congestion_candidate_ids += 1
candidate_indexes = candidates_by_net.setdefault(other_net_id, {}).setdefault(obj_id, set())
candidate_indexes.add(test_geometry_index)
return {
other_net_id: {
obj_id: tuple(sorted(test_geometry_indexes))
for obj_id, test_geometry_indexes in sorted(obj_ids.items())
}
for other_net_id, obj_ids in candidates_by_net.items()
}
def _has_possible_congestion_in_grid(
self,
bounds: tuple[float, float, float, float],
net_id: str,
) -> bool:
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
dynamic_paths = self._dynamic_paths
if gx_min == gx_max and gy_min == gy_max:
net_counts = dynamic_paths.grid_net_counts.get((gx_min, gy_min))
return bool(net_counts and (len(net_counts) > 1 or net_id not in net_counts))
for gx in range(gx_min, gx_max + 1):
for gy in range(gy_min, gy_max + 1):
net_counts = dynamic_paths.grid_net_counts.get((gx, gy))
if net_counts and (len(net_counts) > 1 or net_id not in net_counts):
return True
return False
def has_possible_move_congestion(
self,
result: ComponentResult,
net_id: str,
presence_cache: dict[tuple[str, int, int, int, int], bool] | None = None,
) -> bool:
dynamic_paths = self._dynamic_paths
if not dynamic_paths.dilated:
return False
self._ensure_dynamic_grid()
if not dynamic_paths.grid:
return False
for bounds in result.dilated_bounds:
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
if presence_cache is not None and cache_key in presence_cache:
if self.metrics is not None:
self.metrics.total_congestion_presence_cache_hits += 1
has_possible = presence_cache[cache_key]
else:
if self.metrics is not None:
self.metrics.total_congestion_presence_cache_misses += 1
has_possible = self._has_possible_congestion_in_grid(bounds, net_id)
if presence_cache is not None:
presence_cache[cache_key] = has_possible
if has_possible:
return True
return False
def has_candidate_move_congestion(
self,
result: ComponentResult,
net_id: str,
candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] | None = None,
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
) -> bool:
dynamic_paths = self._dynamic_paths
if not dynamic_paths.dilated:
return False
for bounds in result.dilated_bounds:
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
if candidate_precheck_cache is not None and cache_key in candidate_precheck_cache:
if self.metrics is not None:
self.metrics.total_congestion_candidate_precheck_hits += 1
has_candidates = candidate_precheck_cache[cache_key]
else:
if self.metrics is not None:
self.metrics.total_congestion_candidate_precheck_misses += 1
span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size)
envelope_net_ids = self._get_net_envelope_candidates(
span_bounds,
net_id,
net_envelope_cache,
)
if not envelope_net_ids:
has_candidates = False
else:
grid_net_ids = self._get_grid_span_net_candidates(
span_bounds,
net_id,
grid_net_cache,
)
if not grid_net_ids:
has_candidates = False
else:
has_candidates = bool(set(envelope_net_ids) & set(grid_net_ids))
if candidate_precheck_cache is not None:
candidate_precheck_cache[cache_key] = has_candidates
if has_candidates:
return True
return False
def _get_grid_span_candidates(
self,
bounds: tuple[float, float, float, float],
net_id: str,
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None,
) -> dict[str, tuple[int, ...]]:
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
if broad_phase_cache is not None and cache_key in broad_phase_cache:
if self.metrics is not None:
self.metrics.total_congestion_grid_span_cache_hits += 1
return broad_phase_cache[cache_key]
if self.metrics is not None:
self.metrics.total_congestion_grid_span_cache_misses += 1
dynamic_paths = self._dynamic_paths
candidates_by_net: dict[str, set[int]] = {}
for gx in range(gx_min, gx_max + 1):
for gy in range(gy_min, gy_max + 1):
for other_net_id, obj_ids in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).items():
if other_net_id == net_id:
continue
candidates_by_net.setdefault(other_net_id, set()).update(obj_ids)
frozen = {
other_net_id: tuple(sorted(obj_ids))
for other_net_id, obj_ids in sorted(candidates_by_net.items())
}
if broad_phase_cache is not None:
broad_phase_cache[cache_key] = frozen
return frozen
def _get_grid_span_net_candidates(
self,
bounds: tuple[float, float, float, float],
net_id: str,
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None,
) -> tuple[str, ...]:
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
if grid_net_cache is not None and cache_key in grid_net_cache:
if self.metrics is not None:
self.metrics.total_congestion_grid_net_cache_hits += 1
return grid_net_cache[cache_key]
if self.metrics is not None:
self.metrics.total_congestion_grid_net_cache_misses += 1
dynamic_paths = self._dynamic_paths
candidate_net_ids: set[str] = set()
for gx in range(gx_min, gx_max + 1):
for gy in range(gy_min, gy_max + 1):
for other_net_id in dynamic_paths.grid_net_obj_ids.get((gx, gy), {}):
if other_net_id != net_id:
candidate_net_ids.add(other_net_id)
frozen = tuple(sorted(candidate_net_ids))
if grid_net_cache is not None:
grid_net_cache[cache_key] = frozen
return frozen
def _get_net_envelope_candidates(
self,
bounds: tuple[float, float, float, float],
net_id: str,
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None,
) -> tuple[str, ...]:
dynamic_paths = self._dynamic_paths
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
cache_key = (net_id, gx_min, gy_min, gx_max, gy_max)
if net_envelope_cache is not None and cache_key in net_envelope_cache:
if self.metrics is not None:
self.metrics.total_congestion_net_envelope_cache_hits += 1
cached_net_ids = net_envelope_cache[cache_key]
else:
if self.metrics is not None:
self.metrics.total_congestion_net_envelope_cache_misses += 1
span_bounds = _span_to_bounds(gx_min, gy_min, gx_max, gy_max, self.grid_cell_size)
cached_net_ids = tuple(
sorted(
dynamic_paths.net_envelope_obj_to_net[obj_id]
for obj_id in dynamic_paths.net_envelope_index.intersection(span_bounds)
if dynamic_paths.net_envelope_obj_to_net[obj_id] != net_id
)
)
if net_envelope_cache is not None:
net_envelope_cache[cache_key] = cached_net_ids
return tuple(
other_net_id
for other_net_id in cached_net_ids
if _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id])
)
def _get_verify_net_envelope_candidates(
self,
bounds: tuple[float, float, float, float],
net_id: str,
) -> tuple[str, ...]:
dynamic_paths = self._dynamic_paths
candidate_net_ids: list[str] = []
for obj_id in dynamic_paths.net_envelope_index.intersection(bounds):
other_net_id = dynamic_paths.net_envelope_obj_to_net[obj_id]
if other_net_id == net_id:
continue
if not _bounds_overlap(bounds, dynamic_paths.net_envelopes[other_net_id]):
continue
candidate_net_ids.append(other_net_id)
if self.metrics is not None:
self.metrics.total_verify_dynamic_candidate_nets += len(candidate_net_ids)
return tuple(candidate_net_ids)
def _get_verify_grid_span_obj_ids(
self,
bounds: tuple[float, float, float, float],
other_net_id: str,
) -> tuple[int, ...]:
dynamic_paths = self._dynamic_paths
gx_min, gy_min, gx_max, gy_max = grid_cell_span(bounds, self.grid_cell_size)
obj_ids: set[int] = set()
for gx in range(gx_min, gx_max + 1):
for gy in range(gy_min, gy_max + 1):
obj_ids.update(dynamic_paths.grid_net_obj_ids.get((gx, gy), {}).get(other_net_id, ()))
return tuple(sorted(obj_ids))
def check_move_congestion(
self,
result: ComponentResult,
net_id: str,
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
) -> int:
return self.check_move_congestion_detail(
result,
net_id,
net_envelope_cache=net_envelope_cache,
grid_net_cache=grid_net_cache,
broad_phase_cache=broad_phase_cache,
).soft_overlap_count
def check_move_congestion_detail(
self,
result: ComponentResult,
net_id: str,
*,
frozen_net_ids: frozenset[str] = frozenset(),
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] | None = None,
broad_phase_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] | None = None,
) -> DynamicCongestionDetail:
if self.metrics is not None: if self.metrics is not None:
self.metrics.total_congestion_check_calls += 1 self.metrics.total_congestion_check_calls += 1
dynamic_paths = self._dynamic_paths dynamic_paths = self._dynamic_paths
if not dynamic_paths.geometries: if not dynamic_paths.geometries:
return 0 return DynamicCongestionDetail()
total_bounds = result.total_dilated_bounds candidates_by_net = self._collect_congestion_candidates(
self._ensure_dynamic_grid() result,
dynamic_grid = dynamic_paths.grid net_id,
if not dynamic_grid: net_envelope_cache,
return 0 grid_net_cache,
broad_phase_cache,
)
if not candidates_by_net:
return DynamicCongestionDetail()
return self._check_real_congestion(
result,
candidates_by_net,
frozen_net_ids=frozen_net_ids,
)
gx_min, gy_min, gx_max, gy_max = grid_cell_span(total_bounds, self.grid_cell_size) def verify_path_details(self, net_id: str, components: Sequence[ComponentResult]) -> PathVerificationDetail:
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:
if self.metrics is not None: if self.metrics is not None:
self.metrics.total_verify_path_report_calls += 1 self.metrics.total_verify_path_report_calls += 1
static_collision_count = 0 static_collision_count = 0
dynamic_collision_count = 0 dynamic_collision_count = 0
self_collision_count = 0 self_collision_count = 0
total_length = sum(component.length for component in components) total_length = sum(component.length for component in components)
conflicting_net_ids: set[str] = set()
static_obstacles = self._static_obstacles static_obstacles = self._static_obstacles
dynamic_paths = self._dynamic_paths dynamic_paths = self._dynamic_paths
@ -356,43 +681,45 @@ class RoutingWorld:
if not self._is_in_safety_zone(polygon, obj_id, None, None): if not self._is_in_safety_zone(polygon, obj_id, None, None):
static_collision_count += 1 static_collision_count += 1
self._ensure_dynamic_tree() if dynamic_paths.dilated:
if dynamic_paths.tree is not None:
tree_geometries = dynamic_paths.tree.geometries
for component in components: for component in components:
test_geometries = component.dilated_physical_geometry test_geometries = component.dilated_physical_geometry
res_indices, tree_indices = dynamic_paths.tree.query(test_geometries, predicate="intersects")
if tree_indices.size == 0:
continue
hit_net_ids = numpy.take(dynamic_paths.net_ids_array, tree_indices)
component_hits = [] component_hits = []
for index in range(len(tree_indices)): for new_geometry in test_geometries:
if hit_net_ids[index] == str(net_id): for hit_net_id in self._get_verify_net_envelope_candidates(new_geometry.bounds, str(net_id)):
for obj_id in self._get_verify_grid_span_obj_ids(new_geometry.bounds, hit_net_id):
if not _bounds_overlap(new_geometry.bounds, dynamic_paths.dilated_bounds[obj_id]):
continue continue
if self.metrics is not None: if self.metrics is not None:
self.metrics.total_verify_dynamic_exact_pair_checks += 1 self.metrics.total_verify_dynamic_exact_pair_checks += 1
new_geometry = test_geometries[res_indices[index]] tree_geometry = dynamic_paths.dilated[obj_id]
tree_geometry = tree_geometries[tree_indices[index]] if _has_non_touching_overlap(new_geometry, tree_geometry):
if not new_geometry.touches(tree_geometry) and new_geometry.intersection(tree_geometry).area > 1e-7: component_hits.append(hit_net_id)
component_hits.append(hit_net_ids[index]) break
if component_hits: if component_hits:
dynamic_collision_count += len(numpy.unique(component_hits)) unique_hits = tuple(sorted(set(component_hits)))
dynamic_collision_count += len(unique_hits)
conflicting_net_ids.update(unique_hits)
for index, component in enumerate(components): for index, component in enumerate(components):
for other_index in range(index + 2, len(components)): for other_index in range(index + 2, len(components)):
if components_overlap(component, components[other_index], prefer_actual=True): if components_overlap(component, components[other_index], prefer_actual=True):
self_collision_count += 1 self_collision_count += 1
return RoutingReport( return PathVerificationDetail(
report=RoutingReport(
static_collision_count=static_collision_count, static_collision_count=static_collision_count,
dynamic_collision_count=dynamic_collision_count, dynamic_collision_count=dynamic_collision_count,
self_collision_count=self_collision_count, self_collision_count=self_collision_count,
total_length=total_length, total_length=total_length,
),
conflicting_net_ids=tuple(sorted(conflicting_net_ids)),
) )
def verify_path_report(self, net_id: str, components: Sequence[ComponentResult]) -> RoutingReport:
return self.verify_path_details(net_id, components).report
def ray_cast( def ray_cast(
self, self,
origin: Port, origin: Port,

View file

@ -22,9 +22,18 @@ class DynamicPathIndex:
"index", "index",
"geometries", "geometries",
"dilated", "dilated",
"dilated_bounds",
"net_envelope_index",
"net_envelopes",
"net_envelope_obj_ids",
"net_envelope_obj_to_net",
"tree", "tree",
"obj_ids", "obj_ids",
"grid", "grid",
"grid_net_obj_ids",
"grid_net_counts",
"obj_cells",
"net_to_obj_ids",
"id_counter", "id_counter",
"net_ids_array", "net_ids_array",
"bounds_array", "bounds_array",
@ -35,16 +44,69 @@ class DynamicPathIndex:
self.index = rtree.index.Index() self.index = rtree.index.Index()
self.geometries: dict[int, tuple[str, Polygon]] = {} self.geometries: dict[int, tuple[str, Polygon]] = {}
self.dilated: dict[int, Polygon] = {} self.dilated: dict[int, Polygon] = {}
self.dilated_bounds: dict[int, tuple[float, float, float, float]] = {}
self.net_envelope_index = rtree.index.Index()
self.net_envelopes: dict[str, tuple[float, float, float, float]] = {}
self.net_envelope_obj_ids: dict[str, int] = {}
self.net_envelope_obj_to_net: dict[int, str] = {}
self.tree: STRtree | None = None self.tree: STRtree | None = None
self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32) self.obj_ids: numpy.ndarray = numpy.array([], dtype=numpy.int32)
self.grid: dict[tuple[int, int], list[int]] = {} self.grid: dict[tuple[int, int], list[int]] = {}
self.grid_net_obj_ids: dict[tuple[int, int], dict[str, set[int]]] = {}
self.grid_net_counts: dict[tuple[int, int], dict[str, int]] = {}
self.obj_cells: dict[int, tuple[tuple[int, int], ...]] = {}
self.net_to_obj_ids: dict[str, set[int]] = {}
self.id_counter = 0 self.id_counter = 0
self.net_ids_array = numpy.array([], dtype=object) self.net_ids_array = numpy.array([], dtype=object)
self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4) self.bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
def _combine_net_bounds(self, obj_ids: set[int]) -> tuple[float, float, float, float]:
first_obj_id = next(iter(obj_ids))
minx, miny, maxx, maxy = self.dilated_bounds[first_obj_id]
for obj_id in obj_ids:
bounds = self.dilated_bounds[obj_id]
minx = min(minx, bounds[0])
miny = min(miny, bounds[1])
maxx = max(maxx, bounds[2])
maxy = max(maxy, bounds[3])
return (minx, miny, maxx, maxy)
def _set_net_envelope(self, net_id: str, bounds: tuple[float, float, float, float]) -> None:
old_bounds = self.net_envelopes.get(net_id)
if old_bounds is not None:
obj_id = self.net_envelope_obj_ids[net_id]
self.net_envelope_index.delete(obj_id, old_bounds)
else:
obj_id = len(self.net_envelope_obj_ids)
while obj_id in self.net_envelope_obj_to_net:
obj_id += 1
self.net_envelope_obj_ids[net_id] = obj_id
self.net_envelope_obj_to_net[obj_id] = net_id
self.net_envelopes[net_id] = bounds
self.net_envelope_index.insert(self.net_envelope_obj_ids[net_id], bounds)
def _clear_net_envelope(self, net_id: str) -> None:
old_bounds = self.net_envelopes.pop(net_id, None)
obj_id = self.net_envelope_obj_ids.pop(net_id, None)
if old_bounds is None or obj_id is None:
return
self.net_envelope_index.delete(obj_id, old_bounds)
self.net_envelope_obj_to_net.pop(obj_id, None)
def _refresh_net_envelope(self, net_id: str) -> None:
obj_ids = self.net_to_obj_ids.get(net_id)
if not obj_ids:
self._clear_net_envelope(net_id)
return
self._set_net_envelope(net_id, self._combine_net_bounds(obj_ids))
def invalidate_queries(self) -> None: def invalidate_queries(self) -> None:
self.tree = None self.tree = None
self.grid = {} self.grid = {}
self.grid_net_obj_ids = {}
self.grid_net_counts = {}
self.obj_cells = {}
def ensure_tree(self) -> None: def ensure_tree(self) -> None:
if self.tree is None and self.dilated: if self.tree is None and self.dilated:
@ -65,33 +127,97 @@ class DynamicPathIndex:
self.engine.metrics.total_dynamic_grid_rebuilds += 1 self.engine.metrics.total_dynamic_grid_rebuilds += 1
cell_size = self.engine.grid_cell_size cell_size = self.engine.grid_cell_size
for obj_id, polygon in self.dilated.items(): for obj_id, polygon in self.dilated.items():
for cell in iter_grid_cells(polygon.bounds, cell_size): self._register_grid_membership(obj_id, self.geometries[obj_id][0], polygon.bounds, cell_size=cell_size)
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) 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: 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: if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_path_objects_added += len(geometry) self.engine.metrics.total_dynamic_path_objects_added += len(geometry)
cell_size = self.engine.grid_cell_size
for index, polygon in enumerate(geometry): for index, polygon in enumerate(geometry):
obj_id = self.id_counter obj_id = self.id_counter
self.id_counter += 1 self.id_counter += 1
dilated = dilated_geometry[index] dilated = dilated_geometry[index]
dilated_bounds = dilated.bounds
self.geometries[obj_id] = (net_id, polygon) self.geometries[obj_id] = (net_id, polygon)
self.dilated[obj_id] = dilated self.dilated[obj_id] = dilated
self.index.insert(obj_id, dilated.bounds) self.dilated_bounds[obj_id] = dilated_bounds
self.index.insert(obj_id, dilated_bounds)
self.net_to_obj_ids.setdefault(net_id, set()).add(obj_id)
self._register_grid_membership(obj_id, net_id, dilated_bounds, cell_size=cell_size)
self._refresh_net_envelope(net_id)
self.tree = None
def remove_path(self, net_id: str) -> None: def remove_path(self, net_id: str) -> None:
to_remove = [obj_id for obj_id, (existing_net_id, _) in self.geometries.items() if existing_net_id == net_id] to_remove = list(self.net_to_obj_ids.get(net_id, ()))
self.remove_obj_ids(to_remove) self.remove_obj_ids(to_remove)
def remove_obj_ids(self, obj_ids: list[int]) -> None: def remove_obj_ids(self, obj_ids: list[int]) -> None:
if not obj_ids: if not obj_ids:
return return
self.invalidate_queries()
if self.engine.metrics is not None: if self.engine.metrics is not None:
self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids) self.engine.metrics.total_dynamic_path_objects_removed += len(obj_ids)
affected_nets: set[str] = set()
for obj_id in obj_ids: for obj_id in obj_ids:
self.index.delete(obj_id, self.dilated[obj_id].bounds) net_id, _ = self.geometries[obj_id]
affected_nets.add(net_id)
self._unregister_grid_membership(obj_id, net_id)
self.index.delete(obj_id, self.dilated_bounds[obj_id])
del self.geometries[obj_id] del self.geometries[obj_id]
del self.dilated[obj_id] del self.dilated[obj_id]
del self.dilated_bounds[obj_id]
obj_id_set = self.net_to_obj_ids.get(net_id)
if obj_id_set is not None:
obj_id_set.discard(obj_id)
if not obj_id_set:
self.net_to_obj_ids.pop(net_id, None)
for net_id in affected_nets:
self._refresh_net_envelope(net_id)
self.tree = None

View file

@ -45,6 +45,11 @@ class RouteMetrics:
warm_start_paths_used: int warm_start_paths_used: int
refine_path_calls: int refine_path_calls: int
timeout_events: int timeout_events: int
iteration_reverify_calls: int
iteration_reverified_nets: int
iteration_conflicting_nets: int
iteration_conflict_edges: int
nets_carried_forward: int
score_component_calls: int score_component_calls: int
score_component_total_ns: int score_component_total_ns: int
path_cost_calls: int path_cost_calls: int
@ -61,6 +66,19 @@ class RouteMetrics:
hard_collision_cache_hits: int hard_collision_cache_hits: int
congestion_cache_hits: int congestion_cache_hits: int
congestion_cache_misses: int congestion_cache_misses: int
congestion_presence_cache_hits: int
congestion_presence_cache_misses: int
congestion_presence_skips: int
congestion_candidate_precheck_hits: int
congestion_candidate_precheck_misses: int
congestion_candidate_precheck_skips: int
congestion_grid_net_cache_hits: int
congestion_grid_net_cache_misses: int
congestion_grid_span_cache_hits: int
congestion_grid_span_cache_misses: int
congestion_candidate_nets: int
congestion_net_envelope_cache_hits: int
congestion_net_envelope_cache_misses: int
dynamic_path_objects_added: int dynamic_path_objects_added: int
dynamic_path_objects_removed: int dynamic_path_objects_removed: int
dynamic_tree_rebuilds: int dynamic_tree_rebuilds: int
@ -68,6 +86,7 @@ class RouteMetrics:
static_tree_rebuilds: int static_tree_rebuilds: int
static_raw_tree_rebuilds: int static_raw_tree_rebuilds: int
static_net_tree_rebuilds: int static_net_tree_rebuilds: int
visibility_corner_index_builds: int
visibility_builds: int visibility_builds: int
visibility_corner_pairs_checked: int visibility_corner_pairs_checked: int
visibility_corner_queries_exact: int visibility_corner_queries_exact: int
@ -89,9 +108,13 @@ class RouteMetrics:
ray_cast_candidate_bounds: int ray_cast_candidate_bounds: int
ray_cast_exact_geometry_checks: int ray_cast_exact_geometry_checks: int
congestion_check_calls: int congestion_check_calls: int
congestion_lazy_resolutions: int
congestion_lazy_requeues: int
congestion_candidate_ids: int
congestion_exact_pair_checks: int congestion_exact_pair_checks: int
verify_path_report_calls: int verify_path_report_calls: int
verify_static_buffer_ops: int verify_static_buffer_ops: int
verify_dynamic_candidate_nets: int
verify_dynamic_exact_pair_checks: int verify_dynamic_exact_pair_checks: int
refinement_windows_considered: int refinement_windows_considered: int
refinement_static_bounds_checked: int refinement_static_bounds_checked: int

View file

@ -26,6 +26,11 @@ def process_move(
context: AStarContext, context: AStarContext,
metrics: AStarMetrics, metrics: AStarMetrics,
congestion_cache: dict[tuple, int], congestion_cache: dict[tuple, int],
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
config: SearchRunConfig, config: SearchRunConfig,
move_class: MoveKind, move_class: MoveKind,
params: tuple, params: tuple,
@ -109,6 +114,11 @@ def process_move(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config, config,
move_class, move_class,
abs_key, abs_key,
@ -126,6 +136,11 @@ def add_node(
context: AStarContext, context: AStarContext,
metrics: AStarMetrics, metrics: AStarMetrics,
congestion_cache: dict[tuple, int], congestion_cache: dict[tuple, int],
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
config: SearchRunConfig, config: SearchRunConfig,
move_type: MoveKind, move_type: MoveKind,
cache_key: tuple, cache_key: tuple,
@ -164,16 +179,6 @@ def add_node(
return return
context.static_safe_cache.add(cache_key) context.static_safe_cache.add(cache_key)
total_overlaps = 0
if not config.skip_congestion:
if cache_key in congestion_cache:
context.metrics.total_congestion_cache_hits += 1
total_overlaps = congestion_cache[cache_key]
else:
context.metrics.total_congestion_cache_misses += 1
total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
congestion_cache[cache_key] = total_overlaps
if config.self_collision_check and component_hits_ancestor_chain(result, parent): if config.self_collision_check and component_hits_ancestor_chain(result, parent):
return return
@ -181,7 +186,6 @@ def add_node(
result, result,
start_port=parent_p, 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: if config.max_cost is not None and parent.g_cost + move_cost > config.max_cost:
metrics.pruned_cost += 1 metrics.pruned_cost += 1
@ -192,6 +196,41 @@ def add_node(
metrics.total_pruned_cost += 1 metrics.total_pruned_cost += 1
return return
if state in closed_set and closed_set[state] <= parent.g_cost + move_cost + TOLERANCE_LINEAR:
metrics.pruned_closed_set += 1
metrics.total_pruned_closed_set += 1
return
total_overlaps = 0
if not config.skip_congestion and context.cost_evaluator.collision_engine.has_dynamic_paths():
ce = context.cost_evaluator.collision_engine
if ce.has_possible_move_congestion(result, net_id, congestion_presence_cache):
if ce.has_candidate_move_congestion(
result,
net_id,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
):
if cache_key in congestion_cache:
context.metrics.total_congestion_cache_hits += 1
total_overlaps = congestion_cache[cache_key]
else:
context.metrics.total_congestion_cache_misses += 1
total_overlaps = ce.check_move_congestion(
result,
net_id,
net_envelope_cache=congestion_net_envelope_cache,
grid_net_cache=congestion_grid_net_cache,
broad_phase_cache=congestion_grid_span_cache,
)
congestion_cache[cache_key] = total_overlaps
else:
context.metrics.total_congestion_candidate_precheck_skips += 1
else:
context.metrics.total_congestion_presence_skips += 1
move_cost += total_overlaps * context.congestion_penalty
g_cost = parent.g_cost + move_cost g_cost = parent.g_cost + move_cost
if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR: if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR:
metrics.pruned_closed_set += 1 metrics.pruned_closed_set += 1

View file

@ -63,15 +63,19 @@ def _visible_straight_candidates(
return [] return []
visibility_manager = context.visibility_manager visibility_manager = context.visibility_manager
visibility_manager._ensure_current() visibility_manager.ensure_corner_index_current()
context.metrics.total_visibility_tangent_candidate_scans += 1 context.metrics.total_visibility_tangent_candidate_scans += 1
max_bend_radius = max(search_options.bend_radii, default=0.0) max_bend_radius = max(search_options.bend_radii, default=0.0)
if max_bend_radius <= 0 or not visibility_manager.corners: if max_bend_radius <= 0 or not visibility_manager.corners:
return [] return []
reach = max_reach + max_bend_radius reach = max_reach + max_bend_radius
bounds = (current.x - reach, current.y - reach, current.x + reach, current.y + reach) candidate_ids = visibility_manager.get_tangent_corner_candidates(
candidate_ids = list(visibility_manager.corner_index.intersection(bounds)) current,
min_forward=search_options.min_straight_length,
max_forward=reach,
radii=search_options.bend_radii,
)
if not candidate_ids: if not candidate_ids:
return [] return []
@ -141,6 +145,11 @@ def expand_moves(
context: AStarContext, context: AStarContext,
metrics: AStarMetrics, metrics: AStarMetrics,
congestion_cache: dict[tuple, int], congestion_cache: dict[tuple, int],
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool],
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool],
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]],
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]],
config: SearchRunConfig, config: SearchRunConfig,
) -> None: ) -> None:
search_options = context.options.search search_options = context.options.search
@ -185,6 +194,11 @@ def expand_moves(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config, config,
"straight", "straight",
(int(round(proj_t)),), (int(round(proj_t)),),
@ -242,6 +256,11 @@ def expand_moves(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config, config,
"straight", "straight",
(length,), (length,),
@ -270,6 +289,11 @@ def expand_moves(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config, config,
"bend90", "bend90",
(radius, direction), (radius, direction),
@ -304,6 +328,11 @@ def expand_moves(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config, config,
"sbend", "sbend",
(offset, radius), (offset, radius),

View file

@ -58,7 +58,17 @@ class SearchRunConfig:
class AStarNode: class AStarNode:
__slots__ = ("port", "g_cost", "h_cost", "fh_cost", "parent", "component_result") __slots__ = (
"port",
"g_cost",
"h_cost",
"fh_cost",
"parent",
"component_result",
"base_move_cost",
"cache_key",
"congestion_resolved",
)
def __init__( def __init__(
self, self,
@ -67,6 +77,10 @@ class AStarNode:
h_cost: float, h_cost: float,
parent: AStarNode | None = None, parent: AStarNode | None = None,
component_result: ComponentResult | None = None, component_result: ComponentResult | None = None,
*,
base_move_cost: float = 0.0,
cache_key: tuple | None = None,
congestion_resolved: bool = True,
) -> None: ) -> None:
self.port = port self.port = port
self.g_cost = g_cost self.g_cost = g_cost
@ -74,6 +88,9 @@ class AStarNode:
self.fh_cost = (g_cost + h_cost, h_cost) self.fh_cost = (g_cost + h_cost, h_cost)
self.parent = parent self.parent = parent
self.component_result = component_result self.component_result = component_result
self.base_move_cost = base_move_cost
self.cache_key = cache_key
self.congestion_resolved = congestion_resolved
def __lt__(self, other: AStarNode) -> bool: def __lt__(self, other: AStarNode) -> bool:
return self.fh_cost < other.fh_cost return self.fh_cost < other.fh_cost
@ -94,6 +111,11 @@ class AStarMetrics:
"total_warm_start_paths_used", "total_warm_start_paths_used",
"total_refine_path_calls", "total_refine_path_calls",
"total_timeout_events", "total_timeout_events",
"total_iteration_reverify_calls",
"total_iteration_reverified_nets",
"total_iteration_conflicting_nets",
"total_iteration_conflict_edges",
"total_nets_carried_forward",
"total_score_component_calls", "total_score_component_calls",
"total_score_component_total_ns", "total_score_component_total_ns",
"total_path_cost_calls", "total_path_cost_calls",
@ -110,6 +132,19 @@ class AStarMetrics:
"total_hard_collision_cache_hits", "total_hard_collision_cache_hits",
"total_congestion_cache_hits", "total_congestion_cache_hits",
"total_congestion_cache_misses", "total_congestion_cache_misses",
"total_congestion_presence_cache_hits",
"total_congestion_presence_cache_misses",
"total_congestion_presence_skips",
"total_congestion_candidate_precheck_hits",
"total_congestion_candidate_precheck_misses",
"total_congestion_candidate_precheck_skips",
"total_congestion_grid_net_cache_hits",
"total_congestion_grid_net_cache_misses",
"total_congestion_grid_span_cache_hits",
"total_congestion_grid_span_cache_misses",
"total_congestion_candidate_nets",
"total_congestion_net_envelope_cache_hits",
"total_congestion_net_envelope_cache_misses",
"total_dynamic_path_objects_added", "total_dynamic_path_objects_added",
"total_dynamic_path_objects_removed", "total_dynamic_path_objects_removed",
"total_dynamic_tree_rebuilds", "total_dynamic_tree_rebuilds",
@ -117,6 +152,7 @@ class AStarMetrics:
"total_static_tree_rebuilds", "total_static_tree_rebuilds",
"total_static_raw_tree_rebuilds", "total_static_raw_tree_rebuilds",
"total_static_net_tree_rebuilds", "total_static_net_tree_rebuilds",
"total_visibility_corner_index_builds",
"total_visibility_builds", "total_visibility_builds",
"total_visibility_corner_pairs_checked", "total_visibility_corner_pairs_checked",
"total_visibility_corner_queries_exact", "total_visibility_corner_queries_exact",
@ -138,9 +174,13 @@ class AStarMetrics:
"total_ray_cast_candidate_bounds", "total_ray_cast_candidate_bounds",
"total_ray_cast_exact_geometry_checks", "total_ray_cast_exact_geometry_checks",
"total_congestion_check_calls", "total_congestion_check_calls",
"total_congestion_lazy_resolutions",
"total_congestion_lazy_requeues",
"total_congestion_candidate_ids",
"total_congestion_exact_pair_checks", "total_congestion_exact_pair_checks",
"total_verify_path_report_calls", "total_verify_path_report_calls",
"total_verify_static_buffer_ops", "total_verify_static_buffer_ops",
"total_verify_dynamic_candidate_nets",
"total_verify_dynamic_exact_pair_checks", "total_verify_dynamic_exact_pair_checks",
"total_refinement_windows_considered", "total_refinement_windows_considered",
"total_refinement_static_bounds_checked", "total_refinement_static_bounds_checked",
@ -172,6 +212,11 @@ class AStarMetrics:
self.total_warm_start_paths_used = 0 self.total_warm_start_paths_used = 0
self.total_refine_path_calls = 0 self.total_refine_path_calls = 0
self.total_timeout_events = 0 self.total_timeout_events = 0
self.total_iteration_reverify_calls = 0
self.total_iteration_reverified_nets = 0
self.total_iteration_conflicting_nets = 0
self.total_iteration_conflict_edges = 0
self.total_nets_carried_forward = 0
self.total_score_component_calls = 0 self.total_score_component_calls = 0
self.total_score_component_total_ns = 0 self.total_score_component_total_ns = 0
self.total_path_cost_calls = 0 self.total_path_cost_calls = 0
@ -188,6 +233,19 @@ class AStarMetrics:
self.total_hard_collision_cache_hits = 0 self.total_hard_collision_cache_hits = 0
self.total_congestion_cache_hits = 0 self.total_congestion_cache_hits = 0
self.total_congestion_cache_misses = 0 self.total_congestion_cache_misses = 0
self.total_congestion_presence_cache_hits = 0
self.total_congestion_presence_cache_misses = 0
self.total_congestion_presence_skips = 0
self.total_congestion_candidate_precheck_hits = 0
self.total_congestion_candidate_precheck_misses = 0
self.total_congestion_candidate_precheck_skips = 0
self.total_congestion_grid_net_cache_hits = 0
self.total_congestion_grid_net_cache_misses = 0
self.total_congestion_grid_span_cache_hits = 0
self.total_congestion_grid_span_cache_misses = 0
self.total_congestion_candidate_nets = 0
self.total_congestion_net_envelope_cache_hits = 0
self.total_congestion_net_envelope_cache_misses = 0
self.total_dynamic_path_objects_added = 0 self.total_dynamic_path_objects_added = 0
self.total_dynamic_path_objects_removed = 0 self.total_dynamic_path_objects_removed = 0
self.total_dynamic_tree_rebuilds = 0 self.total_dynamic_tree_rebuilds = 0
@ -195,6 +253,7 @@ class AStarMetrics:
self.total_static_tree_rebuilds = 0 self.total_static_tree_rebuilds = 0
self.total_static_raw_tree_rebuilds = 0 self.total_static_raw_tree_rebuilds = 0
self.total_static_net_tree_rebuilds = 0 self.total_static_net_tree_rebuilds = 0
self.total_visibility_corner_index_builds = 0
self.total_visibility_builds = 0 self.total_visibility_builds = 0
self.total_visibility_corner_pairs_checked = 0 self.total_visibility_corner_pairs_checked = 0
self.total_visibility_corner_queries_exact = 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_candidate_bounds = 0
self.total_ray_cast_exact_geometry_checks = 0 self.total_ray_cast_exact_geometry_checks = 0
self.total_congestion_check_calls = 0 self.total_congestion_check_calls = 0
self.total_congestion_lazy_resolutions = 0
self.total_congestion_lazy_requeues = 0
self.total_congestion_candidate_ids = 0
self.total_congestion_exact_pair_checks = 0 self.total_congestion_exact_pair_checks = 0
self.total_verify_path_report_calls = 0 self.total_verify_path_report_calls = 0
self.total_verify_static_buffer_ops = 0 self.total_verify_static_buffer_ops = 0
self.total_verify_dynamic_candidate_nets = 0
self.total_verify_dynamic_exact_pair_checks = 0 self.total_verify_dynamic_exact_pair_checks = 0
self.total_refinement_windows_considered = 0 self.total_refinement_windows_considered = 0
self.total_refinement_static_bounds_checked = 0 self.total_refinement_static_bounds_checked = 0
@ -249,6 +312,11 @@ class AStarMetrics:
self.total_warm_start_paths_used = 0 self.total_warm_start_paths_used = 0
self.total_refine_path_calls = 0 self.total_refine_path_calls = 0
self.total_timeout_events = 0 self.total_timeout_events = 0
self.total_iteration_reverify_calls = 0
self.total_iteration_reverified_nets = 0
self.total_iteration_conflicting_nets = 0
self.total_iteration_conflict_edges = 0
self.total_nets_carried_forward = 0
self.total_score_component_calls = 0 self.total_score_component_calls = 0
self.total_score_component_total_ns = 0 self.total_score_component_total_ns = 0
self.total_path_cost_calls = 0 self.total_path_cost_calls = 0
@ -265,6 +333,19 @@ class AStarMetrics:
self.total_hard_collision_cache_hits = 0 self.total_hard_collision_cache_hits = 0
self.total_congestion_cache_hits = 0 self.total_congestion_cache_hits = 0
self.total_congestion_cache_misses = 0 self.total_congestion_cache_misses = 0
self.total_congestion_presence_cache_hits = 0
self.total_congestion_presence_cache_misses = 0
self.total_congestion_presence_skips = 0
self.total_congestion_candidate_precheck_hits = 0
self.total_congestion_candidate_precheck_misses = 0
self.total_congestion_candidate_precheck_skips = 0
self.total_congestion_grid_net_cache_hits = 0
self.total_congestion_grid_net_cache_misses = 0
self.total_congestion_grid_span_cache_hits = 0
self.total_congestion_grid_span_cache_misses = 0
self.total_congestion_candidate_nets = 0
self.total_congestion_net_envelope_cache_hits = 0
self.total_congestion_net_envelope_cache_misses = 0
self.total_dynamic_path_objects_added = 0 self.total_dynamic_path_objects_added = 0
self.total_dynamic_path_objects_removed = 0 self.total_dynamic_path_objects_removed = 0
self.total_dynamic_tree_rebuilds = 0 self.total_dynamic_tree_rebuilds = 0
@ -272,6 +353,7 @@ class AStarMetrics:
self.total_static_tree_rebuilds = 0 self.total_static_tree_rebuilds = 0
self.total_static_raw_tree_rebuilds = 0 self.total_static_raw_tree_rebuilds = 0
self.total_static_net_tree_rebuilds = 0 self.total_static_net_tree_rebuilds = 0
self.total_visibility_corner_index_builds = 0
self.total_visibility_builds = 0 self.total_visibility_builds = 0
self.total_visibility_corner_pairs_checked = 0 self.total_visibility_corner_pairs_checked = 0
self.total_visibility_corner_queries_exact = 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_candidate_bounds = 0
self.total_ray_cast_exact_geometry_checks = 0 self.total_ray_cast_exact_geometry_checks = 0
self.total_congestion_check_calls = 0 self.total_congestion_check_calls = 0
self.total_congestion_lazy_resolutions = 0
self.total_congestion_lazy_requeues = 0
self.total_congestion_candidate_ids = 0
self.total_congestion_exact_pair_checks = 0 self.total_congestion_exact_pair_checks = 0
self.total_verify_path_report_calls = 0 self.total_verify_path_report_calls = 0
self.total_verify_static_buffer_ops = 0 self.total_verify_static_buffer_ops = 0
self.total_verify_dynamic_candidate_nets = 0
self.total_verify_dynamic_exact_pair_checks = 0 self.total_verify_dynamic_exact_pair_checks = 0
self.total_refinement_windows_considered = 0 self.total_refinement_windows_considered = 0
self.total_refinement_static_bounds_checked = 0 self.total_refinement_static_bounds_checked = 0
@ -329,6 +415,11 @@ class AStarMetrics:
warm_start_paths_used=self.total_warm_start_paths_used, warm_start_paths_used=self.total_warm_start_paths_used,
refine_path_calls=self.total_refine_path_calls, refine_path_calls=self.total_refine_path_calls,
timeout_events=self.total_timeout_events, timeout_events=self.total_timeout_events,
iteration_reverify_calls=self.total_iteration_reverify_calls,
iteration_reverified_nets=self.total_iteration_reverified_nets,
iteration_conflicting_nets=self.total_iteration_conflicting_nets,
iteration_conflict_edges=self.total_iteration_conflict_edges,
nets_carried_forward=self.total_nets_carried_forward,
score_component_calls=self.total_score_component_calls, score_component_calls=self.total_score_component_calls,
score_component_total_ns=self.total_score_component_total_ns, score_component_total_ns=self.total_score_component_total_ns,
path_cost_calls=self.total_path_cost_calls, path_cost_calls=self.total_path_cost_calls,
@ -345,6 +436,19 @@ class AStarMetrics:
hard_collision_cache_hits=self.total_hard_collision_cache_hits, hard_collision_cache_hits=self.total_hard_collision_cache_hits,
congestion_cache_hits=self.total_congestion_cache_hits, congestion_cache_hits=self.total_congestion_cache_hits,
congestion_cache_misses=self.total_congestion_cache_misses, congestion_cache_misses=self.total_congestion_cache_misses,
congestion_presence_cache_hits=self.total_congestion_presence_cache_hits,
congestion_presence_cache_misses=self.total_congestion_presence_cache_misses,
congestion_presence_skips=self.total_congestion_presence_skips,
congestion_candidate_precheck_hits=self.total_congestion_candidate_precheck_hits,
congestion_candidate_precheck_misses=self.total_congestion_candidate_precheck_misses,
congestion_candidate_precheck_skips=self.total_congestion_candidate_precheck_skips,
congestion_grid_net_cache_hits=self.total_congestion_grid_net_cache_hits,
congestion_grid_net_cache_misses=self.total_congestion_grid_net_cache_misses,
congestion_grid_span_cache_hits=self.total_congestion_grid_span_cache_hits,
congestion_grid_span_cache_misses=self.total_congestion_grid_span_cache_misses,
congestion_candidate_nets=self.total_congestion_candidate_nets,
congestion_net_envelope_cache_hits=self.total_congestion_net_envelope_cache_hits,
congestion_net_envelope_cache_misses=self.total_congestion_net_envelope_cache_misses,
dynamic_path_objects_added=self.total_dynamic_path_objects_added, dynamic_path_objects_added=self.total_dynamic_path_objects_added,
dynamic_path_objects_removed=self.total_dynamic_path_objects_removed, dynamic_path_objects_removed=self.total_dynamic_path_objects_removed,
dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds, dynamic_tree_rebuilds=self.total_dynamic_tree_rebuilds,
@ -352,6 +456,7 @@ class AStarMetrics:
static_tree_rebuilds=self.total_static_tree_rebuilds, static_tree_rebuilds=self.total_static_tree_rebuilds,
static_raw_tree_rebuilds=self.total_static_raw_tree_rebuilds, static_raw_tree_rebuilds=self.total_static_raw_tree_rebuilds,
static_net_tree_rebuilds=self.total_static_net_tree_rebuilds, static_net_tree_rebuilds=self.total_static_net_tree_rebuilds,
visibility_corner_index_builds=self.total_visibility_corner_index_builds,
visibility_builds=self.total_visibility_builds, visibility_builds=self.total_visibility_builds,
visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked, visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked,
visibility_corner_queries_exact=self.total_visibility_corner_queries_exact, 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_candidate_bounds=self.total_ray_cast_candidate_bounds,
ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks, ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks,
congestion_check_calls=self.total_congestion_check_calls, congestion_check_calls=self.total_congestion_check_calls,
congestion_lazy_resolutions=self.total_congestion_lazy_resolutions,
congestion_lazy_requeues=self.total_congestion_lazy_requeues,
congestion_candidate_ids=self.total_congestion_candidate_ids,
congestion_exact_pair_checks=self.total_congestion_exact_pair_checks, congestion_exact_pair_checks=self.total_congestion_exact_pair_checks,
verify_path_report_calls=self.total_verify_path_report_calls, verify_path_report_calls=self.total_verify_path_report_calls,
verify_static_buffer_ops=self.total_verify_static_buffer_ops, verify_static_buffer_ops=self.total_verify_static_buffer_ops,
verify_dynamic_candidate_nets=self.total_verify_dynamic_candidate_nets,
verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks, verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks,
refinement_windows_considered=self.total_refinement_windows_considered, refinement_windows_considered=self.total_refinement_windows_considered,
refinement_static_bounds_checked=self.total_refinement_static_bounds_checked, refinement_static_bounds_checked=self.total_refinement_static_bounds_checked,

View file

@ -30,6 +30,21 @@ class _RoutingState:
timeout_s: float timeout_s: float
initial_paths: dict[str, tuple[ComponentResult, ...]] | None initial_paths: dict[str, tuple[ComponentResult, ...]] | None
accumulated_expanded_nodes: list[tuple[int, int, int]] accumulated_expanded_nodes: list[tuple[int, int, int]]
best_results: dict[str, RoutingResult]
best_completed_nets: int
best_conflict_edges: int
best_dynamic_collisions: int
last_conflict_signature: tuple[tuple[str, str], ...]
last_conflict_edge_count: int
repeated_conflict_count: int
@dataclass(slots=True)
class _IterationReview:
conflicting_nets: set[str]
conflict_edges: set[tuple[str, str]]
completed_net_ids: set[str]
total_dynamic_collisions: int
class PathFinder: class PathFinder:
__slots__ = ( __slots__ = (
@ -136,6 +151,13 @@ class PathFinder:
timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations), timeout_s=max(60.0, 10.0 * num_nets * congestion.max_iterations),
initial_paths=initial_paths, initial_paths=initial_paths,
accumulated_expanded_nodes=[], accumulated_expanded_nodes=[],
best_results={},
best_completed_nets=-1,
best_conflict_edges=10**9,
best_dynamic_collisions=10**9,
last_conflict_signature=(),
last_conflict_edge_count=0,
repeated_conflict_count=0,
) )
if state.initial_paths is None and congestion.warm_start_enabled: if state.initial_paths is None and congestion.warm_start_enabled:
state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order) state.initial_paths = self._build_greedy_warm_start_paths(net_specs, congestion.net_order)
@ -165,6 +187,46 @@ class PathFinder:
) )
return initial_paths return initial_paths
def _replace_installed_paths(self, state: _RoutingState, results: dict[str, RoutingResult]) -> None:
for net_id in state.ordered_net_ids:
self.context.cost_evaluator.collision_engine.remove_path(net_id)
for net_id in state.ordered_net_ids:
result = results.get(net_id)
if result and result.path:
self._install_path(net_id, result.path)
def _update_best_iteration(self, state: _RoutingState, review: _IterationReview) -> bool:
completed_nets = len(review.completed_net_ids)
conflict_edges = len(review.conflict_edges)
dynamic_collisions = review.total_dynamic_collisions
is_better = (
completed_nets > state.best_completed_nets
or (
completed_nets == state.best_completed_nets
and (
conflict_edges < state.best_conflict_edges
or (
conflict_edges == state.best_conflict_edges
and dynamic_collisions < state.best_dynamic_collisions
)
)
)
)
if not is_better:
return False
state.best_results = dict(state.results)
state.best_completed_nets = completed_nets
state.best_conflict_edges = conflict_edges
state.best_dynamic_collisions = dynamic_collisions
return True
def _restore_best_iteration(self, state: _RoutingState) -> None:
if not state.best_results:
return
state.results = dict(state.best_results)
self._replace_installed_paths(state, state.results)
def _route_net_once( def _route_net_once(
self, self,
state: _RoutingState, state: _RoutingState,
@ -235,9 +297,9 @@ class PathFinder:
self, self,
state: _RoutingState, state: _RoutingState,
iteration: int, iteration: int,
reroute_net_ids: set[str],
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None, iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None,
) -> dict[str, RoutingOutcome] | None: ) -> _IterationReview | None:
outcomes: dict[str, RoutingOutcome] = {}
congestion = self.context.options.congestion congestion = self.context.options.congestion
self.metrics.total_route_iterations += 1 self.metrics.total_route_iterations += 1
self.metrics.reset_per_route() self.metrics.reset_per_route()
@ -246,18 +308,63 @@ class PathFinder:
iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None iteration_seed = (congestion.seed + iteration) if congestion.seed is not None else None
random.Random(iteration_seed).shuffle(state.ordered_net_ids) random.Random(iteration_seed).shuffle(state.ordered_net_ids)
for net_id in state.ordered_net_ids: routed_net_ids = [net_id for net_id in state.ordered_net_ids if net_id in reroute_net_ids]
self.metrics.total_nets_carried_forward += len(state.ordered_net_ids) - len(routed_net_ids)
for net_id in routed_net_ids:
if time.monotonic() - state.start_time > state.timeout_s: if time.monotonic() - state.start_time > state.timeout_s:
self.metrics.total_timeout_events += 1 self.metrics.total_timeout_events += 1
return None return None
result = self._route_net_once(state, iteration, net_id) result = self._route_net_once(state, iteration, net_id)
state.results[net_id] = result state.results[net_id] = result
outcomes[net_id] = result.outcome
review = self._reverify_iteration_results(state)
if iteration_callback: if iteration_callback:
iteration_callback(iteration, state.results) iteration_callback(iteration, state.results)
return outcomes return review
def _reverify_iteration_results(self, state: _RoutingState) -> _IterationReview:
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( def _run_iterations(
self, self,
@ -266,10 +373,33 @@ class PathFinder:
) -> bool: ) -> bool:
congestion = self.context.options.congestion congestion = self.context.options.congestion
for iteration in range(congestion.max_iterations): for iteration in range(congestion.max_iterations):
outcomes = self._run_iteration(state, iteration, iteration_callback) review = self._run_iteration(
if outcomes is None: state,
iteration,
set(state.ordered_net_ids),
iteration_callback,
)
if review is None:
return True return True
if not any(outcome in {"colliding", "partial", "unroutable"} for outcome in outcomes.values()): self._update_best_iteration(state, review)
if not any(
result.outcome in {"colliding", "partial", "unroutable"}
for result in state.results.values()
):
return False
current_signature = tuple(sorted(review.conflict_edges))
repeated = (
bool(current_signature)
and (
current_signature == state.last_conflict_signature
or len(current_signature) == state.last_conflict_edge_count
)
)
state.repeated_conflict_count = state.repeated_conflict_count + 1 if repeated else 0
state.last_conflict_signature = current_signature
state.last_conflict_edge_count = len(current_signature)
if state.repeated_conflict_count >= 2:
return False return False
self.context.congestion_penalty *= congestion.multiplier self.context.congestion_penalty *= congestion.multiplier
return False return False
@ -287,12 +417,13 @@ class PathFinder:
self.context.cost_evaluator.collision_engine.remove_path(net_id) self.context.cost_evaluator.collision_engine.remove_path(net_id)
refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path) refined_path = self.refiner.refine_path(net_id, net.start, net.width, result.path)
self._install_path(net_id, refined_path) self._install_path(net_id, refined_path)
report = self.context.cost_evaluator.collision_engine.verify_path_report(net_id, refined_path) # Defer full verification until _verify_results() so we do not
# verify the same refined path twice in one route_all() call.
state.results[net_id] = RoutingResult( state.results[net_id] = RoutingResult(
net_id=net_id, net_id=net_id,
path=tuple(refined_path), path=tuple(refined_path),
reached_target=result.reached_target, reached_target=result.reached_target,
report=report, report=result.report,
) )
def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]: def _verify_results(self, state: _RoutingState) -> dict[str, RoutingResult]:
@ -324,6 +455,7 @@ class PathFinder:
state = self._prepare_state() state = self._prepare_state()
timed_out = self._run_iterations(state, iteration_callback) timed_out = self._run_iterations(state, iteration_callback)
self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes) self.accumulated_expanded_nodes = list(state.accumulated_expanded_nodes)
self._restore_best_iteration(state)
if timed_out: if timed_out:
return self._verify_results(state) return self._verify_results(state)

View file

@ -41,6 +41,11 @@ def route_astar(
open_set: list[_AStarNode] = [] open_set: list[_AStarNode] = []
closed_set: dict[tuple[int, int, int], float] = {} closed_set: dict[tuple[int, int, int], float] = {}
congestion_cache: dict[tuple, int] = {} congestion_cache: dict[tuple, int] = {}
congestion_presence_cache: dict[tuple[str, int, int, int, int], bool] = {}
congestion_candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {}
congestion_net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
congestion_grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
congestion_grid_span_cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {}
start_node = _AStarNode( start_node = _AStarNode(
start, start,
@ -89,6 +94,11 @@ def route_astar(
context, context,
metrics, metrics,
congestion_cache, congestion_cache,
congestion_presence_cache,
congestion_candidate_precheck_cache,
congestion_net_envelope_cache,
congestion_grid_net_cache,
congestion_grid_span_cache,
config=config, config=config,
) )

View file

@ -152,7 +152,8 @@ class CostEvaluator:
weights=active_weights, weights=active_weights,
) )
if danger_map is not None and active_weights.danger_weight: # Skip danger sampling entirely when there are no static obstacles in the KD-tree.
if danger_map is not None and active_weights.danger_weight and danger_map.tree is not None:
cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0 cost_s = danger_map.get_cost(start_port.x, start_port.y) if start_port else 0.0
cost_e = danger_map.get_cost(end_port.x, end_port.y) cost_e = danger_map.get_cost(end_port.x, end_port.y)
if start_port: if start_port:

View file

@ -16,7 +16,15 @@ class VisibilityManager:
""" """
Manages corners of static obstacles for sparse A* / Visibility Graph jumps. Manages corners of static obstacles for sparse A* / Visibility Graph jumps.
""" """
__slots__ = ("collision_engine", "corners", "corner_index", "_corner_graph", "_point_visibility_cache", "_built_static_version") __slots__ = (
"collision_engine",
"corners",
"corner_index",
"_corner_graph",
"_point_visibility_cache",
"_corner_index_version",
"_corner_graph_version",
)
def __init__(self, collision_engine: RoutingWorld) -> None: def __init__(self, collision_engine: RoutingWorld) -> None:
self.collision_engine = collision_engine self.collision_engine = collision_engine
@ -24,8 +32,8 @@ class VisibilityManager:
self.corner_index = rtree.index.Index() self.corner_index = rtree.index.Index()
self._corner_graph: dict[int, list[tuple[float, float, float]]] = {} self._corner_graph: dict[int, list[tuple[float, float, float]]] = {}
self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {} self._point_visibility_cache: dict[tuple[int, int, int], list[tuple[float, float, float]]] = {}
self._built_static_version = -1 self._corner_index_version = -1
self._build() self._corner_graph_version = -1
def clear_cache(self) -> None: def clear_cache(self) -> None:
""" """
@ -35,19 +43,31 @@ class VisibilityManager:
self.corner_index = rtree.index.Index() self.corner_index = rtree.index.Index()
self._corner_graph = {} self._corner_graph = {}
self._point_visibility_cache = {} self._point_visibility_cache = {}
self._build() self._corner_index_version = -1
self._corner_graph_version = -1
def ensure_corner_index_current(self) -> None:
if self._corner_index_version != self.collision_engine.get_static_version():
self._build_corner_index()
def ensure_corner_graph_current(self) -> None:
self.ensure_corner_index_current()
static_version = self.collision_engine.get_static_version()
if self._corner_graph_version != static_version:
self._build_corner_graph()
def _ensure_current(self) -> None: def _ensure_current(self) -> None:
if self._built_static_version != self.collision_engine.get_static_version(): self.ensure_corner_graph_current()
self.clear_cache()
def _build(self) -> None: def _build_corner_index(self) -> None:
"""
Extract corners and pre-compute corner-to-corner visibility.
"""
if self.collision_engine.metrics is not None: if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_builds += 1 self.collision_engine.metrics.total_visibility_corner_index_builds += 1
self._built_static_version = self.collision_engine.get_static_version() self.corners = []
self.corner_index = rtree.index.Index()
self._corner_graph = {}
self._point_visibility_cache = {}
self._corner_graph_version = -1
self._corner_index_version = self.collision_engine.get_static_version()
raw_corners = [] raw_corners = []
for poly in self.collision_engine.iter_static_dilated_geometries(): for poly in self.collision_engine.iter_static_dilated_geometries():
coords = list(poly.exterior.coords) coords = list(poly.exterior.coords)
@ -63,7 +83,6 @@ class VisibilityManager:
if not raw_corners: if not raw_corners:
return return
# Deduplicate repeated corner coordinates
seen = set() seen = set()
for x, y in raw_corners: for x, y in raw_corners:
sx, sy = round(x, 3), round(y, 3) sx, sy = round(x, 3), round(y, 3)
@ -71,10 +90,22 @@ class VisibilityManager:
seen.add((sx, sy)) seen.add((sx, sy))
self.corners.append((sx, sy)) self.corners.append((sx, sy))
# Build spatial index for corners
for i, (x, y) in enumerate(self.corners): for i, (x, y) in enumerate(self.corners):
self.corner_index.insert(i, (x, y, x, y)) self.corner_index.insert(i, (x, y, x, y))
def _build_corner_graph(self) -> None:
"""
Pre-compute corner-to-corner visibility from the current corner index.
"""
self.ensure_corner_index_current()
if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_builds += 1
self._corner_graph = {}
self._corner_graph_version = self.collision_engine.get_static_version()
if not self.corners:
return
# Pre-compute visibility graph between corners # Pre-compute visibility graph between corners
num_corners = len(self.corners) num_corners = len(self.corners)
if num_corners > 200: if num_corners > 200:
@ -98,6 +129,7 @@ class VisibilityManager:
self._corner_graph[i].append((cx, cy, dist)) self._corner_graph[i].append((cx, cy, dist))
def _corner_idx_at(self, origin: Port) -> int | None: def _corner_idx_at(self, origin: Port) -> int | None:
self.ensure_corner_index_current()
ox, oy = round(origin.x, 3), round(origin.y, 3) ox, oy = round(origin.x, 3), round(origin.y, 3)
nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001))) nearby = list(self.corner_index.intersection((ox - 0.001, oy - 0.001, ox + 0.001, oy + 0.001)))
for idx in nearby: for idx in nearby:
@ -106,6 +138,49 @@ class VisibilityManager:
return idx return idx
return None return None
def get_tangent_corner_candidates(
self,
origin: Port,
*,
min_forward: float,
max_forward: float,
radii: tuple[float, ...],
tolerance: float = 2.0,
) -> list[int]:
self.ensure_corner_index_current()
if max_forward <= min_forward or not radii or not self.corners:
return []
candidate_ids: set[int] = set()
x0 = float(origin.x)
y0 = float(origin.y)
def _add_hits(bounds: tuple[float, float, float, float]) -> None:
min_x, min_y, max_x, max_y = bounds
if min_x > max_x or min_y > max_y:
return
candidate_ids.update(self.corner_index.intersection(bounds))
for radius in radii:
if origin.r == 0:
x_bounds = (x0 + min_forward, x0 + max_forward)
_add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance))
_add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance))
elif origin.r == 180:
x_bounds = (x0 - max_forward, x0 - min_forward)
_add_hits((x_bounds[0], y0 + radius - tolerance, x_bounds[1], y0 + radius + tolerance))
_add_hits((x_bounds[0], y0 - radius - tolerance, x_bounds[1], y0 - radius + tolerance))
elif origin.r == 90:
y_bounds = (y0 + min_forward, y0 + max_forward)
_add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1]))
_add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1]))
else:
y_bounds = (y0 - max_forward, y0 - min_forward)
_add_hits((x0 + radius - tolerance, y_bounds[0], x0 + radius + tolerance, y_bounds[1]))
_add_hits((x0 - radius - tolerance, y_bounds[0], x0 - radius + tolerance, y_bounds[1]))
return sorted(candidate_ids)
def get_point_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]: def get_point_visibility(self, origin: Port, max_dist: float = 1000.0) -> list[tuple[float, float, float]]:
""" """
Find visible corners from an arbitrary point. Find visible corners from an arbitrary point.
@ -113,11 +188,13 @@ class VisibilityManager:
""" """
if self.collision_engine.metrics is not None: if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_point_queries += 1 self.collision_engine.metrics.total_visibility_point_queries += 1
self._ensure_current() self.ensure_corner_index_current()
if max_dist < 0: if max_dist < 0:
return [] return []
corner_idx = self._corner_idx_at(origin) corner_idx = self._corner_idx_at(origin)
if corner_idx is not None:
self.ensure_corner_graph_current()
if corner_idx is not None and corner_idx in self._corner_graph: if corner_idx is not None and corner_idx in self._corner_graph:
return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist] return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist]
@ -157,7 +234,7 @@ class VisibilityManager:
""" """
if self.collision_engine.metrics is not None: if self.collision_engine.metrics is not None:
self.collision_engine.metrics.total_visibility_corner_queries_exact += 1 self.collision_engine.metrics.total_visibility_corner_queries_exact += 1
self._ensure_current() self.ensure_corner_graph_current()
if max_dist < 0: if max_dist < 0:
return [] return []

View file

@ -378,6 +378,24 @@ def run_example_06() -> ScenarioOutcome:
def snapshot_example_07() -> ScenarioSnapshot: def snapshot_example_07() -> ScenarioSnapshot:
return _snapshot_example_07_variant(
"example_07_large_scale_routing",
warm_start_enabled=True,
)
def snapshot_example_07_no_warm_start() -> ScenarioSnapshot:
return _snapshot_example_07_variant(
"example_07_large_scale_routing_no_warm_start",
warm_start_enabled=False,
)
def _snapshot_example_07_variant(
name: str,
*,
warm_start_enabled: bool,
) -> ScenarioSnapshot:
bounds = (0, 0, 1000, 1000) bounds = (0, 0, 1000, 1000)
obstacles = [ obstacles = [
box(450, 0, 550, 400), box(450, 0, 550, 400),
@ -420,6 +438,7 @@ def snapshot_example_07() -> ScenarioSnapshot:
"capture_expanded": True, "capture_expanded": True,
"shuffle_nets": True, "shuffle_nets": True,
"seed": 42, "seed": 42,
"warm_start_enabled": warm_start_enabled,
}, },
) )
@ -432,7 +451,7 @@ def snapshot_example_07() -> ScenarioSnapshot:
t0 = perf_counter() t0 = perf_counter()
results = pathfinder.route_all(iteration_callback=iteration_callback) results = pathfinder.route_all(iteration_callback=iteration_callback)
t1 = perf_counter() 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: def run_example_07() -> ScenarioOutcome:
@ -534,6 +553,10 @@ SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
("example_09_unroutable_best_effort", snapshot_example_09), ("example_09_unroutable_best_effort", snapshot_example_09),
) )
PERFORMANCE_SCENARIO_SNAPSHOTS: tuple[tuple[str, ScenarioSnapshotRun], ...] = (
("example_07_large_scale_routing_no_warm_start", snapshot_example_07_no_warm_start),
)
def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]: def capture_all_scenario_snapshots() -> tuple[ScenarioSnapshot, ...]:
return tuple(run() for _, run in SCENARIO_SNAPSHOTS) return tuple(run() for _, run in SCENARIO_SNAPSHOTS)

View file

@ -16,6 +16,12 @@ from inire import (
route, route,
) )
from inire.geometry.components import Straight from inire.geometry.components import Straight
from inire.geometry.collision import RoutingWorld
from inire.results import RoutingReport, RoutingResult
from inire.router._astar_types import AStarContext
from inire.router._router import PathFinder, _IterationReview
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
def test_root_module_exports_only_stable_surface() -> None: def test_root_module_exports_only_stable_surface() -> None:
@ -77,14 +83,177 @@ def test_route_problem_supports_configs_and_debug_data() -> None:
assert run.expanded_nodes assert run.expanded_nodes
assert run.metrics.nodes_expanded > 0 assert run.metrics.nodes_expanded > 0
assert run.metrics.route_iterations >= 1 assert run.metrics.route_iterations >= 1
assert run.metrics.iteration_reverify_calls >= 1
assert run.metrics.iteration_reverified_nets >= 0
assert run.metrics.iteration_conflicting_nets >= 0
assert run.metrics.iteration_conflict_edges >= 0
assert run.metrics.nets_carried_forward >= 0
assert run.metrics.nets_routed >= 1 assert run.metrics.nets_routed >= 1
assert run.metrics.move_cache_abs_misses >= 0 assert run.metrics.move_cache_abs_misses >= 0
assert run.metrics.ray_cast_calls >= 0 assert run.metrics.ray_cast_calls >= 0
assert run.metrics.dynamic_tree_rebuilds >= 0 assert run.metrics.dynamic_tree_rebuilds >= 0
assert run.metrics.visibility_corner_index_builds >= 0
assert run.metrics.visibility_builds >= 0 assert run.metrics.visibility_builds >= 0
assert run.metrics.congestion_grid_span_cache_hits >= 0
assert run.metrics.congestion_grid_span_cache_misses >= 0
assert run.metrics.congestion_presence_cache_hits >= 0
assert run.metrics.congestion_presence_cache_misses >= 0
assert run.metrics.congestion_presence_skips >= 0
assert run.metrics.congestion_candidate_precheck_hits >= 0
assert run.metrics.congestion_candidate_precheck_misses >= 0
assert run.metrics.congestion_candidate_precheck_skips >= 0
assert run.metrics.congestion_candidate_nets >= 0
assert run.metrics.congestion_net_envelope_cache_hits >= 0
assert run.metrics.congestion_net_envelope_cache_misses >= 0
assert run.metrics.congestion_grid_net_cache_hits >= 0
assert run.metrics.congestion_grid_net_cache_misses >= 0
assert run.metrics.congestion_lazy_resolutions >= 0
assert run.metrics.congestion_lazy_requeues >= 0
assert run.metrics.congestion_candidate_ids >= 0
assert run.metrics.verify_dynamic_candidate_nets >= 0
assert run.metrics.verify_path_report_calls >= 0 assert run.metrics.verify_path_report_calls >= 0
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: def test_route_problem_locked_routes_become_static_obstacles() -> None:
locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),) locked = (Straight.generate(Port(10, 50, 0), 80.0, 2.0, dilation=1.0),)
problem = RoutingProblem( problem = RoutingProblem(

View file

@ -1,5 +1,4 @@
import math import math
import pytest import pytest
from shapely.geometry import Polygon 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.components import Bend90, Straight
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router._astar_types import AStarContext, SearchRunConfig from inire.router._astar_types import AStarContext, AStarNode, SearchRunConfig
from inire.router._astar_admission import add_node
from inire.router._search import route_astar from inire.router._search import route_astar
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap from inire.router.danger_map import DangerMap
@ -301,6 +301,27 @@ def test_route_astar_supports_all_visibility_guidance_modes(
assert validation["connectivity_ok"] assert validation["connectivity_ok"]
def test_tangent_corner_mode_avoids_exact_visibility_graph_builds(basic_evaluator: CostEvaluator) -> None:
obstacle = Polygon([(30, 10), (50, 10), (50, 40), (30, 40)])
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
basic_evaluator.danger_map.precompute([obstacle])
context = _build_context(
basic_evaluator,
bounds=BOUNDS,
bend_radii=(10.0,),
sbend_radii=(),
max_straight_length=150.0,
visibility_guidance="tangent_corner",
)
path = _route(context, Port(0, 0, 0), Port(80, 50, 0))
assert path is not None
assert context.metrics.total_visibility_builds == 0
assert context.metrics.total_visibility_corner_pairs_checked == 0
assert context.metrics.total_ray_cast_calls_visibility_build == 0
def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None: def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_evaluator: CostEvaluator) -> None:
context = AStarContext( context = AStarContext(
basic_evaluator, basic_evaluator,
@ -318,3 +339,103 @@ def test_route_astar_repeated_searches_succeed_with_small_cache_limit(basic_eval
path = _route(context, start, target) path = _route(context, start, target)
assert path is not None assert path is not None
assert path[-1].end_port == target assert path[-1].end_port == target
def test_self_collision_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None:
context = _build_context(basic_evaluator, bounds=BOUNDS)
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
parent_result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
parent = AStarNode(parent_result.end_port, g_cost=10.0, h_cost=0.0, parent=root, component_result=parent_result)
open_set: list[AStarNode] = []
closed_set: dict[tuple[int, int, int], float] = {}
add_node(
parent,
parent_result,
target=Port(20, 0, 0),
net_width=2.0,
net_id="netA",
open_set=open_set,
closed_set=closed_set,
context=context,
metrics=context.metrics,
congestion_cache={},
congestion_presence_cache={},
congestion_candidate_precheck_cache={},
congestion_net_envelope_cache={},
congestion_grid_net_cache={},
congestion_grid_span_cache={},
config=SearchRunConfig.from_options(context.options, self_collision_check=True),
move_type="straight",
cache_key=("overlap",),
)
assert not open_set
assert context.metrics.total_congestion_check_calls == 0
assert context.metrics.total_congestion_cache_misses == 0
def test_closed_set_dominance_prunes_before_congestion_check(basic_evaluator: CostEvaluator) -> None:
context = _build_context(basic_evaluator, bounds=BOUNDS)
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
open_set: list[AStarNode] = []
closed_set = {result.end_port.as_tuple(): context.cost_evaluator.score_component(result, start_port=root.port)}
add_node(
root,
result,
target=Port(20, 0, 0),
net_width=2.0,
net_id="netA",
open_set=open_set,
closed_set=closed_set,
context=context,
metrics=context.metrics,
congestion_cache={},
congestion_presence_cache={},
congestion_candidate_precheck_cache={},
congestion_net_envelope_cache={},
congestion_grid_net_cache={},
congestion_grid_span_cache={},
config=SearchRunConfig.from_options(context.options),
move_type="straight",
cache_key=("dominated",),
)
assert not open_set
assert context.metrics.total_congestion_check_calls == 0
assert context.metrics.total_congestion_cache_misses == 0
def test_no_dynamic_paths_skips_congestion_check(basic_evaluator: CostEvaluator) -> None:
context = _build_context(basic_evaluator, bounds=BOUNDS)
root = AStarNode(Port(0, 0, 0), 0.0, 0.0)
result = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
open_set: list[AStarNode] = []
closed_set: dict[tuple[int, int, int], float] = {}
add_node(
root,
result,
target=Port(20, 0, 0),
net_width=2.0,
net_id="netA",
open_set=open_set,
closed_set=closed_set,
context=context,
metrics=context.metrics,
congestion_cache={},
congestion_presence_cache={},
congestion_candidate_precheck_cache={},
congestion_net_envelope_cache={},
congestion_grid_net_cache={},
congestion_grid_span_cache={},
config=SearchRunConfig.from_options(context.options),
move_type="straight",
cache_key=("no-dynamic",),
)
assert open_set
assert context.metrics.total_congestion_check_calls == 0
assert context.metrics.total_congestion_cache_misses == 0

View file

@ -1,6 +1,11 @@
from shapely.geometry import box
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.components import ComponentResult
from inire.geometry.components import Straight from inire.geometry.components import Straight
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router._astar_types import AStarMetrics
from inire.seeds import StraightSeed
def _install_static_straight( def _install_static_straight(
@ -82,6 +87,7 @@ def test_check_move_static_clearance() -> None:
def test_verify_path_report_preserves_long_net_id() -> None: def test_verify_path_report_preserves_long_net_id() -> None:
engine = RoutingWorld(clearance=2.0) engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789" net_id = "net_abcdefghijklmnopqrstuvwxyz_0123456789"
path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)] path = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
geoms = [poly for component in path for poly in component.collision_geometry] geoms = [poly for component in path for poly in component.collision_geometry]
@ -91,10 +97,12 @@ def test_verify_path_report_preserves_long_net_id() -> None:
report = engine.verify_path_report(net_id, path) report = engine.verify_path_report(net_id, path)
assert report.dynamic_collision_count == 0 assert report.dynamic_collision_count == 0
assert engine.metrics.total_verify_dynamic_candidate_nets == 0
def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None: def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> None:
engine = RoutingWorld(clearance=2.0) engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_" shared_prefix = "net_shared_prefix_abcdefghijklmnopqrstuvwxyz_"
net_a = f"{shared_prefix}A" net_a = f"{shared_prefix}A"
net_b = f"{shared_prefix}B" net_b = f"{shared_prefix}B"
@ -115,6 +123,50 @@ def test_verify_path_report_distinguishes_long_net_ids_with_shared_prefix() -> N
report = engine.verify_path_report(net_a, path_a) report = engine.verify_path_report(net_a, path_a)
assert report.dynamic_collision_count == 1 assert report.dynamic_collision_count == 1
assert engine.metrics.total_verify_dynamic_candidate_nets == 1
def test_verify_path_report_uses_net_envelopes_before_dynamic_object_scan() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
path_far = [Straight.generate(Port(100, 100, 0), 20.0, width=2.0, dilation=1.0)]
engine.add_path(
"netB",
[poly for component in path_b for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
)
engine.add_path(
"netFar",
[poly for component in path_far for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_far for poly in component.dilated_collision_geometry],
)
report = engine.verify_path_report("netA", path_a)
assert report.dynamic_collision_count == 1
assert engine.metrics.total_verify_dynamic_candidate_nets == 1
assert engine.metrics.total_verify_dynamic_exact_pair_checks == 1
def test_verify_path_details_returns_conflicting_net_ids() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
path_b = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
engine.add_path(
"netB",
[poly for component in path_b for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
)
detail = engine.verify_path_details("netA", path_a)
assert detail.report.dynamic_collision_count == 1
assert detail.conflicting_net_ids == ("netB",)
def test_remove_path_clears_dynamic_path() -> None: def test_remove_path_clears_dynamic_path() -> None:
@ -129,3 +181,247 @@ def test_remove_path_clears_dynamic_path() -> None:
engine.remove_path("netA") engine.remove_path("netA")
assert list(engine._dynamic_paths.geometries.values()) == [] assert list(engine._dynamic_paths.geometries.values()) == []
assert len(engine._static_obstacles.geometries) == 0 assert len(engine._static_obstacles.geometries) == 0
def test_dynamic_grid_updates_incrementally_on_add_and_remove() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
path_b = [Straight.generate(Port(0, 4, 0), 20.0, width=2.0, dilation=1.0)]
engine.add_path(
"netA",
[poly for component in path_a for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry],
)
engine.add_path(
"netB",
[poly for component in path_b for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
)
dynamic_paths = engine._dynamic_paths
assert dynamic_paths.net_to_obj_ids["netA"]
assert dynamic_paths.net_to_obj_ids["netB"]
assert dynamic_paths.grid
assert engine.metrics.total_dynamic_grid_rebuilds == 0
engine.remove_path("netA")
assert "netA" not in dynamic_paths.net_to_obj_ids
assert "netB" in dynamic_paths.net_to_obj_ids
assert engine.metrics.total_dynamic_grid_rebuilds == 0
assert "netA" not in dynamic_paths.grid_net_obj_ids.get((0, -1), {})
assert "netB" in dynamic_paths.grid_net_obj_ids.get((0, 0), {})
def test_dynamic_net_envelopes_update_incrementally_on_add_and_remove() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
path_a = [Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)]
path_b = [Straight.generate(Port(0, 40, 0), 10.0, width=2.0, dilation=1.0)]
engine.add_path(
"netA",
[poly for component in path_a for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_a for poly in component.dilated_collision_geometry],
)
engine.add_path(
"netB",
[poly for component in path_b for poly in component.collision_geometry],
dilated_geometry=[poly for component in path_b for poly in component.dilated_collision_geometry],
)
dynamic_paths = engine._dynamic_paths
assert set(dynamic_paths.net_envelopes) == {"netA", "netB"}
assert dynamic_paths.net_envelopes["netA"] == (-1.0, -2.0, 21.0, 2.0)
assert dynamic_paths.net_envelopes["netB"] == (-1.0, 38.0, 11.0, 42.0)
assert engine.metrics.total_dynamic_tree_rebuilds == 0
net_b_envelope_obj_id = dynamic_paths.net_envelope_obj_ids["netB"]
assert list(dynamic_paths.net_envelope_index.intersection((-5.0, 35.0, 15.0, 45.0))) == [net_b_envelope_obj_id]
engine.remove_path("netA")
assert "netA" not in dynamic_paths.net_envelopes
assert "netA" not in dynamic_paths.net_envelope_obj_ids
assert "netB" in dynamic_paths.net_envelopes
assert engine.metrics.total_dynamic_tree_rebuilds == 0
def test_congestion_query_uses_per_polygon_bounds() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
blocker = Straight.generate(Port(40, 4, 90), 2.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in blocker.collision_geometry],
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
)
move = ComponentResult(
start_port=Port(0, 0, 0),
collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
end_port=Port(100, 0, 0),
length=100.0,
move_type="straight",
move_spec=StraightSeed(100.0),
physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
dilated_collision_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
dilated_physical_geometry=[box(0, 0, 10, 10), box(90, 0, 100, 10)],
)
assert engine.check_move_congestion(move, "netA") == 0
assert engine.metrics.total_congestion_candidate_nets == 0
assert engine.metrics.total_congestion_candidate_ids == 0
assert engine.metrics.total_congestion_exact_pair_checks == 0
def test_congestion_touching_geometries_do_not_count_as_overlap() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
existing = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
touching = Straight.generate(Port(12, 0, 0), 10.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in existing.collision_geometry],
dilated_geometry=[poly for poly in existing.dilated_collision_geometry],
)
assert engine.check_move_congestion(touching, "netA") == 0
def test_congestion_exact_checks_only_touch_relevant_move_polygons() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
blocker = Straight.generate(Port(0, 0, 0), 10.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in blocker.collision_geometry],
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
)
move = ComponentResult(
start_port=Port(0, 0, 0),
collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
end_port=Port(100, 0, 0),
length=100.0,
move_type="straight",
move_spec=StraightSeed(100.0),
physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
dilated_collision_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
dilated_physical_geometry=[box(0, -2, 10, 2), box(90, -2, 100, 2)],
)
assert engine.check_move_congestion(move, "netA") == 1
assert engine.metrics.total_congestion_candidate_nets == 1
assert engine.metrics.total_congestion_candidate_ids == 1
assert engine.metrics.total_congestion_exact_pair_checks == 1
def test_congestion_grid_span_cache_reuses_broad_phase_candidates() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
cache: dict[tuple[str, int, int, int, int], dict[str, tuple[int, ...]]] = {}
blocker = Straight.generate(Port(15, 0, 0), 20.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in blocker.collision_geometry],
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
)
move_a = Straight.generate(Port(5, 0, 0), 20.0, width=2.0, dilation=1.0)
move_b = Straight.generate(Port(7, 0, 0), 20.0, width=2.0, dilation=1.0)
assert engine.check_move_congestion(
move_a,
"netA",
net_envelope_cache=net_envelope_cache,
grid_net_cache=grid_net_cache,
broad_phase_cache=cache,
) == 1
assert engine.check_move_congestion(
move_b,
"netA",
net_envelope_cache=net_envelope_cache,
grid_net_cache=grid_net_cache,
broad_phase_cache=cache,
) == 1
assert engine.metrics.total_congestion_candidate_nets == 2
assert engine.metrics.total_congestion_net_envelope_cache_misses == 1
assert engine.metrics.total_congestion_net_envelope_cache_hits == 1
assert engine.metrics.total_congestion_grid_net_cache_misses == 1
assert engine.metrics.total_congestion_grid_net_cache_hits == 1
assert engine.metrics.total_congestion_grid_span_cache_misses == 1
assert engine.metrics.total_congestion_grid_span_cache_hits == 1
def test_has_possible_move_congestion_uses_presence_cache_and_skips_empty_spans() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
presence_cache: dict[tuple[str, int, int, int, int], bool] = {}
blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in blocker.collision_geometry],
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
)
empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0)
assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache)
assert not engine.has_possible_move_congestion(empty_move, "netA", presence_cache)
assert engine.metrics.total_congestion_presence_cache_misses == 1
assert engine.metrics.total_congestion_presence_cache_hits == 1
occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
assert engine.has_possible_move_congestion(occupied_move, "netA")
def test_has_candidate_move_congestion_uses_candidate_precheck_cache() -> None:
engine = RoutingWorld(clearance=2.0)
engine.metrics = AStarMetrics()
candidate_precheck_cache: dict[tuple[str, int, int, int, int], bool] = {}
net_envelope_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
grid_net_cache: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}
blocker = Straight.generate(Port(10, 0, 0), 20.0, width=2.0, dilation=1.0)
engine.add_path(
"netB",
[poly for poly in blocker.collision_geometry],
dilated_geometry=[poly for poly in blocker.dilated_collision_geometry],
)
empty_move = Straight.generate(Port(200, 0, 0), 20.0, width=2.0, dilation=1.0)
occupied_move = Straight.generate(Port(0, 0, 0), 20.0, width=2.0, dilation=1.0)
assert not engine.has_candidate_move_congestion(
empty_move,
"netA",
candidate_precheck_cache,
net_envelope_cache,
grid_net_cache,
)
assert not engine.has_candidate_move_congestion(
empty_move,
"netA",
candidate_precheck_cache,
net_envelope_cache,
grid_net_cache,
)
assert engine.has_candidate_move_congestion(
occupied_move,
"netA",
candidate_precheck_cache,
net_envelope_cache,
grid_net_cache,
)
assert engine.metrics.total_congestion_candidate_precheck_misses >= 2
assert engine.metrics.total_congestion_candidate_precheck_hits >= 1

View file

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
import pytest import pytest
from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome from inire.tests.example_scenarios import SCENARIOS, ScenarioOutcome, snapshot_example_07_no_warm_start
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@ -15,6 +15,7 @@ if TYPE_CHECKING:
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1" RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
PERFORMANCE_REPEATS = 3 PERFORMANCE_REPEATS = 3
REGRESSION_FACTOR = 1.5 REGRESSION_FACTOR = 1.5
NO_WARM_START_REGRESSION_SECONDS = 180.0
# Baselines are measured from clean 6a28dcf-style runs without plotting. # Baselines are measured from clean 6a28dcf-style runs without plotting.
BASELINE_SECONDS = { BASELINE_SECONDS = {
@ -67,3 +68,20 @@ def test_example_like_runtime_regression(scenario: tuple[str, Callable[[], Scena
f"{REGRESSION_FACTOR:.1f}x baseline {BASELINE_SECONDS[name]:.4f}s " f"{REGRESSION_FACTOR:.1f}x baseline {BASELINE_SECONDS[name]:.4f}s "
f"from timings {timings!r}" f"from timings {timings!r}"
) )
@pytest.mark.performance
@pytest.mark.skipif(not RUN_PERFORMANCE, reason="set INIRE_RUN_PERFORMANCE=1 to run runtime regression checks")
def test_example_07_no_warm_start_runtime_regression() -> None:
snapshot = snapshot_example_07_no_warm_start()
assert snapshot.total_results == 10
assert snapshot.valid_results == 10
assert snapshot.reached_targets == 10
assert snapshot.metrics.warm_start_paths_built == 0
assert snapshot.metrics.warm_start_paths_used == 0
assert snapshot.duration_s <= NO_WARM_START_REGRESSION_SECONDS, (
"example_07_large_scale_routing_no_warm_start runtime "
f"{snapshot.duration_s:.4f}s exceeded guardrail "
f"{NO_WARM_START_REGRESSION_SECONDS:.1f}s"
)

View file

@ -14,7 +14,7 @@ from inire import (
) )
from inire.router._stack import build_routing_stack from inire.router._stack import build_routing_stack
from inire.seeds import Bend90Seed, PathSeed, StraightSeed from inire.seeds import Bend90Seed, PathSeed, StraightSeed
from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics from inire.tests.example_scenarios import SCENARIOS, _build_evaluator, _build_pathfinder, _net_specs, AStarMetrics, snapshot_example_05
EXPECTED_OUTCOMES = { EXPECTED_OUTCOMES = {
@ -36,6 +36,13 @@ def test_examples_match_legacy_expected_outcomes(name: str, run) -> None:
assert outcome[1:] == EXPECTED_OUTCOMES[name] assert outcome[1:] == EXPECTED_OUTCOMES[name]
def test_example_05_avoids_dynamic_tree_rebuilds() -> None:
snapshot = snapshot_example_05()
assert snapshot.valid_results == 3
assert snapshot.metrics.dynamic_tree_rebuilds == 0
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None: def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
bounds = (-20, -20, 170, 170) bounds = (-20, -20, 170, 170)
obstacles = ( obstacles = (

View file

@ -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 >= 0
assert snapshot.metrics.ray_cast_calls_expand_forward >= 0 assert snapshot.metrics.ray_cast_calls_expand_forward >= 0
assert snapshot.metrics.dynamic_tree_rebuilds >= 0 assert snapshot.metrics.dynamic_tree_rebuilds >= 0
assert snapshot.metrics.visibility_corner_index_builds >= 0
assert snapshot.metrics.visibility_builds >= 0 assert snapshot.metrics.visibility_builds >= 0
assert snapshot.metrics.congestion_grid_span_cache_hits >= 0
assert snapshot.metrics.congestion_grid_span_cache_misses >= 0
assert snapshot.metrics.congestion_candidate_nets >= 0
assert snapshot.metrics.congestion_net_envelope_cache_hits >= 0
assert snapshot.metrics.congestion_net_envelope_cache_misses >= 0
assert snapshot.metrics.congestion_grid_net_cache_hits >= 0
assert snapshot.metrics.congestion_grid_net_cache_misses >= 0
assert snapshot.metrics.congestion_lazy_resolutions >= 0
assert snapshot.metrics.congestion_lazy_requeues >= 0
assert snapshot.metrics.congestion_candidate_ids >= 0
assert snapshot.metrics.verify_dynamic_candidate_nets >= 0
assert snapshot.metrics.refinement_candidates_verified >= 0 assert snapshot.metrics.refinement_candidates_verified >= 0
@ -74,6 +86,7 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat
str(diff_script), str(diff_script),
"--baseline", "--baseline",
str(baseline_dir / "performance_baseline.json"), str(baseline_dir / "performance_baseline.json"),
"--include-performance-only",
"--scenario", "--scenario",
"example_01_simple_route", "example_01_simple_route",
"--output", "--output",
@ -85,3 +98,100 @@ def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Pat
report = output_path.read_text() report = output_path.read_text()
assert "Performance Baseline Diff" in report assert "Performance Baseline Diff" in report
assert "example_01_simple_route" 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

View file

@ -2,6 +2,7 @@ from shapely.geometry import box
from inire.geometry.collision import RoutingWorld from inire.geometry.collision import RoutingWorld
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router._astar_types import AStarMetrics
from inire.router.visibility import VisibilityManager from inire.router.visibility import VisibilityManager
@ -18,3 +19,111 @@ def test_point_visibility_cache_respects_max_distance() -> None:
assert len(near_corners) == 3 assert len(near_corners) == 3
assert len(far_corners) > len(near_corners) assert len(far_corners) > len(near_corners)
assert any(corner[0] >= 100.0 for corner in far_corners) assert any(corner[0] >= 100.0 for corner in far_corners)
def test_visibility_manager_is_lazy_until_queried() -> None:
engine = RoutingWorld(clearance=0.0)
engine.metrics = AStarMetrics()
engine.add_static_obstacle(box(10, 20, 20, 30))
visibility = VisibilityManager(engine)
assert visibility.corners == []
assert engine.metrics.total_visibility_corner_index_builds == 0
assert engine.metrics.total_visibility_builds == 0
visibility.ensure_corner_index_current()
assert visibility.corners
assert engine.metrics.total_visibility_corner_index_builds == 1
assert engine.metrics.total_visibility_builds == 0
def test_exact_corner_visibility_builds_graph_once_per_static_version() -> None:
engine = RoutingWorld(clearance=0.0)
engine.metrics = AStarMetrics()
engine.add_static_obstacle(box(10, 20, 20, 30))
visibility = VisibilityManager(engine)
origin = Port(10, 20, 0)
first = visibility.get_corner_visibility(origin, max_dist=100.0)
second = visibility.get_corner_visibility(origin, max_dist=100.0)
assert second == first
assert engine.metrics.total_visibility_corner_index_builds == 1
assert engine.metrics.total_visibility_builds == 1
def test_clear_cache_invalidates_without_rebuilding_and_static_change_rebuilds_lazily() -> None:
engine = RoutingWorld(clearance=0.0)
engine.metrics = AStarMetrics()
engine.add_static_obstacle(box(10, 20, 20, 30))
visibility = VisibilityManager(engine)
visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0)
assert engine.metrics.total_visibility_corner_index_builds == 1
assert engine.metrics.total_visibility_builds == 1
visibility.clear_cache()
assert visibility.corners == []
assert engine.metrics.total_visibility_corner_index_builds == 1
assert engine.metrics.total_visibility_builds == 1
engine.add_static_obstacle(box(40, 20, 50, 30))
visible = visibility.get_corner_visibility(Port(10, 20, 0), max_dist=100.0)
assert visible == []
assert engine.metrics.total_visibility_corner_index_builds == 2
assert engine.metrics.total_visibility_builds == 2
def test_tangent_corner_candidate_query_matches_bruteforce_filter() -> None:
engine = RoutingWorld(clearance=0.0)
engine.add_static_obstacle(box(10, 20, 20, 30))
engine.add_static_obstacle(box(-35, -15, -25, -5))
engine.add_static_obstacle(box(35, -40, 45, -30))
visibility = VisibilityManager(engine)
radii = (10.0, 20.0)
min_forward = 5.0
max_forward = 60.0
tolerance = 2.0
for origin in (
Port(0, 0, 0),
Port(0, 0, 90),
Port(0, 0, 180),
Port(0, 0, 270),
):
candidate_ids = set(
visibility.get_tangent_corner_candidates(
origin,
min_forward=min_forward,
max_forward=max_forward,
radii=radii,
tolerance=tolerance,
)
)
expected_ids: set[int] = set()
if origin.r == 0:
cos_v, sin_v = 1.0, 0.0
elif origin.r == 90:
cos_v, sin_v = 0.0, 1.0
elif origin.r == 180:
cos_v, sin_v = -1.0, 0.0
else:
cos_v, sin_v = 0.0, -1.0
for idx, (cx, cy) in enumerate(visibility.corners):
dx = cx - origin.x
dy = cy - origin.y
local_x = dx * cos_v + dy * sin_v
local_y = -dx * sin_v + dy * cos_v
if local_x <= min_forward or local_x > max_forward + 0.01:
continue
nearest_radius = min(radii, key=lambda radius: abs(abs(local_y) - radius))
if abs(abs(local_y) - nearest_radius) <= tolerance:
expected_ids.add(idx)
assert candidate_ids == expected_ids

View file

@ -4,13 +4,17 @@ from __future__ import annotations
import argparse import argparse
import json import json
from dataclasses import asdict from dataclasses import asdict
from datetime import datetime
from pathlib import Path from pathlib import Path
from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS
from inire.results import RouteMetrics
SUMMARY_KEYS = ( SUMMARY_KEYS = (
"duration_s", "duration_s",
"valid_results",
"reached_targets",
"route_iterations", "route_iterations",
"nets_routed", "nets_routed",
"nodes_expanded", "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) allowed = None if selected_scenarios is None else set(selected_scenarios)
snapshots: dict[str, dict[str, object]] = {} 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: if allowed is not None and name not in allowed:
continue continue
snapshots[name] = asdict(run()) 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: def _metric_value(snapshot: dict[str, object], key: str) -> float | None:
if key == "duration_s": if key in {"duration_s", "total_results", "valid_results", "reached_targets"}:
return float(snapshot["duration_s"]) return float(snapshot[key])
if key not in snapshot["metrics"]:
return None
return float(snapshot["metrics"][key]) 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)) scenario_names = sorted(set(baseline) | set(current))
lines = [ lines = [
"# Performance Baseline Diff", "# 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) base_snapshot = baseline.get(scenario)
curr_snapshot = current.get(scenario) curr_snapshot = current.get(scenario)
if base_snapshot is None: if base_snapshot is None:
if curr_snapshot is None:
lines.append(f"| {scenario} | added | - | - | - |") lines.append(f"| {scenario} | added | - | - | - |")
continue 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: if curr_snapshot is None:
lines.append(f"| {scenario} | missing | - | - | - |") lines.append(f"| {scenario} | missing | - | - | - |")
continue continue
for key in SUMMARY_KEYS: for key in metric_names:
base_value = _metric_value(base_snapshot, key) base_value = _metric_value(base_snapshot, key)
curr_value = _metric_value(curr_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( lines.append(
f"| {scenario} | {key} | {base_value:.4f} | {curr_value:.4f} | {curr_value - base_value:+.4f} |" f"| {scenario} | {key} | {base_value:.4f} | {curr_value:.4f} | {curr_value - base_value:+.4f} |"
) )
return "\n".join(lines) + "\n" 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: def main() -> None:
parser = argparse.ArgumentParser(description="Diff the committed performance baseline against a fresh run.") parser = argparse.ArgumentParser(description="Diff the committed performance baseline against a fresh run.")
parser.add_argument( parser.add_argument(
@ -95,18 +176,61 @@ def main() -> None:
default=[], default=[],
help="Optional scenario name to include. May be passed more than once.", help="Optional scenario name to include. May be passed more than once.",
) )
parser.add_argument(
"--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() args = parser.parse_args()
selected = tuple(args.scenarios) if args.scenarios else None 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) baseline = _load_baseline(args.baseline, selected)
current = _current_snapshots(selected) current = _current_snapshots(selected, include_performance_only=args.include_performance_only)
report = _render_report(baseline, current) report = _render_report(baseline, current, metrics)
if args.output is None: if args.output is not None:
print(report, end="")
else:
args.output.write_text(report) args.output.write_text(report)
print(f"Wrote {args.output}") 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__": if __name__ == "__main__":

View file

@ -7,7 +7,7 @@ from dataclasses import asdict
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS from inire.tests.example_scenarios import PERFORMANCE_SCENARIO_SNAPSHOTS, SCENARIO_SNAPSHOTS
SUMMARY_METRICS = ( SUMMARY_METRICS = (
@ -24,10 +24,20 @@ SUMMARY_METRICS = (
) )
def _build_payload(selected_scenarios: tuple[str, ...] | None = None) -> dict[str, object]: def _snapshot_registry(include_performance_only: bool) -> tuple[tuple[str, object], ...]:
if not include_performance_only:
return SCENARIO_SNAPSHOTS
return SCENARIO_SNAPSHOTS + PERFORMANCE_SCENARIO_SNAPSHOTS
def _build_payload(
selected_scenarios: tuple[str, ...] | None = None,
*,
include_performance_only: bool = False,
) -> dict[str, object]:
allowed = None if selected_scenarios is None else set(selected_scenarios) allowed = None if selected_scenarios is None else set(selected_scenarios)
snapshots = [] snapshots = []
for name, run in SCENARIO_SNAPSHOTS: for name, run in _snapshot_registry(include_performance_only):
if allowed is not None and name not in allowed: if allowed is not None and name not in allowed:
continue continue
snapshots.append(run()) snapshots.append(run())
@ -103,6 +113,11 @@ def main() -> None:
default=[], default=[],
help="Optional scenario name to include. May be passed more than once.", help="Optional scenario name to include. May be passed more than once.",
) )
parser.add_argument(
"--include-performance-only",
action="store_true",
help="Include performance-only snapshot scenarios that are excluded from the default baseline corpus.",
)
args = parser.parse_args() args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1] repo_root = Path(__file__).resolve().parents[1]
@ -110,7 +125,7 @@ def main() -> None:
docs_dir.mkdir(exist_ok=True) docs_dir.mkdir(exist_ok=True)
selected = tuple(args.scenarios) if args.scenarios else None selected = tuple(args.scenarios) if args.scenarios else None
payload = _build_payload(selected) payload = _build_payload(selected, include_performance_only=args.include_performance_only)
json_path = docs_dir / "performance_baseline.json" json_path = docs_dir / "performance_baseline.json"
markdown_path = docs_dir / "performance.md" markdown_path = docs_dir / "performance.md"