Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sat_proto_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2024 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 <atomic>
18#include <cmath>
19#include <cstdint>
20#include <cstdlib>
21#include <functional>
22#include <limits>
23#include <memory>
24#include <optional>
25#include <string>
26#include <type_traits>
27#include <utility>
28#include <vector>
29
30#include "absl/log/check.h"
31#include "absl/strings/str_cat.h"
32#include "absl/types/span.h"
35#include "ortools/linear_solver/linear_solver.pb.h"
42#include "ortools/sat/cp_model.pb.h"
45#include "ortools/sat/model.h"
47#include "ortools/sat/sat_parameters.pb.h"
51
52namespace operations_research {
53
54namespace {
55
56MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status,
57 bool has_objective) {
58 switch (status) {
59 case sat::CpSolverStatus::UNKNOWN:
60 return MPSOLVER_NOT_SOLVED;
61 case sat::CpSolverStatus::MODEL_INVALID:
62 return MPSOLVER_MODEL_INVALID;
63 case sat::CpSolverStatus::FEASIBLE:
64 return MPSOLVER_FEASIBLE;
65 case sat::CpSolverStatus::INFEASIBLE:
66 return MPSOLVER_INFEASIBLE;
67 case sat::CpSolverStatus::OPTIMAL:
68 return MPSOLVER_OPTIMAL;
69 default: {
70 }
71 }
72 return MPSOLVER_ABNORMAL;
73}
74
75sat::CpSolverStatus FromMPSolverResponseStatus(MPSolverResponseStatus status) {
76 switch (status) {
77 case MPSolverResponseStatus::MPSOLVER_OPTIMAL:
78 return sat::OPTIMAL;
79 case MPSolverResponseStatus::MPSOLVER_INFEASIBLE:
80 return sat::INFEASIBLE;
81 case MPSolverResponseStatus::MPSOLVER_MODEL_INVALID:
82 return sat::MODEL_INVALID;
83 default: {
84 }
85 }
86 return sat::UNKNOWN;
87}
88
89MPSolutionResponse InfeasibleResponse(SolverLogger& logger,
90 std::string message) {
91 SOLVER_LOG(&logger, "Infeasible model detected in sat_solve_proto.\n",
92 message);
93
94 // This is needed for our benchmark scripts.
95 if (logger.LoggingIsEnabled()) {
96 sat::CpSolverResponse cp_response;
97 cp_response.set_status(sat::CpSolverStatus::INFEASIBLE);
98 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
99 }
100
101 MPSolutionResponse response;
102 response.set_status(MPSolverResponseStatus::MPSOLVER_INFEASIBLE);
103 response.set_status_str(message);
104 return response;
105}
106
107MPSolutionResponse InvalidModelResponse(SolverLogger& logger,
108 std::string message) {
109 SOLVER_LOG(&logger, "Invalid model in sat_solve_proto.\n", message);
110
111 // This is needed for our benchmark scripts.
112 if (logger.LoggingIsEnabled()) {
113 sat::CpSolverResponse cp_response;
114 cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
115 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
116 }
117
118 MPSolutionResponse response;
119 response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID);
120 response.set_status_str(message);
121 return response;
122}
123
124MPSolutionResponse InvalidParametersResponse(SolverLogger& logger,
125 std::string message) {
126 SOLVER_LOG(&logger, "Invalid parameters in sat_solve_proto.\n", message);
127
128 // This is needed for our benchmark scripts.
129 if (logger.LoggingIsEnabled()) {
130 sat::CpSolverResponse cp_response;
131 cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
132 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
133 }
134
135 MPSolutionResponse response;
136 response.set_status(
137 MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS);
138 response.set_status_str(message);
139 return response;
140}
141
142MPSolutionResponse TimeLimitResponse(SolverLogger& logger) {
143 SOLVER_LOG(&logger, "Time limit reached in sat_solve_proto.\n");
144
145 // This is needed for our benchmark scripts.
146 if (logger.LoggingIsEnabled()) {
147 sat::CpSolverResponse cp_response;
148 cp_response.set_status(sat::CpSolverStatus::UNKNOWN);
149 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
150 }
151
152 MPSolutionResponse response;
153 response.set_status(MPSolverResponseStatus::MPSOLVER_NOT_SOLVED);
154 response.set_status_str("Time limit reached in sat_solve_proto.");
155 return response;
156}
157
158} // namespace
159
160MPSolutionResponse SatSolveProto(
161 LazyMutableCopy<MPModelRequest> request, std::atomic<bool>* interrupt_solve,
162 std::function<void(const std::string&)> logging_callback,
163 std::function<void(const MPSolution&)> solution_callback) {
164 sat::SatParameters params;
165 params.set_log_search_progress(request->enable_internal_solver_output());
166
167 // TODO(user): We do not support all the parameters here. In particular the
168 // logs before the solver is called will not be appended to the response. Fix
169 // that, and remove code duplication for the logger config. One way should be
170 // to not touch/configure anything if the logger is already created while
171 // calling SolveCpModel() and call a common config function from here or from
172 // inside Solve()?
173 SolverLogger logger;
174 if (logging_callback != nullptr) {
175 logger.AddInfoLoggingCallback(logging_callback);
176 }
177 logger.EnableLogging(params.log_search_progress());
178 logger.SetLogToStdOut(params.log_to_stdout());
179
180 // Set it now so that it can be overwritten by the solver specific parameters.
181 if (request->has_solver_specific_parameters()) {
182 // See EncodeSatParametersAsString() documentation.
183 if constexpr (!std::is_base_of<Message, sat::SatParameters>::value) {
184 if (!params.MergeFromString(request->solver_specific_parameters())) {
185 return InvalidParametersResponse(
186 logger,
187 "solver_specific_parameters is not a valid binary stream of the "
188 "SatParameters proto");
189 }
190 } else {
192 request->solver_specific_parameters(), &params)) {
193 return InvalidParametersResponse(
194 logger,
195 "solver_specific_parameters is not a valid textual representation "
196 "of the SatParameters proto");
197 }
198 }
199 }
200
201 // Validate parameters.
202 {
203 const std::string error = sat::ValidateParameters(params);
204 if (!error.empty()) {
205 return InvalidParametersResponse(
206 logger, absl::StrCat("Invalid CP-SAT parameters: ", error));
207 }
208 }
209
210 // Reconfigure the logger in case the solver_specific_parameters overwrite its
211 // configuration. Note that the invalid parameter message will be logged
212 // before that though according to request.enable_internal_solver_output().
213 logger.EnableLogging(params.log_search_progress());
214 logger.SetLogToStdOut(params.log_to_stdout());
215
216 if (request->has_solver_time_limit_seconds()) {
217 params.set_max_time_in_seconds(request->solver_time_limit_seconds());
218 }
219
220 std::unique_ptr<TimeLimit> time_limit = TimeLimit::FromParameters(params);
221
222 // Model validation and delta handling.
223 MPSolutionResponse response;
224 std::optional<LazyMutableCopy<MPModelProto>> optional_model =
225 GetMPModelOrPopulateResponse(request, &response);
226 if (!optional_model) {
227 // Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can
228 // also close trivial model (empty or trivially infeasible). So this is not
229 // always the MODEL_INVALID status.
230 //
231 // The logging is only needed for our benchmark script, so we use UNKNOWN
232 // here, but we could log the proper status instead.
233 if (logger.LoggingIsEnabled()) {
234 sat::CpSolverResponse cp_response;
235 cp_response.set_status(FromMPSolverResponseStatus(response.status()));
236 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
237 }
238 return response;
239 }
240
241 // We will presolve directly on the MPModelProto, so get a copy or transfer
242 // ownership from the LazyMutableCopy<MPModelProto>().
243 std::unique_ptr<MPModelProto> mp_model =
244 std::move(optional_model).value().copy_or_move_as_unique_ptr();
245
246 // The request is no longer needed after this.
247 // Important: we need to copy the model above before clearing this.
248 std::move(request).dispose();
249
250 // We start by some extra validation since our code do not accept any kind
251 // of input.
252 if (params.mip_treat_high_magnitude_bounds_as_infinity()) {
253 sat::ChangeLargeBoundsToInfinity(params.mip_max_valid_magnitude(),
254 mp_model.get(), &logger);
255 }
256 if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model,
257 &logger)) {
258 return InvalidModelResponse(logger, "Extra CP-SAT validation failed.");
259 }
260
261 // This is good to do before any presolve.
262 if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(),
263 &logger)) {
264 return InfeasibleResponse(logger,
265 "An integer variable has an empty domain");
266 }
267
268 // Coefficients really close to zero can cause issues.
269 // We remove them right away according to our parameters.
270 RemoveNearZeroTerms(params, mp_model.get(), &logger);
271
272 // Note(user): the LP presolvers API is a bit weird and keep a reference to
273 // the given GlopParameters, so we need to make sure it outlive them.
274 const glop::GlopParameters glop_params;
275 std::vector<std::unique_ptr<glop::Preprocessor>> for_postsolve;
276 if (!params.enumerate_all_solutions() && params.mip_presolve_level() > 0) {
278 glop_params, mp_model.get(), &for_postsolve, &logger);
279 switch (status) {
281 // Continue with the solve.
282 break;
284 return InfeasibleResponse(
285 logger, "Problem proven infeasible during MIP presolve");
287 return InvalidModelResponse(
288 logger, "Problem detected invalid during MIP presolve");
289 default:
290 // TODO(user): We put the INFEASIBLE_OR_UNBOUNBED case here since there
291 // is no return status that exactly matches it.
292 if (params.log_search_progress()) {
293 // This is needed for our benchmark scripts.
294 sat::CpSolverResponse cp_response;
295 cp_response.set_status(sat::CpSolverStatus::UNKNOWN);
296 SOLVER_LOG(&logger, "MIP presolve: problem infeasible or unbounded.");
297 LOG(INFO) << CpSolverResponseStats(cp_response);
298 }
299 response.set_status(MPSolverResponseStatus::MPSOLVER_UNKNOWN_STATUS);
301 response.set_status_str(
302 "Problem proven infeasible or unbounded during MIP presolve");
303 }
304 return response;
305 }
306 }
307
308 if (time_limit->LimitReached()) {
309 return TimeLimitResponse(logger);
310 }
311 // We need to do that before the automatic detection of integers.
312 RemoveNearZeroTerms(params, mp_model.get(), &logger);
313
314 SOLVER_LOG(&logger, "");
315 SOLVER_LOG(&logger, "Scaling to pure integer problem.");
316
317 const int num_variables = mp_model->variable_size();
318 std::vector<double> var_scaling(num_variables, 1.0);
319 if (params.mip_automatically_scale_variables()) {
320 var_scaling = sat::DetectImpliedIntegers(mp_model.get(), &logger);
321 if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(),
322 &logger)) {
323 return InfeasibleResponse(
324 logger, "A detected integer variable has an empty domain");
325 }
326 }
327 if (params.mip_var_scaling() != 1.0) {
328 const double max_bound = params.mip_scale_large_domain()
329 ? std::numeric_limits<double>::infinity()
330 : params.mip_max_bound();
331 const std::vector<double> other_scaling = sat::ScaleContinuousVariables(
332 params.mip_var_scaling(), max_bound, mp_model.get());
333 for (int i = 0; i < var_scaling.size(); ++i) {
334 var_scaling[i] *= other_scaling[i];
335 }
336 }
337
338 // Abort if one only want to solve pure-IP model and we don't have one.
339 if (params.only_solve_ip()) {
340 bool all_integer = true;
341 for (const MPVariableProto& var : mp_model->variable()) {
342 if (!var.is_integer()) {
343 all_integer = false;
344 break;
345 }
346 }
347 if (!all_integer) {
348 return InvalidModelResponse(
349 logger,
350 "The model contains non-integer variables but the parameter "
351 "'only_solve_ip' was set. Change this parameter if you "
352 "still want to solve a more constrained version of the original MIP "
353 "where non-integer variables can only take a finite set of values.");
354 }
355 }
356
357 sat::CpModelProto cp_model;
358 if (!ConvertMPModelProtoToCpModelProto(params, *mp_model, &cp_model,
359 &logger)) {
360 return InvalidModelResponse(logger,
361 "Failed to convert model into CP-SAT model");
362 }
363 DCHECK_EQ(cp_model.variables().size(), var_scaling.size());
364 DCHECK_EQ(cp_model.variables().size(), mp_model->variable().size());
365
366 // Copy and scale the hint if there is one.
367 if (mp_model->has_solution_hint()) {
368 auto* cp_model_hint = cp_model.mutable_solution_hint();
369 const int size = mp_model->solution_hint().var_index().size();
370 for (int i = 0; i < size; ++i) {
371 const int var = mp_model->solution_hint().var_index(i);
372 if (var >= var_scaling.size()) continue;
373
374 // To handle weird hint input values, we cap any large value to +/-
375 // mip_max_bound() which is also the min/max value of any variable once
376 // scaled.
377 double value = mp_model->solution_hint().var_value(i) * var_scaling[var];
378 if (std::abs(value) > params.mip_max_bound()) {
379 value = value > 0 ? params.mip_max_bound() : -params.mip_max_bound();
380 }
381
382 cp_model_hint->add_vars(var);
383 cp_model_hint->add_values(static_cast<int64_t>(std::round(value)));
384 }
385 }
386
387 // We no longer need the mp_model after this, reclaime its memory.
388 const int old_num_variables = mp_model->variable().size();
389 const int old_num_constraints = mp_model->constraint().size();
390 const bool is_maximize = mp_model->maximize();
391 mp_model.reset();
392
393 params.set_max_time_in_seconds(time_limit->GetTimeLeft());
394 if (time_limit->GetDeterministicTimeLeft() !=
395 std::numeric_limits<double>::infinity()) {
396 params.set_max_deterministic_time(time_limit->GetDeterministicTimeLeft());
397 }
398
399 // Configure model.
400 sat::Model sat_model;
401 sat_model.Register<SolverLogger>(&logger);
402 sat_model.Add(NewSatParameters(params));
403 if (interrupt_solve != nullptr) {
404 sat_model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
405 interrupt_solve);
406 }
407
408 auto post_solve = [&](const sat::CpSolverResponse& cp_response) {
409 MPSolution mp_solution;
410 mp_solution.set_objective_value(cp_response.objective_value());
411 // Postsolve the bound shift and scaling.
412 glop::ProblemSolution glop_solution((glop::RowIndex(old_num_constraints)),
413 (glop::ColIndex(old_num_variables)));
414 for (int v = 0; v < glop_solution.primal_values.size(); ++v) {
415 glop_solution.primal_values[glop::ColIndex(v)] =
416 static_cast<double>(cp_response.solution(v)) / var_scaling[v];
417 }
418 for (int i = for_postsolve.size(); --i >= 0;) {
419 for_postsolve[i]->RecoverSolution(&glop_solution);
420 }
421 for (int v = 0; v < glop_solution.primal_values.size(); ++v) {
422 mp_solution.add_variable_value(
423 glop_solution.primal_values[glop::ColIndex(v)]);
424 }
425 return mp_solution;
426 };
427
428 if (solution_callback != nullptr) {
430 [&](const sat::CpSolverResponse& cp_response) {
431 solution_callback(post_solve(cp_response));
432 }));
433 }
434
435 // Solve.
436 const sat::CpSolverResponse cp_response =
437 sat::SolveCpModel(cp_model, &sat_model);
438
439 // Convert the response.
440 //
441 // TODO(user): Implement the row and column status.
442 response.mutable_solve_info()->set_solve_wall_time_seconds(
443 cp_response.wall_time());
444 response.mutable_solve_info()->set_solve_user_time_seconds(
445 cp_response.user_time());
446 response.set_status(
447 ToMPSolverResponseStatus(cp_response.status(), cp_model.has_objective()));
448 if (response.status() == MPSOLVER_FEASIBLE ||
449 response.status() == MPSOLVER_OPTIMAL) {
450 response.set_objective_value(cp_response.objective_value());
451 response.set_best_objective_bound(cp_response.best_objective_bound());
452 MPSolution post_solved_solution = post_solve(cp_response);
453 *response.mutable_variable_value() =
454 std::move(*post_solved_solution.mutable_variable_value());
455 }
456
457 // Copy and postsolve any additional solutions.
458 //
459 // TODO(user): Remove the postsolve hack of copying to a response.
460 for (const sat::CpSolverSolution& additional_solution :
461 cp_response.additional_solutions()) {
462 if (absl::MakeConstSpan(additional_solution.values()) ==
463 absl::MakeConstSpan(cp_response.solution())) {
464 continue;
465 }
466 double obj = cp_model.floating_point_objective().offset();
467 for (int i = 0; i < cp_model.floating_point_objective().vars_size(); ++i) {
468 const int32_t var = cp_model.floating_point_objective().vars(i);
469 const double obj_coef = cp_model.floating_point_objective().coeffs(i);
470 obj += additional_solution.values(var) * obj_coef;
471 }
472 // If the scaling factor is unset/zero, it is assumed to be one.
473 if (cp_model.objective().scaling_factor() != 0.0) {
474 obj *= cp_model.objective().scaling_factor();
475 }
476 sat::CpSolverResponse temp;
477 *temp.mutable_solution() = additional_solution.values();
478 temp.set_objective_value(obj);
479 *response.add_additional_solutions() = post_solve(temp);
480 }
481 std::sort(response.mutable_additional_solutions()->pointer_begin(),
482 response.mutable_additional_solutions()->pointer_end(),
483 [is_maximize](const MPSolution* left, const MPSolution* right) {
484 if (is_maximize) {
485 return left->objective_value() > right->objective_value();
486 } else {
487 return left->objective_value() < right->objective_value();
488 }
489 });
490 return response;
491}
492
493std::string SatSolverVersion() { return sat::CpSatSolverVersion(); }
494
495} // namespace operations_research
IntegerValue size
void EnableLogging(bool enable)
Definition logging.h:46
void SetLogToStdOut(bool enable)
Should all messages be displayed on stdout ?
Definition logging.h:52
bool LoggingIsEnabled() const
Returns true iff logging is enabled.
Definition logging.h:49
void AddInfoLoggingCallback(std::function< void(const std::string &message)> callback)
Definition logging.cc:29
static std::unique_ptr< TimeLimit > FromParameters(const Parameters &parameters)
Definition time_limit.h:140
T Add(std::function< T(Model *)> f)
Definition model.h:89
void Register(T *non_owned_class)
Definition model.h:179
int64_t value
IntVar * var
absl::Status status
Definition g_gurobi.cc:44
time_limit
Definition solve.cc:22
ProblemStatus
Different statuses for a given problem.
Definition lp_types.h:107
@ INVALID_PROBLEM
The input problem was invalid (see LinearProgram.IsValid()).
@ INIT
The solver didn't had a chance to prove anything.
bool MakeBoundsOfIntegerVariablesInteger(const SatParameters &params, MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:204
void ChangeLargeBoundsToInfinity(double max_magnitude, MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:237
std::vector< double > DetectImpliedIntegers(MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:482
std::string ValidateParameters(const SatParameters &params)
CpSolverResponse SolveCpModel(const CpModelProto &model_proto, Model *model)
std::function< void(Model *)> NewFeasibleSolutionObserver(const std::function< void(const CpSolverResponse &response)> &callback)
std::vector< double > ScaleContinuousVariables(double scaling, double max_bound, MPModelProto *mp_model)
Definition lp_utils.cc:110
std::string CpSolverResponseStats(const CpSolverResponse &response, bool has_objective)
bool MPModelProtoValidationBeforeConversion(const SatParameters &params, const MPModelProto &mp_model, SolverLogger *logger)
Definition lp_utils.cc:418
In SWIG mode, we don't want anything besides these top-level includes.
MPSolutionResponse SatSolveProto(LazyMutableCopy< MPModelRequest > request, std::atomic< bool > *interrupt_solve, std::function< void(const std::string &)> logging_callback, std::function< void(const MPSolution &)> solution_callback)
std::string SatSolverVersion()
Returns a string that describes the version of the CP-SAT solver.
glop::ProblemStatus ApplyMipPresolveSteps(const glop::GlopParameters &glop_params, MPModelProto *model, std::vector< std::unique_ptr< glop::Preprocessor > > *for_postsolve, SolverLogger *logger)
std::optional< LazyMutableCopy< MPModelProto > > GetMPModelOrPopulateResponse(LazyMutableCopy< MPModelRequest > &request, MPSolutionResponse *response)
bool ProtobufTextFormatMergeFromString(absl::string_view proto_text_string, ProtoType *proto)
Definition proto_utils.h:66
Contains the solution of a LinearProgram as returned by a preprocessor.
Definition lp_data.h:671
std::string message
Definition trace.cc:397
#define SOLVER_LOG(logger,...)
Definition logging.h:109