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