OR-Tools  9.6
primal_dual_hybrid_gradient_test.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 <limits>
21 #include <optional>
22 #include <string>
23 #include <tuple>
24 #include <utility>
25 #include <vector>
26 
27 #include "Eigen/Core"
28 #include "Eigen/SparseCore"
29 #include "absl/log/check.h"
30 #include "absl/status/status.h"
31 #include "absl/status/statusor.h"
32 #include "absl/strings/str_cat.h"
33 #include "gmock/gmock.h"
34 #include "gtest/gtest.h"
35 #include "ortools/base/logging.h"
36 #include "ortools/glop/parameters.pb.h"
37 #include "ortools/linear_solver/linear_solver.pb.h"
44 #include "ortools/pdlp/solve_log.pb.h"
45 #include "ortools/pdlp/solvers.pb.h"
47 #include "ortools/pdlp/test_util.h"
48 
49 namespace operations_research::pdlp {
50 namespace {
51 
54 using ::testing::_;
55 using ::testing::AnyNumber;
56 using ::testing::AnyOf;
57 using ::testing::DoubleNear;
58 using ::testing::ElementsAre;
59 using ::testing::Eq;
60 using ::testing::HasSubstr;
61 using ::testing::IsEmpty;
63 using ::testing::SizeIs;
64 
65 const double kInfinity = std::numeric_limits<double>::infinity();
66 PrimalDualHybridGradientParams CreateSolverParams(
67  const int iteration_limit, const double eps_optimal_absolute,
68  const bool enable_scaling, const int num_threads,
69  const bool use_iteration_limit, const bool use_malitsky_pock_linesearch,
70  const bool use_diagonal_qp_trust_region_solver) {
71  PrimalDualHybridGradientParams params;
72  if (!enable_scaling) {
73  params.set_l2_norm_rescaling(false);
74  params.set_l_inf_ruiz_iterations(0);
75  }
76  if (use_malitsky_pock_linesearch) {
77  params.set_linesearch_rule(
78  PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE);
79  }
80 
81  params.mutable_termination_criteria()
82  ->mutable_simple_optimality_criteria()
83  ->set_eps_optimal_relative(0.0);
84  if (use_iteration_limit) {
85  // This effectively forces convergence on the iteration limit only.
86  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
87  params.mutable_termination_criteria()
88  ->mutable_simple_optimality_criteria()
89  ->set_eps_optimal_absolute(0.0);
90  } else {
91  params.mutable_termination_criteria()
92  ->mutable_simple_optimality_criteria()
93  ->set_eps_optimal_absolute(eps_optimal_absolute);
94  }
95  if (use_diagonal_qp_trust_region_solver) {
96  params.set_use_diagonal_qp_trust_region_solver(true);
97  params.set_diagonal_qp_trust_region_solver_tolerance(1.0e-8);
98  }
99  params.set_num_threads(num_threads);
100  return params;
101 }
102 
103 // Verifies expected termination reason and iteration count for an instance
104 // where an optimal solution exists.
105 // `params` must have been generated by `CreateSolverParams()` with the same
106 // `use_iteration_limit`.
107 // Intended usage:
108 // const bool use_iteration_limit = ...;
109 // PrimalDualHybridGradientParams params =
110 // CreateSolverParams(..., use_iteration_limit, ...);
111 // SolverResult output = PrimalDualHybridGradient(..., params);
112 // VerifyTerminationReasonAndIterationCount(params, output,
113 // use_iteration_limit);
114 void VerifyTerminationReasonAndIterationCount(
115  const PrimalDualHybridGradientParams& params, const SolverResult& output,
116  const bool use_iteration_limit) {
117  if (use_iteration_limit) {
118  // When a PDHG step has zero length PDHG can no longer make progress and
119  // hence terminates immediately. In theory a zero step length implies
120  // optimality but in practice PDHG terminates with a reason of OPTIMAL if
121  // the optimality checks pass and NUMERICAL_ERROR otherwise.
122  // When `use_iteration_limit==true`, `CreateSolverParams()` sets all the
123  // epsilons to 0, which makes the optimality checks harder to pass but not
124  // impossible. Both OPTIMAL and NUMERICAL_ERROR are therefore ok termination
125  // reasons.
126  EXPECT_THAT(
127  output.solve_log.termination_reason(),
128  AnyOf(TERMINATION_REASON_ITERATION_LIMIT,
129  TERMINATION_REASON_NUMERICAL_ERROR, TERMINATION_REASON_OPTIMAL));
130  if (output.solve_log.termination_reason() ==
131  TERMINATION_REASON_ITERATION_LIMIT) {
132  EXPECT_EQ(output.solve_log.iteration_count(),
133  params.termination_criteria().iteration_limit());
134  } else {
135  EXPECT_LE(output.solve_log.iteration_count(),
136  params.termination_criteria().iteration_limit());
137  }
138  } else {
139  EXPECT_EQ(output.solve_log.termination_reason(),
140  TERMINATION_REASON_OPTIMAL);
141  EXPECT_LE(output.solve_log.iteration_count(),
142  params.termination_criteria().iteration_limit());
143  }
144 }
145 
146 // Verifies the primal and dual objective values.
147 void VerifyObjectiveValues(const SolverResult& result,
148  const double objective_value,
149  const double tolerance) {
150  const auto& convergence_info = GetConvergenceInformation(
151  result.solve_log.solution_stats(), result.solve_log.solution_type());
152  ASSERT_TRUE(convergence_info.has_value());
153  EXPECT_THAT(convergence_info->primal_objective(),
154  DoubleNear(objective_value, tolerance));
155  EXPECT_THAT(convergence_info->dual_objective(),
156  DoubleNear(objective_value, tolerance));
157 }
158 
159 class PrimalDualHybridGradientLPTest
160  : public testing::TestWithParam<
161  std::tuple</*enable_scaling=*/bool, /*num_threads=*/int,
162  /*use_iteration_limit=*/bool,
163  /*use_malitsky_pock_linesearch=*/bool>> {
164  protected:
165  PrimalDualHybridGradientParams CreateSolverParamsForFixture(
166  const int iteration_limit, const double eps_optimal_absolute) {
167  const auto [enable_scaling, num_threads, use_iteration_limit,
168  use_malitsky_pock_linesearch] = GetParam();
169  return CreateSolverParams(iteration_limit, eps_optimal_absolute,
170  enable_scaling, num_threads, use_iteration_limit,
171  use_malitsky_pock_linesearch,
172  /*use_diagonal_qp_trust_region_solver=*/false);
173  }
174 
175  void VerifyTerminationReasonAndIterationCountForFixture(
176  const PrimalDualHybridGradientParams& params,
177  const SolverResult& output) {
178  const auto [enable_scaling, num_threads, use_iteration_limit,
179  use_malitsky_pock_linesearch] = GetParam();
180  VerifyTerminationReasonAndIterationCount(params, output,
181  use_iteration_limit);
182  }
183 };
184 
185 class PrimalDualHybridGradientDiagonalQPTest
186  : public testing::TestWithParam<
187  std::tuple</*enable_scaling=*/bool, /*num_threads=*/int,
188  /*use_iteration_limit=*/bool,
189  /*use_malitsky_pock_linesearch=*/bool,
190  /*use_diagonal_qp_trust_region_solver=*/bool>> {
191  protected:
192  PrimalDualHybridGradientParams CreateSolverParamsForFixture(
193  const int iteration_limit, const double eps_optimal_absolute) {
194  const auto [enable_scaling, num_threads, use_iteration_limit,
195  use_malitsky_pock_linesearch,
196  use_diagonal_qp_trust_region_solver] = GetParam();
197  return CreateSolverParams(iteration_limit, eps_optimal_absolute,
198  enable_scaling, num_threads, use_iteration_limit,
199  use_malitsky_pock_linesearch,
200  use_diagonal_qp_trust_region_solver);
201  }
202  void VerifyTerminationReasonAndIterationCountForFixture(
203  const PrimalDualHybridGradientParams& params,
204  const SolverResult& output) {
205  const auto [enable_scaling, num_threads, use_iteration_limit,
206  use_malitsky_pock_linesearch,
207  use_diagonal_qp_trust_region_solver] = GetParam();
208  VerifyTerminationReasonAndIterationCount(params, output,
209  use_iteration_limit);
210  }
211 };
212 
213 class PrimalDualHybridGradientVerbosityTest
214  : public testing::TestWithParam</*verbosity_level=*/int> {};
215 
216 class PresolveDualScalingTest
217  : public testing::TestWithParam<
218  std::tuple</*Dualize=*/bool,
219  /*NegateAndScaleObjective=*/bool>> {};
220 
221 INSTANTIATE_TEST_SUITE_P(
222  QP, PrimalDualHybridGradientDiagonalQPTest,
223  testing::Combine(testing::Bool(), testing::Values(1, 4), testing::Bool(),
224  testing::Bool(), testing::Bool()),
225  [](const testing::TestParamInfo<
226  PrimalDualHybridGradientDiagonalQPTest::ParamType>& info) {
227  return absl::StrCat(
228  std::get<0>(info.param) ? "Scaling" : "NoScaling", "_",
229  std::get<1>(info.param), "Threads_",
230  std::get<2>(info.param) ? "IterationLimit" : "NoIterationLimit", "_",
231  std::get<3>(info.param) ? "MalitskyPockLinesearch"
232  : "AdaptiveLinesearch",
233  "_", std::get<4>(info.param) ? "TRSolverDiag" : "TRSolverNoDiag");
234  });
235 
236 INSTANTIATE_TEST_SUITE_P(
237  LP, PrimalDualHybridGradientLPTest,
238  testing::Combine(testing::Bool(), testing::Values(1, 4), testing::Bool(),
239  testing::Bool()),
240  [](const testing::TestParamInfo<PrimalDualHybridGradientLPTest::ParamType>&
241  info) {
242  return absl::StrCat(
243  std::get<0>(info.param) ? "Scaling" : "NoScaling", "_",
244  std::get<1>(info.param), "Threads_",
245  std::get<2>(info.param) ? "IterationLimit" : "NoIterationLimit", "_",
246  std::get<3>(info.param) ? "MalitskyPockLinesearch"
247  : "AdaptiveLinesearch");
248  });
249 
250 INSTANTIATE_TEST_SUITE_P(Verbosity, PrimalDualHybridGradientVerbosityTest,
251  testing::Values(0, 1, 2, 3, 4));
252 
253 INSTANTIATE_TEST_SUITE_P(
254  PresolveDualScaling, PresolveDualScalingTest,
255  testing::Combine(testing::Bool(), testing::Bool()),
256  [](const testing::TestParamInfo<PresolveDualScalingTest::ParamType>& info) {
257  return absl::StrCat(std::get<1>(info.param) ? "Dualize" : "NoDualize",
258  std::get<0>(info.param) ? "NegateAndScaleObjective"
259  : "NoObjectiveScaling");
260  });
261 
262 TEST_P(PrimalDualHybridGradientLPTest, UnboundedVariables) {
263  const int iteration_upperbound = 980;
264  PrimalDualHybridGradientParams params =
265  CreateSolverParamsForFixture(iteration_upperbound,
266  /*eps_optimal_absolute=*/1.0e-7);
267  params.set_major_iteration_frequency(100);
268 
269  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
270  VerifyTerminationReasonAndIterationCountForFixture(params, output);
271  VerifyObjectiveValues(output, -34.0, 1.0e-6);
272  EXPECT_THAT(output.primal_solution,
273  EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
274  EXPECT_THAT(output.dual_solution,
275  EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
276 
277  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
278  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
279  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 4);
280  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 4);
281 }
282 
283 TEST_P(PrimalDualHybridGradientLPTest, Tiny) {
284  const int iteration_upperbound = 300;
285  PrimalDualHybridGradientParams params =
286  CreateSolverParamsForFixture(iteration_upperbound,
287  /*eps_optimal_absolute=*/1.0e-5);
288  params.set_major_iteration_frequency(60);
289 
290  SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
291  VerifyTerminationReasonAndIterationCountForFixture(params, output);
292  VerifyObjectiveValues(output, -1.0, 1.0e-4);
293  EXPECT_THAT(output.primal_solution,
294  EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
295  EXPECT_THAT(output.dual_solution,
296  EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
297  EXPECT_THAT(output.reduced_costs,
298  EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
299  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
300  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
301  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
302  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
303 }
304 
305 TEST_P(PrimalDualHybridGradientLPTest, CorrelationClusteringOne) {
306  const int iteration_upperbound = 9;
307  PrimalDualHybridGradientParams params =
308  CreateSolverParamsForFixture(iteration_upperbound,
309  /*eps_optimal_absolute=*/1.0e-10);
310  params.set_major_iteration_frequency(2);
311 
312  SolverResult output =
314  VerifyTerminationReasonAndIterationCountForFixture(params, output);
315  VerifyObjectiveValues(output, 1.0, 1.0e-14);
316  EXPECT_THAT(output.primal_solution,
317  EigenArrayNear<double>({1, 1, 0, 1, 0, 0}, 1.0e-14));
318  ASSERT_EQ(output.dual_solution.size(), 3);
319  // There are multiple optimal dual solutions.
320  EXPECT_GE(output.dual_solution[0], 0);
321  EXPECT_GE(output.dual_solution[1], 0);
322  EXPECT_GE(output.dual_solution[2], 0);
323  EXPECT_GE(output.dual_solution[0] + output.dual_solution[1], 1 - 1.0e-14);
324 
325  const auto& convergence_information = GetConvergenceInformation(
326  output.solve_log.solution_stats(), output.solve_log.solution_type());
327  ASSERT_TRUE(convergence_information.has_value());
328  EXPECT_THAT(convergence_information->corrected_dual_objective(),
329  DoubleNear(1, 1.0e-14));
330  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 6);
331  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 6);
332  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
333  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
334 }
335 
336 TEST_P(PrimalDualHybridGradientLPTest, CorrelationClusteringStar) {
337  const int iteration_upperbound = 45;
338  PrimalDualHybridGradientParams params =
339  CreateSolverParamsForFixture(iteration_upperbound,
340  /*eps_optimal_absolute=*/1.0e-6);
341  params.set_major_iteration_frequency(5);
342 
343  SolverResult output =
345  VerifyTerminationReasonAndIterationCountForFixture(params, output);
346  VerifyObjectiveValues(output, 1.5, 1.0e-6);
347  EXPECT_THAT(output.primal_solution,
348  EigenArrayNear<double>({0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, 1.0e-6));
349  EXPECT_THAT(output.dual_solution,
350  EigenArrayNear<double>({0.5, 0.5, 0.5}, 1.0e-6));
351  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 6);
352  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 6);
353  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
354  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
355 }
356 
357 // A double-sided constraint l <= a^T x <= u where neither constraint is tight
358 // at optimum could cause the trust region solver to malfunction if we picked
359 // the wrong dual subgradient. This test verifies that we solve an instance with
360 // such a constraint quickly to high accuracy.
361 TEST_P(PrimalDualHybridGradientLPTest, InactiveTwoSidedConstraint) {
362  const int iteration_upperbound = 500;
363  PrimalDualHybridGradientParams params =
364  CreateSolverParamsForFixture(iteration_upperbound,
365  /*eps_optimal_absolute=*/1.0e-8);
366  params.set_major_iteration_frequency(60);
367 
368  QuadraticProgram qp = TestLp();
369  // This makes this constraint double-sided and inactive at the optimal
370  // solution.
371  qp.constraint_lower_bounds[1] = -10;
372  SolverResult output = PrimalDualHybridGradient(qp, params);
373  VerifyTerminationReasonAndIterationCountForFixture(params, output);
374  VerifyObjectiveValues(output, -34.0, 1.0e-6);
375  EXPECT_THAT(output.primal_solution,
376  EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-7));
377  EXPECT_THAT(output.dual_solution,
378  EigenArrayNear<double>({-2.0, 0.0, 2.375, 2.0 / 3}, 1.0e-7));
379 }
380 
381 TEST_P(PrimalDualHybridGradientLPTest, InfeasiblePrimal) {
382  // This value for `iteration_upperbound` is particularly necessary for
383  // Malistsky and Pock. The adaptive rule detects infeasibility in less than
384  // 500 iterations.
385  const int iteration_upperbound = 2000;
386  PrimalDualHybridGradientParams params =
387  CreateSolverParamsForFixture(iteration_upperbound,
388  /*eps_optimal_absolute=*/1.0e-6);
389  params.set_major_iteration_frequency(5);
390  params.mutable_termination_criteria()->set_eps_primal_infeasible(1.0e-6);
391 
392  SolverResult output =
394  EXPECT_EQ(output.solve_log.termination_reason(),
395  TERMINATION_REASON_PRIMAL_INFEASIBLE);
396  const auto& dual = output.dual_solution;
397  // The following two conditions check if the certificate is correct. For this
398  // problem the set of infeasibility certificates is equal to all the rays of
399  // the form -alpha * (1, 1) with alpha positive.
400  EXPECT_THAT(dual[0] / dual[1], DoubleNear(1, 1.0e-6));
401  EXPECT_LT(dual[1], 0.0);
402  // The reduced costs should be approximately zero. However, a small relative
403  // difference between `dual[0]` and `dual[1]` could translate to a large
404  // absolute difference, and hence large reduced costs. The following test uses
405  // the exact formula to make sure we're not adding the objective vector to the
406  // reduced costs.
407  EXPECT_THAT(output.reduced_costs,
408  EigenArrayNear<double>({std::max(dual[1] - dual[0], 0.0),
409  std::max(dual[0] - dual[1], 0.0)},
410  1.0e-6));
411  EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
412  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 2);
413  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 2);
414  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 2);
415  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 2);
416 }
417 
418 TEST_P(PrimalDualHybridGradientLPTest, InfeasibleDual) {
419  const int iteration_upperbound = 500;
420  PrimalDualHybridGradientParams params =
421  CreateSolverParamsForFixture(iteration_upperbound,
422  /*eps_optimal_absolute=*/1.0e-6);
423  params.set_major_iteration_frequency(5);
424 
425  SolverResult output =
427  EXPECT_EQ(output.solve_log.termination_reason(),
428  TERMINATION_REASON_DUAL_INFEASIBLE);
429  // The following two conditions check if the certificate is correct. For this
430  // problem the set of infeasibility certificates is equal to all the rays of
431  // the form alpha * (1, 1) with alpha positive.
432  EXPECT_THAT(output.primal_solution[0] / output.primal_solution[1],
433  DoubleNear(1, 1.0e-6));
434  EXPECT_GT(output.primal_solution[1], 0.0);
435  EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
436 }
437 
438 TEST_P(PrimalDualHybridGradientLPTest, InfeasiblePrimalDual) {
439  const int iteration_upperbound = 600;
440  PrimalDualHybridGradientParams params =
441  CreateSolverParamsForFixture(iteration_upperbound,
442  /*eps_optimal_absolute=*/1.0e-6);
443  // Adaptive restarts are disabled because they unexpectedly perform worse on
444  // this instance.
445  params.set_restart_strategy(PrimalDualHybridGradientParams::NO_RESTARTS);
446  params.set_major_iteration_frequency(5);
447 
448  SolverResult output =
450  EXPECT_THAT(output.solve_log.termination_reason(),
451  AnyOf(Eq(TERMINATION_REASON_DUAL_INFEASIBLE),
452  Eq(TERMINATION_REASON_PRIMAL_INFEASIBLE)));
453  EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
454 }
455 
456 TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp1) {
457  const int iteration_upperbound = 96;
458  PrimalDualHybridGradientParams params =
459  CreateSolverParamsForFixture(iteration_upperbound,
460  /*eps_optimal_absolute=*/1.0e-6);
461  params.set_major_iteration_frequency(12);
462 
463  SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params);
464  VerifyTerminationReasonAndIterationCountForFixture(params, output);
465  VerifyObjectiveValues(output, 6.0, 1.0e-6);
466  EXPECT_THAT(output.primal_solution,
467  EigenArrayNear<double>({1.0, 0.0}, 1.0e-6));
468  EXPECT_THAT(output.dual_solution,
469  EigenArrayNear(Eigen::ArrayXd::Constant(1, -1.0), 1.0e-6));
470  EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({4.0, 0.0}, 1.0e-6));
471  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 2);
472  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 2);
473  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 1);
474  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 1);
475 }
476 
477 TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp2) {
478  const int iteration_upperbound = 240;
479  PrimalDualHybridGradientParams params =
480  CreateSolverParamsForFixture(iteration_upperbound,
481  /*eps_optimal_absolute=*/1.0e-6);
482  params.set_major_iteration_frequency(12);
483 
484  SolverResult output = PrimalDualHybridGradient(TestDiagonalQp2(), params);
485  VerifyTerminationReasonAndIterationCountForFixture(params, output);
486  VerifyObjectiveValues(output, -5.0, 1.0e-6);
487  EXPECT_THAT(output.primal_solution,
488  EigenArrayNear<double>({3.0, 1.0}, 1.0e-6));
489  EXPECT_THAT(output.dual_solution, ElementsAre(DoubleNear(0.0, 1.0e-6)));
490  EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({0.0, 0.0}, 1.0e-6));
491 }
492 
493 TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp3) {
494  const int iteration_upperbound = 300;
495  PrimalDualHybridGradientParams params =
496  CreateSolverParamsForFixture(iteration_upperbound,
497  /*eps_optimal_absolute=*/1.0e-6);
498  params.set_major_iteration_frequency(15);
499 
500  SolverResult output = PrimalDualHybridGradient(TestDiagonalQp3(), params);
501  VerifyTerminationReasonAndIterationCountForFixture(params, output);
502  VerifyObjectiveValues(output, 2.0, 1.0e-6);
503  EXPECT_THAT(output.primal_solution,
504  EigenArrayNear<double>({2.0, 0.0, 1.0}, 1.0e-6));
505  EXPECT_THAT(output.dual_solution,
506  EigenArrayNear<double>({-1.0, 1.0}, 1.0e-6));
507  EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({0, 0, 0}, 1.0e-6));
508 }
509 
510 // This is like `DiagonalQp1` except it starts with a near-optimal solution and
511 // uses a shorter iteration limit.
512 TEST_P(PrimalDualHybridGradientDiagonalQPTest, QpWarmStart) {
513  const int iteration_upperbound = 35;
514  PrimalDualHybridGradientParams params =
515  CreateSolverParamsForFixture(iteration_upperbound,
516  /*eps_optimal_absolute=*/1.0e-6);
517  params.set_major_iteration_frequency(5);
518  // Disable primal weight updating. In a warm-start situation, the primal
519  // weight should be carried over. In this test, the initial primal weight of 1
520  // is reasonable because the distance from the starting point to the primal
521  // and dual optimal solutions are about the same.
522  params.set_primal_weight_update_smoothing(0.0);
523 
524  PrimalAndDualSolution initial_solution;
525  initial_solution.primal_solution.resize(2);
526  initial_solution.primal_solution << 0.999, 0.001;
527  initial_solution.dual_solution.resize(1);
528  initial_solution.dual_solution << -0.999;
529  SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params,
530  std::move(initial_solution));
531  VerifyTerminationReasonAndIterationCountForFixture(params, output);
532  VerifyObjectiveValues(output, 6.0, 1.0e-6);
533  EXPECT_THAT(output.primal_solution,
534  EigenArrayNear<double>({1.0, 0.0}, 1.0e-6));
535  EXPECT_THAT(output.dual_solution,
536  EigenArrayNear(Eigen::ArrayXd::Constant(1, -1.0), 1.0e-6));
537  EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({4.0, 0.0}, 1.0e-6));
538 }
539 
540 // Tests an LP with no constraints.
541 TEST_P(PrimalDualHybridGradientLPTest, LpWithoutConstraints) {
542  const int iteration_upperbound = 2;
543  PrimalDualHybridGradientParams params =
544  CreateSolverParamsForFixture(iteration_upperbound,
545  /*eps_optimal_absolute=*/1.0e-6);
546 
547  QuadraticProgram qp(3, 0);
548  qp.variable_lower_bounds << -1, -kInfinity, -2;
549  qp.variable_upper_bounds << kInfinity, 4, 10;
550  qp.objective_vector << 1, -1, 2;
551 
552  SolverResult output = PrimalDualHybridGradient(qp, params);
553  VerifyTerminationReasonAndIterationCountForFixture(params, output);
554  VerifyObjectiveValues(output, -9.0, 1.0e-6);
555  EXPECT_THAT(output.primal_solution,
556  EigenArrayNear<double>({-1, 4, -2}, 1.0e-6));
557  EXPECT_THAT(output.dual_solution, SizeIs(0));
558  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 3);
559  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 3);
560  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 0);
561  EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 0);
562 }
563 
564 // Tests an LP with no variables.
565 TEST_P(PrimalDualHybridGradientLPTest, LpWithoutVariables) {
566  const int iteration_upperbound = 2;
567  PrimalDualHybridGradientParams params =
568  CreateSolverParamsForFixture(iteration_upperbound,
569  /*eps_optimal_absolute=*/1.0e-6);
570 
571  QuadraticProgram qp(0, 3);
572  qp.constraint_lower_bounds << -1, -kInfinity, -2;
573  qp.constraint_upper_bounds << kInfinity, 4, 10;
574 
575  SolverResult output = PrimalDualHybridGradient(qp, params);
576  VerifyTerminationReasonAndIterationCountForFixture(params, output);
577  VerifyObjectiveValues(output, 0.0, 1.0e-6);
578  EXPECT_THAT(output.primal_solution, SizeIs(0));
579  EXPECT_THAT(output.dual_solution, EigenArrayNear<double>({0, 0, 0}, 1.0e-6));
580  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 0);
581  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 0);
582  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
583  EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
584 }
585 
586 TEST_P(PrimalDualHybridGradientLPTest, LpWithOnlyFixedVariable) {
587  const int iteration_upperbound = 2;
588  PrimalDualHybridGradientParams params =
589  CreateSolverParamsForFixture(iteration_upperbound,
590  /*eps_optimal_absolute=*/0.0);
591 
592  QuadraticProgram qp(1, 0);
593  qp.variable_lower_bounds << 1;
594  qp.variable_upper_bounds << 1;
595  qp.objective_vector << 1;
596 
597  SolverResult output = PrimalDualHybridGradient(qp, params);
598  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
599  EXPECT_THAT(output.primal_solution,
600  EigenArrayNear(Eigen::ArrayXd::Constant(1, 1.0), 1.0e-6));
601  EXPECT_THAT(output.dual_solution, testing::SizeIs(0));
602  EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
603 }
604 
605 TEST_P(PrimalDualHybridGradientLPTest, InfeasibleLpWithoutVariables) {
606  const int iteration_upperbound = 2;
607  PrimalDualHybridGradientParams params =
608  CreateSolverParamsForFixture(iteration_upperbound,
609  /*eps_optimal_absolute=*/1.0e-6);
610 
611  QuadraticProgram qp(0, 1);
612  qp.constraint_lower_bounds << -1;
613  qp.constraint_upper_bounds << -1;
614 
615  SolverResult output = PrimalDualHybridGradient(qp, params);
616  EXPECT_EQ(output.solve_log.termination_reason(),
617  TERMINATION_REASON_PRIMAL_INFEASIBLE);
618  EXPECT_THAT(output.primal_solution, SizeIs(0));
619  EXPECT_LT(output.dual_solution[0], 0.0);
620  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 0);
621  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 0);
622  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 1);
623  EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 1);
624 }
625 
626 PrimalDualHybridGradientParams ParamsWithNoLimits() {
627  PrimalDualHybridGradientParams params;
628  // This disables the termination limits. A termination criteria must be set
629  // for the solver to terminate.
630  params.mutable_termination_criteria()
631  ->mutable_simple_optimality_criteria()
632  ->set_eps_optimal_relative(0.0);
633  params.mutable_termination_criteria()
634  ->mutable_simple_optimality_criteria()
635  ->set_eps_optimal_absolute(0.0);
636  params.set_record_iteration_stats(true);
637  return params;
638 }
639 
640 TEST(PrimalDualHybridGradientTest, ClearsRunningAverage) {
641  // An arbitrarily chosen major iteration frequency.
642  const int major_iteration_frequency = 17;
643  const int iteration_limit = 100;
644  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
645  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
646  params.set_termination_check_frequency(1);
647  params.set_major_iteration_frequency(major_iteration_frequency);
648  params.set_restart_strategy(PrimalDualHybridGradientParams::NO_RESTARTS);
649 
650  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
651  ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
652  // The first entry in `iteration_stats` corresponds to the starting point.
653  ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
654 
655  for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
656  const auto& stats = output.solve_log.iteration_stats(i);
657  const int iterations_completed = stats.iteration_number();
658  EXPECT_EQ(iterations_completed, i);
659  if (iterations_completed == 0) {
660  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
661  } else if (iterations_completed % major_iteration_frequency == 0) {
662  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_WEIGHTED_AVERAGE_RESET)
663  << "iteration = " << i;
664  } else {
665  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART)
666  << "iteration = " << i;
667  }
668  }
669 }
670 
671 TEST(PrimalDualHybridGradientTest, RestartsEveryMajorIteration) {
672  // An arbitrarily chosen major iteration frequency.
673  const int major_iteration_frequency = 17;
674  const int iteration_limit = 100;
675  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
676  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
677  params.set_termination_check_frequency(1);
678  params.set_major_iteration_frequency(major_iteration_frequency);
679  params.set_restart_strategy(
680  PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION);
681 
682  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
683  ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
684  // The first entry in `iteration_stats` corresponds to the starting point.
685  ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
686 
687  for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
688  const auto& stats = output.solve_log.iteration_stats(i);
689  const int iterations_completed = stats.iteration_number();
690  EXPECT_EQ(iterations_completed, i);
691  if (iterations_completed == 0) {
692  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
693  } else if (iterations_completed % major_iteration_frequency == 0) {
694  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_RESTART_TO_AVERAGE)
695  << "iteration = " << i;
696  } else {
697  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART)
698  << "iteration = " << i;
699  }
700  }
701 }
702 
703 TEST(PrimalDualHybridGradientTest, SolveLogIncludesNameForNamedQP) {
704  PrimalDualHybridGradientParams params;
705  params.mutable_termination_criteria()->set_iteration_limit(1);
706 
707  QuadraticProgram test_lp = TestLp();
708  test_lp.problem_name = "Test LP";
709 
710  SolverResult output = PrimalDualHybridGradient(test_lp, params);
711  EXPECT_EQ(output.solve_log.instance_name(), "Test LP");
712 }
713 
714 TEST(PrimalDualHybridGradientTest, SolveLogOmitsNameForUnnamedQP) {
715  PrimalDualHybridGradientParams params;
716  params.mutable_termination_criteria()->set_iteration_limit(1);
717 
718  QuadraticProgram unnamed_test_lp = TestLp();
719 
720  SolverResult output = PrimalDualHybridGradient(unnamed_test_lp, params);
721  EXPECT_FALSE(output.solve_log.has_instance_name());
722 }
723 
724 TEST(PrimalDualHybridGradientTest, SolveLogIncludesParameters) {
725  PrimalDualHybridGradientParams params;
726  params.mutable_termination_criteria()->set_iteration_limit(1);
727 
728  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
729  EXPECT_EQ(output.solve_log.params().termination_criteria().iteration_limit(),
730  1);
731 }
732 
733 TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsWorkOnTestLp) {
734  PrimalDualHybridGradientParams params;
735  params.set_major_iteration_frequency(16);
736  params.mutable_termination_criteria()->set_iteration_limit(128);
737  params.set_restart_strategy(
738  PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
739  // Low restart threshold.
740  params.set_necessary_reduction_for_restart(0.99);
741  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
742 
743  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
744  EXPECT_THAT(output.primal_solution,
745  EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
746  EXPECT_THAT(output.dual_solution,
747  EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
748  const auto convergence_info = GetConvergenceInformation(
749  output.solve_log.solution_stats(), output.solve_log.solution_type());
750  ASSERT_TRUE(convergence_info.has_value());
751  EXPECT_THAT(convergence_info->primal_objective(), DoubleNear(-34.0, 1.0e-4));
752  EXPECT_THAT(convergence_info->dual_objective(), DoubleNear(-34.0, 1.0e-4));
753 }
754 
755 TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsWorkOnTestQp) {
756  PrimalDualHybridGradientParams params;
757  params.set_major_iteration_frequency(16);
758  params.mutable_termination_criteria()->set_iteration_limit(128);
759  params.set_restart_strategy(
760  PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
761  // Low restart threshold.
762  params.set_necessary_reduction_for_restart(0.99);
763  SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params);
764 
765  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
766 }
767 
768 TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsToAverage) {
769  // An arbitrarily chosen major iteration frequency.
770  const int major_iteration_frequency = 13;
771  const int iteration_limit = 100;
772  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
773  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
774  params.set_termination_check_frequency(1);
775  params.set_major_iteration_frequency(major_iteration_frequency);
776  params.set_restart_strategy(
777  PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
778  params.set_necessary_reduction_for_restart(0.75);
779 
780  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
781  ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
782  // The first entry in `iteration_stats` corresponds to the starting point.
783  ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
784 
785  for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
786  const auto& stats = output.solve_log.iteration_stats(i);
787  const int iterations_completed = stats.iteration_number();
788  EXPECT_EQ(iterations_completed, i);
789  if (iterations_completed == 0) {
790  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
791  } else if (iterations_completed == major_iteration_frequency) {
792  // An explicit restart should be triggered at the end of the first major
793  // iteration.
794  EXPECT_THAT(stats.restart_used(),
795  AnyOf(RESTART_CHOICE_RESTART_TO_AVERAGE,
796  RESTART_CHOICE_WEIGHTED_AVERAGE_RESET))
797  << "iteration = " << i;
798  } else if (iterations_completed % major_iteration_frequency != 0) {
799  // No restarts should happen outside major iterations.
800  EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
801  }
802  }
803 }
804 
805 TEST(PrimalDualHybridGradientTest, PrimalWeightFrozen) {
806  // An arbitrarily chosen major iteration frequency.
807  const int major_iteration_frequency = 17;
808  const int iteration_limit = 100;
809  const double initial_primal_weight = 1.5;
810  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
811  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
812  params.set_major_iteration_frequency(major_iteration_frequency);
813  params.set_restart_strategy(
814  PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION);
815  params.set_initial_primal_weight(initial_primal_weight);
816  params.set_primal_weight_update_smoothing(0.0);
817 
818  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
819 
820  for (const auto& stats : output.solve_log.iteration_stats()) {
821  EXPECT_EQ(stats.primal_weight(), initial_primal_weight)
822  << "iteration = " << stats.iteration_number();
823  }
824 }
825 
826 TEST(PrimalDualHybridGradientTest, ConstantStepSize) {
827  const int iteration_limit = 100;
828  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
829  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
830  params.set_termination_check_frequency(1);
831  params.set_linesearch_rule(
832  PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
833 
834  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
835 
836  ASSERT_FALSE(output.solve_log.iteration_stats().empty());
837  const double initial_step_size =
838  output.solve_log.iteration_stats(0).step_size();
839  for (const auto& stats : output.solve_log.iteration_stats()) {
840  EXPECT_EQ(stats.step_size(), initial_step_size)
841  << "iteration = " << stats.iteration_number();
842  }
843 }
844 
845 TEST(PrimalDualHybridGradientTest, StepSizeScaling) {
846  const int iteration_limit = 1;
847  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
848  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
849  params.set_termination_check_frequency(1);
850  params.set_linesearch_rule(
851  PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
852 
853  SolverResult unscaled_output = PrimalDualHybridGradient(TestLp(), params);
854 
855  ASSERT_FALSE(unscaled_output.solve_log.iteration_stats().empty());
856  const double initial_step_size =
857  unscaled_output.solve_log.iteration_stats(0).step_size();
858 
859  const double kStepSizeScaling = 0.5;
860  params.set_initial_step_size_scaling(kStepSizeScaling);
861  SolverResult scaled_output = PrimalDualHybridGradient(TestLp(), params);
862 
863  ASSERT_FALSE(scaled_output.solve_log.iteration_stats().empty());
864  EXPECT_EQ(scaled_output.solve_log.iteration_stats(0).step_size(),
865  initial_step_size * kStepSizeScaling);
866 }
867 
868 // This verifies that `kkt_matrix_pass_limit` is checked every iteration.
869 TEST(PrimalDualHybridGradientTest, KktMatrixPassTermination) {
870  const int kkt_matrix_pass_limit = 13;
871  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
872  params.mutable_termination_criteria()->set_kkt_matrix_pass_limit(
873  kkt_matrix_pass_limit);
874  params.set_linesearch_rule(
875  PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
876 
877  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
878 
879  ASSERT_FALSE(output.solve_log.iteration_stats().empty());
880  EXPECT_EQ(output.solve_log.termination_reason(),
881  TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT);
882  EXPECT_EQ(output.solve_log.solution_stats().cumulative_kkt_matrix_passes(),
883  kkt_matrix_pass_limit);
884 }
885 
886 TEST(PrimalDualHybridGradientTest,
887  StatsAtEachIterationWithRecordIterationStatsOn) {
888  // An arbitrarily chosen major iteration frequency.
889  const int major_iteration_frequency = 17;
890  const int iteration_limit = 100;
891  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
892  params.set_record_iteration_stats(true);
893  // This is required for `ConvergenceInformation`, `InfeasibilityInformation`,
894  // and `PointMetadata` to be generated on each iteration.
895  params.set_termination_check_frequency(1);
896  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
897  params.set_major_iteration_frequency(major_iteration_frequency);
898 
899  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
900  EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
901  for (const auto& stats : output.solve_log.iteration_stats()) {
902  EXPECT_NE(GetConvergenceInformation(stats, POINT_TYPE_CURRENT_ITERATE),
903  std::nullopt);
904  EXPECT_NE(GetInfeasibilityInformation(stats, POINT_TYPE_CURRENT_ITERATE),
905  std::nullopt);
906  EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_CURRENT_ITERATE),
907  std::nullopt);
908  if (stats.iteration_number() > 0) {
909  EXPECT_NE(GetConvergenceInformation(stats, POINT_TYPE_AVERAGE_ITERATE),
910  std::nullopt);
911  EXPECT_NE(GetInfeasibilityInformation(stats, POINT_TYPE_AVERAGE_ITERATE),
912  std::nullopt);
913  EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_AVERAGE_ITERATE),
914  std::nullopt);
915 
916  EXPECT_NE(
917  GetInfeasibilityInformation(stats, POINT_TYPE_ITERATE_DIFFERENCE),
918  std::nullopt);
919  EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_ITERATE_DIFFERENCE),
920  std::nullopt);
921  }
922  }
923 }
924 
925 TEST(PrimalDualHybridGradientTest,
926  NoIterationStatsWithRecordIterationStatsOff) {
927  // An arbitrarily chosen major iteration frequency.
928  const int major_iteration_frequency = 17;
929  const int iteration_limit = 100;
930  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
931  params.set_record_iteration_stats(false);
932  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
933  params.set_major_iteration_frequency(major_iteration_frequency);
934  // Random projection seeds should have no effect when `record_iteration_stats`
935  // is false.
936  params.add_random_projection_seeds(1);
937 
938  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
939  EXPECT_EQ(output.solve_log.iteration_stats().size(), 0);
940 }
941 
942 TEST(PrimalDualHybridGradientTest, NoRandomProjectionsIfNotRequested) {
943  // An arbitrarily chosen major iteration frequency.
944  const int major_iteration_frequency = 17;
945  const int iteration_limit = 100;
946  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
947  params.set_record_iteration_stats(true);
948  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
949  params.set_major_iteration_frequency(major_iteration_frequency);
950  params.set_termination_check_frequency(1);
951 
952  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
953  EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
954  for (const auto& stats : output.solve_log.iteration_stats()) {
955  EXPECT_THAT(stats.point_metadata(), Not(IsEmpty()));
956  for (const auto& metadata : stats.point_metadata()) {
957  EXPECT_THAT(metadata.random_primal_projections(), IsEmpty());
958  EXPECT_THAT(metadata.random_dual_projections(), IsEmpty());
959  }
960  }
961 }
962 
963 TEST(PrimalDualHybridGradientTest, HasRandomProjectionsIfRequested) {
964  // An arbitrarily chosen major iteration frequency.
965  const int major_iteration_frequency = 17;
966  const int iteration_limit = 100;
967  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
968  params.set_record_iteration_stats(true);
969  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
970  params.set_major_iteration_frequency(major_iteration_frequency);
971  params.set_termination_check_frequency(1);
972  params.add_random_projection_seeds(1);
973  params.add_random_projection_seeds(2);
974 
975  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
976  EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
977  for (const auto& stats : output.solve_log.iteration_stats()) {
978  for (const auto& metadata : stats.point_metadata()) {
979  // There isn't much we can say about the random projection values, so just
980  // check that the right number are present.
981  EXPECT_THAT(metadata.random_primal_projections(), SizeIs(2));
982  EXPECT_THAT(metadata.random_dual_projections(), SizeIs(2));
983  }
984  }
985 }
986 
987 TEST(PrimalDualHybridGradientTest, ProjectInitialPointPrimalBounds) {
988  const int iteration_limit = 5;
989  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
990  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
991 
992  // The default initial solution (zeros) doesn't satisfy the primal variable
993  // bounds. The solver should project it to a valid primal solution.
994  SolverResult output =
996  EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
997  EXPECT_GT(output.primal_solution[0], 0.0);
998 }
999 
1000 TEST(PrimalDualHybridGradientTest, ProjectInitialPointDualBounds) {
1001  const int iteration_limit = 5;
1002  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1003  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
1004  // This initial solution doesn't satisfy the dual variable bounds. The solver
1005  // should project it to a valid dual solution.
1006  PrimalAndDualSolution initial_solution;
1007  initial_solution.primal_solution = Eigen::VectorXd(2);
1008  initial_solution.primal_solution << 1.0, 0.0;
1009  initial_solution.dual_solution = -Eigen::VectorXd::Ones(2);
1010  SolverResult output_nonzero_init = PrimalDualHybridGradient(
1011  SmallInitializationLp(), params, initial_solution);
1012  EXPECT_EQ(output_nonzero_init.solve_log.iteration_stats().size(),
1013  iteration_limit + 1);
1014  EXPECT_LE(output_nonzero_init.dual_solution[0], 0.0);
1015 }
1016 
1017 TEST(PrimalDualHybridGradientTest, DetectsProblemWithInconsistentBounds) {
1018  SolverResult output = PrimalDualHybridGradient(
1019  SmallInvalidProblemLp(), PrimalDualHybridGradientParams());
1020  EXPECT_EQ(output.solve_log.termination_reason(),
1021  TERMINATION_REASON_INVALID_PROBLEM);
1022 }
1023 
1024 TEST(PrimalDualHybridGradientTest, DetectsProblemWithInconsistentSizes) {
1025  QuadraticProgram qp = TinyLp();
1026  qp.objective_vector.resize(0);
1027  SolverResult output =
1028  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1029  EXPECT_EQ(output.solve_log.termination_reason(),
1030  TERMINATION_REASON_INVALID_PROBLEM);
1031 }
1032 
1033 TEST(PrimalDualHybridGradientTest, DetectsUncompressedConstraintMatrix) {
1034  QuadraticProgram qp = TinyLp();
1035  qp.constraint_matrix.uncompress();
1036  SolverResult output =
1037  PrimalDualHybridGradient(std::move(qp), PrimalDualHybridGradientParams());
1038  EXPECT_EQ(output.solve_log.termination_reason(),
1039  TERMINATION_REASON_INVALID_PROBLEM);
1040 }
1041 
1042 TEST(PrimalDualHybridGradientTest, DetectsInvalidParameters) {
1043  PrimalDualHybridGradientParams params;
1044  params.set_num_threads(0);
1045  SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
1046  EXPECT_EQ(output.solve_log.termination_reason(),
1047  TERMINATION_REASON_INVALID_PARAMETER);
1048 }
1049 
1050 TEST(PrimalDualHybridGradientTest, DetectsNanInConstraintMatrix) {
1051  QuadraticProgram qp = TestLp();
1052  qp.constraint_matrix.coeffRef(0, 0) =
1053  std::numeric_limits<double>::quiet_NaN();
1054  SolverResult output =
1055  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1056  EXPECT_EQ(output.solve_log.termination_reason(),
1057  TERMINATION_REASON_INVALID_PROBLEM);
1058 }
1059 
1060 TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintMatrix) {
1061  QuadraticProgram qp = TestLp();
1062  qp.constraint_matrix.coeffRef(0, 0) = 1.0e60;
1063  SolverResult output =
1064  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1065  EXPECT_EQ(output.solve_log.termination_reason(),
1066  TERMINATION_REASON_INVALID_PROBLEM);
1067 }
1068 
1069 TEST(PrimalDualHybridGradientTest,
1070  DetectsExcessivelySmallColNormConstraintMatrix) {
1071  QuadraticProgram qp = TestLp();
1072  qp.constraint_matrix.coeffRef(0, 1) = 1.0e-60;
1073  SolverResult output =
1074  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1075  EXPECT_EQ(output.solve_log.termination_reason(),
1076  TERMINATION_REASON_INVALID_PROBLEM);
1077 }
1078 
1079 TEST(PrimalDualHybridGradientTest,
1080  DetectsExcessivelySmallRowNormConstraintMatrix) {
1081  QuadraticProgram qp = TestLp();
1082  qp.constraint_matrix.coeffRef(2, 0) = 1.0e-60;
1083  SolverResult output =
1084  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1085  EXPECT_EQ(output.solve_log.termination_reason(),
1086  TERMINATION_REASON_INVALID_PROBLEM);
1087 }
1088 
1089 TEST(PrimalDualHybridGradientTest, DetectsNanInConstraintBounds) {
1090  QuadraticProgram qp = TestLp();
1091  qp.constraint_upper_bounds[1] = std::numeric_limits<double>::quiet_NaN();
1092  SolverResult output =
1093  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1094  EXPECT_EQ(output.solve_log.termination_reason(),
1095  TERMINATION_REASON_INVALID_PROBLEM);
1096 }
1097 
1098 TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintUpperBound) {
1099  QuadraticProgram qp = TestLp();
1100  qp.constraint_upper_bounds[1] = 1.0e60;
1101  SolverResult output =
1102  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1103  EXPECT_EQ(output.solve_log.termination_reason(),
1104  TERMINATION_REASON_INVALID_PROBLEM);
1105 }
1106 
1107 TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintLowerBound) {
1108  QuadraticProgram qp = TestLp();
1109  qp.constraint_lower_bounds[2] = -1.0e60;
1110  SolverResult output =
1111  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1112  EXPECT_EQ(output.solve_log.termination_reason(),
1113  TERMINATION_REASON_INVALID_PROBLEM);
1114 }
1115 
1116 TEST(PrimalDualHybridGradientTest, DetectsNanInVariableBound) {
1117  QuadraticProgram qp = TestLp();
1118  qp.variable_lower_bounds[3] = std::numeric_limits<double>::quiet_NaN();
1119  SolverResult output =
1120  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1121  EXPECT_EQ(output.solve_log.termination_reason(),
1122  TERMINATION_REASON_INVALID_PROBLEM);
1123 }
1124 
1125 TEST(PrimalDualHybridGradientTest, DetectsExcessiveVariableBoundGap) {
1126  QuadraticProgram qp = TestLp();
1127  qp.variable_lower_bounds[3] = -1.0e60;
1128  SolverResult output =
1129  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1130  EXPECT_EQ(output.solve_log.termination_reason(),
1131  TERMINATION_REASON_INVALID_PROBLEM);
1132 }
1133 
1134 TEST(PrimalDualHybridGradientTest, DetectsNanInObjectiveVector) {
1135  QuadraticProgram qp = TestLp();
1136  qp.objective_vector[3] = std::numeric_limits<double>::quiet_NaN();
1137  SolverResult output =
1138  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1139  EXPECT_EQ(output.solve_log.termination_reason(),
1140  TERMINATION_REASON_INVALID_PROBLEM);
1141 }
1142 
1143 TEST(PrimalDualHybridGradientTest, DetectsExcessiveObjectiveVector) {
1144  QuadraticProgram qp = TestLp();
1145  qp.objective_vector[3] = -1.0e60;
1146  SolverResult output =
1147  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1148  EXPECT_EQ(output.solve_log.termination_reason(),
1149  TERMINATION_REASON_INVALID_PROBLEM);
1150 }
1151 
1152 TEST(PrimalDualHybridGradientTest, DetectsNanInObjectiveMatrix) {
1153  QuadraticProgram qp = TestDiagonalQp1();
1154  qp.objective_matrix->diagonal()[0] = std::numeric_limits<double>::quiet_NaN();
1155  SolverResult output =
1156  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1157  EXPECT_EQ(output.solve_log.termination_reason(),
1158  TERMINATION_REASON_INVALID_PROBLEM);
1159 }
1160 
1161 TEST(PrimalDualHybridGradientTest, DetectsExcessiveObjectiveMatrix) {
1162  QuadraticProgram qp = TestDiagonalQp1();
1163  qp.objective_matrix->diagonal()[0] = 1.0e60;
1164  SolverResult output =
1165  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1166  EXPECT_EQ(output.solve_log.termination_reason(),
1167  TERMINATION_REASON_INVALID_PROBLEM);
1168 }
1169 
1170 TEST(PrimalDualHybridGradientTest, DetectsNanInInitialPrimalSolution) {
1171  QuadraticProgram qp = TestLp();
1172  PrimalAndDualSolution initial_solution = {
1173  .primal_solution =
1174  Eigen::VectorXd{
1175  {1.0, std::numeric_limits<double>::quiet_NaN(), 1.0, 1.0}},
1176  .dual_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1177  };
1178  SolverResult output = PrimalDualHybridGradient(
1179  qp, PrimalDualHybridGradientParams(), initial_solution);
1180  EXPECT_EQ(output.solve_log.termination_reason(),
1181  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1182 }
1183 
1184 TEST(PrimalDualHybridGradientTest,
1185  DetectsExcessiveValueInInitialPrimalSolution) {
1186  QuadraticProgram qp = TestLp();
1187  PrimalAndDualSolution initial_solution = {
1188  .primal_solution = Eigen::VectorXd{{1.0, 1.0e100, 1.0, 1.0}},
1189  .dual_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1190  };
1191 
1192  SolverResult output = PrimalDualHybridGradient(
1193  qp, PrimalDualHybridGradientParams(), initial_solution);
1194  EXPECT_EQ(output.solve_log.termination_reason(),
1195  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1196 }
1197 
1198 TEST(PrimalDualHybridGradientTest, DetectsIncorrectSizeInitialPrimalSolution) {
1199  QuadraticProgram qp = TestLp();
1200  PrimalAndDualSolution initial_solution = {
1201  .primal_solution = Eigen::VectorXd{{1.0, 1.0, 1.0}},
1202  .dual_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1203  };
1204 
1205  SolverResult output = PrimalDualHybridGradient(
1206  qp, PrimalDualHybridGradientParams(), initial_solution);
1207  EXPECT_EQ(output.solve_log.termination_reason(),
1208  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1209 }
1210 
1211 TEST(PrimalDualHybridGradientTest, DetectsNanInInitialDualSolution) {
1212  QuadraticProgram qp = TestLp();
1213  PrimalAndDualSolution initial_solution = {
1214  .primal_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1215  .dual_solution =
1216  Eigen::VectorXd{
1217  {1.0, std::numeric_limits<double>::quiet_NaN(), 1.0, 1.0}},
1218  };
1219  SolverResult output = PrimalDualHybridGradient(
1220  qp, PrimalDualHybridGradientParams(), initial_solution);
1221  EXPECT_EQ(output.solve_log.termination_reason(),
1222  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1223 }
1224 
1225 TEST(PrimalDualHybridGradientTest, DetectsExcessiveValueInInitialDualSolution) {
1226  QuadraticProgram qp = TestLp();
1227  PrimalAndDualSolution initial_solution = {
1228  .primal_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1229  .dual_solution = Eigen::VectorXd{{1.0, 1.0e100, 1.0, 1.0}},
1230  };
1231 
1232  SolverResult output = PrimalDualHybridGradient(
1233  qp, PrimalDualHybridGradientParams(), initial_solution);
1234  EXPECT_EQ(output.solve_log.termination_reason(),
1235  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1236 }
1237 
1238 TEST(PrimalDualHybridGradientTest, DetectsIncorrectSizeInitialDualSolution) {
1239  QuadraticProgram qp = TestLp();
1240  PrimalAndDualSolution initial_solution = {
1241  .primal_solution = Eigen::VectorXd{{1.0, 1.0, 1.0, 1.0}},
1242  .dual_solution = Eigen::VectorXd{{1.0, 1.0, 1.0}},
1243  };
1244 
1245  SolverResult output = PrimalDualHybridGradient(
1246  qp, PrimalDualHybridGradientParams(), initial_solution);
1247  EXPECT_EQ(output.solve_log.termination_reason(),
1248  TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
1249 }
1250 
1251 TEST(PrimalDualHybridGradientTest, DetectsZeroObjectiveScalingFactor) {
1252  QuadraticProgram qp = TestLp();
1253  qp.objective_scaling_factor = 0.0;
1254 
1255  SolverResult output =
1256  PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
1257  EXPECT_EQ(output.solve_log.termination_reason(),
1258  TERMINATION_REASON_INVALID_PROBLEM);
1259 }
1260 
1261 TEST(PrimalDualHybridGradientTest, ChecksTerminationAtCorrectFrequency) {
1262  // `termination_check_frequency` is chosen so that it does not divide the
1263  // `major_iteration_frequency`.
1264  const int major_iteration_frequency = 5;
1265  const int termination_check_frequency = 2;
1266  const int iteration_limit = 16;
1267  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1268  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
1269  params.set_termination_check_frequency(termination_check_frequency);
1270  params.set_major_iteration_frequency(major_iteration_frequency);
1271 
1272  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
1273  ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
1274 
1275  std::vector<int> termination_checked_at_iteration;
1276 
1277  for (const auto& stats : output.solve_log.iteration_stats()) {
1278  if (stats.convergence_information_size() > 0) {
1279  termination_checked_at_iteration.push_back(stats.iteration_number());
1280  }
1281  }
1282 
1283  // Termination is checked on the first iteration, the last iteration, every
1284  // major iteration, and at the `termination_check_frequency` (with a counter
1285  // reset on every major iteration).
1286  EXPECT_THAT(termination_checked_at_iteration,
1287  ElementsAre(0, 2, 4, 5, 7, 9, 10, 12, 14, 15, 16));
1288 }
1289 
1290 TEST(PrimalDualHybridGradientTest, CallsCallback) {
1291  // `termination_check_frequency` is chosen so that it does not divide the
1292  // `major_iteration_frequency`.
1293  const int major_iteration_frequency = 5;
1294  const int termination_check_frequency = 5;
1295  const int iteration_limit = 16;
1296  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1297  params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
1298  params.set_termination_check_frequency(termination_check_frequency);
1299  params.set_major_iteration_frequency(major_iteration_frequency);
1300 
1301  int callback_count = 0;
1302  SolverResult output = PrimalDualHybridGradient(
1303  TestLp(), params, /*interrupt_solve=*/nullptr,
1304  [&callback_count](const IterationCallbackInfo& callback_info) {
1305  ++callback_count;
1306  });
1307  ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
1308  // The callback should be called at every termination check, that is, at
1309  // iterations 0, 5, 10, 15, and 16, and additionally when returning the final
1310  // solution.
1311  CHECK_EQ(callback_count, 6);
1312 }
1313 
1314 // Returns the unique solution of `TinyLp`.
1315 PrimalAndDualSolution TinyLpSolution() {
1316  PrimalAndDualSolution solution;
1317  solution.primal_solution.resize(4);
1318  solution.primal_solution << 1.0, 0.0, 6.0, 2.0;
1319  solution.dual_solution.resize(3);
1320  solution.dual_solution << 0.5, 4.0, 0.0;
1321  return solution;
1322 }
1323 
1324 TEST(PrimalDualHybridGradientTest, WarmStartedAtOptimum) {
1325  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1326  params.mutable_termination_criteria()
1327  ->mutable_simple_optimality_criteria()
1328  ->set_eps_optimal_absolute(1.0e-10);
1329 
1330  SolverResult output =
1331  PrimalDualHybridGradient(TinyLp(), params, TinyLpSolution());
1332  // The solver should run a termination check at iteration 0 and determine that
1333  // the initial point is the solution of the problem.
1334  EXPECT_THAT(output.primal_solution,
1335  EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-10));
1336  EXPECT_THAT(output.dual_solution,
1337  EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-10));
1338  EXPECT_LE(output.solve_log.iteration_count(), 0);
1339  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
1340 }
1341 
1342 TEST(PrimalDualHybridGradientTest, NoMovementWhenStartedAtOptimum) {
1343  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1344  // Disable scaling because round-off causes a small amount of movement on the
1345  // first iteration.
1346  params.set_l2_norm_rescaling(false);
1347  params.set_l_inf_ruiz_iterations(0);
1348 
1349  PrimalAndDualSolution initial_solution = TinyLpSolution();
1350  SolverResult output =
1351  PrimalDualHybridGradient(TinyLp(), params, initial_solution);
1352  EXPECT_THAT(output.primal_solution, ElementsAre(1, 0, 6, 2));
1353  EXPECT_THAT(output.dual_solution, ElementsAre(0.5, 4.0, 0.0));
1354  EXPECT_LE(output.solve_log.iteration_count(), 1);
1355  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
1356 }
1357 
1358 TEST(PrimalDualHybridGradientTest, EmptyQp) {
1359  MPModelProto proto;
1360  absl::StatusOr<QuadraticProgram> qp =
1361  QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
1362  ASSERT_TRUE(qp.ok()) << qp.status();
1363  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1364  SolverResult output = PrimalDualHybridGradient(*qp, params);
1365  EXPECT_THAT(output.primal_solution, ElementsAre());
1366  EXPECT_THAT(output.dual_solution, ElementsAre());
1367  EXPECT_EQ(output.solve_log.iteration_count(), 0);
1368  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
1369 }
1370 
1371 TEST(PrimalDualHybridGradientTest, RespectsInterrupt) {
1372  std::atomic<bool> interrupt_solve;
1373  PrimalDualHybridGradientParams params;
1374  params.mutable_termination_criteria()
1375  ->mutable_simple_optimality_criteria()
1376  ->set_eps_optimal_absolute(0.0);
1377  params.mutable_termination_criteria()
1378  ->mutable_simple_optimality_criteria()
1379  ->set_eps_optimal_relative(0.0);
1380 
1381  interrupt_solve.store(true);
1382  const SolverResult output =
1383  PrimalDualHybridGradient(TestLp(), params, &interrupt_solve);
1384  EXPECT_EQ(output.solve_log.termination_reason(),
1385  TERMINATION_REASON_INTERRUPTED_BY_USER);
1386 }
1387 
1388 TEST(PrimalDualHybridGradientTest, RespectsInterruptFromCallback) {
1389  std::atomic<bool> interrupt_solve;
1390  PrimalDualHybridGradientParams params;
1391  params.mutable_termination_criteria()
1392  ->mutable_simple_optimality_criteria()
1393  ->set_eps_optimal_absolute(0.0);
1394  params.mutable_termination_criteria()
1395  ->mutable_simple_optimality_criteria()
1396  ->set_eps_optimal_relative(0.0);
1397 
1398  interrupt_solve.store(false);
1399  auto callback = [&](const IterationCallbackInfo& info) {
1400  if (info.iteration_stats.iteration_number() >= 10) {
1401  interrupt_solve.store(true);
1402  }
1403  };
1404 
1405  const SolverResult output =
1406  PrimalDualHybridGradient(TestLp(), params, &interrupt_solve, callback);
1407  EXPECT_EQ(output.solve_log.termination_reason(),
1408  TERMINATION_REASON_INTERRUPTED_BY_USER);
1409  EXPECT_GE(output.solve_log.iteration_count(), 10);
1410 }
1411 
1412 TEST(PrimalDualHybridGradientTest, IgnoresFalseInterrupt) {
1413  std::atomic<bool> interrupt_solve;
1414  PrimalDualHybridGradientParams params;
1415  params.mutable_termination_criteria()
1416  ->mutable_simple_optimality_criteria()
1417  ->set_eps_optimal_absolute(0.0);
1418  params.mutable_termination_criteria()
1419  ->mutable_simple_optimality_criteria()
1420  ->set_eps_optimal_relative(0.0);
1421  params.mutable_termination_criteria()->set_kkt_matrix_pass_limit(1);
1422 
1423  interrupt_solve.store(false);
1424  const SolverResult output =
1425  PrimalDualHybridGradient(TestLp(), params, &interrupt_solve);
1426  EXPECT_EQ(output.solve_log.termination_reason(),
1427  TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT);
1428 }
1429 
1430 TEST(PrimalDualHybridGradientTest, HugeNumThreads) {
1431  const int iteration_upperbound = 10;
1432  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1433  params.mutable_termination_criteria()->set_iteration_limit(
1434  iteration_upperbound);
1435  params.set_num_threads(1'000'000'000);
1436 
1437  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
1438  EXPECT_EQ(output.solve_log.termination_reason(),
1439  TERMINATION_REASON_ITERATION_LIMIT);
1440 }
1441 
1442 TEST(PrimalDualHybridGradientTest, HugeNumShards) {
1443  const int iteration_upperbound = 10;
1444  PrimalDualHybridGradientParams params = ParamsWithNoLimits();
1445  params.mutable_termination_criteria()->set_iteration_limit(
1446  iteration_upperbound);
1447  params.set_num_shards(1'000'000'000);
1448 
1449  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
1450  EXPECT_EQ(output.solve_log.termination_reason(),
1451  TERMINATION_REASON_ITERATION_LIMIT);
1452 }
1453 
1454 TEST(PrimalDualHybridGradientTest, DetailedTerminationCriteria) {
1455  const int iteration_upperbound = 300;
1456  PrimalDualHybridGradientParams params = CreateSolverParams(
1457  iteration_upperbound,
1458  /*eps_optimal_absolute=*/1.0e-5, /*enable_scaling=*/true,
1459  /*num_threads=*/4, /*use_iteration_limit=*/false,
1460  /*use_malitsky_pock_linesearch=*/false,
1461  /*use_diagonal_qp_trust_region_solver=*/false);
1462  params.set_major_iteration_frequency(60);
1463  params.mutable_termination_criteria()->clear_simple_optimality_criteria();
1464  auto* opt_criteria = params.mutable_termination_criteria()
1465  ->mutable_detailed_optimality_criteria();
1466  opt_criteria->set_eps_optimal_primal_residual_absolute(1.0e-5);
1467  opt_criteria->set_eps_optimal_primal_residual_relative(0.0);
1468  opt_criteria->set_eps_optimal_dual_residual_absolute(1.0e-5);
1469  opt_criteria->set_eps_optimal_dual_residual_relative(0.0);
1470  opt_criteria->set_eps_optimal_objective_gap_absolute(1.0e-5);
1471  opt_criteria->set_eps_optimal_objective_gap_relative(0.0);
1472 
1473  SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
1474  VerifyTerminationReasonAndIterationCount(params, output,
1475  /*use_iteration_limit=*/false);
1476  VerifyObjectiveValues(output, -1.0, 1.0e-4);
1477  EXPECT_THAT(output.primal_solution,
1478  EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
1479  EXPECT_THAT(output.dual_solution,
1480  EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
1481  EXPECT_THAT(output.reduced_costs,
1482  EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
1483 }
1484 
1485 // Verifies that the primal and dual solution satisfy the bounds constraints.
1486 // This function uses ASSERTS rather than EXPECTs because it's used with large
1487 // QPs that would spam the logs otherwise.
1488 void VerifyBoundConstraints(const QuadraticProgram& qp,
1489  const Eigen::VectorXd primal_solution,
1490  const Eigen::VectorXd dual_solution) {
1491  for (int64_t i = 0; i < primal_solution.size(); ++i) {
1492  ASSERT_TRUE(std::isfinite(primal_solution[i]))
1493  << i << " " << primal_solution[i];
1494  ASSERT_GE(primal_solution[i], qp.variable_lower_bounds[i]);
1495  ASSERT_LE(primal_solution[i], qp.variable_upper_bounds[i]);
1496  }
1497  for (int i = 0; i < dual_solution.size(); ++i) {
1498  ASSERT_TRUE(std::isfinite(dual_solution[i]))
1499  << i << " " << dual_solution[i];
1500  if (!std::isfinite(qp.constraint_lower_bounds[i])) {
1501  ASSERT_LE(dual_solution[i], 0);
1502  }
1503  if (!std::isfinite(qp.constraint_upper_bounds[i])) {
1504  ASSERT_GE(dual_solution[i], 0);
1505  }
1506  }
1507 }
1508 
1509 // This test doesn't attempt to check what is logged. Rather, it just verifies
1510 // that the code succeeds at each verbosity level. Other than having the
1511 // verbosity level as a parameter, and fixing the other parameters, it is the
1512 // same as `PrimalDualHybridGradientLPTest.Tiny`.
1513 TEST_P(PrimalDualHybridGradientVerbosityTest, TinyLp) {
1514  const int iteration_upperbound = 300;
1515  const int verbosity_level = GetParam();
1516  PrimalDualHybridGradientParams params =
1517  CreateSolverParams(iteration_upperbound,
1518  /*eps_optimal_absolute=*/1.0e-5,
1519  /*enable_scaling=*/false,
1520  /*num_threads=*/1,
1521  /*use_iteration_limit=*/false,
1522  /*use_malitsky_pock_linesearch=*/false,
1523  /*use_diagonal_qp_trust_region_solver=*/false);
1524  params.set_major_iteration_frequency(60);
1525  params.set_verbosity_level(verbosity_level);
1526 
1527  SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
1528  VerifyTerminationReasonAndIterationCount(params, output,
1529  /*use_iteration_limit=*/false);
1530  VerifyObjectiveValues(output, -1.0, 1.0e-4);
1531  EXPECT_THAT(output.primal_solution,
1532  EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
1533  EXPECT_THAT(output.dual_solution,
1534  EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
1535  EXPECT_THAT(output.reduced_costs,
1536  EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
1537  EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
1538  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
1539  EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
1540  EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
1541 }
1542 
1543 TEST(PresolveTest, DetectsProblemWithInconsistentBounds) {
1544  PrimalDualHybridGradientParams params;
1545  params.mutable_presolve_options()->set_use_glop(true);
1546  SolverResult output =
1548  EXPECT_EQ(output.solve_log.termination_reason(),
1549  TERMINATION_REASON_INVALID_PROBLEM);
1550 }
1551 
1552 TEST(PresolveTest, PresolveSolvesToOptimality) {
1553  PrimalDualHybridGradientParams params;
1554  params.mutable_presolve_options()->set_use_glop(true);
1555  SolverResult output = PrimalDualHybridGradient(TestLp(), params);
1556  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
1557  EXPECT_EQ(output.solve_log.iteration_count(), 0);
1558  ASSERT_EQ(output.solve_log.solution_type(), POINT_TYPE_PRESOLVER_SOLUTION);
1559  EXPECT_THAT(output.primal_solution,
1560  EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-10));
1561  EXPECT_THAT(output.dual_solution,
1562  EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-10));
1563  const auto convergence_info = GetConvergenceInformation(
1564  output.solve_log.solution_stats(), POINT_TYPE_PRESOLVER_SOLUTION);
1565  ASSERT_TRUE(convergence_info.has_value());
1566  EXPECT_EQ(convergence_info->candidate_type(), POINT_TYPE_PRESOLVER_SOLUTION);
1567  EXPECT_DOUBLE_EQ(convergence_info->primal_objective(), -34.0);
1568  EXPECT_DOUBLE_EQ(convergence_info->dual_objective(), -34.0);
1569  EXPECT_DOUBLE_EQ(convergence_info->corrected_dual_objective(), -34.0);
1570  EXPECT_DOUBLE_EQ(convergence_info->l_inf_primal_residual(), 0.0);
1571  EXPECT_DOUBLE_EQ(convergence_info->l2_primal_residual(), 0.0);
1572  EXPECT_DOUBLE_EQ(convergence_info->l_inf_dual_residual(), 0.0);
1573  EXPECT_DOUBLE_EQ(convergence_info->l2_dual_residual(), 0.0);
1574  EXPECT_DOUBLE_EQ(convergence_info->l_inf_primal_variable(), 8.0);
1575  EXPECT_DOUBLE_EQ(convergence_info->l2_primal_variable(), 8.5);
1576  EXPECT_DOUBLE_EQ(convergence_info->l_inf_dual_variable(), 2.375);
1577  EXPECT_THAT(convergence_info->l2_dual_variable(),
1578  DoubleNear(3.17569983538, 1.0e-10));
1579 }
1580 
1581 TEST(PresolveTest, SolvesWithGlopScalingEnabled) {
1582  PrimalDualHybridGradientParams params;
1583  params.mutable_presolve_options()->set_use_glop(true);
1584  params.mutable_presolve_options()->mutable_glop_parameters()->set_use_scaling(
1585  true);
1586  params.mutable_presolve_options()
1587  ->mutable_glop_parameters()
1588  ->set_use_preprocessing(false);
1589  QuadraticProgram test_lp = TestLp();
1590  // Change `objective_scaling_factor`, so that glop's presolve scaling will
1591  // react.
1592  test_lp.objective_scaling_factor = 0.1;
1593  SolverResult output = PrimalDualHybridGradient(test_lp, params);
1594  VerifyTerminationReasonAndIterationCount(params, output,
1595  /*use_iteration_limit=*/false);
1596  VerifyObjectiveValues(output, -3.4, 1.0e-6);
1597  EXPECT_THAT(output.primal_solution,
1598  EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
1599  EXPECT_THAT(output.dual_solution,
1600  EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
1601 }
1602 
1603 TEST(PresolveTest, PresolveInfeasible) {
1604  PrimalDualHybridGradientParams params;
1605  params.mutable_presolve_options()->set_use_glop(true);
1606  SolverResult output =
1608  EXPECT_EQ(output.solve_log.termination_reason(),
1609  TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE);
1610  EXPECT_EQ(output.solve_log.solution_type(), POINT_TYPE_PRESOLVER_SOLUTION);
1611  EXPECT_EQ(output.solve_log.iteration_count(), 0);
1612 }
1613 
1614 TEST_P(PresolveDualScalingTest, Dualize) {
1615  auto [dualize, negate_and_scale_objective] = GetParam();
1616  PrimalDualHybridGradientParams params;
1617  params.mutable_presolve_options()->set_use_glop(true);
1618  params.mutable_presolve_options()
1619  ->mutable_glop_parameters()
1620  ->set_solve_dual_problem(dualize ? glop::GlopParameters::ALWAYS_DO
1621  : glop::GlopParameters::NEVER_DO);
1622  params.mutable_termination_criteria()->set_iteration_limit(1000);
1623  QuadraticProgram qp = CorrelationClusteringStarLp();
1624  if (negate_and_scale_objective) {
1625  qp.objective_scaling_factor = -3;
1626  }
1627  SolverResult output = PrimalDualHybridGradient(qp, params);
1628  EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
1629  EXPECT_GT(output.solve_log.iteration_count(), 0);
1630  EXPECT_THAT(output.primal_solution,
1631  EigenArrayNear<double>({0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, 1.0e-10));
1632  EXPECT_THAT(output.dual_solution,
1633  EigenArrayNear<double>({0.5, 0.5, 0.5}, 1.0e-10));
1634 }
1635 
1636 TEST(PresolveTest, PresolveParametersAreUsed) {
1637  QuadraticProgram qp = CorrelationClusteringStarLp();
1638  PrimalDualHybridGradientParams params;
1639  params.mutable_termination_criteria()->set_iteration_limit(0);
1640  params.mutable_presolve_options()->set_use_glop(true);
1641  SolverResult output_1 = PrimalDualHybridGradient(qp, params);
1642  params.mutable_presolve_options()
1643  ->mutable_glop_parameters()
1644  ->set_solve_dual_problem(glop::GlopParameters::ALWAYS_DO);
1645  SolverResult output_2 = PrimalDualHybridGradient(qp, params);
1646  EXPECT_NE(output_1.solve_log.preprocessed_problem_stats().num_variables(),
1647  output_2.solve_log.preprocessed_problem_stats().num_variables());
1648  EXPECT_NE(output_1.solve_log.preprocessed_problem_stats().num_constraints(),
1649  output_2.solve_log.preprocessed_problem_stats().num_constraints());
1650 }
1651 
1652 TEST(ComputeStatusesTest, AtOptimum) {
1653  QuadraticProgram lp = TestLp();
1654  PrimalAndDualSolution solution;
1655  solution.primal_solution.resize(4);
1656  solution.primal_solution << -1, 8, 1, 2.5;
1657  solution.dual_solution.resize(4);
1658  solution.dual_solution << -2, 0, 2.375, 2.0 / 3;
1659  glop::ProblemSolution glop_solution = internal::ComputeStatuses(lp, solution);
1660  EXPECT_THAT(
1661  glop_solution.constraint_statuses,
1662  ElementsAre(ConstraintStatus::FIXED_VALUE, ConstraintStatus::BASIC,
1663  ConstraintStatus::AT_LOWER_BOUND,
1664  ConstraintStatus::AT_LOWER_BOUND));
1665  EXPECT_THAT(
1666  glop_solution.variable_statuses,
1667  ElementsAre(VariableStatus::BASIC, VariableStatus::BASIC,
1668  VariableStatus::BASIC, VariableStatus::AT_LOWER_BOUND));
1669 }
1670 
1671 TEST(ComputeStatusesTest, CoverMoreCases) {
1672  QuadraticProgram lp = TestLp();
1673  lp.variable_upper_bounds[3] = 2.5;
1674  lp.variable_upper_bounds[1] = 8;
1675  PrimalAndDualSolution solution;
1676  solution.primal_solution.resize(4);
1677  solution.primal_solution << -1, 8, 1, 2.5;
1678  solution.dual_solution.resize(4);
1679  solution.dual_solution << -2, 0, 2.375, -1;
1680  glop::ProblemSolution glop_solution = internal::ComputeStatuses(lp, solution);
1681  EXPECT_THAT(
1682  glop_solution.constraint_statuses,
1683  ElementsAre(ConstraintStatus::FIXED_VALUE, ConstraintStatus::BASIC,
1684  ConstraintStatus::AT_LOWER_BOUND,
1685  ConstraintStatus::AT_UPPER_BOUND));
1686  EXPECT_THAT(glop_solution.variable_statuses,
1687  ElementsAre(VariableStatus::BASIC, VariableStatus::AT_UPPER_BOUND,
1688  VariableStatus::BASIC, VariableStatus::FIXED_VALUE));
1689 }
1690 
1691 } // namespace
1692 } // namespace operations_research::pdlp
CpModelProto proto
MPCallback * callback
glop::ProblemSolution ComputeStatuses(const QuadraticProgram &qp, const PrimalAndDualSolution &solution)
absl::StatusOr< QuadraticProgram > QpFromMpModelProto(const MPModelProto &proto, bool relax_integer_variables, bool include_names)
QuadraticProgram SmallDualInfeasibleLp()
Definition: test_util.cc:233
QuadraticProgram TestDiagonalQp2()
Definition: test_util.cc:159
QuadraticProgram CorrelationClusteringStarLp()
Definition: test_util.cc:108
QuadraticProgram TestDiagonalQp3()
Definition: test_util.cc:175
QuadraticProgram TinyLp()
Definition: test_util.cc:67
std::optional< PointMetadata > GetPointMetadata(const IterationStats &stats, const PointType point_type)
QuadraticProgram LpWithoutConstraints()
Definition: test_util.cc:262
QuadraticProgram SmallInvalidProblemLp()
Definition: test_util.cc:191
EigenArrayNearMatcherP2< Eigen::Array< T, Eigen::Dynamic, 1 >, double > EigenArrayNear(absl::Span< const T > data, double tolerance)
Definition: test_util.h:361
QuadraticProgram SmallPrimalDualInfeasibleLp()
Definition: test_util.cc:240
std::optional< InfeasibilityInformation > GetInfeasibilityInformation(const IterationStats &stats, PointType candidate_type)
SolverResult PrimalDualHybridGradient(QuadraticProgram qp, const PrimalDualHybridGradientParams &params, const std::atomic< bool > *interrupt_solve, IterationStatsCallback iteration_stats_callback)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
QuadraticProgram TestLp()
Definition: test_util.cc:33
QuadraticProgram SmallInitializationLp()
Definition: test_util.cc:246
QuadraticProgram SmallPrimalInfeasibleLp()
Definition: test_util.cc:217
QuadraticProgram TestDiagonalQp1()
Definition: test_util.cc:143
QuadraticProgram CorrelationClusteringLp()
Definition: test_util.cc:87
BoolVar Not(BoolVar x)
A convenient wrapper so we can write Not(x) instead of x.Not() which is sometimes clearer.
Definition: cp_model.cc:86
TEST(LinearAssignmentTest, NullMatrix)
double objective_value