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}