condor-pathfinding-grid 0.4.0

Grid pathfinding, preprocessing, replanning, and multi-agent algorithms for Condor.
Documentation
//! Private candidate: incremental stencil repair for Field D* (scaffold).
//!
//! **Hypothesis:** retaining interpolation labels and a repair frontier across
//! updates can reduce recomputation while preserving the declared stencil
//! recurrence contract.
//!
//! **Current implementation:** thin wrapper over
//! [`super::field_d_star_exact_incremental_center_graph::FieldDStarExactIncrementalCenterGraph`]
//! with a diagnostic dirty-cell log. No independent stencil state, label
//! retention, or recompute reduction yet — answers are oracle quality by
//! construction.
//!
//! **Non-negotiable behavior:** match the cold center-graph oracle for quality
//! class, retaining partial/fallback/no-path distinctions. Uses only public
//! [`crate::replanning`] helpers.
//!
//! **Evidence and promotion:** ordinary `replanning` while developing; remains
//! private.

use crate::{
    algorithms::field_d_star_exact_incremental_center_graph::FieldDStarExactIncrementalCenterGraph,
    grid::{Cell, Grid, GridEditError},
    point::Point,
    replanning::{InterpolatedGridReplanner, InterpolatedSearchRequest, InterpolatedSearchResult},
};

/// Incremental stencil-repair [`InterpolatedGridReplanner`] candidate.
///
/// Repair frontier is recorded for diagnostics; answer parity is pinned to the
/// exact center-graph oracle.
pub struct FieldDStarIncrementalStencil {
    oracle: FieldDStarExactIncrementalCenterGraph,
    repair_frontier: Vec<Point>,
}

impl FieldDStarIncrementalStencil {
    /// Stable source-local candidate identity.
    pub const CANDIDATE_ID: &str =
        "interpolated-dynamic-replanning/C001-incremental-stencil-repair";

    /// Scaffold constructor: thin oracle wrapper with an empty diagnostic repair
    /// frontier (no independent stencil state yet).
    #[must_use]
    pub fn new() -> Self {
        Self {
            oracle: FieldDStarExactIncrementalCenterGraph::new(),
            repair_frontier: Vec::new(),
        }
    }

    /// Cells marked dirty since the last successful plan (diagnostics).
    #[must_use]
    pub fn repair_frontier(&self) -> &[Point] {
        &self.repair_frontier
    }
}

impl Default for FieldDStarIncrementalStencil {
    fn default() -> Self {
        Self::new()
    }
}

impl InterpolatedGridReplanner for FieldDStarIncrementalStencil {
    fn name(&self) -> &'static str {
        "field-d-star-incremental-stencil"
    }

    fn initialize(
        &mut self,
        grid: &Grid,
        request: InterpolatedSearchRequest,
    ) -> InterpolatedSearchResult {
        self.repair_frontier.clear();
        self.oracle.initialize(grid, request)
    }

    fn update_cell(&mut self, point: Point, cell: Cell) {
        self.repair_frontier.push(point);
        self.oracle.update_cell(point, cell);
    }

    fn update_cost(&mut self, point: Point, cost: usize) -> Result<(), GridEditError> {
        self.repair_frontier.push(point);
        self.oracle.update_cost(point, cost)
    }

    fn replan(&mut self) -> InterpolatedSearchResult {
        let result = self.oracle.replan();
        self.repair_frontier.clear();
        result
    }
}

#[cfg(test)]
mod tests {
    use super::FieldDStarIncrementalStencil;
    use crate::{
        algorithms::field_d_star_exact_incremental_center_graph::FieldDStarExactIncrementalCenterGraph,
        grid::{Cell, Grid},
        point::Point,
        replanning::{
            InterpolatedGridReplanner, InterpolatedPathOutcomeKind, InterpolatedSearchRequest,
        },
    };
    use condor_core::Point2;

    #[test]
    fn cold_found_matches_field_d_star_quality_class() {
        let grid = Grid::new(5, 5).expect("grid");
        let request = InterpolatedSearchRequest::new(Point2::new(0.5, 0.5), Point2::new(4.5, 4.5));
        let mut stencil = FieldDStarIncrementalStencil::new();
        let mut center = FieldDStarExactIncrementalCenterGraph::new();
        let s = stencil.initialize(&grid, request).expect("valid");
        let c = center.initialize(&grid, request).expect("valid");
        assert_eq!(s.expected_kind(), c.expected_kind());
        assert_eq!(
            s.path_outcome().map(|o| o.kind()),
            Some(InterpolatedPathOutcomeKind::Found)
        );
    }

    #[test]
    fn distinguishes_partial_fallback_no_path() {
        let mut grid = Grid::new(3, 3).expect("grid");
        for x in 0..3 {
            grid.set_cell(Point::new(x, 1), Cell::Blocked)
                .expect("valid");
        }
        let request = InterpolatedSearchRequest::new(Point2::new(0.5, 0.5), Point2::new(2.5, 2.5));
        let mut stencil = FieldDStarIncrementalStencil::new();
        let result = stencil.initialize(&grid, request).expect("valid");
        assert_ne!(
            result.path_outcome().map(|o| o.kind()),
            Some(InterpolatedPathOutcomeKind::Found)
        );
    }

    #[test]
    fn replan_after_local_update() {
        let grid = Grid::new(5, 5).expect("grid");
        let request = InterpolatedSearchRequest::new(Point2::new(0.5, 2.5), Point2::new(4.5, 2.5));
        let mut stencil = FieldDStarIncrementalStencil::new();
        let _ = stencil.initialize(&grid, request).expect("valid");
        stencil.update_cell(Point::new(2, 2), Cell::Blocked);
        assert!(!stencil.repair_frontier().is_empty());
        let _ = stencil.replan().expect("valid");
        assert!(stencil.repair_frontier().is_empty());
    }

    #[test]
    fn retains_candidate_id() {
        assert_eq!(
            FieldDStarIncrementalStencil::CANDIDATE_ID,
            "interpolated-dynamic-replanning/C001-incremental-stencil-repair"
        );
    }
}