OR-Tools  9.6
trust_region_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 <cmath>
18 #include <cstdint>
19 #include <limits>
20 #include <tuple>
21 #include <vector>
22 
23 #include "Eigen/Core"
24 #include "Eigen/SparseCore"
25 #include "absl/strings/str_cat.h"
26 #include "absl/strings/string_view.h"
27 #include "gmock/gmock.h"
28 #include "gtest/gtest.h"
32 #include "ortools/pdlp/sharder.h"
33 #include "ortools/pdlp/test_util.h"
34 
35 namespace operations_research::pdlp {
36 namespace {
37 
38 using ::Eigen::VectorXd;
39 using ::testing::DoubleNear;
40 using ::testing::ElementsAre;
41 
42 constexpr double kInfinity = std::numeric_limits<double>::infinity();
43 
44 class TrustRegion : public testing::TestWithParam<
45  /*use_diagonal_solver=*/bool> {};
46 
47 INSTANTIATE_TEST_SUITE_P(
48  TrustRegionSolvers, TrustRegion, testing::Bool(),
49  [](const testing::TestParamInfo<TrustRegion::ParamType>& info) {
50  return (info.param) ? "UseApproximateTRSolver" : "UseLinearTimeTRSolver";
51  });
52 
53 TEST_P(TrustRegion, SolvesWithoutVariableBounds) {
54  // min x + y
55  // ||(x - 2.0, y - (-5.0))||_2 <= sqrt(2)
56  // [x*, y*] = [1.0, -6.0]
61  center_point << 2.0, -5.0;
62  objective_vector << 1.0, 1.0;
63  const double target_radius = std::sqrt(2.0);
64 
65  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
66 
67  VectorXd expected_solution(2);
68  expected_solution << 1.0, -6.0;
69  const double expected_objective_value = -2.0;
70 
71  if (GetParam()) {
72  TrustRegionResult result = SolveDiagonalTrustRegion(
73  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
75  /*norm_weights=*/VectorXd::Ones(2), target_radius, sharder,
76  /*solve_tolerance=*/1.0e-8);
77  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
78  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
79  } else {
80  TrustRegionResult result = SolveTrustRegion(
82  center_point, /*norm_weights=*/VectorXd::Ones(2), target_radius,
83  sharder);
84  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
85  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
86  }
87 }
88 
89 TEST_P(TrustRegion, SolvesWithVariableBounds) {
90  // min x - y + z
91  // ||(x - 2.0, y - (-5.0), z - 1.0)||_2 <= sqrt(2.0)
92  // x >= 2.0
93  // [x*, y*, z*] = [2.0, -4.0, 0.0]
98  center_point << 2.0, -5.0, 1.0;
99  objective_vector << 1.0, -1.0, 1.0;
100  const double target_radius = std::sqrt(2.0);
101 
102  Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
103 
104  VectorXd expected_solution(3);
105  expected_solution << 2.0, -4.0, 0.0;
106  const double expected_objective_value = -2.0;
107 
108  if (GetParam()) {
109  TrustRegionResult result = SolveDiagonalTrustRegion(
110  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(3),
112  /*norm_weights=*/VectorXd::Ones(3), target_radius, sharder,
113  /*solve_tolerance=*/1.0e-6);
114  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
115  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
116  } else {
117  TrustRegionResult result = SolveTrustRegion(
119  center_point, /*norm_weights=*/VectorXd::Ones(3), target_radius,
120  sharder);
121 
122  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
123  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
124  }
125 }
126 
127 TEST_P(TrustRegion, SolvesAtVariableBounds) {
128  // min x - y
129  // ||(x - 2.0, y - (-5.0))||_2 <= 1
130  // x >= 2.0, y <= -5.0
131  // [x*, y*] = [2.0, -5.0]
132  // The bound constraints block movement from the center point.
134  objective_vector(2);
137  center_point << 2.0, -5.0;
138  objective_vector << 1.0, -1.0;
139  const double target_radius = 1.0;
140 
141  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
142 
143  VectorXd expected_solution(2);
144  expected_solution << 2.0, -5.0;
145  const double expected_objective_value = 0.0;
146 
147  if (GetParam()) {
148  TrustRegionResult result = SolveDiagonalTrustRegion(
149  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
151  /*norm_weights=*/VectorXd::Ones(2), target_radius, sharder,
152  /*solve_tolerance=*/1.0e-6);
153 
154  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
155  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
156  } else {
157  TrustRegionResult result = SolveTrustRegion(
159  center_point, /*norm_weights=*/VectorXd::Ones(2), target_radius,
160  sharder);
161 
162  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
163  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
164  }
165 }
166 
167 TEST_P(TrustRegion, SolvesWithInactiveRadius) {
168  // min x - y + z
169  // ||(x - 2.0, y - (-5.0), z - 1.0)||_2 <= 1
170  // x >= 2.0, y <= -5.0, z >= 0.5
171  // [x*, y*, z*] = [2.0, -5.0, 0.5]
172  // This is a corner case where the radius constraint is not active at the
173  // solution.
175  objective_vector(3);
176  variable_lower_bounds << 2.0, -kInfinity, 0.5;
178  center_point << 2.0, -5.0, 1.0;
179  objective_vector << 1.0, -1.0, 1.0;
180  const double target_radius = 1.0;
181 
182  Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
183 
184  VectorXd expected_solution(3);
185  expected_solution << 2.0, -5.0, 0.5;
186  const double expected_objective_value = -0.5;
187 
188  if (GetParam()) {
189  TrustRegionResult result = SolveDiagonalTrustRegion(
190  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(3),
192  /*norm_weights=*/VectorXd::Ones(3), target_radius, sharder,
193  /*solve_tolerance=*/1.0e-6);
194 
195  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
196  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
197  } else {
198  TrustRegionResult result = SolveTrustRegion(
200  center_point, /*norm_weights=*/VectorXd::Ones(3), target_radius,
201  sharder);
202 
203  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
204  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
205  }
206 }
207 
208 TEST_P(TrustRegion, SolvesWithInfiniteRadius) {
209  // min x - y + z
210  // ||(x - 2.0, y - (-5.0), z - 1.0)||_2 <= Infinity
211  // x >= 2.0, y <= -5.0, z >= 0.5
212  // [x*, y*, z*] = [2.0, -5.0, 0.5]
214  objective_vector(3);
215  variable_lower_bounds << 2.0, -kInfinity, 0.5;
217  center_point << 2.0, -5.0, 1.0;
218  objective_vector << 1.0, -1.0, 1.0;
219  const double target_radius = kInfinity;
220 
221  Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
222 
223  VectorXd expected_solution(3);
224  expected_solution << 2.0, -5.0, 0.5;
225  const double expected_objective_value = -0.5;
226 
227  if (GetParam()) {
228  TrustRegionResult result = SolveDiagonalTrustRegion(
229  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(3),
231  /*norm_weights=*/VectorXd::Ones(3), target_radius, sharder,
232  /*solve_tolerance=*/1.0e-6);
233 
234  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
235  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
236  } else {
237  TrustRegionResult result = SolveTrustRegion(
239  center_point, /*norm_weights=*/VectorXd::Ones(3), target_radius,
240  sharder);
241 
242  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
243  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
244  }
245 }
246 
247 TEST_P(TrustRegion, SolvesWithMixedObjective) {
248  // min 2x + y
249  // ||(x - 2.0, y - 1.0)||_2 <= sqrt(1.25)
250  // x >= 1.0, y >= 0
251  // [x*, y*] = [1.0, 0.5]
252  // We take a positive step in all coordinates. Only the first coordinate
253  // hits its bound.
255  objective_vector(2);
256  variable_lower_bounds << 1.0, 0.0;
258  center_point << 2.0, 1.0;
259  objective_vector << 2.0, 1.0;
260  const double target_radius = std::sqrt(1.25);
261 
262  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
263 
264  VectorXd expected_solution(2);
265  expected_solution << 1.0, 0.5;
266  const double expected_objective_value = -2.5;
267 
268  if (GetParam()) {
269  TrustRegionResult result = SolveDiagonalTrustRegion(
270  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
272  /*norm_weights=*/VectorXd::Ones(2), target_radius, sharder,
273  /*solve_tolerance=*/1.0e-6);
274  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
275  EXPECT_NEAR(result.objective_value, expected_objective_value, 2.0e-6);
276  } else {
277  TrustRegionResult result = SolveTrustRegion(
279  center_point, /*norm_weights=*/VectorXd::Ones(2), target_radius,
280  sharder);
281 
282  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
283  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
284  }
285 }
286 
287 TEST_P(TrustRegion, SolvesWithZeroObjectiveNoBounds) {
288  // min 0*x
289  // ||(x - 2.0)||_2 <= 1
290  // x* = 2.0
292  objective_vector(1);
295  center_point << 2.0;
296  objective_vector << 0.0;
297  const double target_radius = 1.0;
298 
299  Sharder sharder(/*num_elements=*/1, /*num_shards=*/1, nullptr);
300 
301  VectorXd expected_solution(1);
302  expected_solution << 2.0;
303  const double expected_objective_value = 0.0;
304 
305  if (GetParam()) {
306  TrustRegionResult result = SolveDiagonalTrustRegion(
307  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(1),
309  /*norm_weights=*/VectorXd::Ones(1), target_radius, sharder,
310  /*solve_tolerance=*/1.0e-6);
311 
312  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
313  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
314  } else {
315  TrustRegionResult result = SolveTrustRegion(
317  center_point, /*norm_weights=*/VectorXd::Ones(1), target_radius,
318  sharder);
319 
320  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
321  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
322  }
323 }
324 
325 class TrustRegionWithWeights : public testing::TestWithParam<
326  /*use_diagonal_solver=*/bool> {};
327 
328 INSTANTIATE_TEST_SUITE_P(
329  TrustRegionSolverWithWeights, TrustRegionWithWeights, testing::Bool(),
330  [](const testing::TestParamInfo<TrustRegion::ParamType>& info) {
331  return (info.param) ? "UseApproximateTRSolver" : "UseLinearTimeTRSolver";
332  });
333 
334 TEST_P(TrustRegionWithWeights, SolvesWithoutVariableBounds) {
335  // min x + 2.0 y
336  // ||(x - 2.0, y - (-5.0))||_W <= sqrt(3)
337  // norm_weights = [1.0, 2.0]
338  // [x*, y*] = [1.0, -6.0]
343  center_point << 2.0, -5.0;
344  objective_vector << 1.0, 2.0;
345  norm_weights << 1.0, 2.0;
346  const double target_radius = std::sqrt(3.0);
347 
348  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
349 
350  VectorXd expected_solution(2);
351  expected_solution << 1.0, -6.0;
352  const double expected_objective_value = -3.0;
353 
354  if (GetParam()) {
355  TrustRegionResult result = SolveDiagonalTrustRegion(
356  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
358  norm_weights, target_radius, sharder, /*solve_tolerance=*/1.0e-6);
359 
360  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
361  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-5);
362  } else {
363  TrustRegionResult result = SolveTrustRegion(
365  center_point, norm_weights, target_radius, sharder);
366 
367  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
368  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
369  }
370 }
371 
372 TEST_P(TrustRegionWithWeights, SolvesWithVariableBounds) {
373  // min 0.5 x - 2.0 y + 3.0 z
374  // ||(x - 2.0, y - (-5.0), z - 1.0)||_W <= sqrt(5)
375  // x >= 2.0
376  // norm_weights = [0.5, 2.0, 3.0]
377  // [x*, y*, z*] = [2.0, -4.0, 0.0]
382  center_point << 2.0, -5.0, 1.0;
383  objective_vector << 0.5, -2.0, 3.0;
384  norm_weights << 0.5, 2.0, 3.0;
385  const double target_radius = std::sqrt(5.0);
386 
387  Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
388 
389  VectorXd expected_solution(3);
390  expected_solution << 2.0, -4.0, 0.0;
391  const double expected_objective_value = -5.0;
392 
393  if (GetParam()) {
394  TrustRegionResult result = SolveDiagonalTrustRegion(
395  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(3),
397  norm_weights, target_radius, sharder, /*solve_tolerance=*/1.0e-6);
398 
399  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
400  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-5);
401  } else {
402  TrustRegionResult result = SolveTrustRegion(
404  center_point, norm_weights, target_radius, sharder);
405 
406  EXPECT_THAT(result.solution, EigenArrayEq(expected_solution));
407  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
408  }
409 }
410 
411 TEST_P(TrustRegionWithWeights, SolvesWithVariableThatHitsBounds) {
412  // min x + 2y
413  // ||(x - 2.0, y - 1.0)||_2 <= 1
414  // x >= 1.0, y >= 0
415  // [x*, y*] = [1.0, 0.5]
416  // norm_weights = [0.5, 2.0]
417  // We take a positive step in all coordinates. Only the first coordinate
418  // hits its bound.
421  variable_lower_bounds << 1.0, 0.0;
423  center_point << 2.0, 1.0;
424  objective_vector << 1.0, 2.0;
425  norm_weights << 0.5, 2.0;
426  const double target_radius = 1;
427 
428  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
429 
430  VectorXd expected_solution(2);
431  expected_solution << 1.0, 0.5;
432  const double expected_objective_value = -2.0;
433 
434  if (GetParam()) {
435  TrustRegionResult result = SolveDiagonalTrustRegion(
436  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
438  norm_weights, target_radius, sharder,
439  /*solve_tolerance=*/1.0e-6);
440 
441  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
442  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
443  } else {
444  TrustRegionResult result = SolveTrustRegion(
446  center_point, norm_weights, target_radius, sharder);
447 
448  EXPECT_THAT(result.solution,
449  ElementsAre(expected_solution[0],
450  DoubleNear(expected_solution[1], 1.0e-13)));
451  EXPECT_DOUBLE_EQ(result.objective_value, expected_objective_value);
452  }
453 }
454 
455 TEST_P(TrustRegionWithWeights, SolvesWithLargeWeight) {
456  // min 1000.0 x + 2y
457  // ||(x - 2.0, y - 1.0)||_W <= sqrt(500.5)
458  // x >= 1.0, y >= 0
459  // [x*, y*] = [1.0, 0.5]
460  // norm_weights = [500.0, 2.0]
461  // We take a positive step in all coordinates. Only the first coordinate
462  // hits its bound. The large norm weight stresses the code.
465  variable_lower_bounds << 1.0, 0.0;
467  center_point << 2.0, 1.0;
468  objective_vector << 1000.0, 2.0;
469  norm_weights << 500.0, 2.0;
470  const double target_radius = std::sqrt(500.5);
471 
472  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
473 
474  VectorXd expected_solution(2);
475  expected_solution << 1.0, 0.5;
476  const double expected_objective_value = -1001.0;
477 
478  if (GetParam()) {
479  TrustRegionResult result = SolveDiagonalTrustRegion(
480  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
482  norm_weights, target_radius, sharder,
483  /*solve_tolerance=*/1.0e-6);
484 
485  EXPECT_THAT(result.solution, EigenArrayNear(expected_solution, 1.0e-6));
486  EXPECT_NEAR(result.objective_value, expected_objective_value, 1.0e-6);
487  } else {
488  TrustRegionResult result = SolveTrustRegion(
490  center_point, norm_weights, target_radius, sharder);
491 
492  EXPECT_THAT(result.solution,
493  ElementsAre(expected_solution[0],
494  DoubleNear(expected_solution[1], 1.0e-13)));
495  EXPECT_DOUBLE_EQ(result.objective_value, -1001.0);
496  }
497 }
498 
499 TEST(TrustRegionDeathTest, CheckFailsWithNonPositiveWeights) {
500  // min x + y
501  // ||(x - 2.0, y - (-5.0))||_2 <= sqrt(2)
502  // [x*, y*] = [1.0, -6.0]
507  center_point << 2.0, -5.0;
508  objective_vector << 1.0, 1.0;
509  norm_weights << 0.0, 1.0;
510  const double target_radius = std::sqrt(2.0);
511 
512  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
513 
514  EXPECT_DEATH(TrustRegionResult result =
517  norm_weights, target_radius, sharder),
518  "Check failed: norm_weights_are_positive");
519 }
520 
521 TEST(TrustRegionDeathTest, CheckFailsWithNonPositiveWeightsForDiagonalSolver) {
522  // min x + y
523  // ||(x - 2.0, y - (-5.0))||_2 <= sqrt(2)
524  // [x*, y*] = [1.0, -6.0]
529  center_point << 2.0, -5.0;
530  objective_vector << 1.0, 1.0;
531  norm_weights << 0.0, 1.0;
532  const double target_radius = std::sqrt(2.0);
533 
534  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
535 
536  EXPECT_DEATH(
537  TrustRegionResult result = SolveDiagonalTrustRegion(
538  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
540  norm_weights, target_radius, sharder,
541  /*solve_tolerance=*/1.0e-6),
542  "Check failed: norm_weights_are_positive");
543 }
544 
545 TEST(TrustRegionDeathTest, CheckFailsWithNegativeRadius) {
546  // min x + y
547  // ||(x - 2.0, y - (-5.0))||_2 <= sqrt(2)
548  // [x*, y*] = [1.0, -6.0]
550  objective_vector(2);
553  center_point << 2.0, -5.0;
554  objective_vector << 1.0, 1.0;
555  const double target_radius = -std::sqrt(2.0);
556 
557  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
558 
559  EXPECT_DEATH(TrustRegionResult result = SolveTrustRegion(
562  /*norm_weights=*/VectorXd::Ones(2), target_radius, sharder),
563  "Check failed: target_radius >= 0.0");
564 }
565 
566 TEST(TrustRegionDeathTest, CheckFailsWithNegativeRadiusForDiagonalSolver) {
567  // min x + y
568  // ||(x - 2.0, y - (-5.0))||_2 <= sqrt(2)
569  // [x*, y*] = [1.0, -6.0]
571  objective_vector(2);
574  center_point << 2.0, -5.0;
575  objective_vector << 1.0, 1.0;
576  const double target_radius = -std::sqrt(2.0);
577 
578  Sharder sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr);
579 
580  EXPECT_DEATH(
581  TrustRegionResult result = SolveDiagonalTrustRegion(
582  objective_vector, /*objective_matrix_diagonal=*/VectorXd::Zero(2),
584  /*norm_weights=*/VectorXd::Ones(2), target_radius, sharder,
585  /*solve_tolerance=*/1.0e-6),
586  "Check failed: target_radius >= 0.0");
587 }
588 
589 class ComputeLocalizedLagrangianBoundsTest
590  : public testing::TestWithParam<std::tuple<PrimalDualNorm, bool>> {
591  protected:
592  void SetUp() override {
593  const auto [primal_dual_norm, use_diagonal_qp_trust_region_solver] =
594  GetParam();
595  if (use_diagonal_qp_trust_region_solver &&
596  (primal_dual_norm == PrimalDualNorm::kMaxNorm)) {
597  GTEST_SKIP() << "The diagonal QP trust region solver can only be used "
598  << "when the underlying norms are Euclidean.";
599  }
600  }
601 };
602 
603 INSTANTIATE_TEST_SUITE_P(
604  TrustRegionNorm, ComputeLocalizedLagrangianBoundsTest,
605  testing::Combine(testing::Values(PrimalDualNorm::kEuclideanNorm,
607  testing::Bool()),
608  [](const testing::TestParamInfo<
609  ComputeLocalizedLagrangianBoundsTest::ParamType>& info) {
610  const absl::string_view suffix =
611  std::get<1>(info.param) ? "DiagonalTRSolver" : "LinearTimeTRSolver";
612  switch (std::get<0>(info.param)) {
614  return absl::StrCat("EuclideanNorm", "_", suffix);
616  return absl::StrCat("MaxNorm", "_", suffix);
617  }
618  });
619 
620 TEST_P(ComputeLocalizedLagrangianBoundsTest, ZeroGapAtOptimal) {
621  ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
622 
623  VectorXd primal_solution(4), dual_solution(4);
624  primal_solution << -1.0, 8.0, 1.0, 2.5;
625  dual_solution << -2.0, 0.0, 2.375, 2.0 / 3.0;
626 
627  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
628 
629  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
630  lp, primal_solution, dual_solution, primal_dual_norm,
631  /*primal_weight=*/1.0, /*radius=*/1.0,
632  /*primal_product=*/nullptr,
633  /*dual_product=*/nullptr, use_diagonal_qp_solver,
634  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-2);
635 
636  EXPECT_DOUBLE_EQ(bounds.radius, 1.0);
637  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, -20.0);
638  EXPECT_DOUBLE_EQ(bounds.lower_bound, -20.0);
639  EXPECT_DOUBLE_EQ(bounds.upper_bound, -20.0);
640 }
641 
642 // Sets the radius to the exact distance to optimal and checks that the
643 // optimal lagrangian value is contained in the computed interval.
644 TEST_P(ComputeLocalizedLagrangianBoundsTest, OptimalInBoundRange) {
645  ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
646 
647  // x_3 has a lower bound of 2.5.
648  VectorXd primal_solution(4);
649  primal_solution << 0.0, 0.0, 0.0, 3.0;
650  VectorXd dual_solution = VectorXd::Zero(4);
651 
652  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
653 
654  const double primal_distance_squared_to_optimal =
655  0.5 * (1.0 + 8.0 * 8.0 + 1.0 + 0.5 * 0.5);
656  const double dual_distance_squared_to_optimal =
657  0.5 * (4.0 + 2.375 * 2.375 + 4.0 / 9.0);
658  const double distance_to_optimal =
659  primal_dual_norm == PrimalDualNorm::kEuclideanNorm
660  ? std::sqrt(primal_distance_squared_to_optimal +
661  dual_distance_squared_to_optimal)
662  : std::sqrt(std::max(primal_distance_squared_to_optimal,
663  dual_distance_squared_to_optimal));
664 
665  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
666  lp, primal_solution, dual_solution, primal_dual_norm,
667  /*primal_weight=*/1.0,
668  /*radius=*/distance_to_optimal,
669  /*primal_product=*/nullptr,
670  /*dual_product=*/nullptr, use_diagonal_qp_solver,
671  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-6);
672 
673  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, 3.0);
674  EXPECT_LE(bounds.lower_bound, -20.0);
675  EXPECT_GE(bounds.upper_bound, -20.0);
676 }
677 
678 // When the radius is too small, the optimal value will not be contained in
679 // the computed interval.
680 TEST_P(ComputeLocalizedLagrangianBoundsTest, OptimalNotInBoundRange) {
681  ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
682 
683  // x_3 has a lower bound of 2.5.
684  VectorXd primal_solution(4);
685  primal_solution << 0.0, 0.0, 0.0, 3.0;
686  VectorXd dual_solution = VectorXd::Zero(4);
687 
688  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
689 
690  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
691  lp, primal_solution, dual_solution, primal_dual_norm,
692  /*primal_weight=*/1.0,
693  /*radius=*/0.1,
694  /*primal_product=*/nullptr,
695  /*dual_product=*/nullptr, use_diagonal_qp_solver,
696  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-6);
697  const double expected_lagrangian = 3.0;
698  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, expected_lagrangian);
699 
700  // Because the dual solution is all zero, the primal gradient is just the
701  // objective, [5.5, -2, -1, 1]. The dual gradient is the dual subgradient
702  // coefficient minus the primal product. With a zero dual, for one-sided
703  // constraints, the dual subgradient coefficient is the bound, and for
704  // two-sided constraints it is the violated bound (or zero if feasible). Thus,
705  // the dual subgradient coefficients are [12, 7, -4, -1], and the primal
706  // product is [6, 0, 0, -3], giving a dual gradient of [6, 7, -4, 2].
707 
708  switch (primal_dual_norm) {
710  // The target radius r = sqrt(2) * 0.1 ≈ 0.14, and the projected primal
711  // direction is d=[-5.5, 2, 1, -1]. The resulting delta is d / ||d|| * r,
712  // giving an objective delta of ||d|| * r.
713  EXPECT_NEAR(bounds.lower_bound,
714  expected_lagrangian - 0.1 * sqrt(2) * sqrt(36.25), 1.0e-6);
715  // The target radius r = sqrt(2) * 0.1 ≈ 0.14, and the projected dual
716  // direction is d=[6, 0, 0, 2]. The resulting delta is d / ||d|| * r,
717  // giving an objective delta of ||d|| * r.
718  EXPECT_NEAR(bounds.upper_bound,
719  expected_lagrangian + 0.1 * sqrt(2) * sqrt(40.0), 1.0e-6);
720  break;
722  // In this case, r = target_radius * sqrt(2) (because the euclidean norm
723  // includes a factor of 0.5). The projected combined direction is d=[-5.5,
724  // 2, 1, -1; 6, 0, 0, 2]. The resulting primal delta is d[primal] / ||d||
725  // * r, and the resulting dual delta is d[dual] / ||d|| * r.
726  EXPECT_NEAR(bounds.lower_bound,
727  expected_lagrangian - 0.1 * sqrt(2) * 36.25 / sqrt(76.25),
728  1.0e-6);
729  EXPECT_NEAR(bounds.upper_bound,
730  expected_lagrangian + 0.1 * sqrt(2) * 40 / sqrt(76.25),
731  1.0e-6);
732  break;
733  }
734 }
735 
736 // `kEuclideanNorm` isn't covered by this test because the analysis of the
737 // correct solution is more complex.
738 TEST(ComputeLocalizedLagrangianBoundsTest, ProcessesPrimalWeight) {
739  ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
740 
741  // x_3 has a lower bound of 2.5.
742  VectorXd primal_solution(4);
743  primal_solution << 0.0, 0.0, 0.0, 3.0;
744  VectorXd dual_solution = VectorXd::Zero(4);
745 
746  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
747  lp, primal_solution, dual_solution, PrimalDualNorm::kMaxNorm,
748  /*primal_weight=*/100.0,
749  /*radius=*/0.1,
750  /*primal_product=*/nullptr,
751  /*dual_product=*/nullptr,
752  /*use_diagonal_qp_trust_region_solver=*/false,
753  /*diagonal_qp_trust_region_solver_tolerance=*/0.0);
754  const double expected_lagrangian = 3.0;
755  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, expected_lagrangian);
756 
757  // Compared with `OptimalNotInBoundRange`, a primal weight of 100.0 translates
758  // to a 10x smaller radius in the primal and 10x larger radius in the dual.
759  EXPECT_LE(bounds.lower_bound, expected_lagrangian - 0.028);
760  EXPECT_GE(bounds.lower_bound, expected_lagrangian - 0.28);
761  EXPECT_GE(bounds.upper_bound, expected_lagrangian + 2.8);
762  EXPECT_LE(bounds.upper_bound, expected_lagrangian + 28);
763 }
764 
765 // Same as `OptimalInBoundRange` but providing `primal_product` and
766 // `dual_product`.
767 TEST_P(ComputeLocalizedLagrangianBoundsTest, AcceptsCachedProducts) {
768  ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
769 
770  // x_3 has a lower bound of 2.5.
771  VectorXd primal_solution(4);
772  primal_solution << 0.0, 0.0, 0.0, 3.0;
773  VectorXd dual_solution = VectorXd::Zero(4);
774 
775  VectorXd primal_product(4);
776  primal_product << 6.0, 0.0, 0.0, -3.0;
777  VectorXd dual_product = VectorXd::Zero(4);
778 
779  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
780 
781  const double primal_distance_squared_to_optimal =
782  0.5 * (1.0 + 8.0 * 8.0 + 1.0 + 0.5 * 0.5);
783  const double dual_distance_squared_to_optimal =
784  0.5 * (4.0 + 2.375 * 2.375 + 4.0 / 9.0);
785  const double distance_to_optimal =
786  primal_dual_norm == PrimalDualNorm::kEuclideanNorm
787  ? std::sqrt(primal_distance_squared_to_optimal +
788  dual_distance_squared_to_optimal)
789  : std::sqrt(std::max(primal_distance_squared_to_optimal,
790  dual_distance_squared_to_optimal));
791 
792  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
793  lp, primal_solution, dual_solution, primal_dual_norm,
794  /*primal_weight=*/1.0,
795  /*radius=*/distance_to_optimal,
796  /*primal_product=*/&primal_product,
797  /*dual_product=*/&dual_product, use_diagonal_qp_solver,
798  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-6);
799 
800  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, 3.0);
801  EXPECT_LE(bounds.lower_bound, -20.0);
802  EXPECT_GE(bounds.upper_bound, -20.0);
803 }
804 
805 // The LP:
806 // minimize 1.0 x
807 // s.t. 0 <= x <= 1 (as a constraint, not variable bound).
808 QuadraticProgram OneDimLp() {
809  QuadraticProgram lp(1, 1);
810  lp.constraint_lower_bounds << 0;
811  lp.constraint_upper_bounds << 1;
812  lp.variable_lower_bounds << -kInfinity;
813  lp.variable_upper_bounds << kInfinity;
814  std::vector<Eigen::Triplet<double, int64_t>> triplets = {{0, 0, 1}};
815  lp.constraint_matrix.setFromTriplets(triplets.begin(), triplets.end());
816  lp.objective_vector << 1.0;
817  return lp;
818 }
819 
820 // The QP:
821 // minimize 1.0 x + 1.0 * x^2
822 // s.t. 0 <= x <= 1 (as a constraint, not variable bound).
823 QuadraticProgram OneDimQp() {
824  QuadraticProgram qp(1, 1);
825  qp.constraint_lower_bounds << 0;
826  qp.constraint_upper_bounds << 1;
827  qp.variable_lower_bounds << -kInfinity;
828  qp.variable_upper_bounds << kInfinity;
829  std::vector<Eigen::Triplet<double, int64_t>> constraint_matrix_triplets = {
830  {0, 0, 1}};
831  qp.constraint_matrix.setFromTriplets(constraint_matrix_triplets.begin(),
832  constraint_matrix_triplets.end());
833  qp.objective_matrix.emplace();
834  qp.objective_matrix->resize(1);
835  qp.objective_matrix->diagonal() << 2;
836  qp.objective_vector << 1;
837  return qp;
838 }
839 
840 // Helper functions to compute the primal and dual gradient at a given point.
841 VectorXd GetPrimalGradient(const ShardedQuadraticProgram& sharded_qp,
842  const VectorXd& primal_solution,
843  const VectorXd& dual_solution) {
844  const auto dual_product = TransposedMatrixVectorProduct(
845  sharded_qp.Qp().constraint_matrix, dual_solution,
846  sharded_qp.ConstraintMatrixSharder());
847  return ComputePrimalGradient(sharded_qp, primal_solution, dual_product)
848  .gradient;
849 }
850 
851 VectorXd GetDualGradient(const ShardedQuadraticProgram& sharded_qp,
852  const VectorXd& primal_solution,
853  const VectorXd& dual_solution) {
854  const auto primal_product = TransposedMatrixVectorProduct(
855  sharded_qp.TransposedConstraintMatrix(), primal_solution,
856  sharded_qp.TransposedConstraintMatrixSharder());
857  return ComputeDualGradient(sharded_qp, dual_solution, primal_product)
858  .gradient;
859 }
860 
861 struct TestProblemData {
864  VectorXd center_point;
867  VectorXd norm_weights;
868 };
869 
870 // Generates the problem data corresponding to `OneDimLp()` as raw vectors with
871 // center point [x, y] = [0, -1].
872 TestProblemData GenerateTestLpProblemData(const double primal_weight) {
873  VectorXd objective_vector(2), center_point(2), norm_weights(2),
875  objective_vector << 2, -1;
876  center_point << 0, -1;
877  norm_weights << 0.5 * primal_weight, 0.5 / primal_weight;
880  return {.objective_vector = objective_vector,
881  .objective_matrix_diagonal = VectorXd::Zero(2),
882  .center_point = center_point,
883  .variable_lower_bounds = variable_lower_bounds,
884  .variable_upper_bounds = variable_upper_bounds,
885  .norm_weights = norm_weights};
886 }
887 
888 // Generates the problem data corresponding to `OneDimQp()` as raw vectors with
889 // center point [x, y] = [0, -1].
890 TestProblemData GenerateTestQpProblemData(const double primal_weight) {
891  TestProblemData lp_data = GenerateTestLpProblemData(primal_weight);
892  lp_data.objective_matrix_diagonal[0] = 2.0;
893  return lp_data;
894 }
895 
896 // This is a tiny problem where we can compute the exact solution, checking
897 // that `kMaxNorm` and `kEuclideanNorm` give different answers.
898 TEST_P(ComputeLocalizedLagrangianBoundsTest, NormsBehaveDifferently) {
899  ShardedQuadraticProgram lp(OneDimLp(), /*num_threads=*/2, /*num_shards=*/2);
900 
901  VectorXd primal_solution = VectorXd::Zero(1);
902  VectorXd dual_solution(1);
903  dual_solution << -1; // The upper bound is active.
904 
905  // The primal gradient is [2], and the dual gradient is [1]. Hence, the norm
906  // of the gradient is sqrt(5).
907 
908  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
909 
910  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
911  lp, primal_solution, dual_solution, primal_dual_norm,
912  /*primal_weight=*/1.0, /*radius=*/1.0 / std::sqrt(2.0),
913  /*primal_product=*/nullptr,
914  /*dual_product=*/nullptr, use_diagonal_qp_solver,
915  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-6);
916  const double expected_lagrangian = -1;
917  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, expected_lagrangian);
918 
919  switch (primal_dual_norm) {
921  EXPECT_DOUBLE_EQ(bounds.lower_bound, expected_lagrangian - 2.0);
922  EXPECT_DOUBLE_EQ(bounds.upper_bound, expected_lagrangian + 1.0);
923  break;
925  if (use_diagonal_qp_solver) {
926  EXPECT_NEAR(bounds.lower_bound,
927  expected_lagrangian - 4.0 / std::sqrt(5), 1.0e-6);
928  EXPECT_NEAR(bounds.upper_bound,
929  expected_lagrangian + 1.0 / std::sqrt(5), 1.0e-6);
930  } else {
931  EXPECT_DOUBLE_EQ(bounds.lower_bound,
932  expected_lagrangian - 4.0 / std::sqrt(5));
933  EXPECT_DOUBLE_EQ(bounds.upper_bound,
934  expected_lagrangian + 1.0 / std::sqrt(5));
935  }
936  break;
937  }
938 }
939 
940 // Like `NormsBehaveDifferently` but with a larger primal weight.
941 TEST_P(ComputeLocalizedLagrangianBoundsTest,
942  NormsBehaveDifferentlyWithLargePrimalWeight) {
943  ShardedQuadraticProgram lp(OneDimLp(), /*num_threads=*/2, /*num_shards=*/2);
944 
945  VectorXd primal_solution = VectorXd::Zero(1);
946  VectorXd dual_solution(1);
947  dual_solution << -1; // The upper bound is active.
948 
949  // The primal gradient is [2], and the dual gradient is [1].
950 
951  const auto [primal_dual_norm, use_diagonal_qp_solver] = GetParam();
952 
953  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
954  lp, primal_solution, dual_solution, primal_dual_norm,
955  /*primal_weight=*/100.0, /*radius=*/1.0 / std::sqrt(2.0),
956  /*primal_product=*/nullptr,
957  /*dual_product=*/nullptr, use_diagonal_qp_solver,
958  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-8);
959  const double expected_lagrangian = -1;
960  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, expected_lagrangian);
961 
962  switch (primal_dual_norm) {
964  EXPECT_DOUBLE_EQ(bounds.lower_bound, expected_lagrangian - 0.2);
965  EXPECT_DOUBLE_EQ(bounds.upper_bound, expected_lagrangian + 10.0);
966  break;
968  // Given c = [2.0, -1], w = [100.0, 0.01], this value is
969  // dot(c, (c ./ w) / norm(c ./ sqrt.(w))) (in Julia syntax).
970  if (use_diagonal_qp_solver) {
971  EXPECT_NEAR(bounds.upper_bound - bounds.lower_bound, 10.00199980003999,
972  10.002 * 1.0e-8);
973  } else {
974  EXPECT_DOUBLE_EQ(bounds.upper_bound - bounds.lower_bound,
975  10.00199980003999);
976  }
977  break;
978  }
979 }
980 
981 TEST(DiagonalTrustRegionSolverTest, JointSolverWorksWithOneDimQpUnitWeight) {
982  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
983  /*num_shards=*/2);
984  const auto problem_data = GenerateTestQpProblemData(/*primal_weight=*/1.0);
985  TrustRegionResult result = SolveDiagonalTrustRegion(
986  problem_data.objective_vector, problem_data.objective_matrix_diagonal,
987  problem_data.variable_lower_bounds, problem_data.variable_upper_bounds,
988  problem_data.center_point, problem_data.norm_weights,
989  /*target_radius=*/0.5,
990  Sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr),
991  /*solve_tolerance=*/1.0e-6);
992  EXPECT_THAT(result.solution,
993  ElementsAre(DoubleNear(-0.5, 1.0e-6), DoubleNear(-0.5, 1.0e-6)));
994  EXPECT_NEAR(result.solution_step_size, 4.0, 4.0 * 1.0e-6);
995  EXPECT_NEAR(result.objective_value, -1.25, 1.0e-6);
996 }
997 
998 TEST(DiagonalTrustRegionSolverTest,
999  DiagonalQpSolverWorksWithOneDimQpUnitWeight) {
1000  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
1001  /*num_shards=*/2);
1002  VectorXd primal_solution = VectorXd::Zero(1);
1003  VectorXd dual_solution = -1.0 * VectorXd::Ones(1);
1004  VectorXd primal_gradient =
1005  GetPrimalGradient(sharded_qp, primal_solution, dual_solution);
1006  VectorXd dual_gradient =
1007  GetDualGradient(sharded_qp, primal_solution, dual_solution);
1008  TrustRegionResult result = SolveDiagonalQpTrustRegion(
1009  sharded_qp, primal_solution, dual_solution, primal_gradient,
1010  dual_gradient, /*primal_weight=*/1.0, /*target_radius=*/0.5,
1011  /*solve_tolerance=*/1.0e-6);
1012  EXPECT_THAT(result.solution,
1013  ElementsAre(DoubleNear(-0.5, 1.0e-6), DoubleNear(-0.5, 1.0e-6)));
1014  EXPECT_NEAR(result.solution_step_size, 4.0, 4.0 * 1.0e-6);
1015  EXPECT_NEAR(result.objective_value, -1.25, 1.0e-6);
1016 }
1017 
1018 TEST(DiagonalTrustRegionSolverTest, JointSolverWorksWithOneDimQpLargeWeight) {
1019  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
1020  /*num_shards=*/2);
1021  const auto problem_data = GenerateTestQpProblemData(/*primal_weight=*/100.0);
1022  TrustRegionResult result = SolveDiagonalTrustRegion(
1023  problem_data.objective_vector, problem_data.objective_matrix_diagonal,
1024  problem_data.variable_lower_bounds, problem_data.variable_upper_bounds,
1025  problem_data.center_point, problem_data.norm_weights,
1026  /*target_radius=*/std::sqrt(2705.0 / 2) * (5.0 / 13),
1027  Sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr),
1028  /*solve_tolerance=*/1.0e-6);
1029  EXPECT_NEAR(result.solution_step_size, 1.0, 1.0e-6);
1030 }
1031 
1032 TEST(DiagonalTrustRegionSolverTest,
1033  DiagonalQpSolverWorksWithOneDimQpLargeWeight) {
1034  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
1035  /*num_shards=*/2);
1036  VectorXd primal_solution = VectorXd::Zero(1);
1037  VectorXd dual_solution = -1.0 * VectorXd::Ones(1);
1038  VectorXd primal_gradient =
1039  GetPrimalGradient(sharded_qp, primal_solution, dual_solution);
1040  VectorXd dual_gradient =
1041  GetDualGradient(sharded_qp, primal_solution, dual_solution);
1042  TrustRegionResult result = SolveDiagonalQpTrustRegion(
1043  sharded_qp, primal_solution, dual_solution, primal_gradient,
1044  dual_gradient, /*primal_weight=*/100.0,
1045  /*target_radius=*/std::sqrt(2705.0 / 2.0) * (5.0 / 13),
1046  /*solve_tolerance=*/1.0e-6);
1047  EXPECT_NEAR(result.solution_step_size, 1.0, 1.0e-6);
1048 }
1049 
1050 TEST(DiagonalTrustRegionSolverTest, JointSolverWorksWithOneDimQpSmallWeight) {
1051  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
1052  /*num_shards=*/2);
1053  const auto problem_data = GenerateTestQpProblemData(/*primal_weight=*/0.01);
1054  TrustRegionResult result = SolveDiagonalTrustRegion(
1055  problem_data.objective_vector, problem_data.objective_matrix_diagonal,
1056  problem_data.variable_lower_bounds, problem_data.variable_upper_bounds,
1057  problem_data.center_point, problem_data.norm_weights,
1058  /*target_radius=*/0.71063,
1059  Sharder(/*num_elements=*/2, /*num_shards=*/2, nullptr),
1060  /*solve_tolerance=*/1.0e-6);
1061  EXPECT_THAT(result.solution, ElementsAre(DoubleNear(-0.99950025, 1.0e-6),
1062  DoubleNear(-0.9, 1.0e-6)));
1063  EXPECT_NEAR(result.solution_step_size, 0.2, 1.0e-6);
1064  EXPECT_NEAR(result.objective_value, -1.0999996, 1.0e-6);
1065 }
1066 
1067 TEST(DiagonalTrustRegionSolverTest,
1068  DiagonalQpSolverWorksWithOneDimQpSmallWeight) {
1069  ShardedQuadraticProgram sharded_qp(OneDimQp(), /*num_threads=*/2,
1070  /*num_shards=*/2);
1071  VectorXd primal_solution = VectorXd::Zero(1);
1072  VectorXd dual_solution = -1.0 * VectorXd::Ones(1);
1073  VectorXd primal_gradient =
1074  GetPrimalGradient(sharded_qp, primal_solution, dual_solution);
1075  VectorXd dual_gradient =
1076  GetDualGradient(sharded_qp, primal_solution, dual_solution);
1077  TrustRegionResult result = SolveDiagonalQpTrustRegion(
1078  sharded_qp, primal_solution, dual_solution, primal_gradient,
1079  dual_gradient, /*primal_weight=*/0.01,
1080  /*target_radius=*/0.71063,
1081  /*solve_tolerance=*/1.0e-6);
1082  EXPECT_THAT(result.solution, ElementsAre(DoubleNear(-0.99950025, 1.0e-6),
1083  DoubleNear(-0.9, 1.0e-6)));
1084  EXPECT_NEAR(result.solution_step_size, 0.2, 1.0e-6);
1085  EXPECT_NEAR(result.objective_value, -1.0999996, 1.0e-6);
1086 }
1087 
1088 // This is a tiny QP where we can compute the exact solution.
1089 TEST(ComputeLocalizedLagrangianBoundsTest, SolvesForTestQpUnitWeight) {
1090  ShardedQuadraticProgram qp(OneDimQp(), /*num_threads=*/2, /*num_shards=*/2);
1091 
1092  VectorXd primal_solution = VectorXd::Zero(1);
1093  VectorXd dual_solution(1);
1094  dual_solution << -1; // The upper bound is active.
1095 
1096  // The primal gradient is [2], and the dual gradient is [1]. Hence, the norm
1097  // of the gradient is sqrt(5).
1098 
1099  LocalizedLagrangianBounds bounds = ComputeLocalizedLagrangianBounds(
1100  qp, primal_solution, dual_solution, PrimalDualNorm::kEuclideanNorm,
1101  /*primal_weight=*/1.0, /*radius=*/0.5,
1102  /*primal_product=*/nullptr,
1103  /*dual_product=*/nullptr, /*use_diagonal_qp_trust_region_solver=*/true,
1104  /*diagonal_qp_trust_region_solver_tolerance=*/1.0e-6);
1105  const double expected_lagrangian = -1;
1106  EXPECT_DOUBLE_EQ(bounds.lagrangian_value, expected_lagrangian);
1107  EXPECT_NEAR(bounds.upper_bound, expected_lagrangian + 0.5, 1.0e-5);
1108  EXPECT_NEAR(bounds.lower_bound, expected_lagrangian - 0.75, 1.0e-5);
1109 }
1110 
1111 } // namespace
1112 } // namespace operations_research::pdlp
int64_t max
Definition: alldiff_cst.cc:140
SharedBoundsManager * bounds
LagrangianPart ComputeDualGradient(const ShardedQuadraticProgram &sharded_qp, const VectorXd &dual_solution, const VectorXd &primal_product)
LagrangianPart ComputePrimalGradient(const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_product)
VectorXd TransposedMatrixVectorProduct(const Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > &matrix, const VectorXd &vector, const Sharder &sharder)
Definition: sharder.cc:158
TrustRegionResult SolveDiagonalTrustRegion(const VectorXd &objective_vector, const VectorXd &objective_matrix_diagonal, const VectorXd &variable_lower_bounds, const VectorXd &variable_upper_bounds, const VectorXd &center_point, const VectorXd &norm_weights, const double target_radius, const Sharder &sharder, const double solve_tolerance)
TrustRegionResult SolveDiagonalQpTrustRegion(const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, const VectorXd &primal_gradient, const VectorXd &dual_gradient, const double primal_weight, double target_radius, const double solve_tolerance)
TrustRegionResult SolveTrustRegion(const VectorXd &objective_vector, const VectorXd &variable_lower_bounds, const VectorXd &variable_upper_bounds, const VectorXd &center_point, const VectorXd &norm_weights, const double target_radius, const Sharder &sharder)
EigenArrayNearMatcherP2< Eigen::Array< T, Eigen::Dynamic, 1 >, double > EigenArrayNear(absl::Span< const T > data, double tolerance)
Definition: test_util.h:361
EigenArrayEqMatcherP< Eigen::Array< T, Eigen::Dynamic, 1 > > EigenArrayEq(absl::Span< const T > data)
Definition: test_util.h:375
QuadraticProgram TestLp()
Definition: test_util.cc:33
LocalizedLagrangianBounds ComputeLocalizedLagrangianBounds(const ShardedQuadraticProgram &sharded_qp, const VectorXd &primal_solution, const VectorXd &dual_solution, const PrimalDualNorm primal_dual_norm, const double primal_weight, const double radius, const VectorXd *primal_product, const VectorXd *dual_product, const bool use_diagonal_qp_trust_region_solver, const double diagonal_qp_trust_region_solver_tolerance)
int64_t Zero()
NOLINT.
TEST(LinearAssignmentTest, NullMatrix)
VectorXd variable_lower_bounds
VectorXd center_point
VectorXd variable_upper_bounds
VectorXd objective_vector
VectorXd norm_weights
VectorXd objective_matrix_diagonal