Skip to main content

solverforge_solver/scope/solver/
scope_core.rs

1pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
2    score_director: D,
3    best_solution: Option<S>,
4    current_score: Option<S::Score>,
5    best_score: Option<S::Score>,
6    rng: StdRng,
7    start_time: Option<Instant>,
8    paused_at: Option<Instant>,
9    total_step_count: u64,
10    terminate: Option<&'t AtomicBool>,
11    runtime: Option<SolverRuntime<S>>,
12    publication: Publication,
13    yielded_to_parent: bool,
14    environment_mode: EnvironmentMode,
15    stats: SolverStats,
16    time_limit: Option<Duration>,
17    time_deadline: Option<Instant>,
18    progress_callback: ProgressCb,
19    terminal_reason: Option<SolverTerminalReason>,
20    last_best_elapsed: Option<Duration>,
21    best_solution_revision: Option<u64>,
22    solution_revision: u64,
23    construction_frontier: ConstructionFrontier,
24    phase_budget: Option<&'t PhaseBudget>,
25    pub inphase_step_count_limit: Option<u64>,
26    pub inphase_move_count_limit: Option<u64>,
27    pub inphase_score_calc_count_limit: Option<u64>,
28    inphase_best_score_limit: Option<S::Score>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum Publication {
33    Enabled,
34    Disabled,
35}
36
37pub(crate) struct PhaseBudget {
38    step_count_limit: Option<u64>,
39    move_count_limit: Option<u64>,
40    score_calc_count_limit: Option<u64>,
41    step_count: AtomicU64,
42    moves_evaluated: AtomicU64,
43    score_calculations: AtomicU64,
44}
45
46impl PhaseBudget {
47    fn from_scope<S, D, ProgressCb>(scope: &SolverScope<'_, S, D, ProgressCb>) -> Self
48    where
49        S: PlanningSolution,
50        D: Director<S>,
51        ProgressCb: ProgressCallback<S>,
52    {
53        Self {
54            step_count_limit: remaining_limit(
55                scope.inphase_step_count_limit,
56                scope.total_step_count,
57            ),
58            move_count_limit: remaining_limit(
59                scope.inphase_move_count_limit,
60                scope.stats.moves_evaluated,
61            ),
62            score_calc_count_limit: remaining_limit(
63                scope.inphase_score_calc_count_limit,
64                scope.stats.score_calculations,
65            ),
66            step_count: AtomicU64::new(0),
67            moves_evaluated: AtomicU64::new(0),
68            score_calculations: AtomicU64::new(0),
69        }
70    }
71
72    fn has_limits(&self) -> bool {
73        self.step_count_limit.is_some()
74            || self.move_count_limit.is_some()
75            || self.score_calc_count_limit.is_some()
76    }
77
78    fn record_step(&self) {
79        self.step_count.fetch_add(1, Ordering::SeqCst);
80    }
81
82    fn record_evaluated_move(&self) {
83        self.moves_evaluated.fetch_add(1, Ordering::SeqCst);
84    }
85
86    fn record_score_calculation(&self) {
87        self.score_calculations.fetch_add(1, Ordering::SeqCst);
88    }
89
90    fn limit_reached(&self) -> bool {
91        limit_reached(self.step_count_limit, self.step_count.load(Ordering::SeqCst))
92            || limit_reached(
93                self.move_count_limit,
94                self.moves_evaluated.load(Ordering::SeqCst),
95            )
96            || limit_reached(
97                self.score_calc_count_limit,
98                self.score_calculations.load(Ordering::SeqCst),
99            )
100    }
101}
102
103fn remaining_limit(limit: Option<u64>, used: u64) -> Option<u64> {
104    limit.map(|limit| limit.saturating_sub(used))
105}
106
107fn limit_reached(limit: Option<u64>, used: u64) -> bool {
108    limit.is_some_and(|limit| used >= limit)
109}
110
111#[derive(Clone, Copy)]
112pub(crate) struct SolverScopeChildConfig<'t, S: PlanningSolution> {
113    terminate: Option<&'t AtomicBool>,
114    runtime: Option<SolverRuntime<S>>,
115    environment_mode: EnvironmentMode,
116    time_deadline: Option<Instant>,
117    phase_budget: Option<&'t PhaseBudget>,
118    inphase_step_count_limit: Option<u64>,
119    inphase_move_count_limit: Option<u64>,
120    inphase_score_calc_count_limit: Option<u64>,
121    inphase_best_score_limit: Option<S::Score>,
122}
123
124impl<'t, S: PlanningSolution> SolverScopeChildConfig<'t, S> {
125    pub(crate) fn build_scope<PD>(&self, score_director: PD, seed: u64) -> SolverScope<'t, S, PD>
126    where
127        PD: Director<S>,
128    {
129        let terminate = self
130            .terminate
131            .or_else(|| self.runtime.map(|runtime| runtime.cancel_flag()));
132        let mut scope = SolverScope::new(score_director)
133            .with_terminate(terminate)
134            .with_runtime(self.runtime)
135            .without_publication()
136            .with_environment_mode(self.environment_mode)
137            .with_seed(seed);
138        scope.time_deadline = self.time_deadline;
139        scope.phase_budget = self.phase_budget;
140        if self.phase_budget.is_none() {
141            scope.inphase_step_count_limit = self.inphase_step_count_limit;
142            scope.inphase_move_count_limit = self.inphase_move_count_limit;
143            scope.inphase_score_calc_count_limit = self.inphase_score_calc_count_limit;
144        }
145        scope.inphase_best_score_limit = self.inphase_best_score_limit;
146        scope
147    }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub(crate) enum PendingControl {
152    Continue,
153    PauseRequested,
154    CancelRequested,
155    ConfigTerminationRequested,
156}
157
158impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
159    pub fn new(score_director: D) -> Self {
160        let construction_frontier = ConstructionFrontier::new();
161        Self {
162            score_director,
163            best_solution: None,
164            current_score: None,
165            best_score: None,
166            rng: StdRng::from_rng(&mut rand::rng()),
167            start_time: None,
168            paused_at: None,
169            total_step_count: 0,
170            terminate: None,
171            runtime: None,
172            publication: Publication::Enabled,
173            yielded_to_parent: false,
174            environment_mode: EnvironmentMode::default(),
175            stats: SolverStats::default(),
176            time_limit: None,
177            time_deadline: None,
178            progress_callback: (),
179            terminal_reason: None,
180            last_best_elapsed: None,
181            best_solution_revision: None,
182            solution_revision: 1,
183            construction_frontier,
184            phase_budget: None,
185            inphase_step_count_limit: None,
186            inphase_move_count_limit: None,
187            inphase_score_calc_count_limit: None,
188            inphase_best_score_limit: None,
189        }
190    }
191}
192
193impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
194    SolverScope<'t, S, D, ProgressCb>
195{
196    pub fn new_with_callback(
197        score_director: D,
198        callback: ProgressCb,
199        terminate: Option<&'t AtomicBool>,
200        runtime: Option<SolverRuntime<S>>,
201    ) -> Self {
202        let construction_frontier = ConstructionFrontier::new();
203        Self {
204            score_director,
205            best_solution: None,
206            current_score: None,
207            best_score: None,
208            rng: StdRng::from_rng(&mut rand::rng()),
209            start_time: None,
210            paused_at: None,
211            total_step_count: 0,
212            terminate,
213            runtime,
214            publication: Publication::Enabled,
215            yielded_to_parent: false,
216            environment_mode: EnvironmentMode::default(),
217            stats: SolverStats::default(),
218            time_limit: None,
219            time_deadline: None,
220            progress_callback: callback,
221            terminal_reason: None,
222            last_best_elapsed: None,
223            best_solution_revision: None,
224            solution_revision: 1,
225            construction_frontier,
226            phase_budget: None,
227            inphase_step_count_limit: None,
228            inphase_move_count_limit: None,
229            inphase_score_calc_count_limit: None,
230            inphase_best_score_limit: None,
231        }
232    }
233
234    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
235        self.terminate = terminate;
236        self
237    }
238
239    pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
240        self.runtime = runtime;
241        self
242    }
243
244    pub(crate) fn without_publication(mut self) -> Self {
245        self.publication = Publication::Disabled;
246        self
247    }
248
249    pub(crate) fn yielded_to_parent(&self) -> bool {
250        self.yielded_to_parent
251    }
252
253    pub fn with_environment_mode(mut self, environment_mode: EnvironmentMode) -> Self {
254        self.environment_mode = environment_mode;
255        self
256    }
257
258    pub fn with_seed(mut self, seed: u64) -> Self {
259        self.rng = StdRng::seed_from_u64(seed);
260        self
261    }
262
263    pub(crate) fn child_phase_budget(&self) -> PhaseBudget {
264        PhaseBudget::from_scope(self)
265    }
266
267    pub(crate) fn child_config<'a>(
268        &'a self,
269        phase_budget: Option<&'a PhaseBudget>,
270    ) -> SolverScopeChildConfig<'a, S> {
271        let phase_budget = self
272            .phase_budget
273            .or_else(|| phase_budget.filter(|budget| budget.has_limits()));
274        SolverScopeChildConfig {
275            terminate: self.terminate,
276            runtime: self.runtime,
277            environment_mode: self.environment_mode,
278            time_deadline: self.child_time_deadline(),
279            phase_budget,
280            inphase_step_count_limit: self.inphase_step_count_limit,
281            inphase_move_count_limit: self.inphase_move_count_limit,
282            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
283            inphase_best_score_limit: self.inphase_best_score_limit,
284        }
285    }
286
287    fn child_time_deadline(&self) -> Option<Instant> {
288        self.time_deadline.or_else(|| {
289            self.time_limit.map(|limit| {
290                self.start_time
291                    .map(|start| start + limit)
292                    .unwrap_or_else(|| Instant::now() + limit)
293            })
294        })
295    }
296
297    pub fn with_progress_callback<F: ProgressCallback<S>>(
298        self,
299        callback: F,
300    ) -> SolverScope<'t, S, D, F> {
301        SolverScope {
302            score_director: self.score_director,
303            best_solution: self.best_solution,
304            current_score: self.current_score,
305            best_score: self.best_score,
306            rng: self.rng,
307            start_time: self.start_time,
308            paused_at: self.paused_at,
309            total_step_count: self.total_step_count,
310            terminate: self.terminate,
311            runtime: self.runtime,
312            publication: self.publication,
313            yielded_to_parent: self.yielded_to_parent,
314            environment_mode: self.environment_mode,
315            stats: self.stats,
316            time_limit: self.time_limit,
317            time_deadline: self.time_deadline,
318            progress_callback: callback,
319            terminal_reason: self.terminal_reason,
320            last_best_elapsed: self.last_best_elapsed,
321            best_solution_revision: self.best_solution_revision,
322            solution_revision: self.solution_revision,
323            construction_frontier: self.construction_frontier,
324            phase_budget: self.phase_budget,
325            inphase_step_count_limit: self.inphase_step_count_limit,
326            inphase_move_count_limit: self.inphase_move_count_limit,
327            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
328            inphase_best_score_limit: self.inphase_best_score_limit,
329        }
330    }
331
332    pub fn start_solving(&mut self) {
333        self.start_time = Some(Instant::now());
334        self.paused_at = None;
335        self.total_step_count = 0;
336        self.terminal_reason = None;
337        self.last_best_elapsed = None;
338        self.yielded_to_parent = false;
339        self.best_solution_revision = None;
340        self.solution_revision = 1;
341        self.construction_frontier.reset();
342        self.stats.start();
343    }
344
345    pub fn elapsed(&self) -> Option<Duration> {
346        match (self.start_time, self.paused_at) {
347            (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
348            (Some(start), None) => Some(start.elapsed()),
349            _ => None,
350        }
351    }
352
353    pub fn time_since_last_improvement(&self) -> Option<Duration> {
354        let elapsed = self.elapsed()?;
355        let last_best_elapsed = self.last_best_elapsed?;
356        Some(elapsed.saturating_sub(last_best_elapsed))
357    }
358
359    pub fn score_director(&self) -> &D {
360        &self.score_director
361    }
362
363    pub(crate) fn score_director_mut(&mut self) -> &mut D {
364        &mut self.score_director
365    }
366
367    pub fn working_solution(&self) -> &S {
368        self.score_director.working_solution()
369    }
370
371    pub fn mutate<T, F>(&mut self, mutate: F) -> T
372    where
373        F: FnOnce(&mut D) -> T,
374    {
375        self.committed_mutation(mutate)
376    }
377
378    pub fn calculate_score(&mut self) -> S::Score {
379        self.record_score_calculation();
380        let score = self.score_director.calculate_score();
381        self.current_score = Some(score);
382        self.assert_score_consistent("calculate_score", score);
383        score
384    }
385
386    pub(crate) fn assert_score_consistent(&self, context: &str, score: S::Score) {
387        if self.environment_mode != EnvironmentMode::FullAssert {
388            return;
389        }
390        let Some(fresh_score) = self.score_director.fresh_score() else {
391            return;
392        };
393        assert_eq!(
394            score, fresh_score,
395            "score director drift after {context}: cached score {score:?} != fresh score {fresh_score:?}"
396        );
397    }
398
399    pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
400        if self.start_time.is_none() {
401            self.start_solving();
402        }
403        let score = self.calculate_score();
404        let solution = self.score_director.clone_working_solution();
405        self.set_best_solution(solution, score);
406        score
407    }
408
409    pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
410        *self.score_director.working_solution_mut() = solution;
411        self.score_director.reset();
412        self.current_score = None;
413        self.best_solution_revision = None;
414        self.solution_revision = 1;
415        self.construction_frontier.reset();
416        self.calculate_score()
417    }
418
419    pub fn best_solution(&self) -> Option<&S> {
420        self.best_solution.as_ref()
421    }
422
423    pub fn best_score(&self) -> Option<&S::Score> {
424        self.best_score.as_ref()
425    }
426
427    pub fn current_score(&self) -> Option<&S::Score> {
428        self.current_score.as_ref()
429    }
430
431    pub(crate) fn is_scalar_slot_completed(&self, slot_id: ConstructionSlotId) -> bool {
432        self.construction_frontier
433            .is_scalar_completed(slot_id, self.solution_revision)
434    }
435
436    pub(crate) fn mark_scalar_slot_completed(&mut self, slot_id: ConstructionSlotId) {
437        self.construction_frontier
438            .mark_scalar_completed(slot_id, self.solution_revision);
439    }
440
441    pub(crate) fn is_group_slot_completed(&self, slot_id: &ConstructionGroupSlotId) -> bool {
442        self.construction_frontier
443            .is_group_completed(slot_id, self.solution_revision)
444    }
445
446    pub(crate) fn mark_group_slot_completed(&mut self, slot_id: ConstructionGroupSlotId) {
447        self.construction_frontier
448            .mark_group_completed(slot_id, self.solution_revision);
449    }
450
451    pub(crate) fn is_list_element_completed(&self, element_id: ConstructionListElementId) -> bool {
452        self.construction_frontier
453            .is_list_completed(element_id, self.solution_revision)
454    }
455
456    pub(crate) fn mark_list_element_completed(&mut self, element_id: ConstructionListElementId) {
457        self.construction_frontier
458            .mark_list_completed(element_id, self.solution_revision);
459    }
460
461    pub(crate) fn solution_revision(&self) -> u64 {
462        self.solution_revision
463    }
464
465    pub(crate) fn apply_committed_move<M>(&mut self, mov: &M)
466    where
467        M: Move<S>,
468    {
469        self.committed_mutation(|score_director| mov.do_move(score_director));
470    }
471
472    pub(crate) fn apply_committed_change<F>(&mut self, change: F)
473    where
474        F: FnOnce(&mut D),
475    {
476        self.committed_mutation(change);
477    }
478
479    pub(crate) fn construction_frontier(&self) -> &ConstructionFrontier {
480        &self.construction_frontier
481    }
482}