Skip to main content

solverforge_solver/heuristic/move/
composite.rs

1/* CompositeMove - applies two moves in sequence by arena indices.
2
3This move stores indices into two arenas. The moves themselves
4live in their respective arenas - CompositeMove just references them.
5
6# Zero-Erasure Design
7
8No cloning, no boxing - just concrete arena indices.
9*/
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use smallvec::SmallVec;
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::Director;
17
18use crate::stats::CandidateTraceIdentity;
19
20use super::{Move, MoveAffectedEntity, MoveArena, MoveTabuSignature};
21
22/// A move that applies two moves in sequence via arena indices.
23///
24/// The moves live in separate arenas. CompositeMove stores the indices
25/// and arena references needed to execute both moves.
26///
27/// # Type Parameters
28/// * `S` - The planning solution type
29/// * `M1` - The first move type
30/// * `M2` - The second move type
31pub struct CompositeMove<S, M1, M2>
32where
33    S: PlanningSolution,
34    M1: Move<S>,
35    M2: Move<S>,
36{
37    index_1: usize,
38    index_2: usize,
39    _phantom: PhantomData<(fn() -> S, fn() -> M1, fn() -> M2)>,
40}
41
42impl<S, M1, M2> CompositeMove<S, M1, M2>
43where
44    S: PlanningSolution,
45    M1: Move<S>,
46    M2: Move<S>,
47{
48    pub fn new(index_1: usize, index_2: usize) -> Self {
49        Self {
50            index_1,
51            index_2,
52            _phantom: PhantomData,
53        }
54    }
55
56    pub fn index_1(&self) -> usize {
57        self.index_1
58    }
59
60    pub fn index_2(&self) -> usize {
61        self.index_2
62    }
63
64    pub fn is_doable_with_arenas<D: Director<S>>(
65        &self,
66        arena_1: &MoveArena<M1>,
67        arena_2: &MoveArena<M2>,
68        score_director: &D,
69    ) -> bool {
70        let m1 = arena_1.get(self.index_1);
71        let m2 = arena_2.get(self.index_2);
72
73        match (m1, m2) {
74            (Some(m1), Some(m2)) => m1.is_doable(score_director) && m2.is_doable(score_director),
75            _ => false,
76        }
77    }
78
79    /// Executes both moves using the arenas.
80    pub fn do_move_with_arenas<D: Director<S>>(
81        &self,
82        arena_1: &MoveArena<M1>,
83        arena_2: &MoveArena<M2>,
84        score_director: &mut D,
85    ) -> (M1::Undo, M2::Undo) {
86        let m1 = arena_1
87            .get(self.index_1)
88            .expect("composite move first arena index must remain valid");
89        let m2 = arena_2
90            .get(self.index_2)
91            .expect("composite move second arena index must remain valid");
92
93        let first = m1.do_move(score_director);
94        let second = m2.do_move(score_director);
95        (first, second)
96    }
97}
98
99impl<S, M1, M2> Clone for CompositeMove<S, M1, M2>
100where
101    S: PlanningSolution,
102    M1: Move<S>,
103    M2: Move<S>,
104{
105    fn clone(&self) -> Self {
106        *self
107    }
108}
109
110impl<S, M1, M2> Copy for CompositeMove<S, M1, M2>
111where
112    S: PlanningSolution,
113    M1: Move<S>,
114    M2: Move<S>,
115{
116}
117
118impl<S, M1, M2> Debug for CompositeMove<S, M1, M2>
119where
120    S: PlanningSolution,
121    M1: Move<S>,
122    M2: Move<S>,
123{
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("CompositeMove")
126            .field("index_1", &self.index_1)
127            .field("index_2", &self.index_2)
128            .finish()
129    }
130}
131
132mod preview;
133pub(crate) use preview::SequentialPreviewDirector;
134
135/// A cached sequential composite that owns both child moves.
136///
137/// This keeps cartesian selector output valid even after the selector is
138/// reused or dropped.
139pub struct SequentialCompositeMove<S, M> {
140    moves: MoveArena<M>,
141    descriptor_index: usize,
142    entity_indices: SmallVec<[usize; 8]>,
143    variable_name: String,
144    tabu_signature: MoveTabuSignature,
145    require_hard_improvement: bool,
146    _phantom: PhantomData<fn() -> S>,
147}
148
149impl<S, M> SequentialCompositeMove<S, M>
150where
151    S: PlanningSolution,
152    M: Move<S>,
153{
154    pub fn new(
155        first: M,
156        second: M,
157        descriptor_index: usize,
158        entity_indices: SmallVec<[usize; 8]>,
159        variable_name: impl Into<String>,
160        tabu_signature: MoveTabuSignature,
161    ) -> Self {
162        let mut moves = MoveArena::with_capacity(2);
163        moves.push(first);
164        moves.push(second);
165
166        Self {
167            moves,
168            descriptor_index,
169            entity_indices,
170            variable_name: variable_name.into(),
171            tabu_signature,
172            require_hard_improvement: false,
173            _phantom: PhantomData,
174        }
175    }
176
177    pub fn with_require_hard_improvement(mut self, require_hard_improvement: bool) -> Self {
178        self.require_hard_improvement = require_hard_improvement;
179        self
180    }
181
182    fn first_move(&self) -> &M {
183        self.moves
184            .get(0)
185            .expect("sequential composite first move must remain valid")
186    }
187
188    fn second_move(&self) -> &M {
189        self.moves
190            .get(1)
191            .expect("sequential composite second move must remain valid")
192    }
193}
194
195pub struct SequentialCompositeMoveRef<'a, S, M>
196where
197    S: PlanningSolution,
198    M: Move<S>,
199{
200    first: &'a M,
201    second: &'a M,
202    descriptor_index: usize,
203    entity_indices: &'a [usize],
204    variable_name: &'a str,
205    tabu_signature: &'a MoveTabuSignature,
206    require_hard_improvement: bool,
207    _phantom: PhantomData<fn() -> S>,
208}
209
210impl<S, M> Debug for SequentialCompositeMoveRef<'_, S, M>
211where
212    S: PlanningSolution,
213    M: Move<S>,
214{
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        f.debug_struct("SequentialCompositeMoveRef")
217            .field("descriptor_index", &self.descriptor_index)
218            .field("variable_name", &self.variable_name)
219            .field("entity_indices", &self.entity_indices)
220            .finish()
221    }
222}
223
224impl<'a, S, M> SequentialCompositeMoveRef<'a, S, M>
225where
226    S: PlanningSolution,
227    M: Move<S>,
228{
229    pub fn new(
230        first: &'a M,
231        second: &'a M,
232        descriptor_index: usize,
233        entity_indices: &'a [usize],
234        variable_name: &'a str,
235        tabu_signature: &'a MoveTabuSignature,
236        require_hard_improvement: bool,
237    ) -> Self {
238        Self {
239            first,
240            second,
241            descriptor_index,
242            entity_indices,
243            variable_name,
244            tabu_signature,
245            require_hard_improvement,
246            _phantom: PhantomData,
247        }
248    }
249
250    pub fn first(&self) -> &'a M {
251        self.first
252    }
253
254    pub fn second(&self) -> &'a M {
255        self.second
256    }
257}
258
259impl<S, M> Move<S> for SequentialCompositeMoveRef<'_, S, M>
260where
261    S: PlanningSolution,
262    M: Move<S>,
263{
264    type Undo = (M::Undo, M::Undo);
265
266    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
267        if !self.first.is_doable(score_director) {
268            return false;
269        }
270
271        let mut preview = SequentialPreviewDirector::from_director(score_director);
272        let _ = self.first.do_move(&mut preview);
273        self.second.is_doable(&preview)
274    }
275
276    /// Live-director working preview: left applied and undone on the caller's
277    /// director, right checked against the previewed state, score state
278    /// restored before returning. No solution clone.
279    fn is_doable_with_working_preview<D: Director<S>>(&self, score_director: &mut D) -> bool {
280        if !self.first.is_doable_with_working_preview(score_director) {
281            return false;
282        }
283        let score_state = score_director.snapshot_score_state();
284        let undo = self.first.do_move(score_director);
285        let doable = self.second.is_doable_with_working_preview(score_director);
286        self.first.undo_move(score_director, undo);
287        score_director.restore_score_state(score_state);
288        doable
289    }
290
291    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
292        let first = self.first.do_move(score_director);
293        let second = self.second.do_move(score_director);
294        (first, second)
295    }
296
297    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
298        self.second.undo_move(score_director, undo.1);
299        self.first.undo_move(score_director, undo.0);
300    }
301
302    fn descriptor_index(&self) -> usize {
303        self.descriptor_index
304    }
305
306    fn entity_indices(&self) -> &[usize] {
307        self.entity_indices
308    }
309
310    fn variable_name(&self) -> &str {
311        self.variable_name
312    }
313
314    fn requires_hard_improvement(&self) -> bool {
315        self.require_hard_improvement
316            || self.first.requires_hard_improvement()
317            || self.second.requires_hard_improvement()
318    }
319
320    fn requires_score_improvement(&self) -> bool {
321        self.first.requires_score_improvement() || self.second.requires_score_improvement()
322    }
323
324    fn borrowed_tabu_signature(&self) -> Option<&MoveTabuSignature> {
325        Some(self.tabu_signature)
326    }
327
328    fn tabu_signature<D: Director<S>>(&self, _score_director: &D) -> MoveTabuSignature {
329        self.tabu_signature.clone()
330    }
331
332    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
333        Some(CandidateTraceIdentity::composite(
334            "sequential_composite",
335            [
336                self.first.candidate_trace_identity()?,
337                self.second.candidate_trace_identity()?,
338            ],
339        ))
340    }
341
342    fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
343        self.first.for_each_affected_entity(visitor);
344        self.second.for_each_affected_entity(visitor);
345    }
346}
347
348impl<S, M> Debug for SequentialCompositeMove<S, M>
349where
350    S: PlanningSolution,
351    M: Move<S>,
352{
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        f.debug_struct("SequentialCompositeMove")
355            .field("descriptor_index", &self.descriptor_index)
356            .field("variable_name", &self.variable_name)
357            .field("entity_indices", &self.entity_indices)
358            .finish()
359    }
360}
361
362impl<S, M> Move<S> for SequentialCompositeMove<S, M>
363where
364    S: PlanningSolution,
365    M: Move<S>,
366{
367    type Undo = (M::Undo, M::Undo);
368
369    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
370        let first = self.first_move();
371        if !first.is_doable(score_director) {
372            return false;
373        }
374
375        let mut preview = SequentialPreviewDirector::from_director(score_director);
376        let _ = first.do_move(&mut preview);
377        self.second_move().is_doable(&preview)
378    }
379
380    /// Live-director working preview; see `SequentialCompositeMoveRef`'s
381    /// override for the transaction shape.
382    fn is_doable_with_working_preview<D: Director<S>>(&self, score_director: &mut D) -> bool {
383        let first = self.first_move();
384        if !first.is_doable_with_working_preview(score_director) {
385            return false;
386        }
387        let score_state = score_director.snapshot_score_state();
388        let undo = first.do_move(score_director);
389        let doable = self
390            .second_move()
391            .is_doable_with_working_preview(score_director);
392        first.undo_move(score_director, undo);
393        score_director.restore_score_state(score_state);
394        doable
395    }
396
397    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
398        let first = self.first_move().do_move(score_director);
399        let second = self.second_move().do_move(score_director);
400        (first, second)
401    }
402
403    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
404        self.second_move().undo_move(score_director, undo.1);
405        self.first_move().undo_move(score_director, undo.0);
406    }
407
408    fn descriptor_index(&self) -> usize {
409        self.descriptor_index
410    }
411
412    fn entity_indices(&self) -> &[usize] {
413        &self.entity_indices
414    }
415
416    fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
417        self.first_move().for_each_affected_entity(visitor);
418        self.second_move().for_each_affected_entity(visitor);
419    }
420
421    fn variable_name(&self) -> &str {
422        &self.variable_name
423    }
424
425    fn requires_hard_improvement(&self) -> bool {
426        self.require_hard_improvement
427            || self.first_move().requires_hard_improvement()
428            || self.second_move().requires_hard_improvement()
429    }
430
431    fn requires_score_improvement(&self) -> bool {
432        self.first_move().requires_score_improvement()
433            || self.second_move().requires_score_improvement()
434    }
435
436    fn borrowed_tabu_signature(&self) -> Option<&MoveTabuSignature> {
437        Some(&self.tabu_signature)
438    }
439
440    fn tabu_signature<D: Director<S>>(&self, _score_director: &D) -> MoveTabuSignature {
441        self.tabu_signature.clone()
442    }
443
444    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
445        Some(CandidateTraceIdentity::composite(
446            "sequential_composite",
447            [
448                self.first_move().candidate_trace_identity()?,
449                self.second_move().candidate_trace_identity()?,
450            ],
451        ))
452    }
453}