optimization_engine/core/panoc/
panoc_optimizer.rs

1//! PANOC optimizer
2//!
3use crate::{
4    constraints,
5    core::{
6        panoc::panoc_engine::PANOCEngine, panoc::PANOCCache, AlgorithmEngine, ExitStatus,
7        Optimizer, Problem, SolverStatus,
8    },
9    matrix_operations, FunctionCallResult, SolverError,
10};
11use std::time;
12
13const MAX_ITER: usize = 100_usize;
14
15/// Optimizer using the PANOC algorithm
16///
17///
18pub struct PANOCOptimizer<'a, GradientType, ConstraintType, CostType>
19where
20    GradientType: Fn(&[f64], &mut [f64]) -> FunctionCallResult,
21    CostType: Fn(&[f64], &mut f64) -> FunctionCallResult,
22    ConstraintType: constraints::Constraint,
23{
24    panoc_engine: PANOCEngine<'a, GradientType, ConstraintType, CostType>,
25    max_iter: usize,
26    max_duration: Option<time::Duration>,
27}
28
29impl<'a, GradientType, ConstraintType, CostType>
30    PANOCOptimizer<'a, GradientType, ConstraintType, CostType>
31where
32    GradientType: Fn(&[f64], &mut [f64]) -> FunctionCallResult,
33    CostType: Fn(&[f64], &mut f64) -> FunctionCallResult,
34    ConstraintType: constraints::Constraint,
35{
36    /// Constructor of `PANOCOptimizer`
37    ///
38    /// ## Arguments
39    ///
40    /// - problem: definition of optimization problem
41    /// - cache: cache object constructed once
42    ///
43    /// ## Panic
44    ///
45    /// Does not panic
46    pub fn new(
47        problem: Problem<'a, GradientType, ConstraintType, CostType>,
48        cache: &'a mut PANOCCache,
49    ) -> Self {
50        PANOCOptimizer {
51            panoc_engine: PANOCEngine::new(problem, cache),
52            max_iter: MAX_ITER,
53            max_duration: None,
54        }
55    }
56
57    /// Sets the tolerance on the norm of the fixed-point residual
58    ///
59    /// The algorithm will exit if the form of gamma*FPR drops below
60    /// this tolerance
61    ///
62    /// ## Panics
63    ///
64    /// The method panics if the specified tolerance is not positive
65    pub fn with_tolerance(self, tolerance: f64) -> Self {
66        assert!(tolerance > 0.0, "tolerance must be larger than 0");
67
68        self.panoc_engine.cache.tolerance = tolerance;
69        self
70    }
71
72    /// Specify the tolerance $\epsilon$ related to the AKKT condition
73    ///
74    /// $$
75    /// \Vert{}\gamma^{-1}(u-u^+) + \nabla f(u) - \nabla f(u^+){}\Vert \leq \epsilon
76    /// $$
77    ///
78    /// ## Arguments
79    ///
80    /// - `akkt_tolerance`: the AKKT-specific tolerance
81    ///
82    ///
83    /// ## Returns
84    ///
85    /// Returns the current mutable and updated instance of the provided object
86    ///
87    ///  
88    /// ## Panics
89    ///
90    /// The method panics if the provided value of the AKKT-specific tolerance is
91    /// not positive.
92    ///
93    pub fn with_akkt_tolerance(self, akkt_tolerance: f64) -> Self {
94        assert!(akkt_tolerance > 0.0, "akkt_tolerance must be positive");
95        self.panoc_engine.cache.set_akkt_tolerance(akkt_tolerance);
96        self
97    }
98
99    /// Sets the maximum number of iterations
100    ///
101    /// ## Panics
102    ///
103    /// Panics if the provided number of iterations is equal to zero
104    pub fn with_max_iter(mut self, max_iter: usize) -> Self {
105        assert!(max_iter > 0, "max_iter must be larger than 0");
106
107        self.max_iter = max_iter;
108        self
109    }
110
111    /// Sets the maximum solution time, useful in real-time applications
112    pub fn with_max_duration(mut self, max_duation: time::Duration) -> Self {
113        self.max_duration = Some(max_duation);
114        self
115    }
116}
117
118impl<'life, GradientType, ConstraintType, CostType> Optimizer
119    for PANOCOptimizer<'life, GradientType, ConstraintType, CostType>
120where
121    GradientType: Fn(&[f64], &mut [f64]) -> FunctionCallResult + 'life,
122    CostType: Fn(&[f64], &mut f64) -> FunctionCallResult,
123    ConstraintType: constraints::Constraint + 'life,
124{
125    fn solve(&mut self, u: &mut [f64]) -> Result<SolverStatus, SolverError> {
126        let now = instant::Instant::now();
127
128        /*
129         * Initialise [call panoc_engine.init()]
130         * and check whether it returns Ok(())
131         */
132        self.panoc_engine.init(u)?;
133
134        /* Main loop */
135        let mut num_iter: usize = 0;
136        let mut continue_num_iters = true;
137        let mut continue_runtime = true;
138
139        let mut step_flag = self.panoc_engine.step(u)?;
140        if let Some(dur) = self.max_duration {
141            while step_flag && continue_num_iters && continue_runtime {
142                num_iter += 1;
143                continue_num_iters = num_iter < self.max_iter;
144                continue_runtime = now.elapsed() <= dur;
145                step_flag = self.panoc_engine.step(u)?;
146            }
147        } else {
148            while step_flag && continue_num_iters {
149                num_iter += 1;
150                continue_num_iters = num_iter < self.max_iter;
151                step_flag = self.panoc_engine.step(u)?;
152            }
153        }
154
155        // check for possible NaN/inf
156        if !matrix_operations::is_finite(u) {
157            return Err(SolverError::NotFiniteComputation);
158        }
159
160        // exit status
161        let exit_status = if !continue_num_iters {
162            ExitStatus::NotConvergedIterations
163        } else if !continue_runtime {
164            ExitStatus::NotConvergedOutOfTime
165        } else {
166            ExitStatus::Converged
167        };
168
169        // copy u_half_step into u (the algorithm should return u_bar,
170        // because it's always feasible, while u may violate the constraints)
171        u.copy_from_slice(&self.panoc_engine.cache.u_half_step);
172
173        // export solution status (exit status, num iterations and more)
174        Ok(SolverStatus::new(
175            exit_status,
176            num_iter,
177            now.elapsed(),
178            self.panoc_engine.cache.norm_gamma_fpr,
179            self.panoc_engine.cache.cost_value,
180        ))
181    }
182}
183
184/* --------------------------------------------------------------------------------------------- */
185/*       TESTS                                                                                   */
186/* --------------------------------------------------------------------------------------------- */
187#[cfg(test)]
188mod tests {
189
190    use crate::core::constraints::*;
191    use crate::core::panoc::*;
192    use crate::core::*;
193    use crate::{mocks, FunctionCallResult};
194
195    #[test]
196    fn t_panoc_optimizer_rosenbrock() {
197        /* USER PARAMETERS */
198        let tolerance = 1e-6;
199        let a_param = 1.0;
200        let b_param = 200.0;
201        let n_dimension = 2;
202        let lbfgs_memory = 8;
203        let max_iters = 80;
204        let mut u_solution = [-1.5, 0.9];
205
206        /* COST FUNCTION */
207        let cost_gradient = |u: &[f64], grad: &mut [f64]| -> FunctionCallResult {
208            mocks::rosenbrock_grad(a_param, b_param, u, grad);
209            Ok(())
210        };
211        let cost_function = |u: &[f64], c: &mut f64| -> FunctionCallResult {
212            *c = mocks::rosenbrock_cost(a_param, b_param, u);
213            Ok(())
214        };
215        /* CONSTRAINTS */
216        let radius = 2.0;
217        let bounds = constraints::Ball2::new(None, radius);
218        let mut panoc_cache = PANOCCache::new(n_dimension, tolerance, lbfgs_memory);
219        let problem = Problem::new(&bounds, cost_gradient, cost_function);
220        let mut panoc = PANOCOptimizer::new(problem, &mut panoc_cache).with_max_iter(max_iters);
221        let now = instant::Instant::now();
222        let status = panoc.solve(&mut u_solution).unwrap();
223
224        println!("{} iterations", status.iterations());
225        println!("elapsed {:?}", now.elapsed());
226
227        assert_eq!(max_iters, panoc.max_iter);
228        assert!(status.has_converged());
229        assert!(status.iterations() < max_iters);
230        assert!(status.norm_fpr() < tolerance);
231
232        /* CHECK FEASIBILITY */
233        let mut u_project = [0.0; 2];
234        u_project.copy_from_slice(&u_solution);
235        bounds.project(&mut u_project);
236        unit_test_utils::assert_nearly_equal_array(
237            &u_solution,
238            &u_project,
239            1e-12,
240            1e-16,
241            "infeasibility detected",
242        );
243    }
244
245    #[test]
246    fn t_panoc_in_loop() {
247        /* USER PARAMETERS */
248        let tolerance = 1e-5;
249        let mut a_param = 1.0;
250        let mut b_param = 100.0;
251        let n_dimension = 2;
252        let lbfgs_memory = 10;
253        let max_iters = 100;
254        let mut u_solution = [-1.5, 0.9];
255        let mut panoc_cache = PANOCCache::new(n_dimension, tolerance, lbfgs_memory);
256        for i in 1..=100 {
257            b_param *= 1.01;
258            a_param -= 1e-3;
259            // Note: updating `radius` like this because `radius += 0.01` builds up small
260            // numerical errors and is less reliable
261            let radius = 1.0 + 0.01 * (i as f64);
262            let cost_gradient = |u: &[f64], grad: &mut [f64]| -> FunctionCallResult {
263                mocks::rosenbrock_grad(a_param, b_param, u, grad);
264                Ok(())
265            };
266            let cost_function = |u: &[f64], c: &mut f64| -> FunctionCallResult {
267                *c = mocks::rosenbrock_cost(a_param, b_param, u);
268                Ok(())
269            };
270            let bounds = constraints::Ball2::new(None, radius);
271            let problem = Problem::new(&bounds, cost_gradient, cost_function);
272            let mut panoc = PANOCOptimizer::new(problem, &mut panoc_cache).with_max_iter(max_iters);
273
274            let status = panoc.solve(&mut u_solution).unwrap();
275
276            println!("status = {:#?}", status);
277            println!(
278                "parameters: (a={:.4}, b={:.4}, r={:.4}), iters = {}",
279                a_param,
280                b_param,
281                radius,
282                status.iterations()
283            );
284
285            /* CHECK FEASIBILITY */
286            // The norm of u must be <= radius
287            let norm_u = crate::matrix_operations::norm2(&u_solution);
288            assert!(
289                norm_u <= radius + 5e-16,
290                "infeasibility in problem solution"
291            );
292
293            assert_eq!(max_iters, panoc.max_iter);
294            assert!(status.has_converged());
295            assert!(status.iterations() < max_iters);
296            assert!(status.norm_fpr() < tolerance);
297        }
298    }
299
300    #[test]
301    fn t_panoc_optimizer_akkt_tolerance() {
302        /* USER PARAMETERS */
303        let tolerance = 1e-6;
304        let akkt_tolerance = 1e-6;
305        let a_param = 1.0;
306        let b_param = 200.0;
307        let n_dimension = 2;
308        let lbfgs_memory = 8;
309        let max_iters = 580;
310        let mut u_solution = [-1.5, 0.9];
311
312        let cost_gradient = |u: &[f64], grad: &mut [f64]| -> FunctionCallResult {
313            mocks::rosenbrock_grad(a_param, b_param, u, grad);
314            Ok(())
315        };
316        let cost_function = |u: &[f64], c: &mut f64| -> FunctionCallResult {
317            *c = mocks::rosenbrock_cost(a_param, b_param, u);
318            Ok(())
319        };
320
321        let radius = 1.2;
322        let bounds = constraints::Ball2::new(None, radius);
323
324        let mut panoc_cache = PANOCCache::new(n_dimension, tolerance, lbfgs_memory);
325        let problem = Problem::new(&bounds, cost_gradient, cost_function);
326
327        let mut panoc = PANOCOptimizer::new(problem, &mut panoc_cache)
328            .with_max_iter(max_iters)
329            .with_akkt_tolerance(akkt_tolerance);
330
331        let status = panoc.solve(&mut u_solution).unwrap();
332
333        assert_eq!(max_iters, panoc.max_iter);
334        assert!(status.has_converged());
335        assert!(status.iterations() < max_iters);
336        assert!(status.norm_fpr() < tolerance);
337    }
338}