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.
431432ABSL_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
449439ABSL_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
899865bool 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