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

914 lines
35 KiB
C++

// Copyright 2010-2025 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/probing.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <utility>
#include <vector>
#include "absl/container/btree_map.h"
#include "absl/container/btree_set.h"
#include "absl/container/inlined_vector.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/log/vlog_is_on.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "ortools/base/logging.h"
#include "ortools/base/strong_vector.h"
#include "ortools/base/timer.h"
#include "ortools/sat/clause.h"
#include "ortools/sat/implied_bounds.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_base.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_base.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/sat/sat_solver.h"
#include "ortools/sat/util.h"
#include "ortools/util/bitset.h"
#include "ortools/util/logging.h"
#include "ortools/util/sorted_interval_list.h"
#include "ortools/util/strong_integers.h"
#include "ortools/util/time_limit.h"
namespace operations_research {
namespace sat {
Prober::Prober(Model* model)
: trail_(*model->GetOrCreate<Trail>()),
assignment_(model->GetOrCreate<SatSolver>()->Assignment()),
integer_trail_(model->GetOrCreate<IntegerTrail>()),
implied_bounds_(model->GetOrCreate<ImpliedBounds>()),
product_detector_(model->GetOrCreate<ProductDetector>()),
sat_solver_(model->GetOrCreate<SatSolver>()),
time_limit_(model->GetOrCreate<TimeLimit>()),
implication_graph_(model->GetOrCreate<BinaryImplicationGraph>()),
logger_(model->GetOrCreate<SolverLogger>()) {}
bool Prober::ProbeBooleanVariables(const double deterministic_time_limit) {
const int num_variables = sat_solver_->NumVariables();
const VariablesAssignment& assignment = sat_solver_->Assignment();
std::vector<BooleanVariable> bool_vars;
for (BooleanVariable b(0); b < num_variables; ++b) {
if (assignment.VariableIsAssigned(b)) continue;
const Literal literal(b, true);
if (implication_graph_->RepresentativeOf(literal) != literal) {
continue;
}
bool_vars.push_back(b);
}
return ProbeBooleanVariables(deterministic_time_limit, bool_vars);
}
bool Prober::ProbeOneVariableInternal(BooleanVariable b) {
new_integer_bounds_.clear();
propagated_.ResetAllToFalse();
for (const Literal decision : {Literal(b, true), Literal(b, false)}) {
if (assignment_.LiteralIsAssigned(decision)) continue;
++num_decisions_;
CHECK_EQ(sat_solver_->CurrentDecisionLevel(), 0);
const int saved_index = trail_.Index();
sat_solver_->EnqueueDecisionAndBackjumpOnConflict(decision);
sat_solver_->AdvanceDeterministicTime(time_limit_);
if (sat_solver_->ModelIsUnsat()) return false;
if (sat_solver_->CurrentDecisionLevel() == 0) continue;
if (trail_.Index() > saved_index) {
if (callback_ != nullptr) callback_(decision);
}
if (!implied_bounds_->ProcessIntegerTrail(decision)) return false;
product_detector_->ProcessTrailAtLevelOne();
integer_trail_->AppendNewBounds(&new_integer_bounds_);
for (int i = saved_index + 1; i < trail_.Index(); ++i) {
const Literal l = trail_[i];
// We mark on the first run (b.IsPositive()) and check on the second.
if (decision.IsPositive()) {
propagated_.Set(l.Index());
} else {
if (propagated_[l]) {
to_fix_at_true_.push_back(l);
}
}
// Anything not propagated by the BinaryImplicationGraph is a "new"
// binary clause. This is because the BinaryImplicationGraph has the
// highest priority of all propagators.
if (trail_.AssignmentType(l.Variable()) !=
implication_graph_->PropagatorId()) {
new_binary_clauses_.push_back({decision.Negated(), l});
}
}
// Fix variable and add new binary clauses.
if (!sat_solver_->ResetToLevelZero()) return false;
for (const Literal l : to_fix_at_true_) {
if (!sat_solver_->AddUnitClause(l)) return false;
}
to_fix_at_true_.clear();
if (!sat_solver_->FinishPropagation()) return false;
num_new_binary_ += new_binary_clauses_.size();
for (auto binary : new_binary_clauses_) {
sat_solver_->AddBinaryClause(binary.first, binary.second);
}
new_binary_clauses_.clear();
if (!sat_solver_->FinishPropagation()) return false;
}
// We have at most two lower bounds for each variables (one for b==0 and one
// for b==1), so the min of the two is a valid level zero bound! More
// generally, the domain of a variable can be intersected with the union
// of the two propagated domains. This also allow to detect "holes".
//
// TODO(user): More generally, for any clauses (b or not(b) is one), we
// could probe all the literal inside, and for any integer variable, we can
// take the union of the propagated domain as a new domain.
//
// TODO(user): fix binary variable in the same way? It might not be as
// useful since probing on such variable will also fix it. But then we might
// abort probing early, so it might still be good.
std::sort(new_integer_bounds_.begin(), new_integer_bounds_.end(),
[](IntegerLiteral a, IntegerLiteral b) { return a.var < b.var; });
// This is used for the hole detection.
IntegerVariable prev_var = kNoIntegerVariable;
IntegerValue lb_max = kMinIntegerValue;
IntegerValue ub_min = kMaxIntegerValue;
new_integer_bounds_.push_back(IntegerLiteral()); // Sentinel.
const int limit = new_integer_bounds_.size();
for (int i = 0; i < limit; ++i) {
const IntegerVariable var = new_integer_bounds_[i].var;
// Hole detection.
if (i > 0 && PositiveVariable(var) != prev_var) {
if (ub_min + 1 < lb_max) {
// The variable cannot take value in (ub_min, lb_max) !
//
// TODO(user): do not create domain with a complexity that is too
// large?
const Domain old_domain =
integer_trail_->InitialVariableDomain(prev_var);
const Domain new_domain = old_domain.IntersectionWith(
Domain(ub_min.value() + 1, lb_max.value() - 1).Complement());
if (new_domain != old_domain) {
++num_new_holes_;
if (!integer_trail_->UpdateInitialDomain(prev_var, new_domain)) {
return false;
}
}
}
// Reinitialize.
lb_max = kMinIntegerValue;
ub_min = kMaxIntegerValue;
}
prev_var = PositiveVariable(var);
if (VariableIsPositive(var)) {
lb_max = std::max(lb_max, new_integer_bounds_[i].bound);
} else {
ub_min = std::min(ub_min, -new_integer_bounds_[i].bound);
}
// Bound tightening.
if (i == 0 || new_integer_bounds_[i - 1].var != var) continue;
const IntegerValue new_bound = std::min(new_integer_bounds_[i - 1].bound,
new_integer_bounds_[i].bound);
if (new_bound > integer_trail_->LowerBound(var)) {
++num_new_integer_bounds_;
if (!integer_trail_->Enqueue(
IntegerLiteral::GreaterOrEqual(var, new_bound), {}, {})) {
return false;
}
}
}
// We might have updated some integer domain, let's propagate.
return sat_solver_->FinishPropagation();
}
bool Prober::ProbeOneVariable(BooleanVariable b) {
// Resize the propagated sparse bitset.
const int num_variables = sat_solver_->NumVariables();
propagated_.ClearAndResize(LiteralIndex(2 * num_variables));
// Reset the solver in case it was already used.
if (!sat_solver_->ResetToLevelZero()) return false;
const int initial_num_fixed = sat_solver_->LiteralTrail().Index();
if (!ProbeOneVariableInternal(b)) return false;
// Statistics
const int num_fixed = sat_solver_->LiteralTrail().Index();
num_new_literals_fixed_ += num_fixed - initial_num_fixed;
return true;
}
bool Prober::ProbeBooleanVariables(
const double deterministic_time_limit,
absl::Span<const BooleanVariable> bool_vars) {
WallTimer wall_timer;
wall_timer.Start();
// Reset statistics.
num_decisions_ = 0;
num_new_binary_ = 0;
num_new_holes_ = 0;
num_new_integer_bounds_ = 0;
num_new_literals_fixed_ = 0;
// Resize the propagated sparse bitset.
const int num_variables = sat_solver_->NumVariables();
propagated_.ClearAndResize(LiteralIndex(2 * num_variables));
// Reset the solver in case it was already used.
if (!sat_solver_->ResetToLevelZero()) return false;
const int initial_num_fixed = sat_solver_->LiteralTrail().Index();
const double initial_deterministic_time =
time_limit_->GetElapsedDeterministicTime();
const double limit = initial_deterministic_time + deterministic_time_limit;
bool limit_reached = false;
int num_probed = 0;
for (const BooleanVariable b : bool_vars) {
const Literal literal(b, true);
if (implication_graph_->RepresentativeOf(literal) != literal) {
continue;
}
// TODO(user): Instead of an hard deterministic limit, we should probably
// use a lower one, but reset it each time we have found something useful.
if (time_limit_->LimitReached() ||
time_limit_->GetElapsedDeterministicTime() > limit) {
limit_reached = true;
break;
}
// Propagate b=1 and then b=0.
++num_probed;
if (!ProbeOneVariableInternal(b)) {
return false;
}
}
// Update stats.
const int num_fixed = sat_solver_->LiteralTrail().Index();
num_new_literals_fixed_ = num_fixed - initial_num_fixed;
// Display stats.
if (logger_->LoggingIsEnabled()) {
const double time_diff =
time_limit_->GetElapsedDeterministicTime() - initial_deterministic_time;
SOLVER_LOG(logger_, "[Probing] deterministic_time: ", time_diff,
" (limit: ", deterministic_time_limit,
") wall_time: ", wall_timer.Get(), " (",
(limit_reached ? "Aborted " : ""), num_probed, "/",
bool_vars.size(), ")");
if (num_new_literals_fixed_ > 0) {
SOLVER_LOG(logger_,
"[Probing] - new fixed Boolean: ", num_new_literals_fixed_,
" (", FormatCounter(num_fixed), "/",
FormatCounter(sat_solver_->NumVariables()), ")");
}
if (num_new_holes_ > 0) {
SOLVER_LOG(logger_, "[Probing] - new integer holes: ",
FormatCounter(num_new_holes_));
}
if (num_new_integer_bounds_ > 0) {
SOLVER_LOG(logger_, "[Probing] - new integer bounds: ",
FormatCounter(num_new_integer_bounds_));
}
if (num_new_binary_ > 0) {
SOLVER_LOG(logger_, "[Probing] - new binary clause: ",
FormatCounter(num_new_binary_));
}
}
return true;
}
bool Prober::ProbeDnf(absl::string_view name,
absl::Span<const std::vector<Literal>> dnf) {
if (dnf.size() <= 1) return true;
// Reset the solver in case it was already used.
if (!sat_solver_->ResetToLevelZero()) return false;
always_propagated_bounds_.clear();
always_propagated_literals_.clear();
int num_valid_conjunctions = 0;
for (const std::vector<Literal>& conjunction : dnf) {
if (!sat_solver_->ResetToLevelZero()) return false;
if (num_valid_conjunctions > 0 && always_propagated_bounds_.empty() &&
always_propagated_literals_.empty()) {
// We can exit safely as nothing will be propagated.
return true;
}
bool conjunction_is_valid = true;
int num_literals_enqueued = 0;
const int root_trail_index = trail_.Index();
const int root_integer_trail_index = integer_trail_->Index();
for (const Literal& lit : conjunction) {
if (assignment_.LiteralIsAssigned(lit)) {
if (assignment_.LiteralIsTrue(lit)) continue;
conjunction_is_valid = false;
break;
}
++num_literals_enqueued;
const int decision_level_before_enqueue =
sat_solver_->CurrentDecisionLevel();
sat_solver_->EnqueueDecisionAndBackjumpOnConflict(lit);
sat_solver_->AdvanceDeterministicTime(time_limit_);
const int decision_level_after_enqueue =
sat_solver_->CurrentDecisionLevel();
++num_decisions_;
if (sat_solver_->ModelIsUnsat()) return false;
// If the literal has been pushed without any conflict, the level should
// have been increased.
if (decision_level_after_enqueue <= decision_level_before_enqueue) {
conjunction_is_valid = false;
break;
}
// TODO(user): Can we use the callback_?
}
if (conjunction_is_valid) {
++num_valid_conjunctions;
} else {
continue;
}
// Process propagated literals.
new_propagated_literals_.clear();
for (int i = root_trail_index; i < trail_.Index(); ++i) {
const LiteralIndex literal_index = trail_[i].Index();
if (num_valid_conjunctions == 1 ||
always_propagated_literals_.contains(literal_index)) {
new_propagated_literals_.insert(literal_index);
}
}
std::swap(new_propagated_literals_, always_propagated_literals_);
// Process propagated integer bounds.
new_integer_bounds_.clear();
integer_trail_->AppendNewBoundsFrom(root_integer_trail_index,
&new_integer_bounds_);
new_propagated_bounds_.clear();
for (const IntegerLiteral entry : new_integer_bounds_) {
const auto it = always_propagated_bounds_.find(entry.var);
if (num_valid_conjunctions == 1) { // First loop.
new_propagated_bounds_[entry.var] = entry.bound;
} else if (it != always_propagated_bounds_.end()) {
new_propagated_bounds_[entry.var] = std::min(entry.bound, it->second);
}
}
std::swap(new_propagated_bounds_, always_propagated_bounds_);
}
if (!sat_solver_->ResetToLevelZero()) return false;
// Fix literals implied by the dnf.
const int previous_num_literals_fixed = num_new_literals_fixed_;
for (const LiteralIndex literal_index : always_propagated_literals_) {
const Literal lit(literal_index);
if (assignment_.LiteralIsTrue(lit)) continue;
++num_new_literals_fixed_;
if (!sat_solver_->AddUnitClause(lit)) return false;
}
// Fix integer bounds implied by the dnf.
int previous_num_integer_bounds = num_new_integer_bounds_;
for (const auto& [var, bound] : always_propagated_bounds_) {
if (bound > integer_trail_->LowerBound(var)) {
++num_new_integer_bounds_;
if (!integer_trail_->Enqueue(IntegerLiteral::GreaterOrEqual(var, bound),
{}, {})) {
return false;
}
}
}
if (!sat_solver_->FinishPropagation()) return false;
if (num_new_integer_bounds_ > previous_num_integer_bounds ||
num_new_literals_fixed_ > previous_num_literals_fixed) {
VLOG(1) << "ProbeDnf(" << name << ", num_fixed_literals="
<< num_new_literals_fixed_ - previous_num_literals_fixed
<< ", num_pushed_integer_bounds="
<< num_new_integer_bounds_ - previous_num_integer_bounds
<< ", num_valid_conjunctions=" << num_valid_conjunctions << "/"
<< dnf.size() << ")";
}
return true;
}
bool LookForTrivialSatSolution(double deterministic_time_limit, Model* model,
SolverLogger* logger) {
WallTimer wall_timer;
wall_timer.Start();
// Hack to not have empty logger.
if (logger == nullptr) logger = model->GetOrCreate<SolverLogger>();
// Reset the solver in case it was already used.
auto* sat_solver = model->GetOrCreate<SatSolver>();
if (!sat_solver->ResetToLevelZero()) return false;
auto* time_limit = model->GetOrCreate<TimeLimit>();
const int initial_num_fixed = sat_solver->LiteralTrail().Index();
// Note that this code do not care about the non-Boolean part and just try to
// assign the existing Booleans.
SatParameters initial_params = *model->GetOrCreate<SatParameters>();
SatParameters new_params = initial_params;
new_params.set_log_search_progress(false);
new_params.set_max_number_of_conflicts(1);
new_params.set_max_deterministic_time(deterministic_time_limit);
double elapsed_dtime = 0.0;
const int num_times = 1000;
bool limit_reached = false;
auto* random = model->GetOrCreate<ModelRandomGenerator>();
for (int i = 0; i < num_times; ++i) {
if (time_limit->LimitReached() ||
elapsed_dtime > deterministic_time_limit) {
limit_reached = true;
break;
}
// SetParameters() reset the deterministic time to zero inside time_limit.
sat_solver->SetParameters(new_params);
sat_solver->ResetDecisionHeuristic();
const SatSolver::Status result = sat_solver->SolveWithTimeLimit(time_limit);
elapsed_dtime += time_limit->GetElapsedDeterministicTime();
if (result == SatSolver::FEASIBLE) {
SOLVER_LOG(logger, "Trivial exploration found feasible solution!");
time_limit->AdvanceDeterministicTime(elapsed_dtime);
return true;
}
if (!sat_solver->ResetToLevelZero()) {
SOLVER_LOG(logger, "UNSAT during trivial exploration heuristic.");
time_limit->AdvanceDeterministicTime(elapsed_dtime);
return false;
}
// We randomize at the end so that the default params is executed
// at least once.
RandomizeDecisionHeuristic(*random, &new_params);
new_params.set_random_seed(i);
new_params.set_max_deterministic_time(deterministic_time_limit -
elapsed_dtime);
}
// Restore the initial parameters.
sat_solver->SetParameters(initial_params);
sat_solver->ResetDecisionHeuristic();
time_limit->AdvanceDeterministicTime(elapsed_dtime);
if (!sat_solver->ResetToLevelZero()) return false;
if (logger->LoggingIsEnabled()) {
const int num_fixed = sat_solver->LiteralTrail().Index();
const int num_newly_fixed = num_fixed - initial_num_fixed;
const int num_variables = sat_solver->NumVariables();
SOLVER_LOG(logger, "[Random exploration]", " num_fixed: +",
FormatCounter(num_newly_fixed), " (", FormatCounter(num_fixed),
"/", FormatCounter(num_variables), ")",
" dtime: ", elapsed_dtime, "/", deterministic_time_limit,
" wtime: ", wall_timer.Get(),
(limit_reached ? " (Aborted)" : ""));
}
return sat_solver->FinishPropagation();
}
bool FailedLiteralProbingRound(ProbingOptions options, Model* model) {
WallTimer wall_timer;
wall_timer.Start();
options.log_info |= VLOG_IS_ON(1);
// Reset the solver in case it was already used.
auto* sat_solver = model->GetOrCreate<SatSolver>();
if (!sat_solver->ResetToLevelZero()) return false;
// When called from Inprocessing, the implication graph should already be a
// DAG, so these two calls should return right away. But we do need them to
// get the topological order if this is used in isolation.
auto* implication_graph = model->GetOrCreate<BinaryImplicationGraph>();
if (!implication_graph->DetectEquivalences()) return false;
if (!sat_solver->FinishPropagation()) return false;
auto* time_limit = model->GetOrCreate<TimeLimit>();
const int initial_num_fixed = sat_solver->LiteralTrail().Index();
const double initial_deterministic_time =
time_limit->GetElapsedDeterministicTime();
const double limit = initial_deterministic_time + options.deterministic_limit;
const int num_variables = sat_solver->NumVariables();
SparseBitset<LiteralIndex> processed(LiteralIndex(2 * num_variables));
int64_t num_probed = 0;
int64_t num_explicit_fix = 0;
int64_t num_conflicts = 0;
int64_t num_new_binary = 0;
int64_t num_subsumed = 0;
const auto& trail = *(model->Get<Trail>());
const auto& assignment = trail.Assignment();
auto* clause_manager = model->GetOrCreate<ClauseManager>();
const int id = implication_graph->PropagatorId();
const int clause_id = clause_manager->PropagatorId();
// This is only needed when options.use_queue is true.
struct SavedNextLiteral {
LiteralIndex literal_index; // kNoLiteralIndex if we need to backtrack.
int rank; // Cached position_in_order, we prefer lower positions.
bool operator<(const SavedNextLiteral& o) const { return rank < o.rank; }
};
std::vector<SavedNextLiteral> queue;
util_intops::StrongVector<LiteralIndex, int> position_in_order;
// This is only needed when options use_queue is false;
util_intops::StrongVector<LiteralIndex, int> starts;
if (!options.use_queue) starts.resize(2 * num_variables, 0);
// We delay fixing of already assigned literal once we go back to level
// zero.
std::vector<Literal> to_fix;
// Depending on the options. we do not use the same order.
// With tree look, it is better to start with "leaf" first since we try
// to reuse propagation as much as possible. This is also interesting to
// do when extracting binary clauses since we will need to propagate
// everyone anyway, and this should result in less clauses that can be
// removed later by transitive reduction.
//
// However, without tree-look and without the need to extract all binary
// clauses, it is better to just probe the root of the binary implication
// graph. This is exactly what happen when we probe using the topological
// order.
int order_index(0);
std::vector<LiteralIndex> probing_order =
implication_graph->ReverseTopologicalOrder();
if (!options.use_tree_look && !options.extract_binary_clauses) {
std::reverse(probing_order.begin(), probing_order.end());
}
// We only use this for the queue version.
if (options.use_queue) {
position_in_order.assign(2 * num_variables, -1);
for (int i = 0; i < probing_order.size(); ++i) {
position_in_order[probing_order[i]] = i;
}
}
while (!time_limit->LimitReached() &&
time_limit->GetElapsedDeterministicTime() <= limit) {
// We only enqueue literal at level zero if we don't use "tree look".
if (!options.use_tree_look) sat_solver->Backtrack(0);
LiteralIndex next_decision = kNoLiteralIndex;
if (options.use_queue && sat_solver->CurrentDecisionLevel() > 0) {
// TODO(user): Instead of minimizing index in topo order (which might be
// nice for binary extraction), we could try to maximize reusability in
// some way.
const Literal prev_decision =
sat_solver->Decisions()[sat_solver->CurrentDecisionLevel() - 1]
.literal;
const absl::Span<const Literal> list =
implication_graph->Implications(prev_decision.Negated());
const int saved_queue_size = queue.size();
for (const Literal l : list) {
const Literal candidate = l.Negated();
if (processed[candidate]) continue;
if (position_in_order[candidate] == -1) continue;
if (assignment.LiteralIsAssigned(candidate)) {
if (assignment.LiteralIsFalse(candidate)) {
to_fix.push_back(Literal(candidate.Negated()));
}
continue;
}
queue.push_back({candidate.Index(), -position_in_order[candidate]});
}
std::sort(queue.begin() + saved_queue_size, queue.end());
// Probe a literal that implies previous decision.
while (!queue.empty()) {
const LiteralIndex index = queue.back().literal_index;
queue.pop_back();
if (index == kNoLiteralIndex) {
// This is a backtrack marker, go back one level.
CHECK_GT(sat_solver->CurrentDecisionLevel(), 0);
sat_solver->Backtrack(sat_solver->CurrentDecisionLevel() - 1);
continue;
}
const Literal candidate(index);
if (processed[candidate]) continue;
if (assignment.LiteralIsAssigned(candidate)) {
if (assignment.LiteralIsFalse(candidate)) {
to_fix.push_back(Literal(candidate.Negated()));
}
continue;
}
next_decision = candidate.Index();
break;
}
}
if (sat_solver->CurrentDecisionLevel() == 0) {
// Fix any delayed fixed literal.
for (const Literal literal : to_fix) {
if (!assignment.LiteralIsTrue(literal)) {
++num_explicit_fix;
if (!sat_solver->AddUnitClause(literal)) return false;
}
}
to_fix.clear();
if (!sat_solver->FinishPropagation()) return false;
// Probe an unexplored node.
for (; order_index < probing_order.size(); ++order_index) {
const Literal candidate(probing_order[order_index]);
if (processed[candidate]) continue;
if (assignment.LiteralIsAssigned(candidate)) continue;
next_decision = candidate.Index();
break;
}
// The pass is finished.
if (next_decision == kNoLiteralIndex) break;
} else if (next_decision == kNoLiteralIndex) {
const int level = sat_solver->CurrentDecisionLevel();
const Literal prev_decision = sat_solver->Decisions()[level - 1].literal;
const absl::Span<const Literal> list =
implication_graph->Implications(prev_decision.Negated());
// Probe a literal that implies previous decision.
//
// Note that contrary to the queue based implementation, this do not
// process them in a particular order.
int j = starts[prev_decision.NegatedIndex()];
for (int i = 0; i < list.size(); ++i, ++j) {
j %= list.size();
const Literal candidate = Literal(list[j]).Negated();
if (processed[candidate]) continue;
if (assignment.LiteralIsFalse(candidate)) {
// candidate => previous => not(candidate), so we can fix it.
to_fix.push_back(Literal(candidate.Negated()));
continue;
}
// This shouldn't happen if extract_binary_clauses is false.
// We have an equivalence.
if (assignment.LiteralIsTrue(candidate)) continue;
next_decision = candidate.Index();
break;
}
starts[prev_decision.NegatedIndex()] = j;
if (next_decision == kNoLiteralIndex) {
sat_solver->Backtrack(level - 1);
continue;
}
}
++num_probed;
processed.Set(next_decision);
CHECK_NE(next_decision, kNoLiteralIndex);
queue.push_back({kNoLiteralIndex, 0}); // Backtrack marker.
const int level = sat_solver->CurrentDecisionLevel();
const int first_new_trail_index =
sat_solver->EnqueueDecisionAndBackjumpOnConflict(
Literal(next_decision));
const int new_level = sat_solver->CurrentDecisionLevel();
sat_solver->AdvanceDeterministicTime(time_limit);
if (sat_solver->ModelIsUnsat()) return false;
if (new_level <= level) {
++num_conflicts;
// Sync the queue with the new level.
if (options.use_queue) {
if (new_level == 0) {
queue.clear();
} else {
int queue_level = level + 1;
while (queue_level > new_level) {
CHECK(!queue.empty());
if (queue.back().literal_index == kNoLiteralIndex) --queue_level;
queue.pop_back();
}
}
}
// Fix `next_decision` to `false` if not already done.
//
// Even if we fixed something at level zero, next_decision might not be
// fixed! But we can fix it. It can happen because when we propagate
// with clauses, we might have `a => b` but not `not(b) => not(a)`. Like
// `a => b` and clause `(not(a), not(b), c)`, propagating `a` will set
// `c`, but propagating `not(c)` will not do anything.
//
// We "delay" the fixing if we are not at level zero so that we can
// still reuse the current propagation work via tree look.
//
// TODO(user): Can we be smarter here? Maybe we can still fix the
// literal without going back to level zero by simply enqueuing it with
// no reason? it will be backtracked over, but we will still lazily fix
// it later.
if (sat_solver->CurrentDecisionLevel() != 0 ||
assignment.LiteralIsFalse(Literal(next_decision))) {
to_fix.push_back(Literal(next_decision).Negated());
}
}
// Inspect the newly propagated literals. Depending on the options, try to
// extract binary clauses via hyper binary resolution and/or mark the
// literals on the trail so that they do not need to be probed later.
if (new_level == 0) continue;
const Literal last_decision =
sat_solver->Decisions()[new_level - 1].literal;
int num_new_subsumed = 0;
for (int i = first_new_trail_index; i < trail.Index(); ++i) {
const Literal l = trail[i];
if (l == last_decision) continue;
// If we can extract a binary clause that subsume the reason clause, we
// do add the binary and remove the subsumed clause.
//
// TODO(user): We could be slightly more generic and subsume some
// clauses that do not contains last_decision.Negated().
bool subsumed = false;
if (options.subsume_with_binary_clause &&
trail.AssignmentType(l.Variable()) == clause_id) {
for (const Literal lit : trail.Reason(l.Variable())) {
if (lit == last_decision.Negated()) {
subsumed = true;
break;
}
}
if (subsumed) {
++num_new_subsumed;
++num_new_binary;
CHECK(implication_graph->AddBinaryClause(last_decision.Negated(), l));
const int trail_index = trail.Info(l.Variable()).trail_index;
int test = 0;
for (const Literal lit :
clause_manager->ReasonClause(trail_index)->AsSpan()) {
if (lit == l) ++test;
if (lit == last_decision.Negated()) ++test;
}
CHECK_EQ(test, 2);
clause_manager->LazyDetach(clause_manager->ReasonClause(trail_index));
// We need to change the reason now that the clause is cleared.
implication_graph->ChangeReason(trail_index, last_decision);
}
}
if (options.extract_binary_clauses) {
// Anything not propagated by the BinaryImplicationGraph is a "new"
// binary clause. This is because the BinaryImplicationGraph has the
// highest priority of all propagators.
//
// Note(user): This is not 100% true, since when we launch the clause
// propagation for one literal we do finish it before calling again
// the binary propagation.
//
// TODO(user): Think about trying to extract clause that will not
// get removed by transitive reduction later. If we can both extract
// a => c and b => c , ideally we don't want to extract a => c first
// if we already know that a => b.
//
// TODO(user): Similar to previous point, we could find the LCA
// of all literals in the reason for this propagation. And use this
// as a reason for later hyber binary resolution. Like we do when
// this clause subsume the reason.
if (!subsumed && trail.AssignmentType(l.Variable()) != id) {
++num_new_binary;
CHECK(implication_graph->AddBinaryClause(last_decision.Negated(), l));
}
} else {
// If we don't extract binary, we don't need to explore any of
// these literal until more variables are fixed.
processed.Set(l.Index());
}
}
// Inspect the watcher list for last_decision, If we have a blocking
// literal at true (implied by last decision), then we have subsumptions.
//
// The intuition behind this is that if a binary clause (a,b) subsume a
// clause, and we watch a.Negated() for this clause with a blocking
// literal b, then this watch entry will never change because we always
// propagate binary clauses first and the blocking literal will always be
// true. So after many propagations, we hope to have such configuration
// which is quite cheap to test here.
if (options.subsume_with_binary_clause) {
// Tricky: If we have many "decision" and we do not extract the binary
// clause, then the fact that last_decision => literal might not be
// currently encoded in the problem clause, so if we use that relation
// to subsume, we should make sure it is added.
//
// Note that it is okay to add duplicate binary clause, we will clean that
// later.
const bool always_add_binary = sat_solver->CurrentDecisionLevel() > 1 &&
!options.extract_binary_clauses;
for (const auto& w :
clause_manager->WatcherListOnFalse(last_decision.Negated())) {
if (assignment.LiteralIsTrue(w.blocking_literal)) {
if (w.clause->IsRemoved()) continue;
CHECK_NE(w.blocking_literal, last_decision.Negated());
// Add the binary clause if needed. Note that we change the reason
// to a binary one so that we never add the same clause twice.
//
// Tricky: while last_decision would be a valid reason, we need a
// reason that was assigned before this literal, so we use the
// decision at the level where this literal was assigned which is an
// even better reason. Maybe it is just better to change all the
// reason above to a binary one so we don't have an issue here.
if (always_add_binary ||
trail.AssignmentType(w.blocking_literal.Variable()) != id) {
// If the variable was true at level zero, there is no point
// adding the clause.
const auto& info = trail.Info(w.blocking_literal.Variable());
if (info.level > 0) {
++num_new_binary;
CHECK(implication_graph->AddBinaryClause(last_decision.Negated(),
w.blocking_literal));
const Literal d = sat_solver->Decisions()[info.level - 1].literal;
if (d != w.blocking_literal) {
implication_graph->ChangeReason(info.trail_index, d);
}
}
}
++num_new_subsumed;
clause_manager->LazyDetach(w.clause);
}
}
}
if (num_new_subsumed > 0) {
// TODO(user): We might just want to do that even more lazily by
// checking for detached clause while propagating here? and do a big
// cleanup at the end.
clause_manager->CleanUpWatchers();
num_subsumed += num_new_subsumed;
}
}
if (!sat_solver->ResetToLevelZero()) return false;
for (const Literal literal : to_fix) {
++num_explicit_fix;
if (!sat_solver->AddUnitClause(literal)) return false;
}
to_fix.clear();
if (!sat_solver->FinishPropagation()) return false;
// Display stats.
const int num_fixed = sat_solver->LiteralTrail().Index();
const int num_newly_fixed = num_fixed - initial_num_fixed;
const double time_diff =
time_limit->GetElapsedDeterministicTime() - initial_deterministic_time;
const bool limit_reached = time_limit->LimitReached() ||
time_limit->GetElapsedDeterministicTime() > limit;
LOG_IF(INFO, options.log_info)
<< "Probing. "
<< " num_probed: " << num_probed << "/" << probing_order.size()
<< " num_fixed: +" << num_newly_fixed << " (" << num_fixed << "/"
<< num_variables << ")"
<< " explicit_fix:" << num_explicit_fix
<< " num_conflicts:" << num_conflicts
<< " new_binary_clauses: " << num_new_binary
<< " subsumed: " << num_subsumed << " dtime: " << time_diff
<< " wtime: " << wall_timer.Get() << (limit_reached ? " (Aborted)" : "");
return sat_solver->FinishPropagation();
}
} // namespace sat
} // namespace operations_research