optimization_engine/core/panoc/
panoc_optimizer.rs1use 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
15pub 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 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 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 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 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 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 self.panoc_engine.init(u)?;
133
134 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 if !matrix_operations::is_finite(u) {
157 return Err(SolverError::NotFiniteComputation);
158 }
159
160 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 u.copy_from_slice(&self.panoc_engine.cache.u_half_step);
172
173 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#[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 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 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 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 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 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 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 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 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}