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
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
44/// One edit in a grouped scalar move.
45///
46/// The static form keeps the existing direct function pointers.  The dynamic
47/// form carries the already-bound dynamic slot, so move execution never needs
48/// a thread-local group name or a schema lookup.
49pub 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}