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::{ConstructionHeuristicConfig, PhaseConfig, SolverConfig};
6use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
7use solverforge_core::score::{ParseableScore, Score};
8
9use crate::builder::{build_local_search, LocalSearchStrategy, RuntimeModel};
10use crate::descriptor::{
11    build_descriptor_construction_from_bindings, scalar_work_remaining_with_frontier,
12};
13use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
14use crate::manager::solve_specialized_list_construction;
15use crate::phase::construction::{select_construction_capabilities, ConstructionRoute};
16use crate::phase::{sequence::PhaseSequence, Phase};
17use crate::scope::{ProgressCallback, SolverScope};
18
19#[path = "runtime/defaults.rs"]
20mod defaults;
21
22#[cfg(test)]
23mod tests;
24
25#[cfg(test)]
26mod list_tests;
27
28pub struct ListVariableMetadata<S, DM, IDM> {
29    pub cross_distance_meter: DM,
30    pub intra_distance_meter: IDM,
31    pub route_get_fn: Option<fn(&S, usize) -> Vec<usize>>,
32    pub route_set_fn: Option<fn(&mut S, usize, Vec<usize>)>,
33    pub route_depot_fn: Option<fn(&S, usize) -> usize>,
34    pub route_metric_class_fn: Option<fn(&S, usize) -> usize>,
35    pub route_distance_fn: Option<fn(&S, usize, usize, usize) -> i64>,
36    pub route_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
37    _phantom: PhantomData<fn() -> S>,
38}
39
40pub trait ListVariableEntity<S> {
41    type CrossDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug;
42    type IntraDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug + 'static;
43
44    const HAS_LIST_VARIABLE: bool;
45    const LIST_VARIABLE_NAME: &'static str;
46    const LIST_ELEMENT_SOURCE: Option<&'static str>;
47
48    fn list_field(entity: &Self) -> &[usize];
49    fn list_field_mut(entity: &mut Self) -> &mut Vec<usize>;
50    fn list_metadata() -> ListVariableMetadata<S, Self::CrossDistanceMeter, Self::IntraDistanceMeter>;
51}
52
53impl<S, DM, IDM> ListVariableMetadata<S, DM, IDM> {
54    #[allow(clippy::too_many_arguments)]
55    pub fn new(
56        cross_distance_meter: DM,
57        intra_distance_meter: IDM,
58        route_get_fn: Option<fn(&S, usize) -> Vec<usize>>,
59        route_set_fn: Option<fn(&mut S, usize, Vec<usize>)>,
60        route_depot_fn: Option<fn(&S, usize) -> usize>,
61        route_metric_class_fn: Option<fn(&S, usize) -> usize>,
62        route_distance_fn: Option<fn(&S, usize, usize, usize) -> i64>,
63        route_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
64    ) -> Self {
65        Self {
66            cross_distance_meter,
67            intra_distance_meter,
68            route_get_fn,
69            route_set_fn,
70            route_depot_fn,
71            route_metric_class_fn,
72            route_distance_fn,
73            route_feasible_fn,
74            _phantom: PhantomData,
75        }
76    }
77}
78
79pub struct Construction<S, V, DM, IDM>
80where
81    S: PlanningSolution,
82    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
83    DM: Clone + Debug + Send + 'static,
84    IDM: Clone + Debug + Send + 'static,
85{
86    config: Option<ConstructionHeuristicConfig>,
87    descriptor: SolutionDescriptor,
88    model: RuntimeModel<S, V, DM, IDM>,
89}
90
91impl<S, V, DM, IDM> Construction<S, V, DM, IDM>
92where
93    S: PlanningSolution,
94    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
95    DM: Clone + Debug + Send + 'static,
96    IDM: Clone + Debug + Send + 'static,
97{
98    fn new(
99        config: Option<ConstructionHeuristicConfig>,
100        descriptor: SolutionDescriptor,
101        model: RuntimeModel<S, V, DM, IDM>,
102    ) -> Self {
103        Self {
104            config,
105            descriptor,
106            model,
107        }
108    }
109
110    fn solve_list<D, ProgressCb>(
111        &self,
112        config: &ConstructionHeuristicConfig,
113        solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
114        list_variables: &[crate::builder::ListVariableSlot<S, V, DM, IDM>],
115    ) -> bool
116    where
117        D: solverforge_scoring::Director<S>,
118        ProgressCb: ProgressCallback<S>,
119    {
120        if list_variables.is_empty() {
121            panic!("list construction configured against a scalar-only model");
122        }
123
124        solve_specialized_list_construction(
125            config.construction_heuristic_type,
126            config.k,
127            solver_scope,
128            list_variables,
129        )
130    }
131
132    pub(super) fn solve_configured<D, ProgressCb>(
133        &self,
134        config: Option<&ConstructionHeuristicConfig>,
135        solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
136        required_only: bool,
137    ) -> bool
138    where
139        S: PlanningSolution + 'static,
140        S::Score: Score + Copy,
141        DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
142        IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
143        D: solverforge_scoring::Director<S>,
144        ProgressCb: ProgressCallback<S>,
145    {
146        let capabilities = select_construction_capabilities(config, &self.descriptor, &self.model);
147
148        match capabilities.route {
149            ConstructionRoute::SpecializedList => {
150                let Some(config) = config else {
151                    panic!("specialized list construction requires explicit configuration");
152                };
153                self.solve_list(config, solver_scope, &capabilities.list_variables)
154            }
155            ConstructionRoute::Descriptor => {
156                let scalar_remaining = scalar_work_remaining_with_frontier(
157                    &self.descriptor,
158                    solver_scope.construction_frontier(),
159                    solver_scope.solution_revision(),
160                    capabilities.entity_class.as_deref(),
161                    capabilities.variable_name.as_deref(),
162                    solver_scope.working_solution(),
163                );
164                if scalar_remaining {
165                    build_descriptor_construction_from_bindings(
166                        config,
167                        &self.descriptor,
168                        capabilities.scalar_bindings.clone(),
169                    )
170                    .solve(solver_scope);
171                    true
172                } else {
173                    false
174                }
175            }
176            ConstructionRoute::GroupedScalar => {
177                let Some((group_index, group)) = capabilities.scalar_group.as_ref() else {
178                    unreachable!("grouped scalar route requires a selected scalar group");
179                };
180                if !scalar_group_work_remaining(group, solver_scope.working_solution()) {
181                    return false;
182                }
183                record_scalar_assignment_remaining(group, solver_scope);
184                let mut phase = crate::phase::construction::build_scalar_group_construction(
185                    config,
186                    *group_index,
187                    group.clone(),
188                    capabilities.scalar_bindings.clone(),
189                    required_only,
190                );
191                phase.solve(solver_scope);
192                record_scalar_assignment_remaining(group, solver_scope);
193                true
194            }
195            ConstructionRoute::GenericMixed => {
196                crate::phase::construction::solve_construction(config, &self.model, solver_scope)
197            }
198        }
199    }
200}
201
202fn scalar_group_work_remaining<S>(
203    group: &crate::builder::ScalarGroupBinding<S>,
204    solution: &S,
205) -> bool {
206    if let Some(assignment) = group.assignment() {
207        return assignment.unassigned_count(solution) > 0;
208    }
209    group.members.iter().any(|member| {
210        (0..member.entity_count(solution))
211            .any(|entity_index| member.current_value(solution, entity_index).is_none())
212    })
213}
214
215fn record_scalar_assignment_remaining<S, D, ProgressCb>(
216    group: &crate::builder::ScalarGroupBinding<S>,
217    solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
218) where
219    S: PlanningSolution,
220    D: solverforge_scoring::Director<S>,
221    ProgressCb: ProgressCallback<S>,
222{
223    if let Some(assignment) = group.assignment() {
224        let remaining = assignment.remaining_required_count(solver_scope.working_solution());
225        solver_scope
226            .stats_mut()
227            .record_scalar_assignment_required_remaining(group.group_name, remaining);
228    }
229}
230
231impl<S, V, DM, IDM> Debug for Construction<S, V, DM, IDM>
232where
233    S: PlanningSolution,
234    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
235    DM: Clone + Debug + Send + 'static,
236    IDM: Clone + Debug + Send + 'static,
237{
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.debug_struct("Construction")
240            .field("config", &self.config)
241            .field("variable_count", &self.model.variables().len())
242            .finish()
243    }
244}
245
246impl<S, V, DM, IDM, D, ProgressCb> Phase<S, D, ProgressCb> for Construction<S, V, DM, IDM>
247where
248    S: PlanningSolution + 'static,
249    S::Score: Score + Copy,
250    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
251    DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
252    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
253    D: solverforge_scoring::Director<S>,
254    ProgressCb: ProgressCallback<S>,
255{
256    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
257        let ran_child_phase = match self.config.as_ref() {
258            None => defaults::solve_default_construction(self, solver_scope),
259            Some(config) => self.solve_configured(Some(config), solver_scope, false),
260        };
261        if !ran_child_phase {
262            finalize_noop_construction(solver_scope);
263        }
264    }
265
266    fn phase_type_name(&self) -> &'static str {
267        "Construction"
268    }
269}
270
271fn finalize_noop_construction<S, D, ProgressCb>(
272    solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
273) where
274    S: PlanningSolution,
275    D: solverforge_scoring::Director<S>,
276    ProgressCb: ProgressCallback<S>,
277{
278    let had_best = solver_scope.best_score().is_some();
279    solver_scope.update_best_solution();
280    if had_best {
281        solver_scope.promote_current_solution_on_score_tie();
282    }
283}
284
285pub enum RuntimePhase<C, LS> {
286    Construction(C),
287    LocalSearch(LS),
288}
289
290impl<C, LS> Debug for RuntimePhase<C, LS>
291where
292    C: Debug,
293    LS: Debug,
294{
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        match self {
297            Self::Construction(phase) => write!(f, "RuntimePhase::Construction({phase:?})"),
298            Self::LocalSearch(phase) => write!(f, "RuntimePhase::LocalSearch({phase:?})"),
299        }
300    }
301}
302
303impl<S, D, ProgressCb, C, LS> Phase<S, D, ProgressCb> for RuntimePhase<C, LS>
304where
305    S: PlanningSolution,
306    D: solverforge_scoring::Director<S>,
307    ProgressCb: ProgressCallback<S>,
308    C: Phase<S, D, ProgressCb> + Debug,
309    LS: Phase<S, D, ProgressCb> + Debug,
310{
311    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
312        match self {
313            Self::Construction(phase) => phase.solve(solver_scope),
314            Self::LocalSearch(phase) => phase.solve(solver_scope),
315        }
316    }
317
318    fn phase_type_name(&self) -> &'static str {
319        "RuntimePhase"
320    }
321}
322
323pub fn build_phases<S, V, DM, IDM>(
324    config: &SolverConfig,
325    descriptor: &SolutionDescriptor,
326    model: &RuntimeModel<S, V, DM, IDM>,
327) -> PhaseSequence<RuntimePhase<Construction<S, V, DM, IDM>, LocalSearchStrategy<S, V, DM, IDM>>>
328where
329    S: PlanningSolution + 'static,
330    S::Score: Score + ParseableScore,
331    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
332    DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
333    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
334{
335    let mut phases = Vec::new();
336
337    if config.phases.is_empty() {
338        phases.push(RuntimePhase::Construction(Construction::new(
339            None,
340            descriptor.clone(),
341            model.clone(),
342        )));
343        for phase in
344            crate::builder::search::defaults::default_local_search_phases(model, config.random_seed)
345        {
346            phases.push(RuntimePhase::LocalSearch(phase));
347        }
348        return PhaseSequence::new(phases);
349    }
350
351    for phase in &config.phases {
352        match phase {
353            PhaseConfig::ConstructionHeuristic(ch) => {
354                phases.push(RuntimePhase::Construction(Construction::new(
355                    Some(ch.clone()),
356                    descriptor.clone(),
357                    model.clone(),
358                )));
359            }
360            PhaseConfig::LocalSearch(ls) => {
361                phases.push(RuntimePhase::LocalSearch(build_local_search(
362                    Some(ls),
363                    model,
364                    config.random_seed,
365                )));
366            }
367            PhaseConfig::Custom(custom) => {
368                panic!(
369                    "custom phase `{}` requires a typed solution search function",
370                    custom.name
371                );
372            }
373            PhaseConfig::PartitionedSearch(partitioned) => {
374                let name = partitioned
375                    .partitioner
376                    .as_deref()
377                    .unwrap_or("<missing partitioner>");
378                panic!(
379                    "partitioned_search partitioner `{name}` requires typed partitioner registration"
380                );
381            }
382        }
383    }
384
385    PhaseSequence::new(phases)
386}
387
388#[cfg(test)]
389mod construction_routing_tests {
390    use solverforge_config::ConstructionHeuristicType;
391
392    fn should_use_descriptor_path(
393        heuristic: ConstructionHeuristicType,
394        has_scalar_variables: bool,
395        has_list_variables: bool,
396    ) -> bool {
397        if !has_scalar_variables {
398            return false;
399        }
400
401        match heuristic {
402            ConstructionHeuristicType::ListRoundRobin
403            | ConstructionHeuristicType::ListCheapestInsertion
404            | ConstructionHeuristicType::ListRegretInsertion
405            | ConstructionHeuristicType::ListClarkeWright
406            | ConstructionHeuristicType::ListKOpt => false,
407            ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
408                !has_list_variables
409            }
410            ConstructionHeuristicType::FirstFitDecreasing
411            | ConstructionHeuristicType::WeakestFit
412            | ConstructionHeuristicType::WeakestFitDecreasing
413            | ConstructionHeuristicType::StrongestFit
414            | ConstructionHeuristicType::StrongestFitDecreasing
415            | ConstructionHeuristicType::AllocateEntityFromQueue
416            | ConstructionHeuristicType::AllocateToValueFromQueue => true,
417        }
418    }
419
420    #[test]
421    fn pure_scalar_first_fit_uses_descriptor_path() {
422        assert!(should_use_descriptor_path(
423            ConstructionHeuristicType::FirstFit,
424            true,
425            false,
426        ));
427    }
428
429    #[test]
430    fn pure_scalar_cheapest_insertion_uses_descriptor_path() {
431        assert!(should_use_descriptor_path(
432            ConstructionHeuristicType::CheapestInsertion,
433            true,
434            false,
435        ));
436    }
437
438    #[test]
439    fn mixed_first_fit_keeps_generic_construction_path() {
440        assert!(!should_use_descriptor_path(
441            ConstructionHeuristicType::FirstFit,
442            true,
443            true,
444        ));
445    }
446
447    #[test]
448    fn mixed_cheapest_insertion_keeps_generic_construction_path() {
449        assert!(!should_use_descriptor_path(
450            ConstructionHeuristicType::CheapestInsertion,
451            true,
452            true,
453        ));
454    }
455
456    #[test]
457    fn scalar_only_heuristics_still_route_to_descriptor_path() {
458        assert!(should_use_descriptor_path(
459            ConstructionHeuristicType::StrongestFit,
460            true,
461            true,
462        ));
463    }
464}