miniplan 0.2.0

A PDDL planner library built around the pddl crate, with grounding and search utilities
Documentation
use std::collections::VecDeque;
use std::time::Instant;

use crate::error::MiniplanError;
use crate::plan::{Plan, PlanStep};
use crate::search::{Planner, PlannerCapabilities, SearchLimits, SearchOutcome, SearchStats, Task};

/// Breadth-first search planner.
///
/// Explores the state space level by level, guaranteeing the shortest
/// (fewest-step) plan. Does not support action costs.
///
/// # Examples
///
/// ```
/// use miniplan::search::{Bfs, Planner};
///
/// let planner = Bfs::new();
/// assert_eq!(planner.name(), "bfs");
/// ```
#[derive(Default)]
pub struct Bfs {}

impl Bfs {
    /// Create a new BFS planner.
    pub fn new() -> Self {
        Self::default()
    }
}

impl Planner for Bfs {
    fn name(&self) -> &str {
        "bfs"
    }

    fn describe(&self) -> &str {
        "Breadth-first search planner"
    }

    fn capabilities(&self) -> PlannerCapabilities {
        PlannerCapabilities::CLASSICAL | PlannerCapabilities::NEGATIVE_PRECONDS
    }

    fn solve(
        &mut self,
        task: &Task,
        limits: &SearchLimits,
    ) -> Result<SearchOutcome, MiniplanError> {
        let start = Instant::now();
        let mut stats = SearchStats::default();

        let mut queue = VecDeque::new();
        let mut closed = rustc_hash::FxHashSet::default();

        queue.push_back((task.init.clone(), Vec::new()));
        closed.insert(task.init.clone());

        while let Some((state, path)) = queue.pop_front() {
            stats.nodes_expanded += 1;

            if state.satisfies(&task.goal_pos, &task.goal_neg) {
                let plan = build_plan(&path, task);
                stats.plan_cost = plan.cost;
                stats.plan_length = plan.len();
                stats.elapsed = start.elapsed();
                return Ok(SearchOutcome::Plan(plan, stats));
            }

            if let Some(max_nodes) = limits.node_budget
                && stats.nodes_expanded >= max_nodes
            {
                stats.elapsed = start.elapsed();
                return Ok(SearchOutcome::LimitReached(stats));
            }

            if let Some(timeout) = limits.time_budget
                && start.elapsed() >= timeout
            {
                stats.elapsed = start.elapsed();
                return Ok(SearchOutcome::LimitReached(stats));
            }

            for op in &task.operators {
                if state.applicable(op) {
                    let next = state.apply(op);
                    if !closed.contains(&next) {
                        closed.insert(next.clone());
                        stats.nodes_generated += 1;
                        let mut new_path = path.clone();
                        new_path.push(op.id);
                        queue.push_back((next, new_path));
                    }
                }
            }
        }

        stats.elapsed = start.elapsed();
        Ok(SearchOutcome::Unsolvable(stats))
    }
}

fn build_plan(path: &[crate::task::OpId], task: &Task) -> Plan {
    let mut plan = Plan::new();
    let mut total_cost = 0.0;
    for &op_id in path {
        if let Some(op) = task.operators.get(op_id.0) {
            plan.steps.push(PlanStep {
                op_id,
                op_name: op.name.clone(),
            });
            total_cost += op.cost as f64;
        }
    }
    plan.cost = total_cost;
    plan
}