520 lines
18 KiB
C++
520 lines
18 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/sat_decision.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/algorithm/container.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/base/strong_vector.h"
|
|
#include "ortools/sat/model.h"
|
|
#include "ortools/sat/sat_base.h"
|
|
#include "ortools/sat/sat_parameters.pb.h"
|
|
#include "ortools/sat/synchronization.h"
|
|
#include "ortools/sat/util.h"
|
|
#include "ortools/util/bitset.h"
|
|
#include "ortools/util/integer_pq.h"
|
|
#include "ortools/util/strong_integers.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
SatDecisionPolicy::SatDecisionPolicy(Model* model)
|
|
: parameters_(*(model->GetOrCreate<SatParameters>())),
|
|
trail_(*model->GetOrCreate<Trail>()),
|
|
random_(model->GetOrCreate<ModelRandomGenerator>()),
|
|
ls_hints_(model->GetOrCreate<SharedLsSolutionRepository>()) {}
|
|
|
|
void SatDecisionPolicy::IncreaseNumVariables(int num_variables) {
|
|
const int old_num_variables = activities_.size();
|
|
DCHECK_GE(num_variables, activities_.size());
|
|
|
|
activities_.resize(num_variables, parameters_.initial_variables_activity());
|
|
tie_breakers_.resize(num_variables, 0.0);
|
|
num_bumps_.clear();
|
|
pq_need_update_for_var_at_trail_index_.IncreaseSize(num_variables);
|
|
|
|
has_forced_polarity_.resize(num_variables, false);
|
|
forced_polarity_.resize(num_variables);
|
|
has_target_polarity_.resize(num_variables, false);
|
|
target_polarity_.resize(num_variables);
|
|
var_polarity_.resize(num_variables);
|
|
|
|
ResetInitialPolarity(/*from=*/old_num_variables);
|
|
|
|
// Update the priority queue. Note that each addition is in O(1) because
|
|
// the priority is 0.0.
|
|
var_ordering_.Reserve(num_variables);
|
|
if (var_ordering_is_initialized_) {
|
|
for (BooleanVariable var(old_num_variables); var < num_variables; ++var) {
|
|
var_ordering_.Add({var, 0.0, activities_[var]});
|
|
}
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::BeforeConflict(int trail_index) {
|
|
if (parameters_.use_erwa_heuristic()) {
|
|
++num_conflicts_;
|
|
num_conflicts_stack_.push_back({trail_.Index(), 1});
|
|
}
|
|
|
|
if (trail_index > target_length_) {
|
|
target_length_ = trail_index;
|
|
has_target_polarity_.assign(has_target_polarity_.size(), false);
|
|
for (int i = 0; i < trail_index; ++i) {
|
|
const Literal l = trail_[i];
|
|
has_target_polarity_[l.Variable()] = true;
|
|
target_polarity_[l.Variable()] = l.IsPositive();
|
|
}
|
|
}
|
|
|
|
if (trail_index > best_partial_assignment_.size()) {
|
|
best_partial_assignment_.assign(trail_.IteratorAt(0),
|
|
trail_.IteratorAt(trail_index));
|
|
}
|
|
|
|
--num_conflicts_until_rephase_;
|
|
RephaseIfNeeded();
|
|
}
|
|
|
|
void SatDecisionPolicy::RephaseIfNeeded() {
|
|
if (parameters_.polarity_rephase_increment() <= 0) return;
|
|
if (num_conflicts_until_rephase_ > 0) return;
|
|
|
|
VLOG(1) << "End of polarity phase " << polarity_phase_
|
|
<< " target_length: " << target_length_
|
|
<< " best_length: " << best_partial_assignment_.size();
|
|
|
|
++polarity_phase_;
|
|
num_conflicts_until_rephase_ =
|
|
parameters_.polarity_rephase_increment() * (polarity_phase_ + 1);
|
|
|
|
// We always reset the target each time we change phase.
|
|
target_length_ = 0;
|
|
has_target_polarity_.assign(has_target_polarity_.size(), false);
|
|
|
|
// Cycle between different initial polarities. Note that we already start by
|
|
// the default polarity, and this code is reached the first time with a
|
|
// polarity_phase_ of 1.
|
|
switch (polarity_phase_ % 8) {
|
|
case 0:
|
|
ResetInitialPolarity(/*from=*/0);
|
|
break;
|
|
case 1:
|
|
UseLongestAssignmentAsInitialPolarity();
|
|
break;
|
|
case 2:
|
|
ResetInitialPolarity(/*from=*/0, /*inverted=*/true);
|
|
break;
|
|
case 3:
|
|
UseLongestAssignmentAsInitialPolarity();
|
|
break;
|
|
case 4:
|
|
RandomizeCurrentPolarity();
|
|
break;
|
|
case 5:
|
|
UseLongestAssignmentAsInitialPolarity();
|
|
break;
|
|
case 6:
|
|
FlipCurrentPolarity();
|
|
break;
|
|
case 7:
|
|
if (UseLsSolutionAsInitialPolarity()) break;
|
|
UseLongestAssignmentAsInitialPolarity();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::ResetDecisionHeuristic() {
|
|
const int num_variables = activities_.size();
|
|
variable_activity_increment_ = 1.0;
|
|
activities_.assign(num_variables, parameters_.initial_variables_activity());
|
|
tie_breakers_.assign(num_variables, 0.0);
|
|
num_bumps_.clear();
|
|
var_ordering_.Clear();
|
|
|
|
polarity_phase_ = 0;
|
|
num_conflicts_until_rephase_ = parameters_.polarity_rephase_increment();
|
|
|
|
ResetInitialPolarity(/*from=*/0);
|
|
has_target_polarity_.assign(num_variables, false);
|
|
has_forced_polarity_.assign(num_variables, false);
|
|
best_partial_assignment_.clear();
|
|
|
|
num_conflicts_ = 0;
|
|
num_conflicts_stack_.clear();
|
|
|
|
var_ordering_is_initialized_ = false;
|
|
}
|
|
|
|
void SatDecisionPolicy::ResetInitialPolarity(int from, bool inverted) {
|
|
// Sets the initial polarity.
|
|
const int num_variables = activities_.size();
|
|
for (BooleanVariable var(from); var < num_variables; ++var) {
|
|
switch (parameters_.initial_polarity()) {
|
|
case SatParameters::POLARITY_TRUE:
|
|
var_polarity_[var] = inverted ? false : true;
|
|
break;
|
|
case SatParameters::POLARITY_FALSE:
|
|
var_polarity_[var] = inverted ? true : false;
|
|
break;
|
|
case SatParameters::POLARITY_RANDOM:
|
|
var_polarity_[var] = std::uniform_int_distribution<int>(0, 1)(*random_);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::UseLongestAssignmentAsInitialPolarity() {
|
|
// In this special case, we just overwrite partially the current fixed
|
|
// polarity and reset the best best_partial_assignment_ for the next such
|
|
// phase.
|
|
for (const Literal l : best_partial_assignment_) {
|
|
var_polarity_[l.Variable()] = l.IsPositive();
|
|
}
|
|
best_partial_assignment_.clear();
|
|
}
|
|
|
|
bool SatDecisionPolicy::UseLsSolutionAsInitialPolarity() {
|
|
if (!parameters_.polarity_exploit_ls_hints()) return false;
|
|
|
|
if (ls_hints_->NumSolutions() == 0) return false;
|
|
|
|
// This is in term of proto variable.
|
|
// TODO(user): use cp_model_mapping. But this is not needed to experiment
|
|
// on pure sat problems.
|
|
std::shared_ptr<const SharedLsSolutionRepository::Solution> solution =
|
|
ls_hints_->GetRandomBiasedSolution(*random_);
|
|
if (solution->variable_values.size() != var_polarity_.size()) return false;
|
|
|
|
for (int i = 0; i < solution->variable_values.size(); ++i) {
|
|
var_polarity_[BooleanVariable(i)] = solution->variable_values[i] == 1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SatDecisionPolicy::FlipCurrentPolarity() {
|
|
const int num_variables = var_polarity_.size();
|
|
for (BooleanVariable var; var < num_variables; ++var) {
|
|
var_polarity_[var] = !var_polarity_[var];
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::RandomizeCurrentPolarity() {
|
|
const int num_variables = var_polarity_.size();
|
|
for (BooleanVariable var; var < num_variables; ++var) {
|
|
var_polarity_[var] = std::uniform_int_distribution<int>(0, 1)(*random_);
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::ResetActivitiesToFollowBestPartialAssignment() {
|
|
DCHECK_EQ(trail_.CurrentDecisionLevel(), 0);
|
|
CHECK(!activities_.empty());
|
|
const double max_activity =
|
|
*absl::c_max_element(activities_) + variable_activity_increment_;
|
|
const double kDecay = 0.999;
|
|
variable_activity_increment_ =
|
|
max_activity / pow(kDecay, best_partial_assignment_.size() + 1);
|
|
var_ordering_is_initialized_ = false;
|
|
if (max_activity + variable_activity_increment_ >
|
|
parameters_.max_variable_activity_value()) {
|
|
RescaleVariableActivities(1 / parameters_.max_variable_activity_value());
|
|
}
|
|
double weight = 1.0;
|
|
for (int i = 0; i < best_partial_assignment_.size(); ++i) {
|
|
const Literal l = best_partial_assignment_[i];
|
|
weight *= kDecay;
|
|
activities_[l.Variable()] += weight * variable_activity_increment_;
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::InitializeVariableOrdering() {
|
|
const int num_variables = activities_.size();
|
|
|
|
// First, extract the variables without activity, and add the other to the
|
|
// priority queue.
|
|
var_ordering_.Clear();
|
|
tmp_variables_.clear();
|
|
for (BooleanVariable var(0); var < num_variables; ++var) {
|
|
if (!trail_.Assignment().VariableIsAssigned(var)) {
|
|
if (activities_[var] > 0.0) {
|
|
var_ordering_.Add({var, tie_breakers_[var], activities_[var]});
|
|
} else {
|
|
tmp_variables_.push_back(var);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the order of the other according to the parameters_.
|
|
// Note that this is just a "preference" since the priority queue will kind
|
|
// of randomize this. However, it is more efficient than using the tie_breaker
|
|
// which add a big overhead on the priority queue.
|
|
//
|
|
// TODO(user): Experiment and come up with a good set of heuristics.
|
|
switch (parameters_.preferred_variable_order()) {
|
|
case SatParameters::IN_ORDER:
|
|
break;
|
|
case SatParameters::IN_REVERSE_ORDER:
|
|
std::reverse(tmp_variables_.begin(), tmp_variables_.end());
|
|
break;
|
|
case SatParameters::IN_RANDOM_ORDER:
|
|
std::shuffle(tmp_variables_.begin(), tmp_variables_.end(), *random_);
|
|
break;
|
|
}
|
|
|
|
// Add the variables without activity to the queue (in the default order)
|
|
for (const BooleanVariable var : tmp_variables_) {
|
|
var_ordering_.Add({var, tie_breakers_[var], 0.0});
|
|
}
|
|
|
|
// Finish the queue initialization.
|
|
pq_need_update_for_var_at_trail_index_.ClearAndResize(num_variables);
|
|
pq_need_update_for_var_at_trail_index_.SetAllBefore(trail_.Index());
|
|
var_ordering_is_initialized_ = true;
|
|
}
|
|
|
|
void SatDecisionPolicy::SetAssignmentPreference(Literal literal, float weight) {
|
|
if (!parameters_.use_optimization_hints()) return;
|
|
DCHECK_GE(weight, 0.0);
|
|
DCHECK_LE(weight, 1.0);
|
|
|
|
has_forced_polarity_[literal.Variable()] = true;
|
|
forced_polarity_[literal.Variable()] = literal.IsPositive();
|
|
|
|
// The tie_breaker is changed, so we need to reinitialize the priority queue.
|
|
// Note that this doesn't change the activity though.
|
|
tie_breakers_[literal.Variable()] = weight;
|
|
var_ordering_is_initialized_ = false;
|
|
}
|
|
|
|
std::vector<std::pair<Literal, float>> SatDecisionPolicy::AllPreferences()
|
|
const {
|
|
std::vector<std::pair<Literal, float>> prefs;
|
|
for (BooleanVariable var(0); var < var_polarity_.size(); ++var) {
|
|
// TODO(user): we currently assume that if the tie_breaker is zero then
|
|
// no preference was set (which is not 100% correct). Fix that.
|
|
const float value = var_ordering_.GetElement(var.value()).tie_breaker;
|
|
if (value > 0.0) {
|
|
prefs.push_back(std::make_pair(Literal(var, var_polarity_[var]), value));
|
|
}
|
|
}
|
|
return prefs;
|
|
}
|
|
|
|
void SatDecisionPolicy::BumpVariableActivities(
|
|
absl::Span<const Literal> literals) {
|
|
if (parameters_.use_erwa_heuristic()) {
|
|
if (num_bumps_.size() != activities_.size()) {
|
|
num_bumps_.resize(activities_.size(), 0);
|
|
}
|
|
for (const Literal literal : literals) {
|
|
// Note that we don't really need to bump level 0 variables since they
|
|
// will never be backtracked over. However it is faster to simply bump
|
|
// them.
|
|
++num_bumps_[literal.Variable()];
|
|
}
|
|
return;
|
|
}
|
|
|
|
const double max_activity_value = parameters_.max_variable_activity_value();
|
|
for (const Literal literal : literals) {
|
|
const BooleanVariable var = literal.Variable();
|
|
const int level = trail_.Info(var).level;
|
|
if (level == 0) continue;
|
|
activities_[var] += variable_activity_increment_;
|
|
pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
|
|
if (activities_[var] > max_activity_value) {
|
|
RescaleVariableActivities(1.0 / max_activity_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::RescaleVariableActivities(double scaling_factor) {
|
|
variable_activity_increment_ *= scaling_factor;
|
|
for (BooleanVariable var(0); var < activities_.size(); ++var) {
|
|
activities_[var] *= scaling_factor;
|
|
}
|
|
|
|
// When rescaling the activities of all the variables, the order of the
|
|
// active variables in the heap will not change, but we still need to update
|
|
// their weights so that newly inserted elements will compare correctly with
|
|
// already inserted ones.
|
|
//
|
|
// IMPORTANT: we need to reset the full heap from scratch because just
|
|
// multiplying the current weight by scaling_factor is not guaranteed to
|
|
// preserve the order. This is because the activity of two entries may go to
|
|
// zero and the tie-breaking ordering may change their relative order.
|
|
//
|
|
// InitializeVariableOrdering() will be called lazily only if needed.
|
|
var_ordering_is_initialized_ = false;
|
|
}
|
|
|
|
void SatDecisionPolicy::UpdateVariableActivityIncrement() {
|
|
variable_activity_increment_ *= 1.0 / parameters_.variable_activity_decay();
|
|
}
|
|
|
|
Literal SatDecisionPolicy::NextBranch() {
|
|
// Lazily initialize var_ordering_ if needed.
|
|
if (!var_ordering_is_initialized_) {
|
|
InitializeVariableOrdering();
|
|
}
|
|
|
|
// Choose the variable.
|
|
BooleanVariable var;
|
|
const double ratio = parameters_.random_branches_ratio();
|
|
auto zero_to_one = [this]() {
|
|
return std::uniform_real_distribution<double>()(*random_);
|
|
};
|
|
if (ratio != 0.0 && zero_to_one() < ratio) {
|
|
while (true) {
|
|
// TODO(user): This may not be super efficient if almost all the
|
|
// variables are assigned.
|
|
std::uniform_int_distribution<int> index_dist(0,
|
|
var_ordering_.Size() - 1);
|
|
var = var_ordering_.QueueElement(index_dist(*random_)).var;
|
|
if (!trail_.Assignment().VariableIsAssigned(var)) break;
|
|
pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
|
|
var_ordering_.Remove(var.value());
|
|
}
|
|
} else {
|
|
// The loop is done this way in order to leave the final choice in the heap.
|
|
DCHECK(!var_ordering_.IsEmpty());
|
|
var = var_ordering_.Top().var;
|
|
while (trail_.Assignment().VariableIsAssigned(var)) {
|
|
var_ordering_.Pop();
|
|
pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
|
|
DCHECK(!var_ordering_.IsEmpty());
|
|
var = var_ordering_.Top().var;
|
|
}
|
|
}
|
|
|
|
// Choose its polarity (i.e. True of False).
|
|
const double random_ratio = parameters_.random_polarity_ratio();
|
|
if (random_ratio != 0.0 && zero_to_one() < random_ratio) {
|
|
return Literal(var, std::uniform_int_distribution<int>(0, 1)(*random_));
|
|
}
|
|
|
|
if (has_forced_polarity_[var]) return Literal(var, forced_polarity_[var]);
|
|
if (in_stable_phase_ && has_target_polarity_[var]) {
|
|
return Literal(var, target_polarity_[var]);
|
|
}
|
|
return Literal(var, var_polarity_[var]);
|
|
}
|
|
|
|
void SatDecisionPolicy::PqInsertOrUpdate(BooleanVariable var) {
|
|
const WeightedVarQueueElement element{var, tie_breakers_[var],
|
|
activities_[var]};
|
|
if (var_ordering_.Contains(var.value())) {
|
|
// Note that the new weight should always be higher than the old one.
|
|
var_ordering_.IncreasePriority(element);
|
|
} else {
|
|
var_ordering_.Add(element);
|
|
}
|
|
}
|
|
|
|
void SatDecisionPolicy::Untrail(int target_trail_index) {
|
|
// TODO(user): avoid looping twice over the trail?
|
|
if (maybe_enable_phase_saving_ && parameters_.use_phase_saving()) {
|
|
for (int i = target_trail_index; i < trail_.Index(); ++i) {
|
|
const Literal l = trail_[i];
|
|
var_polarity_[l.Variable()] = l.IsPositive();
|
|
}
|
|
}
|
|
|
|
DCHECK_LT(target_trail_index, trail_.Index());
|
|
if (parameters_.use_erwa_heuristic()) {
|
|
if (num_bumps_.size() != activities_.size()) {
|
|
num_bumps_.resize(activities_.size(), 0);
|
|
}
|
|
|
|
// The ERWA parameter between the new estimation of the learning rate and
|
|
// the old one. TODO(user): Expose parameters for these values.
|
|
const double alpha = std::max(0.06, 0.4 - 1e-6 * num_conflicts_);
|
|
|
|
// This counts the number of conflicts since the assignment of the variable
|
|
// at the current trail_index that we are about to untrail.
|
|
int num_conflicts = 0;
|
|
int next_num_conflicts_update =
|
|
num_conflicts_stack_.empty() ? -1
|
|
: num_conflicts_stack_.back().trail_index;
|
|
|
|
int trail_index = trail_.Index();
|
|
while (trail_index > target_trail_index) {
|
|
if (next_num_conflicts_update == trail_index) {
|
|
num_conflicts += num_conflicts_stack_.back().count;
|
|
num_conflicts_stack_.pop_back();
|
|
next_num_conflicts_update =
|
|
num_conflicts_stack_.empty()
|
|
? -1
|
|
: num_conflicts_stack_.back().trail_index;
|
|
}
|
|
const BooleanVariable var = trail_[--trail_index].Variable();
|
|
|
|
// TODO(user): This heuristic can make this code quite slow because
|
|
// all the untrailed variable will cause a priority queue update.
|
|
if (num_conflicts > 0) {
|
|
const int64_t num_bumps = num_bumps_[var];
|
|
double new_rate = 0.0;
|
|
if (num_bumps > 0) {
|
|
num_bumps_[var] = 0;
|
|
new_rate = static_cast<double>(num_bumps) / num_conflicts;
|
|
}
|
|
activities_[var] = alpha * new_rate + (1 - alpha) * activities_[var];
|
|
}
|
|
if (var_ordering_is_initialized_) PqInsertOrUpdate(var);
|
|
}
|
|
if (num_conflicts > 0) {
|
|
if (!num_conflicts_stack_.empty() &&
|
|
num_conflicts_stack_.back().trail_index == trail_.Index()) {
|
|
num_conflicts_stack_.back().count += num_conflicts;
|
|
} else {
|
|
num_conflicts_stack_.push_back({trail_.Index(), num_conflicts});
|
|
}
|
|
}
|
|
} else {
|
|
if (!var_ordering_is_initialized_) return;
|
|
|
|
// Trail index of the next variable that will need a priority queue update.
|
|
int to_update = pq_need_update_for_var_at_trail_index_.Top();
|
|
while (to_update >= target_trail_index) {
|
|
DCHECK_LT(to_update, trail_.Index());
|
|
PqInsertOrUpdate(trail_[to_update].Variable());
|
|
pq_need_update_for_var_at_trail_index_.ClearTop();
|
|
to_update = pq_need_update_for_var_at_trail_index_.Top();
|
|
}
|
|
}
|
|
|
|
// Invariant.
|
|
if (DEBUG_MODE && var_ordering_is_initialized_) {
|
|
for (int trail_index = trail_.Index() - 1; trail_index > target_trail_index;
|
|
--trail_index) {
|
|
const BooleanVariable var = trail_[trail_index].Variable();
|
|
CHECK(var_ordering_.Contains(var.value()));
|
|
CHECK_EQ(activities_[var], var_ordering_.GetElement(var.value()).weight);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|