/*++ Copyright (c) 2017 Microsoft Corporation Module Name: Abstract: Author: Nikolaj Bjorner (nbjorner) Lev Nachmanson (levnach) Revision History: --*/ #include "math/lp/gomory.h" #include "math/lp/int_solver.h" #include "math/lp/lar_solver.h" #include "math/lp/lp_utils.h" namespace lp { enum class row_polarity { UNDEF, MIN, MAX, MIXED}; struct create_cut { lar_term & m_t; // the term to return in the cut mpq & m_k; // the right side of the cut explanation* m_ex; // the conflict explanation unsigned m_inf_col; // a basis column which has to be an integer but has a non integral value const row_strip& m_row; int_solver& lia; mpq m_f; mpq m_one_minus_f; mpq m_fj; mpq m_one_minus_fj; mpq m_abs_max, m_big_number; row_polarity m_polarity; bool m_found_big; u_dependency* m_dep; const impq & get_value(unsigned j) const { return lia.get_value(j); } bool is_int(unsigned j) const { return lia.column_is_int(j) || (lia.is_fixed(j) && lia.lra.column_lower_bound(j).is_int()); } bool is_real(unsigned j) const { return !is_int(j); } bool at_lower(unsigned j) const { return lia.at_lower(j); } bool at_upper(unsigned j) const { return lia.at_upper(j); } const impq & lower_bound(unsigned j) const { return lia.lower_bound(j); } const impq & upper_bound(unsigned j) const { return lia.upper_bound(j); } u_dependency* column_lower_bound_constraint(unsigned j) const { return lia.column_lower_bound_constraint(j); } u_dependency* column_upper_bound_constraint(unsigned j) const { return lia.column_upper_bound_constraint(j); } bool column_is_fixed(unsigned j) const { return lia.lra.column_is_fixed(j); } void push_explanation(u_dependency* d) { for (auto ci : lia.lra.flatten(d)) m_ex->push_back(ci); } void int_case_in_gomory_cut(unsigned j) { lp_assert(is_int(j) && m_fj.is_pos()); TRACE("gomory_cut_detail", tout << " k = " << m_k; tout << ", fj: " << m_fj << ", "; tout << (at_lower(j)?"at_lower":"at_upper")<< std::endl; ); mpq new_a; if (at_lower(j)) { // here we have the product of new_a*(xj - lb(j)), so new_a*lb(j) is added to m_k new_a = m_fj <= m_one_minus_f ? m_fj / m_one_minus_f : ((1 - m_fj) / m_f); lp_assert(new_a.is_pos()); m_k.addmul(new_a, lower_bound(j).x); push_explanation(column_lower_bound_constraint(j)); } else { lp_assert(at_upper(j)); // here we have the expression new_a*(xj - ub), so new_a*ub(j) is added to m_k new_a = - (m_fj <= m_f ? m_fj / m_f : ((1 - m_fj) / m_one_minus_f)); lp_assert(new_a.is_neg()); m_k.addmul(new_a, upper_bound(j).x); push_explanation(column_upper_bound_constraint(j)); } m_t.add_monomial(new_a, j); TRACE("gomory_cut_detail", tout << "new_a = " << new_a << ", k = " << m_k << "\n";); if (numerator(new_a) > m_big_number) m_found_big = true; } void set_polarity(row_polarity p) { if (m_polarity == row_polarity::MIXED) return; if (m_polarity == row_polarity::UNDEF) m_polarity = p; else if (m_polarity != p) m_polarity = row_polarity::MIXED; } void real_case_in_gomory_cut(const mpq & a, unsigned j) { TRACE("gomory_cut_detail_real", tout << "j = " << j << ", a = " << a << ", m_k = " << m_k << "\n";); mpq new_a; if (at_lower(j)) { if (a.is_pos()) { // the delta is a (x - f) is positive it has to grow and fight m_one_minus_f new_a = a / m_one_minus_f; set_polarity(row_polarity::MIN); // reverse the polarity since a = -p.coeff() } else { // the delta is negative and it works again m_f new_a = - a / m_f; set_polarity(row_polarity::MAX); } m_k.addmul(new_a, lower_bound(j).x); // is it a faster operation than // k += lower_bound(j).x * new_a; push_explanation(column_lower_bound_constraint(j)); } else { lp_assert(at_upper(j)); if (a.is_pos()) { // the delta is works again m_f new_a = - a / m_f; set_polarity(row_polarity::MAX); } else { // the delta is positive works again m_one_minus_f new_a = a / m_one_minus_f; set_polarity(row_polarity::MIN); } m_k.addmul(new_a, upper_bound(j).x); // k += upper_bound(j).x * new_a; push_explanation(column_upper_bound_constraint(j)); } m_t.add_monomial(new_a, j); TRACE("gomory_cut_detail_real", tout << "add " << new_a << "*v" << j << ", k: " << m_k << "\n"; tout << "m_t = "; lia.lra.print_term(m_t, tout) << "\nk: " << m_k << "\n";); if (numerator(new_a) > m_big_number) m_found_big = true; } lia_move report_conflict_from_gomory_cut() { lp_assert(m_k.is_pos()); // conflict 0 >= k where k is positive return lia_move::conflict; } std::string var_name(unsigned j) const { return std::string("x") + std::to_string(j); } std::ostream& dump_coeff_val(std::ostream & out, const mpq & a) const { if (a.is_int()) out << a; else if (a >= zero_of_type()) out << "(/ " << numerator(a) << " " << denominator(a) << ")"; else out << "(- (/ " << numerator(-a) << " " << denominator(-a) << "))"; return out; } template void dump_coeff(std::ostream & out, const T& c) const { dump_coeff_val(out << "(* ", c.coeff()) << " " << var_name(c.j()) << ")"; } std::ostream& dump_row_coefficients(std::ostream & out) const { mpq lc(1); for (const auto& p : m_row) lc = lcm(lc, denominator(p.coeff())); for (const auto& p : m_row) dump_coeff_val(out << " (* ", p.coeff()*lc) << " " << var_name(p.var()) << ")"; return out; } void dump_the_row(std::ostream& out) const { out << "; the row, excluding fixed vars\n"; out << "(assert (= (+"; dump_row_coefficients(out) << ") 0))\n"; } void dump_declaration(std::ostream& out, unsigned v) const { out << "(declare-const " << var_name(v) << (is_int(v) ? " Int" : " Real") << ")\n"; } void dump_declarations(std::ostream& out) const { // for a column j the var name is vj for (const auto & p : m_row) dump_declaration(out, p.var()); for (lar_term::ival p : m_t) { if (lia.lra.column_has_term(p.j())) dump_declaration(out, p.j()); } } void dump_lower_bound_expl(std::ostream & out, unsigned j) const { out << "(assert (>= " << var_name(j) << " " << lower_bound(j).x << "))\n"; } void dump_upper_bound_expl(std::ostream & out, unsigned j) const { out << "(assert (<= " << var_name(j) << " " << upper_bound(j).x << "))\n"; } void dump_explanations(std::ostream& out) const { for (const auto & p : m_row) { unsigned j = p.var(); if (j == m_inf_col || (!is_real(j) && p.coeff().is_int())) continue; else if (at_lower(j)) dump_lower_bound_expl(out, j); else { lp_assert(at_upper(j)); dump_upper_bound_expl(out, j); } } } std::ostream& dump_term_coefficients(std::ostream & out) const { for (lar_term::ival p : m_t) dump_coeff(out, p); return out; } std::ostream& dump_term_sum(std::ostream & out) const { return dump_term_coefficients(out << "(+ ") << ")"; } std::ostream& dump_term_ge_k(std::ostream & out) const { return dump_term_sum(out << "(>= ") << " " << m_k << ")"; } void dump_the_cut_assert(std::ostream & out) const { dump_term_ge_k(out << "(assert (not ") << "))\n"; } void dump_cut_and_constraints_as_smt_lemma(std::ostream& out) const { dump_declarations(out); dump_the_row(out); dump_explanations(out); dump_the_cut_assert(out); out << "(check-sat)\n"; } public: void dump(std::ostream& out) { out << "applying cut at:\n"; print_linear_combination_indices_only, mpq>(m_row, out); out << std::endl; for (auto & p : m_row) lia.lra.print_column_info(p.var(), out); out << "inf_col = " << m_inf_col << std::endl; } lia_move cut() { TRACE("gomory_cut", dump(tout);); // If m_polarity is MAX, then // the row constraints the base variable to be at the maximum, // MIN - at the minimum, // MIXED : the row does not constraint the base variable to be at an extremum // UNDEF is the initial state m_polarity = row_polarity::UNDEF; // gomory cut will be m_t >= m_k and the current solution has a property m_t < m_k m_k = 1; m_t.clear(); m_ex->clear(); m_found_big = false; TRACE("gomory_cut_detail", tout << "m_f: " << m_f << ", "; tout << "1 - m_f: " << 1 - m_f << ", get_value(m_inf_col).x - m_f = " << get_value(m_inf_col).x - m_f << "\n";); lp_assert(m_f.is_pos() && (get_value(m_inf_col).x - m_f).is_int()); auto set_polarity_for_int = [&](const mpq & a, lpvar j) { if (a.is_pos()) { if (at_lower(j)) set_polarity(row_polarity::MAX); else if (at_upper(j)) set_polarity(row_polarity::MIN); else set_polarity(row_polarity::MIXED); } else { if (at_lower(j)) set_polarity(row_polarity::MIN); else if (at_upper(j)) set_polarity(row_polarity::MAX); else set_polarity(row_polarity::MIXED); } }; m_abs_max = 0; for (const auto & p : m_row) { mpq t = abs(ceil(p.coeff())); if (t > m_abs_max) m_abs_max = t; } m_big_number = m_abs_max.expt(2); for (const auto & p : m_row) { unsigned j = p.var(); if (j == m_inf_col) continue; // use -p.coeff() to make the format compatible with the format used in: Integrating Simplex with DPLL(T) if (lia.is_fixed(j)) { push_explanation(column_lower_bound_constraint(j)); push_explanation(column_upper_bound_constraint(j)); continue; } if (is_real(j)) real_case_in_gomory_cut(- p.coeff(), j); else { if (!p.coeff().is_int()) { m_fj = fractional_part(-p.coeff()); m_one_minus_fj = 1 - m_fj; int_case_in_gomory_cut(j); } if (m_polarity != row_polarity::MIXED) set_polarity_for_int(p.coeff(), j); } if (m_found_big) { return lia_move::undef; } } if (m_t.is_empty()) { return report_conflict_from_gomory_cut(); } TRACE("gomory_cut", print_linear_combination_of_column_indices_only(m_t.coeffs_as_vector(), tout << "gomory cut: "); tout << " >= " << m_k << std::endl;); m_dep = nullptr; for (auto c : *m_ex) m_dep = lia.lra.join_deps(lia.lra.dep_manager().mk_leaf(c.ci()), m_dep); TRACE("gomory_cut_detail", dump_cut_and_constraints_as_smt_lemma(tout); lia.lra.display(tout)); SASSERT(lia.current_solution_is_inf_on_cut()); lia.settings().stats().m_gomory_cuts++; return lia_move::cut; } create_cut(lar_term & t, mpq & k, explanation* ex, unsigned basic_inf_int_j, const row_strip& row, int_solver& lia) : m_t(t), m_k(k), m_ex(ex), m_inf_col(basic_inf_int_j), m_row(row), lia(lia), m_f(fractional_part(get_value(basic_inf_int_j).x)), m_one_minus_f(1 - m_f) {} }; bool gomory::is_gomory_cut_target(lpvar k) { SASSERT(lia.is_base(k)); const row_strip& row = lra.get_row(lia.row_of_basic_column(k)); // Consider monomial c*x from the row, where x is non-basic. // Then, for each such monomial, one of following conditions // has to hold for the row to be eligible for Gomory cut: // 1) c is integral and x integral varible with an integral value // 2) the value of x is at a bound and has no infinitesimals. unsigned j; for (const auto & p : row) { j = p.var(); if (k == j) continue; if (p.coeff().is_int() && lia.column_is_int(j) && lia.get_value(j).is_int()) continue; if ( !lia.at_bound(j) || lia.get_value(j).y != 0) { TRACE("gomory_cut", tout << "row is not gomory cut target:\n"; lia.display_column(tout, j); tout << "infinitesimal: " << !(lia.get_value(j).y ==0) << "\n";); return false; } } return true; // Condition 1) above can be relaxed even more, allowing any value for x, but it will change the calculation for m_f. } // return the minimal distance from the variable value to an integer mpq get_gomory_score(const int_solver& lia, lpvar j) { const mpq& val = lia.get_value(j).x; auto l = val - floor(val); if (l <= mpq(1, 2)) return l; return mpq(1) - l; } unsigned_vector gomory::gomory_select_int_infeasible_vars(unsigned num_cuts) { std::list sorted_vars; std::unordered_map score; for (lpvar j : lra.r_basis()) { if (!lia.column_is_int_inf(j) || !is_gomory_cut_target(j)) continue; SASSERT(!lia.is_fixed(j)); sorted_vars.push_back(j); score[j] = get_gomory_score(lia, j); } // prefer the variables with the values close to integers sorted_vars.sort([&](lpvar j, lpvar k) { auto diff = score[j] - score[k]; if (diff.is_neg()) return true; if (diff.is_pos()) return false; return lra.usage_in_terms(j) > lra.usage_in_terms(k); }); unsigned_vector ret; unsigned n = static_cast(sorted_vars.size()); while (num_cuts-- && n > 0) { unsigned k = lia.settings().random_next() % n; double k_ratio = k / (double) n; k_ratio *= k_ratio*k_ratio; // square k_ratio to make it smaller k = static_cast(std::floor(k_ratio * n)); // these operations move k to the beginning of the indices range SASSERT(0 <= k && k < n); auto it = sorted_vars.begin(); while(k--) it++; ret.push_back(*it); sorted_vars.erase(it); n--; } return ret; } row_polarity test_row_polarity(const int_solver& lia, const row_strip& row, lpvar basic_j) { row_polarity ret = row_polarity::UNDEF; for (const auto& p : row) { lpvar j = p.var(); if (j == basic_j) continue; if (lia.is_fixed(j)) continue; row_polarity rp; if (p.coeff().is_pos()) { if (lia.at_lower(j)) rp = row_polarity::MAX; else if (lia.at_upper(j)) rp = row_polarity::MIN; else rp = row_polarity::MIXED; } else { if (lia.at_lower(j)) rp = row_polarity::MIN; else if (lia.at_upper(j)) rp = row_polarity::MAX; else rp = row_polarity::MIXED; } if (ret == row_polarity::UNDEF) ret = rp; if (ret != rp) return row_polarity::MIXED; } return ret; } u_dependency* gomory::add_deps(u_dependency* dep, const row_strip& row, lpvar basic_var) { u_dependency* ret = dep; for (const auto& p : row) { lpvar j = p.var(); if (j == basic_var) continue; if (lia.is_fixed(j)) continue; if (lia.is_real(j)) continue; if (!p.coeff().is_int()) continue; // the explanation for all above have been already added if (lia.at_lower(j)) ret = lia.lra.join_deps(lia.column_lower_bound_constraint(j), ret); else { SASSERT(lia.at_upper(j)); ret = lia.lra.join_deps(lia.column_upper_bound_constraint(j), ret); } } return ret; } lia_move gomory::get_gomory_cuts(unsigned num_cuts) { struct cut_result {lar_term t; mpq k; u_dependency *dep;}; vector big_cuts; unsigned_vector columns_for_cuts = gomory_select_int_infeasible_vars(num_cuts); bool has_small_cut = false; // define inline helper functions auto is_small_cut = [&](lar_term const& t) { return all_of(t, [&](auto ci) { return ci.coeff().is_small(); }); }; auto add_cut = [&](const lar_term& t, const mpq& k, u_dependency * dep) { lp::lpvar j = lra.add_term(t.coeffs_as_vector(), UINT_MAX); lra.update_column_type_and_bound(j, lp::lconstraint_kind::GE, k, dep); }; auto _check_feasible = [&](void) { lra.find_feasible_solution(); if (!lra.is_feasible() && !lia.settings().get_cancel_flag()) { lra.get_infeasibility_explanation(*(lia.expl())); return false; } return true; }; // start creating cuts for (unsigned j : columns_for_cuts) { SASSERT(is_gomory_cut_target(j)); unsigned row_index = lia.row_of_basic_column(j); const row_strip& row = lra.get_row(row_index); create_cut cc(lia.get_term(), lia.offset(), lia.expl(), j, row, lia); auto r = cc.cut(); if (r != lia_move::cut) { if (r == lia_move::conflict) return lia_move::conflict; continue; } SASSERT(test_row_polarity(lia, row, j) == cc.m_polarity); if (cc.m_polarity == row_polarity::MAX) lra.update_column_type_and_bound(j, lp::lconstraint_kind::LE, floor(lra.get_column_value(j).x), add_deps(cc.m_dep, row, j)); else if (cc.m_polarity == row_polarity::MIN) lra.update_column_type_and_bound(j, lp::lconstraint_kind::GE, ceil(lra.get_column_value(j).x), add_deps(cc.m_dep, row, j)); if (!is_small_cut(lia.get_term())) { big_cuts.push_back({cc.m_t, cc.m_k, cc.m_dep}); continue; } has_small_cut = true; add_cut(cc.m_t, cc.m_k, cc.m_dep); if (lia.settings().get_cancel_flag()) return lia_move::cancelled; } if (big_cuts.size()) { lra.push(); for (auto const& cut : big_cuts) add_cut(cut.t, cut.k, cut.dep); bool feas = _check_feasible(); lra.pop(1); if (!feas) for (auto const& cut : big_cuts) add_cut(cut.t, cut.k, cut.dep); } if (!_check_feasible()) return lia_move::conflict; if (lra.get_status() == lp_status::CANCELLED) return lia_move::cancelled; if (!lra.has_inf_int()) return lia_move::sat; if (has_small_cut || big_cuts.size()) return lia_move::continue_with_check; lra.move_non_basic_columns_to_bounds(); return lia_move::undef; } gomory::gomory(int_solver& lia): lia(lia), lra(lia.lra) { } }