OR-Tools  9.6
pdlp_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 <optional>
23 #include <string>
24 #include <utility>
25 #include <vector>
26 
27 #include "absl/memory/memory.h"
28 #include "absl/status/status.h"
29 #include "absl/status/statusor.h"
30 #include "absl/strings/str_cat.h"
31 #include "absl/strings/str_join.h"
32 #include "absl/time/time.h"
33 #include "google/protobuf/duration.pb.h"
34 #include "ortools/base/logging.h"
35 #include "ortools/base/protoutil.h"
37 #include "ortools/math_opt/callback.pb.h"
42 #include "ortools/math_opt/model.pb.h"
43 #include "ortools/math_opt/model_parameters.pb.h"
44 #include "ortools/math_opt/model_update.pb.h"
45 #include "ortools/math_opt/parameters.pb.h"
46 #include "ortools/math_opt/result.pb.h"
47 #include "ortools/math_opt/solution.pb.h"
49 #include "ortools/math_opt/sparse_containers.pb.h"
54 #include "ortools/pdlp/solve_log.pb.h"
55 #include "ortools/pdlp/solvers.pb.h"
57 
58 namespace operations_research {
59 namespace math_opt {
60 
62 using pdlp::PrimalDualHybridGradientParams;
63 using pdlp::SolverResult;
64 
65 absl::StatusOr<std::unique_ptr<SolverInterface>> PdlpSolver::New(
66  const ModelProto& model, const InitArgs& init_args) {
67  auto result = absl::WrapUnique(new PdlpSolver);
68  ASSIGN_OR_RETURN(result->pdlp_bridge_, PdlpBridge::FromProto(model));
69  return result;
70 }
71 
72 absl::StatusOr<PrimalDualHybridGradientParams> PdlpSolver::MergeParameters(
73  const SolveParametersProto& parameters) {
74  PrimalDualHybridGradientParams result;
75  std::vector<std::string> warnings;
76  if (parameters.enable_output()) {
77  result.set_verbosity_level(3);
78  }
79  if (parameters.has_threads()) {
80  result.set_num_threads(parameters.threads());
81  }
82  if (parameters.has_time_limit()) {
83  result.mutable_termination_criteria()->set_time_sec_limit(
84  absl::ToDoubleSeconds(
85  util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
86  }
87  if (parameters.has_node_limit()) {
88  warnings.push_back("parameter node_limit not supported for PDLP");
89  }
90  if (parameters.has_cutoff_limit()) {
91  warnings.push_back("parameter cutoff_limit not supported for PDLP");
92  }
93  if (parameters.has_objective_limit()) {
94  warnings.push_back("parameter best_objective_limit not supported for PDLP");
95  }
96  if (parameters.has_best_bound_limit()) {
97  warnings.push_back("parameter best_bound_limit not supported for PDLP");
98  }
99  if (parameters.has_solution_limit()) {
100  warnings.push_back("parameter solution_limit not supported for PDLP");
101  }
102  if (parameters.has_random_seed()) {
103  warnings.push_back("parameter random_seed not supported for PDLP");
104  }
105  if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
106  warnings.push_back("parameter lp_algorithm not supported for PDLP");
107  }
108  if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
109  warnings.push_back("parameter presolve not supported for PDLP");
110  }
111  if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
112  warnings.push_back("parameter cuts not supported for PDLP");
113  }
114  if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
115  warnings.push_back("parameter heuristics not supported for PDLP");
116  }
117  if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
118  warnings.push_back("parameter scaling not supported for PDLP");
119  }
120  if (parameters.has_iteration_limit()) {
121  const int64_t limit = std::min<int64_t>(std::numeric_limits<int32_t>::max(),
122  parameters.iteration_limit());
123  result.mutable_termination_criteria()->set_iteration_limit(
124  static_cast<int32_t>(limit));
125  }
126  result.MergeFrom(parameters.pdlp());
127  if (!warnings.empty()) {
128  return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
129  }
130  return result;
131 }
132 
133 namespace {
134 
135 absl::StatusOr<TerminationProto> ConvertReason(
136  const pdlp::TerminationReason pdlp_reason, const std::string& pdlp_detail) {
137  switch (pdlp_reason) {
138  case pdlp::TERMINATION_REASON_UNSPECIFIED:
139  return TerminateForReason(TERMINATION_REASON_UNSPECIFIED, pdlp_detail);
140  case pdlp::TERMINATION_REASON_OPTIMAL:
141  return TerminateForReason(TERMINATION_REASON_OPTIMAL, pdlp_detail);
142  case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE:
143  return TerminateForReason(TERMINATION_REASON_INFEASIBLE, pdlp_detail);
144  case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE:
145  return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
146  pdlp_detail);
147  case pdlp::TERMINATION_REASON_TIME_LIMIT:
148  return NoSolutionFoundTermination(LIMIT_TIME, pdlp_detail);
149  case pdlp::TERMINATION_REASON_ITERATION_LIMIT:
150  return NoSolutionFoundTermination(LIMIT_ITERATION, pdlp_detail);
151  case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT:
152  return NoSolutionFoundTermination(LIMIT_OTHER, pdlp_detail);
153  case pdlp::TERMINATION_REASON_NUMERICAL_ERROR:
154  return TerminateForReason(TERMINATION_REASON_NUMERICAL_ERROR,
155  pdlp_detail);
156  case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER:
157  return NoSolutionFoundTermination(LIMIT_INTERRUPTED, pdlp_detail);
158  case pdlp::TERMINATION_REASON_INVALID_PROBLEM:
159  // Indicates that the solver detected invalid problem data, e.g.,
160  // inconsistent bounds.
161  return absl::InternalError(
162  absl::StrCat("Invalid problem sent to PDLP solver "
163  "(TERMINATION_REASON_INVALID_PROBLEM): ",
164  pdlp_detail));
165  case pdlp::TERMINATION_REASON_INVALID_INITIAL_SOLUTION:
166  return absl::InvalidArgumentError(
167  absl::StrCat("PDLP solution hint invalid "
168  "(TERMINATION_REASON_INVALID_INITIAL_SOLUTION): ",
169  pdlp_detail));
170  case pdlp::TERMINATION_REASON_INVALID_PARAMETER:
171  // Indicates that an invalid value for the parameters was detected.
172  return absl::InvalidArgumentError(absl::StrCat(
173  "PDLP parameters invalid (TERMINATION_REASON_INVALID_PARAMETER): ",
174  pdlp_detail));
175  case pdlp::TERMINATION_REASON_OTHER:
176  return TerminateForReason(TERMINATION_REASON_OTHER_ERROR, pdlp_detail);
177  default:
178  LOG(FATAL) << "PDLP status: " << ProtoEnumToString(pdlp_reason)
179  << " not implemented.";
180  }
181 }
182 
183 ProblemStatusProto GetProblemStatus(const pdlp::TerminationReason pdlp_reason,
184  const bool has_finite_dual_bound) {
185  ProblemStatusProto problem_status;
186 
187  switch (pdlp_reason) {
188  case pdlp::TERMINATION_REASON_OPTIMAL:
189  problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
190  problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
191  break;
192  case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE:
193  problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
194  problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
195  break;
196  case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE:
197  problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
198  problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
199  break;
200  case pdlp::TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE:
201  problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
202  problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
203  problem_status.set_primal_or_dual_infeasible(true);
204  break;
205  default:
206  problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
207  problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
208  break;
209  }
210  if (has_finite_dual_bound) {
211  problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
212  }
213  return problem_status;
214 }
215 
216 } // namespace
217 
218 absl::StatusOr<SolveResultProto> PdlpSolver::MakeSolveResult(
219  const pdlp::SolverResult& pdlp_result,
220  const ModelSolveParametersProto& model_params) {
221  SolveResultProto result;
222  ASSIGN_OR_RETURN(*result.mutable_termination(),
223  ConvertReason(pdlp_result.solve_log.termination_reason(),
224  pdlp_result.solve_log.termination_string()));
225  ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_solve_time(),
227  absl::Seconds(pdlp_result.solve_log.solve_time_sec())));
228  result.mutable_solve_stats()->set_first_order_iterations(
229  pdlp_result.solve_log.iteration_count());
230  const std::optional<pdlp::ConvergenceInformation> convergence_information =
231  pdlp::GetConvergenceInformation(pdlp_result.solve_log.solution_stats(),
232  pdlp_result.solve_log.solution_type());
233 
234  // TODO(b/195295177): update description after changes to bounds below.
235  // Set default infinite primal/dual bounds. PDLP's default is a minimization
236  // problem for which the default primal and dual bounds are infinity and
237  // -infinity respectively. PDLP provides a scaling factor to flip the signs
238  // for maximization problems. Note that PDLP does not consider solutions that
239  // are feasible up to the solver's tolerances to update these bounds. PDLP
240  // provides a correction function for dual solutions that yields a true dual
241  // bound, but does not provide this function for primal solutions.
242  const double objective_scaling_factor =
243  pdlp_bridge_.pdlp_lp().objective_scaling_factor;
244  result.mutable_solve_stats()->set_best_primal_bound(
245  objective_scaling_factor * std::numeric_limits<double>::infinity());
246  result.mutable_solve_stats()->set_best_dual_bound(
247  -objective_scaling_factor * std::numeric_limits<double>::infinity());
248 
249  switch (pdlp_result.solve_log.termination_reason()) {
250  case pdlp::TERMINATION_REASON_OPTIMAL:
251  case pdlp::TERMINATION_REASON_TIME_LIMIT:
252  case pdlp::TERMINATION_REASON_ITERATION_LIMIT:
253  case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT:
254  case pdlp::TERMINATION_REASON_NUMERICAL_ERROR:
255  case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER: {
256  SolutionProto* solution_proto = result.add_solutions();
257  {
258  auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto(
259  pdlp_result.primal_solution, model_params.variable_values_filter());
260  RETURN_IF_ERROR(maybe_primal.status());
261  PrimalSolutionProto* primal_proto =
262  solution_proto->mutable_primal_solution();
263  primal_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
264  *primal_proto->mutable_variable_values() = *std::move(maybe_primal);
265  // Note: the solution could be primal feasible for termination reasons
266  // other than TERMINATION_REASON_OPTIMAL, but in theory, PDLP could
267  // also be modified to return the best feasible solution encounered in
268  // an early termination run (if any).
269  if (pdlp_result.solve_log.termination_reason() ==
270  pdlp::TERMINATION_REASON_OPTIMAL) {
271  primal_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
272  }
273  if (convergence_information.has_value()) {
274  primal_proto->set_objective_value(
275  convergence_information->primal_objective());
276  // TODO(b/195295177): update to return bounds.
277  // PDLP does not have a primal objective correction function so we
278  // skip the primal bound update.
279  }
280  }
281  {
282  auto maybe_dual = pdlp_bridge_.DualVariablesToProto(
283  pdlp_result.dual_solution, model_params.dual_values_filter());
284  RETURN_IF_ERROR(maybe_dual.status());
285  auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto(
286  pdlp_result.reduced_costs, model_params.reduced_costs_filter());
287  RETURN_IF_ERROR(maybe_reduced.status());
288  DualSolutionProto* dual_proto = solution_proto->mutable_dual_solution();
289  dual_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
290  *dual_proto->mutable_dual_values() = *std::move(maybe_dual);
291  *dual_proto->mutable_reduced_costs() = *std::move(maybe_reduced);
292  // Note: same comment on primal solution status holds here.
293  if (pdlp_result.solve_log.termination_reason() ==
294  pdlp::TERMINATION_REASON_OPTIMAL) {
295  dual_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
296  }
297  if (convergence_information.has_value()) {
298  const double dual_obj = convergence_information->dual_objective();
299  dual_proto->set_objective_value(dual_obj);
300  // TODO(b/195295177): update to use dual_obj or corrected_dual_bound.
301  // Using PDLP's corrected dual objective to update dual bounds.
302  const double corrected_dual_bound =
303  convergence_information->corrected_dual_objective();
304  result.mutable_solve_stats()->set_best_dual_bound(
305  corrected_dual_bound);
306  }
307  }
308  break;
309  }
310  case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: {
311  // NOTE: for primal infeasible problems, PDLP stores the infeasibility
312  // certificate (dual ray) in the dual variables and reduced costs.
313  auto maybe_dual = pdlp_bridge_.DualVariablesToProto(
314  pdlp_result.dual_solution, model_params.dual_values_filter());
315  RETURN_IF_ERROR(maybe_dual.status());
316  auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto(
317  pdlp_result.reduced_costs, model_params.reduced_costs_filter());
318  RETURN_IF_ERROR(maybe_reduced.status());
319  DualRayProto* dual_ray_proto = result.add_dual_rays();
320  *dual_ray_proto->mutable_dual_values() = *std::move(maybe_dual);
321  *dual_ray_proto->mutable_reduced_costs() = *std::move(maybe_reduced);
322  break;
323  }
324  case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: {
325  // NOTE: for dual infeasible problems, PDLP stores the infeasibility
326  // certificate (primal ray) in the primal variables.
327  auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto(
328  pdlp_result.primal_solution, model_params.variable_values_filter());
329  RETURN_IF_ERROR(maybe_primal.status());
330  PrimalRayProto* primal_ray_proto = result.add_primal_rays();
331  *primal_ray_proto->mutable_variable_values() = *std::move(maybe_primal);
332  break;
333  }
334  default:
335  break;
336  }
337  *result.mutable_solve_stats()->mutable_problem_status() =
338  GetProblemStatus(pdlp_result.solve_log.termination_reason(),
339  std::isfinite(result.solve_stats().best_dual_bound()));
340  return result;
341 }
342 
343 absl::StatusOr<SolveResultProto> PdlpSolver::Solve(
344  const SolveParametersProto& parameters,
345  const ModelSolveParametersProto& model_parameters,
346  const MessageCallback message_cb,
347  const CallbackRegistrationProto& callback_registration, const Callback cb,
348  SolveInterrupter* const interrupter) {
349  // TODO(b/183502493): Implement message callback when PDLP supports that.
350  if (message_cb != nullptr) {
351  return absl::InvalidArgumentError(internal::kMessageCallbackNotSupported);
352  }
353 
354  RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration,
355  /*supported_events=*/{}));
356 
357  ASSIGN_OR_RETURN(auto pdlp_params, MergeParameters(parameters));
358 
359  // PDLP returns `(TERMINATION_REASON_INVALID_PROBLEM): The input problem has
360  // inconsistent bounds.` but we want a more detailed error.
361  RETURN_IF_ERROR(pdlp_bridge_.ListInvertedBounds().ToStatus());
362 
363  std::atomic<bool> interrupt = false;
364  const ScopedSolveInterrupterCallback set_interrupt(
365  interrupter, [&]() { interrupt = true; });
366 
367  std::optional<PrimalAndDualSolution> initial_solution;
368  if (!model_parameters.solution_hints().empty()) {
369  initial_solution = pdlp_bridge_.SolutionHintToWarmStart(
370  model_parameters.solution_hints(0));
371  }
372 
373  const SolverResult pdlp_result = PrimalDualHybridGradient(
374  pdlp_bridge_.pdlp_lp(), pdlp_params, initial_solution, &interrupt);
375  return MakeSolveResult(pdlp_result, model_parameters);
376 }
377 
378 absl::StatusOr<bool> PdlpSolver::Update(const ModelUpdateProto& model_update) {
379  return false;
380 }
381 
383 
384 } // namespace math_opt
385 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
absl::StatusOr< SparseDoubleVectorProto > DualVariablesToProto(const Eigen::VectorXd &dual_values, const SparseVectorFilterProto &linear_constraint_filter) const
Definition: pdlp_bridge.cc:195
const pdlp::QuadraticProgram & pdlp_lp() const
Definition: pdlp_bridge.h:53
static absl::StatusOr< PdlpBridge > FromProto(const ModelProto &model_proto)
Definition: pdlp_bridge.cc:80
InvertedBounds ListInvertedBounds() const
Definition: pdlp_bridge.cc:169
pdlp::PrimalAndDualSolution SolutionHintToWarmStart(const SolutionHintProto &solution_hint) const
Definition: pdlp_bridge.cc:209
absl::StatusOr< SparseDoubleVectorProto > PrimalVariablesToProto(const Eigen::VectorXd &primal_values, const SparseVectorFilterProto &variable_filter) const
Definition: pdlp_bridge.cc:189
absl::StatusOr< SparseDoubleVectorProto > ReducedCostsToProto(const Eigen::VectorXd &reduced_costs, const SparseVectorFilterProto &variable_filter) const
Definition: pdlp_bridge.cc:202
absl::StatusOr< bool > Update(const ModelUpdateProto &model_update) override
Definition: pdlp_solver.cc:378
static absl::StatusOr< pdlp::PrimalDualHybridGradientParams > MergeParameters(const SolveParametersProto &parameters)
Definition: pdlp_solver.cc:72
static absl::StatusOr< std::unique_ptr< SolverInterface > > New(const ModelProto &model, const InitArgs &init_args)
Definition: pdlp_solver.cc:65
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, SolveInterrupter *interrupter) override
Definition: pdlp_solver.cc:343
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >(const CallbackDataProto &)> Callback
SatParameters parameters
GRBmodel * model
constexpr absl::string_view kMessageCallbackNotSupported
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_CP_SAT, CpSatSolver::New)
TerminationProto NoSolutionFoundTermination(const LimitProto limit, const absl::string_view detail)
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
SolverResult PrimalDualHybridGradient(QuadraticProgram qp, const PrimalDualHybridGradientParams &params, const std::atomic< bool > *interrupt_solve, IterationStatsCallback iteration_stats_callback)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
Collection of objects used to extend the Constraint Solver library.
std::string ProtoEnumToString(ProtoEnumType enum_value)
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