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::{ListCheapestInsertionPhase, ListRegretInsertionPhase};
32use crate::phase::localsearch::{AcceptedCountForager, LateAcceptanceAcceptor, LocalSearchPhase};
33use crate::problem_spec::ProblemSpec;
34use crate::run::AnyTermination;
35use crate::solver::{SolveResult, Solver};
36
37// Type alias for the config-driven list local search phase
38type ConfigListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
39    S,
40    ListMoveImpl<S, V>,
41    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
42    AnyAcceptor<S>,
43    AnyForager<S>,
44>;
45
46// Type alias for the default list local search phase
47type DefaultListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
48    S,
49    ListMoveImpl<S, V>,
50    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
51    LateAcceptanceAcceptor<S>,
52    AcceptedCountForager<S>,
53>;
54
55/// Monomorphized local search enum for list solver.
56// Variants intentionally differ in size; this enum is constructed once per solve, not in hot paths.
57#[allow(clippy::large_enum_variant)]
58enum ListLocalSearch<S, V, DM, IDM>
59where
60    S: PlanningSolution,
61    V: Clone + PartialEq + Send + Sync + fmt::Debug + 'static,
62    DM: CrossEntityDistanceMeter<S>,
63    IDM: CrossEntityDistanceMeter<S>,
64{
65    Default(DefaultListLocalSearch<S, V, DM, IDM>),
66    Config(ConfigListLocalSearch<S, V, DM, IDM>),
67}
68
69/// Construction phase enum for list solver.
70enum ListConstruction<S, V>
71where
72    S: PlanningSolution,
73    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
74{
75    CheapestInsertion(ListCheapestInsertionPhase<S, V>),
76    RegretInsertion(ListRegretInsertionPhase<S, V>),
77}
78
79/// Problem specification for list variable problems.
80///
81/// Passed to `run_solver` to provide problem-specific construction and local
82/// search phases for solutions using `#[shadow_variable_updates]` (list variables).
83pub struct ListSpec<S, V, DM, IDM> {
84    // List operation function pointers
85    pub list_len: fn(&S, usize) -> usize,
86    pub list_remove: fn(&mut S, usize, usize) -> Option<V>,
87    pub list_insert: fn(&mut S, usize, usize, V),
88    pub list_get: fn(&S, usize, usize) -> Option<V>,
89    pub list_set: fn(&mut S, usize, usize, V),
90    pub list_reverse: fn(&mut S, usize, usize, usize),
91    pub sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
92    pub sublist_insert: fn(&mut S, usize, usize, Vec<V>),
93    pub ruin_remove: fn(&mut S, usize, usize) -> V,
94    pub ruin_insert: fn(&mut S, usize, usize, V),
95    // Construction function pointers
96    pub element_count: fn(&S) -> usize,
97    pub get_assigned: fn(&S) -> Vec<V>,
98    pub entity_count: fn(&S) -> usize,
99    pub list_remove_for_construction: fn(&mut S, usize, usize) -> V,
100    pub index_to_element: fn(&S, usize) -> V,
101    // Distance meters
102    pub cross_distance_meter: DM,
103    pub intra_distance_meter: IDM,
104    // Metadata
105    pub variable_name: &'static str,
106    pub descriptor_index: usize,
107    pub _phantom: PhantomData<fn() -> S>,
108}
109
110impl<S, V, C, DM, IDM> ProblemSpec<S, C> for ListSpec<S, V, DM, IDM>
111where
112    S: PlanningSolution,
113    S::Score: Score + ParseableScore,
114    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
115    C: ConstraintSet<S, S::Score>,
116    DM: CrossEntityDistanceMeter<S> + Clone,
117    IDM: CrossEntityDistanceMeter<S> + Clone,
118{
119    fn is_trivial(&self, solution: &S) -> bool {
120        (self.entity_count)(solution) == 0
121    }
122
123    fn default_time_limit_secs(&self) -> u64 {
124        60
125    }
126
127    fn log_scale(&self, solution: &S) {
128        info!(
129            event = "solve_start",
130            entity_count = (self.entity_count)(solution),
131            element_count = (self.element_count)(solution),
132        );
133    }
134
135    fn build_and_solve(
136        self,
137        director: ScoreDirector<S, C>,
138        config: &SolverConfig,
139        time_limit: Duration,
140        termination: AnyTermination<S, ScoreDirector<S, C>>,
141        terminate: Option<&AtomicBool>,
142        callback: impl Fn(&S) + Send + Sync,
143    ) -> SolveResult<S> {
144        let construction = build_list_construction::<S, V>(
145            config,
146            self.element_count,
147            self.get_assigned,
148            self.entity_count,
149            self.list_len,
150            self.list_insert,
151            self.list_remove_for_construction,
152            self.index_to_element,
153            self.descriptor_index,
154        );
155
156        let ctx = ListContext::new(
157            self.list_len,
158            self.list_remove,
159            self.list_insert,
160            self.list_get,
161            self.list_set,
162            self.list_reverse,
163            self.sublist_remove,
164            self.sublist_insert,
165            self.ruin_remove,
166            self.ruin_insert,
167            self.entity_count,
168            self.cross_distance_meter,
169            self.intra_distance_meter,
170            self.variable_name,
171            self.descriptor_index,
172        );
173
174        let local_search = build_list_local_search::<S, V, DM, IDM>(config, &ctx);
175
176        match (construction, local_search) {
177            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Default(ls)) => {
178                let solver = Solver::new(((), c, ls))
179                    .with_termination(termination)
180                    .with_time_limit(time_limit)
181                    .with_best_solution_callback(callback);
182                if let Some(flag) = terminate {
183                    solver.with_terminate(flag).solve(director)
184                } else {
185                    solver.solve(director)
186                }
187            }
188            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Config(ls)) => {
189                let solver = Solver::new(((), c, ls))
190                    .with_termination(termination)
191                    .with_time_limit(time_limit)
192                    .with_best_solution_callback(callback);
193                if let Some(flag) = terminate {
194                    solver.with_terminate(flag).solve(director)
195                } else {
196                    solver.solve(director)
197                }
198            }
199            (ListConstruction::RegretInsertion(c), ListLocalSearch::Default(ls)) => {
200                let solver = Solver::new(((), c, ls))
201                    .with_termination(termination)
202                    .with_time_limit(time_limit)
203                    .with_best_solution_callback(callback);
204                if let Some(flag) = terminate {
205                    solver.with_terminate(flag).solve(director)
206                } else {
207                    solver.solve(director)
208                }
209            }
210            (ListConstruction::RegretInsertion(c), ListLocalSearch::Config(ls)) => {
211                let solver = Solver::new(((), c, ls))
212                    .with_termination(termination)
213                    .with_time_limit(time_limit)
214                    .with_best_solution_callback(callback);
215                if let Some(flag) = terminate {
216                    solver.with_terminate(flag).solve(director)
217                } else {
218                    solver.solve(director)
219                }
220            }
221        }
222    }
223}
224
225/// Builds the construction phase from config or defaults to cheapest insertion.
226#[allow(clippy::too_many_arguments)]
227fn build_list_construction<S, V>(
228    config: &SolverConfig,
229    element_count: fn(&S) -> usize,
230    get_assigned: fn(&S) -> Vec<V>,
231    entity_count: fn(&S) -> usize,
232    list_len: fn(&S, usize) -> usize,
233    list_insert: fn(&mut S, usize, usize, V),
234    list_remove: fn(&mut S, usize, usize) -> V,
235    index_to_element: fn(&S, usize) -> V,
236    descriptor_index: usize,
237) -> ListConstruction<S, V>
238where
239    S: PlanningSolution,
240    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
241{
242    let ch_type = config
243        .phases
244        .iter()
245        .find_map(|p| {
246            if let PhaseConfig::ConstructionHeuristic(ch) = p {
247                Some(ch.construction_heuristic_type)
248            } else {
249                None
250            }
251        })
252        .unwrap_or(ConstructionHeuristicType::ListCheapestInsertion);
253
254    match ch_type {
255        ConstructionHeuristicType::ListRegretInsertion => {
256            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
257                element_count,
258                get_assigned,
259                entity_count,
260                list_len,
261                list_insert,
262                list_remove,
263                index_to_element,
264                descriptor_index,
265            ))
266        }
267        _ => {
268            // Default: ListCheapestInsertion
269            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
270                element_count,
271                get_assigned,
272                entity_count,
273                list_len,
274                list_insert,
275                list_remove,
276                index_to_element,
277                descriptor_index,
278            ))
279        }
280    }
281}
282
283/// Builds the local search phase from config or defaults.
284fn build_list_local_search<S, V, DM, IDM>(
285    config: &SolverConfig,
286    ctx: &ListContext<S, V, DM, IDM>,
287) -> ListLocalSearch<S, V, DM, IDM>
288where
289    S: PlanningSolution,
290    S::Score: Score,
291    V: Clone + Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
292    DM: CrossEntityDistanceMeter<S> + Clone,
293    IDM: CrossEntityDistanceMeter<S> + Clone,
294{
295    let ls_config = config.phases.iter().find_map(|p| {
296        if let PhaseConfig::LocalSearch(ls) = p {
297            Some(ls)
298        } else {
299            None
300        }
301    });
302
303    let Some(ls) = ls_config else {
304        // Default: LA(400) + AcceptedCount(4) + Union(NearbyChange(20), NearbySwap(20), Reverse)
305        let acceptor = LateAcceptanceAcceptor::<S>::new(400);
306        let forager = AcceptedCountForager::new(4);
307        let move_selector = ListMoveSelectorBuilder::build(None, ctx);
308        return ListLocalSearch::Default(LocalSearchPhase::new(
309            move_selector,
310            acceptor,
311            forager,
312            None,
313        ));
314    };
315
316    let acceptor = ls
317        .acceptor
318        .as_ref()
319        .map(|ac| AcceptorBuilder::build::<S>(ac))
320        .unwrap_or_else(|| AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(400)));
321
322    let forager = ForagerBuilder::build::<S>(ls.forager.as_ref());
323    let move_selector = ListMoveSelectorBuilder::build(ls.move_selector.as_ref(), ctx);
324
325    ListLocalSearch::Config(LocalSearchPhase::new(
326        move_selector,
327        acceptor,
328        forager,
329        None,
330    ))
331}