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}