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