Skip to main content

solverforge_solver/
run.rs

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