rewrite internal search workflow for CP-SAT
This commit is contained in:
@@ -506,7 +506,9 @@ enum CpSolverStatus {
|
||||
// The problem has been proven infeasible.
|
||||
INFEASIBLE = 3;
|
||||
|
||||
// An optimal feasible solution has been found.
|
||||
// An optimal feasible solution has been found. This is also used to indicate
|
||||
// that all solutions where found if this was asked for a satisfiability
|
||||
// problem.
|
||||
OPTIMAL = 4;
|
||||
}
|
||||
|
||||
@@ -548,6 +550,8 @@ message CpSolverResponse {
|
||||
// This will be true iff the solver was asked to find all solutions to a
|
||||
// satisfiability problem (or all optimal solutions to an optimization
|
||||
// problem), and it was successful in doing so.
|
||||
//
|
||||
// TODO(user): Remove as we also use the OPTIMAL vs FEASIBLE status for that.
|
||||
bool all_solutions_were_found = 5;
|
||||
|
||||
// Some statistics about the solve.
|
||||
|
||||
@@ -396,113 +396,5 @@ SatParameters DiversifySearchParameters(const SatParameters& params,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(user): Better stats in the multi thread case.
|
||||
// Should we cumul conflicts, branches... ?
|
||||
//
|
||||
// TODO(user): Rename and make sure this also work for pure feasibility problem.
|
||||
// We do not want to special case this too much by calling other functions.
|
||||
bool MergeOptimizationSolution(const CpSolverResponse& response, bool maximize,
|
||||
CpSolverResponse* best) {
|
||||
// In all cases, we always update the best objective bound similarly.
|
||||
const double current_best_bound = response.best_objective_bound();
|
||||
const double previous_best_bound = best->best_objective_bound();
|
||||
const double new_best_objective_bound =
|
||||
maximize ? std::min(previous_best_bound, current_best_bound)
|
||||
: std::max(previous_best_bound, current_best_bound);
|
||||
const auto cleanup = ::gtl::MakeCleanup([&best, new_best_objective_bound]() {
|
||||
if (best->status() != OPTIMAL) {
|
||||
best->set_best_objective_bound(new_best_objective_bound);
|
||||
} else {
|
||||
best->set_best_objective_bound(best->objective_value());
|
||||
}
|
||||
});
|
||||
|
||||
switch (response.status()) {
|
||||
case CpSolverStatus::FEASIBLE: {
|
||||
const bool is_improving =
|
||||
maximize ? response.objective_value() > best->objective_value()
|
||||
: response.objective_value() < best->objective_value();
|
||||
// TODO(user): return OPTIMAL if objective is tight.
|
||||
if (is_improving || best->status() == CpSolverStatus::UNKNOWN) {
|
||||
// Overwrite solution and fix best_objective_bound.
|
||||
*best = response;
|
||||
return true;
|
||||
}
|
||||
if (new_best_objective_bound != previous_best_bound) {
|
||||
// The new solution has a worse objective value, but a better
|
||||
// best_objective_bound.
|
||||
return true;
|
||||
}
|
||||
|
||||
// We still override an equivalent solution, but return false.
|
||||
//
|
||||
// TODO(user): this is needed for feasibility (enumerate all solution),
|
||||
// but might also add diversity to the LNS starting solution.
|
||||
if (response.objective_value() == best->objective_value() &&
|
||||
best->status() != CpSolverStatus::OPTIMAL) {
|
||||
*best = response;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case CpSolverStatus::INFEASIBLE: {
|
||||
if (best->status() == CpSolverStatus::UNKNOWN ||
|
||||
best->status() == CpSolverStatus::INFEASIBLE) {
|
||||
// Stores the unsat solution.
|
||||
*best = response;
|
||||
return true;
|
||||
} else {
|
||||
// It can happen that the LNS finds the best solution, but does
|
||||
// not prove it. Then another worker pulls in the best solution,
|
||||
// does not improve upon it, returns UNSAT if it has not found a
|
||||
// previous solution, or OPTIMAL with a bad objective value, and
|
||||
// stops all other workers. In that case, if the last solution
|
||||
// found has a FEASIBLE status, it is indeed optimal, and
|
||||
// should be marked as thus.
|
||||
best->set_status(CpSolverStatus::OPTIMAL);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CpSolverStatus::OPTIMAL: {
|
||||
const double previous = best->objective_value();
|
||||
const double current = response.objective_value();
|
||||
if ((maximize && current >= previous) ||
|
||||
(!maximize && current <= previous)) {
|
||||
// We always overwrite the best solution with an at-least as good
|
||||
// optimal solution.
|
||||
*best = response;
|
||||
return true;
|
||||
} else {
|
||||
// We are in the same case as the INFEASIBLE above. Solution
|
||||
// synchronization has forced the solver to exit with a sub-optimal
|
||||
// solution, believing it was optimal.
|
||||
best->set_status(CpSolverStatus::OPTIMAL);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CpSolverStatus::UNKNOWN: {
|
||||
if (best->status() == CpSolverStatus::UNKNOWN) {
|
||||
if (!std::isfinite(best->objective_value())) {
|
||||
// TODO(user): Find a better test for never updated response.
|
||||
*best = response;
|
||||
} else if (maximize) {
|
||||
// Update objective_value and best_objective_bound.
|
||||
best->set_objective_value(
|
||||
std::max(best->objective_value(), response.objective_value()));
|
||||
} else {
|
||||
best->set_objective_value(
|
||||
std::min(best->objective_value(), response.objective_value()));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sat
|
||||
} // namespace operations_research
|
||||
|
||||
@@ -53,15 +53,6 @@ SatParameters DiversifySearchParameters(const SatParameters& params,
|
||||
const CpModelProto& cp_model,
|
||||
const int worker_id, std::string* name);
|
||||
|
||||
// This method updates a given response with a new incoming response.
|
||||
// It returns true if the response is strictly improving upon the 'best' one.
|
||||
//
|
||||
// TODO(user): Separate in two, 1 method for maintaining the best
|
||||
// intermediate solution, and one for cumulating the search statistics in
|
||||
// a final solution.
|
||||
bool MergeOptimizationSolution(const CpSolverResponse& response, bool maximize,
|
||||
CpSolverResponse* best);
|
||||
|
||||
} // namespace sat
|
||||
} // namespace operations_research
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/base/cleanup.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/base/int_type_indexed_vector.h"
|
||||
@@ -526,9 +527,7 @@ std::string CpSolverResponseStats(const CpSolverResponse& response) {
|
||||
namespace {
|
||||
|
||||
void FillSolutionInResponse(const CpModelProto& model_proto, const Model& model,
|
||||
IntegerVariable objective_var,
|
||||
CpSolverResponse* response) {
|
||||
response->set_status(CpSolverStatus::FEASIBLE);
|
||||
response->clear_solution();
|
||||
response->clear_solution_lower_bounds();
|
||||
response->clear_solution_upper_bounds();
|
||||
@@ -593,24 +592,6 @@ void FillSolutionInResponse(const CpModelProto& model_proto, const Model& model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the objective value and the best objective bound.
|
||||
if (model_proto.has_objective()) {
|
||||
const CpObjectiveProto& obj = model_proto.objective();
|
||||
int64 objective_value = 0;
|
||||
for (int i = 0; i < model_proto.objective().vars_size(); ++i) {
|
||||
objective_value +=
|
||||
model_proto.objective().coeffs(i) *
|
||||
model.Get(
|
||||
LowerBound(mapping->Integer(model_proto.objective().vars(i))));
|
||||
}
|
||||
response->set_objective_value(ScaleObjectiveValue(obj, objective_value));
|
||||
response->set_best_objective_bound(ScaleObjectiveValue(
|
||||
obj, integer_trail->LevelZeroLowerBound(objective_var).value()));
|
||||
} else {
|
||||
response->clear_objective_value();
|
||||
response->clear_best_objective_bound();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -1200,59 +1181,13 @@ std::function<SatParameters(Model*)> NewSatParameters(
|
||||
|
||||
namespace {
|
||||
|
||||
// Returns a valid response with valid objective bounds.
|
||||
CpSolverResponse GetDefaultResponse(const CpModelProto& model_proto) {
|
||||
CpSolverResponse response;
|
||||
response.set_status(CpSolverStatus::UNKNOWN);
|
||||
if (model_proto.has_objective()) {
|
||||
const double kInfinity = std::numeric_limits<double>::infinity();
|
||||
const bool maximize = model_proto.objective().scaling_factor() < 0.0;
|
||||
if (maximize) {
|
||||
response.set_objective_value(-kInfinity);
|
||||
response.set_best_objective_bound(kInfinity);
|
||||
} else {
|
||||
response.set_objective_value(kInfinity);
|
||||
response.set_best_objective_bound(-kInfinity);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Because we also use this function for postsolve, we call it with
|
||||
// is_real_solve set to true and avoid doing non-useful work in this case.
|
||||
CpSolverResponse SolveCpModelInternal(
|
||||
const CpModelProto& model_proto, bool is_real_solve,
|
||||
bool log_sequential_search,
|
||||
const std::function<void(const CpSolverResponse&)>&
|
||||
external_solution_observer,
|
||||
SharedResponseManager* shared_response_manager,
|
||||
SharedBoundsManager* shared_bounds_manager, WallTimer* wall_timer,
|
||||
Model* model) {
|
||||
const bool log_search =
|
||||
model->GetOrCreate<SatParameters>()->log_search_progress() ||
|
||||
VLOG_IS_ON(1);
|
||||
if (log_search && log_sequential_search) {
|
||||
LOG(INFO) << absl::StrFormat("*** starting to load the model at %.2fs",
|
||||
wall_timer->Get());
|
||||
}
|
||||
|
||||
// Initialize a default response.
|
||||
CpSolverResponse response = GetDefaultResponse(model_proto);
|
||||
|
||||
auto fill_response_statistics = [&response, model, wall_timer]() {
|
||||
auto* sat_solver = model->Get<SatSolver>();
|
||||
response.set_num_booleans(sat_solver->NumVariables());
|
||||
response.set_num_branches(sat_solver->num_branches());
|
||||
response.set_num_conflicts(sat_solver->num_failures());
|
||||
response.set_num_binary_propagations(sat_solver->num_propagations());
|
||||
response.set_num_integer_propagations(
|
||||
model->Get<IntegerTrail>() == nullptr
|
||||
? 0
|
||||
: model->Get<IntegerTrail>()->num_enqueues());
|
||||
response.set_wall_time(wall_timer->Get());
|
||||
response.set_deterministic_time(
|
||||
model->Get<TimeLimit>()->GetElapsedDeterministicTime());
|
||||
};
|
||||
// Loads a CpModelProto inside the given model.
|
||||
// This should only be called once on a given 'Model' class.
|
||||
//
|
||||
// TODO(user): move to cp_model_loader.h/.cc
|
||||
void LoadCpModel(const CpModelProto& model_proto,
|
||||
SharedResponseManager* shared_response_manager, Model* model) {
|
||||
CHECK(shared_response_manager != nullptr);
|
||||
|
||||
// We will add them all at once after model_proto is loaded.
|
||||
model->GetOrCreate<IntegerEncoder>()->DisableImplicationBetweenLiteral();
|
||||
@@ -1277,6 +1212,7 @@ CpSolverResponse SolveCpModelInternal(
|
||||
// Load the constraints.
|
||||
std::set<std::string> unsupported_types;
|
||||
int num_ignored_constraints = 0;
|
||||
auto* sat_solver = model->GetOrCreate<SatSolver>();
|
||||
for (const ConstraintProto& ct : model_proto.constraints()) {
|
||||
if (mapping->ConstraintIsAlreadyLoaded(&ct)) {
|
||||
++num_ignored_constraints;
|
||||
@@ -1289,14 +1225,14 @@ CpSolverResponse SolveCpModelInternal(
|
||||
}
|
||||
|
||||
// We propagate after each new Boolean constraint but not the integer
|
||||
// ones. So we call Propagate() manually here. Note that we do not do
|
||||
// that in the postsolve as there is some corner case where propagating
|
||||
// ones. So we call FinishPropagation() manually here. Note that we do not
|
||||
// do that in the postsolve as there is some corner case where propagating
|
||||
// after each new constraint can have a quadratic behavior.
|
||||
//
|
||||
// Note that we only do that in debug mode as this can be really slow on
|
||||
// certain types of problems with millions of constraints.
|
||||
if (DEBUG_MODE) {
|
||||
model->GetOrCreate<SatSolver>()->Propagate();
|
||||
if (!sat_solver->FinishPropagation()) return;
|
||||
Trail* trail = model->GetOrCreate<Trail>();
|
||||
const int old_num_fixed = trail->Index();
|
||||
if (trail->Index() > old_num_fixed) {
|
||||
@@ -1304,7 +1240,7 @@ CpSolverResponse SolveCpModelInternal(
|
||||
<< " Boolean variable(s): " << ProtobufDebugString(ct);
|
||||
}
|
||||
}
|
||||
if (model->GetOrCreate<SatSolver>()->IsModelUnsat()) {
|
||||
if (sat_solver->IsModelUnsat()) {
|
||||
VLOG(3) << "UNSAT during extraction (after adding '"
|
||||
<< ConstraintCaseName(ct.constraint_case()) << "'). "
|
||||
<< ProtobufDebugString(ct);
|
||||
@@ -1319,12 +1255,12 @@ CpSolverResponse SolveCpModelInternal(
|
||||
for (const std::string& type : unsupported_types) {
|
||||
VLOG(1) << " - " << type;
|
||||
}
|
||||
return response;
|
||||
return;
|
||||
}
|
||||
|
||||
model->GetOrCreate<IntegerEncoder>()
|
||||
->AddAllImplicationsBetweenAssociatedLiterals();
|
||||
model->GetOrCreate<SatSolver>()->Propagate();
|
||||
if (!sat_solver->FinishPropagation()) return;
|
||||
|
||||
// Auto detect "at least one of" constraints in the PrecedencesPropagator.
|
||||
// Note that we do that before we finish loading the problem (objective and
|
||||
@@ -1334,24 +1270,25 @@ CpSolverResponse SolveCpModelInternal(
|
||||
parameters.auto_detect_greater_than_at_least_one_of()) {
|
||||
model->Mutable<PrecedencesPropagator>()
|
||||
->AddGreaterThanAtLeastOneOfConstraints(model);
|
||||
model->GetOrCreate<SatSolver>()->Propagate();
|
||||
if (!sat_solver->FinishPropagation()) return;
|
||||
}
|
||||
|
||||
// TODO(user): This should be done in the presolve instead.
|
||||
// TODO(user): We don't have a good deterministic time on all constraints,
|
||||
// so this might take more time than wanted.
|
||||
if (is_real_solve && parameters.cp_model_probing_level() > 1) {
|
||||
if (parameters.cp_model_probing_level() > 1) {
|
||||
ProbeBooleanVariables(/*deterministic_time_limit=*/1.0, model);
|
||||
if (!model->GetOrCreate<BinaryImplicationGraph>()
|
||||
->ComputeTransitiveReduction()) {
|
||||
model->GetOrCreate<SatSolver>()->NotifyThatModelIsUnsat();
|
||||
sat_solver->NotifyThatModelIsUnsat();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an objective variable and its associated linear constraint if
|
||||
// needed.
|
||||
IntegerVariable objective_var = kNoIntegerVariable;
|
||||
if (is_real_solve && parameters.linearization_level() > 0) {
|
||||
if (parameters.linearization_level() > 0) {
|
||||
// Linearize some part of the problem and register LP constraint(s).
|
||||
objective_var =
|
||||
AddLPConstraints(model_proto, parameters.linearization_level(), model);
|
||||
@@ -1370,16 +1307,16 @@ CpSolverResponse SolveCpModelInternal(
|
||||
}
|
||||
}
|
||||
|
||||
// Create the objective definition inside the Model so that it can be accessed
|
||||
// by the heuristics than needs it.
|
||||
if (objective_var != kNoIntegerVariable) {
|
||||
// Fill the ObjectiveSynchronizationHelper.
|
||||
ObjectiveSynchronizationHelper* helper =
|
||||
model->GetOrCreate<ObjectiveSynchronizationHelper>();
|
||||
helper->scaling_factor = model_proto.objective().scaling_factor();
|
||||
if (helper->scaling_factor == 0.0) {
|
||||
helper->scaling_factor = 1.0;
|
||||
ObjectiveDefinition* objective = model->GetOrCreate<ObjectiveDefinition>();
|
||||
objective->scaling_factor = model_proto.objective().scaling_factor();
|
||||
if (objective->scaling_factor == 0.0) {
|
||||
objective->scaling_factor = 1.0;
|
||||
}
|
||||
helper->offset = model_proto.objective().offset();
|
||||
helper->objective_var = objective_var;
|
||||
objective->offset = model_proto.objective().offset();
|
||||
objective->objective_var = objective_var;
|
||||
}
|
||||
|
||||
// Intersect the objective domain with the given one if any.
|
||||
@@ -1397,7 +1334,8 @@ CpSolverResponse SolveCpModelInternal(
|
||||
objective_var, user_domain);
|
||||
if (!ok) {
|
||||
VLOG(2) << "UNSAT due to the objective domain.";
|
||||
model->GetOrCreate<SatSolver>()->NotifyThatModelIsUnsat();
|
||||
sat_solver->NotifyThatModelIsUnsat();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the sum take a value inside the objective domain by adding
|
||||
@@ -1420,43 +1358,11 @@ CpSolverResponse SolveCpModelInternal(
|
||||
|
||||
// Note that we do one last propagation at level zero once all the
|
||||
// constraints were added.
|
||||
model->GetOrCreate<SatSolver>()->Propagate();
|
||||
if (!sat_solver->FinishPropagation()) return;
|
||||
|
||||
// Initialize the search strategy function.
|
||||
std::function<LiteralIndex()> next_decision = ConstructSearchStrategy(
|
||||
model_proto, mapping->GetVariableMapping(), objective_var, model);
|
||||
if (VLOG_IS_ON(3)) {
|
||||
next_decision = InstrumentSearchStrategy(
|
||||
model_proto, mapping->GetVariableMapping(), next_decision, model);
|
||||
}
|
||||
|
||||
// Solve.
|
||||
int num_solutions = 0;
|
||||
SatSolver::Status status;
|
||||
|
||||
// TODO(user): remove argument as it isn't used. Pass solution info instead?
|
||||
std::string solution_info;
|
||||
if (model->GetOrCreate<WorkerInfo>() != nullptr) {
|
||||
solution_info = model->GetOrCreate<WorkerInfo>()->worker_name;
|
||||
}
|
||||
const auto solution_observer = [&model_proto, &response, &num_solutions,
|
||||
&model, &solution_info,
|
||||
&external_solution_observer, objective_var,
|
||||
&fill_response_statistics](const Model&) {
|
||||
num_solutions++;
|
||||
FillSolutionInResponse(model_proto, *model, objective_var, &response);
|
||||
fill_response_statistics();
|
||||
response.set_solution_info(
|
||||
absl::StrCat("num_bool:", model->Get<SatSolver>()->NumVariables(), " ",
|
||||
solution_info));
|
||||
external_solution_observer(response);
|
||||
};
|
||||
|
||||
if (shared_response_manager != nullptr && model_proto.has_objective()) {
|
||||
// Watch improved objective best bounds in regular search, or core based
|
||||
// search. It should be disabled for LNS.
|
||||
RegisterObjectiveBestBoundExport(model_proto, log_search, objective_var,
|
||||
wall_timer, shared_response_manager,
|
||||
if (model_proto.has_objective()) {
|
||||
// Watch improved objective best bounds.
|
||||
RegisterObjectiveBestBoundExport(objective_var, shared_response_manager,
|
||||
model);
|
||||
|
||||
// Import objective bounds.
|
||||
@@ -1466,21 +1372,49 @@ CpSolverResponse SolveCpModelInternal(
|
||||
RegisterObjectiveBoundsImport(shared_response_manager, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Level zero variable bounds sharing.
|
||||
if (shared_bounds_manager != nullptr) {
|
||||
RegisterVariableBoundsLevelZeroExport(model_proto, shared_bounds_manager,
|
||||
model);
|
||||
|
||||
RegisterVariableBoundsLevelZeroImport(model_proto, shared_bounds_manager,
|
||||
model);
|
||||
// Solves an already loaded cp_model_proto.
|
||||
// The final CpSolverResponse must be read from the shared_response_manager.
|
||||
//
|
||||
// TODO(user): This should be transformed so that it can be called many times
|
||||
// and resume from the last search state as if it wasn't interuped. That would
|
||||
// allow use to easily interleave different heuristics in the same thread.
|
||||
void SolveLoadedCpModel(const CpModelProto& model_proto,
|
||||
SharedResponseManager* shared_response_manager,
|
||||
Model* model) {
|
||||
CHECK(shared_response_manager != nullptr);
|
||||
std::string solution_info = model->GetOrCreate<WorkerInfo>()->worker_name;
|
||||
if (model->GetOrCreate<SatSolver>()->IsModelUnsat()) {
|
||||
shared_response_manager->NotifyThatImprovingProblemIsInfeasible(
|
||||
absl::StrCat(solution_info, " [loading]"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (log_search && log_sequential_search) {
|
||||
LOG(INFO) << absl::StrFormat("*** starting sequential search at %.2fs",
|
||||
wall_timer->Get());
|
||||
IntegerVariable objective_var = kNoIntegerVariable;
|
||||
if (model_proto.has_objective()) {
|
||||
objective_var = model->Get<ObjectiveDefinition>()->objective_var;
|
||||
CHECK_NE(objective_var, kNoIntegerVariable);
|
||||
}
|
||||
|
||||
// Initialize the search strategy function.
|
||||
auto* mapping = model->Get<CpModelMapping>();
|
||||
std::function<LiteralIndex()> next_decision = ConstructSearchStrategy(
|
||||
model_proto, mapping->GetVariableMapping(), objective_var, model);
|
||||
if (VLOG_IS_ON(3)) {
|
||||
next_decision = InstrumentSearchStrategy(
|
||||
model_proto, mapping->GetVariableMapping(), next_decision, model);
|
||||
}
|
||||
|
||||
// TODO(user): remove argument from the lambda API since it isn't used.
|
||||
const auto solution_observer = [&model_proto, &model, &solution_info,
|
||||
&shared_response_manager](const Model&) {
|
||||
CpSolverResponse response;
|
||||
FillSolutionInResponse(model_proto, *model, &response);
|
||||
response.set_solution_info(solution_info);
|
||||
shared_response_manager->NewSolution(response, model);
|
||||
};
|
||||
|
||||
// Load solution hint.
|
||||
// We follow it and allow for a tiny number of conflicts before giving up.
|
||||
//
|
||||
@@ -1489,6 +1423,7 @@ CpSolverResponse SolveCpModelInternal(
|
||||
// below. Understand why. Also just constrain the solver to find a better
|
||||
// solution right away, it should be more efficient. Note that this is kind
|
||||
// of already done when get_objective_value() is registered above.
|
||||
const SatParameters& parameters = *(model->GetOrCreate<SatParameters>());
|
||||
if (model_proto.has_solution_hint()) {
|
||||
const int64 old_conflict_limit = parameters.max_number_of_conflicts();
|
||||
model->GetOrCreate<SatParameters>()->set_max_number_of_conflicts(10);
|
||||
@@ -1510,8 +1445,9 @@ CpSolverResponse SolveCpModelInternal(
|
||||
SequentialSearch({FollowHint(vars, values, model),
|
||||
SatSolverHeuristic(model), next_decision})};
|
||||
auto no_restart = []() { return false; };
|
||||
status =
|
||||
const SatSolver::Status status =
|
||||
SolveProblemWithPortfolioSearch(decision_policies, {no_restart}, model);
|
||||
|
||||
if (status == SatSolver::Status::FEASIBLE) {
|
||||
bool hint_is_valid = true;
|
||||
if (model_proto.has_objective() && parameters.optimize_with_core()) {
|
||||
@@ -1535,18 +1471,16 @@ CpSolverResponse SolveCpModelInternal(
|
||||
}
|
||||
if (hint_is_valid) {
|
||||
const int old_size = solution_info.size();
|
||||
solution_info += " [hint] ";
|
||||
solution_info += "[hint]";
|
||||
solution_observer(*model);
|
||||
solution_info.resize(old_size);
|
||||
CHECK(SolutionIsFeasible(
|
||||
model_proto, std::vector<int64>(response.solution().begin(),
|
||||
response.solution().end())));
|
||||
|
||||
if (!model_proto.has_objective()) {
|
||||
if (parameters.enumerate_all_solutions()) {
|
||||
model->Add(
|
||||
ExcludeCurrentSolutionWithoutIgnoredVariableAndBacktrack());
|
||||
} else {
|
||||
return response;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
@@ -1558,10 +1492,10 @@ CpSolverResponse SolveCpModelInternal(
|
||||
IntegerLiteral::LowerOrEqual(objective_var,
|
||||
current_internal_objective - 1),
|
||||
{}, {})) {
|
||||
response.set_best_objective_bound(response.objective_value());
|
||||
response.set_status(CpSolverStatus::OPTIMAL);
|
||||
fill_response_statistics();
|
||||
return response;
|
||||
shared_response_manager->NotifyThatImprovingProblemIsInfeasible(
|
||||
absl::StrCat(solution_info, "[hint]"));
|
||||
shared_response_manager->SetStatsFromModel(model);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1571,6 +1505,7 @@ CpSolverResponse SolveCpModelInternal(
|
||||
old_conflict_limit);
|
||||
}
|
||||
|
||||
SatSolver::Status status;
|
||||
if (!model_proto.has_objective()) {
|
||||
while (true) {
|
||||
status = SolveIntegerProblemWithLazyEncoding(
|
||||
@@ -1580,36 +1515,15 @@ CpSolverResponse SolveCpModelInternal(
|
||||
if (!parameters.enumerate_all_solutions()) break;
|
||||
model->Add(ExcludeCurrentSolutionWithoutIgnoredVariableAndBacktrack());
|
||||
}
|
||||
|
||||
// Fill solution status.
|
||||
switch (status) {
|
||||
case SatSolver::LIMIT_REACHED: {
|
||||
response.set_status(num_solutions > 0 ? CpSolverStatus::FEASIBLE
|
||||
: CpSolverStatus::UNKNOWN);
|
||||
break;
|
||||
}
|
||||
case SatSolver::FEASIBLE: {
|
||||
response.set_status(CpSolverStatus::FEASIBLE);
|
||||
break;
|
||||
}
|
||||
case SatSolver::INFEASIBLE: {
|
||||
if (num_solutions > 0) {
|
||||
CHECK(parameters.enumerate_all_solutions());
|
||||
response.set_all_solutions_were_found(true);
|
||||
response.set_status(CpSolverStatus::FEASIBLE);
|
||||
} else {
|
||||
response.set_status(CpSolverStatus::INFEASIBLE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(FATAL) << "Unexpected SatSolver::Status " << status;
|
||||
if (status == SatSolver::INFEASIBLE) {
|
||||
shared_response_manager->NotifyThatImprovingProblemIsInfeasible(
|
||||
solution_info);
|
||||
}
|
||||
} else {
|
||||
// Optimization problem.
|
||||
const CpObjectiveProto& obj = model_proto.objective();
|
||||
VLOG(3) << obj.vars_size() << " terms in the proto objective.";
|
||||
VLOG(3) << "Initial num_bool: " << model->Get<SatSolver>()->NumVariables();
|
||||
VLOG(2) << obj.vars_size() << " terms in the proto objective.";
|
||||
VLOG(2) << "Initial num_bool: " << model->Get<SatSolver>()->NumVariables();
|
||||
|
||||
if (parameters.optimize_with_core()) {
|
||||
std::vector<IntegerVariable> linear_vars;
|
||||
@@ -1635,51 +1549,28 @@ CpSolverResponse SolveCpModelInternal(
|
||||
model);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case SatSolver::LIMIT_REACHED: {
|
||||
model->GetOrCreate<SatSolver>()->Backtrack(0);
|
||||
// Set the best objective bound as the level zero lower bound of the
|
||||
// objective variable.
|
||||
response.set_best_objective_bound(
|
||||
ScaleObjectiveValue(obj, model->Get(LowerBound(objective_var))));
|
||||
if (num_solutions == 0) {
|
||||
// No solution found, we set the best objective value as the level
|
||||
// zero upper bound of the objective variable.
|
||||
response.set_objective_value(
|
||||
ScaleObjectiveValue(obj, model->Get(UpperBound(objective_var))));
|
||||
response.set_status(CpSolverStatus::UNKNOWN);
|
||||
} else {
|
||||
response.set_status(CpSolverStatus::FEASIBLE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SatSolver::FEASIBLE: {
|
||||
// This means that the optimization procedure found the optimal
|
||||
// solution.
|
||||
CHECK_GT(num_solutions, 0);
|
||||
response.set_status(CpSolverStatus::OPTIMAL);
|
||||
response.set_best_objective_bound(response.objective_value());
|
||||
break;
|
||||
}
|
||||
case SatSolver::INFEASIBLE: {
|
||||
if (num_solutions > 0) {
|
||||
// Happens when the main search procedure did not find a solution, but
|
||||
// the hinting or the binary search did.
|
||||
response.set_status(CpSolverStatus::OPTIMAL);
|
||||
response.set_best_objective_bound(response.objective_value());
|
||||
} else {
|
||||
response.set_status(CpSolverStatus::INFEASIBLE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(FATAL) << "Unexpected SatSolver::Status " << status;
|
||||
// The search is done in both case.
|
||||
//
|
||||
// TODO(user): Remove the weird translation INFEASIBLE->FEASIBLE in the
|
||||
// function above?
|
||||
if (status == SatSolver::INFEASIBLE || status == SatSolver::FEASIBLE) {
|
||||
shared_response_manager->NotifyThatImprovingProblemIsInfeasible(
|
||||
solution_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill response.
|
||||
fill_response_statistics();
|
||||
return response;
|
||||
shared_response_manager->SetStatsFromModel(model);
|
||||
}
|
||||
|
||||
// Thin wrapper for the postsolve "solve" and LNS local solve.
|
||||
CpSolverResponse LocalSolve(const CpModelProto& local_proto,
|
||||
WallTimer* wall_timer, Model* local_model) {
|
||||
SharedResponseManager local_response_manager(
|
||||
/*log_updates=*/false, &local_proto, wall_timer);
|
||||
|
||||
LoadCpModel(local_proto, &local_response_manager, local_model);
|
||||
SolveLoadedCpModel(local_proto, &local_response_manager, local_model);
|
||||
return local_response_manager.GetResponse();
|
||||
}
|
||||
|
||||
// TODO(user): If this ever shows up in the profile, we could avoid copying
|
||||
@@ -1716,14 +1607,14 @@ void PostsolveResponse(const CpModelProto& model_proto,
|
||||
{
|
||||
SatParameters params;
|
||||
params.set_linearization_level(0);
|
||||
params.set_cp_model_probing_level(0);
|
||||
postsolve_model.Add(operations_research::sat::NewSatParameters(params));
|
||||
}
|
||||
const CpSolverResponse postsolve_response = SolveCpModelInternal(
|
||||
mapping_proto, /*is_real_solve=*/false, /*log_sequential_search=*/false,
|
||||
[](const CpSolverResponse&) {},
|
||||
/*shared_response_manager=*/nullptr,
|
||||
/*shared_bounds_manager=*/nullptr, wall_timer, &postsolve_model);
|
||||
CHECK_EQ(postsolve_response.status(), CpSolverStatus::FEASIBLE);
|
||||
|
||||
const CpSolverResponse postsolve_response =
|
||||
LocalSolve(mapping_proto, wall_timer, &postsolve_model);
|
||||
CHECK(postsolve_response.status() == CpSolverStatus::FEASIBLE ||
|
||||
postsolve_response.status() == CpSolverStatus::OPTIMAL);
|
||||
|
||||
// We only copy the solution from the postsolve_response to the response.
|
||||
response->clear_solution();
|
||||
@@ -1932,35 +1823,26 @@ bool UpdateDomain(int64 new_lb, int64 new_ub,
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CpSolverResponse SolveCpModelWithLNS(
|
||||
const CpModelProto& model_proto,
|
||||
const std::function<void(const CpSolverResponse&)>& observer,
|
||||
int num_workers, int worker_id,
|
||||
SharedResponseManager* shared_response_manager,
|
||||
SharedBoundsManager* shared_bounds_manager, WallTimer* wall_timer,
|
||||
Model* model) {
|
||||
void SolveCpModelWithLNS(const CpModelProto& model_proto, int num_workers,
|
||||
int worker_id,
|
||||
SharedResponseManager* shared_response_manager,
|
||||
SharedBoundsManager* shared_bounds_manager,
|
||||
WallTimer* wall_timer, Model* model) {
|
||||
CHECK(shared_response_manager != nullptr);
|
||||
SatParameters* parameters = model->GetOrCreate<SatParameters>();
|
||||
|
||||
CpSolverResponse response;
|
||||
if (shared_response_manager != nullptr) {
|
||||
response = shared_response_manager->GetBestResponse();
|
||||
}
|
||||
if (response.solution().empty()) {
|
||||
CpSolverResponse response = shared_response_manager->GetResponse();
|
||||
if (response.status() == CpSolverStatus::UNKNOWN) {
|
||||
parameters->set_stop_after_first_solution(true);
|
||||
response = SolveCpModelInternal(model_proto, /*is_real_solve=*/true,
|
||||
/*log_sequential_search=*/false, observer,
|
||||
/*shared_response_manager=*/nullptr,
|
||||
/*shared_bounds_manager=*/nullptr,
|
||||
wall_timer, model);
|
||||
shared_response_manager->MergeIntoBestResponse(response);
|
||||
}
|
||||
CpModelProto mutable_model_proto = model_proto;
|
||||
if (response.status() != CpSolverStatus::FEASIBLE) {
|
||||
return response;
|
||||
LoadCpModel(model_proto, shared_response_manager, model);
|
||||
SolveLoadedCpModel(model_proto, shared_response_manager, model);
|
||||
response = shared_response_manager->GetResponse();
|
||||
}
|
||||
if (response.status() != CpSolverStatus::FEASIBLE) return;
|
||||
|
||||
const bool focus_on_decision_variables =
|
||||
parameters->lns_focus_on_decision_variables();
|
||||
CpModelProto mutable_model_proto = model_proto;
|
||||
NeighborhoodGeneratorHelper helper(&mutable_model_proto,
|
||||
focus_on_decision_variables);
|
||||
|
||||
@@ -2009,9 +1891,7 @@ CpSolverResponse SolveCpModelWithLNS(
|
||||
num_threads,
|
||||
[&]() {
|
||||
// Synchronize with external world.
|
||||
if (shared_response_manager != nullptr) {
|
||||
response = shared_response_manager->GetBestResponse();
|
||||
}
|
||||
response = shared_response_manager->GetResponse();
|
||||
|
||||
// Update the bounds on mutable model proto.
|
||||
if (shared_bounds_manager != nullptr) {
|
||||
@@ -2085,8 +1965,6 @@ CpSolverResponse SolveCpModelWithLNS(
|
||||
local_model.GetOrCreate<TimeLimit>()->MergeWithGlobalTimeLimit(limit);
|
||||
|
||||
// Presolve and solve the LNS fragment.
|
||||
CpSolverResponse local_response;
|
||||
|
||||
CpModelProto mapping_proto;
|
||||
std::vector<int> postsolve_mapping;
|
||||
PresolveOptions options;
|
||||
@@ -2095,21 +1973,18 @@ CpSolverResponse SolveCpModelWithLNS(
|
||||
options.time_limit = local_model.GetOrCreate<TimeLimit>();
|
||||
PresolveCpModel(options, &local_problem, &mapping_proto,
|
||||
&postsolve_mapping);
|
||||
local_response = SolveCpModelInternal(
|
||||
local_problem, /*is_real_solve=*/true,
|
||||
/*log_sequential_search=*/false,
|
||||
[](const CpSolverResponse& response) {},
|
||||
/*shared_response_manager=*/nullptr,
|
||||
/*shared_bounds_manager=*/nullptr, wall_timer, &local_model);
|
||||
CpSolverResponse local_response =
|
||||
LocalSolve(local_problem, wall_timer, &local_model);
|
||||
PostsolveResponse(model_proto, mapping_proto, postsolve_mapping,
|
||||
wall_timer, &local_response); // Should be mutable?
|
||||
wall_timer, &local_response);
|
||||
local_response.set_solution_info(
|
||||
absl::StrCat(local_response.solution_info(), " ", solution_info));
|
||||
|
||||
const bool neighborhood_is_reduced = neighborhood.is_reduced;
|
||||
return [neighborhood_is_reduced, &num_consecutive_not_fully_solved,
|
||||
&model_proto, &response, &difficulty, local_response, &observer,
|
||||
&generators, selected_generator, &total_num_calls]() {
|
||||
&shared_response_manager, &response, &difficulty,
|
||||
local_response, &generators, selected_generator,
|
||||
&total_num_calls]() {
|
||||
// TODO(user): This is not ideal in multithread because even though
|
||||
// the saved_difficulty will be the same for all thread, we will
|
||||
// Increase()/Decrease() the difficuty sequentially more than once.
|
||||
@@ -2143,49 +2018,32 @@ CpSolverResponse SolveCpModelWithLNS(
|
||||
<< generators[selected_generator]->GetUCBScore(total_num_calls)
|
||||
<< "]";
|
||||
|
||||
// Call the observer if we have a solution.
|
||||
// Report any feasible solution we have. Note that we do not want to
|
||||
// keep the full model in this lambda, so for now we do not report
|
||||
// local stats in the solution.
|
||||
//
|
||||
// TODO(user): depending on the problem, the bound sharing may or
|
||||
// may not restrict the objective though. Uniformize the behavior.
|
||||
if (local_response.status() == CpSolverStatus::OPTIMAL ||
|
||||
local_response.status() == CpSolverStatus::FEASIBLE) {
|
||||
if (neighborhood_is_reduced) {
|
||||
CpSolverResponse globally_valid_solution =
|
||||
GetDefaultResponse(model_proto);
|
||||
globally_valid_solution.set_status(CpSolverStatus::FEASIBLE);
|
||||
*globally_valid_solution.mutable_solution() =
|
||||
local_response.solution();
|
||||
globally_valid_solution.set_solution_info(
|
||||
local_response.solution_info());
|
||||
globally_valid_solution.set_objective_value(
|
||||
local_response.objective_value());
|
||||
observer(globally_valid_solution);
|
||||
} else {
|
||||
// We solved the full model here.
|
||||
observer(local_response);
|
||||
}
|
||||
shared_response_manager->NewSolution(local_response,
|
||||
/*model=*/nullptr);
|
||||
}
|
||||
if (!neighborhood_is_reduced &&
|
||||
(local_response.status() == CpSolverStatus::OPTIMAL ||
|
||||
local_response.status() == CpSolverStatus::INFEASIBLE)) {
|
||||
shared_response_manager->NotifyThatImprovingProblemIsInfeasible(
|
||||
local_response.solution_info());
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// TODO(user): move in MergeOptimizationSolution(), but only do that for
|
||||
// optimization problem.
|
||||
if (response.status() == CpSolverStatus::FEASIBLE) {
|
||||
if (response.objective_value() == response.best_objective_bound()) {
|
||||
response.set_status(CpSolverStatus::OPTIMAL);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
#if !defined(__PORTABLE_PLATFORM__)
|
||||
|
||||
CpSolverResponse SolveCpModelParallel(
|
||||
const CpModelProto& model_proto,
|
||||
const std::function<void(const CpSolverResponse&)>& observer,
|
||||
SharedResponseManager* shared_response_manager, WallTimer* wall_timer,
|
||||
Model* model) {
|
||||
void SolveCpModelParallel(const CpModelProto& model_proto,
|
||||
SharedResponseManager* shared_response_manager,
|
||||
WallTimer* wall_timer, Model* model) {
|
||||
const SatParameters& params = *model->GetOrCreate<SatParameters>();
|
||||
CHECK(!params.enumerate_all_solutions())
|
||||
<< "Enumerating all solutions in parallel is not supported.";
|
||||
@@ -2228,85 +2086,81 @@ CpSolverResponse SolveCpModelParallel(
|
||||
"*** starting parallel search at %.2fs with %i workers: [ %s ]",
|
||||
wall_timer->Get(), num_search_workers, absl::StrJoin(worker_names, ", "));
|
||||
|
||||
{
|
||||
ThreadPool pool("Parallel_search", num_search_workers);
|
||||
pool.StartWorkers();
|
||||
|
||||
for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) {
|
||||
const std::string worker_name = worker_names[worker_id];
|
||||
const SatParameters local_params = worker_parameters[worker_id];
|
||||
|
||||
const auto solution_observer = [worker_name, &mutex, &observer,
|
||||
&first_solution_found_or_search_finished](
|
||||
const CpSolverResponse& r) {
|
||||
observer(r);
|
||||
|
||||
// TODO(user): remove?
|
||||
// Add a callback to know when a first solution is available so we can awaken
|
||||
// the LNS threads.
|
||||
//
|
||||
// TODO(user): We could unregister it when a solution is found.
|
||||
const int callback_id = shared_response_manager->AddSolutionCallback(
|
||||
[&mutex,
|
||||
&first_solution_found_or_search_finished](const CpSolverResponse& r) {
|
||||
absl::MutexLock lock(&mutex);
|
||||
|
||||
// We have found a solution, We can awaken sleeping LNS threads.
|
||||
if (!first_solution_found_or_search_finished.HasBeenNotified()) {
|
||||
first_solution_found_or_search_finished.Notify();
|
||||
}
|
||||
};
|
||||
|
||||
pool.Schedule([&model_proto, solution_observer, stopped, local_params,
|
||||
worker_id, &mutex, num_search_workers, wall_timer,
|
||||
&first_solution_found_or_search_finished,
|
||||
&shared_response_manager, &shared_bounds_manager,
|
||||
worker_name, log_search]() {
|
||||
Model local_model;
|
||||
local_model.Add(NewSatParameters(local_params));
|
||||
local_model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
|
||||
stopped);
|
||||
|
||||
// Stores info that will be used for logs in the local model.
|
||||
WorkerInfo* worker_info = local_model.GetOrCreate<WorkerInfo>();
|
||||
worker_info->worker_name = worker_name;
|
||||
worker_info->worker_id = worker_id;
|
||||
|
||||
CpSolverResponse thread_response;
|
||||
if (local_params.use_lns()) {
|
||||
first_solution_found_or_search_finished.WaitForNotification();
|
||||
// TODO(user,user): Provide a better diversification for different
|
||||
// seeds.
|
||||
thread_response = SolveCpModelWithLNS(
|
||||
model_proto, solution_observer, num_search_workers, worker_id,
|
||||
shared_response_manager, shared_bounds_manager.get(), wall_timer,
|
||||
&local_model);
|
||||
} else {
|
||||
thread_response = SolveCpModelInternal(
|
||||
model_proto, /*is_real_solve=*/true,
|
||||
/*log_sequential_search=*/false, solution_observer,
|
||||
shared_response_manager, shared_bounds_manager.get(), wall_timer,
|
||||
&local_model);
|
||||
}
|
||||
|
||||
// Process final solution. Decide which worker has the 'best'
|
||||
// solution. Note that the solution observer may or may not have been
|
||||
// called.
|
||||
{
|
||||
LOG_IF(INFO, log_search)
|
||||
<< "*** worker '" << worker_name << "' terminates with status "
|
||||
<< ProtoEnumToString<CpSolverStatus>(thread_response.status())
|
||||
<< " and an objective value of "
|
||||
<< thread_response.objective_value();
|
||||
|
||||
shared_response_manager->MergeIntoBestResponse(thread_response);
|
||||
|
||||
// TODO(user): For now we assume that each worker only terminate when
|
||||
// the time limit is reached or when the problem is solved, so we just
|
||||
// abort all other threads and return.
|
||||
absl::MutexLock lock(&mutex);
|
||||
*stopped = true;
|
||||
if (!first_solution_found_or_search_finished.HasBeenNotified()) {
|
||||
first_solution_found_or_search_finished.Notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} // for the dtor of the threadpool (join workers) before returning.
|
||||
return shared_response_manager->GetBestResponse();
|
||||
const auto cleanup =
|
||||
::gtl::MakeCleanup([&shared_response_manager, callback_id]() {
|
||||
shared_response_manager->UnregisterCallback(callback_id);
|
||||
});
|
||||
|
||||
ThreadPool pool("Parallel_search", num_search_workers);
|
||||
pool.StartWorkers();
|
||||
|
||||
for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) {
|
||||
const std::string worker_name = worker_names[worker_id];
|
||||
const SatParameters local_params = worker_parameters[worker_id];
|
||||
pool.Schedule([&model_proto, stopped, local_params, worker_id, &mutex,
|
||||
num_search_workers, wall_timer,
|
||||
&first_solution_found_or_search_finished,
|
||||
&shared_response_manager, &shared_bounds_manager,
|
||||
worker_name]() {
|
||||
Model local_model;
|
||||
local_model.Add(NewSatParameters(local_params));
|
||||
local_model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
|
||||
stopped);
|
||||
|
||||
// Stores info that will be used for logs in the local model.
|
||||
WorkerInfo* worker_info = local_model.GetOrCreate<WorkerInfo>();
|
||||
worker_info->worker_name = worker_name;
|
||||
worker_info->worker_id = worker_id;
|
||||
|
||||
if (local_params.use_lns()) {
|
||||
first_solution_found_or_search_finished.WaitForNotification();
|
||||
// TODO(user,user): Provide a better diversification for different
|
||||
// seeds.
|
||||
SolveCpModelWithLNS(
|
||||
model_proto, num_search_workers, worker_id, shared_response_manager,
|
||||
shared_bounds_manager.get(), wall_timer, &local_model);
|
||||
} else {
|
||||
LoadCpModel(model_proto, shared_response_manager, &local_model);
|
||||
|
||||
// Level zero variable bounds sharing.
|
||||
//
|
||||
// TODO(user): move these function to the shared_bounds_manager class?
|
||||
if (shared_bounds_manager != nullptr) {
|
||||
RegisterVariableBoundsLevelZeroExport(
|
||||
model_proto, shared_bounds_manager.get(), &local_model);
|
||||
RegisterVariableBoundsLevelZeroImport(
|
||||
model_proto, shared_bounds_manager.get(), &local_model);
|
||||
}
|
||||
|
||||
SolveLoadedCpModel(model_proto, shared_response_manager, &local_model);
|
||||
}
|
||||
|
||||
// TODO(user): Accumulate the stats?
|
||||
//
|
||||
// TODO(user): For now we assume that each worker only terminate when
|
||||
// the time limit is reached or when the problem is solved, so we just
|
||||
// abort all other threads and return.
|
||||
{
|
||||
absl::MutexLock lock(&mutex);
|
||||
*stopped = true;
|
||||
if (!first_solution_found_or_search_finished.HasBeenNotified()) {
|
||||
first_solution_found_or_search_finished.Notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endif // __PORTABLE_PLATFORM__
|
||||
@@ -2382,10 +2236,10 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) {
|
||||
// Starts by expanding some constraints if needed.
|
||||
LOG_IF(INFO, log_search) << absl::StrFormat(
|
||||
"*** starting model expansion at %.2fs", wall_timer.Get());
|
||||
CpModelProto new_model = model_proto; // Copy.
|
||||
CpModelProto new_cp_model_proto = model_proto; // Copy.
|
||||
PresolveOptions options;
|
||||
options.log_info = log_search;
|
||||
ExpandCpModel(&new_model, options);
|
||||
ExpandCpModel(&new_cp_model_proto, options);
|
||||
|
||||
// Presolve?
|
||||
std::function<void(CpSolverResponse * response)> postprocess_solution;
|
||||
@@ -2399,15 +2253,15 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) {
|
||||
options.log_info = log_search;
|
||||
options.parameters = *model->GetOrCreate<SatParameters>();
|
||||
options.time_limit = model->GetOrCreate<TimeLimit>();
|
||||
const bool ok = PresolveCpModel(options, &new_model, &mapping_proto,
|
||||
&postsolve_mapping);
|
||||
const bool ok = PresolveCpModel(options, &new_cp_model_proto,
|
||||
&mapping_proto, &postsolve_mapping);
|
||||
if (!ok) {
|
||||
LOG(ERROR) << "Error while presolving, likely due to integer overflow.";
|
||||
CpSolverResponse response;
|
||||
response.set_status(CpSolverStatus::MODEL_INVALID);
|
||||
return response;
|
||||
}
|
||||
LOG_IF(INFO, log_search) << CpModelStats(new_model);
|
||||
LOG_IF(INFO, log_search) << CpModelStats(new_cp_model_proto);
|
||||
postprocess_solution = [&model_proto, mapping_proto, postsolve_mapping,
|
||||
&wall_timer](CpSolverResponse* response) {
|
||||
// Note that it is okay to use the initial model_proto in the postsolve
|
||||
@@ -2431,89 +2285,62 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) {
|
||||
};
|
||||
}
|
||||
|
||||
SharedResponseManager shared_response_manager(new_model);
|
||||
|
||||
SharedResponseManager shared_response_manager(log_search, &new_cp_model_proto,
|
||||
&wall_timer);
|
||||
const auto& observers = model->GetOrCreate<SolutionObservers>()->observers;
|
||||
int num_solutions = 0;
|
||||
std::function<void(const CpSolverResponse&)> observer_function =
|
||||
[&model_proto, &observers, &num_solutions, &wall_timer, &user_timer,
|
||||
&shared_response_manager, &postprocess_solution,
|
||||
log_search](const CpSolverResponse& response) {
|
||||
// We don't do anything if the solution is not improving and we are
|
||||
// solving an optimization problem. Otherwise, we do display and forward
|
||||
// any solution we found.
|
||||
if (!shared_response_manager.MergeIntoBestResponse(response) &&
|
||||
model_proto.has_objective()) {
|
||||
return;
|
||||
}
|
||||
CpSolverResponse best_response =
|
||||
shared_response_manager.GetBestResponse();
|
||||
|
||||
++num_solutions;
|
||||
if (log_search) {
|
||||
if (model_proto.has_objective()) {
|
||||
const bool maximize =
|
||||
model_proto.objective().scaling_factor() < 0.0;
|
||||
LogNewSolution(absl::StrCat(num_solutions), wall_timer.Get(),
|
||||
maximize ? best_response.objective_value()
|
||||
: best_response.best_objective_bound(),
|
||||
maximize ? best_response.best_objective_bound()
|
||||
: best_response.objective_value(),
|
||||
best_response.solution_info());
|
||||
} else {
|
||||
LogNewSatSolution(absl::StrCat(num_solutions), wall_timer.Get(),
|
||||
best_response.solution_info());
|
||||
if (!observers.empty()) {
|
||||
shared_response_manager.AddSolutionCallback(
|
||||
[&model_proto, &observers, &wall_timer, &user_timer,
|
||||
&postprocess_solution](
|
||||
const CpSolverResponse& response_of_presolved_problem) {
|
||||
CpSolverResponse response = response_of_presolved_problem;
|
||||
postprocess_solution(&response);
|
||||
if (!response.solution().empty()) {
|
||||
if (DEBUG_MODE || FLAGS_cp_model_check_intermediate_solutions) {
|
||||
CHECK(SolutionIsFeasible(
|
||||
model_proto, std::vector<int64>(response.solution().begin(),
|
||||
response.solution().end())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (observers.empty()) return;
|
||||
postprocess_solution(&best_response);
|
||||
if (!best_response.solution().empty()) {
|
||||
if (DEBUG_MODE || FLAGS_cp_model_check_intermediate_solutions) {
|
||||
CHECK(SolutionIsFeasible(
|
||||
model_proto,
|
||||
std::vector<int64>(best_response.solution().begin(),
|
||||
best_response.solution().end())));
|
||||
response.set_wall_time(wall_timer.Get());
|
||||
response.set_user_time(user_timer.Get());
|
||||
for (const auto& observer : observers) {
|
||||
observer(response);
|
||||
}
|
||||
}
|
||||
|
||||
best_response.set_wall_time(wall_timer.Get());
|
||||
best_response.set_user_time(user_timer.Get());
|
||||
for (const auto& observer : observers) {
|
||||
observer(best_response);
|
||||
}
|
||||
};
|
||||
|
||||
CpSolverResponse response;
|
||||
});
|
||||
}
|
||||
|
||||
#if defined(__PORTABLE_PLATFORM__)
|
||||
if (/* DISABLES CODE */ (false)) {
|
||||
// We ignore the multithreading parameter in this case.
|
||||
#else // __PORTABLE_PLATFORM__
|
||||
if (params.num_search_workers() > 1) {
|
||||
response =
|
||||
SolveCpModelParallel(new_model, observer_function,
|
||||
&shared_response_manager, &wall_timer, model);
|
||||
SolveCpModelParallel(new_cp_model_proto, &shared_response_manager,
|
||||
&wall_timer, model);
|
||||
#endif // __PORTABLE_PLATFORM__
|
||||
} else if (params.use_lns() && new_model.has_objective() &&
|
||||
} else if (params.use_lns() && new_cp_model_proto.has_objective() &&
|
||||
!params.enumerate_all_solutions()) {
|
||||
// TODO(user,user): Provide a better diversification for different
|
||||
// seeds.
|
||||
const int random_seed = model->GetOrCreate<SatParameters>()->random_seed();
|
||||
response = SolveCpModelWithLNS(
|
||||
new_model, observer_function, 1, random_seed, &shared_response_manager,
|
||||
/*shared_bounds_manager=*/nullptr, &wall_timer, model);
|
||||
SolveCpModelWithLNS(new_cp_model_proto, 1, random_seed,
|
||||
&shared_response_manager,
|
||||
/*shared_bounds_manager=*/nullptr, &wall_timer, model);
|
||||
} else {
|
||||
// Normal sequential run.
|
||||
response = SolveCpModelInternal(
|
||||
new_model, /*is_real_solve=*/true, /*log_sequential_search=*/true,
|
||||
observer_function, &shared_response_manager,
|
||||
/*shared_bounds_manager=*/nullptr, &wall_timer, model);
|
||||
if (log_search) {
|
||||
LOG(INFO) << absl::StrFormat("*** starting to load the model at %.2fs",
|
||||
wall_timer.Get());
|
||||
}
|
||||
LoadCpModel(new_cp_model_proto, &shared_response_manager, model);
|
||||
if (log_search) {
|
||||
LOG(INFO) << absl::StrFormat("*** starting sequential search at %.2fs",
|
||||
wall_timer.Get());
|
||||
}
|
||||
SolveLoadedCpModel(new_cp_model_proto, &shared_response_manager, model);
|
||||
}
|
||||
|
||||
shared_response_manager.MergeIntoBestResponse(response);
|
||||
response = shared_response_manager.GetBestResponse();
|
||||
|
||||
CpSolverResponse response = shared_response_manager.GetResponse();
|
||||
postprocess_solution(&response);
|
||||
if (!response.solution().empty()) {
|
||||
CHECK(SolutionIsFeasible(model_proto,
|
||||
|
||||
@@ -120,7 +120,10 @@ std::vector<int64> AllValuesInDomain(const ProtoWithDomain& proto) {
|
||||
|
||||
// Scale back a objective value to a double value from the original model.
|
||||
inline double ScaleObjectiveValue(const CpObjectiveProto& proto, int64 value) {
|
||||
double result = value + proto.offset();
|
||||
double result = static_cast<double>(value);
|
||||
if (value == kint64min) result = -std::numeric_limits<double>::infinity();
|
||||
if (value == kint64max) result = std::numeric_limits<double>::infinity();
|
||||
result += proto.offset();
|
||||
if (proto.scaling_factor() == 0) return result;
|
||||
return proto.scaling_factor() * result;
|
||||
}
|
||||
|
||||
@@ -255,18 +255,15 @@ std::function<LiteralIndex()> SatSolverHeuristic(Model* model) {
|
||||
}
|
||||
|
||||
std::function<LiteralIndex()> PseudoCost(Model* model) {
|
||||
const ObjectiveSynchronizationHelper* helper =
|
||||
model->Get<ObjectiveSynchronizationHelper>();
|
||||
auto* objective = model->Get<ObjectiveDefinition>();
|
||||
const bool has_objective =
|
||||
helper != nullptr && helper->objective_var != kNoIntegerVariable;
|
||||
|
||||
objective != nullptr && objective->objective_var != kNoIntegerVariable;
|
||||
if (!has_objective) {
|
||||
return []() { return kNoLiteralIndex; };
|
||||
}
|
||||
|
||||
PseudoCosts* pseudo_costs = model->GetOrCreate<PseudoCosts>();
|
||||
|
||||
return [=]() {
|
||||
return [pseudo_costs, model]() {
|
||||
const IntegerVariable chosen_var = pseudo_costs->GetBestDecisionVar();
|
||||
|
||||
if (chosen_var == kNoIntegerVariable) return kNoLiteralIndex;
|
||||
@@ -627,8 +624,13 @@ SatSolver::Status SolveProblemWithPortfolioSearch(
|
||||
if (num_policies == 0) return SatSolver::FEASIBLE;
|
||||
CHECK_EQ(num_policies, restart_policies.size());
|
||||
SatSolver* const solver = model->GetOrCreate<SatSolver>();
|
||||
const ObjectiveSynchronizationHelper* helper =
|
||||
model->Get<ObjectiveSynchronizationHelper>();
|
||||
|
||||
// This is needed for recording the pseudo-costs.
|
||||
IntegerVariable objective_var = kNoIntegerVariable;
|
||||
{
|
||||
const ObjectiveDefinition* objective = model->Get<ObjectiveDefinition>();
|
||||
if (objective != nullptr) objective_var = kNoIntegerVariable;
|
||||
}
|
||||
|
||||
// Note that it is important to do the level-zero propagation if it wasn't
|
||||
// already done because EnqueueDecisionAndBackjumpOnConflict() assumes that
|
||||
@@ -676,9 +678,9 @@ SatSolver::Status SolveProblemWithPortfolioSearch(
|
||||
GetBoundChanges(decision, model);
|
||||
IntegerValue current_obj_lb = kMinIntegerValue;
|
||||
IntegerValue current_obj_ub = kMaxIntegerValue;
|
||||
if (helper != nullptr && helper->objective_var != kNoIntegerVariable) {
|
||||
current_obj_lb = integer_trail->LowerBound(helper->objective_var);
|
||||
current_obj_ub = integer_trail->UpperBound(helper->objective_var);
|
||||
if (objective_var != kNoIntegerVariable) {
|
||||
current_obj_lb = integer_trail->LowerBound(objective_var);
|
||||
current_obj_ub = integer_trail->UpperBound(objective_var);
|
||||
}
|
||||
const int old_level = solver->CurrentDecisionLevel();
|
||||
|
||||
@@ -695,12 +697,10 @@ SatSolver::Status SolveProblemWithPortfolioSearch(
|
||||
solver->EnqueueDecisionAndBackjumpOnConflict(Literal(decision));
|
||||
|
||||
// Update the pseudo costs.
|
||||
if (solver->CurrentDecisionLevel() > old_level && helper != nullptr &&
|
||||
helper->objective_var != kNoIntegerVariable) {
|
||||
const IntegerValue new_obj_lb =
|
||||
integer_trail->LowerBound(helper->objective_var);
|
||||
const IntegerValue new_obj_ub =
|
||||
integer_trail->UpperBound(helper->objective_var);
|
||||
if (solver->CurrentDecisionLevel() > old_level &&
|
||||
objective_var != kNoIntegerVariable) {
|
||||
const IntegerValue new_obj_lb = integer_trail->LowerBound(objective_var);
|
||||
const IntegerValue new_obj_ub = integer_trail->UpperBound(objective_var);
|
||||
const IntegerValue objective_bound_change =
|
||||
(new_obj_lb - current_obj_lb) + (current_obj_ub - new_obj_ub);
|
||||
pseudo_costs->UpdateCost(bound_changes, objective_bound_change);
|
||||
|
||||
@@ -186,24 +186,19 @@ SatSolver::Status SolveProblemWithPortfolioSearch(
|
||||
// search decision.
|
||||
SatSolver::Status SolveIntegerProblemWithLazyEncoding(Model* model);
|
||||
|
||||
// Store relationship between the CpSolverResponse objective and the internal
|
||||
// IntegerVariable the solver tries to minimize.
|
||||
//
|
||||
// TODO(user): This belongs to the CpModelMapping class, move there.
|
||||
struct ObjectiveSynchronizationHelper {
|
||||
// For an optimization problem, this contains the internal integer objective
|
||||
// to minimize and information on how to display it correctly in the logs.
|
||||
struct ObjectiveDefinition {
|
||||
double scaling_factor = 1.0;
|
||||
double offset = 0.0;
|
||||
IntegerVariable objective_var = kNoIntegerVariable;
|
||||
|
||||
int64 UnscaledObjective(double value) const {
|
||||
return static_cast<int64>(std::round(value / scaling_factor - offset));
|
||||
}
|
||||
double ScaledObjective(int64 value) const {
|
||||
return (value + offset) * scaling_factor;
|
||||
double ScaleIntegerObjective(IntegerValue value) const {
|
||||
return (ToDouble(value) + offset) * scaling_factor;
|
||||
}
|
||||
};
|
||||
|
||||
// Callbacks that be called when the search goes back to level 0.
|
||||
// Callbacks that will be called when the search goes back to level 0.
|
||||
// Callbacks should return false if the propagation fails.
|
||||
struct LevelZeroCallbackHelper {
|
||||
std::vector<std::function<bool()>> callbacks;
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace sat {
|
||||
// The two templated types should behave like:
|
||||
// - StopFunction: std::function<bool()>
|
||||
// - SolveNeighborhoodFunction: std::function<std::function<void()>(int seed)>
|
||||
//
|
||||
// TODO(user): As the improving solution become more difficult to find, we
|
||||
// should trigger a lot more parallel LNS than the number of threads, so that we
|
||||
// have a better utilization. For this, add a simple mecanism so that we always
|
||||
// have num_threads working LNS, and do the barrier sync at the end when all the
|
||||
// LNS task have been executed.
|
||||
template <class StopFunction, class SolveNeighborhoodFunction>
|
||||
void OptimizeWithLNS(int num_threads, StopFunction stop_function,
|
||||
SolveNeighborhoodFunction generate_and_solve_function);
|
||||
@@ -51,6 +57,12 @@ void OptimizeWithLNS(int num_threads, StopFunction stop_function,
|
||||
// Note(user): The current logic work well in practice, but has no theoretical
|
||||
// foundation. So it might be possible to use better formulas depending on the
|
||||
// situation.
|
||||
//
|
||||
// TODO(user): In multithread, we get Increase()/Decrease() signal from
|
||||
// different thread potentially working on different difficulty. The class need
|
||||
// to be updated to properly handle this case. Increase()/Decrease() should take
|
||||
// in the difficulty at which the signal was computed, and the update formula
|
||||
// should be changed accordingly.
|
||||
class AdaptiveParameterValue {
|
||||
public:
|
||||
// Initial value is in [0.0, 1.0], both 0.0 and 1.0 are valid.
|
||||
|
||||
@@ -26,40 +26,225 @@
|
||||
namespace operations_research {
|
||||
namespace sat {
|
||||
|
||||
SharedResponseManager::SharedResponseManager(const CpModelProto& proto) {
|
||||
best_response_.set_status(CpSolverStatus::UNKNOWN);
|
||||
if (proto.has_objective()) {
|
||||
const double kInfinity = std::numeric_limits<double>::infinity();
|
||||
is_maximize_ = proto.objective().scaling_factor() < 0.0;
|
||||
if (is_maximize_) {
|
||||
best_response_.set_objective_value(-kInfinity);
|
||||
best_response_.set_best_objective_bound(kInfinity);
|
||||
SharedResponseManager::SharedResponseManager(bool log_updates,
|
||||
const CpModelProto* proto,
|
||||
const WallTimer* wall_timer)
|
||||
: log_updates_(log_updates),
|
||||
model_proto_(*proto),
|
||||
wall_timer_(*wall_timer) {}
|
||||
|
||||
void SharedResponseManager::UpdateInnerObjectiveBounds(
|
||||
const std::string& worker_info, IntegerValue lb, IntegerValue ub) {
|
||||
CHECK(model_proto_.has_objective());
|
||||
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
bool change = false;
|
||||
if (lb > inner_objective_lower_bound_) {
|
||||
change = true;
|
||||
inner_objective_lower_bound_ = lb.value();
|
||||
}
|
||||
if (ub < inner_objective_upper_bound_) {
|
||||
change = true;
|
||||
inner_objective_upper_bound_ = ub.value();
|
||||
}
|
||||
if (log_updates_ && change) {
|
||||
const CpObjectiveProto& obj = model_proto_.objective();
|
||||
double new_lb = ScaleObjectiveValue(obj, inner_objective_lower_bound_);
|
||||
double new_ub = ScaleObjectiveValue(obj, inner_objective_upper_bound_);
|
||||
if (model_proto_.objective().scaling_factor() < 0) {
|
||||
std::swap(new_lb, new_ub);
|
||||
}
|
||||
LogNewSolution("Bound", wall_timer_.Get(), new_lb, new_ub, worker_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Invariant: the status always start at UNKNOWN and can only evolve as follow:
|
||||
// UNKNOWN -> FEASIBLE -> OPTIMAL
|
||||
// UNKNOWN -> INFEASIBLE
|
||||
void SharedResponseManager::NotifyThatImprovingProblemIsInfeasible(
|
||||
const std::string& worker_info) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
if (best_response_.status() == CpSolverStatus::FEASIBLE ||
|
||||
best_response_.status() == CpSolverStatus::OPTIMAL) {
|
||||
// We also use this status to indicate that we enumerated all solutions to
|
||||
// a feasible problem.
|
||||
best_response_.set_status(CpSolverStatus::OPTIMAL);
|
||||
if (!model_proto_.has_objective()) {
|
||||
best_response_.set_all_solutions_were_found(true);
|
||||
}
|
||||
} else {
|
||||
best_response_.set_status(CpSolverStatus::INFEASIBLE);
|
||||
}
|
||||
if (log_updates_) LogNewSatSolution("Done", wall_timer_.Get(), worker_info);
|
||||
}
|
||||
|
||||
IntegerValue SharedResponseManager::GetInnerObjectiveLowerBound() {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return IntegerValue(inner_objective_lower_bound_);
|
||||
}
|
||||
|
||||
IntegerValue SharedResponseManager::GetInnerObjectiveUpperBound() {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return IntegerValue(inner_objective_upper_bound_);
|
||||
}
|
||||
|
||||
int SharedResponseManager::AddSolutionCallback(
|
||||
std::function<void(const CpSolverResponse&)> callback) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
const int id = next_callback_id_++;
|
||||
callbacks_.emplace_back(id, std::move(callback));
|
||||
return id;
|
||||
}
|
||||
|
||||
void SharedResponseManager::UnregisterCallback(int callback_id) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
for (int i = 0; i < callbacks_.size(); ++i) {
|
||||
if (callbacks_[i].first == callback_id) {
|
||||
callbacks_.erase(callbacks_.begin() + i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG(DFATAL) << "Callback id " << callback_id << " not registered.";
|
||||
}
|
||||
|
||||
CpSolverResponse SharedResponseManager::GetResponse() {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
FillObjectiveValuesInBestResponse();
|
||||
return best_response_;
|
||||
}
|
||||
|
||||
void SharedResponseManager::FillObjectiveValuesInBestResponse() {
|
||||
if (!model_proto_.has_objective()) return;
|
||||
const CpObjectiveProto& obj = model_proto_.objective();
|
||||
|
||||
if (best_response_.status() == CpSolverStatus::INFEASIBLE) {
|
||||
best_response_.clear_objective_value();
|
||||
best_response_.clear_best_objective_bound();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the objective value.
|
||||
// If we don't have any solution, we use our inner bound.
|
||||
if (best_response_.status() == CpSolverStatus::UNKNOWN) {
|
||||
best_response_.set_objective_value(
|
||||
ScaleObjectiveValue(obj, inner_objective_upper_bound_));
|
||||
} else {
|
||||
best_response_.set_objective_value(
|
||||
ScaleObjectiveValue(obj, best_solution_objective_value_));
|
||||
}
|
||||
|
||||
// Update the best bound in the response.
|
||||
// If we are at optimal, we set it to the objective value.
|
||||
if (best_response_.status() == CpSolverStatus::OPTIMAL) {
|
||||
best_response_.set_best_objective_bound(best_response_.objective_value());
|
||||
} else {
|
||||
best_response_.set_best_objective_bound(
|
||||
ScaleObjectiveValue(obj, inner_objective_lower_bound_));
|
||||
}
|
||||
}
|
||||
|
||||
void SharedResponseManager::NewSolution(const CpSolverResponse& response,
|
||||
Model* model) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
CHECK_NE(best_response_.status(), CpSolverStatus::INFEASIBLE);
|
||||
|
||||
int64 objective_value = 0;
|
||||
if (model_proto_.has_objective()) {
|
||||
const CpObjectiveProto& obj = model_proto_.objective();
|
||||
auto& repeated_field_values = response.solution().empty()
|
||||
? response.solution_lower_bounds()
|
||||
: response.solution();
|
||||
for (int i = 0; i < obj.vars_size(); ++i) {
|
||||
int64 coeff = obj.coeffs(i);
|
||||
const int ref = obj.vars(i);
|
||||
const int var = PositiveRef(ref);
|
||||
if (!RefIsPositive(ref)) coeff = -coeff;
|
||||
objective_value += coeff * repeated_field_values[var];
|
||||
}
|
||||
|
||||
// Ignore any non-strictly improving solution.
|
||||
// We also perform some basic checks on the inner bounds.
|
||||
CHECK_GE(objective_value, inner_objective_lower_bound_);
|
||||
if (objective_value > inner_objective_upper_bound_) return;
|
||||
|
||||
CHECK_LT(objective_value, best_solution_objective_value_);
|
||||
CHECK_NE(best_response_.status(), CpSolverStatus::OPTIMAL);
|
||||
best_solution_objective_value_ = objective_value;
|
||||
|
||||
// Update the new bound.
|
||||
inner_objective_upper_bound_ = objective_value - 1;
|
||||
}
|
||||
|
||||
// Note that the objective will be filled by
|
||||
// FillObjectiveValuesInBestResponse().
|
||||
best_response_.set_status(CpSolverStatus::FEASIBLE);
|
||||
best_response_.set_solution_info(response.solution_info());
|
||||
*best_response_.mutable_solution() = response.solution();
|
||||
*best_response_.mutable_solution_lower_bounds() =
|
||||
response.solution_lower_bounds();
|
||||
*best_response_.mutable_solution_upper_bounds() =
|
||||
response.solution_upper_bounds();
|
||||
|
||||
// Mark model as OPTIMAL if the inner bound crossed.
|
||||
if (model_proto_.has_objective() &&
|
||||
inner_objective_lower_bound_ > inner_objective_upper_bound_) {
|
||||
best_response_.set_status(CpSolverStatus::OPTIMAL);
|
||||
}
|
||||
|
||||
// Logging.
|
||||
++num_solutions_;
|
||||
if (log_updates_) {
|
||||
std::string solution_info = response.solution_info();
|
||||
if (model != nullptr) {
|
||||
absl::StrAppend(&solution_info,
|
||||
" num_bool:", model->Get<SatSolver>()->NumVariables());
|
||||
}
|
||||
|
||||
if (model_proto_.has_objective()) {
|
||||
const CpObjectiveProto& obj = model_proto_.objective();
|
||||
double lb = ScaleObjectiveValue(obj, inner_objective_lower_bound_);
|
||||
double ub = ScaleObjectiveValue(obj, objective_value);
|
||||
if (model_proto_.objective().scaling_factor() < 0) {
|
||||
std::swap(lb, ub);
|
||||
}
|
||||
LogNewSolution(absl::StrCat(num_solutions_), wall_timer_.Get(), lb, ub,
|
||||
solution_info);
|
||||
} else {
|
||||
best_response_.set_objective_value(kInfinity);
|
||||
best_response_.set_best_objective_bound(-kInfinity);
|
||||
LogNewSatSolution(absl::StrCat(num_solutions_), wall_timer_.Get(),
|
||||
solution_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Call callbacks.
|
||||
// Note that we cannot call function that try to get the mutex_ here.
|
||||
if (!callbacks_.empty()) {
|
||||
FillObjectiveValuesInBestResponse();
|
||||
SetStatsFromModelInternal(model);
|
||||
for (const auto& pair : callbacks_) {
|
||||
pair.second(best_response_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double SharedResponseManager::GetObjectiveValue() {
|
||||
void SharedResponseManager::SetStatsFromModel(Model* model) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return best_response_.objective_value();
|
||||
SetStatsFromModelInternal(model);
|
||||
}
|
||||
|
||||
double SharedResponseManager::GetObjectiveBestBound() {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return best_response_.best_objective_bound();
|
||||
}
|
||||
|
||||
CpSolverResponse SharedResponseManager::GetBestResponse() {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return best_response_;
|
||||
}
|
||||
|
||||
bool SharedResponseManager::MergeIntoBestResponse(
|
||||
const CpSolverResponse& response) {
|
||||
absl::MutexLock mutex_lock(&mutex_);
|
||||
return MergeOptimizationSolution(response, is_maximize_, &best_response_);
|
||||
void SharedResponseManager::SetStatsFromModelInternal(Model* model) {
|
||||
if (model == nullptr) return;
|
||||
auto* sat_solver = model->Get<SatSolver>();
|
||||
auto* integer_trail = model->Get<IntegerTrail>();
|
||||
best_response_.set_num_booleans(sat_solver->NumVariables());
|
||||
best_response_.set_num_branches(sat_solver->num_branches());
|
||||
best_response_.set_num_conflicts(sat_solver->num_failures());
|
||||
best_response_.set_num_binary_propagations(sat_solver->num_propagations());
|
||||
best_response_.set_num_integer_propagations(
|
||||
integer_trail == nullptr ? 0 : integer_trail->num_enqueues());
|
||||
auto* time_limit = model->Get<TimeLimit>();
|
||||
best_response_.set_wall_time(time_limit->GetElapsedTime());
|
||||
best_response_.set_deterministic_time(
|
||||
time_limit->GetElapsedDeterministicTime());
|
||||
}
|
||||
|
||||
SharedBoundsManager::SharedBoundsManager(int num_workers,
|
||||
@@ -259,47 +444,16 @@ void RegisterVariableBoundsLevelZeroImport(
|
||||
}
|
||||
|
||||
void RegisterObjectiveBestBoundExport(
|
||||
const CpModelProto& model_proto, bool log_progress,
|
||||
IntegerVariable objective_var, WallTimer* wall_timer,
|
||||
IntegerVariable objective_var,
|
||||
SharedResponseManager* shared_response_manager, Model* model) {
|
||||
std::string worker_name = model->GetOrCreate<WorkerInfo>()->worker_name;
|
||||
auto* integer_trail = model->Get<IntegerTrail>();
|
||||
auto* worker_info = model->GetOrCreate<WorkerInfo>();
|
||||
const CpObjectiveProto& obj = model_proto.objective();
|
||||
const auto broadcast_objective_lower_bound =
|
||||
[obj, objective_var, wall_timer, integer_trail, worker_info, log_progress,
|
||||
[worker_name, objective_var, integer_trail,
|
||||
shared_response_manager](const std::vector<IntegerVariable>& unused) {
|
||||
const double new_best_bound = ScaleObjectiveValue(
|
||||
obj, integer_trail->LevelZeroLowerBound(objective_var).value());
|
||||
const double current_best_bound =
|
||||
shared_response_manager->GetObjectiveBestBound();
|
||||
const double current_objective_value =
|
||||
shared_response_manager->GetObjectiveValue();
|
||||
|
||||
// TODO(user): we currently display "inf" for the objective if the first
|
||||
// update is a bound update. This will go away when I refactor the code
|
||||
// to not depend on the objective scaling/offset and stay with int64
|
||||
// internally.
|
||||
CpSolverResponse response;
|
||||
response.set_status(CpSolverStatus::UNKNOWN);
|
||||
const double kInfinity = std::numeric_limits<double>::infinity();
|
||||
if (shared_response_manager->IsMaximize() &&
|
||||
new_best_bound < current_best_bound) {
|
||||
response.set_objective_value(-kInfinity);
|
||||
response.set_best_objective_bound(new_best_bound);
|
||||
shared_response_manager->MergeIntoBestResponse(response);
|
||||
if (log_progress) {
|
||||
LogNewSolution("ObjUb", wall_timer->Get(), current_objective_value,
|
||||
new_best_bound, worker_info->worker_name);
|
||||
}
|
||||
} else if (new_best_bound > current_best_bound) {
|
||||
response.set_objective_value(kInfinity);
|
||||
response.set_best_objective_bound(new_best_bound);
|
||||
shared_response_manager->MergeIntoBestResponse(response);
|
||||
if (log_progress) {
|
||||
LogNewSolution("ObjLb", wall_timer->Get(), new_best_bound,
|
||||
current_objective_value, worker_info->worker_name);
|
||||
}
|
||||
}
|
||||
shared_response_manager->UpdateInnerObjectiveBounds(
|
||||
worker_name, integer_trail->LevelZeroLowerBound(objective_var),
|
||||
integer_trail->LevelZeroUpperBound(objective_var));
|
||||
};
|
||||
model->GetOrCreate<GenericLiteralWatcher>()
|
||||
->RegisterLevelZeroModifiedVariablesCallback(
|
||||
@@ -311,62 +465,48 @@ void RegisterObjectiveBoundsImport(
|
||||
auto* solver = model->GetOrCreate<SatSolver>();
|
||||
auto* integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
auto* worker_info = model->GetOrCreate<WorkerInfo>();
|
||||
auto* helper = model->GetOrCreate<ObjectiveSynchronizationHelper>();
|
||||
auto* objective = model->GetOrCreate<ObjectiveDefinition>();
|
||||
const auto import_objective_bounds = [solver, integer_trail, worker_info,
|
||||
helper, shared_response_manager]() {
|
||||
objective, shared_response_manager]() {
|
||||
if (solver->AssumptionLevel() != 0) return true;
|
||||
const double external_bound = shared_response_manager->GetObjectiveValue();
|
||||
const double external_best_bound =
|
||||
shared_response_manager->GetObjectiveBestBound();
|
||||
bool propagate = false;
|
||||
|
||||
const IntegerValue current_objective_upper_bound(
|
||||
integer_trail->UpperBound(helper->objective_var));
|
||||
const IntegerValue current_objective_lower_bound(
|
||||
integer_trail->LowerBound(helper->objective_var));
|
||||
const IntegerValue new_objective_upper_bound(
|
||||
std::isfinite(external_bound)
|
||||
? helper->UnscaledObjective(external_bound) - 1
|
||||
: current_objective_upper_bound.value());
|
||||
const IntegerValue new_objective_lower_bound(
|
||||
std::isfinite(external_best_bound)
|
||||
? helper->UnscaledObjective(external_best_bound)
|
||||
: current_objective_lower_bound.value());
|
||||
if (new_objective_upper_bound < new_objective_lower_bound) {
|
||||
return false;
|
||||
const IntegerValue external_lb =
|
||||
shared_response_manager->GetInnerObjectiveLowerBound();
|
||||
const IntegerValue current_lb =
|
||||
integer_trail->LowerBound(objective->objective_var);
|
||||
if (external_lb > current_lb) {
|
||||
if (!integer_trail->Enqueue(IntegerLiteral::GreaterOrEqual(
|
||||
objective->objective_var, external_lb),
|
||||
{}, {})) {
|
||||
return false;
|
||||
}
|
||||
propagate = true;
|
||||
}
|
||||
if (new_objective_upper_bound >= current_objective_upper_bound &&
|
||||
new_objective_lower_bound <= current_objective_lower_bound) {
|
||||
return true;
|
||||
|
||||
const IntegerValue external_ub =
|
||||
shared_response_manager->GetInnerObjectiveUpperBound();
|
||||
const IntegerValue current_ub =
|
||||
integer_trail->UpperBound(objective->objective_var);
|
||||
if (external_ub < current_ub) {
|
||||
if (!integer_trail->Enqueue(IntegerLiteral::LowerOrEqual(
|
||||
objective->objective_var, external_ub),
|
||||
{}, {})) {
|
||||
return false;
|
||||
}
|
||||
propagate = true;
|
||||
}
|
||||
VLOG(1) << " '" << worker_info->worker_name
|
||||
|
||||
if (!propagate) return true;
|
||||
|
||||
VLOG(1) << "'" << worker_info->worker_name
|
||||
<< "' imports objective bounds: external ["
|
||||
<< helper->ScaledObjective(new_objective_lower_bound.value())
|
||||
<< ", "
|
||||
<< helper->ScaledObjective(new_objective_upper_bound.value())
|
||||
<< "], internal ["
|
||||
<< helper->ScaledObjective(current_objective_lower_bound.value())
|
||||
<< ", "
|
||||
<< helper->ScaledObjective(current_objective_upper_bound.value())
|
||||
<< "]";
|
||||
if (new_objective_upper_bound < current_objective_upper_bound &&
|
||||
!integer_trail->Enqueue(
|
||||
IntegerLiteral::LowerOrEqual(helper->objective_var,
|
||||
new_objective_upper_bound),
|
||||
{}, {})) {
|
||||
return false;
|
||||
}
|
||||
if (new_objective_lower_bound > current_objective_lower_bound &&
|
||||
!integer_trail->Enqueue(
|
||||
IntegerLiteral::GreaterOrEqual(helper->objective_var,
|
||||
new_objective_lower_bound),
|
||||
{}, {})) {
|
||||
return false;
|
||||
}
|
||||
if (!solver->FinishPropagation()) {
|
||||
return false;
|
||||
}
|
||||
<< objective->ScaleIntegerObjective(external_lb) << ", "
|
||||
<< objective->ScaleIntegerObjective(external_ub) << "], current ["
|
||||
<< objective->ScaleIntegerObjective(current_lb) << ", "
|
||||
<< objective->ScaleIntegerObjective(current_ub) << "]";
|
||||
|
||||
return true;
|
||||
return solver->FinishPropagation();
|
||||
};
|
||||
|
||||
model->GetOrCreate<LevelZeroCallbackHelper>()->callbacks.push_back(
|
||||
|
||||
@@ -29,33 +29,88 @@
|
||||
namespace operations_research {
|
||||
namespace sat {
|
||||
|
||||
// Manages the gobal best solution kept by the solver.
|
||||
//
|
||||
// TODO(user): Add two int64 fieds inner_objective_lb/ub and use them instead of
|
||||
// the one in best_response. This way all the code can assume minimization and
|
||||
// not care about the offset and scaling. These should only be used for final
|
||||
// reporting and display! Moreover, that allow to share the currrent objective
|
||||
// upper bound, that is only valid by assuming we want to find a better
|
||||
// objective.
|
||||
// Manages the global best response kept by the solver.
|
||||
// All functions are thread-safe.
|
||||
class SharedResponseManager {
|
||||
public:
|
||||
explicit SharedResponseManager(const CpModelProto& proto);
|
||||
// If log_updates is true, then all updates to the global "state" will be
|
||||
// logged. This class is responsible for our solver log progress.
|
||||
explicit SharedResponseManager(bool log_updates_, const CpModelProto* proto,
|
||||
const WallTimer* wall_timer);
|
||||
|
||||
double GetObjectiveValue();
|
||||
double GetObjectiveBestBound();
|
||||
CpSolverResponse GetBestResponse();
|
||||
// Returns the current solver response. That is the best known response at the
|
||||
// time of the call with the best feasible solution and objective bounds.
|
||||
//
|
||||
// Note that the solver statistics correspond to the last time a better
|
||||
// solution was found or SetStatsFromModel() was called.
|
||||
CpSolverResponse GetResponse();
|
||||
|
||||
// TODO(user): Simple objective update probably do not need to go through
|
||||
// this function, same for solution update.
|
||||
bool MergeIntoBestResponse(const CpSolverResponse& response);
|
||||
// Adds a callback that will be called on each new solution (for
|
||||
// statisfiablity problem) or each improving new solution (for an optimization
|
||||
// problem). Returns its id so it can be unregistered if needed.
|
||||
//
|
||||
// Note that currently the class is waiting for the callback to finish before
|
||||
// accepting any new updates. That could be changed if needed.
|
||||
int AddSolutionCallback(
|
||||
std::function<void(const CpSolverResponse&)> callback);
|
||||
void UnregisterCallback(int callback_id);
|
||||
|
||||
// This never changes after construction, so it is thread-safe to access it.
|
||||
bool IsMaximize() const { return is_maximize_; }
|
||||
// The "inner" objective is the CpModelProto objective without scaling/offset.
|
||||
// Note that these bound correspond to valid bound for the problem of finding
|
||||
// a strictly better objective than the current one. Thus the lower bound is
|
||||
// always a valid bound for the global problem, but the upper bound is NOT.
|
||||
IntegerValue GetInnerObjectiveLowerBound();
|
||||
IntegerValue GetInnerObjectiveUpperBound();
|
||||
|
||||
// Updates the inner objective bounds.
|
||||
void UpdateInnerObjectiveBounds(const std::string& worker_info,
|
||||
IntegerValue lb, IntegerValue ub);
|
||||
|
||||
// Reads the new solution from the response and update our state. For an
|
||||
// optimization problem, we only do something if the solution is strictly
|
||||
// improving.
|
||||
//
|
||||
// TODO(user): Only the follwing fields from response are accessed here, we
|
||||
// might want a tighter API:
|
||||
// - solution_info
|
||||
// - solution
|
||||
// - solution_lower_bounds and solution_upper_bounds.
|
||||
void NewSolution(const CpSolverResponse& response, Model* model);
|
||||
|
||||
// Changes the solution to reflect the fact that the "improving" problem is
|
||||
// infeasible. This means that if we have a solution, we have proven
|
||||
// optimality, otherwise the global problem is infeasible.
|
||||
//
|
||||
// Note that this shouldn't be called before the solution is actually
|
||||
// reported. We check for this case in NewSolution().
|
||||
void NotifyThatImprovingProblemIsInfeasible(const std::string& worker_info);
|
||||
|
||||
// Sets the statistics in the response to the one of the solver inside the
|
||||
// given in-memory model. This does nothing if the model is nullptr.
|
||||
//
|
||||
// TODO(user): Also support merging statistics together.
|
||||
void SetStatsFromModel(Model* model);
|
||||
|
||||
private:
|
||||
bool is_maximize_ = false; // const.
|
||||
CpSolverResponse best_response_;
|
||||
void FillObjectiveValuesInBestResponse();
|
||||
void SetStatsFromModelInternal(Model* model);
|
||||
|
||||
const bool log_updates_;
|
||||
const CpModelProto& model_proto_;
|
||||
const WallTimer& wall_timer_;
|
||||
|
||||
absl::Mutex mutex_;
|
||||
|
||||
CpSolverResponse best_response_;
|
||||
|
||||
int num_solutions_ = 0;
|
||||
int64 inner_objective_lower_bound_ = kint64min;
|
||||
int64 inner_objective_upper_bound_ = kint64max;
|
||||
int64 best_solution_objective_value_ = kint64max;
|
||||
|
||||
int next_callback_id_ = 0;
|
||||
std::vector<std::pair<int, std::function<void(const CpSolverResponse&)>>>
|
||||
callbacks_;
|
||||
};
|
||||
|
||||
// This class manages a pool of lower and upper bounds on a set of variables in
|
||||
@@ -101,9 +156,7 @@ void RegisterVariableBoundsLevelZeroExport(
|
||||
const CpModelProto& model_proto, SharedBoundsManager* shared_bounds_manager,
|
||||
Model* model);
|
||||
|
||||
// Registers a callback to import new objective bounds. It will use callbacks
|
||||
// stored in the ObjectiveSynchronizationHelper to query external objective
|
||||
// bounds.
|
||||
// Registers a callback to import new objective bounds.
|
||||
//
|
||||
// Currently, standard search works fine with it.
|
||||
// LNS search and Core based search do not support it
|
||||
@@ -111,12 +164,8 @@ void RegisterObjectiveBoundsImport(
|
||||
SharedResponseManager* shared_response_manager, Model* model);
|
||||
|
||||
// Registers a callback that will report improving objective best bound.
|
||||
//
|
||||
// TODO(user): A solver can also improve the objective upper bound without
|
||||
// finding a solution and we should maybe share this as well.
|
||||
void RegisterObjectiveBestBoundExport(
|
||||
const CpModelProto& model_proto, bool log_progress,
|
||||
IntegerVariable objective_var, WallTimer* wall_timer,
|
||||
IntegerVariable objective_var,
|
||||
SharedResponseManager* shared_response_manager, Model* model);
|
||||
|
||||
// Stores information on the worker in the parallel context.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
@@ -93,6 +94,100 @@ void ProcessOneColumn(
|
||||
}
|
||||
}
|
||||
|
||||
void AddRegularPositiveTable(
|
||||
const std::vector<IntegerVariable>& vars,
|
||||
const std::vector<std::vector<int64>>& tuples,
|
||||
const std::vector<absl::flat_hash_set<int64>> values_per_var,
|
||||
int64 any_value, Model* model) {
|
||||
IntegerTrail* const integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
const int n = vars.size();
|
||||
std::vector<Literal> tuple_literals;
|
||||
tuple_literals.reserve(tuples.size());
|
||||
if (tuples.size() == 2) {
|
||||
tuple_literals.emplace_back(model->Add(NewBooleanVariable()), true);
|
||||
tuple_literals.emplace_back(tuple_literals[0].Negated());
|
||||
} else if (tuples.size() > 2) {
|
||||
for (int i = 0; i < tuples.size(); ++i) {
|
||||
tuple_literals.emplace_back(model->Add(NewBooleanVariable()), true);
|
||||
}
|
||||
model->Add(ClauseConstraint(tuple_literals));
|
||||
}
|
||||
|
||||
// Fully encode the variables using all the values appearing in the tuples.
|
||||
std::vector<Literal> active_tuple_literals;
|
||||
std::vector<IntegerValue> active_values;
|
||||
std::vector<Literal> any_tuple_literals;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
active_tuple_literals.clear();
|
||||
active_values.clear();
|
||||
any_tuple_literals.clear();
|
||||
const int64 first = tuples[0][i];
|
||||
bool all_equals = true;
|
||||
|
||||
for (int j = 0; j < tuple_literals.size(); ++j) {
|
||||
const int64 v = tuples[j][i];
|
||||
|
||||
if (v != first) {
|
||||
all_equals = false;
|
||||
}
|
||||
|
||||
if (v == any_value) {
|
||||
any_tuple_literals.push_back(tuple_literals[j]);
|
||||
} else {
|
||||
active_tuple_literals.push_back(tuple_literals[j]);
|
||||
active_values.push_back(IntegerValue(v));
|
||||
}
|
||||
}
|
||||
|
||||
if (all_equals && any_tuple_literals.empty() && first != any_value) {
|
||||
model->Add(Equality(vars[i], first));
|
||||
} else if (!active_tuple_literals.empty()) {
|
||||
const std::vector<int64> reached_values(values_per_var[i].begin(),
|
||||
values_per_var[i].end());
|
||||
integer_trail->UpdateInitialDomain(vars[i],
|
||||
Domain::FromValues(reached_values));
|
||||
model->Add(FullyEncodeVariable(vars[i]));
|
||||
ProcessOneColumn(active_tuple_literals, active_values,
|
||||
GetEncoding(vars[i], model), any_tuple_literals, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddFullyPrefixedPositiveTable(
|
||||
const std::vector<IntegerVariable>& vars,
|
||||
const std::vector<std::vector<int64>>& tuples,
|
||||
const std::vector<absl::flat_hash_set<int64>> values_per_var,
|
||||
int64 any_value, Model* model) {
|
||||
const int n = vars.size();
|
||||
std::vector<absl::flat_hash_map<IntegerValue, Literal>> encodings(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
model->Add(FullyEncodeVariable(vars[i]));
|
||||
encodings[i] = GetEncoding(vars[i], model);
|
||||
}
|
||||
|
||||
std::vector<Literal> clause;
|
||||
for (int j = 0; j < tuples.size(); ++j) {
|
||||
clause.clear();
|
||||
bool tuple_is_valid = true;
|
||||
for (int i = 0; i + 1 < n; ++i) {
|
||||
const int64 v = tuples[j][i];
|
||||
if (v == any_value) continue;
|
||||
if (!encodings[i].contains(IntegerValue(v))) {
|
||||
tuple_is_valid = false;
|
||||
break;
|
||||
}
|
||||
clause.push_back(gtl::FindOrDie(encodings[i], IntegerValue(v)).Negated());
|
||||
}
|
||||
const IntegerValue target_value = IntegerValue(tuples[j][n - 1]);
|
||||
if (tuple_is_valid && encodings[n - 1].contains(target_value)) {
|
||||
const Literal target_literal =
|
||||
gtl::FindOrDie(encodings[n - 1], target_value);
|
||||
clause.push_back(target_literal);
|
||||
model->Add(ClauseConstraint(clause));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CompressTuples(const std::vector<int64>& domain_sizes, int64 any_value,
|
||||
@@ -147,26 +242,17 @@ void CompressTuples(const std::vector<int64>& domain_sizes, int64 any_value,
|
||||
void AddTableConstraint(const std::vector<IntegerVariable>& vars,
|
||||
std::vector<std::vector<int64>> tuples, Model* model) {
|
||||
const int n = vars.size();
|
||||
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
|
||||
// Compute the set of possible values for each variable (from the table).
|
||||
std::vector<absl::flat_hash_set<int64>> values_per_var(n);
|
||||
for (const std::vector<int64>& tuple : tuples) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
values_per_var[i].insert(tuple[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter each values_per_var entries using the current variable domain.
|
||||
for (int i = 0; i < n; ++i) {
|
||||
FilterValues(vars[i], model, &values_per_var[i]);
|
||||
}
|
||||
|
||||
// Remove unreachable tuples.
|
||||
int index = 0;
|
||||
while (index < tuples.size()) {
|
||||
bool remove = false;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!gtl::ContainsKey(values_per_var[i], tuples[index][i])) {
|
||||
const int64 value = tuples[index][i];
|
||||
if (!values_per_var[i].contains(value) /* cached */ &&
|
||||
!integer_trail->InitialVariableDomain(vars[i]).Contains(value)) {
|
||||
remove = true;
|
||||
break;
|
||||
}
|
||||
@@ -175,6 +261,9 @@ void AddTableConstraint(const std::vector<IntegerVariable>& vars,
|
||||
tuples[index] = tuples.back();
|
||||
tuples.pop_back();
|
||||
} else {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
values_per_var[i].insert(tuples[index][i]);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
@@ -189,9 +278,21 @@ void AddTableConstraint(const std::vector<IntegerVariable>& vars,
|
||||
for (const std::vector<int64>& tuple : tuples) {
|
||||
prefix = tuple;
|
||||
prefix.pop_back();
|
||||
prefixes.insert(tuple);
|
||||
prefixes.insert(prefix);
|
||||
}
|
||||
double prefix_space_size = 1.0;
|
||||
for (int i = 0; i + 1 < n; ++i) {
|
||||
prefix_space_size *= values_per_var[i].size();
|
||||
}
|
||||
const bool prefix_is_key = prefixes.size() == tuples.size();
|
||||
const bool prefix_is_covering_domain = prefixes.size() == prefix_space_size;
|
||||
|
||||
if (prefix_is_key && !prefix_is_covering_domain) {
|
||||
VLOG(2) << "tuples = " << prefixes.size()
|
||||
<< " space = " << prefix_space_size << " | " << n;
|
||||
} else if (prefix_is_key && prefix_is_covering_domain) {
|
||||
VLOG(2) << "prefix = " << prefixes.size() << " | " << n;
|
||||
}
|
||||
|
||||
// Compress tuples.
|
||||
const int64 any_value = kint64min;
|
||||
@@ -206,93 +307,17 @@ void AddTableConstraint(const std::vector<IntegerVariable>& vars,
|
||||
// selected because these variables are just used by this constraint, so
|
||||
// only the information "can't be selected" is important.
|
||||
//
|
||||
// TODO(user): If a value in one column is unique, we don't need to create a
|
||||
// new BooleanVariable corresponding to this line since we can use the one
|
||||
// TODO(user): If a value in one column is unique, we don't need to create
|
||||
// a new BooleanVariable corresponding to this line since we can use the one
|
||||
// corresponding to this value in that column.
|
||||
//
|
||||
// Note that if there is just one tuple, there is no need to create such
|
||||
// variables since they are not used.
|
||||
std::vector<Literal> tuple_literals;
|
||||
tuple_literals.reserve(tuples.size());
|
||||
if (tuples.size() == 2) {
|
||||
tuple_literals.emplace_back(model->Add(NewBooleanVariable()), true);
|
||||
tuple_literals.emplace_back(tuple_literals[0].Negated());
|
||||
} else if (tuples.size() > 2) {
|
||||
for (int i = 0; i < tuples.size(); ++i) {
|
||||
tuple_literals.emplace_back(model->Add(NewBooleanVariable()), true);
|
||||
}
|
||||
model->Add(ClauseConstraint(tuple_literals));
|
||||
}
|
||||
|
||||
// Fully encode the variables using all the values appearing in the tuples.
|
||||
IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
std::vector<Literal> active_tuple_literals;
|
||||
std::vector<IntegerValue> active_values;
|
||||
std::vector<Literal> any_tuple_literals;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
active_tuple_literals.clear();
|
||||
active_values.clear();
|
||||
any_tuple_literals.clear();
|
||||
const int64 first = tuples[0][i];
|
||||
bool all_equals = true;
|
||||
|
||||
for (int j = 0; j < tuple_literals.size(); ++j) {
|
||||
const int64 v = tuples[j][i];
|
||||
|
||||
if (v != first) {
|
||||
all_equals = false;
|
||||
}
|
||||
|
||||
if (v == any_value) {
|
||||
any_tuple_literals.push_back(tuple_literals[j]);
|
||||
} else {
|
||||
active_tuple_literals.push_back(tuple_literals[j]);
|
||||
active_values.push_back(IntegerValue(v));
|
||||
}
|
||||
}
|
||||
|
||||
if (all_equals && any_tuple_literals.empty() && first != any_value) {
|
||||
model->Add(Equality(vars[i], first));
|
||||
} else if (!active_tuple_literals.empty()) {
|
||||
const std::vector<int64> reached_values(values_per_var[i].begin(),
|
||||
values_per_var[i].end());
|
||||
integer_trail->UpdateInitialDomain(vars[i],
|
||||
Domain::FromValues(reached_values));
|
||||
model->Add(FullyEncodeVariable(vars[i]));
|
||||
ProcessOneColumn(active_tuple_literals, active_values,
|
||||
GetEncoding(vars[i], model), any_tuple_literals, model);
|
||||
}
|
||||
}
|
||||
|
||||
if (prefix_is_key) {
|
||||
std::vector<absl::flat_hash_map<IntegerValue, Literal>> encodings(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
model->Add(FullyEncodeVariable(vars[i]));
|
||||
encodings[i] = GetEncoding(vars[i], model);
|
||||
}
|
||||
|
||||
std::vector<Literal> clause;
|
||||
for (int j = 0; j < tuples.size(); ++j) {
|
||||
clause.clear();
|
||||
bool tuple_is_valid = true;
|
||||
for (int i = 0; i + 1 < n; ++i) {
|
||||
const int64 v = tuples[j][i];
|
||||
if (v == any_value) continue;
|
||||
if (!gtl::ContainsKey(encodings[i], IntegerValue(v))) {
|
||||
tuple_is_valid = false;
|
||||
break;
|
||||
}
|
||||
clause.push_back(
|
||||
gtl::FindOrDie(encodings[i], IntegerValue(v)).Negated());
|
||||
}
|
||||
const IntegerValue target_value = IntegerValue(tuples[j][n - 1]);
|
||||
if (tuple_is_valid && gtl::ContainsKey(encodings[n - 1], target_value)) {
|
||||
const Literal target_literal =
|
||||
gtl::FindOrDie(encodings[n - 1], target_value);
|
||||
clause.push_back(target_literal);
|
||||
model->Add(ClauseConstraint(clause));
|
||||
}
|
||||
}
|
||||
if (prefix_is_key && prefix_is_covering_domain) {
|
||||
AddFullyPrefixedPositiveTable(vars, tuples, values_per_var, any_value,
|
||||
model);
|
||||
} else {
|
||||
AddRegularPositiveTable(vars, tuples, values_per_var, any_value, model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user