Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
pdlp_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 <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/strings/str_split.h"
33#include "absl/strings/string_view.h"
34#include "absl/time/time.h"
38#include "ortools/math_opt/callback.pb.h"
42#include "ortools/math_opt/infeasible_subsystem.pb.h"
43#include "ortools/math_opt/model.pb.h"
44#include "ortools/math_opt/model_parameters.pb.h"
45#include "ortools/math_opt/model_update.pb.h"
46#include "ortools/math_opt/parameters.pb.h"
47#include "ortools/math_opt/result.pb.h"
48#include "ortools/math_opt/solution.pb.h"
50#include "ortools/math_opt/sparse_containers.pb.h"
55#include "ortools/pdlp/solve_log.pb.h"
56#include "ortools/pdlp/solvers.pb.h"
59
60namespace operations_research {
61namespace math_opt {
62
63using pdlp::PrimalAndDualSolution;
64using pdlp::PrimalDualHybridGradientParams;
65using pdlp::SolverResult;
66
67absl::StatusOr<std::unique_ptr<SolverInterface>> PdlpSolver::New(
68 const ModelProto& model, const InitArgs&) {
69 auto result = absl::WrapUnique(new PdlpSolver);
70 ASSIGN_OR_RETURN(result->pdlp_bridge_, PdlpBridge::FromProto(model));
71 return result;
72}
73
74absl::StatusOr<PrimalDualHybridGradientParams> PdlpSolver::MergeParameters(
75 const SolveParametersProto& parameters, const bool has_message_callback) {
76 PrimalDualHybridGradientParams result;
77 std::vector<std::string> warnings;
78 if (parameters.enable_output() || has_message_callback) {
79 result.set_verbosity_level(3);
80 }
81 if (parameters.has_threads()) {
82 result.set_num_threads(parameters.threads());
83 }
84 if (parameters.has_time_limit()) {
85 result.mutable_termination_criteria()->set_time_sec_limit(
86 absl::ToDoubleSeconds(
87 util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
88 }
89 if (parameters.has_node_limit()) {
90 warnings.push_back("parameter node_limit not supported for PDLP");
91 }
92 if (parameters.has_cutoff_limit()) {
93 warnings.push_back("parameter cutoff_limit not supported for PDLP");
94 }
95 if (parameters.has_objective_limit()) {
96 warnings.push_back("parameter best_objective_limit not supported for PDLP");
97 }
98 if (parameters.has_best_bound_limit()) {
99 warnings.push_back("parameter best_bound_limit not supported for PDLP");
100 }
101 if (parameters.has_solution_limit()) {
102 warnings.push_back("parameter solution_limit not supported for PDLP");
103 }
104 if (parameters.has_random_seed()) {
105 warnings.push_back("parameter random_seed not supported for PDLP");
106 }
107 if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED &&
108 parameters.lp_algorithm() != LP_ALGORITHM_FIRST_ORDER) {
109 warnings.push_back("parameter lp_algorithm not supported for PDLP");
110 }
111 if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
112 warnings.push_back("parameter presolve not supported for PDLP");
113 }
114 if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
115 warnings.push_back("parameter cuts not supported for PDLP");
116 }
117 if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
118 warnings.push_back("parameter heuristics not supported for PDLP");
119 }
120 if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
121 warnings.push_back("parameter scaling not supported for PDLP");
122 }
123 if (parameters.has_iteration_limit()) {
124 const int64_t limit = std::min<int64_t>(std::numeric_limits<int32_t>::max(),
125 parameters.iteration_limit());
126 result.mutable_termination_criteria()->set_iteration_limit(
127 static_cast<int32_t>(limit));
128 }
129 result.MergeFrom(parameters.pdlp());
130 if (!warnings.empty()) {
131 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
132 }
133 return result;
134}
135
136namespace {
137
138absl::StatusOr<TerminationProto> ConvertReason(
139 const pdlp::TerminationReason pdlp_reason, absl::string_view pdlp_detail,
140 const bool is_maximize, const double primal_objective,
141 const double dual_objective) {
142 switch (pdlp_reason) {
143 case pdlp::TERMINATION_REASON_UNSPECIFIED:
144 return TerminateForReason(
145 is_maximize, TERMINATION_REASON_OTHER_ERROR,
146 absl::StrCat("PDLP unspecified reason "
147 "(TERMINATION_REASON_UNSPECIFIED): ",
148 pdlp_detail));
149 return TerminateForReason(is_maximize, TERMINATION_REASON_UNSPECIFIED,
150 pdlp_detail);
151 case pdlp::TERMINATION_REASON_OPTIMAL:
152 return OptimalTerminationProto(primal_objective, dual_objective,
153 pdlp_detail);
154 case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE:
156 is_maximize,
157 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
158 pdlp_detail);
159 case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE:
161 is_maximize,
162 /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE,
163 pdlp_detail);
164 case pdlp::TERMINATION_REASON_TIME_LIMIT:
166 is_maximize, LIMIT_TIME,
167 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
168 case pdlp::TERMINATION_REASON_ITERATION_LIMIT:
170 is_maximize, LIMIT_ITERATION,
171 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
172 case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT:
174 is_maximize, LIMIT_OTHER,
175 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
176 case pdlp::TERMINATION_REASON_NUMERICAL_ERROR:
177 return TerminateForReason(is_maximize, TERMINATION_REASON_NUMERICAL_ERROR,
178 pdlp_detail);
179 case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER:
181 is_maximize, LIMIT_INTERRUPTED,
182 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
183 case pdlp::TERMINATION_REASON_INVALID_PROBLEM:
184 // Indicates that the solver detected invalid problem data, e.g.,
185 // inconsistent bounds.
186 return absl::InternalError(
187 absl::StrCat("Invalid problem sent to PDLP solver "
188 "(TERMINATION_REASON_INVALID_PROBLEM): ",
189 pdlp_detail));
190 case pdlp::TERMINATION_REASON_INVALID_INITIAL_SOLUTION:
191 return absl::InvalidArgumentError(
192 absl::StrCat("PDLP solution hint invalid "
193 "(TERMINATION_REASON_INVALID_INITIAL_SOLUTION): ",
194 pdlp_detail));
195 case pdlp::TERMINATION_REASON_INVALID_PARAMETER:
196 // Indicates that an invalid value for the parameters was detected.
197 return absl::InvalidArgumentError(absl::StrCat(
198 "PDLP parameters invalid (TERMINATION_REASON_INVALID_PARAMETER): ",
199 pdlp_detail));
200 case pdlp::TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE:
202 is_maximize,
203 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
204 pdlp_detail);
205 case pdlp::TERMINATION_REASON_OTHER:
206 return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR,
207 pdlp_detail);
208 }
209 return absl::InvalidArgumentError(absl::StrCat(
210 "PDLP status: ", ProtoEnumToString(pdlp_reason), " not implemented."));
211}
212
213} // namespace
214
215absl::StatusOr<SolveResultProto> PdlpSolver::MakeSolveResult(
216 const pdlp::SolverResult& pdlp_result,
217 const ModelSolveParametersProto& model_params) {
218 // PDLP's default is a minimization problem for which the default primal and
219 // dual bounds are infinity and -infinity respectively. PDLP provides a
220 // scaling factor to flip the signs for maximization problems.
221 const double objective_scaling_factor =
222 pdlp_bridge_.pdlp_lp().objective_scaling_factor;
223 const bool is_maximize = objective_scaling_factor < 0.0;
224 SolveResultProto result;
225 const std::optional<pdlp::ConvergenceInformation> convergence_information =
226 pdlp::GetConvergenceInformation(pdlp_result.solve_log.solution_stats(),
227 pdlp_result.solve_log.solution_type());
228 if (convergence_information.has_value()) {
229 *result.mutable_pdlp_output()->mutable_convergence_information() =
230 *convergence_information;
231 }
232 ObjectiveBoundsProto objective_bounds = MakeTrivialBounds(is_maximize);
233 if (convergence_information.has_value()) {
234 objective_bounds.set_primal_bound(
235 convergence_information->primal_objective());
236 objective_bounds.set_dual_bound(convergence_information->dual_objective());
237 }
238 ASSIGN_OR_RETURN(*result.mutable_termination(),
239 ConvertReason(pdlp_result.solve_log.termination_reason(),
240 pdlp_result.solve_log.termination_string(),
241 is_maximize, objective_bounds.primal_bound(),
242 objective_bounds.dual_bound()));
243 ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_solve_time(),
245 absl::Seconds(pdlp_result.solve_log.solve_time_sec())));
246 result.mutable_solve_stats()->set_first_order_iterations(
247 pdlp_result.solve_log.iteration_count());
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 encountered 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 }
277 }
278 {
279 auto maybe_dual = pdlp_bridge_.DualVariablesToProto(
280 pdlp_result.dual_solution, model_params.dual_values_filter());
281 RETURN_IF_ERROR(maybe_dual.status());
282 auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto(
283 pdlp_result.reduced_costs, model_params.reduced_costs_filter());
284 RETURN_IF_ERROR(maybe_reduced.status());
285 DualSolutionProto* dual_proto = solution_proto->mutable_dual_solution();
286 dual_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
287 *dual_proto->mutable_dual_values() = *std::move(maybe_dual);
288 *dual_proto->mutable_reduced_costs() = *std::move(maybe_reduced);
289 // Note: same comment on primal solution status holds here.
290 if (pdlp_result.solve_log.termination_reason() ==
291 pdlp::TERMINATION_REASON_OPTIMAL) {
292 dual_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
293 }
294 if (convergence_information.has_value()) {
295 dual_proto->set_objective_value(
296 convergence_information->dual_objective());
297 }
298 }
299 break;
300 }
301 case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: {
302 // NOTE: for primal infeasible problems, PDLP stores the infeasibility
303 // certificate (dual ray) in the dual variables and reduced costs.
304 auto maybe_dual = pdlp_bridge_.DualVariablesToProto(
305 pdlp_result.dual_solution, model_params.dual_values_filter());
306 RETURN_IF_ERROR(maybe_dual.status());
307 auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto(
308 pdlp_result.reduced_costs, model_params.reduced_costs_filter());
309 RETURN_IF_ERROR(maybe_reduced.status());
310 DualRayProto* dual_ray_proto = result.add_dual_rays();
311 *dual_ray_proto->mutable_dual_values() = *std::move(maybe_dual);
312 *dual_ray_proto->mutable_reduced_costs() = *std::move(maybe_reduced);
313 break;
314 }
315 case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: {
316 // NOTE: for dual infeasible problems, PDLP stores the infeasibility
317 // certificate (primal ray) in the primal variables.
318 auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto(
319 pdlp_result.primal_solution, model_params.variable_values_filter());
320 RETURN_IF_ERROR(maybe_primal.status());
321 PrimalRayProto* primal_ray_proto = result.add_primal_rays();
322 *primal_ray_proto->mutable_variable_values() = *std::move(maybe_primal);
323 break;
324 }
325 default:
326 break;
327 }
328 return result;
329}
330
331absl::StatusOr<SolveResultProto> PdlpSolver::Solve(
332 const SolveParametersProto& parameters,
333 const ModelSolveParametersProto& model_parameters,
334 const MessageCallback message_cb,
335 const CallbackRegistrationProto& callback_registration, const Callback,
336 const SolveInterrupter* const interrupter) {
338 /*supported_events=*/{}));
339
341 auto pdlp_params,
343 /*has_message_callback=*/message_cb != nullptr));
344
345 // PDLP returns `(TERMINATION_REASON_INVALID_PROBLEM): The input problem has
346 // inconsistent bounds.` but we want a more detailed error.
348
349 std::atomic<bool> interrupt = false;
350 const ScopedSolveInterrupterCallback set_interrupt(
351 interrupter, [&]() { interrupt = true; });
352
353 std::optional<PrimalAndDualSolution> initial_solution;
354 if (!model_parameters.solution_hints().empty()) {
355 initial_solution = pdlp_bridge_.SolutionHintToWarmStart(
356 model_parameters.solution_hints(0));
357 }
358
359 std::function<void(const std::string&)> pdlp_callback = nullptr;
360 if (message_cb != nullptr) {
361 pdlp_callback = [&message_cb](const std::string& message) {
362 message_cb(absl::StrSplit(message, '\n'));
363 };
364 }
365
366 const SolverResult pdlp_result =
367 PrimalDualHybridGradient(pdlp_bridge_.pdlp_lp(), pdlp_params,
368 initial_solution, &interrupt, pdlp_callback);
369 return MakeSolveResult(pdlp_result, model_parameters);
370}
371
372absl::StatusOr<bool> PdlpSolver::Update(const ModelUpdateProto&) {
373 return false;
374}
375
376absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
377PdlpSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&,
379 const SolveInterrupter*) {
380 return absl::UnimplementedError(
381 "PDLP does not provide a method to compute an infeasible subsystem");
382}
383
385
386} // namespace math_opt
387} // namespace operations_research
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
InvertedBounds ListInvertedBounds() const
Returns the ids of variables and linear constraints with inverted bounds.
absl::StatusOr< SparseDoubleVectorProto > DualVariablesToProto(const Eigen::VectorXd &dual_values, const SparseVectorFilterProto &linear_constraint_filter) const
static absl::StatusOr< PdlpBridge > FromProto(const ModelProto &model_proto)
const pdlp::QuadraticProgram & pdlp_lp() const
Definition pdlp_bridge.h:53
absl::StatusOr< SparseDoubleVectorProto > PrimalVariablesToProto(const Eigen::VectorXd &primal_values, const SparseVectorFilterProto &variable_filter) const
absl::StatusOr< SparseDoubleVectorProto > ReducedCostsToProto(const Eigen::VectorXd &reduced_costs, const SparseVectorFilterProto &variable_filter) const
pdlp::PrimalAndDualSolution SolutionHintToWarmStart(const SolutionHintProto &solution_hint) const
static absl::StatusOr< std::unique_ptr< SolverInterface > > New(const ModelProto &model, const InitArgs &init_args)
absl::StatusOr< ComputeInfeasibleSubsystemResultProto > ComputeInfeasibleSubsystem(const SolveParametersProto &parameters, MessageCallback message_cb, const SolveInterrupter *interrupter) override
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, const SolveInterrupter *interrupter) override
static absl::StatusOr< pdlp::PrimalDualHybridGradientParams > MergeParameters(const SolveParametersProto &parameters, bool has_message_callback)
Returns the merged parameters and a list of warnings.
absl::StatusOr< bool > Update(const ModelUpdateProto &model_update) override
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >( const CallbackDataProto &)> Callback
SatParameters parameters
GRBmodel * model
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
TerminationProto OptimalTerminationProto(const double finite_primal_objective, const double dual_objective, const absl::string_view detail)
TerminationProto InfeasibleOrUnboundedTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
TerminationProto NoSolutionFoundTerminationProto(const bool is_maximize, const LimitProto limit, const std::optional< double > optional_dual_objective, const absl::string_view detail)
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
TerminationProto InfeasibleTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
ObjectiveBoundsProto MakeTrivialBounds(const bool is_maximize)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtoEnumToString(ProtoEnumType enum_value)
Definition proto_utils.h:50
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
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory)
std::string message
Definition trace.cc:397