condor-pathfinding-core 0.4.0

Neutral cross-domain pathfinding primitives shared by Condor algorithm crates.
Documentation
//! Shared search outcome shape used across Condor lanes.
//!
//! [`SearchOutcome`] separates **successful search computation** (`Found` /
//! `NoPath`, both with stats) from **request validation**, which remains an
//! outer `Result::Err` defined by each owner crate. Domain path and stats types
//! implement [`SearchPathCost`] and [`SearchVisitStats`] so callers can read
//! cost and visit effort without downcasting.
//!
//! # Contract
//!
//! Owners return `Result<SearchOutcome<Path, Stats>, Error>`: `Err` means the
//! request was invalid, could not start, or hit a caller [`crate::SearchBudget`]
//! hard stop ([`crate::BudgetExhausted`]), while [`SearchOutcome::Found`] and
//! [`SearchOutcome::NoPath`] are completed searches that proved a route or
//! unreachability. Cost units and visit counts remain lane-specific, so compare
//! them within one solver family only.

/// Successful search computation, separate from request validation failures.
///
/// `Found` and `NoPath` both carry stats so callers can compare effort even when
/// no route exists. Domain-specific input validation remains an outer `Err` in
/// each owner crate.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SearchOutcome<P, S> {
    /// A route was found together with algorithm-defined work statistics.
    Found {
        /// Domain path value (grid path, free-space polyline, corridor path, …).
        path: P,
        /// Algorithm-defined work counters for this successful search.
        stats: S,
    },
    /// Exhaustive search found no route, but still produced work statistics.
    NoPath {
        /// Algorithm-defined work counters for the exhaustive no-path search.
        stats: S,
    },
}

impl<P, S> SearchOutcome<P, S> {
    /// Successful route with work stats (not a validation error).
    #[must_use]
    pub const fn found(path: P, stats: S) -> Self {
        Self::Found { path, stats }
    }

    /// Exhaustive search found no route; still carries work stats for comparison.
    #[must_use]
    pub const fn no_path(stats: S) -> Self {
        Self::NoPath { stats }
    }

    /// Whether this outcome is [`Self::Found`] (validation failures are outer `Err`).
    #[must_use]
    pub const fn is_found(&self) -> bool {
        matches!(self, Self::Found { .. })
    }

    /// Path when found; `None` for [`Self::NoPath`].
    #[must_use]
    pub const fn path(&self) -> Option<&P> {
        match self {
            Self::Found { path, .. } => Some(path),
            Self::NoPath { .. } => None,
        }
    }

    /// Work stats from either branch (`Found` and `NoPath` both always carry stats).
    #[must_use]
    pub const fn stats(&self) -> &S {
        match self {
            Self::Found { stats, .. } | Self::NoPath { stats } => stats,
        }
    }
}

/// Path value that exposes the cost reported by its search domain.
///
/// Cost units and optimality claims are defined by the owning lane (grid
/// traversal cost, Euclidean polyline length, etc.).
pub trait SearchPathCost {
    /// Cost type used by this path family (for example `f64` Euclidean length).
    type Cost;

    /// Returns the path's domain-specific traversal cost.
    fn path_cost(&self) -> Self::Cost;
}

/// Search statistics that expose an algorithm-defined node-visit count.
///
/// "Node" meaning is algorithm-specific (expanded cells, graph vertices, taut
/// path keys, …); values are comparable within one solver, not across lanes.
pub trait SearchVisitStats {
    /// Returns the number of nodes visited by the search.
    fn visited_nodes(&self) -> usize;
}

impl<P, S> SearchOutcome<P, S>
where
    P: SearchPathCost,
{
    /// Path cost when found; `None` for no-path outcomes.
    #[must_use]
    pub fn cost(&self) -> Option<P::Cost> {
        self.path().map(SearchPathCost::path_cost)
    }
}

impl<P, S> SearchOutcome<P, S>
where
    S: SearchVisitStats,
{
    /// Algorithm-defined visit count from either found or no-path stats.
    #[must_use]
    pub fn visited_nodes(&self) -> usize {
        self.stats().visited_nodes()
    }
}