Skip to main content

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}