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}