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