3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-04-07 09:55:19 +00:00

Gomory cut / branch and bound improvements

Improve fairness of cut generation by switching to find_infeasible_int_var with cascading priorities, allow stronger cuts by inlining terms.
This commit is contained in:
Nikolaj Bjorner 2023-11-07 19:57:30 +01:00
parent 9f0b3cdc25
commit 3d99ed9dd4
5 changed files with 230 additions and 100 deletions

View file

@ -31,7 +31,7 @@ class create_cut {
explanation* m_ex; // the conflict explanation 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 unsigned m_inf_col; // a basis column which has to be an integer but has a non integral value
const row_strip<mpq>& m_row; const row_strip<mpq>& m_row;
const int_solver& lia; int_solver& lia;
mpq m_lcm_den; mpq m_lcm_den;
mpq m_f; mpq m_f;
mpq m_one_minus_f; mpq m_one_minus_f;
@ -133,26 +133,50 @@ class create_cut {
return lia_move::conflict; return lia_move::conflict;
} }
void divd(mpq& r, mpq const& d) {
r /= d;
if (!r.is_int())
r = ceil(r);
}
bool can_divide_by(vector<std::pair<mpq, lpvar>> const& p, mpq const& d) {
mpq lhs(0), rhs(m_k);
mpq max_c(abs(m_k));
for (auto const& [c, v] : p) {
auto c1 = c;
max_c = std::max(max_c, abs(c1));
divd(c1, d);
if (c1 == 0)
return false;
VERIFY(lia.get_value(v).y == 0);
lhs += c1 * lia.get_value(v).x;
}
if (max_c == 1)
return false;
divd(rhs, d);
return lhs < rhs;
}
void adjust_term_and_k_for_some_ints_case_gomory() { void adjust_term_and_k_for_some_ints_case_gomory() {
lp_assert(!m_t.is_empty()); lp_assert(!m_t.is_empty());
// k = 1 + sum of m_t at bounds // k = 1 + sum of m_t at bounds
auto pol = m_t.coeffs_as_vector(); lar_term t = lia.lra.unfold_nested_subterms(m_t);
auto pol = t.coeffs_as_vector();
m_t.clear(); m_t.clear();
if (pol.size() == 1) { if (pol.size() == 1) {
TRACE("gomory_cut_detail", tout << "pol.size() is 1" << std::endl;); TRACE("gomory_cut_detail", tout << "pol.size() is 1" << std::endl;);
unsigned v = pol[0].second; auto const& [a, v] = pol[0];
lp_assert(is_int(v)); lp_assert(is_int(v));
const mpq& a = pol[0].first;
if (a.is_pos()) { // we have av >= k if (a.is_pos()) { // we have av >= k
m_k /= a; divd(m_k, a);
if (!m_k.is_int())
m_k = ceil(m_k);
m_t.add_monomial(mpq(1), v); m_t.add_monomial(mpq(1), v);
} }
else { else {
m_k /= -a; // av >= k
if (!m_k.is_int()) // a/-a*v >= k / - a
m_k = ceil(m_k); // -v >= k / - a
// -v >= ceil(k / -a)
divd(m_k, -a);
m_t.add_monomial(-mpq(1), v); m_t.add_monomial(-mpq(1), v);
} }
} }
@ -162,17 +186,48 @@ class create_cut {
TRACE("gomory_cut_detail", tout << "pol.size() > 1 den: " << m_lcm_den << std::endl;); TRACE("gomory_cut_detail", tout << "pol.size() > 1 den: " << m_lcm_den << std::endl;);
if (!m_lcm_den.is_one()) { if (!m_lcm_den.is_one()) {
// normalize coefficients of integer parameters to be integers. // normalize coefficients of integer parameters to be integers.
for (auto & pi: pol) { for (auto & [c,v]: pol) {
pi.first *= m_lcm_den; c *= m_lcm_den;
SASSERT(!is_int(pi.second) || pi.first.is_int()); SASSERT(!is_int(v) || c.is_int());
} }
m_k *= m_lcm_den; m_k *= m_lcm_den;
} }
for (const auto & pi: pol)
m_t.add_monomial(pi.first, pi.second); // gcd reduction is loss-less:
mpq g(1);
for (const auto & [c, v] : pol)
g = gcd(g, c);
if (g != 1) {
for (auto & [c, v] : pol)
c /= g;
divd(m_k, g);
}
#if 0
// TODO: create self-contained rounding mode to weaken cuts
// whose cofficients are considered too large
// (larger than bounds from the input)
mpq min_c = abs(m_k);
for (const auto & [c, v] : pol)
min_c = std::min(min_c, abs(c));
if (min_c > 1 && can_divide_by(pol, min_c)) {
for (auto& [c, v] : pol)
divd(c, min_c);
divd(m_k, min_c);
}
#endif
for (const auto & [c, v]: pol)
m_t.add_monomial(c, v);
VERIFY(m_t.size() > 0);
} }
TRACE("gomory_cut_detail", tout << "k = " << m_k << std::endl;); TRACE("gomory_cut_detail", tout << "k = " << m_k << std::endl;);
lp_assert(m_k.is_int()); lp_assert(m_k.is_int());
} }
std::string var_name(unsigned j) const { std::string var_name(unsigned j) const {
@ -191,10 +246,9 @@ class create_cut {
template <typename T> template <typename T>
void dump_coeff(std::ostream & out, const T& c) const { void dump_coeff(std::ostream & out, const T& c) const {
out << "( * "; out << "(* ";
dump_coeff_val(out, c.coeff()); dump_coeff_val(out, c.coeff());
auto t = lia.lra.column2tv(c.column()); out << " " << var_name(c.column().index()) << ")";
out << " " << var_name(t.id()) << ")";
} }
std::ostream& dump_row_coefficients(std::ostream & out) const { std::ostream& dump_row_coefficients(std::ostream & out) const {
@ -208,7 +262,7 @@ class create_cut {
void dump_the_row(std::ostream& out) const { void dump_the_row(std::ostream& out) const {
out << "; the row, excluding fixed vars\n"; out << "; the row, excluding fixed vars\n";
out << "(assert ( = ( +"; out << "(assert (= (+";
dump_row_coefficients(out) << ") 0))\n"; dump_row_coefficients(out) << ") 0))\n";
} }
@ -259,12 +313,12 @@ class create_cut {
return dump_term_coefficients(out << "(+ ") << ")"; return dump_term_coefficients(out << "(+ ") << ")";
} }
std::ostream& dump_term_le_k(std::ostream & out) const { std::ostream& dump_term_ge_k(std::ostream & out) const {
return dump_term_sum(out << "(<= ") << " " << m_k << ")"; return dump_term_sum(out << "(>= ") << " " << m_k << ")";
} }
void dump_the_cut_assert(std::ostream & out) const { void dump_the_cut_assert(std::ostream & out) const {
dump_term_le_k(out << "(assert (not ") << "))\n"; dump_term_ge_k(out << "(assert (not ") << "))\n";
} }
void dump_cut_and_constraints_as_smt_lemma(std::ostream& out) const { void dump_cut_and_constraints_as_smt_lemma(std::ostream& out) const {
@ -310,7 +364,6 @@ public:
unsigned j = p.var(); unsigned j = p.var();
if (j == m_inf_col) { if (j == m_inf_col) {
lp_assert(p.coeff() == one_of_type<mpq>()); lp_assert(p.coeff() == one_of_type<mpq>());
TRACE("gomory_cut_detail", tout << "seeing basic var\n";);
continue; continue;
} }
@ -341,13 +394,21 @@ public:
return report_conflict_from_gomory_cut(); return report_conflict_from_gomory_cut();
if (some_int_columns) if (some_int_columns)
adjust_term_and_k_for_some_ints_case_gomory(); adjust_term_and_k_for_some_ints_case_gomory();
TRACE("gomory_cut_detail", dump_cut_and_constraints_as_smt_lemma(tout);); if (!lia.current_solution_is_inf_on_cut()) {
m_ex->clear();
m_t.clear();
m_k = 1;
return lia_move::undef;
}
lp_assert(lia.current_solution_is_inf_on_cut()); // checks that indices are columns lp_assert(lia.current_solution_is_inf_on_cut()); // checks that indices are columns
TRACE("gomory_cut", print_linear_combination_of_column_indices_only(m_t.coeffs_as_vector(), tout << "gomory cut:"); tout << " <= " << m_k << std::endl;); TRACE("gomory_cut", print_linear_combination_of_column_indices_only(m_t.coeffs_as_vector(), tout << "gomory cut: "); tout << " >= " << m_k << std::endl;);
TRACE("gomory_cut_detail", dump_cut_and_constraints_as_smt_lemma(tout);
lia.lra.display(tout));
lia.settings().stats().m_gomory_cuts++;
return lia_move::cut; return lia_move::cut;
} }
create_cut(lar_term & t, mpq & k, explanation* ex, unsigned basic_inf_int_j, const row_strip<mpq>& row, const int_solver& lia) : create_cut(lar_term & t, mpq & k, explanation* ex, unsigned basic_inf_int_j, const row_strip<mpq>& row, int_solver& lia) :
m_t(t), m_t(t),
m_k(k), m_k(k),
m_ex(ex), m_ex(ex),
@ -385,22 +446,26 @@ int gomory::find_basic_var() {
int result = -1; int result = -1;
unsigned min_row_size = UINT_MAX; unsigned min_row_size = UINT_MAX;
#if 0 #if 1
result = lia.select_int_infeasible_var(); result = lia.select_int_infeasible_var(true);
if (result == -1) if (result == -1)
return result; return result;
TRACE("gomory_cut", tout << "row: " << result << "\n");
const row_strip<mpq>& row = lra.get_row(lia.row_of_basic_column(result)); const row_strip<mpq>& row = lra.get_row(lia.row_of_basic_column(result));
if (is_gomory_cut_target(row)) if (is_gomory_cut_target(row))
return result; return result;
result = -1; result = -1;
UNREACHABLE();
#endif #endif
for (unsigned j : lra.r_basis()) { for (unsigned j : lra.r_basis()) {
if (!lia.column_is_int_inf(j)) if (!lia.column_is_int_inf(j))
continue; continue;
const row_strip<mpq>& row = lra.get_row(lia.row_of_basic_column(j)); const row_strip<mpq>& row = lra.get_row(lia.row_of_basic_column(j));
TRACE("gomory_cut", tout << "try j" << j << "\n");
if (!is_gomory_cut_target(row)) if (!is_gomory_cut_target(row))
continue; continue;
IF_VERBOSE(20, lia.display_row_info(verbose_stream(), lia.row_of_basic_column(j))); IF_VERBOSE(20, lia.display_row_info(verbose_stream(), lia.row_of_basic_column(j)));
@ -417,7 +482,6 @@ int gomory::find_basic_var() {
} }
lia_move gomory::operator()() { lia_move gomory::operator()() {
lra.move_non_basic_columns_to_bounds();
int j = find_basic_var(); int j = find_basic_var();
if (j == -1) if (j == -1)
return lia_move::undef; return lia_move::undef;
@ -426,6 +490,7 @@ lia_move gomory::operator()() {
SASSERT(lra.row_is_correct(r)); SASSERT(lra.row_is_correct(r));
SASSERT(is_gomory_cut_target(row)); SASSERT(is_gomory_cut_target(row));
lia.m_upper = false; lia.m_upper = false;
lia.m_cut_vars.push_back(j);
return cut(lia.m_t, lia.m_k, lia.m_ex, j, row); return cut(lia.m_t, lia.m_k, lia.m_ex, j, row);
} }

View file

@ -52,8 +52,8 @@ lia_move int_branch::create_branch_on_column(int j) {
int int_branch::find_inf_int_base_column() { int int_branch::find_inf_int_base_column() {
#if 0 #if 1
return lia.select_int_infeasible_var(); return lia.select_int_infeasible_var(false);
#endif #endif
int result = -1; int result = -1;

View file

@ -180,6 +180,8 @@ namespace lp {
m_ex = e; m_ex = e;
m_ex->clear(); m_ex->clear();
m_upper = false; m_upper = false;
m_cut_vars.reset();
lia_move r = lia_move::undef; lia_move r = lia_move::undef;
if (m_gcd.should_apply()) if (m_gcd.should_apply())
@ -193,12 +195,15 @@ namespace lp {
++m_number_of_calls; ++m_number_of_calls;
if (r == lia_move::undef && m_patcher.should_apply()) r = m_patcher(); if (r == lia_move::undef && m_patcher.should_apply()) r = m_patcher();
if (r == lia_move::undef && should_find_cube()) r = int_cube(*this)(); if (r == lia_move::undef && should_find_cube()) r = int_cube(*this)();
if (r == lia_move::undef) lra.move_non_basic_columns_to_bounds();
if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut(); if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut();
#if 1 m_cut_vars.reset();
#if 0
if (r == lia_move::undef && should_gomory_cut()) r = gomory(*this)(); if (r == lia_move::undef && should_gomory_cut()) r = gomory(*this)();
#else #else
if (r == lia_move::undef && should_gomory_cut()) r = local_gomory(); if (r == lia_move::undef && should_gomory_cut()) r = local_gomory(2);
#endif #endif
m_cut_vars.reset();
if (r == lia_move::undef) r = int_branch(*this)(); if (r == lia_move::undef) r = int_branch(*this)();
return r; return r;
} }
@ -626,71 +631,85 @@ namespace lp {
} }
int int_solver::select_int_infeasible_var() { int int_solver::select_int_infeasible_var(bool check_bounded) {
int result = -1; int r_small_box = -1;
int r_small_value = -1;
int r_any_value = -1;
unsigned n_small_box = 1;
unsigned n_small_value = 1;
unsigned n_any_value = 1;
mpq range; mpq range;
mpq new_range; mpq new_range;
mpq small_value(1024); mpq small_value(1024);
unsigned n = 0;
lar_core_solver & lcs = lra.m_mpq_lar_core_solver; lar_core_solver & lcs = lra.m_mpq_lar_core_solver;
unsigned prev_usage = 0; // to quiet down the compile unsigned prev_usage = 0;
enum state { small_box, is_small_value, any_value, not_found }; auto check_bounded_fn = [&](unsigned j) {
state st = not_found; if (!check_bounded)
return true;
auto const& row = lra.get_row(row_of_basic_column(j));
for (const auto & p : row) {
unsigned j = p.var();
if (!is_base(j) && (!at_bound(j) || !is_zero(get_value(j).y)))
return false;
}
return true;
};
auto add_column = [&](bool improved, int& result, unsigned& n, unsigned j) {
if (result == -1)
result = j;
else if (improved && ((random() % (++n)) == 0))
result = j;
};
for (unsigned j : lra.r_basis()) { for (unsigned j : lra.r_basis()) {
if (!column_is_int_inf(j)) if (!column_is_int_inf(j))
continue; continue;
if (!check_bounded_fn(j))
continue;
if (m_cut_vars.contains(j))
continue;
SASSERT(!is_fixed(j));
unsigned usage = lra.usage_in_terms(j); unsigned usage = lra.usage_in_terms(j);
if (is_boxed(j) && (new_range = lcs.m_r_upper_bounds()[j].x - lcs.m_r_lower_bounds()[j].x - rational(2*usage)) <= small_value) { if (is_boxed(j) && (new_range = lcs.m_r_upper_bounds()[j].x - lcs.m_r_lower_bounds()[j].x - rational(2*usage)) <= small_value) {
SASSERT(!is_fixed(j));
if (st != small_box) { bool improved = new_range <= range || r_small_box == -1;
n = 0; if (improved)
st = small_box;
}
if (n == 0 || new_range < range) {
result = j;
range = new_range; range = new_range;
n = 1; add_column(improved, r_small_box, n_small_box, j);
}
else if (new_range == range && (random() % (++n) == 0)) {
result = j;
}
continue; continue;
} }
if (st == small_box)
continue;
impq const& value = get_value(j); impq const& value = get_value(j);
if (abs(value.x) < small_value || if (abs(value.x) < small_value ||
(has_upper(j) && small_value > upper_bound(j).x - value.x) || (has_upper(j) && small_value > upper_bound(j).x - value.x) ||
(has_lower(j) && small_value > value.x - lower_bound(j).x)) { (has_lower(j) && small_value > value.x - lower_bound(j).x)) {
if (st != is_small_value) { TRACE("gomory_cut", tout << "small j" << j << "\n");
n = 0; add_column(true, r_small_value, n_small_value, j);
st = is_small_value;
}
if (random() % (++n) == 0)
result = j;
}
if (st == is_small_value)
continue; continue;
SASSERT(st == not_found || st == any_value); }
st = any_value; TRACE("gomory_cut", tout << "any j" << j << "\n");
if (n == 0 || usage > prev_usage) { add_column(usage >= prev_usage, r_any_value, n_any_value, j);
result = j; if (usage > prev_usage)
prev_usage = usage; prev_usage = usage;
n = 1;
}
else if (usage > 0 && usage == prev_usage && (random() % (++n) == 0))
result = j;
} }
return result; if (r_small_box != -1 && (random() % 3 != 0))
return r_small_box;
if (r_small_value != -1 && (random() % 3) != 0)
return r_small_value;
if (r_any_value != -1)
return r_any_value;
if (r_small_box != -1)
return r_small_box;
return r_small_value;
} }
void int_solver::simplify(std::function<bool(unsigned)>& is_root) { void int_solver::simplify(std::function<bool(unsigned)>& is_root) {
return; return;
#if 1
// in-processing simplification can go here, such as bounds improvements. // in-processing simplification can go here, such as bounds improvements.
@ -701,17 +720,13 @@ namespace lp {
} }
#endif
#if 1
lp::explanation exp; lp::explanation exp;
m_ex = &exp; m_ex = &exp;
m_t.clear(); m_t.clear();
m_k.reset(); m_k.reset();
if (has_inf_int()) if (has_inf_int())
local_gomory(); local_gomory(5);
#endif
#if 0 #if 0
stopwatch sw; stopwatch sw;
@ -933,35 +948,85 @@ namespace lp {
#endif #endif
} }
lia_move int_solver::local_gomory() { lia_move int_solver::local_gomory(unsigned num_cuts) {
for (unsigned i = 0; i < 2 && has_inf_int() && !settings().get_cancel_flag(); ++i) {
struct ex { explanation m_ex; lar_term m_term; mpq m_k; bool m_is_upper; };
vector<ex> cuts;
for (unsigned i = 0; i < num_cuts && has_inf_int() && !settings().get_cancel_flag(); ++i) {
m_ex->clear(); m_ex->clear();
m_t.clear(); m_t.clear();
m_k.reset(); m_k.reset();
auto r = gomory(*this)(); auto r = gomory(*this)();
IF_VERBOSE(3, verbose_stream() << i << " " << r << "\n"); if (r != lia_move::cut)
if (r != lia_move::cut) break;
return r; cuts.push_back({ *m_ex, m_t, m_k, is_upper() });
u_dependency* dep = nullptr;
for (auto c : *m_ex)
dep = lra.join_deps(lra.dep_manager().mk_leaf(c.ci()), dep);
lp::lpvar term_index = lra.add_term(get_term().coeffs_as_vector(), UINT_MAX);
term_index = lra.map_term_index_to_column_index(term_index);
lra.update_column_type_and_bound(term_index, is_upper() ? lp::lconstraint_kind::LE : lp::lconstraint_kind::GE, get_offset(), dep);
lra.find_feasible_solution();
if (!lra.is_feasible()) {
lra.get_infeasibility_explanation(*m_ex);
return lia_move::conflict;
}
//r = m_patcher();
//if (r != lia_move::undef)
// return r;
} }
m_cut_vars.reset();
auto is_small_cut = [&](ex const& cut) {
return all_of(cut.m_term, [&](auto ci) { return ci.coeff().is_small(); });
};
auto add_cut = [&](ex const& cut) {
u_dependency* dep = nullptr;
for (auto c : cut.m_ex)
dep = lra.join_deps(lra.dep_manager().mk_leaf(c.ci()), dep);
lp::lpvar term_index = lra.add_term(cut.m_term.coeffs_as_vector(), UINT_MAX);
term_index = lra.map_term_index_to_column_index(term_index);
lra.update_column_type_and_bound(term_index,
cut.m_is_upper ? lp::lconstraint_kind::LE : lp::lconstraint_kind::GE,
cut.m_k, dep);
};
auto _check_feasible = [&](void) {
auto st = lra.find_feasible_solution();
if (!lra.is_feasible()) {
lra.get_infeasibility_explanation(*m_ex);
return false;
}
return true;
};
bool has_small = false, has_large = false;
for (auto const& cut : cuts) {
if (!is_small_cut(cut)) {
has_large = true;
continue;
}
has_small = true;
add_cut(cut);
}
if (has_large) {
lra.push();
for (auto const& cut : cuts)
if (!is_small_cut(cut))
add_cut(cut);
bool feas = _check_feasible();
lra.pop(1);
if (!feas)
return lia_move::conflict;
}
if (!_check_feasible())
return lia_move::conflict;
m_ex->clear(); m_ex->clear();
m_t.clear(); m_t.clear();
m_k.reset(); m_k.reset();
if (!has_inf_int()) if (!has_inf_int())
return lia_move::sat; return lia_move::sat;
if (has_small || has_large)
return lia_move::continue_with_check;
lra.move_non_basic_columns_to_bounds();
return lia_move::undef; return lia_move::undef;
} }

View file

@ -63,10 +63,11 @@ class int_solver {
unsigned m_number_of_calls; unsigned m_number_of_calls;
lar_term m_t; // the term to return in the cut lar_term m_t; // the term to return in the cut
mpq m_k; // the right side of the cut mpq m_k; // the right side of the cut
bool m_upper; // cut is an upper bound
explanation *m_ex; // the conflict explanation explanation *m_ex; // the conflict explanation
bool m_upper; // we have a cut m_t*x <= k if m_upper is true nad m_t*x >= k otherwise
hnf_cutter m_hnf_cutter; hnf_cutter m_hnf_cutter;
unsigned m_hnf_cut_period; unsigned m_hnf_cut_period;
unsigned_vector m_cut_vars; // variables that should not be selected for cuts
vector<equality> m_equalities; vector<equality> m_equalities;
public: public:
@ -110,7 +111,7 @@ private:
bool has_upper(unsigned j) const; bool has_upper(unsigned j) const;
unsigned row_of_basic_column(unsigned j) const; unsigned row_of_basic_column(unsigned j) const;
bool cut_indices_are_columns() const; bool cut_indices_are_columns() const;
lia_move local_gomory(); lia_move local_gomory(unsigned num_cuts);
public: public:
std::ostream& display_column(std::ostream & out, unsigned j) const; std::ostream& display_column(std::ostream & out, unsigned j) const;
@ -131,7 +132,7 @@ public:
bool all_columns_are_bounded() const; bool all_columns_are_bounded() const;
lia_move hnf_cut(); lia_move hnf_cut();
int select_int_infeasible_var(); int select_int_infeasible_var(bool check_bounded);
}; };
} }

View file

@ -1678,7 +1678,6 @@ public:
return FC_CONTINUE; return FC_CONTINUE;
} }
for (expr* e : m_not_handled) { for (expr* e : m_not_handled) {
if (!ctx().is_relevant(e)) if (!ctx().is_relevant(e))
continue; continue;