ZonoOpt v1.0.0
Loading...
Searching...
No Matches
MI_Solver.hpp
Go to the documentation of this file.
1#ifndef ZONOOPT_MI_SOLVER_
2#define ZONOOPT_MI_SOLVER_
3
15#include <thread>
16#include <atomic>
17#include <mutex>
18#include <condition_variable>
19#include <sstream>
20#include <iomanip>
21#include <variant>
22#include <memory_resource>
23#include <cmath>
24
25#include "MI_DataStructures.hpp"
27#include "ADMM.hpp"
28
29namespace ZonoOpt::detail {
30
31 class MI_solver
32 {
33 public:
34 explicit MI_solver(const MI_data& data) : data(data), node_queue(comp)
35 {
36 // check settings validity
37 if (!this->data.admm_data->settings.settings_valid())
38 {
39 throw std::invalid_argument("MI_solver setup: invalid settings.");
40 }
41
42 // check number of threads is valid
43 if (this->data.admm_data->settings.n_threads_bnb > std::thread::hardware_concurrency()-1)
44 {
45 std::stringstream ss;
46 ss << "MI_solver setup: number of threads for branch and bound (" << this->data.admm_data->settings.n_threads_bnb
47 << ") + convergence monitoring (1) exceeds available threads (" << std::thread::hardware_concurrency() << ").";
48 throw std::invalid_argument(ss.str());
49 }
50 }
51
52 // solve
53 OptSolution solve()
54 {
55 this->multi_sol = false;
56 auto sol = solver_core();
57 return std::get<OptSolution>(sol);
58 }
59
60 // branch and bound where all possible solutions are returned
61 std::pair<std::vector<OptSolution>, OptSolution> multi_solve(const int max_sols = std::numeric_limits<int>::max())
62 {
63 this->multi_sol = true;
64 auto sol = solver_core(max_sols);
65 return std::get<std::pair<std::vector<OptSolution>, OptSolution>>(sol);
66 }
67
68
69 private:
70
71 struct NodeDeleter
72 {
73 std::pmr::synchronized_pool_resource* pool_ptr;
74
75 explicit NodeDeleter(std::pmr::synchronized_pool_resource* pool_ptr) : pool_ptr(pool_ptr) {}
76
77 void operator()(Node* node) const
78 {
79 if (node)
80 {
81 node->~Node();
82 pool_ptr->deallocate(node, sizeof(Node), alignof(Node));
83 }
84 }
85 };
86
87 struct NodeCompare
88 {
89 bool operator()(const std::unique_ptr<Node, NodeDeleter>& n1, const std::unique_ptr<Node, NodeDeleter>& n2) const
90 {
91 return n1->solution.J > n2->solution.J;
92 }
93 };
94
95 const MI_data data;
96 std::pmr::synchronized_pool_resource pool;
97 NodeCompare comp;
98
99 PriorityQueuePrunable<std::unique_ptr<Node, NodeDeleter>, NodeCompare> node_queue; // priority queue for nodes
100 mutable std::mutex pq_mtx;
101 std::condition_variable pq_cv_bnb; // condition variables for branch-and-bound threads
102
103 bool multi_sol = false;
104 std::shared_ptr<ADMM_data> bnb_data; // data for branch-and-bound threads
105
106 std::atomic<bool> converged = false;
107 std::atomic<bool> done = false;
108 std::atomic<bool> feasible = false; // feasible solution found
109 std::atomic<long int> qp_iter = 0; // number of QP iterations
110 std::atomic<int> iter = 0; // number of iterations
111 std::atomic<zono_float> J_max = std::numeric_limits<zono_float>::infinity(); // upper bound
112 ThreadSafeAccess<Eigen::Vector<zono_float, -1>> z, x, u; // solution vector
113 std::atomic<zono_float> primal_residual = std::numeric_limits<zono_float>::infinity();
114 std::atomic<zono_float> dual_residual = std::numeric_limits<zono_float>::infinity();
115 ThreadSafeIncrementable<double> total_startup_time{0.0};
116 ThreadSafeIncrementable<double> total_run_time{0.0};
117 ThreadSafeMultiset J_threads; // threads for J values
118 ThreadSafeVector<OptSolution> solutions; // solutions found
119
120 // allocate nodes
121 std::unique_ptr<Node, NodeDeleter> make_node(const std::shared_ptr<ADMM_data>& data)
122 {
123 void* mem = pool.allocate(sizeof(Node), alignof(Node));
124 Node* node = new (mem) Node(data);
125 return {node, NodeDeleter(&pool)};
126 }
127
128 std::unique_ptr<Node, NodeDeleter> clone_node(const std::unique_ptr<Node, NodeDeleter>& other)
129 {
130 void* mem = pool.allocate(sizeof(Node), alignof(Node));
131 Node* node = new (mem) Node(*other);
132 return {node, NodeDeleter(&pool)};
133 }
134
135 // solver core
136 std::variant<OptSolution, std::pair<std::vector<OptSolution>, OptSolution>> solver_core(
137 int max_sols = std::numeric_limits<int>::max())
138 {
139 // start timer
140 auto start = std::chrono::high_resolution_clock::now();
141 double run_time;
142
143 // set flags
144 this->done = false;
145 this->converged = false;
146
147 // verbosity
148 std::stringstream ss;
149 if (this->data.admm_data->settings.verbose) {
150 if (this->multi_sol)
151 {
152 ss << "Finding up to " << max_sols << " solutions to MIQP with " << this->data.admm_data->n_x << " variables and "
153 << this->data.admm_data->n_cons << " constraints using " << this->data.admm_data->settings.n_threads_bnb << " branch-and-bound threads.";
154 }
155 else
156 {
157 ss << "Solving MIQP problem with " << this->data.admm_data->n_x << " variables and "
158 << this->data.admm_data->n_cons << " constraints using " << this->data.admm_data->settings.n_threads_bnb << " branch-and-bound threads.";
159 }
160 print_str(ss);
161 }
162
163 auto return_infeasible_solution = [this, &start]() -> std::variant<OptSolution, std::pair<std::vector<OptSolution>, OptSolution>>
164 {
165 OptSolution infeasible_solution;
166 infeasible_solution.infeasible = true;
167 infeasible_solution.run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
168 std::chrono::high_resolution_clock::now() - start).count());
169 infeasible_solution.startup_time = infeasible_solution.run_time;
170 infeasible_solution.iter = 0;
171 infeasible_solution.converged = false;
172 infeasible_solution.J = std::numeric_limits<zono_float>::infinity();
173 infeasible_solution.z = Eigen::Vector<zono_float, -1>::Zero(this->data.admm_data->n_x);
174 infeasible_solution.x = Eigen::Vector<zono_float, -1>::Zero(this->data.admm_data->n_x);
175 infeasible_solution.u = Eigen::Vector<zono_float, -1>::Zero(this->data.admm_data->n_x);
176 infeasible_solution.primal_residual = std::numeric_limits<zono_float>::infinity();
177 infeasible_solution.dual_residual = std::numeric_limits<zono_float>::infinity();
178
179 if (this->multi_sol)
180 {
181 return std::make_pair(this->solutions.get(), infeasible_solution);
182 }
183 else
184 {
185 return infeasible_solution;
186 }
187 };
188
189 // add root node
190 this->bnb_data.reset(this->data.admm_data->clone()); // init
191 this->bnb_data->settings.verbose = false;
192 this->bnb_data->settings.eps_dual = this->data.admm_data->settings.eps_dual_search;
193 this->bnb_data->settings.eps_prim = this->data.admm_data->settings.eps_prim_search;
194 std::unique_ptr<Node, NodeDeleter> root = this->make_node(this->bnb_data);
195 if (!root->run_contractor()) // check for infeasibility during setup via interval contractor
196 {
197 return return_infeasible_solution();
198 }
199
200 // log startup time
201 zono_float startup_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
202 std::chrono::high_resolution_clock::now() - start).count());
203 if (this->data.admm_data->settings.verbose)
204 {
205 ss << "Startup time = " << startup_time << " sec";
206 print_str(ss);
207 }
208
209 // solve root relaxation
210 root->solve();
211 if (root->solution.infeasible)
212 {
213 return return_infeasible_solution();
214 }
215
216 // start threads
217 std::vector<std::thread> bnb_threads;
218 for (int i=0; i<this->data.admm_data->settings.n_threads_bnb; i++)
219 {
220 bnb_threads.emplace_back([this]() { worker_loop(); });
221 }
222
223 // push root to node queue
224 this->push_node(std::move(root));
225
226 // loop and check for exit conditions
227 int print_iter = 0;
228 if (this->data.admm_data->settings.verbose)
229 {
230 ss << std::endl << std::setw(10) << "Iter" << std::setw(10) << "Queue" << std::setw(10) <<
231 "Threads" << std::setw(10) << "Time [s]" << std::setw(10) << "J_min" << std::setw(10) <<
232 "J_max" << std::setw(10) << "Gap [%]" << std::setw(10) << "Feasible" << std::setw(10);
233 print_str(ss);
234 }
235
236 while (!this->done)
237 {
238 // check for timeout
239 run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
240 std::chrono::high_resolution_clock::now() - start).count());
241 if (run_time > this->data.admm_data->settings.t_max)
242 {
243 this->done = true;
244 }
245
246 // check for max nodes
247 int queue_size;
248 {
249 std::lock_guard<std::mutex> lock(pq_mtx);
250 queue_size = static_cast<int>(this->node_queue.size());
251 }
252 if (queue_size > this->data.admm_data->settings.max_nodes)
253 {
254 this->done = true;
255 }
256
257 // check for max iterations
258 if (this->iter >= this->data.admm_data->settings.k_max_bnb)
259 {
260 this->done = true;
261 }
262
263 // check for convergence
264
265 // get lower bound / check if there are no nodes remaining
266 zono_float J_min = -std::numeric_limits<zono_float>::infinity();
267 {
268 std::pair<zono_float, bool> J_min_threads_pair;
269 std::lock_guard<std::mutex> lock(pq_mtx);
270 J_min_threads_pair = this->J_threads.get_min(); // lower bound from active threads
271
272 if (this->node_queue.empty())
273 {
274 if (!J_min_threads_pair.second)
275 {
276 this->done = true; // no nodes remaining
277 this->converged = true;
278 }
279 else
280 {
281 J_min = J_min_threads_pair.first;
282 }
283 }
284 else
285 {
286 J_min = std::min(this->node_queue.top()->solution.J, J_min_threads_pair.first);
287 }
288 }
289
290 // check for convergence based on lower and upper bounds
291 zono_float gap_percent=0;
292 if (!this->multi_sol)
293 {
294 const zono_float gap = std::abs(this->J_max - J_min);
295 gap_percent = std::abs(this->J_max - J_min)/std::abs(this->J_max);
296 if ((gap_percent < this->data.admm_data->settings.eps_r) || (gap < this->data.admm_data->settings.eps_a))
297 {
298 this->done = true;
299 this->converged = true;
300 }
301 }
302 else // check based on number of solutions
303 {
304 if (this->solutions.size() >= max_sols)
305 {
306 this->done = true;
307 this->converged = true;
308 }
309 }
310
311 // verbosity
312 if (this->data.admm_data->settings.verbose && (this->iter >= print_iter))
313 {
314 size_t n_threads = this->J_threads.size();
315 ss << std::setw(10) << this->iter << std::setw(10) << queue_size << std::setw(10)
316 << n_threads << std::setw(10) << run_time << std::setw(10)
317 << J_min << std::setw(10) << this->J_max << std::setw(10)
318 << gap_percent*100.0f << std::setw(10)
319 << (this->feasible ? "true" : "false") << std::endl;
320 print_str(ss);
321 print_iter += this->data.admm_data->settings.verbosity_interval;
322 }
323 }
324
325 // clean up
326 pq_cv_bnb.notify_all(); // notify all threads to stop waiting
327 {
328 std::lock_guard<std::mutex> lock(pq_mtx);
329 this->node_queue.clear();
330 }
331 this->J_threads.clear();
332
333 for (auto& thread : bnb_threads)
334 {
335 if (thread.joinable()) thread.join();
336 }
337
338 this->node_queue.clear(); // nodes need to be freed before pool goes out of scope to avoid race condition
339
340 // assemble solution
341 OptSolution solution;
342 solution.z = this->z.get();
343 solution.J = this->J_max;
344 solution.run_time = 1e-6 * static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(
345 std::chrono::high_resolution_clock::now() - start).count());
346 solution.startup_time = startup_time;
347 solution.iter = this->iter;
348 solution.converged = this->converged;
349 solution.infeasible = !this->feasible;
350
351 solution.x = this->x.get();
352 solution.u = this->u.get();
353 solution.primal_residual = this->primal_residual;
354 solution.dual_residual = this->dual_residual;
355
356 if (this->multi_sol)
357 {
358 if (this->data.admm_data->settings.verbose)
359 {
360 ss << "Found " << this->solutions.size() << " solutions to MIQP problem.";
361 print_str(ss);
362 }
363
364 return std::make_pair(solutions.get(), solution);
365 }
366 else
367 {
368 // verbosity
369 if (this->data.admm_data->settings.verbose)
370 {
371 ss << "Converged = " << (this->converged ? "true" : "false") << ", Feasible = " <<
372 (this->feasible ? "true" : "false") << ", Iterations = " << this->iter << ", Solve time = " <<
373 solution.run_time << " sec, Objective = " << solution.J << ", Average QP iterations = " <<
374 static_cast<double>(this->qp_iter)/ static_cast<double>(this->iter)
375 << ", Average solve time = " << this->total_run_time.get()/ static_cast<double>(this->iter) << " sec, Average startup time = "
376 << this->total_startup_time.get() / static_cast<double>(this->iter) << " sec" << std::endl;
377 print_str(ss);
378 }
379
380 return solution;
381 }
382 }
383
384 // solve node and branch
385 void solve_and_branch(const std::unique_ptr<Node, NodeDeleter>& node)
386 {
387 // objective prior to solving
388 const zono_float J_min_prior = node->solution.J;
389
390 // solve node
391 node->solve(&this->done);
392 if (this->done) return;
393
394 // cleanup function
395 auto cleanup = [&, this]()
396 {
397 // remove J from J_threads vector
398 this->J_threads.remove(J_min_prior);
399
400 // increment nodes evaluated and logging info
401 ++this->iter;
402 this->qp_iter += node->solution.iter;
403 this->total_run_time += node->solution.run_time;
404 this->total_startup_time += node->solution.startup_time;
405 };
406
407 // return if infeasible, not converged, or no optimal solution exists in branch
408 if (!(node->solution.infeasible || !node->solution.converged || (node->solution.J > this->J_max && !this->multi_sol)))
409 {
410 // check if node is integer feasible
411 if (is_integer_feasible(node->solution.z.segment(this->data.idx_b.first, this->data.idx_b.second)))
412 {
413 // rerun with refined tolerance
414 if (this->data.admm_data->settings.polish)
415 {
416 node->update_convergence_tolerances(this->data.admm_data->settings.eps_prim, this->data.admm_data->settings.eps_dual);
417 node->warmstart(node->solution.z, node->solution.u);
418 node->solve();
419 }
420
421 // make sure still integer feasible after refining
422 if (!is_integer_feasible(node->solution.z.segment(this->data.idx_b.first, this->data.idx_b.second)))
423 {
424 branch_most_frac(node);
425 cleanup(); // cleanup function
426 return;
427 }
428
429 // make sure return conditions are not met after refining
430 if (node->solution.infeasible || !node->solution.converged || (node->solution.J > this->J_max && !this->multi_sol))
431 {
432 cleanup(); // cleanup function
433 return;
434 }
435 if (this->multi_sol) // store solution if doing multisol
436 {
437 std::function<bool(const OptSolution&, const OptSolution&)> compare_eq = [this](const OptSolution& a, const OptSolution& b)
438 {
439 return this->check_bin_equal(a, b);
440 };
441 if (!this->solutions.contains(node->solution, compare_eq)) // new solution
442 this->solutions.push_back(node->solution);
443 }
444 if (node->solution.J < this->J_max - zono_eps) // check if node is better than current best
445 {
446 // update incumbent
447 this->J_max = node->solution.J;
448 this->x.set(node->solution.x);
449 this->z.set(node->solution.z);
450 this->u.set(node->solution.u);
451 this->primal_residual = node->solution.primal_residual;
452 this->dual_residual = node->solution.dual_residual;
453 this->feasible = true;
454
455 // prune
456 if (!this->multi_sol) this->prune(node->solution.J);
457 }
458 }
459 else
460 {
461 branch_most_frac(node);
462 }
463 }
464
465 cleanup(); // cleanup function
466 }
467
468 // check if integer feasible, xb is vector of relaxed binary variables
469 bool is_integer_feasible(const Eigen::Ref<const Eigen::Vector<zono_float,-1>> xb) const
470 {
471 const zono_float low = this->data.zero_one_form ? 0 : -1;
472 constexpr zono_float high = 1;
473
474 for (int i=0; i<xb.size(); i++)
475 {
476 if ((std::abs(xb(i) - high) > zono_eps) && (std::abs(xb(i) - low) > zono_eps))
477 return false;
478 }
479 return true;
480 }
481
482 // most fractional branching
483 void branch_most_frac(const std::unique_ptr<Node, NodeDeleter>& node)
484 {
485 // must be at least 1 binary variable
486 if (this->data.idx_b.second <= 0)
487 return;
488
489 const zono_float low = this->data.zero_one_form ? 0 : -1;
490 constexpr zono_float high = 1;
491
492 // round and find most fractional variable
493 const Eigen::Array<zono_float, -1, 1> xb = node->solution.z.segment(this->data.idx_b.first, this->data.idx_b.second).array();
494 Eigen::Array<zono_float, -1, 1> l (xb.size());
495 Eigen::Array<zono_float, -1, 1> u (xb.size());
496 l.setConstant(low);
497 u.setConstant(high);
498 const Eigen::Array<zono_float, -1, 1> d_l = (xb - l).abs();
499 const Eigen::Array<zono_float, -1, 1> d_u = (xb - u).abs();
500 const Eigen::Array<zono_float, -1, 1> d = d_l.min(d_u); // distance to rounded value
501 int idx_most_frac;
502 d.maxCoeff(&idx_most_frac); // index of most fractional variable
503 idx_most_frac = this->data.idx_b.first + idx_most_frac; // convert to original index
504
505 // branch on most fractional variable
506 std::unique_ptr<Node, NodeDeleter> left = this->clone_node(node);
507 std::unique_ptr<Node, NodeDeleter> right = this->clone_node(node);
508
509 // branches
510 const bool left_inf = !left->fix_bound(idx_most_frac, low);
511 const bool right_inf = !right->fix_bound(idx_most_frac, high);
512
513 // warm start
514 left->warmstart(node->solution.z, node->solution.u);
515 right->warmstart(node->solution.z, node->solution.u);
516
517 switch (this->data.admm_data->settings.search_mode)
518 {
519 case (0):
520 {
521 // best first: push both nodes to queue
522 if (!left_inf) this->push_node(std::move(left));
523 if (!right_inf) this->push_node(std::move(right));
524 break;
525 }
526 case (1):
527 {
528 // best dive: push worse nodes to queue, solve better node
529 if (left_inf && right_inf) // both branches infeasible
530 {
531 return; // nothing to do
532 }
533 else if (left_inf)
534 {
535 this->solve_and_branch(right);
536 }
537 else if (right_inf)
538 {
539 this->solve_and_branch(left);
540 }
541 else // both branches feasible
542 {
543 if (left->get_box().width() > right->get_box().width()) // left is worse
544 {
545 this->push_node(std::move(left));
546 this->solve_and_branch(right);
547 }
548 else // right is worse
549 {
550 this->push_node(std::move(right));
551 this->solve_and_branch(left);
552 }
553 }
554 break;
555 }
556 default:
557 {
558 std::stringstream ss;
559 ss << "MI_ADMM_solver: unknown search mode " << this->data.admm_data->settings.search_mode;
560 throw std::runtime_error(ss.str());
561 }
562 }
563 }
564
565 // loop for multithreading
566 void worker_loop()
567 {
568 while (!this->done)
569 {
570 std::unique_ptr<Node, NodeDeleter> node = make_node(this->bnb_data);
571 {
572 std::unique_lock<std::mutex> lock(pq_mtx);
573 pq_cv_bnb.wait(lock, [this]() { return this->done || !this->node_queue.empty(); });
574 if (this->done) return;
575 node = std::move(this->node_queue.pop_top());
576 this->J_threads.add(node->solution.J); // add J to J_threads vector, need to do this before releasing lock
577 }
578 solve_and_branch(node);
579 }
580 }
581
582 // push node to queue
583 void push_node(std::unique_ptr<Node, NodeDeleter>&& node)
584 {
585 std::unique_lock<std::mutex> lock(pq_mtx);
586 this->node_queue.push(std::move(node));
587 pq_cv_bnb.notify_one();
588 }
589
590 // prune
591 void prune(const zono_float J_max)
592 {
593 // create node with J = J_max
594 const std::unique_ptr<Node, NodeDeleter> n = this->make_node(this->bnb_data);
595 n->solution.J = J_max;
596 {
597 std::lock_guard<std::mutex> lock(pq_mtx);
598 this->node_queue.prune(n);
599 }
600 }
601
602 // check if 2 solutions correspond to the same binaries
603 bool check_bin_equal(const OptSolution& sol1, const OptSolution& sol2) const
604 {
605 return (sol1.z.segment(this->data.idx_b.first, this->data.idx_b.second) - sol2.z.segment(this->data.idx_b.first, this->data.idx_b.second))
606 .cwiseAbs().maxCoeff() < zono_eps;
607 }
608
609
610 };
611
612} // namespace ZonoOpt::detail
613
614
615
616
617#endif
ADMM implementation used within ZonoOpt.
Data structures for mixed-integer optimization in ZonoOpt library.
Optimization settings and solution data structures for ZonoOpt library.
#define zono_float
Defines the floating-point type used in ZonoOpt.
Definition ZonoOpt.hpp:43
#define zono_eps
Defines the precision used for floating point comparisons in ZonoOpt.
Definition ZonoOpt.hpp:51