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: &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    config
327        .target
328        .variable_name
329        .as_deref()
330        .is_none_or(|name| name == list_construction.variable_name)
331        && config
332            .target
333            .entity_class
334            .as_deref()
335            .is_none_or(|name| name == list_construction.entity_type_name)
336}
337
338fn matching_list_construction<S, V>(
339    config: Option<&ConstructionHeuristicConfig>,
340    list_construction: &[ConstructionArgs<S, V>],
341) -> Vec<ConstructionArgs<S, V>>
342where
343    S: PlanningSolution,
344    V: Copy + PartialEq + Eq + Hash + Send + Sync + 'static,
345{
346    let Some(config) = config else {
347        return list_construction.to_vec();
348    };
349
350    if !has_explicit_target(config) {
351        return list_construction.to_vec();
352    }
353
354    list_construction
355        .iter()
356        .copied()
357        .filter(|args| list_target_matches(config, args))
358        .collect()
359}
360
361fn normalize_list_construction_config(
362    config: Option<&ConstructionHeuristicConfig>,
363) -> Option<ConstructionHeuristicConfig> {
364    let mut config = config.cloned()?;
365    config.construction_heuristic_type = match config.construction_heuristic_type {
366        ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
367            ConstructionHeuristicType::ListCheapestInsertion
368        }
369        other => other,
370    };
371    Some(config)
372}
373
374fn build_list_construction<S, V>(
375    config: Option<&ConstructionHeuristicConfig>,
376    args: &ConstructionArgs<S, V>,
377) -> ListConstruction<S, V>
378where
379    S: PlanningSolution,
380    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
381{
382    let Some((ch_type, k)) = config.map(|cfg| (cfg.construction_heuristic_type, cfg.k)) else {
383        return ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
384            args.element_count,
385            args.assigned_elements,
386            args.entity_count,
387            args.list_len,
388            args.list_insert,
389            args.list_remove,
390            args.index_to_element,
391            args.descriptor_index,
392        ));
393    };
394
395    match ch_type {
396        ConstructionHeuristicType::ListRoundRobin => {
397            ListConstruction::RoundRobin(ListRoundRobinPhase {
398                element_count: args.element_count,
399                get_assigned: args.assigned_elements,
400                entity_count: args.entity_count,
401                list_len: args.list_len,
402                list_insert: args.list_insert,
403                index_to_element: args.index_to_element,
404                descriptor_index: args.descriptor_index,
405                _phantom: PhantomData,
406            })
407        }
408        ConstructionHeuristicType::ListRegretInsertion => {
409            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
410                args.element_count,
411                args.assigned_elements,
412                args.entity_count,
413                args.list_len,
414                args.list_insert,
415                args.list_remove,
416                args.index_to_element,
417                args.descriptor_index,
418            ))
419        }
420        ConstructionHeuristicType::ListClarkeWright => {
421            match (
422                args.depot_fn,
423                args.distance_fn,
424                args.element_load_fn,
425                args.capacity_fn,
426                args.assign_route_fn,
427            ) {
428                (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
429                    ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
430                        args.element_count,
431                        args.assigned_elements,
432                        args.entity_count,
433                        args.list_len,
434                        assign,
435                        args.index_to_element,
436                        depot,
437                        dist,
438                        load,
439                        cap,
440                        args.merge_feasible_fn,
441                        args.descriptor_index,
442                    ))
443                }
444                _ => {
445                    panic!(
446                        "list_clarke_wright requires depot_fn, distance_fn, element_load_fn, capacity_fn, and assign_route_fn"
447                    );
448                }
449            }
450        }
451        ConstructionHeuristicType::ListKOpt => match (
452            args.k_opt_get_route,
453            args.k_opt_set_route,
454            args.k_opt_depot_fn,
455            args.k_opt_distance_fn,
456        ) {
457            (Some(get_route), Some(set_route), Some(ko_depot), Some(ko_dist)) => {
458                ListConstruction::KOpt(ListKOptPhase::new(
459                    k,
460                    args.entity_count,
461                    get_route,
462                    set_route,
463                    ko_depot,
464                    ko_dist,
465                    args.k_opt_feasible_fn,
466                    args.descriptor_index,
467                ))
468            }
469            _ => {
470                panic!(
471                    "list_k_opt requires k_opt_get_route, k_opt_set_route, k_opt_depot_fn, and k_opt_distance_fn"
472                );
473            }
474        },
475        ConstructionHeuristicType::ListCheapestInsertion => {
476            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
477                args.element_count,
478                args.assigned_elements,
479                args.entity_count,
480                args.list_len,
481                args.list_insert,
482                args.list_remove,
483                args.index_to_element,
484                args.descriptor_index,
485            ))
486        }
487        ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
488            panic!(
489                "generic construction heuristic {:?} must be normalized before list construction",
490                ch_type
491            );
492        }
493        ConstructionHeuristicType::FirstFitDecreasing
494        | ConstructionHeuristicType::WeakestFit
495        | ConstructionHeuristicType::WeakestFitDecreasing
496        | ConstructionHeuristicType::StrongestFit
497        | ConstructionHeuristicType::StrongestFitDecreasing
498        | ConstructionHeuristicType::AllocateEntityFromQueue
499        | ConstructionHeuristicType::AllocateToValueFromQueue => {
500            panic!(
501                "standard construction heuristic {:?} configured against a list variable",
502                ch_type
503            );
504        }
505    }
506}
507
508pub struct Construction<S, V>
509where
510    S: PlanningSolution,
511    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + 'static,
512{
513    config: Option<ConstructionHeuristicConfig>,
514    descriptor: SolutionDescriptor,
515    list_construction: Vec<ConstructionArgs<S, V>>,
516}
517
518impl<S, V> Construction<S, V>
519where
520    S: PlanningSolution,
521    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + 'static,
522{
523    fn new(
524        config: Option<ConstructionHeuristicConfig>,
525        descriptor: SolutionDescriptor,
526        list_construction: Vec<ConstructionArgs<S, V>>,
527    ) -> Self {
528        Self {
529            config,
530            descriptor,
531            list_construction,
532        }
533    }
534}
535
536impl<S, V> Debug for Construction<S, V>
537where
538    S: PlanningSolution,
539    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
540{
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        f.debug_struct("Construction")
543            .field("config", &self.config)
544            .field("list_construction_count", &self.list_construction.len())
545            .finish()
546    }
547}
548
549impl<S, V, D, ProgressCb> Phase<S, D, ProgressCb> for Construction<S, V>
550where
551    S: PlanningSolution + 'static,
552    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
553    D: solverforge_scoring::Director<S>,
554    ProgressCb: ProgressCallback<S>,
555{
556    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
557        let config = self.config.as_ref();
558        let explicit_target = config.is_some_and(has_explicit_target);
559        let entity_class = config.and_then(|cfg| cfg.target.entity_class.as_deref());
560        let variable_name = config.and_then(|cfg| cfg.target.variable_name.as_deref());
561        let standard_matches = config.is_some_and(|_| {
562            standard_target_matches(&self.descriptor, entity_class, variable_name)
563        });
564        let matching_list_construction =
565            matching_list_construction(config, &self.list_construction);
566
567        if let Some(cfg) = config {
568            if explicit_target && !standard_matches && matching_list_construction.is_empty() {
569                panic!(
570                    "construction heuristic matched no planning variables for entity_class={:?} variable_name={:?}",
571                    cfg.target.entity_class,
572                    cfg.target.variable_name
573                );
574            }
575
576            let heuristic = cfg.construction_heuristic_type;
577            if is_list_only_heuristic(heuristic) {
578                assert!(
579                    !self.list_construction.is_empty(),
580                    "list construction heuristic {:?} configured against a solution with no planning list variable",
581                    heuristic
582                );
583                assert!(
584                    !explicit_target || !matching_list_construction.is_empty(),
585                    "list construction heuristic {:?} does not match the targeted planning list variable for entity_class={:?} variable_name={:?}",
586                    heuristic,
587                    cfg.target.entity_class,
588                    cfg.target.variable_name
589                );
590                self.solve_list(solver_scope, &matching_list_construction);
591                return;
592            }
593
594            if is_standard_only_heuristic(heuristic) {
595                assert!(
596                    !explicit_target || standard_matches,
597                    "standard construction heuristic {:?} does not match targeted standard planning variables for entity_class={:?} variable_name={:?}",
598                    heuristic,
599                    cfg.target.entity_class,
600                    cfg.target.variable_name
601                );
602                build_descriptor_construction(Some(cfg), &self.descriptor).solve(solver_scope);
603                return;
604            }
605        }
606
607        if self.list_construction.is_empty() {
608            build_descriptor_construction(config, &self.descriptor).solve(solver_scope);
609            return;
610        }
611
612        let standard_remaining = standard_work_remaining(
613            &self.descriptor,
614            if explicit_target { entity_class } else { None },
615            if explicit_target { variable_name } else { None },
616            solver_scope.working_solution(),
617        );
618        let list_remaining = matching_list_construction
619            .iter()
620            .any(|args| list_work_remaining(args, solver_scope.working_solution()));
621
622        if standard_remaining {
623            build_descriptor_construction(config, &self.descriptor).solve(solver_scope);
624        }
625        if list_remaining {
626            self.solve_list(solver_scope, &matching_list_construction);
627        }
628    }
629
630    fn phase_type_name(&self) -> &'static str {
631        "Construction"
632    }
633}
634
635impl<S, V> Construction<S, V>
636where
637    S: PlanningSolution + 'static,
638    V: Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
639{
640    fn solve_list<D, ProgressCb>(
641        &self,
642        solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
643        list_construction: &[ConstructionArgs<S, V>],
644    ) where
645        D: solverforge_scoring::Director<S>,
646        ProgressCb: ProgressCallback<S>,
647    {
648        if list_construction.is_empty() {
649            panic!("list construction configured against a scalar-only context");
650        }
651        let normalized = normalize_list_construction_config(self.config.as_ref());
652        for args in list_construction {
653            if !list_work_remaining(args, solver_scope.working_solution()) {
654                continue;
655            }
656            build_list_construction(normalized.as_ref(), args).solve(solver_scope);
657        }
658    }
659}
660
661pub enum RuntimePhase<C, LS, VND> {
662    Construction(C),
663    LocalSearch(LS),
664    Vnd(VND),
665}
666
667impl<C, LS, VND> Debug for RuntimePhase<C, LS, VND>
668where
669    C: Debug,
670    LS: Debug,
671    VND: Debug,
672{
673    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674        match self {
675            Self::Construction(phase) => write!(f, "RuntimePhase::Construction({phase:?})"),
676            Self::LocalSearch(phase) => write!(f, "RuntimePhase::LocalSearch({phase:?})"),
677            Self::Vnd(phase) => write!(f, "RuntimePhase::Vnd({phase:?})"),
678        }
679    }
680}
681
682impl<S, D, ProgressCb, C, LS, VND> Phase<S, D, ProgressCb> for RuntimePhase<C, LS, VND>
683where
684    S: PlanningSolution,
685    D: solverforge_scoring::Director<S>,
686    ProgressCb: ProgressCallback<S>,
687    C: Phase<S, D, ProgressCb> + Debug,
688    LS: Phase<S, D, ProgressCb> + Debug,
689    VND: Phase<S, D, ProgressCb> + Debug,
690{
691    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
692        match self {
693            Self::Construction(phase) => phase.solve(solver_scope),
694            Self::LocalSearch(phase) => phase.solve(solver_scope),
695            Self::Vnd(phase) => phase.solve(solver_scope),
696        }
697    }
698
699    fn phase_type_name(&self) -> &'static str {
700        "RuntimePhase"
701    }
702}
703
704pub fn build_phases<S, V, DM, IDM>(
705    config: &SolverConfig,
706    descriptor: &SolutionDescriptor,
707    model: &ModelContext<S, V, DM, IDM>,
708    list_construction: Vec<ConstructionArgs<S, V>>,
709) -> PhaseSequence<RuntimePhase<Construction<S, V>, LocalSearch<S, V, DM, IDM>, Vnd<S, V, DM, IDM>>>
710where
711    S: PlanningSolution + 'static,
712    S::Score: Score + ParseableScore,
713    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
714    DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
715    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
716{
717    let mut phases = Vec::new();
718
719    if config.phases.is_empty() {
720        phases.push(RuntimePhase::Construction(Construction::new(
721            None,
722            descriptor.clone(),
723            list_construction.clone(),
724        )));
725        phases.push(RuntimePhase::LocalSearch(build_local_search(
726            None,
727            model,
728            config.random_seed,
729        )));
730        return PhaseSequence::new(phases);
731    }
732
733    for phase in &config.phases {
734        match phase {
735            PhaseConfig::ConstructionHeuristic(ch) => {
736                phases.push(RuntimePhase::Construction(Construction::new(
737                    Some(ch.clone()),
738                    descriptor.clone(),
739                    list_construction.clone(),
740                )));
741            }
742            PhaseConfig::LocalSearch(ls) => {
743                phases.push(RuntimePhase::LocalSearch(build_local_search(
744                    Some(ls),
745                    model,
746                    config.random_seed,
747                )));
748            }
749            PhaseConfig::Vnd(vnd) => {
750                phases.push(RuntimePhase::Vnd(build_vnd(vnd, model, config.random_seed)));
751            }
752            _ => {
753                panic!("unsupported phase in the runtime");
754            }
755        }
756    }
757
758    PhaseSequence::new(phases)
759}