space_search/
lib.rs

1//! # space-search
2//!
3//! A library providing basic generic depth-first, breadth-first, heuristic-guided, and A* based search space exploration algorithms.
4//!
5//! Implement [`Searchable`] + [`SolutionIdentifiable`] to perform breadth-first or depth-first searching. Implement [`Scoreable`] as well to perform heuristically guided search space exploration. Finally, additionally implement [`CostSearchable`] to perform A* based search exploration. Pass them to [`Searcher`] to create an iterator that will search for a solution.
6//!
7//! [`Searcher`] requires that you specify a `Manager` type that determines the strategy, return result, and optimization of the search algorithm. Choose one of the searchers defined in the hierarchy of the [`search`] module to fit your individual needs.
8//!
9//! * Implement [`Scoreable`] to utilize the `guided` search strategy based managers, which will prioritize searching states with a lower associated score first. Additionally, implement [`CostSearchable`] to make use of the A* based search managers in the `a_star` module. If implementing [`Scoreable`] is too complex or unnecessary for your use case, then you may use the `unguided` search managers, which explore the space naively in a depth-first or breadth-first manner, toggleable by a flag on the manager itself.
10//! * Use a `route` based manager to yield results consisting of the sequence of steps taken from the starting state to the ending state. Use a `no_route` manager to just yield the solution state alone. Route based managers require that your state type implement [`Clone`].
11//! * Implement [`Eq`] + [`std::hash::Hash`] + [`Clone`] for your [`Searchable`] type to benefit from prior explored state checking optimization using a `hashable` manager; if youre unable to, then use an `unhashable` manager, which does not require these additional bounds, but will likely explore the space much less efficiently unless cyclic traversal is not an inherent property of your search space.
12//!
13//! When implementing [`Scoreable`], make sure that lower scoring states are closer to a solution.
14//!
15//! ```
16//! use space_search::*;
17//! use std::{vec, hash::Hash};
18//!
19//! #[derive(Clone, Debug, PartialEq, Eq, Hash)]
20//! struct Pos(i32, i32);
21//!
22//! impl Searchable for Pos {
23//!     fn next_states(&self) -> impl Iterator<Item = Self> {
24//!         let &Pos(x, y) = self;
25//!         [
26//!             Pos(x - 1, y),
27//!             Pos(x, y - 1),
28//!             Pos(x + 1, y),
29//!             Pos(x, y + 1),
30//!         ].into_iter()
31//!     }
32//! }
33//!
34//! impl SolutionIdentifiable for Pos {
35//!     fn is_solution(&self) -> bool {
36//!         let &Pos(x, y) = self;
37//!         x == 5 && y == 5
38//!     }
39//! }
40//!
41//! let mut searcher: Searcher<search::unguided::no_route::hashable::Manager<_>, _> = Searcher::new(Pos(0, 0));
42//! assert_eq!(searcher.next(), Some(Pos(5, 5)));
43//! ```
44
45use std::collections::VecDeque;
46
47pub mod search;
48
49/// Basic trait for depth-first and breadth-first search space exploration.
50///
51/// Implement this + [`SolutionIdentifiable`] for your state type to perform search-space exploration.
52pub trait Searchable: Sized {
53    /// Yield all adjacent explorable states reachable from this state.
54    fn next_states(&self) -> impl Iterator<Item = Self>;
55}
56
57/// Trait that allows a state to be identified as a solution.
58///
59/// Implement this + [`Searchable`] for your state type to perform search-space exploration.
60pub trait SolutionIdentifiable {
61    /// Return `true` if this state is a solution state.
62    fn is_solution(&self) -> bool;
63}
64
65/// Trait for search space exploration guided by a heuristic.
66///
67/// Implement this + [`Searchable`] + [`SolutionIdentifiable`] to perform heuristically-guided search-space exploration.
68///
69/// New states are explored in the order of
70/// lowest-scoring first, biasing the search exploration in the direction of a solution. Ensure the scores
71/// returned by [`Scoreable::score`] are decreasing with the proximity to a solution.
72pub trait Scoreable {
73    /// Type used to represent a state's score.
74    /// Common types can be [`i32`], [`usize`], an ordered float type, or your own type implementing [`Ord`].
75    type Score: Ord;
76
77    /// Score function used for heuristic exploration. New states are explored in the order of
78    /// lowest-scoring first; ensure the scores
79    /// returned by this function decrease with the proximity to a solution.
80    fn score(&self) -> Self::Score;
81}
82
83/// Trait for search space exploration guided by a cost function & heuristic.
84///
85/// Implement this + [`Scoreable`] + [`SolutionIdentifiable`] to perform A* guided search-space exploration.
86///
87/// [`Searchable`] is automatically implemented if this trait is implemented.
88pub trait CostSearchable: Scoreable + Sized {
89    /// Yield all adjacent explorable states reachable from this state, paired with the associated cost
90    /// of traversing from the current state each new state.
91    fn next_states_with_costs(&self) -> impl Iterator<Item = (Self, Self::Score)>;
92}
93
94// this doesnt work unfortunately :(
95// we can only have one blanket impl or the other
96
97// /// Marker trait to auto-implement [`CostSearchable`] for any type that already implements [`Scoreable`] + [`Searchable`]; the cost
98// /// is assumed to be 1 for all states returned by [`Searchable::next_states`].
99// pub trait CostSearchableForSearchable {}
100
101// impl<T> CostSearchable for T
102// where
103//     T: Scoreable + Searchable + CostSearchableForSearchable,
104//     T::Score: One,
105// {
106//     fn next_states_with_costs(&self) -> impl Iterator<Item = (Self, Self::Score)> {
107//         self.next_states().map(|s| (s, T::Score::one()))
108//     }
109// }
110
111impl<T> Searchable for T
112where
113    T: CostSearchable,
114{
115    fn next_states(&self) -> impl Iterator<Item = Self> {
116        self.next_states_with_costs().map(|(s, _)| s)
117    }
118}
119
120/// Internal.
121///
122/// Used to represent states paired with their scores in guided exploration managers.
123struct OrderedSearchable<T, C> {
124    state: T,
125    score: C,
126}
127
128impl<S> From<S> for OrderedSearchable<S, S::Score>
129where
130    S: Scoreable,
131{
132    fn from(state: S) -> Self {
133        let score = state.score();
134        OrderedSearchable { state, score }
135    }
136}
137
138impl<S> From<StateParent<S>> for OrderedSearchable<StateParent<S>, S::Score>
139where
140    S: Scoreable,
141{
142    fn from(pair: StateParent<S>) -> Self {
143        let score = pair.state.score();
144        OrderedSearchable { state: pair, score }
145    }
146}
147
148impl<T, C> PartialEq for OrderedSearchable<T, C>
149where
150    C: PartialEq,
151{
152    fn eq(&self, other: &Self) -> bool {
153        self.score == other.score
154    }
155}
156
157impl<T, C> Eq for OrderedSearchable<T, C> where C: Eq {}
158
159impl<T, C> PartialOrd for OrderedSearchable<T, C>
160where
161    C: PartialOrd,
162{
163    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
164        other.score.partial_cmp(&self.score)
165    }
166}
167
168impl<T, C> Ord for OrderedSearchable<T, C>
169where
170    C: Ord,
171{
172    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
173        other.score.cmp(&self.score)
174    }
175}
176
177/// Internal.
178///
179/// Used to represent states with no additional context in solution-only yielding managers.
180pub struct NoContext<S>(S);
181
182impl<S> AsRef<S> for NoContext<S> {
183    fn as_ref(&self) -> &S {
184        &self.0
185    }
186}
187
188/// Internal.
189///
190/// Used to represent states with the added context of their parent state
191/// in solution-route yielding managers.
192#[derive(Clone)]
193pub struct StateParent<S> {
194    state: S,
195    parent: Option<usize>,
196}
197
198impl<S> AsRef<S> for StateParent<S> {
199    fn as_ref(&self) -> &S {
200        &self.state
201    }
202}
203
204/// Internal.
205///
206/// Used to represent states with the added context of their
207/// cumulative rolling cost for A* based managers.
208pub struct StateCumulativeCost<S, C> {
209    state: S,
210    cumulative_cost: C,
211}
212
213impl<S, C> AsRef<S> for StateCumulativeCost<S, C> {
214    fn as_ref(&self) -> &S {
215        &self.state
216    }
217}
218
219/// Internal.
220///
221/// Used to represent states with the added context of their parent state &
222/// cumulative rolling cost for A* based solution route-yielding managers.
223#[derive(Clone)]
224pub struct StateParentCumulativeCost<S, C> {
225    state: S,
226    parent: Option<usize>,
227    cumulative_cost: C,
228}
229
230impl<S, C> AsRef<S> for StateParentCumulativeCost<S, C> {
231    fn as_ref(&self) -> &S {
232        &self.state
233    }
234}
235
236impl<S, C> From<StateParentCumulativeCost<S, C>> for StateParent<S> {
237    fn from(
238        StateParentCumulativeCost { state, parent, .. }: StateParentCumulativeCost<S, C>,
239    ) -> Self {
240        StateParent { state, parent }
241    }
242}
243
244/// Internal.
245///
246/// Trait abstracting all exploration managers' common functionality.
247pub trait ExplorationManager {
248    type State;
249    type YieldResult;
250    type FringeItem: AsRef<Self::State>;
251    type CurrentStateContext;
252    type NextStatesIterItem;
253
254    fn initialize(initial_state: Self::State) -> Self;
255    fn pop_state(&mut self) -> Option<Self::FringeItem>;
256    fn prepare_result_from(&self, item: Self::FringeItem) -> Self::YieldResult;
257    fn valid_state(&mut self, item: &Self::FringeItem) -> bool;
258    fn place_state(&mut self, item: Self::FringeItem);
259    fn register_current_state(&mut self, item: &Self::FringeItem) -> Self::CurrentStateContext;
260    fn prepare_state(
261        &self,
262        context: &Self::CurrentStateContext,
263        state: Self::NextStatesIterItem,
264    ) -> Self::FringeItem;
265    fn next_states_iter(
266        current_state: &Self::State,
267    ) -> impl Iterator<Item = Self::NextStatesIterItem>;
268}
269
270/// State space exploration iterator.
271///
272/// Create an instance of this to explore a search space.
273pub struct Searcher<M> {
274    pub manager: M,
275}
276
277impl<M> Searcher<M> {
278    /// Create a new search iterator from an initial state.
279    pub fn new(initial_state: M::State) -> Self
280    where
281        M: ExplorationManager,
282    {
283        Self {
284            manager: M::initialize(initial_state),
285        }
286    }
287
288    /// Create a new search iterator from a default initial state.
289    pub fn new_with_default() -> Self
290    where
291        M: ExplorationManager,
292        M::State: Default,
293    {
294        Self::new(Default::default())
295    }
296}
297
298impl<M> Iterator for Searcher<M>
299where
300    M: ExplorationManager,
301    M::State: SolutionIdentifiable,
302{
303    type Item = M::YieldResult;
304
305    fn next(&mut self) -> Option<Self::Item> {
306        loop {
307            let current_state = self.manager.pop_state()?;
308
309            if current_state.as_ref().is_solution() {
310                return Some(self.manager.prepare_result_from(current_state));
311            }
312
313            let context = self.manager.register_current_state(&current_state);
314
315            for item in M::next_states_iter(current_state.as_ref()) {
316                let new_item = self.manager.prepare_state(&context, item);
317                if self.manager.valid_state(&new_item) {
318                    self.manager.place_state(new_item);
319                }
320            }
321        }
322    }
323}
324
325fn prepare_result_from_state_parent_map<S>(
326    parents: &[StateParent<S>],
327    StateParent {
328        mut state,
329        parent: mut maybe_parent_index,
330    }: StateParent<S>,
331) -> Vec<S>
332where
333    S: Clone,
334{
335    let mut result = VecDeque::new();
336    while let Some(parent_index) = maybe_parent_index {
337        result.push_front(state);
338        let StateParent {
339            state: new_state,
340            parent: new_parent_index,
341        } = parents
342            .get(parent_index)
343            .expect("Parent state will always exist if parent index exists")
344            .clone();
345        state = new_state;
346        maybe_parent_index = new_parent_index;
347    }
348    result.push_front(state);
349    result.into()
350}