Skip to main content

solverforge_solver/
basic.rs

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