Skip to main content

solverforge_solver/heuristic/move/
traits.rs

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