condor_core/search.rs
1//! Shared search outcome shape used across Condor lanes.
2//!
3//! [`SearchOutcome`] separates **successful search computation** (`Found` /
4//! `NoPath`, both with stats) from **request validation**, which remains an
5//! outer `Result::Err` defined by each owner crate. Domain path and stats types
6//! implement [`SearchPathCost`] and [`SearchVisitStats`] so callers can read
7//! cost and visit effort without downcasting.
8//!
9//! # Contract
10//!
11//! Owners return `Result<SearchOutcome<Path, Stats>, Error>`: `Err` means the
12//! request was invalid, could not start, or hit a caller [`crate::SearchBudget`]
13//! hard stop ([`crate::BudgetExhausted`]), while [`SearchOutcome::Found`] and
14//! [`SearchOutcome::NoPath`] are completed searches that proved a route or
15//! unreachability. Cost units and visit counts remain lane-specific, so compare
16//! them within one solver family only.
17
18/// Successful search computation, separate from request validation failures.
19///
20/// `Found` and `NoPath` both carry stats so callers can compare effort even when
21/// no route exists. Domain-specific input validation remains an outer `Err` in
22/// each owner crate.
23#[non_exhaustive]
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum SearchOutcome<P, S> {
26 /// A route was found together with algorithm-defined work statistics.
27 Found {
28 /// Domain path value (grid path, free-space polyline, corridor path, …).
29 path: P,
30 /// Algorithm-defined work counters for this successful search.
31 stats: S,
32 },
33 /// Exhaustive search found no route, but still produced work statistics.
34 NoPath {
35 /// Algorithm-defined work counters for the exhaustive no-path search.
36 stats: S,
37 },
38}
39
40impl<P, S> SearchOutcome<P, S> {
41 /// Successful route with work stats (not a validation error).
42 #[must_use]
43 pub const fn found(path: P, stats: S) -> Self {
44 Self::Found { path, stats }
45 }
46
47 /// Exhaustive search found no route; still carries work stats for comparison.
48 #[must_use]
49 pub const fn no_path(stats: S) -> Self {
50 Self::NoPath { stats }
51 }
52
53 /// Whether this outcome is [`Self::Found`] (validation failures are outer `Err`).
54 #[must_use]
55 pub const fn is_found(&self) -> bool {
56 matches!(self, Self::Found { .. })
57 }
58
59 /// Path when found; `None` for [`Self::NoPath`].
60 #[must_use]
61 pub const fn path(&self) -> Option<&P> {
62 match self {
63 Self::Found { path, .. } => Some(path),
64 Self::NoPath { .. } => None,
65 }
66 }
67
68 /// Work stats from either branch (`Found` and `NoPath` both always carry stats).
69 #[must_use]
70 pub const fn stats(&self) -> &S {
71 match self {
72 Self::Found { stats, .. } | Self::NoPath { stats } => stats,
73 }
74 }
75}
76
77/// Path value that exposes the cost reported by its search domain.
78///
79/// Cost units and optimality claims are defined by the owning lane (grid
80/// traversal cost, Euclidean polyline length, etc.).
81pub trait SearchPathCost {
82 /// Cost type used by this path family (for example `f64` Euclidean length).
83 type Cost;
84
85 /// Returns the path's domain-specific traversal cost.
86 fn path_cost(&self) -> Self::Cost;
87}
88
89/// Search statistics that expose an algorithm-defined node-visit count.
90///
91/// "Node" meaning is algorithm-specific (expanded cells, graph vertices, taut
92/// path keys, …); values are comparable within one solver, not across lanes.
93pub trait SearchVisitStats {
94 /// Returns the number of nodes visited by the search.
95 fn visited_nodes(&self) -> usize;
96}
97
98impl<P, S> SearchOutcome<P, S>
99where
100 P: SearchPathCost,
101{
102 /// Path cost when found; `None` for no-path outcomes.
103 #[must_use]
104 pub fn cost(&self) -> Option<P::Cost> {
105 self.path().map(SearchPathCost::path_cost)
106 }
107}
108
109impl<P, S> SearchOutcome<P, S>
110where
111 S: SearchVisitStats,
112{
113 /// Algorithm-defined visit count from either found or no-path stats.
114 #[must_use]
115 pub fn visited_nodes(&self) -> usize {
116 self.stats().visited_nodes()
117 }
118}