Skip to main content

solverforge_solver/heuristic/move/
runtime_compound.rs

1//! Owned compound scalar moves emitted by the one runtime-provider kernel.
2//!
3//! Unlike the older typed `CompoundScalarMove`, this payload owns the callback
4//! reason and carries the shared static/dynamic scalar access protocol. It
5//! never converts a host label into a leaked `&'static str`.
6
7use std::borrow::Cow;
8use std::fmt::{self, Debug};
9
10#[cfg(test)]
11use std::cell::Cell;
12
13use smallvec::{smallvec, SmallVec};
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::Director;
16
17use crate::builder::context::ProviderReasonId;
18use crate::builder::RuntimeScalarEdit;
19use crate::stats::{CandidateTraceCoordinate, CandidateTraceIdentity};
20
21use super::metadata::{encode_option_debug, encode_usize, hash_str, MoveTabuScope};
22use super::{Move, MoveAffectedEntity, MoveTabuSignature};
23
24#[cfg(test)]
25thread_local! {
26    static CLONE_COUNT: Cell<usize> = const { Cell::new(0) };
27}
28
29#[cfg(test)]
30pub(crate) fn reset_runtime_compound_move_clone_count() {
31    CLONE_COUNT.with(|count| count.set(0));
32}
33
34#[cfg(test)]
35pub(crate) fn runtime_compound_move_clone_count() -> usize {
36    CLONE_COUNT.with(Cell::get)
37}
38
39/// Stable provider-family discriminator. The value is part of the exact tabu
40/// identity, so identical edits emitted by distinct provider kinds cannot
41/// accidentally share tabu memory.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[repr(u8)]
44pub enum RuntimeCompoundMoveKind {
45    Grouped = 1,
46    ConflictRepair = 2,
47    CompoundConflictRepair = 3,
48}
49
50impl RuntimeCompoundMoveKind {
51    pub const fn telemetry_label(self) -> &'static str {
52        match self {
53            Self::Grouped => "runtime_scalar_grouped",
54            Self::ConflictRepair => "runtime_scalar_conflict_repair",
55            Self::CompoundConflictRepair => "runtime_scalar_compound_conflict_repair",
56        }
57    }
58
59    const fn trace_operation(self) -> &'static str {
60        match self {
61            Self::Grouped => "runtime_provider_grouped",
62            Self::ConflictRepair => "runtime_provider_conflict_repair",
63            Self::CompoundConflictRepair => "runtime_provider_compound_conflict_repair",
64        }
65    }
66}
67
68/// A selected runtime-provider result. The callback reason is a compact
69/// per-run arena ID. It deliberately never participates in tabu or
70/// candidate-trace identity, and candidate moves never clone/refcount labels.
71pub struct RuntimeCompoundMove<S> {
72    kind: RuntimeCompoundMoveKind,
73    reason: ProviderReasonId,
74    edits: Vec<RuntimeScalarEdit<S>>,
75    entity_indices: Vec<usize>,
76    descriptor_index: usize,
77    require_hard_improvement: bool,
78}
79
80impl<S> RuntimeCompoundMove<S> {
81    pub(crate) fn new(
82        kind: RuntimeCompoundMoveKind,
83        reason: ProviderReasonId,
84        edits: Vec<RuntimeScalarEdit<S>>,
85        require_hard_improvement: bool,
86    ) -> Self {
87        let descriptor_index = edits
88            .first()
89            .map(RuntimeScalarEdit::descriptor_index)
90            .unwrap_or(usize::MAX);
91        let mut entity_indices = edits
92            .iter()
93            .map(|edit| edit.entity_index)
94            .collect::<Vec<_>>();
95        entity_indices.sort_unstable();
96        entity_indices.dedup();
97        Self {
98            kind,
99            reason,
100            edits,
101            entity_indices,
102            descriptor_index,
103            require_hard_improvement,
104        }
105    }
106
107    pub(crate) fn is_doable_on(&self, solution: &S) -> bool {
108        if self.edits.is_empty() || has_duplicate_targets(&self.edits) {
109            return false;
110        }
111        let mut changes_value = false;
112        for edit in &self.edits {
113            if edit.entity_index >= edit.slot.entity_count(solution)
114                || !edit
115                    .slot
116                    .value_is_legal(solution, edit.entity_index, edit.to_value)
117            {
118                return false;
119            }
120            changes_value |= edit.slot.current_value(solution, edit.entity_index) != edit.to_value;
121        }
122        changes_value
123    }
124
125    #[cfg(test)]
126    pub(crate) fn reason_id(&self) -> ProviderReasonId {
127        self.reason
128    }
129}
130
131impl<S> Debug for RuntimeCompoundMove<S> {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        f.debug_struct("RuntimeCompoundMove")
134            .field("kind", &self.kind)
135            .field("reason_id", &self.reason)
136            .field("edits", &self.edits)
137            .field("require_hard_improvement", &self.require_hard_improvement)
138            .finish()
139    }
140}
141
142impl<S> Move<S> for RuntimeCompoundMove<S>
143where
144    S: PlanningSolution,
145{
146    type Undo = SmallVec<[Option<usize>; 8]>;
147
148    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
149        self.is_doable_on(score_director.working_solution())
150    }
151
152    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
153        let mut undo = SmallVec::<[Option<usize>; 8]>::with_capacity(self.edits.len());
154        let affected = unique_affected_entities(&self.edits);
155        for edit in &self.edits {
156            undo.push(
157                edit.slot
158                    .current_value(score_director.working_solution(), edit.entity_index),
159            );
160        }
161        for (descriptor_index, entity_index) in &affected {
162            score_director.before_variable_changed(*descriptor_index, *entity_index);
163        }
164        for edit in &self.edits {
165            edit.slot.set_value(
166                score_director.working_solution_mut(),
167                edit.entity_index,
168                edit.to_value,
169            );
170        }
171        for (descriptor_index, entity_index) in affected.iter().rev() {
172            score_director.after_variable_changed(*descriptor_index, *entity_index);
173        }
174        undo
175    }
176
177    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
178        let affected = unique_affected_entities(&self.edits);
179        for (descriptor_index, entity_index) in &affected {
180            score_director.before_variable_changed(*descriptor_index, *entity_index);
181        }
182        for (edit, old_value) in self.edits.iter().zip(undo) {
183            edit.slot.set_value(
184                score_director.working_solution_mut(),
185                edit.entity_index,
186                old_value,
187            );
188        }
189        for (descriptor_index, entity_index) in affected.iter().rev() {
190            score_director.after_variable_changed(*descriptor_index, *entity_index);
191        }
192    }
193
194    fn descriptor_index(&self) -> usize {
195        self.descriptor_index
196    }
197
198    fn entity_indices(&self) -> &[usize] {
199        &self.entity_indices
200    }
201
202    fn variable_name(&self) -> &str {
203        self.kind.telemetry_label()
204    }
205
206    fn telemetry_label(&self) -> &'static str {
207        self.kind.telemetry_label()
208    }
209
210    fn requires_hard_improvement(&self) -> bool {
211        self.require_hard_improvement
212    }
213
214    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> Cow<'_, MoveTabuSignature> {
215        let first = self
216            .edits
217            .first()
218            .expect("runtime compound move tabu signature requires an edit");
219        let scope = MoveTabuScope::new(first.descriptor_index(), first.variable_name());
220        let mut move_id = smallvec![0xDCA7_0000_0000_1000 ^ self.kind as u8 as u64];
221        let mut undo_move_id = move_id.clone();
222        let mut entity_tokens: SmallVec<[_; 8]> = SmallVec::new();
223        let mut destination_tokens: SmallVec<[_; 8]> = SmallVec::new();
224
225        for edit in &self.edits {
226            let current = edit
227                .slot
228                .current_value(score_director.working_solution(), edit.entity_index);
229            let descriptor = encode_usize(edit.descriptor_index());
230            let variable = hash_str(edit.variable_name());
231            let entity = encode_usize(edit.entity_index);
232            let from = encode_option_debug(current.as_ref());
233            let to = encode_option_debug(edit.to_value.as_ref());
234            let edit_scope = MoveTabuScope::new(edit.descriptor_index(), edit.variable_name());
235
236            move_id.extend([descriptor, variable, entity, from, to]);
237            undo_move_id.extend([descriptor, variable, entity, to, from]);
238            let entity_token = edit_scope.entity_token(entity);
239            if !entity_tokens.contains(&entity_token) {
240                entity_tokens.push(entity_token);
241            }
242            let destination_token = edit_scope.value_token(to);
243            if !destination_tokens.contains(&destination_token) {
244                destination_tokens.push(destination_token);
245            }
246        }
247
248        Cow::Owned(
249            MoveTabuSignature::new(scope, move_id, undo_move_id)
250                .with_entity_tokens(entity_tokens)
251                .with_destination_value_tokens(destination_tokens),
252        )
253    }
254
255    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
256        Some(CandidateTraceIdentity::composite(
257            self.kind.trace_operation(),
258            self.edits.iter().map(|edit| {
259                CandidateTraceIdentity::logical_move(
260                    edit.descriptor_index(),
261                    edit.variable_name(),
262                    "scalar_change",
263                    vec![
264                        CandidateTraceCoordinate::from(edit.entity_index),
265                        CandidateTraceCoordinate::from(edit.to_value),
266                    ],
267                )
268            }),
269        ))
270    }
271
272    fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
273        for edit in &self.edits {
274            visitor(MoveAffectedEntity {
275                descriptor_index: edit.descriptor_index(),
276                entity_index: edit.entity_index,
277                variable_name: edit.variable_name(),
278            });
279        }
280    }
281}
282
283fn has_duplicate_targets<S>(edits: &[RuntimeScalarEdit<S>]) -> bool {
284    let mut targets = SmallVec::<[(usize, usize, usize); 8]>::with_capacity(edits.len());
285    edits.iter().any(|edit| {
286        let target = (
287            edit.descriptor_index(),
288            edit.variable_index(),
289            edit.entity_index,
290        );
291        if targets.contains(&target) {
292            true
293        } else {
294            targets.push(target);
295            false
296        }
297    })
298}
299
300fn unique_affected_entities<S>(edits: &[RuntimeScalarEdit<S>]) -> SmallVec<[(usize, usize); 8]> {
301    let mut affected = SmallVec::new();
302    for edit in edits {
303        let entity = (edit.descriptor_index(), edit.entity_index);
304        if !affected.contains(&entity) {
305            affected.push(entity);
306        }
307    }
308    affected
309}