OR-Tools  9.3
termination_test.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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
18#include "absl/types/optional.h"
19#include "gmock/gmock.h"
20#include "gtest/gtest.h"
22#include "ortools/pdlp/solve_log.pb.h"
23#include "ortools/pdlp/solvers.pb.h"
24
26namespace {
27
29using ::testing::FieldsAre;
30using ::testing::Optional;
31
32QuadraticProgramBoundNorms TestLpBoundNorms() {
33 return {.l2_norm_primal_linear_objective = std::sqrt(36.25),
34 .l2_norm_constraint_bounds = std::sqrt(210.0),
35 .l_inf_norm_primal_linear_objective = 5.5,
36 .l_inf_norm_constraint_bounds = 12.0};
37}
38
39class TerminationTest : public testing::TestWithParam<OptimalityNorm> {
40 protected:
41 void SetUp() override {
42 test_criteria_ = ParseTextOrDie<TerminationCriteria>(R"pb(
43 eps_optimal_absolute: 1.0e-4
44 eps_optimal_relative: 1.0e-4
45 eps_primal_infeasible: 1.0e-6
46 eps_dual_infeasible: 1.0e-6
47 time_sec_limit: 1.0
48 kkt_matrix_pass_limit: 2000
49 iteration_limit: 10)pb");
50 test_criteria_.set_optimality_norm(GetParam());
51 }
52
53 TerminationCriteria test_criteria_;
54};
55
56TEST_P(TerminationTest, NoTerminationWithLargeGap) {
57 IterationStats stats = ParseTextOrDie<IterationStats>(R"pb(
58 convergence_information {
59 # Ensures that optimality conditions are not met.
60 primal_objective: 50.0
61 dual_objective: -50.0
62 })pb");
63 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()),
64 absl::nullopt);
65}
66
67TEST_P(TerminationTest, NoTerminationWithEmptyIterationStats) {
68 IterationStats stats;
69 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()),
70 absl::nullopt);
71}
72
73TEST_P(TerminationTest, TerminationWithNumericalError) {
74 IterationStats stats;
75 absl::optional<TerminationReasonAndPointType> maybe_result =
76 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms(),
77 /*force_numerical_termination=*/true);
78 EXPECT_THAT(
79 maybe_result,
80 Optional(FieldsAre(TERMINATION_REASON_NUMERICAL_ERROR, POINT_TYPE_NONE)));
81}
82
83TEST_P(TerminationTest, TerminationWithTimeLimit) {
84 const auto stats =
85 ParseTextOrDie<IterationStats>(R"pb(cumulative_time_sec: 100.0)pb");
86 absl::optional<TerminationReasonAndPointType> maybe_result =
87 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
88 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_TIME_LIMIT,
89 POINT_TYPE_NONE)));
90}
91
92TEST_P(TerminationTest, TerminationWithKktMatrixPassLimit) {
93 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
94 cumulative_kkt_matrix_passes: 2500)pb");
95 absl::optional<TerminationReasonAndPointType> maybe_result =
96 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
97 EXPECT_THAT(maybe_result,
98 Optional(FieldsAre(TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT,
99 POINT_TYPE_NONE)));
100}
101
102TEST_P(TerminationTest, PrimalInfeasibleFromIterateDifference) {
103 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
104 infeasibility_information: {
105 dual_ray_objective: 1.0
106 max_dual_ray_infeasibility: 1.0e-16
107 candidate_type: POINT_TYPE_ITERATE_DIFFERENCE
108 })pb");
109 absl::optional<TerminationReasonAndPointType> maybe_result =
110 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
111 EXPECT_THAT(maybe_result,
112 Optional(FieldsAre(TERMINATION_REASON_PRIMAL_INFEASIBLE,
113 POINT_TYPE_ITERATE_DIFFERENCE)));
114}
115
116TEST_P(TerminationTest, NoTerminationWithInfeasibleDualRay) {
117 const auto stats_infeasible_ray = ParseTextOrDie<IterationStats>(R"pb(
118 infeasibility_information: {
119 dual_ray_objective: 1.0
120 max_dual_ray_infeasibility: 1.0e-5 # Too large
121 })pb");
122 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infeasible_ray,
123 TestLpBoundNorms()),
124 absl::nullopt);
125}
126
127TEST_P(TerminationTest, NoTerminationWithNegativeDualRayObjective) {
128 const auto stats_wrong_sign = ParseTextOrDie<IterationStats>(R"pb(
129 infeasibility_information: {
130 dual_ray_objective: -1.0 # Wrong sign
131 max_dual_ray_infeasibility: 0.0
132 })pb");
133 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_wrong_sign,
134 TestLpBoundNorms()),
135 absl::nullopt);
136}
137
138TEST_P(TerminationTest, NoTerminationWithZeroDualRayObjective) {
139 const auto stats_objective_zero = ParseTextOrDie<IterationStats>(R"pb(
140 infeasibility_information: {
141 dual_ray_objective: 0.0
142 max_dual_ray_infeasibility: 0.0
143 })pb");
144 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_objective_zero,
145 TestLpBoundNorms()),
146 absl::nullopt);
147}
148
149TEST_P(TerminationTest, DualInfeasibleFromAverageIterate) {
150 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
151 infeasibility_information: {
152 primal_ray_linear_objective: -1.0
153 max_primal_ray_infeasibility: 1.0e-16
154 candidate_type: POINT_TYPE_AVERAGE_ITERATE
155 })pb");
156 absl::optional<TerminationReasonAndPointType> maybe_result =
157 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
158 EXPECT_THAT(maybe_result,
159 Optional(FieldsAre(TERMINATION_REASON_DUAL_INFEASIBLE,
160 POINT_TYPE_AVERAGE_ITERATE)));
161}
162
163TEST_P(TerminationTest, NoTerminationWithInfeasiblePrimalRay) {
164 const auto stats_infeasible_ray = ParseTextOrDie<IterationStats>(R"pb(
165 infeasibility_information: {
166 primal_ray_linear_objective: -1.0
167 max_primal_ray_infeasibility: 1.0e-5 # Too large
168 })pb");
169 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infeasible_ray,
170 TestLpBoundNorms()),
171 absl::nullopt);
172}
173
174TEST_P(TerminationTest, NoTerminationWithPositivePrimalRayObjective) {
175 const auto stats_wrong_sign = ParseTextOrDie<IterationStats>(R"pb(
176 infeasibility_information: {
177 primal_ray_linear_objective: 1.0 # Wrong sign
178 max_primal_ray_infeasibility: 0.0
179 })pb");
180 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_wrong_sign,
181 TestLpBoundNorms()),
182 absl::nullopt);
183}
184
185TEST_P(TerminationTest, NoTerminationWithZeroPrimalRayObjective) {
186 const auto stats_objective_zero = ParseTextOrDie<IterationStats>(R"pb(
187 infeasibility_information: {
188 primal_ray_linear_objective: 0.0
189 max_primal_ray_infeasibility: 0.0
190 })pb");
191 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_objective_zero,
192 TestLpBoundNorms()),
193 absl::nullopt);
194}
195
196TEST_P(TerminationTest, Optimal) {
197 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
198 convergence_information {
199 primal_objective: 1.0
200 dual_objective: 1.0
201 l_inf_primal_residual: 0.0
202 l_inf_dual_residual: 0.0
203 l2_primal_residual: 0.0
204 l2_dual_residual: 0.0
205 candidate_type: POINT_TYPE_CURRENT_ITERATE
206 })pb");
207
208 absl::optional<TerminationReasonAndPointType> maybe_result =
209 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
210 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
211 POINT_TYPE_CURRENT_ITERATE)));
212}
213
214TEST_P(TerminationTest, OptimalEvenWithNumericalError) {
215 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
216 convergence_information {
217 primal_objective: 1.0
218 dual_objective: 1.0
219 l_inf_primal_residual: 0.0
220 l_inf_dual_residual: 0.0
221 l2_primal_residual: 0.0
222 l2_dual_residual: 0.0
223 candidate_type: POINT_TYPE_CURRENT_ITERATE
224 })pb");
225 // Tests that OPTIMAL overrides NUMERICAL_ERROR when
226 // force_numerical_termination == true.
227 absl::optional<TerminationReasonAndPointType> maybe_result =
228 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms(),
229 /*force_numerical_termination=*/true);
230 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
231 POINT_TYPE_CURRENT_ITERATE)));
232}
233
234TEST_P(TerminationTest, NoTerminationWithBadGap) {
235 const auto stats_bad_gap = ParseTextOrDie<IterationStats>(R"pb(
236 convergence_information {
237 primal_objective: 10.0
238 dual_objective: 1.0
239 l_inf_primal_residual: 0.0
240 l_inf_dual_residual: 0.0
241 l2_primal_residual: 0.0
242 l2_dual_residual: 0.0
243 })pb");
244 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_gap,
245 TestLpBoundNorms()),
246 absl::nullopt);
247}
248
249TEST_P(TerminationTest, NoTerminationWithInfiniteGap) {
250 const auto stats_infinite_gap = ParseTextOrDie<IterationStats>(R"pb(
251 convergence_information {
252 primal_objective: 0
253 dual_objective: -Inf
254 l_inf_primal_residual: 0.0
255 l_inf_dual_residual: 0.0
256 l2_primal_residual: 0.0
257 l2_dual_residual: 0.0
258 })pb");
259 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infinite_gap,
260 TestLpBoundNorms()),
261 absl::nullopt);
262}
263
264TEST_P(TerminationTest, NoTerminationWithBadPrimalResidual) {
265 const auto stats_bad_primal = ParseTextOrDie<IterationStats>(R"pb(
266 convergence_information {
267 primal_objective: 1.0
268 dual_objective: 1.0
269 l_inf_primal_residual: 1.0
270 l_inf_dual_residual: 0.0
271 l2_primal_residual: 1.0
272 l2_dual_residual: 0.0
273 })pb");
274 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_primal,
275 TestLpBoundNorms()),
276 absl::nullopt);
277}
278
279TEST_P(TerminationTest, NoTerminationWithBadDualResidual) {
280 const auto stats_bad_dual = ParseTextOrDie<IterationStats>(R"pb(
281 convergence_information {
282 primal_objective: 1.0
283 dual_objective: 1.0
284 l_inf_primal_residual: 0.0
285 l_inf_dual_residual: 1.0
286 l2_primal_residual: 0.0
287 l2_dual_residual: 1.0
288 })pb");
289 EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_dual,
290 TestLpBoundNorms()),
291 absl::nullopt);
292}
293
294// Tests that optimality is checked with non-strict inequalities, as per the
295// definitions in solvers.proto.
296TEST_P(TerminationTest, ZeroToleranceZeroError) {
297 const auto stats = ParseTextOrDie<IterationStats>(R"pb(
298 convergence_information {
299 primal_objective: 1.0
300 dual_objective: 1.0
301 l_inf_primal_residual: 0.0
302 l_inf_dual_residual: 0.0
303 l2_primal_residual: 0.0
304 l2_dual_residual: 0.0
305 candidate_type: POINT_TYPE_CURRENT_ITERATE
306 })pb");
307
308 test_criteria_.set_eps_optimal_absolute(0.0);
309 test_criteria_.set_eps_optimal_relative(0.0);
310
311 absl::optional<TerminationReasonAndPointType> maybe_result =
312 CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
313 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
314 POINT_TYPE_CURRENT_ITERATE)));
315}
316
317INSTANTIATE_TEST_SUITE_P(OptNorm, TerminationTest,
318 testing::Values(OPTIMALITY_NORM_L2,
319 OPTIMALITY_NORM_L_INF));
320
321TEST(TerminationTest, L2AndLInfDiffer) {
322 auto test_criteria = ParseTextOrDie<TerminationCriteria>(R"pb(
323 eps_optimal_relative: 1.0)pb");
324
325 // For L2, optimality requires norm(primal_residual, 2) <= 14.49
326 // For LInf, optimality requires norm(primal_residual, Inf) <= 12.0
327
328 struct {
329 double primal_residual;
330 absl::optional<TerminationReasonAndPointType> expected_l2;
331 absl::optional<TerminationReasonAndPointType> expected_l_inf;
332 } test_configs[] = {
333 {10.0,
334 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
335 .type = POINT_TYPE_CURRENT_ITERATE},
336 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
337 .type = POINT_TYPE_CURRENT_ITERATE}},
338 {13.0,
339 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
340 .type = POINT_TYPE_CURRENT_ITERATE},
341 absl::nullopt},
342 {15.0, absl::nullopt, absl::nullopt}};
343
344 for (const auto& config : test_configs) {
345 IterationStats stats;
346 auto* convergence_info = stats.add_convergence_information();
347 convergence_info->set_primal_objective(1.0);
348 convergence_info->set_dual_objective(1.0);
349 convergence_info->set_l2_primal_residual(config.primal_residual);
350 convergence_info->set_l_inf_primal_residual(config.primal_residual);
351 convergence_info->set_candidate_type(POINT_TYPE_CURRENT_ITERATE);
352
353 test_criteria.set_optimality_norm(OPTIMALITY_NORM_L2);
354
355 absl::optional<TerminationReasonAndPointType> maybe_result =
356 CheckTerminationCriteria(test_criteria, stats, TestLpBoundNorms());
357 ASSERT_TRUE(maybe_result.has_value() == config.expected_l2.has_value())
358 << "primal_residual: " << config.primal_residual;
359 if (config.expected_l2.has_value()) {
360 EXPECT_EQ(maybe_result->reason, config.expected_l2->reason);
361 EXPECT_EQ(maybe_result->type, config.expected_l2->type);
362 }
363
364 test_criteria.set_optimality_norm(OPTIMALITY_NORM_L_INF);
365 maybe_result =
366 CheckTerminationCriteria(test_criteria, stats, TestLpBoundNorms());
367 ASSERT_TRUE(maybe_result.has_value() == config.expected_l_inf.has_value())
368 << "primal_residual: " << config.primal_residual;
369 if (config.expected_l_inf.has_value()) {
370 EXPECT_EQ(maybe_result->reason, config.expected_l_inf->reason);
371 EXPECT_EQ(maybe_result->type, config.expected_l_inf->type);
372 }
373 }
374}
375
376TEST(BoundNormsFromProblemStats, ExtractsBoundNorms) {
377 const auto qp_stats = ParseTextOrDie<QuadraticProgramStats>(R"pb(
378 objective_vector_l2_norm: 4.0
379 combined_bounds_l2_norm: 3.0
380 objective_vector_abs_max: 1.0
381 combined_bounds_max: 2.0
382 )pb");
383 const QuadraticProgramBoundNorms norms = BoundNormsFromProblemStats(qp_stats);
384 EXPECT_EQ(norms.l2_norm_primal_linear_objective, 4.0);
385 EXPECT_EQ(norms.l2_norm_constraint_bounds, 3.0);
386 EXPECT_EQ(norms.l_inf_norm_primal_linear_objective, 1.0);
387 EXPECT_EQ(norms.l_inf_norm_constraint_bounds, 2.0);
388}
389
391 ComputesRelativeResidualsForZeroAbsoluteTolerance) {
392 ConvergenceInformation stats;
393 // If the absolute error tolerance is 0.0, the relative residuals are just the
394 // absolute residuals divided by the corresponding norms (the actual nonzero
395 // value of the relative error tolerance doesn't matter).
396 stats.set_primal_objective(10.0);
397 stats.set_dual_objective(5.0);
398 stats.set_l_inf_primal_residual(1.0);
399 stats.set_l2_primal_residual(1.0);
400 stats.set_l_inf_dual_residual(1.0);
401 stats.set_l2_dual_residual(1.0);
402 const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
403 /*eps_optimal_absolute=*/0.0, /*eps_optimal_relative=*/1.0e-6,
404 TestLpBoundNorms(), stats);
405
406 EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / 12.0);
407 EXPECT_EQ(relative_info.relative_l2_primal_residual, 1.0 / std::sqrt(210.0));
408
409 EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / 5.5);
410 EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / sqrt(36.25));
411
412 // The relative optimality gap should just be the objective difference divided
413 // by the sum of absolute values (the actual nonzero value of the relative
414 // error tolerance doesn't matter).
415 EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / 15.0);
416}
417
419 ComputesRelativeResidualsForZeroRelativeTolerance) {
420 ConvergenceInformation stats;
421 // If the relative error tolerance is 0.0, all of the relative residuals and
422 // the relative optimality gap should be 0.0, no matter what the absolute
423 // error tolerance is.
424 stats.set_primal_objective(10.0);
425 stats.set_dual_objective(5.0);
426 stats.set_l_inf_primal_residual(1.0);
427 stats.set_l2_primal_residual(1.0);
428 stats.set_l_inf_dual_residual(1.0);
429 stats.set_l2_dual_residual(1.0);
430 const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
431 /*eps_optimal_absolute=*/0.0, /*eps_optimal_relative=*/0.0,
432 TestLpBoundNorms(), stats);
433
434 EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 0.0);
435 EXPECT_EQ(relative_info.relative_l2_primal_residual, 0.0);
436 EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 0.0);
437 EXPECT_EQ(relative_info.relative_l2_dual_residual, 0.0);
438 EXPECT_EQ(relative_info.relative_optimality_gap, 0.0);
439}
440
442 ComputesCorrectRelativeResidualsForEqualTolerances) {
443 ConvergenceInformation stats;
444 // If the absolute error tolerance and relative error tolerance are equal (and
445 // nonzero), the relative residuals are the absolute residuals divided by 1.0
446 // plus the corresponding norms.
447 stats.set_primal_objective(10.0);
448 stats.set_dual_objective(5.0);
449 stats.set_l_inf_primal_residual(1.0);
450 stats.set_l2_primal_residual(1.0);
451 stats.set_l_inf_dual_residual(1.0);
452 stats.set_l2_dual_residual(1.0);
453 const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
454 /*eps_optimal_absolute=*/1.0e-6, /*eps_optimal_relative=*/1.0e-6,
455 TestLpBoundNorms(), stats);
456
457 EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / (1.0 + 12.0));
458 EXPECT_EQ(relative_info.relative_l2_primal_residual,
459 1.0 / (1.0 + std::sqrt(210.0)));
460
461 EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / (1.0 + 5.5));
462 EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / (1.0 + sqrt(36.25)));
463
464 // The relative optimality gap should just be the objective difference divided
465 // by 1.0 + the sum of absolute values.
466 EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / (1.0 + 15.0));
467}
468
469} // namespace
470} // namespace operations_research::pdlp
T ParseTextOrDie(const std::string &input)
Definition: protobuf_util.h:77
absl::optional< TerminationReasonAndPointType > CheckTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const QuadraticProgramBoundNorms &bound_norms, const bool force_numerical_termination)
Definition: termination.cc:90
RelativeConvergenceInformation ComputeRelativeResiduals(const double eps_optimal_absolute, const double eps_optimal_relative, const QuadraticProgramBoundNorms &norms, const ConvergenceInformation &stats)
Definition: termination.cc:147
QuadraticProgramBoundNorms BoundNormsFromProblemStats(const QuadraticProgramStats &stats)
Definition: termination.cc:138
TEST(LinearAssignmentTest, NullMatrix)
TerminationCriteria test_criteria_