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> Clone for RuntimeCompoundMove<S> {
80 fn clone(&self) -> Self {
81 #[cfg(test)]
82 CLONE_COUNT.with(|count| count.set(count.get().saturating_add(1)));
83 Self {
84 kind: self.kind,
85 reason: self.reason,
86 edits: self.edits.clone(),
87 entity_indices: self.entity_indices.clone(),
88 descriptor_index: self.descriptor_index,
89 require_hard_improvement: self.require_hard_improvement,
90 }
91 }
92}
93
94impl<S> RuntimeCompoundMove<S> {
95 pub(crate) fn new(
96 kind: RuntimeCompoundMoveKind,
97 reason: ProviderReasonId,
98 edits: Vec<RuntimeScalarEdit<S>>,
99 require_hard_improvement: bool,
100 ) -> Self {
101 let descriptor_index = edits
102 .first()
103 .map(RuntimeScalarEdit::descriptor_index)
104 .unwrap_or(usize::MAX);
105 let mut entity_indices = edits
106 .iter()
107 .map(|edit| edit.entity_index)
108 .collect::<Vec<_>>();
109 entity_indices.sort_unstable();
110 entity_indices.dedup();
111 Self {
112 kind,
113 reason,
114 edits,
115 entity_indices,
116 descriptor_index,
117 require_hard_improvement,
118 }
119 }
120
121 pub(crate) fn is_doable_on(&self, solution: &S) -> bool {
122 if self.edits.is_empty() || has_duplicate_targets(&self.edits) {
123 return false;
124 }
125 let mut changes_value = false;
126 for edit in &self.edits {
127 if edit.entity_index >= edit.slot.entity_count(solution)
128 || !edit
129 .slot
130 .value_is_legal(solution, edit.entity_index, edit.to_value)
131 {
132 return false;
133 }
134 changes_value |= edit.slot.current_value(solution, edit.entity_index) != edit.to_value;
135 }
136 changes_value
137 }
138
139 #[cfg(test)]
140 pub(crate) fn reason_id(&self) -> ProviderReasonId {
141 self.reason
142 }
143}
144
145impl<S> Debug for RuntimeCompoundMove<S> {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.debug_struct("RuntimeCompoundMove")
148 .field("kind", &self.kind)
149 .field("reason_id", &self.reason)
150 .field("edits", &self.edits)
151 .field("require_hard_improvement", &self.require_hard_improvement)
152 .finish()
153 }
154}
155
156impl<S> Move<S> for RuntimeCompoundMove<S>
157where
158 S: PlanningSolution,
159{
160 type Undo = Vec<Option<usize>>;
161
162 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
163 self.is_doable_on(score_director.working_solution())
164 }
165
166 fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
167 let mut undo = Vec::with_capacity(self.edits.len());
168 let affected = unique_affected_entities(&self.edits);
169 for edit in &self.edits {
170 undo.push(
171 edit.slot
172 .current_value(score_director.working_solution(), edit.entity_index),
173 );
174 }
175 for (descriptor_index, entity_index) in &affected {
176 score_director.before_variable_changed(*descriptor_index, *entity_index);
177 }
178 for edit in &self.edits {
179 edit.slot.set_value(
180 score_director.working_solution_mut(),
181 edit.entity_index,
182 edit.to_value,
183 );
184 }
185 for (descriptor_index, entity_index) in affected.iter().rev() {
186 score_director.after_variable_changed(*descriptor_index, *entity_index);
187 }
188 undo
189 }
190
191 fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
192 let affected = unique_affected_entities(&self.edits);
193 for (descriptor_index, entity_index) in &affected {
194 score_director.before_variable_changed(*descriptor_index, *entity_index);
195 }
196 for (edit, old_value) in self.edits.iter().zip(undo) {
197 edit.slot.set_value(
198 score_director.working_solution_mut(),
199 edit.entity_index,
200 old_value,
201 );
202 }
203 for (descriptor_index, entity_index) in affected.iter().rev() {
204 score_director.after_variable_changed(*descriptor_index, *entity_index);
205 }
206 }
207
208 fn descriptor_index(&self) -> usize {
209 self.descriptor_index
210 }
211
212 fn entity_indices(&self) -> &[usize] {
213 &self.entity_indices
214 }
215
216 fn variable_name(&self) -> &str {
217 self.kind.telemetry_label()
218 }
219
220 fn telemetry_label(&self) -> &'static str {
221 self.kind.telemetry_label()
222 }
223
224 fn requires_hard_improvement(&self) -> bool {
225 self.require_hard_improvement
226 }
227
228 fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
229 let first = self
230 .edits
231 .first()
232 .expect("runtime compound move tabu signature requires an edit");
233 let scope = MoveTabuScope::new(first.descriptor_index(), first.variable_name());
234 let mut move_id = smallvec![0xDCA7_0000_0000_1000 ^ self.kind as u8 as u64];
235 let mut undo_move_id = move_id.clone();
236 let mut entity_tokens: SmallVec<[_; 8]> = SmallVec::new();
237 let mut destination_tokens: SmallVec<[_; 8]> = SmallVec::new();
238
239 for edit in &self.edits {
240 let current = edit
241 .slot
242 .current_value(score_director.working_solution(), edit.entity_index);
243 let descriptor = encode_usize(edit.descriptor_index());
244 let variable = hash_str(edit.variable_name());
245 let entity = encode_usize(edit.entity_index);
246 let from = encode_option_debug(current.as_ref());
247 let to = encode_option_debug(edit.to_value.as_ref());
248 let edit_scope = MoveTabuScope::new(edit.descriptor_index(), edit.variable_name());
249
250 move_id.extend([descriptor, variable, entity, from, to]);
251 undo_move_id.extend([descriptor, variable, entity, to, from]);
252 let entity_token = edit_scope.entity_token(entity);
253 if !entity_tokens.contains(&entity_token) {
254 entity_tokens.push(entity_token);
255 }
256 let destination_token = edit_scope.value_token(to);
257 if !destination_tokens.contains(&destination_token) {
258 destination_tokens.push(destination_token);
259 }
260 }
261
262 MoveTabuSignature::new(scope, move_id, undo_move_id)
263 .with_entity_tokens(entity_tokens)
264 .with_destination_value_tokens(destination_tokens)
265 }
266
267 fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
268 Some(CandidateTraceIdentity::composite(
269 self.kind.trace_operation(),
270 self.edits.iter().map(|edit| {
271 CandidateTraceIdentity::logical_move(
272 edit.descriptor_index(),
273 edit.variable_name(),
274 "scalar_change",
275 vec![
276 CandidateTraceCoordinate::from(edit.entity_index),
277 CandidateTraceCoordinate::from(edit.to_value),
278 ],
279 )
280 }),
281 ))
282 }
283
284 fn for_each_affected_entity(&self, visitor: &mut dyn FnMut(MoveAffectedEntity<'_>)) {
285 for edit in &self.edits {
286 visitor(MoveAffectedEntity {
287 descriptor_index: edit.descriptor_index(),
288 entity_index: edit.entity_index,
289 variable_name: edit.variable_name(),
290 });
291 }
292 }
293}
294
295fn has_duplicate_targets<S>(edits: &[RuntimeScalarEdit<S>]) -> bool {
296 let mut targets = Vec::with_capacity(edits.len());
297 edits.iter().any(|edit| {
298 let target = (
299 edit.descriptor_index(),
300 edit.variable_index(),
301 edit.entity_index,
302 );
303 if targets.contains(&target) {
304 true
305 } else {
306 targets.push(target);
307 false
308 }
309 })
310}
311
312fn unique_affected_entities<S>(edits: &[RuntimeScalarEdit<S>]) -> Vec<(usize, usize)> {
313 let mut affected = Vec::new();
314 for edit in edits {
315 let entity = (edit.descriptor_index(), edit.entity_index);
316 if !affected.contains(&entity) {
317 affected.push(entity);
318 }
319 }
320 affected
321}