Google OR-Tools v9.15
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-2025 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/log/log.h"
32#include "absl/strings/str_cat.h"
33#include "absl/types/span.h"
45#include "ortools/sat/model.h"
51
52namespace operations_research {
53
54namespace {
55
56MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status,
57 bool has_objective) {
58 switch (status) {
64 return MPSOLVER_FEASIBLE;
68 return MPSOLVER_OPTIMAL;
69 default: {
70 }
71 }
72 return MPSOLVER_ABNORMAL;
73}
74
75sat::CpSolverStatus FromMPSolverResponseStatus(MPSolverResponseStatus status) {
76 switch (status) {
78 return sat::OPTIMAL;
80 return sat::INFEASIBLE;
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;
98 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
99 }
100
101 MPSolutionResponse response;
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;
115 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
116 }
117
118 MPSolutionResponse response;
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;
132 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
133 }
134
135 MPSolutionResponse response;
136 response.set_status(
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;
149 SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
150 }
151
152 MPSolutionResponse response;
154 response.set_status_str("Time limit reached in sat_solve_proto.");
155 return response;
156}
157
158} // namespace
159
162 sat::CpSolverResponse* cp_response,
163 std::function<void(const MPSolution&)> solution_callback) {
164 SolverLogger* logger = sat_model->GetOrCreate<SolverLogger>();
165 sat::SatParameters& params = *sat_model->GetOrCreate<sat::SatParameters>();
166 TimeLimit* time_limit = sat_model->GetOrCreate<TimeLimit>();
167
168 // Model validation and delta handling.
169 MPSolutionResponse response;
170 std::optional<LazyMutableCopy<MPModelProto>> optional_model =
171 GetMPModelOrPopulateResponse(request, &response);
172 if (!optional_model) {
173 // Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can
174 // also close trivial model (empty or trivially infeasible). So this is not
175 // always the MODEL_INVALID status.
176 //
177 // The logging is only needed for our benchmark script, so we use UNKNOWN
178 // here, but we could log the proper status instead.
179 if (logger->LoggingIsEnabled()) {
180 sat::CpSolverResponse cp_response;
181 cp_response.set_status(FromMPSolverResponseStatus(response.status()));
182 SOLVER_LOG(logger, CpSolverResponseStats(cp_response));
183 }
184 return response;
185 }
186
187 // We will presolve directly on the MPModelProto, so get a copy or transfer
188 // ownership from the LazyMutableCopy<MPModelProto>().
189 std::unique_ptr<MPModelProto> mp_model =
190 std::move(optional_model).value().copy_or_move_as_unique_ptr();
191
192 // The request is no longer needed after this.
193 // Important: we need to copy the model above before clearing this.
194 std::move(request).dispose();
195
196 // We start by some extra validation since our code do not accept any kind
197 // of input.
198 if (params.mip_treat_high_magnitude_bounds_as_infinity()) {
199 sat::ChangeLargeBoundsToInfinity(params.mip_max_valid_magnitude(),
200 mp_model.get(), logger);
201 }
202 if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model, logger)) {
203 return InvalidModelResponse(*logger, "Extra CP-SAT validation failed.");
204 }
205
206 // This is good to do before any presolve.
207 if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(),
208 logger)) {
209 return InfeasibleResponse(*logger,
210 "An integer variable has an empty domain");
211 }
212
213 // Coefficients really close to zero can cause issues.
214 // We remove them right away according to our parameters.
215 RemoveNearZeroTerms(params, mp_model.get(), logger);
216
217 // Note(user): the LP presolvers API is a bit weird and keep a reference to
218 // the given GlopParameters, so we need to make sure it outlive them.
219 const glop::GlopParameters glop_params;
220 std::vector<std::unique_ptr<glop::Preprocessor>> for_postsolve;
221 if (!params.enumerate_all_solutions() && params.mip_presolve_level() > 0) {
223 glop_params, mp_model.get(), &for_postsolve, logger);
224 switch (status) {
226 // Continue with the solve.
227 break;
229 return InfeasibleResponse(
230 *logger, "Problem proven infeasible during MIP presolve");
232 return InvalidModelResponse(
233 *logger, "Problem detected invalid during MIP presolve");
234 default:
235 // TODO(user): We put the INFEASIBLE_OR_UNBOUNBED case here since there
236 // is no return status that exactly matches it.
237 if (params.log_search_progress()) {
238 // This is needed for our benchmark scripts.
239 sat::CpSolverResponse cp_response;
241 SOLVER_LOG(logger, "MIP presolve: problem infeasible or unbounded.");
242 LOG(INFO) << CpSolverResponseStats(cp_response);
243 }
246 response.set_status_str(
247 "Problem proven infeasible or unbounded during MIP presolve");
248 }
249 return response;
250 }
251 }
252
253 if (time_limit->LimitReached()) {
254 return TimeLimitResponse(*logger);
255 }
256 // We need to do that before the automatic detection of integers.
257 RemoveNearZeroTerms(params, mp_model.get(), logger);
258
259 SOLVER_LOG(logger, "");
260 SOLVER_LOG(logger, "Scaling to pure integer problem.");
261
262 const int num_variables = mp_model->variable_size();
263 std::vector<double> var_scaling(num_variables, 1.0);
264 if (params.mip_automatically_scale_variables()) {
265 var_scaling = sat::DetectImpliedIntegers(mp_model.get(), logger);
266 if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(),
267 logger)) {
268 return InfeasibleResponse(
269 *logger, "A detected integer variable has an empty domain");
270 }
271 }
272 if (params.mip_var_scaling() != 1.0) {
273 const double max_bound = params.mip_scale_large_domain()
274 ? std::numeric_limits<double>::infinity()
275 : params.mip_max_bound();
276 const std::vector<double> other_scaling = sat::ScaleContinuousVariables(
277 params.mip_var_scaling(), max_bound, mp_model.get());
278 for (int i = 0; i < var_scaling.size(); ++i) {
279 var_scaling[i] *= other_scaling[i];
280 }
281 }
282
283 // Abort if one only want to solve pure-IP model and we don't have one.
284 if (params.only_solve_ip()) {
285 bool all_integer = true;
286 for (const MPVariableProto& var : mp_model->variable()) {
287 if (!var.is_integer()) {
288 all_integer = false;
289 break;
290 }
291 }
292 if (!all_integer) {
293 return InvalidModelResponse(
294 *logger,
295 "The model contains non-integer variables but the parameter "
296 "'only_solve_ip' was set. Change this parameter if you "
297 "still want to solve a more constrained version of the original MIP "
298 "where non-integer variables can only take a finite set of values.");
299 }
300 }
301
302 sat::CpModelProto cp_model;
303 if (!ConvertMPModelProtoToCpModelProto(params, *mp_model, &cp_model,
304 logger)) {
305 return InvalidModelResponse(*logger,
306 "Failed to convert model into CP-SAT model");
307 }
308 DCHECK_EQ(cp_model.variables().size(), var_scaling.size());
309 DCHECK_EQ(cp_model.variables().size(), mp_model->variable().size());
310
311 // Copy and scale the hint if there is one.
312 if (mp_model->has_solution_hint()) {
313 auto* cp_model_hint = cp_model.mutable_solution_hint();
314 const int size = mp_model->solution_hint().var_index().size();
315 for (int i = 0; i < size; ++i) {
316 const int var = mp_model->solution_hint().var_index(i);
317 if (var >= var_scaling.size()) continue;
318
319 // To handle weird hint input values, we cap any large value to +/-
320 // mip_max_bound() which is also the min/max value of any variable once
321 // scaled.
322 double value = mp_model->solution_hint().var_value(i) * var_scaling[var];
323 if (std::abs(value) > params.mip_max_bound()) {
324 value = value > 0 ? params.mip_max_bound() : -params.mip_max_bound();
325 }
326
327 cp_model_hint->add_vars(var);
328 cp_model_hint->add_values(static_cast<int64_t>(std::round(value)));
329 }
330 }
331
332 // We no longer need the mp_model after this, reclaime its memory.
333 const int old_num_variables = mp_model->variable().size();
334 const int old_num_constraints = mp_model->constraint().size();
335 const bool is_maximize = mp_model->maximize();
336 mp_model.reset();
337
338 // Configure model.
339 auto post_solve = [&](const sat::CpSolverResponse& sat_response) {
340 MPSolution mp_solution;
341 mp_solution.set_objective_value(sat_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>(sat_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& sat_response) {
362 solution_callback(post_solve(sat_response));
363 }));
364 }
365
366 // Solve.
367 *cp_response = sat::SolveCpModel(cp_model, sat_model);
368
369 // Convert the response.
370 //
371 // TODO(user): Implement the row and column status.
372 response.mutable_solve_info()->set_solve_wall_time_seconds(
373 cp_response->wall_time());
374 response.mutable_solve_info()->set_solve_user_time_seconds(
375 cp_response->user_time());
376 response.set_status(ToMPSolverResponseStatus(cp_response->status(),
377 cp_model.has_objective()));
378 if (response.status() == MPSOLVER_FEASIBLE ||
379 response.status() == MPSOLVER_OPTIMAL) {
380 response.set_objective_value(cp_response->objective_value());
381 response.set_best_objective_bound(cp_response->best_objective_bound());
382 MPSolution post_solved_solution = post_solve(*cp_response);
383 *response.mutable_variable_value() =
384 std::move(*post_solved_solution.mutable_variable_value());
385 }
386
387 // Copy and postsolve any additional solutions.
388 //
389 // TODO(user): Remove the postsolve hack of copying to a response.
390 for (const sat::CpSolverSolution& additional_solution :
391 cp_response->additional_solutions()) {
392 if (absl::MakeConstSpan(additional_solution.values()) ==
393 absl::MakeConstSpan(cp_response->solution())) {
394 continue;
395 }
396 double obj = cp_model.floating_point_objective().offset();
397 for (int i = 0; i < cp_model.floating_point_objective().vars_size(); ++i) {
398 const int32_t var = cp_model.floating_point_objective().vars(i);
399 const double obj_coef = cp_model.floating_point_objective().coeffs(i);
400 obj += additional_solution.values(var) * obj_coef;
401 }
402 // If the scaling factor is unset/zero, it is assumed to be one.
403 if (cp_model.objective().scaling_factor() != 0.0) {
404 obj *= cp_model.objective().scaling_factor();
405 }
407 *temp.mutable_solution() = additional_solution.values();
408 temp.set_objective_value(obj);
409 *response.add_additional_solutions() = post_solve(temp);
410 }
411 std::sort(response.mutable_additional_solutions()->pointer_begin(),
412 response.mutable_additional_solutions()->pointer_end(),
413 [is_maximize](const MPSolution* left, const MPSolution* right) {
414 if (is_maximize) {
415 return left->objective_value() > right->objective_value();
416 } else {
417 return left->objective_value() < right->objective_value();
418 }
419 });
420 return response;
421}
422
424 LazyMutableCopy<MPModelRequest> request, std::atomic<bool>* interrupt_solve,
425 std::function<void(const std::string&)> logging_callback,
426 std::function<void(const MPSolution&)> solution_callback,
427 std::function<void(const double)> best_bound_callback) {
428 sat::Model sat_model;
429 sat::SatParameters& params = *sat_model.GetOrCreate<sat::SatParameters>();
430 params.set_log_search_progress(request->enable_internal_solver_output());
431
432 // TODO(user): We do not support all the parameters here. In particular the
433 // logs before the solver is called will not be appended to the response. Fix
434 // that, and remove code duplication for the logger config. One way should be
435 // to not touch/configure anything if the logger is already created while
436 // calling SolveCpModel() and call a common config function from here or from
437 // inside Solve()?
438 SolverLogger* logger = sat_model.GetOrCreate<SolverLogger>();
439 if (logging_callback != nullptr) {
440 logger->AddInfoLoggingCallback(logging_callback);
441 }
442 logger->EnableLogging(params.log_search_progress());
443 logger->SetLogToStdOut(params.log_to_stdout());
444
445 // Set it now so that it can be overwritten by the solver specific parameters.
446 if (request->has_solver_specific_parameters()) {
447 // See EncodeSatParametersAsString() documentation.
448 if constexpr (!std::is_base_of<Message, sat::SatParameters>::value) {
449 if (!params.MergeFromString(request->solver_specific_parameters())) {
450 return InvalidParametersResponse(
451 *logger,
452 "solver_specific_parameters is not a valid binary stream of the "
453 "SatParameters proto");
454 }
455 } else {
457 request->solver_specific_parameters(), &params)) {
458 return InvalidParametersResponse(
459 *logger,
460 "solver_specific_parameters is not a valid textual representation "
461 "of the SatParameters proto");
462 }
463 }
464 }
465
466 // Validate parameters.
467 {
468 const std::string error = sat::ValidateParameters(params);
469 if (!error.empty()) {
470 return InvalidParametersResponse(
471 *logger, absl::StrCat("Invalid CP-SAT parameters: ", error));
472 }
473 }
474
475 // Reconfigure the logger in case the solver_specific_parameters overwrite its
476 // configuration. Note that the invalid parameter message will be logged
477 // before that though according to request.enable_internal_solver_output().
478 logger->EnableLogging(params.log_search_progress());
479 logger->SetLogToStdOut(params.log_to_stdout());
480
481 if (request->has_solver_time_limit_seconds()) {
482 params.set_max_time_in_seconds(request->solver_time_limit_seconds());
483 }
484
485 sat_model.GetOrCreate<TimeLimit>()->ResetLimitFromParameters(params);
486
487 if (interrupt_solve != nullptr) {
488 sat_model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
489 interrupt_solve);
490 }
491
492 if (best_bound_callback != nullptr) {
493 sat_model.Add(sat::NewBestBoundCallback(best_bound_callback));
494 }
495
496 sat::CpSolverResponse cp_response;
497 return SatSolveProtoInternal(std::move(request), &sat_model, &cp_response,
498 solution_callback);
499}
500
501std::string SatSolverVersion() { return sat::CpSatSolverVersion(); }
502
503} // namespace operations_research
void set_status(::operations_research::MPSolverResponseStatus value)
::google::protobuf::RepeatedField< double > *PROTOBUF_NONNULL mutable_variable_value()
void EnableLogging(bool enable)
Definition logging.h:51
void SetLogToStdOut(bool enable)
Definition logging.h:57
void AddInfoLoggingCallback(std::function< void(const std::string &message)> callback)
Definition logging.cc:45
const ::operations_research::sat::IntegerVariableProto & variables(int index) const
const ::operations_research::sat::FloatObjectiveProto & floating_point_objective() const
::operations_research::sat::PartialVariableAssignment *PROTOBUF_NONNULL mutable_solution_hint()
const ::operations_research::sat::CpObjectiveProto & objective() const
const ::operations_research::sat::CpSolverSolution & additional_solutions(int index) const
::google::protobuf::RepeatedField<::int64_t > *PROTOBUF_NONNULL mutable_solution()
void set_status(::operations_research::sat::CpSolverStatus value)
::operations_research::sat::CpSolverStatus status() const
T Add(std::function< T(Model *)> f)
Definition model.h:104
bool MakeBoundsOfIntegerVariablesInteger(const SatParameters &params, MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:207
void ChangeLargeBoundsToInfinity(double max_magnitude, MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:240
std::vector< double > DetectImpliedIntegers(MPModelProto *mp_model, SolverLogger *logger)
Definition lp_utils.cc:485
std::string ValidateParameters(const SatParameters &params)
std::string CpSatSolverVersion()
Returns a string that describes the version of the solver.
CpSolverResponse SolveCpModel(const CpModelProto &model_proto, Model *model)
std::function< void(Model *)> NewBestBoundCallback(const std::function< void(double)> &callback)
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:112
std::string CpSolverResponseStats(const CpSolverResponse &response, bool has_objective)
bool MPModelProtoValidationBeforeConversion(const SatParameters &params, const MPModelProto &mp_model, SolverLogger *logger)
Definition lp_utils.cc:421
OR-Tools root namespace.
std::string SatSolverVersion()
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::function< void(const double)> best_bound_callback)
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)
MPSolutionResponse SatSolveProtoInternal(LazyMutableCopy< MPModelRequest > request, sat::Model *sat_model, sat::CpSolverResponse *cp_response, std::function< void(const MPSolution &)> solution_callback)
bool ProtobufTextFormatMergeFromString(absl::string_view proto_text_string, ProtoType *proto)
Definition proto_utils.h:79
#define SOLVER_LOG(logger,...)
Definition logging.h:114