Skip to main content

solverforge_solver/phase/exhaustive/
mod.rs

1//! Exhaustive search phase using branch-and-bound.
2//!
3//! Exhaustive search explores the entire solution space systematically,
4//! using pruning to avoid exploring branches that cannot improve on the
5//! best solution found so far.
6//!
7//! # Exploration Types
8//!
9//! - **Depth First**: Explores deepest nodes first (memory efficient)
10//! - **Breadth First**: Explores level by level (finds shortest paths)
11//! - **Score First**: Explores best-scoring nodes first (greedy)
12//! - **Optimistic Bound First**: Explores most promising bounds first (A*)
13
14mod bounder;
15mod decider;
16mod node;
17
18use std::collections::BinaryHeap;
19use std::fmt::Debug;
20
21use solverforge_core::domain::PlanningSolution;
22use solverforge_scoring::ScoreDirector;
23
24use crate::phase::Phase;
25use crate::scope::BestSolutionCallback;
26use crate::scope::{PhaseScope, SolverScope};
27
28pub use bounder::{BounderType, FixedOffsetBounder, ScoreBounder, SimpleScoreBounder};
29pub use decider::{ExhaustiveSearchDecider, SimpleDecider};
30pub use node::{ExhaustiveSearchNode, MoveSequence};
31
32/// Type of exploration strategy for exhaustive search.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum ExplorationType {
35    /// Depth-first search: explores deepest nodes first.
36    /// Most memory efficient, but may take longer to find optimal.
37    #[default]
38    DepthFirst,
39
40    /// Breadth-first search: explores level by level.
41    /// Memory intensive, but finds shortest solution paths.
42    BreadthFirst,
43
44    /// Score-first search: explores best-scoring nodes first.
45    /// Greedy approach that may find good solutions quickly.
46    ScoreFirst,
47
48    /// Optimistic bound first: explores most promising nodes first.
49    /// A*-like behavior, requires a good bounder.
50    OptimisticBoundFirst,
51}
52
53impl std::fmt::Display for ExplorationType {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            ExplorationType::DepthFirst => write!(f, "DepthFirst"),
57            ExplorationType::BreadthFirst => write!(f, "BreadthFirst"),
58            ExplorationType::ScoreFirst => write!(f, "ScoreFirst"),
59            ExplorationType::OptimisticBoundFirst => write!(f, "OptimisticBoundFirst"),
60        }
61    }
62}
63
64/// Configuration for exhaustive search phase.
65#[derive(Debug, Clone)]
66pub struct ExhaustiveSearchConfig {
67    /// The exploration type to use.
68    pub exploration_type: ExplorationType,
69    /// Maximum number of nodes to explore (None = unlimited).
70    pub node_limit: Option<u64>,
71    /// Maximum depth to explore (None = unlimited).
72    pub depth_limit: Option<usize>,
73    /// Whether to enable pruning based on bounds.
74    pub enable_pruning: bool,
75}
76
77impl Default for ExhaustiveSearchConfig {
78    fn default() -> Self {
79        Self {
80            exploration_type: ExplorationType::DepthFirst,
81            node_limit: Some(10_000),
82            depth_limit: None,
83            enable_pruning: true,
84        }
85    }
86}
87
88/// A node wrapper for priority queue ordering.
89struct PriorityNode<S: PlanningSolution> {
90    index: usize,
91    node: ExhaustiveSearchNode<S>,
92    exploration_type: ExplorationType,
93}
94
95impl<S: PlanningSolution> PriorityNode<S> {
96    fn new(index: usize, node: ExhaustiveSearchNode<S>, exploration_type: ExplorationType) -> Self {
97        Self {
98            index,
99            node,
100            exploration_type,
101        }
102    }
103}
104
105impl<S: PlanningSolution> Eq for PriorityNode<S> {}
106
107impl<S: PlanningSolution> PartialEq for PriorityNode<S> {
108    fn eq(&self, other: &Self) -> bool {
109        self.index == other.index
110    }
111}
112
113impl<S: PlanningSolution> Ord for PriorityNode<S> {
114    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115        match self.exploration_type {
116            ExplorationType::DepthFirst => {
117                // Higher depth = higher priority
118                self.node.depth().cmp(&other.node.depth())
119            }
120            ExplorationType::BreadthFirst => {
121                // Lower depth = higher priority (reversed)
122                other.node.depth().cmp(&self.node.depth())
123            }
124            ExplorationType::ScoreFirst => {
125                // Better score = higher priority
126                self.node
127                    .score()
128                    .partial_cmp(other.node.score())
129                    .unwrap_or(std::cmp::Ordering::Equal)
130            }
131            ExplorationType::OptimisticBoundFirst => {
132                // Better bound = higher priority
133                match (self.node.optimistic_bound(), other.node.optimistic_bound()) {
134                    (Some(a), Some(b)) => a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal),
135                    (Some(_), None) => std::cmp::Ordering::Greater,
136                    (None, Some(_)) => std::cmp::Ordering::Less,
137                    (None, None) => std::cmp::Ordering::Equal,
138                }
139            }
140        }
141    }
142}
143
144impl<S: PlanningSolution> PartialOrd for PriorityNode<S> {
145    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
146        Some(self.cmp(other))
147    }
148}
149
150/// Exhaustive search phase that explores all possible solutions.
151///
152/// This phase systematically explores the solution space using a
153/// branch-and-bound algorithm. It maintains a tree of partial solutions
154/// and uses pruning to avoid exploring branches that cannot improve
155/// on the best solution found.
156///
157/// # Type Parameters
158/// * `Dec` - The decider type that generates child nodes
159///
160/// # Example
161///
162/// ```
163/// use solverforge_solver::phase::exhaustive::{ExhaustiveSearchPhase, SimpleDecider, ExhaustiveSearchConfig};
164/// use solverforge_core::domain::PlanningSolution;
165/// use solverforge_core::score::SimpleScore;
166///
167/// #[derive(Clone, Debug)]
168/// struct MySolution {
169///     values: Vec<Option<i32>>,
170///     score: Option<SimpleScore>,
171/// }
172///
173/// impl PlanningSolution for MySolution {
174///     type Score = SimpleScore;
175///     fn score(&self) -> Option<Self::Score> { self.score }
176///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
177/// }
178///
179/// fn set_value(s: &mut MySolution, idx: usize, v: Option<i32>) {
180///     if let Some(slot) = s.values.get_mut(idx) { *slot = v; }
181/// }
182///
183/// let decider = SimpleDecider::<MySolution, i32>::new(0, "value", vec![1, 2, 3], set_value);
184/// let phase = ExhaustiveSearchPhase::depth_first(decider);
185/// ```
186pub struct ExhaustiveSearchPhase<Dec> {
187    /// The decider that generates child nodes.
188    decider: Dec,
189    /// Configuration for this phase.
190    config: ExhaustiveSearchConfig,
191}
192
193impl<Dec: Debug> Debug for ExhaustiveSearchPhase<Dec> {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.debug_struct("ExhaustiveSearchPhase")
196            .field("decider", &self.decider)
197            .field("config", &self.config)
198            .finish()
199    }
200}
201
202impl<Dec> ExhaustiveSearchPhase<Dec> {
203    /// Creates a new exhaustive search phase.
204    pub fn new(decider: Dec, config: ExhaustiveSearchConfig) -> Self {
205        Self { decider, config }
206    }
207
208    /// Creates a depth-first exhaustive search phase.
209    pub fn depth_first(decider: Dec) -> Self {
210        Self::new(
211            decider,
212            ExhaustiveSearchConfig {
213                exploration_type: ExplorationType::DepthFirst,
214                ..Default::default()
215            },
216        )
217    }
218
219    /// Creates a breadth-first exhaustive search phase.
220    pub fn breadth_first(decider: Dec) -> Self {
221        Self::new(
222            decider,
223            ExhaustiveSearchConfig {
224                exploration_type: ExplorationType::BreadthFirst,
225                ..Default::default()
226            },
227        )
228    }
229
230    /// Creates a score-first exhaustive search phase.
231    pub fn score_first(decider: Dec) -> Self {
232        Self::new(
233            decider,
234            ExhaustiveSearchConfig {
235                exploration_type: ExplorationType::ScoreFirst,
236                ..Default::default()
237            },
238        )
239    }
240
241    /// Returns the phase type name.
242    pub fn phase_type_name(&self) -> &'static str {
243        "ExhaustiveSearch"
244    }
245}
246
247impl<S, D, BestCb, Dec> Phase<S, D, BestCb> for ExhaustiveSearchPhase<Dec>
248where
249    S: PlanningSolution,
250    D: ScoreDirector<S>,
251    BestCb: BestSolutionCallback<S>,
252    Dec: ExhaustiveSearchDecider<S, D>,
253{
254    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
255        let mut phase_scope = PhaseScope::new(solver_scope, 0);
256
257        // Get total entities
258        let total_entities = self.decider.total_entities(phase_scope.score_director());
259        if total_entities == 0 {
260            return;
261        }
262
263        // Calculate initial score
264        let initial_score = phase_scope.calculate_score();
265
266        // Create root node
267        let root = ExhaustiveSearchNode::root(initial_score);
268
269        // Initialize best score
270        let mut best_score: Option<S::Score> = None;
271        let mut nodes_explored: u64 = 0;
272
273        // Priority queue for node exploration
274        let mut frontier: BinaryHeap<PriorityNode<S>> = BinaryHeap::new();
275        frontier.push(PriorityNode::new(0, root, self.config.exploration_type));
276
277        // Node storage
278        let mut all_nodes: Vec<ExhaustiveSearchNode<S>> = Vec::new();
279
280        while let Some(priority_node) = frontier.pop() {
281            let node = priority_node.node;
282            let node_index = all_nodes.len();
283
284            // Check node limit
285            if let Some(limit) = self.config.node_limit {
286                if nodes_explored >= limit {
287                    break;
288                }
289            }
290
291            // Check depth limit
292            if let Some(limit) = self.config.depth_limit {
293                if node.depth() >= limit {
294                    continue;
295                }
296            }
297
298            // Check pruning
299            if self.config.enable_pruning {
300                if let Some(ref best) = best_score {
301                    if node.can_prune(best) {
302                        continue;
303                    }
304                }
305            }
306
307            nodes_explored += 1;
308
309            // Check if this is a complete solution (leaf node)
310            if node.is_leaf(total_entities) {
311                let is_better = match &best_score {
312                    None => true,
313                    Some(best) => node.score() > best,
314                };
315
316                if is_better {
317                    best_score = Some(*node.score());
318                    phase_scope.update_best_solution();
319                }
320
321                all_nodes.push(node);
322                continue;
323            }
324
325            // Expand node to generate children
326            let children = self
327                .decider
328                .expand(node_index, &node, phase_scope.score_director_mut());
329
330            // Store current node
331            all_nodes.push(node);
332
333            // Add children to frontier
334            for child in children {
335                // Skip if prunable
336                if self.config.enable_pruning {
337                    if let Some(ref best) = best_score {
338                        if child.can_prune(best) {
339                            continue;
340                        }
341                    }
342                }
343
344                let child_index = all_nodes.len() + frontier.len();
345                frontier.push(PriorityNode::new(
346                    child_index,
347                    child,
348                    self.config.exploration_type,
349                ));
350            }
351        }
352    }
353
354    fn phase_type_name(&self) -> &'static str {
355        "ExhaustiveSearch"
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use solverforge_core::score::SimpleScore;
363
364    #[derive(Clone, Debug)]
365    struct TestSolution {
366        score: Option<SimpleScore>,
367    }
368
369    impl PlanningSolution for TestSolution {
370        type Score = SimpleScore;
371
372        fn score(&self) -> Option<Self::Score> {
373            self.score
374        }
375
376        fn set_score(&mut self, score: Option<Self::Score>) {
377            self.score = score;
378        }
379    }
380
381    // Dummy setter for tests
382    fn set_row(_s: &mut TestSolution, _idx: usize, _v: Option<i32>) {}
383
384    #[test]
385    fn test_exploration_type_display() {
386        assert_eq!(format!("{}", ExplorationType::DepthFirst), "DepthFirst");
387        assert_eq!(format!("{}", ExplorationType::BreadthFirst), "BreadthFirst");
388        assert_eq!(format!("{}", ExplorationType::ScoreFirst), "ScoreFirst");
389        assert_eq!(
390            format!("{}", ExplorationType::OptimisticBoundFirst),
391            "OptimisticBoundFirst"
392        );
393    }
394
395    #[test]
396    fn test_exploration_type_default() {
397        assert_eq!(ExplorationType::default(), ExplorationType::DepthFirst);
398    }
399
400    #[test]
401    fn test_config_default() {
402        let config = ExhaustiveSearchConfig::default();
403        assert_eq!(config.exploration_type, ExplorationType::DepthFirst);
404        assert_eq!(config.node_limit, Some(10_000));
405        assert!(config.depth_limit.is_none());
406        assert!(config.enable_pruning);
407    }
408
409    #[test]
410    fn test_phase_type_name() {
411        let decider: SimpleDecider<TestSolution, i32> =
412            SimpleDecider::new(0, "row", vec![0, 1, 2, 3], set_row);
413        let phase = ExhaustiveSearchPhase::depth_first(decider);
414
415        assert_eq!(phase.phase_type_name(), "ExhaustiveSearch");
416    }
417
418    #[test]
419    fn test_phase_debug() {
420        let decider: SimpleDecider<TestSolution, i32> =
421            SimpleDecider::new(0, "row", vec![0, 1, 2, 3], set_row);
422        let phase = ExhaustiveSearchPhase::depth_first(decider);
423
424        let debug = format!("{:?}", phase);
425        assert!(debug.contains("ExhaustiveSearchPhase"));
426        assert!(debug.contains("DepthFirst"));
427    }
428}