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::Score;
18use solverforge_scoring::{ConstraintSet, TypedScoreDirector};
19use tokio::sync::mpsc;
20use tracing::info;
21
22use crate::phase::basic::{BasicConstructionPhase, BasicLocalSearchPhase};
23use crate::scope::SolverScope;
24use crate::solver::Solver;
25use crate::termination::{
26    OrTermination, StepCountTermination, TimeTermination, UnimprovedStepCountTermination,
27    UnimprovedTimeTermination,
28};
29
30/// Default time limit in seconds.
31const DEFAULT_TIME_LIMIT_SECS: u64 = 30;
32
33/// Solves a basic variable problem using construction heuristic + late acceptance local search.
34///
35/// This function is called by macro-generated `solve()` methods for solutions
36/// using `#[basic_variable_config]`.
37///
38/// # Type Parameters
39///
40/// * `S` - The solution type (must implement `PlanningSolution`)
41/// * `C` - The constraint set type
42///
43/// # Arguments
44///
45/// * `solution` - The initial solution to solve
46/// * `finalize_fn` - Function to prepare derived fields before solving
47/// * `constraints_fn` - Function that creates the constraint set
48/// * `get_variable` - Gets the planning variable value for an entity
49/// * `set_variable` - Sets the planning variable value for an entity
50/// * `value_count` - Returns the number of valid values
51/// * `entity_count_fn` - Returns the number of entities
52/// * `_descriptor` - Solution descriptor (unused, for future extensions)
53/// * `_entity_count` - Entity count function (unused, for future extensions)
54/// * `_variable_field` - Variable field name (unused, for future extensions)
55/// * `_descriptor_index` - Descriptor index (unused, for future extensions)
56#[allow(clippy::too_many_arguments)]
57pub fn run_solver<S, C>(
58    solution: S,
59    finalize_fn: fn(&mut S),
60    constraints_fn: fn() -> C,
61    get_variable: fn(&S, usize) -> Option<usize>,
62    set_variable: fn(&mut S, usize, Option<usize>),
63    value_count: fn(&S) -> usize,
64    entity_count_fn: fn(&S) -> usize,
65    _descriptor: fn() -> SolutionDescriptor,
66    _entity_count: fn(&S, usize) -> usize,
67    _variable_field: &'static str,
68    _descriptor_index: usize,
69) -> S
70where
71    S: PlanningSolution,
72    S::Score: Score,
73    C: ConstraintSet<S, S::Score>,
74{
75    // Create a channel but ignore the receiver - no streaming needed
76    let (sender, _receiver) = mpsc::unbounded_channel();
77    run_solver_with_channel(
78        solution,
79        finalize_fn,
80        constraints_fn,
81        get_variable,
82        set_variable,
83        value_count,
84        entity_count_fn,
85        None,
86        sender,
87    )
88}
89
90/// Solves a basic variable problem with channel-based solution streaming.
91///
92/// Logs solver progress via `tracing`. Optionally accepts a termination flag.
93/// Solutions are sent through the channel as they improve.
94#[allow(clippy::too_many_arguments)]
95pub fn run_solver_with_channel<S, C>(
96    mut solution: S,
97    finalize_fn: fn(&mut S),
98    constraints_fn: fn() -> C,
99    get_variable: fn(&S, usize) -> Option<usize>,
100    set_variable: fn(&mut S, usize, Option<usize>),
101    value_count: fn(&S) -> usize,
102    entity_count_fn: fn(&S) -> usize,
103    terminate: Option<&AtomicBool>,
104    sender: mpsc::UnboundedSender<(S, S::Score)>,
105) -> S
106where
107    S: PlanningSolution,
108    S::Score: Score,
109    C: ConstraintSet<S, S::Score>,
110{
111    finalize_fn(&mut solution);
112
113    let config = SolverConfig::load("solver.toml").unwrap_or_default();
114    let n_entities = entity_count_fn(&solution);
115    let n_values = value_count(&solution);
116
117    info!(
118        event = "solve_start",
119        entity_count = n_entities,
120        value_count = n_values,
121    );
122
123    // Create score director
124    let constraints = constraints_fn();
125    let director = TypedScoreDirector::new(solution, constraints);
126
127    // Handle empty case - nothing to solve, return immediately
128    if n_entities == 0 || n_values == 0 {
129        let mut solver_scope = SolverScope::new(director);
130        solver_scope.start_solving();
131        let score = solver_scope.calculate_score();
132        info!(event = "solve_end", score = %score);
133        return solver_scope.take_best_or_working_solution();
134    }
135
136    // Build phases
137    let construction =
138        BasicConstructionPhase::new(get_variable, set_variable, entity_count_fn, value_count);
139
140    let local_search = BasicLocalSearchPhase::new(
141        get_variable,
142        set_variable,
143        entity_count_fn,
144        value_count,
145        sender,
146    );
147
148    // Build solver with termination configuration
149    let result = solve_with_termination(
150        director,
151        construction,
152        local_search,
153        terminate,
154        config.termination.as_ref(),
155    );
156
157    let score = result.score().unwrap_or_default();
158    info!(event = "solve_end", score = %score);
159    result
160}
161
162fn solve_with_termination<S, D, G, T, E, V>(
163    director: D,
164    construction: BasicConstructionPhase<S, G, T, E, V>,
165    local_search: BasicLocalSearchPhase<S, G, T, E, V>,
166    terminate: Option<&AtomicBool>,
167    term_config: Option<&solverforge_config::TerminationConfig>,
168) -> S
169where
170    S: PlanningSolution,
171    S::Score: Score,
172    D: solverforge_scoring::ScoreDirector<S>,
173    G: Fn(&S, usize) -> Option<usize> + Send,
174    T: Fn(&mut S, usize, Option<usize>) + Send,
175    E: Fn(&S) -> usize + Send,
176    V: Fn(&S) -> usize + Send,
177{
178    let time_limit = term_config
179        .and_then(|c| c.time_limit())
180        .unwrap_or(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS));
181    let time = TimeTermination::new(time_limit);
182
183    // Build termination based on config
184    if let Some(step_limit) = term_config.and_then(|c| c.step_count_limit) {
185        let step = StepCountTermination::new(step_limit);
186        let termination: OrTermination<_, S, D> = OrTermination::new((time, step));
187        build_and_solve(
188            construction,
189            local_search,
190            termination,
191            terminate,
192            director,
193            time_limit,
194        )
195    } else if let Some(unimproved_step_limit) =
196        term_config.and_then(|c| c.unimproved_step_count_limit)
197    {
198        let unimproved = UnimprovedStepCountTermination::<S>::new(unimproved_step_limit);
199        let termination: OrTermination<_, S, D> = OrTermination::new((time, unimproved));
200        build_and_solve(
201            construction,
202            local_search,
203            termination,
204            terminate,
205            director,
206            time_limit,
207        )
208    } else if let Some(unimproved_time) = term_config.and_then(|c| c.unimproved_time_limit()) {
209        let unimproved = UnimprovedTimeTermination::<S>::new(unimproved_time);
210        let termination: OrTermination<_, S, D> = OrTermination::new((time, unimproved));
211        build_and_solve(
212            construction,
213            local_search,
214            termination,
215            terminate,
216            director,
217            time_limit,
218        )
219    } else {
220        let termination: OrTermination<_, S, D> = OrTermination::new((time,));
221        build_and_solve(
222            construction,
223            local_search,
224            termination,
225            terminate,
226            director,
227            time_limit,
228        )
229    }
230}
231
232fn build_and_solve<S, D, G, T, E, V, Term>(
233    construction: BasicConstructionPhase<S, G, T, E, V>,
234    local_search: BasicLocalSearchPhase<S, G, T, E, V>,
235    termination: Term,
236    terminate: Option<&AtomicBool>,
237    director: D,
238    time_limit: Duration,
239) -> S
240where
241    S: PlanningSolution,
242    S::Score: Score,
243    D: solverforge_scoring::ScoreDirector<S>,
244    G: Fn(&S, usize) -> Option<usize> + Send,
245    T: Fn(&mut S, usize, Option<usize>) + Send,
246    E: Fn(&S) -> usize + Send,
247    V: Fn(&S) -> usize + Send,
248    Term: crate::termination::Termination<S, D>,
249{
250    match terminate {
251        Some(flag) => Solver::new((construction, local_search))
252            .with_termination(termination)
253            .with_time_limit(time_limit)
254            .with_terminate(flag)
255            .solve(director),
256        None => Solver::new((construction, local_search))
257            .with_termination(termination)
258            .with_time_limit(time_limit)
259            .solve(director),
260    }
261}