Skip to main content

solverforge_solver/
list_solver.rs

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