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/// * `_variable_field` - Variable field name (unused, for future extensions)
63/// * `_descriptor_index` - Descriptor index (unused, for future extensions)
64#[allow(clippy::too_many_arguments)]
65pub fn run_solver<S, C>(
66    solution: S,
67    finalize_fn: fn(&mut S),
68    constraints_fn: fn() -> C,
69    get_variable: fn(&S, usize) -> Option<usize>,
70    set_variable: fn(&mut S, usize, Option<usize>),
71    value_count: fn(&S) -> usize,
72    entity_count_fn: fn(&S) -> usize,
73    descriptor: fn() -> SolutionDescriptor,
74    entity_count_by_descriptor: fn(&S, usize) -> usize,
75    _variable_field: &'static str,
76    _descriptor_index: usize,
77) -> S
78where
79    S: PlanningSolution,
80    S::Score: Score + ParseableScore,
81    C: ConstraintSet<S, S::Score>,
82{
83    // Create a channel but ignore the receiver - no streaming needed
84    let (sender, _receiver) = mpsc::unbounded_channel();
85    run_solver_with_channel(
86        solution,
87        finalize_fn,
88        constraints_fn,
89        get_variable,
90        set_variable,
91        value_count,
92        entity_count_fn,
93        descriptor,
94        entity_count_by_descriptor,
95        None,
96        sender,
97    )
98}
99
100/// Solves a basic variable problem with channel-based solution streaming.
101///
102/// Logs solver progress via `tracing`. Optionally accepts a termination flag.
103/// Solutions are sent through the channel as they improve.
104#[allow(clippy::too_many_arguments)]
105pub fn run_solver_with_channel<S, C>(
106    mut solution: S,
107    finalize_fn: fn(&mut S),
108    constraints_fn: fn() -> C,
109    get_variable: fn(&S, usize) -> Option<usize>,
110    set_variable: fn(&mut S, usize, Option<usize>),
111    value_count: fn(&S) -> usize,
112    entity_count_fn: fn(&S) -> usize,
113    descriptor: fn() -> SolutionDescriptor,
114    entity_count_by_descriptor: fn(&S, usize) -> usize,
115    terminate: Option<&AtomicBool>,
116    sender: mpsc::UnboundedSender<(S, S::Score)>,
117) -> S
118where
119    S: PlanningSolution,
120    S::Score: Score + ParseableScore,
121    C: ConstraintSet<S, S::Score>,
122{
123    finalize_fn(&mut solution);
124
125    let config = SolverConfig::load("solver.toml").unwrap_or_default();
126    let n_entities = entity_count_fn(&solution);
127    let n_values = value_count(&solution);
128
129    info!(
130        event = "solve_start",
131        entity_count = n_entities,
132        value_count = n_values,
133    );
134
135    // Create score director with real entity counter for selector iteration
136    let constraints = constraints_fn();
137    let director = TypedScoreDirector::with_descriptor(
138        solution,
139        constraints,
140        descriptor(),
141        entity_count_by_descriptor,
142    );
143
144    // Handle empty case - nothing to solve, return immediately
145    if n_entities == 0 || n_values == 0 {
146        let mut solver_scope = SolverScope::new(director);
147        solver_scope.start_solving();
148        let score = solver_scope.calculate_score();
149        info!(event = "solve_end", score = %score);
150        let solution = solver_scope.take_best_or_working_solution();
151        let _ = sender.send((solution.clone(), score));
152        return solution;
153    }
154
155    // Build construction phase with BestFitForager
156    let values: Vec<usize> = (0..n_values).collect();
157    let entity_selector = FromSolutionEntitySelector::new(0);
158    let value_selector = StaticTypedValueSelector::new(values);
159    let placer = QueuedEntityPlacer::new(
160        entity_selector,
161        value_selector,
162        get_variable,
163        set_variable,
164        0,
165        "variable",
166    );
167    let construction = ConstructionHeuristicPhase::new(placer, BestFitForager::new());
168
169    // Build local search phase with Late Acceptance
170    // Unified move selector: ChangeMove + SwapMove via EitherMove
171    let values: Vec<usize> = (0..n_values).collect();
172    let change_selector =
173        EitherChangeMoveSelector::simple(get_variable, set_variable, 0, "variable", values);
174    let swap_selector = EitherSwapMoveSelector::simple(get_variable, set_variable, 0, "variable");
175    let move_selector = UnionMoveSelector::new(change_selector, swap_selector);
176    let acceptor = SimulatedAnnealingAcceptor::default();
177    let forager = AcceptedCountForager::new(1);
178    let local_search = LocalSearchPhase::new(move_selector, acceptor, forager, None);
179
180    // Build solver with termination configuration
181    let result = solve_with_termination(
182        director,
183        construction,
184        local_search,
185        terminate,
186        config.termination.as_ref(),
187        sender,
188    );
189
190    let score = result.solution.score().unwrap_or_default();
191    info!(
192        event = "solve_end",
193        score = %score,
194        steps = result.stats.step_count,
195        moves_evaluated = result.stats.moves_evaluated,
196    );
197    result.solution
198}
199
200fn solve_with_termination<S, D, M1, M2, P, Fo, MS, A, Fo2>(
201    director: D,
202    construction: ConstructionHeuristicPhase<S, M1, P, Fo>,
203    local_search: LocalSearchPhase<S, M2, MS, A, Fo2>,
204    terminate: Option<&AtomicBool>,
205    term_config: Option<&solverforge_config::TerminationConfig>,
206    sender: mpsc::UnboundedSender<(S, S::Score)>,
207) -> SolveResult<S>
208where
209    S: PlanningSolution,
210    S::Score: Score + ParseableScore,
211    D: solverforge_scoring::ScoreDirector<S>,
212    M1: crate::heuristic::r#move::Move<S>,
213    M2: crate::heuristic::r#move::Move<S>,
214    P: crate::phase::construction::EntityPlacer<S, M1>,
215    Fo: crate::phase::construction::ConstructionForager<S, M1>,
216    MS: crate::heuristic::selector::MoveSelector<S, M2>,
217    A: crate::phase::localsearch::Acceptor<S>,
218    Fo2: crate::phase::localsearch::LocalSearchForager<S, M2>,
219{
220    let time_limit = term_config
221        .and_then(|c| c.time_limit())
222        .unwrap_or(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS));
223    let time = TimeTermination::new(time_limit);
224
225    // Check best_score_limit (T2: parse and wire to BestScoreTermination)
226    let best_score_target: Option<S::Score> = term_config
227        .and_then(|c| c.best_score_limit.as_ref())
228        .and_then(|s| S::Score::parse(s).ok());
229
230    // Build termination based on config
231    if let Some(target) = best_score_target {
232        let best_score = BestScoreTermination::new(target);
233        let termination: OrTermination<_, S, D> = OrTermination::new((time, best_score));
234        build_and_solve(
235            construction,
236            local_search,
237            termination,
238            terminate,
239            director,
240            time_limit,
241            sender,
242        )
243    } else if let Some(step_limit) = term_config.and_then(|c| c.step_count_limit) {
244        let step = StepCountTermination::new(step_limit);
245        let termination: OrTermination<_, S, D> = OrTermination::new((time, step));
246        build_and_solve(
247            construction,
248            local_search,
249            termination,
250            terminate,
251            director,
252            time_limit,
253            sender,
254        )
255    } else if let Some(unimproved_step_limit) =
256        term_config.and_then(|c| c.unimproved_step_count_limit)
257    {
258        let unimproved = UnimprovedStepCountTermination::<S>::new(unimproved_step_limit);
259        let termination: OrTermination<_, S, D> = OrTermination::new((time, unimproved));
260        build_and_solve(
261            construction,
262            local_search,
263            termination,
264            terminate,
265            director,
266            time_limit,
267            sender,
268        )
269    } else if let Some(unimproved_time) = term_config.and_then(|c| c.unimproved_time_limit()) {
270        let unimproved = UnimprovedTimeTermination::<S>::new(unimproved_time);
271        let termination: OrTermination<_, S, D> = OrTermination::new((time, unimproved));
272        build_and_solve(
273            construction,
274            local_search,
275            termination,
276            terminate,
277            director,
278            time_limit,
279            sender,
280        )
281    } else {
282        let termination: OrTermination<_, S, D> = OrTermination::new((time,));
283        build_and_solve(
284            construction,
285            local_search,
286            termination,
287            terminate,
288            director,
289            time_limit,
290            sender,
291        )
292    }
293}
294
295fn build_and_solve<S, D, M1, M2, P, Fo, MS, A, Fo2, Term>(
296    construction: ConstructionHeuristicPhase<S, M1, P, Fo>,
297    local_search: LocalSearchPhase<S, M2, MS, A, Fo2>,
298    termination: Term,
299    terminate: Option<&AtomicBool>,
300    director: D,
301    time_limit: Duration,
302    sender: mpsc::UnboundedSender<(S, S::Score)>,
303) -> SolveResult<S>
304where
305    S: PlanningSolution,
306    S::Score: Score + ParseableScore,
307    D: solverforge_scoring::ScoreDirector<S>,
308    M1: crate::heuristic::r#move::Move<S>,
309    M2: crate::heuristic::r#move::Move<S>,
310    P: crate::phase::construction::EntityPlacer<S, M1>,
311    Fo: crate::phase::construction::ConstructionForager<S, M1>,
312    MS: crate::heuristic::selector::MoveSelector<S, M2>,
313    A: crate::phase::localsearch::Acceptor<S>,
314    Fo2: crate::phase::localsearch::LocalSearchForager<S, M2>,
315    Term: crate::termination::Termination<S, D>,
316{
317    let callback_sender = sender.clone();
318    let callback: Box<dyn Fn(&S) + Send + Sync> = Box::new(move |solution: &S| {
319        let score = solution.score().unwrap_or_default();
320        let _ = callback_sender.send((solution.clone(), score));
321    });
322
323    let result = match terminate {
324        Some(flag) => Solver::new(((), construction, local_search))
325            .with_termination(termination)
326            .with_time_limit(time_limit)
327            .with_terminate(flag)
328            .with_best_solution_callback(callback)
329            .solve(director),
330        None => Solver::new(((), construction, local_search))
331            .with_termination(termination)
332            .with_time_limit(time_limit)
333            .with_best_solution_callback(callback)
334            .solve(director),
335    };
336
337    // Send the final solution so the receiver always gets the last state
338    let final_score = result.solution.score().unwrap_or_default();
339    let _ = sender.send((result.solution.clone(), final_score));
340
341    result
342}