1313 lines
48 KiB
C++
1313 lines
48 KiB
C++
// Copyright 2010-2025 Google LLC
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "ortools/sat/pb_constraint.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/hash/hash.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/strings/str_format.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/base/strong_vector.h"
|
|
#include "ortools/sat/enforcement.h"
|
|
#include "ortools/sat/sat_base.h"
|
|
#include "ortools/sat/sat_parameters.pb.h"
|
|
#include "ortools/util/bitset.h"
|
|
#include "ortools/util/saturated_arithmetic.h"
|
|
#include "ortools/util/stats.h"
|
|
#include "ortools/util/strong_integers.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
namespace {
|
|
|
|
bool LiteralComparator(const LiteralWithCoeff& a, const LiteralWithCoeff& b) {
|
|
return a.literal.Index() < b.literal.Index();
|
|
}
|
|
|
|
bool CoeffComparator(const LiteralWithCoeff& a, const LiteralWithCoeff& b) {
|
|
if (a.coefficient == b.coefficient) {
|
|
return a.literal.Index() < b.literal.Index();
|
|
}
|
|
return a.coefficient < b.coefficient;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool ComputeBooleanLinearExpressionCanonicalForm(
|
|
std::vector<LiteralWithCoeff>* cst, Coefficient* bound_shift,
|
|
Coefficient* max_value) {
|
|
// Note(user): For some reason, the IntType checking doesn't work here ?! that
|
|
// is a bit worrying, but the code seems to behave correctly.
|
|
*bound_shift = 0;
|
|
*max_value = 0;
|
|
|
|
// First, sort by literal to remove duplicate literals.
|
|
// This also removes terms with a zero coefficient.
|
|
std::sort(cst->begin(), cst->end(), LiteralComparator);
|
|
int index = 0;
|
|
LiteralWithCoeff* representative = nullptr;
|
|
for (int i = 0; i < cst->size(); ++i) {
|
|
const LiteralWithCoeff current = (*cst)[i];
|
|
if (current.coefficient == 0) continue;
|
|
if (representative != nullptr &&
|
|
current.literal.Variable() == representative->literal.Variable()) {
|
|
if (current.literal == representative->literal) {
|
|
if (!SafeAddInto(current.coefficient, &(representative->coefficient))) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// Here current_literal is equal to (1 - representative).
|
|
if (!SafeAddInto(-current.coefficient,
|
|
&(representative->coefficient))) {
|
|
return false;
|
|
}
|
|
if (!SafeAddInto(-current.coefficient, bound_shift)) return false;
|
|
}
|
|
} else {
|
|
if (representative != nullptr && representative->coefficient == 0) {
|
|
--index;
|
|
}
|
|
(*cst)[index] = current;
|
|
representative = &((*cst)[index]);
|
|
++index;
|
|
}
|
|
}
|
|
if (representative != nullptr && representative->coefficient == 0) {
|
|
--index;
|
|
}
|
|
cst->resize(index);
|
|
|
|
// Then, make all coefficients positive by replacing a term "-c x" into
|
|
// "c(1-x) - c" which is the same as "c(not x) - c".
|
|
for (int i = 0; i < cst->size(); ++i) {
|
|
const LiteralWithCoeff current = (*cst)[i];
|
|
if (current.coefficient < 0) {
|
|
if (!SafeAddInto(-current.coefficient, bound_shift)) return false;
|
|
(*cst)[i].coefficient = -current.coefficient;
|
|
(*cst)[i].literal = current.literal.Negated();
|
|
}
|
|
if (!SafeAddInto((*cst)[i].coefficient, max_value)) return false;
|
|
}
|
|
|
|
// Finally sort by increasing coefficients.
|
|
std::sort(cst->begin(), cst->end(), CoeffComparator);
|
|
DCHECK_GE(*max_value, 0);
|
|
return true;
|
|
}
|
|
|
|
bool ApplyLiteralMapping(
|
|
const util_intops::StrongVector<LiteralIndex, LiteralIndex>& mapping,
|
|
std::vector<LiteralWithCoeff>* cst, Coefficient* bound_shift,
|
|
Coefficient* max_value) {
|
|
int index = 0;
|
|
Coefficient shift_due_to_fixed_variables(0);
|
|
for (const LiteralWithCoeff& entry : *cst) {
|
|
if (mapping[entry.literal] >= 0) {
|
|
(*cst)[index] =
|
|
LiteralWithCoeff(Literal(mapping[entry.literal]), entry.coefficient);
|
|
++index;
|
|
} else if (mapping[entry.literal] == kTrueLiteralIndex) {
|
|
if (!SafeAddInto(-entry.coefficient, &shift_due_to_fixed_variables)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// Nothing to do if the literal is false.
|
|
DCHECK_EQ(mapping[entry.literal], kFalseLiteralIndex);
|
|
}
|
|
}
|
|
cst->resize(index);
|
|
if (cst->empty()) {
|
|
*bound_shift = shift_due_to_fixed_variables;
|
|
*max_value = 0;
|
|
return true;
|
|
}
|
|
const bool result =
|
|
ComputeBooleanLinearExpressionCanonicalForm(cst, bound_shift, max_value);
|
|
if (!SafeAddInto(shift_due_to_fixed_variables, bound_shift)) return false;
|
|
return result;
|
|
}
|
|
|
|
// TODO(user): Also check for no duplicates literals + unit tests.
|
|
bool BooleanLinearExpressionIsCanonical(
|
|
absl::Span<const Literal> enforcement_literals,
|
|
absl::Span<const LiteralWithCoeff> cst) {
|
|
if (!std::is_sorted(enforcement_literals.begin(),
|
|
enforcement_literals.end())) {
|
|
return false;
|
|
}
|
|
Coefficient previous(1);
|
|
for (LiteralWithCoeff term : cst) {
|
|
if (term.coefficient < previous) return false;
|
|
previous = term.coefficient;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// TODO(user): Use more complex simplification like dividing by the gcd of
|
|
// everyone and using less different coefficients if possible.
|
|
void SimplifyCanonicalBooleanLinearConstraint(
|
|
std::vector<LiteralWithCoeff>* cst, Coefficient* rhs) {
|
|
// Replace all coefficient >= rhs by rhs + 1 (these literal must actually be
|
|
// false). Note that the linear sum of literals remains canonical.
|
|
//
|
|
// TODO(user): It is probably better to remove these literals and have other
|
|
// constraint setting them to false from the symmetry finder perspective.
|
|
for (LiteralWithCoeff& x : *cst) {
|
|
if (x.coefficient > *rhs) x.coefficient = *rhs + 1;
|
|
}
|
|
}
|
|
|
|
Coefficient ComputeCanonicalRhs(Coefficient upper_bound,
|
|
Coefficient bound_shift,
|
|
Coefficient max_value) {
|
|
Coefficient rhs = upper_bound;
|
|
if (!SafeAddInto(bound_shift, &rhs)) {
|
|
if (bound_shift > 0) {
|
|
// Positive overflow. The constraint is trivially true.
|
|
// This is because the canonical linear expression is in [0, max_value].
|
|
return max_value;
|
|
} else {
|
|
// Negative overflow. The constraint is infeasible.
|
|
return Coefficient(-1);
|
|
}
|
|
}
|
|
if (rhs < 0) return Coefficient(-1);
|
|
return std::min(max_value, rhs);
|
|
}
|
|
|
|
Coefficient ComputeNegatedCanonicalRhs(Coefficient lower_bound,
|
|
Coefficient bound_shift,
|
|
Coefficient max_value) {
|
|
// The new bound is "max_value - (lower_bound + bound_shift)", but we must
|
|
// pay attention to possible overflows.
|
|
Coefficient shifted_lb = lower_bound;
|
|
if (!SafeAddInto(bound_shift, &shifted_lb)) {
|
|
if (bound_shift > 0) {
|
|
// Positive overflow. The constraint is infeasible.
|
|
return Coefficient(-1);
|
|
} else {
|
|
// Negative overflow. The constraint is trivially satisfiable.
|
|
return max_value;
|
|
}
|
|
}
|
|
if (shifted_lb <= 0) {
|
|
// If shifted_lb <= 0 then the constraint is trivially satisfiable. We test
|
|
// this so we are sure that max_value - shifted_lb doesn't overflow below.
|
|
return max_value;
|
|
}
|
|
return max_value - shifted_lb;
|
|
}
|
|
|
|
bool CanonicalBooleanLinearProblem::AddLinearConstraint(
|
|
bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound,
|
|
Coefficient upper_bound, std::vector<LiteralWithCoeff>* cst) {
|
|
// Canonicalize the linear expression of the constraint.
|
|
Coefficient bound_shift;
|
|
Coefficient max_value;
|
|
if (!ComputeBooleanLinearExpressionCanonicalForm(cst, &bound_shift,
|
|
&max_value)) {
|
|
return false;
|
|
}
|
|
if (use_upper_bound) {
|
|
const Coefficient rhs =
|
|
ComputeCanonicalRhs(upper_bound, bound_shift, max_value);
|
|
if (!AddConstraint(*cst, max_value, rhs)) return false;
|
|
}
|
|
if (use_lower_bound) {
|
|
// We transform the constraint into an upper-bounded one.
|
|
for (int i = 0; i < cst->size(); ++i) {
|
|
(*cst)[i].literal = (*cst)[i].literal.Negated();
|
|
}
|
|
const Coefficient rhs =
|
|
ComputeNegatedCanonicalRhs(lower_bound, bound_shift, max_value);
|
|
if (!AddConstraint(*cst, max_value, rhs)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CanonicalBooleanLinearProblem::AddConstraint(
|
|
absl::Span<const LiteralWithCoeff> cst, Coefficient max_value,
|
|
Coefficient rhs) {
|
|
if (rhs < 0) return false; // Trivially unsatisfiable.
|
|
if (rhs >= max_value) return true; // Trivially satisfiable.
|
|
constraints_.emplace_back(cst.begin(), cst.end());
|
|
rhs_.push_back(rhs);
|
|
SimplifyCanonicalBooleanLinearConstraint(&constraints_.back(), &rhs_.back());
|
|
return true;
|
|
}
|
|
|
|
void MutableUpperBoundedLinearConstraint::ClearAndResize(int num_variables) {
|
|
if (terms_.size() != num_variables) {
|
|
terms_.assign(num_variables, Coefficient(0));
|
|
non_zeros_.ClearAndResize(BooleanVariable(num_variables));
|
|
rhs_ = 0;
|
|
max_sum_ = 0;
|
|
} else {
|
|
ClearAll();
|
|
}
|
|
}
|
|
|
|
void MutableUpperBoundedLinearConstraint::ClearAll() {
|
|
// TODO(user): We could be more efficient and have only one loop here.
|
|
for (BooleanVariable var : non_zeros_.PositionsSetAtLeastOnce()) {
|
|
terms_[var] = Coefficient(0);
|
|
}
|
|
non_zeros_.ResetAllToFalse();
|
|
rhs_ = 0;
|
|
max_sum_ = 0;
|
|
}
|
|
|
|
// TODO(user): Also reduce the trivially false literal when coeff > rhs_ ?
|
|
void MutableUpperBoundedLinearConstraint::ReduceCoefficients() {
|
|
CHECK_LT(rhs_, max_sum_) << "Trivially sat.";
|
|
Coefficient removed_sum(0);
|
|
const Coefficient bound = max_sum_ - rhs_;
|
|
for (BooleanVariable var : PossibleNonZeros()) {
|
|
const Coefficient diff = GetCoefficient(var) - bound;
|
|
if (diff > 0) {
|
|
removed_sum += diff;
|
|
terms_[var] = (terms_[var] > 0) ? bound : -bound;
|
|
}
|
|
}
|
|
rhs_ -= removed_sum;
|
|
max_sum_ -= removed_sum;
|
|
DCHECK_EQ(max_sum_, ComputeMaxSum());
|
|
}
|
|
|
|
std::string MutableUpperBoundedLinearConstraint::DebugString() {
|
|
std::string result;
|
|
for (BooleanVariable var : PossibleNonZeros()) {
|
|
if (!result.empty()) result += " + ";
|
|
result += absl::StrFormat("%d[%s]", GetCoefficient(var).value(),
|
|
GetLiteral(var).DebugString());
|
|
}
|
|
result += absl::StrFormat(" <= %d", rhs_.value());
|
|
return result;
|
|
}
|
|
|
|
// TODO(user): Keep this for DCHECK(), but maintain the slack incrementally
|
|
// instead of recomputing it.
|
|
Coefficient MutableUpperBoundedLinearConstraint::ComputeSlackForTrailPrefix(
|
|
const Trail& trail, int trail_index) const {
|
|
Coefficient activity(0);
|
|
for (BooleanVariable var : PossibleNonZeros()) {
|
|
if (GetCoefficient(var) == 0) continue;
|
|
if (trail.Assignment().LiteralIsTrue(GetLiteral(var)) &&
|
|
trail.Info(var).trail_index < trail_index) {
|
|
activity += GetCoefficient(var);
|
|
}
|
|
}
|
|
return rhs_ - activity;
|
|
}
|
|
|
|
Coefficient MutableUpperBoundedLinearConstraint::
|
|
ReduceCoefficientsAndComputeSlackForTrailPrefix(const Trail& trail,
|
|
int trail_index) {
|
|
Coefficient activity(0);
|
|
Coefficient removed_sum(0);
|
|
const Coefficient bound = max_sum_ - rhs_;
|
|
for (BooleanVariable var : PossibleNonZeros()) {
|
|
if (GetCoefficient(var) == 0) continue;
|
|
const Coefficient diff = GetCoefficient(var) - bound;
|
|
if (trail.Assignment().LiteralIsTrue(GetLiteral(var)) &&
|
|
trail.Info(var).trail_index < trail_index) {
|
|
if (diff > 0) {
|
|
removed_sum += diff;
|
|
terms_[var] = (terms_[var] > 0) ? bound : -bound;
|
|
}
|
|
activity += GetCoefficient(var);
|
|
} else {
|
|
// Because we assume the slack (rhs - activity) to be negative, we have
|
|
// coeff + rhs - max_sum_ <= coeff + rhs - (activity + coeff)
|
|
// <= slack
|
|
// < 0
|
|
CHECK_LE(diff, 0);
|
|
}
|
|
}
|
|
rhs_ -= removed_sum;
|
|
max_sum_ -= removed_sum;
|
|
DCHECK_EQ(max_sum_, ComputeMaxSum());
|
|
return rhs_ - activity;
|
|
}
|
|
|
|
void MutableUpperBoundedLinearConstraint::ReduceSlackTo(
|
|
const Trail& trail, int trail_index, Coefficient initial_slack,
|
|
Coefficient target) {
|
|
// Positive slack.
|
|
const Coefficient slack = initial_slack;
|
|
DCHECK_EQ(slack, ComputeSlackForTrailPrefix(trail, trail_index));
|
|
CHECK_LE(target, slack);
|
|
CHECK_GE(target, 0);
|
|
|
|
// This is not strictly needed, but true in our use case:
|
|
// The variable assigned at trail_index was causing a conflict.
|
|
const Coefficient coeff = GetCoefficient(trail[trail_index].Variable());
|
|
CHECK_LT(slack, coeff);
|
|
|
|
// Nothing to do if the slack is already target.
|
|
if (slack == target) return;
|
|
|
|
// Applies the algorithm described in the .h
|
|
const Coefficient diff = slack - target;
|
|
rhs_ -= diff;
|
|
for (BooleanVariable var : PossibleNonZeros()) {
|
|
if (GetCoefficient(var) == 0) continue;
|
|
if (trail.Assignment().LiteralIsTrue(GetLiteral(var)) &&
|
|
trail.Info(var).trail_index < trail_index) {
|
|
continue;
|
|
}
|
|
if (GetCoefficient(var) > diff) {
|
|
terms_[var] = (terms_[var] > 0) ? terms_[var] - diff : terms_[var] + diff;
|
|
max_sum_ -= diff;
|
|
} else {
|
|
max_sum_ -= GetCoefficient(var);
|
|
terms_[var] = 0;
|
|
}
|
|
}
|
|
DCHECK_EQ(max_sum_, ComputeMaxSum());
|
|
}
|
|
|
|
void MutableUpperBoundedLinearConstraint::CopyIntoVector(
|
|
std::vector<LiteralWithCoeff>* output) {
|
|
output->clear();
|
|
for (BooleanVariable var : non_zeros_.PositionsSetAtLeastOnce()) {
|
|
const Coefficient coeff = GetCoefficient(var);
|
|
if (coeff != 0) {
|
|
output->push_back(LiteralWithCoeff(GetLiteral(var), GetCoefficient(var)));
|
|
}
|
|
}
|
|
std::sort(output->begin(), output->end(), CoeffComparator);
|
|
}
|
|
|
|
Coefficient MutableUpperBoundedLinearConstraint::ComputeMaxSum() const {
|
|
Coefficient result(0);
|
|
for (BooleanVariable var : non_zeros_.PositionsSetAtLeastOnce()) {
|
|
result += GetCoefficient(var);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
UpperBoundedLinearConstraint::UpperBoundedLinearConstraint(
|
|
const std::vector<Literal>& enforcement_literals,
|
|
const std::vector<LiteralWithCoeff>& cst)
|
|
: is_marked_for_deletion_(false),
|
|
is_learned_(false),
|
|
first_reason_trail_index_(-1),
|
|
activity_(0.0),
|
|
enforcement_id_(-1) {
|
|
DCHECK(!cst.empty());
|
|
DCHECK(
|
|
std::is_sorted(enforcement_literals.begin(), enforcement_literals.end()));
|
|
DCHECK(std::is_sorted(cst.begin(), cst.end(), CoeffComparator));
|
|
literals_.reserve(cst.size());
|
|
|
|
// Reserve the space for coeffs_ and starts_ (it is slightly more efficient).
|
|
{
|
|
int size = 0;
|
|
Coefficient prev(0); // Ignore initial zeros.
|
|
for (LiteralWithCoeff term : cst) {
|
|
if (term.coefficient != prev) {
|
|
prev = term.coefficient;
|
|
++size;
|
|
}
|
|
}
|
|
coeffs_.reserve(size);
|
|
starts_.reserve(size + 1);
|
|
}
|
|
|
|
Coefficient prev(0);
|
|
for (LiteralWithCoeff term : cst) {
|
|
if (term.coefficient != prev) {
|
|
prev = term.coefficient;
|
|
coeffs_.push_back(term.coefficient);
|
|
starts_.push_back(literals_.size());
|
|
}
|
|
literals_.push_back(term.literal);
|
|
}
|
|
|
|
// Sentinel.
|
|
starts_.push_back(literals_.size());
|
|
|
|
hash_ = absl::Hash<
|
|
std::pair<std::vector<Literal>, std::vector<LiteralWithCoeff>>>()(
|
|
{enforcement_literals, cst});
|
|
}
|
|
|
|
void UpperBoundedLinearConstraint::AddToConflict(
|
|
MutableUpperBoundedLinearConstraint* conflict) {
|
|
CHECK_LT(enforcement_id_, 0) << "Enforcement literals are not supported";
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
conflict->AddTerm(literal, coeffs_[coeff_index]);
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
conflict->AddToRhs(rhs_);
|
|
}
|
|
|
|
bool UpperBoundedLinearConstraint::HasIdenticalTermsAndEnforcement(
|
|
absl::Span<const Literal> enforcement_literals,
|
|
absl::Span<const LiteralWithCoeff> cst,
|
|
EnforcementPropagator* enforcement_propagator) {
|
|
if (enforcement_literals !=
|
|
enforcement_propagator->GetEnforcementLiterals(enforcement_id_)) {
|
|
return false;
|
|
}
|
|
if (cst.size() != literals_.size()) return false;
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (LiteralWithCoeff term : cst) {
|
|
if (literals_[literal_index] != term.literal) return false;
|
|
if (coeffs_[coeff_index] != term.coefficient) return false;
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) {
|
|
++coeff_index;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UpperBoundedLinearConstraint::InitializeRhs(
|
|
EnforcementStatus enforcement_status,
|
|
absl::Span<const Literal> enforcement_literals, Coefficient rhs,
|
|
int trail_index, Coefficient* threshold, Trail* trail,
|
|
PbConstraintsEnqueueHelper* helper) {
|
|
// Compute the slack from the assigned variables with a trail index
|
|
// smaller than the given trail_index. The variable at trail_index has not
|
|
// yet been propagated.
|
|
rhs_ = rhs;
|
|
Coefficient slack = rhs;
|
|
|
|
// sum_at_previous_level[i] is the sum of assigned literals with a level <
|
|
// i. Since we want the sums up to sum_at_previous_level[last_level + 1],
|
|
// the size of the vector must be last_level + 2.
|
|
const int last_level = trail->CurrentDecisionLevel();
|
|
std::vector<Coefficient> sum_at_previous_level(last_level + 2,
|
|
Coefficient(0));
|
|
|
|
int max_relevant_trail_index = 0;
|
|
if (trail_index > 0) {
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
const BooleanVariable var = literal.Variable();
|
|
const Coefficient coeff = coeffs_[coeff_index];
|
|
if (trail->Assignment().LiteralIsTrue(literal) &&
|
|
trail->Info(var).trail_index < trail_index) {
|
|
max_relevant_trail_index =
|
|
std::max(max_relevant_trail_index, trail->Info(var).trail_index);
|
|
slack -= coeff;
|
|
sum_at_previous_level[trail->Info(var).level + 1] += coeff;
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
|
|
// The constraint is infeasible provided the current propagated trail.
|
|
if (slack < 0 && enforcement_literals.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Cumulative sum.
|
|
for (int i = 1; i < sum_at_previous_level.size(); ++i) {
|
|
sum_at_previous_level[i] += sum_at_previous_level[i - 1];
|
|
}
|
|
}
|
|
|
|
// Check the no-propagation at earlier level precondition.
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
const BooleanVariable var = literal.Variable();
|
|
const int level = trail->Assignment().VariableIsAssigned(var)
|
|
? trail->Info(var).level
|
|
: last_level;
|
|
if (level > 0) {
|
|
CHECK_LE(coeffs_[coeff_index], rhs_ - sum_at_previous_level[level])
|
|
<< "var should have been propagated at an earlier level !";
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
|
|
// Initial propagation.
|
|
//
|
|
// TODO(user): The source trail index for the propagation reason (i.e.
|
|
// max_relevant_trail_index) may be higher than necessary (for some of the
|
|
// propagated literals). Currently this works with FillReason(), but it was a
|
|
// source of a really nasty bug (see CL 68906167) because of the (rhs == 1)
|
|
// optim. Find a good way to test the logic.
|
|
index_ = coeffs_.size() - 1;
|
|
already_propagated_end_ = literals_.size();
|
|
Update(slack, threshold);
|
|
if (enforcement_status == EnforcementStatus::IS_FALSE ||
|
|
enforcement_status == EnforcementStatus::CANNOT_PROPAGATE ||
|
|
*threshold >= 0) {
|
|
return true;
|
|
}
|
|
return Propagate(max_relevant_trail_index, threshold, trail,
|
|
enforcement_status, enforcement_literals, helper);
|
|
}
|
|
|
|
bool UpperBoundedLinearConstraint::Propagate(
|
|
int trail_index, Coefficient* threshold, Trail* trail,
|
|
EnforcementStatus enforcement_status,
|
|
absl::Span<const Literal> enforcement_literals,
|
|
PbConstraintsEnqueueHelper* helper, bool* need_untrail_inspection) {
|
|
DCHECK_LT(*threshold, 0);
|
|
DCHECK_NE(enforcement_status, EnforcementStatus::IS_FALSE);
|
|
DCHECK_NE(enforcement_status, EnforcementStatus::CANNOT_PROPAGATE);
|
|
const Coefficient slack = GetSlackFromThreshold(*threshold);
|
|
|
|
if (slack < 0) {
|
|
// This can happen if the slack became negative while the status was
|
|
// IS_FALSE or CANNOT_PROPAGATE.
|
|
DCHECK(!enforcement_literals.empty());
|
|
if (enforcement_status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) {
|
|
// Propagate the unique unassigned enforcement literal.
|
|
for (const Literal literal : enforcement_literals) {
|
|
if (!trail->Assignment().LiteralIsAssigned(literal)) {
|
|
helper->Enqueue(literal.Negated(), trail_index, this, trail);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
// Conflict.
|
|
FillReason(*trail, trail_index, enforcement_literals,
|
|
/*propagated_variable=*/kNoBooleanVariable,
|
|
&helper->temporary_tuples, &helper->conflict);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
while (index_ >= 0 && coeffs_[index_] > slack) --index_;
|
|
if (need_untrail_inspection != nullptr) {
|
|
*need_untrail_inspection = true;
|
|
}
|
|
|
|
// Check propagation.
|
|
BooleanVariable first_propagated_variable(-1);
|
|
for (int i = starts_[index_ + 1]; i < already_propagated_end_; ++i) {
|
|
if (trail->Assignment().LiteralIsFalse(literals_[i])) continue;
|
|
if (trail->Assignment().LiteralIsTrue(literals_[i])) {
|
|
const int literal_trail_index =
|
|
trail->Info(literals_[i].Variable()).trail_index;
|
|
if (literal_trail_index > trail_index) {
|
|
if (enforcement_status == EnforcementStatus::IS_ENFORCED) {
|
|
// Conflict.
|
|
FillReason(*trail, trail_index, enforcement_literals,
|
|
literals_[i].Variable(), &helper->temporary_tuples,
|
|
&helper->conflict);
|
|
helper->conflict.push_back(literals_[i].Negated());
|
|
Update(slack, threshold);
|
|
return false;
|
|
} else {
|
|
// Propagate the unique unassigned enforcement literal.
|
|
for (const Literal literal : enforcement_literals) {
|
|
if (!trail->Assignment().LiteralIsAssigned(literal)) {
|
|
helper->Enqueue(literal.Negated(), literal_trail_index, this,
|
|
trail);
|
|
break;
|
|
}
|
|
}
|
|
Update(slack, threshold);
|
|
return true;
|
|
}
|
|
}
|
|
} else if (enforcement_status == EnforcementStatus::IS_ENFORCED) {
|
|
// Propagation.
|
|
if (first_propagated_variable < 0) {
|
|
if (first_reason_trail_index_ == -1) {
|
|
first_reason_trail_index_ = trail->Index();
|
|
}
|
|
helper->Enqueue(literals_[i].Negated(), trail_index, this, trail);
|
|
first_propagated_variable = literals_[i].Variable();
|
|
} else {
|
|
// Note that the reason for first_propagated_variable is always a
|
|
// valid reason for literals_[i].Variable() because we process the
|
|
// variable in increasing coefficient order.
|
|
trail->EnqueueWithSameReasonAs(literals_[i].Negated(),
|
|
first_propagated_variable);
|
|
}
|
|
}
|
|
}
|
|
Update(slack, threshold);
|
|
DCHECK_GE(*threshold, 0);
|
|
return true;
|
|
}
|
|
|
|
void UpperBoundedLinearConstraint::FillReason(
|
|
const Trail& trail, int source_trail_index,
|
|
absl::Span<const Literal> enforcement_literals,
|
|
BooleanVariable propagated_variable,
|
|
std::vector<std::tuple<int, int, int>>* temporary_tuples,
|
|
std::vector<Literal>* reason) {
|
|
bool enforcement_propagation = false;
|
|
reason->clear();
|
|
for (const Literal literal : enforcement_literals) {
|
|
if (trail.Assignment().LiteralIsTrue(literal)) {
|
|
reason->push_back(literal.Negated());
|
|
} else if (literal.Variable() == propagated_variable) {
|
|
enforcement_propagation = true;
|
|
}
|
|
}
|
|
|
|
// propagated_variable is set to kNoBooleanVariable when the constraint
|
|
// becomes enforced when the slack is already negative. In this case, or when
|
|
// the enforcement can be propagated, the reason must include the literal
|
|
// which makes the slack become negative, called the "extra literal reason".
|
|
const bool add_extra_literal_reason =
|
|
enforcement_propagation || propagated_variable == kNoBooleanVariable;
|
|
|
|
// Optimization for an "at most one" constraint. Note that the
|
|
// source_trail_index set by InitializeRhs() is ok in this case.
|
|
if (rhs_ == 1 && !add_extra_literal_reason) {
|
|
reason->push_back(trail[source_trail_index].Negated());
|
|
return;
|
|
}
|
|
|
|
// Compute all the literals of the constraint that were assigned to true at
|
|
// the time of the propagation, sorted by trail index.
|
|
// Vector of (trail_index, literal_index, coeff_index) tuples.
|
|
std::vector<std::tuple<int, int, int>>& true_literals = *temporary_tuples;
|
|
true_literals.clear();
|
|
Coefficient propagated_variable_coefficient(0);
|
|
{
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
if (literal.Variable() == propagated_variable) {
|
|
propagated_variable_coefficient = coeffs_[coeff_index];
|
|
}
|
|
if (trail.Assignment().LiteralIsTrue(literal) &&
|
|
trail.Info(literal.Variable()).trail_index <= source_trail_index) {
|
|
true_literals.push_back({trail.Info(literal.Variable()).trail_index,
|
|
literal_index, coeff_index});
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
std::sort(true_literals.begin(), true_literals.end());
|
|
}
|
|
|
|
// Compute the initial reason which is formed by all the literals of the
|
|
// constraint that were assigned to true at the time of the propagation. We
|
|
// remove literals with a level of 0 since they are not needed. We also
|
|
// compute the slack at the time.
|
|
Coefficient slack = rhs_;
|
|
int new_size = 0;
|
|
for (int i = 0; i < true_literals.size(); ++i) {
|
|
auto [trail_index, literal_index, coeff_index] = true_literals[i];
|
|
const Literal literal = literals_[literal_index];
|
|
const Coefficient coeff = coeffs_[coeff_index];
|
|
if (coeff > slack) {
|
|
propagated_variable_coefficient = coeff;
|
|
if (add_extra_literal_reason) {
|
|
reason->push_back(literal.Negated());
|
|
}
|
|
break;
|
|
}
|
|
if (trail.Info(literal.Variable()).level > 0) {
|
|
true_literals[new_size++] = {trail_index, literal_index, coeff_index};
|
|
}
|
|
slack -= coeff.value();
|
|
}
|
|
true_literals.resize(new_size);
|
|
DCHECK_GT(propagated_variable_coefficient, slack);
|
|
DCHECK_GE(propagated_variable_coefficient, 0);
|
|
|
|
// In both cases, we can't minimize the reason further.
|
|
if (true_literals.size() <= 1 || coeffs_.size() == 1) {
|
|
for (const auto& [unused1, literal_index, unused2] : true_literals) {
|
|
reason->push_back(literals_[literal_index].Negated());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Remove literals with high trail indices from the reason as long as the
|
|
// limit is strictly positive.
|
|
Coefficient limit = propagated_variable_coefficient - slack;
|
|
DCHECK_GE(limit, 1);
|
|
for (int i = true_literals.size() - 1; i >= 0; --i) {
|
|
const auto [_, literal_index, coeff_index] = true_literals[i];
|
|
const Coefficient coeff = coeffs_[coeff_index];
|
|
if (coeff < limit) {
|
|
limit -= coeff.value();
|
|
} else {
|
|
reason->push_back(literals_[literal_index].Negated());
|
|
}
|
|
}
|
|
DCHECK_GE(limit, 1);
|
|
}
|
|
|
|
Coefficient UpperBoundedLinearConstraint::ComputeCancelation(
|
|
const Trail& trail, int trail_index,
|
|
const MutableUpperBoundedLinearConstraint& conflict) {
|
|
Coefficient result(0);
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
if (!trail.Assignment().VariableIsAssigned(literal.Variable()) ||
|
|
trail.Info(literal.Variable()).trail_index >= trail_index) {
|
|
result += conflict.CancelationAmount(literal, coeffs_[coeff_index]);
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void UpperBoundedLinearConstraint::ResolvePBConflict(
|
|
const Trail& trail, BooleanVariable var,
|
|
MutableUpperBoundedLinearConstraint* conflict,
|
|
Coefficient* conflict_slack) {
|
|
CHECK_LT(enforcement_id_, 0) << "Enforcement literals are not supported";
|
|
const int limit_trail_index = trail.Info(var).trail_index;
|
|
|
|
// Compute the constraint activity at the time and the coefficient of the
|
|
// variable var.
|
|
Coefficient activity(0);
|
|
Coefficient var_coeff(0);
|
|
int literal_index = 0;
|
|
int coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
if (literal.Variable() == var) {
|
|
// The variable must be of the opposite sign in the current conflict.
|
|
CHECK_NE(literal, conflict->GetLiteral(var));
|
|
var_coeff = coeffs_[coeff_index];
|
|
} else if (trail.Assignment().LiteralIsTrue(literal) &&
|
|
trail.Info(literal.Variable()).trail_index < limit_trail_index) {
|
|
activity += coeffs_[coeff_index];
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
|
|
// Special case.
|
|
if (activity > rhs_) {
|
|
// This constraint is already a conflict.
|
|
// Use this one instead to start the resolution.
|
|
//
|
|
// TODO(user): Investigate if this is a good idea. It doesn't happen often,
|
|
// but does happened. Maybe we can detect this before in Propagate()? The
|
|
// setup is:
|
|
// - At a given trail_index, var is propagated and added on the trail.
|
|
// - There is some constraint literals assigned to true with a trail index
|
|
// in (trail_index, var.trail_index).
|
|
// - Their sum is high enough to cause a conflict.
|
|
// - But individually, their coefficients are too small to be propagated, so
|
|
// the conflict is not yet detected. It will be when these variables are
|
|
// processed by PropagateNext().
|
|
conflict->ClearAll();
|
|
AddToConflict(conflict);
|
|
*conflict_slack = rhs_ - activity;
|
|
DCHECK_EQ(*conflict_slack,
|
|
conflict->ComputeSlackForTrailPrefix(trail, limit_trail_index));
|
|
return;
|
|
}
|
|
|
|
// This is the slack of *this for the trail prefix < limit_trail_index.
|
|
const Coefficient slack = rhs_ - activity;
|
|
CHECK_GE(slack, 0);
|
|
|
|
// This is the slack of the conflict at the same level.
|
|
DCHECK_EQ(*conflict_slack,
|
|
conflict->ComputeSlackForTrailPrefix(trail, limit_trail_index));
|
|
|
|
// TODO(user): If there is more "cancelation" than the min_coeffs below when
|
|
// we add the two constraints, the resulting slack may be even lower. Taking
|
|
// that into account is probably good.
|
|
const Coefficient cancelation =
|
|
DEBUG_MODE ? ComputeCancelation(trail, limit_trail_index, *conflict)
|
|
: Coefficient(0);
|
|
|
|
// When we add the two constraints together, the slack of the result for the
|
|
// trail < limit_trail_index - 1 must be negative. We know that its value is
|
|
// <= slack1 + slack2 - min(coeffs), so we have nothing to do if this bound is
|
|
// already negative.
|
|
const Coefficient conflict_var_coeff = conflict->GetCoefficient(var);
|
|
const Coefficient min_coeffs = std::min(var_coeff, conflict_var_coeff);
|
|
const Coefficient new_slack_ub = slack + *conflict_slack - min_coeffs;
|
|
CHECK_LT(*conflict_slack, conflict_var_coeff);
|
|
CHECK_LT(slack, var_coeff);
|
|
if (new_slack_ub < 0) {
|
|
AddToConflict(conflict);
|
|
DCHECK_EQ(*conflict_slack + slack - cancelation,
|
|
conflict->ComputeSlackForTrailPrefix(trail, limit_trail_index));
|
|
return;
|
|
}
|
|
|
|
// We need to relax one or both of the constraints so the new slack is < 0.
|
|
// Using the relaxation described in ReduceSlackTo(), we can have this new
|
|
// slack bound:
|
|
//
|
|
// (slack - diff) + (conflict_slack - conflict_diff)
|
|
// - min(var_coeff - diff, conflict_var_coeff - conflict_diff).
|
|
//
|
|
// For all diff in [0, slack)
|
|
// For all conflict_diff in [0, conflict_slack)
|
|
Coefficient diff(0);
|
|
Coefficient conflict_diff(0);
|
|
|
|
// Is relaxing the constraint with the highest coeff enough?
|
|
if (new_slack_ub < std::max(var_coeff, conflict_var_coeff) - min_coeffs) {
|
|
const Coefficient reduc = new_slack_ub + 1;
|
|
if (var_coeff < conflict_var_coeff) {
|
|
conflict_diff += reduc;
|
|
} else {
|
|
diff += reduc;
|
|
}
|
|
} else {
|
|
// Just reduce the slack of both constraints to zero.
|
|
//
|
|
// TODO(user): The best will be to relax as little as possible.
|
|
diff = slack;
|
|
conflict_diff = *conflict_slack;
|
|
}
|
|
|
|
// Relax the conflict.
|
|
CHECK_GE(conflict_diff, 0);
|
|
CHECK_LE(conflict_diff, *conflict_slack);
|
|
if (conflict_diff > 0) {
|
|
conflict->ReduceSlackTo(trail, limit_trail_index, *conflict_slack,
|
|
*conflict_slack - conflict_diff);
|
|
*conflict_slack -= conflict_diff;
|
|
}
|
|
|
|
// We apply the same algorithm as the one in ReduceSlackTo() but on
|
|
// the non-mutable representation and add it on the fly into conflict.
|
|
CHECK_GE(diff, 0);
|
|
CHECK_LE(diff, slack);
|
|
if (diff == 0) {
|
|
// Special case if there if no relaxation is needed.
|
|
AddToConflict(conflict);
|
|
return;
|
|
}
|
|
|
|
literal_index = 0;
|
|
coeff_index = 0;
|
|
for (Literal literal : literals_) {
|
|
if (trail.Assignment().LiteralIsTrue(literal) &&
|
|
trail.Info(literal.Variable()).trail_index < limit_trail_index) {
|
|
conflict->AddTerm(literal, coeffs_[coeff_index]);
|
|
} else {
|
|
const Coefficient new_coeff = coeffs_[coeff_index] - diff;
|
|
if (new_coeff > 0) {
|
|
// TODO(user): track the cancelation here so we can update
|
|
// *conflict_slack properly.
|
|
conflict->AddTerm(literal, new_coeff);
|
|
}
|
|
}
|
|
++literal_index;
|
|
if (literal_index == starts_[coeff_index + 1]) ++coeff_index;
|
|
}
|
|
|
|
// And the rhs.
|
|
conflict->AddToRhs(rhs_ - diff);
|
|
}
|
|
|
|
void UpperBoundedLinearConstraint::Untrail(Coefficient* threshold,
|
|
int trail_index) {
|
|
const Coefficient slack = GetSlackFromThreshold(*threshold);
|
|
while (index_ + 1 < coeffs_.size() && coeffs_[index_ + 1] <= slack) ++index_;
|
|
Update(slack, threshold);
|
|
if (first_reason_trail_index_ >= trail_index) {
|
|
first_reason_trail_index_ = -1;
|
|
}
|
|
}
|
|
|
|
// TODO(user): This is relatively slow. Take the "transpose" all at once, and
|
|
// maybe put small constraints first on the to_update_ lists.
|
|
bool PbConstraints::AddConstraint(
|
|
const std::vector<Literal>& enforcement_literals,
|
|
const std::vector<LiteralWithCoeff>& cst, Coefficient rhs, Trail* trail) {
|
|
SCOPED_TIME_STAT(&stats_);
|
|
DCHECK(!cst.empty());
|
|
DCHECK(
|
|
std::is_sorted(enforcement_literals.begin(), enforcement_literals.end()));
|
|
DCHECK(std::is_sorted(cst.begin(), cst.end(), CoeffComparator));
|
|
|
|
// Special case if this is the first constraint.
|
|
if (constraints_.empty()) {
|
|
to_update_.resize(trail->NumVariables() << 1);
|
|
enqueue_helper_.propagator_id = propagator_id_;
|
|
enqueue_helper_.reasons.resize(trail->NumVariables());
|
|
propagation_trail_index_ = trail->Index();
|
|
}
|
|
|
|
std::unique_ptr<UpperBoundedLinearConstraint> c(
|
|
new UpperBoundedLinearConstraint(enforcement_literals, cst));
|
|
std::vector<UpperBoundedLinearConstraint*>& duplicate_candidates =
|
|
possible_duplicates_[c->hash()];
|
|
|
|
// Optimization if the constraint terms are duplicates.
|
|
for (UpperBoundedLinearConstraint* candidate : duplicate_candidates) {
|
|
if (candidate->HasIdenticalTermsAndEnforcement(enforcement_literals, cst,
|
|
enforcement_propagator_)) {
|
|
if (rhs < candidate->Rhs()) {
|
|
// TODO(user): the index is needed to give the correct thresholds_ entry
|
|
// to InitializeRhs() below, but this linear scan is not super
|
|
// efficient.
|
|
ConstraintIndex i(0);
|
|
while (i < constraints_.size() &&
|
|
constraints_[i.value()].get() != candidate) {
|
|
++i;
|
|
}
|
|
CHECK_LT(i, constraints_.size());
|
|
return candidate->InitializeRhs(
|
|
enforcement_propagator_->Status(candidate->enforcement_id()),
|
|
enforcement_literals, rhs, propagation_trail_index_,
|
|
&thresholds_[i], trail, &enqueue_helper_);
|
|
} else {
|
|
// The constraint is redundant, so there is nothing to do.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
thresholds_.push_back(Coefficient(0));
|
|
const EnforcementStatus enforcement_status =
|
|
enforcement_propagator_->Status(enforcement_literals);
|
|
if (!c->InitializeRhs(enforcement_status, enforcement_literals, rhs,
|
|
propagation_trail_index_, &thresholds_.back(), trail,
|
|
&enqueue_helper_)) {
|
|
thresholds_.pop_back();
|
|
return false;
|
|
}
|
|
|
|
const ConstraintIndex cst_index(constraints_.size());
|
|
enforcement_status_changed_.Resize(cst_index + 1);
|
|
if (!enforcement_literals.empty()) {
|
|
c->set_enforcement_id(enforcement_propagator_->Register(
|
|
enforcement_literals,
|
|
[&, cst_index](EnforcementId, EnforcementStatus status) {
|
|
if (status == EnforcementStatus::IS_ENFORCED ||
|
|
status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) {
|
|
enforcement_status_changed_.Set(cst_index);
|
|
}
|
|
}));
|
|
}
|
|
duplicate_candidates.push_back(c.get());
|
|
constraints_.emplace_back(c.release());
|
|
for (LiteralWithCoeff term : cst) {
|
|
DCHECK_LT(term.literal.Index(), to_update_.size());
|
|
to_update_[term.literal].push_back(ConstraintIndexWithCoeff(
|
|
trail->Assignment().VariableIsAssigned(term.literal.Variable()),
|
|
cst_index, term.coefficient));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool PbConstraints::AddLearnedConstraint(
|
|
const std::vector<LiteralWithCoeff>& cst, Coefficient rhs, Trail* trail) {
|
|
DeleteSomeLearnedConstraintIfNeeded();
|
|
const int old_num_constraints = constraints_.size();
|
|
const bool result =
|
|
AddConstraint(/*enforcement_literals=*/{}, cst, rhs, trail);
|
|
|
|
// The second test is to avoid marking a problem constraint as learned because
|
|
// of the "reuse last constraint" optimization.
|
|
if (result && constraints_.size() > old_num_constraints) {
|
|
constraints_.back()->set_is_learned(true);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool PbConstraints::PropagateConstraint(ConstraintIndex index, Trail* trail,
|
|
int source_trail_index,
|
|
bool* need_untrail_inspection) {
|
|
UpperBoundedLinearConstraint* const cst = constraints_[index.value()].get();
|
|
const EnforcementStatus enforcement_status =
|
|
enforcement_propagator_->Status(cst->enforcement_id());
|
|
if (enforcement_status == EnforcementStatus::IS_FALSE ||
|
|
enforcement_status == EnforcementStatus::CANNOT_PROPAGATE) {
|
|
return true;
|
|
}
|
|
bool conflict = false;
|
|
++num_constraint_lookups_;
|
|
const int old_value = cst->already_propagated_end();
|
|
if (!cst->Propagate(source_trail_index, &thresholds_[index], trail,
|
|
enforcement_status,
|
|
enforcement_propagator_->GetEnforcementLiterals(
|
|
cst->enforcement_id()),
|
|
&enqueue_helper_, need_untrail_inspection)) {
|
|
trail->MutableConflict()->swap(enqueue_helper_.conflict);
|
|
conflicting_constraint_index_ = index;
|
|
conflict = true;
|
|
|
|
// We bump the activity of the conflict.
|
|
BumpActivity(constraints_[index.value()].get());
|
|
}
|
|
num_inspected_constraint_literals_ +=
|
|
old_value - cst->already_propagated_end();
|
|
return !conflict;
|
|
}
|
|
|
|
bool PbConstraints::PropagateNext(Trail* trail) {
|
|
SCOPED_TIME_STAT(&stats_);
|
|
const int source_trail_index = propagation_trail_index_;
|
|
const Literal true_literal = (*trail)[propagation_trail_index_];
|
|
++propagation_trail_index_;
|
|
|
|
// We need to update ALL threshold, otherwise the Untrail() will not be
|
|
// synchronized.
|
|
bool conflict = false;
|
|
num_threshold_updates_ += to_update_[true_literal].size();
|
|
for (ConstraintIndexWithCoeff& update : to_update_[true_literal]) {
|
|
const Coefficient threshold =
|
|
thresholds_[update.index] - update.coefficient;
|
|
thresholds_[update.index] = threshold;
|
|
if (threshold < 0 && !conflict) {
|
|
conflict = !PropagateConstraint(update.index, trail, source_trail_index,
|
|
&update.need_untrail_inspection);
|
|
}
|
|
}
|
|
return !conflict;
|
|
}
|
|
|
|
bool PbConstraints::Propagate(Trail* trail) {
|
|
for (const ConstraintIndex index :
|
|
enforcement_status_changed_.PositionsSetAtLeastOnce()) {
|
|
if (thresholds_[index] < 0 &&
|
|
!PropagateConstraint(index, trail, propagation_trail_index_)) {
|
|
enforcement_status_changed_.ResetAllToFalse();
|
|
return false;
|
|
}
|
|
}
|
|
enforcement_status_changed_.ResetAllToFalse();
|
|
|
|
const int old_index = trail->Index();
|
|
while (trail->Index() == old_index && propagation_trail_index_ < old_index) {
|
|
if (!PropagateNext(trail)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void PbConstraints::Untrail(const Trail& trail, int trail_index) {
|
|
SCOPED_TIME_STAT(&stats_);
|
|
to_untrail_.ClearAndResize(ConstraintIndex(constraints_.size()));
|
|
while (propagation_trail_index_ > trail_index) {
|
|
--propagation_trail_index_;
|
|
const Literal literal = trail[propagation_trail_index_];
|
|
for (ConstraintIndexWithCoeff& update : to_update_[literal]) {
|
|
thresholds_[update.index] += update.coefficient;
|
|
|
|
// Only the constraints which were inspected during Propagate() need
|
|
// inspection during Untrail().
|
|
if (update.need_untrail_inspection) {
|
|
update.need_untrail_inspection = false;
|
|
to_untrail_.Set(update.index);
|
|
}
|
|
}
|
|
}
|
|
for (ConstraintIndex cst_index : to_untrail_.PositionsSetAtLeastOnce()) {
|
|
constraints_[cst_index.value()]->Untrail(&(thresholds_[cst_index]),
|
|
trail_index);
|
|
}
|
|
}
|
|
|
|
absl::Span<const Literal> PbConstraints::Reason(const Trail& trail,
|
|
int trail_index,
|
|
int64_t /*conflict_id*/) const {
|
|
SCOPED_TIME_STAT(&stats_);
|
|
const PbConstraintsEnqueueHelper::ReasonInfo& reason_info =
|
|
enqueue_helper_.reasons[trail_index];
|
|
std::vector<Literal>* reason = trail.GetEmptyVectorToStoreReason(trail_index);
|
|
reason_info.pb_constraint->FillReason(
|
|
trail, reason_info.source_trail_index,
|
|
enforcement_propagator_->GetEnforcementLiterals(
|
|
reason_info.pb_constraint->enforcement_id()),
|
|
trail[trail_index].Variable(), &enqueue_helper_.temporary_tuples, reason);
|
|
return *reason;
|
|
}
|
|
|
|
UpperBoundedLinearConstraint* PbConstraints::ReasonPbConstraint(
|
|
int trail_index) const {
|
|
const PbConstraintsEnqueueHelper::ReasonInfo& reason_info =
|
|
enqueue_helper_.reasons[trail_index];
|
|
return reason_info.pb_constraint;
|
|
}
|
|
|
|
// TODO(user): Because num_constraints also include problem constraints, the
|
|
// policy may not be what we want if there is a big number of problem
|
|
// constraints. Fix this.
|
|
void PbConstraints::ComputeNewLearnedConstraintLimit() {
|
|
const int num_constraints = constraints_.size();
|
|
target_number_of_learned_constraint_ =
|
|
num_constraints + parameters_->pb_cleanup_increment();
|
|
num_learned_constraint_before_cleanup_ =
|
|
static_cast<int>(target_number_of_learned_constraint_ /
|
|
parameters_->pb_cleanup_ratio()) -
|
|
num_constraints;
|
|
}
|
|
|
|
void PbConstraints::DeleteSomeLearnedConstraintIfNeeded() {
|
|
if (num_learned_constraint_before_cleanup_ == 0) {
|
|
// First time.
|
|
ComputeNewLearnedConstraintLimit();
|
|
return;
|
|
}
|
|
--num_learned_constraint_before_cleanup_;
|
|
if (num_learned_constraint_before_cleanup_ > 0) return;
|
|
SCOPED_TIME_STAT(&stats_);
|
|
|
|
// Mark the constraint that needs to be deleted.
|
|
// We do that in two pass, first we extract the activities.
|
|
std::vector<double> activities;
|
|
for (int i = 0; i < constraints_.size(); ++i) {
|
|
const UpperBoundedLinearConstraint& constraint = *(constraints_[i].get());
|
|
CHECK_LT(constraint.enforcement_id(), 0)
|
|
<< "Enforcement literals are not supported";
|
|
if (constraint.is_learned() && !constraint.is_used_as_a_reason()) {
|
|
activities.push_back(constraint.activity());
|
|
}
|
|
}
|
|
|
|
// Then we compute the cutoff threshold.
|
|
// Note that we can't delete constraint used as a reason!!
|
|
std::sort(activities.begin(), activities.end());
|
|
const int num_constraints_to_delete =
|
|
constraints_.size() - target_number_of_learned_constraint_;
|
|
CHECK_GT(num_constraints_to_delete, 0);
|
|
if (num_constraints_to_delete >= activities.size()) {
|
|
// Unlikely, but may happen, so in this case, we just delete all the
|
|
// constraint that can possibly be deleted
|
|
for (int i = 0; i < constraints_.size(); ++i) {
|
|
UpperBoundedLinearConstraint& constraint = *(constraints_[i].get());
|
|
if (constraint.is_learned() && !constraint.is_used_as_a_reason()) {
|
|
constraint.MarkForDeletion();
|
|
}
|
|
}
|
|
} else {
|
|
const double limit_activity = activities[num_constraints_to_delete];
|
|
int num_constraint_at_limit_activity = 0;
|
|
for (int i = num_constraints_to_delete; i >= 0; --i) {
|
|
if (activities[i] == limit_activity) {
|
|
++num_constraint_at_limit_activity;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Mark for deletion all the constraints under this threshold.
|
|
// We only keep the most recent constraint amongst the one with the activity
|
|
// exactly equal ot limit_activity, it is why the loop is in the reverse
|
|
// order.
|
|
for (int i = constraints_.size() - 1; i >= 0; --i) {
|
|
UpperBoundedLinearConstraint& constraint = *(constraints_[i].get());
|
|
if (constraint.is_learned() && !constraint.is_used_as_a_reason()) {
|
|
if (constraint.activity() <= limit_activity) {
|
|
if (constraint.activity() == limit_activity &&
|
|
num_constraint_at_limit_activity > 0) {
|
|
--num_constraint_at_limit_activity;
|
|
} else {
|
|
constraint.MarkForDeletion();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally we delete the marked constraint and compute the next limit.
|
|
DeleteConstraintMarkedForDeletion();
|
|
ComputeNewLearnedConstraintLimit();
|
|
}
|
|
|
|
void PbConstraints::BumpActivity(UpperBoundedLinearConstraint* constraint) {
|
|
if (!constraint->is_learned()) return;
|
|
const double max_activity = parameters_->max_clause_activity_value();
|
|
constraint->set_activity(constraint->activity() +
|
|
constraint_activity_increment_);
|
|
if (constraint->activity() > max_activity) {
|
|
RescaleActivities(1.0 / max_activity);
|
|
}
|
|
}
|
|
|
|
void PbConstraints::RescaleActivities(double scaling_factor) {
|
|
constraint_activity_increment_ *= scaling_factor;
|
|
for (int i = 0; i < constraints_.size(); ++i) {
|
|
constraints_[i]->set_activity(constraints_[i]->activity() * scaling_factor);
|
|
}
|
|
}
|
|
|
|
void PbConstraints::UpdateActivityIncrement() {
|
|
const double decay = parameters_->clause_activity_decay();
|
|
constraint_activity_increment_ *= 1.0 / decay;
|
|
}
|
|
|
|
void PbConstraints::DeleteConstraintMarkedForDeletion() {
|
|
util_intops::StrongVector<ConstraintIndex, ConstraintIndex> index_mapping(
|
|
constraints_.size(), ConstraintIndex(-1));
|
|
ConstraintIndex new_index(0);
|
|
for (ConstraintIndex i(0); i < constraints_.size(); ++i) {
|
|
if (!constraints_[i.value()]->is_marked_for_deletion()) {
|
|
index_mapping[i] = new_index;
|
|
if (new_index < i) {
|
|
constraints_[new_index.value()] = std::move(constraints_[i.value()]);
|
|
thresholds_[new_index] = thresholds_[i];
|
|
}
|
|
++new_index;
|
|
} else {
|
|
// Remove it from possible_duplicates_.
|
|
UpperBoundedLinearConstraint* c = constraints_[i.value()].get();
|
|
// We only delete learned constraints, and learned constraints never have
|
|
// enforcement literals.
|
|
CHECK_LT(c->enforcement_id(), 0);
|
|
std::vector<UpperBoundedLinearConstraint*>& ref =
|
|
possible_duplicates_[c->hash()];
|
|
for (int i = 0; i < ref.size(); ++i) {
|
|
if (ref[i] == c) {
|
|
std::swap(ref[i], ref.back());
|
|
ref.pop_back();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
constraints_.resize(new_index.value());
|
|
thresholds_.resize(new_index.value());
|
|
|
|
// This is the slow part, we need to remap all the ConstraintIndex to the
|
|
// new ones.
|
|
for (LiteralIndex lit(0); lit < to_update_.size(); ++lit) {
|
|
std::vector<ConstraintIndexWithCoeff>& updates = to_update_[lit];
|
|
int new_index = 0;
|
|
for (int i = 0; i < updates.size(); ++i) {
|
|
const ConstraintIndex m = index_mapping[updates[i].index];
|
|
if (m != -1) {
|
|
updates[new_index] = updates[i];
|
|
updates[new_index].index = m;
|
|
++new_index;
|
|
}
|
|
}
|
|
updates.resize(new_index);
|
|
}
|
|
}
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|