Files
ortools-clone/ortools/sat/cp_model_postsolve.cc

383 lines
14 KiB
C++

// Copyright 2010-2021 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/cp_model_postsolve.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <vector>
#include "ortools/base/logging.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_utils.h"
#include "ortools/util/sorted_interval_list.h"
namespace operations_research {
namespace sat {
// This postsolve is "special". If the clause is not satisfied, we fix the
// first literal in the clause to true (even if it was fixed to false). This
// allows to handle more complex presolve operations used by the SAT presolver.
//
// Also, any "free" Boolean should be fixed to some value for the subsequent
// postsolve steps.
void PostsolveClause(const ConstraintProto& ct, std::vector<Domain>* domains) {
const int size = ct.bool_or().literals_size();
CHECK_NE(size, 0);
bool satisfied = false;
for (int i = 0; i < size; ++i) {
const int ref = ct.bool_or().literals(i);
const int var = PositiveRef(ref);
if ((*domains)[var].IsFixed()) {
if ((*domains)[var].FixedValue() == (RefIsPositive(ref) ? 1 : 0)) {
satisfied = true;
}
} else {
// We still need to assign free variable. Any value should work.
(*domains)[PositiveRef(ref)] = Domain(0);
}
}
if (satisfied) return;
// Change the value of the first variable (which was chosen at presolve).
const int first_ref = ct.bool_or().literals(0);
(*domains)[PositiveRef(first_ref)] = Domain(RefIsPositive(first_ref) ? 1 : 0);
}
void PostsolveExactlyOne(const ConstraintProto& ct,
std::vector<Domain>* domains) {
bool satisfied = false;
std::vector<int> free_variables;
for (const int ref : ct.exactly_one().literals()) {
const int var = PositiveRef(ref);
if ((*domains)[var].IsFixed()) {
if ((*domains)[var].FixedValue() == (RefIsPositive(ref) ? 1 : 0)) {
CHECK(!satisfied) << "Two variables at one in exactly one.";
satisfied = true;
}
} else {
free_variables.push_back(ref);
}
}
if (!satisfied) {
// Fix one at true.
CHECK(!free_variables.empty()) << "All zero in exactly one";
const int ref = free_variables.back();
(*domains)[PositiveRef(ref)] = Domain(RefIsPositive(ref) ? 1 : 0);
free_variables.pop_back();
}
// Fix any free variable left at false.
for (const int ref : free_variables) {
(*domains)[PositiveRef(ref)] = Domain(RefIsPositive(ref) ? 0 : 1);
}
}
// For now we set the first unset enforcement literal to false.
// There must be one.
void SetEnforcementLiteralToFalse(const ConstraintProto& ct,
std::vector<Domain>* domains) {
CHECK(!ct.enforcement_literal().empty());
bool has_free_enforcement_literal = false;
for (const int enf : ct.enforcement_literal()) {
if ((*domains)[PositiveRef(enf)].IsFixed()) continue;
has_free_enforcement_literal = true;
if (RefIsPositive(enf)) {
(*domains)[enf] = Domain(0);
} else {
(*domains)[PositiveRef(enf)] = Domain(1);
}
break;
}
if (!has_free_enforcement_literal) {
LOG(FATAL)
<< "Unsatisfied linear constraint with no free enforcement literal: "
<< ct.ShortDebugString();
}
}
// Here we simply assign all non-fixed variable to a feasible value. Which
// should always exists by construction.
void PostsolveLinear(const ConstraintProto& ct, std::vector<Domain>* domains) {
int64_t fixed_activity = 0;
const int size = ct.linear().vars().size();
std::vector<int> free_vars;
std::vector<int64_t> free_coeffs;
for (int i = 0; i < size; ++i) {
const int var = ct.linear().vars(i);
const int64_t coeff = ct.linear().coeffs(i);
CHECK_LT(var, domains->size());
if (coeff == 0) continue;
if ((*domains)[var].IsFixed()) {
fixed_activity += (*domains)[var].FixedValue() * coeff;
} else {
free_vars.push_back(var);
free_coeffs.push_back(coeff);
}
}
if (free_vars.empty()) {
const Domain rhs = ReadDomainFromProto(ct.linear());
if (!rhs.Contains(fixed_activity)) {
SetEnforcementLiteralToFalse(ct, domains);
}
return;
}
// Fast track for the most common case.
const Domain initial_rhs = ReadDomainFromProto(ct.linear());
if (free_vars.size() == 1) {
const int var = free_vars[0];
const Domain domain = initial_rhs.AdditionWith(Domain(-fixed_activity))
.InverseMultiplicationBy(free_coeffs[0])
.IntersectionWith((*domains)[var]);
if (domain.IsEmpty()) {
SetEnforcementLiteralToFalse(ct, domains);
return;
}
(*domains)[var] = Domain(domain.SmallestValue());
return;
}
// The postsolve code is a bit involved if there is more than one free
// variable, we have to postsolve them one by one.
//
// Here we recompute the same domains as during the presolve. Everything is
// like if we where substiting the variable one by one:
// terms[i] + fixed_activity \in rhs_domains[i]
// In the reverse order.
std::vector<Domain> rhs_domains;
rhs_domains.push_back(initial_rhs);
for (int i = 0; i + 1 < free_vars.size(); ++i) {
// Note that these should be exactly the same computation as the one done
// during presolve and should be exact. However, we have some tests that do
// not comply, so we don't check exactness here. Also, as long as we don't
// get empty domain below, and the complexity of the domain do not explode
// here, we should be fine.
Domain term = (*domains)[free_vars[i]].MultiplicationBy(-free_coeffs[i]);
rhs_domains.push_back(term.AdditionWith(rhs_domains.back()));
}
for (int i = free_vars.size() - 1; i >= 0; --i) {
// Choose a value for free_vars[i] that fall into rhs_domains[i] -
// fixed_activity. This will crash if the intersection is empty, but it
// shouldn't be.
const int var = free_vars[i];
const int64_t coeff = free_coeffs[i];
const Domain domain = rhs_domains[i]
.AdditionWith(Domain(-fixed_activity))
.InverseMultiplicationBy(coeff)
.IntersectionWith((*domains)[var]);
// TODO(user): I am not 100% that the algo here might cover all the presolve
// case, so if this fail, it might indicate an issue here and not in the
// presolve/solver code.
CHECK(!domain.IsEmpty()) << ct.ShortDebugString();
const int64_t value = domain.SmallestValue();
(*domains)[var] = Domain(value);
fixed_activity += coeff * value;
}
DCHECK(initial_rhs.Contains(fixed_activity));
}
namespace {
int64_t EvaluateLinearExpression(const LinearExpressionProto& expr,
const std::vector<Domain>& domains) {
int64_t value = expr.offset();
for (int i = 0; i < expr.vars_size(); ++i) {
const int ref = expr.vars(i);
const int64_t increment =
domains[PositiveRef(expr.vars(i))].FixedValue() * expr.coeffs(i);
value += RefIsPositive(ref) ? increment : -increment;
}
return value;
}
} // namespace
// Compute the max of each expression, and assign it to the target expr (which
// must be of the form +ref or -ref);
// We only support post-solving the case were the target is unassigned,
// but everything else is fixed.
void PostsolveLinMax(const ConstraintProto& ct, std::vector<Domain>* domains) {
int64_t max_value = std::numeric_limits<int64_t>::min();
for (const LinearExpressionProto& expr : ct.lin_max().exprs()) {
max_value = std::max(max_value, EvaluateLinearExpression(expr, *domains));
}
const int target_ref = GetSingleRefFromExpression(ct.lin_max().target());
const int target_var = PositiveRef(target_ref);
(*domains)[target_var] = (*domains)[target_var].IntersectionWith(
Domain(RefIsPositive(target_ref) ? max_value : -max_value));
CHECK(!(*domains)[target_var].IsEmpty());
}
// We only support 3 cases in the presolve currently.
void PostsolveElement(const ConstraintProto& ct, std::vector<Domain>* domains) {
const int index_ref = ct.element().index();
const int index_var = PositiveRef(index_ref);
const int target_ref = ct.element().target();
const int target_var = PositiveRef(target_ref);
// Deal with non-fixed target and non-fixed index. This only happen if
// whatever the value of the index and selected variable, we can choose a
// valid target, so we just fix the index to its min value in this case.
if (!(*domains)[target_var].IsFixed() && !(*domains)[index_var].IsFixed()) {
const int64_t index_var_value = (*domains)[index_var].Min();
(*domains)[index_var] = Domain(index_var_value);
// If the selected variable is not fixed, we also need to fix it.
const int selected_ref = ct.element().vars(
RefIsPositive(index_ref) ? index_var_value : -index_var_value);
const int selected_var = PositiveRef(selected_ref);
if (!(*domains)[selected_var].IsFixed()) {
(*domains)[selected_var] = Domain((*domains)[selected_var].Min());
}
}
// Deal with fixed index.
if ((*domains)[index_var].IsFixed()) {
const int64_t index_var_value = (*domains)[index_var].FixedValue();
const int selected_ref = ct.element().vars(
RefIsPositive(index_ref) ? index_var_value : -index_var_value);
const int selected_var = PositiveRef(selected_ref);
if ((*domains)[selected_var].IsFixed()) {
const int64_t selected_value = (*domains)[selected_var].FixedValue();
(*domains)[target_var] = (*domains)[target_var].IntersectionWith(
Domain(RefIsPositive(target_ref) == RefIsPositive(selected_ref)
? selected_value
: -selected_value));
DCHECK(!(*domains)[target_var].IsEmpty());
} else {
const bool same_sign =
(selected_var == selected_ref) == (target_var == target_ref);
const Domain target_domain = (*domains)[target_var];
const Domain selected_domain = same_sign
? (*domains)[selected_var]
: (*domains)[selected_var].Negation();
const Domain final = target_domain.IntersectionWith(selected_domain);
const int64_t value = final.SmallestValue();
(*domains)[target_var] =
(*domains)[target_var].IntersectionWith(Domain(value));
(*domains)[selected_var] = (*domains)[selected_var].IntersectionWith(
Domain(same_sign ? value : -value));
DCHECK(!(*domains)[target_var].IsEmpty());
DCHECK(!(*domains)[selected_var].IsEmpty());
}
return;
}
// Deal with fixed target (and constant vars).
const int64_t target_value = (*domains)[target_var].FixedValue();
int selected_index_value = -1;
for (const int64_t v : (*domains)[index_var].Values()) {
const int64_t i = index_var == index_ref ? v : -v;
if (i < 0 || i >= ct.element().vars_size()) continue;
const int ref = ct.element().vars(i);
const int var = PositiveRef(ref);
const int64_t value = (*domains)[var].FixedValue();
if (RefIsPositive(target_ref) == RefIsPositive(ref)) {
if (value == target_value) {
selected_index_value = i;
break;
}
} else {
if (value == -target_value) {
selected_index_value = i;
break;
}
}
}
CHECK_NE(selected_index_value, -1);
(*domains)[index_var] = (*domains)[index_var].IntersectionWith(Domain(
RefIsPositive(index_ref) ? selected_index_value : -selected_index_value));
DCHECK(!(*domains)[index_var].IsEmpty());
}
void PostsolveResponse(const int64_t num_variables_in_original_model,
const CpModelProto& mapping_proto,
const std::vector<int>& postsolve_mapping,
std::vector<int64_t>* solution) {
CHECK_EQ(solution->size(), postsolve_mapping.size());
// Read the initial variable domains, either from the fixed solution of the
// presolved problems or from the mapping model.
std::vector<Domain> domains(mapping_proto.variables_size());
for (int i = 0; i < postsolve_mapping.size(); ++i) {
CHECK_LE(postsolve_mapping[i], domains.size());
domains[postsolve_mapping[i]] = Domain((*solution)[i]);
}
for (int i = 0; i < domains.size(); ++i) {
if (domains[i].IsEmpty()) {
domains[i] = ReadDomainFromProto(mapping_proto.variables(i));
}
CHECK(!domains[i].IsEmpty());
}
// Process the constraints in reverse order.
const int num_constraints = mapping_proto.constraints_size();
for (int i = num_constraints - 1; i >= 0; i--) {
const ConstraintProto& ct = mapping_proto.constraints(i);
// We ignore constraint with an enforcement literal set to false. If the
// enforcement is still unclear, we still process this constraint.
bool constraint_can_be_ignored = false;
for (const int enf : ct.enforcement_literal()) {
const int var = PositiveRef(enf);
const bool is_false =
domains[var].IsFixed() &&
RefIsPositive(enf) == (domains[var].FixedValue() == 0);
if (is_false) {
constraint_can_be_ignored = true;
break;
}
}
if (constraint_can_be_ignored) continue;
switch (ct.constraint_case()) {
case ConstraintProto::kBoolOr:
PostsolveClause(ct, &domains);
break;
case ConstraintProto::kExactlyOne:
PostsolveExactlyOne(ct, &domains);
break;
case ConstraintProto::kLinear:
PostsolveLinear(ct, &domains);
break;
case ConstraintProto::kLinMax:
PostsolveLinMax(ct, &domains);
break;
case ConstraintProto::kElement:
PostsolveElement(ct, &domains);
break;
default:
// This should never happen as we control what kind of constraint we
// add to the mapping_proto;
LOG(FATAL) << "Unsupported constraint: " << ct.ShortDebugString();
}
}
// Fill the response.
// Maybe fix some still unfixed variable.
solution->clear();
CHECK_LE(num_variables_in_original_model, domains.size());
for (int i = 0; i < num_variables_in_original_model; ++i) {
solution->push_back(domains[i].SmallestValue());
}
}
} // namespace sat
} // namespace operations_research