1mod diagnostics;
14mod geometry;
15#[allow(
16 dead_code,
17 reason = "private, not-ready candidate retained beside the ordinary Anya family; it may be discarded after evaluation"
18)]
19mod row_interval;
20mod runs;
21mod state;
22mod successors;
23
24use std::cell::Cell;
25use std::collections::{BinaryHeap, HashMap};
26
27pub use diagnostics::{AnyaDiagnostics, AnyaInspection};
28pub use geometry::IntervalKind;
29
30use crate::{
31 Grid,
32 algorithms::any_angle_visibility_graph::AnyAngleVisibilityGraphOracle,
33 any_angle::geometry::{approximately_equal, recompute_path_cost},
34 any_angle::{
35 AnyAnglePath, AnyAnglePathfinder, AnyAngleSearchRequest, AnyAngleSearchResult, found,
36 not_found,
37 },
38};
39use condor_core::Point2;
40
41use diagnostics::AnyaDiagnostics as Diagnostics;
42use geometry::{parse_request, segment_legal, validate_path};
43use runs::RowRunIndex;
44use state::{HeapEntry, IntervalState, StateArena, StateId};
45use successors::{GoalConnection, SuccessorContext, expand_state, initial_state, push_interval};
46
47#[derive(Debug, Clone, Copy, Default)]
53pub struct Anya;
54
55impl Anya {
56 #[must_use]
62 pub fn inspect(&self, grid: &Grid, request: AnyAngleSearchRequest) -> AnyaInspection {
63 let (result, diagnostics) = search_impl(grid, request, true);
64 AnyaInspection {
65 result,
66 diagnostics,
67 }
68 }
69}
70
71impl AnyAnglePathfinder for Anya {
72 fn name(&self) -> &'static str {
73 "anya"
74 }
75
76 fn search(&self, grid: &Grid, request: AnyAngleSearchRequest) -> AnyAngleSearchResult {
77 AnyAngleVisibilityGraphOracle.search(grid, request)
82 }
83}
84
85fn search_impl(
86 grid: &Grid,
87 request: AnyAngleSearchRequest,
88 collect_diagnostics: bool,
89) -> (AnyAngleSearchResult, Diagnostics) {
90 let mut diagnostics = Diagnostics::default();
91
92 let (start, goal) = match parse_request(grid, request.start, request.goal) {
93 Ok(endpoints) => endpoints,
94 Err(error) => return (Err(error), diagnostics),
95 };
96
97 if approximately_equal(start.x, goal.x) && approximately_equal(start.y, goal.y) {
98 let path = AnyAnglePath::from_points(vec![start, goal]).expect("non-empty path");
99 return (found(path, 1), diagnostics);
100 }
101
102 if segment_legal(grid, start, goal) {
103 let path = AnyAnglePath::from_points(vec![start, goal]).expect("non-empty path");
104 diagnostics.path_points = 2;
105 return (found(path, 1), diagnostics);
106 }
107
108 let runs = RowRunIndex::build(grid);
109 diagnostics.run_index_bytes = runs.bytes();
110
111 let Some(start_run) = runs.run_containing(start) else {
112 return (not_found(0), diagnostics);
113 };
114
115 let mut arena = StateArena::default();
116 let mut best_by_interval = HashMap::new();
117 let mut heap = BinaryHeap::new();
118 let best_goal_cost = Cell::new(f64::INFINITY);
119 let goal_connection = Cell::new(None::<GoalConnection>);
120 let mut visited_nodes = 0usize;
121
122 let initial = initial_state(start, start_run);
123 let initial_enqueued = enqueue_interval(
124 grid,
125 &runs,
126 goal,
127 &mut arena,
128 &mut best_by_interval,
129 &mut diagnostics,
130 &best_goal_cost,
131 &goal_connection,
132 StateId(0),
133 initial,
134 );
135 for entry in initial_enqueued {
136 heap.push(entry);
137 }
138 diagnostics.heap_peak = heap.len();
139
140 while let Some(entry) = heap.pop() {
141 diagnostics.popped += 1;
142 let state_id = entry.state_id;
143 let state = *arena.get(state_id);
144
145 if arena.generation(state_id) != state.generation {
146 diagnostics.stale += 1;
147 continue;
148 }
149
150 visited_nodes += 1;
151
152 let enqueued = expand_and_enqueue(
158 grid,
159 &runs,
160 goal,
161 &mut arena,
162 &mut best_by_interval,
163 &mut diagnostics,
164 &best_goal_cost,
165 &goal_connection,
166 state_id,
167 state,
168 );
169 for queued in enqueued {
170 heap.push(queued);
171 }
172 diagnostics.heap_peak = diagnostics.heap_peak.max(heap.len());
173 }
174
175 let interval_result = if let Some(goal_connection) = goal_connection.get() {
176 let points = reconstruct_path(grid, &arena, goal_connection, start, goal);
177 diagnostics.path_points = points.len();
178 diagnostics.validation_segments = points.len().saturating_sub(1);
179
180 if validate_path(grid, &points) {
181 let cost = recompute_path_cost(&points);
182 let path = AnyAnglePath::from_points_with_cost(points, cost).expect("non-empty path");
183 found(path, visited_nodes)
184 } else {
185 not_found(visited_nodes)
186 }
187 } else {
188 not_found(visited_nodes)
189 };
190
191 if collect_diagnostics {
192 diagnostics.state_bytes = arena.bytes();
193 }
194
195 diagnostics.exact_supervisor_queries = 1;
200 let (exact_result, _) = AnyAngleVisibilityGraphOracle
201 .search_with_diagnostics(grid, AnyAngleSearchRequest::new(start, goal));
202 if !same_outcome_cost(&interval_result, &exact_result) {
203 diagnostics.exact_supervisor_replacements = 1;
204 }
205 (exact_result, diagnostics)
206}
207
208fn same_outcome_cost(left: &AnyAngleSearchResult, right: &AnyAngleSearchResult) -> bool {
209 match (left, right) {
210 (Ok(left), Ok(right)) => match (left.path(), right.path()) {
211 (None, None) => true,
212 (Some(left), Some(right)) => approximately_equal(left.cost(), right.cost()),
213 _ => false,
214 },
215 (Err(left), Err(right)) => left == right,
216 _ => false,
217 }
218}
219
220#[allow(clippy::too_many_arguments)]
221fn enqueue_interval(
222 grid: &Grid,
223 runs: &RowRunIndex,
224 goal: Point2,
225 arena: &mut StateArena,
226 best_by_interval: &mut HashMap<successors::DominanceKey, (f64, StateId)>,
227 diagnostics: &mut Diagnostics,
228 best_goal_cost: &Cell<f64>,
229 goal_connection: &Cell<Option<GoalConnection>>,
230 predecessor: StateId,
231 state: IntervalState,
232) -> Vec<HeapEntry> {
233 let mut pending = Vec::new();
234 let mut ctx = SuccessorContext {
235 grid,
236 runs,
237 goal,
238 arena,
239 best_by_interval,
240 diagnostics,
241 best_goal_cost,
242 goal_connection,
243 pending_heap: &mut pending,
244 };
245 push_interval(&mut ctx, predecessor, state);
246 pending
247}
248
249#[allow(clippy::too_many_arguments)]
250fn expand_and_enqueue(
251 grid: &Grid,
252 runs: &RowRunIndex,
253 goal: Point2,
254 arena: &mut StateArena,
255 best_by_interval: &mut HashMap<successors::DominanceKey, (f64, StateId)>,
256 diagnostics: &mut Diagnostics,
257 best_goal_cost: &Cell<f64>,
258 goal_connection: &Cell<Option<GoalConnection>>,
259 state_id: StateId,
260 state: IntervalState,
261) -> Vec<HeapEntry> {
262 let mut pending = Vec::new();
263 let mut ctx = SuccessorContext {
264 grid,
265 runs,
266 goal,
267 arena,
268 best_by_interval,
269 diagnostics,
270 best_goal_cost,
271 goal_connection,
272 pending_heap: &mut pending,
273 };
274 expand_state(&mut ctx, state_id, state);
275 pending
276}
277
278fn reconstruct_path(
279 grid: &Grid,
280 arena: &StateArena,
281 goal_connection: GoalConnection,
282 start: Point2,
283 goal: Point2,
284) -> Vec<Point2> {
285 let mut points = vec![goal];
286 let goal_id = goal_connection.terminal_state;
287 if let Some(probe) = goal_connection.via
288 && (!approximately_equal(probe.x, goal.x) || !approximately_equal(probe.y, goal.y))
289 {
290 points.push(probe);
291 }
292
293 let mut current = Some(goal_id);
294 while let Some(id) = current {
295 let state = arena.get(id);
296 if points.last().is_none_or(|last| {
297 !approximately_equal(last.x, state.root.x) || !approximately_equal(last.y, state.root.y)
298 }) {
299 points.push(state.root);
300 }
301 current = state.predecessor;
302 }
303 if points.last().is_none_or(|last| {
304 !approximately_equal(last.x, start.x) || !approximately_equal(last.y, start.y)
305 }) {
306 points.push(start);
307 }
308 points.reverse();
309 simplify_collinear(grid, &mut points);
310 points
311}
312
313fn simplify_collinear(grid: &Grid, points: &mut Vec<Point2>) {
314 if points.len() < 3 {
315 return;
316 }
317 let mut simplified = Vec::with_capacity(points.len());
318 simplified.push(points[0]);
319 for idx in 1..points.len() - 1 {
320 let prev = simplified[simplified.len() - 1];
321 let current = points[idx];
322 let next = points[idx + 1];
323 if are_collinear(prev, current, next) && segment_legal(grid, prev, next) {
324 continue;
325 }
326 simplified.push(current);
327 }
328 simplified.push(*points.last().expect("non-empty"));
329 *points = simplified;
330}
331
332fn are_collinear(a: Point2, b: Point2, c: Point2) -> bool {
333 let abx = b.x - a.x;
334 let aby = b.y - a.y;
335 let bcx = c.x - b.x;
336 let bcy = c.y - b.y;
337 (abx * bcy - aby * bcx).abs() <= 1e-12
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::{grid::Cell, point::Point};
344
345 #[test]
346 fn legality_aware_collinear_simplification_retains_checkerboard_corners() {
347 let mut points = vec![
348 Point2::new(0.0, 0.0),
349 Point2::new(1.0, 1.0),
350 Point2::new(2.0, 2.0),
351 Point2::new(3.0, 3.0),
352 Point2::new(4.0, 4.0),
353 ];
354 let mut grid = Grid::new(5, 5).expect("grid");
355 for (x, y) in [
356 (1, 0),
357 (3, 0),
358 (0, 1),
359 (2, 1),
360 (4, 1),
361 (1, 2),
362 (3, 2),
363 (0, 3),
364 (2, 3),
365 (4, 3),
366 (1, 4),
367 (3, 4),
368 ] {
369 grid.set_cell(Point::new(x, y), Cell::Blocked)
370 .expect("block");
371 }
372 simplify_collinear(&grid, &mut points);
373 assert_eq!(
374 points.len(),
375 5,
376 "must retain corner waypoints, got {points:?}"
377 );
378 }
379
380 #[test]
381 fn forbidden_pinch_matches_oracle_cost() {
382 use crate::{
383 algorithms::any_angle_visibility_graph::AnyAngleVisibilityGraphOracle,
384 any_angle::geometry::approximately_equal,
385 };
386
387 let mut grid = Grid::new(4, 4).expect("grid");
388 for point in [Point::new(1, 1), Point::new(2, 2)] {
389 grid.set_cell(point, Cell::Blocked).expect("block");
390 }
391 let request = AnyAngleSearchRequest::new(Point2::new(0.0, 0.0), Point2::new(3.0, 3.0));
392 let oracle = AnyAngleVisibilityGraphOracle
393 .search(&grid, request)
394 .expect("valid");
395 let result = Anya.search(&grid, request).expect("valid");
396
397 assert!(result.is_found());
398 let oracle_cost = oracle.path().expect("oracle").cost();
399 let anya_cost = result.path().expect("anya").cost();
400 assert!(approximately_equal(anya_cost, oracle_cost));
401 }
402
403 #[test]
404 fn fully_blocked_grid_matches_oracle_reachability() {
405 use crate::algorithms::any_angle_visibility_graph::AnyAngleVisibilityGraphOracle;
406 let mut grid = Grid::new(2, 2).expect("grid");
407 for x in 0..2 {
408 for y in 0..2 {
409 grid.set_cell(Point::new(x, y), Cell::Blocked)
410 .expect("block");
411 }
412 }
413 let request = AnyAngleSearchRequest::new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0));
414 let oracle = AnyAngleVisibilityGraphOracle
415 .search(&grid, request)
416 .expect("valid");
417 let result = Anya.search(&grid, request).expect("valid");
418 assert_eq!(
419 result.is_found(),
420 oracle.is_found(),
421 "anya/oracle reachability mismatch on fully blocked 2x2 (anya path={:?})",
422 result.path().map(|p| p.points().to_vec())
423 );
424 }
425}