Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
primal_dual_hybrid_gradient.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
14// We solve a QP, which we call the "original QP", by applying preprocessing
15// including presolve and rescaling, which produces a new QP that we call the
16// "working QP". We then solve the working QP using the Primal Dual Hybrid
17// Gradient algorithm (PDHG). The optimality criteria are evaluated using the
18// original QP. There are three main modules in this file:
19// * The free function `PrimalDualHybridGradient()`, which is the user API, and
20// is responsible for input validation that doesn't use
21// ShardedQuadraticProgram, creating a `PreprocessSolver`, and calling
22// `PreprocessSolver::PreprocessAndSolve()`.
23// * The class `PreprocessSolver`, which is responsible for everything that
24// touches the original QP, including input validation that uses
25// ShardedQuadraticProgram, the preprocessing, converting solutions to the
26// working QP back to solutions to the original QP, and termination checks. It
27// also creates a `Solver` object and calls `Solver::Solve()`.
28// * The class `Solver`, which is responsible for everything that only touches
29// the working QP. It keeps a pointer to `PreprocessSolver` and calls methods
30// on it when it needs access to the original QP, e.g. termination checks.
31// When feasibility polishing is enabled the main solve's `Solver` object
32// creates additional `Solver` objects periodically to do the feasibility
33// polishing (in `Solver::TryPrimalPolishing()` and
34// `Solver::TryDualPolishing()`).
35// The main reason for having two separate classes `PreprocessSolver` and
36// `Solver` is the fact that feasibility polishing mode uses a single
37// `PreprocessSolver` object with multiple `Solver` objects.
38
40
41#include <algorithm>
42#include <atomic>
43#include <cmath>
44#include <cstdint>
45#include <functional>
46#include <limits>
47#include <optional>
48#include <random>
49#include <string>
50#include <utility>
51#include <vector>
52
53#include "Eigen/Core"
54#include "Eigen/SparseCore"
55#include "absl/algorithm/container.h"
56#include "absl/base/nullability.h"
57#include "absl/base/optimization.h"
58#include "absl/log/check.h"
59#include "absl/log/log.h"
60#include "absl/status/status.h"
61#include "absl/status/statusor.h"
62#include "absl/strings/str_cat.h"
63#include "absl/strings/str_format.h"
64#include "absl/strings/string_view.h"
65#include "absl/time/clock.h"
66#include "absl/time/time.h"
67#include "google/protobuf/repeated_ptr_field.h"
69#include "ortools/base/timer.h"
87
89
90namespace {
91
92using ::Eigen::VectorXd;
93using ::operations_research::SolverLogger;
94
95using IterationStatsCallback =
96 std::function<void(const IterationCallbackInfo&)>;
97
98// Computes a `num_threads` that is capped by the problem size and `num_shards`,
99// if specified, to avoid creating unusable threads.
100int NumThreads(const int num_threads, const int num_shards,
101 const QuadraticProgram& qp, SolverLogger& logger) {
102 int capped_num_threads = num_threads;
103 if (num_shards > 0) {
104 capped_num_threads = std::min(capped_num_threads, num_shards);
105 }
106 const int64_t problem_limit = std::max(qp.variable_lower_bounds.size(),
107 qp.constraint_lower_bounds.size());
108 capped_num_threads =
109 static_cast<int>(std::min(int64_t{capped_num_threads}, problem_limit));
110 capped_num_threads = std::max(capped_num_threads, 1);
111 if (capped_num_threads != num_threads) {
112 SOLVER_LOG(&logger, "WARNING: Reducing num_threads from ", num_threads,
113 " to ", capped_num_threads,
114 " because additional threads would be useless.");
115 }
116 return capped_num_threads;
117}
118
119// If `num_shards` is positive, returns it. Otherwise returns a reasonable
120// number of shards to use with `ShardedQuadraticProgram` for the given
121// `num_threads`.
122int NumShards(const int num_threads, const int num_shards) {
123 if (num_shards > 0) return num_shards;
124 return num_threads == 1 ? 1 : 4 * num_threads;
125}
126
127std::string ConvergenceInformationString(
128 const ConvergenceInformation& convergence_information,
129 const RelativeConvergenceInformation& relative_information,
130 const OptimalityNorm residual_norm) {
131 constexpr absl::string_view kFormatStr =
132 "%#12.6g %#12.6g %#12.6g | %#12.6g %#12.6g %#12.6g | %#12.6g %#12.6g | "
133 "%#12.6g %#12.6g";
134 switch (residual_norm) {
136 return absl::StrFormat(
137 kFormatStr, relative_information.relative_l_inf_primal_residual,
138 relative_information.relative_l_inf_dual_residual,
139 relative_information.relative_optimality_gap,
140 convergence_information.l_inf_primal_residual(),
141 convergence_information.l_inf_dual_residual(),
142 convergence_information.primal_objective() -
143 convergence_information.dual_objective(),
144 convergence_information.primal_objective(),
145 convergence_information.dual_objective(),
146 convergence_information.l2_primal_variable(),
147 convergence_information.l2_dual_variable());
149 return absl::StrFormat(kFormatStr,
150 relative_information.relative_l2_primal_residual,
151 relative_information.relative_l2_dual_residual,
152 relative_information.relative_optimality_gap,
153 convergence_information.l2_primal_residual(),
154 convergence_information.l2_dual_residual(),
155 convergence_information.primal_objective() -
156 convergence_information.dual_objective(),
157 convergence_information.primal_objective(),
158 convergence_information.dual_objective(),
159 convergence_information.l2_primal_variable(),
160 convergence_information.l2_dual_variable());
162 return absl::StrFormat(
163 kFormatStr,
164 convergence_information.l_inf_componentwise_primal_residual(),
165 convergence_information.l_inf_componentwise_dual_residual(),
166 relative_information.relative_optimality_gap,
167 convergence_information.l_inf_primal_residual(),
168 convergence_information.l_inf_dual_residual(),
169 convergence_information.primal_objective() -
170 convergence_information.dual_objective(),
171 convergence_information.primal_objective(),
172 convergence_information.dual_objective(),
173 convergence_information.l2_primal_variable(),
174 convergence_information.l2_dual_variable());
176 LOG(FATAL) << "Residual norm not specified.";
177 }
178 LOG(FATAL) << "Invalid residual norm " << residual_norm << ".";
179}
180
181std::string ConvergenceInformationShortString(
182 const ConvergenceInformation& convergence_information,
183 const RelativeConvergenceInformation& relative_information,
184 const OptimalityNorm residual_norm) {
185 constexpr absl::string_view kFormatStr =
186 "%#10.4g %#10.4g %#10.4g | %#10.4g %#10.4g";
187 switch (residual_norm) {
189 return absl::StrFormat(
190 kFormatStr, relative_information.relative_l_inf_primal_residual,
191 relative_information.relative_l_inf_dual_residual,
192 relative_information.relative_optimality_gap,
193 convergence_information.primal_objective(),
194 convergence_information.dual_objective());
196 return absl::StrFormat(kFormatStr,
197 relative_information.relative_l2_primal_residual,
198 relative_information.relative_l2_dual_residual,
199 relative_information.relative_optimality_gap,
200 convergence_information.primal_objective(),
201 convergence_information.dual_objective());
203 return absl::StrFormat(
204 kFormatStr,
205 convergence_information.l_inf_componentwise_primal_residual(),
206 convergence_information.l_inf_componentwise_dual_residual(),
207 relative_information.relative_optimality_gap,
208 convergence_information.primal_objective(),
209 convergence_information.dual_objective());
211 LOG(FATAL) << "Residual norm not specified.";
212 }
213 LOG(FATAL) << "Invalid residual norm " << residual_norm << ".";
214}
215
216// Logs a description of `iter_stats`, based on the
217// `iter_stats.convergence_information` entry with
218// `.candidate_type()==preferred_candidate` if one exists, otherwise based on
219// the first value, if any. `termination_criteria.optimality_norm` determines
220// which residual norms from `iter_stats.convergence_information` are used.
221void LogIterationStats(int verbosity_level, bool use_feasibility_polishing,
222 IterationType iteration_type,
223 const IterationStats& iter_stats,
224 const TerminationCriteria& termination_criteria,
225 const QuadraticProgramBoundNorms& bound_norms,
226 PointType preferred_candidate, SolverLogger& logger) {
227 std::string iteration_string =
228 verbosity_level >= 3
229 ? absl::StrFormat("%6d %8.1f %6.1f", iter_stats.iteration_number(),
230 iter_stats.cumulative_kkt_matrix_passes(),
231 iter_stats.cumulative_time_sec())
232 : absl::StrFormat("%6d %6.1f", iter_stats.iteration_number(),
233 iter_stats.cumulative_time_sec());
234 auto convergence_information =
235 GetConvergenceInformation(iter_stats, preferred_candidate);
236 if (!convergence_information.has_value() &&
237 iter_stats.convergence_information_size() > 0) {
238 convergence_information = iter_stats.convergence_information(0);
239 }
240 const char* phase_string = [&]() {
241 if (use_feasibility_polishing) {
242 switch (iteration_type) {
244 return "n ";
246 return "p ";
248 return "d ";
252 return "t ";
253 }
254 ABSL_UNREACHABLE();
255 } else {
256 return "";
257 }
258 }();
259 if (convergence_information.has_value()) {
260 const char* iterate_string = [&]() {
261 if (verbosity_level >= 4) {
262 switch (convergence_information->candidate_type()) {
264 return "C ";
266 return "A ";
268 return "D ";
270 return "F ";
271 default:
272 return "? ";
273 }
274 } else {
275 return "";
276 }
277 }();
278 const RelativeConvergenceInformation relative_information =
280 EffectiveOptimalityCriteria(termination_criteria),
281 *convergence_information, bound_norms);
282 std::string convergence_string =
283 verbosity_level >= 3
284 ? ConvergenceInformationString(
285 *convergence_information, relative_information,
286 termination_criteria.optimality_norm())
287 : ConvergenceInformationShortString(
288 *convergence_information, relative_information,
289 termination_criteria.optimality_norm());
290 SOLVER_LOG(&logger, phase_string, iterate_string, iteration_string, " | ",
291 convergence_string);
292 } else {
293 // No convergence information, just log the basic work stats.
294 SOLVER_LOG(&logger, phase_string, verbosity_level >= 4 ? "? " : "",
295 iteration_string);
296 }
297}
298
299void LogIterationStatsHeader(int verbosity_level,
300 bool use_feasibility_polishing,
301 SolverLogger& logger) {
302 std::string work_labels =
303 verbosity_level >= 3
304 ? absl::StrFormat("%6s %8s %6s", "iter#", "kkt_pass", "time")
305 : absl::StrFormat("%6s %6s", "iter#", "time");
306 std::string convergence_labels =
307 verbosity_level >= 3
308 ? absl::StrFormat(
309 "%12s %12s %12s | %12s %12s %12s | %12s %12s | %12s %12s",
310 "rel_prim_res", "rel_dual_res", "rel_gap", "prim_resid",
311 "dual_resid", "obj_gap", "prim_obj", "dual_obj", "prim_var_l2",
312 "dual_var_l2")
313 : absl::StrFormat("%10s %10s %10s | %10s %10s", "rel_p_res",
314 "rel_d_res", "rel_gap", "prim_obj", "dual_obj");
315 SOLVER_LOG(&logger, use_feasibility_polishing ? "f " : "",
316 verbosity_level >= 4 ? "I " : "", work_labels, " | ",
317 convergence_labels);
318}
319
320enum class InnerStepOutcome {
321 kSuccessful,
322 kForceNumericalTermination,
323};
324
325// Makes the closing changes to `solve_log` and builds a `SolverResult`.
326// NOTE: `primal_solution`, `dual_solution`, and `solve_log` are passed by
327// value. To avoid unnecessary copying, move these objects (i.e. use
328// `std::move()`).
329SolverResult ConstructSolverResult(VectorXd primal_solution,
330 VectorXd dual_solution,
331 const IterationStats& stats,
332 TerminationReason termination_reason,
333 PointType output_type, SolveLog solve_log) {
334 solve_log.set_iteration_count(stats.iteration_number());
335 solve_log.set_termination_reason(termination_reason);
336 solve_log.set_solution_type(output_type);
337 solve_log.set_solve_time_sec(stats.cumulative_time_sec());
338 *solve_log.mutable_solution_stats() = stats;
339 return SolverResult{.primal_solution = std::move(primal_solution),
340 .dual_solution = std::move(dual_solution),
341 .solve_log = std::move(solve_log)};
342}
343
344// See comment at top of file.
345class PreprocessSolver {
346 public:
347 // Assumes that `qp` and `params` are valid.
348 // Note that the `qp` is intentionally passed by value.
349 // `logger` is captured, and must outlive the class.
350 // NOTE: Many `PreprocessSolver` methods accept a `params` argument. This is
351 // passed as an argument instead of stored as a member variable to support
352 // using different `params` in different contexts with the same
353 // `PreprocessSolver` object.
354 explicit PreprocessSolver(QuadraticProgram qp,
355 const PrimalDualHybridGradientParams& params,
356 SolverLogger* logger);
357
358 // Not copyable or movable (because `glop::MainLpPreprocessor` isn't).
359 PreprocessSolver(const PreprocessSolver&) = delete;
360 PreprocessSolver& operator=(const PreprocessSolver&) = delete;
361 PreprocessSolver(PreprocessSolver&&) = delete;
362 PreprocessSolver& operator=(PreprocessSolver&&) = delete;
363
364 // Zero is used if `initial_solution` is nullopt. If `interrupt_solve` is not
365 // nullptr, then the solver will periodically check if
366 // `interrupt_solve->load()` is true, in which case the solve will terminate
367 // with `TERMINATION_REASON_INTERRUPTED_BY_USER`. Ownership is not
368 // transferred. If `iteration_stats_callback` is not nullptr, then at each
369 // termination step (when iteration stats are logged),
370 // `iteration_stats_callback` will also be called with those iteration stats.
371 SolverResult PreprocessAndSolve(
372 const PrimalDualHybridGradientParams& params,
373 std::optional<PrimalAndDualSolution> initial_solution,
374 const std::atomic<bool>* interrupt_solve,
375 IterationStatsCallback iteration_stats_callback);
376
377 // Returns a `TerminationReasonAndPointType` when the termination criteria are
378 // satisfied, otherwise returns nothing. The pointers to working_* can be
379 // nullptr if an iterate of that type is not available. For the iterate types
380 // that are available, uses the primal and dual vectors to compute solution
381 // statistics and adds them to the stats proto.
382 // `full_stats` is used for checking work-based termination criteria,
383 // but `stats` is updated with convergence information.
384 // NOTE: The primal and dual input pairs should be scaled solutions.
385 // TODO(user): Move this long argument list into a struct.
386 std::optional<TerminationReasonAndPointType>
387 UpdateIterationStatsAndCheckTermination(
388 const PrimalDualHybridGradientParams& params,
389 bool force_numerical_termination, const VectorXd& working_primal_current,
390 const VectorXd& working_dual_current,
391 const VectorXd* working_primal_average,
392 const VectorXd* working_dual_average,
393 const VectorXd* working_primal_delta, const VectorXd* working_dual_delta,
394 const VectorXd& last_primal_start_point,
395 const VectorXd& last_dual_start_point,
396 const std::atomic<bool>* interrupt_solve, IterationType iteration_type,
397 const IterationStats& full_stats, IterationStats& stats);
398
399 // Computes solution statistics for the primal and dual input pair, which
400 // should be a scaled solution. If `convergence_information != nullptr`,
401 // populates it with convergence information, and similarly if
402 // `infeasibility_information != nullptr`, populates it with infeasibility
403 // information.
404 void ComputeConvergenceAndInfeasibilityFromWorkingSolution(
405 const PrimalDualHybridGradientParams& params,
406 const VectorXd& working_primal, const VectorXd& working_dual,
407 PointType candidate_type, ConvergenceInformation* convergence_information,
408 InfeasibilityInformation* infeasibility_information) const;
409
410 // Returns a `SolverResult` for the original problem, given a `SolverResult`
411 // from the scaled or preprocessed problem. Also computes the reduced costs.
412 // NOTE: `result` is passed by value. To avoid unnecessary copying, move this
413 // object (i.e. use `std::move()`).
414 SolverResult ConstructOriginalSolverResult(
415 const PrimalDualHybridGradientParams& params, SolverResult result,
416 SolverLogger& logger) const;
417
418 const ShardedQuadraticProgram& ShardedWorkingQp() const {
419 return sharded_qp_;
420 }
421
422 // Swaps `variable_lower_bounds` and `variable_upper_bounds` with the ones on
423 // the sharded quadratic program.
424 void SwapVariableBounds(VectorXd& variable_lower_bounds,
425 VectorXd& variable_upper_bounds) {
426 sharded_qp_.SwapVariableBounds(variable_lower_bounds,
427 variable_upper_bounds);
428 }
429
430 // Swaps `constraint_lower_bounds` and `constraint_upper_bounds` with the ones
431 // on the sharded quadratic program.
432 void SwapConstraintBounds(VectorXd& constraint_lower_bounds,
433 VectorXd& constraint_upper_bounds) {
434 sharded_qp_.SwapConstraintBounds(constraint_lower_bounds,
435 constraint_upper_bounds);
436 }
437
438 // Swaps `objective` with the objective vector on the quadratic program.
439 // Swapping `objective_matrix` is not yet supported because it hasn't been
440 // needed.
441 void SwapObjectiveVector(VectorXd& objective) {
442 sharded_qp_.SwapObjectiveVector(objective);
443 }
444
445 const QuadraticProgramBoundNorms& OriginalBoundNorms() const {
446 return original_bound_norms_;
447 }
448
449 SolverLogger& Logger() { return logger_; }
450
451 private:
452 struct PresolveInfo {
453 explicit PresolveInfo(ShardedQuadraticProgram original_qp,
454 const PrimalDualHybridGradientParams& params)
455 : preprocessor_parameters(PreprocessorParameters(params)),
456 preprocessor(&preprocessor_parameters),
457 sharded_original_qp(std::move(original_qp)),
458 trivial_col_scaling_vec(
459 OnesVector(sharded_original_qp.PrimalSharder())),
460 trivial_row_scaling_vec(
461 OnesVector(sharded_original_qp.DualSharder())) {}
462
463 glop::GlopParameters preprocessor_parameters;
464 glop::MainLpPreprocessor preprocessor;
465 ShardedQuadraticProgram sharded_original_qp;
466 bool presolved_problem_was_maximization = false;
467 const VectorXd trivial_col_scaling_vec, trivial_row_scaling_vec;
468 };
469
470 // TODO(user): experiment with different preprocessor types.
471 static glop::GlopParameters PreprocessorParameters(
472 const PrimalDualHybridGradientParams& params);
473
474 // If presolve is enabled, moves `sharded_qp_` to
475 // `presolve_info_.sharded_original_qp` and computes the presolved linear
476 // program and installs it in `sharded_qp_`. Clears `initial_solution` if
477 // presolve is enabled. If presolve solves the problem completely returns the
478 // appropriate `TerminationReason`. Otherwise returns nullopt. If presolve
479 // is disabled or an error occurs modifies nothing and returns nullopt.
480 std::optional<TerminationReason> ApplyPresolveIfEnabled(
481 const PrimalDualHybridGradientParams& params,
482 std::optional<PrimalAndDualSolution>* initial_solution);
483
484 void ComputeAndApplyRescaling(const PrimalDualHybridGradientParams& params,
485 VectorXd& starting_primal_solution,
486 VectorXd& starting_dual_solution);
487
488 void LogQuadraticProgramStats(const QuadraticProgramStats& stats) const;
489
490 double InitialPrimalWeight(const PrimalDualHybridGradientParams& params,
491 double l2_norm_primal_linear_objective,
492 double l2_norm_constraint_bounds) const;
493
494 PrimalAndDualSolution RecoverOriginalSolution(
495 PrimalAndDualSolution working_solution) const;
496
497 // Adds one entry of `PointMetadata` to `stats` using the input solutions.
498 void AddPointMetadata(const PrimalDualHybridGradientParams& params,
499 const VectorXd& primal_solution,
500 const VectorXd& dual_solution, PointType point_type,
501 const VectorXd& last_primal_start_point,
502 const VectorXd& last_dual_start_point,
503 IterationStats& stats) const;
504
505 const QuadraticProgram& Qp() const { return sharded_qp_.Qp(); }
506
507 const int num_threads_;
508 const int num_shards_;
509
510 // The bound norms of the original problem.
511 QuadraticProgramBoundNorms original_bound_norms_;
512
513 // This is the QP that PDHG is run on. It is modified by presolve and
514 // rescaling, if those are enabled, and then serves as the
515 // `ShardedWorkingQp()` when calling `Solver::Solve()`. The original problem
516 // is available in `presolve_info_->sharded_original_qp` if
517 // `presolve_info_.has_value()`, and otherwise can be obtained by undoing the
518 // scaling of `sharded_qp_` by `col_scaling_vec_` and `row_scaling_vec_`.
519 ShardedQuadraticProgram sharded_qp_;
520
521 // Set iff presolve is enabled.
522 std::optional<PresolveInfo> presolve_info_;
523
524 // The scaling vectors that map the original (or presolved) quadratic program
525 // to the working version. See
526 // `ShardedQuadraticProgram::RescaleQuadraticProgram()` for details.
527 VectorXd col_scaling_vec_;
528 VectorXd row_scaling_vec_;
529
530 // A counter used to trigger writing iteration headers.
531 int log_counter_ = 0;
532 absl::Time time_of_last_log_ = absl::InfinitePast();
533 SolverLogger& logger_;
534 IterationStatsCallback iteration_stats_callback_;
535};
536
537// See comment at top of file.
538class Solver {
539 public:
540 // `preprocess_solver` should not be nullptr, and the `PreprocessSolver`
541 // object must outlive this `Solver` object. Ownership is not transferred.
542 explicit Solver(const PrimalDualHybridGradientParams& params,
543 VectorXd starting_primal_solution,
544 VectorXd starting_dual_solution, double initial_step_size,
545 double initial_primal_weight,
546 PreprocessSolver* preprocess_solver);
547
548 // Not copyable or movable (because there are const members).
549 Solver(const Solver&) = delete;
550 Solver& operator=(const Solver&) = delete;
551 Solver(Solver&&) = delete;
552 Solver& operator=(Solver&&) = delete;
553
554 const QuadraticProgram& WorkingQp() const { return ShardedWorkingQp().Qp(); }
555
556 const ShardedQuadraticProgram& ShardedWorkingQp() const {
557 return preprocess_solver_->ShardedWorkingQp();
558 }
559
560 // Runs PDHG iterations on the instance that has been initialized in `Solver`.
561 // If `interrupt_solve` is not nullptr, then the solver will periodically
562 // check if `interrupt_solve->load()` is true, in which case the solve will
563 // terminate with `TERMINATION_REASON_INTERRUPTED_BY_USER`. Ownership is not
564 // transferred.
565 // `solve_log` should contain initial problem statistics.
566 // On return, `SolveResult.reduced_costs` will be empty, and the solution will
567 // be to the preprocessed/scaled problem.
568 SolverResult Solve(IterationType iteration_type,
569 const std::atomic<bool>* interrupt_solve,
570 SolveLog solve_log);
571
572 private:
573 struct NextSolutionAndDelta {
574 VectorXd value;
575 // `delta` is `value` - current_solution.
576 VectorXd delta;
577 };
578
579 struct DistanceBasedRestartInfo {
580 double distance_moved_last_restart_period;
581 int length_of_last_restart_period;
582 };
583
584 // Movement terms (weighted squared norms of primal and dual deltas) larger
585 // than this cause termination because iterates are diverging, and likely to
586 // cause infinite and NaN values.
587 constexpr static double kDivergentMovement = 1.0e100;
588
589 // The total number of iterations in feasibility polishing is at most
590 // `4 * iterations_completed_ / kFeasibilityIterationFraction`.
591 // One factor of two is because there are both primal and dual feasibility
592 // polishing phases, and the other factor of two is because
593 // `next_feasibility_polishing_iteration` increases by a factor of 2 each
594 // feasibility polishing phase, so the sum of iteration limits is at most
595 // twice the last value.
596 constexpr static int kFeasibilityIterationFraction = 8;
597
598 // Attempts to solve primal and dual feasibility subproblems starting at the
599 // average iterate, for at most `iteration_limit` iterations each. If
600 // successful, returns a `SolverResult`, otherwise nullopt. Appends
601 // information about the polishing phases to
602 // `solve_log.feasibility_polishing_details`.
603 std::optional<SolverResult> TryFeasibilityPolishing(
604 int iteration_limit, const std::atomic<bool>* interrupt_solve,
605 SolveLog& solve_log);
606
607 // Tries to find primal feasibility, adds the solve log details to
608 // `solve_log.feasibility_polishing_details`, and returns the result.
609 SolverResult TryPrimalPolishing(VectorXd starting_primal_solution,
610 int iteration_limit,
611 const std::atomic<bool>* interrupt_solve,
612 SolveLog& solve_log);
613
614 // Tries to find dual feasibility, adds the solve log details to
615 // `solve_log.feasibility_polishing_details`, and returns the result.
616 SolverResult TryDualPolishing(VectorXd starting_dual_solution,
617 int iteration_limit,
618 const std::atomic<bool>* interrupt_solve,
619 SolveLog& solve_log);
620
621 NextSolutionAndDelta ComputeNextPrimalSolution(double primal_step_size) const;
622
623 NextSolutionAndDelta ComputeNextDualSolution(
624 double dual_step_size, double extrapolation_factor,
625 const NextSolutionAndDelta& next_primal_solution,
626 const VectorXd* absl_nullable next_primal_product = nullptr) const;
627
628 std::pair<double, double> ComputeMovementTerms(
629 const VectorXd& delta_primal, const VectorXd& delta_dual) const;
630
631 double ComputeMovement(const VectorXd& delta_primal,
632 const VectorXd& delta_dual) const;
633
634 double ComputeNonlinearity(const VectorXd& delta_primal,
635 const VectorXd& next_dual_product) const;
636
637 // Sets current_primal_product_ and current_dual_product_ based on
638 // current_primal_solution_ and current_dual_solution_ respectively.
639 void SetCurrentPrimalAndDualProducts();
640
641 // Creates all the simple-to-compute statistics in stats.
642 IterationStats CreateSimpleIterationStats(RestartChoice restart_used) const;
643
644 // Returns the total work so far from the main iterations and feasibility
645 // polishing phases.
646 IterationStats TotalWorkSoFar(const SolveLog& solve_log) const;
647
648 RestartChoice ChooseRestartToApply(bool is_major_iteration);
649
650 VectorXd PrimalAverage() const;
651
652 VectorXd DualAverage() const;
653
654 double ComputeNewPrimalWeight() const;
655
656 // Picks the primal and dual solutions according to `output_type`, and makes
657 // the closing changes to `solve_log`. This function should only be called
658 // once when the solver is finishing its execution.
659 // NOTE: `primal_solution` and `dual_solution` are used as the output except
660 // when `output_type` is `POINT_TYPE_CURRENT_ITERATE` or
661 // `POINT_TYPE_ITERATE_DIFFERENCE`, in which case the values are computed from
662 // `Solver` data.
663 // NOTE: `primal_solution`, `dual_solution`, and `solve_log` are passed by
664 // value. To avoid unnecessary copying, move these objects (i.e. use
665 // `std::move()`).
666 SolverResult PickSolutionAndConstructSolverResult(
667 VectorXd primal_solution, VectorXd dual_solution,
668 const IterationStats& stats, TerminationReason termination_reason,
669 PointType output_type, SolveLog solve_log) const;
670
671 double DistanceTraveledFromLastStart(const VectorXd& primal_solution,
672 const VectorXd& dual_solution) const;
673
674 LocalizedLagrangianBounds ComputeLocalizedBoundsAtCurrent() const;
675
676 LocalizedLagrangianBounds ComputeLocalizedBoundsAtAverage() const;
677
678 // Applies the given `RestartChoice`. If a restart is chosen, updates the
679 // state of the algorithm accordingly and computes a new primal weight.
680 void ApplyRestartChoice(RestartChoice restart_to_apply);
681
682 std::optional<SolverResult> MajorIterationAndTerminationCheck(
683 IterationType iteration_type, bool force_numerical_termination,
684 const std::atomic<bool>* interrupt_solve,
685 const IterationStats& work_from_feasibility_polishing,
686 SolveLog& solve_log);
687
688 bool ShouldDoAdaptiveRestartHeuristic(double candidate_normalized_gap) const;
689
690 RestartChoice DetermineDistanceBasedRestartChoice() const;
691
692 void ResetAverageToCurrent();
693
694 void LogNumericalTermination(const Eigen::VectorXd& primal_delta,
695 const Eigen::VectorXd& dual_delta) const;
696
697 void LogInnerIterationLimitHit() const;
698
699 // Takes a step based on the Malitsky and Pock linesearch algorithm.
700 // (https://arxiv.org/pdf/1608.08883.pdf)
701 // The current implementation is provably convergent (at an optimal rate)
702 // for LP programs (provided we do not change the primal weight at every major
703 // iteration). Further, we have observed that this rule is very sensitive to
704 // the parameter choice whenever we apply the primal weight recomputation
705 // heuristic.
706 InnerStepOutcome TakeMalitskyPockStep();
707
708 // Takes a step based on the adaptive heuristic presented in Section 3.1 of
709 // https://arxiv.org/pdf/2106.04756.pdf (further generalized to QP).
710 InnerStepOutcome TakeAdaptiveStep();
711
712 // Takes a constant-size step.
713 InnerStepOutcome TakeConstantSizeStep();
714
715 const PrimalDualHybridGradientParams params_;
716
717 VectorXd current_primal_solution_;
718 VectorXd current_dual_solution_;
719 VectorXd current_primal_delta_;
720 VectorXd current_dual_delta_;
721
722 ShardedWeightedAverage primal_average_;
723 ShardedWeightedAverage dual_average_;
724
725 double step_size_;
726 double primal_weight_;
727
728 PreprocessSolver* preprocess_solver_;
729
730 // For Malitsky-Pock linesearch only: `step_size_` / previous_step_size
731 double ratio_last_two_step_sizes_;
732 // For adaptive restarts only.
733 double normalized_gap_at_last_trial_ =
734 std::numeric_limits<double>::infinity();
735 // For adaptive restarts only.
736 double normalized_gap_at_last_restart_ =
737 std::numeric_limits<double>::infinity();
738
739 // `preprocessing_time_sec_` is added to the current value of `timer_` when
740 // computing `cumulative_time_sec` in iteration stats.
741 double preprocessing_time_sec_;
742 WallTimer timer_;
743 int iterations_completed_;
744 int num_rejected_steps_;
745 // A cache of `constraint_matrix * current_primal_solution_`.
746 // Malitsky-Pock linesearch only.
747 std::optional<VectorXd> current_primal_product_;
748 // A cache of `constraint_matrix.transpose() * current_dual_solution_`.
749 VectorXd current_dual_product_;
750 // The primal point at which the algorithm was last restarted from, or
751 // the initial primal starting point if no restart has occurred.
752 VectorXd last_primal_start_point_;
753 // The dual point at which the algorithm was last restarted from, or
754 // the initial dual starting point if no restart has occurred.
755 VectorXd last_dual_start_point_;
756 // Information for deciding whether to trigger a distance-based restart.
757 // The distances are initialized to +inf to force a restart during the first
758 // major iteration check.
759 DistanceBasedRestartInfo distance_based_restart_info_ = {
760 .distance_moved_last_restart_period =
761 std::numeric_limits<double>::infinity(),
762 .length_of_last_restart_period = 1,
763 };
764};
765
766PreprocessSolver::PreprocessSolver(QuadraticProgram qp,
767 const PrimalDualHybridGradientParams& params,
768 SolverLogger* logger)
769 : num_threads_(
770 NumThreads(params.num_threads(), params.num_shards(), qp, *logger)),
771 num_shards_(NumShards(num_threads_, params.num_shards())),
772 sharded_qp_(std::move(qp), num_threads_, num_shards_,
773 params.scheduler_type(), nullptr),
774 logger_(*logger) {}
775
776SolverResult ErrorSolverResult(const TerminationReason reason,
777 const std::string& message,
778 SolverLogger& logger) {
779 SolveLog error_log;
780 error_log.set_termination_reason(reason);
781 error_log.set_termination_string(message);
782 SOLVER_LOG(&logger,
783 "The solver did not run because of invalid input: ", message);
784 return SolverResult{.solve_log = error_log};
785}
786
787// If `check_excessively_small_values`, in addition to checking for extreme
788// values that PDLP can't handle, also check for extremely small constraint
789// bounds, variable bound gaps, and objective vector values that PDLP can handle
790// but that cause problems for other code such as glop's presolve.
791std::optional<SolverResult> CheckProblemStats(
792 const QuadraticProgramStats& problem_stats, const double objective_offset,
793 bool check_excessively_small_values, SolverLogger& logger) {
794 const double kExcessiveInputValue = 1e50;
795 const double kExcessivelySmallInputValue = 1e-50;
796 const double kMaxDynamicRange = 1e20;
797 if (std::isnan(problem_stats.constraint_matrix_l2_norm())) {
798 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
799 "Constraint matrix has a NAN.", logger);
800 }
801 if (problem_stats.constraint_matrix_abs_max() > kExcessiveInputValue) {
802 return ErrorSolverResult(
803 TERMINATION_REASON_INVALID_PROBLEM,
804 absl::StrCat("Constraint matrix has a non-zero with absolute value ",
805 problem_stats.constraint_matrix_abs_max(),
806 " which exceeds limit of ", kExcessiveInputValue, "."),
807 logger);
808 }
809 if (problem_stats.constraint_matrix_abs_max() >
810 kMaxDynamicRange * problem_stats.constraint_matrix_abs_min()) {
812 &logger, "WARNING: Constraint matrix has largest absolute value ",
813 problem_stats.constraint_matrix_abs_max(),
814 " and smallest non-zero absolute value ",
815 problem_stats.constraint_matrix_abs_min(), " performance may suffer.");
816 }
817 if (problem_stats.constraint_matrix_col_min_l_inf_norm() > 0 &&
818 problem_stats.constraint_matrix_col_min_l_inf_norm() <
819 kExcessivelySmallInputValue) {
820 return ErrorSolverResult(
821 TERMINATION_REASON_INVALID_PROBLEM,
822 absl::StrCat("Constraint matrix has a column with Linf norm ",
823 problem_stats.constraint_matrix_col_min_l_inf_norm(),
824 " which is less than limit of ",
825 kExcessivelySmallInputValue, "."),
826 logger);
827 }
828 if (problem_stats.constraint_matrix_row_min_l_inf_norm() > 0 &&
829 problem_stats.constraint_matrix_row_min_l_inf_norm() <
830 kExcessivelySmallInputValue) {
831 return ErrorSolverResult(
832 TERMINATION_REASON_INVALID_PROBLEM,
833 absl::StrCat("Constraint matrix has a row with Linf norm ",
834 problem_stats.constraint_matrix_row_min_l_inf_norm(),
835 " which is less than limit of ",
836 kExcessivelySmallInputValue, "."),
837 logger);
838 }
839 if (std::isnan(problem_stats.combined_bounds_l2_norm())) {
840 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
841 "Constraint bounds vector has a NAN.", logger);
842 }
843 if (problem_stats.combined_bounds_max() > kExcessiveInputValue) {
844 return ErrorSolverResult(
845 TERMINATION_REASON_INVALID_PROBLEM,
846 absl::StrCat("Combined constraint bounds vector has a non-zero with "
847 "absolute value ",
848 problem_stats.combined_bounds_max(),
849 " which exceeds limit of ", kExcessiveInputValue, "."),
850 logger);
851 }
852 if (check_excessively_small_values &&
853 problem_stats.combined_bounds_min() > 0 &&
854 problem_stats.combined_bounds_min() < kExcessivelySmallInputValue) {
855 return ErrorSolverResult(
856 TERMINATION_REASON_INVALID_PROBLEM,
857 absl::StrCat("Combined constraint bounds vector has a non-zero with "
858 "absolute value ",
859 problem_stats.combined_bounds_min(),
860 " which is less than the limit of ",
861 kExcessivelySmallInputValue, "."),
862 logger);
863 }
864 if (problem_stats.combined_bounds_max() >
865 kMaxDynamicRange * problem_stats.combined_bounds_min()) {
866 SOLVER_LOG(&logger,
867 "WARNING: Combined constraint bounds vector has largest "
868 "absolute value ",
869 problem_stats.combined_bounds_max(),
870 " and smallest non-zero absolute value ",
871 problem_stats.combined_bounds_min(),
872 "; performance may suffer.");
873 }
874 if (std::isnan(problem_stats.combined_variable_bounds_l2_norm())) {
875 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
876 "Variable bounds vector has a NAN.", logger);
877 }
878 if (problem_stats.combined_variable_bounds_max() > kExcessiveInputValue) {
879 return ErrorSolverResult(
880 TERMINATION_REASON_INVALID_PROBLEM,
881 absl::StrCat("Combined variable bounds vector has a non-zero with "
882 "absolute value ",
883 problem_stats.combined_variable_bounds_max(),
884 " which exceeds limit of ", kExcessiveInputValue, "."),
885 logger);
886 }
887 if (check_excessively_small_values &&
888 problem_stats.combined_variable_bounds_min() > 0 &&
889 problem_stats.combined_variable_bounds_min() <
890 kExcessivelySmallInputValue) {
891 return ErrorSolverResult(
892 TERMINATION_REASON_INVALID_PROBLEM,
893 absl::StrCat("Combined variable bounds vector has a non-zero with "
894 "absolute value ",
895 problem_stats.combined_variable_bounds_min(),
896 " which is less than the limit of ",
897 kExcessivelySmallInputValue, "."),
898 logger);
899 }
900 if (problem_stats.combined_variable_bounds_max() >
901 kMaxDynamicRange * problem_stats.combined_variable_bounds_min()) {
903 &logger,
904 "WARNING: Combined variable bounds vector has largest absolute value ",
905 problem_stats.combined_variable_bounds_max(),
906 " and smallest non-zero absolute value ",
907 problem_stats.combined_variable_bounds_min(),
908 "; performance may suffer.");
909 }
910 if (problem_stats.variable_bound_gaps_max() >
911 kMaxDynamicRange * problem_stats.variable_bound_gaps_min()) {
912 SOLVER_LOG(&logger,
913 "WARNING: Variable bound gap vector has largest absolute value ",
914 problem_stats.variable_bound_gaps_max(),
915 " and smallest non-zero absolute value ",
916 problem_stats.variable_bound_gaps_min(),
917 "; performance may suffer.");
918 }
919 if (std::isnan(objective_offset)) {
920 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
921 "Objective offset is NAN.", logger);
922 }
923 if (std::abs(objective_offset) > kExcessiveInputValue) {
924 return ErrorSolverResult(
925 TERMINATION_REASON_INVALID_PROBLEM,
926 absl::StrCat("Objective offset ", objective_offset,
927 " has absolute value which exceeds limit of ",
928 kExcessiveInputValue, "."),
929 logger);
930 }
931 if (std::isnan(problem_stats.objective_vector_l2_norm())) {
932 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
933 "Objective vector has a NAN.", logger);
934 }
935 if (problem_stats.objective_vector_abs_max() > kExcessiveInputValue) {
936 return ErrorSolverResult(
937 TERMINATION_REASON_INVALID_PROBLEM,
938 absl::StrCat("Objective vector has a non-zero with absolute value ",
939 problem_stats.objective_vector_abs_max(),
940 " which exceeds limit of ", kExcessiveInputValue, "."),
941 logger);
942 }
943 if (check_excessively_small_values &&
944 problem_stats.objective_vector_abs_min() > 0 &&
945 problem_stats.objective_vector_abs_min() < kExcessivelySmallInputValue) {
946 return ErrorSolverResult(
947 TERMINATION_REASON_INVALID_PROBLEM,
948 absl::StrCat("Objective vector has a non-zero with absolute value ",
949 problem_stats.objective_vector_abs_min(),
950 " which is less than the limit of ",
951 kExcessivelySmallInputValue, "."),
952 logger);
953 }
954 if (problem_stats.objective_vector_abs_max() >
955 kMaxDynamicRange * problem_stats.objective_vector_abs_min()) {
956 SOLVER_LOG(&logger, "WARNING: Objective vector has largest absolute value ",
957 problem_stats.objective_vector_abs_max(),
958 " and smallest non-zero absolute value ",
959 problem_stats.objective_vector_abs_min(),
960 "; performance may suffer.");
961 }
962 if (std::isnan(problem_stats.objective_matrix_l2_norm())) {
963 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
964 "Objective matrix has a NAN.", logger);
965 }
966 if (problem_stats.objective_matrix_abs_max() > kExcessiveInputValue) {
967 return ErrorSolverResult(
968 TERMINATION_REASON_INVALID_PROBLEM,
969 absl::StrCat("Objective matrix has a non-zero with absolute value ",
970 problem_stats.objective_matrix_abs_max(),
971 " which exceeds limit of ", kExcessiveInputValue, "."),
972 logger);
973 }
974 if (problem_stats.objective_matrix_abs_max() >
975 kMaxDynamicRange * problem_stats.objective_matrix_abs_min()) {
976 SOLVER_LOG(&logger, "WARNING: Objective matrix has largest absolute value ",
977 problem_stats.objective_matrix_abs_max(),
978 " and smallest non-zero absolute value ",
979 problem_stats.objective_matrix_abs_min(),
980 "; performance may suffer.");
981 }
982 return std::nullopt;
983}
984
985std::optional<SolverResult> CheckInitialSolution(
986 const ShardedQuadraticProgram& sharded_qp,
987 const PrimalAndDualSolution& initial_solution, SolverLogger& logger) {
988 const double kExcessiveInputValue = 1e50;
989 if (initial_solution.primal_solution.size() != sharded_qp.PrimalSize()) {
990 return ErrorSolverResult(
991 TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
992 absl::StrCat("Initial primal solution has size ",
993 initial_solution.primal_solution.size(),
994 " which differs from problem primal size ",
995 sharded_qp.PrimalSize()),
996 logger);
997 }
998 if (std::isnan(
999 Norm(initial_solution.primal_solution, sharded_qp.PrimalSharder()))) {
1000 return ErrorSolverResult(TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
1001 "Initial primal solution has a NAN.", logger);
1002 }
1003 if (const double norm = LInfNorm(initial_solution.primal_solution,
1004 sharded_qp.PrimalSharder());
1005 norm > kExcessiveInputValue) {
1006 return ErrorSolverResult(
1007 TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
1008 absl::StrCat(
1009 "Initial primal solution has an entry with absolute value ", norm,
1010 " which exceeds limit of ", kExcessiveInputValue),
1011 logger);
1012 }
1013 if (initial_solution.dual_solution.size() != sharded_qp.DualSize()) {
1014 return ErrorSolverResult(
1015 TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
1016 absl::StrCat("Initial dual solution has size ",
1017 initial_solution.dual_solution.size(),
1018 " which differs from problem dual size ",
1019 sharded_qp.DualSize()),
1020 logger);
1021 }
1022 if (std::isnan(
1023 Norm(initial_solution.dual_solution, sharded_qp.DualSharder()))) {
1024 return ErrorSolverResult(TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
1025 "Initial dual solution has a NAN.", logger);
1026 }
1027 if (const double norm =
1028 LInfNorm(initial_solution.dual_solution, sharded_qp.DualSharder());
1029 norm > kExcessiveInputValue) {
1030 return ErrorSolverResult(
1031 TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
1032 absl::StrCat("Initial dual solution has an entry with absolute value ",
1033 norm, " which exceeds limit of ", kExcessiveInputValue),
1034 logger);
1035 }
1036 return std::nullopt;
1037}
1038
1039SolverResult PreprocessSolver::PreprocessAndSolve(
1040 const PrimalDualHybridGradientParams& params,
1041 std::optional<PrimalAndDualSolution> initial_solution,
1042 const std::atomic<bool>* interrupt_solve,
1043 IterationStatsCallback iteration_stats_callback) {
1044 WallTimer timer;
1045 timer.Start();
1046 SolveLog solve_log;
1047 if (params.verbosity_level() >= 1) {
1048 SOLVER_LOG(&logger_, "Solving with PDLP parameters: ", params);
1049 }
1050 if (Qp().problem_name.has_value()) {
1051 solve_log.set_instance_name(*Qp().problem_name);
1052 }
1053 *solve_log.mutable_params() = params;
1054 sharded_qp_.ReplaceLargeConstraintBoundsWithInfinity(
1055 params.infinite_constraint_bound_threshold());
1056 if (!HasValidBounds(sharded_qp_)) {
1057 return ErrorSolverResult(
1058 TERMINATION_REASON_INVALID_PROBLEM,
1059 "The input problem has invalid bounds (after replacing large "
1060 "constraint bounds with infinity): some variable or constraint has "
1061 "lower_bound > upper_bound, lower_bound == inf, or upper_bound == "
1062 "-inf.",
1063 logger_);
1064 }
1065 if (Qp().objective_matrix.has_value() &&
1066 !sharded_qp_.PrimalSharder().ParallelTrueForAllShards(
1067 [&](const Sharder::Shard& shard) -> bool {
1068 return (shard(Qp().objective_matrix->diagonal()).array() >= 0.0)
1069 .all();
1070 })) {
1071 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
1072 "The objective is not convex (i.e., the objective "
1073 "matrix contains negative or NAN entries).",
1074 logger_);
1075 }
1076 *solve_log.mutable_original_problem_stats() = ComputeStats(sharded_qp_);
1077 const QuadraticProgramStats& original_problem_stats =
1078 solve_log.original_problem_stats();
1079 if (auto maybe_result =
1080 CheckProblemStats(original_problem_stats, Qp().objective_offset,
1081 params.presolve_options().use_glop(), logger_);
1082 maybe_result.has_value()) {
1083 return *maybe_result;
1084 }
1085 if (initial_solution.has_value()) {
1086 if (auto maybe_result =
1087 CheckInitialSolution(sharded_qp_, *initial_solution, logger_);
1088 maybe_result.has_value()) {
1089 return *maybe_result;
1090 }
1091 }
1092 original_bound_norms_ = BoundNormsFromProblemStats(original_problem_stats);
1093 const std::string preprocessing_string = absl::StrCat(
1094 params.presolve_options().use_glop() ? "presolving and " : "",
1095 "rescaling:");
1096 if (params.verbosity_level() >= 1) {
1097 SOLVER_LOG(&logger_, "Problem stats before ", preprocessing_string);
1098 LogQuadraticProgramStats(solve_log.original_problem_stats());
1099 }
1100 iteration_stats_callback_ = std::move(iteration_stats_callback);
1101 std::optional<TerminationReason> maybe_terminate =
1102 ApplyPresolveIfEnabled(params, &initial_solution);
1103 if (maybe_terminate.has_value()) {
1104 // Glop also feeds zero primal and dual solutions when the preprocessor
1105 // has a non-INIT status. When the preprocessor status is optimal the
1106 // vectors have length 0. When the status is something else the lengths
1107 // may be non-zero, but that's OK since we don't promise to produce a
1108 // meaningful solution in that case.
1109 IterationStats iteration_stats;
1110 iteration_stats.set_cumulative_time_sec(timer.Get());
1111 solve_log.set_preprocessing_time_sec(iteration_stats.cumulative_time_sec());
1112 VectorXd working_primal = ZeroVector(sharded_qp_.PrimalSharder());
1113 VectorXd working_dual = ZeroVector(sharded_qp_.DualSharder());
1114 ComputeConvergenceAndInfeasibilityFromWorkingSolution(
1115 params, working_primal, working_dual, POINT_TYPE_PRESOLVER_SOLUTION,
1116 iteration_stats.add_convergence_information(),
1117 iteration_stats.add_infeasibility_information());
1118 std::optional<TerminationReasonAndPointType> earned_termination =
1119 CheckIterateTerminationCriteria(params.termination_criteria(),
1120 iteration_stats, original_bound_norms_,
1121 /*force_numerical_termination=*/false);
1122 if (!earned_termination.has_value()) {
1123 earned_termination = CheckSimpleTerminationCriteria(
1124 params.termination_criteria(), iteration_stats, interrupt_solve);
1125 }
1126 TerminationReason final_termination_reason;
1127 if (earned_termination.has_value() &&
1128 (earned_termination->reason == TERMINATION_REASON_OPTIMAL ||
1129 earned_termination->reason == TERMINATION_REASON_PRIMAL_INFEASIBLE ||
1130 earned_termination->reason == TERMINATION_REASON_DUAL_INFEASIBLE)) {
1131 final_termination_reason = earned_termination->reason;
1132 } else {
1133 if (*maybe_terminate == TERMINATION_REASON_OPTIMAL) {
1134 final_termination_reason = TERMINATION_REASON_NUMERICAL_ERROR;
1135 SOLVER_LOG(
1136 &logger_,
1137 "WARNING: Presolve claimed to solve the LP optimally but the "
1138 "solution doesn't satisfy the optimality criteria.");
1139 } else {
1140 final_termination_reason = *maybe_terminate;
1141 }
1142 }
1143 return ConstructOriginalSolverResult(
1144 params,
1145 ConstructSolverResult(
1146 std::move(working_primal), std::move(working_dual),
1147 std::move(iteration_stats), final_termination_reason,
1148 POINT_TYPE_PRESOLVER_SOLUTION, std::move(solve_log)),
1149 logger_);
1150 }
1151
1152 VectorXd starting_primal_solution;
1153 VectorXd starting_dual_solution;
1154 // The current solution is updated by `ComputeAndApplyRescaling`.
1155 if (initial_solution.has_value()) {
1156 starting_primal_solution = std::move(initial_solution->primal_solution);
1157 starting_dual_solution = std::move(initial_solution->dual_solution);
1158 } else {
1159 SetZero(sharded_qp_.PrimalSharder(), starting_primal_solution);
1160 SetZero(sharded_qp_.DualSharder(), starting_dual_solution);
1161 }
1162 // The following projections are necessary since all our checks assume that
1163 // the primal and dual variable bounds are satisfied.
1164 ProjectToPrimalVariableBounds(sharded_qp_, starting_primal_solution);
1165 ProjectToDualVariableBounds(sharded_qp_, starting_dual_solution);
1166
1167 ComputeAndApplyRescaling(params, starting_primal_solution,
1168 starting_dual_solution);
1169 *solve_log.mutable_preprocessed_problem_stats() = ComputeStats(sharded_qp_);
1170 if (params.verbosity_level() >= 1) {
1171 SOLVER_LOG(&logger_, "Problem stats after ", preprocessing_string);
1172 LogQuadraticProgramStats(solve_log.preprocessed_problem_stats());
1173 }
1174
1175 double step_size = 0.0;
1176 if (params.linesearch_rule() ==
1177 PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE) {
1178 std::mt19937 random(1);
1179 double inverse_step_size;
1180 const auto lipschitz_result =
1182 sharded_qp_, std::nullopt, std::nullopt,
1183 /*desired_relative_error=*/0.2, /*failure_probability=*/0.0005,
1184 random);
1185 // With high probability, `lipschitz_result.singular_value` is within
1186 // +/- `lipschitz_result.estimated_relative_error
1187 // * lipschitz_result.singular_value`
1188 const double lipschitz_term_upper_bound =
1189 lipschitz_result.singular_value /
1190 (1.0 - lipschitz_result.estimated_relative_error);
1191 inverse_step_size = lipschitz_term_upper_bound;
1192 step_size = inverse_step_size > 0.0 ? 1.0 / inverse_step_size : 1.0;
1193 } else {
1194 // This initial step size is designed to err on the side of being too big.
1195 // This is because
1196 // (i) too-big steps are rejected and hence don't hurt us other than
1197 // wasting
1198 // an iteration and
1199 // (ii) the step size adjustment algorithm shrinks the step size as far as
1200 // needed in a single iteration but raises it slowly.
1201 // The tiny constant is there to keep the step size finite in the case of a
1202 // trivial LP with no constraints.
1203 step_size =
1204 1.0 /
1205 std::max(
1206 1.0e-20,
1207 solve_log.preprocessed_problem_stats().constraint_matrix_abs_max());
1208 }
1209 step_size *= params.initial_step_size_scaling();
1210
1211 const double primal_weight = InitialPrimalWeight(
1212 params, solve_log.preprocessed_problem_stats().objective_vector_l2_norm(),
1213 solve_log.preprocessed_problem_stats().combined_bounds_l2_norm());
1214
1215 Solver solver(params, starting_primal_solution, starting_dual_solution,
1216 step_size, primal_weight, this);
1217 solve_log.set_preprocessing_time_sec(timer.Get());
1218 SolverResult result = solver.Solve(IterationType::kNormal, interrupt_solve,
1219 std::move(solve_log));
1220 return ConstructOriginalSolverResult(params, std::move(result), logger_);
1221}
1222
1223glop::GlopParameters PreprocessSolver::PreprocessorParameters(
1224 const PrimalDualHybridGradientParams& params) {
1225 glop::GlopParameters glop_params;
1226 // TODO(user): Test if dualization helps or hurts performance.
1227 glop_params.set_solve_dual_problem(glop::GlopParameters::NEVER_DO);
1228 // Experiments show that this preprocessing step can hurt because it relaxes
1229 // variable bounds.
1230 glop_params.set_use_implied_free_preprocessor(false);
1231 // We do our own scaling.
1232 glop_params.set_use_scaling(false);
1233 if (params.presolve_options().has_glop_parameters()) {
1234 glop_params.MergeFrom(params.presolve_options().glop_parameters());
1235 }
1236 return glop_params;
1237}
1238
1239TerminationReason GlopStatusToTerminationReason(
1240 const glop::ProblemStatus glop_status, SolverLogger& logger) {
1241 switch (glop_status) {
1242 case glop::ProblemStatus::OPTIMAL:
1244 case glop::ProblemStatus::INVALID_PROBLEM:
1246 case glop::ProblemStatus::ABNORMAL:
1247 case glop::ProblemStatus::IMPRECISE:
1249 case glop::ProblemStatus::PRIMAL_INFEASIBLE:
1250 case glop::ProblemStatus::DUAL_INFEASIBLE:
1251 case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED:
1252 case glop::ProblemStatus::DUAL_UNBOUNDED:
1253 case glop::ProblemStatus::PRIMAL_UNBOUNDED:
1255 default:
1256 SOLVER_LOG(&logger, "WARNING: Unexpected preprocessor status ",
1257 glop_status);
1259 }
1260}
1261
1262std::optional<TerminationReason> PreprocessSolver::ApplyPresolveIfEnabled(
1263 const PrimalDualHybridGradientParams& params,
1264 std::optional<PrimalAndDualSolution>* const initial_solution) {
1265 const bool presolve_enabled = params.presolve_options().use_glop();
1266 if (!presolve_enabled) {
1267 return std::nullopt;
1268 }
1269 if (!IsLinearProgram(Qp())) {
1270 SOLVER_LOG(&logger_,
1271 "WARNING: Skipping presolve, which is only supported for linear "
1272 "programs");
1273 return std::nullopt;
1274 }
1275 absl::StatusOr<MPModelProto> model = QpToMpModelProto(Qp());
1276 if (!model.ok()) {
1277 SOLVER_LOG(&logger_,
1278 "WARNING: Skipping presolve because of error converting to "
1279 "MPModelProto: ",
1280 model.status());
1281 return std::nullopt;
1282 }
1283 if (initial_solution->has_value()) {
1284 SOLVER_LOG(&logger_,
1285 "WARNING: Ignoring initial solution. Initial solutions are "
1286 "ignored when presolve is on.");
1287 initial_solution->reset();
1288 }
1289 glop::LinearProgram glop_lp;
1290 glop::MPModelProtoToLinearProgram(*model, &glop_lp);
1291 // Save RAM.
1292 model->Clear();
1293 presolve_info_.emplace(std::move(sharded_qp_), params);
1294 // To simplify our code we ignore the return value indicating whether
1295 // postprocessing is required. We simply call `RecoverSolution()`
1296 // unconditionally, which may do nothing.
1297 presolve_info_->preprocessor.Run(&glop_lp);
1298 presolve_info_->presolved_problem_was_maximization =
1299 glop_lp.IsMaximizationProblem();
1300 MPModelProto output;
1301 glop::LinearProgramToMPModelProto(glop_lp, &output);
1302 // This will only fail if given an invalid LP, which shouldn't happen.
1303 absl::StatusOr<QuadraticProgram> presolved_qp =
1304 QpFromMpModelProto(output, /*relax_integer_variables=*/false);
1305 CHECK_OK(presolved_qp.status());
1306 // `MPModelProto` doesn't support scaling factors, so if `glop_lp` has an
1307 // `objective_scaling_factor` it won't be set in output and `presolved_qp`.
1308 // The scaling factor of `presolved_qp` isn't actually used anywhere, but we
1309 // set it for completeness.
1310 presolved_qp->objective_scaling_factor = glop_lp.objective_scaling_factor();
1311 sharded_qp_ = ShardedQuadraticProgram(std::move(*presolved_qp), num_threads_,
1312 num_shards_, params.scheduler_type());
1313 // A status of `INIT` means the preprocessor created a (usually) smaller
1314 // problem that needs solving. Other statuses mean the preprocessor solved
1315 // the problem completely.
1316 if (presolve_info_->preprocessor.status() != glop::ProblemStatus::INIT) {
1317 col_scaling_vec_ = OnesVector(sharded_qp_.PrimalSharder());
1318 row_scaling_vec_ = OnesVector(sharded_qp_.DualSharder());
1319 return GlopStatusToTerminationReason(presolve_info_->preprocessor.status(),
1320 logger_);
1321 }
1322 return std::nullopt;
1323}
1324
1325void PreprocessSolver::ComputeAndApplyRescaling(
1326 const PrimalDualHybridGradientParams& params,
1327 VectorXd& starting_primal_solution, VectorXd& starting_dual_solution) {
1328 ScalingVectors scaling = ApplyRescaling(
1329 RescalingOptions{.l_inf_ruiz_iterations = params.l_inf_ruiz_iterations(),
1330 .l2_norm_rescaling = params.l2_norm_rescaling()},
1331 sharded_qp_);
1332 row_scaling_vec_ = std::move(scaling.row_scaling_vec);
1333 col_scaling_vec_ = std::move(scaling.col_scaling_vec);
1334
1335 CoefficientWiseQuotientInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1336 starting_primal_solution);
1337 CoefficientWiseQuotientInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1338 starting_dual_solution);
1339}
1340
1341void PreprocessSolver::LogQuadraticProgramStats(
1342 const QuadraticProgramStats& stats) const {
1343 SOLVER_LOG(&logger_,
1344 absl::StrFormat("There are %i variables, %i constraints, and %i "
1345 "constraint matrix nonzeros.",
1346 stats.num_variables(), stats.num_constraints(),
1347 stats.constraint_matrix_num_nonzeros()));
1348 if (Qp().constraint_matrix.nonZeros() > 0) {
1349 SOLVER_LOG(&logger_,
1350 absl::StrFormat("Absolute values of nonzero constraint matrix "
1351 "elements: largest=%f, "
1352 "smallest=%f, avg=%f",
1353 stats.constraint_matrix_abs_max(),
1354 stats.constraint_matrix_abs_min(),
1355 stats.constraint_matrix_abs_avg()));
1356 SOLVER_LOG(
1357 &logger_,
1358 absl::StrFormat("Constraint matrix, infinity norm: max(row & col)=%f, "
1359 "min_col=%f, min_row=%f",
1360 stats.constraint_matrix_abs_max(),
1361 stats.constraint_matrix_col_min_l_inf_norm(),
1362 stats.constraint_matrix_row_min_l_inf_norm()));
1363 SOLVER_LOG(
1364 &logger_,
1365 absl::StrFormat(
1366 "Constraint bounds statistics (max absolute value per row): "
1367 "largest=%f, smallest=%f, avg=%f, l2_norm=%f",
1368 stats.combined_bounds_max(), stats.combined_bounds_min(),
1369 stats.combined_bounds_avg(), stats.combined_bounds_l2_norm()));
1370 }
1371 if (!IsLinearProgram(Qp())) {
1372 SOLVER_LOG(&logger_,
1373 absl::StrFormat("There are %i nonzero diagonal coefficients in "
1374 "the objective matrix.",
1375 stats.objective_matrix_num_nonzeros()));
1376 SOLVER_LOG(
1377 &logger_,
1378 absl::StrFormat(
1379 "Absolute values of nonzero objective matrix elements: largest=%f, "
1380 "smallest=%f, avg=%f",
1381 stats.objective_matrix_abs_max(), stats.objective_matrix_abs_min(),
1382 stats.objective_matrix_abs_avg()));
1383 }
1384 SOLVER_LOG(&logger_, absl::StrFormat("Absolute values of objective vector "
1385 "elements: largest=%f, smallest=%f, "
1386 "avg=%f, l2_norm=%f",
1387 stats.objective_vector_abs_max(),
1388 stats.objective_vector_abs_min(),
1389 stats.objective_vector_abs_avg(),
1390 stats.objective_vector_l2_norm()));
1391 SOLVER_LOG(
1392 &logger_,
1393 absl::StrFormat(
1394 "Gaps between variable upper and lower bounds: #finite=%i of %i, "
1395 "largest=%f, smallest=%f, avg=%f",
1396 stats.variable_bound_gaps_num_finite(), stats.num_variables(),
1397 stats.variable_bound_gaps_max(), stats.variable_bound_gaps_min(),
1398 stats.variable_bound_gaps_avg()));
1399}
1400
1401double PreprocessSolver::InitialPrimalWeight(
1402 const PrimalDualHybridGradientParams& params,
1403 const double l2_norm_primal_linear_objective,
1404 const double l2_norm_constraint_bounds) const {
1405 if (params.has_initial_primal_weight()) {
1406 return params.initial_primal_weight();
1407 }
1408 if (l2_norm_primal_linear_objective > 0.0 &&
1409 l2_norm_constraint_bounds > 0.0) {
1410 // The hand-wavy motivation for this choice is that the objective vector
1411 // has units of (objective units)/(primal units) and the constraint
1412 // bounds vector has units of (objective units)/(dual units),
1413 // therefore this ratio has units (dual units)/(primal units). By
1414 // dimensional analysis, these are the same units as the primal weight.
1415 return l2_norm_primal_linear_objective / l2_norm_constraint_bounds;
1416 } else {
1417 return 1.0;
1418 }
1419}
1420
1421PrimalAndDualSolution PreprocessSolver::RecoverOriginalSolution(
1422 PrimalAndDualSolution working_solution) const {
1423 glop::ProblemSolution glop_solution(glop::RowIndex{0}, glop::ColIndex{0});
1424 if (presolve_info_.has_value()) {
1425 // We compute statuses relative to the working problem so we can detect when
1426 // variables are at their bounds without floating-point roundoff induced by
1427 // scaling.
1428 glop_solution = internal::ComputeStatuses(Qp(), working_solution);
1429 }
1430 CoefficientWiseProductInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1431 working_solution.primal_solution);
1432 CoefficientWiseProductInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1433 working_solution.dual_solution);
1434 if (presolve_info_.has_value()) {
1435 glop_solution.primal_values =
1436 glop::DenseRow(working_solution.primal_solution.begin(),
1437 working_solution.primal_solution.end());
1438 glop_solution.dual_values =
1439 glop::DenseColumn(working_solution.dual_solution.begin(),
1440 working_solution.dual_solution.end());
1441 // We got the working QP by calling `LinearProgramToMPModelProto()` and
1442 // `QpFromMpModelProto()`. We need to negate the duals if the LP resulting
1443 // from presolve was a max problem.
1444 if (presolve_info_->presolved_problem_was_maximization) {
1445 for (glop::RowIndex i{0}; i < glop_solution.dual_values.size(); ++i) {
1446 glop_solution.dual_values[i] *= -1;
1447 }
1448 }
1449 presolve_info_->preprocessor.RecoverSolution(&glop_solution);
1450 PrimalAndDualSolution solution;
1451 solution.primal_solution =
1452 Eigen::Map<Eigen::VectorXd>(glop_solution.primal_values.data(),
1453 glop_solution.primal_values.size().value());
1454 solution.dual_solution =
1455 Eigen::Map<Eigen::VectorXd>(glop_solution.dual_values.data(),
1456 glop_solution.dual_values.size().value());
1457 // We called `QpToMpModelProto()` and `MPModelProtoToLinearProgram()` to
1458 // convert our original QP into input for glop's preprocessor. The former
1459 // multiplies the objective vector by `objective_scaling_factor`, which
1460 // multiplies the duals by that factor as well. To undo this we divide by
1461 // `objective_scaling_factor`.
1462 solution.dual_solution /=
1463 presolve_info_->sharded_original_qp.Qp().objective_scaling_factor;
1464 // Glop's preprocessor sometimes violates the primal bounds constraints. To
1465 // be safe we project both primal and dual.
1466 ProjectToPrimalVariableBounds(presolve_info_->sharded_original_qp,
1467 solution.primal_solution);
1468 ProjectToDualVariableBounds(presolve_info_->sharded_original_qp,
1469 solution.dual_solution);
1470 return solution;
1471 } else {
1472 return working_solution;
1473 }
1474}
1475
1476void SetActiveSetInformation(const ShardedQuadraticProgram& sharded_qp,
1477 const VectorXd& primal_solution,
1478 const VectorXd& dual_solution,
1479 const VectorXd& primal_start_point,
1480 const VectorXd& dual_start_point,
1481 PointMetadata& metadata) {
1482 CHECK_EQ(primal_solution.size(), sharded_qp.PrimalSize());
1483 CHECK_EQ(dual_solution.size(), sharded_qp.DualSize());
1484 CHECK_EQ(primal_start_point.size(), sharded_qp.PrimalSize());
1485 CHECK_EQ(dual_start_point.size(), sharded_qp.DualSize());
1486
1487 const QuadraticProgram& qp = sharded_qp.Qp();
1488 metadata.set_active_primal_variable_count(
1489 static_cast<int64_t>(sharded_qp.PrimalSharder().ParallelSumOverShards(
1490 [&](const Sharder::Shard& shard) {
1491 const auto primal_shard = shard(primal_solution);
1492 const auto lower_bound_shard = shard(qp.variable_lower_bounds);
1493 const auto upper_bound_shard = shard(qp.variable_upper_bounds);
1494 return (primal_shard.array() > lower_bound_shard.array() &&
1495 primal_shard.array() < upper_bound_shard.array())
1496 .count();
1497 })));
1498
1499 // Most of the computation from the previous `ParallelSumOverShards` is
1500 // duplicated here. However the overhead shouldn't be too large, and using
1501 // `ParallelSumOverShards` is simpler than just using `ParallelForEachShard`.
1502 metadata.set_active_primal_variable_change(
1503 static_cast<int64_t>(sharded_qp.PrimalSharder().ParallelSumOverShards(
1504 [&](const Sharder::Shard& shard) {
1505 const auto primal_shard = shard(primal_solution);
1506 const auto primal_start_shard = shard(primal_start_point);
1507 const auto lower_bound_shard = shard(qp.variable_lower_bounds);
1508 const auto upper_bound_shard = shard(qp.variable_upper_bounds);
1509 return ((primal_shard.array() > lower_bound_shard.array() &&
1510 primal_shard.array() < upper_bound_shard.array()) !=
1511 (primal_start_shard.array() > lower_bound_shard.array() &&
1512 primal_start_shard.array() < upper_bound_shard.array()))
1513 .count();
1514 })));
1515
1516 metadata.set_active_dual_variable_count(
1517 static_cast<int64_t>(sharded_qp.DualSharder().ParallelSumOverShards(
1518 [&](const Sharder::Shard& shard) {
1519 const auto dual_shard = shard(dual_solution);
1520 const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
1521 const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
1522 const double kInfinity = std::numeric_limits<double>::infinity();
1523 return (dual_shard.array() != 0.0 ||
1524 (lower_bound_shard.array() == -kInfinity &&
1525 upper_bound_shard.array() == kInfinity))
1526 .count();
1527 })));
1528
1529 metadata.set_active_dual_variable_change(
1530 static_cast<int64_t>(sharded_qp.DualSharder().ParallelSumOverShards(
1531 [&](const Sharder::Shard& shard) {
1532 const auto dual_shard = shard(dual_solution);
1533 const auto dual_start_shard = shard(dual_start_point);
1534 const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
1535 const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
1536 const double kInfinity = std::numeric_limits<double>::infinity();
1537 return ((dual_shard.array() != 0.0 ||
1538 (lower_bound_shard.array() == -kInfinity &&
1539 upper_bound_shard.array() == kInfinity)) !=
1540 (dual_start_shard.array() != 0.0 ||
1541 (lower_bound_shard.array() == -kInfinity &&
1542 upper_bound_shard.array() == kInfinity)))
1543 .count();
1544 })));
1545}
1546
1547void PreprocessSolver::AddPointMetadata(
1548 const PrimalDualHybridGradientParams& params,
1549 const VectorXd& primal_solution, const VectorXd& dual_solution,
1550 PointType point_type, const VectorXd& last_primal_start_point,
1551 const VectorXd& last_dual_start_point, IterationStats& stats) const {
1552 PointMetadata metadata;
1553 metadata.set_point_type(point_type);
1554 std::vector<int> random_projection_seeds(
1555 params.random_projection_seeds().begin(),
1556 params.random_projection_seeds().end());
1557 SetRandomProjections(sharded_qp_, primal_solution, dual_solution,
1558 random_projection_seeds, metadata);
1559 if (point_type != POINT_TYPE_ITERATE_DIFFERENCE) {
1560 SetActiveSetInformation(sharded_qp_, primal_solution, dual_solution,
1561 last_primal_start_point, last_dual_start_point,
1562 metadata);
1563 }
1564 *stats.add_point_metadata() = metadata;
1565}
1566
1567std::optional<TerminationReasonAndPointType>
1568PreprocessSolver::UpdateIterationStatsAndCheckTermination(
1569 const PrimalDualHybridGradientParams& params,
1570 bool force_numerical_termination, const VectorXd& working_primal_current,
1571 const VectorXd& working_dual_current,
1572 const VectorXd* working_primal_average,
1573 const VectorXd* working_dual_average, const VectorXd* working_primal_delta,
1574 const VectorXd* working_dual_delta, const VectorXd& last_primal_start_point,
1575 const VectorXd& last_dual_start_point,
1576 const std::atomic<bool>* interrupt_solve,
1577 const IterationType iteration_type, const IterationStats& full_stats,
1578 IterationStats& stats) {
1579 ComputeConvergenceAndInfeasibilityFromWorkingSolution(
1580 params, working_primal_current, working_dual_current,
1581 POINT_TYPE_CURRENT_ITERATE, stats.add_convergence_information(),
1582 stats.add_infeasibility_information());
1583 AddPointMetadata(params, working_primal_current, working_dual_current,
1584 POINT_TYPE_CURRENT_ITERATE, last_primal_start_point,
1585 last_dual_start_point, stats);
1586 if (working_primal_average != nullptr && working_dual_average != nullptr) {
1587 ComputeConvergenceAndInfeasibilityFromWorkingSolution(
1588 params, *working_primal_average, *working_dual_average,
1589 POINT_TYPE_AVERAGE_ITERATE, stats.add_convergence_information(),
1590 stats.add_infeasibility_information());
1591 AddPointMetadata(params, *working_primal_average, *working_dual_average,
1592 POINT_TYPE_AVERAGE_ITERATE, last_primal_start_point,
1593 last_dual_start_point, stats);
1594 }
1595 // Undoing presolve doesn't work for iterate differences.
1596 if (!presolve_info_.has_value() && working_primal_delta != nullptr &&
1597 working_dual_delta != nullptr) {
1598 ComputeConvergenceAndInfeasibilityFromWorkingSolution(
1599 params, *working_primal_delta, *working_dual_delta,
1600 POINT_TYPE_ITERATE_DIFFERENCE, nullptr,
1601 stats.add_infeasibility_information());
1602 AddPointMetadata(params, *working_primal_delta, *working_dual_delta,
1603 POINT_TYPE_ITERATE_DIFFERENCE, last_primal_start_point,
1604 last_dual_start_point, stats);
1605 }
1606 constexpr int kLogEvery = 15;
1607 absl::Time logging_time = absl::Now();
1608 if (params.verbosity_level() >= 2 &&
1609 (params.log_interval_seconds() == 0.0 ||
1610 logging_time - time_of_last_log_ >=
1611 absl::Seconds(params.log_interval_seconds()))) {
1612 if (log_counter_ == 0) {
1613 LogIterationStatsHeader(params.verbosity_level(),
1614 params.use_feasibility_polishing(), logger_);
1615 }
1616 LogIterationStats(params.verbosity_level(),
1617 params.use_feasibility_polishing(), iteration_type, stats,
1618 params.termination_criteria(), original_bound_norms_,
1619 POINT_TYPE_AVERAGE_ITERATE, logger_);
1620 if (params.verbosity_level() >= 4) {
1621 // If the convergence information doesn't contain an average iterate, the
1622 // previous `LogIterationStats()` will report some other iterate (usually
1623 // the current one) so don't repeat logging it.
1624 if (GetConvergenceInformation(stats, POINT_TYPE_AVERAGE_ITERATE) !=
1625 std::nullopt) {
1626 LogIterationStats(
1627 params.verbosity_level(), params.use_feasibility_polishing(),
1628 iteration_type, stats, params.termination_criteria(),
1629 original_bound_norms_, POINT_TYPE_CURRENT_ITERATE, logger_);
1630 }
1631 }
1632 time_of_last_log_ = logging_time;
1633 if (++log_counter_ >= kLogEvery) {
1634 log_counter_ = 0;
1635 }
1636 }
1637 if (iteration_stats_callback_ != nullptr) {
1638 iteration_stats_callback_(
1639 {.iteration_type = iteration_type,
1640 .termination_criteria = params.termination_criteria(),
1641 .iteration_stats = stats,
1642 .bound_norms = original_bound_norms_});
1643 }
1644
1645 if (const auto termination = CheckIterateTerminationCriteria(
1646 params.termination_criteria(), stats, original_bound_norms_,
1647 force_numerical_termination);
1648 termination.has_value()) {
1649 return termination;
1650 }
1651 return CheckSimpleTerminationCriteria(params.termination_criteria(),
1652 full_stats, interrupt_solve);
1653}
1654
1655void PreprocessSolver::ComputeConvergenceAndInfeasibilityFromWorkingSolution(
1656 const PrimalDualHybridGradientParams& params,
1657 const VectorXd& working_primal, const VectorXd& working_dual,
1658 PointType candidate_type, ConvergenceInformation* convergence_information,
1659 InfeasibilityInformation* infeasibility_information) const {
1660 const TerminationCriteria::DetailedOptimalityCriteria criteria =
1661 EffectiveOptimalityCriteria(params.termination_criteria());
1662 const double primal_epsilon_ratio =
1663 EpsilonRatio(criteria.eps_optimal_primal_residual_absolute(),
1664 criteria.eps_optimal_primal_residual_relative());
1665 const double dual_epsilon_ratio =
1666 EpsilonRatio(criteria.eps_optimal_dual_residual_absolute(),
1667 criteria.eps_optimal_dual_residual_relative());
1668 if (presolve_info_.has_value()) {
1669 // Undoing presolve doesn't make sense for iterate differences.
1670 CHECK_NE(candidate_type, POINT_TYPE_ITERATE_DIFFERENCE);
1671
1672 PrimalAndDualSolution original = RecoverOriginalSolution(
1673 {.primal_solution = working_primal, .dual_solution = working_dual});
1674 if (convergence_information != nullptr) {
1675 *convergence_information = ComputeConvergenceInformation(
1676 params, presolve_info_->sharded_original_qp,
1677 presolve_info_->trivial_col_scaling_vec,
1678 presolve_info_->trivial_row_scaling_vec, original.primal_solution,
1679 original.dual_solution, primal_epsilon_ratio, dual_epsilon_ratio,
1680 candidate_type);
1681 }
1682 if (infeasibility_information != nullptr) {
1683 VectorXd primal_copy =
1684 CloneVector(original.primal_solution,
1685 presolve_info_->sharded_original_qp.PrimalSharder());
1686 ProjectToPrimalVariableBounds(presolve_info_->sharded_original_qp,
1687 primal_copy,
1688 /*use_feasibility_bounds=*/true);
1689 // Only iterate differences should be able to violate the dual variable
1690 // bounds, and iterate differences aren't used when presolve is enabled.
1691 *infeasibility_information = ComputeInfeasibilityInformation(
1692 params, presolve_info_->sharded_original_qp,
1693 presolve_info_->trivial_col_scaling_vec,
1694 presolve_info_->trivial_row_scaling_vec, primal_copy,
1695 original.dual_solution, original.primal_solution, candidate_type);
1696 }
1697 } else {
1698 if (convergence_information != nullptr) {
1699 *convergence_information = ComputeConvergenceInformation(
1700 params, sharded_qp_, col_scaling_vec_, row_scaling_vec_,
1701 working_primal, working_dual, primal_epsilon_ratio,
1702 dual_epsilon_ratio, candidate_type);
1703 }
1704 if (infeasibility_information != nullptr) {
1705 VectorXd primal_copy =
1706 CloneVector(working_primal, sharded_qp_.PrimalSharder());
1707 ProjectToPrimalVariableBounds(sharded_qp_, primal_copy,
1708 /*use_feasibility_bounds=*/true);
1709 if (candidate_type == POINT_TYPE_ITERATE_DIFFERENCE) {
1710 // Iterate differences might not satisfy the dual variable bounds.
1711 VectorXd dual_copy =
1712 CloneVector(working_dual, sharded_qp_.DualSharder());
1713 ProjectToDualVariableBounds(sharded_qp_, dual_copy);
1714 *infeasibility_information = ComputeInfeasibilityInformation(
1715 params, sharded_qp_, col_scaling_vec_, row_scaling_vec_,
1716 primal_copy, dual_copy, working_primal, candidate_type);
1717 } else {
1718 *infeasibility_information = ComputeInfeasibilityInformation(
1719 params, sharded_qp_, col_scaling_vec_, row_scaling_vec_,
1720 primal_copy, working_dual, working_primal, candidate_type);
1721 }
1722 }
1723 }
1724}
1725
1726// `result` is used both as the input and as the temporary that will be
1727// returned.
1728SolverResult PreprocessSolver::ConstructOriginalSolverResult(
1729 const PrimalDualHybridGradientParams& params, SolverResult result,
1730 SolverLogger& logger) const {
1731 const bool use_zero_primal_objective =
1732 result.solve_log.termination_reason() ==
1734 if (presolve_info_.has_value()) {
1735 // Transform the solutions so they match the original unscaled problem.
1736 PrimalAndDualSolution original_solution = RecoverOriginalSolution(
1737 {.primal_solution = std::move(result.primal_solution),
1738 .dual_solution = std::move(result.dual_solution)});
1739 result.primal_solution = std::move(original_solution.primal_solution);
1740 if (result.solve_log.termination_reason() ==
1741 TERMINATION_REASON_DUAL_INFEASIBLE) {
1742 ProjectToPrimalVariableBounds(presolve_info_->sharded_original_qp,
1743 result.primal_solution,
1744 /*use_feasibility_bounds=*/true);
1745 }
1746 // If presolve is enabled, no projection of the dual is performed when
1747 // checking for infeasibility.
1748 result.dual_solution = std::move(original_solution.dual_solution);
1749 // `RecoverOriginalSolution` doesn't recover reduced costs so we need to
1750 // compute them with respect to the original problem.
1751 result.reduced_costs = ReducedCosts(
1752 params, presolve_info_->sharded_original_qp, result.primal_solution,
1753 result.dual_solution, use_zero_primal_objective);
1754 } else {
1755 if (result.solve_log.termination_reason() ==
1756 TERMINATION_REASON_DUAL_INFEASIBLE) {
1757 ProjectToPrimalVariableBounds(sharded_qp_, result.primal_solution,
1758 /*use_feasibility_bounds=*/true);
1759 }
1760 if (result.solve_log.termination_reason() ==
1761 TERMINATION_REASON_PRIMAL_INFEASIBLE) {
1762 ProjectToDualVariableBounds(sharded_qp_, result.dual_solution);
1763 }
1764 result.reduced_costs =
1765 ReducedCosts(params, sharded_qp_, result.primal_solution,
1766 result.dual_solution, use_zero_primal_objective);
1767 // Transform the solutions so they match the original unscaled problem.
1768 CoefficientWiseProductInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1769 result.primal_solution);
1770 CoefficientWiseProductInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1771 result.dual_solution);
1773 col_scaling_vec_, sharded_qp_.PrimalSharder(), result.reduced_costs);
1774 }
1775 IterationType iteration_type;
1776 switch (result.solve_log.solution_type()) {
1778 iteration_type = IterationType::kFeasibilityPolishingTermination;
1779 break;
1781 iteration_type = IterationType::kPresolveTermination;
1782 break;
1783 default:
1784 iteration_type = IterationType::kNormalTermination;
1785 break;
1786 }
1787 if (iteration_stats_callback_ != nullptr) {
1788 iteration_stats_callback_(
1789 {.iteration_type = iteration_type,
1790 .termination_criteria = params.termination_criteria(),
1791 .iteration_stats = result.solve_log.solution_stats(),
1792 .bound_norms = original_bound_norms_});
1793 }
1794
1795 if (params.verbosity_level() >= 1) {
1796 SOLVER_LOG(&logger, "Termination reason: ",
1797 TerminationReason_Name(result.solve_log.termination_reason()));
1798 SOLVER_LOG(&logger, "Solution point type: ",
1799 PointType_Name(result.solve_log.solution_type()));
1800 SOLVER_LOG(&logger, "Final solution stats:");
1801 LogIterationStatsHeader(params.verbosity_level(),
1802 params.use_feasibility_polishing(), logger);
1803 LogIterationStats(params.verbosity_level(),
1804 params.use_feasibility_polishing(), iteration_type,
1805 result.solve_log.solution_stats(),
1806 params.termination_criteria(), original_bound_norms_,
1807 result.solve_log.solution_type(), logger);
1808 const auto& convergence_info = GetConvergenceInformation(
1809 result.solve_log.solution_stats(), result.solve_log.solution_type());
1810 if (convergence_info.has_value()) {
1811 if (std::isfinite(convergence_info->corrected_dual_objective())) {
1812 SOLVER_LOG(&logger, "Dual objective after infeasibility correction: ",
1813 convergence_info->corrected_dual_objective());
1814 }
1815 }
1816 }
1817 return result;
1818}
1819
1820Solver::Solver(const PrimalDualHybridGradientParams& params,
1821 VectorXd starting_primal_solution,
1822 VectorXd starting_dual_solution, const double initial_step_size,
1823 const double initial_primal_weight,
1824 PreprocessSolver* preprocess_solver)
1825 : params_(params),
1826 current_primal_solution_(std::move(starting_primal_solution)),
1827 current_dual_solution_(std::move(starting_dual_solution)),
1828 primal_average_(&preprocess_solver->ShardedWorkingQp().PrimalSharder()),
1829 dual_average_(&preprocess_solver->ShardedWorkingQp().DualSharder()),
1830 step_size_(initial_step_size),
1831 primal_weight_(initial_primal_weight),
1832 preprocess_solver_(preprocess_solver) {}
1833
1834Solver::NextSolutionAndDelta Solver::ComputeNextPrimalSolution(
1835 double primal_step_size) const {
1836 const int64_t primal_size = ShardedWorkingQp().PrimalSize();
1837 NextSolutionAndDelta result = {
1838 .value = VectorXd(primal_size),
1839 .delta = VectorXd(primal_size),
1840 };
1841 const QuadraticProgram& qp = WorkingQp();
1842 // This computes the primal portion of the PDHG algorithm:
1843 // argmin_x[gradient(f)(`current_primal_solution_`)^T x + g(x)
1844 // + `current_dual_solution_`^T K x
1845 // + (0.5 / `primal_step_size`) * norm(x - `current_primal_solution_`)^2]
1846 // See Sections 2 - 3 of Chambolle and Pock and the comment in the header.
1847 // We omitted the constant terms from Chambolle and Pock's (7).
1848 // This minimization is easy to do in closed form since it can be separated
1849 // into independent problems for each of the primal variables.
1850 ShardedWorkingQp().PrimalSharder().ParallelForEachShard(
1851 [&](const Sharder::Shard& shard) {
1852 if (!IsLinearProgram(qp)) {
1853 // TODO(user): Does changing this to auto (so it becomes an
1854 // Eigen deferred result), or inlining it below, change performance?
1855 const VectorXd diagonal_scaling =
1856 primal_step_size *
1857 shard(qp.objective_matrix->diagonal()).array() +
1858 1.0;
1859 shard(result.value) =
1860 (shard(current_primal_solution_) -
1861 primal_step_size *
1862 (shard(qp.objective_vector) - shard(current_dual_product_)))
1863 // Scale i-th element by 1 / (1 + `primal_step_size` * Q_{ii})
1864 .cwiseQuotient(diagonal_scaling)
1865 .cwiseMin(shard(qp.variable_upper_bounds))
1866 .cwiseMax(shard(qp.variable_lower_bounds));
1867 } else {
1868 // The formula in the LP case is simplified for better performance.
1869 shard(result.value) =
1870 (shard(current_primal_solution_) -
1871 primal_step_size *
1872 (shard(qp.objective_vector) - shard(current_dual_product_)))
1873 .cwiseMin(shard(qp.variable_upper_bounds))
1874 .cwiseMax(shard(qp.variable_lower_bounds));
1875 }
1876 shard(result.delta) =
1877 shard(result.value) - shard(current_primal_solution_);
1878 });
1879 return result;
1880}
1881
1882Solver::NextSolutionAndDelta Solver::ComputeNextDualSolution(
1883 double dual_step_size, double extrapolation_factor,
1884 const NextSolutionAndDelta& next_primal_solution,
1885 const VectorXd* absl_nullable next_primal_product) const {
1886 const int64_t dual_size = ShardedWorkingQp().DualSize();
1887 NextSolutionAndDelta result = {
1888 .value = VectorXd(dual_size),
1889 .delta = VectorXd(dual_size),
1890 };
1891 const QuadraticProgram& qp = WorkingQp();
1892 std::optional<VectorXd> extrapolated_primal;
1893 if (!next_primal_product) {
1894 extrapolated_primal.emplace(ShardedWorkingQp().PrimalSize());
1895 ShardedWorkingQp().PrimalSharder().ParallelForEachShard(
1896 [&](const Sharder::Shard& shard) {
1897 shard(*extrapolated_primal) =
1898 (shard(next_primal_solution.value) +
1899 extrapolation_factor * shard(next_primal_solution.delta));
1900 });
1901 }
1902 ShardedWorkingQp().TransposedConstraintMatrixSharder().ParallelForEachShard(
1903 [&](const Sharder::Shard& shard) {
1904 VectorXd temp;
1905 if (next_primal_product) {
1906 CHECK(current_primal_product_.has_value());
1907 temp = shard(current_dual_solution_) -
1908 dual_step_size *
1909 (-extrapolation_factor * shard(*current_primal_product_) +
1910 (extrapolation_factor + 1) * shard(*next_primal_product));
1911 } else {
1912 temp = shard(current_dual_solution_) -
1913 dual_step_size *
1914 shard(ShardedWorkingQp().TransposedConstraintMatrix())
1915 .transpose() *
1916 extrapolated_primal.value();
1917 }
1918
1919 // Each element of the argument of `.cwiseMin()` is the critical point
1920 // of the respective 1D minimization problem if it's negative.
1921 // Likewise the argument to the `.cwiseMax()` is the critical point if
1922 // positive.
1923 shard(result.value) =
1924 VectorXd::Zero(temp.size())
1925 .cwiseMin(temp +
1926 dual_step_size * shard(qp.constraint_upper_bounds))
1927 .cwiseMax(temp +
1928 dual_step_size * shard(qp.constraint_lower_bounds));
1929 shard(result.delta) =
1930 (shard(result.value) - shard(current_dual_solution_));
1931 });
1932 return result;
1933}
1934
1935std::pair<double, double> Solver::ComputeMovementTerms(
1936 const VectorXd& delta_primal, const VectorXd& delta_dual) const {
1937 return {SquaredNorm(delta_primal, ShardedWorkingQp().PrimalSharder()),
1938 SquaredNorm(delta_dual, ShardedWorkingQp().DualSharder())};
1939}
1940
1941double Solver::ComputeMovement(const VectorXd& delta_primal,
1942 const VectorXd& delta_dual) const {
1943 const auto [primal_squared_norm, dual_squared_norm] =
1944 ComputeMovementTerms(delta_primal, delta_dual);
1945 return (0.5 * primal_weight_ * primal_squared_norm) +
1946 (0.5 / primal_weight_) * dual_squared_norm;
1947}
1948
1949double Solver::ComputeNonlinearity(const VectorXd& delta_primal,
1950 const VectorXd& next_dual_product) const {
1951 // Lemma 1 in Chambolle and Pock includes a term with L_f, the Lipshitz
1952 // constant of f. This is zero in our formulation.
1953 return ShardedWorkingQp().PrimalSharder().ParallelSumOverShards(
1954 [&](const Sharder::Shard& shard) {
1955 return -shard(delta_primal)
1956 .dot(shard(next_dual_product) -
1957 shard(current_dual_product_));
1958 });
1959}
1960
1961void Solver::SetCurrentPrimalAndDualProducts() {
1962 if (params_.linesearch_rule() ==
1963 PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE) {
1964 current_primal_product_ = TransposedMatrixVectorProduct(
1965 ShardedWorkingQp().TransposedConstraintMatrix(),
1966 current_primal_solution_,
1967 ShardedWorkingQp().TransposedConstraintMatrixSharder());
1968 } else {
1969 current_primal_product_.reset();
1970 }
1971 current_dual_product_ = TransposedMatrixVectorProduct(
1972 WorkingQp().constraint_matrix, current_dual_solution_,
1973 ShardedWorkingQp().ConstraintMatrixSharder());
1974}
1975
1976IterationStats Solver::CreateSimpleIterationStats(
1977 RestartChoice restart_used) const {
1978 IterationStats stats;
1979 double num_kkt_passes_per_rejected_step = 1.0;
1980 if (params_.linesearch_rule() ==
1981 PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE) {
1982 num_kkt_passes_per_rejected_step = 0.5;
1983 }
1984 stats.set_iteration_number(iterations_completed_);
1985 stats.set_cumulative_rejected_steps(num_rejected_steps_);
1986 // TODO(user): This formula doesn't account for kkt passes in major
1987 // iterations.
1988 stats.set_cumulative_kkt_matrix_passes(iterations_completed_ +
1989 num_kkt_passes_per_rejected_step *
1990 num_rejected_steps_);
1991 stats.set_cumulative_time_sec(preprocessing_time_sec_ + timer_.Get());
1992 stats.set_restart_used(restart_used);
1993 stats.set_step_size(step_size_);
1994 stats.set_primal_weight(primal_weight_);
1995 return stats;
1996}
1997
1998double Solver::DistanceTraveledFromLastStart(
1999 const VectorXd& primal_solution, const VectorXd& dual_solution) const {
2000 return std::sqrt((0.5 * primal_weight_) *
2001 SquaredDistance(primal_solution,
2002 last_primal_start_point_,
2003 ShardedWorkingQp().PrimalSharder()) +
2004 (0.5 / primal_weight_) *
2005 SquaredDistance(dual_solution, last_dual_start_point_,
2006 ShardedWorkingQp().DualSharder()));
2007}
2008
2009LocalizedLagrangianBounds Solver::ComputeLocalizedBoundsAtCurrent() const {
2010 const double distance_traveled_by_current = DistanceTraveledFromLastStart(
2011 current_primal_solution_, current_dual_solution_);
2013 ShardedWorkingQp(), current_primal_solution_, current_dual_solution_,
2014 PrimalDualNorm::kEuclideanNorm, primal_weight_,
2015 distance_traveled_by_current,
2016 /*primal_product=*/current_primal_product_.has_value()
2017 ? &current_primal_product_.value()
2018 : nullptr,
2019 &current_dual_product_, params_.use_diagonal_qp_trust_region_solver(),
2020 params_.diagonal_qp_trust_region_solver_tolerance());
2021}
2022
2023LocalizedLagrangianBounds Solver::ComputeLocalizedBoundsAtAverage() const {
2024 // TODO(user): These vectors are recomputed again for termination checks
2025 // and again if we eventually restart to the average.
2026 VectorXd average_primal = PrimalAverage();
2027 VectorXd average_dual = DualAverage();
2028
2029 const double distance_traveled_by_average =
2030 DistanceTraveledFromLastStart(average_primal, average_dual);
2031
2033 ShardedWorkingQp(), average_primal, average_dual,
2034 PrimalDualNorm::kEuclideanNorm, primal_weight_,
2035 distance_traveled_by_average,
2036 /*primal_product=*/nullptr, /*dual_product=*/nullptr,
2037 params_.use_diagonal_qp_trust_region_solver(),
2038 params_.diagonal_qp_trust_region_solver_tolerance());
2039}
2040
2041bool AverageHasBetterPotential(
2042 const LocalizedLagrangianBounds& local_bounds_at_average,
2043 const LocalizedLagrangianBounds& local_bounds_at_current) {
2044 return BoundGap(local_bounds_at_average) /
2045 MathUtil::Square(local_bounds_at_average.radius) <
2046 BoundGap(local_bounds_at_current) /
2047 MathUtil::Square(local_bounds_at_current.radius);
2048}
2049
2050double NormalizedGap(
2051 const LocalizedLagrangianBounds& local_bounds_at_candidate) {
2052 const double distance_traveled_by_candidate =
2053 local_bounds_at_candidate.radius;
2054 return BoundGap(local_bounds_at_candidate) / distance_traveled_by_candidate;
2055}
2056
2057// TODO(user): Review / cleanup adaptive heuristic.
2058bool Solver::ShouldDoAdaptiveRestartHeuristic(
2059 double candidate_normalized_gap) const {
2060 const double gap_reduction_ratio =
2061 candidate_normalized_gap / normalized_gap_at_last_restart_;
2062 if (gap_reduction_ratio < params_.sufficient_reduction_for_restart()) {
2063 return true;
2064 }
2065 if (gap_reduction_ratio < params_.necessary_reduction_for_restart() &&
2066 candidate_normalized_gap > normalized_gap_at_last_trial_) {
2067 // We've made the "necessary" amount of progress, and iterates appear to
2068 // be getting worse, so restart.
2069 return true;
2070 }
2071 return false;
2072}
2073
2074RestartChoice Solver::DetermineDistanceBasedRestartChoice() const {
2075 // The following checks are safeguards that normally should not be triggered.
2076 if (primal_average_.NumTerms() == 0) {
2078 } else if (distance_based_restart_info_.length_of_last_restart_period == 0) {
2080 }
2081 const int restart_period_length = primal_average_.NumTerms();
2082 const double distance_moved_this_restart_period_by_average =
2083 DistanceTraveledFromLastStart(primal_average_.ComputeAverage(),
2084 dual_average_.ComputeAverage());
2085 const double distance_moved_last_restart_period =
2086 distance_based_restart_info_.distance_moved_last_restart_period;
2087
2088 // A restart should be triggered when the normalized distance traveled by
2089 // the average is at least a constant factor smaller than the last.
2090 // TODO(user): Experiment with using `.necessary_reduction_for_restart()`
2091 // as a heuristic when deciding if a restart should be triggered.
2092 if ((distance_moved_this_restart_period_by_average / restart_period_length) <
2093 params_.sufficient_reduction_for_restart() *
2094 (distance_moved_last_restart_period /
2095 distance_based_restart_info_.length_of_last_restart_period)) {
2096 // Restart at current solution when it yields a smaller normalized potential
2097 // function value than the average (heuristic suggested by ohinder@).
2098 if (AverageHasBetterPotential(ComputeLocalizedBoundsAtAverage(),
2099 ComputeLocalizedBoundsAtCurrent())) {
2101 } else {
2103 }
2104 } else {
2106 }
2107}
2108
2109RestartChoice Solver::ChooseRestartToApply(const bool is_major_iteration) {
2110 if (!primal_average_.HasNonzeroWeight() &&
2111 !dual_average_.HasNonzeroWeight()) {
2113 }
2114 // TODO(user): This forced restart is very important for the performance of
2115 // `ADAPTIVE_HEURISTIC`. Test if the impact comes primarily from the first
2116 // forced restart (which would unseat a good initial starting point that could
2117 // prevent restarts early in the solve) or if it's really needed for the full
2118 // duration of the solve. If it is really needed, should we then trigger major
2119 // iterations on powers of two?
2120 const int restart_length = primal_average_.NumTerms();
2121 if (restart_length >= iterations_completed_ / 2 &&
2122 params_.restart_strategy() ==
2123 PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC) {
2124 if (AverageHasBetterPotential(ComputeLocalizedBoundsAtAverage(),
2125 ComputeLocalizedBoundsAtCurrent())) {
2127 } else {
2129 }
2130 }
2131 if (is_major_iteration) {
2132 switch (params_.restart_strategy()) {
2133 case PrimalDualHybridGradientParams::NO_RESTARTS:
2135 case PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION:
2137 case PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC: {
2138 const LocalizedLagrangianBounds local_bounds_at_average =
2139 ComputeLocalizedBoundsAtAverage();
2140 const LocalizedLagrangianBounds local_bounds_at_current =
2141 ComputeLocalizedBoundsAtCurrent();
2142 double normalized_gap;
2143 RestartChoice choice;
2144 if (AverageHasBetterPotential(local_bounds_at_average,
2145 local_bounds_at_current)) {
2146 normalized_gap = NormalizedGap(local_bounds_at_average);
2148 } else {
2149 normalized_gap = NormalizedGap(local_bounds_at_current);
2151 }
2152 if (ShouldDoAdaptiveRestartHeuristic(normalized_gap)) {
2153 return choice;
2154 } else {
2155 normalized_gap_at_last_trial_ = normalized_gap;
2157 }
2158 }
2159 case PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED: {
2160 return DetermineDistanceBasedRestartChoice();
2161 }
2162 default:
2163 LOG(FATAL) << "Unrecognized restart_strategy "
2164 << params_.restart_strategy();
2166 }
2167 } else {
2169 }
2170}
2171
2172VectorXd Solver::PrimalAverage() const {
2173 if (primal_average_.HasNonzeroWeight()) {
2174 return primal_average_.ComputeAverage();
2175 } else {
2176 return current_primal_solution_;
2177 }
2178}
2179
2180VectorXd Solver::DualAverage() const {
2181 if (dual_average_.HasNonzeroWeight()) {
2182 return dual_average_.ComputeAverage();
2183 } else {
2184 return current_dual_solution_;
2185 }
2186}
2187
2188double Solver::ComputeNewPrimalWeight() const {
2189 const double primal_distance =
2190 Distance(current_primal_solution_, last_primal_start_point_,
2191 ShardedWorkingQp().PrimalSharder());
2192 const double dual_distance =
2193 Distance(current_dual_solution_, last_dual_start_point_,
2194 ShardedWorkingQp().DualSharder());
2195 // This choice of a nonzero tolerance balances performance and numerical
2196 // issues caused by very huge or very tiny weights. It was picked as the best
2197 // among {0.0, 1.0e-20, 2.0e-16, 1.0e-10, 1.0e-5} on the preprocessed MIPLIB
2198 // dataset. The effect of changing this value is relatively minor overall.
2199 constexpr double kNonzeroTol = 1.0e-10;
2200 if (primal_distance <= kNonzeroTol || primal_distance >= 1.0 / kNonzeroTol ||
2201 dual_distance <= kNonzeroTol || dual_distance >= 1.0 / kNonzeroTol) {
2202 return primal_weight_;
2203 }
2204 const double smoothing_param = params_.primal_weight_update_smoothing();
2205 const double unsmoothed_new_primal_weight = dual_distance / primal_distance;
2206 const double new_primal_weight =
2207 std::exp(smoothing_param * std::log(unsmoothed_new_primal_weight) +
2208 (1.0 - smoothing_param) * std::log(primal_weight_));
2209 if (params_.verbosity_level() >= 4) {
2210 SOLVER_LOG(&preprocess_solver_->Logger(), "New computed primal weight is ",
2211 new_primal_weight, " at iteration ", iterations_completed_);
2212 }
2213 return new_primal_weight;
2214}
2215
2216SolverResult Solver::PickSolutionAndConstructSolverResult(
2217 VectorXd primal_solution, VectorXd dual_solution,
2218 const IterationStats& stats, TerminationReason termination_reason,
2219 PointType output_type, SolveLog solve_log) const {
2220 switch (output_type) {
2222 AssignVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder(),
2223 primal_solution);
2224 AssignVector(current_dual_solution_, ShardedWorkingQp().DualSharder(),
2225 dual_solution);
2226 break;
2228 AssignVector(current_primal_delta_, ShardedWorkingQp().PrimalSharder(),
2229 primal_solution);
2230 AssignVector(current_dual_delta_, ShardedWorkingQp().DualSharder(),
2231 dual_solution);
2232 break;
2235 break;
2236 default:
2237 // Default to average whenever `output_type` is `POINT_TYPE_NONE`.
2238 output_type = POINT_TYPE_AVERAGE_ITERATE;
2239 break;
2240 }
2241 return ConstructSolverResult(
2242 std::move(primal_solution), std::move(dual_solution), stats,
2243 termination_reason, output_type, std::move(solve_log));
2244}
2245
2246void Solver::ApplyRestartChoice(const RestartChoice restart_to_apply) {
2247 switch (restart_to_apply) {
2250 return;
2252 if (params_.verbosity_level() >= 4) {
2253 SOLVER_LOG(&preprocess_solver_->Logger(),
2254 "Restarted to current on iteration ", iterations_completed_,
2255 " after ", primal_average_.NumTerms(), " iterations");
2256 }
2257 break;
2259 if (params_.verbosity_level() >= 4) {
2260 SOLVER_LOG(&preprocess_solver_->Logger(),
2261 "Restarted to average on iteration ", iterations_completed_,
2262 " after ", primal_average_.NumTerms(), " iterations");
2263 }
2264 current_primal_solution_ = primal_average_.ComputeAverage();
2265 current_dual_solution_ = dual_average_.ComputeAverage();
2266 SetCurrentPrimalAndDualProducts();
2267 break;
2268 }
2269 primal_weight_ = ComputeNewPrimalWeight();
2270 ratio_last_two_step_sizes_ = 1;
2271 if (params_.restart_strategy() ==
2272 PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC) {
2273 // It's important for the theory that the distances here are calculated
2274 // given the new primal weight.
2275 const LocalizedLagrangianBounds local_bounds_at_last_restart =
2276 ComputeLocalizedBoundsAtCurrent();
2277 const double distance_traveled_since_last_restart =
2278 local_bounds_at_last_restart.radius;
2279 normalized_gap_at_last_restart_ = BoundGap(local_bounds_at_last_restart) /
2280 distance_traveled_since_last_restart;
2281 normalized_gap_at_last_trial_ = std::numeric_limits<double>::infinity();
2282 } else if (params_.restart_strategy() ==
2283 PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED) {
2284 // Update parameters for distance-based restarts.
2285 distance_based_restart_info_ = {
2286 .distance_moved_last_restart_period = DistanceTraveledFromLastStart(
2287 current_primal_solution_, current_dual_solution_),
2288 .length_of_last_restart_period = primal_average_.NumTerms()};
2289 }
2290 primal_average_.Clear();
2291 dual_average_.Clear();
2292 AssignVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder(),
2293 /*dest=*/last_primal_start_point_);
2294 AssignVector(current_dual_solution_, ShardedWorkingQp().DualSharder(),
2295 /*dest=*/last_dual_start_point_);
2296}
2297
2298// Adds the work (`iteration_number`, `cumulative_kkt_matrix_passes`,
2299// `cumulative_rejected_steps`, and `cumulative_time_sec`) from
2300// `additional_work_stats` to `stats` and returns the result.
2301// `stats` is intentionally passed by value.
2302IterationStats AddWorkStats(IterationStats stats,
2303 const IterationStats& additional_work_stats) {
2304 stats.set_iteration_number(stats.iteration_number() +
2305 additional_work_stats.iteration_number());
2306 stats.set_cumulative_kkt_matrix_passes(
2307 stats.cumulative_kkt_matrix_passes() +
2308 additional_work_stats.cumulative_kkt_matrix_passes());
2309 stats.set_cumulative_rejected_steps(
2310 stats.cumulative_rejected_steps() +
2311 additional_work_stats.cumulative_rejected_steps());
2312 stats.set_cumulative_time_sec(stats.cumulative_time_sec() +
2313 additional_work_stats.cumulative_time_sec());
2314 return stats;
2315}
2316
2317// Accumulates and returns the work (`iteration_number`,
2318// `cumulative_kkt_matrix_passes`, `cumulative_rejected_steps`, and
2319// `cumulative_time_sec`) from `solve_log.feasibility_polishing_details`.
2320IterationStats WorkFromFeasibilityPolishing(const SolveLog& solve_log) {
2321 IterationStats result;
2322 for (const FeasibilityPolishingDetails& feasibility_polishing_detail :
2323 solve_log.feasibility_polishing_details()) {
2324 result = AddWorkStats(std::move(result),
2325 feasibility_polishing_detail.solution_stats());
2326 }
2327 return result;
2328}
2329
2330bool TerminationReasonIsInterrupted(const TerminationReason reason) {
2332}
2333
2334bool TerminationReasonIsWorkLimitNotInterrupted(
2335 const TerminationReason reason) {
2336 return reason == TERMINATION_REASON_ITERATION_LIMIT ||
2339}
2340
2341// Note: `TERMINATION_REASON_INTERRUPTED_BY_USER` is treated as a work limit
2342// (that was determined in real-time by the user).
2343bool TerminationReasonIsWorkLimit(const TerminationReason reason) {
2344 return TerminationReasonIsWorkLimitNotInterrupted(reason) ||
2345 TerminationReasonIsInterrupted(reason);
2346}
2347
2348bool DoFeasibilityPolishingAfterLimitsReached(
2349 const PrimalDualHybridGradientParams& params,
2350 const TerminationReason reason) {
2351 if (TerminationReasonIsWorkLimitNotInterrupted(reason)) {
2352 return params.apply_feasibility_polishing_after_limits_reached();
2353 }
2354 if (TerminationReasonIsInterrupted(reason)) {
2355 return params.apply_feasibility_polishing_if_solver_is_interrupted();
2356 }
2357 return false;
2358}
2359
2360std::optional<SolverResult> Solver::MajorIterationAndTerminationCheck(
2361 const IterationType iteration_type, const bool force_numerical_termination,
2362 const std::atomic<bool>* interrupt_solve,
2363 const IterationStats& work_from_feasibility_polishing,
2364 SolveLog& solve_log) {
2365 const int major_iteration_cycle =
2366 iterations_completed_ % params_.major_iteration_frequency();
2367 const bool is_major_iteration =
2368 major_iteration_cycle == 0 && iterations_completed_ > 0;
2369 // Just decide what to do for now. The actual restart, if any, is
2370 // performed after the termination check.
2371 const RestartChoice restart = force_numerical_termination
2373 : ChooseRestartToApply(is_major_iteration);
2374 IterationStats stats = CreateSimpleIterationStats(restart);
2375 IterationStats full_work_stats =
2376 AddWorkStats(stats, work_from_feasibility_polishing);
2377 std::optional<TerminationReasonAndPointType> simple_termination_reason =
2378 CheckSimpleTerminationCriteria(params_.termination_criteria(),
2379 full_work_stats, interrupt_solve);
2380 const bool check_termination =
2381 major_iteration_cycle % params_.termination_check_frequency() == 0 ||
2382 simple_termination_reason.has_value() || force_numerical_termination;
2383 // We check termination on every major iteration.
2384 DCHECK(!is_major_iteration || check_termination);
2385 if (check_termination) {
2386 // Check for termination and update iteration stats with both simple and
2387 // solution statistics. The later are computationally harder to compute and
2388 // hence only computed here.
2389 VectorXd primal_average = PrimalAverage();
2390 VectorXd dual_average = DualAverage();
2391
2392 const std::optional<TerminationReasonAndPointType>
2393 maybe_termination_reason =
2394 preprocess_solver_->UpdateIterationStatsAndCheckTermination(
2395 params_, force_numerical_termination, current_primal_solution_,
2396 current_dual_solution_,
2397 primal_average_.HasNonzeroWeight() ? &primal_average : nullptr,
2398 dual_average_.HasNonzeroWeight() ? &dual_average : nullptr,
2399 current_primal_delta_.size() > 0 ? &current_primal_delta_
2400 : nullptr,
2401 current_dual_delta_.size() > 0 ? &current_dual_delta_ : nullptr,
2402 last_primal_start_point_, last_dual_start_point_,
2403 interrupt_solve, iteration_type, full_work_stats, stats);
2404 if (params_.record_iteration_stats()) {
2405 *solve_log.add_iteration_stats() = stats;
2406 }
2407 // We've terminated.
2408 if (maybe_termination_reason.has_value()) {
2409 if (iteration_type == IterationType::kNormal &&
2410 DoFeasibilityPolishingAfterLimitsReached(
2411 params_, maybe_termination_reason->reason)) {
2412 const std::optional<SolverResult> feasibility_result =
2413 TryFeasibilityPolishing(
2414 iterations_completed_ / kFeasibilityIterationFraction,
2415 interrupt_solve, solve_log);
2416 if (feasibility_result.has_value()) {
2417 LOG(INFO) << "Returning result from feasibility polishing after "
2418 "limits reached";
2419 return *feasibility_result;
2420 }
2421 }
2422 IterationStats terminating_full_stats =
2423 AddWorkStats(stats, work_from_feasibility_polishing);
2424 return PickSolutionAndConstructSolverResult(
2425 std::move(primal_average), std::move(dual_average),
2426 terminating_full_stats, maybe_termination_reason->reason,
2427 maybe_termination_reason->type, std::move(solve_log));
2428 }
2429 } else if (params_.record_iteration_stats()) {
2430 // Record simple iteration stats only.
2431 *solve_log.add_iteration_stats() = stats;
2432 }
2433 ApplyRestartChoice(restart);
2434 return std::nullopt;
2435}
2436
2437void Solver::ResetAverageToCurrent() {
2438 primal_average_.Clear();
2439 dual_average_.Clear();
2440 primal_average_.Add(current_primal_solution_, /*weight=*/1.0);
2441 dual_average_.Add(current_dual_solution_, /*weight=*/1.0);
2442}
2443
2444void Solver::LogNumericalTermination(const Eigen::VectorXd& primal_delta,
2445 const Eigen::VectorXd& dual_delta) const {
2446 if (params_.verbosity_level() >= 2) {
2447 auto [primal_squared_norm, dual_squared_norm] =
2448 ComputeMovementTerms(primal_delta, dual_delta);
2449 SOLVER_LOG(&preprocess_solver_->Logger(),
2450 "Forced numerical termination at iteration ",
2451 iterations_completed_, " with primal delta squared norm ",
2452 primal_squared_norm, " dual delta squared norm ",
2453 dual_squared_norm, " primal weight ", primal_weight_);
2454 }
2455}
2456
2457void Solver::LogInnerIterationLimitHit() const {
2458 SOLVER_LOG(&preprocess_solver_->Logger(),
2459 "WARNING: Inner iteration limit reached at iteration ",
2460 iterations_completed_);
2461}
2462
2463InnerStepOutcome Solver::TakeMalitskyPockStep() {
2464 InnerStepOutcome outcome = InnerStepOutcome::kSuccessful;
2465 const double primal_step_size = step_size_ / primal_weight_;
2466 NextSolutionAndDelta next_primal_solution =
2467 ComputeNextPrimalSolution(primal_step_size);
2468 // The theory by Malitsky and Pock holds for any new_step_size in the interval
2469 // [`step_size`, `step_size` * sqrt(1 + `ratio_last_two_step_sizes_`)].
2470 // `dilating_coeff` determines where in this interval the new step size lands.
2471 // NOTE: Malitsky and Pock use theta for `ratio_last_two_step_sizes`.
2472 double dilating_coeff =
2473 1 + (params_.malitsky_pock_parameters().step_size_interpolation() *
2474 (sqrt(1 + ratio_last_two_step_sizes_) - 1));
2475 double new_primal_step_size = primal_step_size * dilating_coeff;
2476 double step_size_downscaling =
2477 params_.malitsky_pock_parameters().step_size_downscaling_factor();
2478 double contraction_factor =
2479 params_.malitsky_pock_parameters().linesearch_contraction_factor();
2480 const double dual_weight = primal_weight_ * primal_weight_;
2481 int inner_iterations = 0;
2482 VectorXd next_primal_product(current_dual_solution_.size());
2483 ShardedWorkingQp().TransposedConstraintMatrixSharder().ParallelForEachShard(
2484 [&](const Sharder::Shard& shard) {
2485 shard(next_primal_product) =
2486 shard(ShardedWorkingQp().TransposedConstraintMatrix()).transpose() *
2487 next_primal_solution.value;
2488 });
2489
2490 for (bool accepted_step = false; !accepted_step; ++inner_iterations) {
2491 if (inner_iterations >= 60) {
2492 LogInnerIterationLimitHit();
2493 ResetAverageToCurrent();
2494 outcome = InnerStepOutcome::kForceNumericalTermination;
2495 break;
2496 }
2497 const double new_last_two_step_sizes_ratio =
2498 new_primal_step_size / primal_step_size;
2499 NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2500 dual_weight * new_primal_step_size, new_last_two_step_sizes_ratio,
2501 next_primal_solution, &next_primal_product);
2502
2503 VectorXd next_dual_product = TransposedMatrixVectorProduct(
2504 WorkingQp().constraint_matrix, next_dual_solution.value,
2505 ShardedWorkingQp().ConstraintMatrixSharder());
2506 double delta_dual_norm =
2507 Norm(next_dual_solution.delta, ShardedWorkingQp().DualSharder());
2508 double delta_dual_prod_norm =
2509 Distance(current_dual_product_, next_dual_product,
2510 ShardedWorkingQp().PrimalSharder());
2511 if (primal_weight_ * new_primal_step_size * delta_dual_prod_norm <=
2512 contraction_factor * delta_dual_norm) {
2513 // Accept new_step_size as a good step.
2514 step_size_ = new_primal_step_size * primal_weight_;
2515 ratio_last_two_step_sizes_ = new_last_two_step_sizes_ratio;
2516 // Malitsky and Pock guarantee uses a nonsymmetric weighted average,
2517 // the primal variable average involves the initial point, while the dual
2518 // doesn't. See Theorem 2 in https://arxiv.org/pdf/1608.08883.pdf for
2519 // details.
2520 if (!primal_average_.HasNonzeroWeight()) {
2521 primal_average_.Add(
2522 current_primal_solution_,
2523 /*weight=*/new_primal_step_size * new_last_two_step_sizes_ratio);
2524 }
2525
2526 current_primal_solution_ = std::move(next_primal_solution.value);
2527 current_dual_solution_ = std::move(next_dual_solution.value);
2528 current_dual_product_ = std::move(next_dual_product);
2529 current_primal_product_ = std::move(next_primal_product);
2530 primal_average_.Add(current_primal_solution_,
2531 /*weight=*/new_primal_step_size);
2532 dual_average_.Add(current_dual_solution_,
2533 /*weight=*/new_primal_step_size);
2534 const double movement =
2535 ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2536 if (movement == 0.0) {
2537 LogNumericalTermination(next_primal_solution.delta,
2538 next_dual_solution.delta);
2539 ResetAverageToCurrent();
2540 outcome = InnerStepOutcome::kForceNumericalTermination;
2541 } else if (movement > kDivergentMovement) {
2542 LogNumericalTermination(next_primal_solution.delta,
2543 next_dual_solution.delta);
2544 outcome = InnerStepOutcome::kForceNumericalTermination;
2545 }
2546 current_primal_delta_ = std::move(next_primal_solution.delta);
2547 current_dual_delta_ = std::move(next_dual_solution.delta);
2548 break;
2549 } else {
2550 new_primal_step_size = step_size_downscaling * new_primal_step_size;
2551 }
2552 }
2553 // `inner_iterations` isn't incremented for the accepted step.
2554 num_rejected_steps_ += inner_iterations;
2555 return outcome;
2556}
2557
2558InnerStepOutcome Solver::TakeAdaptiveStep() {
2559 InnerStepOutcome outcome = InnerStepOutcome::kSuccessful;
2560 int inner_iterations = 0;
2561 for (bool accepted_step = false; !accepted_step; ++inner_iterations) {
2562 if (inner_iterations >= 60) {
2563 LogInnerIterationLimitHit();
2564 ResetAverageToCurrent();
2565 outcome = InnerStepOutcome::kForceNumericalTermination;
2566 break;
2567 }
2568 const double primal_step_size = step_size_ / primal_weight_;
2569 const double dual_step_size = step_size_ * primal_weight_;
2570 NextSolutionAndDelta next_primal_solution =
2571 ComputeNextPrimalSolution(primal_step_size);
2572 NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2573 dual_step_size, /*extrapolation_factor=*/1.0, next_primal_solution);
2574 const double movement =
2575 ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2576 if (movement == 0.0) {
2577 LogNumericalTermination(next_primal_solution.delta,
2578 next_dual_solution.delta);
2579 ResetAverageToCurrent();
2580 outcome = InnerStepOutcome::kForceNumericalTermination;
2581 break;
2582 } else if (movement > kDivergentMovement) {
2583 LogNumericalTermination(next_primal_solution.delta,
2584 next_dual_solution.delta);
2585 outcome = InnerStepOutcome::kForceNumericalTermination;
2586 break;
2587 }
2588 VectorXd next_dual_product = TransposedMatrixVectorProduct(
2589 WorkingQp().constraint_matrix, next_dual_solution.value,
2590 ShardedWorkingQp().ConstraintMatrixSharder());
2591 const double nonlinearity =
2592 ComputeNonlinearity(next_primal_solution.delta, next_dual_product);
2593
2594 // See equation (5) in https://arxiv.org/pdf/2106.04756.pdf.
2595 const double step_size_limit =
2596 nonlinearity > 0 ? movement / nonlinearity
2597 : std::numeric_limits<double>::infinity();
2598
2599 if (step_size_ <= step_size_limit) {
2600 current_primal_solution_ = std::move(next_primal_solution.value);
2601 current_dual_solution_ = std::move(next_dual_solution.value);
2602 current_dual_product_ = std::move(next_dual_product);
2603 current_primal_product_.reset();
2604 current_primal_delta_ = std::move(next_primal_solution.delta);
2605 current_dual_delta_ = std::move(next_dual_solution.delta);
2606 primal_average_.Add(current_primal_solution_, /*weight=*/step_size_);
2607 dual_average_.Add(current_dual_solution_, /*weight=*/step_size_);
2608 accepted_step = true;
2609 }
2610 const double total_steps_attempted =
2611 num_rejected_steps_ + inner_iterations + iterations_completed_ + 1;
2612 // Our step sizes are a factor 1 - (`total_steps_attempted` + 1)^(-
2613 // `step_size_reduction_exponent`) smaller than they could be as a margin to
2614 // reduce rejected steps.
2615 // The std::isinf() test protects against NAN if std::pow() == 1.0.
2616 const double first_term =
2617 std::isinf(step_size_limit)
2618 ? step_size_limit
2619 : (1 - std::pow(total_steps_attempted + 1.0,
2620 -params_.adaptive_linesearch_parameters()
2621 .step_size_reduction_exponent())) *
2622 step_size_limit;
2623 const double second_term =
2624 (1 + std::pow(total_steps_attempted + 1.0,
2625 -params_.adaptive_linesearch_parameters()
2626 .step_size_growth_exponent())) *
2627 step_size_;
2628 // From the first term when we have to reject a step, `step_size_`
2629 // decreases by a factor of at least 1 - (`total_steps_attempted` + 1)^(-
2630 // `step_size_reduction_exponent`). From the second term we increase
2631 // `step_size_` by a factor of at most 1 + (`total_steps_attempted` +
2632 // 1)^(-`step_size_growth_exponent`) Therefore if more than order
2633 // (`total_steps_attempted` + 1)^(`step_size_reduction_exponent`
2634 // - `step_size_growth_exponent`) fraction of the time we have a rejected
2635 // step, we overall decrease `step_size_`. When `step_size_` is
2636 // sufficiently small we stop having rejected steps.
2637 step_size_ = std::min(first_term, second_term);
2638 }
2639 // `inner_iterations` is incremented for the accepted step.
2640 num_rejected_steps_ += inner_iterations - 1;
2641 return outcome;
2642}
2643
2644InnerStepOutcome Solver::TakeConstantSizeStep() {
2645 const double primal_step_size = step_size_ / primal_weight_;
2646 const double dual_step_size = step_size_ * primal_weight_;
2647 NextSolutionAndDelta next_primal_solution =
2648 ComputeNextPrimalSolution(primal_step_size);
2649 NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2650 dual_step_size, /*extrapolation_factor=*/1.0, next_primal_solution);
2651 const double movement =
2652 ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2653 if (movement == 0.0) {
2654 LogNumericalTermination(next_primal_solution.delta,
2655 next_dual_solution.delta);
2656 ResetAverageToCurrent();
2657 return InnerStepOutcome::kForceNumericalTermination;
2658 } else if (movement > kDivergentMovement) {
2659 LogNumericalTermination(next_primal_solution.delta,
2660 next_dual_solution.delta);
2661 return InnerStepOutcome::kForceNumericalTermination;
2662 }
2663 VectorXd next_dual_product = TransposedMatrixVectorProduct(
2664 WorkingQp().constraint_matrix, next_dual_solution.value,
2665 ShardedWorkingQp().ConstraintMatrixSharder());
2666 current_primal_solution_ = std::move(next_primal_solution.value);
2667 current_dual_solution_ = std::move(next_dual_solution.value);
2668 current_dual_product_ = std::move(next_dual_product);
2669 current_primal_product_.reset();
2670 current_primal_delta_ = std::move(next_primal_solution.delta);
2671 current_dual_delta_ = std::move(next_dual_solution.delta);
2672 primal_average_.Add(current_primal_solution_, /*weight=*/step_size_);
2673 dual_average_.Add(current_dual_solution_, /*weight=*/step_size_);
2674 return InnerStepOutcome::kSuccessful;
2675}
2676
2677IterationStats Solver::TotalWorkSoFar(const SolveLog& solve_log) const {
2678 IterationStats stats = CreateSimpleIterationStats(RESTART_CHOICE_NO_RESTART);
2679 IterationStats full_stats =
2680 AddWorkStats(stats, WorkFromFeasibilityPolishing(solve_log));
2681 return full_stats;
2682}
2683
2684FeasibilityPolishingDetails BuildFeasibilityPolishingDetails(
2685 PolishingPhaseType phase_type, int iteration_count,
2686 const PrimalDualHybridGradientParams& params, const SolveLog& solve_log) {
2687 FeasibilityPolishingDetails details;
2688 details.set_polishing_phase_type(phase_type);
2689 details.set_main_iteration_count(iteration_count);
2690 *details.mutable_params() = params;
2691 details.set_termination_reason(solve_log.termination_reason());
2692 details.set_iteration_count(solve_log.iteration_count());
2693 details.set_solve_time_sec(solve_log.solve_time_sec());
2694 *details.mutable_solution_stats() = solve_log.solution_stats();
2695 details.set_solution_type(solve_log.solution_type());
2696 absl::c_copy(solve_log.iteration_stats(),
2697 google::protobuf::RepeatedPtrFieldBackInserter(
2698 details.mutable_iteration_stats()));
2699 return details;
2700}
2701
2702std::optional<SolverResult> Solver::TryFeasibilityPolishing(
2703 const int iteration_limit, const std::atomic<bool>* interrupt_solve,
2704 SolveLog& solve_log) {
2705 TerminationCriteria::DetailedOptimalityCriteria optimality_criteria =
2706 EffectiveOptimalityCriteria(params_.termination_criteria());
2707
2708 VectorXd average_primal = PrimalAverage();
2709 VectorXd average_dual = DualAverage();
2710
2711 ConvergenceInformation first_convergence_info;
2712 preprocess_solver_->ComputeConvergenceAndInfeasibilityFromWorkingSolution(
2713 params_, average_primal, average_dual, POINT_TYPE_AVERAGE_ITERATE,
2714 &first_convergence_info, nullptr);
2715
2716 // NOTE: Even though the objective gap sometimes is reduced by feasibility
2717 // polishing, it is usually increased, and an experiment (on MIPLIB2017)
2718 // shows that this test reduces the iteration count by 3-4% on average.
2719 if (!ObjectiveGapMet(optimality_criteria, first_convergence_info)) {
2720 std::optional<TerminationReasonAndPointType> simple_termination_reason =
2721 CheckSimpleTerminationCriteria(params_.termination_criteria(),
2722 TotalWorkSoFar(solve_log),
2723 interrupt_solve);
2724 if (!(simple_termination_reason.has_value() &&
2725 DoFeasibilityPolishingAfterLimitsReached(
2726 params_, simple_termination_reason->reason))) {
2727 if (params_.verbosity_level() >= 2) {
2728 SOLVER_LOG(&preprocess_solver_->Logger(),
2729 "Skipping feasibility polishing because the objective gap "
2730 "is too large.");
2731 }
2732 return std::nullopt;
2733 }
2734 }
2735
2736 if (params_.verbosity_level() >= 2) {
2737 SOLVER_LOG(&preprocess_solver_->Logger(),
2738 "Starting primal feasibility polishing");
2739 }
2740 SolverResult primal_result = TryPrimalPolishing(
2741 std::move(average_primal), iteration_limit, interrupt_solve, solve_log);
2742
2743 if (params_.verbosity_level() >= 2) {
2744 SOLVER_LOG(
2745 &preprocess_solver_->Logger(),
2746 "Primal feasibility polishing termination reason: ",
2747 TerminationReason_Name(primal_result.solve_log.termination_reason()));
2748 }
2749 if (TerminationReasonIsWorkLimit(
2750 primal_result.solve_log.termination_reason())) {
2751 // Have we also reached the overall work limit? If so, consider finishing
2752 // the final polishing phase.
2753 std::optional<TerminationReasonAndPointType> simple_termination_reason =
2754 CheckSimpleTerminationCriteria(params_.termination_criteria(),
2755 TotalWorkSoFar(solve_log),
2756 interrupt_solve);
2757 if (!(simple_termination_reason.has_value() &&
2758 DoFeasibilityPolishingAfterLimitsReached(
2759 params_, simple_termination_reason->reason))) {
2760 return std::nullopt;
2761 }
2762 } else if (primal_result.solve_log.termination_reason() !=
2763 TERMINATION_REASON_OPTIMAL) {
2764 // Note: `TERMINATION_REASON_PRIMAL_INFEASIBLE` could happen normally, but
2765 // we haven't ensured that the correct solution is returned in that case, so
2766 // we ignore the polishing result indicating infeasibility.
2767 // `TERMINATION_REASON_NUMERICAL_ERROR` can occur, but would be surprising
2768 // and interesting. Other termination reasons are probably bugs.
2769 SOLVER_LOG(&preprocess_solver_->Logger(),
2770 "WARNING: Primal feasibility polishing terminated with error ",
2771 primal_result.solve_log.termination_reason());
2772 return std::nullopt;
2773 }
2774
2775 if (params_.verbosity_level() >= 2) {
2776 SOLVER_LOG(&preprocess_solver_->Logger(),
2777 "Starting dual feasibility polishing");
2778 }
2779 SolverResult dual_result = TryDualPolishing(
2780 std::move(average_dual), iteration_limit, interrupt_solve, solve_log);
2781
2782 if (params_.verbosity_level() >= 2) {
2783 SOLVER_LOG(
2784 &preprocess_solver_->Logger(),
2785 "Dual feasibility polishing termination reason: ",
2786 TerminationReason_Name(dual_result.solve_log.termination_reason()));
2787 }
2788
2789 IterationStats full_stats = TotalWorkSoFar(solve_log);
2790 std::optional<TerminationReasonAndPointType> simple_termination_reason =
2791 CheckSimpleTerminationCriteria(params_.termination_criteria(), full_stats,
2792 interrupt_solve);
2793 if (TerminationReasonIsWorkLimit(
2794 dual_result.solve_log.termination_reason())) {
2795 // Have we also reached the overall work limit? If so, consider falling out
2796 // of the "if" test and returning the polishing solution anyway.
2797 if (simple_termination_reason.has_value() &&
2798 DoFeasibilityPolishingAfterLimitsReached(
2799 params_, simple_termination_reason->reason)) {
2800 preprocess_solver_->ComputeConvergenceAndInfeasibilityFromWorkingSolution(
2801 params_, primal_result.primal_solution, dual_result.dual_solution,
2802 POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
2803 full_stats.add_convergence_information(), nullptr);
2804 return ConstructSolverResult(
2805 std::move(primal_result.primal_solution),
2806 std::move(dual_result.dual_solution), full_stats,
2807 simple_termination_reason->reason,
2808 POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION, solve_log);
2809 } else {
2810 return std::nullopt;
2811 }
2812 } else if (dual_result.solve_log.termination_reason() !=
2813 TERMINATION_REASON_OPTIMAL) {
2814 // Note: The comment in the corresponding location when checking the
2815 // termination reason for primal feasibility polishing applies here
2816 // too.
2817 SOLVER_LOG(&preprocess_solver_->Logger(),
2818 "WARNING: Dual feasibility polishing terminated with error ",
2819 dual_result.solve_log.termination_reason());
2820 return std::nullopt;
2821 }
2822
2823 preprocess_solver_->ComputeConvergenceAndInfeasibilityFromWorkingSolution(
2824 params_, primal_result.primal_solution, dual_result.dual_solution,
2825 POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
2826 full_stats.add_convergence_information(), nullptr);
2827 if (params_.verbosity_level() >= 2) {
2828 SOLVER_LOG(&preprocess_solver_->Logger(),
2829 "solution stats for polished solution:");
2830 LogIterationStatsHeader(params_.verbosity_level(),
2831 /*use_feasibility_polishing=*/true,
2832 preprocess_solver_->Logger());
2833 LogIterationStats(params_.verbosity_level(),
2834 /*use_feasibility_polishing=*/true,
2835 IterationType::kFeasibilityPolishingTermination,
2836 full_stats, params_.termination_criteria(),
2837 preprocess_solver_->OriginalBoundNorms(),
2838 POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
2839 preprocess_solver_->Logger());
2840 }
2841 std::optional<TerminationReasonAndPointType> earned_termination =
2842 CheckIterateTerminationCriteria(params_.termination_criteria(),
2843 full_stats,
2844 preprocess_solver_->OriginalBoundNorms(),
2845 /*force_numerical_termination=*/false);
2846 if (earned_termination.has_value() ||
2847 (simple_termination_reason.has_value() &&
2848 DoFeasibilityPolishingAfterLimitsReached(
2849 params_, simple_termination_reason->reason))) {
2850 return ConstructSolverResult(
2851 std::move(primal_result.primal_solution),
2852 std::move(dual_result.dual_solution), full_stats,
2853 earned_termination.has_value() ? earned_termination->reason
2854 : simple_termination_reason->reason,
2855 POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION, solve_log);
2856 }
2857 // Note: A typical termination check would now call
2858 // `CheckSimpleTerminationCriteria`. However, there is no obvious iterate to
2859 // use for a `SolverResult`. Instead, allow the main iteration loop to notice
2860 // if the termination criteria checking has caused us to meet the simple
2861 // termination criteria. This would be a rare case anyway, since dual
2862 // feasibility polishing didn't terminate because of a work limit, and
2863 // `full_stats` is created shortly after `TryDualPolishing` returned.
2864 return std::nullopt;
2865}
2866
2867TerminationCriteria ReduceWorkLimitsByPreviousWork(
2868 TerminationCriteria criteria, const int iteration_limit,
2869 const IterationStats& previous_work,
2870 bool apply_feasibility_polishing_after_limits_reached) {
2871 if (apply_feasibility_polishing_after_limits_reached) {
2872 criteria.set_iteration_limit(iteration_limit);
2873 criteria.set_kkt_matrix_pass_limit(std::numeric_limits<double>::infinity());
2874 criteria.set_time_sec_limit(std::numeric_limits<double>::infinity());
2875 } else {
2876 criteria.set_iteration_limit(std::max(
2877 0, std::min(iteration_limit, criteria.iteration_limit() -
2878 previous_work.iteration_number())));
2879 criteria.set_kkt_matrix_pass_limit(
2880 std::max(0.0, criteria.kkt_matrix_pass_limit() -
2881 previous_work.cumulative_kkt_matrix_passes()));
2882 criteria.set_time_sec_limit(std::max(
2883 0.0, criteria.time_sec_limit() - previous_work.cumulative_time_sec()));
2884 }
2885 return criteria;
2886}
2887
2888SolverResult Solver::TryPrimalPolishing(
2889 VectorXd starting_primal_solution, const int iteration_limit,
2890 const std::atomic<bool>* interrupt_solve, SolveLog& solve_log) {
2891 PrimalDualHybridGradientParams primal_feasibility_params = params_;
2892 *primal_feasibility_params.mutable_termination_criteria() =
2893 ReduceWorkLimitsByPreviousWork(
2894 params_.termination_criteria(), iteration_limit,
2895 TotalWorkSoFar(solve_log),
2896 params_.apply_feasibility_polishing_after_limits_reached());
2897 if (params_.apply_feasibility_polishing_if_solver_is_interrupted()) {
2898 interrupt_solve = nullptr;
2899 }
2900
2901 // This will save the original objective after the swap.
2902 VectorXd objective;
2903 SetZero(ShardedWorkingQp().PrimalSharder(), objective);
2904 preprocess_solver_->SwapObjectiveVector(objective);
2905
2906 TerminationCriteria::DetailedOptimalityCriteria criteria =
2907 EffectiveOptimalityCriteria(params_.termination_criteria());
2908 const double kInfinity = std::numeric_limits<double>::infinity();
2909 criteria.set_eps_optimal_dual_residual_absolute(kInfinity);
2910 criteria.set_eps_optimal_dual_residual_relative(kInfinity);
2911 criteria.set_eps_optimal_objective_gap_absolute(kInfinity);
2912 criteria.set_eps_optimal_objective_gap_relative(kInfinity);
2913 *primal_feasibility_params.mutable_termination_criteria()
2914 ->mutable_detailed_optimality_criteria() = criteria;
2915 // TODO(user): Evaluate disabling primal weight updates for this primal
2916 // feasibility problem.
2917 VectorXd primal_feasibility_starting_dual;
2918 SetZero(ShardedWorkingQp().DualSharder(), primal_feasibility_starting_dual);
2919 Solver primal_solver(primal_feasibility_params,
2920 std::move(starting_primal_solution),
2921 std::move(primal_feasibility_starting_dual), step_size_,
2922 primal_weight_, preprocess_solver_);
2923 SolveLog primal_solve_log;
2924 // Stop `timer_`, since the time in `primal_solver.Solve()` will be recorded
2925 // by its timer.
2926 timer_.Stop();
2927 SolverResult primal_result = primal_solver.Solve(
2928 IterationType::kPrimalFeasibility, interrupt_solve, primal_solve_log);
2929 timer_.Start();
2930 // Restore the original objective.
2931 preprocess_solver_->SwapObjectiveVector(objective);
2932
2933 *solve_log.add_feasibility_polishing_details() =
2934 BuildFeasibilityPolishingDetails(
2935 POLISHING_PHASE_TYPE_PRIMAL_FEASIBILITY, iterations_completed_,
2936 primal_feasibility_params, primal_result.solve_log);
2937 return primal_result;
2938}
2939
2940VectorXd MapFiniteValuesToZero(const Sharder& sharder, const VectorXd& input) {
2941 VectorXd output(input.size());
2942 const auto make_finite_values_zero = [](const double x) {
2943 return std::isfinite(x) ? 0.0 : x;
2944 };
2945 sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
2946 shard(output) = shard(input).unaryExpr(make_finite_values_zero);
2947 });
2948 return output;
2949}
2950
2951SolverResult Solver::TryDualPolishing(VectorXd starting_dual_solution,
2952 const int iteration_limit,
2953 const std::atomic<bool>* interrupt_solve,
2954 SolveLog& solve_log) {
2955 PrimalDualHybridGradientParams dual_feasibility_params = params_;
2956 *dual_feasibility_params.mutable_termination_criteria() =
2957 ReduceWorkLimitsByPreviousWork(
2958 params_.termination_criteria(), iteration_limit,
2959 TotalWorkSoFar(solve_log),
2960 params_.apply_feasibility_polishing_after_limits_reached());
2961 if (params_.apply_feasibility_polishing_if_solver_is_interrupted()) {
2962 interrupt_solve = nullptr;
2963 }
2964
2965 // These will initially contain the homogenous variable and constraint
2966 // bounds, but will contain the original variable and constraint bounds
2967 // after the swap.
2968 VectorXd constraint_lower_bounds = MapFiniteValuesToZero(
2969 ShardedWorkingQp().DualSharder(), WorkingQp().constraint_lower_bounds);
2970 VectorXd constraint_upper_bounds = MapFiniteValuesToZero(
2971 ShardedWorkingQp().DualSharder(), WorkingQp().constraint_upper_bounds);
2972 VectorXd variable_lower_bounds = MapFiniteValuesToZero(
2973 ShardedWorkingQp().PrimalSharder(), WorkingQp().variable_lower_bounds);
2974 VectorXd variable_upper_bounds = MapFiniteValuesToZero(
2975 ShardedWorkingQp().PrimalSharder(), WorkingQp().variable_upper_bounds);
2976 preprocess_solver_->SwapConstraintBounds(constraint_lower_bounds,
2977 constraint_upper_bounds);
2978 preprocess_solver_->SwapVariableBounds(variable_lower_bounds,
2979 variable_upper_bounds);
2980
2981 TerminationCriteria::DetailedOptimalityCriteria criteria =
2982 EffectiveOptimalityCriteria(params_.termination_criteria());
2983 const double kInfinity = std::numeric_limits<double>::infinity();
2984 criteria.set_eps_optimal_primal_residual_absolute(kInfinity);
2985 criteria.set_eps_optimal_primal_residual_relative(kInfinity);
2986 criteria.set_eps_optimal_objective_gap_absolute(kInfinity);
2987 criteria.set_eps_optimal_objective_gap_relative(kInfinity);
2988 *dual_feasibility_params.mutable_termination_criteria()
2989 ->mutable_detailed_optimality_criteria() = criteria;
2990 // TODO(user): Evaluate disabling primal weight updates for this dual
2991 // feasibility problem.
2992 VectorXd dual_feasibility_starting_primal;
2993 SetZero(ShardedWorkingQp().PrimalSharder(), dual_feasibility_starting_primal);
2994 Solver dual_solver(dual_feasibility_params,
2995 std::move(dual_feasibility_starting_primal),
2996 std::move(starting_dual_solution), step_size_,
2997 primal_weight_, preprocess_solver_);
2998 SolveLog dual_solve_log;
2999 // Stop `timer_`, since the time in `dual_solver.Solve()` will be recorded
3000 // by its timer.
3001 timer_.Stop();
3002 SolverResult dual_result = dual_solver.Solve(IterationType::kDualFeasibility,
3003 interrupt_solve, dual_solve_log);
3004 timer_.Start();
3005 // Restore the original bounds.
3006 preprocess_solver_->SwapConstraintBounds(constraint_lower_bounds,
3007 constraint_upper_bounds);
3008 preprocess_solver_->SwapVariableBounds(variable_lower_bounds,
3009 variable_upper_bounds);
3010 *solve_log.add_feasibility_polishing_details() =
3011 BuildFeasibilityPolishingDetails(
3012 POLISHING_PHASE_TYPE_DUAL_FEASIBILITY, iterations_completed_,
3013 dual_feasibility_params, dual_result.solve_log);
3014 return dual_result;
3015}
3016
3017SolverResult Solver::Solve(const IterationType iteration_type,
3018 const std::atomic<bool>* interrupt_solve,
3019 SolveLog solve_log) {
3020 preprocessing_time_sec_ = solve_log.preprocessing_time_sec();
3021 timer_.Start();
3022 last_primal_start_point_ =
3023 CloneVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder());
3024 last_dual_start_point_ =
3025 CloneVector(current_dual_solution_, ShardedWorkingQp().DualSharder());
3026 // Note: Any cached values computed here also need to be recomputed after a
3027 // restart.
3028
3029 ratio_last_two_step_sizes_ = 1;
3030 SetCurrentPrimalAndDualProducts();
3031
3032 // This is set to true if we can't proceed any more because of numerical
3033 // issues. We may or may not have found the optimal solution.
3034 bool force_numerical_termination = false;
3035
3036 int next_feasibility_polishing_iteration = 100;
3037
3038 num_rejected_steps_ = 0;
3039
3040 IterationStats work_from_feasibility_polishing =
3041 WorkFromFeasibilityPolishing(solve_log);
3042 for (iterations_completed_ = 0;; ++iterations_completed_) {
3043 // This code performs the logic of the major iterations and termination
3044 // checks. It may modify the current solution and primal weight (e.g., when
3045 // performing a restart).
3046 const std::optional<SolverResult> maybe_result =
3047 MajorIterationAndTerminationCheck(
3048 iteration_type, force_numerical_termination, interrupt_solve,
3049 work_from_feasibility_polishing, solve_log);
3050 if (maybe_result.has_value()) {
3051 return maybe_result.value();
3052 }
3053
3054 if (params_.use_feasibility_polishing() &&
3055 iteration_type == IterationType::kNormal &&
3056 iterations_completed_ >= next_feasibility_polishing_iteration) {
3057 const std::optional<SolverResult> feasibility_result =
3058 TryFeasibilityPolishing(
3059 iterations_completed_ / kFeasibilityIterationFraction,
3060 interrupt_solve, solve_log);
3061 if (feasibility_result.has_value()) {
3062 return *feasibility_result;
3063 }
3064 next_feasibility_polishing_iteration *= 2;
3065 // Update work to include new feasibility phases.
3066 work_from_feasibility_polishing = WorkFromFeasibilityPolishing(solve_log);
3067 }
3068
3069 // TODO(user): If we use a step rule that could reject many steps in a
3070 // row, we should add a termination check within this loop also. For the
3071 // Malitsky and Pock rule, we perform a termination check and declare
3072 // NUMERICAL_ERROR whenever we hit 60 inner iterations.
3073 InnerStepOutcome outcome;
3074 switch (params_.linesearch_rule()) {
3075 case PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE:
3076 outcome = TakeMalitskyPockStep();
3077 break;
3078 case PrimalDualHybridGradientParams::ADAPTIVE_LINESEARCH_RULE:
3079 outcome = TakeAdaptiveStep();
3080 break;
3081 case PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE:
3082 outcome = TakeConstantSizeStep();
3083 break;
3084 default:
3085 LOG(FATAL) << "Unrecognized linesearch rule "
3086 << params_.linesearch_rule();
3087 }
3088 if (outcome == InnerStepOutcome::kForceNumericalTermination) {
3089 force_numerical_termination = true;
3090 }
3091 } // loop over iterations
3092}
3093
3094} // namespace
3095
3098 const std::atomic<bool>* interrupt_solve,
3099 std::function<void(const std::string&)> message_callback,
3100 IterationStatsCallback iteration_stats_callback) {
3101 return PrimalDualHybridGradient(std::move(qp), params, std::nullopt,
3102 interrupt_solve, std::move(message_callback),
3103 std::move(iteration_stats_callback));
3104}
3105
3106// See comment at top of file.
3109 std::optional<PrimalAndDualSolution> initial_solution,
3110 const std::atomic<bool>* interrupt_solve,
3111 std::function<void(const std::string&)> message_callback,
3112 IterationStatsCallback iteration_stats_callback) {
3113 SolverLogger logger;
3114 logger.EnableLogging(true);
3115 if (message_callback) {
3116 logger.AddInfoLoggingCallback(std::move(message_callback));
3117 } else {
3118 logger.SetLogToStdOut(true);
3119 }
3120 const absl::Status params_status =
3122 if (!params_status.ok()) {
3123 return ErrorSolverResult(TERMINATION_REASON_INVALID_PARAMETER,
3124 params_status.ToString(), logger);
3125 }
3126 if (!qp.constraint_matrix.isCompressed()) {
3127 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
3128 "constraint_matrix must be in compressed format. "
3129 "Call constraint_matrix.makeCompressed()",
3130 logger);
3131 }
3132 const absl::Status dimensions_status = ValidateQuadraticProgramDimensions(qp);
3133 if (!dimensions_status.ok()) {
3134 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
3135 dimensions_status.ToString(), logger);
3136 }
3137 if (qp.objective_scaling_factor == 0) {
3138 return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
3139 "The objective scaling factor cannot be zero.",
3140 logger);
3141 }
3142 if (params.use_feasibility_polishing() && !IsLinearProgram(qp)) {
3143 return ErrorSolverResult(
3145 "use_feasibility_polishing is only implemented for linear programs.",
3146 logger);
3147 }
3148 PreprocessSolver solver(std::move(qp), params, &logger);
3149 return solver.PreprocessAndSolve(params, std::move(initial_solution),
3150 interrupt_solve,
3151 std::move(iteration_stats_callback));
3152}
3153
3154namespace internal {
3155
3158 glop::ProblemSolution glop_solution(
3159 glop::RowIndex(solution.dual_solution.size()),
3160 glop::ColIndex(solution.primal_solution.size()));
3161 // This doesn't matter much as glop's preprocessor doesn't use this much.
3162 // We pick IMPRECISE since we are often calling this code early in the solve.
3163 glop_solution.status = glop::ProblemStatus::IMPRECISE;
3164 for (glop::RowIndex i{0}; i.value() < solution.dual_solution.size(); ++i) {
3165 if (qp.constraint_lower_bounds[i.value()] ==
3166 qp.constraint_upper_bounds[i.value()]) {
3167 glop_solution.constraint_statuses[i] =
3169 } else if (solution.dual_solution[i.value()] > 0) {
3170 glop_solution.constraint_statuses[i] =
3172 } else if (solution.dual_solution[i.value()] < 0) {
3173 glop_solution.constraint_statuses[i] =
3175 } else {
3177 }
3178 }
3179
3180 for (glop::ColIndex i{0}; i.value() < solution.primal_solution.size(); ++i) {
3181 const bool at_lb = solution.primal_solution[i.value()] <=
3182 qp.variable_lower_bounds[i.value()];
3183 const bool at_ub = solution.primal_solution[i.value()] >=
3184 qp.variable_upper_bounds[i.value()];
3185 // Note that `ShardedWeightedAverage` is designed so that variables at their
3186 // bounds will be exactly at their bounds even with floating-point roundoff.
3187 if (at_lb) {
3188 if (at_ub) {
3190 } else {
3191 glop_solution.variable_statuses[i] =
3193 }
3194 } else {
3195 if (at_ub) {
3196 glop_solution.variable_statuses[i] =
3198 } else {
3200 }
3201 }
3202 }
3203 return glop_solution;
3204}
3205
3206} // namespace internal
3207
3208} // namespace operations_research::pdlp
double Get() const
Definition timer.h:44
void Start()
Definition timer.h:30
void AddInfoLoggingCallback(std::function< void(const std::string &message)> callback)
Definition logging.cc:45
constexpr Fractional kInfinity
Definition lp_types.h:87
Fractional SquaredNorm(const SparseColumn &v)
Definition lp_utils.cc:36
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition solve.cc:62
glop::ProblemSolution ComputeStatuses(const QuadraticProgram &qp, const PrimalAndDualSolution &solution)
std::optional< TerminationReasonAndPointType > CheckSimpleTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const std::atomic< bool > *interrupt_solve)
absl::StatusOr< MPModelProto > QpToMpModelProto(const QuadraticProgram &qp)
void SetZero(const Sharder &sharder, VectorXd &dest)
Definition sharder.cc:175
InfeasibilityInformation ComputeInfeasibilityInformation(const PrimalDualHybridGradientParams &params, const ShardedQuadraticProgram &scaled_sharded_qp, const Eigen::VectorXd &col_scaling_vec, const Eigen::VectorXd &row_scaling_vec, const Eigen::VectorXd &scaled_primal_ray, const Eigen::VectorXd &scaled_dual_ray, const Eigen::VectorXd &primal_solution_for_residual_tests, PointType candidate_type)
QuadraticProgramStats ComputeStats(const ShardedQuadraticProgram &qp)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram &qp)
absl::Status ValidatePrimalDualHybridGradientParams(const PrimalDualHybridGradientParams &params)
void SetRandomProjections(const ShardedQuadraticProgram &sharded_qp, const Eigen::VectorXd &primal_solution, const Eigen::VectorXd &dual_solution, const std::vector< int > &random_projection_seeds, PointMetadata &metadata)
double SquaredDistance(const VectorXd &vector1, const VectorXd &vector2, const Sharder &sharder)
Definition sharder.cc:254
double LInfNorm(const VectorXd &vector, const Sharder &sharder)
Definition sharder.cc:232
TerminationCriteria::DetailedOptimalityCriteria EffectiveOptimalityCriteria(const TerminationCriteria &termination_criteria)
double Distance(const VectorXd &vector1, const VectorXd &vector2, const Sharder &sharder)
Definition sharder.cc:261
VectorXd ReducedCosts(const PrimalDualHybridGradientParams &params, const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, bool use_zero_primal_objective)
VectorXd TransposedMatrixVectorProduct(const Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > &matrix, const VectorXd &vector, const Sharder &sharder)
Definition sharder.cc:160
const ::std::string & PointType_Name(T value)
QuadraticProgramBoundNorms BoundNormsFromProblemStats(const QuadraticProgramStats &stats)
double EpsilonRatio(const double epsilon_absolute, const double epsilon_relative)
SingularValueAndIterations EstimateMaximumSingularValueOfConstraintMatrix(const ShardedQuadraticProgram &sharded_qp, const std::optional< VectorXd > &primal_solution, const std::optional< VectorXd > &dual_solution, const double desired_relative_error, const double failure_probability, std::mt19937 &mt_generator)
void ProjectToPrimalVariableBounds(const ShardedQuadraticProgram &sharded_qp, VectorXd &primal, const bool use_feasibility_bounds)
absl::StatusOr< QuadraticProgram > QpFromMpModelProto(const MPModelProto &proto, bool relax_integer_variables, bool include_names)
const ::std::string & TerminationReason_Name(T value)
bool IsLinearProgram(const QuadraticProgram &qp)
void ProjectToDualVariableBounds(const ShardedQuadraticProgram &sharded_qp, VectorXd &dual)
std::optional< TerminationReasonAndPointType > CheckIterateTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const QuadraticProgramBoundNorms &bound_norms, const bool force_numerical_termination)
void CoefficientWiseProductInPlace(const VectorXd &scale, const Sharder &sharder, VectorXd &dest)
Definition sharder.cc:213
void CoefficientWiseQuotientInPlace(const VectorXd &scale, const Sharder &sharder, VectorXd &dest)
Definition sharder.cc:220
VectorXd ZeroVector(const Sharder &sharder)
Definition sharder.cc:181
VectorXd CloneVector(const VectorXd &vec, const Sharder &sharder)
Definition sharder.cc:207
SolverResult PrimalDualHybridGradient(QuadraticProgram qp, const PrimalDualHybridGradientParams &params, const std::atomic< bool > *interrupt_solve, std::function< void(const std::string &)> message_callback, IterationStatsCallback iteration_stats_callback)
VectorXd OnesVector(const Sharder &sharder)
Definition sharder.cc:187
bool HasValidBounds(const ShardedQuadraticProgram &sharded_qp)
ScalingVectors ApplyRescaling(const RescalingOptions &rescaling_options, ShardedQuadraticProgram &sharded_qp)
bool ObjectiveGapMet(const TerminationCriteria::DetailedOptimalityCriteria &optimality_criteria, const ConvergenceInformation &stats)
ConvergenceInformation ComputeConvergenceInformation(const PrimalDualHybridGradientParams &params, const ShardedQuadraticProgram &scaled_sharded_qp, const Eigen::VectorXd &col_scaling_vec, const Eigen::VectorXd &row_scaling_vec, const Eigen::VectorXd &scaled_primal_solution, const Eigen::VectorXd &scaled_dual_solution, const double componentwise_primal_residual_offset, const double componentwise_dual_residual_offset, PointType candidate_type)
LocalizedLagrangianBounds ComputeLocalizedLagrangianBounds(const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, const PrimalDualNorm primal_dual_norm, const double primal_weight, const double radius, const VectorXd *primal_product, const VectorXd *dual_product, const bool use_diagonal_qp_trust_region_solver, const double diagonal_qp_trust_region_solver_tolerance)
double Norm(const VectorXd &vector, const Sharder &sharder)
Definition sharder.cc:250
RelativeConvergenceInformation ComputeRelativeResiduals(const TerminationCriteria::DetailedOptimalityCriteria &optimality_criteria, const ConvergenceInformation &stats, const QuadraticProgramBoundNorms &bound_norms)
void AssignVector(const VectorXd &vec, const Sharder &sharder, VectorXd &dest)
Definition sharder.cc:201
double BoundGap(const LocalizedLagrangianBounds &bounds)
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
STL namespace.
static int input(yyscan_t yyscanner)
ConstraintStatusColumn constraint_statuses
Definition lp_data.h:699
Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > constraint_matrix
#define SOLVER_LOG(logger,...)
Definition logging.h:114