Skip to main content

solverforge_solver/phase/exhaustive/
node.rs

1/* Exhaustive search node representation.
2
3Each node represents a partial solution state in the search tree.
4*/
5
6use std::marker::PhantomData;
7
8use crate::heuristic::Move;
9use solverforge_core::domain::PlanningSolution;
10
11/* A node in the exhaustive search tree.
12
13Each node represents a partial solution state, containing:
14- The depth in the search tree (number of variables assigned)
15- The score at this node
16- An optimistic bound (best possible score from this node)
17- The move sequence to reach this node from the root
18*/
19#[derive(Debug, Clone)]
20pub struct ExhaustiveSearchNode<S: PlanningSolution> {
21    // Depth in the search tree (0 = root, number of assignments made).
22    depth: usize,
23
24    // The score at this node after applying all moves.
25    score: S::Score,
26
27    // Optimistic bound: best possible score achievable from this node.
28    // Used for pruning branches that cannot improve on the best solution.
29    optimistic_bound: Option<S::Score>,
30
31    // Index of the entity being assigned at this node.
32    entity_index: Option<usize>,
33
34    // Index of the value assigned at this node.
35    value_index: Option<usize>,
36
37    // Parent node index in the node list (None for root).
38    parent_index: Option<usize>,
39
40    // Whether this node has been expanded.
41    expanded: bool,
42}
43
44impl<S: PlanningSolution> ExhaustiveSearchNode<S> {
45    pub fn root(score: S::Score) -> Self {
46        Self {
47            depth: 0,
48            score,
49            optimistic_bound: None,
50            entity_index: None,
51            value_index: None,
52            parent_index: None,
53            expanded: false,
54        }
55    }
56
57    pub fn child(
58        parent_index: usize,
59        depth: usize,
60        score: S::Score,
61        entity_index: usize,
62        value_index: usize,
63    ) -> Self {
64        Self {
65            depth,
66            score,
67            optimistic_bound: None,
68            entity_index: Some(entity_index),
69            value_index: Some(value_index),
70            parent_index: Some(parent_index),
71            expanded: false,
72        }
73    }
74
75    #[inline]
76    pub fn depth(&self) -> usize {
77        self.depth
78    }
79
80    #[inline]
81    pub fn score(&self) -> &S::Score {
82        &self.score
83    }
84
85    pub fn set_score(&mut self, score: S::Score) {
86        self.score = score;
87    }
88
89    #[inline]
90    pub fn optimistic_bound(&self) -> Option<&S::Score> {
91        self.optimistic_bound.as_ref()
92    }
93
94    pub fn set_optimistic_bound(&mut self, bound: S::Score) {
95        self.optimistic_bound = Some(bound);
96    }
97
98    #[inline]
99    pub fn entity_index(&self) -> Option<usize> {
100        self.entity_index
101    }
102
103    #[inline]
104    pub fn value_index(&self) -> Option<usize> {
105        self.value_index
106    }
107
108    #[inline]
109    pub fn parent_index(&self) -> Option<usize> {
110        self.parent_index
111    }
112
113    // Returns whether this node has been expanded.
114    #[inline]
115    pub fn is_expanded(&self) -> bool {
116        self.expanded
117    }
118
119    /// Marks this node as expanded.
120    pub fn mark_expanded(&mut self) {
121        self.expanded = true;
122    }
123
124    pub fn is_leaf(&self, total_entities: usize) -> bool {
125        self.depth >= total_entities
126    }
127
128    pub fn can_prune(&self, best_score: &S::Score) -> bool {
129        match &self.optimistic_bound {
130            Some(bound) => bound <= best_score,
131            None => false,
132        }
133    }
134}
135
136/* Tracks the move sequence to reconstruct a solution path.
137
138# Type Parameters
139* `S` - The planning solution type
140* `M` - The move type
141*/
142#[derive(Debug, Clone)]
143pub struct MoveSequence<S, M>
144where
145    S: PlanningSolution,
146    M: Move<S>,
147{
148    // The sequence of moves from root to current node.
149    moves: Vec<M>,
150    _phantom: PhantomData<fn() -> S>,
151}
152
153impl<S, M> MoveSequence<S, M>
154where
155    S: PlanningSolution,
156    M: Move<S>,
157{
158    pub fn new() -> Self {
159        Self {
160            moves: Vec::new(),
161            _phantom: PhantomData,
162        }
163    }
164
165    pub fn with_capacity(capacity: usize) -> Self {
166        Self {
167            moves: Vec::with_capacity(capacity),
168            _phantom: PhantomData,
169        }
170    }
171
172    pub fn push(&mut self, m: M) {
173        self.moves.push(m);
174    }
175
176    /// Removes and returns the last move.
177    pub fn pop(&mut self) -> Option<M> {
178        self.moves.pop()
179    }
180
181    pub fn len(&self) -> usize {
182        self.moves.len()
183    }
184
185    pub fn is_empty(&self) -> bool {
186        self.moves.is_empty()
187    }
188
189    /// Returns an iterator over the moves.
190    pub fn iter(&self) -> impl Iterator<Item = &M> {
191        self.moves.iter()
192    }
193
194    /// Clears all moves from the sequence.
195    pub fn clear(&mut self) {
196        self.moves.clear();
197    }
198}
199
200impl<S, M> Default for MoveSequence<S, M>
201where
202    S: PlanningSolution,
203    M: Move<S>,
204{
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::heuristic::r#move::ChangeMove;
214    use solverforge_core::score::SoftScore;
215
216    #[derive(Clone, Debug)]
217    struct TestSolution {
218        values: Vec<Option<i32>>,
219        score: Option<SoftScore>,
220    }
221
222    impl PlanningSolution for TestSolution {
223        type Score = SoftScore;
224
225        fn score(&self) -> Option<Self::Score> {
226            self.score
227        }
228
229        fn set_score(&mut self, score: Option<Self::Score>) {
230            self.score = score;
231        }
232    }
233
234    // Typed getter - zero erasure
235    fn get_value(s: &TestSolution, idx: usize) -> Option<i32> {
236        s.values.get(idx).copied().flatten()
237    }
238
239    // Typed setter - zero erasure
240    fn set_value(s: &mut TestSolution, idx: usize, v: Option<i32>) {
241        if let Some(val) = s.values.get_mut(idx) {
242            *val = v;
243        }
244    }
245
246    #[test]
247    fn test_root_node() {
248        let node: ExhaustiveSearchNode<TestSolution> = ExhaustiveSearchNode::root(SoftScore::of(0));
249
250        assert_eq!(node.depth(), 0);
251        assert_eq!(node.score(), &SoftScore::of(0));
252        assert!(node.parent_index().is_none());
253        assert!(node.entity_index().is_none());
254        assert!(!node.is_expanded());
255    }
256
257    #[test]
258    fn test_child_node() {
259        let node: ExhaustiveSearchNode<TestSolution> =
260            ExhaustiveSearchNode::child(0, 1, SoftScore::of(-1), 0, 2);
261
262        assert_eq!(node.depth(), 1);
263        assert_eq!(node.score(), &SoftScore::of(-1));
264        assert_eq!(node.parent_index(), Some(0));
265        assert_eq!(node.entity_index(), Some(0));
266        assert_eq!(node.value_index(), Some(2));
267    }
268
269    #[test]
270    fn test_is_leaf() {
271        let node: ExhaustiveSearchNode<TestSolution> =
272            ExhaustiveSearchNode::child(0, 4, SoftScore::of(0), 3, 1);
273
274        assert!(node.is_leaf(4));
275        assert!(!node.is_leaf(5));
276    }
277
278    #[test]
279    fn test_optimistic_bound_pruning() {
280        let mut node: ExhaustiveSearchNode<TestSolution> =
281            ExhaustiveSearchNode::root(SoftScore::of(-5));
282
283        // No bound set - cannot prune
284        assert!(!node.can_prune(&SoftScore::of(0)));
285
286        // Set bound worse than best - can prune
287        node.set_optimistic_bound(SoftScore::of(-2));
288        assert!(node.can_prune(&SoftScore::of(0)));
289
290        // Set bound better than best - cannot prune
291        node.set_optimistic_bound(SoftScore::of(5));
292        assert!(!node.can_prune(&SoftScore::of(0)));
293    }
294
295    type TestMove = ChangeMove<TestSolution, i32>;
296
297    #[test]
298    fn test_move_sequence() {
299        let mut seq: MoveSequence<TestSolution, TestMove> = MoveSequence::new();
300
301        assert!(seq.is_empty());
302        assert_eq!(seq.len(), 0);
303
304        seq.push(ChangeMove::new(
305            0,
306            Some(42),
307            get_value,
308            set_value,
309            "test",
310            0,
311        ));
312        assert_eq!(seq.len(), 1);
313
314        let m = seq.pop();
315        assert!(m.is_some());
316        assert!(seq.is_empty());
317    }
318}