Skip to main content

solverforge_solver/
run.rs

1/* Unified solver entry point.
2
3This module provides the single `run_solver` function used by both basic
4variable and list variable problems via the `ProblemSpec` trait.
5*/
6
7use std::fmt;
8use std::sync::atomic::AtomicBool;
9use std::time::Duration;
10
11use solverforge_config::SolverConfig;
12use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
13use solverforge_core::score::{ParseableScore, Score};
14use solverforge_scoring::{ConstraintSet, Director, ScoreDirector};
15use tokio::sync::mpsc;
16use tracing::info;
17
18use crate::problem_spec::ProblemSpec;
19use crate::scope::{BestSolutionCallback, SolverScope};
20use crate::termination::{
21    BestScoreTermination, OrTermination, StepCountTermination, Termination, TimeTermination,
22    UnimprovedStepCountTermination, UnimprovedTimeTermination,
23};
24
25/// Monomorphized termination enum for config-driven solver configurations.
26///
27/// Avoids repeated branching across termination overloads by capturing the
28/// selected termination variant upfront.
29pub enum AnyTermination<S: PlanningSolution, D: Director<S>> {
30    Default(OrTermination<(TimeTermination,), S, D>),
31    WithBestScore(OrTermination<(TimeTermination, BestScoreTermination<S::Score>), S, D>),
32    WithStepCount(OrTermination<(TimeTermination, StepCountTermination), S, D>),
33    WithUnimprovedStep(OrTermination<(TimeTermination, UnimprovedStepCountTermination<S>), S, D>),
34    WithUnimprovedTime(OrTermination<(TimeTermination, UnimprovedTimeTermination<S>), S, D>),
35}
36
37impl<S: PlanningSolution, D: Director<S>> fmt::Debug for AnyTermination<S, D> {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::Default(_) => write!(f, "AnyTermination::Default"),
41            Self::WithBestScore(_) => write!(f, "AnyTermination::WithBestScore"),
42            Self::WithStepCount(_) => write!(f, "AnyTermination::WithStepCount"),
43            Self::WithUnimprovedStep(_) => write!(f, "AnyTermination::WithUnimprovedStep"),
44            Self::WithUnimprovedTime(_) => write!(f, "AnyTermination::WithUnimprovedTime"),
45        }
46    }
47}
48
49impl<S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>> Termination<S, D, BestCb>
50    for AnyTermination<S, D>
51where
52    S::Score: Score,
53{
54    fn is_terminated(&self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
55        match self {
56            Self::Default(t) => t.is_terminated(solver_scope),
57            Self::WithBestScore(t) => t.is_terminated(solver_scope),
58            Self::WithStepCount(t) => t.is_terminated(solver_scope),
59            Self::WithUnimprovedStep(t) => t.is_terminated(solver_scope),
60            Self::WithUnimprovedTime(t) => t.is_terminated(solver_scope),
61        }
62    }
63
64    fn install_inphase_limits(&self, solver_scope: &mut SolverScope<S, D, BestCb>) {
65        match self {
66            Self::Default(t) => t.install_inphase_limits(solver_scope),
67            Self::WithBestScore(t) => t.install_inphase_limits(solver_scope),
68            Self::WithStepCount(t) => t.install_inphase_limits(solver_scope),
69            Self::WithUnimprovedStep(t) => t.install_inphase_limits(solver_scope),
70            Self::WithUnimprovedTime(t) => t.install_inphase_limits(solver_scope),
71        }
72    }
73}
74
75/// Builds a termination from config, returning both the termination and the time limit.
76pub fn build_termination<S, C>(
77    config: &SolverConfig,
78    default_secs: u64,
79) -> (AnyTermination<S, ScoreDirector<S, C>>, Duration)
80where
81    S: PlanningSolution,
82    S::Score: Score + ParseableScore,
83    C: ConstraintSet<S, S::Score>,
84{
85    let term_config = config.termination.as_ref();
86    let time_limit = term_config
87        .and_then(|c| c.time_limit())
88        .unwrap_or(Duration::from_secs(default_secs));
89    let time = TimeTermination::new(time_limit);
90
91    let best_score_target: Option<S::Score> = term_config
92        .and_then(|c| c.best_score_limit.as_ref())
93        .and_then(|s| S::Score::parse(s).ok());
94
95    let termination = if let Some(target) = best_score_target {
96        AnyTermination::WithBestScore(OrTermination::new((
97            time,
98            BestScoreTermination::new(target),
99        )))
100    } else if let Some(step_limit) = term_config.and_then(|c| c.step_count_limit) {
101        AnyTermination::WithStepCount(OrTermination::new((
102            time,
103            StepCountTermination::new(step_limit),
104        )))
105    } else if let Some(unimproved_step_limit) =
106        term_config.and_then(|c| c.unimproved_step_count_limit)
107    {
108        AnyTermination::WithUnimprovedStep(OrTermination::new((
109            time,
110            UnimprovedStepCountTermination::<S>::new(unimproved_step_limit),
111        )))
112    } else if let Some(unimproved_time) = term_config.and_then(|c| c.unimproved_time_limit()) {
113        AnyTermination::WithUnimprovedTime(OrTermination::new((
114            time,
115            UnimprovedTimeTermination::<S>::new(unimproved_time),
116        )))
117    } else {
118        AnyTermination::Default(OrTermination::new((time,)))
119    };
120
121    (termination, time_limit)
122}
123
124/* Solves a problem using the given `ProblemSpec` for problem-specific logic.
125
126This is the unified entry point for both basic variable and list variable
127problems. The shared logic (config loading, director creation, trivial-case
128handling, termination building, callback setup, final send) lives here.
129Problem-specific construction and local search are delegated to `spec`.
130*/
131#[allow(clippy::too_many_arguments)]
132pub fn run_solver<S, C, Spec>(
133    mut solution: S,
134    finalize_fn: fn(&mut S),
135    constraints_fn: fn() -> C,
136    descriptor: fn() -> SolutionDescriptor,
137    entity_count_by_descriptor: fn(&S, usize) -> usize,
138    terminate: Option<&AtomicBool>,
139    sender: mpsc::UnboundedSender<(S, S::Score)>,
140    spec: Spec,
141) -> S
142where
143    S: PlanningSolution,
144    S::Score: Score + ParseableScore,
145    C: ConstraintSet<S, S::Score>,
146    Spec: ProblemSpec<S, C>,
147{
148    finalize_fn(&mut solution);
149
150    let config = SolverConfig::load("solver.toml").unwrap_or_default();
151
152    spec.log_scale(&solution);
153    let trivial = spec.is_trivial(&solution);
154
155    let constraints = constraints_fn();
156    let director = ScoreDirector::with_descriptor(
157        solution,
158        constraints,
159        descriptor(),
160        entity_count_by_descriptor,
161    );
162
163    if trivial {
164        let mut solver_scope = SolverScope::new(director);
165        solver_scope.start_solving();
166        let score = solver_scope.calculate_score();
167        info!(event = "solve_end", score = %score);
168        let solution = solver_scope.take_best_or_working_solution();
169        let _ = sender.send((solution.clone(), score));
170        return solution;
171    }
172
173    let (termination, time_limit) =
174        build_termination::<S, C>(&config, spec.default_time_limit_secs());
175
176    let callback_sender = sender.clone();
177    let callback = move |solution: &S| {
178        let score = solution.score().unwrap_or_default();
179        let _ = callback_sender.send((solution.clone(), score));
180    };
181
182    let result = spec.build_and_solve(
183        director,
184        &config,
185        time_limit,
186        termination,
187        terminate,
188        callback,
189    );
190
191    let final_score = result.solution.score().unwrap_or_default();
192    let _ = sender.send((result.solution.clone(), final_score));
193
194    info!(
195        event = "solve_end",
196        score = %final_score,
197        steps = result.stats.step_count,
198        moves_evaluated = result.stats.moves_evaluated,
199    );
200    result.solution
201}