[CP-SAT] rewrite cut massaging procedure (cover, flow, knapsack)

This commit is contained in:
Laurent Perron
2023-01-24 17:03:43 +01:00
parent 3eaf52397f
commit 73e4f75445
4 changed files with 401 additions and 620 deletions

View File

@@ -47,7 +47,7 @@ namespace sat {
std::string CutTerm::DebugString() const {
return absl::StrCat("coeff=", coeff.value(), " lp=", lp_value,
" range=", bound_diff.value(), " expr_size=", expr_size);
" range=", bound_diff.value());
}
bool CutTerm::Complement(IntegerValue* rhs) {
@@ -56,7 +56,7 @@ bool CutTerm::Complement(IntegerValue* rhs) {
if (!AddProductTo(-coeff, bound_diff, rhs)) return false;
// We keep the same expression variable.
for (int i = 0; i < expr_size; ++i) {
for (int i = 0; i < 2; ++i) {
expr_coeffs[i] = -expr_coeffs[i];
}
expr_offset = bound_diff - expr_offset;
@@ -93,8 +93,8 @@ bool CutData::AppendOneTerm(IntegerVariable var, IntegerValue coeff,
if (bound_diff == 0) return true;
CutTerm entry;
entry.expr_size = 1;
entry.expr_vars[0] = var;
entry.expr_coeffs[1] = 0;
entry.bound_diff = bound_diff;
if (complement) {
// X = -(UB - X) + UB
@@ -188,7 +188,7 @@ void CutDataBuilder::RegisterAllBooleansTerms(const CutData& cut) {
for (int i = 0; i < size; ++i) {
const CutTerm& term = cut.terms[i];
if (term.bound_diff != 1) continue;
if (term.expr_size != 1) continue;
if (!term.IsSimple()) continue;
if (term.expr_coeffs[0] > 0) {
direct_index_[term.expr_vars[0]] = i;
} else {
@@ -199,7 +199,7 @@ void CutDataBuilder::RegisterAllBooleansTerms(const CutData& cut) {
void CutDataBuilder::AddOrMergeTerm(const CutTerm& term, IntegerValue t,
CutData* cut) {
DCHECK_EQ(term.expr_size, 1);
DCHECK(term.IsSimple());
const IntegerVariable var = term.expr_vars[0];
const int new_index = cut->terms.size();
const auto [it, inserted] =
@@ -232,7 +232,8 @@ bool CutDataBuilder::ConvertToLinearConstraint(const CutData& cut,
tmp_map_.clear();
IntegerValue new_rhs = cut.rhs;
for (const CutTerm& term : cut.terms) {
for (int i = 0; i < term.expr_size; ++i) {
for (int i = 0; i < 2; ++i) {
if (term.expr_coeffs[i] == 0) continue;
if (!AddProductTo(term.coeff, term.expr_coeffs[i],
&tmp_map_[term.expr_vars[i]])) {
return false;
@@ -389,8 +390,6 @@ IntegerRoundingCutHelper::~IntegerRoundingCutHelper() {
stats.push_back({"rounding_cut/num_initial_ibs_", total_num_initial_ibs_});
stats.push_back(
{"rounding_cut/num_initial_merges_", total_num_initial_merges_});
stats.push_back({"rounding_cut/num_initial_complements_",
total_num_initial_complements_});
stats.push_back({"rounding_cut/num_pos_lifts", total_num_pos_lifts_});
stats.push_back({"rounding_cut/num_neg_lifts", total_num_neg_lifts_});
stats.push_back(
@@ -525,7 +524,7 @@ double IntegerRoundingCutHelper::GetScaledViolation(
bool IntegerRoundingCutHelper::HasComplementedImpliedBound(
const CutTerm& entry, ImpliedBoundsProcessor* ib_processor) {
if (ib_processor == nullptr) return false;
if (entry.expr_size != 1) return false;
if (!entry.IsSimple()) return false;
if (entry.bound_diff == 1) return false;
const ImpliedBoundsProcessor::BestImpliedBoundInfo info =
ib_processor->GetCachedImpliedBoundInfo(
@@ -534,132 +533,48 @@ bool IntegerRoundingCutHelper::HasComplementedImpliedBound(
return info.bool_var != kNoIntegerVariable;
}
// Important: The cut_builder_ must have been reset.
bool IntegerRoundingCutHelper::TryToExpandWithLowerImpliedbound(
IntegerValue factor_t, int i, bool complement, CutData* cut,
ImpliedBoundsProcessor* ib_processor) {
CutTerm& term = cut->terms[i];
// We only want to expand non-Boolean and non-slack term!
if (term.bound_diff <= 1) return false;
if (term.expr_size != 1) return false;
// Try lower bounded direction for implied bound.
// This kind should always be beneficial if it exists:
//
// Because X = bound_diff * B + S
// We can replace coeff * X by the expression before applying f:
// = f(coeff * bound_diff) * B + f(coeff) * [X - bound_diff * B]
// = f(coeff) * X + (f(coeff * bound_diff) - f(coeff) * bound_diff] * B
// So we can "lift" B into the cut with a non-negative coefficient.
//
// Note that this lifting is really the same as if we used that implied
// bound before since in f(coeff * bound_diff) * B + f(coeff) * S, if we
// replace S by its value [X - bound_diff * B] we get the same result.
//
// TODO(user): Ignore if bound_diff == 1 ? But we can still merge B with
// another entry if it exists, so it can still be good in this case.
//
// TODO(user): Only do it if coeff_b > 0 ? But again we could still merge
// B with an existing Boolean for a better cut even if coeff_b == 0.
const IntegerVariable ib_var = term.expr_coeffs[0] > 0
? term.expr_vars[0]
: NegationOf(term.expr_vars[0]);
const ImpliedBoundsProcessor::BestImpliedBoundInfo info =
ib_processor->GetCachedImpliedBoundInfo(ib_var);
const IntegerValue lb = -term.expr_offset;
const IntegerValue bound_diff = info.implied_bound - lb;
if (bound_diff <= 0) return false;
if (info.bool_var == kNoIntegerVariable) return false;
if (ProdOverflow(factor_t, CapProdI(term.coeff, bound_diff))) return false;
// We have X = info.diff * Boolean + slack.
CutTerm bool_term;
bool_term.coeff = term.coeff * bound_diff;
bool_term.expr_size = 1;
bool_term.expr_vars[0] = info.bool_var;
bool_term.bound_diff = IntegerValue(1);
bool_term.lp_value = info.bool_lp_value;
if (info.is_positive) {
bool_term.expr_coeffs[0] = IntegerValue(1);
bool_term.expr_offset = IntegerValue(0);
} else {
bool_term.expr_coeffs[0] = IntegerValue(-1);
bool_term.expr_offset = IntegerValue(1);
}
// Create slack.
// The expression is term.exp - bound_diff * bool_term
// The variable shouldn't be the same.
DCHECK_EQ(term.expr_size, 1);
DCHECK_EQ(bool_term.expr_size, 1);
DCHECK_NE(term.expr_vars[0], bool_term.expr_vars[0]);
CutTerm slack_term;
slack_term.expr_size = 2;
slack_term.expr_vars[0] = term.expr_vars[0];
slack_term.expr_coeffs[0] = term.expr_coeffs[0];
slack_term.expr_vars[1] = bool_term.expr_vars[0];
slack_term.expr_coeffs[1] = -bound_diff * bool_term.expr_coeffs[0];
slack_term.expr_offset =
term.expr_offset - bound_diff * bool_term.expr_offset;
slack_term.lp_value = info.SlackLpValue(lb);
slack_term.coeff = term.coeff;
slack_term.bound_diff = term.bound_diff;
// It should be good to use IB, but sometime we have things like
// 7.3 = 2 * bool@1 + 5.3 and the expanded Boolean is at its upper bound.
// It is always good to complement such variable.
//
// Note that here we do more and just complement anything closer to UB.
//
// TODO(user): Because of merges, we might have entry with a coefficient of
// zero than are not useful. Remove them.
if (complement && bool_term.lp_value > 0.5) {
if (bool_term.Complement(&cut->rhs)) ++total_num_initial_complements_;
}
term = slack_term;
cut_builder_.AddOrMergeTerm(bool_term, factor_t, cut);
return true;
}
// TODO(user): This is slow, 50% of run time on a2c1s1.pb.gz. Optimize!
bool IntegerRoundingCutHelper::ComputeCut(
RoundingOptions options, const CutData& base_ct,
ImpliedBoundsProcessor* ib_processor) {
bool all_booleans = true;
bool some_relevant_positions = false;
for (const CutTerm& term : base_ct.terms) {
if (term.bound_diff != 1) all_booleans = false;
if (term.HasRelevantLpValue()) some_relevant_positions = true;
}
// TODO(user): Maybe this shouldn't be called on such constraint.
if (!some_relevant_positions) {
VLOG(2) << "Issue, nothing to cut.";
return false;
}
// No need to process any implied bounds if only Booleans are involved.
if (all_booleans) ib_processor = nullptr;
// Try IB before heuristic?
// This should be better except it can mess up the norm and the divisors.
best_cut_ = base_ct;
if (options.use_ib_before_heuristic && ib_processor != nullptr) {
cut_builder_.RegisterAllBooleansTerms(best_cut_);
const int old_size = static_cast<int>(best_cut_.terms.size());
bool abort = true;
for (int i = 0; i < old_size; ++i) {
if (best_cut_.terms[i].bound_diff <= 1) continue;
if (!best_cut_.terms[i].HasRelevantLpValue()) continue;
if (TryToExpandWithLowerImpliedbound(IntegerValue(1), i,
/*complement=*/true, &best_cut_,
ib_processor)) {
if (options.prefer_positive_ib && best_cut_.terms[i].coeff < 0) {
// We complement the term before trying the implied bound.
if (best_cut_.terms[i].Complement(&best_cut_.rhs)) {
if (ib_processor->TryToExpandWithLowerImpliedbound(
IntegerValue(1), i,
/*complement=*/true, &best_cut_, &cut_builder_)) {
++total_num_initial_ibs_;
abort = false;
continue;
}
best_cut_.terms[i].Complement(&best_cut_.rhs);
}
}
if (ib_processor->TryToExpandWithLowerImpliedbound(
IntegerValue(1), i,
/*complement=*/true, &best_cut_, &cut_builder_)) {
abort = false;
++total_num_initial_ibs_;
}
}
total_num_initial_merges_ += cut_builder_.NumMergesSinceLastClear();
// TODO(user): We assume that this is called with and without the option
// use_ib_before_heuristic, so that we can abort if no IB has been applied
// since then we will redo the computation. This is not really clean.
if (abort) return false;
}
// Our heuristic will try to generate a few different cuts, and we will keep
@@ -840,10 +755,10 @@ bool IntegerRoundingCutHelper::ComputeCut(
// We only want to expand non-Boolean and non-slack term!
if (term.bound_diff <= 1) continue;
if (term.expr_size != 1) continue;
if (!term.IsSimple()) continue;
if (TryToExpandWithLowerImpliedbound(factor_t, i, /*complement=*/false,
&best_cut_, ib_processor)) {
if (ib_processor->TryToExpandWithLowerImpliedbound(
factor_t, i, /*complement=*/false, &best_cut_, &cut_builder_)) {
++num_ib_used_;
++total_num_pos_lifts_;
continue;
@@ -894,8 +809,8 @@ bool IntegerRoundingCutHelper::ComputeCut(
// We reverse the is_positive meaning here since we have (1 - B).
CutTerm bool_term;
bool_term.coeff = bound_diff * term.coeff;
bool_term.expr_size = 1;
bool_term.expr_vars[0] = info.bool_var;
bool_term.expr_coeffs[1] = 0;
bool_term.bound_diff = IntegerValue(1);
bool_term.lp_value = 1.0 - info.bool_lp_value;
if (!info.is_positive) {
@@ -909,7 +824,6 @@ bool IntegerRoundingCutHelper::ComputeCut(
// Create the slack term in X = diff * (1 - B) - S
CutTerm slack_term;
slack_term.coeff = -term.coeff;
slack_term.expr_size = 2;
slack_term.expr_vars[0] = term.expr_vars[0];
slack_term.expr_coeffs[0] = -term.expr_coeffs[0];
slack_term.expr_vars[1] = bool_term.expr_vars[0];
@@ -1018,45 +932,33 @@ CoverCutHelper::~CoverCutHelper() {
std::vector<std::pair<std::string, int64_t>> stats;
stats.push_back({"cover_cut/num_overflows", total_num_overflow_abort_});
stats.push_back({"cover_cut/num_lifting", total_num_lifting_});
stats.push_back({"cover_cut/num_cover_not_bool", total_num_cover_not_bool_});
stats.push_back({"cover_cut/num_implied_bounds", total_num_ibs_});
shared_stats_->AddStats(stats);
}
bool CoverCutHelper::TrySimpleKnapsack(const CutData& input) {
cut_.Clear();
alt_cut_.Clear();
base_ct_ = input;
if (base_ct_.rhs < 0) return false;
// Now make each coeff positive.
//
// TODO(user): maybe we should do it all at once to avoid some overflow
// condition.
for (CutTerm& term : base_ct_.terms) {
if (term.coeff >= 0) continue;
if (!term.Complement(&base_ct_.rhs)) {
++total_num_overflow_abort_;
return false;
}
}
// Try a simple cover heuristic.
// Look for violated CUT of the form: sum (UB - X) or (X - LB) >= 1.
std::sort(base_ct_.terms.begin(), base_ct_.terms.end(),
// Try a simple cover heuristic.
// Look for violated CUT of the form: sum (UB - X) or (X - LB) >= 1.
int CoverCutHelper::GetCoverSize(int relevant_size, IntegerValue* rhs) {
relevant_size =
std::partition(
base_ct_.terms.begin(), base_ct_.terms.begin() + relevant_size,
[](const CutTerm& t) { return t.LpDistToMaxValue() < 0.9999; }) -
base_ct_.terms.begin();
std::sort(base_ct_.terms.begin(), base_ct_.terms.begin() + relevant_size,
[](const CutTerm& a, const CutTerm& b) {
if (a.LpDistToMaxValue() == b.LpDistToMaxValue()) {
const double dist_a = a.LpDistToMaxValue();
const double dist_b = b.LpDistToMaxValue();
if (dist_a == dist_b) {
// Prefer low coefficients if the distance is the same.
return a.coeff < b.coeff;
}
return a.LpDistToMaxValue() < b.LpDistToMaxValue();
return dist_a < dist_b;
});
double activity = 0.0;
int cover_size = 0;
IntegerValue rhs = base_ct_.rhs;
const int base_size = base_ct_.terms.size();
for (int i = 0; i < base_size; ++i) {
int cover_size = relevant_size;
*rhs = base_ct_.rhs;
for (int i = 0; i < relevant_size; ++i) {
const CutTerm& term = base_ct_.terms[i];
activity += term.LpDistToMaxValue();
@@ -1072,11 +974,11 @@ bool CoverCutHelper::TrySimpleKnapsack(const CutData& input) {
break;
}
if (!AddProductTo(-term.coeff, term.bound_diff, &rhs)) {
if (!AddProductTo(-term.coeff, term.bound_diff, rhs)) {
// Abort early if we run into overflow.
// In that case, rhs must be negative, and we can try this cover still.
cover_size = i;
CHECK_LT(rhs, 0);
CHECK_LT(*rhs, 0);
break;
}
}
@@ -1088,15 +990,13 @@ bool CoverCutHelper::TrySimpleKnapsack(const CutData& input) {
// possible violation. Note also that we lift as much as possible, so we don't
// necessarily optimize for the cut efficacity though. But we do get a
// stronger cut.
if (rhs >= 0) return false;
if (cover_size == 0) return false;
if (*rhs >= 0) return 0;
if (cover_size == 0) return 0;
// Transform to a minimal cover. We want to greedily remove the largest coeff
// first, so we have more chance for the "lifting" below which can increase
// the cut violation. If the coeff are the same, we prefer to remove high
// distance from upper bound first.
//
// We compute the cut at the same time.
std::sort(base_ct_.terms.begin(), base_ct_.terms.begin() + cover_size,
[](const CutTerm& a, const CutTerm& b) {
if (a.coeff == b.coeff) {
@@ -1106,13 +1006,63 @@ bool CoverCutHelper::TrySimpleKnapsack(const CutData& input) {
});
for (int i = 0; i < cover_size; ++i) {
const CutTerm& t = base_ct_.terms[i];
if (t.bound_diff * t.coeff + rhs >= 0) continue;
rhs += t.bound_diff * t.coeff;
if (t.bound_diff * t.coeff + *rhs >= 0) continue;
*rhs += t.bound_diff * t.coeff;
std::swap(base_ct_.terms[i], base_ct_.terms[--cover_size]);
}
CHECK_GT(cover_size, 0);
// Generate alternative cut.
GenerateLetchfordSouliLifting(base_ct_, cover_size);
return cover_size;
}
bool CoverCutHelper::MakeAllTermsPositive() {
// Make sure each coeff is positive.
//
// TODO(user): maybe we should do it all at once to avoid some overflow
// condition.
for (CutTerm& term : base_ct_.terms) {
if (term.coeff >= 0) continue;
if (!term.Complement(&base_ct_.rhs)) {
++total_num_overflow_abort_;
return false;
}
}
// We should have aborted early if the base constraint was already infeasible.
CHECK_GE(base_ct_.rhs, 0);
return true;
}
bool CoverCutHelper::TrySimpleKnapsack(const CutData& input,
ImpliedBoundsProcessor* ib_processor) {
cut_.Clear();
base_ct_ = input;
if (!MakeAllTermsPositive()) return false;
if (ib_processor != nullptr) {
cut_builder_.RegisterAllBooleansTerms(base_ct_);
const int old_size = static_cast<int>(base_ct_.terms.size());
for (int i = 0; i < old_size; ++i) {
// We only look at non-Boolean with an lp value not close to the upper
// bound.
const CutTerm& term = base_ct_.terms[i];
if (term.bound_diff <= 1) continue;
if (term.lp_value + 1e-4 > static_cast<double>(term.bound_diff.value())) {
continue;
}
if (ib_processor->TryToExpandWithLowerImpliedbound(
IntegerValue(1), i,
/*complement=*/false, &base_ct_, &cut_builder_)) {
++total_num_ibs_;
}
}
}
IntegerValue rhs;
const int base_size = static_cast<int>(base_ct_.terms.size());
const int cover_size = GetCoverSize(base_size, &rhs);
if (cover_size == 0) return false;
// The cut is just that the sum of variable cannot be at their max value.
base_ct_.rhs = IntegerValue(-1);
@@ -1193,29 +1143,54 @@ bool CoverCutHelper::TrySimpleKnapsack(const CutData& input) {
return true;
}
bool CoverCutHelper::GenerateLetchfordSouliLifting(const CutData& base_ct,
int cover_size) {
alt_cut_.Clear();
bool CoverCutHelper::TryWithLetchfordSouliLifting(
const CutData& input, ImpliedBoundsProcessor* ib_processor) {
cut_.Clear();
base_ct_ = input;
if (!MakeAllTermsPositive()) return false;
// Perform IB expansion with no restriction, all coeff should still be
// positive.
//
// TODO(user): Merge Boolean terms that are complement of each other.
if (ib_processor != nullptr) {
cut_builder_.RegisterAllBooleansTerms(base_ct_);
const int old_size = static_cast<int>(base_ct_.terms.size());
for (int i = 0; i < old_size; ++i) {
if (base_ct_.terms[i].bound_diff <= 1) continue;
if (ib_processor->TryToExpandWithLowerImpliedbound(
IntegerValue(1), i,
/*complement=*/false, &base_ct_, &cut_builder_)) {
++total_num_ibs_;
}
}
}
// TODO(user): we currently only deal with Boolean in the cover. Fix.
const int num_bools =
std::partition(base_ct_.terms.begin(), base_ct_.terms.end(),
[](const CutTerm& t) { return t.bound_diff == 1; }) -
base_ct_.terms.begin();
if (num_bools == 0) return false;
IntegerValue rhs;
const int cover_size = GetCoverSize(num_bools, &rhs);
if (cover_size == 0) return false;
// Collect the weight in the cover.
IntegerValue sum(0);
std::vector<IntegerValue> cover_weights;
const int base_size = base_ct.terms.size();
for (int i = 0; i < cover_size; ++i) {
// TODO(user): we currently only deal with Boolean in the cover. Fix.
if (base_ct.terms[i].bound_diff != 1) {
++total_num_cover_not_bool_;
return false;
}
cover_weights.push_back(base_ct.terms[i].coeff);
sum = CapAddI(sum, base_ct.terms[i].coeff);
CHECK_EQ(base_ct_.terms[i].bound_diff, 1);
CHECK_GT(base_ct_.terms[i].coeff, 0);
cover_weights.push_back(base_ct_.terms[i].coeff);
sum = CapAddI(sum, base_ct_.terms[i].coeff);
}
if (AtMinOrMaxInt64(sum.value())) {
++total_num_overflow_abort_;
return false;
}
CHECK_GT(sum, base_ct.rhs);
CHECK_GT(sum, base_ct_.rhs);
// Compute the correct threshold so that if we round down larger weights to
// p/q. We have sum of the weight in cover == base_rhs.
@@ -1225,13 +1200,13 @@ bool CoverCutHelper::GenerateLetchfordSouliLifting(const CutData& base_ct,
std::sort(cover_weights.begin(), cover_weights.end());
for (int i = 0; i < cover_size; ++i) {
q = IntegerValue(cover_weights.size() - i);
if (previous_sum + cover_weights[i] * q > base_ct.rhs) {
p = base_ct.rhs - previous_sum;
if (previous_sum + cover_weights[i] * q > base_ct_.rhs) {
p = base_ct_.rhs - previous_sum;
break;
}
previous_sum += cover_weights[i];
}
CHECK_GE(q, 1) << cover_size << " " << base_size;
CHECK_GE(q, 1);
// Compute thresholds.
// For the first q values, thresholds[i] is the smallest integer such that
@@ -1251,7 +1226,7 @@ bool CoverCutHelper::GenerateLetchfordSouliLifting(const CutData& base_ct,
for (int i = q.value(); i < cover_size; ++i) {
thresholds.push_back(thresholds.back() + cover_weights[i]);
}
CHECK_EQ(thresholds.back(), base_ct.rhs + 1);
CHECK_EQ(thresholds.back(), base_ct_.rhs + 1);
// Generate the cut.
//
@@ -1264,8 +1239,10 @@ bool CoverCutHelper::GenerateLetchfordSouliLifting(const CutData& base_ct,
temp_cut_.rhs = IntegerValue(cover_size - 1);
temp_cut_.terms.clear();
num_lifting_ = 0;
const int base_size = static_cast<int>(base_ct_.terms.size());
for (int i = 0; i < base_size; ++i) {
const CutTerm& term = base_ct.terms[i];
const CutTerm& term = base_ct_.terms[i];
const IntegerValue coeff = term.coeff;
IntegerValue cut_coeff(1);
if (coeff < thresholds[0]) {
@@ -1279,13 +1256,15 @@ bool CoverCutHelper::GenerateLetchfordSouliLifting(const CutData& base_ct,
if (coeff < thresholds[i]) break;
cut_coeff = IntegerValue(i + 1);
}
if (cut_coeff != 0 && i >= cover_size) ++num_lifting_;
if (cut_coeff > 1 && i < cover_size) ++num_lifting_; // happen?
}
temp_cut_.terms.push_back(term);
temp_cut_.terms.back().coeff = cut_coeff;
}
if (!cut_builder_.ConvertToLinearConstraint(temp_cut_, &alt_cut_)) {
alt_cut_.Clear();
if (!cut_builder_.ConvertToLinearConstraint(temp_cut_, &cut_)) {
cut_.Clear();
++total_num_overflow_abort_;
return false;
}
@@ -1451,14 +1430,6 @@ CutGenerator CreateSquareCutGenerator(AffineExpression y, AffineExpression x,
return result;
}
void ImpliedBoundsProcessor::ProcessUpperBoundedConstraint(
const absl::StrongVector<IntegerVariable, double>& lp_values,
LinearConstraint* cut) {
ProcessUpperBoundedConstraintWithSlackCreation(
/*substitute_only_inner_variables=*/false, IntegerVariable(0), lp_values,
cut, nullptr);
}
ImpliedBoundsProcessor::BestImpliedBoundInfo
ImpliedBoundsProcessor::GetCachedImpliedBoundInfo(IntegerVariable var) const {
auto it = cache_.find(var);
@@ -1550,136 +1521,98 @@ void ImpliedBoundsProcessor::RecomputeCacheAndSeparateSomeImpliedBoundCuts(
}
}
void ImpliedBoundsProcessor::ProcessUpperBoundedConstraintWithSlackCreation(
bool substitute_only_inner_variables, IntegerVariable first_slack,
const absl::StrongVector<IntegerVariable, double>& lp_values,
LinearConstraint* cut, std::vector<SlackInfo>* slack_infos) {
if (cache_.empty()) return; // Nothing to do.
tmp_terms_.clear();
IntegerValue new_ub = cut->ub;
bool changed = false;
// Important: The cut_builder_ must have been reset.
bool ImpliedBoundsProcessor::TryToExpandWithLowerImpliedbound(
IntegerValue factor_t, int i, bool complement, CutData* cut,
CutDataBuilder* builder) {
CutTerm& term = cut->terms[i];
// TODO(user): we could relax a bit this test.
int64_t overflow_detection = 0;
// We only want to expand non-Boolean and non-slack term!
if (term.bound_diff <= 1) return false;
if (!term.IsSimple()) return false;
CHECK_EQ(IntTypeAbs(term.expr_coeffs[0]), 1);
const int size = cut->vars.size();
for (int i = 0; i < size; ++i) {
// Note that the caller is supposed to choose between var/NegationOf(var)
// by changing the cut.
IntegerVariable var = cut->vars[i];
IntegerValue coeff = cut->coeffs[i];
// Find the best implied bound to use.
// TODO(user): We could also use implied upper bound, that is try with
// NegationOf(var).
const BestImpliedBoundInfo info = GetCachedImpliedBoundInfo(var);
const int old_size = tmp_terms_.size();
const IntegerValue lb = integer_trail_->LevelZeroLowerBound(var);
const IntegerValue bound_diff = info.implied_bound - lb;
const double slack_lp_value = info.SlackLpValue(lb);
// Shall we keep the original term ?
bool keep_term = false;
if (bound_diff <= 0) keep_term = true;
if (info.bool_var == kNoIntegerVariable) {
keep_term = true;
} else {
if (integer_trail_->IsFixed(info.bool_var)) keep_term = true;
if (CapProd(std::abs(coeff.value()), bound_diff.value()) ==
std::numeric_limits<int64_t>::max()) {
keep_term = true;
}
}
// TODO(user): On some problem, not replacing the variable at their bound
// by an implied bounds seems beneficial. This is especially the case on
// g200x740.mps.gz
//
// Note that in ComputeCut() the variable with an LP value at the bound do
// not contribute to the cut efficacity (no loss) but do contribute to the
// various heuristic based on the coefficient magnitude.
if (substitute_only_inner_variables) {
const IntegerValue ub = integer_trail_->LevelZeroUpperBound(var);
if (lp_values[var] - ToDouble(lb) < 1e-2) keep_term = true;
if (ToDouble(ub) - lp_values[var] < 1e-2) keep_term = true;
}
// This is when we do not add slack.
if (slack_infos == nullptr) {
// We do not want to loose anything, so we only replace if the slack lp is
// zero.
if (slack_lp_value > 1e-6) keep_term = true;
}
if (keep_term) {
tmp_terms_.push_back({var, coeff});
} else {
// Substitute.
SlackInfo slack_info;
slack_info.lp_value = slack_lp_value;
slack_info.lb = 0;
slack_info.ub = integer_trail_->LevelZeroUpperBound(var) - lb;
if (info.is_positive) {
// X = Indicator * diff + lb + Slack
tmp_terms_.push_back({info.bool_var, coeff * bound_diff});
if (!AddProductTo(-coeff, lb, &new_ub)) {
VLOG(2) << "Overflow";
return;
}
if (slack_infos != nullptr) {
tmp_terms_.push_back({first_slack, coeff});
first_slack += 2;
// slack = X - Indicator * bound_diff - lb;
slack_info.terms.push_back({var, IntegerValue(1)});
slack_info.terms.push_back({info.bool_var, -bound_diff});
slack_info.offset = -lb;
slack_infos->push_back(slack_info);
}
} else {
// X = (1 - Indicator) * (diff) + lb + Slack
// X = -Indicator * (diff) + lb + diff + Slack
tmp_terms_.push_back({info.bool_var, -coeff * bound_diff});
if (!AddProductTo(-coeff, lb + bound_diff, &new_ub)) {
VLOG(2) << "Overflow";
return;
}
if (slack_infos != nullptr) {
tmp_terms_.push_back({first_slack, coeff});
first_slack += 2;
// slack = X + Indicator * bound_diff - lb - diff;
slack_info.terms.push_back({var, IntegerValue(1)});
slack_info.terms.push_back({info.bool_var, +bound_diff});
slack_info.offset = -lb - bound_diff;
slack_infos->push_back(slack_info);
}
}
changed = true;
}
// Add all the new terms coefficient to the overflow detection to avoid
// issue when merging terms referring to the same variable.
for (int i = old_size; i < tmp_terms_.size(); ++i) {
overflow_detection =
CapAdd(overflow_detection, std::abs(tmp_terms_[i].second.value()));
}
}
if (overflow_detection >= kMaxIntegerValue) {
VLOG(2) << "Overflow";
return;
}
if (!changed) return;
// Update the cut.
// Try lower bounded direction for implied bound.
// This kind should always be beneficial if it exists:
//
// Note that because of our overflow_detection variable, there should be
// no integer overflow when we merge identical terms.
cut->lb = kMinIntegerValue; // Not relevant.
cut->ub = new_ub;
CleanTermsAndFillConstraint(&tmp_terms_, cut);
// Because X = bound_diff * B + S
// We can replace coeff * X by the expression before applying f:
// = f(coeff * bound_diff) * B + f(coeff) * [X - bound_diff * B]
// = f(coeff) * X + (f(coeff * bound_diff) - f(coeff) * bound_diff] * B
// So we can "lift" B into the cut with a non-negative coefficient.
//
// Note that this lifting is really the same as if we used that implied
// bound before since in f(coeff * bound_diff) * B + f(coeff) * S, if we
// replace S by its value [X - bound_diff * B] we get the same result.
//
// TODO(user): Ignore if bound_diff == 1 ? But we can still merge B with
// another entry if it exists, so it can still be good in this case.
//
// TODO(user): Only do it if coeff_b > 0 ? But again we could still merge
// B with an existing Boolean for a better cut even if coeff_b == 0.
const IntegerVariable ib_var = term.expr_coeffs[0] > 0
? term.expr_vars[0]
: NegationOf(term.expr_vars[0]);
const ImpliedBoundsProcessor::BestImpliedBoundInfo info =
GetCachedImpliedBoundInfo(ib_var);
const IntegerValue lb = -term.expr_offset;
const IntegerValue bound_diff = info.implied_bound - lb;
if (bound_diff <= 0) return false;
if (info.bool_var == kNoIntegerVariable) return false;
if (ProdOverflow(factor_t, CapProdI(term.coeff, bound_diff))) return false;
// We have X = info.diff * Boolean + slack.
CutTerm bool_term;
bool_term.coeff = term.coeff * bound_diff;
bool_term.expr_vars[0] = info.bool_var;
bool_term.expr_coeffs[1] = 0;
bool_term.bound_diff = IntegerValue(1);
bool_term.lp_value = info.bool_lp_value;
if (info.is_positive) {
bool_term.expr_coeffs[0] = IntegerValue(1);
bool_term.expr_offset = IntegerValue(0);
} else {
bool_term.expr_coeffs[0] = IntegerValue(-1);
bool_term.expr_offset = IntegerValue(1);
}
// Create slack.
// The expression is term.exp - bound_diff * bool_term
// The variable shouldn't be the same.
DCHECK_NE(term.expr_vars[0], bool_term.expr_vars[0]);
CutTerm slack_term;
slack_term.expr_vars[0] = term.expr_vars[0];
slack_term.expr_coeffs[0] = term.expr_coeffs[0];
slack_term.expr_vars[1] = bool_term.expr_vars[0];
slack_term.expr_coeffs[1] = -bound_diff * bool_term.expr_coeffs[0];
slack_term.expr_offset =
term.expr_offset - bound_diff * bool_term.expr_offset;
slack_term.lp_value = info.SlackLpValue(lb);
slack_term.coeff = term.coeff;
slack_term.bound_diff = term.bound_diff;
// It should be good to use IB, but sometime we have things like
// 7.3 = 2 * bool@1 + 5.3 and the expanded Boolean is at its upper bound.
// It is always good to complement such variable.
//
// Note that here we do more and just complement anything closer to UB.
//
// TODO(user): Because of merges, we might have entry with a coefficient of
// zero than are not useful. Remove them.
if (complement) {
if (bool_term.lp_value > 0.5) {
bool_term.Complement(&cut->rhs);
}
if (slack_term.lp_value >
0.5 * static_cast<double>(slack_term.bound_diff.value())) {
slack_term.Complement(&cut->rhs);
}
}
term = slack_term;
builder->AddOrMergeTerm(bool_term, factor_t, cut);
return true;
}
std::string SingleNodeFlow::DebugString() const {
@@ -2011,58 +1944,6 @@ bool FlowCoverCutHelper::GenerateCut(const SingleNodeFlow& data) {
return true;
}
bool ImpliedBoundsProcessor::DebugSlack(IntegerVariable first_slack,
const LinearConstraint& initial_cut,
const LinearConstraint& cut,
const std::vector<SlackInfo>& info) {
tmp_terms_.clear();
IntegerValue new_ub = cut.ub;
for (int i = 0; i < cut.vars.size(); ++i) {
// Simple copy for non-slack variables.
if (cut.vars[i] < first_slack) {
tmp_terms_.push_back({cut.vars[i], cut.coeffs[i]});
continue;
}
// Replace slack by its definition.
const IntegerValue multiplier = cut.coeffs[i];
const int index = (cut.vars[i].value() - first_slack.value()) / 2;
for (const std::pair<IntegerVariable, IntegerValue>& term :
info[index].terms) {
tmp_terms_.push_back({term.first, term.second * multiplier});
}
new_ub -= multiplier * info[index].offset;
}
LinearConstraint tmp_cut;
tmp_cut.lb = kMinIntegerValue; // Not relevant.
tmp_cut.ub = new_ub;
CleanTermsAndFillConstraint(&tmp_terms_, &tmp_cut);
MakeAllVariablesPositive(&tmp_cut);
// We need to canonicalize the initial_cut too for comparison. Note that we
// only use this for debug, so we don't care too much about the memory and
// extra time.
// TODO(user): Expose CanonicalizeConstraint() from the manager.
LinearConstraint tmp_copy;
tmp_terms_.clear();
for (int i = 0; i < initial_cut.vars.size(); ++i) {
tmp_terms_.push_back({initial_cut.vars[i], initial_cut.coeffs[i]});
}
tmp_copy.lb = kMinIntegerValue; // Not relevant.
tmp_copy.ub = new_ub;
CleanTermsAndFillConstraint(&tmp_terms_, &tmp_copy);
MakeAllVariablesPositive(&tmp_copy);
if (tmp_cut == tmp_copy) return true;
LOG(INFO) << first_slack;
LOG(INFO) << tmp_copy.DebugString();
LOG(INFO) << cut.DebugString();
LOG(INFO) << tmp_cut.DebugString();
return false;
}
namespace {
int64_t SumOfKMinValues(const absl::btree_set<int64_t>& values, int k) {

View File

@@ -59,6 +59,7 @@ struct CutGenerator {
// and implied bound substitution.
struct CutTerm {
bool IsBoolean() const { return bound_diff == 1; }
bool IsSimple() const { return expr_coeffs[1] == 0; }
bool HasRelevantLpValue() const { return lp_value > 1e-2; }
double LpDistToMaxValue() const {
return static_cast<double>(bound_diff.value()) - lp_value;
@@ -78,7 +79,7 @@ struct CutTerm {
// X = the given LinearExpression.
// We only support size 1 or 2 here which allow to inline the memory.
int expr_size = 0;
// When a coefficient is zero, we don't care about the variable.
IntegerValue expr_offset = IntegerValue(0);
std::array<IntegerVariable, 2> expr_vars;
std::array<IntegerValue, 2> expr_coeffs;
@@ -157,45 +158,9 @@ class ImpliedBoundsProcessor {
void RecomputeCacheAndSeparateSomeImpliedBoundCuts(
const absl::StrongVector<IntegerVariable, double>& lp_values);
// Processes and updates the given cut.
//
// Note that we always look for implied bound under the variable as they
// appear. One can change coefficient sign and a variable to its negation to
// try the other way around.
void ProcessUpperBoundedConstraint(
const absl::StrongVector<IntegerVariable, double>& lp_values,
LinearConstraint* cut);
// Same as ProcessUpperBoundedConstraint() but instead of just using
// var >= coeff * binary + lb we use var == slack + coeff * binary + lb where
// slack is a new temporary variable that we create.
//
// The new slack will be such that slack_infos[(slack - first_slack) / 2]
// contains its definition so that we can properly handle it in the cut
// generation and substitute it back later.
struct SlackInfo {
// This slack is equal to sum of terms + offset.
std::vector<std::pair<IntegerVariable, IntegerValue>> terms;
IntegerValue offset;
// The slack bounds and current lp_value.
IntegerValue lb = IntegerValue(0);
IntegerValue ub = IntegerValue(0);
double lp_value = 0.0;
};
void ProcessUpperBoundedConstraintWithSlackCreation(
bool substitute_only_inner_variables, IntegerVariable first_slack,
const absl::StrongVector<IntegerVariable, double>& lp_values,
LinearConstraint* cut, std::vector<SlackInfo>* slack_infos);
// Only used for debugging.
//
// Substituting back the slack created by the function above should give
// exactly the same cut as the original one.
bool DebugSlack(IntegerVariable first_slack,
const LinearConstraint& initial_cut,
const LinearConstraint& cut,
const std::vector<SlackInfo>& info);
bool TryToExpandWithLowerImpliedbound(IntegerValue factor_t, int i,
bool complement, CutData* cut,
CutDataBuilder* builder);
// Add a new variable that could be used in the new cuts.
// Note that the cache must be computed to take this into account.
@@ -242,9 +207,6 @@ class ImpliedBoundsProcessor {
// Data from the constructor.
IntegerTrail* integer_trail_;
ImpliedBounds* implied_bounds_;
// Temporary memory used by ProcessUpperBoundedConstraint().
mutable std::vector<std::pair<IntegerVariable, IntegerValue>> tmp_terms_;
};
// A single node flow relaxation is a constraint of the form
@@ -312,15 +274,14 @@ class FlowCoverCutHelper {
IntegerTrail* integer_trail, ImpliedBoundsProcessor* ib_helper);
// Try to generate a cut for the given single node flow problem. Returns true
// if a cut was generated. It can be accessed by cut()/mutable_cut().
// if a cut was generated. It can be accessed by cut().
bool GenerateCut(const SingleNodeFlow& data);
// If successful, info about the last generated cut.
LinearConstraint* mutable_cut() { return &cut_; }
const LinearConstraint& cut() const { return cut_; }
// Single line of text that we append to the cut log line.
std::string Info() {
std::string Info() const {
return absl::StrCat("lift=", num_lifting_, " slack=", slack_.value(),
" #in=", num_in_ignored_, "|", num_in_flow_, "|",
num_in_bin_, " #out:", num_out_capa_, "|",
@@ -420,6 +381,7 @@ std::function<IntegerValue(IntegerValue)> GetSuperAdditiveRoundingFunction(
struct RoundingOptions {
IntegerValue max_scaling = IntegerValue(60);
bool use_ib_before_heuristic = true;
bool prefer_positive_ib = true;
};
class IntegerRoundingCutHelper {
public:
@@ -427,18 +389,16 @@ class IntegerRoundingCutHelper {
// Returns true on success. The cut can be accessed via cut().
bool ComputeCut(RoundingOptions options, const CutData& base_ct,
ImpliedBoundsProcessor* ib_processor);
ImpliedBoundsProcessor* ib_processor = nullptr);
// If successful, info about the last generated cut.
LinearConstraint* mutable_cut() { return &cut_; }
const LinearConstraint& cut() const { return cut_; }
// Returns the number of implied bound lifted Booleans in the last
// ComputeCut() call. Useful for investigation.
int NumLiftedBooleans() const { return num_ib_used_; }
void SetSharedStatistics(SharedStatistics* stats) { shared_stats_ = stats; }
// Single line of text that we append to the cut log line.
std::string Info() const { return absl::StrCat("ib_lift=", num_ib_used_); }
private:
bool HasComplementedImpliedBound(const CutTerm& entry,
ImpliedBoundsProcessor* ib_processor);
@@ -447,10 +407,6 @@ class IntegerRoundingCutHelper {
IntegerValue remainder_threshold,
const CutData& cut);
bool TryToExpandWithLowerImpliedbound(IntegerValue factor_t, int i,
bool complement, CutData* cut,
ImpliedBoundsProcessor* ib_processor);
// The helper is just here to reuse the memory for these vectors.
std::vector<IntegerValue> divisors_;
std::vector<IntegerValue> remainders_;
@@ -479,7 +435,6 @@ class IntegerRoundingCutHelper {
int64_t total_num_initial_ibs_ = 0;
int64_t total_num_initial_merges_ = 0;
int64_t total_num_initial_complements_ = 0;
};
// Helper to find knapsack cover cuts.
@@ -489,21 +444,9 @@ class CoverCutHelper {
// Try to find a cut with a knapsack heuristic.
// If this returns true, you can get the cut via cut().
bool TrySimpleKnapsack(const CutData& input);
bool TrySimpleKnapsack(const CutData& input,
ImpliedBoundsProcessor* ib_processor = nullptr);
// If successful, info about the last generated cut.
LinearConstraint* mutable_cut() { return &cut_; }
const LinearConstraint& cut() const { return cut_; }
// Single line of text that we append to the cut log line.
const std::string Info() { return absl::StrCat("lift=", num_lifting_); }
// Provides an alternative cut with a different lifting procedure.
LinearConstraint* mutable_alt_cut() { return &alt_cut_; }
const LinearConstraint& alt_cut() const { return alt_cut_; }
// Visible for testing.
//
// Applies the lifting procedure described in "On Lifted Cover Inequalities: A
// New Lifting Procedure with Unusual Properties", Adam N. Letchford, Georgia
// Souli.
@@ -529,11 +472,25 @@ class CoverCutHelper {
// - Also, f() should be super additive on the value <= rhs, i.e. f(a + b) >=
// f(a) + f(b), so it is always good to use implied bounds of the form X =
// bound * B + Slack.
bool GenerateLetchfordSouliLifting(const CutData& base_ct, int cover_size);
bool TryWithLetchfordSouliLifting(
const CutData& input, ImpliedBoundsProcessor* ib_processor = nullptr);
// If successful, info about the last generated cut.
const LinearConstraint& cut() const { return cut_; }
// Single line of text that we append to the cut log line.
std::string Info() const { return absl::StrCat("lift=", num_lifting_); }
void SetSharedStatistics(SharedStatistics* stats) { shared_stats_ = stats; }
private:
// This changes base_ct_, returns false on overflow.
bool MakeAllTermsPositive();
// This looks at base_ct_ and reoder the terms so that the first ones are in
// the cover. return zero if no interesting cover was found.
int GetCoverSize(int relevant_size, IntegerValue* rhs);
// Here to reuse memory.
CutData base_ct_;
CutData temp_cut_;
@@ -542,13 +499,13 @@ class CoverCutHelper {
// Stats.
SharedStatistics* shared_stats_ = nullptr;
int64_t num_lifting_ = 0;
int64_t total_num_lifting_ = 0;
int64_t total_num_cover_not_bool_ = 0;
int64_t total_num_ibs_ = 0;
int64_t total_num_overflow_abort_ = 0;
// Stores the cut for output.
LinearConstraint cut_;
LinearConstraint alt_cut_;
};
// A cut generator for z = x * y (x and y >= 0).
@@ -648,7 +605,7 @@ bool BuildMaxAffineUpConstraint(
CutGenerator CreateMaxAffineCutGenerator(
LinearExpression target, IntegerVariable var,
std::vector<std::pair<IntegerValue, IntegerValue>> affines,
const std::string cut_name, Model* model);
std::string cut_name, Model* model);
// Extracts the variables that have a Literal view from base variables and
// create a generator that will returns constraint of the form "at_most_one"

View File

@@ -856,148 +856,124 @@ bool LinearProgrammingConstraint::AddCutFromConstraints(
expanded_lp_solution_, flow_cover_cut_helper_.Info());
}
// We will create "artificial" variables after this index that will be
// substituted back into LP variables afterwards. Also not that we only use
// positive variable indices for these new variables, so that algorithm that
// take their negation will not mess up the indexing.
const IntegerVariable first_new_var(expanded_lp_solution_.size());
CHECK_EQ(first_new_var.value() % 2, 0);
// TODO(user): for TrySimpleKnapsack() and IntegerRoundingCut() the implied
// bounds heuristic is not the same. Clean this up.
saved_cut_ = cut_;
for (int heuristic = 0; heuristic < 2; ++heuristic) {
if (heuristic > 0) cut_ = saved_cut_;
// Unlike for the knapsack cuts, it might not be always beneficial to
// process the implied bounds even though it seems to be better in average.
//
// TODO(user): Perform more experiments, in particular with which bound we
// use and if we complement or not before the MIR rounding. Other solvers
// seems to try different complementation strategies in a "postprocessing"
// and we don't. Try this too.
//
// TODO(user): substitute_only_inner_variables = true is really helpful on
// some instance like "mtest4ma.mps". We should probably have a dedicated
// heuristic depending on the type of cut we do below.
tmp_ib_slack_infos_.clear();
if (heuristic > 0) {
const bool substitute_only_inner_variables =
absl::Bernoulli(*random_, 0.5);
implied_bounds_processor_.ProcessUpperBoundedConstraintWithSlackCreation(
substitute_only_inner_variables, first_new_var, expanded_lp_solution_,
&cut_, &tmp_ib_slack_infos_);
DCHECK(implied_bounds_processor_.DebugSlack(first_new_var, saved_cut_,
cut_, tmp_ib_slack_infos_));
// No need to recompute the same thing if we didn't do any substitution.
if (tmp_ib_slack_infos_.empty()) break;
}
// Fills data for computing the cut.
tmp_lp_values_.clear();
tmp_var_lbs_.clear();
tmp_var_ubs_.clear();
for (const IntegerVariable var : cut_.vars) {
if (var >= first_new_var) {
CHECK(VariableIsPositive(var));
const auto& info =
tmp_ib_slack_infos_[(var.value() - first_new_var.value()) / 2];
tmp_lp_values_.push_back(info.lp_value);
tmp_var_lbs_.push_back(info.lb);
tmp_var_ubs_.push_back(info.ub);
} else {
tmp_lp_values_.push_back(expanded_lp_solution_[var]);
tmp_var_lbs_.push_back(integer_trail_->LevelZeroLowerBound(var));
tmp_var_ubs_.push_back(integer_trail_->LevelZeroUpperBound(var));
CHECK_GT(tmp_var_ubs_.back(), tmp_var_lbs_.back());
}
}
// TODO(user): Keep track of the potential overflow here.
if (!base_ct_.FillFromParallelVectors(cut_, tmp_lp_values_, tmp_var_lbs_,
tmp_var_ubs_)) {
continue;
}
// Add slack.
const IntegerVariable first_slack(
first_new_var + IntegerVariable(2 * tmp_ib_slack_infos_.size()));
tmp_slack_rows_.clear();
for (const auto& pair : integer_multipliers) {
const RowIndex row = pair.first;
const IntegerValue coeff = pair.second;
const auto status = simplex_.GetConstraintStatus(row);
if (status == glop::ConstraintStatus::FIXED_VALUE) continue;
CutTerm entry;
entry.coeff = IntTypeAbs(coeff);
entry.lp_value = 0.0;
entry.bound_diff =
CapSub(integer_lp_[row].ub.value(), integer_lp_[row].lb.value());
entry.expr_size = 1;
entry.expr_vars[0] =
first_slack + 2 * IntegerVariable(tmp_slack_rows_.size());
if (coeff > 0) {
// Slack = ub - constraint;
entry.expr_coeffs[0] = IntegerValue(-1);
entry.expr_offset = integer_lp_[row].ub;
} else {
// Slack = constraint - lb;
entry.expr_coeffs[0] = IntegerValue(1);
entry.expr_offset = -integer_lp_[row].lb;
}
base_ct_.terms.push_back(entry);
tmp_slack_rows_.push_back(row);
}
// Try cover approach to find cut.
// We always use IB substitution with positive coeff here.
if (cover_cut_helper_.TrySimpleKnapsack(base_ct_)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_K"), cover_cut_helper_.Info(), first_new_var,
first_slack, tmp_ib_slack_infos_, cover_cut_helper_.mutable_cut());
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_K2"), "", first_new_var, first_slack,
tmp_ib_slack_infos_, cover_cut_helper_.mutable_alt_cut());
}
// Try integer rounding heuristic to find cut.
RoundingOptions options;
options.max_scaling = parameters_.max_integer_rounding_scaling();
if (integer_rounding_cut_helper_.ComputeCut(options, base_ct_,
&implied_bounds_processor_)) {
at_least_one_added |= PostprocessAndAddCut(
name,
absl::StrCat("num_lifted_booleans=",
integer_rounding_cut_helper_.NumLiftedBooleans()),
first_new_var, first_slack, tmp_ib_slack_infos_,
integer_rounding_cut_helper_.mutable_cut());
}
// Note that this always complement the terms to have an lp value closer to
// zero.
//
// TODO(user): Keep track of the potential overflow here.
if (!base_ct_.FillFromLinearConstraint(cut_, expanded_lp_solution_,
integer_trail_)) {
return false;
}
// If there are no integer (all Booleans), no need to try implied bounds
// heurititics. By setting this to nullptr, we are a bit faster.
bool some_ints = false;
bool some_relevant_positions = false;
for (const CutTerm& term : base_ct_.terms) {
if (term.bound_diff > 1) some_ints = true;
if (term.HasRelevantLpValue()) some_relevant_positions = true;
}
// If all value are integer, we will not be able to cut anything.
if (!some_relevant_positions) return false;
ImpliedBoundsProcessor* ib_processor =
some_ints ? &implied_bounds_processor_ : nullptr;
// Add constraint slack.
const IntegerVariable first_slack(expanded_lp_solution_.size());
CHECK_EQ(first_slack.value() % 2, 0);
tmp_slack_rows_.clear();
for (const auto& pair : integer_multipliers) {
const RowIndex row = pair.first;
const IntegerValue coeff = pair.second;
const auto status = simplex_.GetConstraintStatus(row);
if (status == glop::ConstraintStatus::FIXED_VALUE) continue;
CutTerm entry;
entry.coeff = IntTypeAbs(coeff);
entry.lp_value = 0.0;
entry.bound_diff =
CapSub(integer_lp_[row].ub.value(), integer_lp_[row].lb.value());
entry.expr_vars[0] =
first_slack + 2 * IntegerVariable(tmp_slack_rows_.size());
entry.expr_coeffs[1] = 0;
if (coeff > 0) {
// Slack = ub - constraint;
entry.expr_coeffs[0] = IntegerValue(-1);
entry.expr_offset = integer_lp_[row].ub;
} else {
// Slack = constraint - lb;
entry.expr_coeffs[0] = IntegerValue(1);
entry.expr_offset = -integer_lp_[row].lb;
}
base_ct_.terms.push_back(entry);
tmp_slack_rows_.push_back(row);
}
// Try cover approach to find cut.
// TODO(user): Optimize by merging common steps (sort mainly).
if (cover_cut_helper_.TrySimpleKnapsack(base_ct_, ib_processor)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_KB"), cover_cut_helper_.Info(), first_slack,
cover_cut_helper_.cut());
}
if (cover_cut_helper_.TryWithLetchfordSouliLifting(base_ct_, ib_processor)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_KL"), cover_cut_helper_.Info(), first_slack,
cover_cut_helper_.cut());
}
// Try integer rounding heuristic to find cut.
RoundingOptions options;
options.max_scaling = parameters_.max_integer_rounding_scaling();
options.use_ib_before_heuristic = false;
if (integer_rounding_cut_helper_.ComputeCut(options, base_ct_,
ib_processor)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_R"), integer_rounding_cut_helper_.Info(),
first_slack, integer_rounding_cut_helper_.cut());
}
options.use_ib_before_heuristic = true;
options.prefer_positive_ib = false;
if (ib_processor != nullptr && integer_rounding_cut_helper_.ComputeCut(
options, base_ct_, ib_processor)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_RB"), integer_rounding_cut_helper_.Info(),
first_slack, integer_rounding_cut_helper_.cut());
}
options.use_ib_before_heuristic = true;
options.prefer_positive_ib = true;
if (ib_processor != nullptr && integer_rounding_cut_helper_.ComputeCut(
options, base_ct_, ib_processor)) {
at_least_one_added |= PostprocessAndAddCut(
absl::StrCat(name, "_RBP"), integer_rounding_cut_helper_.Info(),
first_slack, integer_rounding_cut_helper_.cut());
}
return at_least_one_added;
}
bool LinearProgrammingConstraint::PostprocessAndAddCut(
const std::string& name, const std::string& info,
IntegerVariable first_new_var, IntegerVariable first_slack,
const std::vector<ImpliedBoundsProcessor::SlackInfo>& ib_slack_infos,
LinearConstraint* cut) {
IntegerVariable first_slack, const LinearConstraint& cut) {
// Compute the activity. Warning: the cut no longer have the same size so we
// cannot use tmp_lp_values_. Note that the substitution below shouldn't
// change the activity by definition.
double activity = 0.0;
for (int i = 0; i < cut->vars.size(); ++i) {
if (cut->vars[i] < first_new_var) {
activity +=
ToDouble(cut->coeffs[i]) * expanded_lp_solution_[cut->vars[i]];
for (int i = 0; i < cut.vars.size(); ++i) {
if (cut.vars[i] < first_slack) {
activity += ToDouble(cut.coeffs[i]) * expanded_lp_solution_[cut.vars[i]];
}
}
const double kMinViolation = 1e-4;
const double violation = activity - ToDouble(cut->ub);
const double violation = activity - ToDouble(cut.ub);
if (violation < kMinViolation) {
VLOG(3) << "Bad cut " << activity << " <= " << ToDouble(cut->ub);
VLOG(3) << "Bad cut " << activity << " <= " << ToDouble(cut.ub);
return false;
}
@@ -1005,44 +981,19 @@ bool LinearProgrammingConstraint::PostprocessAndAddCut(
{
int num_slack = 0;
tmp_scattered_vector_.ClearAndResize(integer_variables_.size());
IntegerValue cut_ub = cut->ub;
IntegerValue cut_ub = cut.ub;
bool overflow = false;
for (int i = 0; i < cut->vars.size(); ++i) {
const IntegerVariable var = cut->vars[i];
for (int i = 0; i < cut.vars.size(); ++i) {
const IntegerVariable var = cut.vars[i];
// Simple copy for non-slack variables.
if (var < first_new_var) {
if (var < first_slack) {
const glop::ColIndex col =
mirror_lp_variable_.at(PositiveVariable(var));
if (VariableIsPositive(var)) {
tmp_scattered_vector_.Add(col, cut->coeffs[i]);
tmp_scattered_vector_.Add(col, cut.coeffs[i]);
} else {
tmp_scattered_vector_.Add(col, -cut->coeffs[i]);
}
continue;
}
// Replace slack from bound substitution.
if (var < first_slack) {
const IntegerValue multiplier = cut->coeffs[i];
const int index = (var.value() - first_new_var.value()) / 2;
CHECK_LT(index, ib_slack_infos.size());
tmp_terms_.clear();
for (const std::pair<IntegerVariable, IntegerValue>& term :
ib_slack_infos[index].terms) {
tmp_terms_.push_back(
{mirror_lp_variable_.at(PositiveVariable(term.first)),
VariableIsPositive(term.first) ? term.second : -term.second});
}
if (!tmp_scattered_vector_.AddLinearExpressionMultiple(multiplier,
tmp_terms_)) {
overflow = true;
break;
}
if (!AddProductTo(multiplier, -ib_slack_infos[index].offset, &cut_ub)) {
overflow = true;
break;
tmp_scattered_vector_.Add(col, -cut.coeffs[i]);
}
continue;
}
@@ -1051,7 +1002,7 @@ bool LinearProgrammingConstraint::PostprocessAndAddCut(
++num_slack;
const int slack_index = (var.value() - first_slack.value()) / 2;
const glop::RowIndex row = tmp_slack_rows_[slack_index];
const IntegerValue multiplier = cut->coeffs[i];
const IntegerValue multiplier = cut.coeffs[i];
if (!tmp_scattered_vector_.AddLinearExpressionMultiple(
multiplier, integer_lp_[row].terms)) {
overflow = true;
@@ -1066,23 +1017,18 @@ bool LinearProgrammingConstraint::PostprocessAndAddCut(
VLOG(3) << " num_slack: " << num_slack;
tmp_scattered_vector_.ConvertToLinearConstraint(integer_variables_, cut_ub,
cut);
&cut_);
}
// Display some stats used for investigation of cut generation.
const std::string extra_info =
absl::StrCat(info, " num_ib_substitutions=", ib_slack_infos.size());
const double new_violation =
ComputeActivity(*cut, expanded_lp_solution_) - ToDouble(cut->ub);
ComputeActivity(cut_, expanded_lp_solution_) - ToDouble(cut_.ub);
if (std::abs(violation - new_violation) >= 1e-4) {
VLOG(1) << "Violation discrepancy after slack removal. "
<< " before = " << violation << " after = " << new_violation;
}
DivideByGCD(cut);
return constraint_manager_.AddCut(*cut, name, expanded_lp_solution_,
extra_info);
DivideByGCD(&cut_);
return constraint_manager_.AddCut(cut_, name, expanded_lp_solution_, info);
}
// TODO(user): This can be still too slow on some problems like

View File

@@ -290,11 +290,9 @@ class LinearProgrammingConstraint : public PropagatorInterface,
integer_multipliers);
// Second half of AddCutFromConstraints().
bool PostprocessAndAddCut(
const std::string& name, const std::string& info,
IntegerVariable first_new_var, IntegerVariable first_slack,
const std::vector<ImpliedBoundsProcessor::SlackInfo>& ib_slack_infos,
LinearConstraint* cut);
bool PostprocessAndAddCut(const std::string& name, const std::string& info,
IntegerVariable first_slack,
const LinearConstraint& cut);
// Computes and adds the corresponding type of cuts.
// This can currently only be called at the root node.
@@ -449,7 +447,6 @@ class LinearProgrammingConstraint : public PropagatorInterface,
std::vector<IntegerValue> tmp_var_lbs_;
std::vector<IntegerValue> tmp_var_ubs_;
std::vector<glop::RowIndex> tmp_slack_rows_;
std::vector<ImpliedBoundsProcessor::SlackInfo> tmp_ib_slack_infos_;
std::vector<std::pair<glop::ColIndex, IntegerValue>> tmp_terms_;
// Used by AddCGCuts().