Skip to main content

solverforge_solver/
basic.rs

1//! Basic variable solver for simple assignment problems.
2//!
3//! This module provides `BasicSpec` for problems using `#[basic_variable_config]`,
4//! where each entity has a single planning variable that can be assigned from a
5//! fixed value range.
6//!
7//! Logging levels:
8//! - **INFO**: Solver start/end, phase summaries, problem scale
9//! - **DEBUG**: Individual steps with timing and scores
10//! - **TRACE**: Move evaluation details
11
12use std::sync::atomic::AtomicBool;
13use std::time::Duration;
14
15use solverforge_config::{PhaseConfig, SolverConfig};
16use solverforge_core::domain::PlanningSolution;
17use solverforge_core::score::{ParseableScore, Score};
18use solverforge_scoring::{ConstraintSet, ScoreDirector};
19use tracing::info;
20
21use crate::builder::basic_selector::BasicLeafSelector;
22use crate::builder::{
23    AcceptorBuilder, AnyAcceptor, BasicContext, BasicMoveSelectorBuilder, ForagerBuilder,
24};
25use crate::heuristic::r#move::EitherMove;
26use crate::heuristic::selector::decorator::UnionMoveSelector;
27use crate::heuristic::selector::decorator::VecUnionSelector;
28use crate::heuristic::selector::{
29    EitherChangeMoveSelector, EitherSwapMoveSelector, FromSolutionEntitySelector,
30    StaticTypedValueSelector,
31};
32use crate::phase::construction::{BestFitForager, ConstructionHeuristicPhase, QueuedEntityPlacer};
33use crate::phase::localsearch::{LocalSearchPhase, SimulatedAnnealingAcceptor};
34use crate::problem_spec::ProblemSpec;
35use crate::run::AnyTermination;
36use crate::solver::{SolveResult, Solver};
37
38// Type alias for the config-driven local search phase
39type ConfigLocalSearch<S> = LocalSearchPhase<
40    S,
41    EitherMove<S, usize>,
42    VecUnionSelector<S, EitherMove<S, usize>, BasicLeafSelector<S>>,
43    AnyAcceptor<S>,
44    crate::builder::AnyForager<S>,
45>;
46
47// Type alias for the default local search phase (SA + UnionMoveSelector)
48type DefaultLocalSearch<S> = LocalSearchPhase<
49    S,
50    EitherMove<S, usize>,
51    UnionMoveSelector<
52        S,
53        EitherMove<S, usize>,
54        EitherChangeMoveSelector<
55            S,
56            usize,
57            FromSolutionEntitySelector,
58            StaticTypedValueSelector<S, usize>,
59        >,
60        EitherSwapMoveSelector<S, usize, FromSolutionEntitySelector, FromSolutionEntitySelector>,
61    >,
62    SimulatedAnnealingAcceptor,
63    crate::phase::localsearch::AcceptedCountForager<S>,
64>;
65
66/// Monomorphized phase enum for config-driven basic solver.
67enum BasicLocalSearch<S: PlanningSolution>
68where
69    S::Score: Score,
70{
71    Default(DefaultLocalSearch<S>),
72    Config(ConfigLocalSearch<S>),
73}
74
75/// Problem specification for basic variable problems.
76///
77/// Passed to `run_solver` to provide problem-specific construction and local
78/// search phases for solutions using `#[basic_variable_config]`.
79pub struct BasicSpec<S> {
80    pub get_variable: fn(&S, usize) -> Option<usize>,
81    pub set_variable: fn(&mut S, usize, Option<usize>),
82    pub value_count: fn(&S) -> usize,
83    pub entity_count_fn: fn(&S) -> usize,
84    pub variable_field: &'static str,
85    pub descriptor_index: usize,
86}
87
88impl<S, C> ProblemSpec<S, C> for BasicSpec<S>
89where
90    S: PlanningSolution,
91    S::Score: Score + ParseableScore,
92    C: ConstraintSet<S, S::Score>,
93{
94    fn is_trivial(&self, solution: &S) -> bool {
95        (self.entity_count_fn)(solution) == 0 || (self.value_count)(solution) == 0
96    }
97
98    fn default_time_limit_secs(&self) -> u64 {
99        30
100    }
101
102    fn log_scale(&self, solution: &S) {
103        info!(
104            event = "solve_start",
105            entity_count = (self.entity_count_fn)(solution),
106            value_count = (self.value_count)(solution),
107        );
108    }
109
110    fn build_and_solve(
111        self,
112        director: ScoreDirector<S, C>,
113        config: &SolverConfig,
114        time_limit: Duration,
115        termination: AnyTermination<S, ScoreDirector<S, C>>,
116        terminate: Option<&AtomicBool>,
117        callback: impl Fn(&S) + Send + Sync,
118    ) -> SolveResult<S> {
119        let n_values = (self.value_count)(director.working_solution());
120        let values: Vec<usize> = (0..n_values).collect();
121        let entity_selector = FromSolutionEntitySelector::new(0);
122        let value_selector = StaticTypedValueSelector::new(values.clone());
123        let placer = QueuedEntityPlacer::new(
124            entity_selector,
125            value_selector,
126            self.get_variable,
127            self.set_variable,
128            0,
129            self.variable_field,
130        );
131        let construction = ConstructionHeuristicPhase::new(placer, BestFitForager::new());
132
133        let local_search = build_local_search::<S>(
134            config,
135            self.get_variable,
136            self.set_variable,
137            values,
138            self.variable_field,
139            self.descriptor_index,
140        );
141
142        match local_search {
143            BasicLocalSearch::Default(ls) => {
144                let solver = Solver::new(((), construction, ls))
145                    .with_termination(termination)
146                    .with_time_limit(time_limit)
147                    .with_best_solution_callback(callback);
148                if let Some(flag) = terminate {
149                    solver.with_terminate(flag).solve(director)
150                } else {
151                    solver.solve(director)
152                }
153            }
154            BasicLocalSearch::Config(ls) => {
155                let solver = Solver::new(((), construction, ls))
156                    .with_termination(termination)
157                    .with_time_limit(time_limit)
158                    .with_best_solution_callback(callback);
159                if let Some(flag) = terminate {
160                    solver.with_terminate(flag).solve(director)
161                } else {
162                    solver.solve(director)
163                }
164            }
165        }
166    }
167}
168
169/// Builds the local search phase from config or falls back to defaults.
170fn build_local_search<S>(
171    config: &SolverConfig,
172    get_variable: fn(&S, usize) -> Option<usize>,
173    set_variable: fn(&mut S, usize, Option<usize>),
174    values: Vec<usize>,
175    variable_field: &'static str,
176    descriptor_index: usize,
177) -> BasicLocalSearch<S>
178where
179    S: PlanningSolution,
180    S::Score: Score,
181{
182    // Find first local search phase config
183    let ls_config = config.phases.iter().find_map(|p| {
184        if let PhaseConfig::LocalSearch(ls) = p {
185            Some(ls)
186        } else {
187            None
188        }
189    });
190
191    let Some(ls) = ls_config else {
192        // No phases configured — use default SA + union(Change, Swap)
193        let change_selector = EitherChangeMoveSelector::simple(
194            get_variable,
195            set_variable,
196            descriptor_index,
197            variable_field,
198            values,
199        );
200        let swap_selector = EitherSwapMoveSelector::simple(
201            get_variable,
202            set_variable,
203            descriptor_index,
204            variable_field,
205        );
206        let move_selector = UnionMoveSelector::new(change_selector, swap_selector);
207        let acceptor = SimulatedAnnealingAcceptor::default();
208        let forager = crate::phase::localsearch::AcceptedCountForager::new(1);
209        return BasicLocalSearch::Default(LocalSearchPhase::new(
210            move_selector,
211            acceptor,
212            forager,
213            None,
214        ));
215    };
216
217    // Config-driven: build acceptor, forager, move selector from config
218    let acceptor = ls
219        .acceptor
220        .as_ref()
221        .map(|ac| AcceptorBuilder::build::<S>(ac))
222        .unwrap_or_else(|| AnyAcceptor::SimulatedAnnealing(SimulatedAnnealingAcceptor::default()));
223
224    let forager = ForagerBuilder::build::<S>(ls.forager.as_ref());
225
226    let ctx = BasicContext {
227        get_variable,
228        set_variable,
229        values,
230        descriptor_index,
231        variable_field,
232    };
233
234    let move_selector = BasicMoveSelectorBuilder::build(ls.move_selector.as_ref(), &ctx);
235
236    BasicLocalSearch::Config(LocalSearchPhase::new(
237        move_selector,
238        acceptor,
239        forager,
240        None,
241    ))
242}