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, ListClarkeWrightPhase, 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> + 'static,
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    ClarkeWright(ListClarkeWrightPhase<S, V>),
78}
79
80/// Problem specification for list variable problems.
81///
82/// Passed to `run_solver` to provide problem-specific construction and local
83/// search phases for solutions using `#[shadow_variable_updates]` (list variables).
84pub struct ListSpec<S, V, DM, IDM> {
85    // List operation function pointers
86    pub list_len: fn(&S, usize) -> usize,
87    pub list_remove: fn(&mut S, usize, usize) -> Option<V>,
88    pub list_insert: fn(&mut S, usize, usize, V),
89    pub list_get: fn(&S, usize, usize) -> Option<V>,
90    pub list_set: fn(&mut S, usize, usize, V),
91    pub list_reverse: fn(&mut S, usize, usize, usize),
92    pub sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
93    pub sublist_insert: fn(&mut S, usize, usize, Vec<V>),
94    pub ruin_remove: fn(&mut S, usize, usize) -> V,
95    pub ruin_insert: fn(&mut S, usize, usize, V),
96    // Construction function pointers
97    pub element_count: fn(&S) -> usize,
98    pub get_assigned: fn(&S) -> Vec<V>,
99    pub entity_count: fn(&S) -> usize,
100    pub list_remove_for_construction: fn(&mut S, usize, usize) -> V,
101    pub index_to_element: fn(&S, usize) -> V,
102    // Distance meters
103    pub cross_distance_meter: DM,
104    pub intra_distance_meter: IDM,
105    // Clarke-Wright fields (all None if not using Clarke-Wright construction)
106    pub depot_fn: Option<fn(&S) -> usize>,
107    pub distance_fn: Option<fn(usize, usize) -> i64>,
108    pub element_load_fn: Option<fn(&S, usize) -> i64>,
109    pub capacity_fn: Option<fn(&S) -> i64>,
110    pub assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
111    // Metadata
112    pub variable_name: &'static str,
113    pub descriptor_index: usize,
114    pub _phantom: PhantomData<fn() -> S>,
115}
116
117impl<S, V, C, DM, IDM> ProblemSpec<S, C> for ListSpec<S, V, DM, IDM>
118where
119    S: PlanningSolution,
120    S::Score: Score + ParseableScore,
121    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
122    C: ConstraintSet<S, S::Score>,
123    DM: CrossEntityDistanceMeter<S> + Clone,
124    IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
125{
126    fn is_trivial(&self, solution: &S) -> bool {
127        (self.entity_count)(solution) == 0
128    }
129
130    fn default_time_limit_secs(&self) -> u64 {
131        60
132    }
133
134    fn log_scale(&self, solution: &S) {
135        info!(
136            event = "solve_start",
137            entity_count = (self.entity_count)(solution),
138            element_count = (self.element_count)(solution),
139        );
140    }
141
142    fn build_and_solve(
143        self,
144        director: ScoreDirector<S, C>,
145        config: &SolverConfig,
146        time_limit: Duration,
147        termination: AnyTermination<S, ScoreDirector<S, C>>,
148        terminate: Option<&AtomicBool>,
149        callback: impl Fn(&S) + Send + Sync,
150    ) -> SolveResult<S> {
151        let construction = build_list_construction::<S, V>(
152            config,
153            self.element_count,
154            self.get_assigned,
155            self.entity_count,
156            self.list_len,
157            self.list_insert,
158            self.list_remove_for_construction,
159            self.index_to_element,
160            self.descriptor_index,
161            self.depot_fn,
162            self.distance_fn,
163            self.element_load_fn,
164            self.capacity_fn,
165            self.assign_route_fn,
166        );
167
168        let ctx = ListContext::new(
169            self.list_len,
170            self.list_remove,
171            self.list_insert,
172            self.list_get,
173            self.list_set,
174            self.list_reverse,
175            self.sublist_remove,
176            self.sublist_insert,
177            self.ruin_remove,
178            self.ruin_insert,
179            self.entity_count,
180            self.cross_distance_meter,
181            self.intra_distance_meter,
182            self.variable_name,
183            self.descriptor_index,
184        );
185
186        let local_search = build_list_local_search::<S, V, DM, IDM>(config, &ctx);
187
188        match (construction, local_search) {
189            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Default(ls)) => {
190                let solver = Solver::new(((), c, ls))
191                    .with_termination(termination)
192                    .with_time_limit(time_limit)
193                    .with_best_solution_callback(callback);
194                if let Some(flag) = terminate {
195                    solver.with_terminate(flag).solve(director)
196                } else {
197                    solver.solve(director)
198                }
199            }
200            (ListConstruction::CheapestInsertion(c), ListLocalSearch::Config(ls)) => {
201                let solver = Solver::new(((), c, ls))
202                    .with_termination(termination)
203                    .with_time_limit(time_limit)
204                    .with_best_solution_callback(callback);
205                if let Some(flag) = terminate {
206                    solver.with_terminate(flag).solve(director)
207                } else {
208                    solver.solve(director)
209                }
210            }
211            (ListConstruction::RegretInsertion(c), ListLocalSearch::Default(ls)) => {
212                let solver = Solver::new(((), c, ls))
213                    .with_termination(termination)
214                    .with_time_limit(time_limit)
215                    .with_best_solution_callback(callback);
216                if let Some(flag) = terminate {
217                    solver.with_terminate(flag).solve(director)
218                } else {
219                    solver.solve(director)
220                }
221            }
222            (ListConstruction::RegretInsertion(c), ListLocalSearch::Config(ls)) => {
223                let solver = Solver::new(((), c, ls))
224                    .with_termination(termination)
225                    .with_time_limit(time_limit)
226                    .with_best_solution_callback(callback);
227                if let Some(flag) = terminate {
228                    solver.with_terminate(flag).solve(director)
229                } else {
230                    solver.solve(director)
231                }
232            }
233            (ListConstruction::ClarkeWright(c), ListLocalSearch::Default(ls)) => {
234                let solver = Solver::new(((), c, ls))
235                    .with_termination(termination)
236                    .with_time_limit(time_limit)
237                    .with_best_solution_callback(callback);
238                if let Some(flag) = terminate {
239                    solver.with_terminate(flag).solve(director)
240                } else {
241                    solver.solve(director)
242                }
243            }
244            (ListConstruction::ClarkeWright(c), ListLocalSearch::Config(ls)) => {
245                let solver = Solver::new(((), c, ls))
246                    .with_termination(termination)
247                    .with_time_limit(time_limit)
248                    .with_best_solution_callback(callback);
249                if let Some(flag) = terminate {
250                    solver.with_terminate(flag).solve(director)
251                } else {
252                    solver.solve(director)
253                }
254            }
255        }
256    }
257}
258
259/// Builds the construction phase from config or defaults to cheapest insertion.
260#[allow(clippy::too_many_arguments)]
261fn build_list_construction<S, V>(
262    config: &SolverConfig,
263    element_count: fn(&S) -> usize,
264    get_assigned: fn(&S) -> Vec<V>,
265    entity_count: fn(&S) -> usize,
266    list_len: fn(&S, usize) -> usize,
267    list_insert: fn(&mut S, usize, usize, V),
268    list_remove: fn(&mut S, usize, usize) -> V,
269    index_to_element: fn(&S, usize) -> V,
270    descriptor_index: usize,
271    depot_fn: Option<fn(&S) -> usize>,
272    distance_fn: Option<fn(usize, usize) -> i64>,
273    element_load_fn: Option<fn(&S, usize) -> i64>,
274    capacity_fn: Option<fn(&S) -> i64>,
275    assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
276) -> ListConstruction<S, V>
277where
278    S: PlanningSolution,
279    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
280{
281    let ch_type = config
282        .phases
283        .iter()
284        .find_map(|p| {
285            if let PhaseConfig::ConstructionHeuristic(ch) = p {
286                Some(ch.construction_heuristic_type)
287            } else {
288                None
289            }
290        })
291        .unwrap_or(ConstructionHeuristicType::ListCheapestInsertion);
292
293    match ch_type {
294        ConstructionHeuristicType::ListRegretInsertion => {
295            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
296                element_count,
297                get_assigned,
298                entity_count,
299                list_len,
300                list_insert,
301                list_remove,
302                index_to_element,
303                descriptor_index,
304            ))
305        }
306        ConstructionHeuristicType::ListClarkeWright => {
307            match (
308                depot_fn,
309                distance_fn,
310                element_load_fn,
311                capacity_fn,
312                assign_route_fn,
313            ) {
314                (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
315                    ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
316                        element_count,
317                        get_assigned,
318                        entity_count,
319                        assign,
320                        index_to_element,
321                        depot,
322                        dist,
323                        load,
324                        cap,
325                        descriptor_index,
326                    ))
327                }
328                _ => {
329                    tracing::warn!(
330                        "ListClarkeWright selected but one or more required fields \
331                         (depot_fn, distance_fn, element_load_fn, capacity_fn, assign_route_fn) \
332                         are None — falling back to ListCheapestInsertion"
333                    );
334                    ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
335                        element_count,
336                        get_assigned,
337                        entity_count,
338                        list_len,
339                        list_insert,
340                        list_remove,
341                        index_to_element,
342                        descriptor_index,
343                    ))
344                }
345            }
346        }
347        _ => {
348            // Default: ListCheapestInsertion
349            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
350                element_count,
351                get_assigned,
352                entity_count,
353                list_len,
354                list_insert,
355                list_remove,
356                index_to_element,
357                descriptor_index,
358            ))
359        }
360    }
361}
362
363/// Builds the local search phase from config or defaults.
364fn build_list_local_search<S, V, DM, IDM>(
365    config: &SolverConfig,
366    ctx: &ListContext<S, V, DM, IDM>,
367) -> ListLocalSearch<S, V, DM, IDM>
368where
369    S: PlanningSolution,
370    S::Score: Score,
371    V: Clone + Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
372    DM: CrossEntityDistanceMeter<S> + Clone,
373    IDM: CrossEntityDistanceMeter<S> + Clone,
374{
375    let ls_config = config.phases.iter().find_map(|p| {
376        if let PhaseConfig::LocalSearch(ls) = p {
377            Some(ls)
378        } else {
379            None
380        }
381    });
382
383    let Some(ls) = ls_config else {
384        // Default: LA(400) + AcceptedCount(4) + Union(NearbyChange(20), NearbySwap(20), Reverse)
385        let acceptor = LateAcceptanceAcceptor::<S>::new(400);
386        let forager = AcceptedCountForager::new(4);
387        let move_selector = ListMoveSelectorBuilder::build(None, ctx);
388        return ListLocalSearch::Default(LocalSearchPhase::new(
389            move_selector,
390            acceptor,
391            forager,
392            None,
393        ));
394    };
395
396    let acceptor = ls
397        .acceptor
398        .as_ref()
399        .map(|ac| AcceptorBuilder::build::<S>(ac))
400        .unwrap_or_else(|| AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(400)));
401
402    let forager = ForagerBuilder::build::<S>(ls.forager.as_ref());
403    let move_selector = ListMoveSelectorBuilder::build(ls.move_selector.as_ref(), ctx);
404
405    ListLocalSearch::Config(LocalSearchPhase::new(
406        move_selector,
407        acceptor,
408        forager,
409        None,
410    ))
411}