Google OR-Tools v9.12
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-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 <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
69
70absl::StatusOr<std::unique_ptr<SolverInterface>> PdlpSolver::New(
71 const ModelProto& model, const InitArgs&) {
72 auto result = absl::WrapUnique(new PdlpSolver);
73 ASSIGN_OR_RETURN(result->pdlp_bridge_, PdlpBridge::FromProto(model));
74 return result;
75}
76
77absl::StatusOr<PrimalDualHybridGradientParams> PdlpSolver::MergeParameters(
78 const SolveParametersProto& parameters, const bool has_message_callback) {
79 PrimalDualHybridGradientParams result;
80 std::vector<std::string> warnings;
81 if (parameters.enable_output() || has_message_callback) {
82 result.set_verbosity_level(3);
83 }
84 if (parameters.has_threads()) {
85 result.set_num_threads(parameters.threads());
86 }
87 if (parameters.has_time_limit()) {
88 result.mutable_termination_criteria()->set_time_sec_limit(
89 absl::ToDoubleSeconds(
90 util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
91 }
92 if (parameters.has_node_limit()) {
93 warnings.push_back("parameter node_limit not supported for PDLP");
94 }
95 if (parameters.has_cutoff_limit()) {
96 warnings.push_back("parameter cutoff_limit not supported for PDLP");
97 }
98 if (parameters.has_objective_limit()) {
99 warnings.push_back("parameter best_objective_limit not supported for PDLP");
100 }
101 if (parameters.has_best_bound_limit()) {
102 warnings.push_back("parameter best_bound_limit not supported for PDLP");
103 }
104 if (parameters.has_solution_limit()) {
105 warnings.push_back("parameter solution_limit not supported for PDLP");
106 }
107 if (parameters.has_random_seed()) {
108 warnings.push_back("parameter random_seed not supported for PDLP");
109 }
110 if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED &&
111 parameters.lp_algorithm() != LP_ALGORITHM_FIRST_ORDER) {
112 warnings.push_back("parameter lp_algorithm not supported for PDLP");
113 }
114 if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
115 warnings.push_back("parameter presolve not supported for PDLP");
116 }
117 if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
118 warnings.push_back("parameter cuts not supported for PDLP");
119 }
120 if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
121 warnings.push_back("parameter heuristics not supported for PDLP");
122 }
123 if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
124 warnings.push_back("parameter scaling not supported for PDLP");
125 }
126 if (parameters.has_iteration_limit()) {
127 const int64_t limit = std::min<int64_t>(std::numeric_limits<int32_t>::max(),
128 parameters.iteration_limit());
129 result.mutable_termination_criteria()->set_iteration_limit(
130 static_cast<int32_t>(limit));
131 }
132 result.MergeFrom(parameters.pdlp());
133 if (!warnings.empty()) {
134 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
135 }
136 return result;
137}
138
139namespace {
140
141absl::StatusOr<TerminationProto> ConvertReason(
142 const pdlp::TerminationReason pdlp_reason, absl::string_view pdlp_detail,
143 const bool is_maximize, const double primal_objective,
144 const double dual_objective) {
145 switch (pdlp_reason) {
146 case pdlp::TERMINATION_REASON_UNSPECIFIED:
147 return TerminateForReason(
148 is_maximize, TERMINATION_REASON_OTHER_ERROR,
149 absl::StrCat("PDLP unspecified reason "
150 "(TERMINATION_REASON_UNSPECIFIED): ",
151 pdlp_detail));
152 return TerminateForReason(is_maximize, TERMINATION_REASON_UNSPECIFIED,
153 pdlp_detail);
154 case pdlp::TERMINATION_REASON_OPTIMAL:
155 return OptimalTerminationProto(primal_objective, dual_objective,
156 pdlp_detail);
157 case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE:
159 is_maximize,
160 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
161 pdlp_detail);
162 case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE:
164 is_maximize,
165 /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE,
166 pdlp_detail);
167 case pdlp::TERMINATION_REASON_TIME_LIMIT:
169 is_maximize, LIMIT_TIME,
170 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
171 case pdlp::TERMINATION_REASON_ITERATION_LIMIT:
173 is_maximize, LIMIT_ITERATION,
174 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
175 case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT:
177 is_maximize, LIMIT_OTHER,
178 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
179 case pdlp::TERMINATION_REASON_NUMERICAL_ERROR:
180 return TerminateForReason(is_maximize, TERMINATION_REASON_NUMERICAL_ERROR,
181 pdlp_detail);
182 case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER:
184 is_maximize, LIMIT_INTERRUPTED,
185 /*optional_dual_objective=*/std::nullopt, pdlp_detail);
186 case pdlp::TERMINATION_REASON_INVALID_PROBLEM:
187 // Indicates that the solver detected invalid problem data, e.g.,
188 // inconsistent bounds.
189 return absl::InternalError(
190 absl::StrCat("Invalid problem sent to PDLP solver "
191 "(TERMINATION_REASON_INVALID_PROBLEM): ",
192 pdlp_detail));
193 case pdlp::TERMINATION_REASON_INVALID_INITIAL_SOLUTION:
194 return absl::InvalidArgumentError(
195 absl::StrCat("PDLP solution hint invalid "
196 "(TERMINATION_REASON_INVALID_INITIAL_SOLUTION): ",
197 pdlp_detail));
198 case pdlp::TERMINATION_REASON_INVALID_PARAMETER:
199 // Indicates that an invalid value for the parameters was detected.
200 return absl::InvalidArgumentError(absl::StrCat(
201 "PDLP parameters invalid (TERMINATION_REASON_INVALID_PARAMETER): ",
202 pdlp_detail));
203 case pdlp::TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE:
205 is_maximize,
206 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
207 pdlp_detail);
208 case pdlp::TERMINATION_REASON_OTHER:
209 return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR,
210 pdlp_detail);
211 }
212 return absl::InvalidArgumentError(absl::StrCat(
213 "PDLP status: ", ProtoEnumToString(pdlp_reason), " not implemented."));
214}
215
216} // namespace
217
218absl::StatusOr<SolveResultProto> PdlpSolver::MakeSolveResult(
219 const pdlp::SolverResult& pdlp_result,
220 const ModelSolveParametersProto& model_params) {
221 // PDLP's default is a minimization problem for which the default primal and
222 // dual bounds are infinity and -infinity respectively. PDLP provides a
223 // scaling factor to flip the signs for maximization problems.
224 const double objective_scaling_factor =
225 pdlp_bridge_.pdlp_lp().objective_scaling_factor;
226 const bool is_maximize = objective_scaling_factor < 0.0;
227 SolveResultProto result;
228 const std::optional<pdlp::ConvergenceInformation> convergence_information =
229 pdlp::GetConvergenceInformation(pdlp_result.solve_log.solution_stats(),
230 pdlp_result.solve_log.solution_type());
231 if (convergence_information.has_value()) {
232 *result.mutable_pdlp_output()->mutable_convergence_information() =
233 *convergence_information;
234 }
235 ObjectiveBoundsProto objective_bounds = MakeTrivialBounds(is_maximize);
236 if (convergence_information.has_value()) {
237 objective_bounds.set_primal_bound(
238 convergence_information->primal_objective());
239 objective_bounds.set_dual_bound(convergence_information->dual_objective());
240 }
241 ASSIGN_OR_RETURN(*result.mutable_termination(),
242 ConvertReason(pdlp_result.solve_log.termination_reason(),
243 pdlp_result.solve_log.termination_string(),
244 is_maximize, objective_bounds.primal_bound(),
245 objective_bounds.dual_bound()));
246 ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_solve_time(),
248 absl::Seconds(pdlp_result.solve_log.solve_time_sec())));
249 result.mutable_solve_stats()->set_first_order_iterations(
250 pdlp_result.solve_log.iteration_count());
251
252 switch (pdlp_result.solve_log.termination_reason()) {
253 case pdlp::TERMINATION_REASON_OPTIMAL:
254 case pdlp::TERMINATION_REASON_TIME_LIMIT:
255 case pdlp::TERMINATION_REASON_ITERATION_LIMIT:
256 case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT:
257 case pdlp::TERMINATION_REASON_NUMERICAL_ERROR:
258 case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER: {
259 SolutionProto* solution_proto = result.add_solutions();
260 {
261 auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto(
262 pdlp_result.primal_solution, model_params.variable_values_filter());
263 RETURN_IF_ERROR(maybe_primal.status());
264 PrimalSolutionProto* primal_proto =
265 solution_proto->mutable_primal_solution();
266 primal_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
267 *primal_proto->mutable_variable_values() = *std::move(maybe_primal);
268 // Note: the solution could be primal feasible for termination reasons
269 // other than TERMINATION_REASON_OPTIMAL, but in theory, PDLP could
270 // also be modified to return the best feasible solution encountered in
271 // an early termination run (if any).
272 if (pdlp_result.solve_log.termination_reason() ==
273 pdlp::TERMINATION_REASON_OPTIMAL) {
274 primal_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
275 }
276 if (convergence_information.has_value()) {
277 primal_proto->set_objective_value(
278 convergence_information->primal_objective());
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 dual_proto->set_objective_value(
299 convergence_information->dual_objective());
300 }
301 }
302 break;
303 }
304 case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: {
305 // NOTE: for primal infeasible problems, PDLP stores the infeasibility
306 // certificate (dual ray) in the dual variables and reduced costs.
307 auto maybe_dual = pdlp_bridge_.DualVariablesToProto(
308 pdlp_result.dual_solution, model_params.dual_values_filter());
309 RETURN_IF_ERROR(maybe_dual.status());
310 auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto(
311 pdlp_result.reduced_costs, model_params.reduced_costs_filter());
312 RETURN_IF_ERROR(maybe_reduced.status());
313 DualRayProto* dual_ray_proto = result.add_dual_rays();
314 *dual_ray_proto->mutable_dual_values() = *std::move(maybe_dual);
315 *dual_ray_proto->mutable_reduced_costs() = *std::move(maybe_reduced);
316 break;
317 }
318 case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: {
319 // NOTE: for dual infeasible problems, PDLP stores the infeasibility
320 // certificate (primal ray) in the primal variables.
321 auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto(
322 pdlp_result.primal_solution, model_params.variable_values_filter());
323 RETURN_IF_ERROR(maybe_primal.status());
324 PrimalRayProto* primal_ray_proto = result.add_primal_rays();
325 *primal_ray_proto->mutable_variable_values() = *std::move(maybe_primal);
326 break;
327 }
328 default:
329 break;
330 }
331 return result;
332}
333
334absl::StatusOr<SolveResultProto> PdlpSolver::Solve(
335 const SolveParametersProto& parameters,
336 const ModelSolveParametersProto& model_parameters,
337 const MessageCallback message_cb,
338 const CallbackRegistrationProto& callback_registration, const Callback,
339 const SolveInterrupter* const interrupter) {
341 model_parameters, kPdlpSupportedStructures, "PDLP"));
343 /*supported_events=*/{}));
344
346 auto pdlp_params,
347 MergeParameters(parameters,
348 /*has_message_callback=*/message_cb != nullptr));
349
350 // PDLP returns `(TERMINATION_REASON_INVALID_PROBLEM): The input problem has
351 // inconsistent bounds.` but we want a more detailed error.
352 RETURN_IF_ERROR(pdlp_bridge_.ListInvertedBounds().ToStatus());
353
354 std::atomic<bool> interrupt = false;
355 const ScopedSolveInterrupterCallback set_interrupt(
356 interrupter, [&]() { interrupt = true; });
357
358 std::optional<PrimalAndDualSolution> initial_solution;
359 if (!model_parameters.solution_hints().empty()) {
360 initial_solution = pdlp_bridge_.SolutionHintToWarmStart(
361 model_parameters.solution_hints(0));
362 }
363
364 std::function<void(const std::string&)> pdlp_callback = nullptr;
365 if (message_cb != nullptr) {
366 pdlp_callback = [&message_cb](const std::string& message) {
367 message_cb(absl::StrSplit(message, '\n'));
368 };
369 }
370
371 const SolverResult pdlp_result =
372 PrimalDualHybridGradient(pdlp_bridge_.pdlp_lp(), pdlp_params,
373 initial_solution, &interrupt, pdlp_callback);
374 return MakeSolveResult(pdlp_result, model_parameters);
375}
376
377absl::StatusOr<bool> PdlpSolver::Update(const ModelUpdateProto&) {
378 return false;
379}
380
381absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
382PdlpSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&,
384 const SolveInterrupter*) {
385 return absl::UnimplementedError(
386 "PDLP does not provide a method to compute an infeasible subsystem");
387}
388
390
391} // namespace math_opt
392} // namespace operations_research
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
static absl::StatusOr< PdlpBridge > FromProto(const ModelProto &model_proto)
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
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
absl::Status ModelSolveParametersAreSupported(const ModelSolveParametersProto &model_parameters, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
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)
constexpr SupportedProblemStructures kPdlpSupportedStructures
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)