diff --git a/ortools/pdlp/primal_dual_hybrid_gradient.cc b/ortools/pdlp/primal_dual_hybrid_gradient.cc index 080e1bad6c..6fb2cec63f 100644 --- a/ortools/pdlp/primal_dual_hybrid_gradient.cc +++ b/ortools/pdlp/primal_dual_hybrid_gradient.cc @@ -32,6 +32,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" +#include "ortools/base/check.h" #include "ortools/base/logging.h" #include "ortools/base/mathutil.h" #include "ortools/base/timer.h" @@ -175,8 +176,8 @@ std::string ToString(const IterationStats& iter_stats, if (convergence_information.has_value()) { const RelativeConvergenceInformation relative_information = ComputeRelativeResiduals( - EffectiveOptimalityCriteria(termination_criteria), bound_norms, - *convergence_information); + EffectiveOptimalityCriteria(termination_criteria), + *convergence_information, bound_norms); return absl::StrCat(iteration_string, " | ", ToString(*convergence_information, relative_information, termination_criteria.optimality_norm())); @@ -200,8 +201,8 @@ std::string ToShortString(const IterationStats& iter_stats, if (convergence_information.has_value()) { const RelativeConvergenceInformation relative_information = ComputeRelativeResiduals( - EffectiveOptimalityCriteria(termination_criteria), bound_norms, - *convergence_information); + EffectiveOptimalityCriteria(termination_criteria), + *convergence_information, bound_norms); return absl::StrCat( iteration_string, " | ", ToShortString(*convergence_information, relative_information, diff --git a/ortools/pdlp/sharder_test.cc b/ortools/pdlp/sharder_test.cc index 1ee7ae7aaa..ecd52b9415 100644 --- a/ortools/pdlp/sharder_test.cc +++ b/ortools/pdlp/sharder_test.cc @@ -150,7 +150,6 @@ TEST(SharderTest, UniformSharderExcessiveShards) { VerifySharder(sharder, 7, {1, 1, 1, 1, 1}); } -// Regression test for b/225385276 TEST(SharderTest, UniformSharderHugeNumShards) { Sharder sharder(/*num_elements=*/5, /*num_shards=*/1'000'000'000, nullptr); EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0, 1, 2, 3, 4, 5)); diff --git a/ortools/pdlp/solvers_proto_validation.cc b/ortools/pdlp/solvers_proto_validation.cc index 2a3ef693e8..008abcfa0d 100644 --- a/ortools/pdlp/solvers_proto_validation.cc +++ b/ortools/pdlp/solvers_proto_validation.cc @@ -36,6 +36,7 @@ absl::Status CheckNonNegative(const double value, } return absl::OkStatus(); } + absl::Status ValidateTerminationCriteria(const TerminationCriteria& criteria) { if (criteria.optimality_norm() != OPTIMALITY_NORM_L_INF && criteria.optimality_norm() != OPTIMALITY_NORM_L2) { @@ -100,11 +101,13 @@ absl::Status ValidateTerminationCriteria(const TerminationCriteria& criteria) { CheckNonNegative(criteria.eps_dual_infeasible(), "eps_dual_infeasible")); RETURN_IF_ERROR( CheckNonNegative(criteria.time_sec_limit(), "time_sec_limit")); + if (criteria.iteration_limit() < 0) { return InvalidArgumentError("iteration_limit must be non-negative"); } RETURN_IF_ERROR(CheckNonNegative(criteria.kkt_matrix_pass_limit(), "kkt_matrix_pass_limit")); + return OkStatus(); } diff --git a/ortools/pdlp/solvers_proto_validation_test.cc b/ortools/pdlp/solvers_proto_validation_test.cc index 039d358cb8..ff280e2a43 100644 --- a/ortools/pdlp/solvers_proto_validation_test.cc +++ b/ortools/pdlp/solvers_proto_validation_test.cc @@ -28,6 +28,7 @@ namespace operations_research::pdlp { namespace { using ::google::protobuf::util::ParseTextOrDie; + using ::testing::HasSubstr; TEST(ValidateTerminationCriteria, DefaultIsValid) { diff --git a/ortools/pdlp/termination.cc b/ortools/pdlp/termination.cc index 4ff7a6160f..7ccec93e00 100644 --- a/ortools/pdlp/termination.cc +++ b/ortools/pdlp/termination.cc @@ -24,18 +24,27 @@ namespace operations_research::pdlp { -namespace { - -bool OptimalityCriteriaMet( - const OptimalityNorm optimality_norm, +bool ObjectiveGapMet( const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, - const ConvergenceInformation& stats, - const QuadraticProgramBoundNorms& bound_norms) { + const ConvergenceInformation& stats) { + if (std::isinf(optimality_criteria.eps_optimal_objective_gap_absolute()) || + std::isinf(optimality_criteria.eps_optimal_objective_gap_relative())) { + return true; + } const double abs_obj = std::abs(stats.primal_objective()) + std::abs(stats.dual_objective()); const double gap = std::abs(stats.primal_objective() - stats.dual_objective()); + return std::isfinite(abs_obj) && + gap <= optimality_criteria.eps_optimal_objective_gap_absolute() + + optimality_criteria.eps_optimal_objective_gap_relative() * + abs_obj; +} +bool OptimalityCriteriaMet( + const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, + const ConvergenceInformation& stats, const OptimalityNorm optimality_norm, + const QuadraticProgramBoundNorms& bound_norms) { double primal_err; double primal_err_baseline; double dual_err; @@ -59,20 +68,25 @@ bool OptimalityCriteriaMet( << OptimalityNorm_Name(optimality_norm); } - return dual_err <= - optimality_criteria.eps_optimal_dual_residual_absolute() + - optimality_criteria.eps_optimal_dual_residual_relative() * - dual_err_baseline && - primal_err <= - optimality_criteria.eps_optimal_primal_residual_absolute() + - optimality_criteria.eps_optimal_primal_residual_relative() * - primal_err_baseline && - std::isfinite(abs_obj) && - gap <= optimality_criteria.eps_optimal_objective_gap_absolute() + - optimality_criteria.eps_optimal_objective_gap_relative() * - abs_obj; + const bool primal_err_ok = + std::isinf(optimality_criteria.eps_optimal_primal_residual_absolute()) || + std::isinf(optimality_criteria.eps_optimal_primal_residual_relative()) || + primal_err <= + optimality_criteria.eps_optimal_primal_residual_absolute() + + optimality_criteria.eps_optimal_primal_residual_relative() * + primal_err_baseline; + const bool dual_err_ok = + std::isinf(optimality_criteria.eps_optimal_dual_residual_absolute()) || + std::isinf(optimality_criteria.eps_optimal_dual_residual_relative()) || + dual_err <= optimality_criteria.eps_optimal_dual_residual_absolute() + + optimality_criteria.eps_optimal_dual_residual_relative() * + dual_err_baseline; + return primal_err_ok && dual_err_ok && + ObjectiveGapMet(optimality_criteria, stats); } +namespace { + // Checks if the criteria for primal infeasibility are approximately // satisfied; see https://developers.google.com/optimization/lp/pdlp_math for // more details. @@ -97,6 +111,7 @@ bool DualInfeasibilityCriteriaMet(double eps_dual_infeasible, } } // namespace + TerminationCriteria::DetailedOptimalityCriteria EffectiveOptimalityCriteria( const TerminationCriteria& termination_criteria) { if (termination_criteria.has_detailed_optimality_criteria()) { @@ -165,8 +180,8 @@ std::optional CheckTerminationCriteria( TerminationCriteria::DetailedOptimalityCriteria optimality_criteria = EffectiveOptimalityCriteria(criteria); for (const auto& convergence_stats : stats.convergence_information()) { - if (OptimalityCriteriaMet(criteria.optimality_norm(), optimality_criteria, - convergence_stats, bound_norms)) { + if (OptimalityCriteriaMet(optimality_criteria, convergence_stats, + criteria.optimality_norm(), bound_norms)) { return TerminationReasonAndPointType{ .reason = TERMINATION_REASON_OPTIMAL, .type = convergence_stats.candidate_type()}; @@ -209,11 +224,14 @@ QuadraticProgramBoundNorms BoundNormsFromProblemStats( RelativeConvergenceInformation ComputeRelativeResiduals( const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, - const QuadraticProgramBoundNorms& norms, - const ConvergenceInformation& stats) { + const ConvergenceInformation& stats, + const QuadraticProgramBoundNorms& bound_norms) { auto eps_ratio = [](const double absolute, const double relative) { - return relative == 0.0 ? std::numeric_limits::infinity() - : absolute / relative; + // The both-infinite case avoids a NAN. + return (std::isinf(absolute) && std::isinf(relative)) + ? 1.0 + : (relative == 0.0 ? std::numeric_limits::infinity() + : absolute / relative); }; const double eps_ratio_primal = eps_ratio(optimality_criteria.eps_optimal_primal_residual_absolute(), @@ -227,16 +245,16 @@ RelativeConvergenceInformation ComputeRelativeResiduals( RelativeConvergenceInformation info; info.relative_l_inf_primal_residual = stats.l_inf_primal_residual() / - (eps_ratio_primal + norms.l_inf_norm_constraint_bounds); + (eps_ratio_primal + bound_norms.l_inf_norm_constraint_bounds); info.relative_l2_primal_residual = stats.l2_primal_residual() / - (eps_ratio_primal + norms.l2_norm_constraint_bounds); + (eps_ratio_primal + bound_norms.l2_norm_constraint_bounds); info.relative_l_inf_dual_residual = stats.l_inf_dual_residual() / - (eps_ratio_dual + norms.l_inf_norm_primal_linear_objective); + (eps_ratio_dual + bound_norms.l_inf_norm_primal_linear_objective); info.relative_l2_dual_residual = stats.l2_dual_residual() / - (eps_ratio_dual + norms.l2_norm_primal_linear_objective); + (eps_ratio_dual + bound_norms.l2_norm_primal_linear_objective); const double abs_obj = std::abs(stats.primal_objective()) + std::abs(stats.dual_objective()); const double gap = stats.primal_objective() - stats.dual_objective(); diff --git a/ortools/pdlp/termination.h b/ortools/pdlp/termination.h index af757c5738..e71b3dcb4f 100644 --- a/ortools/pdlp/termination.h +++ b/ortools/pdlp/termination.h @@ -44,6 +44,7 @@ TerminationCriteria::DetailedOptimalityCriteria EffectiveOptimalityCriteria( // unit tests where no TerminationCriteria is naturally available. TerminationCriteria::DetailedOptimalityCriteria EffectiveOptimalityCriteria( const TerminationCriteria::SimpleOptimalityCriteria& simple_criteria); + // Checks if any of the simple termination criteria are satisfied by `stats`, // and returns a termination reason if so, and nullopt otherwise. The "simple" // termination criteria are `time_sec_limit`, `iteration_limit`, @@ -108,9 +109,19 @@ struct RelativeConvergenceInformation { RelativeConvergenceInformation ComputeRelativeResiduals( const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, - const QuadraticProgramBoundNorms& norms, + const ConvergenceInformation& stats, + const QuadraticProgramBoundNorms& bound_norms); + +bool ObjectiveGapMet( + const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, const ConvergenceInformation& stats); +// Determines if the optimality criteria are met. +bool OptimalityCriteriaMet( + const TerminationCriteria::DetailedOptimalityCriteria& optimality_criteria, + const ConvergenceInformation& stats, const OptimalityNorm optimality_norm, + const QuadraticProgramBoundNorms& bound_norms); + } // namespace operations_research::pdlp #endif // PDLP_TERMINATION_H_ diff --git a/ortools/pdlp/termination_test.cc b/ortools/pdlp/termination_test.cc index c71a34f68e..b6e748bd8c 100644 --- a/ortools/pdlp/termination_test.cc +++ b/ortools/pdlp/termination_test.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include "gmock/gmock.h" @@ -24,10 +25,8 @@ #include "ortools/pdlp/solvers.pb.h" namespace operations_research::pdlp { - -bool operator==( - const TerminationCriteria::DetailedOptimalityCriteria& lhs, - const TerminationCriteria::DetailedOptimalityCriteria& rhs) { +bool operator==(const TerminationCriteria::DetailedOptimalityCriteria& lhs, + const TerminationCriteria::DetailedOptimalityCriteria& rhs) { if (lhs.eps_optimal_primal_residual_absolute() != rhs.eps_optimal_primal_residual_absolute()) { return false; @@ -69,6 +68,13 @@ QuadraticProgramBoundNorms TestLpBoundNorms() { .l_inf_norm_constraint_bounds = 12.0}; } +QuadraticProgramBoundNorms ZeroLpBoundNorms() { + return {.l2_norm_primal_linear_objective = 0.0, + .l2_norm_constraint_bounds = 0.0, + .l_inf_norm_primal_linear_objective = 0.0, + .l_inf_norm_constraint_bounds = 0.0}; +} + class SimpleTerminationTest : public testing::Test { protected: void SetUp() override { @@ -99,6 +105,7 @@ class TerminationTest : public testing::TestWithParam { TerminationCriteria test_criteria_; }; + class DetailedRelativeTerminationTest : public testing::TestWithParam { protected: @@ -143,15 +150,17 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaOverload) { const auto criteria = ParseTextOrDie( R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb"); - EXPECT_THAT(EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie(R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT( + EffectiveOptimalityCriteria(criteria), + Eq(ParseTextOrDie( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb"))); } TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) { @@ -160,15 +169,17 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) { eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4 })pb"); - EXPECT_THAT(EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie(R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT( + EffectiveOptimalityCriteria(criteria), + Eq(ParseTextOrDie( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb"))); } TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) { @@ -182,21 +193,23 @@ TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) { eps_optimal_objective_gap_relative: 6.0e-4 })pb"); EXPECT_THAT(EffectiveOptimalityCriteria(criteria), - Eq(criteria.detailed_optimality_criteria())); + Eq(criteria.detailed_optimality_criteria())); } TEST(EffectiveOptimalityCriteriaTest, DeprecatedInput) { const auto criteria = ParseTextOrDie( R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb"); - EXPECT_THAT(EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie(R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT( + EffectiveOptimalityCriteria(criteria), + Eq(ParseTextOrDie( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb"))); } TEST_P(DetailedRelativeTerminationTest, TerminationWithNearOptimal) { @@ -210,6 +223,12 @@ TEST_P(DetailedRelativeTerminationTest, TerminationWithNearOptimal) { l2_dual_residual: 6e-4 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_TRUE(ObjectiveGapMet(test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0))); + EXPECT_TRUE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_THAT( CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), Optional( @@ -227,6 +246,12 @@ TEST_P(DetailedRelativeTerminationTest, NoTerminationWithExcessiveGap) { l2_dual_residual: 6e-4 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(ObjectiveGapMet(test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0))); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -243,6 +268,10 @@ TEST_P(DetailedRelativeTerminationTest, l2_dual_residual: 6e-4 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -259,6 +288,10 @@ TEST_P(DetailedRelativeTerminationTest, l2_dual_residual: 7e-4 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -274,6 +307,12 @@ TEST_P(DetailedAbsoluteTerminationTest, TerminationWithNearOptimal) { l2_dual_residual: 9e-5 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_TRUE(ObjectiveGapMet(test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0))); + EXPECT_TRUE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_THAT( CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), Optional( @@ -291,6 +330,12 @@ TEST_P(DetailedAbsoluteTerminationTest, NoTerminationWithExcessiveGap) { l2_dual_residual: 9e-5 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(ObjectiveGapMet(test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0))); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -307,6 +352,10 @@ TEST_P(DetailedAbsoluteTerminationTest, l2_dual_residual: 9e-5 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -323,6 +372,10 @@ TEST_P(DetailedAbsoluteTerminationTest, l2_dual_residual: 11e-5 candidate_type: POINT_TYPE_CURRENT_ITERATE })pb"); + EXPECT_FALSE(OptimalityCriteriaMet( + test_criteria_.detailed_optimality_criteria(), + stats.convergence_information(0), test_criteria_.optimality_norm(), + TestLpBoundNorms())); EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()), absl::nullopt); } @@ -540,6 +593,70 @@ TEST_P(TerminationTest, TerminationWithNearOptimal) { POINT_TYPE_CURRENT_ITERATE))); } +TEST_P(TerminationTest, TerminationWithNonOptimalInfiniteAbsoluteTolerances) { + const auto stats = ParseTextOrDie(R"pb( + convergence_information { + primal_objective: 1.0 + dual_objective: 1.0 + l_inf_primal_residual: 1.0 + l_inf_dual_residual: 1.0 + l2_primal_residual: 1.0 + l2_dual_residual: 1.0 + candidate_type: POINT_TYPE_CURRENT_ITERATE + })pb"); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_absolute( + std::numeric_limits::infinity()); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_relative( + 0); + std::optional maybe_result = + CheckTerminationCriteria(test_criteria_, stats, ZeroLpBoundNorms()); + EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL, + POINT_TYPE_CURRENT_ITERATE))); +} + +TEST_P(TerminationTest, TerminationWithNonOptimalInfiniteRelativeTolerances) { + const auto stats = ParseTextOrDie(R"pb( + convergence_information { + primal_objective: 0.0 + dual_objective: 0.0 + l_inf_primal_residual: 1.0 + l_inf_dual_residual: 1.0 + l2_primal_residual: 1.0 + l2_dual_residual: 1.0 + candidate_type: POINT_TYPE_CURRENT_ITERATE + })pb"); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_absolute( + 0); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_relative( + std::numeric_limits::infinity()); + std::optional maybe_result = + CheckTerminationCriteria(test_criteria_, stats, ZeroLpBoundNorms()); + EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL, + POINT_TYPE_CURRENT_ITERATE))); +} + +TEST_P(TerminationTest, + TerminationWithNonOptimalInfiniteAbsoluteAndRelativeTolerances) { + const auto stats = ParseTextOrDie(R"pb( + convergence_information { + primal_objective: 1.0 + dual_objective: 1.0 + l_inf_primal_residual: 1.0 + l_inf_dual_residual: 1.0 + l2_primal_residual: 1.0 + l2_dual_residual: 1.0 + candidate_type: POINT_TYPE_CURRENT_ITERATE + })pb"); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_absolute( + std::numeric_limits::infinity()); + test_criteria_.mutable_simple_optimality_criteria()->set_eps_optimal_relative( + std::numeric_limits::infinity()); + std::optional maybe_result = + CheckTerminationCriteria(test_criteria_, stats, ZeroLpBoundNorms()); + EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL, + POINT_TYPE_CURRENT_ITERATE))); +} + TEST_P(TerminationTest, OptimalEvenWithNumericalError) { const auto stats = ParseTextOrDie(R"pb( convergence_information { @@ -766,8 +883,8 @@ TEST(ComputeRelativeResiduals, termination_criteria.mutable_simple_optimality_criteria() ->set_eps_optimal_relative(1.0e-6); const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( - EffectiveOptimalityCriteria(termination_criteria), TestLpBoundNorms(), - stats); + EffectiveOptimalityCriteria(termination_criteria), stats, + TestLpBoundNorms()); EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / 12.0); EXPECT_EQ(relative_info.relative_l2_primal_residual, 1.0 / std::sqrt(210.0)); @@ -797,7 +914,7 @@ TEST(ComputeRelativeResiduals, opt_criteria.set_eps_optimal_absolute(0.0); opt_criteria.set_eps_optimal_relative(0.0); const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( - EffectiveOptimalityCriteria(opt_criteria), TestLpBoundNorms(), stats); + EffectiveOptimalityCriteria(opt_criteria), stats, TestLpBoundNorms()); EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 0.0); EXPECT_EQ(relative_info.relative_l2_primal_residual, 0.0); @@ -822,7 +939,7 @@ TEST(ComputeRelativeResiduals, opt_criteria.set_eps_optimal_absolute(1.0e-6); opt_criteria.set_eps_optimal_relative(1.0e-6); const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( - EffectiveOptimalityCriteria(opt_criteria), TestLpBoundNorms(), stats); + EffectiveOptimalityCriteria(opt_criteria), stats, TestLpBoundNorms()); EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / (1.0 + 12.0)); EXPECT_EQ(relative_info.relative_l2_primal_residual, @@ -856,7 +973,7 @@ TEST(ComputeRelativeResiduals, opt_criteria.set_eps_optimal_objective_gap_absolute(3.0e-8); opt_criteria.set_eps_optimal_objective_gap_relative(3.0e-7); const RelativeConvergenceInformation relative_info = - ComputeRelativeResiduals(opt_criteria, TestLpBoundNorms(), stats); + ComputeRelativeResiduals(opt_criteria, stats, TestLpBoundNorms()); EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / (0.01 + 12.0)); EXPECT_EQ(relative_info.relative_l2_primal_residual, @@ -871,5 +988,88 @@ TEST(ComputeRelativeResiduals, EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / (0.1 + 15.0)); } +TEST(ComputeRelativeResiduals, + ComputesCorrectRelativeResidualsForInfiniteAbsoluteTolerances) { + ConvergenceInformation stats; + stats.set_primal_objective(10.0); + stats.set_dual_objective(5.0); + stats.set_l_inf_primal_residual(1.0); + stats.set_l2_primal_residual(1.0); + stats.set_l_inf_dual_residual(1.0); + stats.set_l2_dual_residual(1.0); + TerminationCriteria::SimpleOptimalityCriteria opt_criteria; + opt_criteria.set_eps_optimal_absolute( + std::numeric_limits::infinity()); + opt_criteria.set_eps_optimal_relative(1.0e-6); + const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( + EffectiveOptimalityCriteria(opt_criteria), stats, TestLpBoundNorms()); + + // If absolute tolerance is infinite the relative residuals are zero. + EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 0.0); + EXPECT_EQ(relative_info.relative_l2_primal_residual, 0.0); + EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 0.0); + EXPECT_EQ(relative_info.relative_l2_dual_residual, 0.0); + EXPECT_EQ(relative_info.relative_optimality_gap, 0.0); +} + +TEST(ComputeRelativeResiduals, + ComputesCorrectRelativeResidualsForInfiniteRelativeTolerances) { + ConvergenceInformation stats; + stats.set_primal_objective(10.0); + stats.set_dual_objective(5.0); + stats.set_l_inf_primal_residual(1.0); + stats.set_l2_primal_residual(1.0); + stats.set_l_inf_dual_residual(1.0); + stats.set_l2_dual_residual(1.0); + TerminationCriteria::SimpleOptimalityCriteria opt_criteria; + opt_criteria.set_eps_optimal_absolute(1.0e-6); + opt_criteria.set_eps_optimal_relative( + std::numeric_limits::infinity()); + const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( + EffectiveOptimalityCriteria(opt_criteria), stats, TestLpBoundNorms()); + + EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / 12.0); + EXPECT_EQ(relative_info.relative_l2_primal_residual, 1.0 / std::sqrt(210.0)); + + EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / 5.5); + EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / sqrt(36.25)); + + // The relative optimality gap should just be the objective difference divided + // by the sum of absolute values. + EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / 15.0); +} + +TEST(ComputeRelativeResiduals, + ComputesCorrectRelativeResidualsForInfiniteAbsoluteAndRelativeTolerances) { + ConvergenceInformation stats; + // If the absolute error tolerance and relative error tolerance are both + // infinity (and nonzero), the relative residuals are the absolute residuals + // divided by 1.0 plus the corresponding norms. + stats.set_primal_objective(10.0); + stats.set_dual_objective(5.0); + stats.set_l_inf_primal_residual(1.0); + stats.set_l2_primal_residual(1.0); + stats.set_l_inf_dual_residual(1.0); + stats.set_l2_dual_residual(1.0); + TerminationCriteria::SimpleOptimalityCriteria opt_criteria; + opt_criteria.set_eps_optimal_absolute( + std::numeric_limits::infinity()); + opt_criteria.set_eps_optimal_relative( + std::numeric_limits::infinity()); + const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals( + EffectiveOptimalityCriteria(opt_criteria), stats, TestLpBoundNorms()); + + EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / (1.0 + 12.0)); + EXPECT_EQ(relative_info.relative_l2_primal_residual, + 1.0 / (1.0 + std::sqrt(210.0))); + + EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / (1.0 + 5.5)); + EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / (1.0 + sqrt(36.25))); + + // The relative optimality gap should just be the objective difference divided + // by 1.0 + the sum of absolute values. + EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / (1.0 + 15.0)); +} + } // namespace } // namespace operations_research::pdlp