Skip to main content

solverforge_solver/
list_solver.rs

1/* List variable solver for routing and scheduling problems.
2
3This module provides `ListSpec` for problems with list variables
4(e.g., vehicle routes, shift schedules). The solver configuration
5(construction type, move selectors, acceptor, forager, termination) is
6driven by `solver.toml`.
7
8Logging levels:
9- **INFO**: Solver start/end, phase summaries, problem scale
10- **DEBUG**: Individual steps with timing and scores
11- **TRACE**: Move evaluation details
12*/
13
14use std::fmt;
15use std::marker::PhantomData;
16use std::sync::atomic::AtomicBool;
17use std::time::Duration;
18
19use solverforge_config::{ConstructionHeuristicType, PhaseConfig, SolverConfig};
20use solverforge_core::domain::PlanningSolution;
21use solverforge_core::score::{ParseableScore, Score};
22use solverforge_scoring::{ConstraintSet, ScoreDirector};
23use tracing::info;
24
25use crate::builder::list_selector::ListLeafSelector;
26use crate::builder::{
27    AcceptorBuilder, AnyAcceptor, AnyForager, ForagerBuilder, ListContext, ListMoveSelectorBuilder,
28};
29use crate::heuristic::r#move::ListMoveImpl;
30use crate::heuristic::selector::decorator::VecUnionSelector;
31use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
32use crate::manager::{
33    ListCheapestInsertionPhase, ListClarkeWrightPhase, ListKOptPhase, ListRegretInsertionPhase,
34};
35use crate::phase::localsearch::{AcceptedCountForager, LateAcceptanceAcceptor, LocalSearchPhase};
36use crate::problem_spec::ProblemSpec;
37use crate::run::AnyTermination;
38use crate::solver::{SolveResult, Solver};
39
40// Type alias for the config-driven list local search phase
41type ConfigListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
42    S,
43    ListMoveImpl<S, V>,
44    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
45    AnyAcceptor<S>,
46    AnyForager<S>,
47>;
48
49// Type alias for the default list local search phase
50type DefaultListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
51    S,
52    ListMoveImpl<S, V>,
53    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
54    LateAcceptanceAcceptor<S>,
55    AcceptedCountForager<S>,
56>;
57
58// Monomorphized local search enum for list solver.
59// Variants intentionally differ in size; this enum is constructed once per solve, not in hot paths.
60#[allow(clippy::large_enum_variant)]
61enum ListLocalSearch<S, V, DM, IDM>
62where
63    S: PlanningSolution,
64    V: Clone + PartialEq + Send + Sync + fmt::Debug + 'static,
65    DM: CrossEntityDistanceMeter<S>,
66    IDM: CrossEntityDistanceMeter<S> + 'static,
67{
68    Default(DefaultListLocalSearch<S, V, DM, IDM>),
69    Config(ConfigListLocalSearch<S, V, DM, IDM>),
70}
71
72// Construction phase enum for list solver.
73enum ListConstruction<S, V>
74where
75    S: PlanningSolution,
76    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
77{
78    CheapestInsertion(ListCheapestInsertionPhase<S, V>),
79    RegretInsertion(ListRegretInsertionPhase<S, V>),
80    ClarkeWright(ListClarkeWrightPhase<S, V>),
81    KOpt(ListKOptPhase<S, V>),
82}
83
84/// Problem specification for list variable problems.
85///
86/// Passed to `run_solver` to provide problem-specific construction and local
87/// search phases for solutions using `#[shadow_variable_updates]` (list variables).
88pub struct ListSpec<S, V, DM, IDM> {
89    // List operation function pointers
90    pub list_len: fn(&S, usize) -> usize,
91    pub list_remove: fn(&mut S, usize, usize) -> Option<V>,
92    pub list_insert: fn(&mut S, usize, usize, V),
93    pub list_get: fn(&S, usize, usize) -> Option<V>,
94    pub list_set: fn(&mut S, usize, usize, V),
95    pub list_reverse: fn(&mut S, usize, usize, usize),
96    pub sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
97    pub sublist_insert: fn(&mut S, usize, usize, Vec<V>),
98    pub ruin_remove: fn(&mut S, usize, usize) -> V,
99    pub ruin_insert: fn(&mut S, usize, usize, V),
100    // Construction function pointers
101    pub element_count: fn(&S) -> usize,
102    pub get_assigned: fn(&S) -> Vec<V>,
103    pub entity_count: fn(&S) -> usize,
104    pub list_remove_for_construction: fn(&mut S, usize, usize) -> V,
105    pub index_to_element: fn(&S, usize) -> V,
106    // Distance meters
107    pub cross_distance_meter: DM,
108    pub intra_distance_meter: IDM,
109    // Clarke-Wright fields (all None if not using Clarke-Wright construction)
110    pub depot_fn: Option<fn(&S) -> usize>,
111    pub distance_fn: Option<fn(&S, usize, usize) -> i64>,
112    pub element_load_fn: Option<fn(&S, usize) -> i64>,
113    pub capacity_fn: Option<fn(&S) -> i64>,
114    pub assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
115    pub merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
116    // KOpt fields (all None if not using KOpt polishing)
117    pub k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
118    pub k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
119    pub k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
120    pub k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
121    pub k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
122    // Metadata
123    pub variable_name: &'static str,
124    pub descriptor_index: usize,
125    pub _phantom: PhantomData<fn() -> S>,
126}
127
128impl<S, V, C, DM, IDM> ProblemSpec<S, C> for ListSpec<S, V, DM, IDM>
129where
130    S: PlanningSolution,
131    S::Score: Score + ParseableScore,
132    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
133    C: ConstraintSet<S, S::Score>,
134    DM: CrossEntityDistanceMeter<S> + Clone,
135    IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
136{
137    fn is_trivial(&self, solution: &S) -> bool {
138        (self.entity_count)(solution) == 0
139    }
140
141    fn default_time_limit_secs(&self) -> u64 {
142        60
143    }
144
145    fn log_scale(&self, solution: &S) {
146        info!(
147            event = "solve_start",
148            entity_count = (self.entity_count)(solution),
149            element_count = (self.element_count)(solution),
150        );
151    }
152
153    fn build_and_solve(
154        self,
155        director: ScoreDirector<S, C>,
156        config: &SolverConfig,
157        time_limit: Duration,
158        termination: AnyTermination<S, ScoreDirector<S, C>>,
159        terminate: Option<&AtomicBool>,
160        callback: impl Fn(&S) + Send + Sync,
161    ) -> SolveResult<S> {
162        let construction = build_list_construction::<S, V>(
163            config,
164            self.element_count,
165            self.get_assigned,
166            self.entity_count,
167            self.list_len,
168            self.list_insert,
169            self.list_remove_for_construction,
170            self.index_to_element,
171            self.descriptor_index,
172            self.depot_fn,
173            self.distance_fn,
174            self.element_load_fn,
175            self.capacity_fn,
176            self.assign_route_fn,
177            self.merge_feasible_fn,
178            self.k_opt_get_route,
179            self.k_opt_set_route,
180            self.k_opt_depot_fn,
181            self.k_opt_distance_fn,
182            self.k_opt_feasible_fn,
183        );
184
185        let ctx = ListContext::new(
186            self.list_len,
187            self.list_remove,
188            self.list_insert,
189            self.list_get,
190            self.list_set,
191            self.list_reverse,
192            self.sublist_remove,
193            self.sublist_insert,
194            self.ruin_remove,
195            self.ruin_insert,
196            self.entity_count,
197            self.cross_distance_meter,
198            self.intra_distance_meter,
199            self.variable_name,
200            self.descriptor_index,
201        );
202
203        let local_search = build_list_local_search::<S, V, DM, IDM>(config, &ctx);
204
205        match (construction, local_search) {
206            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Default(ls)) => {
207                let solver = Solver::new(((), c, ls))
208                    .with_termination(termination)
209                    .with_time_limit(time_limit)
210                    .with_best_solution_callback(callback);
211                if let Some(flag) = terminate {
212                    solver.with_terminate(flag).solve(director)
213                } else {
214                    solver.solve(director)
215                }
216            }
217            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Config(ls)) => {
218                let solver = Solver::new(((), c, ls))
219                    .with_termination(termination)
220                    .with_time_limit(time_limit)
221                    .with_best_solution_callback(callback);
222                if let Some(flag) = terminate {
223                    solver.with_terminate(flag).solve(director)
224                } else {
225                    solver.solve(director)
226                }
227            }
228            (ListConstruction::RegretInsertion(c), ListLocalSearch::Default(ls)) => {
229                let solver = Solver::new(((), c, ls))
230                    .with_termination(termination)
231                    .with_time_limit(time_limit)
232                    .with_best_solution_callback(callback);
233                if let Some(flag) = terminate {
234                    solver.with_terminate(flag).solve(director)
235                } else {
236                    solver.solve(director)
237                }
238            }
239            (ListConstruction::RegretInsertion(c), ListLocalSearch::Config(ls)) => {
240                let solver = Solver::new(((), c, ls))
241                    .with_termination(termination)
242                    .with_time_limit(time_limit)
243                    .with_best_solution_callback(callback);
244                if let Some(flag) = terminate {
245                    solver.with_terminate(flag).solve(director)
246                } else {
247                    solver.solve(director)
248                }
249            }
250            (ListConstruction::ClarkeWright(c), ListLocalSearch::Default(ls)) => {
251                let solver = Solver::new(((), c, ls))
252                    .with_termination(termination)
253                    .with_time_limit(time_limit)
254                    .with_best_solution_callback(callback);
255                if let Some(flag) = terminate {
256                    solver.with_terminate(flag).solve(director)
257                } else {
258                    solver.solve(director)
259                }
260            }
261            (ListConstruction::ClarkeWright(c), ListLocalSearch::Config(ls)) => {
262                let solver = Solver::new(((), c, ls))
263                    .with_termination(termination)
264                    .with_time_limit(time_limit)
265                    .with_best_solution_callback(callback);
266                if let Some(flag) = terminate {
267                    solver.with_terminate(flag).solve(director)
268                } else {
269                    solver.solve(director)
270                }
271            }
272            (ListConstruction::KOpt(c), ListLocalSearch::Default(ls)) => {
273                let solver = Solver::new(((), c, ls))
274                    .with_termination(termination)
275                    .with_time_limit(time_limit)
276                    .with_best_solution_callback(callback);
277                if let Some(flag) = terminate {
278                    solver.with_terminate(flag).solve(director)
279                } else {
280                    solver.solve(director)
281                }
282            }
283            (ListConstruction::KOpt(c), ListLocalSearch::Config(ls)) => {
284                let solver = Solver::new(((), c, ls))
285                    .with_termination(termination)
286                    .with_time_limit(time_limit)
287                    .with_best_solution_callback(callback);
288                if let Some(flag) = terminate {
289                    solver.with_terminate(flag).solve(director)
290                } else {
291                    solver.solve(director)
292                }
293            }
294        }
295    }
296}
297
298// Builds the construction phase from config or defaults to cheapest insertion.
299#[allow(clippy::too_many_arguments)]
300fn build_list_construction<S, V>(
301    config: &SolverConfig,
302    element_count: fn(&S) -> usize,
303    get_assigned: fn(&S) -> Vec<V>,
304    entity_count: fn(&S) -> usize,
305    list_len: fn(&S, usize) -> usize,
306    list_insert: fn(&mut S, usize, usize, V),
307    list_remove: fn(&mut S, usize, usize) -> V,
308    index_to_element: fn(&S, usize) -> V,
309    descriptor_index: usize,
310    depot_fn: Option<fn(&S) -> usize>,
311    distance_fn: Option<fn(&S, usize, usize) -> i64>,
312    element_load_fn: Option<fn(&S, usize) -> i64>,
313    capacity_fn: Option<fn(&S) -> i64>,
314    assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
315    merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
316    k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
317    k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
318    k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
319    k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
320    k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
321) -> ListConstruction<S, V>
322where
323    S: PlanningSolution,
324    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
325{
326    let (ch_type, k) = config
327        .phases
328        .iter()
329        .find_map(|p| {
330            if let PhaseConfig::ConstructionHeuristic(ch) = p {
331                Some((ch.construction_heuristic_type, ch.k))
332            } else {
333                None
334            }
335        })
336        .unwrap_or((ConstructionHeuristicType::ListCheapestInsertion, 2));
337
338    match ch_type {
339        ConstructionHeuristicType::ListRegretInsertion => {
340            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
341                element_count,
342                get_assigned,
343                entity_count,
344                list_len,
345                list_insert,
346                list_remove,
347                index_to_element,
348                descriptor_index,
349            ))
350        }
351        ConstructionHeuristicType::ListClarkeWright => {
352            match (
353                depot_fn,
354                distance_fn,
355                element_load_fn,
356                capacity_fn,
357                assign_route_fn,
358            ) {
359                (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
360                    ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
361                        element_count,
362                        get_assigned,
363                        entity_count,
364                        assign,
365                        index_to_element,
366                        depot,
367                        dist,
368                        load,
369                        cap,
370                        merge_feasible_fn,
371                        descriptor_index,
372                    ))
373                }
374                _ => {
375                    tracing::warn!(
376                        "ListClarkeWright selected but one or more required fields \
377                         (depot_fn, distance_fn, element_load_fn, capacity_fn, assign_route_fn) \
378                         are None — falling back to ListCheapestInsertion"
379                    );
380                    ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
381                        element_count,
382                        get_assigned,
383                        entity_count,
384                        list_len,
385                        list_insert,
386                        list_remove,
387                        index_to_element,
388                        descriptor_index,
389                    ))
390                }
391            }
392        }
393        ConstructionHeuristicType::ListKOpt => {
394            match (
395                k_opt_get_route,
396                k_opt_set_route,
397                k_opt_depot_fn,
398                k_opt_distance_fn,
399            ) {
400                (Some(get_route), Some(set_route), Some(ko_depot), Some(ko_dist)) => {
401                    ListConstruction::KOpt(ListKOptPhase::new(
402                        k,
403                        entity_count,
404                        get_route,
405                        set_route,
406                        ko_depot,
407                        ko_dist,
408                        k_opt_feasible_fn,
409                        descriptor_index,
410                    ))
411                }
412                _ => {
413                    tracing::warn!(
414                        "ListKOpt selected but one or more required fields \
415                         (k_opt_get_route, k_opt_set_route, k_opt_depot_fn, k_opt_distance_fn) \
416                         are None — falling back to ListCheapestInsertion"
417                    );
418                    ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
419                        element_count,
420                        get_assigned,
421                        entity_count,
422                        list_len,
423                        list_insert,
424                        list_remove,
425                        index_to_element,
426                        descriptor_index,
427                    ))
428                }
429            }
430        }
431        _ => {
432            // Default: ListCheapestInsertion
433            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
434                element_count,
435                get_assigned,
436                entity_count,
437                list_len,
438                list_insert,
439                list_remove,
440                index_to_element,
441                descriptor_index,
442            ))
443        }
444    }
445}
446
447// Builds the local search phase from config or defaults.
448fn build_list_local_search<S, V, DM, IDM>(
449    config: &SolverConfig,
450    ctx: &ListContext<S, V, DM, IDM>,
451) -> ListLocalSearch<S, V, DM, IDM>
452where
453    S: PlanningSolution,
454    S::Score: Score,
455    V: Clone + Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
456    DM: CrossEntityDistanceMeter<S> + Clone,
457    IDM: CrossEntityDistanceMeter<S> + Clone,
458{
459    let ls_config = config.phases.iter().find_map(|p| {
460        if let PhaseConfig::LocalSearch(ls) = p {
461            Some(ls)
462        } else {
463            None
464        }
465    });
466
467    let Some(ls) = ls_config else {
468        // Default: LA(400) + AcceptedCount(4) + Union(NearbyChange(20), NearbySwap(20), Reverse)
469        let acceptor = LateAcceptanceAcceptor::<S>::new(400);
470        let forager = AcceptedCountForager::new(4);
471        let move_selector = ListMoveSelectorBuilder::build(None, ctx);
472        return ListLocalSearch::Default(LocalSearchPhase::new(
473            move_selector,
474            acceptor,
475            forager,
476            None,
477        ));
478    };
479
480    let acceptor = ls
481        .acceptor
482        .as_ref()
483        .map(|ac| AcceptorBuilder::build::<S>(ac))
484        .unwrap_or_else(|| AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(400)));
485
486    let forager = ForagerBuilder::build::<S>(ls.forager.as_ref());
487    let move_selector = ListMoveSelectorBuilder::build(ls.move_selector.as_ref(), ctx);
488
489    ListLocalSearch::Config(LocalSearchPhase::new(
490        move_selector,
491        acceptor,
492        forager,
493        None,
494    ))
495}