1use crate::{
11 Grid,
12 algorithms::any_angle_visibility_graph_kernel::{
13 DijkstraSearchStats, VisibilityGraphBuildStats, VisibilityGraphCsr, add_undirected_edge,
14 build_visibility_csr, elide_collinear_points, node_index, path_endpoints_match_request,
15 run_dijkstra_csr_with_overlay,
16 },
17 any_angle::geometry::{
18 approximately_equal, canonicalize_grid_vertex, extract_boundary_edges, is_endpoint_valid,
19 retained_visibility_vertices, segment_is_legal, validated_any_angle_path,
20 },
21 any_angle::{
22 AnyAngleSearchError, AnyAngleSearchRequest, AnyAngleSearchResult, AnyAngleSearchStats,
23 },
24 search::SearchOutcome,
25};
26use condor_core::Point2;
27
28pub const PREPARED_ANY_ANGLE_NODE_BUDGET: usize = 10_000;
33
34pub const PREPARED_ANY_ANGLE_DIRECTED_EDGE_BUDGET: usize = 32_000_000;
39
40pub const PREPARED_ANY_ANGLE_RETAINED_BYTES_BUDGET: usize = 256_000_000;
42
43pub const PREPARED_ANY_ANGLE_CSR_U32_INDEX_LIMIT: u32 = u32::MAX;
45
46pub const PREPARED_ANY_ANGLE_DEFAULT_BENCHMARK_NODE_BUDGET: usize = 2_000;
53
54#[derive(Debug, Clone, Copy, Default)]
59pub struct PreparedAnyAngleGridBuilder;
60
61impl PreparedAnyAngleGridBuilder {
62 #[must_use]
64 pub const fn new() -> Self {
65 Self
66 }
67
68 #[must_use]
70 pub const fn name(&self) -> &'static str {
71 "prepared-any-angle-grid"
72 }
73
74 pub fn preprocess(
81 &self,
82 grid: &Grid,
83 ) -> Result<PreparedAnyAngleGrid, PreparedAnyAngleGridBuildError> {
84 PreparedAnyAngleGrid::build(grid)
85 }
86
87 #[must_use]
92 pub fn default_benchmark_admission(&self, grid: &Grid) -> PreparedAnyAngleBenchmarkAdmission {
93 let node_count = retained_visibility_vertices(grid).len();
94 match preflight_build_limits(node_count) {
95 Err(error) => PreparedAnyAngleBenchmarkAdmission::Unsupported { error },
96 Ok(()) if node_count > PREPARED_ANY_ANGLE_DEFAULT_BENCHMARK_NODE_BUDGET => {
97 PreparedAnyAngleBenchmarkAdmission::DeferredHeavy { node_count }
98 }
99 Ok(()) => PreparedAnyAngleBenchmarkAdmission::Measure { node_count },
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq)]
109pub struct PreparedAnyAngleGrid {
110 grid: Grid,
111 nodes: Vec<Point2>,
112 csr: VisibilityGraphCsr,
113 build_diagnostics: PreparedAnyAngleBuildDiagnostics,
114}
115
116impl PreparedAnyAngleGrid {
117 #[must_use]
119 pub fn builder() -> PreparedAnyAngleGridBuilder {
120 PreparedAnyAngleGridBuilder::new()
121 }
122
123 #[must_use]
125 pub fn name(&self) -> &'static str {
126 PreparedAnyAngleGridBuilder::new().name()
127 }
128
129 #[must_use]
131 pub fn grid(&self) -> &Grid {
132 &self.grid
133 }
134
135 #[must_use]
137 pub fn nodes(&self) -> &[Point2] {
138 &self.nodes
139 }
140
141 #[must_use]
143 pub fn build_diagnostics(&self) -> &PreparedAnyAngleBuildDiagnostics {
144 &self.build_diagnostics
145 }
146
147 pub fn search(&self, request: AnyAngleSearchRequest) -> AnyAngleSearchResult {
149 self.search_with_diagnostics(request).0
150 }
151
152 pub fn search_with_diagnostics(
154 &self,
155 request: AnyAngleSearchRequest,
156 ) -> (AnyAngleSearchResult, PreparedAnyAngleQueryDiagnostics) {
157 let Some(start) = canonicalize_grid_vertex(request.start) else {
158 return (
159 Err(AnyAngleSearchError::InvalidStart {
160 point: request.start,
161 }),
162 PreparedAnyAngleQueryDiagnostics::default(),
163 );
164 };
165 let Some(goal) = canonicalize_grid_vertex(request.goal) else {
166 return (
167 Err(AnyAngleSearchError::InvalidGoal {
168 point: request.goal,
169 }),
170 PreparedAnyAngleQueryDiagnostics::default(),
171 );
172 };
173 if !is_endpoint_valid(&self.grid, start) {
174 return (
175 Err(AnyAngleSearchError::InvalidStart {
176 point: request.start,
177 }),
178 PreparedAnyAngleQueryDiagnostics::default(),
179 );
180 }
181 if !is_endpoint_valid(&self.grid, goal) {
182 return (
183 Err(AnyAngleSearchError::InvalidGoal {
184 point: request.goal,
185 }),
186 PreparedAnyAngleQueryDiagnostics::default(),
187 );
188 }
189 let request = AnyAngleSearchRequest::new(start, goal);
190
191 if approximately_equal(request.start.x, request.goal.x)
192 && approximately_equal(request.start.y, request.goal.y)
193 {
194 let path = validated_any_angle_path(&self.grid, vec![request.start, request.goal])
195 .expect("start equals goal path is non-empty");
196 return (
197 Ok(SearchOutcome::found(
198 path,
199 AnyAngleSearchStats { visited_nodes: 1 },
200 )),
201 PreparedAnyAngleQueryDiagnostics {
202 path_point_count: 2,
203 ..PreparedAnyAngleQueryDiagnostics::default()
204 },
205 );
206 }
207
208 let mut query_diagnostics = PreparedAnyAngleQueryDiagnostics {
209 direct_start_goal_tested: true,
210 ..Default::default()
211 };
212 query_diagnostics.endpoint_visibility_tests += 1;
213 if segment_is_legal(&self.grid, request.start, request.goal) {
214 query_diagnostics.direct_start_goal_visible = true;
215 let path = validated_any_angle_path(&self.grid, vec![request.start, request.goal])
216 .expect("direct visible segment should validate");
217 return (
218 Ok(SearchOutcome::found(
219 path,
220 AnyAngleSearchStats { visited_nodes: 1 },
221 )),
222 PreparedAnyAngleQueryDiagnostics {
223 path_point_count: 2,
224 ..query_diagnostics
225 },
226 );
227 }
228
229 let (supplemental_nodes, overlay_edges, start_index, goal_index, overlay_stats) =
230 build_overlay_search_graph(&self.grid, &self.nodes, request.start, request.goal);
231 query_diagnostics.endpoint_visibility_tests += overlay_stats.endpoint_visibility_tests;
232 query_diagnostics.endpoint_accepted_edges += overlay_stats.endpoint_accepted_edges;
233
234 let mut search_stats = DijkstraSearchStats::default();
235 let watch = crate::search::BudgetWatch::start(request.budget);
236 let search = match run_dijkstra_csr_with_overlay(
237 &self.csr,
238 self.nodes.len(),
239 &overlay_edges,
240 start_index,
241 goal_index,
242 &mut search_stats,
243 &watch,
244 ) {
245 Ok(outcome) => outcome,
246 Err(reason) => {
247 return (
248 Err(crate::any_angle::budget_error(reason)),
249 query_diagnostics,
250 );
251 }
252 };
253 query_diagnostics.settled_nodes = search_stats.settled_nodes;
254 query_diagnostics.pushes = search_stats.pushes;
255 query_diagnostics.stale_pops = search_stats.stale_pops;
256
257 let Some(predecessors) = search else {
258 return (
259 Ok(SearchOutcome::no_path(AnyAngleSearchStats {
260 visited_nodes: query_diagnostics.settled_nodes,
261 })),
262 query_diagnostics,
263 );
264 };
265
266 let mut path_points =
267 reconstruct_prepared_path(&self.nodes, &supplemental_nodes, &predecessors, goal_index);
268 elide_collinear_points(&self.grid, &mut path_points);
269 query_diagnostics.path_point_count = path_points.len();
270
271 let Ok(path) = validated_any_angle_path(&self.grid, path_points) else {
272 return (
273 Ok(SearchOutcome::no_path(AnyAngleSearchStats {
274 visited_nodes: query_diagnostics.settled_nodes,
275 })),
276 query_diagnostics,
277 );
278 };
279 if !path_endpoints_match_request(&path, request) {
280 return (
281 Ok(SearchOutcome::no_path(AnyAngleSearchStats {
282 visited_nodes: query_diagnostics.settled_nodes,
283 })),
284 query_diagnostics,
285 );
286 }
287
288 (
289 Ok(SearchOutcome::found(
290 path,
291 AnyAngleSearchStats {
292 visited_nodes: query_diagnostics.settled_nodes,
293 },
294 )),
295 query_diagnostics,
296 )
297 }
298
299 fn build(grid: &Grid) -> Result<Self, PreparedAnyAngleGridBuildError> {
300 let boundary_edges = extract_boundary_edges(grid).len();
301 let nodes = retained_visibility_vertices(grid);
302 preflight_build_limits(nodes.len())?;
303
304 let mut build_stats = VisibilityGraphBuildStats::default();
305 let csr = build_visibility_csr(grid, &nodes, &mut build_stats);
306 let node_bytes = nodes.len() * size_of::<Point2>();
307 let build_diagnostics = PreparedAnyAngleBuildDiagnostics {
308 boundary_edges,
309 prepared_nodes: nodes.len(),
310 directed_edges: csr.directed_edge_count(),
311 visibility_checks: build_stats.visibility_checks,
312 accepted_undirected_edges: build_stats.accepted_undirected_edges,
313 retained_bytes: node_bytes + csr.retained_bytes(),
314 };
315
316 Ok(Self {
317 grid: grid.clone(),
318 nodes,
319 csr,
320 build_diagnostics,
321 })
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum PreparedAnyAngleBenchmarkAdmission {
328 Measure { node_count: usize },
330 DeferredHeavy { node_count: usize },
332 Unsupported {
334 error: PreparedAnyAngleGridBuildError,
335 },
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub struct PreparedAnyAngleBuildDiagnostics {
341 pub boundary_edges: usize,
342 pub prepared_nodes: usize,
343 pub directed_edges: usize,
344 pub visibility_checks: usize,
345 pub accepted_undirected_edges: usize,
346 pub retained_bytes: usize,
347}
348
349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
351pub struct PreparedAnyAngleQueryDiagnostics {
352 pub endpoint_visibility_tests: usize,
353 pub endpoint_accepted_edges: usize,
354 pub direct_start_goal_tested: bool,
355 pub direct_start_goal_visible: bool,
356 pub settled_nodes: usize,
357 pub pushes: usize,
358 pub stale_pops: usize,
359 pub path_point_count: usize,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
364#[non_exhaustive]
365pub enum PreparedAnyAngleGridBuildError {
366 #[error("prepared any-angle graph exceeds supported node budget ({node_count} > {limit})")]
368 NodeBudgetExceeded { node_count: usize, limit: usize },
369 #[error(
371 "prepared any-angle graph exceeds supported directed-edge budget ({edge_count} > {limit})"
372 )]
373 DirectedEdgeBudgetExceeded { edge_count: usize, limit: usize },
374 #[error("prepared any-angle graph exceeds supported retained-byte budget ({bytes} > {limit})")]
376 RetainedBytesBudgetExceeded { bytes: usize, limit: usize },
377 #[error("prepared any-angle graph exceeds CSR u32 index capacity ({index_count} > {limit})")]
379 CsrIndexCapacityExceeded { index_count: u64, limit: u32 },
380}
381
382fn preflight_build_limits(node_count: usize) -> Result<(), PreparedAnyAngleGridBuildError> {
383 if node_count > PREPARED_ANY_ANGLE_NODE_BUDGET {
384 return Err(PreparedAnyAngleGridBuildError::NodeBudgetExceeded {
385 node_count,
386 limit: PREPARED_ANY_ANGLE_NODE_BUDGET,
387 });
388 }
389
390 let directed_edge_upper_bound = node_count.checked_mul(node_count.saturating_sub(1)).ok_or(
391 PreparedAnyAngleGridBuildError::DirectedEdgeBudgetExceeded {
392 edge_count: usize::MAX,
393 limit: PREPARED_ANY_ANGLE_DIRECTED_EDGE_BUDGET,
394 },
395 )?;
396 if directed_edge_upper_bound > PREPARED_ANY_ANGLE_DIRECTED_EDGE_BUDGET {
397 return Err(PreparedAnyAngleGridBuildError::DirectedEdgeBudgetExceeded {
398 edge_count: directed_edge_upper_bound,
399 limit: PREPARED_ANY_ANGLE_DIRECTED_EDGE_BUDGET,
400 });
401 }
402
403 let retained_bytes = conservative_retained_bytes_upper_bound(node_count).ok_or(
404 PreparedAnyAngleGridBuildError::RetainedBytesBudgetExceeded {
405 bytes: usize::MAX,
406 limit: PREPARED_ANY_ANGLE_RETAINED_BYTES_BUDGET,
407 },
408 )?;
409 if retained_bytes > PREPARED_ANY_ANGLE_RETAINED_BYTES_BUDGET {
410 return Err(
411 PreparedAnyAngleGridBuildError::RetainedBytesBudgetExceeded {
412 bytes: retained_bytes,
413 limit: PREPARED_ANY_ANGLE_RETAINED_BYTES_BUDGET,
414 },
415 );
416 }
417
418 let offset_capacity = node_count.saturating_add(1) as u64;
419 let index_capacity = directed_edge_upper_bound as u64;
420 if offset_capacity > u64::from(PREPARED_ANY_ANGLE_CSR_U32_INDEX_LIMIT)
421 || index_capacity > u64::from(PREPARED_ANY_ANGLE_CSR_U32_INDEX_LIMIT)
422 {
423 return Err(PreparedAnyAngleGridBuildError::CsrIndexCapacityExceeded {
424 index_count: offset_capacity.max(index_capacity),
425 limit: PREPARED_ANY_ANGLE_CSR_U32_INDEX_LIMIT,
426 });
427 }
428
429 Ok(())
430}
431
432fn conservative_retained_bytes_upper_bound(node_count: usize) -> Option<usize> {
433 let directed_edges = node_count.checked_mul(node_count.saturating_sub(1))?;
434 let node_bytes = node_count.checked_mul(size_of::<Point2>())?;
435 let offset_bytes = node_count.saturating_add(1).checked_mul(size_of::<u32>())?;
436 let neighbor_bytes = directed_edges.checked_mul(size_of::<u32>())?;
437 let weight_bytes = directed_edges.checked_mul(size_of::<f64>())?;
438 node_bytes
439 .checked_add(offset_bytes)?
440 .checked_add(neighbor_bytes)?
441 .checked_add(weight_bytes)
442}
443
444#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
445struct OverlayBuildStats {
446 endpoint_visibility_tests: usize,
447 endpoint_accepted_edges: usize,
448}
449
450type OverlaySearchGraph = (
451 Vec<Point2>,
452 Vec<Vec<(usize, f64)>>,
453 usize,
454 usize,
455 OverlayBuildStats,
456);
457
458fn build_overlay_search_graph(
459 grid: &Grid,
460 prepared_nodes: &[Point2],
461 start: Point2,
462 goal: Point2,
463) -> OverlaySearchGraph {
464 let prepared_count = prepared_nodes.len();
465 let mut supplemental_nodes = Vec::new();
466 let mut stats = OverlayBuildStats::default();
467
468 let start_index = resolve_search_node_index(prepared_nodes, &mut supplemental_nodes, start);
469 let goal_index = resolve_search_node_index(prepared_nodes, &mut supplemental_nodes, goal);
470
471 let node_count = prepared_count + supplemental_nodes.len();
472 let mut overlay_edges = vec![Vec::new(); node_count];
473 overlay_endpoint_edges(
474 grid,
475 prepared_nodes,
476 &supplemental_nodes,
477 &mut overlay_edges,
478 prepared_count,
479 start_index,
480 goal_index,
481 start,
482 &mut stats,
483 );
484 overlay_endpoint_edges(
485 grid,
486 prepared_nodes,
487 &supplemental_nodes,
488 &mut overlay_edges,
489 prepared_count,
490 goal_index,
491 start_index,
492 goal,
493 &mut stats,
494 );
495
496 stats.endpoint_visibility_tests += 1;
497 if segment_is_legal(grid, start, goal) {
498 let weight = start.distance_to(goal);
499 add_undirected_edge(&mut overlay_edges, start_index, goal_index, weight);
500 stats.endpoint_accepted_edges += 1;
501 }
502
503 (
504 supplemental_nodes,
505 overlay_edges,
506 start_index,
507 goal_index,
508 stats,
509 )
510}
511
512fn reconstruct_prepared_path(
513 prepared_nodes: &[Point2],
514 supplemental_nodes: &[Point2],
515 predecessors: &[Option<usize>],
516 goal_index: usize,
517) -> Vec<Point2> {
518 let prepared_count = prepared_nodes.len();
519 let point_for = |index: usize| {
520 if index < prepared_count {
521 prepared_nodes[index]
522 } else {
523 supplemental_nodes[index - prepared_count]
524 }
525 };
526
527 let mut path = vec![point_for(goal_index)];
528 let mut current = goal_index;
529 while let Some(previous) = predecessors[current] {
530 path.push(point_for(previous));
531 if previous == current {
532 break;
533 }
534 current = previous;
535 }
536 path.reverse();
537 path
538}
539
540fn resolve_search_node_index(
541 prepared_nodes: &[Point2],
542 supplemental_nodes: &mut Vec<Point2>,
543 point: Point2,
544) -> usize {
545 match node_index(prepared_nodes, point) {
546 Some(index) => index,
547 None => {
548 let index = prepared_nodes.len() + supplemental_nodes.len();
549 supplemental_nodes.push(point);
550 index
551 }
552 }
553}
554
555#[allow(clippy::too_many_arguments)]
556fn overlay_endpoint_edges(
557 grid: &Grid,
558 prepared_nodes: &[Point2],
559 supplemental_nodes: &[Point2],
560 overlay_edges: &mut [Vec<(usize, f64)>],
561 prepared_count: usize,
562 endpoint_index: usize,
563 other_endpoint_index: usize,
564 endpoint: Point2,
565 stats: &mut OverlayBuildStats,
566) {
567 if endpoint_index < prepared_count {
568 return;
569 }
570
571 for (node_index, node) in prepared_nodes.iter().enumerate().take(prepared_count) {
572 if node_index == endpoint_index || node_index == other_endpoint_index {
573 continue;
574 }
575 let node = *node;
576 stats.endpoint_visibility_tests += 1;
577 if !segment_is_legal(grid, endpoint, node) {
578 continue;
579 }
580 let weight = endpoint.distance_to(node);
581 add_undirected_edge(overlay_edges, endpoint_index, node_index, weight);
582 stats.endpoint_accepted_edges += 1;
583 }
584
585 for (local_index, node) in supplemental_nodes.iter().enumerate() {
586 let node_index = prepared_count + local_index;
587 if node_index == endpoint_index || node_index == other_endpoint_index {
588 continue;
589 }
590 let node = *node;
591 stats.endpoint_visibility_tests += 1;
592 if !segment_is_legal(grid, endpoint, node) {
593 continue;
594 }
595 let weight = endpoint.distance_to(node);
596 add_undirected_edge(overlay_edges, endpoint_index, node_index, weight);
597 stats.endpoint_accepted_edges += 1;
598 }
599}