Skip to content

Commit cd4ed09

Browse files
committed
[CP-SAT] new scaling method, bugfixes, special LRAT compatible sat presolve
1 parent 2feb7e0 commit cd4ed09

File tree

5 files changed

+300
-25
lines changed

5 files changed

+300
-25
lines changed

ortools/linear_solver/proto_solver/sat_proto_solver.cc

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,23 @@ MPSolutionResponse SatSolveProtoInternal(
269269
*logger, "A detected integer variable has an empty domain");
270270
}
271271
}
272-
if (params.mip_var_scaling() != 1.0) {
272+
273+
// Try to restrict bounds further before we scale.
274+
if (params.mip_presolve_restrict_bounds() &&
275+
!params.keep_all_feasible_solutions_in_presolve() &&
276+
!params.enumerate_all_solutions()) {
277+
sat::RestrictBoundsWithDualReasoning(params, mp_model.get(), logger);
278+
}
279+
280+
if (params.mip_scale_to_max_bound()) {
281+
const std::vector<double> other_scaling =
282+
sat::ScaleContinuousVariablesUpToMaxBound(params.mip_max_bound(),
283+
params.mip_wanted_precision(),
284+
mp_model.get(), logger);
285+
for (int i = 0; i < var_scaling.size(); ++i) {
286+
var_scaling[i] *= other_scaling[i];
287+
}
288+
} else if (params.mip_var_scaling() != 1.0) {
273289
const double max_bound = params.mip_scale_large_domain()
274290
? std::numeric_limits<double>::infinity()
275291
: params.mip_max_bound();

ortools/sat/cp_model_presolve.cc

Lines changed: 260 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6108,7 +6108,7 @@ void ExtractClauses(bool merge_into_bool_and,
61086108
// how we perform the postsolve.
61096109
absl::flat_hash_map<int, int> ref_to_bool_and;
61106110
for (int i = 0; i < container.NumClauses(); ++i) {
6111-
const std::vector<Literal>& clause = container.Clause(i);
6111+
const auto& clause = container.Clause(i);
61126112
if (clause.empty()) continue;
61136113

61146114
// bool_and.
@@ -8926,6 +8926,176 @@ bool CpModelPresolver::PresolvePureSatPart() {
89268926
return true;
89278927
}
89288928

8929+
namespace {
8930+
class BasicClauseContainer {
8931+
public:
8932+
void SetNumVariables(int /*num_variables*/) {}
8933+
void AddBinaryClause(Literal a, Literal b) { AddClause({a, b}); }
8934+
void AddClause(absl::Span<const Literal> clause) { clauses_.Add(clause); }
8935+
8936+
int NumClauses() const { return clauses_.size(); }
8937+
absl::Span<const Literal> Clause(int index) const { return clauses_[index]; }
8938+
8939+
private:
8940+
CompactVectorVector<int, Literal> clauses_;
8941+
};
8942+
} // namespace
8943+
8944+
bool CpModelPresolver::PresolvePureSatProblem() {
8945+
if (context_->ModelIsUnsat()) return true;
8946+
if (context_->params().keep_all_feasible_solutions_in_presolve()) return true;
8947+
8948+
// Crash if the problem is not pure SAT.
8949+
const int num_variables = context_->working_model->variables().size();
8950+
const int num_constraints = context_->working_model->constraints().size();
8951+
const std::string error_msg =
8952+
"cp_model_pure_sat_presolve can only be used with pure SAT problems.";
8953+
if (context_->working_model->has_objective()) {
8954+
LOG(FATAL) << error_msg;
8955+
}
8956+
for (int i = 0; i < num_variables; ++i) {
8957+
if (!context_->CanBeUsedAsLiteral(i)) {
8958+
LOG(FATAL) << error_msg;
8959+
}
8960+
}
8961+
for (int i = 0; i < num_constraints; ++i) {
8962+
const ConstraintProto& ct = context_->working_model->constraints(i);
8963+
if (!(ct.constraint_case() == ConstraintProto::kBoolOr ||
8964+
ct.constraint_case() == ConstraintProto::kBoolAnd ||
8965+
ct.constraint_case() == ConstraintProto::CONSTRAINT_NOT_SET)) {
8966+
LOG(FATAL) << error_msg;
8967+
}
8968+
}
8969+
8970+
Model local_model;
8971+
local_model.GetOrCreate<TimeLimit>()->MergeWithGlobalTimeLimit(time_limit_);
8972+
auto* sat_solver = local_model.GetOrCreate<SatSolver>();
8973+
sat_solver->SetNumVariables(num_variables);
8974+
8975+
std::vector<int> new_to_old_index;
8976+
new_to_old_index.reserve(num_variables);
8977+
for (int i = 0; i < num_variables; ++i) {
8978+
new_to_old_index.push_back(i);
8979+
}
8980+
8981+
// 1) Load the problem in the SAT solver.
8982+
// 1.1) Fix variables if any.
8983+
auto ref_to_literal = [](int ref) {
8984+
return Literal(BooleanVariable(PositiveRef(ref)), RefIsPositive(ref));
8985+
};
8986+
for (int var = 0; var < num_variables; ++var) {
8987+
if (context_->IsFixed(var)) {
8988+
if (context_->LiteralIsTrue(var)) {
8989+
if (!sat_solver->AddUnitClause({ref_to_literal(var)})) return false;
8990+
} else {
8991+
if (!sat_solver->AddUnitClause({ref_to_literal(NegatedRef(var))})) {
8992+
return false;
8993+
}
8994+
}
8995+
}
8996+
}
8997+
// 1.2) Load the clauses.
8998+
std::vector<Literal> clause;
8999+
for (int i = 0; i < context_->working_model->constraints_size(); ++i) {
9000+
const ConstraintProto& ct = context_->working_model->constraints(i);
9001+
if (ct.constraint_case() == ConstraintProto::kBoolOr) {
9002+
clause.clear();
9003+
for (const int ref : ct.enforcement_literal()) {
9004+
clause.push_back(ref_to_literal(ref).Negated());
9005+
}
9006+
for (const int ref : ct.bool_or().literals()) {
9007+
clause.push_back(ref_to_literal(ref));
9008+
}
9009+
sat_solver->AddProblemClause(clause);
9010+
} else if (ct.constraint_case() == ConstraintProto::kBoolAnd) {
9011+
clause.clear();
9012+
for (const int ref : ct.enforcement_literal()) {
9013+
clause.push_back(ref_to_literal(ref).Negated());
9014+
}
9015+
clause.push_back(Literal(kNoLiteralIndex)); // will be replaced below.
9016+
for (const int ref : ct.bool_and().literals()) {
9017+
clause.back() = ref_to_literal(ref);
9018+
sat_solver->AddProblemClause(clause);
9019+
}
9020+
} else {
9021+
DCHECK_EQ(ct.constraint_case(), ConstraintProto::CONSTRAINT_NOT_SET);
9022+
continue;
9023+
}
9024+
context_->working_model->mutable_constraints(i)->Clear();
9025+
context_->UpdateConstraintVariableUsage(i);
9026+
}
9027+
if (sat_solver->ModelIsUnsat()) return false;
9028+
9029+
// Some problems are formulated in such a way that our SAT heuristics
9030+
// simply works without conflict. Get them out of the way first because it
9031+
// is possible that the presolve lose this "lucky" ordering. This is in
9032+
// particular the case on the SAT14.crafted.complete-xxx-... problems.
9033+
if (!LookForTrivialSatSolution(/*deterministic_time_limit=*/1.0, &local_model,
9034+
logger_)) {
9035+
return false;
9036+
}
9037+
if (sat_solver->LiteralTrail().Index() == num_variables) {
9038+
// Problem solved! We should be able to assign the solution.
9039+
CHECK(FixFromAssignment(sat_solver->Assignment(), new_to_old_index,
9040+
context_));
9041+
return true;
9042+
}
9043+
9044+
// 2) Do a few rounds of inprocessing.
9045+
SatPresolveOptions options;
9046+
options.log_info = true; // log_info;
9047+
options.extract_binary_clauses_in_probing = false;
9048+
options.use_transitive_reduction = false;
9049+
options.deterministic_time_limit =
9050+
context_->params().presolve_probing_deterministic_time_limit();
9051+
options.use_equivalence_sat_sweeping =
9052+
context_->params().inprocessing_use_sat_sweeping();
9053+
9054+
auto* inprocessing = local_model.GetOrCreate<Inprocessing>();
9055+
inprocessing->ProvideLogger(logger_);
9056+
// TODO(user): re-index the variables to remove the unused ones between
9057+
// each iteration?
9058+
for (int i = 0; i < context_->params().max_presolve_iterations(); ++i) {
9059+
if (time_limit_->LimitReached()) break;
9060+
context_->UpdateRuleStats("presolve: iteration");
9061+
if (!inprocessing->PresolveLoop(options)) return false;
9062+
time_limit_->AdvanceDeterministicTime(
9063+
local_model.GetOrCreate<TimeLimit>()->GetElapsedDeterministicTime());
9064+
}
9065+
9066+
// 3) Extract the new clauses.
9067+
SatPostsolver sat_postsolver(num_variables);
9068+
for (const auto& c : local_model.GetOrCreate<PostsolveClauses>()->clauses) {
9069+
sat_postsolver.Add(c[0], c);
9070+
}
9071+
for (int i = 0; i < sat_solver->LiteralTrail().Index(); ++i) {
9072+
sat_postsolver.FixVariable(sat_solver->LiteralTrail()[i]);
9073+
}
9074+
// TODO(user): can we improve ExtractClauses() to avoid the intermediate
9075+
// container?
9076+
BasicClauseContainer clauses_container;
9077+
if (!sat_solver->ExtractClauses(&clauses_container)) return false;
9078+
ExtractClauses(/*merge_into_bool_and=*/true, new_to_old_index,
9079+
clauses_container, context_->working_model);
9080+
const std::string name =
9081+
absl::GetFlag(FLAGS_cp_model_debug_postsolve) ? "sat_postsolver" : "";
9082+
ExtractClauses(/*merge_into_bool_and=*/false, new_to_old_index,
9083+
sat_postsolver, context_->mapping_model, name);
9084+
9085+
// Update the constraints <-> variables graph.
9086+
context_->UpdateNewConstraintsVariableUsage();
9087+
9088+
// We mark as removed any variables removed by the pure SAT presolve.
9089+
// This is mainly to discover or avoid bug as we might have stale entries
9090+
// in our encoding hash-map for instance.
9091+
for (int var = 0; var < num_variables; ++var) {
9092+
if (context_->VarToConstraints(var).empty()) {
9093+
context_->MarkVariableAsRemoved(var);
9094+
}
9095+
}
9096+
return true;
9097+
}
9098+
89299099
void CpModelPresolver::ShiftObjectiveWithExactlyOnes() {
89309100
if (context_->ModelIsUnsat()) return;
89319101

@@ -13993,6 +14163,71 @@ bool CpModelPresolver::CanonicalizeAllLinears() {
1399314163
CpSolverStatus CpModelPresolver::Presolve() {
1399414164
context_->InitializeNewDomains();
1399514165

14166+
if (context_->params().cp_model_pure_sat_presolve()) {
14167+
context_->UpdateNewConstraintsVariableUsage();
14168+
DCHECK(context_->ConstraintVariableUsageIsConsistent());
14169+
14170+
if (!PresolvePureSatProblem()) {
14171+
(void)context_->NotifyThatModelIsUnsat(
14172+
"Proved Infeasible during SAT presolve");
14173+
return InfeasibleStatus();
14174+
}
14175+
14176+
// Sync the domains and initialize the mapping model variables.
14177+
context_->WriteVariableDomainsToProto();
14178+
14179+
// Starts the postsolve mapping model.
14180+
std::vector<int> fixed_postsolve_mapping;
14181+
InitializeMappingModelVariables(context_->AllDomains(),
14182+
&fixed_postsolve_mapping,
14183+
context_->mapping_model);
14184+
14185+
// Remove all the unused variables from the presolved model.
14186+
postsolve_mapping_->clear();
14187+
std::vector<int> mapping(context_->working_model->variables_size(), -1);
14188+
int num_unused_variables = 0;
14189+
for (int i = 0; i < context_->working_model->variables_size(); ++i) {
14190+
if (mapping[i] != -1) continue; // Already mapped.
14191+
if (context_->VariableWasRemoved(i)) continue;
14192+
14193+
// Deal with unused variables.
14194+
//
14195+
// If the variable is not fixed, we have multiple feasible solutions for
14196+
// this variable, so we can't remove it if we want all of them.
14197+
if (context_->VariableIsNotUsedAnymore(i) &&
14198+
(!context_->params().keep_all_feasible_solutions_in_presolve() ||
14199+
context_->IsFixed(i))) {
14200+
// Tricky. Variables that were not removed by a presolve rule should be
14201+
// fixed first during postsolve, so that more complex postsolve rules
14202+
// can use their values. One way to do that is to fix them here.
14203+
//
14204+
// We prefer to fix them to zero if possible.
14205+
++num_unused_variables;
14206+
FillDomainInProto(Domain(context_->DomainOf(i).SmallestValue()),
14207+
context_->mapping_model->mutable_variables(
14208+
fixed_postsolve_mapping[i]));
14209+
continue;
14210+
}
14211+
mapping[i] = postsolve_mapping_->size();
14212+
postsolve_mapping_->push_back(fixed_postsolve_mapping[i]);
14213+
}
14214+
context_->UpdateRuleStats(absl::StrCat("presolve: ", num_unused_variables,
14215+
" unused variables removed."));
14216+
14217+
MaybePermuteVariablesRandomly(mapping);
14218+
14219+
DCHECK(context_->ConstraintVariableUsageIsConsistent());
14220+
const int old_size = postsolve_mapping_->size();
14221+
ApplyVariableMapping(absl::MakeSpan(mapping), context_->working_model,
14222+
postsolve_mapping_);
14223+
CHECK_EQ(old_size, postsolve_mapping_->size());
14224+
14225+
// Compact all non-empty constraint at the beginning.
14226+
RemoveEmptyConstraints();
14227+
14228+
return LogAndValidatePresolvedModel();
14229+
}
14230+
1399614231
// If the objective is a floating point one, we scale it.
1399714232
//
1399814233
// TODO(user): We should probably try to delay this even more. For that we
@@ -14180,7 +14415,7 @@ CpSolverStatus CpModelPresolver::Presolve() {
1418014415
if (!time_limit_->LimitReached()) {
1418114416
if (!PresolvePureSatPart()) {
1418214417
(void)context_->NotifyThatModelIsUnsat(
14183-
"Proven Infeasible during SAT presolve");
14418+
"Proved Infeasible during SAT presolve");
1418414419
return InfeasibleStatus();
1418514420
}
1418614421
}
@@ -14386,7 +14621,7 @@ CpSolverStatus CpModelPresolver::Presolve() {
1438614621

1438714622
// Deal with unused variables.
1438814623
//
14389-
// If the variable is not fixed, we have multiple feasible solution for
14624+
// If the variable is not fixed, we have multiple feasible solutions for
1439014625
// this variable, so we can't remove it if we want all of them.
1439114626
if (context_->VariableIsNotUsedAnymore(i) &&
1439214627
(!context_->params().keep_all_feasible_solutions_in_presolve() ||
@@ -14420,21 +14655,7 @@ CpSolverStatus CpModelPresolver::Presolve() {
1442014655
context_->UpdateRuleStats(absl::StrCat("presolve: ", num_unused_variables,
1442114656
" unused variables removed."));
1442214657

14423-
if (context_->params().permute_variable_randomly()) {
14424-
// The mapping might merge variable, so we have to be careful here.
14425-
const int n = postsolve_mapping_->size();
14426-
std::vector<int> perm(n);
14427-
std::iota(perm.begin(), perm.end(), 0);
14428-
std::shuffle(perm.begin(), perm.end(), *context_->random());
14429-
for (int i = 0; i < context_->working_model->variables_size(); ++i) {
14430-
if (mapping[i] != -1) mapping[i] = perm[mapping[i]];
14431-
}
14432-
std::vector<int> new_postsolve_mapping(n);
14433-
for (int i = 0; i < n; ++i) {
14434-
new_postsolve_mapping[perm[i]] = (*postsolve_mapping_)[i];
14435-
}
14436-
*postsolve_mapping_ = std::move(new_postsolve_mapping);
14437-
}
14658+
MaybePermuteVariablesRandomly(mapping);
1443814659

1443914660
DCHECK(context_->ConstraintVariableUsageIsConsistent());
1444014661
CanonicalizeRoutesConstraintNodeExpressions(context_);
@@ -14452,7 +14673,28 @@ CpSolverStatus CpModelPresolver::Presolve() {
1445214673
context_->UpdateRuleStats(absl::StrCat(
1445314674
"deductions: ", context_->deductions.NumDeductions(), " stored"));
1445414675
}
14676+
return LogAndValidatePresolvedModel();
14677+
}
14678+
14679+
void CpModelPresolver::MaybePermuteVariablesRandomly(
14680+
std::vector<int>& mapping) {
14681+
if (!context_->params().permute_variable_randomly()) return;
14682+
// The mapping might merge variable, so we have to be careful here.
14683+
const int n = postsolve_mapping_->size();
14684+
std::vector<int> perm(n);
14685+
std::iota(perm.begin(), perm.end(), 0);
14686+
std::shuffle(perm.begin(), perm.end(), *context_->random());
14687+
for (int i = 0; i < context_->working_model->variables_size(); ++i) {
14688+
if (mapping[i] != -1) mapping[i] = perm[mapping[i]];
14689+
}
14690+
std::vector<int> new_postsolve_mapping(n);
14691+
for (int i = 0; i < n; ++i) {
14692+
new_postsolve_mapping[perm[i]] = (*postsolve_mapping_)[i];
14693+
}
14694+
*postsolve_mapping_ = std::move(new_postsolve_mapping);
14695+
}
1445514696

14697+
CpSolverStatus CpModelPresolver::LogAndValidatePresolvedModel() {
1445614698
// Stats and checks.
1445714699
if (logger_->LoggingIsEnabled()) context_->LogInfo();
1445814700

ortools/sat/cp_model_presolve.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,14 @@ class CpModelPresolver {
293293
bool* remove_subset, bool* remove_superset,
294294
bool* stop_processing_superset);
295295

296-
// Run SAT specific presolve code.
296+
// Runs SAT specific presolve code.
297297
// Returns false on UNSAT.
298298
bool PresolvePureSatPart();
299299

300+
// Runs a simplified presolve code for pure SAT problems.
301+
// Returns false on UNSAT.
302+
bool PresolvePureSatProblem();
303+
300304
// Extracts AtMostOne constraint from Linear constraint.
301305
void ExtractAtMostOneFromLinear(ConstraintProto* ct);
302306

@@ -398,6 +402,9 @@ class CpModelPresolver {
398402
std::string_view reason);
399403
ABSL_MUST_USE_RESULT bool MarkOptionalIntervalAsFalse(ConstraintProto* ct);
400404

405+
void MaybePermuteVariablesRandomly(std::vector<int>& mapping);
406+
CpSolverStatus LogAndValidatePresolvedModel();
407+
401408
std::vector<int>* postsolve_mapping_;
402409
PresolveContext* context_;
403410
SolutionCrush& solution_crush_;

ortools/sat/sat_parameters.proto

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ option java_multiple_files = true;
2424
// Contains the definitions for all the sat algorithm parameters and their
2525
// default values.
2626
//
27-
// NEXT TAG: 360
27+
// NEXT TAG: 361
2828
message SatParameters {
2929
// In some context, like in a portfolio of search, it makes sense to name a
3030
// given parameters set for logging purpose.
@@ -507,6 +507,10 @@ message SatParameters {
507507
// Whether we also use the sat presolve when cp_model_presolve is true.
508508
optional bool cp_model_use_sat_presolve = 93 [default = true];
509509

510+
// Whether to use a simplified pure sat presolve only when cp_model_presolve
511+
// is true. Crashes if the model is not pure SAT.
512+
optional bool cp_model_pure_sat_presolve = 360 [default = false];
513+
510514
// If we try to load at most ones and exactly ones constraints when running
511515
// the pure SAT presolve. Or if we just ignore them.
512516
//

0 commit comments

Comments
 (0)