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