[CP-SAT] speedup presolve; fix platform non determinism fix @2056; add symmetry detection for model with scheduling constraints
This commit is contained in:
@@ -1544,12 +1544,19 @@ bool BinaryImplicationGraph::TransformIntoMaxCliques(
|
||||
implications_.size());
|
||||
|
||||
// We starts by processing larger constraints first.
|
||||
std::sort(at_most_ones->begin(), at_most_ones->end(),
|
||||
[](const std::vector<Literal> a, const std::vector<Literal> b) {
|
||||
return a.size() > b.size();
|
||||
});
|
||||
for (std::vector<Literal>& clique : *at_most_ones) {
|
||||
const int old_size = clique.size();
|
||||
// But we want the output order to be stable.
|
||||
std::vector<std::pair<int, int>> index_size_vector;
|
||||
index_size_vector.reserve(at_most_ones->size());
|
||||
for (int i = 0; i < at_most_ones->size(); ++i) {
|
||||
index_size_vector.push_back({i, (*at_most_ones)[i].size()});
|
||||
}
|
||||
std::stable_sort(
|
||||
index_size_vector.begin(), index_size_vector.end(),
|
||||
[](const std::pair<int, int> a, const std::pair<int, int>& b) {
|
||||
return a.second > b.second;
|
||||
});
|
||||
for (const auto [index, old_size] : index_size_vector) {
|
||||
std::vector<Literal>& clique = (*at_most_ones)[index];
|
||||
if (time_limit_->LimitReached()) break;
|
||||
|
||||
// Remap the clique to only use representative.
|
||||
|
||||
@@ -583,6 +583,7 @@ class BinaryImplicationGraph : public SatPropagator {
|
||||
// This function will transform each of the given constraint into a maximal
|
||||
// one in the underlying implication graph. Constraints that are redundant
|
||||
// after other have been expanded (i.e. included into) will be cleared.
|
||||
// Note that the order of constraints will be conserved.
|
||||
//
|
||||
// Returns false if the model is detected to be UNSAT (this needs to call
|
||||
// DetectEquivalences() if not already done).
|
||||
|
||||
@@ -110,18 +110,21 @@ message ElementConstraintProto {
|
||||
repeated int32 vars = 3;
|
||||
}
|
||||
|
||||
// This "special" constraint not only enforces (start + size == end) and (size
|
||||
// >= 0) but can also be referred by other constraints using this "interval"
|
||||
// concept.
|
||||
// This is not really a constraint. It is there so it can be referred by other
|
||||
// constraints using this "interval" concept.
|
||||
//
|
||||
// IMPORTANT: All intervals must appear before they are used. This is enforced
|
||||
// by our validation.
|
||||
//
|
||||
// IMPORTANT: For now, this constraint do not enforce any relations on the
|
||||
// components, and it is up to the client to add in the model:
|
||||
// - enforcement => start + size == end.
|
||||
// - enforcement => size >= 0 // Only needed if size is not already >= 0.
|
||||
//
|
||||
// IMPORTANT: For now, we just support affine relation. We could easily
|
||||
// create an intermediate variable to support full linear expression, but this
|
||||
// isn't done currently.
|
||||
message IntervalConstraintProto {
|
||||
// IMPORTANT: For now, this constraint do not enforce any relations on the
|
||||
// view, and a linear constraint must be added together with this to enforce
|
||||
// enforcement => start + size == end. An enforcement => size >=0 might also
|
||||
// be needed.
|
||||
//
|
||||
// IMPORTANT: For now, we just support affine relation. We could easily
|
||||
// create an intermediate variable to support full linear expression, but this
|
||||
// isn't done currently.
|
||||
LinearExpressionProto start = 4;
|
||||
LinearExpressionProto end = 5;
|
||||
LinearExpressionProto size = 6;
|
||||
|
||||
@@ -147,6 +147,11 @@ std::string ValidateArgumentReferencesInConstraint(const CpModelProto& model,
|
||||
return absl::StrCat("Out of bound interval ", i, " in constraint #", c,
|
||||
" : ", ProtobufShortDebugString(ct));
|
||||
}
|
||||
if (i >= c) {
|
||||
return absl::StrCat("Interval ", i, " in constraint #", c,
|
||||
" must appear before in the list of constraints :",
|
||||
ProtobufShortDebugString(ct));
|
||||
}
|
||||
if (model.constraints(i).constraint_case() !=
|
||||
ConstraintProto::ConstraintCase::kInterval) {
|
||||
return absl::StrCat(
|
||||
|
||||
@@ -830,7 +830,11 @@ void DetectOptionalVariables(const CpModelProto& model_proto, Model* m) {
|
||||
mapping->Integer(var),
|
||||
mapping->Literal(enforcement_intersection[var].front()));
|
||||
}
|
||||
VLOG(2) << "Auto-detected " << num_optionals << " optional variables.";
|
||||
|
||||
if (num_optionals > 0) {
|
||||
SOLVER_LOG(m->GetOrCreate<SolverLogger>(), "Auto-detected ", num_optionals,
|
||||
" optional variables.");
|
||||
}
|
||||
}
|
||||
|
||||
void AddFullEncodingFromSearchBranching(const CpModelProto& model_proto,
|
||||
|
||||
@@ -410,7 +410,8 @@ bool CpModelPresolver::PresolveAtMostOrExactlyOne(ConstraintProto* ct) {
|
||||
// Deal with duplicate variable reference.
|
||||
context_->tmp_literal_set.clear();
|
||||
for (const int literal : *literals) {
|
||||
if (context_->tmp_literal_set.contains(literal)) {
|
||||
const auto [_, inserted] = context_->tmp_literal_set.insert(literal);
|
||||
if (!inserted) {
|
||||
if (!context_->SetLiteralToFalse(literal)) return false;
|
||||
context_->UpdateRuleStats(absl::StrCat(name, "duplicate literals"));
|
||||
}
|
||||
@@ -440,7 +441,6 @@ bool CpModelPresolver::PresolveAtMostOrExactlyOne(ConstraintProto* ct) {
|
||||
}
|
||||
return RemoveConstraint(ct);
|
||||
}
|
||||
context_->tmp_literal_set.insert(literal);
|
||||
}
|
||||
|
||||
// Remove fixed variables.
|
||||
@@ -3796,6 +3796,7 @@ bool CpModelPresolver::PresolveNoOverlap(ConstraintProto* ct) {
|
||||
}
|
||||
}
|
||||
|
||||
bool move_constraint_last = false;
|
||||
if (!constant_intervals.empty()) {
|
||||
// Sort constant_intervals by start min.
|
||||
std::sort(constant_intervals.begin(), constant_intervals.end(),
|
||||
@@ -3847,6 +3848,7 @@ bool CpModelPresolver::PresolveNoOverlap(ConstraintProto* ct) {
|
||||
new_interval->mutable_start()->set_offset(new_start);
|
||||
new_interval->mutable_size()->set_offset(new_end - new_start);
|
||||
new_interval->mutable_end()->set_offset(new_end);
|
||||
move_constraint_last = true;
|
||||
}
|
||||
|
||||
// Cleanup the original proto.
|
||||
@@ -3880,6 +3882,16 @@ bool CpModelPresolver::PresolveNoOverlap(ConstraintProto* ct) {
|
||||
return RemoveConstraint(ct);
|
||||
}
|
||||
|
||||
// Unfortunately, because we want all intervals to appear before a constraint
|
||||
// that uses them, we need to move the constraint last when we merged constant
|
||||
// intervals.
|
||||
if (move_constraint_last) {
|
||||
changed = true;
|
||||
*context_->working_model->add_constraints() = *ct;
|
||||
context_->UpdateNewConstraintsVariableUsage();
|
||||
return RemoveConstraint(ct);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -6043,10 +6055,8 @@ bool CpModelPresolver::ProcessSetPPC() {
|
||||
if (c1 == c2) continue;
|
||||
|
||||
CHECK_LT(c1, c2);
|
||||
if (compared_constraints.contains(std::pair<int, int>(c1, c2))) {
|
||||
continue;
|
||||
}
|
||||
compared_constraints.insert({c1, c2});
|
||||
const auto [_, inserted] = compared_constraints.insert({c1, c2});
|
||||
if (!inserted) continue;
|
||||
|
||||
// Hard limit on number of comparisons to avoid spending too much time
|
||||
// here.
|
||||
@@ -7100,6 +7110,10 @@ void CpModelPresolver::PresolveToFixPoint() {
|
||||
ModelCopy::ModelCopy(PresolveContext* context) : context_(context) {}
|
||||
|
||||
// TODO(user): Merge with the phase 1 of the presolve code.
|
||||
//
|
||||
// TODO(user): It seems easy to forget to update this if any new constraint
|
||||
// contains an interval or if we add a field to an existing constraint. Find a
|
||||
// way to remind contributor to not forget this.
|
||||
bool ModelCopy::ImportAndSimplifyConstraints(
|
||||
const CpModelProto& in_model, const std::vector<int>& ignored_constraints) {
|
||||
const absl::flat_hash_set<int> ignored_constraints_set(
|
||||
@@ -7111,10 +7125,8 @@ bool ModelCopy::ImportAndSimplifyConstraints(
|
||||
if (ignored_constraints_set.contains(c)) continue;
|
||||
|
||||
const ConstraintProto& ct = in_model.constraints(c);
|
||||
if (OneEnforcementLiteralIsFalse(ct) &&
|
||||
ct.constraint_case() != ConstraintProto::kInterval) {
|
||||
continue;
|
||||
}
|
||||
if (OneEnforcementLiteralIsFalse(ct)) continue;
|
||||
|
||||
switch (ct.constraint_case()) {
|
||||
case ConstraintProto::CONSTRAINT_NOT_SET: {
|
||||
break;
|
||||
@@ -7143,27 +7155,23 @@ bool ModelCopy::ImportAndSimplifyConstraints(
|
||||
if (!CopyInterval(ct, c)) return CreateUnsatModel();
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kNoOverlap: {
|
||||
CopyAndMapNoOverlap(ct);
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kNoOverlap2D: {
|
||||
CopyAndMapNoOverlap2D(ct);
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kCumulative: {
|
||||
CopyAndMapCumulative(ct);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
*context_->working_model->add_constraints() = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-map interval indices for new constraints.
|
||||
// TODO(user): Support removing unperformed intervals.
|
||||
for (int c = starting_constraint_index_;
|
||||
c < context_->working_model->constraints_size(); ++c) {
|
||||
ConstraintProto& ct_ref = *context_->working_model->mutable_constraints(c);
|
||||
ApplyToAllIntervalIndices(
|
||||
[this](int* ref) {
|
||||
const auto& it = interval_mapping_.find(*ref);
|
||||
if (it != interval_mapping_.end()) {
|
||||
*ref = it->second;
|
||||
}
|
||||
},
|
||||
&ct_ref);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7353,7 +7361,6 @@ bool ModelCopy::CopyExactlyOne(const ConstraintProto& ct) {
|
||||
}
|
||||
|
||||
bool ModelCopy::CopyInterval(const ConstraintProto& ct, int c) {
|
||||
// TODO(user): remove non performed intervals.
|
||||
CHECK_EQ(starting_constraint_index_, 0)
|
||||
<< "Adding new interval constraints to partially filled model is not "
|
||||
"supported.";
|
||||
@@ -7362,6 +7369,55 @@ bool ModelCopy::CopyInterval(const ConstraintProto& ct, int c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModelCopy::CopyAndMapNoOverlap(const ConstraintProto& ct) {
|
||||
// Note that we don't copy names or enforcement_literal (not supported) here.
|
||||
auto* new_ct =
|
||||
context_->working_model->add_constraints()->mutable_no_overlap();
|
||||
new_ct->mutable_intervals()->Reserve(ct.no_overlap().intervals().size());
|
||||
for (const int index : ct.no_overlap().intervals()) {
|
||||
const auto it = interval_mapping_.find(index);
|
||||
if (it == interval_mapping_.end()) continue;
|
||||
new_ct->add_intervals(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelCopy::CopyAndMapNoOverlap2D(const ConstraintProto& ct) {
|
||||
// Note that we don't copy names or enforcement_literal (not supported) here.
|
||||
auto* new_ct =
|
||||
context_->working_model->add_constraints()->mutable_no_overlap_2d();
|
||||
new_ct->set_boxes_with_null_area_can_overlap(
|
||||
ct.no_overlap_2d().boxes_with_null_area_can_overlap());
|
||||
|
||||
const int num_intervals = ct.no_overlap_2d().x_intervals().size();
|
||||
new_ct->mutable_x_intervals()->Reserve(num_intervals);
|
||||
new_ct->mutable_y_intervals()->Reserve(num_intervals);
|
||||
for (int i = 0; i < num_intervals; ++i) {
|
||||
const auto x_it = interval_mapping_.find(ct.no_overlap_2d().x_intervals(i));
|
||||
if (x_it == interval_mapping_.end()) continue;
|
||||
const auto y_it = interval_mapping_.find(ct.no_overlap_2d().y_intervals(i));
|
||||
if (y_it == interval_mapping_.end()) continue;
|
||||
new_ct->add_x_intervals(x_it->second);
|
||||
new_ct->add_y_intervals(y_it->second);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelCopy::CopyAndMapCumulative(const ConstraintProto& ct) {
|
||||
// Note that we don't copy names or enforcement_literal (not supported) here.
|
||||
auto* new_ct =
|
||||
context_->working_model->add_constraints()->mutable_cumulative();
|
||||
*new_ct->mutable_capacity() = ct.cumulative().capacity();
|
||||
|
||||
const int num_intervals = ct.cumulative().intervals().size();
|
||||
new_ct->mutable_intervals()->Reserve(num_intervals);
|
||||
new_ct->mutable_demands()->Reserve(num_intervals);
|
||||
for (int i = 0; i < num_intervals; ++i) {
|
||||
const auto it = interval_mapping_.find(ct.cumulative().intervals(i));
|
||||
if (it == interval_mapping_.end()) continue;
|
||||
new_ct->add_intervals(it->second);
|
||||
*new_ct->add_demands() = ct.cumulative().demands(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool ModelCopy::CreateUnsatModel() {
|
||||
context_->working_model->mutable_constraints()->Clear();
|
||||
context_->working_model->add_constraints()->mutable_bool_or();
|
||||
|
||||
@@ -257,6 +257,13 @@ class ModelCopy {
|
||||
bool CopyExactlyOne(const ConstraintProto& ct);
|
||||
bool CopyInterval(const ConstraintProto& ct, int c);
|
||||
|
||||
// These function remove unperformed intervals. Note that they requires
|
||||
// interval to appear before (validated) as they test unperformed by testing
|
||||
// if interval_mapping_ is empty.
|
||||
void CopyAndMapNoOverlap(const ConstraintProto& ct);
|
||||
void CopyAndMapNoOverlap2D(const ConstraintProto& ct);
|
||||
void CopyAndMapCumulative(const ConstraintProto& ct);
|
||||
|
||||
PresolveContext* context_;
|
||||
int64_t skipped_non_zero_ = 0;
|
||||
|
||||
|
||||
@@ -215,8 +215,13 @@ std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
|
||||
get_implication_node(NegatedRef(ref_a)));
|
||||
};
|
||||
|
||||
// We need to keep track of this for scheduling constraints.
|
||||
absl::flat_hash_map<int, int> interval_constraint_index_to_node;
|
||||
|
||||
// Add constraints to the graph.
|
||||
for (const ConstraintProto& constraint : problem.constraints()) {
|
||||
for (int constraint_index = 0; constraint_index < problem.constraints_size();
|
||||
++constraint_index) {
|
||||
const ConstraintProto& constraint = problem.constraints(constraint_index);
|
||||
const int constraint_node = initial_equivalence_classes->size();
|
||||
std::vector<int64_t> color = {CONSTRAINT_NODE,
|
||||
constraint.constraint_case()};
|
||||
@@ -299,6 +304,76 @@ std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kInterval: {
|
||||
// We create 3 constraint nodes (for start, size and end) including the
|
||||
// offset. We connect these to their terms like for a linear constraint.
|
||||
std::vector<int> nodes;
|
||||
for (int indicator = 0; indicator <= 2; ++indicator) {
|
||||
const LinearExpressionProto& expr =
|
||||
indicator == 0 ? constraint.interval().start()
|
||||
: indicator == 1 ? constraint.interval().size()
|
||||
: constraint.interval().end();
|
||||
|
||||
std::vector<int64_t> local_color = color;
|
||||
local_color.push_back(indicator);
|
||||
local_color.push_back(expr.offset());
|
||||
const int local_node = new_node(local_color);
|
||||
nodes.push_back(local_node);
|
||||
|
||||
for (int i = 0; i < expr.vars().size(); ++i) {
|
||||
const int ref = expr.vars(i);
|
||||
const int var_node = PositiveRef(ref);
|
||||
const int64_t coeff =
|
||||
RefIsPositive(ref) ? expr.coeffs(i) : -expr.coeffs(i);
|
||||
graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
|
||||
}
|
||||
}
|
||||
|
||||
// We will only map enforcement literal to the start_node below because
|
||||
// it has the same index as the constraint_node.
|
||||
interval_constraint_index_to_node[constraint_index] = constraint_node;
|
||||
CHECK_EQ(nodes[0], constraint_node);
|
||||
|
||||
// Make sure that if one node is mapped to another one, its other two
|
||||
// components are the same.
|
||||
graph->AddArc(nodes[0], nodes[1]);
|
||||
graph->AddArc(nodes[1], nodes[2]);
|
||||
graph->AddArc(nodes[2], nodes[0]); // TODO(user): not needed?
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kNoOverlap: {
|
||||
// Note(user): This require that intervals appear before they are used.
|
||||
// We currently enforce this at validation, otherwise we need two passes
|
||||
// here and in a bunch of other places.
|
||||
CHECK_EQ(constraint_node, new_node(color));
|
||||
for (const int interval : constraint.no_overlap().intervals()) {
|
||||
graph->AddArc(interval_constraint_index_to_node.at(interval),
|
||||
constraint_node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConstraintProto::kNoOverlap2D: {
|
||||
// Note(user): This require that intervals appear before they are used.
|
||||
// We currently enforce this at validation, otherwise we need two passes
|
||||
// here and in a bunch of other places.
|
||||
//
|
||||
// TODO(user): With this graph encoding, we loose the symmetry that the
|
||||
// dimension x can be swapped with the dimension y. I think it is
|
||||
// possible to encode this by creating two extra nodes X and
|
||||
// Y, each connected to all the x and all the y, but I have to think
|
||||
// more about it.
|
||||
CHECK_EQ(constraint_node, new_node(color));
|
||||
const int size = constraint.no_overlap_2d().x_intervals().size();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const int x = constraint.no_overlap_2d().x_intervals(i);
|
||||
const int y = constraint.no_overlap_2d().y_intervals(i);
|
||||
graph->AddArc(interval_constraint_index_to_node.at(x),
|
||||
constraint_node);
|
||||
graph->AddArc(interval_constraint_index_to_node.at(x),
|
||||
interval_constraint_index_to_node.at(y));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// If the model contains any non-supported constraints, return an empty
|
||||
// graph.
|
||||
|
||||
@@ -1547,7 +1547,7 @@ LinearRelaxation ComputeLinearRelaxation(const CpModelProto& model_proto,
|
||||
// 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);
|
||||
&relaxation.at_most_ones, 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user