1use std::fmt::{self, Debug};
2
3use smallvec::{smallvec, SmallVec};
4use solverforge_core::domain::{DynamicScalarVariableSlot, PlanningSolution};
5use solverforge_scoring::Director;
6
7use crate::stats::{CandidateTraceCoordinate, CandidateTraceIdentity};
8
9use super::metadata::{
10 encode_option_usize, encode_usize, hash_str, MoveTabuScope, MoveTabuSignature,
11};
12use super::{Move, MoveAffectedEntity};
13
14pub const COMPOUND_SCALAR_VARIABLE: &str = "compound_scalar";
15
16pub type ScalarEditLegality<S> = fn(&S, usize, usize, Option<usize>) -> bool;
17
18enum CompoundScalarEditAccess<S> {
19 Static {
20 getter: fn(&S, usize, usize) -> Option<usize>,
21 setter: fn(&mut S, usize, usize, Option<usize>),
22 value_is_legal: Option<ScalarEditLegality<S>>,
23 },
24 Dynamic(DynamicScalarVariableSlot<S>),
25}
26
27impl<S> Clone for CompoundScalarEditAccess<S> {
28 fn clone(&self) -> Self {
29 match self {
30 Self::Static {
31 getter,
32 setter,
33 value_is_legal,
34 } => Self::Static {
35 getter: *getter,
36 setter: *setter,
37 value_is_legal: *value_is_legal,
38 },
39 Self::Dynamic(slot) => Self::Dynamic(slot.clone()),
40 }
41 }
42}
43
44pub struct CompoundScalarEdit<S> {
50 pub descriptor_index: usize,
51 pub entity_index: usize,
52 pub variable_index: usize,
53 pub variable_name: &'static str,
54 pub to_value: Option<usize>,
55 access: CompoundScalarEditAccess<S>,
56}
57
58impl<S> Clone for CompoundScalarEdit<S> {
59 fn clone(&self) -> Self {
60 Self {
61 descriptor_index: self.descriptor_index,
62 entity_index: self.entity_index,
63 variable_index: self.variable_index,
64 variable_name: self.variable_name,
65 to_value: self.to_value,
66 access: self.access.clone(),
67 }
68 }
69}
70
71impl<S> CompoundScalarEdit<S> {
72 #[allow(clippy::too_many_arguments)]
73 pub fn static_edit(
74 descriptor_index: usize,
75 entity_index: usize,
76 variable_index: usize,
77 variable_name: &'static str,
78 to_value: Option<usize>,
79 getter: fn(&S, usize, usize) -> Option<usize>,
80 setter: fn(&mut S, usize, usize, Option<usize>),
81 value_is_legal: Option<ScalarEditLegality<S>>,
82 ) -> Self {
83 Self {
84 descriptor_index,
85 entity_index,
86 variable_index,
87 variable_name,
88 to_value,
89 access: CompoundScalarEditAccess::Static {
90 getter,
91 setter,
92 value_is_legal,
93 },
94 }
95 }
96
97 pub fn dynamic_edit(
98 descriptor_index: usize,
99 entity_index: usize,
100 variable_index: usize,
101 variable_name: &'static str,
102 to_value: Option<usize>,
103 slot: DynamicScalarVariableSlot<S>,
104 ) -> Self {
105 debug_assert_eq!(descriptor_index, slot.descriptor_index());
106 debug_assert_eq!(variable_index, slot.descriptor_variable_index());
107 debug_assert_eq!(variable_name, slot.variable_name);
108 Self {
109 descriptor_index,
110 entity_index,
111 variable_index,
112 variable_name,
113 to_value,
114 access: CompoundScalarEditAccess::Dynamic(slot),
115 }
116 }
117
118 pub fn with_value_is_legal(mut self, value_is_legal: ScalarEditLegality<S>) -> Self {
119 if let CompoundScalarEditAccess::Static {
120 value_is_legal: legality,
121 ..
122 } = &mut self.access
123 {
124 *legality = Some(value_is_legal);
125 }
126 self
127 }
128}
129
130impl<S> CompoundScalarEdit<S> {
131 pub(crate) fn current_value(&self, solution: &S) -> Option<usize> {
132 match &self.access {
133 CompoundScalarEditAccess::Static { getter, .. } => {
134 getter(solution, self.entity_index, self.variable_index)
135 }
136 CompoundScalarEditAccess::Dynamic(slot) => {
137 slot.current_value(solution, self.entity_index)
138 }
139 }
140 }
141
142 pub(crate) fn set_value(&self, solution: &mut S, value: Option<usize>) {
143 match &self.access {
144 CompoundScalarEditAccess::Static { setter, .. } => {
145 setter(solution, self.entity_index, self.variable_index, value)
146 }
147 CompoundScalarEditAccess::Dynamic(slot) => {
148 slot.set_value(solution, self.entity_index, value)
149 }
150 }
151 }
152
153 pub(crate) fn value_is_legal(&self, solution: &S) -> bool {
154 match &self.access {
155 CompoundScalarEditAccess::Static {
156 value_is_legal: Some(legality),
157 ..
158 } => legality(
159 solution,
160 self.entity_index,
161 self.variable_index,
162 self.to_value,
163 ),
164 CompoundScalarEditAccess::Static {
165 value_is_legal: None,
166 ..
167 } => true,
168 CompoundScalarEditAccess::Dynamic(slot) => {
169 slot.value_is_legal(solution, self.entity_index, self.to_value)
170 }
171 }
172 }
173}
174
175impl<S> Debug for CompoundScalarEdit<S> {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.debug_struct("CompoundScalarEdit")
178 .field("descriptor_index", &self.descriptor_index)
179 .field("entity_index", &self.entity_index)
180 .field("variable_index", &self.variable_index)
181 .field("variable_name", &self.variable_name)
182 .field("to_value", &self.to_value)
183 .finish()
184 }
185}
186
187#[derive(Clone)]
188pub struct CompoundScalarMove<S> {
189 reason: &'static str,
190 variable_label: &'static str,
191 edits: Vec<CompoundScalarEdit<S>>,
192 entity_indices: Vec<usize>,
193 require_hard_improvement: bool,
194 construction_value_order_key: Option<i64>,
195}
196
197impl<S> CompoundScalarMove<S> {
198 pub fn new(reason: &'static str, edits: Vec<CompoundScalarEdit<S>>) -> Self {
199 Self::with_label(reason, COMPOUND_SCALAR_VARIABLE, edits)
200 }
201
202 pub fn with_label(
203 reason: &'static str,
204 variable_label: &'static str,
205 edits: Vec<CompoundScalarEdit<S>>,
206 ) -> Self {
207 let mut entity_indices = edits
208 .iter()
209 .map(|edit| edit.entity_index)
210 .collect::<Vec<_>>();
211 entity_indices.sort_unstable();
212 entity_indices.dedup();
213 Self {
214 reason,
215 variable_label,
216 edits,
217 entity_indices,
218 require_hard_improvement: false,
219 construction_value_order_key: None,
220 }
221 }
222
223 pub fn with_require_hard_improvement(mut self, require_hard_improvement: bool) -> Self {
224 self.require_hard_improvement = require_hard_improvement;
225 self
226 }
227
228 pub(crate) fn with_construction_value_order_key(mut self, order_key: Option<i64>) -> Self {
229 self.construction_value_order_key = order_key;
230 self
231 }
232
233 pub(crate) fn construction_value_order_key(&self) -> Option<i64> {
234 self.construction_value_order_key
235 }
236
237 pub fn edits(&self) -> &[CompoundScalarEdit<S>] {
238 &self.edits
239 }
240
241 pub fn reason(&self) -> &'static str {
242 self.reason
243 }
244
245 pub(crate) fn is_doable_on(&self, solution: &S) -> bool
246 where
247 S: PlanningSolution,
248 {
249 if self.edits.is_empty() {
250 return false;
251 }
252
253 let mut changes_value = false;
254 for edit in &self.edits {
255 if !edit.value_is_legal(solution) {
256 return false;
257 }
258 changes_value |= edit.current_value(solution) != edit.to_value;
259 }
260 changes_value
261 }
262}
263
264impl<S> Debug for CompoundScalarMove<S> {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.debug_struct("CompoundScalarMove")
267 .field("reason", &self.reason)
268 .field("variable_label", &self.variable_label)
269 .field("edits", &self.edits)
270 .field("require_hard_improvement", &self.require_hard_improvement)
271 .field(
272 "construction_value_order_key",
273 &self.construction_value_order_key,
274 )
275 .finish()
276 }
277}
278
279impl<S> Move<S> for CompoundScalarMove<S>
280where
281 S: PlanningSolution,
282{
283 type Undo = Vec<Option<usize>>;
284
285 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
286 self.is_doable_on(score_director.working_solution())
287 }
288
289 fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
290 let mut undo = Vec::with_capacity(self.edits.len());
291 let affected = unique_affected_entities(&self.edits);
292 for edit in &self.edits {
293 let old_value = edit.current_value(score_director.working_solution());
294 undo.push(old_value);
295 }
296 for (descriptor_index, entity_index) in &affected {
297 score_director.before_variable_changed(*descriptor_index, *entity_index);
298 }
299 for edit in &self.edits {
300 edit.set_value(score_director.working_solution_mut(), edit.to_value);
301 }
302 for (descriptor_index, entity_index) in affected.iter().rev() {
303 score_director.after_variable_changed(*descriptor_index, *entity_index);
304 }
305 undo
306 }
307
308 fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
309 let affected = unique_affected_entities(&self.edits);
310 for (descriptor_index, entity_index) in &affected {
311 score_director.before_variable_changed(*descriptor_index, *entity_index);
312 }
313 for (edit, old_value) in self.edits.iter().zip(undo) {
314 edit.set_value(score_director.working_solution_mut(), old_value);
315 }
316 for (descriptor_index, entity_index) in affected.iter().rev() {
317 score_director.after_variable_changed(*descriptor_index, *entity_index);
318 }
319 }
320
321 fn descriptor_index(&self) -> usize {
322 self.edits
323 .first()
324 .map(|edit| edit.descriptor_index)
325 .unwrap_or(usize::MAX)
326 }
327
328 fn entity_indices(&self) -> &[usize] {
329 &self.entity_indices
330 }
331
332 fn variable_name(&self) -> &str {
333 self.variable_label
334 }
335
336 fn telemetry_label(&self) -> &'static str {
337 self.reason
338 }
339
340 fn requires_hard_improvement(&self) -> bool {
341 self.require_hard_improvement
342 }
343
344 fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
345 let scope = MoveTabuScope::new(self.descriptor_index(), self.variable_label);
346 let mut move_id = smallvec![hash_str(self.reason)];
347 let mut undo_move_id = smallvec![hash_str(self.reason)];
348 let mut entity_tokens: SmallVec<[_; 8]> = SmallVec::new();
349 let mut destination_tokens: SmallVec<[_; 8]> = SmallVec::new();
350
351 for edit in &self.edits {
352 let current = edit.current_value(score_director.working_solution());
353 let descriptor = encode_usize(edit.descriptor_index);
354 let entity = encode_usize(edit.entity_index);
355 let variable = hash_str(edit.variable_name);
356 let from = encode_option_usize(current);
357 let to = encode_option_usize(edit.to_value);
358 let edit_scope = MoveTabuScope::new(edit.descriptor_index, edit.variable_name);
359
360 move_id.extend([descriptor, entity, variable, from, to]);
361 undo_move_id.extend([descriptor, entity, variable, to, from]);
362 entity_tokens.push(edit_scope.entity_token(entity));
363 destination_tokens.push(edit_scope.value_token(to));
364 }
365
366 MoveTabuSignature::new(scope, move_id, undo_move_id)
367 .with_entity_tokens(entity_tokens)
368 .with_destination_value_tokens(destination_tokens)
369 }
370
371 fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
372 let children = self.edits.iter().map(|edit| {
373 CandidateTraceIdentity::logical_move(
374 edit.descriptor_index,
375 edit.variable_name,
376 "scalar_change",
377 vec![
378 CandidateTraceCoordinate::from(edit.entity_index),
379 CandidateTraceCoordinate::from(edit.to_value),
380 ],
381 )
382 });
383 Some(CandidateTraceIdentity::composite(self.reason, children))
384 }
385
386 fn for_each_affected_entity(&self, visitor: &mut dyn FnMut(MoveAffectedEntity<'_>)) {
387 for edit in &self.edits {
388 visitor(MoveAffectedEntity {
389 descriptor_index: edit.descriptor_index,
390 entity_index: edit.entity_index,
391 variable_name: edit.variable_name,
392 });
393 }
394 }
395}
396
397fn unique_affected_entities<S>(edits: &[CompoundScalarEdit<S>]) -> Vec<(usize, usize)> {
398 let mut affected = Vec::new();
399 for edit in edits {
400 let entity = (edit.descriptor_index, edit.entity_index);
401 if !affected.contains(&entity) {
402 affected.push(entity);
403 }
404 }
405 affected
406}