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