diff --git a/DOCS.md b/DOCS.md index 2534a17..3aa2688 100644 --- a/DOCS.md +++ b/DOCS.md @@ -186,6 +186,7 @@ Use `RoutingProblem.initial_paths` to provide semantic per-net seeds. Seeds are Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`. The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**. +Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. The counter baseline is currently observational and is not enforced as a CI gate. ## 9. Tuning Notes diff --git a/docs/performance.md b/docs/performance.md index 3144243..713c986 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -3,23 +3,25 @@ Generated on 2026-03-31 by `scripts/record_performance_baseline.py`. The full machine-readable snapshot lives in `docs/performance_baseline.json`. +Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot. | Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls | | :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | | example_01_simple_route | 0.0042 | 1 | 1 | 1 | 1 | 1 | 2 | 22 | 11 | 7 | 2 | 2 | 0 | 3 | -| example_02_congestion_resolution | 0.3335 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 | -| example_03_locked_paths | 0.1810 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 | -| example_04_sbends_and_radii | 2.0151 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 | -| example_05_orientation_stress | 0.2438 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 | -| example_06_bend_collision_models | 4.1636 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 | -| example_07_large_scale_routing | 1.3759 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 | -| example_08_custom_bend_geometry | 0.2437 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 | +| example_02_congestion_resolution | 0.3418 | 3 | 3 | 3 | 1 | 3 | 366 | 1176 | 1413 | 668 | 8 | 4 | 0 | 35 | +| example_03_locked_paths | 0.1827 | 2 | 2 | 2 | 2 | 2 | 191 | 681 | 904 | 307 | 5 | 4 | 0 | 14 | +| example_04_sbends_and_radii | 1.9938 | 2 | 2 | 2 | 1 | 2 | 15 | 18218 | 123 | 65 | 4 | 3 | 0 | 6 | +| example_05_orientation_stress | 0.2458 | 3 | 3 | 3 | 2 | 6 | 286 | 1243 | 1624 | 681 | 12 | 3 | 412 | 12 | +| example_06_bend_collision_models | 4.1186 | 3 | 3 | 3 | 3 | 3 | 240 | 40530 | 1026 | 629 | 6 | 6 | 0 | 9 | +| example_07_large_scale_routing | 1.3734 | 10 | 10 | 10 | 1 | 10 | 78 | 11151 | 372 | 227 | 20 | 11 | 0 | 30 | +| example_08_custom_bend_geometry | 0.2410 | 2 | 2 | 2 | 2 | 2 | 18 | 2308 | 78 | 56 | 4 | 4 | 0 | 6 | | example_09_unroutable_best_effort | 0.0052 | 1 | 0 | 0 | 1 | 1 | 3 | 13 | 16 | 10 | 1 | 0 | 0 | 1 | ## Full Counter Set Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters. +These counters are currently observational only and are not enforced as CI regression gates. Tracked metric keys: -nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries, visibility_corner_hits, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, ray_cast_calls, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_exact_pair_checks +nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, 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 diff --git a/docs/performance_baseline.json b/docs/performance_baseline.json index 0c3c030..cfb369b 100644 --- a/docs/performance_baseline.json +++ b/docs/performance_baseline.json @@ -3,12 +3,17 @@ "generator": "scripts/record_performance_baseline.py", "scenarios": [ { - "duration_s": 0.0041740520391613245, + "duration_s": 0.00415895797777921, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 8, + "danger_map_cache_misses": 13, + "danger_map_lookup_calls": 21, + "danger_map_query_calls": 0, + "danger_map_total_ns": 27079, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 2, "dynamic_path_objects_removed": 1, @@ -23,14 +28,31 @@ "nets_reached_target": 1, "nets_routed": 1, "nodes_expanded": 2, + "path_cost_calls": 0, "pruned_closed_set": 0, "pruned_cost": 4, "pruned_hard_collision": 0, "ray_cast_calls": 22, + "ray_cast_calls_expand_forward": 1, + "ray_cast_calls_expand_snap": 1, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 8, + "ray_cast_calls_visibility_build": 12, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, "ray_cast_candidate_bounds": 12, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 1, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 1, + "score_component_calls": 11, + "score_component_total_ns": 59404, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -40,12 +62,15 @@ "verify_path_report_calls": 3, "verify_static_buffer_ops": 0, "visibility_builds": 2, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 12, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 0, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 1, "warm_start_paths_built": 1, "warm_start_paths_used": 1 }, @@ -55,12 +80,17 @@ "valid_results": 1 }, { - "duration_s": 0.3335385399404913, + "duration_s": 0.34182924893684685, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 1433, + "danger_map_cache_misses": 775, + "danger_map_lookup_calls": 2208, + "danger_map_query_calls": 0, + "danger_map_total_ns": 2165333, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 32, "dynamic_path_objects_removed": 17, @@ -75,14 +105,31 @@ "nets_reached_target": 3, "nets_routed": 3, "nodes_expanded": 366, + "path_cost_calls": 14, "pruned_closed_set": 157, "pruned_cost": 208, "pruned_hard_collision": 380, "ray_cast_calls": 1176, + "ray_cast_calls_expand_forward": 363, + "ray_cast_calls_expand_snap": 19, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 529, + "ray_cast_calls_visibility_build": 12, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 253, "ray_cast_candidate_bounds": 925, "ray_cast_exact_geometry_checks": 136, "refine_path_calls": 3, + "refinement_candidate_side_extents": 26, + "refinement_candidates_accepted": 2, + "refinement_candidates_built": 26, + "refinement_candidates_verified": 26, + "refinement_dynamic_bounds_checked": 20, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 10, "route_iterations": 1, + "score_component_calls": 976, + "score_component_total_ns": 4650167, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -92,12 +139,15 @@ "verify_path_report_calls": 35, "verify_static_buffer_ops": 0, "visibility_builds": 4, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 12, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 18991, + "visibility_tangent_candidate_ray_tests": 253, + "visibility_tangent_candidate_scans": 363, "warm_start_paths_built": 3, "warm_start_paths_used": 3 }, @@ -107,12 +157,17 @@ "valid_results": 3 }, { - "duration_s": 0.1809853739105165, + "duration_s": 0.18274989898782223, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 624, + "danger_map_cache_misses": 414, + "danger_map_lookup_calls": 1038, + "danger_map_query_calls": 0, + "danger_map_total_ns": 1001517, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 17, "dynamic_path_objects_removed": 10, @@ -127,14 +182,31 @@ "nets_reached_target": 2, "nets_routed": 2, "nodes_expanded": 191, + "path_cost_calls": 9, "pruned_closed_set": 97, "pruned_cost": 140, "pruned_hard_collision": 181, "ray_cast_calls": 681, + "ray_cast_calls_expand_forward": 189, + "ray_cast_calls_expand_snap": 8, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 407, + "ray_cast_calls_visibility_build": 24, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 53, "ray_cast_candidate_bounds": 179, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 2, + "refinement_candidate_side_extents": 8, + "refinement_candidates_accepted": 1, + "refinement_candidates_built": 8, + "refinement_candidates_verified": 8, + "refinement_dynamic_bounds_checked": 2, + "refinement_static_bounds_checked": 2, + "refinement_windows_considered": 2, "route_iterations": 2, + "score_component_calls": 504, + "score_component_total_ns": 2184569, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 1, @@ -144,12 +216,15 @@ "verify_path_report_calls": 14, "verify_static_buffer_ops": 69, "visibility_builds": 4, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 24, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 476, + "visibility_tangent_candidate_ray_tests": 53, + "visibility_tangent_candidate_scans": 189, "warm_start_paths_built": 2, "warm_start_paths_used": 2 }, @@ -159,12 +234,17 @@ "valid_results": 2 }, { - "duration_s": 2.0151148419827223, + "duration_s": 1.993830946041271, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 75, + "danger_map_cache_misses": 120, + "danger_map_lookup_calls": 195, + "danger_map_query_calls": 0, + "danger_map_total_ns": 207556, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 14, "dynamic_path_objects_removed": 7, @@ -179,14 +259,31 @@ "nets_reached_target": 2, "nets_routed": 2, "nodes_expanded": 15, + "path_cost_calls": 0, "pruned_closed_set": 2, "pruned_cost": 25, "pruned_hard_collision": 16, "ray_cast_calls": 18218, + "ray_cast_calls_expand_forward": 13, + "ray_cast_calls_expand_snap": 1, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 56, + "ray_cast_calls_visibility_build": 18148, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, "ray_cast_candidate_bounds": 50717, "ray_cast_exact_geometry_checks": 21265, "refine_path_calls": 2, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 1, + "score_component_calls": 90, + "score_component_total_ns": 410130, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 1, @@ -196,12 +293,15 @@ "verify_path_report_calls": 6, "verify_static_buffer_ops": 0, "visibility_builds": 3, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 18148, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 394, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 13, "warm_start_paths_built": 2, "warm_start_paths_used": 2 }, @@ -211,12 +311,17 @@ "valid_results": 2 }, { - "duration_s": 0.2437819039914757, + "duration_s": 0.24581307696644217, "metrics": { "congestion_cache_hits": 2, "congestion_cache_misses": 412, "congestion_check_calls": 412, "congestion_exact_pair_checks": 66, + "danger_map_cache_hits": 1386, + "danger_map_cache_misses": 693, + "danger_map_lookup_calls": 2079, + "danger_map_query_calls": 0, + "danger_map_total_ns": 1805113, "dynamic_grid_rebuilds": 3, "dynamic_path_objects_added": 37, "dynamic_path_objects_removed": 25, @@ -231,14 +336,31 @@ "nets_reached_target": 6, "nets_routed": 6, "nodes_expanded": 286, + "path_cost_calls": 2, "pruned_closed_set": 139, "pruned_cost": 505, "pruned_hard_collision": 14, "ray_cast_calls": 1243, + "ray_cast_calls_expand_forward": 280, + "ray_cast_calls_expand_snap": 3, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 951, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 9, "ray_cast_candidate_bounds": 45, "ray_cast_exact_geometry_checks": 43, "refine_path_calls": 3, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 2, + "score_component_calls": 1198, + "score_component_total_ns": 4292875, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 3, @@ -248,12 +370,15 @@ "verify_path_report_calls": 12, "verify_static_buffer_ops": 0, "visibility_builds": 3, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 0, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 1483, + "visibility_tangent_candidate_ray_tests": 9, + "visibility_tangent_candidate_scans": 280, "warm_start_paths_built": 2, "warm_start_paths_used": 2 }, @@ -263,12 +388,17 @@ "valid_results": 3 }, { - "duration_s": 4.163613382959738, + "duration_s": 4.1186372829834, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 1183, + "danger_map_cache_misses": 731, + "danger_map_lookup_calls": 1914, + "danger_map_query_calls": 731, + "danger_map_total_ns": 18374289, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 36, "dynamic_path_objects_removed": 18, @@ -283,14 +413,31 @@ "nets_reached_target": 3, "nets_routed": 3, "nodes_expanded": 240, + "path_cost_calls": 0, "pruned_closed_set": 108, "pruned_cost": 204, "pruned_hard_collision": 85, "ray_cast_calls": 40530, + "ray_cast_calls_expand_forward": 237, + "ray_cast_calls_expand_snap": 3, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 408, + "ray_cast_calls_visibility_build": 39848, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 34, "ray_cast_candidate_bounds": 121732, "ray_cast_exact_geometry_checks": 36858, "refine_path_calls": 3, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 3, + "score_component_calls": 842, + "score_component_total_ns": 20652599, "static_net_tree_rebuilds": 3, "static_raw_tree_rebuilds": 3, "static_safe_cache_hits": 141, @@ -300,12 +447,15 @@ "verify_path_report_calls": 9, "verify_static_buffer_ops": 54, "visibility_builds": 6, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 39848, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 2400, + "visibility_tangent_candidate_ray_tests": 34, + "visibility_tangent_candidate_scans": 237, "warm_start_paths_built": 3, "warm_start_paths_used": 3 }, @@ -315,12 +465,17 @@ "valid_results": 3 }, { - "duration_s": 1.375933071016334, + "duration_s": 1.373430646955967, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 233, + "danger_map_cache_misses": 448, + "danger_map_lookup_calls": 681, + "danger_map_query_calls": 448, + "danger_map_total_ns": 10728422, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 88, "dynamic_path_objects_removed": 44, @@ -335,14 +490,31 @@ "nets_reached_target": 10, "nets_routed": 10, "nodes_expanded": 78, + "path_cost_calls": 0, "pruned_closed_set": 20, "pruned_cost": 64, "pruned_hard_collision": 61, "ray_cast_calls": 11151, + "ray_cast_calls_expand_forward": 68, + "ray_cast_calls_expand_snap": 6, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 232, + "ray_cast_calls_visibility_build": 10768, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 77, "ray_cast_candidate_bounds": 21198, "ray_cast_exact_geometry_checks": 11651, "refine_path_calls": 10, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 1, + "score_component_calls": 291, + "score_component_total_ns": 11574800, "static_net_tree_rebuilds": 10, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 6, @@ -352,12 +524,15 @@ "verify_path_report_calls": 30, "verify_static_buffer_ops": 132, "visibility_builds": 11, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 10768, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 34735, + "visibility_tangent_candidate_ray_tests": 77, + "visibility_tangent_candidate_scans": 68, "warm_start_paths_built": 10, "warm_start_paths_used": 10 }, @@ -367,12 +542,17 @@ "valid_results": 10 }, { - "duration_s": 0.2436628290452063, + "duration_s": 0.2410298540489748, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 58, + "danger_map_cache_misses": 110, + "danger_map_lookup_calls": 168, + "danger_map_query_calls": 0, + "danger_map_total_ns": 178104, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 12, "dynamic_path_objects_removed": 6, @@ -387,14 +567,31 @@ "nets_reached_target": 2, "nets_routed": 2, "nodes_expanded": 18, + "path_cost_calls": 0, "pruned_closed_set": 6, "pruned_cost": 16, "pruned_hard_collision": 0, "ray_cast_calls": 2308, + "ray_cast_calls_expand_forward": 16, + "ray_cast_calls_expand_snap": 2, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 38, + "ray_cast_calls_visibility_build": 2252, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, "ray_cast_candidate_bounds": 3802, "ray_cast_exact_geometry_checks": 1904, "refine_path_calls": 2, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 2, + "score_component_calls": 72, + "score_component_total_ns": 352865, "static_net_tree_rebuilds": 2, "static_raw_tree_rebuilds": 0, "static_safe_cache_hits": 2, @@ -404,12 +601,15 @@ "verify_path_report_calls": 6, "verify_static_buffer_ops": 0, "visibility_builds": 4, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 2252, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 0, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 16, "warm_start_paths_built": 2, "warm_start_paths_used": 2 }, @@ -419,12 +619,17 @@ "valid_results": 2 }, { - "duration_s": 0.0052433289820328355, + "duration_s": 0.0052388140466064215, "metrics": { "congestion_cache_hits": 0, "congestion_cache_misses": 0, "congestion_check_calls": 0, "congestion_exact_pair_checks": 0, + "danger_map_cache_hits": 10, + "danger_map_cache_misses": 20, + "danger_map_lookup_calls": 30, + "danger_map_query_calls": 20, + "danger_map_total_ns": 502052, "dynamic_grid_rebuilds": 0, "dynamic_path_objects_added": 1, "dynamic_path_objects_removed": 0, @@ -439,14 +644,31 @@ "nets_reached_target": 0, "nets_routed": 1, "nodes_expanded": 3, + "path_cost_calls": 0, "pruned_closed_set": 0, "pruned_cost": 4, "pruned_hard_collision": 2, "ray_cast_calls": 13, + "ray_cast_calls_expand_forward": 3, + "ray_cast_calls_expand_snap": 0, + "ray_cast_calls_other": 0, + "ray_cast_calls_straight_static": 10, + "ray_cast_calls_visibility_build": 0, + "ray_cast_calls_visibility_query": 0, + "ray_cast_calls_visibility_tangent": 0, "ray_cast_candidate_bounds": 5, "ray_cast_exact_geometry_checks": 0, "refine_path_calls": 0, + "refinement_candidate_side_extents": 0, + "refinement_candidates_accepted": 0, + "refinement_candidates_built": 0, + "refinement_candidates_verified": 0, + "refinement_dynamic_bounds_checked": 0, + "refinement_static_bounds_checked": 0, + "refinement_windows_considered": 0, "route_iterations": 1, + "score_component_calls": 14, + "score_component_total_ns": 538947, "static_net_tree_rebuilds": 1, "static_raw_tree_rebuilds": 1, "static_safe_cache_hits": 0, @@ -456,12 +678,15 @@ "verify_path_report_calls": 1, "verify_static_buffer_ops": 1, "visibility_builds": 0, - "visibility_corner_hits": 0, + "visibility_corner_hits_exact": 0, "visibility_corner_pairs_checked": 0, - "visibility_corner_queries": 0, + "visibility_corner_queries_exact": 0, "visibility_point_cache_hits": 0, "visibility_point_cache_misses": 0, "visibility_point_queries": 0, + "visibility_tangent_candidate_corner_checks": 10, + "visibility_tangent_candidate_ray_tests": 0, + "visibility_tangent_candidate_scans": 3, "warm_start_paths_built": 0, "warm_start_paths_used": 0 }, diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py index acc7398..01367b4 100644 --- a/inire/geometry/collision.py +++ b/inire/geometry/collision.py @@ -103,7 +103,13 @@ class RoutingWorld: self._dynamic_paths.remove_path(net_id) def check_move_straight_static(self, start_port: Port, length: float, net_width: float) -> bool: - reach = self.ray_cast(start_port, start_port.r, max_dist=length + 0.01, net_width=net_width) + reach = self.ray_cast( + start_port, + start_port.r, + max_dist=length + 0.01, + net_width=net_width, + caller="straight_static", + ) return reach < length - 0.001 def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool: @@ -393,9 +399,24 @@ class RoutingWorld: angle_deg: float, max_dist: float = 2000.0, net_width: float | None = None, + caller: str = "other", ) -> float: if self.metrics is not None: self.metrics.total_ray_cast_calls += 1 + if caller == "straight_static": + self.metrics.total_ray_cast_calls_straight_static += 1 + elif caller == "expand_snap": + self.metrics.total_ray_cast_calls_expand_snap += 1 + elif caller == "expand_forward": + self.metrics.total_ray_cast_calls_expand_forward += 1 + elif caller == "visibility_build": + self.metrics.total_ray_cast_calls_visibility_build += 1 + elif caller == "visibility_query": + self.metrics.total_ray_cast_calls_visibility_query += 1 + elif caller == "visibility_tangent": + self.metrics.total_ray_cast_calls_visibility_tangent += 1 + else: + self.metrics.total_ray_cast_calls_other += 1 static_obstacles = self._static_obstacles tree: STRtree | None is_rect_array: numpy.ndarray | None diff --git a/inire/results.py b/inire/results.py index 0ac5a29..b405117 100644 --- a/inire/results.py +++ b/inire/results.py @@ -45,6 +45,14 @@ class RouteMetrics: warm_start_paths_used: int refine_path_calls: int timeout_events: int + score_component_calls: int + score_component_total_ns: int + path_cost_calls: int + danger_map_lookup_calls: int + danger_map_cache_hits: int + danger_map_cache_misses: int + danger_map_query_calls: int + danger_map_total_ns: int move_cache_abs_hits: int move_cache_abs_misses: int move_cache_rel_hits: int @@ -62,12 +70,22 @@ class RouteMetrics: static_net_tree_rebuilds: int visibility_builds: int visibility_corner_pairs_checked: int - visibility_corner_queries: int - visibility_corner_hits: int + visibility_corner_queries_exact: int + visibility_corner_hits_exact: int visibility_point_queries: int visibility_point_cache_hits: int visibility_point_cache_misses: int + visibility_tangent_candidate_scans: int + visibility_tangent_candidate_corner_checks: int + visibility_tangent_candidate_ray_tests: int ray_cast_calls: int + ray_cast_calls_straight_static: int + ray_cast_calls_expand_snap: int + ray_cast_calls_expand_forward: int + ray_cast_calls_visibility_build: int + ray_cast_calls_visibility_query: int + ray_cast_calls_visibility_tangent: int + ray_cast_calls_other: int ray_cast_candidate_bounds: int ray_cast_exact_geometry_checks: int congestion_check_calls: int @@ -75,6 +93,13 @@ class RouteMetrics: verify_path_report_calls: int verify_static_buffer_ops: int verify_dynamic_exact_pair_checks: int + refinement_windows_considered: int + refinement_static_bounds_checked: int + refinement_dynamic_bounds_checked: int + refinement_candidate_side_extents: int + refinement_candidates_built: int + refinement_candidates_verified: int + refinement_candidates_accepted: int @dataclass(frozen=True, slots=True) diff --git a/inire/router/_astar_moves.py b/inire/router/_astar_moves.py index 56aae96..a921aab 100644 --- a/inire/router/_astar_moves.py +++ b/inire/router/_astar_moves.py @@ -64,6 +64,7 @@ def _visible_straight_candidates( visibility_manager = context.visibility_manager visibility_manager._ensure_current() + context.metrics.total_visibility_tangent_candidate_scans += 1 max_bend_radius = max(search_options.bend_radii, default=0.0) if max_bend_radius <= 0 or not visibility_manager.corners: return [] @@ -76,6 +77,7 @@ def _visible_straight_candidates( scored: list[tuple[float, float, float, float, float]] = [] for idx in candidate_ids: + context.metrics.total_visibility_tangent_candidate_corner_checks += 1 cx, cy = visibility_manager.corners[idx] dx = cx - current.x dy = cy - current.y @@ -101,8 +103,15 @@ def _visible_straight_candidates( collision_engine = context.cost_evaluator.collision_engine tangent_candidates: set[int] = set() for _, dist, length, dx, dy in sorted(scored)[:4]: + context.metrics.total_visibility_tangent_candidate_ray_tests += 1 angle = math.degrees(math.atan2(dy, dx)) - corner_reach = collision_engine.ray_cast(current, angle, max_dist=dist + 0.05, net_width=net_width) + corner_reach = collision_engine.ray_cast( + current, + angle, + max_dist=dist + 0.05, + net_width=net_width, + caller="visibility_tangent", + ) if corner_reach < dist - 0.01: continue qlen = int(round(length)) @@ -156,7 +165,13 @@ def expand_moves( dy_local = perp_t if proj_t > 0 and abs(perp_t) < 1e-6 and cp.r == target.r: - max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, proj_t + 1.0, net_width=net_width) + max_reach = context.cost_evaluator.collision_engine.ray_cast( + cp, + cp.r, + proj_t + 1.0, + net_width=net_width, + caller="expand_snap", + ) if max_reach >= proj_t - 0.01 and ( prev_straight_length is None or proj_t < prev_straight_length - TOLERANCE_LINEAR ): @@ -175,7 +190,13 @@ def expand_moves( (int(round(proj_t)),), ) - max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.r, search_options.max_straight_length, net_width=net_width) + max_reach = context.cost_evaluator.collision_engine.ray_cast( + cp, + cp.r, + search_options.max_straight_length, + net_width=net_width, + caller="expand_forward", + ) candidate_lengths = [ search_options.min_straight_length, max_reach, diff --git a/inire/router/_astar_types.py b/inire/router/_astar_types.py index 0092cbd..14cc7bc 100644 --- a/inire/router/_astar_types.py +++ b/inire/router/_astar_types.py @@ -94,6 +94,14 @@ class AStarMetrics: "total_warm_start_paths_used", "total_refine_path_calls", "total_timeout_events", + "total_score_component_calls", + "total_score_component_total_ns", + "total_path_cost_calls", + "total_danger_map_lookup_calls", + "total_danger_map_cache_hits", + "total_danger_map_cache_misses", + "total_danger_map_query_calls", + "total_danger_map_total_ns", "total_move_cache_abs_hits", "total_move_cache_abs_misses", "total_move_cache_rel_hits", @@ -111,12 +119,22 @@ class AStarMetrics: "total_static_net_tree_rebuilds", "total_visibility_builds", "total_visibility_corner_pairs_checked", - "total_visibility_corner_queries", - "total_visibility_corner_hits", + "total_visibility_corner_queries_exact", + "total_visibility_corner_hits_exact", "total_visibility_point_queries", "total_visibility_point_cache_hits", "total_visibility_point_cache_misses", + "total_visibility_tangent_candidate_scans", + "total_visibility_tangent_candidate_corner_checks", + "total_visibility_tangent_candidate_ray_tests", "total_ray_cast_calls", + "total_ray_cast_calls_straight_static", + "total_ray_cast_calls_expand_snap", + "total_ray_cast_calls_expand_forward", + "total_ray_cast_calls_visibility_build", + "total_ray_cast_calls_visibility_query", + "total_ray_cast_calls_visibility_tangent", + "total_ray_cast_calls_other", "total_ray_cast_candidate_bounds", "total_ray_cast_exact_geometry_checks", "total_congestion_check_calls", @@ -124,6 +142,13 @@ class AStarMetrics: "total_verify_path_report_calls", "total_verify_static_buffer_ops", "total_verify_dynamic_exact_pair_checks", + "total_refinement_windows_considered", + "total_refinement_static_bounds_checked", + "total_refinement_dynamic_bounds_checked", + "total_refinement_candidate_side_extents", + "total_refinement_candidates_built", + "total_refinement_candidates_verified", + "total_refinement_candidates_accepted", "last_expanded_nodes", "nodes_expanded", "moves_generated", @@ -147,6 +172,14 @@ class AStarMetrics: self.total_warm_start_paths_used = 0 self.total_refine_path_calls = 0 self.total_timeout_events = 0 + self.total_score_component_calls = 0 + self.total_score_component_total_ns = 0 + self.total_path_cost_calls = 0 + self.total_danger_map_lookup_calls = 0 + self.total_danger_map_cache_hits = 0 + self.total_danger_map_cache_misses = 0 + self.total_danger_map_query_calls = 0 + self.total_danger_map_total_ns = 0 self.total_move_cache_abs_hits = 0 self.total_move_cache_abs_misses = 0 self.total_move_cache_rel_hits = 0 @@ -164,12 +197,22 @@ class AStarMetrics: self.total_static_net_tree_rebuilds = 0 self.total_visibility_builds = 0 self.total_visibility_corner_pairs_checked = 0 - self.total_visibility_corner_queries = 0 - self.total_visibility_corner_hits = 0 + self.total_visibility_corner_queries_exact = 0 + self.total_visibility_corner_hits_exact = 0 self.total_visibility_point_queries = 0 self.total_visibility_point_cache_hits = 0 self.total_visibility_point_cache_misses = 0 + self.total_visibility_tangent_candidate_scans = 0 + self.total_visibility_tangent_candidate_corner_checks = 0 + self.total_visibility_tangent_candidate_ray_tests = 0 self.total_ray_cast_calls = 0 + self.total_ray_cast_calls_straight_static = 0 + self.total_ray_cast_calls_expand_snap = 0 + self.total_ray_cast_calls_expand_forward = 0 + self.total_ray_cast_calls_visibility_build = 0 + self.total_ray_cast_calls_visibility_query = 0 + self.total_ray_cast_calls_visibility_tangent = 0 + self.total_ray_cast_calls_other = 0 self.total_ray_cast_candidate_bounds = 0 self.total_ray_cast_exact_geometry_checks = 0 self.total_congestion_check_calls = 0 @@ -177,6 +220,13 @@ class AStarMetrics: self.total_verify_path_report_calls = 0 self.total_verify_static_buffer_ops = 0 self.total_verify_dynamic_exact_pair_checks = 0 + self.total_refinement_windows_considered = 0 + self.total_refinement_static_bounds_checked = 0 + self.total_refinement_dynamic_bounds_checked = 0 + self.total_refinement_candidate_side_extents = 0 + self.total_refinement_candidates_built = 0 + self.total_refinement_candidates_verified = 0 + self.total_refinement_candidates_accepted = 0 self.last_expanded_nodes: list[tuple[int, int, int]] = [] self.nodes_expanded = 0 self.moves_generated = 0 @@ -199,6 +249,14 @@ class AStarMetrics: self.total_warm_start_paths_used = 0 self.total_refine_path_calls = 0 self.total_timeout_events = 0 + self.total_score_component_calls = 0 + self.total_score_component_total_ns = 0 + self.total_path_cost_calls = 0 + self.total_danger_map_lookup_calls = 0 + self.total_danger_map_cache_hits = 0 + self.total_danger_map_cache_misses = 0 + self.total_danger_map_query_calls = 0 + self.total_danger_map_total_ns = 0 self.total_move_cache_abs_hits = 0 self.total_move_cache_abs_misses = 0 self.total_move_cache_rel_hits = 0 @@ -216,12 +274,22 @@ class AStarMetrics: self.total_static_net_tree_rebuilds = 0 self.total_visibility_builds = 0 self.total_visibility_corner_pairs_checked = 0 - self.total_visibility_corner_queries = 0 - self.total_visibility_corner_hits = 0 + self.total_visibility_corner_queries_exact = 0 + self.total_visibility_corner_hits_exact = 0 self.total_visibility_point_queries = 0 self.total_visibility_point_cache_hits = 0 self.total_visibility_point_cache_misses = 0 + self.total_visibility_tangent_candidate_scans = 0 + self.total_visibility_tangent_candidate_corner_checks = 0 + self.total_visibility_tangent_candidate_ray_tests = 0 self.total_ray_cast_calls = 0 + self.total_ray_cast_calls_straight_static = 0 + self.total_ray_cast_calls_expand_snap = 0 + self.total_ray_cast_calls_expand_forward = 0 + self.total_ray_cast_calls_visibility_build = 0 + self.total_ray_cast_calls_visibility_query = 0 + self.total_ray_cast_calls_visibility_tangent = 0 + self.total_ray_cast_calls_other = 0 self.total_ray_cast_candidate_bounds = 0 self.total_ray_cast_exact_geometry_checks = 0 self.total_congestion_check_calls = 0 @@ -229,6 +297,13 @@ class AStarMetrics: self.total_verify_path_report_calls = 0 self.total_verify_static_buffer_ops = 0 self.total_verify_dynamic_exact_pair_checks = 0 + self.total_refinement_windows_considered = 0 + self.total_refinement_static_bounds_checked = 0 + self.total_refinement_dynamic_bounds_checked = 0 + self.total_refinement_candidate_side_extents = 0 + self.total_refinement_candidates_built = 0 + self.total_refinement_candidates_verified = 0 + self.total_refinement_candidates_accepted = 0 def reset_per_route(self) -> None: self.nodes_expanded = 0 @@ -254,6 +329,14 @@ class AStarMetrics: warm_start_paths_used=self.total_warm_start_paths_used, refine_path_calls=self.total_refine_path_calls, timeout_events=self.total_timeout_events, + score_component_calls=self.total_score_component_calls, + score_component_total_ns=self.total_score_component_total_ns, + path_cost_calls=self.total_path_cost_calls, + danger_map_lookup_calls=self.total_danger_map_lookup_calls, + danger_map_cache_hits=self.total_danger_map_cache_hits, + danger_map_cache_misses=self.total_danger_map_cache_misses, + danger_map_query_calls=self.total_danger_map_query_calls, + danger_map_total_ns=self.total_danger_map_total_ns, move_cache_abs_hits=self.total_move_cache_abs_hits, move_cache_abs_misses=self.total_move_cache_abs_misses, move_cache_rel_hits=self.total_move_cache_rel_hits, @@ -271,12 +354,22 @@ class AStarMetrics: static_net_tree_rebuilds=self.total_static_net_tree_rebuilds, visibility_builds=self.total_visibility_builds, visibility_corner_pairs_checked=self.total_visibility_corner_pairs_checked, - visibility_corner_queries=self.total_visibility_corner_queries, - visibility_corner_hits=self.total_visibility_corner_hits, + visibility_corner_queries_exact=self.total_visibility_corner_queries_exact, + visibility_corner_hits_exact=self.total_visibility_corner_hits_exact, visibility_point_queries=self.total_visibility_point_queries, visibility_point_cache_hits=self.total_visibility_point_cache_hits, visibility_point_cache_misses=self.total_visibility_point_cache_misses, + visibility_tangent_candidate_scans=self.total_visibility_tangent_candidate_scans, + visibility_tangent_candidate_corner_checks=self.total_visibility_tangent_candidate_corner_checks, + visibility_tangent_candidate_ray_tests=self.total_visibility_tangent_candidate_ray_tests, ray_cast_calls=self.total_ray_cast_calls, + ray_cast_calls_straight_static=self.total_ray_cast_calls_straight_static, + ray_cast_calls_expand_snap=self.total_ray_cast_calls_expand_snap, + ray_cast_calls_expand_forward=self.total_ray_cast_calls_expand_forward, + ray_cast_calls_visibility_build=self.total_ray_cast_calls_visibility_build, + ray_cast_calls_visibility_query=self.total_ray_cast_calls_visibility_query, + ray_cast_calls_visibility_tangent=self.total_ray_cast_calls_visibility_tangent, + ray_cast_calls_other=self.total_ray_cast_calls_other, ray_cast_candidate_bounds=self.total_ray_cast_candidate_bounds, ray_cast_exact_geometry_checks=self.total_ray_cast_exact_geometry_checks, congestion_check_calls=self.total_congestion_check_calls, @@ -284,6 +377,13 @@ class AStarMetrics: verify_path_report_calls=self.total_verify_path_report_calls, verify_static_buffer_ops=self.total_verify_static_buffer_ops, verify_dynamic_exact_pair_checks=self.total_verify_dynamic_exact_pair_checks, + refinement_windows_considered=self.total_refinement_windows_considered, + refinement_static_bounds_checked=self.total_refinement_static_bounds_checked, + refinement_dynamic_bounds_checked=self.total_refinement_dynamic_bounds_checked, + refinement_candidate_side_extents=self.total_refinement_candidate_side_extents, + refinement_candidates_built=self.total_refinement_candidates_built, + refinement_candidates_verified=self.total_refinement_candidates_verified, + refinement_candidates_accepted=self.total_refinement_candidates_accepted, ) diff --git a/inire/router/_router.py b/inire/router/_router.py index 801e1b8..ed71cce 100644 --- a/inire/router/_router.py +++ b/inire/router/_router.py @@ -48,6 +48,8 @@ class PathFinder: self.metrics = self.context.metrics if metrics is None else metrics self.context.metrics = self.metrics self.context.cost_evaluator.collision_engine.metrics = self.metrics + if self.context.cost_evaluator.danger_map is not None: + self.context.cost_evaluator.danger_map.metrics = self.metrics self.refiner = PathRefiner(self.context) self.accumulated_expanded_nodes: list[tuple[int, int, int]] = [] diff --git a/inire/router/cost.py b/inire/router/cost.py index eaf9d66..dc36447 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -1,5 +1,6 @@ from __future__ import annotations +from time import perf_counter_ns from typing import TYPE_CHECKING import numpy as np @@ -130,10 +131,16 @@ class CostEvaluator: start_port: Port | None = None, weights: ObjectiveWeights | None = None, ) -> float: + metrics = self.collision_engine.metrics + if metrics is not None: + metrics.total_score_component_calls += 1 + start_ns = perf_counter_ns() active_weights = self._resolve_weights(weights) danger_map = self.danger_map end_port = component.end_port if danger_map is not None and not danger_map.is_within_bounds(end_port.x, end_port.y): + if metrics is not None: + metrics.total_score_component_total_ns += perf_counter_ns() - start_ns return 1e15 move_radius = None @@ -155,6 +162,8 @@ class CostEvaluator: total_cost += component.length * active_weights.danger_weight * (cost_s + cost_m + cost_e) / 3.0 else: total_cost += component.length * active_weights.danger_weight * cost_e + if metrics is not None: + metrics.total_score_component_total_ns += perf_counter_ns() - start_ns return total_cost def component_penalty( @@ -181,6 +190,9 @@ class CostEvaluator: *, weights: ObjectiveWeights | None = None, ) -> float: + metrics = self.collision_engine.metrics + if metrics is not None: + metrics.total_path_cost_calls += 1 active_weights = self._resolve_weights(weights) total = 0.0 current_port = start_port diff --git a/inire/router/danger_map.py b/inire/router/danger_map.py index 12b3b14..c9ca9d3 100644 --- a/inire/router/danger_map.py +++ b/inire/router/danger_map.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import OrderedDict +from time import perf_counter_ns from typing import TYPE_CHECKING import numpy @@ -8,6 +9,7 @@ from scipy.spatial import cKDTree if TYPE_CHECKING: from shapely.geometry import Polygon + from inire.router._astar_types import AStarMetrics _COST_CACHE_SIZE = 100000 @@ -18,7 +20,7 @@ class DangerMap: A proximity cost evaluator using a KD-Tree of obstacle boundary points. Scales with obstacle perimeter rather than design area. """ - __slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache') + __slots__ = ('minx', 'miny', 'maxx', 'maxy', 'resolution', 'safety_threshold', 'k', 'tree', '_cost_cache', 'metrics') def __init__( self, @@ -42,6 +44,7 @@ class DangerMap: self.k = k self.tree: cKDTree | None = None self._cost_cache: OrderedDict[tuple[int, int], float] = OrderedDict() + self.metrics: AStarMetrics | None = None def precompute(self, obstacles: list[Polygon]) -> None: """ @@ -82,17 +85,28 @@ class DangerMap: Get the proximity cost at a specific coordinate using the KD-Tree. Coordinates are quantized to 1nm to improve cache performance. """ + metrics = self.metrics + if metrics is not None: + metrics.total_danger_map_lookup_calls += 1 + start_ns = perf_counter_ns() qx_milli = int(round(x * 1000)) qy_milli = int(round(y * 1000)) key = (qx_milli, qy_milli) if key in self._cost_cache: + if metrics is not None: + metrics.total_danger_map_cache_hits += 1 + metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns self._cost_cache.move_to_end(key) return self._cost_cache[key] + if metrics is not None: + metrics.total_danger_map_cache_misses += 1 cost = self._compute_cost_quantized(qx_milli, qy_milli) self._cost_cache[key] = cost if len(self._cost_cache) > _COST_CACHE_SIZE: self._cost_cache.popitem(last=False) + if metrics is not None: + metrics.total_danger_map_total_ns += perf_counter_ns() - start_ns return cost def _compute_cost_quantized(self, qx_milli: int, qy_milli: int) -> float: @@ -102,6 +116,8 @@ class DangerMap: return 1e15 if self.tree is None: return 0.0 + if self.metrics is not None: + self.metrics.total_danger_map_query_calls += 1 dist, _ = self.tree.query([qx, qy], distance_upper_bound=self.safety_threshold) if dist >= self.safety_threshold: return 0.0 diff --git a/inire/router/refiner.py b/inire/router/refiner.py index ee9c9e9..5f3a6c2 100644 --- a/inire/router/refiner.py +++ b/inire/router/refiner.py @@ -128,6 +128,7 @@ class PathRefiner: x_max = max(0.0, float(local_dx)) + 0.01 for bounds in self.collision_engine.iter_static_obstacle_bounds(query_bounds): + self.context.metrics.total_refinement_static_bounds_checked += 1 local_corners = ( self._to_local_xy(start, bounds[0], bounds[1]), self._to_local_xy(start, bounds[0], bounds[3]), @@ -144,6 +145,7 @@ class PathRefiner: negative_anchors.add(obs_min_y) for bounds in self.collision_engine.iter_dynamic_path_bounds(query_bounds): + self.context.metrics.total_refinement_dynamic_bounds_checked += 1 local_corners = ( self._to_local_xy(start, bounds[0], bounds[1]), self._to_local_xy(start, bounds[0], bounds[3]), @@ -166,6 +168,7 @@ class PathRefiner: if anchor < min(0.0, float(local_dy)) + 0.01: direct_extents.add(anchor - pad) + self.context.metrics.total_refinement_candidate_side_extents += len(direct_extents) return sorted(direct_extents, key=lambda value: (abs(value), value)) def _build_same_orientation_dogleg( @@ -243,6 +246,7 @@ class PathRefiner: local_dx, _ = self._to_local(window_start, window_end) if local_dx < 4.0 * min_radius - 0.01: continue + self.context.metrics.total_refinement_windows_considered += 1 windows.append((start_idx, end_idx)) return windows @@ -270,12 +274,15 @@ class PathRefiner: replacement = self._build_same_orientation_dogleg(window_start, window_end, net_width, radius, side_extent) if replacement is None: continue + self.context.metrics.total_refinement_candidates_built += 1 candidate_path = path[:start_idx] + replacement + path[end_idx:] + self.context.metrics.total_refinement_candidates_verified += 1 report = self.collision_engine.verify_path_report(net_id, candidate_path) if not report.is_valid: continue candidate_cost = self.path_cost(candidate_path) if candidate_cost + 1e-6 < best_candidate_cost: + self.context.metrics.total_refinement_candidates_accepted += 1 best_candidate_cost = candidate_cost best_path = candidate_path diff --git a/inire/router/visibility.py b/inire/router/visibility.py index 4fc51ed..00f9668 100644 --- a/inire/router/visibility.py +++ b/inire/router/visibility.py @@ -93,7 +93,7 @@ class VisibilityManager: dx, dy = cx - p1.x, cy - p1.y dist = numpy.sqrt(dx**2 + dy**2) angle = numpy.degrees(numpy.arctan2(dy, dx)) - reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05) + reach = self.collision_engine.ray_cast(p1, angle, max_dist=dist + 0.05, caller="visibility_build") if reach >= dist - 0.01: self._corner_graph[i].append((cx, cy, dist)) @@ -143,7 +143,7 @@ class VisibilityManager: continue angle = numpy.degrees(numpy.arctan2(dy, dx)) - reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05) + reach = self.collision_engine.ray_cast(origin, angle, max_dist=dist + 0.05, caller="visibility_query") if reach >= dist - 0.01: visible.append((cx, cy, dist)) @@ -156,7 +156,7 @@ class VisibilityManager: This avoids the expensive arbitrary-point visibility scan in hot search paths. """ if self.collision_engine.metrics is not None: - self.collision_engine.metrics.total_visibility_corner_queries += 1 + self.collision_engine.metrics.total_visibility_corner_queries_exact += 1 self._ensure_current() if max_dist < 0: return [] @@ -164,6 +164,6 @@ class VisibilityManager: corner_idx = self._corner_idx_at(origin) if corner_idx is not None and corner_idx in self._corner_graph: if self.collision_engine.metrics is not None: - self.collision_engine.metrics.total_visibility_corner_hits += 1 + self.collision_engine.metrics.total_visibility_corner_hits_exact += 1 return [corner for corner in self._corner_graph[corner_idx] if corner[2] <= max_dist] return [] diff --git a/inire/tests/test_performance_reporting.py b/inire/tests/test_performance_reporting.py index ebf7f60..58954de 100644 --- a/inire/tests/test_performance_reporting.py +++ b/inire/tests/test_performance_reporting.py @@ -16,10 +16,14 @@ def test_snapshot_example_01_exposes_metrics() -> None: assert snapshot.metrics.route_iterations >= 1 assert snapshot.metrics.nets_routed >= 1 assert snapshot.metrics.nodes_expanded > 0 + assert snapshot.metrics.score_component_calls >= 0 + assert snapshot.metrics.danger_map_lookup_calls >= 0 assert snapshot.metrics.move_cache_abs_misses >= 0 assert snapshot.metrics.ray_cast_calls >= 0 + assert snapshot.metrics.ray_cast_calls_expand_forward >= 0 assert snapshot.metrics.dynamic_tree_rebuilds >= 0 assert snapshot.metrics.visibility_builds >= 0 + assert snapshot.metrics.refinement_candidates_verified >= 0 def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None: @@ -43,3 +47,41 @@ def test_record_performance_baseline_script_writes_selected_scenario(tmp_path: P assert payload["generator"] == "scripts/record_performance_baseline.py" assert [entry["name"] for entry in payload["scenarios"]] == ["example_01_simple_route"] assert (tmp_path / "performance.md").exists() + + +def test_diff_performance_baseline_script_writes_selected_scenario(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[2] + record_script = repo_root / "scripts" / "record_performance_baseline.py" + diff_script = repo_root / "scripts" / "diff_performance_baseline.py" + baseline_dir = tmp_path / "baseline" + baseline_dir.mkdir() + output_path = tmp_path / "diff.md" + + subprocess.run( + [ + sys.executable, + str(record_script), + "--output-dir", + str(baseline_dir), + "--scenario", + "example_01_simple_route", + ], + check=True, + ) + subprocess.run( + [ + sys.executable, + str(diff_script), + "--baseline", + str(baseline_dir / "performance_baseline.json"), + "--scenario", + "example_01_simple_route", + "--output", + str(output_path), + ], + check=True, + ) + + report = output_path.read_text() + assert "Performance Baseline Diff" in report + assert "example_01_simple_route" in report diff --git a/scripts/diff_performance_baseline.py b/scripts/diff_performance_baseline.py new file mode 100644 index 0000000..89a0a47 --- /dev/null +++ b/scripts/diff_performance_baseline.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict +from pathlib import Path + +from inire.tests.example_scenarios import SCENARIO_SNAPSHOTS + + +SUMMARY_KEYS = ( + "duration_s", + "route_iterations", + "nets_routed", + "nodes_expanded", + "ray_cast_calls", + "moves_generated", + "moves_added", + "congestion_check_calls", + "verify_path_report_calls", +) + + +def _current_snapshots(selected_scenarios: tuple[str, ...] | None) -> dict[str, dict[str, object]]: + allowed = None if selected_scenarios is None else set(selected_scenarios) + snapshots: dict[str, dict[str, object]] = {} + for name, run in SCENARIO_SNAPSHOTS: + if allowed is not None and name not in allowed: + continue + snapshots[name] = asdict(run()) + return snapshots + + +def _load_baseline(path: Path, selected_scenarios: tuple[str, ...] | None) -> dict[str, dict[str, object]]: + payload = json.loads(path.read_text()) + allowed = None if selected_scenarios is None else set(selected_scenarios) + return { + entry["name"]: entry + for entry in payload["scenarios"] + if allowed is None or entry["name"] in allowed + } + + +def _metric_value(snapshot: dict[str, object], key: str) -> float: + if key == "duration_s": + return float(snapshot["duration_s"]) + return float(snapshot["metrics"][key]) + + +def _render_report(baseline: dict[str, dict[str, object]], current: dict[str, dict[str, object]]) -> str: + scenario_names = sorted(set(baseline) | set(current)) + lines = [ + "# Performance Baseline Diff", + "", + "| Scenario | Metric | Baseline | Current | Delta |", + "| :-- | :-- | --: | --: | --: |", + ] + for scenario in scenario_names: + base_snapshot = baseline.get(scenario) + curr_snapshot = current.get(scenario) + if base_snapshot is None: + lines.append(f"| {scenario} | added | - | - | - |") + continue + if curr_snapshot is None: + lines.append(f"| {scenario} | missing | - | - | - |") + continue + for key in SUMMARY_KEYS: + base_value = _metric_value(base_snapshot, key) + curr_value = _metric_value(curr_snapshot, key) + lines.append( + f"| {scenario} | {key} | {base_value:.4f} | {curr_value:.4f} | {curr_value - base_value:+.4f} |" + ) + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Diff the committed performance baseline against a fresh run.") + parser.add_argument( + "--baseline", + type=Path, + default=Path("docs/performance_baseline.json"), + help="Baseline JSON to compare against.", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Optional file to write the report to. Defaults to stdout.", + ) + parser.add_argument( + "--scenario", + action="append", + dest="scenarios", + default=[], + help="Optional scenario name to include. May be passed more than once.", + ) + args = parser.parse_args() + + selected = tuple(args.scenarios) if args.scenarios else None + baseline = _load_baseline(args.baseline, selected) + current = _current_snapshots(selected) + report = _render_report(baseline, current) + + if args.output is None: + print(report, end="") + else: + args.output.write_text(report) + print(f"Wrote {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/record_performance_baseline.py b/scripts/record_performance_baseline.py index 8d731be..e254da2 100644 --- a/scripts/record_performance_baseline.py +++ b/scripts/record_performance_baseline.py @@ -46,6 +46,7 @@ def _render_markdown(payload: dict[str, object]) -> str: f"Generated on {payload['generated_on']} by `{payload['generator']}`.", "", "The full machine-readable snapshot lives in `docs/performance_baseline.json`.", + "Use `scripts/diff_performance_baseline.py` to compare a fresh run against that snapshot.", "", "| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |", "| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |", @@ -77,6 +78,7 @@ def _render_markdown(payload: dict[str, object]) -> str: "## Full Counter Set", "", "Each scenario entry in `docs/performance_baseline.json` records the full `RouteMetrics` snapshot, including cache, index, congestion, and verification counters.", + "These counters are currently observational only and are not enforced as CI regression gates.", "", "Tracked metric keys:", "",