OR-Tools  9.6
iteration_stats_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 <cmath>
17 #include <optional>
18 #include <utility>
19 
20 #include "Eigen/Core"
21 #include "gmock/gmock.h"
22 #include "gtest/gtest.h"
26 #include "ortools/pdlp/solve_log.pb.h"
27 #include "ortools/pdlp/solvers.pb.h"
28 #include "ortools/pdlp/test_util.h"
29 
30 namespace operations_research::pdlp {
31 namespace {
32 
34 
35 using ::testing::AllOf;
36 using ::testing::Each;
37 using ::testing::ElementsAre;
38 using ::testing::Eq;
39 using ::testing::Ge;
40 using ::testing::Le;
41 using ::testing::Ne;
42 using ::testing::SizeIs;
43 
44 TEST(CorrectedDualTest, SimpleLpWithSuboptimalDual) {
45  const int num_threads = 2;
46  const int num_shards = 10;
47  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
48 
49  Eigen::VectorXd primal_solution(4), dual_solution(4);
50  // Set the primal variables that have primal gradients at their bounds, so
51  // that the primal gradients are reduced costs.
52  primal_solution << 0, 0, 6, 2.5;
53  dual_solution << -2, 0, 2.375, 1;
54  const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
55  PrimalDualHybridGradientParams(), sharded_qp, primal_solution,
56  dual_solution,
57  /*componentwise_primal_residual_offset=*/1.0,
58  /*componentwise_dual_residual_offset=*/1.0, POINT_TYPE_CURRENT_ITERATE);
59  // -36.5 = -14 - 24 - 9.5 - 1 - 3 + 15
60  EXPECT_DOUBLE_EQ(stats.dual_objective(), -36.5);
61  EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -36.5);
62 }
63 
64 // This is similar to `SimpleLpWithSuboptimalDual`, except with
65 // x_2 = 2. In the dual correction calculation, the corresponding bound is 6, so
66 // the primal gradient will be treated as a residual of 0.5 instead of a dual
67 // correction of -3, but in the corrected dual objective it is still treated as
68 // a dual correction.
69 TEST(CorrectedDualTest, SimpleLpWithVariableFarFromBoundAsResiduals) {
70  const int num_threads = 2;
71  const int num_shards = 10;
72  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
73 
74  Eigen::VectorXd primal_solution(4), dual_solution(4);
75  primal_solution << 0, 0, 2, 2.5;
76  dual_solution << -2, 0, 2.375, 1;
77  PrimalDualHybridGradientParams params;
78  params.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(true);
79  const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
80  params, sharded_qp, primal_solution, dual_solution,
81  /*componentwise_primal_residual_offset=*/1.0,
82  /*componentwise_dual_residual_offset=*/1.0, POINT_TYPE_CURRENT_ITERATE);
83  // -33.5 = -14 - 24 - 9.5 - 1 + 15
84  EXPECT_DOUBLE_EQ(stats.dual_objective(), -33.5);
85  EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -36.5);
86  EXPECT_DOUBLE_EQ(stats.l_inf_dual_residual(), 0.5);
87  EXPECT_DOUBLE_EQ(stats.l2_dual_residual(), 0.5);
88  EXPECT_DOUBLE_EQ(stats.l_inf_componentwise_dual_residual(), 0.25);
89 }
90 
91 TEST(CorrectedDualTest, SimpleLpWithVariableFarFromBoundAsReducedCosts) {
92  const int num_threads = 2;
93  const int num_shards = 10;
94  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
95 
96  Eigen::VectorXd primal_solution(4), dual_solution(4);
97  primal_solution << 0, 0, 2, 2.5;
98  dual_solution << -2, 0, 2.375, 1;
99  PrimalDualHybridGradientParams params;
100  params.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(false);
101  const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
102  params, sharded_qp, primal_solution, dual_solution,
103  /*componentwise_primal_residual_offset=*/1.0,
104  /*componentwise_dual_residual_offset=*/1.0, POINT_TYPE_CURRENT_ITERATE);
105  // -36.5 = -14 - 24 - 9.5 - 1 - 3 + 15
106  EXPECT_DOUBLE_EQ(stats.dual_objective(), -36.5);
107  EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -36.5);
108  EXPECT_DOUBLE_EQ(stats.l_inf_dual_residual(), 0.0);
109  EXPECT_DOUBLE_EQ(stats.l2_dual_residual(), 0.0);
110  EXPECT_DOUBLE_EQ(stats.l_inf_componentwise_dual_residual(), 0.0);
111 }
112 
113 TEST(CorrectedDualObjective, QpSuboptimal) {
114  const int num_threads = 2;
115  const int num_shards = 10;
116  ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
117  num_shards);
118 
119  Eigen::VectorXd primal_solution(2), dual_solution(1);
120  dual_solution << -3;
121  primal_solution << -2.0, 2.0;
122  const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
123  PrimalDualHybridGradientParams(), sharded_qp, primal_solution,
124  dual_solution,
125  /*componentwise_primal_residual_offset=*/1.0,
126  /*componentwise_dual_residual_offset=*/1.0, POINT_TYPE_CURRENT_ITERATE);
127  // primal gradient vector: [-6, 4]
128  // Constant term: 5
129  // Quadratic term: -(16+4)/2 = -10
130  // Dual objective term: -3 * 1
131  // Primal variables at bounds term: 2*-6 + -2*4 = -20
132  // -28.0 = 5 - 10 - 3 - 20
133  EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -28.0);
134 }
135 
136 TEST(RandomProjectionsTest, OneRandomProjectionsOfZeroVector) {
137  const int num_threads = 2;
138  const int num_shards = 10;
139  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
140 
141  PointMetadata metadata;
142  SetRandomProjections(sharded_qp, /*primal_solution=*/Eigen::VectorXd::Zero(4),
143  /*dual_solution=*/Eigen::VectorXd::Zero(4),
144  /*random_projection_seeds=*/{1}, metadata);
145  EXPECT_THAT(metadata.random_primal_projections(), ElementsAre(0.0));
146  EXPECT_THAT(metadata.random_dual_projections(), ElementsAre(0.0));
147 }
148 
149 TEST(RandomProjectionsTest, TwoRandomProjectionsOfVector) {
150  const int num_threads = 2;
151  const int num_shards = 10;
152  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
153 
154  PointMetadata metadata;
155  SetRandomProjections(sharded_qp, /*primal_solution=*/Eigen::VectorXd::Ones(4),
156  /*dual_solution=*/Eigen::VectorXd::Zero(4),
157  /*random_projection_seeds=*/{1, 2}, metadata);
158  EXPECT_THAT(metadata.random_primal_projections(), SizeIs(2));
159  EXPECT_THAT(metadata.random_dual_projections(), SizeIs(2));
160  // The primal solution has norm 2; the random projection should only reduce
161  // the norm. Obtaining 0.0 is a probability-zero event.
162  EXPECT_THAT(metadata.random_primal_projections(),
163  Each(AllOf(Ge(-2.0), Le(2.0), Ne(0.0))));
164  EXPECT_THAT(metadata.random_dual_projections(), Each(Eq(0.0)));
165 }
166 
167 TEST(ReducedCostsTest, SimpleLp) {
168  const int num_threads = 2;
169  const int num_shards = 10;
170  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
171 
172  Eigen::VectorXd primal_solution(4), dual_solution(4);
173  // Use a primal solution at the relevant bounds, to ensure handling as
174  // reduced costs.
175  primal_solution << 0.0, -2.0, 6.0, 3.5;
176  dual_solution << 1.0, 0.0, 0.0, -2.0;
177  // c is: [5.5, -2, -1, 1]
178  // -A^T y is: [-2, -1, 2, -4]
179  // c - A^T y is: [3.5, -3.0, 1.0, -3.0].
180  EXPECT_THAT(ReducedCosts(PrimalDualHybridGradientParams(), sharded_qp,
181  primal_solution, dual_solution),
182  ElementsAre(0.0, 0.0, 0.0, -3.0));
183  EXPECT_THAT(ReducedCosts(PrimalDualHybridGradientParams(), sharded_qp,
184  primal_solution, dual_solution,
185  /*use_zero_primal_objective=*/true),
186  ElementsAre(0.0, 0.0, 0.0, -4.0));
187 }
188 
189 TEST(ReducedCostsTest, SimpleLpWithGapResiduals) {
190  const int num_threads = 2;
191  const int num_shards = 10;
192  ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
193 
194  Eigen::VectorXd primal_solution(4), dual_solution(4);
195  primal_solution = Eigen::VectorXd::Zero(4);
196  dual_solution << 1.0, 0.0, 0.0, -1.0;
197  PrimalDualHybridGradientParams params_true, params_false;
198  params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
199  true);
200  params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
201  false);
202  // c is: [5.5, -2, -1, 1]
203  // -A^T y is: [-2, -1, 0.5, -3]
204  // c - A^T y is: [3.5, -3.0, -0.5, -2.0].
205  // When the primal variable is 0.0 and the bound is not 0.0, c - A^T y is
206  // handled as a residual when
207  // `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true and as
208  // a reduced cost otherwise.
209  EXPECT_THAT(
210  ReducedCosts(params_true, sharded_qp, primal_solution, dual_solution),
211  ElementsAre(0.0, 0.0, 0.0, 0.0));
212  EXPECT_THAT(
213  ReducedCosts(params_false, sharded_qp, primal_solution, dual_solution),
214  ElementsAre(0.0, 0.0, -0.5, -2.0));
215  // The primal variables are closer to the bound, c - A^T y is handled as a
216  // reduced cost regardless of the value of
217  // `handle_some_primal_gradients_on_finite_bounds_as_residuals`.
218  primal_solution << 0.0, 0.0, 4.0, 3.0;
219  EXPECT_THAT(
220  ReducedCosts(params_true, sharded_qp, primal_solution, dual_solution),
221  ElementsAre(0.0, 0.0, -0.5, -2.0));
222  EXPECT_THAT(
223  ReducedCosts(params_false, sharded_qp, primal_solution, dual_solution),
224  ElementsAre(0.0, 0.0, -0.5, -2.0));
225 }
226 
227 TEST(ReducedCostsTest, SimpleQp) {
228  const int num_threads = 2;
229  const int num_shards = 10;
230  ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
231  num_shards);
232 
233  Eigen::VectorXd primal_solution(2), dual_solution(1);
234  primal_solution << 1.0, 2.0;
235  dual_solution << 0.0;
236  PrimalDualHybridGradientParams params_true, params_false;
237  params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
238  true);
239  params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
240  false);
241  // Q*x is: [4.0, 2.0]
242  // c is: [-1, -1]
243  // A^T y is zero.
244  // If `handle_some_primal_gradients_on_finite_bounds_as_residuals` is
245  // true the second primal gradient term is handled as a residual, not a
246  // reduced cost.
247  EXPECT_THAT(
248  ReducedCosts(params_true, sharded_qp, primal_solution, dual_solution),
249  ElementsAre(3.0, 0.0));
250  EXPECT_THAT(
251  ReducedCosts(params_false, sharded_qp, primal_solution, dual_solution),
252  ElementsAre(3.0, 1.0));
253  EXPECT_THAT(
254  ReducedCosts(params_true, sharded_qp, primal_solution, dual_solution,
255  /*use_zero_primal_objective=*/true),
256  ElementsAre(0.0, 0.0));
257  EXPECT_THAT(
258  ReducedCosts(params_false, sharded_qp, primal_solution, dual_solution,
259  /*use_zero_primal_objective=*/true),
260  ElementsAre(0.0, 0.0));
261 }
262 
263 TEST(GetConvergenceInformation, GetsCorrectEntry) {
264  const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
265  convergence_information {
266  candidate_type: POINT_TYPE_CURRENT_ITERATE
267  primal_objective: 1.0
268  }
269  convergence_information {
270  candidate_type: POINT_TYPE_AVERAGE_ITERATE
271  primal_objective: 2.0
272  }
273  )pb");
274  const auto average_info =
275  GetConvergenceInformation(test_stats, POINT_TYPE_AVERAGE_ITERATE);
276  ASSERT_TRUE(average_info.has_value());
277  EXPECT_EQ(average_info->candidate_type(), POINT_TYPE_AVERAGE_ITERATE);
278  EXPECT_EQ(average_info->primal_objective(), 2.0);
279 
280  const auto current_info =
281  GetConvergenceInformation(test_stats, POINT_TYPE_CURRENT_ITERATE);
282  ASSERT_TRUE(current_info.has_value());
283  EXPECT_EQ(current_info->candidate_type(), POINT_TYPE_CURRENT_ITERATE);
284  EXPECT_EQ(current_info->primal_objective(), 1.0);
285 
286  EXPECT_THAT(
287  GetConvergenceInformation(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
288  Eq(std::nullopt));
289 }
290 
291 TEST(GetInfeasibilityInformation, GetsCorrectEntry) {
292  const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
293  infeasibility_information {
294  candidate_type: POINT_TYPE_CURRENT_ITERATE
295  primal_ray_linear_objective: 1.0
296  }
297  infeasibility_information {
298  candidate_type: POINT_TYPE_AVERAGE_ITERATE
299  primal_ray_linear_objective: 2.0
300  }
301  )pb");
302  const auto average_info =
303  GetInfeasibilityInformation(test_stats, POINT_TYPE_AVERAGE_ITERATE);
304  ASSERT_TRUE(average_info.has_value());
305  EXPECT_EQ(average_info->candidate_type(), POINT_TYPE_AVERAGE_ITERATE);
306  EXPECT_EQ(average_info->primal_ray_linear_objective(), 2.0);
307 
308  const auto current_info =
309  GetInfeasibilityInformation(test_stats, POINT_TYPE_CURRENT_ITERATE);
310  ASSERT_TRUE(current_info.has_value());
311  EXPECT_EQ(current_info->candidate_type(), POINT_TYPE_CURRENT_ITERATE);
312  EXPECT_EQ(current_info->primal_ray_linear_objective(), 1.0);
313 
314  EXPECT_THAT(
315  GetInfeasibilityInformation(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
316  Eq(std::nullopt));
317 }
318 
319 TEST(GetPointMetadata, GetsCorrectEntry) {
320  const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
321  point_metadata {
322  point_type: POINT_TYPE_CURRENT_ITERATE
323  active_primal_variable_count: 1
324  }
325  point_metadata {
326  point_type: POINT_TYPE_AVERAGE_ITERATE
327  active_primal_variable_count: 2
328  }
329  )pb");
330  const auto average_info =
331  GetPointMetadata(test_stats, POINT_TYPE_AVERAGE_ITERATE);
332  ASSERT_TRUE(average_info.has_value());
333  EXPECT_EQ(average_info->point_type(), POINT_TYPE_AVERAGE_ITERATE);
334  EXPECT_EQ(average_info->active_primal_variable_count(), 2);
335 
336  const auto current_info =
337  GetPointMetadata(test_stats, POINT_TYPE_CURRENT_ITERATE);
338  ASSERT_TRUE(current_info.has_value());
339  EXPECT_EQ(current_info->point_type(), POINT_TYPE_CURRENT_ITERATE);
340  EXPECT_EQ(current_info->active_primal_variable_count(), 1);
341 
342  EXPECT_THAT(GetPointMetadata(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
343  Eq(std::nullopt));
344 }
345 
346 } // namespace
347 } // namespace operations_research::pdlp
T ParseTextOrDie(const std::string &input)
Definition: protobuf_util.h:77
VectorXd ReducedCosts(const PrimalDualHybridGradientParams &params, const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, bool use_zero_primal_objective)
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)
std::optional< PointMetadata > GetPointMetadata(const IterationStats &stats, const PointType point_type)
ConvergenceInformation ComputeScaledConvergenceInformation(const PrimalDualHybridGradientParams &params, const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, const double componentwise_primal_residual_offset, const double componentwise_dual_residual_offset, PointType candidate_type)
std::optional< InfeasibilityInformation > GetInfeasibilityInformation(const IterationStats &stats, PointType candidate_type)
std::optional< ConvergenceInformation > GetConvergenceInformation(const IterationStats &stats, PointType candidate_type)
QuadraticProgram TestLp()
Definition: test_util.cc:33
QuadraticProgram TestDiagonalQp1()
Definition: test_util.cc:143
int64_t Zero()
NOLINT.
TEST(LinearAssignmentTest, NullMatrix)
pdlp::QuadraticProgram SimpleLp()