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"
29using ::testing::FieldsAre;
30using ::testing::Optional;
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};
39class TerminationTest :
public testing::TestWithParam<OptimalityNorm> {
41 void SetUp()
override {
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
48 kkt_matrix_pass_limit: 2000
49 iteration_limit: 10)pb");
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
67TEST_P(TerminationTest, NoTerminationWithEmptyIterationStats) {
73TEST_P(TerminationTest, TerminationWithNumericalError) {
75 absl::optional<TerminationReasonAndPointType> maybe_result =
80 Optional(FieldsAre(TERMINATION_REASON_NUMERICAL_ERROR, POINT_TYPE_NONE)));
83TEST_P(TerminationTest, TerminationWithTimeLimit) {
85 ParseTextOrDie<IterationStats>(R
"pb(cumulative_time_sec: 100.0)pb");
86 absl::optional<TerminationReasonAndPointType> maybe_result =
88 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_TIME_LIMIT,
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 =
97 EXPECT_THAT(maybe_result,
98 Optional(FieldsAre(TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT,
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
109 absl::optional<TerminationReasonAndPointType> maybe_result =
111 EXPECT_THAT(maybe_result,
112 Optional(FieldsAre(TERMINATION_REASON_PRIMAL_INFEASIBLE,
113 POINT_TYPE_ITERATE_DIFFERENCE)));
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
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
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
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
156 absl::optional<TerminationReasonAndPointType> maybe_result =
158 EXPECT_THAT(maybe_result,
159 Optional(FieldsAre(TERMINATION_REASON_DUAL_INFEASIBLE,
160 POINT_TYPE_AVERAGE_ITERATE)));
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
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
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
196TEST_P(TerminationTest, Optimal) {
197 const auto stats = ParseTextOrDie<IterationStats>(R
"pb(
198 convergence_information {
199 primal_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
208 absl::optional<TerminationReasonAndPointType> maybe_result =
210 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
211 POINT_TYPE_CURRENT_ITERATE)));
214TEST_P(TerminationTest, OptimalEvenWithNumericalError) {
215 const auto stats = ParseTextOrDie<IterationStats>(R
"pb(
216 convergence_information {
217 primal_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
227 absl::optional<TerminationReasonAndPointType> maybe_result =
230 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
231 POINT_TYPE_CURRENT_ITERATE)));
234TEST_P(TerminationTest, NoTerminationWithBadGap) {
235 const auto stats_bad_gap = ParseTextOrDie<IterationStats>(R
"pb(
236 convergence_information {
237 primal_objective: 10.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
249TEST_P(TerminationTest, NoTerminationWithInfiniteGap) {
250 const auto stats_infinite_gap = ParseTextOrDie<IterationStats>(R
"pb(
251 convergence_information {
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
264TEST_P(TerminationTest, NoTerminationWithBadPrimalResidual) {
265 const auto stats_bad_primal = ParseTextOrDie<IterationStats>(R
"pb(
266 convergence_information {
267 primal_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
279TEST_P(TerminationTest, NoTerminationWithBadDualResidual) {
280 const auto stats_bad_dual = ParseTextOrDie<IterationStats>(R
"pb(
281 convergence_information {
282 primal_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
296TEST_P(TerminationTest, ZeroToleranceZeroError) {
297 const auto stats = ParseTextOrDie<IterationStats>(R
"pb(
298 convergence_information {
299 primal_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
311 absl::optional<TerminationReasonAndPointType> maybe_result =
313 EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
314 POINT_TYPE_CURRENT_ITERATE)));
317INSTANTIATE_TEST_SUITE_P(OptNorm, TerminationTest,
318 testing::Values(OPTIMALITY_NORM_L2,
319 OPTIMALITY_NORM_L_INF));
321TEST(TerminationTest, L2AndLInfDiffer) {
322 auto test_criteria = ParseTextOrDie<TerminationCriteria>(R
"pb(
323 eps_optimal_relative: 1.0)pb");
329 double primal_residual;
330 absl::optional<TerminationReasonAndPointType> expected_l2;
331 absl::optional<TerminationReasonAndPointType> expected_l_inf;
334 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
335 .type = POINT_TYPE_CURRENT_ITERATE},
336 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
337 .type = POINT_TYPE_CURRENT_ITERATE}},
339 TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
340 .type = POINT_TYPE_CURRENT_ITERATE},
342 {15.0, absl::nullopt, absl::nullopt}};
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);
353 test_criteria.set_optimality_norm(OPTIMALITY_NORM_L2);
355 absl::optional<TerminationReasonAndPointType> maybe_result =
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);
364 test_criteria.set_optimality_norm(OPTIMALITY_NORM_L_INF);
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);
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
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);
391 ComputesRelativeResidualsForZeroAbsoluteTolerance) {
392 ConvergenceInformation stats;
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);
404 TestLpBoundNorms(), stats);
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));
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));
415 EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / 15.0);
419 ComputesRelativeResidualsForZeroRelativeTolerance) {
420 ConvergenceInformation stats;
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);
432 TestLpBoundNorms(), stats);
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);
442 ComputesCorrectRelativeResidualsForEqualTolerances) {
443 ConvergenceInformation stats;
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);
455 TestLpBoundNorms(), stats);
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)));
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)));
466 EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / (1.0 + 15.0));
T ParseTextOrDie(const std::string &input)
absl::optional< TerminationReasonAndPointType > CheckTerminationCriteria(const TerminationCriteria &criteria, const IterationStats &stats, const QuadraticProgramBoundNorms &bound_norms, const bool force_numerical_termination)
RelativeConvergenceInformation ComputeRelativeResiduals(const double eps_optimal_absolute, const double eps_optimal_relative, const QuadraticProgramBoundNorms &norms, const ConvergenceInformation &stats)
QuadraticProgramBoundNorms BoundNormsFromProblemStats(const QuadraticProgramStats &stats)
TEST(LinearAssignmentTest, NullMatrix)
TerminationCriteria test_criteria_