OR-Tools  9.6
primal_dual_hybrid_gradient.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
15 
16 #include <algorithm>
17 #include <atomic>
18 #include <cmath>
19 #include <cstdint>
20 #include <functional>
21 #include <limits>
22 #include <optional>
23 #include <random>
24 #include <string>
25 #include <utility>
26 #include <vector>
27 
28 #include "Eigen/Core"
29 #include "Eigen/SparseCore"
30 #include "absl/log/check.h"
31 #include "absl/status/status.h"
32 #include "absl/status/statusor.h"
33 #include "absl/strings/str_cat.h"
34 #include "absl/strings/str_format.h"
35 #include "absl/strings/string_view.h"
36 #include "ortools/base/logging.h"
37 #include "ortools/base/mathutil.h"
38 #include "ortools/base/timer.h"
39 #include "ortools/glop/parameters.pb.h"
41 #include "ortools/linear_solver/linear_solver.pb.h"
49 #include "ortools/pdlp/sharder.h"
50 #include "ortools/pdlp/solve_log.pb.h"
51 #include "ortools/pdlp/solvers.pb.h"
55 
56 namespace operations_research::pdlp {
57 
58 namespace {
59 
60 using ::Eigen::VectorXd;
61 
62 using IterationStatsCallback =
63  std::function<void(const IterationCallbackInfo&)>;
64 
65 // Computes a `num_threads` that is capped by the problem size and `num_shards`,
66 // if specified, to avoid creating unusable threads.
67 int NumThreads(const int num_threads, const int num_shards,
68  const QuadraticProgram& qp) {
69  int capped_num_threads = num_threads;
70  if (num_shards > 0) {
71  capped_num_threads = std::min(capped_num_threads, num_shards);
72  }
73  const int64_t problem_limit = std::max(qp.variable_lower_bounds.size(),
74  qp.constraint_lower_bounds.size());
75  capped_num_threads =
76  static_cast<int>(std::min(int64_t{capped_num_threads}, problem_limit));
77  capped_num_threads = std::max(capped_num_threads, 1);
78  if (capped_num_threads != num_threads) {
79  LOG(WARNING) << "Reducing num_threads from " << num_threads << " to "
80  << capped_num_threads
81  << " because additional threads would be useless.";
82  }
83  return capped_num_threads;
84 }
85 
86 // If `num_shards` is positive, returns it. Otherwise returns a reasonable
87 // number of shards to use with `ShardedQuadraticProgram` for the given
88 // `num_threads`.
89 int NumShards(const int num_threads, const int num_shards) {
90  if (num_shards > 0) return num_shards;
91  return num_threads == 1 ? 1 : 4 * num_threads;
92 }
93 
94 std::string ToString(const ConvergenceInformation& convergence_information,
95  const RelativeConvergenceInformation& relative_information,
96  const OptimalityNorm residual_norm) {
97  constexpr absl::string_view kFormatStr =
98  "%#12.6g %#12.6g %#12.6g | %#12.6g %#12.6g %#12.6g | %#12.6g %#12.6g | "
99  "%#12.6g %#12.6g";
100  switch (residual_norm) {
101  case OPTIMALITY_NORM_L_INF:
102  return absl::StrFormat(
103  kFormatStr, relative_information.relative_l_inf_primal_residual,
104  relative_information.relative_l_inf_dual_residual,
105  relative_information.relative_optimality_gap,
106  convergence_information.l_inf_primal_residual(),
107  convergence_information.l_inf_dual_residual(),
108  convergence_information.primal_objective() -
109  convergence_information.dual_objective(),
110  convergence_information.primal_objective(),
111  convergence_information.dual_objective(),
112  convergence_information.l2_primal_variable(),
113  convergence_information.l2_dual_variable());
114  case OPTIMALITY_NORM_L2:
115  return absl::StrFormat(kFormatStr,
116  relative_information.relative_l2_primal_residual,
117  relative_information.relative_l2_dual_residual,
118  relative_information.relative_optimality_gap,
119  convergence_information.l2_primal_residual(),
120  convergence_information.l2_dual_residual(),
121  convergence_information.primal_objective() -
122  convergence_information.dual_objective(),
123  convergence_information.primal_objective(),
124  convergence_information.dual_objective(),
125  convergence_information.l2_primal_variable(),
126  convergence_information.l2_dual_variable());
127  case OPTIMALITY_NORM_L_INF_COMPONENTWISE:
128  return absl::StrFormat(
129  kFormatStr,
130  convergence_information.l_inf_componentwise_primal_residual(),
131  convergence_information.l_inf_componentwise_dual_residual(),
132  relative_information.relative_optimality_gap,
133  convergence_information.l_inf_primal_residual(),
134  convergence_information.l_inf_dual_residual(),
135  convergence_information.primal_objective() -
136  convergence_information.dual_objective(),
137  convergence_information.primal_objective(),
138  convergence_information.dual_objective(),
139  convergence_information.l2_primal_variable(),
140  convergence_information.l2_dual_variable());
141  case OPTIMALITY_NORM_UNSPECIFIED:
142  LOG(FATAL) << "Invalid residual norm.";
143  }
144 }
145 
146 std::string ToShortString(
147  const ConvergenceInformation& convergence_information,
148  const RelativeConvergenceInformation& relative_information,
149  const OptimalityNorm residual_norm) {
150  constexpr absl::string_view kFormatStr =
151  "%#10.4g %#10.4g %#10.4g | %#10.4g %#10.4g";
152  switch (residual_norm) {
153  case OPTIMALITY_NORM_L_INF:
154  return absl::StrFormat(
155  kFormatStr, relative_information.relative_l_inf_primal_residual,
156  relative_information.relative_l_inf_dual_residual,
157  relative_information.relative_optimality_gap,
158  convergence_information.primal_objective(),
159  convergence_information.dual_objective());
160  case OPTIMALITY_NORM_L2:
161  return absl::StrFormat(kFormatStr,
162  relative_information.relative_l2_primal_residual,
163  relative_information.relative_l2_dual_residual,
164  relative_information.relative_optimality_gap,
165  convergence_information.primal_objective(),
166  convergence_information.dual_objective());
167  case OPTIMALITY_NORM_L_INF_COMPONENTWISE:
168  return absl::StrFormat(
169  kFormatStr,
170  convergence_information.l_inf_componentwise_primal_residual(),
171  convergence_information.l_inf_componentwise_dual_residual(),
172  relative_information.relative_optimality_gap,
173  convergence_information.primal_objective(),
174  convergence_information.dual_objective());
175  case OPTIMALITY_NORM_UNSPECIFIED:
176  LOG(FATAL) << "Invalid residual norm.";
177  }
178 }
179 
180 // Returns a string describing `iter_stats`, based on the
181 // `iter_stats.convergence_information` entry with
182 // `.candidate_type()==preferred_candidate` if one exists, otherwise based on
183 // the first value, if any. `termination_criteria.optimality_norm` determines
184 // which residual norms from `iter_stats.convergence_information` are used.
185 std::string ToString(const IterationStats& iter_stats,
186  const TerminationCriteria& termination_criteria,
187  const QuadraticProgramBoundNorms& bound_norms,
188  PointType preferred_candidate) {
189  std::string iteration_string =
190  absl::StrFormat("%6d %8.1f %6.1f", iter_stats.iteration_number(),
191  iter_stats.cumulative_kkt_matrix_passes(),
192  iter_stats.cumulative_time_sec());
193  auto convergence_information =
194  GetConvergenceInformation(iter_stats, preferred_candidate);
195  if (!convergence_information.has_value() &&
196  iter_stats.convergence_information_size() > 0) {
197  convergence_information = iter_stats.convergence_information(0);
198  }
199  if (convergence_information.has_value()) {
200  const RelativeConvergenceInformation relative_information =
202  EffectiveOptimalityCriteria(termination_criteria),
203  *convergence_information, bound_norms);
204  return absl::StrCat(iteration_string, " | ",
205  ToString(*convergence_information, relative_information,
206  termination_criteria.optimality_norm()));
207  }
208  return iteration_string;
209 }
210 
211 std::string ToShortString(const IterationStats& iter_stats,
212  const TerminationCriteria& termination_criteria,
213  const QuadraticProgramBoundNorms& bound_norms,
214  PointType preferred_candidate) {
215  std::string iteration_string =
216  absl::StrFormat("%6d %6.1f", iter_stats.iteration_number(),
217  iter_stats.cumulative_time_sec());
218  auto convergence_information =
219  GetConvergenceInformation(iter_stats, preferred_candidate);
220  if (!convergence_information.has_value() &&
221  iter_stats.convergence_information_size() > 0) {
222  convergence_information = iter_stats.convergence_information(0);
223  }
224  if (convergence_information.has_value()) {
225  const RelativeConvergenceInformation relative_information =
227  EffectiveOptimalityCriteria(termination_criteria),
228  *convergence_information, bound_norms);
229  return absl::StrCat(
230  iteration_string, " | ",
231  ToShortString(*convergence_information, relative_information,
232  termination_criteria.optimality_norm()));
233  }
234  return iteration_string;
235 }
236 
237 // Returns a label string corresponding to the format of `ToString()`.
238 std::string ConvergenceInformationLabelString() {
239  return absl::StrFormat(
240  "%12s %12s %12s | %12s %12s %12s | %12s %12s | %12s %12s", "rel_prim_res",
241  "rel_dual_res", "rel_gap", "prim_resid", "dual_resid", "obj_gap",
242  "prim_obj", "dual_obj", "prim_var_l2", "dual_var_l2");
243 }
244 
245 std::string ConvergenceInformationLabelShortString() {
246  return absl::StrFormat("%10s %10s %10s | %10s %10s", "rel_p_res", "rel_d_res",
247  "rel_gap", "prim_obj", "dual_obj");
248 }
249 
250 std::string IterationStatsLabelString() {
251  return absl::StrCat(
252  absl::StrFormat("%6s %8s %6s", "iter#", "kkt_pass", "time"), " | ",
253  ConvergenceInformationLabelString());
254 }
255 
256 std::string IterationStatsLabelShortString() {
257  return absl::StrCat(absl::StrFormat("%6s %6s", "iter#", "time"), " | ",
258  ConvergenceInformationLabelShortString());
259 }
260 
261 enum class InnerStepOutcome {
262  kSuccessful,
263  kForceNumericalTermination,
264 };
265 
266 // Makes the closing changes to `solve_log` and builds a `SolverResult`.
267 // NOTE: `primal_solution`, `dual_solution`, and `solve_log` are passed by
268 // value. To avoid unnecessary copying, move these objects (i.e. use
269 // `std::move()`).
270 SolverResult ConstructSolverResult(VectorXd primal_solution,
271  VectorXd dual_solution,
272  const IterationStats& stats,
273  TerminationReason termination_reason,
274  PointType output_type, SolveLog solve_log) {
275  solve_log.set_iteration_count(stats.iteration_number());
276  solve_log.set_termination_reason(termination_reason);
277  solve_log.set_solution_type(output_type);
278  solve_log.set_solve_time_sec(stats.cumulative_time_sec());
279  *solve_log.mutable_solution_stats() = stats;
280  return SolverResult{.primal_solution = std::move(primal_solution),
281  .dual_solution = std::move(dual_solution),
282  .solve_log = std::move(solve_log)};
283 }
284 
285 class PreprocessSolver {
286  public:
287  // Assumes that `qp` and `params` are valid.
288  // Note that the `qp` is intentionally passed by value.
289  // NOTE: Many `PreprocessSolver` methods accept a `params` argument. This is
290  // passed as an argument instead of stored as a member variable to support
291  // using different `params` in different contexts with the same
292  // `PreprocessSolver` object.
293  explicit PreprocessSolver(QuadraticProgram qp,
294  const PrimalDualHybridGradientParams& params);
295 
296  // Not copyable or movable (because `glop::MainLpPreprocessor` isn't).
297  PreprocessSolver(const PreprocessSolver&) = delete;
298  PreprocessSolver& operator=(const PreprocessSolver&) = delete;
299  PreprocessSolver(PreprocessSolver&&) = delete;
300  PreprocessSolver& operator=(PreprocessSolver&&) = delete;
301 
302  // Zero is used if `initial_solution` is nullopt. If `interrupt_solve` is not
303  // nullptr, then the solver will periodically check if
304  // `interrupt_solve->load()` is true, in which case the solve will terminate
305  // with `TERMINATION_REASON_INTERRUPTED_BY_USER`. Ownership is not
306  // transferred. If `iteration_stats_callback` is not nullptr, then at each
307  // termination step (when iteration stats are logged),
308  // `iteration_stats_callback` will also be called with those iteration stats.
309  SolverResult PreprocessAndSolve(
310  const PrimalDualHybridGradientParams& params,
311  std::optional<PrimalAndDualSolution> initial_solution,
312  const std::atomic<bool>* interrupt_solve,
313  IterationStatsCallback iteration_stats_callback);
314 
315  // Returns a `TerminationReasonAndPointType` when the termination criteria are
316  // satisfied, otherwise returns nothing. The pointers to working_* can be
317  // nullptr if an iterate of that type is not available. For the iterate types
318  // that are available, uses the primal and dual vectors to compute solution
319  // statistics and adds them to the stats proto.
320  // NOTE: The primal and dual input pairs should be scaled solutions.
321  std::optional<TerminationReasonAndPointType>
322  UpdateIterationStatsAndCheckTermination(
323  const PrimalDualHybridGradientParams& params,
324  bool force_numerical_termination, const VectorXd& working_primal_current,
325  const VectorXd& working_dual_current,
326  const VectorXd* working_primal_average,
327  const VectorXd* working_dual_average,
328  const VectorXd* working_primal_delta, const VectorXd* working_dual_delta,
329  const VectorXd& last_primal_start_point,
330  const VectorXd& last_dual_start_point,
331  const std::atomic<bool>* interrupt_solve, IterationStats& stats) const;
332 
333  // Returns the solution statistics for the primal and dual input pair, which
334  // should be a scaled solution.
335  ConvergenceInformation ComputeConvergenceInformationFromWorkingSolution(
336  const PrimalDualHybridGradientParams& params,
337  const VectorXd& working_primal, const VectorXd& working_dual,
338  PointType candidate_type) const;
339 
340  // Returns a `SolverResult` for the original problem, given a `SolverResult`
341  // from the scaled or preprocessed problem. Also computes the reduced costs.
342  // NOTE: `result` is passed by value. To avoid unnecessary copying, move this
343  // object (i.e. use `std::move()`).
344  SolverResult ConstructOriginalSolverResult(
345  const PrimalDualHybridGradientParams& params, SolverResult result) const;
346 
347  const ShardedQuadraticProgram& ShardedWorkingQp() const {
348  return sharded_qp_;
349  }
350 
351  // Returns elapsed time (including preprocessing) in seconds.
352  double GetElapsedTime() const { return timer_.Get(); }
353 
354  private:
355  struct PresolveInfo {
356  explicit PresolveInfo(ShardedQuadraticProgram original_qp,
357  const PrimalDualHybridGradientParams& params)
358  : preprocessor_parameters(PreprocessorParameters(params)),
360  sharded_original_qp(std::move(original_qp)),
362  OnesVector(sharded_original_qp.PrimalSharder())),
364  OnesVector(sharded_original_qp.DualSharder())) {}
365 
366  glop::GlopParameters preprocessor_parameters;
367  glop::MainLpPreprocessor preprocessor;
368  ShardedQuadraticProgram sharded_original_qp;
371  };
372 
373  // TODO(user): experiment with different preprocessor types.
374  static glop::GlopParameters PreprocessorParameters(
375  const PrimalDualHybridGradientParams& params);
376 
377  // If presolve is enabled, moves `sharded_qp_` to
378  // `presolve_info_.sharded_original_qp` and computes the presolved linear
379  // program and installs it in `sharded_qp_`. Clears `initial_solution` if
380  // presolve is enabled. If presolve solves the problem completely returns the
381  // appropriate `TerminationReason`. Otherwise returns nullopt. If presolve
382  // is disabled or an error occurs modifies nothing and returns nullopt.
383  std::optional<TerminationReason> ApplyPresolveIfEnabled(
384  const PrimalDualHybridGradientParams& params,
385  std::optional<PrimalAndDualSolution>* initial_solution);
386 
387  void ComputeAndApplyRescaling(const PrimalDualHybridGradientParams& params,
388  VectorXd& starting_primal_solution,
389  VectorXd& starting_dual_solution);
390 
391  void LogQuadraticProgramStats(const QuadraticProgramStats& stats);
392 
393  double InitialPrimalWeight(const PrimalDualHybridGradientParams& params,
394  double l2_norm_primal_linear_objective,
395  double l2_norm_constraint_bounds) const;
396 
397  PrimalAndDualSolution RecoverOriginalSolution(
398  PrimalAndDualSolution working_solution) const;
399 
400  // Adds one entry of convergence information and infeasibility information to
401  // `stats` using the input solutions. `primal_solution` and `dual_solution`
402  // are solutions for `sharded_qp`. `col_scaling_vec` and `row_scaling_vec` are
403  // used to implicitly unscale `sharded_qp` when computing the relevant
404  // information.
405  void AddConvergenceAndInfeasibilityInformation(
406  const PrimalDualHybridGradientParams& params,
407  const VectorXd& primal_solution, const VectorXd& dual_solution,
408  const ShardedQuadraticProgram& sharded_qp,
409  const VectorXd& col_scaling_vec, const VectorXd& row_scaling_vec,
410  PointType candidate_type, IterationStats& stats) const;
411 
412  // Adds one entry of `PointMetadata` to `stats` using the input solutions.
413  void AddPointMetadata(const PrimalDualHybridGradientParams& params,
414  const VectorXd& primal_solution,
415  const VectorXd& dual_solution, PointType point_type,
416  const VectorXd& last_primal_start_point,
417  const VectorXd& last_dual_start_point,
418  IterationStats& stats) const;
419 
420  const QuadraticProgram& Qp() const { return sharded_qp_.Qp(); }
421 
422  const int num_threads_;
423  const int num_shards_;
424 
425  // The bound norms of the original problem.
426  QuadraticProgramBoundNorms original_bound_norms_;
427 
428  // This is the QP that PDHG is run on. It is modified by presolve and
429  // rescaling, if those are enabled, and then serves as the
430  // `ShardedWorkingQp()` when calling `Solver::Solve()`. The original problem
431  // is available in `presolve_info_->sharded_original_qp` if
432  // `presolve_info_.has_value()`, and otherwise can be obtained by undoing the
433  // scaling of `sharded_qp_` by `col_scaling_vec_` and `row_scaling_vec_`.
434  ShardedQuadraticProgram sharded_qp_;
435 
436  // Set iff presolve is enabled.
437  std::optional<PresolveInfo> presolve_info_;
438 
439  // The scaling vectors that map the original (or presolved) quadratic program
440  // to the working version. See
441  // `ShardedQuadraticProgram::RescaleQuadraticProgram()` for details.
442  VectorXd col_scaling_vec_;
443  VectorXd row_scaling_vec_;
444 
445  WallTimer timer_;
446  IterationStatsCallback iteration_stats_callback_;
447 };
448 
449 class Solver {
450  public:
451  // `preprocess_solver` should not be nullptr, and the `PreprocessSolver`
452  // object must outlive this `Solver` object. Ownership is not transferred.
453  explicit Solver(const PrimalDualHybridGradientParams& params,
454  VectorXd starting_primal_solution,
455  VectorXd starting_dual_solution, double initial_step_size,
456  double initial_primal_weight,
457  const PreprocessSolver* preprocess_solver);
458 
459  // Not copyable or movable (because there are const members).
460  Solver(const Solver&) = delete;
461  Solver& operator=(const Solver&) = delete;
462  Solver(Solver&&) = delete;
463  Solver& operator=(Solver&&) = delete;
464 
465  const QuadraticProgram& WorkingQp() const { return ShardedWorkingQp().Qp(); }
466 
467  const ShardedQuadraticProgram& ShardedWorkingQp() const {
468  return preprocess_solver_->ShardedWorkingQp();
469  }
470 
471  // Runs PDHG iterations on the instance that has been initialized in `Solver`.
472  // If `interrupt_solve` is not nullptr, then the solver will periodically
473  // check if `interrupt_solve->load()` is true, in which case the solve will
474  // terminate with `TERMINATION_REASON_INTERRUPTED_BY_USER`. Ownership is not
475  // transferred.
476  // `solve_log` should contain initial problem statistics.
477  // On return, `SolveResult.reduced_costs` will be empty, and the solution will
478  // be to the preprocessed/scaled problem.
479  SolverResult Solve(const std::atomic<bool>* interrupt_solve,
480  SolveLog solve_log);
481 
482  private:
483  struct NextSolutionAndDelta {
484  VectorXd value;
485  // `delta` is `value` - current_solution.
486  VectorXd delta;
487  };
488 
489  struct DistanceBasedRestartInfo {
492  };
493 
494  // Movement terms (weighted squared norms of primal and dual deltas) larger
495  // than this cause termination because iterates are diverging, and likely to
496  // cause infinite and NaN values.
497  constexpr static double kDivergentMovement = 1.0e100;
498 
499  NextSolutionAndDelta ComputeNextPrimalSolution(double primal_step_size) const;
500 
501  NextSolutionAndDelta ComputeNextDualSolution(
502  double dual_step_size, double extrapolation_factor,
503  const NextSolutionAndDelta& next_primal) const;
504 
505  double ComputeMovement(const VectorXd& delta_primal,
506  const VectorXd& delta_dual) const;
507 
508  double ComputeNonlinearity(const VectorXd& delta_primal,
509  const VectorXd& next_dual_product) const;
510 
511  // Creates all the simple-to-compute statistics in stats.
512  IterationStats CreateSimpleIterationStats(RestartChoice restart_used) const;
513 
514  RestartChoice ChooseRestartToApply(bool is_major_iteration);
515 
516  VectorXd PrimalAverage() const;
517 
518  VectorXd DualAverage() const;
519 
520  double ComputeNewPrimalWeight() const;
521 
522  // Picks the primal and dual solutions according to `output_type`, and makes
523  // the closing changes to `solve_log`. This function should only be called
524  // once when the solver is finishing its execution.
525  // NOTE: `primal_solution` and `dual_solution` are used as the output except
526  // when `output_type` is `POINT_TYPE_CURRENT_ITERATE` or
527  // `POINT_TYPE_ITERATE_DIFFERENCE`, in which case the values are computed from
528  // `Solver` data.
529  // NOTE: `primal_solution`, `dual_solution`, and `solve_log` are passed by
530  // value. To avoid unnecessary copying, move these objects (i.e. use
531  // `std::move()`).
532  SolverResult PickSolutionAndConstructSolverResult(
533  VectorXd primal_solution, VectorXd dual_solution,
534  const IterationStats& stats, TerminationReason termination_reason,
535  PointType output_type, SolveLog solve_log) const;
536 
537  double DistanceTraveledFromLastStart(const VectorXd& primal_solution,
538  const VectorXd& dual_solution) const;
539 
540  LocalizedLagrangianBounds ComputeLocalizedBoundsAtCurrent() const;
541 
542  LocalizedLagrangianBounds ComputeLocalizedBoundsAtAverage() const;
543 
544  // Applies the given `RestartChoice`. If a restart is chosen, updates the
545  // state of the algorithm accordingly and computes a new primal weight.
546  void ApplyRestartChoice(RestartChoice restart_to_apply);
547 
548  std::optional<SolverResult> MajorIterationAndTerminationCheck(
549  bool force_numerical_termination,
550  const std::atomic<bool>* interrupt_solve, SolveLog& solve_log);
551 
552  bool ShouldDoAdaptiveRestartHeuristic(double candidate_normalized_gap) const;
553 
554  RestartChoice DetermineDistanceBasedRestartChoice() const;
555 
556  void ResetAverageToCurrent();
557 
558  void LogNumericalTermination() const;
559 
560  void LogInnerIterationLimitHit() const;
561 
562  // Takes a step based on the Malitsky and Pock linesearch algorithm.
563  // (https://arxiv.org/pdf/1608.08883.pdf)
564  // The current implementation is provably convergent (at an optimal rate)
565  // for LP programs (provided we do not change the primal weight at every major
566  // iteration). Further, we have observed that this rule is very sensitive to
567  // the parameter choice whenever we apply the primal weight recomputation
568  // heuristic.
569  InnerStepOutcome TakeMalitskyPockStep();
570 
571  // Takes a step based on the adaptive heuristic presented in Section 3.1 of
572  // https://arxiv.org/pdf/2106.04756.pdf (further generalized to QP).
573  InnerStepOutcome TakeAdaptiveStep();
574 
575  // Takes a constant-size step.
576  InnerStepOutcome TakeConstantSizeStep();
577 
578  const PrimalDualHybridGradientParams params_;
579 
580  VectorXd current_primal_solution_;
581  VectorXd current_dual_solution_;
582  VectorXd current_primal_delta_;
583  VectorXd current_dual_delta_;
584 
585  ShardedWeightedAverage primal_average_;
586  ShardedWeightedAverage dual_average_;
587 
588  double step_size_;
589  double primal_weight_;
590 
591  const PreprocessSolver* preprocess_solver_;
592 
593  // For Malitsky-Pock linesearch only: `step_size_` / previous_step_size
594  double ratio_last_two_step_sizes_;
595  // For adaptive restarts only.
596  double normalized_gap_at_last_trial_ =
597  std::numeric_limits<double>::infinity();
598  // For adaptive restarts only.
599  double normalized_gap_at_last_restart_ =
600  std::numeric_limits<double>::infinity();
601  int iterations_completed_;
602  int num_rejected_steps_;
603  // A cache of `constraint_matrix.transpose() * current_dual_solution_`.
604  VectorXd current_dual_product_;
605  // The primal point at which the algorithm was last restarted from, or
606  // the initial primal starting point if no restart has occurred.
607  VectorXd last_primal_start_point_;
608  // The dual point at which the algorithm was last restarted from, or
609  // the initial dual starting point if no restart has occurred.
610  VectorXd last_dual_start_point_;
611  // Information for deciding whether to trigger a distance-based restart.
612  // The distances are initialized to +inf to force a restart during the first
613  // major iteration check.
614  DistanceBasedRestartInfo distance_based_restart_info_ = {
615  .distance_moved_last_restart_period =
616  std::numeric_limits<double>::infinity(),
617  .length_of_last_restart_period = 1,
618  };
619 };
620 
621 PreprocessSolver::PreprocessSolver(QuadraticProgram qp,
622  const PrimalDualHybridGradientParams& params)
623  : num_threads_(NumThreads(params.num_threads(), params.num_shards(), qp)),
624  num_shards_(NumShards(num_threads_, params.num_shards())),
625  sharded_qp_(std::move(qp), num_threads_, num_shards_) {}
626 
627 SolverResult ErrorSolverResult(const TerminationReason reason,
628  const std::string& message) {
629  SolveLog error_log;
630  error_log.set_termination_reason(reason);
631  error_log.set_termination_string(message);
632  LOG(WARNING) << "The solver did not run because of invalid input: "
633  << message;
634  return SolverResult{.solve_log = error_log};
635 }
636 
637 std::optional<SolverResult> CheckProblemStats(
638  const QuadraticProgramStats& problem_stats) {
639  const double kExcessiveInputValue = 1e50;
640  const double kExcessivelySmallInputValue = 1e-50;
641  const double kMaxDynamicRange = 1e20;
642  if (std::isnan(problem_stats.constraint_matrix_l2_norm())) {
643  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
644  "Constraint matrix has a NAN.");
645  }
646  if (problem_stats.constraint_matrix_abs_max() > kExcessiveInputValue) {
647  return ErrorSolverResult(
648  TERMINATION_REASON_INVALID_PROBLEM,
649  absl::StrCat("Constraint matrix has a non-zero with absolute value ",
650  problem_stats.constraint_matrix_abs_max(),
651  " which exceeds limit of ", kExcessiveInputValue, "."));
652  }
653  if (problem_stats.constraint_matrix_abs_max() >
654  kMaxDynamicRange * problem_stats.constraint_matrix_abs_min()) {
655  LOG(WARNING) << "Constraint matrix has largest absolute value "
656  << problem_stats.constraint_matrix_abs_max()
657  << " and smallest non-zero absolute value "
658  << problem_stats.constraint_matrix_abs_min()
659  << " performance may suffer.";
660  }
661  if (problem_stats.constraint_matrix_col_min_l_inf_norm() > 0 &&
662  problem_stats.constraint_matrix_col_min_l_inf_norm() <
663  kExcessivelySmallInputValue) {
664  return ErrorSolverResult(
665  TERMINATION_REASON_INVALID_PROBLEM,
666  absl::StrCat("Constraint matrix has a column with Linf norm ",
667  problem_stats.constraint_matrix_col_min_l_inf_norm(),
668  " which is less than limit of ",
669  kExcessivelySmallInputValue, "."));
670  }
671  if (problem_stats.constraint_matrix_row_min_l_inf_norm() > 0 &&
672  problem_stats.constraint_matrix_row_min_l_inf_norm() <
673  kExcessivelySmallInputValue) {
674  return ErrorSolverResult(
675  TERMINATION_REASON_INVALID_PROBLEM,
676  absl::StrCat("Constraint matrix has a row with Linf norm ",
677  problem_stats.constraint_matrix_row_min_l_inf_norm(),
678  " which is less than limit of ",
679  kExcessivelySmallInputValue, "."));
680  }
681  if (std::isnan(problem_stats.combined_bounds_l2_norm())) {
682  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
683  "Constraint bounds vector has a NAN.");
684  }
685  if (problem_stats.combined_bounds_max() > kExcessiveInputValue) {
686  return ErrorSolverResult(
687  TERMINATION_REASON_INVALID_PROBLEM,
688  absl::StrCat("Combined constraint bounds vector has a non-zero with "
689  "absolute value ",
690  problem_stats.combined_bounds_max(),
691  " which exceeds limit of ", kExcessiveInputValue, "."));
692  }
693  if (problem_stats.combined_bounds_max() >
694  kMaxDynamicRange * problem_stats.combined_bounds_min()) {
695  LOG(WARNING)
696  << "Combined constraint bounds vector has largest absolute value "
697  << problem_stats.combined_bounds_max()
698  << " and smallest non-zero absolute value "
699  << problem_stats.combined_bounds_min() << "; performance may suffer.";
700  }
701  if (std::isnan(problem_stats.variable_bound_gaps_l2_norm())) {
702  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
703  "Variable bounds vector has a NAN.");
704  }
705  if (problem_stats.variable_bound_gaps_max() > kExcessiveInputValue) {
706  return ErrorSolverResult(
707  TERMINATION_REASON_INVALID_PROBLEM,
708  absl::StrCat("Variable bound gaps vector has a finite non-zero with "
709  "absolute value ",
710  problem_stats.variable_bound_gaps_max(),
711  " which exceeds limit of ", kExcessiveInputValue, "."));
712  }
713  if (problem_stats.variable_bound_gaps_max() >
714  kMaxDynamicRange * problem_stats.variable_bound_gaps_min()) {
715  LOG(WARNING) << "Variable bound gap vector has largest absolute value "
716  << problem_stats.variable_bound_gaps_max()
717  << " and smallest non-zero absolute value "
718  << problem_stats.variable_bound_gaps_min()
719  << "; performance may suffer.";
720  }
721  if (std::isnan(problem_stats.objective_vector_l2_norm())) {
722  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
723  "Objective vector has a NAN.");
724  }
725  if (problem_stats.objective_vector_abs_max() > kExcessiveInputValue) {
726  return ErrorSolverResult(
727  TERMINATION_REASON_INVALID_PROBLEM,
728  absl::StrCat("Objective vector has a non-zero with absolute value ",
729  problem_stats.objective_vector_abs_max(),
730  " which exceeds limit of ", kExcessiveInputValue, "."));
731  }
732  if (problem_stats.objective_vector_abs_max() >
733  kMaxDynamicRange * problem_stats.objective_vector_abs_min()) {
734  LOG(WARNING) << "Objective vector has largest absolute value "
735  << problem_stats.objective_vector_abs_max()
736  << " and smallest non-zero absolute value "
737  << problem_stats.objective_vector_abs_min()
738  << "; performance may suffer.";
739  }
740  if (std::isnan(problem_stats.objective_matrix_l2_norm())) {
741  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
742  "Objective matrix has a NAN.");
743  }
744  if (problem_stats.objective_matrix_abs_max() > kExcessiveInputValue) {
745  return ErrorSolverResult(
746  TERMINATION_REASON_INVALID_PROBLEM,
747  absl::StrCat("Objective matrix has a non-zero with absolute value ",
748  problem_stats.objective_matrix_abs_max(),
749  " which exceeds limit of ", kExcessiveInputValue, "."));
750  }
751  if (problem_stats.objective_matrix_abs_max() >
752  kMaxDynamicRange * problem_stats.objective_matrix_abs_min()) {
753  LOG(WARNING) << "Objective matrix has largest absolute value "
754  << problem_stats.objective_matrix_abs_max()
755  << " and smallest non-zero absolute value "
756  << problem_stats.objective_matrix_abs_min()
757  << "; performance may suffer.";
758  }
759  return std::nullopt;
760 }
761 
762 std::optional<SolverResult> CheckInitialSolution(
763  const ShardedQuadraticProgram& sharded_qp,
764  const PrimalAndDualSolution& initial_solution) {
765  const double kExcessiveInputValue = 1e50;
766  if (initial_solution.primal_solution.size() != sharded_qp.PrimalSize()) {
767  return ErrorSolverResult(
768  TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
769  absl::StrCat("Initial primal solution has size ",
770  initial_solution.primal_solution.size(),
771  " which differs from problem primal size ",
772  sharded_qp.PrimalSize()));
773  }
774  if (std::isnan(
775  Norm(initial_solution.primal_solution, sharded_qp.PrimalSharder()))) {
776  return ErrorSolverResult(TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
777  "Initial primal solution has a NAN.");
778  }
779  if (const double norm = LInfNorm(initial_solution.primal_solution,
780  sharded_qp.PrimalSharder());
781  norm > kExcessiveInputValue) {
782  return ErrorSolverResult(
783  TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
784  absl::StrCat(
785  "Initial primal solution has an entry with absolute value ", norm,
786  " which exceeds limit of ", kExcessiveInputValue));
787  }
788  if (initial_solution.dual_solution.size() != sharded_qp.DualSize()) {
789  return ErrorSolverResult(
790  TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
791  absl::StrCat("Initial dual solution has size ",
792  initial_solution.dual_solution.size(),
793  " which differs from problem dual size ",
794  sharded_qp.DualSize()));
795  }
796  if (std::isnan(
797  Norm(initial_solution.dual_solution, sharded_qp.DualSharder()))) {
798  return ErrorSolverResult(TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
799  "Initial dual solution has a NAN.");
800  }
801  if (const double norm =
802  LInfNorm(initial_solution.dual_solution, sharded_qp.DualSharder());
803  norm > kExcessiveInputValue) {
804  return ErrorSolverResult(
805  TERMINATION_REASON_INVALID_INITIAL_SOLUTION,
806  absl::StrCat("Initial dual solution has an entry with absolute value ",
807  norm, " which exceeds limit of ", kExcessiveInputValue));
808  }
809  return std::nullopt;
810 }
811 
812 SolverResult PreprocessSolver::PreprocessAndSolve(
813  const PrimalDualHybridGradientParams& params,
814  std::optional<PrimalAndDualSolution> initial_solution,
815  const std::atomic<bool>* interrupt_solve,
816  IterationStatsCallback iteration_stats_callback) {
817  SolveLog solve_log;
818  if (Qp().problem_name.has_value()) {
819  solve_log.set_instance_name(*Qp().problem_name);
820  }
821  *solve_log.mutable_params() = params;
822  *solve_log.mutable_original_problem_stats() =
823  ComputeStats(sharded_qp_, params.infinite_constraint_bound_threshold());
824  const QuadraticProgramStats& original_problem_stats =
825  solve_log.original_problem_stats();
826  if (auto maybe_result = CheckProblemStats(original_problem_stats);
827  maybe_result.has_value()) {
828  return *maybe_result;
829  }
830  if (initial_solution.has_value()) {
831  if (auto maybe_result =
832  CheckInitialSolution(sharded_qp_, *initial_solution);
833  maybe_result.has_value()) {
834  return *maybe_result;
835  }
836  }
837  original_bound_norms_ = BoundNormsFromProblemStats(original_problem_stats);
838  const std::string preprocessing_string = absl::StrCat(
839  params.presolve_options().use_glop() ? "presolving and " : "",
840  "rescaling:");
841  if (params.verbosity_level() >= 1) {
842  LOG(INFO) << "Problem stats before " << preprocessing_string;
843  LogQuadraticProgramStats(solve_log.original_problem_stats());
844  }
845  timer_.Start();
846  iteration_stats_callback_ = std::move(iteration_stats_callback);
847  std::optional<TerminationReason> maybe_terminate =
848  ApplyPresolveIfEnabled(params, &initial_solution);
849  if (maybe_terminate.has_value()) {
850  // Glop also feeds zero primal and dual solutions when the preprocessor
851  // has a non-INIT status. When the preprocessor status is optimal the
852  // vectors have length 0. When the status is something else the lengths
853  // may be non-zero, but that's OK since we don't promise to produce a
854  // meaningful solution in that case.
855  IterationStats iteration_stats;
856  iteration_stats.set_cumulative_time_sec(GetElapsedTime());
857  solve_log.set_preprocessing_time_sec(iteration_stats.cumulative_time_sec());
858  VectorXd working_primal = ZeroVector(sharded_qp_.PrimalSharder());
859  VectorXd working_dual = ZeroVector(sharded_qp_.DualSharder());
860  PrimalAndDualSolution original = RecoverOriginalSolution(
861  {.primal_solution = working_primal, .dual_solution = working_dual});
862  AddConvergenceAndInfeasibilityInformation(
863  params, original.primal_solution, original.dual_solution,
864  presolve_info_->sharded_original_qp,
865  presolve_info_->trivial_col_scaling_vec,
866  presolve_info_->trivial_row_scaling_vec, POINT_TYPE_PRESOLVER_SOLUTION,
867  iteration_stats);
868  std::optional<TerminationReasonAndPointType> earned_termination =
869  CheckIterateTerminationCriteria(params.termination_criteria(),
870  iteration_stats, original_bound_norms_,
871  /*force_numerical_termination=*/false);
872  if (!earned_termination.has_value()) {
873  earned_termination = CheckSimpleTerminationCriteria(
874  params.termination_criteria(), iteration_stats, interrupt_solve);
875  }
876  TerminationReason final_termination_reason;
877  if (earned_termination.has_value() &&
878  (earned_termination->reason == TERMINATION_REASON_OPTIMAL ||
879  earned_termination->reason == TERMINATION_REASON_PRIMAL_INFEASIBLE ||
880  earned_termination->reason == TERMINATION_REASON_DUAL_INFEASIBLE)) {
881  final_termination_reason = earned_termination->reason;
882  } else {
883  if (*maybe_terminate == TERMINATION_REASON_OPTIMAL) {
884  final_termination_reason = TERMINATION_REASON_NUMERICAL_ERROR;
885  LOG(WARNING) << "Presolve claimed to solve the LP optimally but the "
886  "solution doesn't satisfy the optimality criteria.";
887  } else {
888  final_termination_reason = *maybe_terminate;
889  }
890  }
891  return ConstructOriginalSolverResult(
892  params, ConstructSolverResult(
893  std::move(working_primal), std::move(working_dual),
894  std::move(iteration_stats), final_termination_reason,
895  POINT_TYPE_PRESOLVER_SOLUTION, std::move(solve_log)));
896  }
897 
898  VectorXd starting_primal_solution;
899  VectorXd starting_dual_solution;
900  // The current solution is updated by `ComputeAndApplyRescaling`.
901  if (initial_solution.has_value()) {
902  starting_primal_solution = std::move(initial_solution->primal_solution);
903  starting_dual_solution = std::move(initial_solution->dual_solution);
904  } else {
905  SetZero(sharded_qp_.PrimalSharder(), starting_primal_solution);
906  SetZero(sharded_qp_.DualSharder(), starting_dual_solution);
907  }
908  // The following projections are necessary since all our checks assume that
909  // the primal and dual variable bounds are satisfied.
910  ProjectToPrimalVariableBounds(sharded_qp_, starting_primal_solution);
911  ProjectToDualVariableBounds(sharded_qp_, starting_dual_solution);
912 
913  ComputeAndApplyRescaling(params, starting_primal_solution,
914  starting_dual_solution);
915  *solve_log.mutable_preprocessed_problem_stats() =
916  ComputeStats(sharded_qp_, params.infinite_constraint_bound_threshold());
917  if (params.verbosity_level() >= 1) {
918  LOG(INFO) << "Problem stats after " << preprocessing_string;
919  LogQuadraticProgramStats(solve_log.preprocessed_problem_stats());
920  }
921 
922  double step_size = 0.0;
923  if (params.linesearch_rule() ==
924  PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE) {
925  std::mt19937 random(1);
926  double inverse_step_size;
927  const auto lipschitz_result =
929  sharded_qp_, std::nullopt, std::nullopt,
930  /*desired_relative_error=*/0.2, /*failure_probability=*/0.0005,
931  random);
932  // With high probability, `lipschitz_result.singular_value` is within
933  // +/- `lipschitz_result.estimated_relative_error
934  // * lipschitz_result.singular_value`
935  const double lipschitz_term_upper_bound =
936  lipschitz_result.singular_value /
937  (1.0 - lipschitz_result.estimated_relative_error);
938  inverse_step_size = lipschitz_term_upper_bound;
939  step_size = inverse_step_size > 0.0 ? 1.0 / inverse_step_size : 1.0;
940  } else {
941  // This initial step size is designed to err on the side of being too big.
942  // This is because
943  // (i) too-big steps are rejected and hence don't hurt us other than
944  // wasting
945  // an iteration and
946  // (ii) the step size adjustment algorithm shrinks the step size as far as
947  // needed in a single iteration but raises it slowly.
948  // The tiny constant is there to keep the step size finite in the case of a
949  // trivial LP with no constraints.
950  step_size =
951  1.0 /
952  std::max(
953  1.0e-20,
954  solve_log.preprocessed_problem_stats().constraint_matrix_abs_max());
955  }
956  step_size *= params.initial_step_size_scaling();
957 
958  const double primal_weight = InitialPrimalWeight(
959  params, solve_log.preprocessed_problem_stats().objective_vector_l2_norm(),
960  solve_log.preprocessed_problem_stats().combined_bounds_l2_norm());
961  solve_log.set_preprocessing_time_sec(GetElapsedTime());
962 
963  Solver solver(params, starting_primal_solution, starting_dual_solution,
964  step_size, primal_weight, this);
965  SolverResult result = solver.Solve(interrupt_solve, std::move(solve_log));
966  return ConstructOriginalSolverResult(params, std::move(result));
967 }
968 
969 void LogInfoWithoutPrefix(absl::string_view message) {
970  LOG(INFO).NoPrefix() << message;
971 }
972 
973 glop::GlopParameters PreprocessSolver::PreprocessorParameters(
974  const PrimalDualHybridGradientParams& params) {
975  glop::GlopParameters glop_params;
976  // TODO(user): Test if dualization helps or hurts performance.
977  glop_params.set_solve_dual_problem(glop::GlopParameters::NEVER_DO);
978  // Experiments show that this preprocessing step can hurt because it relaxes
979  // variable bounds.
980  glop_params.set_use_implied_free_preprocessor(false);
981  // We do our own scaling.
982  glop_params.set_use_scaling(false);
983  if (params.presolve_options().has_glop_parameters()) {
984  glop_params.MergeFrom(params.presolve_options().glop_parameters());
985  }
986  return glop_params;
987 }
988 
989 TerminationReason GlopStatusToTerminationReason(
990  const glop::ProblemStatus glop_status) {
991  switch (glop_status) {
993  return TERMINATION_REASON_OPTIMAL;
994  case glop::ProblemStatus::INVALID_PROBLEM:
995  return TERMINATION_REASON_INVALID_PROBLEM;
997  case glop::ProblemStatus::IMPRECISE:
998  return TERMINATION_REASON_NUMERICAL_ERROR;
999  case glop::ProblemStatus::PRIMAL_INFEASIBLE:
1000  case glop::ProblemStatus::DUAL_INFEASIBLE:
1001  case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED:
1002  case glop::ProblemStatus::DUAL_UNBOUNDED:
1003  case glop::ProblemStatus::PRIMAL_UNBOUNDED:
1004  return TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE;
1005  default:
1006  LOG(WARNING) << "Unexpected preprocessor status " << glop_status;
1007  return TERMINATION_REASON_OTHER;
1008  }
1009 }
1010 
1011 std::optional<TerminationReason> PreprocessSolver::ApplyPresolveIfEnabled(
1012  const PrimalDualHybridGradientParams& params,
1013  std::optional<PrimalAndDualSolution>* const initial_solution) {
1014  const bool presolve_enabled = params.presolve_options().use_glop();
1015  if (!presolve_enabled) {
1016  return std::nullopt;
1017  }
1018  if (!IsLinearProgram(Qp())) {
1019  LOG(WARNING)
1020  << "Skipping presolve, which is only supported for linear programs";
1021  return std::nullopt;
1022  }
1023  absl::StatusOr<MPModelProto> model = QpToMpModelProto(Qp());
1024  if (!model.ok()) {
1025  LOG(WARNING)
1026  << "Skipping presolve because of error converting to MPModelProto: "
1027  << model.status();
1028  return std::nullopt;
1029  }
1030  if (initial_solution->has_value()) {
1031  LOG(WARNING) << "Ignoring initial solution. Initial solutions "
1032  "are ignored when presolve is on.";
1033  initial_solution->reset();
1034  }
1035  glop::LinearProgram glop_lp;
1037  // Save RAM.
1038  model->Clear();
1039  presolve_info_.emplace(std::move(sharded_qp_), params);
1040  // To simplify our code we ignore the return value indicating whether
1041  // postprocessing is required. We simply call `RecoverSolution()`
1042  // unconditionally, which may do nothing.
1043  presolve_info_->preprocessor.Run(&glop_lp);
1044  presolve_info_->presolved_problem_was_maximization =
1045  glop_lp.IsMaximizationProblem();
1046  MPModelProto output;
1047  glop::LinearProgramToMPModelProto(glop_lp, &output);
1048  // This will only fail if given an invalid LP, which shouldn't happen.
1049  absl::StatusOr<QuadraticProgram> presolved_qp =
1050  QpFromMpModelProto(output, /*relax_integer_variables=*/false);
1051  CHECK_OK(presolved_qp.status());
1052  // `MPModelProto` doesn't support scaling factors, so if `glop_lp` has an
1053  // `objective_scaling_factor` it won't be set in output and `presolved_qp`.
1054  // The scaling factor of `presolved_qp` isn't actually used anywhere, but we
1055  // set it for completeness.
1056  presolved_qp->objective_scaling_factor = glop_lp.objective_scaling_factor();
1057  sharded_qp_ = ShardedQuadraticProgram(std::move(*presolved_qp), num_threads_,
1058  num_shards_);
1059  // A status of `INIT` means the preprocessor created a (usually) smaller
1060  // problem that needs solving. Other statuses mean the preprocessor solved
1061  // the problem completely.
1062  if (presolve_info_->preprocessor.status() != glop::ProblemStatus::INIT) {
1063  col_scaling_vec_ = OnesVector(sharded_qp_.PrimalSharder());
1064  row_scaling_vec_ = OnesVector(sharded_qp_.DualSharder());
1065  return GlopStatusToTerminationReason(presolve_info_->preprocessor.status());
1066  }
1067  return std::nullopt;
1068 }
1069 
1070 void PreprocessSolver::ComputeAndApplyRescaling(
1071  const PrimalDualHybridGradientParams& params,
1072  VectorXd& starting_primal_solution, VectorXd& starting_dual_solution) {
1073  ScalingVectors scaling = ApplyRescaling(
1074  RescalingOptions{.l_inf_ruiz_iterations = params.l_inf_ruiz_iterations(),
1075  .l2_norm_rescaling = params.l2_norm_rescaling()},
1076  sharded_qp_);
1077  row_scaling_vec_ = std::move(scaling.row_scaling_vec);
1078  col_scaling_vec_ = std::move(scaling.col_scaling_vec);
1079 
1080  CoefficientWiseQuotientInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1081  starting_primal_solution);
1082  CoefficientWiseQuotientInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1083  starting_dual_solution);
1084 }
1085 
1086 void PreprocessSolver::LogQuadraticProgramStats(
1087  const QuadraticProgramStats& stats) {
1088  LOG(INFO) << absl::StrFormat(
1089  "There are %i variables, %i constraints, and %i ",
1090  stats.num_variables(), stats.num_constraints(),
1091  stats.constraint_matrix_num_nonzeros())
1092  << "constraint matrix nonzeros.";
1093  if (Qp().constraint_matrix.nonZeros() > 0) {
1094  LOG(INFO) << "Absolute values of nonzero constraint matrix elements: "
1095  << absl::StrFormat("largest=%f, smallest=%f, avg=%f",
1096  stats.constraint_matrix_abs_max(),
1097  stats.constraint_matrix_abs_min(),
1098  stats.constraint_matrix_abs_avg());
1099  LOG(INFO) << "Constraint matrix, infinity norm: "
1100  << absl::StrFormat("max(row & col)=%f, min_col=%f, min_row=%f",
1101  stats.constraint_matrix_abs_max(),
1102  stats.constraint_matrix_col_min_l_inf_norm(),
1103  stats.constraint_matrix_row_min_l_inf_norm());
1104  LOG(INFO) << "Constraint bounds statistics (max absolute value per row): "
1105  << absl::StrFormat("largest=%f, smallest=%f, avg=%f, l2_norm=%f",
1106  stats.combined_bounds_max(),
1107  stats.combined_bounds_min(),
1108  stats.combined_bounds_avg(),
1109  stats.combined_bounds_l2_norm());
1110  }
1111  if (!IsLinearProgram(Qp())) {
1112  LOG(INFO) << absl::StrFormat(
1113  "There are %i nonzero diagonal coefficients in the objective matrix.",
1114  stats.objective_matrix_num_nonzeros());
1115  LOG(INFO) << "Absolute values of nonzero objective matrix elements: "
1116  << absl::StrFormat("largest=%f, smallest=%f, avg=%f",
1117  stats.objective_matrix_abs_max(),
1118  stats.objective_matrix_abs_min(),
1119  stats.objective_matrix_abs_avg());
1120  }
1121  LOG(INFO) << "Absolute values of objective vector elements: "
1122  << absl::StrFormat("largest=%f, smallest=%f, avg=%f, l2_norm=%f",
1123  stats.objective_vector_abs_max(),
1124  stats.objective_vector_abs_min(),
1125  stats.objective_vector_abs_avg(),
1126  stats.objective_vector_l2_norm());
1127 
1128  LOG(INFO) << "Gaps between variable upper and lower bounds: "
1129  << absl::StrFormat(
1130  "#finite=%i of %i, largest=%f, smallest=%f, avg=%f",
1131  stats.variable_bound_gaps_num_finite(),
1132  stats.num_variables(), stats.variable_bound_gaps_max(),
1133  stats.variable_bound_gaps_min(),
1134  stats.variable_bound_gaps_avg());
1135 }
1136 
1137 double PreprocessSolver::InitialPrimalWeight(
1138  const PrimalDualHybridGradientParams& params,
1139  const double l2_norm_primal_linear_objective,
1140  const double l2_norm_constraint_bounds) const {
1141  if (params.has_initial_primal_weight()) {
1142  return params.initial_primal_weight();
1143  }
1144  if (l2_norm_primal_linear_objective > 0.0 &&
1145  l2_norm_constraint_bounds > 0.0) {
1146  // The hand-wavy motivation for this choice is that the objective vector
1147  // has units of (objective units)/(primal units) and the constraint
1148  // bounds vector has units of (objective units)/(dual units),
1149  // therefore this ratio has units (dual units)/(primal units). By
1150  // dimensional analysis, these are the same units as the primal weight.
1151  return l2_norm_primal_linear_objective / l2_norm_constraint_bounds;
1152  } else {
1153  return 1.0;
1154  }
1155 }
1156 
1157 PrimalAndDualSolution PreprocessSolver::RecoverOriginalSolution(
1158  PrimalAndDualSolution working_solution) const {
1159  glop::ProblemSolution glop_solution(glop::RowIndex{0}, glop::ColIndex{0});
1160  if (presolve_info_.has_value()) {
1161  // We compute statuses relative to the working problem so we can detect when
1162  // variables are at their bounds without floating-point roundoff induced by
1163  // scaling.
1164  glop_solution = internal::ComputeStatuses(Qp(), working_solution);
1165  }
1166  CoefficientWiseProductInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1167  working_solution.primal_solution);
1168  CoefficientWiseProductInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1169  working_solution.dual_solution);
1170  if (presolve_info_.has_value()) {
1171  glop_solution.primal_values =
1172  glop::DenseRow(working_solution.primal_solution.begin(),
1173  working_solution.primal_solution.end());
1174  glop_solution.dual_values =
1175  glop::DenseColumn(working_solution.dual_solution.begin(),
1176  working_solution.dual_solution.end());
1177  // We got the working QP by calling `LinearProgramToMPModelProto()` and
1178  // `QpFromMpModelProto()`. We need to negate the duals if the LP resulting
1179  // from presolve was a max problem.
1180  if (presolve_info_->presolved_problem_was_maximization) {
1181  for (glop::RowIndex i{0}; i < glop_solution.dual_values.size(); ++i) {
1182  glop_solution.dual_values[i] *= -1;
1183  }
1184  }
1185  presolve_info_->preprocessor.RecoverSolution(&glop_solution);
1186  PrimalAndDualSolution solution;
1187  solution.primal_solution =
1188  Eigen::Map<Eigen::VectorXd>(glop_solution.primal_values.data(),
1189  glop_solution.primal_values.size().value());
1190  solution.dual_solution =
1191  Eigen::Map<Eigen::VectorXd>(glop_solution.dual_values.data(),
1192  glop_solution.dual_values.size().value());
1193  // We called `QpToMpModelProto()` and `MPModelProtoToLinearProgram()` to
1194  // convert our original QP into input for glop's preprocessor. The former
1195  // multiplies the objective vector by `objective_scaling_factor`, which
1196  // multiplies the duals by that factor as well. To undo this we divide by
1197  // `objective_scaling_factor`.
1198  solution.dual_solution /=
1199  presolve_info_->sharded_original_qp.Qp().objective_scaling_factor;
1200  // Glop's preprocessor sometimes violates the primal bounds constraints. To
1201  // be safe we project both primal and dual.
1202  ProjectToPrimalVariableBounds(presolve_info_->sharded_original_qp,
1203  solution.primal_solution);
1204  ProjectToDualVariableBounds(presolve_info_->sharded_original_qp,
1205  solution.dual_solution);
1206  return solution;
1207  } else {
1208  return working_solution;
1209  }
1210 }
1211 
1212 void PreprocessSolver::AddConvergenceAndInfeasibilityInformation(
1213  const PrimalDualHybridGradientParams& params,
1214  const VectorXd& primal_solution, const VectorXd& dual_solution,
1215  const ShardedQuadraticProgram& sharded_qp, const VectorXd& col_scaling_vec,
1216  const VectorXd& row_scaling_vec, PointType candidate_type,
1217  IterationStats& stats) const {
1218  const TerminationCriteria::DetailedOptimalityCriteria criteria =
1219  EffectiveOptimalityCriteria(params.termination_criteria());
1220  *stats.add_convergence_information() = ComputeConvergenceInformation(
1221  params, sharded_qp, col_scaling_vec, row_scaling_vec, primal_solution,
1222  dual_solution,
1223  EpsilonRatio(criteria.eps_optimal_primal_residual_absolute(),
1224  criteria.eps_optimal_primal_residual_relative()),
1225  EpsilonRatio(criteria.eps_optimal_dual_residual_absolute(),
1226  criteria.eps_optimal_dual_residual_relative()),
1227  candidate_type);
1228  *stats.add_infeasibility_information() = ComputeInfeasibilityInformation(
1229  params, sharded_qp, col_scaling_vec, row_scaling_vec, primal_solution,
1230  dual_solution, candidate_type);
1231 }
1232 
1233 void SetActiveSetInformation(const ShardedQuadraticProgram& sharded_qp,
1234  const VectorXd& primal_solution,
1235  const VectorXd& dual_solution,
1236  const VectorXd& primal_start_point,
1237  const VectorXd& dual_start_point,
1238  PointMetadata& metadata) {
1239  CHECK_EQ(primal_solution.size(), sharded_qp.PrimalSize());
1240  CHECK_EQ(dual_solution.size(), sharded_qp.DualSize());
1241  CHECK_EQ(primal_start_point.size(), sharded_qp.PrimalSize());
1242  CHECK_EQ(dual_start_point.size(), sharded_qp.DualSize());
1243 
1244  const QuadraticProgram& qp = sharded_qp.Qp();
1245  metadata.set_active_primal_variable_count(
1246  static_cast<int64_t>(sharded_qp.PrimalSharder().ParallelSumOverShards(
1247  [&](const Sharder::Shard& shard) {
1248  const auto primal_shard = shard(primal_solution);
1249  const auto lower_bound_shard = shard(qp.variable_lower_bounds);
1250  const auto upper_bound_shard = shard(qp.variable_upper_bounds);
1251  return (primal_shard.array() > lower_bound_shard.array() &&
1252  primal_shard.array() < upper_bound_shard.array())
1253  .count();
1254  })));
1255 
1256  // Most of the computation from the previous `ParallelSumOverShards` is
1257  // duplicated here. However the overhead shouldn't be too large, and using
1258  // `ParallelSumOverShards` is simpler than just using `ParallelForEachShard`.
1259  metadata.set_active_primal_variable_change(
1260  static_cast<int64_t>(sharded_qp.PrimalSharder().ParallelSumOverShards(
1261  [&](const Sharder::Shard& shard) {
1262  const auto primal_shard = shard(primal_solution);
1263  const auto primal_start_shard = shard(primal_start_point);
1264  const auto lower_bound_shard = shard(qp.variable_lower_bounds);
1265  const auto upper_bound_shard = shard(qp.variable_upper_bounds);
1266  return ((primal_shard.array() > lower_bound_shard.array() &&
1267  primal_shard.array() < upper_bound_shard.array()) !=
1268  (primal_start_shard.array() > lower_bound_shard.array() &&
1269  primal_start_shard.array() < upper_bound_shard.array()))
1270  .count();
1271  })));
1272 
1273  metadata.set_active_dual_variable_count(
1274  static_cast<int64_t>(sharded_qp.DualSharder().ParallelSumOverShards(
1275  [&](const Sharder::Shard& shard) {
1276  const auto dual_shard = shard(dual_solution);
1277  const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
1278  const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
1279  const double kInfinity = std::numeric_limits<double>::infinity();
1280  return (dual_shard.array() != 0.0 ||
1281  (lower_bound_shard.array() == -kInfinity &&
1282  upper_bound_shard.array() == kInfinity))
1283  .count();
1284  })));
1285 
1286  metadata.set_active_dual_variable_change(
1287  static_cast<int64_t>(sharded_qp.DualSharder().ParallelSumOverShards(
1288  [&](const Sharder::Shard& shard) {
1289  const auto dual_shard = shard(dual_solution);
1290  const auto dual_start_shard = shard(dual_start_point);
1291  const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
1292  const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
1293  const double kInfinity = std::numeric_limits<double>::infinity();
1294  return ((dual_shard.array() != 0.0 ||
1295  (lower_bound_shard.array() == -kInfinity &&
1296  upper_bound_shard.array() == kInfinity)) !=
1297  (dual_start_shard.array() != 0.0 ||
1298  (lower_bound_shard.array() == -kInfinity &&
1299  upper_bound_shard.array() == kInfinity)))
1300  .count();
1301  })));
1302 }
1303 
1304 void PreprocessSolver::AddPointMetadata(
1305  const PrimalDualHybridGradientParams& params,
1306  const VectorXd& primal_solution, const VectorXd& dual_solution,
1307  PointType point_type, const VectorXd& last_primal_start_point,
1308  const VectorXd& last_dual_start_point, IterationStats& stats) const {
1309  PointMetadata metadata;
1310  metadata.set_point_type(point_type);
1311  std::vector<int> random_projection_seeds(
1312  params.random_projection_seeds().begin(),
1313  params.random_projection_seeds().end());
1314  SetRandomProjections(sharded_qp_, primal_solution, dual_solution,
1315  random_projection_seeds, metadata);
1316  if (point_type != POINT_TYPE_ITERATE_DIFFERENCE) {
1317  SetActiveSetInformation(sharded_qp_, primal_solution, dual_solution,
1318  last_primal_start_point, last_dual_start_point,
1319  metadata);
1320  }
1321  *stats.add_point_metadata() = metadata;
1322 }
1323 
1324 std::optional<TerminationReasonAndPointType>
1325 PreprocessSolver::UpdateIterationStatsAndCheckTermination(
1326  const PrimalDualHybridGradientParams& params,
1327  bool force_numerical_termination, const VectorXd& working_primal_current,
1328  const VectorXd& working_dual_current,
1329  const VectorXd* working_primal_average,
1330  const VectorXd* working_dual_average, const VectorXd* working_primal_delta,
1331  const VectorXd* working_dual_delta, const VectorXd& last_primal_start_point,
1332  const VectorXd& last_dual_start_point,
1333  const std::atomic<bool>* interrupt_solve, IterationStats& stats) const {
1334  if (presolve_info_.has_value()) {
1335  { // This block exists to destroy `original_current` to save RAM.
1336  PrimalAndDualSolution original_current =
1337  RecoverOriginalSolution({.primal_solution = working_primal_current,
1338  .dual_solution = working_dual_current});
1339  AddConvergenceAndInfeasibilityInformation(
1340  params, original_current.primal_solution,
1341  original_current.dual_solution, presolve_info_->sharded_original_qp,
1342  presolve_info_->trivial_col_scaling_vec,
1343  presolve_info_->trivial_row_scaling_vec, POINT_TYPE_CURRENT_ITERATE,
1344  stats);
1345  }
1346  if (working_primal_average != nullptr && working_dual_average != nullptr) {
1347  PrimalAndDualSolution original_average =
1348  RecoverOriginalSolution({.primal_solution = *working_primal_average,
1349  .dual_solution = *working_dual_average});
1350  AddConvergenceAndInfeasibilityInformation(
1351  params, original_average.primal_solution,
1352  original_average.dual_solution, presolve_info_->sharded_original_qp,
1353  presolve_info_->trivial_col_scaling_vec,
1354  presolve_info_->trivial_row_scaling_vec, POINT_TYPE_AVERAGE_ITERATE,
1355  stats);
1356  }
1357  } else {
1358  AddConvergenceAndInfeasibilityInformation(
1359  params, working_primal_current, working_dual_current, sharded_qp_,
1360  col_scaling_vec_, row_scaling_vec_, POINT_TYPE_CURRENT_ITERATE, stats);
1361  if (working_primal_average != nullptr && working_dual_average != nullptr) {
1362  AddConvergenceAndInfeasibilityInformation(
1363  params, *working_primal_average, *working_dual_average, sharded_qp_,
1364  col_scaling_vec_, row_scaling_vec_, POINT_TYPE_AVERAGE_ITERATE,
1365  stats);
1366  }
1367  }
1368  AddPointMetadata(params, working_primal_current, working_dual_current,
1369  POINT_TYPE_CURRENT_ITERATE, last_primal_start_point,
1370  last_dual_start_point, stats);
1371  if (working_primal_average != nullptr && working_dual_average != nullptr) {
1372  AddPointMetadata(params, *working_primal_average, *working_dual_average,
1373  POINT_TYPE_AVERAGE_ITERATE, last_primal_start_point,
1374  last_dual_start_point, stats);
1375  }
1376  if (working_primal_delta != nullptr && working_dual_delta != nullptr) {
1377  if (presolve_info_.has_value()) {
1378  PrimalAndDualSolution original_delta =
1379  RecoverOriginalSolution({.primal_solution = *working_primal_delta,
1380  .dual_solution = *working_dual_delta});
1381  *stats.add_infeasibility_information() = ComputeInfeasibilityInformation(
1382  params, presolve_info_->sharded_original_qp,
1383  presolve_info_->trivial_col_scaling_vec,
1384  presolve_info_->trivial_row_scaling_vec,
1385  original_delta.primal_solution, original_delta.dual_solution,
1386  POINT_TYPE_ITERATE_DIFFERENCE);
1387  } else {
1388  *stats.add_infeasibility_information() = ComputeInfeasibilityInformation(
1389  params, sharded_qp_, col_scaling_vec_, row_scaling_vec_,
1390  *working_primal_delta, *working_dual_delta,
1391  POINT_TYPE_ITERATE_DIFFERENCE);
1392  }
1393  AddPointMetadata(params, *working_primal_delta, *working_dual_delta,
1394  POINT_TYPE_ITERATE_DIFFERENCE, last_primal_start_point,
1395  last_dual_start_point, stats);
1396  }
1397  constexpr int kLogEvery = 15;
1398  static std::atomic_int log_counter{0};
1399  if (params.verbosity_level() >= 4) {
1400  if (log_counter == 0) {
1401  LogInfoWithoutPrefix(absl::StrCat("I ", IterationStatsLabelString()));
1402  }
1403  LogInfoWithoutPrefix(absl::StrCat(
1404  "A ", ToString(stats, params.termination_criteria(),
1405  original_bound_norms_, POINT_TYPE_AVERAGE_ITERATE)));
1406  LogInfoWithoutPrefix(absl::StrCat(
1407  "C ", ToString(stats, params.termination_criteria(),
1408  original_bound_norms_, POINT_TYPE_CURRENT_ITERATE)));
1409  } else if (params.verbosity_level() >= 3) {
1410  if (log_counter == 0) {
1411  LogInfoWithoutPrefix(IterationStatsLabelString());
1412  }
1413  LogInfoWithoutPrefix(ToString(stats, params.termination_criteria(),
1414  original_bound_norms_,
1415  POINT_TYPE_AVERAGE_ITERATE));
1416  } else if (params.verbosity_level() >= 2) {
1417  if (log_counter == 0) {
1418  LogInfoWithoutPrefix(IterationStatsLabelShortString());
1419  }
1420  LogInfoWithoutPrefix(ToShortString(stats, params.termination_criteria(),
1421  original_bound_norms_,
1422  POINT_TYPE_AVERAGE_ITERATE));
1423  }
1424  if (++log_counter >= kLogEvery) {
1425  log_counter = 0;
1426  }
1427  if (iteration_stats_callback_ != nullptr) {
1428  iteration_stats_callback_(
1429  {.termination_criteria = params.termination_criteria(),
1430  .iteration_stats = stats,
1431  .bound_norms = original_bound_norms_});
1432  }
1433 
1434  if (const auto termination = CheckIterateTerminationCriteria(
1435  params.termination_criteria(), stats, original_bound_norms_,
1436  force_numerical_termination);
1437  termination.has_value()) {
1438  return termination;
1439  }
1440  return CheckSimpleTerminationCriteria(params.termination_criteria(), stats,
1441  interrupt_solve);
1442 }
1443 
1444 ConvergenceInformation
1445 PreprocessSolver::ComputeConvergenceInformationFromWorkingSolution(
1446  const PrimalDualHybridGradientParams& params,
1447  const VectorXd& working_primal, const VectorXd& working_dual,
1448  PointType candidate_type) const {
1449  const TerminationCriteria::DetailedOptimalityCriteria criteria =
1450  EffectiveOptimalityCriteria(params.termination_criteria());
1451  const double primal_epsilon_ratio =
1452  EpsilonRatio(criteria.eps_optimal_primal_residual_absolute(),
1453  criteria.eps_optimal_primal_residual_relative());
1454  const double dual_epsilon_ratio =
1455  EpsilonRatio(criteria.eps_optimal_dual_residual_absolute(),
1456  criteria.eps_optimal_dual_residual_relative());
1457  if (presolve_info_.has_value()) {
1458  PrimalAndDualSolution original = RecoverOriginalSolution(
1459  {.primal_solution = working_primal, .dual_solution = working_dual});
1461  params, presolve_info_->sharded_original_qp,
1462  presolve_info_->trivial_col_scaling_vec,
1463  presolve_info_->trivial_row_scaling_vec, original.primal_solution,
1464  original.dual_solution, primal_epsilon_ratio, dual_epsilon_ratio,
1465  candidate_type);
1466  } else {
1468  params, sharded_qp_, col_scaling_vec_, row_scaling_vec_, working_primal,
1469  working_dual, primal_epsilon_ratio, dual_epsilon_ratio, candidate_type);
1470  }
1471 }
1472 
1473 // `result` is used both as the input and as the temporary that will be
1474 // returned.
1475 SolverResult PreprocessSolver::ConstructOriginalSolverResult(
1476  const PrimalDualHybridGradientParams& params, SolverResult result) const {
1477  const bool use_zero_primal_objective =
1478  result.solve_log.termination_reason() ==
1479  TERMINATION_REASON_PRIMAL_INFEASIBLE;
1480  if (presolve_info_.has_value()) {
1481  // Transform the solutions so they match the original unscaled problem.
1482  PrimalAndDualSolution original_solution = RecoverOriginalSolution(
1483  {.primal_solution = std::move(result.primal_solution),
1484  .dual_solution = std::move(result.dual_solution)});
1485  result.primal_solution = std::move(original_solution.primal_solution);
1486  result.dual_solution = std::move(original_solution.dual_solution);
1487  // `RecoverOriginalSolution` doesn't recover reduced costs so we need to
1488  // compute them with respect to the original problem.
1489  result.reduced_costs = ReducedCosts(
1490  params, presolve_info_->sharded_original_qp, result.primal_solution,
1491  result.dual_solution, use_zero_primal_objective);
1492  } else {
1493  result.reduced_costs =
1494  ReducedCosts(params, sharded_qp_, result.primal_solution,
1495  result.dual_solution, use_zero_primal_objective);
1496  // Transform the solutions so they match the original unscaled problem.
1497  CoefficientWiseProductInPlace(col_scaling_vec_, sharded_qp_.PrimalSharder(),
1498  result.primal_solution);
1499  CoefficientWiseProductInPlace(row_scaling_vec_, sharded_qp_.DualSharder(),
1500  result.dual_solution);
1502  col_scaling_vec_, sharded_qp_.PrimalSharder(), result.reduced_costs);
1503  }
1504  if (iteration_stats_callback_ != nullptr) {
1505  iteration_stats_callback_(
1506  {.termination_criteria = params.termination_criteria(),
1507  .iteration_stats = result.solve_log.solution_stats(),
1508  .bound_norms = original_bound_norms_});
1509  }
1510 
1511  if (params.verbosity_level() >= 1) {
1512  LOG(INFO) << "Termination reason: "
1513  << TerminationReason_Name(result.solve_log.termination_reason());
1514  LOG(INFO) << "Solution point type: "
1515  << PointType_Name(result.solve_log.solution_type());
1516  LOG(INFO) << "Final solution stats:";
1517  LOG(INFO) << IterationStatsLabelString();
1518  LOG(INFO) << ToString(result.solve_log.solution_stats(),
1519  params.termination_criteria(), original_bound_norms_,
1520  result.solve_log.solution_type());
1521  const auto& convergence_info = GetConvergenceInformation(
1522  result.solve_log.solution_stats(), result.solve_log.solution_type());
1523  if (convergence_info.has_value()) {
1524  if (std::isfinite(convergence_info->corrected_dual_objective())) {
1525  LOG(INFO) << "Dual objective after infeasibility correction: "
1526  << convergence_info->corrected_dual_objective();
1527  }
1528  }
1529  }
1530  return result;
1531 }
1532 
1533 Solver::Solver(const PrimalDualHybridGradientParams& params,
1534  VectorXd starting_primal_solution,
1535  VectorXd starting_dual_solution, const double initial_step_size,
1536  const double initial_primal_weight,
1537  const PreprocessSolver* preprocess_solver)
1538  : params_(params),
1539  current_primal_solution_(std::move(starting_primal_solution)),
1540  current_dual_solution_(std::move(starting_dual_solution)),
1541  primal_average_(&preprocess_solver->ShardedWorkingQp().PrimalSharder()),
1542  dual_average_(&preprocess_solver->ShardedWorkingQp().DualSharder()),
1543  step_size_(initial_step_size),
1544  primal_weight_(initial_primal_weight),
1545  preprocess_solver_(preprocess_solver) {}
1546 
1547 Solver::NextSolutionAndDelta Solver::ComputeNextPrimalSolution(
1548  double primal_step_size) const {
1549  const int64_t primal_size = ShardedWorkingQp().PrimalSize();
1550  NextSolutionAndDelta result = {
1551  .value = VectorXd(primal_size),
1552  .delta = VectorXd(primal_size),
1553  };
1554  const QuadraticProgram& qp = WorkingQp();
1555  // This computes the primal portion of the PDHG algorithm:
1556  // argmin_x[gradient(f)(`current_primal_solution_`)^T x + g(x)
1557  // + `current_dual_solution_`^T K x
1558  // + (0.5 / `primal_step_size`) * norm(x - `current_primal_solution_`)^2]
1559  // See Sections 2 - 3 of Chambolle and Pock and the comment in the header.
1560  // We omitted the constant terms from Chambolle and Pock's (7).
1561  // This minimization is easy to do in closed form since it can be separated
1562  // into independent problems for each of the primal variables.
1563  ShardedWorkingQp().PrimalSharder().ParallelForEachShard(
1564  [&](const Sharder::Shard& shard) {
1565  if (!IsLinearProgram(qp)) {
1566  // TODO(user): Does changing this to auto (so it becomes an
1567  // Eigen deferred result), or inlining it below, change performance?
1568  const VectorXd diagonal_scaling =
1569  primal_step_size *
1570  shard(qp.objective_matrix->diagonal()).array() +
1571  1.0;
1572  shard(result.value) =
1573  (shard(current_primal_solution_) -
1574  primal_step_size *
1575  (shard(qp.objective_vector) - shard(current_dual_product_)))
1576  // Scale i-th element by 1 / (1 + `primal_step_size` * Q_{ii})
1577  .cwiseQuotient(diagonal_scaling)
1578  .cwiseMin(shard(qp.variable_upper_bounds))
1579  .cwiseMax(shard(qp.variable_lower_bounds));
1580  } else {
1581  // The formula in the LP case is simplified for better performance.
1582  shard(result.value) =
1583  (shard(current_primal_solution_) -
1584  primal_step_size *
1585  (shard(qp.objective_vector) - shard(current_dual_product_)))
1586  .cwiseMin(shard(qp.variable_upper_bounds))
1587  .cwiseMax(shard(qp.variable_lower_bounds));
1588  }
1589  shard(result.delta) =
1590  shard(result.value) - shard(current_primal_solution_);
1591  });
1592  return result;
1593 }
1594 
1595 Solver::NextSolutionAndDelta Solver::ComputeNextDualSolution(
1596  double dual_step_size, double extrapolation_factor,
1597  const NextSolutionAndDelta& next_primal_solution) const {
1598  const int64_t dual_size = ShardedWorkingQp().DualSize();
1599  NextSolutionAndDelta result = {
1600  .value = VectorXd(dual_size),
1601  .delta = VectorXd(dual_size),
1602  };
1603  const QuadraticProgram& qp = WorkingQp();
1604  VectorXd extrapolated_primal(ShardedWorkingQp().PrimalSize());
1605  ShardedWorkingQp().PrimalSharder().ParallelForEachShard(
1606  [&](const Sharder::Shard& shard) {
1607  shard(extrapolated_primal) =
1608  (shard(next_primal_solution.value) +
1609  extrapolation_factor * shard(next_primal_solution.delta));
1610  });
1611  // TODO(user): Refactor this multiplication so that we only do one matrix
1612  // vector mutiply for the primal variable. This only applies to Malitsky and
1613  // Pock and not to the adaptive step size rule.
1614  ShardedWorkingQp().TransposedConstraintMatrixSharder().ParallelForEachShard(
1615  [&](const Sharder::Shard& shard) {
1616  VectorXd temp =
1617  shard(current_dual_solution_) -
1618  dual_step_size *
1619  shard(ShardedWorkingQp().TransposedConstraintMatrix())
1620  .transpose() *
1621  extrapolated_primal;
1622  // Each element of the argument of `.cwiseMin()` is the critical point
1623  // of the respective 1D minimization problem if it's negative.
1624  // Likewise the argument to the `.cwiseMax()` is the critical point if
1625  // positive.
1626  shard(result.value) =
1627  VectorXd::Zero(temp.size())
1628  .cwiseMin(temp +
1629  dual_step_size * shard(qp.constraint_upper_bounds))
1630  .cwiseMax(temp +
1631  dual_step_size * shard(qp.constraint_lower_bounds));
1632  shard(result.delta) =
1633  (shard(result.value) - shard(current_dual_solution_));
1634  });
1635  return result;
1636 }
1637 
1638 double Solver::ComputeMovement(const VectorXd& delta_primal,
1639  const VectorXd& delta_dual) const {
1640  const double primal_movement =
1641  (0.5 * primal_weight_) *
1642  SquaredNorm(delta_primal, ShardedWorkingQp().PrimalSharder());
1643  const double dual_movement =
1644  (0.5 / primal_weight_) *
1645  SquaredNorm(delta_dual, ShardedWorkingQp().DualSharder());
1646  return primal_movement + dual_movement;
1647 }
1648 
1649 double Solver::ComputeNonlinearity(const VectorXd& delta_primal,
1650  const VectorXd& next_dual_product) const {
1651  // Lemma 1 in Chambolle and Pock includes a term with L_f, the Lipshitz
1652  // constant of f. This is zero in our formulation.
1653  return ShardedWorkingQp().PrimalSharder().ParallelSumOverShards(
1654  [&](const Sharder::Shard& shard) {
1655  return -shard(delta_primal)
1656  .dot(shard(next_dual_product) -
1657  shard(current_dual_product_));
1658  });
1659 }
1660 
1661 IterationStats Solver::CreateSimpleIterationStats(
1662  RestartChoice restart_used) const {
1663  IterationStats stats;
1664  double num_kkt_passes_per_rejected_step = 1.0;
1665  if (params_.linesearch_rule() ==
1666  PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE) {
1667  num_kkt_passes_per_rejected_step = 0.5;
1668  }
1669  stats.set_iteration_number(iterations_completed_);
1670  stats.set_cumulative_rejected_steps(num_rejected_steps_);
1671  // TODO(user): This formula doesn't account for kkt passes in major
1672  // iterations.
1673  stats.set_cumulative_kkt_matrix_passes(iterations_completed_ +
1674  num_kkt_passes_per_rejected_step *
1675  num_rejected_steps_);
1676  stats.set_cumulative_time_sec(preprocess_solver_->GetElapsedTime());
1677  stats.set_restart_used(restart_used);
1678  stats.set_step_size(step_size_);
1679  stats.set_primal_weight(primal_weight_);
1680  return stats;
1681 }
1682 
1683 double Solver::DistanceTraveledFromLastStart(
1684  const VectorXd& primal_solution, const VectorXd& dual_solution) const {
1685  return std::sqrt((0.5 * primal_weight_) *
1686  SquaredDistance(primal_solution,
1687  last_primal_start_point_,
1688  ShardedWorkingQp().PrimalSharder()) +
1689  (0.5 / primal_weight_) *
1690  SquaredDistance(dual_solution, last_dual_start_point_,
1691  ShardedWorkingQp().DualSharder()));
1692 }
1693 
1694 LocalizedLagrangianBounds Solver::ComputeLocalizedBoundsAtCurrent() const {
1695  const double distance_traveled_by_current = DistanceTraveledFromLastStart(
1696  current_primal_solution_, current_dual_solution_);
1698  ShardedWorkingQp(), current_primal_solution_, current_dual_solution_,
1699  PrimalDualNorm::kEuclideanNorm, primal_weight_,
1700  distance_traveled_by_current,
1701  /*primal_product=*/nullptr, &current_dual_product_,
1702  params_.use_diagonal_qp_trust_region_solver(),
1703  params_.diagonal_qp_trust_region_solver_tolerance());
1704 }
1705 
1706 LocalizedLagrangianBounds Solver::ComputeLocalizedBoundsAtAverage() const {
1707  // TODO(user): These vectors are recomputed again for termination checks
1708  // and again if we eventually restart to the average.
1709  VectorXd average_primal = PrimalAverage();
1710  VectorXd average_dual = DualAverage();
1711 
1712  const double distance_traveled_by_average =
1713  DistanceTraveledFromLastStart(average_primal, average_dual);
1714 
1716  ShardedWorkingQp(), average_primal, average_dual,
1717  PrimalDualNorm::kEuclideanNorm, primal_weight_,
1718  distance_traveled_by_average,
1719  /*primal_product=*/nullptr, /*dual_product=*/nullptr,
1720  params_.use_diagonal_qp_trust_region_solver(),
1721  params_.diagonal_qp_trust_region_solver_tolerance());
1722 }
1723 
1724 bool AverageHasBetterPotential(
1725  const LocalizedLagrangianBounds& local_bounds_at_average,
1726  const LocalizedLagrangianBounds& local_bounds_at_current) {
1727  return BoundGap(local_bounds_at_average) /
1728  MathUtil::Square(local_bounds_at_average.radius) <
1729  BoundGap(local_bounds_at_current) /
1730  MathUtil::Square(local_bounds_at_current.radius);
1731 }
1732 
1733 double NormalizedGap(
1734  const LocalizedLagrangianBounds& local_bounds_at_candidate) {
1735  const double distance_traveled_by_candidate =
1736  local_bounds_at_candidate.radius;
1737  return BoundGap(local_bounds_at_candidate) / distance_traveled_by_candidate;
1738 }
1739 
1740 // TODO(user): Review / cleanup adaptive heuristic.
1741 bool Solver::ShouldDoAdaptiveRestartHeuristic(
1742  double candidate_normalized_gap) const {
1743  const double gap_reduction_ratio =
1744  candidate_normalized_gap / normalized_gap_at_last_restart_;
1745  if (gap_reduction_ratio < params_.sufficient_reduction_for_restart()) {
1746  return true;
1747  }
1748  if (gap_reduction_ratio < params_.necessary_reduction_for_restart() &&
1749  candidate_normalized_gap > normalized_gap_at_last_trial_) {
1750  // We've made the "necessary" amount of progress, and iterates appear to
1751  // be getting worse, so restart.
1752  return true;
1753  }
1754  return false;
1755 }
1756 
1757 RestartChoice Solver::DetermineDistanceBasedRestartChoice() const {
1758  // The following checks are safeguards that normally should not be triggered.
1759  if (primal_average_.NumTerms() == 0) {
1760  return RESTART_CHOICE_NO_RESTART;
1761  } else if (distance_based_restart_info_.length_of_last_restart_period == 0) {
1762  return RESTART_CHOICE_RESTART_TO_AVERAGE;
1763  }
1764  const int restart_period_length = primal_average_.NumTerms();
1765  const double distance_moved_this_restart_period_by_average =
1766  DistanceTraveledFromLastStart(primal_average_.ComputeAverage(),
1767  dual_average_.ComputeAverage());
1768  const double distance_moved_last_restart_period =
1769  distance_based_restart_info_.distance_moved_last_restart_period;
1770 
1771  // A restart should be triggered when the normalized distance traveled by
1772  // the average is at least a constant factor smaller than the last.
1773  // TODO(user): Experiment with using `.necessary_reduction_for_restart()`
1774  // as a heuristic when deciding if a restart should be triggered.
1775  if ((distance_moved_this_restart_period_by_average / restart_period_length) <
1776  params_.sufficient_reduction_for_restart() *
1778  distance_based_restart_info_.length_of_last_restart_period)) {
1779  // Restart at current solution when it yields a smaller normalized potential
1780  // function value than the average (heuristic suggested by ohinder@).
1781  if (AverageHasBetterPotential(ComputeLocalizedBoundsAtAverage(),
1782  ComputeLocalizedBoundsAtCurrent())) {
1783  return RESTART_CHOICE_RESTART_TO_AVERAGE;
1784  } else {
1785  return RESTART_CHOICE_WEIGHTED_AVERAGE_RESET;
1786  }
1787  } else {
1788  return RESTART_CHOICE_NO_RESTART;
1789  }
1790 }
1791 
1792 RestartChoice Solver::ChooseRestartToApply(const bool is_major_iteration) {
1793  if (!primal_average_.HasNonzeroWeight() &&
1794  !dual_average_.HasNonzeroWeight()) {
1795  return RESTART_CHOICE_NO_RESTART;
1796  }
1797  // TODO(user): This forced restart is very important for the performance of
1798  // `ADAPTIVE_HEURISTIC`. Test if the impact comes primarily from the first
1799  // forced restart (which would unseat a good initial starting point that could
1800  // prevent restarts early in the solve) or if it's really needed for the full
1801  // duration of the solve. If it is really needed, should we then trigger major
1802  // iterations on powers of two?
1803  const int restart_length = primal_average_.NumTerms();
1804  if (restart_length >= iterations_completed_ / 2 &&
1805  params_.restart_strategy() ==
1806  PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC) {
1807  if (AverageHasBetterPotential(ComputeLocalizedBoundsAtAverage(),
1808  ComputeLocalizedBoundsAtCurrent())) {
1809  return RESTART_CHOICE_RESTART_TO_AVERAGE;
1810  } else {
1811  return RESTART_CHOICE_WEIGHTED_AVERAGE_RESET;
1812  }
1813  }
1814  if (is_major_iteration) {
1815  switch (params_.restart_strategy()) {
1816  case PrimalDualHybridGradientParams::NO_RESTARTS:
1817  return RESTART_CHOICE_WEIGHTED_AVERAGE_RESET;
1818  case PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION:
1819  return RESTART_CHOICE_RESTART_TO_AVERAGE;
1820  case PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC: {
1821  const LocalizedLagrangianBounds local_bounds_at_average =
1822  ComputeLocalizedBoundsAtAverage();
1823  const LocalizedLagrangianBounds local_bounds_at_current =
1824  ComputeLocalizedBoundsAtCurrent();
1825  double normalized_gap;
1826  RestartChoice choice;
1827  if (AverageHasBetterPotential(local_bounds_at_average,
1828  local_bounds_at_current)) {
1829  normalized_gap = NormalizedGap(local_bounds_at_average);
1830  choice = RESTART_CHOICE_RESTART_TO_AVERAGE;
1831  } else {
1832  normalized_gap = NormalizedGap(local_bounds_at_current);
1833  choice = RESTART_CHOICE_WEIGHTED_AVERAGE_RESET;
1834  }
1835  if (ShouldDoAdaptiveRestartHeuristic(normalized_gap)) {
1836  return choice;
1837  } else {
1838  normalized_gap_at_last_trial_ = normalized_gap;
1839  return RESTART_CHOICE_NO_RESTART;
1840  }
1841  }
1842  case PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED: {
1843  return DetermineDistanceBasedRestartChoice();
1844  }
1845  default:
1846  LOG(FATAL) << "Unrecognized restart_strategy "
1847  << params_.restart_strategy();
1848  return RESTART_CHOICE_UNSPECIFIED;
1849  }
1850  } else {
1851  return RESTART_CHOICE_NO_RESTART;
1852  }
1853 }
1854 
1855 VectorXd Solver::PrimalAverage() const {
1856  if (primal_average_.HasNonzeroWeight()) {
1857  return primal_average_.ComputeAverage();
1858  } else {
1859  return current_primal_solution_;
1860  }
1861 }
1862 
1863 VectorXd Solver::DualAverage() const {
1864  if (dual_average_.HasNonzeroWeight()) {
1865  return dual_average_.ComputeAverage();
1866  } else {
1867  return current_dual_solution_;
1868  }
1869 }
1870 
1871 double Solver::ComputeNewPrimalWeight() const {
1872  const double primal_distance =
1873  Distance(current_primal_solution_, last_primal_start_point_,
1874  ShardedWorkingQp().PrimalSharder());
1875  const double dual_distance =
1876  Distance(current_dual_solution_, last_dual_start_point_,
1877  ShardedWorkingQp().DualSharder());
1878  // This choice of a nonzero tolerance balances performance and numerical
1879  // issues caused by very huge or very tiny weights. It was picked as the best
1880  // among {0.0, 1.0e-20, 2.0e-16, 1.0e-10, 1.0e-5} on the preprocessed MIPLIB
1881  // dataset. The effect of changing this value is relatively minor overall.
1882  constexpr double kNonzeroTol = 1.0e-10;
1883  if (primal_distance <= kNonzeroTol || primal_distance >= 1.0 / kNonzeroTol ||
1884  dual_distance <= kNonzeroTol || dual_distance >= 1.0 / kNonzeroTol) {
1885  return primal_weight_;
1886  }
1887  const double smoothing_param = params_.primal_weight_update_smoothing();
1888  const double unsmoothed_new_primal_weight = dual_distance / primal_distance;
1889  const double new_primal_weight =
1890  std::exp(smoothing_param * std::log(unsmoothed_new_primal_weight) +
1891  (1.0 - smoothing_param) * std::log(primal_weight_));
1892  LOG_IF(INFO, params_.verbosity_level() >= 4)
1893  << "New computed primal weight is " << new_primal_weight
1894  << " at iteration " << iterations_completed_;
1895  return new_primal_weight;
1896 }
1897 
1898 SolverResult Solver::PickSolutionAndConstructSolverResult(
1899  VectorXd primal_solution, VectorXd dual_solution,
1900  const IterationStats& stats, TerminationReason termination_reason,
1901  PointType output_type, SolveLog solve_log) const {
1902  switch (output_type) {
1903  case POINT_TYPE_CURRENT_ITERATE:
1904  AssignVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder(),
1905  primal_solution);
1906  AssignVector(current_dual_solution_, ShardedWorkingQp().DualSharder(),
1907  dual_solution);
1908  break;
1909  case POINT_TYPE_ITERATE_DIFFERENCE:
1910  AssignVector(current_primal_delta_, ShardedWorkingQp().PrimalSharder(),
1911  primal_solution);
1912  AssignVector(current_dual_delta_, ShardedWorkingQp().DualSharder(),
1913  dual_solution);
1914  break;
1915  case POINT_TYPE_AVERAGE_ITERATE:
1916  case POINT_TYPE_PRESOLVER_SOLUTION:
1917  break;
1918  default:
1919  // Default to average whenever `output_type` is `POINT_TYPE_NONE`.
1920  output_type = POINT_TYPE_AVERAGE_ITERATE;
1921  break;
1922  }
1923  return ConstructSolverResult(
1924  std::move(primal_solution), std::move(dual_solution), stats,
1925  termination_reason, output_type, std::move(solve_log));
1926 }
1927 
1928 void Solver::ApplyRestartChoice(const RestartChoice restart_to_apply) {
1929  switch (restart_to_apply) {
1930  case RESTART_CHOICE_UNSPECIFIED:
1931  case RESTART_CHOICE_NO_RESTART:
1932  return;
1933  case RESTART_CHOICE_WEIGHTED_AVERAGE_RESET:
1934  LOG_IF(INFO, params_.verbosity_level() >= 4)
1935  << "Restarted to current on iteration " << iterations_completed_
1936  << " after " << primal_average_.NumTerms() << " iterations";
1937  break;
1938  case RESTART_CHOICE_RESTART_TO_AVERAGE:
1939  LOG_IF(INFO, params_.verbosity_level() >= 4)
1940  << "Restarted to average on iteration " << iterations_completed_
1941  << " after " << primal_average_.NumTerms() << " iterations";
1942  current_primal_solution_ = primal_average_.ComputeAverage();
1943  current_dual_solution_ = dual_average_.ComputeAverage();
1944  current_dual_product_ = TransposedMatrixVectorProduct(
1945  WorkingQp().constraint_matrix, current_dual_solution_,
1946  ShardedWorkingQp().ConstraintMatrixSharder());
1947  break;
1948  }
1949  primal_weight_ = ComputeNewPrimalWeight();
1950  ratio_last_two_step_sizes_ = 1;
1951  if (params_.restart_strategy() ==
1952  PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC) {
1953  // It's important for the theory that the distances here are calculated
1954  // given the new primal weight.
1955  const LocalizedLagrangianBounds local_bounds_at_last_restart =
1956  ComputeLocalizedBoundsAtCurrent();
1957  const double distance_traveled_since_last_restart =
1958  local_bounds_at_last_restart.radius;
1959  normalized_gap_at_last_restart_ = BoundGap(local_bounds_at_last_restart) /
1960  distance_traveled_since_last_restart;
1961  normalized_gap_at_last_trial_ = std::numeric_limits<double>::infinity();
1962  } else if (params_.restart_strategy() ==
1963  PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED) {
1964  // Update parameters for distance-based restarts.
1965  distance_based_restart_info_ = {
1966  .distance_moved_last_restart_period = DistanceTraveledFromLastStart(
1967  current_primal_solution_, current_dual_solution_),
1968  .length_of_last_restart_period = primal_average_.NumTerms()};
1969  }
1970  primal_average_.Clear();
1971  dual_average_.Clear();
1972  AssignVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder(),
1973  /*dest=*/last_primal_start_point_);
1974  AssignVector(current_dual_solution_, ShardedWorkingQp().DualSharder(),
1975  /*dest=*/last_dual_start_point_);
1976 }
1977 
1978 std::optional<SolverResult> Solver::MajorIterationAndTerminationCheck(
1979  bool force_numerical_termination, const std::atomic<bool>* interrupt_solve,
1980  SolveLog& solve_log) {
1981  const int major_iteration_cycle =
1982  iterations_completed_ % params_.major_iteration_frequency();
1983  const bool is_major_iteration =
1984  major_iteration_cycle == 0 && iterations_completed_ > 0;
1985  // Just decide what to do for now. The actual restart, if any, is
1986  // performed after the termination check.
1987  const RestartChoice restart = force_numerical_termination
1988  ? RESTART_CHOICE_NO_RESTART
1989  : ChooseRestartToApply(is_major_iteration);
1990  IterationStats stats = CreateSimpleIterationStats(restart);
1991  const bool check_termination =
1992  major_iteration_cycle % params_.termination_check_frequency() == 0 ||
1993  CheckSimpleTerminationCriteria(params_.termination_criteria(), stats,
1994  interrupt_solve)
1995  .has_value() ||
1996  force_numerical_termination;
1997  // We check termination on every major iteration.
1998  DCHECK(!is_major_iteration || check_termination);
1999  if (check_termination) {
2000  // Check for termination and update iteration stats with both simple and
2001  // solution statistics. The later are computationally harder to compute and
2002  // hence only computed here.
2003  VectorXd primal_average = PrimalAverage();
2004  VectorXd dual_average = DualAverage();
2005 
2006  const std::optional<TerminationReasonAndPointType>
2007  maybe_termination_reason =
2008  preprocess_solver_->UpdateIterationStatsAndCheckTermination(
2009  params_, force_numerical_termination, current_primal_solution_,
2010  current_dual_solution_,
2011  primal_average_.HasNonzeroWeight() ? &primal_average : nullptr,
2012  dual_average_.HasNonzeroWeight() ? &dual_average : nullptr,
2013  current_primal_delta_.size() > 0 ? &current_primal_delta_
2014  : nullptr,
2015  current_dual_delta_.size() > 0 ? &current_dual_delta_ : nullptr,
2016  last_primal_start_point_, last_dual_start_point_,
2017  interrupt_solve, stats);
2018  if (params_.record_iteration_stats()) {
2019  *solve_log.add_iteration_stats() = stats;
2020  }
2021  // We've terminated.
2022  if (maybe_termination_reason.has_value()) {
2023  return PickSolutionAndConstructSolverResult(
2024  std::move(primal_average), std::move(dual_average), stats,
2025  maybe_termination_reason->reason, maybe_termination_reason->type,
2026  std::move(solve_log));
2027  }
2028  } else if (params_.record_iteration_stats()) {
2029  // Record simple iteration stats only.
2030  *solve_log.add_iteration_stats() = stats;
2031  }
2032  ApplyRestartChoice(restart);
2033  return std::nullopt;
2034 }
2035 
2036 void Solver::ResetAverageToCurrent() {
2037  primal_average_.Clear();
2038  dual_average_.Clear();
2039  primal_average_.Add(current_primal_solution_, /*weight=*/1.0);
2040  dual_average_.Add(current_dual_solution_, /*weight=*/1.0);
2041 }
2042 
2043 void Solver::LogNumericalTermination() const {
2044  LOG(WARNING) << "Forced numerical termination at iteration "
2045  << iterations_completed_;
2046 }
2047 
2048 void Solver::LogInnerIterationLimitHit() const {
2049  LOG(WARNING) << "Inner iteration limit reached at iteration "
2050  << iterations_completed_;
2051 }
2052 
2053 InnerStepOutcome Solver::TakeMalitskyPockStep() {
2054  InnerStepOutcome outcome = InnerStepOutcome::kSuccessful;
2055  const double primal_step_size = step_size_ / primal_weight_;
2056  NextSolutionAndDelta next_primal_solution =
2057  ComputeNextPrimalSolution(primal_step_size);
2058  // The theory by Malitsky and Pock holds for any new_step_size in the interval
2059  // [`step_size`, `step_size` * sqrt(1 + `ratio_last_two_step_sizes_`)].
2060  // `dilating_coeff` determines where in this interval the new step size lands.
2061  // NOTE: Malitsky and Pock use theta for `ratio_last_two_step_sizes`.
2062  double dilating_coeff =
2063  1 + (params_.malitsky_pock_parameters().step_size_interpolation() *
2064  (sqrt(1 + ratio_last_two_step_sizes_) - 1));
2065  double new_primal_step_size = primal_step_size * dilating_coeff;
2066  double step_size_downscaling =
2067  params_.malitsky_pock_parameters().step_size_downscaling_factor();
2068  double contraction_factor =
2069  params_.malitsky_pock_parameters().linesearch_contraction_factor();
2070  const double dual_weight = primal_weight_ * primal_weight_;
2071  int inner_iterations = 0;
2072  for (bool accepted_step = false; !accepted_step; ++inner_iterations) {
2073  if (inner_iterations >= 60) {
2074  LogInnerIterationLimitHit();
2075  ResetAverageToCurrent();
2076  outcome = InnerStepOutcome::kForceNumericalTermination;
2077  break;
2078  }
2079  const double new_last_two_step_sizes_ratio =
2080  new_primal_step_size / primal_step_size;
2081  NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2082  dual_weight * new_primal_step_size, new_last_two_step_sizes_ratio,
2083  next_primal_solution);
2084 
2085  VectorXd next_dual_product = TransposedMatrixVectorProduct(
2086  WorkingQp().constraint_matrix, next_dual_solution.value,
2087  ShardedWorkingQp().ConstraintMatrixSharder());
2088  double delta_dual_norm =
2089  Norm(next_dual_solution.delta, ShardedWorkingQp().DualSharder());
2090  double delta_dual_prod_norm =
2091  Distance(current_dual_product_, next_dual_product,
2092  ShardedWorkingQp().PrimalSharder());
2093  if (primal_weight_ * new_primal_step_size * delta_dual_prod_norm <=
2094  contraction_factor * delta_dual_norm) {
2095  // Accept new_step_size as a good step.
2096  step_size_ = new_primal_step_size * primal_weight_;
2097  ratio_last_two_step_sizes_ = new_last_two_step_sizes_ratio;
2098  // Malitsky and Pock guarantee uses a nonsymmetric weighted average,
2099  // the primal variable average involves the initial point, while the dual
2100  // doesn't. See Theorem 2 in https://arxiv.org/pdf/1608.08883.pdf for
2101  // details.
2102  if (!primal_average_.HasNonzeroWeight()) {
2103  primal_average_.Add(
2104  current_primal_solution_,
2105  /*weight=*/new_primal_step_size * new_last_two_step_sizes_ratio);
2106  }
2107 
2108  current_primal_solution_ = std::move(next_primal_solution.value);
2109  current_dual_solution_ = std::move(next_dual_solution.value);
2110  current_dual_product_ = std::move(next_dual_product);
2111  primal_average_.Add(current_primal_solution_,
2112  /*weight=*/new_primal_step_size);
2113  dual_average_.Add(current_dual_solution_,
2114  /*weight=*/new_primal_step_size);
2115  const double movement =
2116  ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2117  if (movement == 0.0) {
2118  LogNumericalTermination();
2119  ResetAverageToCurrent();
2120  outcome = InnerStepOutcome::kForceNumericalTermination;
2121  } else if (movement > kDivergentMovement) {
2122  LogNumericalTermination();
2123  outcome = InnerStepOutcome::kForceNumericalTermination;
2124  }
2125  current_primal_delta_ = std::move(next_primal_solution.delta);
2126  current_dual_delta_ = std::move(next_dual_solution.delta);
2127  break;
2128  } else {
2129  new_primal_step_size = step_size_downscaling * new_primal_step_size;
2130  }
2131  }
2132  // `inner_iterations` isn't incremented for the accepted step.
2133  num_rejected_steps_ += inner_iterations;
2134  return outcome;
2135 }
2136 
2137 InnerStepOutcome Solver::TakeAdaptiveStep() {
2138  bool force_numerical_termination = false;
2139  for (bool accepted_step = false; !accepted_step;) {
2140  const double primal_step_size = step_size_ / primal_weight_;
2141  const double dual_step_size = step_size_ * primal_weight_;
2142  NextSolutionAndDelta next_primal_solution =
2143  ComputeNextPrimalSolution(primal_step_size);
2144  NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2145  dual_step_size, /*extrapolation_factor=*/1.0, next_primal_solution);
2146  const double movement =
2147  ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2148  if (movement == 0.0) {
2149  LogNumericalTermination();
2150  ResetAverageToCurrent();
2151  force_numerical_termination = true;
2152  break;
2153  } else if (movement > kDivergentMovement) {
2154  LogNumericalTermination();
2155  force_numerical_termination = true;
2156  break;
2157  }
2158  VectorXd next_dual_product = TransposedMatrixVectorProduct(
2159  WorkingQp().constraint_matrix, next_dual_solution.value,
2160  ShardedWorkingQp().ConstraintMatrixSharder());
2161  const double nonlinearity =
2162  ComputeNonlinearity(next_primal_solution.delta, next_dual_product);
2163 
2164  // See equation (5) in https://arxiv.org/pdf/2106.04756.pdf.
2165  const double step_size_limit =
2166  nonlinearity > 0 ? movement / nonlinearity
2167  : std::numeric_limits<double>::infinity();
2168 
2169  if (step_size_ <= step_size_limit) {
2170  current_primal_solution_ = std::move(next_primal_solution.value);
2171  current_dual_solution_ = std::move(next_dual_solution.value);
2172  current_dual_product_ = std::move(next_dual_product);
2173  current_primal_delta_ = std::move(next_primal_solution.delta);
2174  current_dual_delta_ = std::move(next_dual_solution.delta);
2175  primal_average_.Add(current_primal_solution_, /*weight=*/step_size_);
2176  dual_average_.Add(current_dual_solution_, /*weight=*/step_size_);
2177  accepted_step = true;
2178  }
2179  const double total_steps_attempted =
2180  num_rejected_steps_ + iterations_completed_ + 1;
2181  // Our step sizes are a factor 1 - (`total_steps_attempted` + 1)^(-
2182  // `step_size_reduction_exponent`) smaller than they could be as a margin to
2183  // reduce rejected steps.
2184  const double first_term =
2185  (1 - std::pow(total_steps_attempted + 1.0,
2186  -params_.adaptive_linesearch_parameters()
2187  .step_size_reduction_exponent())) *
2188  step_size_limit;
2189  const double second_term =
2190  (1 + std::pow(total_steps_attempted + 1.0,
2191  -params_.adaptive_linesearch_parameters()
2192  .step_size_growth_exponent())) *
2193  step_size_;
2194  // From the first term when we have to reject a step, `step_size_`
2195  // decreases by a factor of at least 1 - (`total_steps_attempted` + 1)^(-
2196  // `step_size_reduction_exponent`). From the second term we increase
2197  // `step_size_` by a factor of at most 1 + (`total_steps_attempted` +
2198  // 1)^(-`step_size_growth_exponent`) Therefore if more than order
2199  // (`total_steps_attempted` + 1)^(`step_size_reduction_exponent`
2200  // - `step_size_growth_exponent`) fraction of the time we have a rejected
2201  // step, we overall decrease `step_size_`. When `step_size_` is
2202  // sufficiently small we stop having rejected steps.
2203  step_size_ = std::min(first_term, second_term);
2204  if (!accepted_step) {
2205  ++num_rejected_steps_;
2206  }
2207  }
2208  if (force_numerical_termination) {
2209  return InnerStepOutcome::kForceNumericalTermination;
2210  }
2211  return InnerStepOutcome::kSuccessful;
2212 }
2213 
2214 InnerStepOutcome Solver::TakeConstantSizeStep() {
2215  const double primal_step_size = step_size_ / primal_weight_;
2216  const double dual_step_size = step_size_ * primal_weight_;
2217  NextSolutionAndDelta next_primal_solution =
2218  ComputeNextPrimalSolution(primal_step_size);
2219  NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution(
2220  dual_step_size, /*extrapolation_factor=*/1.0, next_primal_solution);
2221  const double movement =
2222  ComputeMovement(next_primal_solution.delta, next_dual_solution.delta);
2223  if (movement == 0.0) {
2224  LogNumericalTermination();
2225  ResetAverageToCurrent();
2226  return InnerStepOutcome::kForceNumericalTermination;
2227  } else if (movement > kDivergentMovement) {
2228  LogNumericalTermination();
2229  return InnerStepOutcome::kForceNumericalTermination;
2230  }
2231  VectorXd next_dual_product = TransposedMatrixVectorProduct(
2232  WorkingQp().constraint_matrix, next_dual_solution.value,
2233  ShardedWorkingQp().ConstraintMatrixSharder());
2234  current_primal_solution_ = std::move(next_primal_solution.value);
2235  current_dual_solution_ = std::move(next_dual_solution.value);
2236  current_dual_product_ = std::move(next_dual_product);
2237  current_primal_delta_ = std::move(next_primal_solution.delta);
2238  current_dual_delta_ = std::move(next_dual_solution.delta);
2239  primal_average_.Add(current_primal_solution_, /*weight=*/step_size_);
2240  dual_average_.Add(current_dual_solution_, /*weight=*/step_size_);
2241  return InnerStepOutcome::kSuccessful;
2242 }
2243 
2244 SolverResult Solver::Solve(const std::atomic<bool>* interrupt_solve,
2245  SolveLog solve_log) {
2246  last_primal_start_point_ =
2247  CloneVector(current_primal_solution_, ShardedWorkingQp().PrimalSharder());
2248  last_dual_start_point_ =
2249  CloneVector(current_dual_solution_, ShardedWorkingQp().DualSharder());
2250  // Note: Any cached values computed here also need to be recomputed after a
2251  // restart.
2252 
2253  ratio_last_two_step_sizes_ = 1;
2254  current_dual_product_ = TransposedMatrixVectorProduct(
2255  WorkingQp().constraint_matrix, current_dual_solution_,
2256  ShardedWorkingQp().ConstraintMatrixSharder());
2257 
2258  // This is set to true if we can't proceed any more because of numerical
2259  // issues. We may or may not have found the optimal solution.
2260  bool force_numerical_termination = false;
2261 
2262  num_rejected_steps_ = 0;
2263 
2264  for (iterations_completed_ = 0;; ++iterations_completed_) {
2265  // This code performs the logic of the major iterations and termination
2266  // checks. It may modify the current solution and primal weight (e.g., when
2267  // performing a restart).
2268  const std::optional<SolverResult> maybe_result =
2269  MajorIterationAndTerminationCheck(force_numerical_termination,
2270  interrupt_solve, solve_log);
2271  if (maybe_result.has_value()) {
2272  return maybe_result.value();
2273  }
2274 
2275  // TODO(user): If we use a step rule that could reject many steps in a
2276  // row, we should add a termination check within this loop also. For the
2277  // Malitsky and Pock rule, we perform a termination check and declare
2278  // NUMERICAL_ERROR whenever we hit 60 inner iterations.
2279  InnerStepOutcome outcome;
2280  switch (params_.linesearch_rule()) {
2281  case PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE:
2282  outcome = TakeMalitskyPockStep();
2283  break;
2284  case PrimalDualHybridGradientParams::ADAPTIVE_LINESEARCH_RULE:
2285  outcome = TakeAdaptiveStep();
2286  break;
2287  case PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE:
2288  outcome = TakeConstantSizeStep();
2289  break;
2290  default:
2291  LOG(FATAL) << "Unrecognized linesearch rule "
2292  << params_.linesearch_rule();
2293  }
2294  if (outcome == InnerStepOutcome::kForceNumericalTermination) {
2295  force_numerical_termination = true;
2296  }
2297  } // loop over iterations
2298 }
2299 
2300 } // namespace
2301 
2303  QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
2304  const std::atomic<bool>* interrupt_solve,
2305  IterationStatsCallback iteration_stats_callback) {
2306  return PrimalDualHybridGradient(std::move(qp), params, std::nullopt,
2307  interrupt_solve,
2308  std::move(iteration_stats_callback));
2309 }
2310 
2312  QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
2313  std::optional<PrimalAndDualSolution> initial_solution,
2314  const std::atomic<bool>* interrupt_solve,
2315  IterationStatsCallback iteration_stats_callback) {
2316  const absl::Status params_status =
2318  if (!params_status.ok()) {
2319  return ErrorSolverResult(TERMINATION_REASON_INVALID_PARAMETER,
2320  params_status.ToString());
2321  }
2322  if (!qp.constraint_matrix.isCompressed()) {
2323  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
2324  "constraint_matrix must be in compressed format. "
2325  "Call constraint_matrix.makeCompressed()");
2326  }
2327  const absl::Status dimensions_status = ValidateQuadraticProgramDimensions(qp);
2328  if (!dimensions_status.ok()) {
2329  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
2330  dimensions_status.ToString());
2331  }
2332  if (!HasValidBounds(qp)) {
2333  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
2334  "The input problem has inconsistent bounds.");
2335  }
2336  if (qp.objective_scaling_factor == 0) {
2337  return ErrorSolverResult(TERMINATION_REASON_INVALID_PROBLEM,
2338  "The objective scaling factor cannot be zero.");
2339  }
2340  PreprocessSolver solver(std::move(qp), params);
2341  return solver.PreprocessAndSolve(params, std::move(initial_solution),
2342  interrupt_solve,
2343  std::move(iteration_stats_callback));
2344 }
2345 
2346 namespace internal {
2347 
2349  const PrimalAndDualSolution& solution) {
2350  glop::ProblemSolution glop_solution(
2351  glop::RowIndex(solution.dual_solution.size()),
2352  glop::ColIndex(solution.primal_solution.size()));
2353  // This doesn't matter much as glop's preprocessor doesn't use this much.
2354  // We pick IMPRECISE since we are often calling this code early in the solve.
2355  glop_solution.status = glop::ProblemStatus::IMPRECISE;
2356  for (glop::RowIndex i{0}; i.value() < solution.dual_solution.size(); ++i) {
2357  if (qp.constraint_lower_bounds[i.value()] ==
2358  qp.constraint_upper_bounds[i.value()]) {
2359  glop_solution.constraint_statuses[i] =
2360  glop::ConstraintStatus::FIXED_VALUE;
2361  } else if (solution.dual_solution[i.value()] > 0) {
2362  glop_solution.constraint_statuses[i] =
2363  glop::ConstraintStatus::AT_LOWER_BOUND;
2364  } else if (solution.dual_solution[i.value()] < 0) {
2365  glop_solution.constraint_statuses[i] =
2366  glop::ConstraintStatus::AT_UPPER_BOUND;
2367  } else {
2368  glop_solution.constraint_statuses[i] = glop::ConstraintStatus::BASIC;
2369  }
2370  }
2371 
2372  for (glop::ColIndex i{0}; i.value() < solution.primal_solution.size(); ++i) {
2373  const bool at_lb = solution.primal_solution[i.value()] <=
2374  qp.variable_lower_bounds[i.value()];
2375  const bool at_ub = solution.primal_solution[i.value()] >=
2376  qp.variable_upper_bounds[i.value()];
2377  // Note that `ShardedWeightedAverage` is designed so that variables at their
2378  // bounds will be exactly at their bounds even with floating-point roundoff.
2379  if (at_lb) {
2380  if (at_ub) {
2381  glop_solution.variable_statuses[i] = glop::VariableStatus::FIXED_VALUE;
2382  } else {
2383  glop_solution.variable_statuses[i] =
2384  glop::VariableStatus::AT_LOWER_BOUND;
2385  }
2386  } else {
2387  if (at_ub) {
2388  glop_solution.variable_statuses[i] =
2389  glop::VariableStatus::AT_UPPER_BOUND;
2390  } else {
2391  glop_solution.variable_statuses[i] = glop::VariableStatus::BASIC;
2392  }
2393  }
2394  }
2395  return glop_solution;
2396 }
2397 
2398 } // namespace internal
2399 
2400 } // namespace operations_research::pdlp
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
void Start()
Definition: timer.h:31
double Get() const
Definition: timer.h:45
GRBmodel * model
Fractional Square(Fractional f)
Fractional SquaredNorm(const SparseColumn &v)
void MPModelProtoToLinearProgram(const MPModelProto &input, LinearProgram *output)
Definition: proto_utils.cc:51
StrictITIVector< ColIndex, Fractional > DenseRow
Definition: lp_types.h:341
void LinearProgramToMPModelProto(const LinearProgram &input, MPModelProto *output)
Definition: proto_utils.cc:20
StrictITIVector< RowIndex, Fractional > DenseColumn
Definition: lp_types.h:370
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
glop::ProblemSolution ComputeStatuses(const QuadraticProgram &qp, const PrimalAndDualSolution &solution)
absl::StatusOr< QuadraticProgram > QpFromMpModelProto(const MPModelProto &proto, bool relax_integer_variables, bool include_names)
double EpsilonRatio(const double epsilon_absolute, const double epsilon_relative)
Definition: termination.cc:231
void SetZero(const Sharder &sharder, VectorXd &dest)
Definition: sharder.cc:173
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram &qp)
double SquaredDistance(const VectorXd &vector1, const VectorXd &vector2, const Sharder &sharder)
Definition: sharder.cc:252
double LInfNorm(const VectorXd &vector, const Sharder &sharder)
Definition: sharder.cc:230
double Distance(const VectorXd &vector1, const VectorXd &vector2, const Sharder &sharder)
Definition: sharder.cc:259
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:158
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, PointType candidate_type)
SolverResult PrimalDualHybridGradient(QuadraticProgram qp, const PrimalDualHybridGradientParams &params, std::optional< PrimalAndDualSolution > initial_solution, const std::atomic< bool > *interrupt_solve, IterationStatsCallback iteration_stats_callback)
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)
TerminationCriteria::DetailedOptimalityCriteria EffectiveOptimalityCriteria(const TerminationCriteria &termination_criteria)
Definition: termination.cc:127
bool HasValidBounds(const QuadraticProgram &qp)
absl::StatusOr< MPModelProto > QpToMpModelProto(const QuadraticProgram &qp)
std::optional< TerminationReasonAndPointType > CheckIterateTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const QuadraticProgramBoundNorms &bound_norms, const bool force_numerical_termination)
Definition: termination.cc:187
RelativeConvergenceInformation ComputeRelativeResiduals(const TerminationCriteria::DetailedOptimalityCriteria &optimality_criteria, const ConvergenceInformation &stats, const QuadraticProgramBoundNorms &bound_norms)
Definition: termination.cc:240
bool IsLinearProgram(const QuadraticProgram &qp)
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)
void ProjectToDualVariableBounds(const ShardedQuadraticProgram &sharded_qp, VectorXd &dual)
void CoefficientWiseProductInPlace(const VectorXd &scale, const Sharder &sharder, VectorXd &dest)
Definition: sharder.cc:211
void CoefficientWiseQuotientInPlace(const VectorXd &scale, const Sharder &sharder, VectorXd &dest)
Definition: sharder.cc:218
std::optional< TerminationReasonAndPointType > CheckSimpleTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const std::atomic< bool > *interrupt_solve)
Definition: termination.cc:162
VectorXd CloneVector(const VectorXd &vec, const Sharder &sharder)
Definition: sharder.cc:205
ScalingVectors ApplyRescaling(const RescalingOptions &rescaling_options, ShardedQuadraticProgram &sharded_qp)
QuadraticProgramStats ComputeStats(const ShardedQuadraticProgram &qp, const double infinite_constraint_bound_threshold)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
QuadraticProgramBoundNorms BoundNormsFromProblemStats(const QuadraticProgramStats &stats)
Definition: termination.cc:222
void ProjectToPrimalVariableBounds(const ShardedQuadraticProgram &sharded_qp, VectorXd &primal)
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:248
VectorXd ZeroVector(const Sharder &sharder)
Definition: sharder.cc:179
void AssignVector(const VectorXd &vec, const Sharder &sharder, VectorXd &dest)
Definition: sharder.cc:199
VectorXd OnesVector(const Sharder &sharder)
Definition: sharder.cc:185
double BoundGap(const LocalizedLagrangianBounds &bounds)
Definition: trust_region.h:113
absl::Status ValidatePrimalDualHybridGradientParams(const PrimalDualHybridGradientParams &params)
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)
const absl::string_view ToString(MPSolver::OptimizationProblemType optimization_problem_type)
int64_t Zero()
NOLINT.
bool presolved_problem_was_maximization
glop::MainLpPreprocessor preprocessor
VectorXd value
const VectorXd trivial_row_scaling_vec
ShardedQuadraticProgram sharded_original_qp
const VectorXd trivial_col_scaling_vec
double distance_moved_last_restart_period
VectorXd delta
int length_of_last_restart_period
glop::GlopParameters preprocessor_parameters
ConstraintStatusColumn constraint_statuses
Definition: lp_data.h:690
Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > constraint_matrix
std::string message
Definition: trace.cc:399