Skip to main content

solverforge_solver/
list_solver.rs

1//! List variable solver for routing and scheduling problems.
2//!
3//! This module provides `run_list_solver` for problems using 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::sync::atomic::AtomicBool;
15use std::time::Duration;
16
17use solverforge_config::{ConstructionHeuristicType, PhaseConfig, SolverConfig};
18use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
19use solverforge_core::score::{ParseableScore, Score};
20use solverforge_scoring::{ConstraintSet, Director, ScoreDirector};
21use tokio::sync::mpsc;
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::scope::BestSolutionCallback;
34use crate::scope::SolverScope;
35use crate::solver::Solver;
36use crate::termination::{
37    BestScoreTermination, OrTermination, StepCountTermination, Termination, TimeTermination,
38    UnimprovedStepCountTermination, UnimprovedTimeTermination,
39};
40
41/// Default time limit in seconds for list solvers.
42const DEFAULT_TIME_LIMIT_SECS: u64 = 60;
43
44/// Monomorphized termination enum reused from basic solver.
45pub(crate) enum AnyListTermination<S: PlanningSolution, D: Director<S>> {
46    Default(OrTermination<(TimeTermination,), S, D>),
47    WithBestScore(OrTermination<(TimeTermination, BestScoreTermination<S::Score>), S, D>),
48    WithStepCount(OrTermination<(TimeTermination, StepCountTermination), S, D>),
49    WithUnimprovedStep(OrTermination<(TimeTermination, UnimprovedStepCountTermination<S>), S, D>),
50    WithUnimprovedTime(OrTermination<(TimeTermination, UnimprovedTimeTermination<S>), S, D>),
51}
52
53impl<S: PlanningSolution, D: Director<S>> fmt::Debug for AnyListTermination<S, D> {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::Default(_) => write!(f, "AnyListTermination::Default"),
57            Self::WithBestScore(_) => write!(f, "AnyListTermination::WithBestScore"),
58            Self::WithStepCount(_) => write!(f, "AnyListTermination::WithStepCount"),
59            Self::WithUnimprovedStep(_) => write!(f, "AnyListTermination::WithUnimprovedStep"),
60            Self::WithUnimprovedTime(_) => write!(f, "AnyListTermination::WithUnimprovedTime"),
61        }
62    }
63}
64
65impl<S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>> Termination<S, D, BestCb>
66    for AnyListTermination<S, D>
67where
68    S::Score: Score,
69{
70    fn is_terminated(&self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
71        match self {
72            Self::Default(t) => t.is_terminated(solver_scope),
73            Self::WithBestScore(t) => t.is_terminated(solver_scope),
74            Self::WithStepCount(t) => t.is_terminated(solver_scope),
75            Self::WithUnimprovedStep(t) => t.is_terminated(solver_scope),
76            Self::WithUnimprovedTime(t) => t.is_terminated(solver_scope),
77        }
78    }
79
80    fn install_inphase_limits(&self, solver_scope: &mut SolverScope<S, D, BestCb>) {
81        match self {
82            Self::Default(t) => t.install_inphase_limits(solver_scope),
83            Self::WithBestScore(t) => t.install_inphase_limits(solver_scope),
84            Self::WithStepCount(t) => t.install_inphase_limits(solver_scope),
85            Self::WithUnimprovedStep(t) => t.install_inphase_limits(solver_scope),
86            Self::WithUnimprovedTime(t) => t.install_inphase_limits(solver_scope),
87        }
88    }
89}
90
91// Type alias for the config-driven list local search phase
92type ConfigListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
93    S,
94    ListMoveImpl<S, V>,
95    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
96    AnyAcceptor<S>,
97    AnyForager<S>,
98>;
99
100// Type alias for the default list local search phase
101type DefaultListLocalSearch<S, V, DM, IDM> = LocalSearchPhase<
102    S,
103    ListMoveImpl<S, V>,
104    VecUnionSelector<S, ListMoveImpl<S, V>, ListLeafSelector<S, V, DM, IDM>>,
105    LateAcceptanceAcceptor<S>,
106    AcceptedCountForager<S>,
107>;
108
109/// Monomorphized local search enum for list solver.
110// Variants intentionally differ in size; this enum is constructed once per solve, not in hot paths.
111#[allow(clippy::large_enum_variant)]
112enum ListLocalSearch<S, V, DM, IDM>
113where
114    S: PlanningSolution,
115    V: Clone + PartialEq + Send + Sync + fmt::Debug + 'static,
116    DM: CrossEntityDistanceMeter<S>,
117    IDM: CrossEntityDistanceMeter<S>,
118{
119    Default(DefaultListLocalSearch<S, V, DM, IDM>),
120    Config(ConfigListLocalSearch<S, V, DM, IDM>),
121}
122
123/// Construction phase enum for list solver.
124enum ListConstruction<S, V>
125where
126    S: PlanningSolution,
127    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
128{
129    CheapestInsertion(ListCheapestInsertionPhase<S, V>),
130    RegretInsertion(ListRegretInsertionPhase<S, V>),
131}
132
133/// Solves a list variable problem using construction heuristic + local search.
134///
135/// Called by macro-generated `solve()` methods for solutions using
136/// `#[shadow_variable_updates]` (list variables). When phases are configured in
137/// `solver.toml`, the construction type, acceptor, forager, and move selectors
138/// are built from config; otherwise defaults are used.
139///
140/// # Type Parameters
141///
142/// * `S` - The solution type
143/// * `V` - The list element type
144/// * `C` - The constraint set type
145/// * `DM` - Cross-entity distance meter type
146/// * `IDM` - Intra-entity distance meter type
147///
148/// # Default Behavior (no config)
149///
150/// - Construction: `ListCheapestInsertion`
151/// - Acceptor: `LateAcceptance(400)`
152/// - Forager: `AcceptedCount(4)`
153/// - Move selector: `Union(NearbyListChange(20), NearbyListSwap(20), ListReverse)`
154/// - Termination: 60s
155#[allow(clippy::too_many_arguments)]
156pub fn run_list_solver<S, V, C, DM, IDM>(
157    mut solution: S,
158    finalize_fn: fn(&mut S),
159    constraints_fn: fn() -> C,
160    // List operation function pointers
161    list_len: fn(&S, usize) -> usize,
162    list_remove: fn(&mut S, usize, usize) -> Option<V>,
163    list_insert: fn(&mut S, usize, usize, V),
164    list_get: fn(&S, usize, usize) -> Option<V>,
165    list_set: fn(&mut S, usize, usize, V),
166    list_reverse: fn(&mut S, usize, usize, usize),
167    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
168    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
169    ruin_remove: fn(&mut S, usize, usize) -> V,
170    ruin_insert: fn(&mut S, usize, usize, V),
171    // Construction function pointers
172    element_count: fn(&S) -> usize,
173    get_assigned: fn(&S) -> Vec<V>,
174    entity_count: fn(&S) -> usize,
175    list_remove_for_construction: fn(&mut S, usize, usize) -> V,
176    index_to_element: fn(&S, usize) -> V,
177    // Distance meters
178    cross_distance_meter: DM,
179    intra_distance_meter: IDM,
180    // Metadata
181    variable_name: &'static str,
182    descriptor_index: usize,
183    // Solver infrastructure
184    descriptor: fn() -> SolutionDescriptor,
185    entity_count_by_descriptor: fn(&S, usize) -> usize,
186    terminate: Option<&AtomicBool>,
187    sender: mpsc::UnboundedSender<(S, S::Score)>,
188) -> S
189where
190    S: PlanningSolution,
191    S::Score: Score + ParseableScore,
192    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
193    C: ConstraintSet<S, S::Score>,
194    DM: CrossEntityDistanceMeter<S> + Clone,
195    IDM: CrossEntityDistanceMeter<S> + Clone,
196{
197    finalize_fn(&mut solution);
198
199    let config = SolverConfig::load("solver.toml").unwrap_or_default();
200    let n_entities = entity_count(&solution);
201    let n_elements = element_count(&solution);
202
203    info!(
204        event = "solve_start",
205        entity_count = n_entities,
206        element_count = n_elements,
207    );
208
209    let constraints = constraints_fn();
210    let director = ScoreDirector::with_descriptor(
211        solution,
212        constraints,
213        descriptor(),
214        entity_count_by_descriptor,
215    );
216
217    if n_entities == 0 {
218        let mut solver_scope = SolverScope::new(director);
219        solver_scope.start_solving();
220        let score = solver_scope.calculate_score();
221        info!(event = "solve_end", score = %score);
222        let solution = solver_scope.take_best_or_working_solution();
223        let _ = sender.send((solution.clone(), score));
224        return solution;
225    }
226
227    // Build construction phase
228    let construction = build_list_construction::<S, V>(
229        &config,
230        element_count,
231        get_assigned,
232        entity_count,
233        list_len,
234        list_insert,
235        list_remove_for_construction,
236        index_to_element,
237        descriptor_index,
238    );
239
240    let ctx = ListContext::new(
241        list_len,
242        list_remove,
243        list_insert,
244        list_get,
245        list_set,
246        list_reverse,
247        sublist_remove,
248        sublist_insert,
249        ruin_remove,
250        ruin_insert,
251        entity_count,
252        cross_distance_meter,
253        intra_distance_meter,
254        variable_name,
255        descriptor_index,
256    );
257
258    // Build local search phase
259    let local_search = build_list_local_search::<S, V, DM, IDM>(&config, &ctx);
260
261    // Build termination
262    let term_config = config.termination.as_ref();
263    let time_limit = term_config
264        .and_then(|c| c.time_limit())
265        .unwrap_or(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS));
266    let time = TimeTermination::new(time_limit);
267
268    let best_score_target: Option<S::Score> = term_config
269        .and_then(|c| c.best_score_limit.as_ref())
270        .and_then(|s| S::Score::parse(s).ok());
271
272    let termination: AnyListTermination<S, ScoreDirector<S, C>> =
273        if let Some(target) = best_score_target {
274            AnyListTermination::WithBestScore(OrTermination::new((
275                time,
276                BestScoreTermination::new(target),
277            )))
278        } else if let Some(step_limit) = term_config.and_then(|c| c.step_count_limit) {
279            AnyListTermination::WithStepCount(OrTermination::new((
280                time,
281                StepCountTermination::new(step_limit),
282            )))
283        } else if let Some(unimproved_step_limit) =
284            term_config.and_then(|c| c.unimproved_step_count_limit)
285        {
286            AnyListTermination::WithUnimprovedStep(OrTermination::new((
287                time,
288                UnimprovedStepCountTermination::<S>::new(unimproved_step_limit),
289            )))
290        } else if let Some(unimproved_time) = term_config.and_then(|c| c.unimproved_time_limit()) {
291            AnyListTermination::WithUnimprovedTime(OrTermination::new((
292                time,
293                UnimprovedTimeTermination::<S>::new(unimproved_time),
294            )))
295        } else {
296            AnyListTermination::Default(OrTermination::new((time,)))
297        };
298
299    let callback_sender = sender.clone();
300    let callback = move |solution: &S| {
301        let score = solution.score().unwrap_or_default();
302        let _ = callback_sender.send((solution.clone(), score));
303    };
304
305    // Run solver using the construction + local search phases
306    let result = match (construction, local_search) {
307        (ListConstruction::CheapestInsertion(c), ListLocalSearch::Default(ls)) => {
308            let solver = Solver::new(((), c, ls))
309                .with_termination(termination)
310                .with_time_limit(time_limit)
311                .with_best_solution_callback(callback);
312            if let Some(flag) = terminate {
313                solver.with_terminate(flag).solve(director)
314            } else {
315                solver.solve(director)
316            }
317        }
318        (ListConstruction::CheapestInsertion(c), ListLocalSearch::Config(ls)) => {
319            let solver = Solver::new(((), c, ls))
320                .with_termination(termination)
321                .with_time_limit(time_limit)
322                .with_best_solution_callback(callback);
323            if let Some(flag) = terminate {
324                solver.with_terminate(flag).solve(director)
325            } else {
326                solver.solve(director)
327            }
328        }
329        (ListConstruction::RegretInsertion(c), ListLocalSearch::Default(ls)) => {
330            let solver = Solver::new(((), c, ls))
331                .with_termination(termination)
332                .with_time_limit(time_limit)
333                .with_best_solution_callback(callback);
334            if let Some(flag) = terminate {
335                solver.with_terminate(flag).solve(director)
336            } else {
337                solver.solve(director)
338            }
339        }
340        (ListConstruction::RegretInsertion(c), ListLocalSearch::Config(ls)) => {
341            let solver = Solver::new(((), c, ls))
342                .with_termination(termination)
343                .with_time_limit(time_limit)
344                .with_best_solution_callback(callback);
345            if let Some(flag) = terminate {
346                solver.with_terminate(flag).solve(director)
347            } else {
348                solver.solve(director)
349            }
350        }
351    };
352
353    let final_score = result.solution.score().unwrap_or_default();
354    let _ = sender.send((result.solution.clone(), final_score));
355
356    info!(
357        event = "solve_end",
358        score = %final_score,
359        steps = result.stats.step_count,
360        moves_evaluated = result.stats.moves_evaluated,
361    );
362    result.solution
363}
364
365/// Builds the construction phase from config or defaults to cheapest insertion.
366#[allow(clippy::too_many_arguments)]
367fn build_list_construction<S, V>(
368    config: &SolverConfig,
369    element_count: fn(&S) -> usize,
370    get_assigned: fn(&S) -> Vec<V>,
371    entity_count: fn(&S) -> usize,
372    list_len: fn(&S, usize) -> usize,
373    list_insert: fn(&mut S, usize, usize, V),
374    list_remove: fn(&mut S, usize, usize) -> V,
375    index_to_element: fn(&S, usize) -> V,
376    descriptor_index: usize,
377) -> ListConstruction<S, V>
378where
379    S: PlanningSolution,
380    V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
381{
382    let ch_type = config
383        .phases
384        .iter()
385        .find_map(|p| {
386            if let PhaseConfig::ConstructionHeuristic(ch) = p {
387                Some(ch.construction_heuristic_type)
388            } else {
389                None
390            }
391        })
392        .unwrap_or(ConstructionHeuristicType::ListCheapestInsertion);
393
394    match ch_type {
395        ConstructionHeuristicType::ListRegretInsertion => {
396            ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
397                element_count,
398                get_assigned,
399                entity_count,
400                list_len,
401                list_insert,
402                list_remove,
403                index_to_element,
404                descriptor_index,
405            ))
406        }
407        _ => {
408            // Default: ListCheapestInsertion
409            ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
410                element_count,
411                get_assigned,
412                entity_count,
413                list_len,
414                list_insert,
415                list_remove,
416                index_to_element,
417                descriptor_index,
418            ))
419        }
420    }
421}
422
423/// Builds the local search phase from config or defaults.
424fn build_list_local_search<S, V, DM, IDM>(
425    config: &SolverConfig,
426    ctx: &ListContext<S, V, DM, IDM>,
427) -> ListLocalSearch<S, V, DM, IDM>
428where
429    S: PlanningSolution,
430    S::Score: Score,
431    V: Clone + Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
432    DM: CrossEntityDistanceMeter<S> + Clone,
433    IDM: CrossEntityDistanceMeter<S> + Clone,
434{
435    let ls_config = config.phases.iter().find_map(|p| {
436        if let PhaseConfig::LocalSearch(ls) = p {
437            Some(ls)
438        } else {
439            None
440        }
441    });
442
443    let Some(ls) = ls_config else {
444        // Default: LA(400) + AcceptedCount(4) + Union(NearbyChange(20), NearbySwap(20), Reverse)
445        let acceptor = LateAcceptanceAcceptor::<S>::new(400);
446        let forager = AcceptedCountForager::new(4);
447        let move_selector = ListMoveSelectorBuilder::build(None, ctx);
448        return ListLocalSearch::Default(LocalSearchPhase::new(
449            move_selector,
450            acceptor,
451            forager,
452            None,
453        ));
454    };
455
456    let acceptor = ls
457        .acceptor
458        .as_ref()
459        .map(|ac| AcceptorBuilder::build::<S>(ac))
460        .unwrap_or_else(|| AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(400)));
461
462    let forager = ForagerBuilder::build::<S>(ls.forager.as_ref());
463    let move_selector = ListMoveSelectorBuilder::build(ls.move_selector.as_ref(), ctx);
464
465    ListLocalSearch::Config(LocalSearchPhase::new(
466        move_selector,
467        acceptor,
468        forager,
469        None,
470    ))
471}