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}