Skip to main content

solverforge_solver/heuristic/move/
traits.rs

1// Move trait definition.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6use solverforge_scoring::Director;
7
8use super::MoveTabuSignature;
9use crate::stats::CandidateTraceIdentity;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct MoveAffectedEntity<'a> {
13    pub descriptor_index: usize,
14    pub entity_index: usize,
15    pub variable_name: &'a str,
16}
17
18/// A move that modifies one or more planning variables.
19///
20/// Moves are fully monomorphized for maximum performance - no boxing, no virtual dispatch.
21/// Undo is handled by move-owned typed data, not boxed director callbacks.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25///
26/// # Implementation Notes
27/// - Moves should be lightweight
28/// - Moves are NEVER cloned - ownership transfers via arena indices
29/// - Methods are generic over D to allow use with concrete directors
30pub trait Move<S: PlanningSolution>: Send + Sync + Debug {
31    type Undo: Send;
32
33    /* Returns true if this move can be executed in the current state.
34
35    A move is not doable if:
36    - The source value equals the destination value (no change)
37    - The move's own structural and value-domain rules reject it
38    - The move would violate hard constraints that can be detected early
39    */
40    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool;
41
42    /* Executes this move, modifying the working solution.
43
44    This method modifies the planning variables through the score director.
45    Returns concrete undo data that can be passed back to `undo_move`.
46    */
47    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo;
48
49    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo);
50
51    /* Trial path with a retained solve scratch pool of retired undo
52    journals. The default ignores the pool and delegates to the plain pair;
53    long-journal families override both to recycle undo storage across
54    trials, so arbitrary-long edit sets do not grow the heap per trial. */
55    fn do_move_with_scratch<D: Director<S>>(
56        &self,
57        score_director: &mut D,
58        journal_pool: &mut Vec<Self::Undo>,
59    ) -> Self::Undo {
60        let _ = journal_pool;
61        self.do_move(score_director)
62    }
63
64    fn undo_move_with_scratch<D: Director<S>>(
65        &self,
66        score_director: &mut D,
67        undo: Self::Undo,
68        journal_pool: &mut Vec<Self::Undo>,
69    ) {
70        let _ = journal_pool;
71        self.undo_move(score_director, undo)
72    }
73
74    /* Doability check that may run an explicit working preview on the LIVE
75    director.
76
77    Default: identical to `is_doable` with the director reborrowed shared.
78    Sequential/compound families override this where their later legs read
79    the earlier legs' effects; the phase trial path calls THIS variant so the
80    preview mutation is owned by the phase-level orchestration instead of a
81    clone-out substitute director.
82    */
83    fn is_doable_with_working_preview<D: Director<S>>(&self, score_director: &mut D) -> bool {
84        self.is_doable(score_director)
85    }
86
87    fn descriptor_index(&self) -> usize;
88
89    fn entity_indices(&self) -> &[usize];
90
91    fn variable_name(&self) -> &str;
92
93    fn telemetry_label(&self) -> &'static str {
94        "move"
95    }
96
97    fn requires_hard_improvement(&self) -> bool {
98        false
99    }
100
101    fn requires_score_improvement(&self) -> bool {
102        false
103    }
104
105    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature;
106
107    /* Borrows this move's prebuilt signature instead of building a fresh one.
108
109    Moves that own their completed signature (static compositions) return
110    `Some`. Dynamic builders return `None`, and callers fall back to
111    `tabu_signature`. */
112    fn borrowed_tabu_signature(&self) -> Option<&MoveTabuSignature> {
113        None
114    }
115
116    /// Returns the canonical identity used by opt-in candidate-pull tracing.
117    ///
118    /// Returns an explicit logical-coordinate identity for candidate tracing.
119    ///
120    /// This intentionally does *not* derive from tabu metadata: tabu keys may
121    /// contain representation-specific hashes. A move family that has not
122    /// supplied a cross-representation coordinate grammar returns `None`, and
123    /// the resulting trace is explicitly non-qualifying rather than faking a
124    /// comparable key.
125    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
126        None
127    }
128
129    /// Visits every entity this move changes, passing a concrete visitor by
130    /// value. `V` stays monomorphized end to end: no `dyn` dispatch, no
131    /// intermediate collection, every affected entity reported exactly once
132    /// in the move's own canonical order.
133    fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
134        let descriptor_index = self.descriptor_index();
135        let variable_name = self.variable_name();
136        for &entity_index in self.entity_indices() {
137            visitor(MoveAffectedEntity {
138                descriptor_index,
139                entity_index,
140                variable_name,
141            });
142        }
143    }
144}