argmin/core/executor.rs
1// Copyright 2018-2024 argmin developers
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8use crate::core::checkpointing::Checkpoint;
9use crate::core::observers::{Observe, ObserverMode, Observers};
10use crate::core::{
11 Error, OptimizationResult, Problem, Solver, State, TerminationReason, TerminationStatus, KV,
12};
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15use web_time::Instant;
16
17/// Solves an optimization problem with a solver
18pub struct Executor<O, S, I> {
19 /// Solver
20 solver: S,
21 /// Problem
22 problem: Problem<O>,
23 /// State
24 state: Option<I>,
25 /// Storage for observers
26 observers: Observers<I>,
27 /// Checkpoint
28 checkpoint: Option<Box<dyn Checkpoint<S, I>>>,
29 /// Timeout
30 timeout: Option<std::time::Duration>,
31 /// Indicates whether Ctrl-C functionality should be active or not
32 ctrlc: bool,
33 /// Indicates whether to time execution or not
34 timer: bool,
35}
36
37impl<O, S, I> Executor<O, S, I>
38where
39 S: Solver<O, I>,
40 I: State,
41{
42 /// Constructs an `Executor` from a user defined problem and a solver.
43 ///
44 /// # Example
45 ///
46 /// ```
47 /// # use argmin::core::Executor;
48 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
49 /// #
50 /// # type Rosenbrock = TestProblem;
51 /// # type Newton = TestSolver;
52 /// #
53 /// // Construct an instance of the desired solver
54 /// let solver = Newton::new();
55 ///
56 /// // `Rosenbrock` implements `CostFunction` and `Gradient` as required by the
57 /// // `SteepestDescent` solver
58 /// let problem = Rosenbrock {};
59 ///
60 /// // Create instance of `Executor` with `problem` and `solver`
61 /// let executor = Executor::new(problem, solver);
62 /// ```
63 pub fn new(problem: O, solver: S) -> Self {
64 let state = Some(I::new());
65 Executor {
66 solver,
67 problem: Problem::new(problem),
68 state,
69 observers: Observers::new(),
70 checkpoint: None,
71 timeout: None,
72 ctrlc: true,
73 timer: false,
74 }
75 }
76
77 /// This method gives mutable access to the internal state of the solver. This allows for
78 /// initializing the state before running the `Executor`. The options for initialization depend
79 /// on the type of state used by the chosen solver. Common types of state are
80 /// [`IterState`](`crate::core::IterState`),
81 /// [`PopulationState`](`crate::core::PopulationState`), and
82 /// [`LinearProgramState`](`crate::core::LinearProgramState`). Please see the documentation of
83 /// the desired solver for information about which state is used.
84 ///
85 /// # Example
86 ///
87 /// ```
88 /// # use argmin::core::Executor;
89 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
90 /// #
91 /// # let solver = TestSolver::new();
92 /// # let problem = TestProblem::new();
93 /// # let init_param = vec![1.0f64, 0.0];
94 /// #
95 /// // Create instance of `Executor` with `problem` and `solver`
96 /// let executor = Executor::new(problem, solver)
97 /// // Configure and initialize internal state.
98 /// .configure(|state| state.param(init_param).max_iters(10));
99 /// ```
100 #[must_use]
101 pub fn configure<F: FnOnce(I) -> I>(mut self, init: F) -> Self {
102 let state = self.state.take().unwrap();
103 let state = init(state);
104 self.state = Some(state);
105 self
106 }
107
108 /// Runs the executor by applying the solver to the optimization problem.
109 ///
110 /// # Example
111 ///
112 /// ```
113 /// # use argmin::core::{Error, Executor};
114 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
115 /// #
116 /// # fn main() -> Result<(), Error> {
117 /// # let solver = TestSolver::new();
118 /// # let problem = TestProblem::new();
119 /// #
120 /// # let init_param = vec![1.0f64, 0.0];
121 /// #
122 /// // Create instance of `Executor` with `problem` and `solver`
123 /// let result = Executor::new(problem, solver)
124 /// // Configure and initialize internal state.
125 /// .configure(|state| state.param(init_param).max_iters(100))
126 /// # .configure(|state| state.max_iters(1))
127 /// // Execute solver
128 /// .run()?;
129 /// # Ok(())
130 /// # }
131 /// ```
132 pub fn run(mut self) -> Result<OptimizationResult<O, S, I>, Error> {
133 // First, load checkpoint if given.
134 if let Some(checkpoint) = self.checkpoint.as_ref() {
135 if let Some((solver, state)) = checkpoint.load()? {
136 self.state = Some(state);
137 self.solver = solver;
138 }
139 }
140 let total_time = if self.timer {
141 Some(Instant::now())
142 } else {
143 None
144 };
145
146 let state = self.state.take().unwrap();
147
148 let interrupt = Arc::new(AtomicBool::new(false));
149
150 if self.ctrlc {
151 #[cfg(feature = "ctrlc")]
152 {
153 // Set up the Ctrl-C handler
154 let interp = interrupt.clone();
155 // This is currently a hack to allow checkpoints to be run again within the
156 // same program (usually not really a use case anyway). Unfortunately, this
157 // means that any subsequent run started afterwards will not have Ctrl-C
158 // handling available... This should also be a problem in case one tries to run
159 // two consecutive optimizations. There is ongoing work in the ctrlc crate
160 // (channels and such) which may solve this problem. So far, we have to live
161 // with this.
162 let handler = move || {
163 interp.store(true, Ordering::SeqCst);
164 };
165 match ctrlc::set_handler(handler) {
166 Err(ctrlc::Error::MultipleHandlers) => Ok(()),
167 interp => interp,
168 }?;
169 }
170 }
171
172 // Only call `init` of `solver` if the current iteration number is 0. This avoids that
173 // `init` is called when starting from a checkpoint (because `init` could change the state
174 // of the `solver`, which would overwrite the state restored from the checkpoint).
175 let mut state = if state.get_iter() == 0 {
176 let (mut state, kv) = self.solver.init(&mut self.problem, state)?;
177 state.update();
178
179 if !self.observers.is_empty() {
180 let kv = kv.unwrap_or(kv![]);
181
182 // Observe after init
183 self.observers
184 .observe_init(self.solver.name(), &state, &kv)?;
185 }
186
187 state.func_counts(&self.problem);
188 state
189 } else {
190 state
191 };
192
193 while !interrupt.load(Ordering::SeqCst) {
194 // check first if it has already terminated
195 // This should probably be solved better.
196 // First, check if it isn't already terminated. If it isn't, evaluate the
197 // stopping criteria. If `self.terminate()` is called without the checking
198 // whether it has terminated already, then it may overwrite a termination set
199 // within `next_iter()`!
200 state = if !state.terminated() {
201 let term = self.solver.terminate_internal(&state);
202 if let TerminationStatus::Terminated(reason) = term {
203 state.terminate_with(reason)
204 } else {
205 state
206 }
207 } else {
208 state
209 };
210 // Now check once more if the algorithm has terminated. If yes, then break.
211 if state.terminated() {
212 break;
213 }
214
215 // Start time measurement
216 let start = if self.timer {
217 Some(Instant::now())
218 } else {
219 None
220 };
221
222 let (state_t, kv) = self.solver.next_iter(&mut self.problem, state)?;
223 state = state_t;
224
225 state.func_counts(&self.problem);
226
227 // End time measurement
228 let duration = if self.timer {
229 Some(start.unwrap().elapsed())
230 } else {
231 None
232 };
233
234 state.update();
235
236 if !self.observers.is_empty() {
237 let mut log = if let Some(kv) = kv { kv } else { KV::new() };
238
239 if self.timer {
240 let duration = duration.unwrap();
241 let tmp = kv!(
242 "time" => duration.as_secs_f64();
243 );
244 log = log.merge(tmp);
245 }
246 self.observers.observe_iter(&state, &log)?;
247 }
248
249 // increment iteration number
250 state.increment_iter();
251
252 if let Some(checkpoint) = self.checkpoint.as_ref() {
253 checkpoint.save_cond(&self.solver, &state, state.get_iter())?;
254 }
255
256 if self.timer {
257 // Increase accumulated total_time
258 total_time.map(|total_time| state.time(Some(total_time.elapsed())));
259
260 // If a timeout is set, check if timeout is reached
261 if let (Some(timeout), Some(total_time)) = (self.timeout, total_time) {
262 if total_time.elapsed() > timeout {
263 state = state.terminate_with(TerminationReason::Timeout);
264 }
265 }
266 }
267
268 // Check if termination occurred in the meantime
269 if state.terminated() {
270 break;
271 }
272 }
273
274 if interrupt.load(Ordering::SeqCst) {
275 // Solver execution has been interrupted manually
276 state = state.terminate_with(TerminationReason::Interrupt);
277 }
278
279 if !self.observers.is_empty() {
280 self.observers.observe_final(&state)?;
281 }
282
283 Ok(OptimizationResult::new(self.problem, self.solver, state))
284 }
285
286 /// Adds an observer to the executor. Observers are required to implement the
287 /// [`Observe`](`crate::core::observers::Observe`) trait.
288 /// The parameter `mode` defines the conditions under which the observer will be called. See
289 /// [`ObserverMode`](`crate::core::observers::ObserverMode`) for details.
290 ///
291 /// It is possible to add multiple observers.
292 ///
293 /// # Example
294 ///
295 /// ```
296 /// # use argmin::core::{Error, Executor, observers::ObserverMode};
297 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
298 /// # use argmin_observer_slog::SlogLogger;
299 /// #
300 /// # fn main() -> Result<(), Error> {
301 /// # let solver = TestSolver::new();
302 /// # let problem = TestProblem::new();
303 /// #
304 /// // Create instance of `Executor` with `problem` and `solver`
305 /// let executor = Executor::new(problem, solver)
306 /// .add_observer(SlogLogger::term(), ObserverMode::Always);
307 /// # Ok(())
308 /// # }
309 /// ```
310 #[must_use]
311 pub fn add_observer<OBS: Observe<I> + 'static>(
312 mut self,
313 observer: OBS,
314 mode: ObserverMode,
315 ) -> Self {
316 self.observers.push(observer, mode);
317 self
318 }
319
320 /// Configures checkpointing
321 ///
322 /// # Example
323 ///
324 /// ```
325 /// # use argmin::core::{Error, Executor};
326 /// # #[cfg(feature = "serde1")]
327 /// # use argmin::core::checkpointing::CheckpointingFrequency;
328 /// # use argmin_checkpointing_file::FileCheckpoint;
329 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
330 /// #
331 /// # fn main() -> Result<(), Error> {
332 /// # let solver = TestSolver::new();
333 /// # let problem = TestProblem::new();
334 /// #
335 /// # #[cfg(feature = "serde1")]
336 /// let checkpoint = FileCheckpoint::new(
337 /// // Directory where checkpoints are saved to
338 /// ".checkpoints",
339 /// // Filename of checkpoint
340 /// "rosenbrock_optim",
341 /// // How often checkpoints should be saved
342 /// CheckpointingFrequency::Every(20)
343 /// );
344 ///
345 /// // Create instance of `Executor` with `problem` and `solver`
346 /// # #[cfg(feature = "serde1")]
347 /// let executor = Executor::new(problem, solver)
348 /// // Add checkpointing
349 /// .checkpointing(checkpoint);
350 /// # Ok(())
351 /// # }
352 /// ```
353 #[must_use]
354 pub fn checkpointing<C: 'static + Checkpoint<S, I>>(mut self, checkpoint: C) -> Self {
355 self.checkpoint = Some(Box::new(checkpoint));
356 self
357 }
358
359 /// Enables or disables CTRL-C handling (default: enabled). The CTRL-C handling gracefully
360 /// stops the solver if it is canceled via CTRL-C (SIGINT). Requires the optional `ctrlc`
361 /// feature to be set.
362 ///
363 /// Note that this does not work with nested `Executor`s. If a solver executes another solver
364 /// internally, the inner solver needs to disable CTRL-C handling.
365 ///
366 /// # Example
367 ///
368 /// ```
369 /// # use argmin::core::{Error, Executor};
370 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
371 /// #
372 /// # fn main() -> Result<(), Error> {
373 /// # let solver = TestSolver::new();
374 /// # let problem = TestProblem::new();
375 /// #
376 /// // Create instance of `Executor` with `problem` and `solver`
377 /// let executor = Executor::new(problem, solver).ctrlc(false);
378 /// # Ok(())
379 /// # }
380 /// ```
381 #[must_use]
382 pub fn ctrlc(mut self, ctrlc: bool) -> Self {
383 self.ctrlc = ctrlc;
384 self
385 }
386
387 /// Enables or disables timing of individual iterations (default: false).
388 ///
389 /// In case a timeout is set, this will automatically be set to true.
390 ///
391 /// # Example
392 ///
393 /// ```
394 /// # use argmin::core::{Error, Executor};
395 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
396 /// #
397 /// # fn main() -> Result<(), Error> {
398 /// # let solver = TestSolver::new();
399 /// # let problem = TestProblem::new();
400 /// #
401 /// // Create instance of `Executor` with `problem` and `solver`
402 /// let executor = Executor::new(problem, solver).timer(false);
403 /// # Ok(())
404 /// # }
405 /// ```
406 #[must_use]
407 pub fn timer(mut self, timer: bool) -> Self {
408 if self.timeout.is_none() {
409 self.timer = timer;
410 }
411 self
412 }
413
414 /// Sets a timeout for the run.
415 ///
416 /// The optimization run is stopped once the timeout is exceeded. Note that the check is
417 /// performed after each iteration, therefore the actual runtime can exceed the the set
418 /// duration.
419 /// This also enables time measurements.
420 ///
421 /// # Example
422 ///
423 /// ```
424 /// # use argmin::core::{Error, Executor};
425 /// # use argmin::core::test_utils::{TestSolver, TestProblem};
426 /// #
427 /// # fn main() -> Result<(), Error> {
428 /// # let solver = TestSolver::new();
429 /// # let problem = TestProblem::new();
430 /// #
431 /// // Create instance of `Executor` with `problem` and `solver`
432 /// let executor = Executor::new(problem, solver).timeout(std::time::Duration::from_secs(30));
433 /// # Ok(())
434 /// # }
435 /// ```
436 #[must_use]
437 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
438 self.timer = true;
439 self.timeout = Some(timeout);
440 self
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::core::test_utils::{TestProblem, TestSolver};
448 use crate::core::IterState;
449 use approx::assert_relative_eq;
450
451 #[test]
452 fn test_update() {
453 let problem = TestProblem::new();
454 let solver = TestSolver::new();
455
456 let mut executor = Executor::new(problem, solver).configure(
457 |config: IterState<Vec<f64>, (), (), (), (), f64>| config.param(vec![0.0, 0.0]),
458 );
459
460 // 1) Parameter vector changes, but not cost (continues to be `Inf`)
461 let new_param = vec![1.0, 1.0];
462 executor.state = Some(executor.state.take().unwrap().param(new_param.clone()));
463 executor.state.as_mut().unwrap().update();
464 assert_eq!(
465 *executor.state.as_ref().unwrap().get_best_param().unwrap(),
466 new_param
467 );
468 assert!(executor
469 .state
470 .as_ref()
471 .unwrap()
472 .get_best_cost()
473 .is_infinite());
474 assert!(executor
475 .state
476 .as_ref()
477 .unwrap()
478 .get_best_cost()
479 .is_sign_positive());
480
481 // 2) Parameter vector and cost changes to something better
482 let new_param = vec![2.0, 2.0];
483 let new_cost = 10.0;
484 executor.state = Some(
485 executor
486 .state
487 .take()
488 .unwrap()
489 .param(new_param.clone())
490 .cost(new_cost),
491 );
492 executor.state.as_mut().unwrap().update();
493 assert_eq!(
494 *executor.state.as_ref().unwrap().get_best_param().unwrap(),
495 new_param
496 );
497 assert_relative_eq!(
498 executor.state.as_ref().unwrap().get_best_cost(),
499 new_cost,
500 epsilon = f64::EPSILON
501 );
502
503 // 3) Parameter vector and cost changes to something worse
504 let old_param = executor
505 .state
506 .as_ref()
507 .unwrap()
508 .get_best_param()
509 .unwrap()
510 .clone();
511 let new_param = vec![3.0, 3.0];
512 let old_cost = executor.state.as_ref().unwrap().get_best_cost();
513 let new_cost = old_cost + 1.0;
514 executor.state = Some(
515 executor
516 .state
517 .take()
518 .unwrap()
519 .param(new_param)
520 .cost(new_cost),
521 );
522 executor.state.as_mut().unwrap().update();
523 assert_eq!(
524 executor
525 .state
526 .as_ref()
527 .unwrap()
528 .get_best_param()
529 .unwrap()
530 .clone(),
531 old_param
532 );
533 assert_relative_eq!(
534 executor.state.as_ref().unwrap().get_best_cost(),
535 old_cost,
536 epsilon = f64::EPSILON
537 );
538
539 // 4) `-Inf` is better than `Inf`
540 let solver = TestSolver {};
541 let mut executor = Executor::new(problem, solver).configure(
542 |config: IterState<Vec<f64>, (), (), (), (), f64>| config.param(vec![0.0, 0.0]),
543 );
544
545 let new_param = vec![1.0, 1.0];
546 let new_cost = f64::NEG_INFINITY;
547 executor.state = Some(
548 executor
549 .state
550 .take()
551 .unwrap()
552 .param(new_param.clone())
553 .cost(new_cost),
554 );
555 executor.state.as_mut().unwrap().update();
556 assert_eq!(
557 *executor.state.as_ref().unwrap().get_best_param().unwrap(),
558 new_param
559 );
560 assert!(executor
561 .state
562 .as_ref()
563 .unwrap()
564 .get_best_cost()
565 .is_infinite());
566 assert!(executor
567 .state
568 .as_ref()
569 .unwrap()
570 .get_best_cost()
571 .is_sign_negative());
572
573 // 5) `Inf` is worse than `-Inf`
574 let old_param = executor
575 .state
576 .as_ref()
577 .unwrap()
578 .get_best_param()
579 .unwrap()
580 .clone();
581 let new_param = vec![6.0, 6.0];
582 let new_cost = f64::INFINITY;
583 executor.state = Some(
584 executor
585 .state
586 .take()
587 .unwrap()
588 .param(new_param)
589 .cost(new_cost),
590 );
591 executor.state.as_mut().unwrap().update();
592 assert_eq!(
593 executor
594 .state
595 .as_ref()
596 .unwrap()
597 .get_best_param()
598 .unwrap()
599 .clone(),
600 old_param
601 );
602 assert!(executor
603 .state
604 .as_ref()
605 .unwrap()
606 .get_best_cost()
607 .is_infinite());
608 assert!(executor
609 .state
610 .as_ref()
611 .unwrap()
612 .get_best_cost()
613 .is_sign_negative());
614 }
615
616 /// The solver's `init` should not be called when started from a checkpoint.
617 /// See https://github.com/argmin-rs/argmin/issues/199.
618 #[test]
619 #[cfg(feature = "serde1")]
620 fn test_checkpointing_solver_initialization() {
621 use std::cell::RefCell;
622
623 use crate::core::{
624 checkpointing::CheckpointingFrequency, test_utils::TestProblem, ArgminFloat,
625 CostFunction,
626 };
627 use serde::{Deserialize, Serialize};
628
629 #[derive(Clone)]
630 pub struct FakeCheckpoint {
631 pub frequency: CheckpointingFrequency,
632 pub solver: RefCell<Option<OptimizationAlgorithm>>,
633 pub state: RefCell<Option<IterState<Vec<f64>, (), (), (), (), f64>>>,
634 }
635
636 impl Checkpoint<OptimizationAlgorithm, IterState<Vec<f64>, (), (), (), (), f64>>
637 for FakeCheckpoint
638 {
639 fn save(
640 &self,
641 solver: &OptimizationAlgorithm,
642 state: &IterState<Vec<f64>, (), (), (), (), f64>,
643 ) -> Result<(), Error> {
644 *self.solver.borrow_mut() = Some(solver.clone());
645 *self.state.borrow_mut() = Some(state.clone());
646 Ok(())
647 }
648
649 fn load(
650 &self,
651 ) -> Result<
652 Option<(
653 OptimizationAlgorithm,
654 IterState<Vec<f64>, (), (), (), (), f64>,
655 )>,
656 Error,
657 > {
658 if self.solver.borrow().is_none() {
659 return Ok(None);
660 }
661 Ok(Some((
662 self.solver.borrow().clone().unwrap(),
663 self.state.borrow().clone().unwrap(),
664 )))
665 }
666
667 fn frequency(&self) -> CheckpointingFrequency {
668 self.frequency
669 }
670 }
671
672 // Fake optimization algorithm which holds internal state which changes over time
673 #[derive(Clone, Serialize, Deserialize)]
674 struct OptimizationAlgorithm {
675 pub internal_state: u64,
676 }
677
678 // Implement Solver for OptimizationAlgorithm
679 impl<O, P, F> Solver<O, IterState<P, (), (), (), (), F>> for OptimizationAlgorithm
680 where
681 O: CostFunction<Param = P, Output = F>,
682 P: Clone,
683 F: ArgminFloat,
684 {
685 fn name(&self) -> &str {
686 "OptimizationAlgorithm"
687 }
688
689 // Only resets internal_state to 1
690 fn init(
691 &mut self,
692 _problem: &mut Problem<O>,
693 state: IterState<P, (), (), (), (), F>,
694 ) -> Result<(IterState<P, (), (), (), (), F>, Option<KV>), Error> {
695 self.internal_state = 1;
696 Ok((state, None))
697 }
698
699 // Increment internal_state
700 fn next_iter(
701 &mut self,
702 _problem: &mut Problem<O>,
703 state: IterState<P, (), (), (), (), F>,
704 ) -> Result<(IterState<P, (), (), (), (), F>, Option<KV>), Error> {
705 self.internal_state += 1;
706 Ok((state, None))
707 }
708
709 // Avoid terminating early because param does not change
710 fn terminate(&mut self, _state: &IterState<P, (), (), (), (), F>) -> TerminationStatus {
711 TerminationStatus::NotTerminated
712 }
713
714 // Avoid terminating early because param does not change
715 fn terminate_internal(
716 &mut self,
717 state: &IterState<P, (), (), (), (), F>,
718 ) -> TerminationStatus {
719 if state.get_iter() >= state.get_max_iters() {
720 TerminationStatus::Terminated(TerminationReason::MaxItersReached)
721 } else {
722 TerminationStatus::NotTerminated
723 }
724 }
725 }
726
727 // Create random test problem
728 let problem = TestProblem::new();
729
730 // solver instance
731 let solver = OptimizationAlgorithm { internal_state: 0 };
732
733 // Create a checkpoint
734 let checkpoint = FakeCheckpoint {
735 frequency: CheckpointingFrequency::Always,
736 solver: RefCell::new(None),
737 state: RefCell::new(None),
738 };
739
740 // Create and run executor
741 let executor = Executor::new(problem, solver)
742 .configure(|state| state.param(vec![1.0f64, 1.0]).max_iters(10))
743 .checkpointing(checkpoint.clone());
744
745 let OptimizationResult { solver, .. } = executor.run().unwrap();
746
747 // internal_state should be 11
748 // (1 from init plus 10 iterations where it is incremented by 1)
749 assert_eq!(solver.internal_state, 11);
750
751 // Create and run solver again
752 let executor = Executor::new(problem, solver)
753 .configure(|state| state.param(vec![1.0f64, 1.0]).max_iters(10))
754 .checkpointing(checkpoint);
755
756 let OptimizationResult { solver, .. } = executor.run().unwrap();
757
758 // internal_state should still be 11
759 // (1 from init plus 10 iterations where it is incremented by 1)
760 assert_eq!(solver.internal_state, 11);
761
762 // Delete old checkpointing file
763 let _ = std::fs::remove_file(".checkpoints/init_test.arg");
764 }
765
766 #[test]
767 fn test_timeout() {
768 let solver = TestSolver::new();
769 let problem = TestProblem::new();
770 let timeout = std::time::Duration::from_secs(2);
771
772 let executor = Executor::new(problem, solver).timer(true);
773 assert!(executor.timer);
774 assert!(executor.timeout.is_none());
775
776 let executor = Executor::new(problem, solver).timer(false);
777 assert!(!executor.timer);
778 assert!(executor.timeout.is_none());
779
780 let executor = Executor::new(problem, solver).timeout(timeout);
781 assert!(executor.timer);
782 assert_eq!(executor.timeout, Some(timeout));
783
784 let executor = Executor::new(problem, solver).timeout(timeout).timer(false);
785 assert!(executor.timer);
786 assert_eq!(executor.timeout, Some(timeout));
787
788 let executor = Executor::new(problem, solver).timer(false).timeout(timeout);
789 assert!(executor.timer);
790 assert_eq!(executor.timeout, Some(timeout));
791 }
792}