solverforge_solver/heuristic/move/
ruin.rs

1//! RuinMove - unassigns a subset of entities for Large Neighborhood Search.
2//!
3//! This move "ruins" (unassigns) selected entities, allowing a construction
4//! heuristic to reassign them. This is the fundamental building block for
5//! Large Neighborhood Search (LNS) algorithms.
6//!
7//! # Zero-Erasure Design
8//!
9//! Uses typed function pointers for variable access. No `dyn Any`, no downcasting.
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use smallvec::SmallVec;
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::ScoreDirector;
17
18use super::Move;
19
20/// A move that unassigns multiple entities for Large Neighborhood Search.
21///
22/// This move sets the planning variable to `None` for a set of entities,
23/// creating "gaps" that a construction heuristic can fill. Combined with
24/// construction, this enables exploring distant regions of the search space.
25///
26/// # Type Parameters
27/// * `S` - The planning solution type
28/// * `V` - The variable value type
29///
30/// # Example
31///
32/// ```
33/// use solverforge_solver::heuristic::r#move::RuinMove;
34/// use solverforge_core::domain::PlanningSolution;
35/// use solverforge_core::score::SimpleScore;
36///
37/// #[derive(Clone, Debug)]
38/// struct Task { assigned_to: Option<i32>, score: Option<SimpleScore> }
39/// #[derive(Clone, Debug)]
40/// struct Schedule { tasks: Vec<Task>, score: Option<SimpleScore> }
41///
42/// impl PlanningSolution for Schedule {
43///     type Score = SimpleScore;
44///     fn score(&self) -> Option<Self::Score> { self.score }
45///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
46/// }
47///
48/// fn get_task(s: &Schedule, idx: usize) -> Option<i32> {
49///     s.tasks.get(idx).and_then(|t| t.assigned_to)
50/// }
51/// fn set_task(s: &mut Schedule, idx: usize, v: Option<i32>) {
52///     if let Some(t) = s.tasks.get_mut(idx) { t.assigned_to = v; }
53/// }
54///
55/// // Ruin entities 0, 2, and 4
56/// let m = RuinMove::<Schedule, i32>::new(
57///     &[0, 2, 4],
58///     get_task, set_task,
59///     "assigned_to", 0,
60/// );
61/// ```
62#[derive(Clone)]
63pub struct RuinMove<S, V> {
64    /// Indices of entities to unassign
65    entity_indices: SmallVec<[usize; 8]>,
66    /// Get current value for an entity
67    getter: fn(&S, usize) -> Option<V>,
68    /// Set value for an entity
69    setter: fn(&mut S, usize, Option<V>),
70    variable_name: &'static str,
71    descriptor_index: usize,
72    _phantom: PhantomData<V>,
73}
74
75impl<S, V: Debug> Debug for RuinMove<S, V> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("RuinMove")
78            .field("entities", &self.entity_indices.as_slice())
79            .field("variable_name", &self.variable_name)
80            .finish()
81    }
82}
83
84impl<S, V> RuinMove<S, V> {
85    /// Creates a new ruin move with typed function pointers.
86    ///
87    /// # Arguments
88    /// * `entity_indices` - Indices of entities to unassign
89    /// * `getter` - Function to get current value
90    /// * `setter` - Function to set value
91    /// * `variable_name` - Name of the planning variable
92    /// * `descriptor_index` - Entity descriptor index
93    pub fn new(
94        entity_indices: &[usize],
95        getter: fn(&S, usize) -> Option<V>,
96        setter: fn(&mut S, usize, Option<V>),
97        variable_name: &'static str,
98        descriptor_index: usize,
99    ) -> Self {
100        Self {
101            entity_indices: SmallVec::from_slice(entity_indices),
102            getter,
103            setter,
104            variable_name,
105            descriptor_index,
106            _phantom: PhantomData,
107        }
108    }
109
110    /// Returns the entity indices being ruined.
111    pub fn entity_indices_slice(&self) -> &[usize] {
112        &self.entity_indices
113    }
114
115    /// Returns the number of entities being ruined.
116    pub fn ruin_count(&self) -> usize {
117        self.entity_indices.len()
118    }
119}
120
121impl<S, V> Move<S> for RuinMove<S, V>
122where
123    S: PlanningSolution,
124    V: Clone + Send + Sync + Debug + 'static,
125{
126    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
127        // At least one entity must be currently assigned
128        let solution = score_director.working_solution();
129        self.entity_indices
130            .iter()
131            .any(|&idx| (self.getter)(solution, idx).is_some())
132    }
133
134    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
135        let getter = self.getter;
136        let setter = self.setter;
137        let descriptor = self.descriptor_index;
138        let variable_name = self.variable_name;
139
140        // Collect old values for undo
141        let old_values: SmallVec<[(usize, Option<V>); 8]> = self
142            .entity_indices
143            .iter()
144            .map(|&idx| {
145                let old = getter(score_director.working_solution(), idx);
146                (idx, old)
147            })
148            .collect();
149
150        // Unassign all entities
151        for &idx in &self.entity_indices {
152            score_director.before_variable_changed(descriptor, idx, variable_name);
153            setter(score_director.working_solution_mut(), idx, None);
154            score_director.after_variable_changed(descriptor, idx, variable_name);
155        }
156
157        // Register undo to restore old values
158        score_director.register_undo(Box::new(move |s: &mut S| {
159            for (idx, old_value) in old_values {
160                setter(s, idx, old_value);
161            }
162        }));
163    }
164
165    fn descriptor_index(&self) -> usize {
166        self.descriptor_index
167    }
168
169    fn entity_indices(&self) -> &[usize] {
170        &self.entity_indices
171    }
172
173    fn variable_name(&self) -> &str {
174        self.variable_name
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
182    use solverforge_core::score::SimpleScore;
183    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
184    use std::any::TypeId;
185
186    #[derive(Clone, Debug)]
187    struct Task {
188        assigned_to: Option<i32>,
189    }
190
191    #[derive(Clone, Debug)]
192    struct Schedule {
193        tasks: Vec<Task>,
194        score: Option<SimpleScore>,
195    }
196
197    impl PlanningSolution for Schedule {
198        type Score = SimpleScore;
199        fn score(&self) -> Option<Self::Score> {
200            self.score
201        }
202        fn set_score(&mut self, score: Option<Self::Score>) {
203            self.score = score;
204        }
205    }
206
207    fn get_tasks(s: &Schedule) -> &Vec<Task> {
208        &s.tasks
209    }
210    fn get_tasks_mut(s: &mut Schedule) -> &mut Vec<Task> {
211        &mut s.tasks
212    }
213
214    fn get_assigned(s: &Schedule, idx: usize) -> Option<i32> {
215        s.tasks.get(idx).and_then(|t| t.assigned_to)
216    }
217    fn set_assigned(s: &mut Schedule, idx: usize, v: Option<i32>) {
218        if let Some(t) = s.tasks.get_mut(idx) {
219            t.assigned_to = v;
220        }
221    }
222
223    fn create_director(
224        assignments: &[Option<i32>],
225    ) -> SimpleScoreDirector<Schedule, impl Fn(&Schedule) -> SimpleScore> {
226        let tasks: Vec<Task> = assignments
227            .iter()
228            .map(|&a| Task { assigned_to: a })
229            .collect();
230        let solution = Schedule { tasks, score: None };
231        let extractor = Box::new(TypedEntityExtractor::new(
232            "Task",
233            "tasks",
234            get_tasks,
235            get_tasks_mut,
236        ));
237        let entity_desc =
238            EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks").with_extractor(extractor);
239        let descriptor =
240            SolutionDescriptor::new("Schedule", TypeId::of::<Schedule>()).with_entity(entity_desc);
241        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
242    }
243
244    #[test]
245    fn ruin_single_entity() {
246        let mut director = create_director(&[Some(1), Some(2), Some(3)]);
247
248        let m = RuinMove::<Schedule, i32>::new(&[1], get_assigned, set_assigned, "assigned_to", 0);
249
250        assert!(m.is_doable(&director));
251
252        {
253            let mut recording = RecordingScoreDirector::new(&mut director);
254            m.do_move(&mut recording);
255
256            assert_eq!(get_assigned(recording.working_solution(), 0), Some(1));
257            assert_eq!(get_assigned(recording.working_solution(), 1), None);
258            assert_eq!(get_assigned(recording.working_solution(), 2), Some(3));
259
260            recording.undo_changes();
261        }
262
263        // Restored
264        assert_eq!(get_assigned(director.working_solution(), 1), Some(2));
265    }
266
267    #[test]
268    fn ruin_multiple_entities() {
269        let mut director = create_director(&[Some(1), Some(2), Some(3), Some(4)]);
270
271        let m = RuinMove::<Schedule, i32>::new(
272            &[0, 2, 3],
273            get_assigned,
274            set_assigned,
275            "assigned_to",
276            0,
277        );
278
279        assert!(m.is_doable(&director));
280        assert_eq!(m.ruin_count(), 3);
281
282        {
283            let mut recording = RecordingScoreDirector::new(&mut director);
284            m.do_move(&mut recording);
285
286            assert_eq!(get_assigned(recording.working_solution(), 0), None);
287            assert_eq!(get_assigned(recording.working_solution(), 1), Some(2));
288            assert_eq!(get_assigned(recording.working_solution(), 2), None);
289            assert_eq!(get_assigned(recording.working_solution(), 3), None);
290
291            recording.undo_changes();
292        }
293
294        // All restored
295        assert_eq!(get_assigned(director.working_solution(), 0), Some(1));
296        assert_eq!(get_assigned(director.working_solution(), 2), Some(3));
297        assert_eq!(get_assigned(director.working_solution(), 3), Some(4));
298    }
299
300    #[test]
301    fn ruin_already_unassigned_is_doable() {
302        // One assigned, one unassigned
303        let director = create_director(&[Some(1), None]);
304
305        // Ruin both - still doable because entity 0 is assigned
306        let m =
307            RuinMove::<Schedule, i32>::new(&[0, 1], get_assigned, set_assigned, "assigned_to", 0);
308
309        assert!(m.is_doable(&director));
310    }
311
312    #[test]
313    fn ruin_all_unassigned_not_doable() {
314        let director = create_director(&[None, None]);
315
316        let m =
317            RuinMove::<Schedule, i32>::new(&[0, 1], get_assigned, set_assigned, "assigned_to", 0);
318
319        assert!(!m.is_doable(&director));
320    }
321}