Skip to main content

solverforge_solver/
runtime.rs

1use std::fmt::{self, Debug};
2use std::hash::Hash;
3use std::marker::PhantomData;
4
5use solverforge_config::{
6    ConstructionHeuristicConfig, ConstructionHeuristicType, PhaseConfig, SolverConfig,
7};
8use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
9use solverforge_core::score::{ParseableScore, Score};
10
11use crate::builder::{build_local_search, build_vnd, LocalSearch, ModelContext, Vnd};
12use crate::descriptor_standard::{
13    build_descriptor_construction, standard_target_matches, standard_work_remaining,
14};
15use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
16use crate::manager::{
17    ListCheapestInsertionPhase, ListClarkeWrightPhase, ListKOptPhase, ListRegretInsertionPhase,
18};
19use crate::phase::{sequence::PhaseSequence, Phase};
20use crate::scope::{PhaseScope, ProgressCallback, SolverScope, StepScope};
21
22#[cfg(test)]
23#[path = "runtime_tests.rs"]
24mod tests;
25
26#[cfg(test)]
27#[path = "list_solver_tests.rs"]
28mod list_tests;
29
30pub struct ListVariableMetadata<S, DM, IDM> {
31    pub cross_distance_meter: DM,
32    pub intra_distance_meter: IDM,
33    pub merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
34    pub cw_depot_fn: Option<fn(&S) -> usize>,
35    pub cw_distance_fn: Option<fn(&S, usize, usize) -> i64>,
36    pub cw_element_load_fn: Option<fn(&S, usize) -> i64>,
37    pub cw_capacity_fn: Option<fn(&S) -> i64>,
38    pub cw_assign_route_fn: Option<fn(&mut S, usize, Vec<usize>)>,
39    pub k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
40    pub k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
41    pub k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
42    pub k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
43    pub k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
44    _phantom: PhantomData<fn() -> S>,
45}
46
47pub trait ListVariableEntity<S> {
48    type CrossDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug;
49    type IntraDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug + 'static;
50
51    const HAS_LIST_VARIABLE: bool;
52    const LIST_VARIABLE_NAME: &'static str;
53    const LIST_ELEMENT_SOURCE: Option<&'static str>;
54
55    fn list_field(entity: &Self) -> &[usize];
56    fn list_field_mut(entity: &mut Self) -> &mut Vec<usize>;
57    fn list_metadata() -> ListVariableMetadata<S, Self::CrossDistanceMeter, Self::IntraDistanceMeter>;
58}
59
60impl<S, DM, IDM> ListVariableMetadata<S, DM, IDM> {
61    #[allow(clippy::too_many_arguments)]
62    pub fn new(
63        cross_distance_meter: DM,
64        intra_distance_meter: IDM,
65        merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
66        cw_depot_fn: Option<fn(&S) -> usize>,
67        cw_distance_fn: Option<fn(&S, usize, usize) -> i64>,
68        cw_element_load_fn: Option<fn(&S, usize) -> i64>,
69        cw_capacity_fn: Option<fn(&S) -> i64>,
70        cw_assign_route_fn: Option<fn(&mut S, usize, Vec<usize>)>,
71        k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
72        k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
73        k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
74        k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
75        k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
76    ) -> Self {
77        Self {
78            cross_distance_meter,
79            intra_distance_meter,
80            merge_feasible_fn,
81            cw_depot_fn,
82            cw_distance_fn,
83            cw_element_load_fn,
84            cw_capacity_fn,
85            cw_assign_route_fn,
86            k_opt_get_route,
87            k_opt_set_route,
88            k_opt_depot_fn,
89            k_opt_distance_fn,
90            k_opt_feasible_fn,
91            _phantom: PhantomData,
92        }
93    }
94}
95
96enum ListConstruction<S, V>
97where
98    S: PlanningSolution,
99    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + 'static,
100{
101    RoundRobin(ListRoundRobinPhase<S, V>),
102    CheapestInsertion(ListCheapestInsertionPhase<S, V>),
103    RegretInsertion(ListRegretInsertionPhase<S, V>),
104    ClarkeWright(ListClarkeWrightPhase<S, V>),
105    KOpt(ListKOptPhase<S, V>),
106}
107
108impl<S, V> Debug for ListConstruction<S, V>
109where
110    S: PlanningSolution,
111    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
112{
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::RoundRobin(phase) => write!(f, "ListConstruction::RoundRobin({phase:?})"),
116            Self::CheapestInsertion(phase) => {
117                write!(f, "ListConstruction::CheapestInsertion({phase:?})")
118            }
119            Self::RegretInsertion(phase) => {
120                write!(f, "ListConstruction::RegretInsertion({phase:?})")
121            }
122            Self::ClarkeWright(phase) => {
123                write!(f, "ListConstruction::ClarkeWright({phase:?})")
124            }
125            Self::KOpt(phase) => write!(f, "ListConstruction::KOpt({phase:?})"),
126        }
127    }
128}
129
130impl<S, V, D, BestCb> Phase<S, D, BestCb> for ListConstruction<S, V>
131where
132    S: PlanningSolution,
133    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
134    D: solverforge_scoring::Director<S>,
135    BestCb: crate::scope::ProgressCallback<S>,
136{
137    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, BestCb>) {
138        match self {
139            Self::RoundRobin(phase) => phase.solve(solver_scope),
140            Self::CheapestInsertion(phase) => phase.solve(solver_scope),
141            Self::RegretInsertion(phase) => phase.solve(solver_scope),
142            Self::ClarkeWright(phase) => phase.solve(solver_scope),
143            Self::KOpt(phase) => phase.solve(solver_scope),
144        }
145    }
146
147    fn phase_type_name(&self) -> &'static str {
148        "ListConstruction"
149    }
150}
151
152struct ListRoundRobinPhase<S, V>
153where
154    S: PlanningSolution,
155    V: Copy + PartialEq + Eq + Hash + Send + Sync + 'static,
156{
157    element_count: fn(&S) -> usize,
158    get_assigned: fn(&S) -> Vec<V>,
159    entity_count: fn(&S) -> usize,
160    list_len: fn(&S, usize) -> usize,
161    list_insert: fn(&mut S, usize, usize, V),
162    index_to_element: fn(&S, usize) -> V,
163    descriptor_index: usize,
164    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
165}
166
167impl<S, V> Debug for ListRoundRobinPhase<S, V>
168where
169    S: PlanningSolution,
170    V: Copy + PartialEq + Eq + Hash + Send + Sync + 'static,
171{
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.debug_struct("ListRoundRobinPhase").finish()
174    }
175}
176
177impl<S, V, D, ProgressCb> Phase<S, D, ProgressCb> for ListRoundRobinPhase<S, V>
178where
179    S: PlanningSolution,
180    V: Copy + PartialEq + Eq + Hash + Send + Sync + Debug + 'static,
181    D: solverforge_scoring::Director<S>,
182    ProgressCb: ProgressCallback<S>,
183{
184    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
185        let mut phase_scope = PhaseScope::new(solver_scope, 0);
186
187        let solution = phase_scope.score_director().working_solution();
188        let n_elements = (self.element_count)(solution);
189        let n_entities = (self.entity_count)(solution);
190
191        if n_entities == 0 || n_elements == 0 {
192            let _score = phase_scope.score_director_mut().calculate_score();
193            phase_scope.update_best_solution();
194            return;
195        }
196
197        let assigned: Vec<V> = (self.get_assigned)(phase_scope.score_director().working_solution());
198        if assigned.len() >= n_elements {
199            let _score = phase_scope.score_director_mut().calculate_score();
200            phase_scope.update_best_solution();
201            return;
202        }
203
204        let assigned_set: std::collections::HashSet<V> = assigned.into_iter().collect();
205        let mut entity_idx = 0;
206
207        for elem_idx in 0..n_elements {
208            if phase_scope
209                .solver_scope_mut()
210                .should_terminate_construction()
211            {
212                break;
213            }
214
215            let element =
216                (self.index_to_element)(phase_scope.score_director().working_solution(), elem_idx);
217            if assigned_set.contains(&element) {
218                continue;
219            }
220
221            let mut step_scope = StepScope::new(&mut phase_scope);
222
223            {
224                let sd = step_scope.score_director_mut();
225                let insert_pos = (self.list_len)(sd.working_solution(), entity_idx);
226                sd.before_variable_changed(self.descriptor_index, entity_idx);
227                (self.list_insert)(sd.working_solution_mut(), entity_idx, insert_pos, element);
228                sd.after_variable_changed(self.descriptor_index, entity_idx);
229            }
230
231            let step_score = step_scope.calculate_score();
232            step_scope.set_step_score(step_score);
233            step_scope.complete();
234
235            entity_idx = (entity_idx + 1) % n_entities;
236        }
237
238        phase_scope.update_best_solution();
239    }
240
241    fn phase_type_name(&self) -> &'static str {
242        "ListRoundRobin"
243    }
244}
245
246pub struct ConstructionArgs<S, V> {
247    pub element_count: fn(&S) -> usize,
248    pub assigned_elements: fn(&S) -> Vec<V>,
249    pub entity_count: fn(&S) -> usize,
250    pub list_len: fn(&S, usize) -> usize,
251    pub list_insert: fn(&mut S, usize, usize, V),
252    pub list_remove: fn(&mut S, usize, usize) -> V,
253    pub index_to_element: fn(&S, usize) -> V,
254    pub descriptor_index: usize,
255    pub entity_type_name: &'static str,
256    pub variable_name: &'static str,
257    pub depot_fn: Option<fn(&S) -> usize>,
258    pub distance_fn: Option<fn(&S, usize, usize) -> i64>,
259    pub element_load_fn: Option<fn(&S, usize) -> i64>,
260    pub capacity_fn: Option<fn(&S) -> i64>,
261    pub assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
262    pub merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
263    pub k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
264    pub k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
265    pub k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
266    pub k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
267    pub k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
268}
269
270impl<S, V> Clone for ConstructionArgs<S, V> {
271    fn clone(&self) -> Self {
272        *self
273    }
274}
275
276impl<S, V> Copy for ConstructionArgs<S, V> {}
277
278fn list_work_remaining<S, V>(args: &ConstructionArgs<S, V>, solution: &S) -> bool
279where
280    S: PlanningSolution,
281    V: Copy + PartialEq + Eq + Hash + Send + Sync + 'static,
282{
283    (args.assigned_elements)(solution).len() < (args.element_count)(solution)
284}
285
286fn has_explicit_target(config: &ConstructionHeuristicConfig) -> bool {
287    config.target.variable_name.is_some() || config.target.entity_class.is_some()
288}
289
290fn is_list_only_heuristic(heuristic: ConstructionHeuristicType) -> bool {
291    matches!(
292        heuristic,
293        ConstructionHeuristicType::ListRoundRobin
294            | ConstructionHeuristicType::ListCheapestInsertion
295            | ConstructionHeuristicType::ListRegretInsertion
296            | ConstructionHeuristicType::ListClarkeWright
297            | ConstructionHeuristicType::ListKOpt
298    )
299}
300
301fn is_standard_only_heuristic(heuristic: ConstructionHeuristicType) -> bool {
302    matches!(
303        heuristic,
304        ConstructionHeuristicType::FirstFitDecreasing
305            | ConstructionHeuristicType::WeakestFit
306            | ConstructionHeuristicType::WeakestFitDecreasing
307            | ConstructionHeuristicType::StrongestFit
308            | ConstructionHeuristicType::StrongestFitDecreasing
309            | ConstructionHeuristicType::AllocateEntityFromQueue
310            | ConstructionHeuristicType::AllocateToValueFromQueue
311    )
312}
313
314fn list_target_matches<S, V>(
315    config: &ConstructionHeuristicConfig,
316    list_construction: Option<&ConstructionArgs<S, V>>,
317) -> bool
318where
319    S: PlanningSolution,
320    V: Copy + PartialEq + Eq + Hash + Send + Sync + 'static,
321{
322    if !has_explicit_target(config) {
323        return false;
324    }
325
326    let Some(list_construction) = list_construction else {
327        return false;
328    };
329
330    config
331        .target
332        .variable_name
333        .as_deref()
334        .is_none_or(|name| name == list_construction.variable_name)
335        && config
336            .target
337            .entity_class
338            .as_deref()
339            .is_none_or(|name| name == list_construction.entity_type_name)
340}
341
342fn normalize_list_construction_config(
343    config: Option<&ConstructionHeuristicConfig>,
344) -> Option<ConstructionHeuristicConfig> {
345    let mut config = config.cloned()?;
346    config.construction_heuristic_type = match config.construction_heuristic_type {
347        ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
348            ConstructionHeuristicType::ListCheapestInsertion
349        }
350        other => other,
351    };
352    Some(config)
353}
354
355fn build_list_construction<S, V>(
356    config: Option<&ConstructionHeuristicConfig>,
357    args: &ConstructionArgs<S, V>,
358) -> ListConstruction<S, V>
359where
360    S: PlanningSolution,
361    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
362{
363    let Some((ch_type, k)) = config.map(|cfg| (cfg.construction_heuristic_type, cfg.k)) else {
364        return ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
365            args.element_count,
366            args.assigned_elements,
367            args.entity_count,
368            args.list_len,
369            args.list_insert,
370            args.list_remove,
371            args.index_to_element,
372            args.descriptor_index,
373        ));
374    };
375
376    match ch_type {
377        ConstructionHeuristicType::ListRoundRobin => {
378            ListConstruction::RoundRobin(ListRoundRobinPhase {
379                element_count: args.element_count,
380                get_assigned: args.assigned_elements,
381                entity_count: args.entity_count,
382                list_len: args.list_len,
383                list_insert: args.list_insert,
384                index_to_element: args.index_to_element,
385                descriptor_index: args.descriptor_index,
386                _phantom: PhantomData,
387            })
388        }
389        ConstructionHeuristicType::ListRegretInsertion => {
390            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
391                args.element_count,
392                args.assigned_elements,
393                args.entity_count,
394                args.list_len,
395                args.list_insert,
396                args.list_remove,
397                args.index_to_element,
398                args.descriptor_index,
399            ))
400        }
401        ConstructionHeuristicType::ListClarkeWright => {
402            match (
403                args.depot_fn,
404                args.distance_fn,
405                args.element_load_fn,
406                args.capacity_fn,
407                args.assign_route_fn,
408            ) {
409                (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
410                    ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
411                        args.element_count,
412                        args.assigned_elements,
413                        args.entity_count,
414                        args.list_len,
415                        assign,
416                        args.index_to_element,
417                        depot,
418                        dist,
419                        load,
420                        cap,
421                        args.merge_feasible_fn,
422                        args.descriptor_index,
423                    ))
424                }
425                _ => {
426                    panic!(
427                        "list_clarke_wright requires depot_fn, distance_fn, element_load_fn, capacity_fn, and assign_route_fn"
428                    );
429                }
430            }
431        }
432        ConstructionHeuristicType::ListKOpt => match (
433            args.k_opt_get_route,
434            args.k_opt_set_route,
435            args.k_opt_depot_fn,
436            args.k_opt_distance_fn,
437        ) {
438            (Some(get_route), Some(set_route), Some(ko_depot), Some(ko_dist)) => {
439                ListConstruction::KOpt(ListKOptPhase::new(
440                    k,
441                    args.entity_count,
442                    get_route,
443                    set_route,
444                    ko_depot,
445                    ko_dist,
446                    args.k_opt_feasible_fn,
447                    args.descriptor_index,
448                ))
449            }
450            _ => {
451                panic!(
452                    "list_k_opt requires k_opt_get_route, k_opt_set_route, k_opt_depot_fn, and k_opt_distance_fn"
453                );
454            }
455        },
456        ConstructionHeuristicType::ListCheapestInsertion => {
457            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
458                args.element_count,
459                args.assigned_elements,
460                args.entity_count,
461                args.list_len,
462                args.list_insert,
463                args.list_remove,
464                args.index_to_element,
465                args.descriptor_index,
466            ))
467        }
468        ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
469            panic!(
470                "generic construction heuristic {:?} must be normalized before list construction",
471                ch_type
472            );
473        }
474        ConstructionHeuristicType::FirstFitDecreasing
475        | ConstructionHeuristicType::WeakestFit
476        | ConstructionHeuristicType::WeakestFitDecreasing
477        | ConstructionHeuristicType::StrongestFit
478        | ConstructionHeuristicType::StrongestFitDecreasing
479        | ConstructionHeuristicType::AllocateEntityFromQueue
480        | ConstructionHeuristicType::AllocateToValueFromQueue => {
481            panic!(
482                "standard construction heuristic {:?} configured against a list variable",
483                ch_type
484            );
485        }
486    }
487}
488
489pub struct Construction<S, V>
490where
491    S: PlanningSolution,
492    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + 'static,
493{
494    config: Option<ConstructionHeuristicConfig>,
495    descriptor: SolutionDescriptor,
496    list_construction: Option<ConstructionArgs<S, V>>,
497}
498
499impl<S, V> Construction<S, V>
500where
501    S: PlanningSolution,
502    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + 'static,
503{
504    fn new(
505        config: Option<ConstructionHeuristicConfig>,
506        descriptor: SolutionDescriptor,
507        list_construction: Option<ConstructionArgs<S, V>>,
508    ) -> Self {
509        Self {
510            config,
511            descriptor,
512            list_construction,
513        }
514    }
515}
516
517impl<S, V> Debug for Construction<S, V>
518where
519    S: PlanningSolution,
520    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
521{
522    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523        f.debug_struct("Construction")
524            .field("config", &self.config)
525            .field("has_list_construction", &self.list_construction.is_some())
526            .finish()
527    }
528}
529
530impl<S, V, D, ProgressCb> Phase<S, D, ProgressCb> for Construction<S, V>
531where
532    S: PlanningSolution + 'static,
533    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
534    D: solverforge_scoring::Director<S>,
535    ProgressCb: ProgressCallback<S>,
536{
537    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
538        let config = self.config.as_ref();
539        let explicit_target = config.is_some_and(has_explicit_target);
540        let entity_class = config.and_then(|cfg| cfg.target.entity_class.as_deref());
541        let variable_name = config.and_then(|cfg| cfg.target.variable_name.as_deref());
542        let standard_matches = config.is_some_and(|_| {
543            standard_target_matches(&self.descriptor, entity_class, variable_name)
544        });
545        let list_matches =
546            config.is_some_and(|cfg| list_target_matches(cfg, self.list_construction.as_ref()));
547
548        if let Some(cfg) = config {
549            if explicit_target && !standard_matches && !list_matches {
550                panic!(
551                    "construction heuristic matched no planning variables for entity_class={:?} variable_name={:?}",
552                    cfg.target.entity_class,
553                    cfg.target.variable_name
554                );
555            }
556
557            let heuristic = cfg.construction_heuristic_type;
558            if is_list_only_heuristic(heuristic) {
559                assert!(
560                    self.list_construction.is_some(),
561                    "list construction heuristic {:?} configured against a solution with no planning list variable",
562                    heuristic
563                );
564                assert!(
565                    !explicit_target || list_matches,
566                    "list construction heuristic {:?} does not match the targeted planning list variable for entity_class={:?} variable_name={:?}",
567                    heuristic,
568                    cfg.target.entity_class,
569                    cfg.target.variable_name
570                );
571                self.solve_list(solver_scope);
572                return;
573            }
574
575            if is_standard_only_heuristic(heuristic) {
576                assert!(
577                    !explicit_target || standard_matches,
578                    "standard construction heuristic {:?} does not match targeted standard planning variables for entity_class={:?} variable_name={:?}",
579                    heuristic,
580                    cfg.target.entity_class,
581                    cfg.target.variable_name
582                );
583                build_descriptor_construction(Some(cfg), &self.descriptor).solve(solver_scope);
584                return;
585            }
586        }
587
588        if self.list_construction.is_none() {
589            build_descriptor_construction(config, &self.descriptor).solve(solver_scope);
590            return;
591        }
592
593        let standard_remaining = standard_work_remaining(
594            &self.descriptor,
595            if explicit_target { entity_class } else { None },
596            if explicit_target { variable_name } else { None },
597            solver_scope.working_solution(),
598        );
599        let list_remaining = self
600            .list_construction
601            .as_ref()
602            .map(|args| {
603                (!explicit_target || list_matches)
604                    && list_work_remaining(args, solver_scope.working_solution())
605            })
606            .unwrap_or(false);
607
608        if standard_remaining {
609            build_descriptor_construction(config, &self.descriptor).solve(solver_scope);
610        }
611        if list_remaining {
612            self.solve_list(solver_scope);
613        }
614    }
615
616    fn phase_type_name(&self) -> &'static str {
617        "Construction"
618    }
619}
620
621impl<S, V> Construction<S, V>
622where
623    S: PlanningSolution + 'static,
624    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
625{
626    fn solve_list<D, ProgressCb>(&self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>)
627    where
628        D: solverforge_scoring::Director<S>,
629        ProgressCb: ProgressCallback<S>,
630    {
631        let Some(args) = self.list_construction.as_ref() else {
632            panic!("list construction configured against a scalar-only context");
633        };
634        let normalized = normalize_list_construction_config(self.config.as_ref());
635        build_list_construction(normalized.as_ref(), args).solve(solver_scope);
636    }
637}
638
639pub enum RuntimePhase<C, LS, VND> {
640    Construction(C),
641    LocalSearch(LS),
642    Vnd(VND),
643}
644
645impl<C, LS, VND> Debug for RuntimePhase<C, LS, VND>
646where
647    C: Debug,
648    LS: Debug,
649    VND: Debug,
650{
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        match self {
653            Self::Construction(phase) => write!(f, "RuntimePhase::Construction({phase:?})"),
654            Self::LocalSearch(phase) => write!(f, "RuntimePhase::LocalSearch({phase:?})"),
655            Self::Vnd(phase) => write!(f, "RuntimePhase::Vnd({phase:?})"),
656        }
657    }
658}
659
660impl<S, D, ProgressCb, C, LS, VND> Phase<S, D, ProgressCb> for RuntimePhase<C, LS, VND>
661where
662    S: PlanningSolution,
663    D: solverforge_scoring::Director<S>,
664    ProgressCb: ProgressCallback<S>,
665    C: Phase<S, D, ProgressCb> + Debug,
666    LS: Phase<S, D, ProgressCb> + Debug,
667    VND: Phase<S, D, ProgressCb> + Debug,
668{
669    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
670        match self {
671            Self::Construction(phase) => phase.solve(solver_scope),
672            Self::LocalSearch(phase) => phase.solve(solver_scope),
673            Self::Vnd(phase) => phase.solve(solver_scope),
674        }
675    }
676
677    fn phase_type_name(&self) -> &'static str {
678        "RuntimePhase"
679    }
680}
681
682pub fn build_phases<S, V, DM, IDM>(
683    config: &SolverConfig,
684    descriptor: &SolutionDescriptor,
685    model: &ModelContext<S, V, DM, IDM>,
686    list_construction: Option<ConstructionArgs<S, V>>,
687) -> PhaseSequence<RuntimePhase<Construction<S, V>, LocalSearch<S, V, DM, IDM>, Vnd<S, V, DM, IDM>>>
688where
689    S: PlanningSolution + 'static,
690    S::Score: Score + ParseableScore,
691    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
692    DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
693    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
694{
695    let mut phases = Vec::new();
696
697    if config.phases.is_empty() {
698        phases.push(RuntimePhase::Construction(Construction::new(
699            None,
700            descriptor.clone(),
701            list_construction,
702        )));
703        phases.push(RuntimePhase::LocalSearch(build_local_search(
704            None,
705            model,
706            config.random_seed,
707        )));
708        return PhaseSequence::new(phases);
709    }
710
711    for phase in &config.phases {
712        match phase {
713            PhaseConfig::ConstructionHeuristic(ch) => {
714                phases.push(RuntimePhase::Construction(Construction::new(
715                    Some(ch.clone()),
716                    descriptor.clone(),
717                    list_construction,
718                )));
719            }
720            PhaseConfig::LocalSearch(ls) => {
721                phases.push(RuntimePhase::LocalSearch(build_local_search(
722                    Some(ls),
723                    model,
724                    config.random_seed,
725                )));
726            }
727            PhaseConfig::Vnd(vnd) => {
728                phases.push(RuntimePhase::Vnd(build_vnd(vnd, model, config.random_seed)));
729            }
730            _ => {
731                panic!("unsupported phase in the runtime");
732            }
733        }
734    }
735
736    PhaseSequence::new(phases)
737}