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