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