Skip to main content

solverforge_solver/heuristic/move/
compound_scalar.rs

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