Skip to main content

miniplan/
plan.rs

1//! Plan representation.
2//!
3//! A plan is a totally-ordered sequence of grounded operator applications
4//! whose execution transforms the initial state into a goal-satisfying state.
5//! Plans here are **sequential** — there is no partial ordering or parallelism.
6//! They are produced by the planners in [`crate::search`].
7//!
8//! The [`Plan`] struct carries two invariants:
9//!
10//! - `steps` records *which* grounded operators to apply and in what order.
11//! - `cost` is the numeric sum of the executed operators' costs, so empty
12//!   plans always have `cost == 0.0`.
13//!
14//! The [`Display`](std::fmt::Display) impl on [`Plan`] emits a PDDL-style
15//! human-readable format.
16
17use std::fmt;
18
19use crate::task::OpId;
20
21/// A single step in a [`Plan`].
22///
23/// A `PlanStep` names one grounded-operator application inside a plan.
24/// The `op_id` is the authoritative identifier (an index into the
25/// [`Task`](crate::task::Task)'s operator list), while `op_name` is a
26/// redundant, pretty-printed copy carried along so that plans can be
27/// displayed or serialised without needing the originating `Task`.
28#[derive(Debug, Clone, PartialEq)]
29pub struct PlanStep {
30    /// The operator ID that identifies this step.
31    ///
32    /// Indexes into `crate::task::Task::operators` of the task that
33    /// produced this plan. See [`OpId`] for details.
34    pub op_id: OpId,
35    /// A human-readable operator name (typically [`Operator::name`](crate::task::Operator::name),
36    /// e.g. `"move-A-B"`).
37    ///
38    /// Not guaranteed unique; intended for display and serialisation only —
39    /// do not parse it to recover an operator.
40    pub op_name: String,
41}
42
43/// The output produced by a planner when search succeeds.
44///
45/// This is the structure wrapped by [`SearchOutcome::Plan`](crate::search::SearchOutcome::Plan).
46///
47/// # Invariants
48///
49/// - `steps` is ordered: `steps[0]` is applied first.
50/// - `cost` equals the sum of the applied operators' cost values; it is not
51///   necessarily `steps.len()` because operator costs need not be unit.
52/// - An empty plan (`steps` is empty, `cost == 0.0`) is a valid result when
53///   the initial state already satisfies the goal.
54///
55/// # Examples
56///
57/// ```
58/// use miniplan::plan::{Plan, PlanStep};
59/// use miniplan::task::OpId;
60///
61/// let plan = Plan::new();
62/// assert!(plan.is_empty());
63/// assert_eq!(plan.len(), 0);
64/// assert_eq!(plan.cost, 0.0);
65///
66/// let mut plan = Plan::new();
67/// plan.steps.push(PlanStep { op_id: OpId(0), op_name: "pick".into() });
68/// plan.cost = 1.0;
69/// assert!(!plan.is_empty());
70/// assert_eq!(plan.len(), 1);
71///
72/// assert_eq!(
73///     plan.to_string(),
74///     "; cost = 1\n; length = 1\n(pick)\n"
75/// );
76/// ```
77#[derive(Debug, Clone, PartialEq)]
78pub struct Plan {
79    /// Ordered list of [`PlanStep`]s; execute left-to-right.
80    pub steps: Vec<PlanStep>,
81    /// Accumulated cost of the steps.
82    ///
83    /// Must stay in sync with `steps` — callers mutate both together.
84    pub cost: f64,
85}
86
87impl Default for Plan {
88    /// Creates an empty plan with `steps` empty and `cost` set to `0.0`.
89    ///
90    /// Equivalent to [`Plan::new`].
91    fn default() -> Self {
92        Self {
93            steps: Vec::new(),
94            cost: 0.0,
95        }
96    }
97}
98
99impl Plan {
100    /// Create an empty plan.
101    ///
102    /// The returned value satisfies `steps.is_empty()` and `cost == 0.0`.
103    /// This is equivalent to `Plan::default()`.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use miniplan::plan::Plan;
109    ///
110    /// let plan = Plan::new();
111    /// assert!(plan.is_empty());
112    /// assert_eq!(plan.cost, 0.0);
113    /// ```
114    #[must_use]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Returns `true` if the plan has no steps.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use miniplan::plan::Plan;
125    ///
126    /// assert!(Plan::new().is_empty());
127    /// ```
128    #[must_use]
129    pub fn is_empty(&self) -> bool {
130        self.steps.is_empty()
131    }
132
133    /// Returns the number of steps in the plan.
134    ///
135    /// This counts steps, not cost.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use miniplan::plan::Plan;
141    ///
142    /// assert_eq!(Plan::new().len(), 0);
143    /// ```
144    #[must_use]
145    pub fn len(&self) -> usize {
146        self.steps.len()
147    }
148}
149
150/// Human-readable display format for a [`Plan`].
151///
152/// Emits a PDDL-style plan file:
153///
154/// - Line 1: `; cost = <cost>` (semicolons are PDDL plan comments).
155/// - Line 2: `; length = <len>`.
156/// - Lines 3..: one `(<op_name>)` per step, each terminated by `\n`.
157///
158/// The trailing newline after the last step matches the `writeln!` macro.
159///
160/// # Examples
161///
162/// ```
163/// use miniplan::plan::{Plan, PlanStep};
164/// use miniplan::task::OpId;
165///
166/// let mut plan = Plan::new();
167/// plan.steps.push(PlanStep { op_id: OpId(0), op_name: "pick".into() });
168/// plan.steps.push(PlanStep { op_id: OpId(1), op_name: "place".into() });
169/// plan.cost = 2.0;
170///
171/// assert_eq!(
172///     plan.to_string(),
173///     "; cost = 2\n; length = 2\n(pick)\n(place)\n"
174/// );
175/// ```
176impl fmt::Display for Plan {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        writeln!(f, "; cost = {}", self.cost)?;
179        writeln!(f, "; length = {}", self.len())?;
180        for step in &self.steps {
181            writeln!(f, "({})", step.op_name)?;
182        }
183        Ok(())
184    }
185}