1888 lines
74 KiB
C++
1888 lines
74 KiB
C++
// Copyright 2010-2022 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/linear_relaxation.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/base/attributes.h"
|
|
#include "absl/container/btree_map.h"
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/meta/type_traits.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/base/stl_util.h"
|
|
#include "ortools/base/strong_vector.h"
|
|
#include "ortools/sat/circuit.h" // for ReindexArcs.
|
|
#include "ortools/sat/clause.h"
|
|
#include "ortools/sat/cp_model.pb.h"
|
|
#include "ortools/sat/cp_model_mapping.h"
|
|
#include "ortools/sat/cp_model_utils.h"
|
|
#include "ortools/sat/cuts.h"
|
|
#include "ortools/sat/diffn_cuts.h"
|
|
#include "ortools/sat/implied_bounds.h"
|
|
#include "ortools/sat/integer.h"
|
|
#include "ortools/sat/integer_expr.h"
|
|
#include "ortools/sat/intervals.h"
|
|
#include "ortools/sat/linear_constraint.h"
|
|
#include "ortools/sat/model.h"
|
|
#include "ortools/sat/precedences.h"
|
|
#include "ortools/sat/presolve_util.h"
|
|
#include "ortools/sat/routing_cuts.h"
|
|
#include "ortools/sat/sat_base.h"
|
|
#include "ortools/sat/sat_parameters.pb.h"
|
|
#include "ortools/sat/sat_solver.h"
|
|
#include "ortools/sat/scheduling_cuts.h"
|
|
#include "ortools/sat/util.h"
|
|
#include "ortools/util/logging.h"
|
|
#include "ortools/util/saturated_arithmetic.h"
|
|
#include "ortools/util/sorted_interval_list.h"
|
|
#include "ortools/util/strong_integers.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
namespace {
|
|
|
|
std::pair<IntegerValue, IntegerValue> GetMinAndMaxNotEncoded(
|
|
IntegerVariable var,
|
|
const absl::flat_hash_set<IntegerValue>& encoded_values,
|
|
const Model& model) {
|
|
CHECK(VariableIsPositive(var));
|
|
const PositiveOnlyIndex index = GetPositiveOnlyIndex(var);
|
|
|
|
const auto* domains = model.Get<IntegerDomains>();
|
|
if (domains == nullptr || index >= domains->size()) {
|
|
return {kMaxIntegerValue, kMinIntegerValue};
|
|
}
|
|
|
|
// The domain can be large, but the list of values shouldn't, so this
|
|
// runs in O(encoded_values.size());
|
|
IntegerValue min = kMaxIntegerValue;
|
|
for (const int64_t v : (*domains)[index].Values()) {
|
|
if (!encoded_values.contains(IntegerValue(v))) {
|
|
min = IntegerValue(v);
|
|
break;
|
|
}
|
|
}
|
|
|
|
IntegerValue max = kMinIntegerValue;
|
|
const Domain negated_domain = (*domains)[index].Negation();
|
|
for (const int64_t v : negated_domain.Values()) {
|
|
if (!encoded_values.contains(IntegerValue(-v))) {
|
|
max = IntegerValue(-v);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {min, max};
|
|
}
|
|
|
|
bool LinMaxContainsOnlyOneVarInExpressions(const ConstraintProto& ct) {
|
|
CHECK_EQ(ct.constraint_case(), ConstraintProto::ConstraintCase::kLinMax);
|
|
int current_var = -1;
|
|
for (const LinearExpressionProto& expr : ct.lin_max().exprs()) {
|
|
if (expr.vars().empty()) continue;
|
|
if (expr.vars().size() > 1) return false;
|
|
const int var = PositiveRef(expr.vars(0));
|
|
if (current_var == -1) {
|
|
current_var = var;
|
|
} else if (var != current_var) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Collect all the affines expressions in a LinMax constraint.
|
|
// It checks that these are indeed affine expressions, and that they all share
|
|
// the same variable.
|
|
// It returns the shared variable, as well as a vector of pairs
|
|
// (coefficient, offset) when each affine is coefficient * shared_var + offset.
|
|
void CollectAffineExpressionWithSingleVariable(
|
|
const ConstraintProto& ct, CpModelMapping* mapping, IntegerVariable* var,
|
|
std::vector<std::pair<IntegerValue, IntegerValue>>* affines) {
|
|
DCHECK(LinMaxContainsOnlyOneVarInExpressions(ct));
|
|
CHECK_EQ(ct.constraint_case(), ConstraintProto::ConstraintCase::kLinMax);
|
|
*var = kNoIntegerVariable;
|
|
affines->clear();
|
|
for (const LinearExpressionProto& expr : ct.lin_max().exprs()) {
|
|
if (expr.vars().empty()) {
|
|
affines->push_back({IntegerValue(0), IntegerValue(expr.offset())});
|
|
} else {
|
|
CHECK_EQ(expr.vars().size(), 1);
|
|
const IntegerVariable affine_var = mapping->Integer(expr.vars(0));
|
|
if (*var == kNoIntegerVariable) {
|
|
*var = PositiveVariable(affine_var);
|
|
}
|
|
if (VariableIsPositive(affine_var)) {
|
|
CHECK_EQ(affine_var, *var);
|
|
affines->push_back(
|
|
{IntegerValue(expr.coeffs(0)), IntegerValue(expr.offset())});
|
|
} else {
|
|
CHECK_EQ(NegationOf(affine_var), *var);
|
|
affines->push_back(
|
|
{IntegerValue(-expr.coeffs(0)), IntegerValue(expr.offset())});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void AppendRelaxationForEqualityEncoding(IntegerVariable var,
|
|
const Model& model,
|
|
LinearRelaxation* relaxation,
|
|
int* num_tight, int* num_loose) {
|
|
const auto* encoder = model.Get<IntegerEncoder>();
|
|
const auto* integer_trail = model.Get<IntegerTrail>();
|
|
if (encoder == nullptr || integer_trail == nullptr) return;
|
|
|
|
std::vector<Literal> at_most_one_ct;
|
|
absl::flat_hash_set<IntegerValue> encoded_values;
|
|
std::vector<ValueLiteralPair> encoding;
|
|
{
|
|
const std::vector<ValueLiteralPair>& initial_encoding =
|
|
encoder->PartialDomainEncoding(var);
|
|
if (initial_encoding.empty()) return;
|
|
for (const auto value_literal : initial_encoding) {
|
|
const Literal literal = value_literal.literal;
|
|
|
|
// Note that we skip pairs that do not have an Integer view.
|
|
if (encoder->GetLiteralView(literal) == kNoIntegerVariable &&
|
|
encoder->GetLiteralView(literal.Negated()) == kNoIntegerVariable) {
|
|
continue;
|
|
}
|
|
|
|
encoding.push_back(value_literal);
|
|
at_most_one_ct.push_back(literal);
|
|
encoded_values.insert(value_literal.value);
|
|
}
|
|
}
|
|
if (encoded_values.empty()) return;
|
|
|
|
// TODO(user): PartialDomainEncoding() filter pair corresponding to literal
|
|
// set to false, however the initial variable Domain is not always updated. As
|
|
// a result, these min/max can be larger than in reality. Try to fix this even
|
|
// if in practice this is a rare occurrence, as the presolve should have
|
|
// propagated most of what we can.
|
|
const auto [min_not_encoded, max_not_encoded] =
|
|
GetMinAndMaxNotEncoded(var, encoded_values, model);
|
|
|
|
// This means that there are no non-encoded value and we have a full encoding.
|
|
// We substract the minimum value to reduce its size.
|
|
if (min_not_encoded == kMaxIntegerValue) {
|
|
const IntegerValue rhs = encoding[0].value;
|
|
LinearConstraintBuilder at_least_one(&model, IntegerValue(1),
|
|
kMaxIntegerValue);
|
|
LinearConstraintBuilder encoding_ct(&model, rhs, rhs);
|
|
encoding_ct.AddTerm(var, IntegerValue(1));
|
|
for (const auto value_literal : encoding) {
|
|
const Literal lit = value_literal.literal;
|
|
CHECK(at_least_one.AddLiteralTerm(lit, IntegerValue(1)));
|
|
|
|
const IntegerValue delta = value_literal.value - rhs;
|
|
if (delta != IntegerValue(0)) {
|
|
CHECK_GE(delta, IntegerValue(0));
|
|
CHECK(encoding_ct.AddLiteralTerm(lit, -delta));
|
|
}
|
|
}
|
|
|
|
// It is possible that the linear1 encoding respect our overflow
|
|
// precondition but not the Var = sum bool * value one. In this case, we
|
|
// just don't encode it this way. Hopefully, most normal model will not run
|
|
// into this.
|
|
const LinearConstraint lc = encoding_ct.Build();
|
|
if (!PossibleOverflow(*integer_trail, lc)) {
|
|
relaxation->linear_constraints.push_back(at_least_one.Build());
|
|
relaxation->linear_constraints.push_back(std::move(lc));
|
|
relaxation->at_most_ones.push_back(at_most_one_ct);
|
|
++*num_tight;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// In this special case, the two constraints below can be merged into an
|
|
// equality: var = rhs + sum l_i * (value_i - rhs).
|
|
if (min_not_encoded == max_not_encoded) {
|
|
const IntegerValue rhs = min_not_encoded;
|
|
LinearConstraintBuilder encoding_ct(&model, rhs, rhs);
|
|
encoding_ct.AddTerm(var, IntegerValue(1));
|
|
for (const auto value_literal : encoding) {
|
|
CHECK(encoding_ct.AddLiteralTerm(value_literal.literal,
|
|
rhs - value_literal.value));
|
|
}
|
|
|
|
const LinearConstraint lc = encoding_ct.Build();
|
|
if (!PossibleOverflow(*integer_trail, lc)) {
|
|
relaxation->at_most_ones.push_back(at_most_one_ct);
|
|
relaxation->linear_constraints.push_back(std::move(lc));
|
|
++*num_tight;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// min + sum l_i * (value_i - min) <= var.
|
|
// Note that this might overflow in corner cases, so we need to prevent that.
|
|
const IntegerValue d_min = min_not_encoded;
|
|
LinearConstraintBuilder lower_bound_ct(&model, d_min, kMaxIntegerValue);
|
|
lower_bound_ct.AddTerm(var, IntegerValue(1));
|
|
for (const auto value_literal : encoding) {
|
|
CHECK(lower_bound_ct.AddLiteralTerm(value_literal.literal,
|
|
d_min - value_literal.value));
|
|
}
|
|
const LinearConstraint built_lb_ct = lower_bound_ct.Build();
|
|
if (!PossibleOverflow(*integer_trail, built_lb_ct)) {
|
|
relaxation->linear_constraints.push_back(std::move(built_lb_ct));
|
|
++*num_loose;
|
|
}
|
|
|
|
// var <= max + sum l_i * (value_i - max).
|
|
// Note that this might overflow in corner cases, so we need to prevent that.
|
|
const IntegerValue d_max = max_not_encoded;
|
|
LinearConstraintBuilder upper_bound_ct(&model, kMinIntegerValue, d_max);
|
|
upper_bound_ct.AddTerm(var, IntegerValue(1));
|
|
for (const auto value_literal : encoding) {
|
|
CHECK(upper_bound_ct.AddLiteralTerm(value_literal.literal,
|
|
d_max - value_literal.value));
|
|
}
|
|
const LinearConstraint built_ub_ct = upper_bound_ct.Build();
|
|
if (!PossibleOverflow(*integer_trail, built_ub_ct)) {
|
|
relaxation->linear_constraints.push_back(std::move(built_ub_ct));
|
|
++*num_loose;
|
|
}
|
|
|
|
// Note that empty/trivial constraints will be filtered later.
|
|
relaxation->at_most_ones.push_back(at_most_one_ct);
|
|
}
|
|
|
|
void AppendPartialGreaterThanEncodingRelaxation(IntegerVariable var,
|
|
const Model& model,
|
|
LinearRelaxation* relaxation) {
|
|
const auto* integer_trail = model.Get<IntegerTrail>();
|
|
const auto* encoder = model.Get<IntegerEncoder>();
|
|
if (integer_trail == nullptr || encoder == nullptr) return;
|
|
|
|
const auto& greater_than_encoding = encoder->PartialGreaterThanEncoding(var);
|
|
if (greater_than_encoding.empty()) return;
|
|
|
|
// Start by the var >= side.
|
|
// And also add the implications between used literals.
|
|
{
|
|
IntegerValue prev_used_bound = integer_trail->LowerBound(var);
|
|
LinearConstraintBuilder builder(&model, prev_used_bound, kMaxIntegerValue);
|
|
builder.AddTerm(var, IntegerValue(1));
|
|
LiteralIndex prev_literal_index = kNoLiteralIndex;
|
|
for (const auto entry : greater_than_encoding) {
|
|
if (entry.value <= prev_used_bound) continue;
|
|
|
|
const LiteralIndex literal_index = entry.literal.Index();
|
|
const IntegerValue diff = prev_used_bound - entry.value;
|
|
|
|
// Skip the entry if the literal doesn't have a view.
|
|
if (!builder.AddLiteralTerm(entry.literal, diff)) continue;
|
|
if (prev_literal_index != kNoLiteralIndex) {
|
|
// Add var <= prev_var, which is the same as var + not(prev_var) <= 1
|
|
relaxation->at_most_ones.push_back(
|
|
{Literal(literal_index), Literal(prev_literal_index).Negated()});
|
|
}
|
|
prev_used_bound = entry.value;
|
|
prev_literal_index = literal_index;
|
|
}
|
|
|
|
// Note that by construction, this shouldn't be able to overflow.
|
|
relaxation->linear_constraints.push_back(builder.Build());
|
|
}
|
|
|
|
// Do the same for the var <= side by using NegationOfVar().
|
|
// Note that we do not need to add the implications between literals again.
|
|
{
|
|
IntegerValue prev_used_bound = integer_trail->LowerBound(NegationOf(var));
|
|
LinearConstraintBuilder builder(&model, prev_used_bound, kMaxIntegerValue);
|
|
builder.AddTerm(var, IntegerValue(-1));
|
|
for (const auto entry :
|
|
encoder->PartialGreaterThanEncoding(NegationOf(var))) {
|
|
if (entry.value <= prev_used_bound) continue;
|
|
const IntegerValue diff = prev_used_bound - entry.value;
|
|
|
|
// Skip the entry if the literal doesn't have a view.
|
|
if (!builder.AddLiteralTerm(entry.literal, diff)) continue;
|
|
prev_used_bound = entry.value;
|
|
}
|
|
|
|
// Note that by construction, this shouldn't be able to overflow.
|
|
relaxation->linear_constraints.push_back(builder.Build());
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool AllLiteralsHaveViews(const IntegerEncoder& encoder,
|
|
const std::vector<Literal>& literals) {
|
|
for (const Literal lit : literals) {
|
|
if (!encoder.LiteralOrNegationHasView(lit)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void AppendBoolOrRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
LinearConstraintBuilder lc(model, IntegerValue(1), kMaxIntegerValue);
|
|
for (const int enforcement_ref : ct.enforcement_literal()) {
|
|
CHECK(lc.AddLiteralTerm(mapping->Literal(NegatedRef(enforcement_ref)),
|
|
IntegerValue(1)));
|
|
}
|
|
for (const int ref : ct.bool_or().literals()) {
|
|
CHECK(lc.AddLiteralTerm(mapping->Literal(ref), IntegerValue(1)));
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
|
|
void AppendBoolAndRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation,
|
|
ActivityBoundHelper* activity_helper) {
|
|
if (!HasEnforcementLiteral(ct)) return;
|
|
|
|
// TODO(user): These constraints can be many, and if they are not regrouped
|
|
// in big at most ones, then they should probably only added lazily as cuts.
|
|
// Regroup this with future clique-cut separation logic.
|
|
//
|
|
// Note that for the case with only one enforcement, what we do below is
|
|
// already done by the clique merging code.
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
if (ct.enforcement_literal().size() == 1) {
|
|
const Literal enforcement = mapping->Literal(ct.enforcement_literal(0));
|
|
for (const int ref : ct.bool_and().literals()) {
|
|
relaxation->at_most_ones.push_back(
|
|
{enforcement, mapping->Literal(ref).Negated()});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If we have many_literals => many_fixed literal, it is important to
|
|
// try to use a tight big-M if we can. This is important on neos-957323.pb.gz
|
|
// for instance.
|
|
//
|
|
// We split the literal into disjoint AMO and we encode each with
|
|
// sum Not(literals) <= sum Not(enforcement)
|
|
//
|
|
// Note that what we actually do is use the decomposition into at most one
|
|
// and add a constraint for each part rather than just adding the sum of them.
|
|
//
|
|
// TODO(user): More generally, do not miss the same structure if the bool_and
|
|
// was expanded into many clauses!
|
|
//
|
|
// TODO(user): It is not 100% clear that just not adding one constraint is
|
|
// worse. Relaxation is worse, but then we have less constraint.
|
|
LinearConstraintBuilder builder(model);
|
|
if (activity_helper != nullptr) {
|
|
std::vector<int> negated_lits;
|
|
for (const int ref : ct.bool_and().literals()) {
|
|
negated_lits.push_back(NegatedRef(ref));
|
|
}
|
|
for (absl::Span<const int> part :
|
|
activity_helper->PartitionLiteralsIntoAmo(negated_lits)) {
|
|
builder.Clear();
|
|
for (const int negated_ref : part) {
|
|
CHECK(builder.AddLiteralTerm(mapping->Literal(negated_ref)));
|
|
}
|
|
for (const int enforcement_ref : ct.enforcement_literal()) {
|
|
CHECK(builder.AddLiteralTerm(
|
|
mapping->Literal(NegatedRef(enforcement_ref)), IntegerValue(-1)));
|
|
}
|
|
relaxation->linear_constraints.push_back(
|
|
builder.BuildConstraint(kMinIntegerValue, IntegerValue(0)));
|
|
}
|
|
} else {
|
|
for (const int ref : ct.bool_and().literals()) {
|
|
builder.Clear();
|
|
CHECK(builder.AddLiteralTerm(mapping->Literal(NegatedRef(ref))));
|
|
for (const int enforcement_ref : ct.enforcement_literal()) {
|
|
CHECK(builder.AddLiteralTerm(
|
|
mapping->Literal(NegatedRef(enforcement_ref)), IntegerValue(-1)));
|
|
}
|
|
relaxation->linear_constraints.push_back(
|
|
builder.BuildConstraint(kMinIntegerValue, IntegerValue(0)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void AppendAtMostOneRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
relaxation->at_most_ones.push_back(
|
|
mapping->Literals(ct.at_most_one().literals()));
|
|
}
|
|
|
|
void AppendExactlyOneRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
auto* encoder = model->GetOrCreate<IntegerEncoder>();
|
|
|
|
const std::vector<Literal> literals =
|
|
mapping->Literals(ct.exactly_one().literals());
|
|
if (AllLiteralsHaveViews(*encoder, literals)) {
|
|
LinearConstraintBuilder lc(model, IntegerValue(1), IntegerValue(1));
|
|
for (const Literal lit : literals) {
|
|
CHECK(lc.AddLiteralTerm(lit, IntegerValue(1)));
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
} else {
|
|
// We just encode the at most one part that might be partially linearized
|
|
// later.
|
|
relaxation->at_most_ones.push_back(literals);
|
|
}
|
|
}
|
|
|
|
std::vector<Literal> CreateAlternativeLiteralsWithView(
|
|
int num_literals, Model* model, LinearRelaxation* relaxation) {
|
|
auto* encoder = model->GetOrCreate<IntegerEncoder>();
|
|
|
|
if (num_literals == 1) {
|
|
// This is not supposed to happen, but it is easy enough to cover, just
|
|
// in case. We might however want to use encoder->GetTrueLiteral().
|
|
const IntegerVariable var = model->Add(NewIntegerVariable(1, 1));
|
|
const Literal lit =
|
|
encoder->GetOrCreateLiteralAssociatedToEquality(var, IntegerValue(1));
|
|
return {lit};
|
|
}
|
|
|
|
if (num_literals == 2) {
|
|
const IntegerVariable var = model->Add(NewIntegerVariable(0, 1));
|
|
const Literal lit =
|
|
encoder->GetOrCreateLiteralAssociatedToEquality(var, IntegerValue(1));
|
|
|
|
// TODO(user): We shouldn't need to create this view ideally. Even better,
|
|
// we should be able to handle Literal natively in the linear relaxation,
|
|
// but that is a lot of work.
|
|
const IntegerVariable var2 = model->Add(NewIntegerVariable(0, 1));
|
|
encoder->AssociateToIntegerEqualValue(lit.Negated(), var2, IntegerValue(1));
|
|
|
|
return {lit, lit.Negated()};
|
|
}
|
|
|
|
std::vector<Literal> literals;
|
|
LinearConstraintBuilder lc_builder(model, IntegerValue(1), IntegerValue(1));
|
|
for (int i = 0; i < num_literals; ++i) {
|
|
const IntegerVariable var = model->Add(NewIntegerVariable(0, 1));
|
|
const Literal lit =
|
|
encoder->GetOrCreateLiteralAssociatedToEquality(var, IntegerValue(1));
|
|
literals.push_back(lit);
|
|
CHECK(lc_builder.AddLiteralTerm(lit, IntegerValue(1)));
|
|
}
|
|
model->Add(ExactlyOneConstraint(literals));
|
|
relaxation->linear_constraints.push_back(lc_builder.Build());
|
|
return literals;
|
|
}
|
|
|
|
void AppendCircuitRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
const int num_arcs = ct.circuit().literals_size();
|
|
CHECK_EQ(num_arcs, ct.circuit().tails_size());
|
|
CHECK_EQ(num_arcs, ct.circuit().heads_size());
|
|
|
|
// Each node must have exactly one incoming and one outgoing arc (note
|
|
// that it can be the unique self-arc of this node too).
|
|
absl::btree_map<int, std::vector<Literal>> incoming_arc_constraints;
|
|
absl::btree_map<int, std::vector<Literal>> outgoing_arc_constraints;
|
|
for (int i = 0; i < num_arcs; i++) {
|
|
const Literal arc = mapping->Literal(ct.circuit().literals(i));
|
|
const int tail = ct.circuit().tails(i);
|
|
const int head = ct.circuit().heads(i);
|
|
|
|
// Make sure this literal has a view.
|
|
model->Add(NewIntegerVariableFromLiteral(arc));
|
|
outgoing_arc_constraints[tail].push_back(arc);
|
|
incoming_arc_constraints[head].push_back(arc);
|
|
}
|
|
for (const auto* node_map :
|
|
{&outgoing_arc_constraints, &incoming_arc_constraints}) {
|
|
for (const auto& entry : *node_map) {
|
|
const std::vector<Literal>& exactly_one = entry.second;
|
|
if (exactly_one.size() > 1) {
|
|
LinearConstraintBuilder at_least_one_lc(model, IntegerValue(1),
|
|
kMaxIntegerValue);
|
|
for (const Literal l : exactly_one) {
|
|
CHECK(at_least_one_lc.AddLiteralTerm(l, IntegerValue(1)));
|
|
}
|
|
|
|
// We separate the two constraints.
|
|
relaxation->at_most_ones.push_back(exactly_one);
|
|
relaxation->linear_constraints.push_back(at_least_one_lc.Build());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AppendRoutesRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
const int num_arcs = ct.routes().literals_size();
|
|
CHECK_EQ(num_arcs, ct.routes().tails_size());
|
|
CHECK_EQ(num_arcs, ct.routes().heads_size());
|
|
|
|
// Each node except node zero must have exactly one incoming and one outgoing
|
|
// arc (note that it can be the unique self-arc of this node too). For node
|
|
// zero, the number of incoming arcs should be the same as the number of
|
|
// outgoing arcs.
|
|
absl::btree_map<int, std::vector<Literal>> incoming_arc_constraints;
|
|
absl::btree_map<int, std::vector<Literal>> outgoing_arc_constraints;
|
|
for (int i = 0; i < num_arcs; i++) {
|
|
const Literal arc = mapping->Literal(ct.routes().literals(i));
|
|
const int tail = ct.routes().tails(i);
|
|
const int head = ct.routes().heads(i);
|
|
|
|
// Make sure this literal has a view.
|
|
model->Add(NewIntegerVariableFromLiteral(arc));
|
|
outgoing_arc_constraints[tail].push_back(arc);
|
|
incoming_arc_constraints[head].push_back(arc);
|
|
}
|
|
for (const auto* node_map :
|
|
{&outgoing_arc_constraints, &incoming_arc_constraints}) {
|
|
for (const auto& entry : *node_map) {
|
|
if (entry.first == 0) continue;
|
|
const std::vector<Literal>& exactly_one = entry.second;
|
|
if (exactly_one.size() > 1) {
|
|
LinearConstraintBuilder at_least_one_lc(model, IntegerValue(1),
|
|
kMaxIntegerValue);
|
|
for (const Literal l : exactly_one) {
|
|
CHECK(at_least_one_lc.AddLiteralTerm(l, IntegerValue(1)));
|
|
}
|
|
|
|
// We separate the two constraints.
|
|
relaxation->at_most_ones.push_back(exactly_one);
|
|
relaxation->linear_constraints.push_back(at_least_one_lc.Build());
|
|
}
|
|
}
|
|
}
|
|
LinearConstraintBuilder zero_node_balance_lc(model, IntegerValue(0),
|
|
IntegerValue(0));
|
|
for (const Literal& incoming_arc : incoming_arc_constraints[0]) {
|
|
CHECK(zero_node_balance_lc.AddLiteralTerm(incoming_arc, IntegerValue(1)));
|
|
}
|
|
for (const Literal& outgoing_arc : outgoing_arc_constraints[0]) {
|
|
CHECK(zero_node_balance_lc.AddLiteralTerm(outgoing_arc, IntegerValue(-1)));
|
|
}
|
|
relaxation->linear_constraints.push_back(zero_node_balance_lc.Build());
|
|
}
|
|
|
|
void AddCircuitCutGenerator(const ConstraintProto& ct, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
std::vector<int> tails(ct.circuit().tails().begin(),
|
|
ct.circuit().tails().end());
|
|
std::vector<int> heads(ct.circuit().heads().begin(),
|
|
ct.circuit().heads().end());
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
std::vector<Literal> literals = mapping->Literals(ct.circuit().literals());
|
|
const int num_nodes = ReindexArcs(&tails, &heads);
|
|
|
|
relaxation->cut_generators.push_back(CreateStronglyConnectedGraphCutGenerator(
|
|
num_nodes, tails, heads, literals, m));
|
|
}
|
|
|
|
void AddRoutesCutGenerator(const ConstraintProto& ct, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
std::vector<int> tails(ct.routes().tails().begin(),
|
|
ct.routes().tails().end());
|
|
std::vector<int> heads(ct.routes().heads().begin(),
|
|
ct.routes().heads().end());
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
std::vector<Literal> literals = mapping->Literals(ct.routes().literals());
|
|
|
|
int num_nodes = 0;
|
|
for (int i = 0; i < ct.routes().tails_size(); ++i) {
|
|
num_nodes = std::max(num_nodes, 1 + ct.routes().tails(i));
|
|
num_nodes = std::max(num_nodes, 1 + ct.routes().heads(i));
|
|
}
|
|
if (ct.routes().demands().empty() || ct.routes().capacity() == 0) {
|
|
relaxation->cut_generators.push_back(
|
|
CreateStronglyConnectedGraphCutGenerator(num_nodes, tails, heads,
|
|
literals, m));
|
|
} else {
|
|
const std::vector<int64_t> demands(ct.routes().demands().begin(),
|
|
ct.routes().demands().end());
|
|
relaxation->cut_generators.push_back(CreateCVRPCutGenerator(
|
|
num_nodes, tails, heads, literals, demands, ct.routes().capacity(), m));
|
|
}
|
|
}
|
|
|
|
// Scan the intervals of a cumulative/no_overlap constraint, and its capacity (1
|
|
// for the no_overlap). It returns the index of the makespan interval if found,
|
|
// or std::nullopt otherwise.
|
|
//
|
|
// Currently, this requires the capacity to be fixed in order to scan for a
|
|
// makespan interval.
|
|
//
|
|
// The makespan interval has the following property:
|
|
// - its end is fixed at the horizon
|
|
// - it is always present
|
|
// - its demand is the capacity of the cumulative/no_overlap.
|
|
// - its size is > 0.
|
|
//
|
|
// These property ensures that all other intervals ends before the start of
|
|
// the makespan interval.
|
|
std::optional<int> DetectMakespan(
|
|
const std::vector<IntervalVariable>& intervals,
|
|
const std::vector<AffineExpression>& demands,
|
|
const AffineExpression& capacity, Model* model) {
|
|
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
|
IntervalsRepository* repository = model->GetOrCreate<IntervalsRepository>();
|
|
|
|
// TODO(user): Supports variable capacity.
|
|
if (!integer_trail->IsFixed(capacity)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Detect the horizon (max of all end max of all intervals).
|
|
IntegerValue horizon = kMinIntegerValue;
|
|
for (int i = 0; i < intervals.size(); ++i) {
|
|
if (repository->IsAbsent(intervals[i])) continue;
|
|
horizon = std::max(
|
|
horizon, integer_trail->UpperBound(repository->End(intervals[i])));
|
|
}
|
|
|
|
const IntegerValue capacity_value = integer_trail->FixedValue(capacity);
|
|
for (int i = 0; i < intervals.size(); ++i) {
|
|
if (!repository->IsPresent(intervals[i])) continue;
|
|
const AffineExpression& end = repository->End(intervals[i]);
|
|
if (integer_trail->IsFixed(demands[i]) &&
|
|
integer_trail->FixedValue(demands[i]) == capacity_value &&
|
|
integer_trail->IsFixed(end) &&
|
|
integer_trail->FixedValue(end) == horizon &&
|
|
integer_trail->LowerBound(repository->Size(intervals[i])) > 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::optional<AffineExpression> DetectMakespanFromPrecedences(
|
|
const SchedulingConstraintHelper& helper, Model* model) {
|
|
if (helper.NumTasks() == 0) return {};
|
|
|
|
const absl::Span<const AffineExpression> ends = helper.Ends();
|
|
std::vector<IntegerVariable> end_vars;
|
|
for (const AffineExpression& end : ends) {
|
|
// TODO(user): Deal with constant end.
|
|
if (end.var == kNoIntegerVariable) return {};
|
|
if (end.coeff != 1) return {};
|
|
end_vars.push_back(end.var);
|
|
}
|
|
|
|
std::vector<FullIntegerPrecedence> output;
|
|
auto* precedences = model->GetOrCreate<PrecedenceRelations>();
|
|
precedences->ComputeFullPrecedences(end_vars, &output);
|
|
for (const auto& p : output) {
|
|
// TODO(user): What if we have more than one candidate makespan ?
|
|
if (p.indices.size() != ends.size()) continue;
|
|
|
|
// We have a Makespan!
|
|
// We want p.var - min_delta >= max(end).
|
|
IntegerValue min_delta = kMaxIntegerValue;
|
|
for (int i = 0; i < p.indices.size(); ++i) {
|
|
// end_vars[indices[i]] + p.offset[i] <= p.var
|
|
// [interval_end - end_offset][indices[i]] + p.offset[i] <= p.var
|
|
//
|
|
// TODO(user): It is possible this min_delta becomes positive but for a
|
|
// real makespan, we could make sure it is <= 0. This can happen if we
|
|
// have an optional interval because we didn't compute the offset assuming
|
|
// this interval was present. We could potentially fix it if we know that
|
|
// presence_literal => p.var >= end.
|
|
min_delta =
|
|
std::min(min_delta, p.offsets[i] - ends[p.indices[i]].constant);
|
|
}
|
|
VLOG(2) << "Makespan detected >= ends + " << min_delta;
|
|
return AffineExpression(p.var, IntegerValue(1), -min_delta);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void AppendNoOverlapRelaxationAndCutGenerator(const ConstraintProto& ct,
|
|
Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
std::vector<IntervalVariable> intervals =
|
|
mapping->Intervals(ct.no_overlap().intervals());
|
|
const IntegerValue one(1);
|
|
std::vector<AffineExpression> demands(intervals.size(), one);
|
|
IntervalsRepository* repository = model->GetOrCreate<IntervalsRepository>();
|
|
|
|
std::optional<AffineExpression> makespan;
|
|
const std::optional<int> makespan_index =
|
|
DetectMakespan(intervals, demands, /*capacity=*/one, model);
|
|
if (makespan_index.has_value()) {
|
|
makespan = repository->Start(intervals[makespan_index.value()]);
|
|
demands.pop_back(); // the vector is filled with ones.
|
|
intervals.erase(intervals.begin() + makespan_index.value());
|
|
}
|
|
|
|
SchedulingConstraintHelper* helper = repository->GetOrCreateHelper(intervals);
|
|
if (!helper->SynchronizeAndSetTimeDirection(true)) return;
|
|
SchedulingDemandHelper* demands_helper =
|
|
repository->GetOrCreateDemandHelper(helper, demands);
|
|
|
|
if (!makespan.has_value()) {
|
|
makespan = DetectMakespanFromPrecedences(*helper, model);
|
|
}
|
|
|
|
AddCumulativeRelaxation(/*capacity=*/one, helper, demands_helper, makespan,
|
|
model, relaxation);
|
|
if (model->GetOrCreate<SatParameters>()->linearization_level() > 1) {
|
|
AddNoOverlapCutGenerator(helper, makespan, model, relaxation);
|
|
}
|
|
}
|
|
|
|
void AppendCumulativeRelaxationAndCutGenerator(const ConstraintProto& ct,
|
|
Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
std::vector<IntervalVariable> intervals =
|
|
mapping->Intervals(ct.cumulative().intervals());
|
|
std::vector<AffineExpression> demands =
|
|
mapping->Affines(ct.cumulative().demands());
|
|
const AffineExpression capacity = mapping->Affine(ct.cumulative().capacity());
|
|
const std::optional<int> makespan_index =
|
|
DetectMakespan(intervals, demands, capacity, model);
|
|
std::optional<AffineExpression> makespan;
|
|
IntervalsRepository* repository = model->GetOrCreate<IntervalsRepository>();
|
|
if (makespan_index.has_value()) {
|
|
// We remove the makespan data from the intervals the demands vector.
|
|
makespan = repository->Start(intervals[makespan_index.value()]);
|
|
demands.erase(demands.begin() + makespan_index.value());
|
|
intervals.erase(intervals.begin() + makespan_index.value());
|
|
}
|
|
|
|
// We try to linearize the energy of each task (size * demand).
|
|
SchedulingConstraintHelper* helper = repository->GetOrCreateHelper(intervals);
|
|
if (!helper->SynchronizeAndSetTimeDirection(true)) return;
|
|
SchedulingDemandHelper* demands_helper =
|
|
repository->GetOrCreateDemandHelper(helper, demands);
|
|
|
|
if (!makespan.has_value()) {
|
|
makespan = DetectMakespanFromPrecedences(*helper, model);
|
|
}
|
|
|
|
// We can now add the relaxation and the cut generators.
|
|
AddCumulativeRelaxation(capacity, helper, demands_helper, makespan, model,
|
|
relaxation);
|
|
if (model->GetOrCreate<SatParameters>()->linearization_level() > 1) {
|
|
AddCumulativeCutGenerator(capacity, helper, demands_helper, makespan, model,
|
|
relaxation);
|
|
}
|
|
}
|
|
|
|
// This relaxation will compute the bounding box of all tasks in the cumulative,
|
|
// and add the constraint that the sum of energies of each task must fit in the
|
|
// capacity * span area.
|
|
void AddCumulativeRelaxation(const AffineExpression& capacity,
|
|
SchedulingConstraintHelper* helper,
|
|
SchedulingDemandHelper* demands_helper,
|
|
const std::optional<AffineExpression>& makespan,
|
|
Model* model, LinearRelaxation* relaxation) {
|
|
const int num_intervals = helper->NumTasks();
|
|
demands_helper->CacheAllEnergyValues();
|
|
|
|
IntegerValue min_of_starts = kMaxIntegerValue;
|
|
IntegerValue max_of_ends = kMinIntegerValue;
|
|
int num_variable_energies = 0;
|
|
int num_optionals = 0;
|
|
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
|
for (int index = 0; index < num_intervals; ++index) {
|
|
if (helper->IsAbsent(index)) continue;
|
|
if (helper->IsOptional(index) && demands_helper->EnergyMin(index) >= 0) {
|
|
num_optionals++;
|
|
}
|
|
if (!helper->SizeIsFixed(index) || !demands_helper->DemandIsFixed(index)) {
|
|
num_variable_energies++;
|
|
}
|
|
min_of_starts = std::min(min_of_starts, helper->StartMin(index));
|
|
max_of_ends = std::max(max_of_ends, helper->EndMax(index));
|
|
}
|
|
|
|
VLOG(2) << "Span [" << min_of_starts << ".." << max_of_ends << "] with "
|
|
<< num_optionals << " optional intervals, and "
|
|
<< num_variable_energies << " variable energy tasks out of "
|
|
<< num_intervals << " intervals"
|
|
<< (makespan.has_value() ? ", and 1 makespan" : "");
|
|
|
|
// If nothing is variable, the linear relaxation will already be enforced
|
|
// by the scheduling propagators.
|
|
if (num_variable_energies + num_optionals == 0) return;
|
|
|
|
LinearConstraintBuilder lc(model, kMinIntegerValue, IntegerValue(0));
|
|
int num_intervals_added = 0;
|
|
for (int index = 0; index < num_intervals; ++index) {
|
|
if (helper->IsAbsent(index)) continue;
|
|
if (helper->IsOptional(index)) {
|
|
const IntegerValue energy_min = demands_helper->EnergyMin(index);
|
|
if (energy_min == 0) continue;
|
|
if (!lc.AddLiteralTerm(helper->PresenceLiteral(index), energy_min)) {
|
|
return;
|
|
}
|
|
} else {
|
|
const std::vector<LiteralValueValue>& product =
|
|
demands_helper->DecomposedEnergies()[index];
|
|
if (!product.empty()) {
|
|
// The energy is defined if the vector is not empty.
|
|
if (!lc.AddDecomposedProduct(product)) return;
|
|
} else {
|
|
// The energy is not a decomposed product, but it could still be
|
|
// constant or linear. If not, a McCormick relaxation will be
|
|
// introduced. AddQuadraticLowerBound() supports all cases.
|
|
lc.AddQuadraticLowerBound(helper->Sizes()[index],
|
|
demands_helper->Demands()[index],
|
|
integer_trail);
|
|
}
|
|
}
|
|
++num_intervals_added;
|
|
}
|
|
if (num_intervals_added == 0) return;
|
|
|
|
// Create and link span_start and span_end to the starts and ends of the
|
|
// tasks.
|
|
//
|
|
// TODO(user): In some cases, we could have only one task that can be
|
|
// first.
|
|
const AffineExpression span_start = min_of_starts;
|
|
const AffineExpression span_end =
|
|
makespan.has_value() ? makespan.value() : max_of_ends;
|
|
lc.AddTerm(span_end, -integer_trail->UpperBound(capacity));
|
|
lc.AddTerm(span_start, integer_trail->UpperBound(capacity));
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
|
|
// Adds the energetic relaxation sum(areas) <= bounding box area.
|
|
void AppendNoOverlap2dRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
CHECK(ct.has_no_overlap_2d());
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
std::vector<IntervalVariable> x_intervals =
|
|
mapping->Intervals(ct.no_overlap_2d().x_intervals());
|
|
std::vector<IntervalVariable> y_intervals =
|
|
mapping->Intervals(ct.no_overlap_2d().y_intervals());
|
|
|
|
auto* integer_trail = model->GetOrCreate<IntegerTrail>();
|
|
auto* intervals_repository = model->GetOrCreate<IntervalsRepository>();
|
|
|
|
IntegerValue x_min = kMaxIntegerValue;
|
|
IntegerValue x_max = kMinIntegerValue;
|
|
IntegerValue y_min = kMaxIntegerValue;
|
|
IntegerValue y_max = kMinIntegerValue;
|
|
std::vector<AffineExpression> x_sizes;
|
|
std::vector<AffineExpression> y_sizes;
|
|
for (int i = 0; i < ct.no_overlap_2d().x_intervals_size(); ++i) {
|
|
x_sizes.push_back(intervals_repository->Size(x_intervals[i]));
|
|
y_sizes.push_back(intervals_repository->Size(y_intervals[i]));
|
|
x_min = std::min(x_min, integer_trail->LevelZeroLowerBound(
|
|
intervals_repository->Start(x_intervals[i])));
|
|
x_max = std::max(x_max, integer_trail->LevelZeroUpperBound(
|
|
intervals_repository->End(x_intervals[i])));
|
|
y_min = std::min(y_min, integer_trail->LevelZeroLowerBound(
|
|
intervals_repository->Start(y_intervals[i])));
|
|
y_max = std::max(y_max, integer_trail->LevelZeroUpperBound(
|
|
intervals_repository->End(y_intervals[i])));
|
|
}
|
|
|
|
const IntegerValue max_area =
|
|
IntegerValue(CapProd(CapSub(x_max.value(), x_min.value()),
|
|
CapSub(y_max.value(), y_min.value())));
|
|
if (max_area == kMaxIntegerValue) return;
|
|
|
|
auto* product_decomposer = model->GetOrCreate<ProductDecomposer>();
|
|
LinearConstraintBuilder lc(model, IntegerValue(0), max_area);
|
|
for (int i = 0; i < ct.no_overlap_2d().x_intervals_size(); ++i) {
|
|
if (intervals_repository->IsPresent(x_intervals[i]) &&
|
|
intervals_repository->IsPresent(y_intervals[i])) {
|
|
const std::vector<LiteralValueValue> energy =
|
|
product_decomposer->TryToDecompose(x_sizes[i], y_sizes[i]);
|
|
if (!energy.empty()) {
|
|
if (!lc.AddDecomposedProduct(energy)) return;
|
|
} else {
|
|
lc.AddQuadraticLowerBound(x_sizes[i], y_sizes[i], integer_trail);
|
|
}
|
|
} else if (intervals_repository->IsPresent(x_intervals[i]) ||
|
|
intervals_repository->IsPresent(y_intervals[i]) ||
|
|
(intervals_repository->PresenceLiteral(x_intervals[i]) ==
|
|
intervals_repository->PresenceLiteral(y_intervals[i]))) {
|
|
// We have only one active literal.
|
|
const Literal presence_literal =
|
|
intervals_repository->IsPresent(x_intervals[i])
|
|
? intervals_repository->PresenceLiteral(y_intervals[i])
|
|
: intervals_repository->PresenceLiteral(x_intervals[i]);
|
|
const IntegerValue area_min =
|
|
integer_trail->LevelZeroLowerBound(x_sizes[i]) *
|
|
integer_trail->LevelZeroLowerBound(y_sizes[i]);
|
|
if (area_min != 0) {
|
|
// Not including the term if we don't have a view is ok.
|
|
(void)lc.AddLiteralTerm(presence_literal, area_min);
|
|
}
|
|
}
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
|
|
void AppendLinMaxRelaxationPart1(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
|
|
// We want to linearize target = max(exprs[1], exprs[2], ..., exprs[d]).
|
|
// Part 1: Encode target >= max(exprs[1], exprs[2], ..., exprs[d])
|
|
const LinearExpression negated_target =
|
|
NegationOf(mapping->GetExprFromProto(ct.lin_max().target()));
|
|
for (int i = 0; i < ct.lin_max().exprs_size(); ++i) {
|
|
const LinearExpression expr =
|
|
mapping->GetExprFromProto(ct.lin_max().exprs(i));
|
|
LinearConstraintBuilder lc(model, kMinIntegerValue, IntegerValue(0));
|
|
lc.AddLinearExpression(negated_target);
|
|
lc.AddLinearExpression(expr);
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
}
|
|
|
|
// TODO(user): experiment with:
|
|
// 1) remove this code
|
|
// 2) keep this code
|
|
// 3) remove this code and create the cut generator at level 1.
|
|
void AppendMaxAffineRelaxation(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
IntegerVariable var;
|
|
std::vector<std::pair<IntegerValue, IntegerValue>> affines;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
CollectAffineExpressionWithSingleVariable(ct, mapping, &var, &affines);
|
|
if (var == kNoIntegerVariable ||
|
|
model->GetOrCreate<IntegerTrail>()->IsFixed(var)) {
|
|
return;
|
|
}
|
|
|
|
CHECK(VariableIsPositive(var));
|
|
const LinearExpression target_expr =
|
|
PositiveVarExpr(mapping->GetExprFromProto(ct.lin_max().target()));
|
|
LinearConstraintBuilder builder(model);
|
|
if (BuildMaxAffineUpConstraint(target_expr, var, affines, model, &builder)) {
|
|
relaxation->linear_constraints.push_back(builder.Build());
|
|
}
|
|
}
|
|
|
|
void AddMaxAffineCutGenerator(const ConstraintProto& ct, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
IntegerVariable var;
|
|
std::vector<std::pair<IntegerValue, IntegerValue>> affines;
|
|
auto* mapping = model->GetOrCreate<CpModelMapping>();
|
|
CollectAffineExpressionWithSingleVariable(ct, mapping, &var, &affines);
|
|
if (var == kNoIntegerVariable ||
|
|
model->GetOrCreate<IntegerTrail>()->IsFixed(var)) {
|
|
return;
|
|
}
|
|
|
|
// If the target is constant, propagation is enough.
|
|
if (ct.lin_max().target().vars().empty()) return;
|
|
|
|
const LinearExpression target_expr =
|
|
PositiveVarExpr(mapping->GetExprFromProto(ct.lin_max().target()));
|
|
relaxation->cut_generators.push_back(CreateMaxAffineCutGenerator(
|
|
target_expr, var, affines, "AffineMax", model));
|
|
}
|
|
|
|
// Part 2: Encode upper bound on X.
|
|
//
|
|
// Add linking constraint to the CP solver
|
|
// sum zi = 1 and for all i, zi => max = expr_i.
|
|
void AppendLinMaxRelaxationPart2(
|
|
IntegerVariable target, const std::vector<Literal>& alternative_literals,
|
|
const std::vector<LinearExpression>& exprs, Model* model,
|
|
LinearRelaxation* relaxation) {
|
|
const int num_exprs = exprs.size();
|
|
GenericLiteralWatcher* watcher = model->GetOrCreate<GenericLiteralWatcher>();
|
|
|
|
// First add the CP constraints.
|
|
for (int i = 0; i < num_exprs; ++i) {
|
|
LinearExpression local_expr;
|
|
local_expr.vars = NegationOf(exprs[i].vars);
|
|
local_expr.vars.push_back(target);
|
|
local_expr.coeffs = exprs[i].coeffs;
|
|
local_expr.coeffs.push_back(IntegerValue(1));
|
|
IntegerSumLE* upper_bound =
|
|
new IntegerSumLE({alternative_literals[i]}, local_expr.vars,
|
|
local_expr.coeffs, exprs[i].offset, model);
|
|
upper_bound->RegisterWith(watcher);
|
|
model->TakeOwnership(upper_bound);
|
|
}
|
|
|
|
// For the relaxation, we use different constraints with a stronger linear
|
|
// relaxation as explained in the .h
|
|
std::vector<std::vector<IntegerValue>> sum_of_max_corner_diff(
|
|
num_exprs, std::vector<IntegerValue>(num_exprs, IntegerValue(0)));
|
|
|
|
// Cache coefficients.
|
|
// TODO(user): Remove hash_map ?
|
|
absl::flat_hash_map<std::pair<int, IntegerVariable>, IntegerValue> cache;
|
|
for (int i = 0; i < num_exprs; ++i) {
|
|
for (int j = 0; j < exprs[i].vars.size(); ++j) {
|
|
cache[std::make_pair(i, exprs[i].vars[j])] = exprs[i].coeffs[j];
|
|
}
|
|
}
|
|
const auto get_coeff = [&cache](IntegerVariable var, int index) {
|
|
const auto it = cache.find(std::make_pair(index, var));
|
|
if (it == cache.end()) return IntegerValue(0);
|
|
return it->second;
|
|
};
|
|
|
|
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
|
std::vector<IntegerVariable> active_vars;
|
|
for (int i = 0; i + 1 < num_exprs; ++i) {
|
|
for (int j = i + 1; j < num_exprs; ++j) {
|
|
active_vars = exprs[i].vars;
|
|
active_vars.insert(active_vars.end(), exprs[j].vars.begin(),
|
|
exprs[j].vars.end());
|
|
gtl::STLSortAndRemoveDuplicates(&active_vars);
|
|
for (const IntegerVariable x_var : active_vars) {
|
|
const IntegerValue diff = get_coeff(x_var, j) - get_coeff(x_var, i);
|
|
if (diff == 0) continue;
|
|
|
|
const IntegerValue lb = integer_trail->LevelZeroLowerBound(x_var);
|
|
const IntegerValue ub = integer_trail->LevelZeroUpperBound(x_var);
|
|
sum_of_max_corner_diff[i][j] += std::max(diff * lb, diff * ub);
|
|
sum_of_max_corner_diff[j][i] += std::max(-diff * lb, -diff * ub);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < num_exprs; ++i) {
|
|
LinearConstraintBuilder lc(model, kMinIntegerValue, IntegerValue(0));
|
|
lc.AddTerm(target, IntegerValue(1));
|
|
for (int j = 0; j < exprs[i].vars.size(); ++j) {
|
|
lc.AddTerm(exprs[i].vars[j], -exprs[i].coeffs[j]);
|
|
}
|
|
for (int j = 0; j < num_exprs; ++j) {
|
|
CHECK(lc.AddLiteralTerm(alternative_literals[j],
|
|
-exprs[j].offset - sum_of_max_corner_diff[i][j]));
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
}
|
|
|
|
void AppendLinearConstraintRelaxation(const ConstraintProto& ct,
|
|
bool linearize_enforced_constraints,
|
|
Model* model,
|
|
LinearRelaxation* relaxation,
|
|
ActivityBoundHelper* activity_helper) {
|
|
auto* mapping = model->Get<CpModelMapping>();
|
|
|
|
// Note that we ignore the holes in the domain.
|
|
//
|
|
// TODO(user): In LoadLinearConstraint() we already created intermediate
|
|
// Booleans for each disjoint interval, we should reuse them here if
|
|
// possible.
|
|
//
|
|
// TODO(user): process the "at most one" part of a == 1 separately?
|
|
const IntegerValue rhs_domain_min = IntegerValue(ct.linear().domain(0));
|
|
const IntegerValue rhs_domain_max =
|
|
IntegerValue(ct.linear().domain(ct.linear().domain_size() - 1));
|
|
if (rhs_domain_min == std::numeric_limits<int64_t>::min() &&
|
|
rhs_domain_max == std::numeric_limits<int64_t>::max())
|
|
return;
|
|
|
|
if (!HasEnforcementLiteral(ct)) {
|
|
LinearConstraintBuilder lc(model, rhs_domain_min, rhs_domain_max);
|
|
for (int i = 0; i < ct.linear().vars_size(); i++) {
|
|
const int ref = ct.linear().vars(i);
|
|
const int64_t coeff = ct.linear().coeffs(i);
|
|
lc.AddTerm(mapping->Integer(ref), IntegerValue(coeff));
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
return;
|
|
}
|
|
|
|
// Reified version.
|
|
if (!linearize_enforced_constraints) return;
|
|
|
|
// We linearize fully reified constraints of size 1 all together for a given
|
|
// variable. But we need to process half-reified ones.
|
|
if (!mapping->IsHalfEncodingConstraint(&ct) && ct.linear().vars_size() <= 1) {
|
|
return;
|
|
}
|
|
|
|
std::vector<Literal> enforcing_literals;
|
|
enforcing_literals.reserve(ct.enforcement_literal_size());
|
|
for (const int enforcement_ref : ct.enforcement_literal()) {
|
|
enforcing_literals.push_back(mapping->Literal(enforcement_ref));
|
|
}
|
|
|
|
// Compute min/max activity.
|
|
std::vector<std::pair<int, int64_t>> bool_terms;
|
|
IntegerValue min_activity(0);
|
|
IntegerValue max_activity(0);
|
|
const auto integer_trail = model->GetOrCreate<IntegerTrail>();
|
|
for (int i = 0; i < ct.linear().vars_size(); i++) {
|
|
const int ref = ct.linear().vars(i);
|
|
const IntegerValue coeff(ct.linear().coeffs(i));
|
|
const IntegerVariable int_var = mapping->Integer(ref);
|
|
|
|
// Everything here should have a view.
|
|
CHECK_NE(int_var, kNoIntegerVariable);
|
|
|
|
const IntegerValue lb = integer_trail->LowerBound(int_var);
|
|
const IntegerValue ub = integer_trail->UpperBound(int_var);
|
|
if (lb == 0 && ub == 1 && activity_helper != nullptr) {
|
|
bool_terms.push_back({ref, coeff.value()});
|
|
} else {
|
|
if (coeff > 0) {
|
|
min_activity += coeff * lb;
|
|
max_activity += coeff * ub;
|
|
} else {
|
|
min_activity += coeff * ub;
|
|
max_activity += coeff * lb;
|
|
}
|
|
}
|
|
}
|
|
if (activity_helper != nullptr) {
|
|
min_activity +=
|
|
IntegerValue(activity_helper->ComputeMinActivity(bool_terms));
|
|
max_activity +=
|
|
IntegerValue(activity_helper->ComputeMaxActivity(bool_terms));
|
|
}
|
|
|
|
if (rhs_domain_min > min_activity) {
|
|
// And(ei) => terms >= rhs_domain_min
|
|
// <=> Sum_i (~ei * (rhs_domain_min - min_activity)) + terms >=
|
|
// rhs_domain_min
|
|
LinearConstraintBuilder lc(model, rhs_domain_min, kMaxIntegerValue);
|
|
for (const Literal& literal : enforcing_literals) {
|
|
CHECK(
|
|
lc.AddLiteralTerm(literal.Negated(), rhs_domain_min - min_activity));
|
|
}
|
|
for (int i = 0; i < ct.linear().vars_size(); i++) {
|
|
const int ref = ct.linear().vars(i);
|
|
const IntegerValue coeff(ct.linear().coeffs(i));
|
|
const IntegerVariable int_var = mapping->Integer(ref);
|
|
lc.AddTerm(int_var, coeff);
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
if (rhs_domain_max < max_activity) {
|
|
// And(ei) => terms <= rhs_domain_max
|
|
// <=> Sum_i (~ei * (rhs_domain_max - max_activity)) + terms <=
|
|
// rhs_domain_max
|
|
LinearConstraintBuilder lc(model, kMinIntegerValue, rhs_domain_max);
|
|
for (const Literal& literal : enforcing_literals) {
|
|
CHECK(
|
|
lc.AddLiteralTerm(literal.Negated(), rhs_domain_max - max_activity));
|
|
}
|
|
for (int i = 0; i < ct.linear().vars_size(); i++) {
|
|
const int ref = ct.linear().vars(i);
|
|
const IntegerValue coeff(ct.linear().coeffs(i));
|
|
const IntegerVariable int_var = mapping->Integer(ref);
|
|
lc.AddTerm(int_var, coeff);
|
|
}
|
|
relaxation->linear_constraints.push_back(lc.Build());
|
|
}
|
|
}
|
|
|
|
// Add a static and a dynamic linear relaxation of the CP constraint to the set
|
|
// of linear constraints. The highest linearization_level is, the more types of
|
|
// constraint we encode. This method should be called only for
|
|
// linearization_level > 0. The static part is just called a relaxation and is
|
|
// called at the root node of the search. The dynamic part is implemented
|
|
// through a set of linear cut generators that will be called throughout the
|
|
// search.
|
|
//
|
|
// TODO(user): In full generality, we could encode all the constraint as an LP.
|
|
// TODO(user): Add unit tests for this method.
|
|
// TODO(user): Remove and merge with model loading.
|
|
void TryToLinearizeConstraint(const CpModelProto& /*model_proto*/,
|
|
const ConstraintProto& ct,
|
|
int linearization_level, Model* model,
|
|
LinearRelaxation* relaxation,
|
|
ActivityBoundHelper* activity_helper) {
|
|
CHECK_EQ(model->GetOrCreate<SatSolver>()->CurrentDecisionLevel(), 0);
|
|
DCHECK_GT(linearization_level, 0);
|
|
|
|
const int old_index = relaxation->linear_constraints.size();
|
|
switch (ct.constraint_case()) {
|
|
case ConstraintProto::ConstraintCase::kBoolOr: {
|
|
if (linearization_level > 1) {
|
|
AppendBoolOrRelaxation(ct, model, relaxation);
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kBoolAnd: {
|
|
if (linearization_level > 1) {
|
|
AppendBoolAndRelaxation(ct, model, relaxation, activity_helper);
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kAtMostOne: {
|
|
AppendAtMostOneRelaxation(ct, model, relaxation);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kExactlyOne: {
|
|
AppendExactlyOneRelaxation(ct, model, relaxation);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kIntProd: {
|
|
const LinearArgumentProto& int_prod = ct.int_prod();
|
|
if (int_prod.exprs_size() == 2 &&
|
|
LinearExpressionProtosAreEqual(int_prod.exprs(0),
|
|
int_prod.exprs(1))) {
|
|
AppendSquareRelaxation(ct, model, relaxation);
|
|
AddSquareCutGenerator(ct, linearization_level, model, relaxation);
|
|
} else {
|
|
// No relaxation, just a cut generator .
|
|
AddIntProdCutGenerator(ct, linearization_level, model, relaxation);
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kLinMax: {
|
|
AppendLinMaxRelaxationPart1(ct, model, relaxation);
|
|
const bool is_affine_max = LinMaxContainsOnlyOneVarInExpressions(ct);
|
|
if (is_affine_max) {
|
|
AppendMaxAffineRelaxation(ct, model, relaxation);
|
|
}
|
|
|
|
// Add cut generators.
|
|
if (linearization_level > 1) {
|
|
if (is_affine_max) {
|
|
AddMaxAffineCutGenerator(ct, model, relaxation);
|
|
} else if (ct.lin_max().exprs().size() < 100) {
|
|
AddLinMaxCutGenerator(ct, model, relaxation);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kAllDiff: {
|
|
AddAllDiffRelaxationAndCutGenerator(ct, linearization_level, model,
|
|
relaxation);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kLinear: {
|
|
AppendLinearConstraintRelaxation(
|
|
ct, /*linearize_enforced_constraints=*/linearization_level > 1, model,
|
|
relaxation, activity_helper);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kCircuit: {
|
|
AppendCircuitRelaxation(ct, model, relaxation);
|
|
if (linearization_level > 1) {
|
|
AddCircuitCutGenerator(ct, model, relaxation);
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kRoutes: {
|
|
AppendRoutesRelaxation(ct, model, relaxation);
|
|
if (linearization_level > 1) {
|
|
AddRoutesCutGenerator(ct, model, relaxation);
|
|
}
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kNoOverlap: {
|
|
AppendNoOverlapRelaxationAndCutGenerator(ct, model, relaxation);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kCumulative: {
|
|
AppendCumulativeRelaxationAndCutGenerator(ct, model, relaxation);
|
|
break;
|
|
}
|
|
case ConstraintProto::ConstraintCase::kNoOverlap2D: {
|
|
// TODO(user): Use the same pattern as the other 2 scheduling methods:
|
|
// - single function
|
|
// - generate helpers once
|
|
//
|
|
// Adds an energetic relaxation (sum of areas fits in bounding box).
|
|
AppendNoOverlap2dRelaxation(ct, model, relaxation);
|
|
if (linearization_level > 1) {
|
|
// Adds a completion time cut generator and an energetic cut generator.
|
|
AddNoOverlap2dCutGenerator(ct, model, relaxation);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
|
|
if (DEBUG_MODE) {
|
|
const auto& integer_trail = *model->GetOrCreate<IntegerTrail>();
|
|
for (int i = old_index; i < relaxation->linear_constraints.size(); ++i) {
|
|
const bool issue =
|
|
PossibleOverflow(integer_trail, relaxation->linear_constraints[i]);
|
|
if (issue) {
|
|
LOG(INFO) << "Possible overflow in linearization of: "
|
|
<< ct.ShortDebugString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cut generators.
|
|
|
|
void AddIntProdCutGenerator(const ConstraintProto& ct, int linearization_level,
|
|
Model* m, LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
if (ct.int_prod().exprs_size() != 2) return;
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
|
|
// Constraint is z == x * y.
|
|
AffineExpression z = mapping->Affine(ct.int_prod().target());
|
|
AffineExpression x = mapping->Affine(ct.int_prod().exprs(0));
|
|
AffineExpression y = mapping->Affine(ct.int_prod().exprs(1));
|
|
|
|
IntegerTrail* const integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
IntegerValue x_lb = integer_trail->LowerBound(x);
|
|
IntegerValue x_ub = integer_trail->UpperBound(x);
|
|
IntegerValue y_lb = integer_trail->LowerBound(y);
|
|
IntegerValue y_ub = integer_trail->UpperBound(y);
|
|
|
|
// We currently only support variables with non-negative domains.
|
|
if (x_lb < 0 && x_ub > 0) return;
|
|
if (y_lb < 0 && y_ub > 0) return;
|
|
|
|
// Change signs to return to the case where all variables are a domain
|
|
// with non negative values only.
|
|
if (x_ub <= 0) {
|
|
x = x.Negated();
|
|
z = z.Negated();
|
|
}
|
|
if (y_ub <= 0) {
|
|
y = y.Negated();
|
|
z = z.Negated();
|
|
}
|
|
|
|
relaxation->cut_generators.push_back(CreatePositiveMultiplicationCutGenerator(
|
|
z, x, y, linearization_level, m));
|
|
}
|
|
|
|
void AppendSquareRelaxation(const ConstraintProto& ct, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
IntegerTrail* const integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
|
|
// Constraint is square == x * x.
|
|
AffineExpression square = mapping->Affine(ct.int_prod().target());
|
|
AffineExpression x = mapping->Affine(ct.int_prod().exprs(0));
|
|
IntegerValue x_lb = integer_trail->LowerBound(x);
|
|
IntegerValue x_ub = integer_trail->UpperBound(x);
|
|
|
|
if (x_lb == x_ub) return;
|
|
|
|
// We currently only support variables with non-negative domains.
|
|
if (x_lb < 0 && x_ub > 0) return;
|
|
|
|
// Change the sigh of x if its domain is non-positive.
|
|
if (x_ub <= 0) {
|
|
x = x.Negated();
|
|
const IntegerValue tmp = x_ub;
|
|
x_ub = -x_lb;
|
|
x_lb = -tmp;
|
|
}
|
|
|
|
// Check for potential overflows.
|
|
if (x_ub > (int64_t{1} << 31)) return;
|
|
DCHECK_GE(x_lb, 0);
|
|
|
|
relaxation->linear_constraints.push_back(
|
|
ComputeHyperplanAboveSquare(x, square, x_lb, x_ub, m));
|
|
|
|
relaxation->linear_constraints.push_back(
|
|
ComputeHyperplanBelowSquare(x, square, x_lb, m));
|
|
// TODO(user): We could add all or some below_hyperplans.
|
|
if (x_lb + 1 < x_ub) {
|
|
// The hyperplan will use x_ub - 1 and x_ub.
|
|
relaxation->linear_constraints.push_back(
|
|
ComputeHyperplanBelowSquare(x, square, x_ub - 1, m));
|
|
}
|
|
}
|
|
|
|
void AddSquareCutGenerator(const ConstraintProto& ct, int linearization_level,
|
|
Model* m, LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
IntegerTrail* const integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
|
|
// Constraint is square == x * x.
|
|
const AffineExpression square = mapping->Affine(ct.int_prod().target());
|
|
AffineExpression x = mapping->Affine(ct.int_prod().exprs(0));
|
|
const IntegerValue x_lb = integer_trail->LowerBound(x);
|
|
const IntegerValue x_ub = integer_trail->UpperBound(x);
|
|
|
|
// We currently only support variables with non-negative domains.
|
|
if (x_lb < 0 && x_ub > 0) return;
|
|
|
|
// Change the sigh of x if its domain is non-positive.
|
|
if (x_ub <= 0) {
|
|
x = x.Negated();
|
|
}
|
|
|
|
relaxation->cut_generators.push_back(
|
|
CreateSquareCutGenerator(square, x, linearization_level, m));
|
|
}
|
|
|
|
void AddAllDiffRelaxationAndCutGenerator(const ConstraintProto& ct,
|
|
int linearization_level, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
auto* integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
const int num_exprs = ct.all_diff().exprs_size();
|
|
|
|
const std::vector<AffineExpression> exprs =
|
|
mapping->Affines(ct.all_diff().exprs());
|
|
|
|
// Build union of affine expressions domains to check if this is a
|
|
// permutation.
|
|
Domain union_of_domains;
|
|
for (const AffineExpression& expr : exprs) {
|
|
if (integer_trail->IsFixed(expr)) {
|
|
union_of_domains = union_of_domains.UnionWith(
|
|
Domain(integer_trail->FixedValue(expr).value()));
|
|
} else {
|
|
union_of_domains = union_of_domains.UnionWith(
|
|
integer_trail->InitialVariableDomain(expr.var)
|
|
.MultiplicationBy(expr.coeff.value())
|
|
.AdditionWith(Domain(expr.constant.value())));
|
|
}
|
|
}
|
|
|
|
if (union_of_domains.Size() == num_exprs) {
|
|
// In case of a permutation, the linear constraint is tight.
|
|
int64_t sum_of_values = 0;
|
|
for (const int64_t v : union_of_domains.Values()) {
|
|
sum_of_values += v;
|
|
}
|
|
LinearConstraintBuilder relax(m, sum_of_values, sum_of_values);
|
|
for (const AffineExpression& expr : exprs) {
|
|
relax.AddTerm(expr, 1);
|
|
}
|
|
relaxation->linear_constraints.push_back(relax.Build());
|
|
} else if (num_exprs <=
|
|
m->GetOrCreate<SatParameters>()->max_all_diff_cut_size() &&
|
|
linearization_level > 1) {
|
|
relaxation->cut_generators.push_back(
|
|
CreateAllDifferentCutGenerator(exprs, m));
|
|
}
|
|
}
|
|
|
|
bool IntervalIsVariable(const IntervalVariable interval,
|
|
IntervalsRepository* intervals_repository) {
|
|
// Ignore absent rectangles.
|
|
if (intervals_repository->IsAbsent(interval)) {
|
|
return false;
|
|
}
|
|
|
|
// Checks non-present intervals.
|
|
if (!intervals_repository->IsPresent(interval)) {
|
|
return true;
|
|
}
|
|
|
|
// Checks variable sized intervals.
|
|
if (intervals_repository->MinSize(interval) !=
|
|
intervals_repository->MaxSize(interval)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void AddCumulativeCutGenerator(const AffineExpression& capacity,
|
|
SchedulingConstraintHelper* helper,
|
|
SchedulingDemandHelper* demands_helper,
|
|
const std::optional<AffineExpression>& makespan,
|
|
Model* m, LinearRelaxation* relaxation) {
|
|
relaxation->cut_generators.push_back(CreateCumulativeTimeTableCutGenerator(
|
|
helper, demands_helper, capacity, m));
|
|
relaxation->cut_generators.push_back(
|
|
CreateCumulativeCompletionTimeCutGenerator(helper, demands_helper,
|
|
capacity, m));
|
|
relaxation->cut_generators.push_back(CreateCumulativePrecedenceCutGenerator(
|
|
helper, demands_helper, capacity, m));
|
|
|
|
// Checks if at least one rectangle has a variable size, is optional, or if
|
|
// the demand or the capacity are variable.
|
|
bool has_variable_part = false;
|
|
IntegerTrail* integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
for (int i = 0; i < helper->NumTasks(); ++i) {
|
|
if (!helper->SizeIsFixed(i)) {
|
|
has_variable_part = true;
|
|
break;
|
|
}
|
|
// Checks variable demand.
|
|
if (!demands_helper->DemandIsFixed(i)) {
|
|
has_variable_part = true;
|
|
break;
|
|
}
|
|
}
|
|
if (has_variable_part || !integer_trail->IsFixed(capacity)) {
|
|
relaxation->cut_generators.push_back(CreateCumulativeEnergyCutGenerator(
|
|
helper, demands_helper, capacity, makespan, m));
|
|
}
|
|
}
|
|
|
|
void AddNoOverlapCutGenerator(SchedulingConstraintHelper* helper,
|
|
const std::optional<AffineExpression>& makespan,
|
|
Model* m, LinearRelaxation* relaxation) {
|
|
relaxation->cut_generators.push_back(
|
|
CreateNoOverlapPrecedenceCutGenerator(helper, m));
|
|
relaxation->cut_generators.push_back(
|
|
CreateNoOverlapCompletionTimeCutGenerator(helper, m));
|
|
|
|
// Checks if at least one rectangle has a variable size or is optional.
|
|
bool has_variable_or_optional_part = false;
|
|
for (int i = 0; i < helper->NumTasks(); ++i) {
|
|
if (helper->IsAbsent(i)) continue;
|
|
if (!helper->SizeIsFixed(i) || !helper->IsPresent(i)) {
|
|
has_variable_or_optional_part = true;
|
|
break;
|
|
}
|
|
}
|
|
if (has_variable_or_optional_part) {
|
|
relaxation->cut_generators.push_back(
|
|
CreateNoOverlapEnergyCutGenerator(helper, makespan, m));
|
|
}
|
|
}
|
|
|
|
void AddNoOverlap2dCutGenerator(const ConstraintProto& ct, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
std::vector<IntervalVariable> x_intervals =
|
|
mapping->Intervals(ct.no_overlap_2d().x_intervals());
|
|
std::vector<IntervalVariable> y_intervals =
|
|
mapping->Intervals(ct.no_overlap_2d().y_intervals());
|
|
|
|
IntervalsRepository* intervals_repository =
|
|
m->GetOrCreate<IntervalsRepository>();
|
|
SchedulingConstraintHelper* x_helper =
|
|
intervals_repository->GetOrCreateHelper(x_intervals);
|
|
SchedulingConstraintHelper* y_helper =
|
|
intervals_repository->GetOrCreateHelper(y_intervals);
|
|
|
|
relaxation->cut_generators.push_back(
|
|
CreateNoOverlap2dCompletionTimeCutGenerator(x_helper, y_helper, m));
|
|
|
|
// Checks if at least one rectangle has a variable dimension or is optional.
|
|
bool has_variable_part = false;
|
|
for (int i = 0; i < x_intervals.size(); ++i) {
|
|
// Ignore absent rectangles.
|
|
if (intervals_repository->IsAbsent(x_intervals[i]) ||
|
|
intervals_repository->IsAbsent(y_intervals[i])) {
|
|
continue;
|
|
}
|
|
|
|
// Checks non-present intervals.
|
|
if (!intervals_repository->IsPresent(x_intervals[i]) ||
|
|
!intervals_repository->IsPresent(y_intervals[i])) {
|
|
has_variable_part = true;
|
|
break;
|
|
}
|
|
|
|
// Checks variable sized intervals.
|
|
if (intervals_repository->MinSize(x_intervals[i]) !=
|
|
intervals_repository->MaxSize(x_intervals[i]) ||
|
|
intervals_repository->MinSize(y_intervals[i]) !=
|
|
intervals_repository->MaxSize(y_intervals[i])) {
|
|
has_variable_part = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!has_variable_part) return;
|
|
|
|
SchedulingDemandHelper* x_demands_helper =
|
|
intervals_repository->GetOrCreateDemandHelper(y_helper,
|
|
x_helper->Sizes());
|
|
SchedulingDemandHelper* y_demands_helper =
|
|
intervals_repository->GetOrCreateDemandHelper(x_helper,
|
|
y_helper->Sizes());
|
|
|
|
std::vector<std::vector<LiteralValueValue>> energies;
|
|
const int num_rectangles = x_helper->NumTasks();
|
|
auto* product_decomposer = m->GetOrCreate<ProductDecomposer>();
|
|
for (int i = 0; i < num_rectangles; ++i) {
|
|
energies.push_back(product_decomposer->TryToDecompose(
|
|
x_helper->Sizes()[i], y_helper->Sizes()[i]));
|
|
}
|
|
|
|
relaxation->cut_generators.push_back(CreateNoOverlap2dEnergyCutGenerator(
|
|
x_helper, y_helper, x_demands_helper, y_demands_helper, energies, m));
|
|
}
|
|
|
|
void AddLinMaxCutGenerator(const ConstraintProto& ct, Model* m,
|
|
LinearRelaxation* relaxation) {
|
|
if (!m->GetOrCreate<SatParameters>()->add_lin_max_cuts()) return;
|
|
if (HasEnforcementLiteral(ct)) return;
|
|
|
|
// TODO(user): Support linearization of general target expression.
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
if (ct.lin_max().target().vars_size() != 1) return;
|
|
if (ct.lin_max().target().coeffs(0) != 1) return;
|
|
if (ct.lin_max().target().offset() != 0) return;
|
|
|
|
const IntegerVariable target =
|
|
mapping->Integer(ct.lin_max().target().vars(0));
|
|
std::vector<LinearExpression> exprs;
|
|
exprs.reserve(ct.lin_max().exprs_size());
|
|
for (int i = 0; i < ct.lin_max().exprs_size(); ++i) {
|
|
// Note: Cut generator requires all expressions to contain only positive
|
|
// vars.
|
|
exprs.push_back(
|
|
PositiveVarExpr(mapping->GetExprFromProto(ct.lin_max().exprs(i))));
|
|
}
|
|
|
|
const std::vector<Literal> alternative_literals =
|
|
CreateAlternativeLiteralsWithView(exprs.size(), m, relaxation);
|
|
|
|
// TODO(user): Move this out of here.
|
|
//
|
|
// Add initial big-M linear relaxation.
|
|
// z_vars[i] == 1 <=> target = exprs[i].
|
|
AppendLinMaxRelaxationPart2(target, alternative_literals, exprs, m,
|
|
relaxation);
|
|
|
|
std::vector<IntegerVariable> z_vars;
|
|
auto* encoder = m->GetOrCreate<IntegerEncoder>();
|
|
for (const Literal lit : alternative_literals) {
|
|
z_vars.push_back(encoder->GetLiteralView(lit));
|
|
CHECK_NE(z_vars.back(), kNoIntegerVariable);
|
|
}
|
|
relaxation->cut_generators.push_back(
|
|
CreateLinMaxCutGenerator(target, exprs, z_vars, m));
|
|
}
|
|
|
|
// If we have an exactly one between literals l_i, and each l_i => var ==
|
|
// value_i, then we can add a strong linear relaxation: var = sum l_i * value_i.
|
|
//
|
|
// This codes detect this and add the corresponding linear equations.
|
|
//
|
|
// TODO(user): We can do something similar with just an at most one, however
|
|
// it is harder to detect that if all literal are false then none of the implied
|
|
// value can be taken.
|
|
void AppendElementEncodingRelaxation(Model* m, LinearRelaxation* relaxation) {
|
|
auto* integer_trail = m->GetOrCreate<IntegerTrail>();
|
|
auto* element_encodings = m->GetOrCreate<ElementEncodings>();
|
|
|
|
int num_exactly_one_elements = 0;
|
|
for (const IntegerVariable var :
|
|
element_encodings->GetElementEncodedVariables()) {
|
|
for (const auto& [index, literal_value_list] :
|
|
element_encodings->Get(var)) {
|
|
IntegerValue min_value = kMaxIntegerValue;
|
|
for (const auto& literal_value : literal_value_list) {
|
|
min_value = std::min(min_value, literal_value.value);
|
|
}
|
|
|
|
LinearConstraintBuilder builder(m, -min_value, -min_value);
|
|
builder.AddTerm(var, IntegerValue(-1));
|
|
for (const auto& [value, literal] : literal_value_list) {
|
|
const IntegerValue delta_min = value - min_value;
|
|
if (delta_min != 0) {
|
|
// If the term has no view, we abort.
|
|
if (!builder.AddLiteralTerm(literal, delta_min)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
++num_exactly_one_elements;
|
|
const LinearConstraint lc = builder.Build();
|
|
if (!PossibleOverflow(*integer_trail, lc)) {
|
|
relaxation->linear_constraints.push_back(std::move(lc));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (num_exactly_one_elements != 0) {
|
|
auto* logger = m->GetOrCreate<SolverLogger>();
|
|
SOLVER_LOG(logger,
|
|
"[ElementLinearRelaxation]"
|
|
" #from_exactly_one:",
|
|
num_exactly_one_elements);
|
|
}
|
|
}
|
|
|
|
LinearRelaxation ComputeLinearRelaxation(const CpModelProto& model_proto,
|
|
Model* m) {
|
|
LinearRelaxation relaxation;
|
|
const SatParameters& params = *m->GetOrCreate<SatParameters>();
|
|
|
|
// Collect AtMostOne to compute better Big-M.
|
|
ActivityBoundHelper activity_bound_helper;
|
|
if (params.linearization_level() > 1) {
|
|
activity_bound_helper.AddAllAtMostOnes(model_proto);
|
|
}
|
|
|
|
// Linearize the constraints.
|
|
for (const auto& ct : model_proto.constraints()) {
|
|
TryToLinearizeConstraint(model_proto, ct, params.linearization_level(), m,
|
|
&relaxation, &activity_bound_helper);
|
|
}
|
|
|
|
// Linearize the encoding of variable that are fully encoded.
|
|
int num_loose_equality_encoding_relaxations = 0;
|
|
int num_tight_equality_encoding_relaxations = 0;
|
|
int num_inequality_encoding_relaxations = 0;
|
|
auto* mapping = m->GetOrCreate<CpModelMapping>();
|
|
for (int i = 0; i < model_proto.variables_size(); ++i) {
|
|
if (mapping->IsBoolean(i)) continue;
|
|
|
|
const IntegerVariable var = mapping->Integer(i);
|
|
if (m->Get(IsFixed(var))) continue;
|
|
|
|
// We first try to linearize the values encoding.
|
|
AppendRelaxationForEqualityEncoding(
|
|
var, *m, &relaxation, &num_tight_equality_encoding_relaxations,
|
|
&num_loose_equality_encoding_relaxations);
|
|
|
|
// Then we try to linearize the inequality encoding. Note that on some
|
|
// problem like pizza27i.mps.gz, adding both equality and inequality
|
|
// encoding is a must.
|
|
//
|
|
// Even if the variable is fully encoded, sometimes not all its associated
|
|
// literal have a view (if they are not part of the original model for
|
|
// instance).
|
|
//
|
|
// TODO(user): Should we add them to the LP anyway? this isn't clear as
|
|
// we can sometimes create a lot of Booleans like this.
|
|
const int old = relaxation.linear_constraints.size();
|
|
AppendPartialGreaterThanEncodingRelaxation(var, *m, &relaxation);
|
|
if (relaxation.linear_constraints.size() > old) {
|
|
++num_inequality_encoding_relaxations;
|
|
}
|
|
}
|
|
|
|
// TODO(user): This is similar to AppendRelaxationForEqualityEncoding() above.
|
|
// Investigate if we can merge the code.
|
|
if (params.linearization_level() >= 2) {
|
|
AppendElementEncodingRelaxation(m, &relaxation);
|
|
}
|
|
|
|
// TODO(user): I am not sure this is still needed. Investigate and explain why
|
|
// or remove.
|
|
if (!m->GetOrCreate<SatSolver>()->FinishPropagation()) {
|
|
return relaxation;
|
|
}
|
|
|
|
// We display the stats before linearizing the at most ones.
|
|
auto* logger = m->GetOrCreate<SolverLogger>();
|
|
if (num_tight_equality_encoding_relaxations != 0 ||
|
|
num_loose_equality_encoding_relaxations != 0 ||
|
|
num_inequality_encoding_relaxations != 0) {
|
|
SOLVER_LOG(logger,
|
|
"[EncodingLinearRelaxation]"
|
|
" #tight_equality:",
|
|
num_tight_equality_encoding_relaxations,
|
|
" #loose_equality:", num_loose_equality_encoding_relaxations,
|
|
" #inequality:", num_inequality_encoding_relaxations);
|
|
}
|
|
if (!relaxation.linear_constraints.empty() ||
|
|
!relaxation.at_most_ones.empty()) {
|
|
SOLVER_LOG(logger,
|
|
"[LinearRelaxationBeforeCliqueExpansion]"
|
|
" #linear:",
|
|
relaxation.linear_constraints.size(),
|
|
" #at_most_ones:", relaxation.at_most_ones.size());
|
|
}
|
|
|
|
// Linearize the at most one constraints. Note that we transform them
|
|
// into maximum "at most one" first and we removes redundant ones.
|
|
m->GetOrCreate<BinaryImplicationGraph>()->TransformIntoMaxCliques(
|
|
&relaxation.at_most_ones,
|
|
SafeDoubleToInt64(params.merge_at_most_one_work_limit()));
|
|
for (const std::vector<Literal>& at_most_one : relaxation.at_most_ones) {
|
|
if (at_most_one.empty()) continue;
|
|
|
|
LinearConstraintBuilder lc(m, kMinIntegerValue, IntegerValue(1));
|
|
for (const Literal literal : at_most_one) {
|
|
// Note that it is okay to simply ignore the literal if it has no
|
|
// integer view.
|
|
const bool unused ABSL_ATTRIBUTE_UNUSED =
|
|
lc.AddLiteralTerm(literal, IntegerValue(1));
|
|
}
|
|
relaxation.linear_constraints.push_back(lc.Build());
|
|
}
|
|
|
|
// We converted all at_most_one to LP constraints, so we need to clear them
|
|
// so that we don't do extra work in the connected component computation.
|
|
relaxation.at_most_ones.clear();
|
|
|
|
// Remove size one LP constraints, they are not useful.
|
|
relaxation.linear_constraints.erase(
|
|
std::remove_if(
|
|
relaxation.linear_constraints.begin(),
|
|
relaxation.linear_constraints.end(),
|
|
[](const LinearConstraint& lc) { return lc.vars.size() <= 1; }),
|
|
relaxation.linear_constraints.end());
|
|
|
|
// We add a clique cut generation over all Booleans of the problem.
|
|
// Note that in practice this might regroup independent LP together.
|
|
//
|
|
// TODO(user): compute connected components of the original problem and
|
|
// split these cuts accordingly.
|
|
if (params.linearization_level() > 1 && params.add_clique_cuts()) {
|
|
LinearConstraintBuilder builder(m);
|
|
for (int i = 0; i < model_proto.variables_size(); ++i) {
|
|
if (!mapping->IsBoolean(i)) continue;
|
|
|
|
// Note that it is okay to simply ignore the literal if it has no
|
|
// integer view.
|
|
const bool unused ABSL_ATTRIBUTE_UNUSED =
|
|
builder.AddLiteralTerm(mapping->Literal(i), IntegerValue(1));
|
|
}
|
|
|
|
// We add a generator touching all the variable in the builder.
|
|
const LinearExpression& expr = builder.BuildExpression();
|
|
if (!expr.vars.empty()) {
|
|
relaxation.cut_generators.push_back(
|
|
CreateCliqueCutGenerator(expr.vars, m));
|
|
}
|
|
}
|
|
|
|
if (!relaxation.linear_constraints.empty() ||
|
|
!relaxation.cut_generators.empty()) {
|
|
SOLVER_LOG(logger,
|
|
"[FinalLinearRelaxation]"
|
|
" #linear:",
|
|
relaxation.linear_constraints.size(),
|
|
" #cut_generators:", relaxation.cut_generators.size());
|
|
}
|
|
|
|
return relaxation;
|
|
}
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|