OR-Tools  9.6
sat_proto_solver.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
15 
16 #include <algorithm>
17 #include <cmath>
18 #include <cstdint>
19 #include <functional>
20 #include <limits>
21 #include <memory>
22 #include <string>
23 #include <type_traits>
24 #include <utility>
25 #include <vector>
26 
27 #include "absl/status/statusor.h"
28 #include "absl/types/span.h"
29 #include "ortools/linear_solver/linear_solver.pb.h"
33 #include "ortools/sat/cp_model.pb.h"
35 #include "ortools/sat/lp_utils.h"
37 #include "ortools/sat/sat_parameters.pb.h"
38 #include "ortools/util/logging.h"
40 
41 namespace operations_research {
42 
43 namespace {
44 
45 #if defined(PROTOBUF_INTERNAL_IMPL)
46 using google::protobuf::Message;
47 #else
48 using google::protobuf::Message;
49 #endif
50 
51 // Proto-lite disables some features of protos (see
52 // go/abp-libraries/proto2-lite) and messages inherit from MessageLite directly
53 // instead of inheriting from Message (which is itself a specialization of
54 // MessageLite).
55 constexpr bool kProtoLiteSatParameters =
57 
58 MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status,
59  bool has_objective) {
60  switch (status) {
61  case sat::CpSolverStatus::UNKNOWN:
62  return MPSOLVER_NOT_SOLVED;
64  return MPSOLVER_MODEL_INVALID;
66  return MPSOLVER_FEASIBLE;
68  return MPSOLVER_INFEASIBLE;
70  return MPSOLVER_OPTIMAL;
71  default: {
72  }
73  }
74  return MPSOLVER_ABNORMAL;
75 }
76 
77 sat::CpSolverStatus FromMPSolverResponseStatus(MPSolverResponseStatus status) {
78  switch (status) {
79  case MPSolverResponseStatus::MPSOLVER_OPTIMAL:
80  return sat::OPTIMAL;
81  case MPSolverResponseStatus::MPSOLVER_INFEASIBLE:
82  return sat::INFEASIBLE;
83  case MPSolverResponseStatus::MPSOLVER_MODEL_INVALID:
84  return sat::MODEL_INVALID;
85  default: {
86  }
87  }
88  return sat::UNKNOWN;
89 }
90 
91 MPSolutionResponse InfeasibleResponse(SolverLogger& logger,
92  std::string message) {
93  SOLVER_LOG(&logger, "Infeasible model detected in sat_solve_proto.\n",
94  message);
95 
96  // This is needed for our benchmark scripts.
97  if (logger.LoggingIsEnabled()) {
98  sat::CpSolverResponse cp_response;
99  cp_response.set_status(sat::CpSolverStatus::INFEASIBLE);
100  SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
101  }
102 
103  MPSolutionResponse response;
104  response.set_status(MPSolverResponseStatus::MPSOLVER_INFEASIBLE);
105  response.set_status_str(message);
106  return response;
107 }
108 
109 MPSolutionResponse ModelInvalidResponse(SolverLogger& logger,
110  std::string message) {
111  SOLVER_LOG(&logger, "Invalid model/parameters in sat_solve_proto.\n",
112  message);
113 
114  // This is needed for our benchmark scripts.
115  if (logger.LoggingIsEnabled()) {
116  sat::CpSolverResponse cp_response;
117  cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
118  SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
119  }
120 
121  MPSolutionResponse response;
122  response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID);
123  response.set_status_str(message);
124  return response;
125 }
126 
127 } // namespace
128 
129 absl::StatusOr<MPSolutionResponse> SatSolveProto(
130  MPModelRequest request, std::atomic<bool>* interrupt_solve,
131  std::function<void(const std::string&)> logging_callback,
132  std::function<void(const MPSolution&)> solution_callback) {
133  sat::SatParameters params;
134  params.set_log_search_progress(request.enable_internal_solver_output());
135  // Set it now so that it can be overwritten by the solver specific parameters.
136  if (request.has_solver_specific_parameters()) {
137  // See EncodeSatParametersAsString() documentation.
138  if (kProtoLiteSatParameters) {
139  if (!params.MergeFromString(request.solver_specific_parameters())) {
140  return absl::InvalidArgumentError(
141  "solver_specific_parameters is not a valid binary stream of the "
142  "SatParameters proto");
143  }
144  } else {
146  request.solver_specific_parameters(), &params)) {
147  return absl::InvalidArgumentError(
148  "solver_specific_parameters is not a valid textual representation "
149  "of the SatParameters proto");
150  }
151  }
152  }
153  if (request.has_solver_time_limit_seconds()) {
154  params.set_max_time_in_seconds(request.solver_time_limit_seconds());
155  }
156 
157  // TODO(user): We do not support all the parameters here. In particular the
158  // logs before the solver is called will not be appended to the response. Fix
159  // that, and remove code duplication for the logger config. One way should be
160  // to not touch/configure anything if the logger is already created while
161  // calling SolveCpModel() and call a common config function from here or from
162  // inside Solve()?
163  SolverLogger logger;
164  if (logging_callback != nullptr) {
165  logger.AddInfoLoggingCallback(logging_callback);
166  }
167  logger.EnableLogging(params.log_search_progress());
168  logger.SetLogToStdOut(params.log_to_stdout());
169 
170  // Model validation and delta handling.
171  MPSolutionResponse response;
173  &response)) {
174  // Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can
175  // also close trivial model (empty or trivially infeasible). So this is not
176  // always the MODEL_INVALID status.
177  //
178  // The logging is only needed for our benchmark script, so we use UNKNOWN
179  // here, but we could log the proper status instead.
180  if (logger.LoggingIsEnabled()) {
181  sat::CpSolverResponse cp_response;
182  cp_response.set_status(FromMPSolverResponseStatus(response.status()));
183  SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
184  }
185  return response;
186  }
187 
188  // We start by some extra validation since our code do not accept any kind
189  // of input.
190  MPModelProto* const mp_model = request.mutable_model();
191  if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model,
192  &logger)) {
193  return ModelInvalidResponse(logger, "Extra CP-SAT validation failed.");
194  }
195 
196  {
197  const std::string error = sat::ValidateParameters(params);
198  if (!error.empty()) {
199  return ModelInvalidResponse(
200  logger, absl::StrCat("Invalid CP-SAT parameters: ", error));
201  }
202  }
203 
204  // This is good to do before any presolve.
205  if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model, &logger)) {
206  return InfeasibleResponse(logger,
207  "An integer variable has an empty domain");
208  }
209 
210  // Coefficients really close to zero can cause issues.
211  // We remove them right away according to our parameters.
212  RemoveNearZeroTerms(params, mp_model, &logger);
213 
214  // Note(user): the LP presolvers API is a bit weird and keep a reference to
215  // the given GlopParameters, so we need to make sure it outlive them.
216  const glop::GlopParameters glop_params;
217  std::vector<std::unique_ptr<glop::Preprocessor>> for_postsolve;
218  if (!params.enumerate_all_solutions()) {
220  ApplyMipPresolveSteps(glop_params, mp_model, &for_postsolve, &logger);
221  switch (status) {
223  // Continue with the solve.
224  break;
226  return InfeasibleResponse(
227  logger, "Problem proven infeasible during MIP presolve");
229  return ModelInvalidResponse(
230  logger, "Problem detected invalid during MIP presolve");
231  default:
232  // TODO(user): We put the INFEASIBLE_OR_UNBOUNBED case here since there
233  // is no return status that exactly matches it.
234  if (params.log_search_progress()) {
235  // This is needed for our benchmark scripts.
236  sat::CpSolverResponse cp_response;
237  cp_response.set_status(sat::CpSolverStatus::UNKNOWN);
238  LOG(INFO) << CpSolverResponseStats(cp_response);
239  }
240  response.set_status(MPSolverResponseStatus::MPSOLVER_UNKNOWN_STATUS);
242  response.set_status_str(
243  "Problem proven infeasible or unbounded during MIP presolve");
244  }
245  return response;
246  }
247  }
248 
249  // We need to do that before the automatic detection of integers.
250  RemoveNearZeroTerms(params, mp_model, &logger);
251 
252  SOLVER_LOG(&logger, "");
253  SOLVER_LOG(&logger, "Scaling to pure integer problem.");
254 
255  const int num_variables = mp_model->variable_size();
256  std::vector<double> var_scaling(num_variables, 1.0);
257  if (params.mip_automatically_scale_variables()) {
258  var_scaling = sat::DetectImpliedIntegers(mp_model, &logger);
259  if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model, &logger)) {
260  return InfeasibleResponse(
261  logger, "A detected integer variable has an empty domain");
262  }
263  }
264  if (params.mip_var_scaling() != 1.0) {
265  const double max_bound = params.mip_scale_large_domain()
266  ? std::numeric_limits<double>::infinity()
267  : params.mip_max_bound();
268  const std::vector<double> other_scaling = sat::ScaleContinuousVariables(
269  params.mip_var_scaling(), max_bound, mp_model);
270  for (int i = 0; i < var_scaling.size(); ++i) {
271  var_scaling[i] *= other_scaling[i];
272  }
273  }
274 
275  // Abort if one only want to solve pure-IP model and we don't have one.
276  if (params.only_solve_ip()) {
277  bool all_integer = true;
278  for (const MPVariableProto& var : mp_model->variable()) {
279  if (!var.is_integer()) {
280  all_integer = false;
281  break;
282  }
283  }
284  if (!all_integer) {
285  return ModelInvalidResponse(
286  logger,
287  "The model contains non-integer variables but the parameter "
288  "'only_solve_ip' was set. Change this parameter if you "
289  "still want to solve a more constrained version of the original MIP "
290  "where non-integer variables can only take a finite set of values.");
291  }
292  }
293 
294  sat::CpModelProto cp_model;
295  if (!ConvertMPModelProtoToCpModelProto(params, *mp_model, &cp_model,
296  &logger)) {
297  return ModelInvalidResponse(logger,
298  "Failed to convert model into CP-SAT model");
299  }
300  DCHECK_EQ(cp_model.variables().size(), var_scaling.size());
301  DCHECK_EQ(cp_model.variables().size(), mp_model->variable().size());
302 
303  // Copy and scale the hint if there is one.
304  if (request.model().has_solution_hint()) {
305  auto* cp_model_hint = cp_model.mutable_solution_hint();
306  const int size = request.model().solution_hint().var_index().size();
307  for (int i = 0; i < size; ++i) {
308  const int var = request.model().solution_hint().var_index(i);
309  if (var >= var_scaling.size()) continue;
310 
311  // To handle weird hint input values, we cap any large value to +/-
312  // mip_max_bound() which is also the min/max value of any variable once
313  // scaled.
314  double value =
315  request.model().solution_hint().var_value(i) * var_scaling[var];
316  if (std::abs(value) > params.mip_max_bound()) {
317  value = value > 0 ? params.mip_max_bound() : -params.mip_max_bound();
318  }
319 
320  cp_model_hint->add_vars(var);
321  cp_model_hint->add_values(static_cast<int64_t>(std::round(value)));
322  }
323  }
324 
325  // We no longer need the request. Reclaim its memory.
326  const int old_num_variables = mp_model->variable().size();
327  const int old_num_constraints = mp_model->constraint().size();
328  request.Clear();
329 
330  // Configure model.
331  sat::Model sat_model;
332  sat_model.Register<SolverLogger>(&logger);
333  sat_model.Add(NewSatParameters(params));
334  if (interrupt_solve != nullptr) {
335  sat_model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
336  interrupt_solve);
337  }
338 
339  auto post_solve = [&](const sat::CpSolverResponse& cp_response) {
340  MPSolution mp_solution;
341  mp_solution.set_objective_value(cp_response.objective_value());
342  // Postsolve the bound shift and scaling.
343  glop::ProblemSolution glop_solution((glop::RowIndex(old_num_constraints)),
344  (glop::ColIndex(old_num_variables)));
345  for (int v = 0; v < glop_solution.primal_values.size(); ++v) {
346  glop_solution.primal_values[glop::ColIndex(v)] =
347  static_cast<double>(cp_response.solution(v)) / var_scaling[v];
348  }
349  for (int i = for_postsolve.size(); --i >= 0;) {
350  for_postsolve[i]->RecoverSolution(&glop_solution);
351  }
352  for (int v = 0; v < glop_solution.primal_values.size(); ++v) {
353  mp_solution.add_variable_value(
354  glop_solution.primal_values[glop::ColIndex(v)]);
355  }
356  return mp_solution;
357  };
358 
359  if (solution_callback != nullptr) {
361  [&](const sat::CpSolverResponse& cp_response) {
362  solution_callback(post_solve(cp_response));
363  }));
364  }
365 
366  // Solve.
367  const sat::CpSolverResponse cp_response =
368  sat::SolveCpModel(cp_model, &sat_model);
369 
370  // Convert the response.
371  //
372  // TODO(user): Implement the row and column status.
373  response.mutable_solve_info()->set_solve_wall_time_seconds(
374  cp_response.wall_time());
375  response.mutable_solve_info()->set_solve_user_time_seconds(
376  cp_response.user_time());
377  response.set_status(
378  ToMPSolverResponseStatus(cp_response.status(), cp_model.has_objective()));
379  if (response.status() == MPSOLVER_FEASIBLE ||
380  response.status() == MPSOLVER_OPTIMAL) {
381  response.set_objective_value(cp_response.objective_value());
382  response.set_best_objective_bound(cp_response.best_objective_bound());
383  MPSolution post_solved_solution = post_solve(cp_response);
384  *response.mutable_variable_value() =
385  std::move(*post_solved_solution.mutable_variable_value());
386  }
387 
388  // Copy and postsolve any additional solutions.
389  //
390  // TODO(user): Remove the postsolve hack of copying to a response.
391  for (const sat::CpSolverSolution& additional_solution :
392  cp_response.additional_solutions()) {
393  if (absl::MakeConstSpan(additional_solution.values()) ==
394  absl::MakeConstSpan(cp_response.solution())) {
395  continue;
396  }
397  double obj = cp_model.floating_point_objective().offset();
398  for (int i = 0; i < cp_model.floating_point_objective().vars_size(); ++i) {
399  const int32_t var = cp_model.floating_point_objective().vars(i);
400  const double obj_coef = cp_model.floating_point_objective().coeffs(i);
401  obj += additional_solution.values(var) * obj_coef;
402  }
403  // If the scaling factor is unset/zero, it is assumed to be one.
404  if (cp_model.objective().scaling_factor() != 0.0) {
405  obj *= cp_model.objective().scaling_factor();
406  }
407  sat::CpSolverResponse temp;
408  *temp.mutable_solution() = additional_solution.values();
409  temp.set_objective_value(obj);
410  *response.add_additional_solutions() = post_solve(temp);
411  }
412  const bool is_maximize = request.model().maximize();
413  std::sort(response.mutable_additional_solutions()->pointer_begin(),
414  response.mutable_additional_solutions()->pointer_end(),
415  [is_maximize](const MPSolution* left, const MPSolution* right) {
416  if (is_maximize) {
417  return left->objective_value() > right->objective_value();
418  } else {
419  return left->objective_value() < right->objective_value();
420  }
421  });
422  return response;
423 }
424 
425 std::string EncodeSatParametersAsString(const sat::SatParameters& parameters) {
426  if (kProtoLiteSatParameters) {
427  // Here we use SerializeToString() instead of SerializeAsString() since the
428  // later ignores errors and returns an empty string instead (which can be a
429  // valid value when no fields are set).
430  std::string bytes;
431  CHECK(parameters.SerializeToString(&bytes));
432  return bytes;
433  }
434 
436 }
437 
438 std::string SatSolverVersion() { return sat::CpSatSolverVersion(); }
439 
440 } // namespace operations_research
void SetLogToStdOut(bool enable)
Definition: util/logging.h:45
void AddInfoLoggingCallback(std::function< void(const std::string &message)> callback)
Definition: util/logging.cc:26
A simple class to enforce both an elapsed time limit and a deterministic time limit in the same threa...
Definition: time_limit.h:106
Class that owns everything related to a particular optimization model.
Definition: sat/model.h:42
T Add(std::function< T(Model *)> f)
This makes it possible to have a nicer API on the client side, and it allows both of these forms:
Definition: sat/model.h:85
void Register(T *non_owned_class)
Register a non-owned class that will be "singleton" in the model.
Definition: sat/model.h:175
T * GetOrCreate()
Returns an object of type T that is unique to this model (like a "local" singleton).
Definition: sat/model.h:110
SatParameters parameters
SharedResponseManager * response
int64_t value
IntVar * var
Definition: expr_array.cc:1874
absl::Status status
Definition: g_gurobi.cc:41
std::function< void(Model *)> NewFeasibleSolutionObserver(const std::function< void(const CpSolverResponse &response)> &observer)
Creates a solution observer with the model with model.Add(NewFeasibleSolutionObserver([](response){....
std::function< SatParameters(Model *)> NewSatParameters(const std::string &params)
Creates parameters for the solver, which you can add to the model with.
std::string CpSolverResponseStats(const CpSolverResponse &response, bool has_objective)
Returns a string with some statistics on the solver response.
std::string ValidateParameters(const SatParameters &params)
std::string CpSatSolverVersion()
Returns a string that describes the version of the solver.
void RemoveNearZeroTerms(const SatParameters &params, MPModelProto *mp_model, SolverLogger *logger)
bool ConvertMPModelProtoToCpModelProto(const SatParameters &params, const MPModelProto &mp_model, CpModelProto *cp_model, SolverLogger *logger)
bool MPModelProtoValidationBeforeConversion(const SatParameters &params, const MPModelProto &mp_model, SolverLogger *logger)
CpSolverResponse SolveCpModel(const CpModelProto &model_proto, Model *model)
Solves the given CpModelProto.
bool MakeBoundsOfIntegerVariablesInteger(const SatParameters &params, MPModelProto *mp_model, SolverLogger *logger)
std::vector< double > ScaleContinuousVariables(double scaling, double max_bound, MPModelProto *mp_model)
std::vector< double > DetectImpliedIntegers(MPModelProto *mp_model, SolverLogger *logger)
Collection of objects used to extend the Constraint Solver library.
bool ExtractValidMPModelInPlaceOrPopulateResponseStatus(MPModelRequest *request, MPSolutionResponse *response)
Like ExtractValidMPModelOrPopulateResponseStatus(), but works in-place: if the MPModel needed extract...
std::string ProtobufShortDebugString(const P &message)
absl::StatusOr< MPSolutionResponse > SatSolveProto(MPModelRequest request, std::atomic< bool > *interrupt_solve, std::function< void(const std::string &)> logging_callback, std::function< void(const MPSolution &)> solution_callback)
glop::ProblemStatus ApplyMipPresolveSteps(const glop::GlopParameters &glop_params, MPModelProto *model, std::vector< std::unique_ptr< glop::Preprocessor >> *for_postsolve, SolverLogger *logger)
std::string SatSolverVersion()
std::string EncodeSatParametersAsString(const sat::SatParameters &parameters)
bool ProtobufTextFormatMergeFromString(absl::string_view proto_text_string, ProtoType *proto)
std::string message
Definition: trace.cc:399
#define SOLVER_LOG(logger,...)
Definition: util/logging.h:69