OR-Tools  9.2
cp_sat_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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 <atomic>
17#include <cmath>
18#include <cstdint>
19#include <functional>
20#include <limits>
21#include <memory>
22#include <string>
23#include <utility>
24#include <vector>
25
28#include "absl/memory/memory.h"
29#include "absl/status/status.h"
30#include "absl/status/statusor.h"
31#include "absl/strings/match.h"
32#include "absl/strings/str_cat.h"
33#include "absl/strings/str_join.h"
34#include "absl/strings/str_split.h"
35#include "absl/time/clock.h"
36#include "absl/time/time.h"
39#include "ortools/math_opt/callback.pb.h"
45#include "ortools/math_opt/model.pb.h"
46#include "ortools/math_opt/model_parameters.pb.h"
47#include "ortools/math_opt/model_update.pb.h"
48#include "ortools/math_opt/parameters.pb.h"
49#include "ortools/math_opt/result.pb.h"
50#include "ortools/math_opt/solution.pb.h"
51#include "ortools/math_opt/sparse_containers.pb.h"
55#include "absl/status/status.h"
58
59namespace operations_research {
60namespace math_opt {
61
62namespace {
63
64constexpr double kInf = std::numeric_limits<double>::infinity();
65
66// Returns a list of warnings from parameter settings that were
67// invalid/unsupported (specific to CP-SAT), one element per bad parameter.
68std::vector<std::string> SetSolveParameters(
69 const SolveParametersProto& parameters, const bool has_message_callback,
70 MPModelRequest& request) {
71 std::vector<std::string> warnings;
72 if (parameters.has_time_limit()) {
73 request.set_solver_time_limit_seconds(absl::ToDoubleSeconds(
74 util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
75 }
76
77 // Build CP SAT parameters by first initializing them from the common
78 // parameters, and then using the values in `solver_specific_parameters` to
79 // overwrite them if necessary.
80 //
81 // We don't need to set max_time_in_seconds since we already pass it in the
82 // `request.solver_time_limit_seconds`. The logic of `SatSolveProto()` will
83 // apply the logic we want here.
84 sat::SatParameters sat_parameters;
85
86 // By default CP-SAT catches SIGINT (Ctrl-C) to interrupt the solve but we
87 // don't want this behavior when the users uses CP-SAT through MathOpt.
88 sat_parameters.set_catch_sigint_signal(false);
89
90 if (parameters.has_random_seed()) {
91 sat_parameters.set_random_seed(parameters.random_seed());
92 }
93 if (parameters.has_threads()) {
94 sat_parameters.set_num_search_workers(parameters.threads());
95 }
96 if (parameters.has_relative_gap_limit()) {
97 sat_parameters.set_relative_gap_limit(parameters.relative_gap_limit());
98 }
99
100 if (parameters.has_absolute_gap_limit()) {
101 sat_parameters.set_absolute_gap_limit(parameters.absolute_gap_limit());
102 }
103 if (parameters.has_cutoff_limit()) {
104 warnings.push_back(
105 "The cutoff_limit parameter is not supported for CP-SAT.");
106 }
107 if (parameters.has_best_bound_limit()) {
108 warnings.push_back(
109 "The best_bound_limit parameter is not supported for CP-SAT.");
110 }
111 if (parameters.has_objective_limit()) {
112 warnings.push_back(
113 "The objective_limit parameter is not supported for CP-SAT.");
114 }
115 if (parameters.has_solution_limit()) {
116 if (parameters.solution_limit() == 1) {
117 sat_parameters.set_stop_after_first_solution(true);
118 } else {
119 warnings.push_back(absl::StrCat(
120 "The CP-SAT solver only supports value 1 for solution_limit, found: ",
121 parameters.solution_limit()));
122 }
123 }
124
125 if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
126 warnings.push_back(
127 absl::StrCat("Setting the LP Algorithm (was set to ",
128 ProtoEnumToString(parameters.lp_algorithm()),
129 ") is not supported for CP_SAT solver"));
130 }
131 if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
132 switch (parameters.presolve()) {
133 case EMPHASIS_OFF:
134 sat_parameters.set_cp_model_presolve(false);
135 break;
136 case EMPHASIS_LOW:
137 case EMPHASIS_MEDIUM:
138 case EMPHASIS_HIGH:
139 case EMPHASIS_VERY_HIGH:
140 sat_parameters.set_cp_model_presolve(true);
141 break;
142 default:
143 LOG(FATAL) << "Presolve emphasis: "
144 << ProtoEnumToString(parameters.presolve())
145 << " unknown, error setting CP-SAT parameters";
146 }
147 }
148 if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
149 warnings.push_back(absl::StrCat("Setting the scaling (was set to ",
150 ProtoEnumToString(parameters.scaling()),
151 ") is not supported for CP_SAT solver"));
152 }
153 if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
154 switch (parameters.cuts()) {
155 case EMPHASIS_OFF:
156 // This is not very maintainable, but CP-SAT doesn't expose the
157 // parameters we need.
158 sat_parameters.set_add_cg_cuts(false);
159 sat_parameters.set_add_mir_cuts(false);
160 sat_parameters.set_add_zero_half_cuts(false);
161 sat_parameters.set_add_clique_cuts(false);
162 sat_parameters.set_max_all_diff_cut_size(0);
163 sat_parameters.set_add_lin_max_cuts(false);
164 break;
165 case EMPHASIS_LOW:
166 case EMPHASIS_MEDIUM:
167 case EMPHASIS_HIGH:
168 case EMPHASIS_VERY_HIGH:
169 break;
170 default:
171 LOG(FATAL) << "Cut emphasis: " << ProtoEnumToString(parameters.cuts())
172 << " unknown, error setting CP-SAT parameters";
173 }
174 }
175 if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
176 warnings.push_back(absl::StrCat("Setting the heuristics (was set to ",
177 ProtoEnumToString(parameters.heuristics()),
178 ") is not supported for CP_SAT solver"));
179 }
180 sat_parameters.MergeFrom(parameters.cp_sat());
181
182 // We want to override specifically SAT parameters independently from the user
183 // input when a message callback is used to prevent wrongful writes to stdout
184 // or disabling of messages via these parameters.
185 if (has_message_callback) {
186 // When enable_internal_solver_output is used, CP-SAT solver actually has
187 // the same effect as setting log_search_progress to true.
188 sat_parameters.set_log_search_progress(true);
189
190 // Default value of log_to_stdout is true; but even if it was not the case,
191 // we don't want to write to stdout when a message callback is used.
192 sat_parameters.set_log_to_stdout(false);
193 } else {
194 // We only set enable_internal_solver_output when we have no message
195 // callback.
196 request.set_enable_internal_solver_output(parameters.enable_output());
197 }
198
199 request.set_solver_specific_parameters(
200 EncodeSatParametersAsString(sat_parameters));
201 return warnings;
202}
203
204absl::StatusOr<std::pair<SolveStatsProto, TerminationProto>>
205GetTerminationAndStats(const bool is_interrupted, const bool maximize,
206 const MPSolutionResponse& response) {
207 SolveStatsProto solve_stats;
208 TerminationProto termination;
209
210 // Set default status and bounds.
211 solve_stats.mutable_problem_status()->set_primal_status(
212 FEASIBILITY_STATUS_UNDETERMINED);
213 solve_stats.set_best_primal_bound(maximize ? -kInf : kInf);
214 solve_stats.mutable_problem_status()->set_dual_status(
215 FEASIBILITY_STATUS_UNDETERMINED);
216 solve_stats.set_best_dual_bound(maximize ? kInf : -kInf);
217
218 // Set terminations and update status and bounds as appropriate.
219 switch (response.status()) {
220 case MPSOLVER_OPTIMAL:
221 termination =
222 TerminateForReason(TERMINATION_REASON_OPTIMAL, response.status_str());
223 solve_stats.mutable_problem_status()->set_primal_status(
224 FEASIBILITY_STATUS_FEASIBLE);
225 solve_stats.set_best_primal_bound(response.objective_value());
226 solve_stats.mutable_problem_status()->set_dual_status(
227 FEASIBILITY_STATUS_FEASIBLE);
228 solve_stats.set_best_dual_bound(response.best_objective_bound());
229 break;
231 termination = TerminateForReason(TERMINATION_REASON_INFEASIBLE,
232 response.status_str());
233 solve_stats.mutable_problem_status()->set_primal_status(
234 FEASIBILITY_STATUS_INFEASIBLE);
235 break;
237 // For a basic unbounded problem, CP-SAT internally returns
238 // INFEASIBLE_OR_UNBOUNDED after presolve but MPSolver statuses don't
239 // support that thus it get transformed in MPSOLVER_UNKNOWN_STATUS with
240 // a status_str of
241 //
242 // "Problem proven infeasible or unbounded during MIP presolve"
243 //
244 // There may be some other cases where CP-SAT returns UNKNOWN here so we
245 // only return TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED when the
246 // status_str is detected. Otherwise we return OTHER_ERROR.
247 //
248 // TODO(b/202159173): A better solution would be to use CP-SAT API
249 // directly which may help further improve the statuses.
250 if (absl::StrContains(response.status_str(), "infeasible or unbounded")) {
251 termination = TerminateForReason(
252 TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, response.status_str());
253 solve_stats.mutable_problem_status()->set_primal_or_dual_infeasible(
254 true);
255 } else {
256 termination = TerminateForReason(TERMINATION_REASON_OTHER_ERROR,
257 response.status_str());
258 }
259 break;
261 termination = FeasibleTermination(
262 is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED,
263 response.status_str());
264 solve_stats.mutable_problem_status()->set_primal_status(
265 FEASIBILITY_STATUS_FEASIBLE);
266 solve_stats.set_best_primal_bound(response.objective_value());
267 solve_stats.set_best_dual_bound(response.best_objective_bound());
268 if (std::isfinite(response.best_objective_bound())) {
269 solve_stats.mutable_problem_status()->set_dual_status(
270 FEASIBILITY_STATUS_FEASIBLE);
271 }
272 break;
274 termination = NoSolutionFoundTermination(
275 is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED,
276 response.status_str());
277 break;
279 return absl::InternalError(
280 absl::StrCat("cp-sat solver returned MODEL_INVALID, details: ",
281 response.status_str()));
282 default:
283 return absl::InternalError(
284 absl::StrCat("unexpected solve status: ", response.status()));
285 }
286 return std::make_pair(std::move(solve_stats), std::move(termination));
287}
288} // namespace
289
290absl::StatusOr<std::unique_ptr<SolverInterface>> CpSatSolver::New(
291 const ModelProto& model, const InitArgs& init_args) {
292 ASSIGN_OR_RETURN(MPModelProto cp_sat_model,
294 std::vector variable_ids(model.variables().ids().begin(),
295 model.variables().ids().end());
296 // TODO(b/204083726): Remove this check if QP support is added
297 if (!model.objective().quadratic_coefficients().row_ids().empty()) {
298 return absl::InvalidArgumentError(
299 "MathOpt does not currently support CP-SAT models with quadratic "
300 "objectives");
301 }
302 // We must use WrapUnique here since the constructor is private.
303 return absl::WrapUnique(
304 new CpSatSolver(std::move(cp_sat_model), std::move(variable_ids)));
305}
306
307absl::StatusOr<SolveResultProto> CpSatSolver::Solve(
308 const SolveParametersProto& parameters,
309 const ModelSolveParametersProto& model_parameters,
310 const MessageCallback message_cb,
311 const CallbackRegistrationProto& callback_registration, const Callback cb,
312 SolveInterrupter* const interrupter) {
313 const absl::Time start = absl::Now();
314
316 callback_registration,
317 /*supported_events=*/{CALLBACK_EVENT_MIP_SOLUTION}));
318 if (callback_registration.add_lazy_constraints()) {
319 return absl::InvalidArgumentError(
320 "CallbackRegistrationProto.add_lazy_constraints=true is not supported "
321 "for CP-SAT.");
322 }
323 // We need not check callback_registration.add_cuts, as cuts can only be added
324 // at event MIP_NODE which we have already validated is not supported.
325
326 SolveResultProto result;
327 MPModelRequest req;
328 // Here we must make a copy since Solve() can be called multiple times with
329 // different parameters. Hence we can't move `cp_sat_model`.
330 *req.mutable_model() = cp_sat_model_;
332 {
333 std::vector<std::string> param_warnings =
334 SetSolveParameters(parameters,
335 /*has_message_callback=*/message_cb != nullptr, req);
336 if (!param_warnings.empty()) {
337 if (parameters.strictness().bad_parameter()) {
338 return absl::InvalidArgumentError(absl::StrJoin(param_warnings, "; "));
339 } else {
340 for (std::string& warning : param_warnings) {
341 result.add_warnings(std::move(warning));
342 }
343 }
344 }
345 }
346
347 if (!model_parameters.solution_hints().empty()) {
348 int i = 0;
349 for (const auto [id, val] :
350 MakeView(model_parameters.solution_hints(0).variable_values())) {
351 while (variable_ids_[i] < id) {
352 ++i;
353 }
356 }
357 }
358
359 // We need to chain the user interrupter through a local interrupter, because
360 // if we termiante early from a callback request, we don't want to incorrectly
361 // modify the input state.
362 SolveInterrupter local_interrupter;
363 std::atomic<bool> interrupt_solve = false;
364 local_interrupter.AddInterruptionCallback([&]() { interrupt_solve = true; });
365
366 // Setup a callback on the user provided so that we interrupt the solver.
367 const ScopedSolveInterrupterCallback scoped_interrupt_cb(
368 interrupter, [&]() { local_interrupter.Interrupt(); });
369
370 std::function<void(const std::string&)> logging_callback;
371 if (message_cb != nullptr) {
372 logging_callback = [&](const std::string& message) {
373 message_cb(absl::StrSplit(message, '\n'));
374 };
375 }
376
377 const absl::flat_hash_set<CallbackEventProto> events =
378 EventSet(callback_registration);
379 std::function<void(const MPSolution&)> solution_callback;
380 absl::Status callback_error = absl::OkStatus();
381 if (events.contains(CALLBACK_EVENT_MIP_SOLUTION)) {
382 solution_callback = [this, &cb, &callback_error, &local_interrupter,
383 &model_parameters](const MPSolution& mp_solution) {
384 if (!callback_error.ok()) {
385 // A previous callback failed.
386 return;
387 }
388 CallbackDataProto cb_data;
389 cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION);
390 *cb_data.mutable_primal_solution_vector() =
391 ExtractSolution(mp_solution.variable_value(), model_parameters);
392 const absl::StatusOr<CallbackResultProto> cb_result = cb(cb_data);
393 if (!cb_result.ok()) {
394 callback_error = cb_result.status();
395 // Note: we will be returning a status error, we do not need to worry
396 // about interpreting this as TERMINATION_REASON_INTERRUPTED.
397 local_interrupter.Interrupt();
398 } else if (cb_result->terminate()) {
399 local_interrupter.Interrupt();
400 }
401 // Note cb_result.cuts and cb_result.suggested solutions are not
402 // supported by CP-SAT and we have validated they are empty.
403 };
404 }
405
407 SatSolveProto(std::move(req), &interrupt_solve,
408 logging_callback, solution_callback));
409 RETURN_IF_ERROR(callback_error) << "error in callback";
410 ASSIGN_OR_RETURN((auto [solve_stats, termination]),
411 GetTerminationAndStats(local_interrupter.IsInterrupted(),
412 cp_sat_model_.maximize(), response));
413 *result.mutable_solve_stats() = std::move(solve_stats);
414 *result.mutable_termination() = std::move(termination);
415 if (response.status() == MPSOLVER_OPTIMAL ||
416 response.status() == MPSOLVER_FEASIBLE) {
417 PrimalSolutionProto& solution =
418 *result.add_solutions()->mutable_primal_solution();
419 *solution.mutable_variable_values() =
420 ExtractSolution(response.variable_value(), model_parameters);
421 solution.set_objective_value(response.objective_value());
422 solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
423 }
424
426 absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
427
428 return result;
429}
430
431bool CpSatSolver::CanUpdate(const ModelUpdateProto& model_update) {
432 return false;
433}
434
435absl::Status CpSatSolver::Update(const ModelUpdateProto& model_update) {
436 // This function should never be called since CanUpdate() always returns
437 // false.
438 return absl::InternalError("CP-SAT solver does not support incrementalism");
439}
440
441CpSatSolver::CpSatSolver(MPModelProto cp_sat_model,
442 std::vector<int64_t> variable_ids)
443 : cp_sat_model_(std::move(cp_sat_model)),
444 variable_ids_(std::move(variable_ids)) {}
445
446SparseDoubleVectorProto CpSatSolver::ExtractSolution(
447 const absl::Span<const double> cp_sat_variable_values,
448 const ModelSolveParametersProto& model_parameters) const {
449 // Pre-condition: we assume one-to-one correspondence of input variables to
450 // solution's variables.
451 CHECK_EQ(cp_sat_variable_values.size(), variable_ids_.size());
452
453 SparseVectorFilterPredicate predicate(
454 model_parameters.variable_values_filter());
455 SparseDoubleVectorProto result;
456 for (int i = 0; i < variable_ids_.size(); ++i) {
457 const int64_t id = variable_ids_[i];
458 const double value = cp_sat_variable_values[i];
459 if (predicate.AcceptsAndUpdate(id, value)) {
460 result.add_ids(id);
461 result.add_values(value);
462 }
463 }
464 return result;
465}
466
468
469} // namespace math_opt
470} // namespace operations_research
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:702
#define CHECK_OK(x)
Definition: base/logging.h:44
#define LOG(severity)
Definition: base/logging.h:420
::operations_research::PartialVariableAssignment * mutable_solution_hint()
::operations_research::MPModelProto * mutable_model()
void set_solver_type(::operations_research::MPModelRequest_SolverType value)
static constexpr SolverType SAT_INTEGER_PROGRAMMING
bool CanUpdate(const ModelUpdateProto &model_update) override
absl::Status Update(const ModelUpdateProto &model_update) override
static absl::StatusOr< std::unique_ptr< SolverInterface > > New(const ModelProto &model, const InitArgs &init_args)
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, SolveInterrupter *interrupter) override
CallbackId AddInterruptionCallback(Callback callback)
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >(const CallbackDataProto &)> Callback
SatParameters parameters
SharedResponseManager * response
int64_t value
absl::Span< const int64_t > variable_ids
GRBmodel * model
const int FATAL
Definition: log_severity.h:32
TerminationProto FeasibleTermination(const LimitProto limit, const absl::string_view detail)
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_CP_SAT, CpSatSolver::New)
absl::StatusOr<::operations_research::MPModelProto > MathOptModelToMPModelProto(const ::operations_research::math_opt::ModelProto &model)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
TerminationProto NoSolutionFoundTermination(const LimitProto limit, const absl::string_view detail)
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
absl::flat_hash_set< CallbackEventProto > EventSet(const CallbackRegistrationProto &callback_registration)
Collection of objects used to extend the Constraint Solver library.
std::string ProtoEnumToString(ProtoEnumType enum_value)
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)
std::string EncodeSatParametersAsString(const sat::SatParameters &parameters)
STL namespace.
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition: protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition: protoutil.h:27
int64_t start
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition: status_macros.h:48
#define RETURN_IF_ERROR(expr)
Definition: status_macros.h:29
std::string message
Definition: trace.cc:398