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

1214 lines
45 KiB
C++
Raw Permalink Normal View History

2025-01-10 11:35:44 +01:00
// 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/feasibility_jump.h"
#include <stdlib.h>
#include <algorithm>
2023-05-24 11:42:11 +02:00
#include <atomic>
#include <cmath>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
2023-04-05 14:00:06 +02:00
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/functional/any_invocable.h"
#include "absl/functional/bind_front.h"
#include "absl/functional/function_ref.h"
#include "absl/log/check.h"
2025-03-04 21:09:32 +01:00
#include "absl/log/log.h"
2025-03-13 15:38:23 +01:00
#include "absl/log/vlog_is_on.h"
#include "absl/random/bit_gen_ref.h"
#include "absl/random/distributions.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "ortools/algorithms/binary_search.h"
#include "ortools/sat/combine_solutions.h"
#include "ortools/sat/constraint_violation.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_checker.h"
#include "ortools/sat/cp_model_utils.h"
#include "ortools/sat/integer_base.h"
2023-05-24 11:42:11 +02:00
#include "ortools/sat/linear_model.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/sat/subsolver.h"
#include "ortools/sat/synchronization.h"
#include "ortools/sat/util.h"
#include "ortools/util/sorted_interval_list.h"
2023-05-24 11:42:11 +02:00
#include "ortools/util/strong_integers.h"
namespace operations_research::sat {
namespace {
// How much do we discount moves we might fix later.
constexpr double kCompoundDiscount = 1. / 1024;
} // namespace
void JumpTable::SetComputeFunction(
absl::AnyInvocable<std::pair<int64_t, double>(int) const> compute_jump) {
compute_jump_ = std::move(compute_jump);
}
void JumpTable::RecomputeAll(int num_variables) {
deltas_.resize(num_variables);
scores_.resize(num_variables);
needs_recomputation_.assign(num_variables, true);
}
void JumpTable::SetJump(int var, int64_t delta, double score) {
deltas_[var] = delta;
scores_[var] = score;
needs_recomputation_[var] = false;
}
void JumpTable::Recompute(int var) { needs_recomputation_[var] = true; }
bool JumpTable::JumpIsUpToDate(int var) const {
const auto& [delta, score] = compute_jump_(var);
if (delta != deltas_[var]) {
LOG(ERROR) << "Incorrect delta for var " << var << ": " << deltas_[var]
<< " (should be " << delta << ")";
}
bool score_ok = true;
if (abs(score - scores_[var]) / std::max(abs(score), 1.0) > 1e-2) {
score_ok = false;
LOG(ERROR) << "Incorrect score for var " << var << ": " << scores_[var]
2024-10-07 11:08:38 +02:00
<< " (should be " << score << ") "
<< " delta = " << delta;
}
return delta == deltas_[var] && score_ok;
}
std::pair<int64_t, double> JumpTable::GetJump(int var) {
if (needs_recomputation_[var]) {
needs_recomputation_[var] = false;
std::tie(deltas_[var], scores_[var]) = compute_jump_(var);
}
return std::make_pair(deltas_[var], scores_[var]);
}
SharedLsStates::~SharedLsStates() {
// Do a final collection.
for (int i = 0; i < states_.size(); ++i) {
CollectStatistics(*states_[i].get());
}
// Display aggregated states
for (const auto& [options, counters] : options_to_stats_) {
stat_tables_->AddLsStat(
absl::StrCat(name_, "_", options.name()), counters.num_batches,
options_to_num_restarts_[options] + counters.num_perturbations,
counters.num_linear_moves, counters.num_general_moves,
counters.num_compound_moves, counters.num_backtracks,
counters.num_weight_updates, counters.num_scores_computed);
}
}
FeasibilityJumpSolver::~FeasibilityJumpSolver() {
stat_tables_->AddTimingStat(*this);
}
void FeasibilityJumpSolver::ImportState() {
state_ = states_->GetNextState();
if (state_->move == nullptr) {
const int num_variables = var_domains_.size();
state_->move = std::make_unique<CompoundMoveBuilder>(num_variables);
}
}
void FeasibilityJumpSolver::ReleaseState() { states_->Release(state_); }
2025-03-04 21:09:32 +01:00
bool FeasibilityJumpSolver::Initialize() {
// For now we just disable or enable it.
// But in the future we might have more variation.
if (params_.feasibility_jump_linearization_level() == 0) {
2025-03-04 21:09:32 +01:00
evaluator_ = std::make_unique<LsEvaluator>(linear_model_->model_proto(),
params_, &time_limit_);
} else {
2025-03-04 21:09:32 +01:00
evaluator_ = std::make_unique<LsEvaluator>(
linear_model_->model_proto(), params_,
linear_model_->ignored_constraints(),
linear_model_->additional_constraints(), &time_limit_);
}
2025-03-04 21:09:32 +01:00
if (time_limit_.LimitReached()) {
evaluator_.reset();
return false;
}
is_initialized_ = true;
const int num_variables = linear_model_->model_proto().variables().size();
var_domains_.resize(num_variables);
for (int v = 0; v < num_variables; ++v) {
var_domains_.Set(
v, ReadDomainFromProto(linear_model_->model_proto().variables(v)));
}
var_domains_.InitializeObjective(linear_model_->model_proto());
vars_to_scan_.ClearAndReserve(num_variables);
var_occurs_in_non_linear_constraint_.resize(num_variables);
for (int c = 0; c < evaluator_->NumNonLinearConstraints(); ++c) {
for (int v : evaluator_->GeneralConstraintToVars(c)) {
var_occurs_in_non_linear_constraint_[v] = true;
}
}
2025-03-04 21:09:32 +01:00
return true;
}
namespace {
int64_t ComputeRange(int64_t range, double range_ratio) {
return static_cast<int64_t>(
std::ceil(static_cast<double>(range) * range_ratio));
}
// TODO(user): Optimize and move to the Domain class.
// TODO(user): Improve entropy on non continuous domains.
int64_t RandomValueNearMin(const Domain& domain, double range_ratio,
absl::BitGenRef random) {
if (domain.Size() == 1) return domain.FixedValue();
if (domain.Size() == 2) {
return absl::Bernoulli(random, 1 - range_ratio) ? domain.Min()
: domain.Max();
}
const int64_t range = ComputeRange(domain.Max() - domain.Min(), range_ratio);
return domain.ValueAtOrBefore(domain.Min() +
absl::LogUniform<int64_t>(random, 0, range));
2023-04-05 14:00:06 +02:00
}
int64_t RandomValueNearMax(const Domain& domain, double range_ratio,
absl::BitGenRef random) {
if (domain.Size() == 1) return domain.FixedValue();
if (domain.Size() == 2) {
return absl::Bernoulli(random, 1 - range_ratio) ? domain.Max()
: domain.Min();
}
const int64_t range = ComputeRange(domain.Max() - domain.Min(), range_ratio);
return domain.ValueAtOrAfter(domain.Max() -
absl::LogUniform<int64_t>(random, 0, range));
}
int64_t RandomValueNearValue(const Domain& domain, int64_t value,
double range_ratio, absl::BitGenRef random) {
DCHECK(!domain.IsFixed());
if (domain.Min() >= value) {
return RandomValueNearMin(domain, range_ratio, random);
}
if (domain.Max() <= value) {
return RandomValueNearMax(domain, range_ratio, random);
}
// Split up or down, and choose value in split domain.
const Domain greater_domain =
domain.IntersectionWith({value + 1, domain.Max()});
const double choose_greater_probability =
static_cast<double>(greater_domain.Size()) /
static_cast<double>(domain.Size() - 1);
if (absl::Bernoulli(random, choose_greater_probability)) {
return RandomValueNearMin(greater_domain, range_ratio, random);
} else {
return RandomValueNearMax(
domain.IntersectionWith({domain.Min(), value - 1}), range_ratio,
random);
}
}
} // namespace
void FeasibilityJumpSolver::ResetCurrentSolution(
bool use_hint, bool use_objective, double perturbation_probability) {
const int num_variables = linear_model_->model_proto().variables().size();
const double default_value_probability = 1.0 - perturbation_probability;
const double range_ratio =
params_.feasibility_jump_var_perburbation_range_ratio();
std::vector<int64_t>& solution = state_->solution;
state_->base_solution = nullptr;
// Resize the solution if needed.
solution.resize(num_variables);
// Starts with values closest to zero.
2023-04-05 14:00:06 +02:00
for (int var = 0; var < num_variables; ++var) {
if (var_domains_[var].IsFixed()) {
solution[var] = var_domains_[var].FixedValue();
continue;
}
if (absl::Bernoulli(random_, default_value_probability)) {
solution[var] = var_domains_[var].SmallestValue();
} else {
solution[var] =
RandomValueNearValue(var_domains_[var], 0, range_ratio, random_);
}
2023-04-05 14:00:06 +02:00
}
// Use objective half of the time (if the model has one).
if (use_objective && linear_model_->model_proto().has_objective()) {
const int num_terms =
linear_model_->model_proto().objective().vars().size();
2023-04-05 14:00:06 +02:00
for (int i = 0; i < num_terms; ++i) {
const int var = linear_model_->model_proto().objective().vars(i);
if (var_domains_[var].IsFixed()) continue;
if (linear_model_->model_proto().objective().coeffs(i) > 0) {
if (absl::Bernoulli(random_, default_value_probability)) {
solution[var] = var_domains_[var].Min();
} else {
solution[var] =
RandomValueNearMin(var_domains_[var], range_ratio, random_);
}
2023-04-05 14:00:06 +02:00
} else {
if (absl::Bernoulli(random_, default_value_probability)) {
solution[var] = var_domains_[var].Max();
} else {
solution[var] =
RandomValueNearMax(var_domains_[var], range_ratio, random_);
}
}
}
}
2023-11-16 19:46:56 +01:00
if (use_hint && linear_model_->model_proto().has_solution_hint()) {
2023-11-16 19:46:56 +01:00
const auto& hint = linear_model_->model_proto().solution_hint();
for (int i = 0; i < hint.vars().size(); ++i) {
solution[hint.vars(i)] = hint.values(i);
}
}
}
void FeasibilityJumpSolver::PerturbateCurrentSolution(
double perturbation_probability) {
if (perturbation_probability == 0.0) return;
const int num_variables = linear_model_->model_proto().variables().size();
const double perturbation_ratio =
params_.feasibility_jump_var_perburbation_range_ratio();
std::vector<int64_t>& solution = state_->solution;
for (int var = 0; var < num_variables; ++var) {
if (var_domains_[var].IsFixed()) continue;
if (absl::Bernoulli(random_, perturbation_probability)) {
solution[var] = RandomValueNearValue(var_domains_[var], solution[var],
perturbation_ratio, random_);
}
}
}
2023-04-05 14:00:06 +02:00
std::string FeasibilityJumpSolver::OneLineStats() const {
// Moves and evaluations in the general iterations.
const std::string general_str =
state_->counters.num_general_evals == 0 &&
state_->counters.num_general_moves == 0
? ""
: absl::StrCat(
" gen{mvs:", FormatCounter(state_->counters.num_general_moves),
" evals:", FormatCounter(state_->counters.num_general_evals),
"}");
const std::string compound_str =
state_->counters.num_compound_moves == 0 &&
state_->counters.num_backtracks == 0
? ""
: absl::StrCat(" comp{mvs:",
FormatCounter(state_->counters.num_compound_moves),
" btracks:",
FormatCounter(state_->counters.num_backtracks), "}");
// Improving jumps and infeasible constraints.
const int num_infeasible_cts = evaluator_->NumInfeasibleConstraints();
const std::string non_solution_str =
num_infeasible_cts == 0
? ""
: absl::StrCat(" #good_moves:", FormatCounter(vars_to_scan_.size()),
" #inf_cts:",
FormatCounter(evaluator_->NumInfeasibleConstraints()));
return absl::StrCat(
"batch:", state_->counters.num_batches,
" lin{mvs:", FormatCounter(state_->counters.num_linear_moves),
" evals:", FormatCounter(state_->counters.num_linear_evals), "}",
general_str, compound_str, non_solution_str,
" #w_updates:", FormatCounter(state_->counters.num_weight_updates),
" #perturb:", FormatCounter(state_->counters.num_perturbations));
2023-04-05 14:00:06 +02:00
}
std::function<void()> FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) {
task_generated_ = true; // Atomic.
return [this] {
// We delay initialization to the first task as it might be a bit slow
// to scan the whole model, so we want to do this part in parallel.
2025-03-04 21:09:32 +01:00
if (!is_initialized_) {
if (!Initialize()) {
return;
}
}
2023-04-05 14:00:06 +02:00
// Load the next state to work on.
ImportState();
// If we found a new best solution, we will restart all violation ls (we
// still finish each batch though). We will also reset the luby sequence.
bool new_best_solution_was_found = false;
if (type() == SubSolver::INCOMPLETE) {
2025-07-21 17:25:33 +02:00
const int64_t best = shared_response_->GetBestSolutionObjective().value();
if (best < state_->last_solution_rank) {
states_->ResetLubyCounter();
new_best_solution_was_found = true;
state_->last_solution_rank = best;
}
}
bool reset_weights = false;
if (new_best_solution_was_found || state_->num_batches_before_change <= 0) {
reset_weights = true;
if (state_->options.use_restart) {
states_->CollectStatistics(*state_);
state_->options.Randomize(params_, &random_);
state_->counters = LsCounters(); // Reset.
} else {
state_->options.Randomize(params_, &random_);
}
if (type() == SubSolver::INCOMPLETE) {
// This is not used once we have a solution, and setting it to false
// allow to fix the logs.
state_->options.use_objective = false;
}
const bool first_time = (state_->num_restarts == 0);
if (state_->options.use_restart || first_time ||
new_best_solution_was_found) {
if (type() == SubSolver::INCOMPLETE) {
// Choose a base solution for this neighborhood.
2025-07-21 17:25:33 +02:00
state_->base_solution =
shared_response_->SolutionPool().GetSolutionToImprove(random_);
state_->solution = state_->base_solution->variable_values;
++state_->num_solutions_imported;
} else {
if (!first_time) {
// Register this solution before we reset the search.
const int num_violations = evaluator_->ViolatedConstraints().size();
shared_hints_->AddSolution(state_->solution, num_violations);
}
ResetCurrentSolution(/*use_hint=*/first_time,
state_->options.use_objective,
state_->options.perturbation_probability);
}
} else {
PerturbateCurrentSolution(
params_.feasibility_jump_var_randomization_probability());
}
2023-04-05 14:00:06 +02:00
if (state_->options.use_restart) {
++state_->num_restarts;
states_->ConfigureNextLubyRestart(state_);
} else {
// TODO(user): Tune the improvement constant, maybe use luby.
++state_->counters.num_perturbations;
state_->num_batches_before_change =
params_.violation_ls_perturbation_period();
2023-04-05 14:00:06 +02:00
}
}
// Between chunk, we synchronize bounds.
2025-07-21 17:25:33 +02:00
//
// TODO(user): This do not play well with optimizing solution whose
// objective lag behind... Basically, we can run LS on old solution but will
// only consider it feasible if it improve the best known solution.
bool recompute_compound_weights = false;
if (linear_model_->model_proto().has_objective()) {
const IntegerValue lb = shared_response_->GetInnerObjectiveLowerBound();
const IntegerValue ub = shared_response_->GetInnerObjectiveUpperBound();
if (lb != state_->saved_inner_objective_lb ||
ub != state_->saved_inner_objective_ub) {
recompute_compound_weights = true;
}
state_->saved_inner_objective_lb = lb;
state_->saved_inner_objective_ub = ub;
if (ub < lb) return; // Search is finished.
if (evaluator_->ReduceObjectiveBounds(lb.value(), ub.value())) {
recompute_compound_weights = true;
}
}
// Update the variable domains with the last information.
if (!var_domains_.UpdateFromSharedBounds()) return;
2023-04-05 14:00:06 +02:00
// Checks the current solution is compatible with updated domains.
{
// Make sure the solution is within the potentially updated domain.
// This also initialize var_domains_.CanIncrease()/CanDecrease().
const int num_vars = state_->solution.size();
for (int var = 0; var < num_vars; ++var) {
const int64_t old_value = state_->solution[var];
const int64_t new_value = var_domains_[var].ClosestValue(old_value);
if (new_value != old_value) {
state_->solution[var] = new_value;
recompute_compound_weights = true;
}
var_domains_.OnValueChange(var, new_value);
2023-04-05 14:00:06 +02:00
}
// Check if compound move search might backtrack out of the new domains.
if (!state_->move->StackValuesInDomains(var_domains_.AsSpan())) {
2023-11-02 18:06:20 +01:00
recompute_compound_weights = true;
}
}
// Search for feasible solution.
// We always recompute that since we might have loaded from a different
// state.
evaluator_->ComputeAllViolations(state_->solution);
if (reset_weights) {
state_->bump_value = 1.0;
state_->weights.assign(evaluator_->NumEvaluatorConstraints(), 1.0);
2023-11-02 18:06:20 +01:00
recompute_compound_weights = true;
}
if (recompute_compound_weights) {
state_->move->Clear();
if (state_->options.use_compound_moves) {
state_->compound_weights.assign(state_->weights.begin(),
state_->weights.end());
for (int c = 0; c < state_->weights.size(); ++c) {
2023-11-02 18:06:20 +01:00
if (evaluator_->IsViolated(c)) continue;
state_->compound_weights[c] *= kCompoundDiscount;
2023-11-02 18:06:20 +01:00
}
state_->compound_weight_changed.clear();
state_->in_compound_weight_changed.assign(state_->weights.size(),
false);
state_->compound_move_max_discrepancy = 0;
}
}
if (!state_->options.use_compound_moves) {
DCHECK_EQ(state_->move->Size(), 0);
}
++state_->counters.num_batches;
if (DoSomeLinearIterations() && DoSomeGeneralIterations()) {
// Checks for infeasibility induced by the non supported constraints.
2025-07-21 17:25:33 +02:00
//
// TODO(user): Checking the objective is faster and we could avoid to
// check feasibility if we are not going to keep the solution anyway.
if (SolutionIsFeasible(linear_model_->model_proto(), state_->solution)) {
auto pointers = PushAndMaybeCombineSolution(
shared_response_, linear_model_->model_proto(), state_->solution,
absl::StrCat(name(), "_", state_->options.name(), "(",
OneLineStats(), ")"),
2025-07-21 17:25:33 +02:00
state_->base_solution);
// If we pushed a new solution, we use it as a new "base" so that we
// will have a smaller delta on the next solution we find.
state_->base_solution = pointers.pushed_solution;
} else {
shared_response_->LogMessage(name(), "infeasible solution. Aborting.");
model_is_supported_ = false;
}
}
2023-04-05 14:00:06 +02:00
// Update dtime.
// Since we execute only one task at the time, this is safe.
{
// TODO(user): Find better names. DeterministicTime() is maintained by
// this class while deterministic_time() is the one saved in the SubSolver
// base class).
const double current_dtime = DeterministicTime();
const double delta = current_dtime - deterministic_time();
// Because deterministic_time() is computed with a sum of difference, it
// might be slighlty different than DeterministicTime() and we don't want
// to go backward, even by 1e-18.
if (delta >= 0) {
AddTaskDeterministicDuration(delta);
shared_time_limit_->AdvanceDeterministicTime(delta);
}
2023-04-05 14:00:06 +02:00
}
ReleaseState();
task_generated_ = false; // Atomic.
};
}
double FeasibilityJumpSolver::ComputeScore(absl::Span<const double> weights,
int var, int64_t delta,
bool linear_only) {
++state_->counters.num_scores_computed;
double score = evaluator_->WeightedViolationDelta(
linear_only, weights, var, delta, absl::MakeSpan(state_->solution));
constexpr double kEpsilon = 1.0 / std::numeric_limits<int64_t>::max();
score += kEpsilon * delta * evaluator_->ObjectiveCoefficient(var);
return score;
}
std::pair<int64_t, double> FeasibilityJumpSolver::ComputeLinearJump(int var) {
DCHECK(!var_domains_[var].IsFixed());
const int64_t current_value = state_->solution[var];
++state_->counters.num_linear_evals;
const LinearIncrementalEvaluator& linear_evaluator =
evaluator_->LinearEvaluator();
if (var_domains_.HasTwoValues(var)) {
const int64_t min_value = var_domains_[var].Min();
const int64_t max_value = var_domains_[var].Max();
const int64_t delta = current_value == min_value ? max_value - min_value
: min_value - max_value;
return std::make_pair(
delta, ComputeScore(ScanWeights(), var, delta, /*linear_only=*/true));
}
// In practice, after a few iterations, the chance of finding an improving
// move is slim, and we can test that fairly easily with at most two
// queries!
//
// Tricky/Annoying: if the value is not in the domain, we returns it.
const int64_t p1 = var_domains_[var].ValueAtOrBefore(current_value - 1);
const int64_t p2 = var_domains_[var].ValueAtOrAfter(current_value + 1);
std::pair<int64_t, double> best_jump;
const double v1 = var_domains_[var].Contains(p1)
? ComputeScore(ScanWeights(), var, p1 - current_value,
/*linear_only=*/true)
: std::numeric_limits<double>::infinity();
if (v1 < 0.0) {
// Point p1 is improving. Look for best before it.
// Note that we can exclude all point after current_value since it is
// worse and we assume convexity.
const Domain dom = var_domains_[var].IntersectionWith(
Domain(std::numeric_limits<int64_t>::min(), p1 - 1));
if (dom.IsEmpty()) {
best_jump = {p1, v1};
} else {
tmp_breakpoints_ =
linear_evaluator.SlopeBreakpoints(var, current_value, dom);
best_jump = ConvexMinimum<int64_t, double>(
/*is_to_the_right=*/true, {p1, v1}, tmp_breakpoints_,
[this, var, current_value](int64_t jump_value) {
return ComputeScore(ScanWeights(), var, jump_value - current_value,
/*linear_only=*/true);
});
}
} else {
const double v2 = var_domains_[var].Contains(p2)
? ComputeScore(ScanWeights(), var, p2 - current_value,
/*linear_only=*/true)
: std::numeric_limits<double>::infinity();
if (v2 < 0.0) {
// Point p2 is improving. Look for best after it.
// Similarly, we exclude the other points by convexity.
const Domain dom = var_domains_[var].IntersectionWith(
Domain(p2 + 1, std::numeric_limits<int64_t>::max()));
if (dom.IsEmpty()) {
best_jump = {p2, v2};
} else {
2023-04-05 14:00:06 +02:00
tmp_breakpoints_ =
linear_evaluator.SlopeBreakpoints(var, current_value, dom);
best_jump = ConvexMinimum<int64_t, double>(
/*is_to_the_right=*/false, {p2, v2}, tmp_breakpoints_,
[this, var, current_value](int64_t jump_value) {
return ComputeScore(ScanWeights(), var,
jump_value - current_value,
/*linear_only=*/true);
});
}
} else {
// We have no improving point, result is either p1 or p2. This is the
// most common scenario, and require no breakpoint computation!
// Choose the direction which increases violation the least,
// disambiguating by best objective.
if (v1 < v2) {
best_jump = {p1, v1};
} else {
best_jump = {p2, v2};
}
}
}
DCHECK_NE(best_jump.first, current_value);
return std::make_pair(best_jump.first - current_value, best_jump.second);
}
std::pair<int64_t, double> FeasibilityJumpSolver::ComputeGeneralJump(int var) {
if (!var_occurs_in_non_linear_constraint_[var]) {
return ComputeLinearJump(var);
}
Domain domain = var_domains_[var];
if (domain.IsFixed()) return std::make_pair(0, 0.0);
++state_->counters.num_general_evals;
const int64_t current_value = state_->solution[var];
domain = domain.IntersectionWith(
Domain(current_value, current_value).Complement());
std::pair<int64_t, double> result;
for (int i = 0; i < domain.NumIntervals(); ++i) {
const int64_t min_delta = domain[i].start - current_value;
const int64_t max_delta = domain[i].end - current_value;
const auto& [delta, score] = RangeConvexMinimum<int64_t, double>(
min_delta, max_delta + 1, [&](int64_t delta) -> double {
return ComputeScore(ScanWeights(), var, delta, /*linear_only=*/false);
});
if (i == 0 || score < result.second) {
result = std::make_pair(delta, score);
}
}
DCHECK(domain.Contains(current_value + result.first))
<< current_value << "+" << result.first << " not in domain "
<< domain.ToString();
return result;
}
void FeasibilityJumpSolver::UpdateViolatedConstraintWeights() {
++state_->counters.num_weight_updates;
// Because we update the weight incrementally, it is better to not have a
// super high magnitude, otherwise doing +max_weight and then -max_weight
// will just ignore any constraint with a small weight and our
// DCHECK(JumpIsUpToDate(var)) will fail more often.
const double kMaxWeight = 1e10;
const double kBumpFactor = 1.0 / params_.feasibility_jump_decay();
const int num_variables = var_domains_.size();
if (state_->options.use_decay) {
state_->bump_value *= kBumpFactor;
}
// Note that ViolatedConstraints() might contain only linear constraint
// depending on how it was initialized and updated.
bool rescale = false;
num_ops_ += evaluator_->ViolatedConstraints().size();
for (const int c : evaluator_->ViolatedConstraints()) {
DCHECK(evaluator_->IsViolated(c));
if (state_->options.use_compound_moves) {
DCHECK_EQ(state_->compound_weights[c], state_->weights[c]);
}
state_->weights[c] += state_->bump_value;
if (state_->options.use_compound_moves) {
state_->compound_weights[c] = state_->weights[c];
}
if (state_->weights[c] > kMaxWeight) {
rescale = true;
}
}
if (rescale) {
const double factor = 1.0 / kMaxWeight;
state_->bump_value *= factor;
for (int c = 0; c < state_->weights.size(); ++c) {
state_->weights[c] *= factor;
if (state_->options.use_compound_moves) {
state_->compound_weights[c] *= factor;
}
}
jumps_.RecomputeAll(num_variables);
return;
}
// Update weight incrementally.
//
// To maximize floating point precision, we compute the change to jump value
// first and then apply it in one go. Also, in most situation the change is
// purely integer and should fit exactly on a double, so we don't depend on
// the order in which constraint are listed.
LinearIncrementalEvaluator* linear_evaluator =
evaluator_->MutableLinearEvaluator();
linear_evaluator->ClearAffectedVariables();
for_weight_update_.resize(num_variables);
num_ops_ += evaluator_->ViolatedConstraints().size();
for (const int c : evaluator_->ViolatedConstraints()) {
if (c < evaluator_->NumLinearConstraints()) {
linear_evaluator->UpdateScoreOnWeightUpdate(
c, jumps_.Deltas(), absl::MakeSpan(for_weight_update_));
} else {
for (const int v : evaluator_->ConstraintToVars(c)) {
jumps_.Recompute(v);
AddVarToScan(v);
}
}
}
// Recompute the affected jumps.
// Note that the constraint violations are unaffected.
absl::Span<double> scores = jumps_.MutableScores();
for (const int var : linear_evaluator->VariablesAffectedByLastUpdate()) {
// Apply the delta.
//
// TODO(user): We could compute the minimal bump that would lead to a
// good move. That might change depending on the jump value though, so
// we can only do that easily for Booleans.
if (!var_domains_.HasTwoValues(var)) {
jumps_.Recompute(var);
} else {
// We may know the correct score for binary vars.
scores[var] += state_->bump_value * for_weight_update_[var];
}
AddVarToScan(var);
}
}
bool FeasibilityJumpSolver::DoSomeLinearIterations() {
2023-04-05 14:00:06 +02:00
if (VLOG_IS_ON(1)) {
shared_response_->LogMessageWithThrottling(name(), OneLineStats());
}
// TODO(user): It should be possible to support compound moves with
// the specialized linear code, but lets keep it simpler for now.
if (state_->options.use_compound_moves) return true;
evaluator_->RecomputeViolatedList(/*linear_only=*/true);
jumps_.SetComputeFunction(
absl::bind_front(&FeasibilityJumpSolver::ComputeLinearJump, this));
RecomputeVarsToScan();
// Do a batch of a given dtime.
// Outer loop: when no more greedy moves, update the weight.
const double dtime_threshold =
DeterministicTime() + params_.feasibility_jump_batch_dtime();
while (DeterministicTime() < dtime_threshold) {
// Inner loop: greedy descent.
while (DeterministicTime() < dtime_threshold) {
// Take the best jump score amongst some random candidates.
// It is okay if we pick twice the same, we don't really care.
int best_var;
int64_t best_value;
double best_score;
if (!ScanRelevantVariables(/*num_to_scan=*/5, &best_var, &best_value,
&best_score)) {
break;
}
// Perform the move.
++state_->counters.num_linear_moves;
const int64_t prev_value = state_->solution[best_var];
state_->solution[best_var] = best_value;
evaluator_->UpdateLinearScores(best_var, prev_value, best_value,
state_->weights, jumps_.Deltas(),
jumps_.MutableScores());
evaluator_->UpdateViolatedList();
var_domains_.OnValueChange(best_var, best_value);
MarkJumpsThatNeedToBeRecomputed(best_var);
if (var_domains_.HasTwoValues(best_var)) {
// We already know the score of undoing the move we just did, and that
// this is optimal.
jumps_.SetJump(best_var, prev_value - best_value, -best_score);
}
}
if (time_limit_crossed_) return false;
2023-04-05 14:00:06 +02:00
// We will update the weight unless the queue is non-empty.
if (vars_to_scan_.empty()) {
// Note that we only count linear constraint as violated here.
if (evaluator_->ViolatedConstraints().empty()) return true;
UpdateViolatedConstraintWeights();
}
}
return false;
}
// Update the jump scores.
//
// We incrementally maintain the score (except for best_var).
// However for non-Boolean, we still need to recompute the jump value.
// We will do that in a lazy fashion.
//
// TODO(user): In the paper, they just recompute the scores and only
// change the jump values when the constraint weight changes. Experiment?
// Note however that the current code is quite fast.
//
// TODO(user): For non-Boolean, we could easily detect if a non-improving
// score cannot become improving. We don't need to add such variable to
// the queue.
void FeasibilityJumpSolver::MarkJumpsThatNeedToBeRecomputed(int changed_var) {
// To keep DCHECKs happy. Note that we migh overwrite this afterwards with the
// known score/jump of undoing the move.
jumps_.Recompute(changed_var);
// Generic part.
// No optimization there, we just update all touched variables.
// We need to do this before the Linear part, so that the status is correct in
// AddVarToScan() for variable with two values.
num_ops_ += evaluator_->VarToGeneralConstraints(changed_var).size();
for (const int c : evaluator_->VarToGeneralConstraints(changed_var)) {
num_ops_ += evaluator_->GeneralConstraintToVars(c).size();
for (const int var : evaluator_->GeneralConstraintToVars(c)) {
jumps_.Recompute(var);
AddVarToScan(var);
}
}
// Linear part.
num_ops_ += evaluator_->VariablesAffectedByLastLinearUpdate().size();
for (const int var : evaluator_->VariablesAffectedByLastLinearUpdate()) {
if (!var_domains_.HasTwoValues(var)) {
jumps_.Recompute(var);
}
AddVarToScan(var);
}
}
bool FeasibilityJumpSolver::DoSomeGeneralIterations() {
if (!state_->options.use_compound_moves &&
evaluator_->NumNonLinearConstraints() == 0) {
return true;
}
// Non-linear constraints are not evaluated in the linear phase.
evaluator_->ComputeAllNonLinearViolations(state_->solution);
evaluator_->RecomputeViolatedList(/*linear_only=*/false);
if (evaluator_->NumNonLinearConstraints() == 0) {
jumps_.SetComputeFunction(
absl::bind_front(&FeasibilityJumpSolver::ComputeLinearJump, this));
} else {
jumps_.SetComputeFunction(
absl::bind_front(&FeasibilityJumpSolver::ComputeGeneralJump, this));
}
RecomputeVarsToScan();
const double dtime_threshold =
DeterministicTime() + params_.feasibility_jump_batch_dtime();
while (DeterministicTime() < dtime_threshold) {
int var;
int64_t new_value;
double score;
const bool found_move = ScanRelevantVariables(
/*num_to_scan=*/3, &var, &new_value, &score);
const bool backtrack =
!found_move && state_->move->Backtrack(&var, &new_value, &score);
if (found_move || backtrack) {
if (backtrack) ++state_->counters.num_backtracks;
DCHECK_NE(var, -1) << var << " " << found_move << " " << backtrack;
// Perform the move.
++state_->counters.num_general_moves;
const int64_t prev_value = state_->solution[var];
DCHECK_NE(prev_value, new_value);
state_->solution[var] = new_value;
// Update the linear part and non-linear part.
evaluator_->UpdateLinearScores(var, prev_value, new_value, ScanWeights(),
jumps_.Deltas(), jumps_.MutableScores());
evaluator_->UpdateNonLinearViolations(var, prev_value, state_->solution);
evaluator_->UpdateViolatedList();
var_domains_.OnValueChange(var, new_value);
if (state_->options.use_compound_moves && !backtrack) {
// `!backtrack` is just an optimisation - we can never break any new
// constraints on backtrack, so we can never change any
// compound_weight_.
for (const auto& c : evaluator_->last_update_violation_changes()) {
if (evaluator_->IsViolated(c) &&
state_->compound_weights[c] != state_->weights[c]) {
state_->compound_weights[c] = state_->weights[c];
if (!state_->in_compound_weight_changed[c]) {
state_->in_compound_weight_changed[c] = true;
state_->compound_weight_changed.push_back(c);
}
for (const int v : evaluator_->ConstraintToVars(c)) {
jumps_.Recompute(v);
// Vars will be added in MarkJumpsThatNeedToBeRecomputed().
}
} else if (!evaluator_->IsViolated(c) &&
!state_->in_compound_weight_changed[c] &&
state_->compound_weights[c] == state_->weights[c]) {
state_->in_compound_weight_changed[c] = true;
state_->compound_weight_changed.push_back(c);
}
}
}
// Check that the score for undoing the move is -score with both the
// default weights (which may be `state_->weights` or
// `state_->compound_weights`), and with `weights` explicitly.
// TODO(user): Re-enable DCHECK.
if (/* DISABLES CODE */ false && !state_->options.use_decay) {
DCHECK_EQ(-score, ComputeScore(state_->weights, var,
prev_value - new_value, false));
DCHECK_EQ(-score, ComputeScore(ScanWeights(), var,
prev_value - new_value, false));
}
MarkJumpsThatNeedToBeRecomputed(var);
if (var_domains_.HasTwoValues(var)) {
// We already know the score of the only possible move (undoing what we
// just did).
jumps_.SetJump(var, prev_value - new_value, -score);
}
if (state_->options.use_compound_moves && !backtrack) {
// Make sure we can undo the move.
DCHECK_NE(prev_value, state_->solution[var]);
state_->move->Push(var, prev_value, score);
if (state_->move->Score() < 0) {
state_->counters.num_compound_moves += state_->move->Size();
state_->move->Clear();
state_->compound_move_max_discrepancy = 0;
}
}
continue;
} else if (time_limit_crossed_) {
return false;
}
DCHECK_EQ(state_->move->Size(), 0);
if (evaluator_->ViolatedConstraints().empty()) return true;
if (state_->options.use_compound_moves) {
ResetChangedCompoundWeights();
}
if (!state_->options.use_compound_moves ||
++state_->compound_move_max_discrepancy > 2) {
state_->compound_move_max_discrepancy = 0;
UpdateViolatedConstraintWeights();
}
}
return false;
}
void FeasibilityJumpSolver::ResetChangedCompoundWeights() {
if (!state_->options.use_compound_moves) return;
DCHECK_EQ(state_->move->Size(), 0);
num_ops_ += state_->compound_weight_changed.size();
for (const int c : state_->compound_weight_changed) {
state_->in_compound_weight_changed[c] = false;
2023-11-02 18:06:20 +01:00
const double expected_weight =
(evaluator_->IsViolated(c) ? 1.0 : kCompoundDiscount) *
state_->weights[c];
if (state_->compound_weights[c] == expected_weight) continue;
state_->compound_weights[c] = expected_weight;
num_ops_ += evaluator_->ConstraintToVars(c).size();
for (const int var : evaluator_->ConstraintToVars(c)) {
jumps_.Recompute(var);
AddVarToScan(var);
}
}
state_->compound_weight_changed.clear();
}
bool FeasibilityJumpSolver::ShouldExtendCompoundMove(double score,
double novelty) {
if (state_->move->Score() + score - std::max(novelty, 0.0) < 0) {
return true;
}
return score < state_->move->BestChildScore();
}
bool FeasibilityJumpSolver::ScanRelevantVariables(int num_to_scan,
int* best_var,
int64_t* best_value,
double* best_score) {
if (time_limit_crossed_) return false;
if (state_->move->Discrepancy() > state_->compound_move_max_discrepancy) {
return false;
}
double best_scan_score = 0.0;
int num_good = 0;
int best_index = -1;
*best_var = -1;
*best_score = 0.0;
auto remove_var_to_scan_at_index = [&](int index) {
in_vars_to_scan_[vars_to_scan_[index]] = false;
vars_to_scan_[index] = vars_to_scan_.back();
vars_to_scan_.pop_back();
if (best_index == vars_to_scan_.size()) {
best_index = index;
}
};
while (!vars_to_scan_.empty() && num_good < num_to_scan) {
num_ops_ += 6; // We are slow here.
const int index = absl::Uniform<int>(random_, 0, vars_to_scan_.size());
const int var = vars_to_scan_[index];
DCHECK_GE(var, 0);
DCHECK(in_vars_to_scan_[var]);
if (!ShouldScan(var)) {
remove_var_to_scan_at_index(index);
continue;
}
const auto [delta, scan_score] = jumps_.GetJump(var);
if ((state_->counters.num_general_evals +
state_->counters.num_linear_evals) %
100 ==
0 &&
shared_time_limit_ != nullptr && shared_time_limit_->LimitReached()) {
time_limit_crossed_ = true;
return false;
}
const int64_t current_value = state_->solution[var];
DCHECK(var_domains_[var].Contains(current_value + delta))
<< var << " " << current_value << "+" << delta << " not in "
<< var_domains_[var].ToString();
DCHECK(!var_domains_[var].IsFixed());
if (scan_score >= 0) {
remove_var_to_scan_at_index(index);
continue;
}
double score = scan_score;
if (state_->options.use_compound_moves) {
// We only use compound moves in general iterations.
score = ComputeScore(
state_->weights, var, delta,
/*linear_only=*/!var_occurs_in_non_linear_constraint_[var]);
if (!ShouldExtendCompoundMove(score, score - scan_score)) {
remove_var_to_scan_at_index(index);
continue;
}
}
++num_good;
if (scan_score < best_scan_score) {
DCHECK_NE(delta, 0) << score;
*best_var = var;
*best_value = current_value + delta;
*best_score = score;
best_scan_score = scan_score;
best_index = index;
}
}
if (best_index != -1) {
DCHECK_GT(num_good, 0);
DCHECK_GE(*best_var, 0);
DCHECK_EQ(*best_var, vars_to_scan_[best_index]);
remove_var_to_scan_at_index(best_index);
return true;
}
return false;
}
void FeasibilityJumpSolver::AddVarToScan(int var) {
DCHECK_GE(var, 0);
if (in_vars_to_scan_[var]) return;
if (!ShouldScan(var)) return;
vars_to_scan_.push_back(var);
in_vars_to_scan_[var] = true;
}
bool FeasibilityJumpSolver::ShouldScan(int var) const {
DCHECK_GE(var, 0);
if (state_->move->OnStack(var)) return false;
if (!jumps_.NeedRecomputation(var)) {
// We already have the score/jump of that variable.
const double score = jumps_.Score(var);
return score < 0.0;
}
// See RecomputeVarsToScan(), we shouldn't have any fixed variable here.
DCHECK(!var_domains_.IsFixed(var));
// Return true iff var is has a better objective value in its domain.
if (var_domains_.HasBetterObjectiveValue(var)) return true;
// We will need to recompute the score. Lets skip variable for which we known
// in advance that there will be no good score.
//
// For the objective, we don't care if it is violated or not, we only want
// to scan variable that might improve it (and thus reduce its violation if it
// is violated).
//
// TODO(user): We should generalize the objective logic to all constraint.
// There is no point scanning a variable of a violated constraint if it is at
// the wrong bound and cannot improve the violation!
return evaluator_->NumViolatedConstraintsForVarIgnoringObjective(var) > 0;
}
void FeasibilityJumpSolver::RecomputeVarsToScan() {
const int num_variables = var_domains_.size();
jumps_.RecomputeAll(num_variables);
DCHECK(SlowCheckNumViolatedConstraints());
in_vars_to_scan_.assign(num_variables, false);
vars_to_scan_.clear();
// Since the fixed status never changes during one batch, we marks such
// variable as "in_vars_to_scan_" even if we don't add them here. This allow
// to skip them without any extra lookup.
for (const int var : var_domains_.FixedVariables()) {
in_vars_to_scan_[var] = true;
}
num_ops_ += evaluator_->ViolatedConstraints().size();
for (const int c : evaluator_->ViolatedConstraints()) {
num_ops_ += evaluator_->ConstraintToVars(c).size();
for (const int v : evaluator_->ConstraintToVars(c)) {
AddVarToScan(v);
}
}
}
bool FeasibilityJumpSolver::SlowCheckNumViolatedConstraints() const {
std::vector<int> result;
result.assign(var_domains_.size(), 0);
for (const int c : evaluator_->ViolatedConstraints()) {
if (evaluator_->IsObjectiveConstraint(c)) continue;
for (const int v : evaluator_->ConstraintToVars(c)) {
++result[v];
}
}
for (int v = 0; v < result.size(); ++v) {
CHECK_EQ(result[v],
evaluator_->NumViolatedConstraintsForVarIgnoringObjective(v));
}
return true;
}
void CompoundMoveBuilder::Clear() {
for (const UnitMove& move : stack_) {
var_on_stack_[move.var] = false;
}
stack_.clear();
}
bool CompoundMoveBuilder::OnStack(int var) const {
return !stack_.empty() && var_on_stack_[var];
}
bool CompoundMoveBuilder::Backtrack(int* var, int64_t* value, double* score) {
if (stack_.empty()) return false;
*var = stack_.back().var;
*value = stack_.back().prev_value;
*score = stack_.back().score;
var_on_stack_[*var] = false;
stack_.pop_back();
if (!stack_.empty()) {
++stack_.back().discrepancy;
}
return true;
}
void CompoundMoveBuilder::Push(int var, int64_t prev_value, double score) {
DCHECK(!var_on_stack_[var]);
if (!stack_.empty()) {
stack_.back().best_child_score =
std::min(stack_.back().best_child_score, score);
}
var_on_stack_[var] = true;
stack_.push_back({
.var = var,
.prev_value = prev_value,
.score = -score,
.cumulative_score = Score() + score,
.discrepancy = Discrepancy(),
});
}
bool CompoundMoveBuilder::StackValuesInDomains(
absl::Span<const Domain> var_domains) const {
for (const UnitMove& move : stack_) {
if (!var_domains[move.var].Contains(move.prev_value)) return false;
}
return true;
}
} // namespace operations_research::sat