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(¤t_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}