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