Skip to main content

solverforge_solver/heuristic/move/
compound_scalar.rs

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
28/// One edit in a grouped scalar move.
29///
30/// The static form keeps the existing direct function pointers.  The dynamic
31/// form carries the already-bound dynamic slot, so move execution never needs
32/// a thread-local group name or a schema lookup.
33pub 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}