Skip to content

Commit 62fbfbc

Browse files
committed
[CP-SAT] more work on lrat, regroup linear1 presolve methods
1 parent bdfadac commit 62fbfbc

15 files changed

+521
-258
lines changed

ortools/sat/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,18 @@ cc_test(
386386
],
387387
)
388388

389+
cc_library(
390+
name = "presolve_encoding",
391+
srcs = ["presolve_encoding.cc"],
392+
hdrs = ["presolve_encoding.h"],
393+
deps = [
394+
":cp_model_utils",
395+
":presolve_context",
396+
"//ortools/util:sorted_interval_list",
397+
"@abseil-cpp//absl/log",
398+
],
399+
)
400+
389401
cc_proto_library(
390402
name = "cp_model_cc_proto",
391403
visibility = ["//visibility:public"],
@@ -1322,6 +1334,7 @@ cc_library(
13221334
":model",
13231335
":precedences",
13241336
":presolve_context",
1337+
":presolve_encoding",
13251338
":presolve_util",
13261339
":probing",
13271340
":sat_base",

ortools/sat/cp_model_presolve.cc

Lines changed: 95 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
#include "ortools/sat/model.h"
7777
#include "ortools/sat/precedences.h"
7878
#include "ortools/sat/presolve_context.h"
79+
#include "ortools/sat/presolve_encoding.h"
7980
#include "ortools/sat/presolve_util.h"
8081
#include "ortools/sat/probing.h"
8182
#include "ortools/sat/sat_base.h"
@@ -430,20 +431,9 @@ bool CpModelPresolver::PresolveBoolOr(ConstraintProto* ct) {
430431
// done elsewhere.
431432
ABSL_MUST_USE_RESULT bool CpModelPresolver::MarkConstraintAsFalse(
432433
ConstraintProto* ct, std::string_view reason) {
433-
DCHECK(!reason.empty());
434-
if (HasEnforcementLiteral(*ct)) {
435-
// Change the constraint to a bool_or.
436-
ct->mutable_bool_or()->clear_literals();
437-
for (const int lit : ct->enforcement_literal()) {
438-
ct->mutable_bool_or()->add_literals(NegatedRef(lit));
439-
}
440-
ct->clear_enforcement_literal();
441-
PresolveBoolOr(ct);
442-
context_->UpdateRuleStats(reason);
443-
return true;
444-
} else {
445-
return context_->NotifyThatModelIsUnsat(reason);
446-
}
434+
if (!context_->MarkConstraintAsFalse(ct, reason)) return false;
435+
if (ct->constraint_case() == ConstraintProto::kBoolOr) PresolveBoolOr(ct);
436+
return true;
447437
}
448438

449439
ABSL_MUST_USE_RESULT bool CpModelPresolver::MarkOptionalIntervalAsFalse(
@@ -870,30 +860,6 @@ int GetFirstVar(ExpressionList exprs) {
870860
return -1;
871861
}
872862

873-
bool IsAffineIntAbs(const ConstraintProto& ct) {
874-
if (ct.constraint_case() != ConstraintProto::kLinMax ||
875-
ct.lin_max().exprs_size() != 2 || ct.lin_max().target().vars_size() > 1 ||
876-
ct.lin_max().exprs(0).vars_size() != 1 ||
877-
ct.lin_max().exprs(1).vars_size() != 1) {
878-
return false;
879-
}
880-
881-
const LinearArgumentProto& lin_max = ct.lin_max();
882-
if (lin_max.exprs(0).offset() != -lin_max.exprs(1).offset()) return false;
883-
if (PositiveRef(lin_max.exprs(0).vars(0)) !=
884-
PositiveRef(lin_max.exprs(1).vars(0))) {
885-
return false;
886-
}
887-
888-
const int64_t left_coeff = RefIsPositive(lin_max.exprs(0).vars(0))
889-
? lin_max.exprs(0).coeffs(0)
890-
: -lin_max.exprs(0).coeffs(0);
891-
const int64_t right_coeff = RefIsPositive(lin_max.exprs(1).vars(0))
892-
? lin_max.exprs(1).coeffs(0)
893-
: -lin_max.exprs(1).coeffs(0);
894-
return left_coeff == -right_coeff;
895-
}
896-
897863
} // namespace
898864

899865
bool CpModelPresolver::PropagateAndReduceAffineMax(ConstraintProto* ct) {
@@ -12293,9 +12259,9 @@ void CpModelPresolver::FindBigAtMostOneAndLinearOverlap(
1229312259

1229412260
for (int x = 0; x < context_->working_model->variables().size(); ++x) {
1229512261
// We pick a variable x that appear in some AMO.
12262+
if (helper->NumAmoForVariable(x) == 0) continue;
1229612263
if (time_limit_->LimitReached()) break;
1229712264
if (timer.WorkLimitIsReached()) break;
12298-
if (helper->NumAmoForVariable(x) == 0) continue;
1229912265

1230012266
amo_cts.clear();
1230112267
timer.TrackSimpleLoop(context_->VarToConstraints(x).size());
@@ -13363,121 +13329,6 @@ void CpModelPresolver::ProcessVariableInTwoAtMostOrExactlyOne(int var) {
1336313329
}
1336413330
}
1336513331

13366-
// If we have a bunch of constraint of the form literal => Y \in domain and
13367-
// another constraint Y = f(X), we can remove Y, that constraint, and transform
13368-
// all linear1 from constraining Y to constraining X.
13369-
//
13370-
// We can for instance do it for Y = abs(X) or Y = X^2 easily. More complex
13371-
// function might be trickier.
13372-
//
13373-
// Note that we can't always do it in the reverse direction though!
13374-
// If we have l => X = -1, we can't transfer that to abs(X) for instance, since
13375-
// X=1 will also map to abs(-1). We can only do it if for all implied domain D
13376-
// we have f^-1(f(D)) = D, which is not easy to check.
13377-
void CpModelPresolver::MaybeTransferLinear1ToAnotherVariable(int var) {
13378-
// Find the extra constraint and do basic CHECKs.
13379-
int other_c;
13380-
int num_others = 0;
13381-
std::vector<int> to_rewrite;
13382-
for (const int c : context_->VarToConstraints(var)) {
13383-
if (c >= 0) {
13384-
const ConstraintProto& ct = context_->working_model->constraints(c);
13385-
if (ct.constraint_case() == ConstraintProto::kLinear &&
13386-
ct.linear().vars().size() == 1) {
13387-
to_rewrite.push_back(c);
13388-
continue;
13389-
}
13390-
}
13391-
++num_others;
13392-
other_c = c;
13393-
}
13394-
if (num_others != 1) return;
13395-
if (other_c < 0) return;
13396-
13397-
// In general constraint with more than two variable can't be removed.
13398-
// Similarly for linear2 with non-fixed rhs as we would need to check the form
13399-
// of all implied domain.
13400-
const auto& other_ct = context_->working_model->constraints(other_c);
13401-
if (context_->ConstraintToVars(other_c).size() != 2 ||
13402-
!other_ct.enforcement_literal().empty() ||
13403-
other_ct.constraint_case() == ConstraintProto::kLinear) {
13404-
return;
13405-
}
13406-
13407-
// This will be the rewriting function. It takes the implied domain of var
13408-
// from linear1, and return a pair {new_var, new_var_implied_domain}.
13409-
std::function<std::pair<int, Domain>(const Domain& implied)> transfer_f =
13410-
nullptr;
13411-
13412-
// We only support a few cases.
13413-
//
13414-
// TODO(user): implement more! Note that the linear2 case was tempting, but if
13415-
// we don't have an equality, we can't transfer, and if we do, we actually
13416-
// have affine equivalence already.
13417-
if (other_ct.constraint_case() == ConstraintProto::kLinMax &&
13418-
other_ct.lin_max().target().vars().size() == 1 &&
13419-
other_ct.lin_max().target().vars(0) == var &&
13420-
std::abs(other_ct.lin_max().target().coeffs(0)) == 1 &&
13421-
IsAffineIntAbs(other_ct)) {
13422-
context_->UpdateRuleStats("linear1: transferred from abs(X) to X");
13423-
const LinearExpressionProto& target = other_ct.lin_max().target();
13424-
const LinearExpressionProto& expr = other_ct.lin_max().exprs(0);
13425-
transfer_f = [target = target, expr = expr](const Domain& implied) {
13426-
Domain target_domain =
13427-
implied.ContinuousMultiplicationBy(target.coeffs(0))
13428-
.AdditionWith(Domain(target.offset()));
13429-
target_domain = target_domain.IntersectionWith(
13430-
Domain(0, std::numeric_limits<int64_t>::max()));
13431-
13432-
// We have target = abs(expr).
13433-
const Domain expr_domain =
13434-
target_domain.UnionWith(target_domain.Negation());
13435-
const Domain new_domain = expr_domain.AdditionWith(Domain(-expr.offset()))
13436-
.InverseMultiplicationBy(expr.coeffs(0));
13437-
return std::make_pair(expr.vars(0), new_domain);
13438-
};
13439-
}
13440-
13441-
if (transfer_f == nullptr) {
13442-
context_->UpdateRuleStats(
13443-
"TODO linear1: appear in only one extra 2-var constraint");
13444-
return;
13445-
}
13446-
13447-
// Applies transfer_f to all linear1.
13448-
std::sort(to_rewrite.begin(), to_rewrite.end());
13449-
const Domain var_domain = context_->DomainOf(var);
13450-
for (const int c : to_rewrite) {
13451-
ConstraintProto* ct = context_->working_model->mutable_constraints(c);
13452-
if (ct->linear().vars(0) != var || ct->linear().coeffs(0) != 1) {
13453-
// This shouldn't happen.
13454-
LOG(INFO) << "Aborted in MaybeTransferLinear1ToAnotherVariable()";
13455-
return;
13456-
}
13457-
13458-
const Domain implied =
13459-
var_domain.IntersectionWith(ReadDomainFromProto(ct->linear()));
13460-
auto [new_var, new_domain] = transfer_f(implied);
13461-
const Domain current = context_->DomainOf(new_var);
13462-
new_domain = new_domain.IntersectionWith(current);
13463-
if (new_domain.IsEmpty()) {
13464-
if (!MarkConstraintAsFalse(ct, "linear1: unsat transfer")) return;
13465-
} else if (new_domain == current) {
13466-
ct->Clear();
13467-
} else {
13468-
ct->mutable_linear()->set_vars(0, new_var);
13469-
FillDomainInProto(new_domain, ct->mutable_linear());
13470-
}
13471-
context_->UpdateConstraintVariableUsage(c);
13472-
}
13473-
13474-
// Copy other_ct to the mapping model and delete var!
13475-
context_->NewMappingConstraint(other_ct, __FILE__, __LINE__);
13476-
context_->working_model->mutable_constraints(other_c)->Clear();
13477-
context_->UpdateConstraintVariableUsage(other_c);
13478-
context_->MarkVariableAsRemoved(var);
13479-
}
13480-
1348113332
// TODO(user): We can still remove the variable even if we want to keep
1348213333
// all feasible solutions for the cases when we have a full encoding.
1348313334
// Similarly this shouldn't break symmetry, but we do need to do it for all
@@ -13499,13 +13350,46 @@ void CpModelPresolver::ProcessVariableOnlyUsedInEncoding(int var) {
1349913350
return;
1350013351
}
1350113352

13502-
if (!context_->VariableIsOnlyUsedInEncodingAndMaybeInObjective(var)) {
13503-
if (context_->VariableIsOnlyUsedInLinear1AndOneExtraConstraint(var)) {
13504-
MaybeTransferLinear1ToAnotherVariable(var);
13505-
return;
13353+
const bool is_only_used_in_linear1 =
13354+
context_->VariableIsOnlyUsedInLinear1AndOneExtraConstraint(var);
13355+
const bool is_only_used_in_encoding =
13356+
context_->VariableIsOnlyUsedInEncodingAndMaybeInObjective(var);
13357+
if (!is_only_used_in_encoding && is_only_used_in_linear1) {
13358+
VariableEncodingLocalModel local_model;
13359+
local_model.var = var;
13360+
local_model.single_constraint_using_the_var_outside_the_local_model = -1;
13361+
local_model.var_in_more_than_one_constraint_outside_the_local_model = false;
13362+
for (const int c : context_->VarToConstraints(var)) {
13363+
if (c >= 0) {
13364+
const ConstraintProto& ct = context_->working_model->constraints(c);
13365+
if (ct.constraint_case() == ConstraintProto::kLinear &&
13366+
ct.linear().vars().size() == 1 && ct.linear().vars(0) == var) {
13367+
local_model.linear1_constraints.push_back(c);
13368+
continue;
13369+
}
13370+
}
13371+
if (c == kObjectiveConstraint) {
13372+
local_model.variable_coeff_in_objective =
13373+
context_->ObjectiveMap().at(var);
13374+
} else if (
13375+
local_model.single_constraint_using_the_var_outside_the_local_model ==
13376+
-1 &&
13377+
c >= 0) {
13378+
// First "other" constraint.
13379+
local_model.single_constraint_using_the_var_outside_the_local_model = c;
13380+
} else {
13381+
// We have a second "other" constraint.
13382+
local_model.single_constraint_using_the_var_outside_the_local_model =
13383+
-1;
13384+
local_model.var_in_more_than_one_constraint_outside_the_local_model =
13385+
true;
13386+
}
1350613387
}
13388+
13389+
MaybeTransferLinear1ToAnotherVariable(local_model, context_);
1350713390
return;
1350813391
}
13392+
if (!is_only_used_in_encoding) return;
1350913393

1351013394
// Presolve newly created constraints.
1351113395
const int old_size = context_->working_model->constraints_size();
@@ -13643,18 +13527,19 @@ bool CpModelPresolver::ProcessChangedVariables(std::vector<bool>* in_queue,
1364313527
if (!context_->CanonicalizeOneObjectiveVariable(v)) return false;
1364413528

1364513529
in_queue->resize(context_->working_model->constraints_size(), false);
13530+
const int size_before = queue->size();
1364613531
for (const int c : context_->VarToConstraints(v)) {
1364713532
if (c >= 0 && !(*in_queue)[c]) {
1364813533
(*in_queue)[c] = true;
1364913534
queue->push_back(c);
1365013535
}
1365113536
}
13537+
13538+
// Make sure the order is deterministic! because var_to_constraints[]
13539+
// order changes from one run to the next.
13540+
std::sort(queue->begin() + size_before, queue->end());
1365213541
}
1365313542
context_->modified_domains.ResetAllToFalse();
13654-
13655-
// Make sure the order is deterministic! because var_to_constraints[]
13656-
// order changes from one run to the next.
13657-
std::sort(queue->begin(), queue->end());
1365813543
return !queue->empty();
1365913544
}
1366013545

@@ -13871,47 +13756,58 @@ void CpModelPresolver::PresolveToFixPoint() {
1387113756
// TODO(user): ideally we should "wake-up" any constraint that contains an
1387213757
// absent interval in the main propagation loop above. But we currently don't
1387313758
// maintain such list.
13874-
const int num_constraints = context_->working_model->constraints_size();
13875-
for (int c = 0; c < num_constraints; ++c) {
13876-
if (time_limit_->LimitReached()) break;
13877-
ConstraintProto* ct = context_->working_model->mutable_constraints(c);
13878-
switch (ct->constraint_case()) {
13879-
case ConstraintProto::kNoOverlap:
13880-
// Filter out absent intervals.
13881-
if (PresolveNoOverlap(ct)) {
13882-
context_->UpdateConstraintVariableUsage(c);
13883-
}
13884-
break;
13885-
case ConstraintProto::kNoOverlap2D:
13886-
// Filter out absent intervals.
13887-
if (PresolveNoOverlap2D(c, ct)) {
13888-
context_->UpdateConstraintVariableUsage(c);
13889-
}
13890-
break;
13891-
case ConstraintProto::kCumulative:
13892-
// Filter out absent intervals.
13893-
if (PresolveCumulative(ct)) {
13894-
context_->UpdateConstraintVariableUsage(c);
13895-
}
13896-
break;
13897-
case ConstraintProto::kBoolOr: {
13898-
// Try to infer domain reductions from clauses and the saved "implies in
13899-
// domain" relations.
13900-
for (const auto& pair :
13901-
context_->deductions.ProcessClause(ct->bool_or().literals())) {
13902-
bool modified = false;
13903-
if (!context_->IntersectDomainWith(pair.first, pair.second,
13904-
&modified)) {
13905-
return;
13759+
if (!time_limit_->LimitReached()) {
13760+
const int num_constraints = context_->working_model->constraints_size();
13761+
TimeLimitCheckEveryNCalls bool_or_check_time_limit(100, time_limit_);
13762+
for (int c = 0; c < num_constraints; ++c) {
13763+
ConstraintProto* ct = context_->working_model->mutable_constraints(c);
13764+
// We don't want to check the time limit at each "small" constraint as
13765+
// there could be many.
13766+
bool check_time_limit = false;
13767+
13768+
switch (ct->constraint_case()) {
13769+
case ConstraintProto::kNoOverlap:
13770+
// Filter out absent intervals.
13771+
if (PresolveNoOverlap(ct)) {
13772+
context_->UpdateConstraintVariableUsage(c);
13773+
}
13774+
check_time_limit = true;
13775+
break;
13776+
case ConstraintProto::kNoOverlap2D:
13777+
// Filter out absent intervals.
13778+
if (PresolveNoOverlap2D(c, ct)) {
13779+
context_->UpdateConstraintVariableUsage(c);
13780+
}
13781+
check_time_limit = true;
13782+
break;
13783+
case ConstraintProto::kCumulative:
13784+
// Filter out absent intervals.
13785+
if (PresolveCumulative(ct)) {
13786+
context_->UpdateConstraintVariableUsage(c);
1390613787
}
13907-
if (modified) {
13908-
context_->UpdateRuleStats("deductions: reduced variable domain");
13788+
check_time_limit = true;
13789+
break;
13790+
case ConstraintProto::kBoolOr: {
13791+
// Try to infer domain reductions from clauses and the saved "implies
13792+
// in domain" relations.
13793+
for (const auto& pair :
13794+
context_->deductions.ProcessClause(ct->bool_or().literals())) {
13795+
bool modified = false;
13796+
if (!context_->IntersectDomainWith(pair.first, pair.second,
13797+
&modified)) {
13798+
return;
13799+
}
13800+
if (modified) {
13801+
context_->UpdateRuleStats("deductions: reduced variable domain");
13802+
}
1390913803
}
13804+
if (bool_or_check_time_limit.LimitReached()) check_time_limit = true;
13805+
break;
1391013806
}
13911-
break;
13807+
default:
13808+
break;
1391213809
}
13913-
default:
13914-
break;
13810+
if (check_time_limit && time_limit_->LimitReached()) break;
1391513811
}
1391613812
}
1391713813

0 commit comments

Comments
 (0)