ZonoOpt v1.0.0
Loading...
Searching...
No Matches
ADMM.hpp
Go to the documentation of this file.
1#ifndef ZONOOPT_ADMM_HPP_
2#define ZONOOPT_ADMM_HPP_
3
16#include <vector>
17#include <chrono>
18#include <stdexcept>
19#include <iostream>
20#include <sstream>
21#include <memory>
22#include <set>
23#include <cmath>
24#include <random>
25#include <atomic>
26
27#include "Eigen/Dense"
28#include "Eigen/Sparse"
29#include "CholeskyUtilities.hpp"
30#include "Intervals.hpp"
33
34/*
35 Primary reference:
36 Boyd, Stephen, et al.
37 "Distributed optimization and statistical learning via the alternating direction method of multipliers."
38 Foundations and TrendsĀ® in Machine learning 3.1 (2011): 1-122.
39*/
40
41namespace ZonoOpt::detail {
46 struct ADMM_data : std::enable_shared_from_this<ADMM_data>
47 {
48 Eigen::SparseMatrix<zono_float> P, A, AT;
49 Eigen::SparseMatrix<zono_float, Eigen::RowMajor> A_rm;
50 Eigen::Vector<zono_float, -1> q, b;
51 Eigen::Vector<zono_float, 1> c;
52 LDLT_data ldlt_data_M, ldlt_data_AAT;
53 int n_x, n_cons;
54 zono_float sqrt_n_x;
55 std::shared_ptr<Box> x_box;
56 OptSettings settings;
57
58 // constructor
59 ADMM_data() = default;
60
61 ADMM_data(const Eigen::SparseMatrix<zono_float>& P, const Eigen::Vector<zono_float, -1>& q,
62 const Eigen::SparseMatrix<zono_float>& A, const Eigen::Vector<zono_float, -1>& b,
63 const Eigen::Vector<zono_float, -1>& x_l, const Eigen::Vector<zono_float, -1>& x_u,
64 const zono_float c=0, const OptSettings& settings= OptSettings())
65 {
66 set(P, q, A, b, x_l, x_u, c, settings);
67 }
68
69 // set method
70 void set(const Eigen::SparseMatrix<zono_float>& P, const Eigen::Vector<zono_float, -1>& q,
71 const Eigen::SparseMatrix<zono_float>& A, const Eigen::Vector<zono_float, -1>& b,
72 const Eigen::Vector<zono_float, -1>& x_l, const Eigen::Vector<zono_float, -1>& x_u,
73 const zono_float c=0, const OptSettings& settings= OptSettings())
74 {
75 this->P = P;
76 this->q = q;
77 this->A = A;
78 this->AT = A.transpose();
79 this->A_rm = A;
80 this->b = b;
81 this->c(0) = c;
82
83 this->n_x = static_cast<int>(P.rows());
84 this->n_cons = static_cast<int>(A.rows());
85 this->sqrt_n_x = std::sqrt(static_cast<zono_float>(this->n_x));
86
87 this->x_box = std::make_shared<Box>(x_l, x_u);
88
89 if (!settings.settings_valid()) throw std::invalid_argument("ADMM data: invalid settings.");
90 this->settings = settings;
91 }
92
93 // clone method
94 ADMM_data* clone() const
95 {
96 const auto new_data = new ADMM_data(*this);
97 new_data->x_box = std::make_shared<Box>(*this->x_box);
98 return new_data;
99 }
100 };
101
102 // print function
103 inline void print_str(std::stringstream &ss)
104 {
105 #ifdef IS_PYTHON_ENV
106 py::print(ss.str());
107 #else
108 std::cout << ss.str() << std::endl;
109 #endif
110 ss.str("");
111 }
112
113 // utilities
114
119 class ADMM_solver
120 {
121 public:
122
128 explicit ADMM_solver(const ADMM_data& data)
129 {
130 // store data and settings
131 this->data = std::make_shared<ADMM_data>(data);
132 this->eps_prim = data.settings.eps_prim;
133 this->eps_dual = data.settings.eps_dual;
134
135 // flags
136 this->is_warmstarted = false;
137 }
138
144 explicit ADMM_solver(const std::shared_ptr<ADMM_data>& data)
145 {
146 // store data and settings
147 this->data = data;
148 this->eps_prim = data->settings.eps_prim;
149 this->eps_dual = data->settings.eps_dual;
150
151 // flags
152 this->is_warmstarted = false;
153 }
154
160 ADMM_solver(const ADMM_solver& other)
161 {
162 this->data = other.data;
163 this->x0 = other.x0;
164 this->u0 = other.u0;
165 this->is_warmstarted = other.is_warmstarted;
166 this->eps_dual = other.eps_dual;
167 this->eps_prim = other.eps_prim;
168 }
169
174 virtual ~ADMM_solver() = default;
175
182 virtual void warmstart(const Eigen::Vector<zono_float, -1>& x0,
183 const Eigen::Vector<zono_float, -1>& u0)
184 {
185 // copy in warm start variables
186 this->x0 = x0;
187 this->u0 = u0;
188
189 // set flag
190 this->is_warmstarted = true;
191 }
192
197 virtual void factorize()
198 {
199 auto t0 = std::chrono::high_resolution_clock::now();
200 double run_time;
201 std::stringstream ss;
202 if (!this->data->ldlt_data_M.factorized)
203 {
204 t0 = std::chrono::high_resolution_clock::now();
205 this->factorize_M();
206 if (this->data->settings.verbose)
207 {
208 run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
209 std::chrono::high_resolution_clock::now() - t0).count());
210 ss << "M factorization time = " << run_time << " sec";
211 print_str(ss);
212 }
213 }
214 if (!this->data->ldlt_data_AAT.factorized)
215 {
216 t0 = std::chrono::high_resolution_clock::now();
217 this->factorize_AAT();
218 if (this->data->settings.verbose)
219 {
220 run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
221 std::chrono::high_resolution_clock::now() - t0).count());
222 ss << "A*A^T factorization time = " << run_time << " sec";
223 print_str(ss);
224 }
225 }
226 }
227
234 OptSolution solve(std::atomic<bool>* stop)
235 {
236 // declare solution
237 OptSolution solution;
238
239 // startup
240 if (const bool contractor_feasible = this->startup(*this->data->x_box, solution); !contractor_feasible)
241 {
242 return solution;
243 }
244
245 // solve
246 solve_core(*this->data->x_box, solution, stop);
247 return solution;
248 }
249 OptSolution solve() { return this->solve(nullptr); }
250
251 protected:
252
253 // protected fields
254 std::shared_ptr<ADMM_data> data;
255 zono_float eps_prim=static_cast<zono_float>(1e-3), eps_dual=static_cast<zono_float>(1e-3);
256
257 // startup method
258 bool startup(Box& x_box, OptSolution& solution, const std::set<int>& contract_inds=std::set<int>())
259 {
260 // start timer
261 const auto start = std::chrono::high_resolution_clock::now();
262
263 // reset verbosity
264 std::stringstream ss;
265
266 // check that problem data is consistent
267 if (!this->check_problem_dimensions())
268 {
269 throw std::invalid_argument("ADMM solve: inconsistent problem data dimensions.");
270 }
271 if (this->data->settings.verbose)
272 {
273 ss << "Solving ADMM problem with " << this->data->n_x << " variables and " << this->data->n_cons << " constraints.";
274 print_str(ss);
275 }
276
277 // factorize if not already done
278 this->factorize();
279
280 // apply contractor
281 bool contractor_feasible = true;
282 if (this->data->settings.use_interval_contractor)
283 {
284 const auto t0 = std::chrono::high_resolution_clock::now();
285 if (contract_inds.empty())
286 {
287 contractor_feasible = x_box.contract(this->data->A_rm, this->data->b, this->data->settings.contractor_iter);
288 }
289 else
290 {
291 contractor_feasible = x_box.contract_subset(this->data->A_rm, this->data->b, this->data->settings.contractor_iter,
292 this->data->A, contract_inds, this->data->settings.contractor_tree_search_depth);
293 }
294
295 if (this->data->settings.verbose)
296 {
297 const double run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
298 std::chrono::high_resolution_clock::now() - t0).count());
299 ss << "Interval contractor time = " << run_time << " sec";
300 print_str(ss);
301 }
302 }
303
304 // log startup time
305 const double startup_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
306 std::chrono::high_resolution_clock::now() - start).count());
307 solution.startup_time = startup_time;
308 solution.run_time = startup_time;
309
310 // early exit if contractor is infeasible
311 if (!contractor_feasible)
312 {
313 if (this->data->settings.verbose)
314 {
315 ss << "Infeasibility detected via interval contractor";
316 print_str(ss);
317 }
318
319 solution.infeasible = true;
320 solution.iter = 0;
321 solution.converged = false;
322 solution.primal_residual = std::numeric_limits<zono_float>::infinity();
323 solution.dual_residual = std::numeric_limits<zono_float>::infinity();
324 solution.J = std::numeric_limits<zono_float>::infinity();
325 solution.x = Eigen::Vector<zono_float, -1>::Zero(this->data->n_x);
326 solution.z = solution.x; // 0
327 solution.u = solution.x; // 0
328 }
329 return contractor_feasible;
330 }
331
332 // core solve method
333 virtual void solve_core(const Box& x_box, OptSolution& solution, std::atomic<bool>* stop)
334 {
335 // start clock
336 auto start = std::chrono::high_resolution_clock::now();
337 std::stringstream ss;
338
339 // initial values
340 Eigen::Vector<zono_float, -1> xk, zk, uk, zkm1, rhs, x_nu;
341 if (this->is_warmstarted)
342 {
343 xk = this->x0;
344 uk = this->u0;
345 }
346 else
347 {
348 xk = 0.5*(x_box.lower() + x_box.upper());
349 uk = Eigen::Vector<zono_float, -1>::Zero(this->data->n_x);
350 }
351 zk = xk;
352 rhs = Eigen::Vector<zono_float, -1>::Zero(this->data->n_x + this->data->n_cons);
353 rhs.segment(this->data->n_x, this->data->n_cons) = this->data->b; // unchanging
354 zkm1 = zk;
355
356 // init residuals
357 zono_float rp_k=std::numeric_limits<zono_float>::infinity(), rd_k=std::numeric_limits<zono_float>::infinity();
358
359 // init loop
360 int k = 0;
361 double run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
362 std::chrono::high_resolution_clock::now() - start).count());
363 bool converged = false, infeasible = false;
364
365 while ((k < this->data->settings.k_max_admm) && (run_time+solution.startup_time < this->data->settings.t_max) && !converged && !infeasible
366 && !(stop && (*stop)))
367 {
368 // x update
369 rhs.segment(0, this->data->n_x) = -this->data->q + this->data->settings.rho*(zk - uk);
370 x_nu = solve_LDLT(this->data->ldlt_data_M, rhs);
371 xk = x_nu.segment(0, this->data->n_x);
372
373 // z update
374 zk = xk + uk;
375 x_box.project(zk);
376
377 // u update
378 uk += xk - zk;
379
380 // check for infeasibility certificate
381 if (k % this->data->settings.k_inf_check == 0)
382 {
383 infeasible = this->is_infeasibility_certificate(zk - xk, xk, x_box);
384 if (this->data->settings.verbose && infeasible)
385 {
386 ss << "Infeasibility certificate detected at iteration " << k;
387 print_str(ss);
388 }
389 }
390
391 // check convergence
392 if (this->data->settings.inf_norm_conv)
393 {
394 rp_k = (xk - zk).cwiseAbs().maxCoeff();
395 rd_k = this->data->settings.rho*(zk - zkm1).cwiseAbs().maxCoeff();
396 converged = (rp_k < this->eps_prim && rd_k < this->eps_dual);
397 }
398 else
399 {
400 rp_k = (xk - zk).norm();
401 rd_k = this->data->settings.rho*(zk - zkm1).norm();
402 converged = (rp_k < this->data->sqrt_n_x*this->eps_prim && rd_k < this->data->sqrt_n_x*this->eps_dual);
403 }
404
405 // increment
406 zkm1 = zk;
407 ++k;
408
409 // get time
410 run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
411 std::chrono::high_resolution_clock::now() - start).count());
412
413 // verbosity
414 if (this->data->settings.verbose && (k % this->data->settings.verbosity_interval == 0))
415 {
416 ss << "k = " << k << ": primal residual = " << rp_k << ", dual residual = "
417 << rd_k << ", run time = " << run_time << " sec";
418 print_str(ss);
419 }
420 }
421
422 // verbosity
423 if (this->data->settings.verbose)
424 {
425 if (converged)
426 {
427 ss << "ADMM converged in " << k << " iterations.";
428 print_str(ss);
429 }
430 else if (infeasible)
431 {
432 ss << "ADMM detected infeasibility.";
433 print_str(ss);
434 }
435 else
436 {
437 ss << "ADMM did not converge in " << k << " iterations.";
438 print_str(ss);
439 }
440 }
441
442 // reset flags
443 this->is_warmstarted = false;
444
445 // build output
446 solution.x = xk;
447 solution.z = zk;
448 solution.u = uk;
449 solution.J = (0.5*zk.transpose()*this->data->P*zk + this->data->q.transpose()*zk + this->data->c)(0);
450 solution.primal_residual = rp_k;
451 solution.dual_residual = rd_k;
452 solution.run_time = run_time+solution.startup_time;
453 solution.iter = k;
454 solution.converged = converged;
455 solution.infeasible = infeasible;
456 }
457
458 // warm start
459 Eigen::Vector<zono_float, -1> x0, u0;
460
461 // flags
462 bool is_warmstarted = false;
463
464 // factor problem data
465 void factorize_M() const
466 {
467 // system matrix
468 Eigen::SparseMatrix<zono_float> M (this->data->n_x + this->data->n_cons, this->data->n_x + this->data->n_cons);
469
470 Eigen::SparseMatrix<zono_float> I (this->data->n_x, this->data->n_x);
471 I.setIdentity();
472 Eigen::SparseMatrix<zono_float> Phi = this->data->P + this->data->settings.rho*I;
473
474 std::vector<Eigen::Triplet<zono_float>> triplets;
475 get_triplets_offset<zono_float>(Phi, triplets, 0, 0);
476 get_triplets_offset<zono_float>(this->data->A, triplets, this->data->n_x, 0);
477 get_triplets_offset<zono_float>(this->data->AT, triplets, 0, this->data->n_x);
478 M.setFromTriplets(triplets.begin(), triplets.end());
479
480 // factorize system matrix
481 Eigen::SimplicialLDLT<Eigen::SparseMatrix<zono_float>> ldlt_solver_M;
482 ldlt_solver_M.compute(M);
483 if (ldlt_solver_M.info() != Eigen::Success)
484 throw std::runtime_error("ADMM: factorization of problem data failed, most likely A is not full row rank");
485
486 get_LDLT_data(ldlt_solver_M, this->data->ldlt_data_M);
487 }
488
489 void factorize_AAT() const
490 {
491 // factorize A*AT
492 const Eigen::SparseMatrix<zono_float> AAT = this->data->A*this->data->AT;
493 Eigen::SimplicialLDLT<Eigen::SparseMatrix<zono_float>> ldlt_solver_AAT;
494 ldlt_solver_AAT.compute(AAT);
495 if (ldlt_solver_AAT.info() != Eigen::Success)
496 throw std::runtime_error("ADMM: factorization of A*A^T failed, most likely A is not full row rank");
497 get_LDLT_data(ldlt_solver_AAT, this->data->ldlt_data_AAT);
498 }
499
500 // check for infeasibility certificate
501 bool is_infeasibility_certificate(const Eigen::Vector<zono_float, -1>& ek,
502 const Eigen::Vector<zono_float, -1>& xk, const Box& x_box) const
503 {
504 // project ek onto row space of A (i.e. column space of AT)
505 Eigen::Vector<zono_float, -1> A_e = this->data->A*ek;
506 Eigen::Vector<zono_float, -1> AAT_inv_A_e = solve_LDLT(this->data->ldlt_data_AAT, A_e);
507 Eigen::Vector<zono_float, -1> ek_proj = this->data->AT*AAT_inv_A_e;
508
509 // check if this is an infeasibility certificate
510 const zono_float e_x = ek_proj.dot(xk);
511 const Interval e_box = x_box.dot(ek_proj);
512 return !e_box.contains(e_x);
513 }
514
515 bool check_problem_dimensions() const
516 {
517 const bool prob_data_consistent = (this->data->P.rows() == this->data->n_x && this->data->P.cols() == this->data->n_x &&
518 this->data->q.size() == this->data->n_x && this->data->A.rows() == this->data->n_cons &&
519 this->data->A.cols() == this->data->n_x && this->data->b.size() == this->data->n_cons &&
520 this->data->x_box->size() == static_cast<size_t>(this->data->n_x));
521
522 bool warm_start_consistent;
523 if (this->is_warmstarted)
524 {
525 warm_start_consistent = (this->x0.size() == this->data->n_x && this->u0.size() == this->data->n_x &&
526 this->data->x_box->size() == static_cast<size_t>(this->data->n_x));
527 }
528 else
529 warm_start_consistent = true;
530
531 return prob_data_consistent && warm_start_consistent;
532 }
533 };
534
535} // end namespace ZonoOpt::detail
536
537#endif
Internal utilities for Cholesky factorization using Eigen's LDLT solver.
Interval and box classes.
Optimization settings and solution data structures for ZonoOpt library.
Utilities for sparse matrix operations in ZonoOpt library.
Box (i.e., interval vector) class.
Definition Intervals.hpp:718
Interval dot(const Eigen::Vector< zono_float, -1 > &x) const
Linear map with vector.
Definition Intervals.hpp:1102
const Eigen::Vector< zono_float, -1 > & upper() const
Get upper bounds.
Definition Intervals.hpp:868
bool contract_subset(const Eigen::SparseMatrix< zono_float, Eigen::RowMajor > &A_rm, const Eigen::Vector< zono_float, -1 > &b, int iter, const Eigen::SparseMatrix< zono_float > &A, const std::set< int > &inds, int tree_search_depth)
Interval contractor over a subset of the dimensions of the box.
Definition Intervals.hpp:1028
bool contract(const Eigen::SparseMatrix< zono_float, Eigen::RowMajor > &A, const Eigen::Vector< zono_float, -1 > &b, const int iter)
Interval contractor.
Definition Intervals.hpp:999
virtual void project(Eigen::Ref< Eigen::Vector< zono_float, -1 > > x) const
Projects vector onto the Box.
Definition Intervals.hpp:841
const Eigen::Vector< zono_float, -1 > & lower() const
Get lower bounds.
Definition Intervals.hpp:862
#define zono_float
Defines the floating-point type used in ZonoOpt.
Definition ZonoOpt.hpp:43
bool contains(zono_float y) const
checks whether interval contains a value
Definition Intervals.hpp:213
Interval class.
Definition Intervals.hpp:372
Settings for optimization routines in ZonoOpt library.
Definition SolverDataStructures.hpp:26
bool settings_valid() const
Checks whether settings struct is valid.
Definition SolverDataStructures.hpp:102
Solution data structure for optimization routines in ZonoOpt library.
Definition SolverDataStructures.hpp:153
zono_float dual_residual
dual residual, corresponds to optimality
Definition SolverDataStructures.hpp:189
bool infeasible
true if optimization problem is provably infeasible
Definition SolverDataStructures.hpp:175
Eigen::Vector< zono_float, -1 > x
ADMM primal variable, approximately equal to z when converged.
Definition SolverDataStructures.hpp:180
Eigen::Vector< zono_float, -1 > z
solution vector
Definition SolverDataStructures.hpp:157
double run_time
time to compute solution
Definition SolverDataStructures.hpp:163
zono_float J
objective
Definition SolverDataStructures.hpp:160
zono_float primal_residual
primal residual, corresponds to feasibility
Definition SolverDataStructures.hpp:186
int iter
number of iterations
Definition SolverDataStructures.hpp:169
double startup_time
time to factorize matrices and run interval contractors
Definition SolverDataStructures.hpp:166
bool converged
true if optimization has converged
Definition SolverDataStructures.hpp:172
Eigen::Vector< zono_float, -1 > u
ADMM dual variable.
Definition SolverDataStructures.hpp:183