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