1#ifndef ZONOOPT_MI_SOLVER_
2#define ZONOOPT_MI_SOLVER_
18#include <condition_variable>
22#include <memory_resource>
29namespace ZonoOpt::detail {
34 explicit MI_solver(
const MI_data& data) : data(data), node_queue(comp)
37 if (!this->data.admm_data->settings.settings_valid())
39 throw std::invalid_argument(
"MI_solver setup: invalid settings.");
43 if (this->data.admm_data->settings.n_threads_bnb >
static_cast<int>(std::thread::hardware_concurrency())-1)
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());
55 this->multi_sol =
false;
56 auto sol = solver_core();
57 return std::get<OptSolution>(sol);
61 std::pair<std::vector<OptSolution>, OptSolution> multi_solve(
const int max_sols = std::numeric_limits<int>::max())
63 this->multi_sol =
true;
64 auto sol = solver_core(max_sols);
65 return std::get<std::pair<std::vector<OptSolution>, OptSolution>>(sol);
73 std::pmr::synchronized_pool_resource* pool_ptr;
75 explicit NodeDeleter(std::pmr::synchronized_pool_resource* pool_ptr) : pool_ptr(pool_ptr) {}
77 void operator()(Node* node)
const
82 pool_ptr->deallocate(node,
sizeof(Node),
alignof(Node));
89 bool operator()(
const std::unique_ptr<Node, NodeDeleter>& n1,
const std::unique_ptr<Node, NodeDeleter>& n2)
const
91 return n1->solution.J > n2->solution.J;
96 std::pmr::synchronized_pool_resource pool;
99 PriorityQueuePrunable<std::unique_ptr<Node, NodeDeleter>, NodeCompare> node_queue;
100 mutable std::mutex pq_mtx;
101 std::condition_variable pq_cv_bnb;
103 bool multi_sol =
false;
104 std::shared_ptr<ADMM_data> bnb_data;
106 std::atomic<bool> converged =
false;
107 std::atomic<bool> done =
false;
108 std::atomic<bool> feasible =
false;
109 std::atomic<long int> qp_iter = 0;
110 std::atomic<int> iter = 0;
111 std::atomic<zono_float> J_max = std::numeric_limits<zono_float>::infinity();
112 ThreadSafeAccess<Eigen::Vector<
zono_float, -1>> z, x, u;
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;
118 ThreadSafeVector<OptSolution> solutions;
121 std::unique_ptr<Node, NodeDeleter> make_node(
const std::shared_ptr<ADMM_data>& data)
123 void* mem = pool.allocate(
sizeof(Node),
alignof(Node));
124 Node* node =
new (mem) Node(data);
125 return {node, NodeDeleter(&pool)};
128 std::unique_ptr<Node, NodeDeleter> clone_node(
const std::unique_ptr<Node, NodeDeleter>& other)
130 void* mem = pool.allocate(
sizeof(Node),
alignof(Node));
131 Node* node =
new (mem) Node(*other);
132 return {node, NodeDeleter(&pool)};
136 std::variant<OptSolution, std::pair<std::vector<OptSolution>, OptSolution>> solver_core(
137 int max_sols = std::numeric_limits<int>::max())
140 auto start = std::chrono::high_resolution_clock::now();
145 this->converged =
false;
148 std::stringstream ss;
149 if (this->data.admm_data->settings.verbose) {
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.";
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.";
163 auto return_infeasible_solution = [
this, &start]() -> std::variant<OptSolution, std::pair<std::vector<OptSolution>, OptSolution>>
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();
181 return std::make_pair(this->solutions.get(), infeasible_solution);
185 return infeasible_solution;
190 this->bnb_data.reset(this->data.admm_data->clone());
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())
197 return return_infeasible_solution();
201 double 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)
205 ss <<
"Startup time = " << startup_time <<
" sec";
211 if (root->solution.infeasible)
213 return return_infeasible_solution();
217 std::vector<std::thread> bnb_threads;
218 for (
int i=0; i<this->data.admm_data->settings.n_threads_bnb; i++)
220 bnb_threads.emplace_back([
this]() { worker_loop(); });
224 this->push_node(std::move(root));
228 if (this->data.admm_data->settings.verbose)
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);
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)
249 std::lock_guard<std::mutex> lock(pq_mtx);
250 queue_size =
static_cast<int>(this->node_queue.size());
252 if (queue_size > this->data.admm_data->settings.max_nodes)
258 if (this->iter >= this->data.admm_data->settings.k_max_bnb)
266 zono_float J_min = -std::numeric_limits<zono_float>::infinity();
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();
272 if (this->node_queue.empty())
274 if (!J_min_threads_pair.second)
277 this->converged =
true;
281 J_min = J_min_threads_pair.first;
286 J_min = std::min(this->node_queue.top()->solution.J, J_min_threads_pair.first);
292 if (!this->multi_sol)
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))
299 this->converged =
true;
304 if (this->solutions.size() >=
static_cast<size_t>(max_sols))
307 this->converged =
true;
312 if (this->data.admm_data->settings.verbose && (this->iter >= print_iter))
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;
321 print_iter += this->data.admm_data->settings.verbosity_interval;
326 pq_cv_bnb.notify_all();
328 std::lock_guard<std::mutex> lock(pq_mtx);
329 this->node_queue.clear();
331 this->J_threads.clear();
333 for (
auto& thread : bnb_threads)
335 if (thread.joinable()) thread.join();
338 this->node_queue.clear();
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;
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;
358 if (this->data.admm_data->settings.verbose)
360 ss <<
"Found " << this->solutions.size() <<
" solutions to MIQP problem.";
364 return std::make_pair(solutions.get(), solution);
369 if (this->data.admm_data->settings.verbose)
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;
385 void solve_and_branch(
const std::unique_ptr<Node, NodeDeleter>& node)
388 const zono_float J_min_prior = node->solution.J;
391 node->solve(&this->done);
392 if (this->done)
return;
395 auto cleanup = [&,
this]()
398 this->J_threads.remove(J_min_prior);
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;
408 if (!(node->solution.infeasible || !node->solution.converged || (node->solution.J > this->J_max && !this->multi_sol)))
411 if (is_integer_feasible(node->solution.z.segment(this->data.idx_b.first, this->data.idx_b.second)))
414 if (this->data.admm_data->settings.polish)
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);
422 if (!is_integer_feasible(node->solution.z.segment(this->data.idx_b.first, this->data.idx_b.second)))
424 branch_most_frac(node);
430 if (node->solution.infeasible || !node->solution.converged || (node->solution.J > this->J_max && !this->multi_sol))
437 std::function<bool(
const OptSolution&,
const OptSolution&)> compare_eq = [
this](
const OptSolution& a,
const OptSolution& b)
439 return this->check_bin_equal(a, b);
441 if (!this->solutions.contains(node->solution, compare_eq))
442 this->solutions.push_back(node->solution);
444 if (node->solution.J < this->J_max -
zono_eps)
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;
456 if (!this->multi_sol) this->prune(node->solution.J);
461 branch_most_frac(node);
469 bool is_integer_feasible(
const Eigen::Ref<
const Eigen::Vector<zono_float,-1>> xb)
const
471 const zono_float low = this->data.zero_one_form ? zero : -one;
474 for (
int i=0; i<xb.size(); i++)
476 if ((std::abs(xb(i) - high) >
zono_eps) && (std::abs(xb(i) - low) >
zono_eps))
483 void branch_most_frac(
const std::unique_ptr<Node, NodeDeleter>& node)
486 if (this->data.idx_b.second <= 0)
489 const zono_float low = this->data.zero_one_form ? zero : -one;
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());
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);
502 d.maxCoeff(&idx_most_frac);
503 idx_most_frac = this->data.idx_b.first + idx_most_frac;
506 std::unique_ptr<Node, NodeDeleter> left = this->clone_node(node);
507 std::unique_ptr<Node, NodeDeleter> right = this->clone_node(node);
510 const bool left_inf = !left->fix_bound(idx_most_frac, low);
511 const bool right_inf = !right->fix_bound(idx_most_frac, high);
514 left->warmstart(node->solution.z, node->solution.u);
515 right->warmstart(node->solution.z, node->solution.u);
517 switch (this->data.admm_data->settings.search_mode)
522 if (!left_inf) this->push_node(std::move(left));
523 if (!right_inf) this->push_node(std::move(right));
529 if (left_inf && right_inf)
535 this->solve_and_branch(right);
539 this->solve_and_branch(left);
543 if (left->get_box().width() > right->get_box().width())
545 this->push_node(std::move(left));
546 this->solve_and_branch(right);
550 this->push_node(std::move(right));
551 this->solve_and_branch(left);
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());
570 std::unique_ptr<Node, NodeDeleter> node = make_node(this->bnb_data);
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 = this->node_queue.pop_top();
576 this->J_threads.add(node->solution.J);
578 solve_and_branch(node);
583 void push_node(std::unique_ptr<Node, NodeDeleter>&& node)
585 std::unique_lock<std::mutex> lock(pq_mtx);
586 this->node_queue.push(std::move(node));
587 pq_cv_bnb.notify_one();
594 const std::unique_ptr<Node, NodeDeleter> n = this->make_node(this->bnb_data);
595 n->solution.J = J_max;
597 std::lock_guard<std::mutex> lock(pq_mtx);
598 this->node_queue.prune(n);
603 bool check_bin_equal(
const OptSolution& sol1,
const OptSolution& sol2)
const
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))
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