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, SolutionDescriptor};
16use solverforge_core::ConstraintRef;
17use solverforge_scoring::{ConstraintMetadata, Director, DirectorScoreState};
18
19use crate::stats::CandidateTraceIdentity;
20
21use super::{Move, 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
133pub(crate) struct SequentialPreviewDirector<S: PlanningSolution> {
134    working_solution: S,
135    descriptor: SolutionDescriptor,
136    constraint_metadata: Vec<(ConstraintRef, bool)>,
137    entity_counts: Vec<Option<usize>>,
138    total_entity_count: Option<usize>,
139}
140
141impl<S: PlanningSolution> SequentialPreviewDirector<S> {
142    pub(crate) fn from_director<D: Director<S>>(score_director: &D) -> Self {
143        let descriptor = score_director.solution_descriptor();
144        let entity_counts = (0..descriptor.entity_descriptor_count())
145            .map(|descriptor_index| score_director.entity_count(descriptor_index))
146            .collect();
147
148        Self {
149            working_solution: score_director.clone_working_solution(),
150            descriptor: descriptor.clone(),
151            constraint_metadata: score_director
152                .constraint_metadata()
153                .into_iter()
154                .map(|metadata| (metadata.constraint_ref.clone(), metadata.is_hard))
155                .collect(),
156            entity_counts,
157            total_entity_count: score_director.total_entity_count(),
158        }
159    }
160}
161
162impl<S: PlanningSolution> Director<S> for SequentialPreviewDirector<S> {
163    fn working_solution(&self) -> &S {
164        &self.working_solution
165    }
166
167    fn working_solution_mut(&mut self) -> &mut S {
168        &mut self.working_solution
169    }
170
171    fn calculate_score(&mut self) -> S::Score {
172        panic!("preview directors are only for selector generation")
173    }
174
175    fn solution_descriptor(&self) -> &SolutionDescriptor {
176        &self.descriptor
177    }
178
179    fn clone_working_solution(&self) -> S {
180        self.working_solution.clone()
181    }
182
183    fn before_variable_changed(&mut self, _descriptor_index: usize, _entity_index: usize) {
184        self.working_solution.set_score(None);
185    }
186
187    fn after_variable_changed(&mut self, descriptor_index: usize, entity_index: usize) {
188        self.working_solution
189            .update_entity_shadows(descriptor_index, entity_index);
190        self.working_solution.set_score(None);
191    }
192
193    fn entity_count(&self, descriptor_index: usize) -> Option<usize> {
194        self.entity_counts.get(descriptor_index).copied().flatten()
195    }
196
197    fn total_entity_count(&self) -> Option<usize> {
198        self.total_entity_count
199    }
200
201    fn constraint_metadata(&self) -> Vec<ConstraintMetadata<'_>> {
202        self.constraint_metadata
203            .iter()
204            .map(|(constraint_ref, is_hard)| ConstraintMetadata::new(constraint_ref, *is_hard))
205            .collect()
206    }
207
208    fn is_incremental(&self) -> bool {
209        false
210    }
211
212    fn snapshot_score_state(&self) -> DirectorScoreState<S::Score> {
213        DirectorScoreState {
214            solution_score: self.working_solution.score(),
215            committed_score: self.working_solution.score(),
216            initialized: self.working_solution.score().is_some(),
217        }
218    }
219
220    fn restore_score_state(&mut self, state: DirectorScoreState<S::Score>) {
221        self.working_solution.set_score(state.solution_score);
222    }
223}
224
225/// A cached sequential composite that owns both child moves.
226///
227/// This keeps cartesian selector output valid even after the selector is
228/// reused or dropped.
229pub struct SequentialCompositeMove<S, M> {
230    moves: MoveArena<M>,
231    descriptor_index: usize,
232    entity_indices: SmallVec<[usize; 8]>,
233    variable_name: String,
234    tabu_signature: MoveTabuSignature,
235    require_hard_improvement: bool,
236    _phantom: PhantomData<fn() -> S>,
237}
238
239impl<S, M> SequentialCompositeMove<S, M>
240where
241    S: PlanningSolution,
242    M: Move<S>,
243{
244    pub fn new(
245        first: M,
246        second: M,
247        descriptor_index: usize,
248        entity_indices: SmallVec<[usize; 8]>,
249        variable_name: impl Into<String>,
250        tabu_signature: MoveTabuSignature,
251    ) -> Self {
252        let mut moves = MoveArena::with_capacity(2);
253        moves.push(first);
254        moves.push(second);
255
256        Self {
257            moves,
258            descriptor_index,
259            entity_indices,
260            variable_name: variable_name.into(),
261            tabu_signature,
262            require_hard_improvement: false,
263            _phantom: PhantomData,
264        }
265    }
266
267    pub fn with_require_hard_improvement(mut self, require_hard_improvement: bool) -> Self {
268        self.require_hard_improvement = require_hard_improvement;
269        self
270    }
271
272    fn first_move(&self) -> &M {
273        self.moves
274            .get(0)
275            .expect("sequential composite first move must remain valid")
276    }
277
278    fn second_move(&self) -> &M {
279        self.moves
280            .get(1)
281            .expect("sequential composite second move must remain valid")
282    }
283}
284
285pub struct SequentialCompositeMoveRef<'a, S, M>
286where
287    S: PlanningSolution,
288    M: Move<S>,
289{
290    first: &'a M,
291    second: &'a M,
292    descriptor_index: usize,
293    entity_indices: &'a [usize],
294    variable_name: &'a str,
295    tabu_signature: &'a MoveTabuSignature,
296    require_hard_improvement: bool,
297    _phantom: PhantomData<fn() -> S>,
298}
299
300impl<S, M> Debug for SequentialCompositeMoveRef<'_, S, M>
301where
302    S: PlanningSolution,
303    M: Move<S>,
304{
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("SequentialCompositeMoveRef")
307            .field("descriptor_index", &self.descriptor_index)
308            .field("variable_name", &self.variable_name)
309            .field("entity_indices", &self.entity_indices)
310            .finish()
311    }
312}
313
314impl<'a, S, M> SequentialCompositeMoveRef<'a, S, M>
315where
316    S: PlanningSolution,
317    M: Move<S>,
318{
319    pub fn new(
320        first: &'a M,
321        second: &'a M,
322        descriptor_index: usize,
323        entity_indices: &'a [usize],
324        variable_name: &'a str,
325        tabu_signature: &'a MoveTabuSignature,
326        require_hard_improvement: bool,
327    ) -> Self {
328        Self {
329            first,
330            second,
331            descriptor_index,
332            entity_indices,
333            variable_name,
334            tabu_signature,
335            require_hard_improvement,
336            _phantom: PhantomData,
337        }
338    }
339
340    pub fn first(&self) -> &'a M {
341        self.first
342    }
343
344    pub fn second(&self) -> &'a M {
345        self.second
346    }
347}
348
349impl<S, M> Clone for SequentialCompositeMoveRef<'_, S, M>
350where
351    S: PlanningSolution,
352    M: Move<S>,
353{
354    fn clone(&self) -> Self {
355        Self {
356            first: self.first,
357            second: self.second,
358            descriptor_index: self.descriptor_index,
359            entity_indices: self.entity_indices,
360            variable_name: self.variable_name,
361            tabu_signature: self.tabu_signature,
362            require_hard_improvement: self.require_hard_improvement,
363            _phantom: PhantomData,
364        }
365    }
366}
367
368impl<S, M> Move<S> for SequentialCompositeMoveRef<'_, S, M>
369where
370    S: PlanningSolution,
371    M: Move<S>,
372{
373    type Undo = (M::Undo, M::Undo);
374
375    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
376        if !self.first.is_doable(score_director) {
377            return false;
378        }
379
380        let mut preview = SequentialPreviewDirector::from_director(score_director);
381        let _ = self.first.do_move(&mut preview);
382        self.second.is_doable(&preview)
383    }
384
385    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
386        let first = self.first.do_move(score_director);
387        let second = self.second.do_move(score_director);
388        (first, second)
389    }
390
391    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
392        self.second.undo_move(score_director, undo.1);
393        self.first.undo_move(score_director, undo.0);
394    }
395
396    fn descriptor_index(&self) -> usize {
397        self.descriptor_index
398    }
399
400    fn entity_indices(&self) -> &[usize] {
401        self.entity_indices
402    }
403
404    fn variable_name(&self) -> &str {
405        self.variable_name
406    }
407
408    fn requires_hard_improvement(&self) -> bool {
409        self.require_hard_improvement
410            || self.first.requires_hard_improvement()
411            || self.second.requires_hard_improvement()
412    }
413
414    fn requires_score_improvement(&self) -> bool {
415        self.first.requires_score_improvement() || self.second.requires_score_improvement()
416    }
417
418    fn tabu_signature<D: Director<S>>(&self, _score_director: &D) -> MoveTabuSignature {
419        self.tabu_signature.clone()
420    }
421
422    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
423        Some(CandidateTraceIdentity::composite(
424            "sequential_composite",
425            [
426                self.first.candidate_trace_identity()?,
427                self.second.candidate_trace_identity()?,
428            ],
429        ))
430    }
431}
432
433impl<S, M> Clone for SequentialCompositeMove<S, M>
434where
435    S: PlanningSolution,
436    M: Move<S> + Clone,
437{
438    fn clone(&self) -> Self {
439        Self::new(
440            self.first_move().clone(),
441            self.second_move().clone(),
442            self.descriptor_index,
443            self.entity_indices.clone(),
444            self.variable_name.clone(),
445            self.tabu_signature.clone(),
446        )
447        .with_require_hard_improvement(self.require_hard_improvement)
448    }
449}
450
451impl<S, M> Debug for SequentialCompositeMove<S, M>
452where
453    S: PlanningSolution,
454    M: Move<S>,
455{
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        f.debug_struct("SequentialCompositeMove")
458            .field("descriptor_index", &self.descriptor_index)
459            .field("variable_name", &self.variable_name)
460            .field("entity_indices", &self.entity_indices)
461            .finish()
462    }
463}
464
465impl<S, M> Move<S> for SequentialCompositeMove<S, M>
466where
467    S: PlanningSolution,
468    M: Move<S>,
469{
470    type Undo = (M::Undo, M::Undo);
471
472    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
473        let first = self.first_move();
474        if !first.is_doable(score_director) {
475            return false;
476        }
477
478        let mut preview = SequentialPreviewDirector::from_director(score_director);
479        let _ = first.do_move(&mut preview);
480        self.second_move().is_doable(&preview)
481    }
482
483    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
484        let first = self.first_move().do_move(score_director);
485        let second = self.second_move().do_move(score_director);
486        (first, second)
487    }
488
489    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
490        self.second_move().undo_move(score_director, undo.1);
491        self.first_move().undo_move(score_director, undo.0);
492    }
493
494    fn descriptor_index(&self) -> usize {
495        self.descriptor_index
496    }
497
498    fn entity_indices(&self) -> &[usize] {
499        &self.entity_indices
500    }
501
502    fn variable_name(&self) -> &str {
503        &self.variable_name
504    }
505
506    fn requires_hard_improvement(&self) -> bool {
507        self.require_hard_improvement
508            || self.first_move().requires_hard_improvement()
509            || self.second_move().requires_hard_improvement()
510    }
511
512    fn requires_score_improvement(&self) -> bool {
513        self.first_move().requires_score_improvement()
514            || self.second_move().requires_score_improvement()
515    }
516
517    fn tabu_signature<D: Director<S>>(&self, _score_director: &D) -> MoveTabuSignature {
518        self.tabu_signature.clone()
519    }
520
521    fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
522        Some(CandidateTraceIdentity::composite(
523            "sequential_composite",
524            [
525                self.first_move().candidate_trace_identity()?,
526                self.second_move().candidate_trace_identity()?,
527            ],
528        ))
529    }
530}