solverforge_solver/heuristic/move/
runtime_compound.rs1use std::fmt::{self, Debug};
8
9#[cfg(test)]
10use std::cell::Cell;
11
12use smallvec::{smallvec, SmallVec};
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::Director;
15
16use crate::builder::context::ProviderReasonId;
17use crate::builder::RuntimeScalarEdit;
18use crate::stats::{CandidateTraceCoordinate, CandidateTraceIdentity};
19
20use super::metadata::{encode_option_debug, encode_usize, hash_str, MoveTabuScope};
21use super::{Move, MoveAffectedEntity, MoveTabuSignature};
22
23#[cfg(test)]
24thread_local! {
25 static CLONE_COUNT: Cell<usize> = const { Cell::new(0) };
26}
27
28#[cfg(test)]
29pub(crate) fn reset_runtime_compound_move_clone_count() {
30 CLONE_COUNT.with(|count| count.set(0));
31}
32
33#[cfg(test)]
34pub(crate) fn runtime_compound_move_clone_count() -> usize {
35 CLONE_COUNT.with(Cell::get)
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42#[repr(u8)]
43pub enum RuntimeCompoundMoveKind {
44 Grouped = 1,
45 ConflictRepair = 2,
46 CompoundConflictRepair = 3,
47}
48
49impl RuntimeCompoundMoveKind {
50 pub const fn telemetry_label(self) -> &'static str {
51 match self {
52 Self::Grouped => "runtime_scalar_grouped",
53 Self::ConflictRepair => "runtime_scalar_conflict_repair",
54 Self::CompoundConflictRepair => "runtime_scalar_compound_conflict_repair",
55 }
56 }
57
58 const fn trace_operation(self) -> &'static str {
59 match self {
60 Self::Grouped => "runtime_provider_grouped",
61 Self::ConflictRepair => "runtime_provider_conflict_repair",
62 Self::CompoundConflictRepair => "runtime_provider_compound_conflict_repair",
63 }
64 }
65}
66
67pub struct RuntimeCompoundMove<S> {
71 kind: RuntimeCompoundMoveKind,
72 reason: ProviderReasonId,
73 edits: Vec<RuntimeScalarEdit<S>>,
74 entity_indices: Vec<usize>,
75 descriptor_index: usize,
76 require_hard_improvement: bool,
77}
78
79impl<S> RuntimeCompoundMove<S> {
80 pub(crate) fn new(
81 kind: RuntimeCompoundMoveKind,
82 reason: ProviderReasonId,
83 edits: Vec<RuntimeScalarEdit<S>>,
84 require_hard_improvement: bool,
85 ) -> Self {
86 let descriptor_index = edits
87 .first()
88 .map(RuntimeScalarEdit::descriptor_index)
89 .unwrap_or(usize::MAX);
90 let mut entity_indices = edits
91 .iter()
92 .map(|edit| edit.entity_index)
93 .collect::<Vec<_>>();
94 entity_indices.sort_unstable();
95 entity_indices.dedup();
96 Self {
97 kind,
98 reason,
99 edits,
100 entity_indices,
101 descriptor_index,
102 require_hard_improvement,
103 }
104 }
105
106 pub(crate) fn is_doable_on(&self, solution: &S) -> bool {
107 if self.edits.is_empty() || has_duplicate_targets(&self.edits) {
108 return false;
109 }
110 let mut changes_value = false;
111 for edit in &self.edits {
112 if edit.entity_index >= edit.slot.entity_count(solution)
113 || !edit
114 .slot
115 .value_is_legal(solution, edit.entity_index, edit.to_value)
116 {
117 return false;
118 }
119 changes_value |= edit.slot.current_value(solution, edit.entity_index) != edit.to_value;
120 }
121 changes_value
122 }
123
124 #[cfg(test)]
125 pub(crate) fn reason_id(&self) -> ProviderReasonId {
126 self.reason
127 }
128}
129
130impl<S> Debug for RuntimeCompoundMove<S> {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.debug_struct("RuntimeCompoundMove")
133 .field("kind", &self.kind)
134 .field("reason_id", &self.reason)
135 .field("edits", &self.edits)
136 .field("require_hard_improvement", &self.require_hard_improvement)
137 .finish()
138 }
139}
140
141impl<S> Move<S> for RuntimeCompoundMove<S>
142where
143 S: PlanningSolution,
144{
145 type Undo = SmallVec<[Option<usize>; 8]>;
146
147 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
148 self.is_doable_on(score_director.working_solution())
149 }
150
151 fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
152 let mut undo = SmallVec::<[Option<usize>; 8]>::with_capacity(self.edits.len());
153 let affected = unique_affected_entities(&self.edits);
154 for edit in &self.edits {
155 undo.push(
156 edit.slot
157 .current_value(score_director.working_solution(), edit.entity_index),
158 );
159 }
160 for (descriptor_index, entity_index) in &affected {
161 score_director.before_variable_changed(*descriptor_index, *entity_index);
162 }
163 for edit in &self.edits {
164 edit.slot.set_value(
165 score_director.working_solution_mut(),
166 edit.entity_index,
167 edit.to_value,
168 );
169 }
170 for (descriptor_index, entity_index) in affected.iter().rev() {
171 score_director.after_variable_changed(*descriptor_index, *entity_index);
172 }
173 undo
174 }
175
176 fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
177 let affected = unique_affected_entities(&self.edits);
178 for (descriptor_index, entity_index) in &affected {
179 score_director.before_variable_changed(*descriptor_index, *entity_index);
180 }
181 for (edit, old_value) in self.edits.iter().zip(undo) {
182 edit.slot.set_value(
183 score_director.working_solution_mut(),
184 edit.entity_index,
185 old_value,
186 );
187 }
188 for (descriptor_index, entity_index) in affected.iter().rev() {
189 score_director.after_variable_changed(*descriptor_index, *entity_index);
190 }
191 }
192
193 fn descriptor_index(&self) -> usize {
194 self.descriptor_index
195 }
196
197 fn entity_indices(&self) -> &[usize] {
198 &self.entity_indices
199 }
200
201 fn variable_name(&self) -> &str {
202 self.kind.telemetry_label()
203 }
204
205 fn telemetry_label(&self) -> &'static str {
206 self.kind.telemetry_label()
207 }
208
209 fn requires_hard_improvement(&self) -> bool {
210 self.require_hard_improvement
211 }
212
213 fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
214 let first = self
215 .edits
216 .first()
217 .expect("runtime compound move tabu signature requires an edit");
218 let scope = MoveTabuScope::new(first.descriptor_index(), first.variable_name());
219 let mut move_id = smallvec![0xDCA7_0000_0000_1000 ^ self.kind as u8 as u64];
220 let mut undo_move_id = move_id.clone();
221 let mut entity_tokens: SmallVec<[_; 8]> = SmallVec::new();
222 let mut destination_tokens: SmallVec<[_; 8]> = SmallVec::new();
223
224 for edit in &self.edits {
225 let current = edit
226 .slot
227 .current_value(score_director.working_solution(), edit.entity_index);
228 let descriptor = encode_usize(edit.descriptor_index());
229 let variable = hash_str(edit.variable_name());
230 let entity = encode_usize(edit.entity_index);
231 let from = encode_option_debug(current.as_ref());
232 let to = encode_option_debug(edit.to_value.as_ref());
233 let edit_scope = MoveTabuScope::new(edit.descriptor_index(), edit.variable_name());
234
235 move_id.extend([descriptor, variable, entity, from, to]);
236 undo_move_id.extend([descriptor, variable, entity, to, from]);
237 let entity_token = edit_scope.entity_token(entity);
238 if !entity_tokens.contains(&entity_token) {
239 entity_tokens.push(entity_token);
240 }
241 let destination_token = edit_scope.value_token(to);
242 if !destination_tokens.contains(&destination_token) {
243 destination_tokens.push(destination_token);
244 }
245 }
246
247 MoveTabuSignature::new(scope, move_id, undo_move_id)
248 .with_entity_tokens(entity_tokens)
249 .with_destination_value_tokens(destination_tokens)
250 }
251
252 fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
253 Some(CandidateTraceIdentity::composite(
254 self.kind.trace_operation(),
255 self.edits.iter().map(|edit| {
256 CandidateTraceIdentity::logical_move(
257 edit.descriptor_index(),
258 edit.variable_name(),
259 "scalar_change",
260 vec![
261 CandidateTraceCoordinate::from(edit.entity_index),
262 CandidateTraceCoordinate::from(edit.to_value),
263 ],
264 )
265 }),
266 ))
267 }
268
269 fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
270 for edit in &self.edits {
271 visitor(MoveAffectedEntity {
272 descriptor_index: edit.descriptor_index(),
273 entity_index: edit.entity_index,
274 variable_name: edit.variable_name(),
275 });
276 }
277 }
278}
279
280fn has_duplicate_targets<S>(edits: &[RuntimeScalarEdit<S>]) -> bool {
281 let mut targets = SmallVec::<[(usize, usize, usize); 8]>::with_capacity(edits.len());
282 edits.iter().any(|edit| {
283 let target = (
284 edit.descriptor_index(),
285 edit.variable_index(),
286 edit.entity_index,
287 );
288 if targets.contains(&target) {
289 true
290 } else {
291 targets.push(target);
292 false
293 }
294 })
295}
296
297fn unique_affected_entities<S>(edits: &[RuntimeScalarEdit<S>]) -> SmallVec<[(usize, usize); 8]> {
298 let mut affected = SmallVec::new();
299 for edit in edits {
300 let entity = (edit.descriptor_index(), edit.entity_index);
301 if !affected.contains(&entity) {
302 affected.push(entity);
303 }
304 }
305 affected
306}