Skip to main content

solverforge_solver/
basic.rs

1//! Basic variable solver for simple assignment problems.
2//!
3//! This module provides `run_solver` 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::SolverConfig;
16use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
17use solverforge_core::score::{ParseableScore, Score};
18use solverforge_scoring::{ConstraintSet, TypedScoreDirector};
19use tokio::sync::mpsc;
20use tracing::info;
21
22use crate::heuristic::selector::decorator::UnionMoveSelector;
23use crate::heuristic::selector::{
24    EitherChangeMoveSelector, EitherSwapMoveSelector, FromSolutionEntitySelector,
25    StaticTypedValueSelector,
26};
27use crate::phase::construction::{BestFitForager, ConstructionHeuristicPhase, QueuedEntityPlacer};
28use crate::phase::localsearch::{
29    AcceptedCountForager, LocalSearchPhase, SimulatedAnnealingAcceptor,
30};
31use crate::scope::SolverScope;
32use crate::solver::{SolveResult, Solver};
33use crate::termination::{
34    BestScoreTermination, OrTermination, StepCountTermination, TimeTermination,
35    UnimprovedStepCountTermination, UnimprovedTimeTermination,
36};
37
38/// Default time limit in seconds.
39const DEFAULT_TIME_LIMIT_SECS: u64 = 30;
40
41/// Solves a basic variable problem using construction heuristic + late acceptance local search.
42///
43/// This function is called by macro-generated `solve()` methods for solutions
44/// using `#[basic_variable_config]`.
45///
46/// # Type Parameters
47///
48/// * `S` - The solution type (must implement `PlanningSolution`)
49/// * `C` - The constraint set type
50///
51/// # Arguments
52///
53/// * `solution` - The initial solution to solve
54/// * `finalize_fn` - Function to prepare derived fields before solving
55/// * `constraints_fn` - Function that creates the constraint set
56/// * `get_variable` - Gets the planning variable value for an entity
57/// * `set_variable` - Sets the planning variable value for an entity
58/// * `value_count` - Returns the number of valid values
59/// * `entity_count_fn` - Returns the number of entities
60/// * `descriptor` - Solution descriptor for solver infrastructure
61/// * `entity_count_by_descriptor` - Returns entity count for a given descriptor index
62/// * `terminate` - Optional external termination flag
63/// * `sender` - Channel for streaming best solutions as they are found
64/// * `_variable_field` - Variable field name (unused, for future extensions)
65/// * `_descriptor_index` - Descriptor index (unused, for future extensions)
66#[allow(clippy::too_many_arguments)]
67pub fn run_solver<S, C>(
68    mut solution: S,
69    finalize_fn: fn(&mut S),
70    constraints_fn: fn() -> C,
71    get_variable: fn(&S, usize) -> Option<usize>,
72    set_variable: fn(&mut S, usize, Option<usize>),
73    value_count: fn(&S) -> usize,
74    entity_count_fn: fn(&S) -> usize,
75    descriptor: fn() -> SolutionDescriptor,
76    entity_count_by_descriptor: fn(&S, usize) -> usize,
77    terminate: Option<&AtomicBool>,
78    sender: mpsc::UnboundedSender<(S, S::Score)>,
79    _variable_field: &'static str,
80    _descriptor_index: usize,
81) -> S
82where
83    S: PlanningSolution,
84    S::Score: Score + ParseableScore,
85    C: ConstraintSet<S, S::Score>,
86{
87    finalize_fn(&mut solution);
88
89    let config = SolverConfig::load("solver.toml").unwrap_or_default();
90    let n_entities = entity_count_fn(&solution);
91    let n_values = value_count(&solution);
92
93    info!(
94        event = "solve_start",
95        entity_count = n_entities,
96        value_count = n_values,
97    );
98
99    // Create score director with real entity counter for selector iteration
100    let constraints = constraints_fn();
101    let director = TypedScoreDirector::with_descriptor(
102        solution,
103        constraints,
104        descriptor(),
105        entity_count_by_descriptor,
106    );
107
108    // Handle empty case - nothing to solve, return immediately
109    if n_entities == 0 || n_values == 0 {
110        let mut solver_scope = SolverScope::new(director);
111        solver_scope.start_solving();
112        let score = solver_scope.calculate_score();
113        info!(event = "solve_end", score = %score);
114        let solution = solver_scope.take_best_or_working_solution();
115        let _ = sender.send((solution.clone(), score));
116        return solution;
117    }
118
119    // Build construction phase with BestFitForager
120    let values: Vec<usize> = (0..n_values).collect();
121    let entity_selector = FromSolutionEntitySelector::new(0);
122    let value_selector = StaticTypedValueSelector::new(values);
123    let placer = QueuedEntityPlacer::new(
124        entity_selector,
125        value_selector,
126        get_variable,
127        set_variable,
128        0,
129        "variable",
130    );
131    let construction = ConstructionHeuristicPhase::new(placer, BestFitForager::new());
132
133    // Build local search phase with Late Acceptance
134    // Unified move selector: ChangeMove + SwapMove via EitherMove
135    let values: Vec<usize> = (0..n_values).collect();
136    let change_selector =
137        EitherChangeMoveSelector::simple(get_variable, set_variable, 0, "variable", values);
138    let swap_selector = EitherSwapMoveSelector::simple(get_variable, set_variable, 0, "variable");
139    let move_selector = UnionMoveSelector::new(change_selector, swap_selector);
140    let acceptor = SimulatedAnnealingAcceptor::default();
141    let forager = AcceptedCountForager::new(1);
142    let local_search = LocalSearchPhase::new(move_selector, acceptor, forager, None);
143
144    // Build solver with termination configuration
145    let result = solve_with_termination(
146        director,
147        construction,
148        local_search,
149        terminate,
150        config.termination.as_ref(),
151        sender,
152    );
153
154    let score = result.solution.score().unwrap_or_default();
155    info!(
156        event = "solve_end",
157        score = %score,
158        steps = result.stats.step_count,
159        moves_evaluated = result.stats.moves_evaluated,
160    );
161    result.solution
162}
163
164macro_rules! build_and_solve_with {
165    ($construction:expr, $local_search:expr, $termination:expr, $terminate:expr, $director:expr, $time_limit:expr, $sender:expr) => {
166        build_and_solve(
167            $construction,
168            $local_search,
169            $termination,
170            $terminate,
171            $director,
172            $time_limit,
173            $sender,
174        )
175    };
176}
177
178fn solve_with_termination<S, D, M1, M2, P, Fo, MS, A, Fo2>(
179    director: D,
180    construction: ConstructionHeuristicPhase<S, M1, P, Fo>,
181    local_search: LocalSearchPhase<S, M2, MS, A, Fo2>,
182    terminate: Option<&AtomicBool>,
183    term_config: Option<&solverforge_config::TerminationConfig>,
184    sender: mpsc::UnboundedSender<(S, S::Score)>,
185) -> SolveResult<S>
186where
187    S: PlanningSolution,
188    S::Score: Score + ParseableScore,
189    D: solverforge_scoring::ScoreDirector<S>,
190    M1: crate::heuristic::r#move::Move<S>,
191    M2: crate::heuristic::r#move::Move<S>,
192    P: crate::phase::construction::EntityPlacer<S, M1>,
193    Fo: crate::phase::construction::ConstructionForager<S, M1>,
194    MS: crate::heuristic::selector::MoveSelector<S, M2>,
195    A: crate::phase::localsearch::Acceptor<S>,
196    Fo2: crate::phase::localsearch::LocalSearchForager<S, M2>,
197{
198    let time_limit = term_config
199        .and_then(|c| c.time_limit())
200        .unwrap_or(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS));
201    let time = TimeTermination::new(time_limit);
202
203    let best_score_target: Option<S::Score> = term_config
204        .and_then(|c| c.best_score_limit.as_ref())
205        .and_then(|s| S::Score::parse(s).ok());
206
207    if let Some(target) = best_score_target {
208        build_and_solve_with!(
209            construction,
210            local_search,
211            OrTermination::<_, S, D>::new((time, BestScoreTermination::new(target))),
212            terminate,
213            director,
214            time_limit,
215            sender
216        )
217    } else if let Some(step_limit) = term_config.and_then(|c| c.step_count_limit) {
218        build_and_solve_with!(
219            construction,
220            local_search,
221            OrTermination::<_, S, D>::new((time, StepCountTermination::new(step_limit))),
222            terminate,
223            director,
224            time_limit,
225            sender
226        )
227    } else if let Some(unimproved_step_limit) =
228        term_config.and_then(|c| c.unimproved_step_count_limit)
229    {
230        build_and_solve_with!(
231            construction,
232            local_search,
233            OrTermination::<_, S, D>::new((
234                time,
235                UnimprovedStepCountTermination::<S>::new(unimproved_step_limit)
236            )),
237            terminate,
238            director,
239            time_limit,
240            sender
241        )
242    } else if let Some(unimproved_time) = term_config.and_then(|c| c.unimproved_time_limit()) {
243        build_and_solve_with!(
244            construction,
245            local_search,
246            OrTermination::<_, S, D>::new((
247                time,
248                UnimprovedTimeTermination::<S>::new(unimproved_time)
249            )),
250            terminate,
251            director,
252            time_limit,
253            sender
254        )
255    } else {
256        build_and_solve_with!(
257            construction,
258            local_search,
259            OrTermination::<_, S, D>::new((time,)),
260            terminate,
261            director,
262            time_limit,
263            sender
264        )
265    }
266}
267
268fn build_and_solve<S, D, M1, M2, P, Fo, MS, A, Fo2, Term>(
269    construction: ConstructionHeuristicPhase<S, M1, P, Fo>,
270    local_search: LocalSearchPhase<S, M2, MS, A, Fo2>,
271    termination: Term,
272    terminate: Option<&AtomicBool>,
273    director: D,
274    time_limit: Duration,
275    sender: mpsc::UnboundedSender<(S, S::Score)>,
276) -> SolveResult<S>
277where
278    S: PlanningSolution,
279    S::Score: Score + ParseableScore,
280    D: solverforge_scoring::ScoreDirector<S>,
281    M1: crate::heuristic::r#move::Move<S>,
282    M2: crate::heuristic::r#move::Move<S>,
283    P: crate::phase::construction::EntityPlacer<S, M1>,
284    Fo: crate::phase::construction::ConstructionForager<S, M1>,
285    MS: crate::heuristic::selector::MoveSelector<S, M2>,
286    A: crate::phase::localsearch::Acceptor<S>,
287    Fo2: crate::phase::localsearch::LocalSearchForager<S, M2>,
288    Term: crate::termination::Termination<S, D>,
289{
290    let callback_sender = sender.clone();
291    let callback: Box<dyn Fn(&S) + Send + Sync> = Box::new(move |solution: &S| {
292        let score = solution.score().unwrap_or_default();
293        let _ = callback_sender.send((solution.clone(), score));
294    });
295
296    let result = match terminate {
297        Some(flag) => Solver::new(((), construction, local_search))
298            .with_termination(termination)
299            .with_time_limit(time_limit)
300            .with_best_solution_callback(callback)
301            .with_terminate(flag)
302            .solve(director),
303        None => Solver::new(((), construction, local_search))
304            .with_termination(termination)
305            .with_time_limit(time_limit)
306            .with_best_solution_callback(callback)
307            .solve(director),
308    };
309
310    // Send the final solution so the receiver always gets the last state
311    let final_score = result.solution.score().unwrap_or_default();
312    let _ = sender.send((result.solution.clone(), final_score));
313
314    result
315}