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}