Files
ortools-clone/ortools/pdlp/primal_dual_hybrid_gradient_test.cc
Corentin Le Molgat edb5359fd9 export from google3
2025-05-13 18:22:18 +02:00

2330 lines
101 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/pdlp/primal_dual_hybrid_gradient.h"
#include <atomic>
#include <cmath>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/container/flat_hash_map.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/glop/parameters.pb.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/lp_data/lp_data.h"
#include "ortools/lp_data/lp_types.h"
#include "ortools/pdlp/iteration_stats.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/quadratic_program_io.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
#include "ortools/pdlp/termination.h"
#include "ortools/pdlp/test_util.h"
namespace operations_research::pdlp {
namespace {
using ::Eigen::VectorXd;
using ::operations_research::glop::ConstraintStatus;
using ::operations_research::glop::VariableStatus;
using ::testing::AnyOf;
using ::testing::DoubleNear;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pair;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
const double kInfinity = std::numeric_limits<double>::infinity();
PrimalDualHybridGradientParams CreateSolverParams(
const int iteration_limit, const double eps_optimal_absolute,
const bool enable_scaling, const int num_threads,
const bool use_iteration_limit, const bool use_malitsky_pock_linesearch,
const bool use_diagonal_qp_trust_region_solver) {
PrimalDualHybridGradientParams params;
if (!enable_scaling) {
params.set_l2_norm_rescaling(false);
params.set_l_inf_ruiz_iterations(0);
}
if (use_malitsky_pock_linesearch) {
params.set_linesearch_rule(
PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE);
}
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_relative(0.0);
if (use_iteration_limit) {
// This effectively forces convergence on the iteration limit only.
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(0.0);
} else {
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(eps_optimal_absolute);
}
if (use_diagonal_qp_trust_region_solver) {
params.set_use_diagonal_qp_trust_region_solver(true);
params.set_diagonal_qp_trust_region_solver_tolerance(1.0e-8);
}
params.set_num_threads(num_threads);
// Protect against infinite loops if the termination criteria fail.
params.mutable_termination_criteria()->set_kkt_matrix_pass_limit(1'000'000.0);
return params;
}
// Verifies expected termination reason and iteration count for an instance
// where an optimal solution exists.
// `params` must have been generated by `CreateSolverParams()` with the same
// `use_iteration_limit`.
// Intended usage:
// const bool use_iteration_limit = ...;
// PrimalDualHybridGradientParams params =
// CreateSolverParams(..., use_iteration_limit, ...);
// SolverResult output = PrimalDualHybridGradient(..., params);
// VerifyTerminationReasonAndIterationCount(params, output,
// use_iteration_limit);
void VerifyTerminationReasonAndIterationCount(
const PrimalDualHybridGradientParams& params, const SolverResult& output,
const bool use_iteration_limit) {
if (use_iteration_limit) {
// When a PDHG step has zero length PDHG can no longer make progress and
// hence terminates immediately. In theory a zero step length implies
// optimality but in practice PDHG terminates with a reason of OPTIMAL if
// the optimality checks pass and NUMERICAL_ERROR otherwise.
// When `use_iteration_limit==true`, `CreateSolverParams()` sets all the
// epsilons to 0, which makes the optimality checks harder to pass but not
// impossible. Both OPTIMAL and NUMERICAL_ERROR are therefore ok termination
// reasons.
EXPECT_THAT(
output.solve_log.termination_reason(),
AnyOf(TERMINATION_REASON_ITERATION_LIMIT,
TERMINATION_REASON_NUMERICAL_ERROR, TERMINATION_REASON_OPTIMAL));
if (output.solve_log.termination_reason() ==
TERMINATION_REASON_ITERATION_LIMIT) {
EXPECT_EQ(output.solve_log.iteration_count(),
params.termination_criteria().iteration_limit());
} else {
EXPECT_LE(output.solve_log.iteration_count(),
params.termination_criteria().iteration_limit());
}
} else {
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_OPTIMAL);
EXPECT_LE(output.solve_log.iteration_count(),
params.termination_criteria().iteration_limit());
}
}
// Verifies the primal and dual objective values.
void VerifyObjectiveValues(const SolverResult& result,
const double objective_value,
const double tolerance) {
const auto& convergence_info = GetConvergenceInformation(
result.solve_log.solution_stats(), result.solve_log.solution_type());
ASSERT_TRUE(convergence_info.has_value());
EXPECT_THAT(convergence_info->primal_objective(),
DoubleNear(objective_value, tolerance));
EXPECT_THAT(convergence_info->dual_objective(),
DoubleNear(objective_value, tolerance));
}
class PrimalDualHybridGradientLPTest
: public testing::TestWithParam<
std::tuple</*enable_scaling=*/bool, /*num_threads=*/int,
/*use_iteration_limit=*/bool,
/*use_malitsky_pock_linesearch=*/bool>> {
protected:
PrimalDualHybridGradientParams CreateSolverParamsForFixture(
const int iteration_limit, const double eps_optimal_absolute) {
const auto [enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch] = GetParam();
return CreateSolverParams(iteration_limit, eps_optimal_absolute,
enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch,
/*use_diagonal_qp_trust_region_solver=*/false);
}
void VerifyTerminationReasonAndIterationCountForFixture(
const PrimalDualHybridGradientParams& params,
const SolverResult& output) {
const auto [enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch] = GetParam();
VerifyTerminationReasonAndIterationCount(params, output,
use_iteration_limit);
}
};
class PrimalDualHybridGradientDiagonalQPTest
: public testing::TestWithParam<
std::tuple</*enable_scaling=*/bool, /*num_threads=*/int,
/*use_iteration_limit=*/bool,
/*use_malitsky_pock_linesearch=*/bool,
/*use_diagonal_qp_trust_region_solver=*/bool>> {
protected:
PrimalDualHybridGradientParams CreateSolverParamsForFixture(
const int iteration_limit, const double eps_optimal_absolute) {
const auto [enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch,
use_diagonal_qp_trust_region_solver] = GetParam();
return CreateSolverParams(iteration_limit, eps_optimal_absolute,
enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch,
use_diagonal_qp_trust_region_solver);
}
void VerifyTerminationReasonAndIterationCountForFixture(
const PrimalDualHybridGradientParams& params,
const SolverResult& output) {
const auto [enable_scaling, num_threads, use_iteration_limit,
use_malitsky_pock_linesearch,
use_diagonal_qp_trust_region_solver] = GetParam();
VerifyTerminationReasonAndIterationCount(params, output,
use_iteration_limit);
}
};
class PrimalDualHybridGradientVerbosityTest
: public testing::TestWithParam</*verbosity_level=*/int> {};
class PresolveDualScalingTest
: public testing::TestWithParam<
std::tuple</*Dualize=*/bool,
/*NegateAndScaleObjective=*/bool>> {};
INSTANTIATE_TEST_SUITE_P(
QP, PrimalDualHybridGradientDiagonalQPTest,
testing::Combine(testing::Bool(), testing::Values(1, 4), testing::Bool(),
testing::Bool(), testing::Bool()),
[](const testing::TestParamInfo<
PrimalDualHybridGradientDiagonalQPTest::ParamType>& info) {
return absl::StrCat(
std::get<0>(info.param) ? "Scaling" : "NoScaling", "_",
std::get<1>(info.param), "Threads_",
std::get<2>(info.param) ? "IterationLimit" : "NoIterationLimit", "_",
std::get<3>(info.param) ? "MalitskyPockLinesearch"
: "AdaptiveLinesearch",
"_", std::get<4>(info.param) ? "TRSolverDiag" : "TRSolverNoDiag");
});
INSTANTIATE_TEST_SUITE_P(
LP, PrimalDualHybridGradientLPTest,
testing::Combine(testing::Bool(), testing::Values(1, 4), testing::Bool(),
testing::Bool()),
[](const testing::TestParamInfo<PrimalDualHybridGradientLPTest::ParamType>&
info) {
return absl::StrCat(
std::get<0>(info.param) ? "Scaling" : "NoScaling", "_",
std::get<1>(info.param), "Threads_",
std::get<2>(info.param) ? "IterationLimit" : "NoIterationLimit", "_",
std::get<3>(info.param) ? "MalitskyPockLinesearch"
: "AdaptiveLinesearch");
});
INSTANTIATE_TEST_SUITE_P(Verbosity, PrimalDualHybridGradientVerbosityTest,
testing::Values(0, 1, 2, 3, 4));
INSTANTIATE_TEST_SUITE_P(
PresolveDualScaling, PresolveDualScalingTest,
testing::Combine(testing::Bool(), testing::Bool()),
[](const testing::TestParamInfo<PresolveDualScalingTest::ParamType>& info) {
return absl::StrCat(std::get<1>(info.param) ? "Dualize" : "NoDualize",
std::get<0>(info.param) ? "NegateAndScaleObjective"
: "NoObjectiveScaling");
});
TEST_P(PrimalDualHybridGradientLPTest, UnboundedVariables) {
const int iteration_upperbound = 980;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-7);
params.set_major_iteration_frequency(100);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, -34.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 4);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 4);
}
TEST_P(PrimalDualHybridGradientLPTest, Tiny) {
const int iteration_upperbound = 300;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-5);
params.set_major_iteration_frequency(60);
SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, -1.0, 1.0e-4);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
EXPECT_THAT(output.reduced_costs,
EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
}
TEST_P(PrimalDualHybridGradientLPTest, CorrelationClusteringOne) {
const int iteration_upperbound = 9;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-10);
params.set_major_iteration_frequency(2);
SolverResult output =
PrimalDualHybridGradient(CorrelationClusteringLp(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 1.0, 1.0e-14);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1, 1, 0, 1, 0, 0}, 1.0e-14));
ASSERT_EQ(output.dual_solution.size(), 3);
// There are multiple optimal dual solutions.
EXPECT_GE(output.dual_solution[0], 0);
EXPECT_GE(output.dual_solution[1], 0);
EXPECT_GE(output.dual_solution[2], 0);
EXPECT_GE(output.dual_solution[0] + output.dual_solution[1], 1 - 1.0e-14);
const auto& convergence_information = GetConvergenceInformation(
output.solve_log.solution_stats(), output.solve_log.solution_type());
ASSERT_TRUE(convergence_information.has_value());
EXPECT_THAT(convergence_information->corrected_dual_objective(),
DoubleNear(1, 1.0e-14));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 6);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 6);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
}
TEST_P(PrimalDualHybridGradientLPTest, CorrelationClusteringStar) {
const int iteration_upperbound = 45;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(5);
SolverResult output =
PrimalDualHybridGradient(CorrelationClusteringStarLp(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 1.5, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, 1.0e-6));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 0.5, 0.5}, 1.0e-6));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 6);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 6);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
}
// A double-sided constraint l <= a^T x <= u where neither constraint is tight
// at optimum could cause the trust region solver to malfunction if we picked
// the wrong dual subgradient. This test verifies that we solve an instance with
// such a constraint quickly to high accuracy.
TEST_P(PrimalDualHybridGradientLPTest, InactiveTwoSidedConstraint) {
const int iteration_upperbound = 500;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-8);
params.set_major_iteration_frequency(60);
QuadraticProgram qp = TestLp();
// This makes this constraint double-sided and inactive at the optimal
// solution.
qp.constraint_lower_bounds[1] = -10;
SolverResult output = PrimalDualHybridGradient(qp, params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, -34.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-7));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-2.0, 0.0, 2.375, 2.0 / 3}, 1.0e-7));
}
TEST_P(PrimalDualHybridGradientLPTest, InfeasiblePrimal) {
// This value for `iteration_upperbound` is particularly necessary for
// Malistsky and Pock. The adaptive rule detects infeasibility in less than
// 500 iterations.
const int iteration_upperbound = 2000;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(5);
params.mutable_termination_criteria()->set_eps_primal_infeasible(1.0e-6);
SolverResult output =
PrimalDualHybridGradient(SmallPrimalInfeasibleLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_PRIMAL_INFEASIBLE);
const auto& dual = output.dual_solution;
// The following two conditions check if the certificate is correct. For this
// problem the set of infeasibility certificates is equal to all the rays of
// the form -alpha * (1, 1) with alpha positive.
EXPECT_THAT(dual[0] / dual[1], DoubleNear(1, 1.0e-6));
EXPECT_LT(dual[1], 0.0);
// The reduced costs should be approximately zero. However, a small relative
// difference between `dual[0]` and `dual[1]` could translate to a large
// absolute difference, and hence large reduced costs. The following test uses
// the exact formula to make sure we're not adding the objective vector to the
// reduced costs.
EXPECT_THAT(
output.reduced_costs,
EigenArrayNear<double>({dual[1] - dual[0], dual[0] - dual[1]}, 1.0e-6));
EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 2);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 2);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 2);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 2);
}
TEST_P(PrimalDualHybridGradientLPTest, InfeasibleDual) {
const int iteration_upperbound = 500;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(5);
SolverResult output =
PrimalDualHybridGradient(SmallDualInfeasibleLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_DUAL_INFEASIBLE);
// The following two conditions check if the certificate is correct. For this
// problem the set of infeasibility certificates is equal to all the rays of
// the form alpha * (1, 1) with alpha positive.
EXPECT_THAT(output.primal_solution[0] / output.primal_solution[1],
DoubleNear(1, 1.0e-6));
EXPECT_GT(output.primal_solution[1], 0.0);
EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
}
TEST_P(PrimalDualHybridGradientLPTest, InfeasiblePrimalWithReducedCosts) {
// A trivial LP that has reduced costs within the dual ray.
// min x
// Constraint: 2 <= x
// Variable: 0 <= x <= 1
QuadraticProgram lp(1, 1);
lp.objective_vector = Eigen::VectorXd{{1}};
lp.constraint_lower_bounds = Eigen::VectorXd{{2}};
lp.constraint_upper_bounds =
Eigen::VectorXd{{std::numeric_limits<double>::infinity()}};
lp.variable_lower_bounds = Eigen::VectorXd{{0}};
lp.variable_upper_bounds = Eigen::VectorXd{{1}};
lp.constraint_matrix.coeffRef(0, 0) = 1.0;
lp.constraint_matrix.makeCompressed();
const int iteration_upperbound = 100;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(5);
params.mutable_termination_criteria()->set_eps_primal_infeasible(1.0e-6);
SolverResult output = PrimalDualHybridGradient(lp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_PRIMAL_INFEASIBLE);
const auto& dual = output.dual_solution;
EXPECT_GT(dual[0], 0.0);
EXPECT_EQ(output.reduced_costs[0], -dual[0]);
EXPECT_LT(output.solve_log.iteration_count(), iteration_upperbound);
}
TEST_P(PrimalDualHybridGradientLPTest, InfeasiblePrimalDual) {
const int iteration_upperbound = 600;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
// Adaptive restarts are disabled because they unexpectedly perform worse on
// this instance.
params.set_restart_strategy(PrimalDualHybridGradientParams::NO_RESTARTS);
params.set_major_iteration_frequency(5);
SolverResult output =
PrimalDualHybridGradient(SmallPrimalDualInfeasibleLp(), params);
EXPECT_THAT(output.solve_log.termination_reason(),
AnyOf(Eq(TERMINATION_REASON_DUAL_INFEASIBLE),
Eq(TERMINATION_REASON_PRIMAL_INFEASIBLE)));
EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
}
TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp1) {
const int iteration_upperbound = 96;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(12);
SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 6.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1.0, 0.0}, 1.0e-6));
EXPECT_THAT(output.dual_solution,
EigenArrayNear(Eigen::ArrayXd::Constant(1, -1.0), 1.0e-6));
EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({4.0, 0.0}, 1.0e-6));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 2);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 2);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 1);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 1);
}
TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp2) {
const int iteration_upperbound = 240;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(12);
SolverResult output = PrimalDualHybridGradient(TestDiagonalQp2(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, -5.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({3.0, 1.0}, 1.0e-6));
EXPECT_THAT(output.dual_solution, ElementsAre(DoubleNear(0.0, 1.0e-6)));
EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({0.0, 0.0}, 1.0e-6));
}
TEST_P(PrimalDualHybridGradientDiagonalQPTest, DiagonalQp3) {
const int iteration_upperbound = 300;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(15);
SolverResult output = PrimalDualHybridGradient(TestDiagonalQp3(), params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 2.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({2.0, 0.0, 1.0}, 1.0e-6));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-1.0, 1.0}, 1.0e-6));
EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({0, 0, 0}, 1.0e-6));
}
// This is like `DiagonalQp1` except it starts with a near-optimal solution and
// uses a shorter iteration limit.
TEST_P(PrimalDualHybridGradientDiagonalQPTest, QpWarmStart) {
const int iteration_upperbound = 35;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
params.set_major_iteration_frequency(5);
// Disable primal weight updating. In a warm-start situation, the primal
// weight should be carried over. In this test, the initial primal weight of 1
// is reasonable because the distance from the starting point to the primal
// and dual optimal solutions are about the same.
params.set_primal_weight_update_smoothing(0.0);
const PrimalAndDualSolution initial_solution = {
.primal_solution = Eigen::VectorXd{{0.999, 0.001}},
.dual_solution = Eigen::VectorXd{{-0.999}},
};
SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params,
std::move(initial_solution));
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 6.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1.0, 0.0}, 1.0e-6));
EXPECT_THAT(output.dual_solution,
EigenArrayNear(Eigen::ArrayXd::Constant(1, -1.0), 1.0e-6));
EXPECT_THAT(output.reduced_costs, EigenArrayNear<double>({4.0, 0.0}, 1.0e-6));
}
// Tests an LP with no constraints.
TEST_P(PrimalDualHybridGradientLPTest, LpWithoutConstraints) {
const int iteration_upperbound = 2;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
QuadraticProgram qp(3, 0);
qp.variable_lower_bounds = Eigen::VectorXd{{-1, -kInfinity, -2}};
qp.variable_upper_bounds = Eigen::VectorXd{{kInfinity, 4, 10}};
qp.objective_vector = Eigen::VectorXd{{1, -1, 2}};
SolverResult output = PrimalDualHybridGradient(qp, params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, -9.0, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 4, -2}, 1.0e-6));
EXPECT_THAT(output.dual_solution, SizeIs(0));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 3);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 3);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 0);
EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 0);
}
// Tests an LP with no variables.
TEST_P(PrimalDualHybridGradientLPTest, LpWithoutVariables) {
const int iteration_upperbound = 2;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
QuadraticProgram qp(0, 3);
qp.constraint_lower_bounds = Eigen::VectorXd{{-1, -kInfinity, -2}};
qp.constraint_upper_bounds = Eigen::VectorXd{{kInfinity, 4, 10}};
SolverResult output = PrimalDualHybridGradient(qp, params);
VerifyTerminationReasonAndIterationCountForFixture(params, output);
VerifyObjectiveValues(output, 0.0, 1.0e-6);
EXPECT_THAT(output.primal_solution, SizeIs(0));
EXPECT_THAT(output.dual_solution, EigenArrayNear<double>({0, 0, 0}, 1.0e-6));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 0);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 0);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
}
TEST_P(PrimalDualHybridGradientLPTest, LpWithOnlyFixedVariable) {
const int iteration_upperbound = 2;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/0.0);
QuadraticProgram qp(1, 0);
qp.variable_lower_bounds = Eigen::VectorXd{{1}};
qp.variable_upper_bounds = Eigen::VectorXd{{1}};
qp.objective_vector = Eigen::VectorXd{{1}};
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
EXPECT_THAT(output.primal_solution,
EigenArrayNear(Eigen::ArrayXd::Constant(1, 1.0), 1.0e-6));
EXPECT_THAT(output.dual_solution, testing::SizeIs(0));
EXPECT_LE(output.solve_log.iteration_count(), iteration_upperbound);
}
TEST_P(PrimalDualHybridGradientLPTest, InfeasibleLpWithoutVariables) {
const int iteration_upperbound = 2;
PrimalDualHybridGradientParams params =
CreateSolverParamsForFixture(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-6);
QuadraticProgram qp(0, 1);
qp.constraint_lower_bounds = Eigen::VectorXd{{-1}};
qp.constraint_upper_bounds = Eigen::VectorXd{{-1}};
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_PRIMAL_INFEASIBLE);
EXPECT_THAT(output.primal_solution, SizeIs(0));
EXPECT_LT(output.dual_solution[0], 0.0);
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 0);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 0);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 1);
EXPECT_EQ(output.solve_log.preprocessed_problem_stats().num_constraints(), 1);
}
PrimalDualHybridGradientParams ParamsWithNoLimits() {
PrimalDualHybridGradientParams params;
// This disables the termination limits. A termination criteria must be set
// for the solver to terminate.
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_relative(0.0);
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(0.0);
params.set_record_iteration_stats(true);
return params;
}
TEST(PrimalDualHybridGradientTest, ClearsRunningAverage) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(1);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_restart_strategy(PrimalDualHybridGradientParams::NO_RESTARTS);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
// The first entry in `iteration_stats` corresponds to the starting point.
ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
const auto& stats = output.solve_log.iteration_stats(i);
const int iterations_completed = stats.iteration_number();
EXPECT_EQ(iterations_completed, i);
if (iterations_completed == 0) {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
} else if (iterations_completed % major_iteration_frequency == 0) {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_WEIGHTED_AVERAGE_RESET)
<< "iteration = " << i;
} else {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART)
<< "iteration = " << i;
}
}
}
TEST(PrimalDualHybridGradientTest, RestartsEveryMajorIteration) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(1);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_restart_strategy(
PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
// The first entry in `iteration_stats` corresponds to the starting point.
ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
const auto& stats = output.solve_log.iteration_stats(i);
const int iterations_completed = stats.iteration_number();
EXPECT_EQ(iterations_completed, i);
if (iterations_completed == 0) {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
} else if (iterations_completed % major_iteration_frequency == 0) {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_RESTART_TO_AVERAGE)
<< "iteration = " << i;
} else {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART)
<< "iteration = " << i;
}
}
}
TEST(PrimalDualHybridGradientTest, SolveLogIncludesNameForNamedQP) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_iteration_limit(1);
QuadraticProgram test_lp = TestLp();
test_lp.problem_name = "Test LP";
SolverResult output = PrimalDualHybridGradient(test_lp, params);
EXPECT_EQ(output.solve_log.instance_name(), "Test LP");
}
TEST(PrimalDualHybridGradientTest, SolveLogOmitsNameForUnnamedQP) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_iteration_limit(1);
QuadraticProgram unnamed_test_lp = TestLp();
SolverResult output = PrimalDualHybridGradient(unnamed_test_lp, params);
EXPECT_FALSE(output.solve_log.has_instance_name());
}
TEST(PrimalDualHybridGradientTest, SolveLogIncludesParameters) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_iteration_limit(1);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.params().termination_criteria().iteration_limit(),
1);
}
TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsWorkOnTestLp) {
PrimalDualHybridGradientParams params;
params.set_major_iteration_frequency(16);
params.mutable_termination_criteria()->set_iteration_limit(128);
params.set_restart_strategy(
PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
// Low restart threshold.
params.set_necessary_reduction_for_restart(0.99);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
const auto convergence_info = GetConvergenceInformation(
output.solve_log.solution_stats(), output.solve_log.solution_type());
ASSERT_TRUE(convergence_info.has_value());
EXPECT_THAT(convergence_info->primal_objective(), DoubleNear(-34.0, 1.0e-4));
EXPECT_THAT(convergence_info->dual_objective(), DoubleNear(-34.0, 1.0e-4));
}
TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsWorkOnTestQp) {
PrimalDualHybridGradientParams params;
params.set_major_iteration_frequency(16);
params.mutable_termination_criteria()->set_iteration_limit(128);
params.set_restart_strategy(
PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
// Low restart threshold.
params.set_necessary_reduction_for_restart(0.99);
SolverResult output = PrimalDualHybridGradient(TestDiagonalQp1(), params);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest, AdaptiveDistanceBasedRestartsToAverage) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 13;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(1);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_restart_strategy(
PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED);
params.set_necessary_reduction_for_restart(0.75);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
// The first entry in `iteration_stats` corresponds to the starting point.
ASSERT_EQ(output.solve_log.iteration_stats_size(), iteration_limit + 1);
for (int i = 0; i < output.solve_log.iteration_stats_size(); ++i) {
const auto& stats = output.solve_log.iteration_stats(i);
const int iterations_completed = stats.iteration_number();
EXPECT_EQ(iterations_completed, i);
if (iterations_completed == 0) {
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
} else if (iterations_completed == major_iteration_frequency) {
// An explicit restart should be triggered at the end of the first major
// iteration.
EXPECT_THAT(stats.restart_used(),
AnyOf(RESTART_CHOICE_RESTART_TO_AVERAGE,
RESTART_CHOICE_WEIGHTED_AVERAGE_RESET))
<< "iteration = " << i;
} else if (iterations_completed % major_iteration_frequency != 0) {
// No restarts should happen outside major iterations.
EXPECT_EQ(stats.restart_used(), RESTART_CHOICE_NO_RESTART);
}
}
}
TEST(PrimalDualHybridGradientTest, PrimalWeightFrozen) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
const double initial_primal_weight = 1.5;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_restart_strategy(
PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION);
params.set_initial_primal_weight(initial_primal_weight);
params.set_primal_weight_update_smoothing(0.0);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
for (const auto& stats : output.solve_log.iteration_stats()) {
EXPECT_EQ(stats.primal_weight(), initial_primal_weight)
<< "iteration = " << stats.iteration_number();
}
}
TEST(PrimalDualHybridGradientTest, ConstantStepSize) {
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(1);
params.set_linesearch_rule(
PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_FALSE(output.solve_log.iteration_stats().empty());
const double initial_step_size =
output.solve_log.iteration_stats(0).step_size();
for (const auto& stats : output.solve_log.iteration_stats()) {
EXPECT_EQ(stats.step_size(), initial_step_size)
<< "iteration = " << stats.iteration_number();
}
}
TEST(PrimalDualHybridGradientTest, StepSizeScaling) {
const int iteration_limit = 1;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(1);
params.set_linesearch_rule(
PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
SolverResult unscaled_output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_FALSE(unscaled_output.solve_log.iteration_stats().empty());
const double initial_step_size =
unscaled_output.solve_log.iteration_stats(0).step_size();
const double kStepSizeScaling = 0.5;
params.set_initial_step_size_scaling(kStepSizeScaling);
SolverResult scaled_output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_FALSE(scaled_output.solve_log.iteration_stats().empty());
EXPECT_EQ(scaled_output.solve_log.iteration_stats(0).step_size(),
initial_step_size * kStepSizeScaling);
}
// This verifies that `kkt_matrix_pass_limit` is checked every iteration.
TEST(PrimalDualHybridGradientTest, KktMatrixPassTermination) {
const int kkt_matrix_pass_limit = 13;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_kkt_matrix_pass_limit(
kkt_matrix_pass_limit);
params.set_linesearch_rule(
PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_FALSE(output.solve_log.iteration_stats().empty());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT);
EXPECT_EQ(output.solve_log.solution_stats().cumulative_kkt_matrix_passes(),
kkt_matrix_pass_limit);
}
TEST(PrimalDualHybridGradientTest,
StatsAtEachIterationWithRecordIterationStatsOn) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.set_record_iteration_stats(true);
// This is required for `ConvergenceInformation`, `InfeasibilityInformation`,
// and `PointMetadata` to be generated on each iteration.
params.set_termination_check_frequency(1);
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_major_iteration_frequency(major_iteration_frequency);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
for (const auto& stats : output.solve_log.iteration_stats()) {
EXPECT_NE(GetConvergenceInformation(stats, POINT_TYPE_CURRENT_ITERATE),
std::nullopt);
EXPECT_NE(GetInfeasibilityInformation(stats, POINT_TYPE_CURRENT_ITERATE),
std::nullopt);
EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_CURRENT_ITERATE),
std::nullopt);
if (stats.iteration_number() > 0) {
EXPECT_NE(GetConvergenceInformation(stats, POINT_TYPE_AVERAGE_ITERATE),
std::nullopt);
EXPECT_NE(GetInfeasibilityInformation(stats, POINT_TYPE_AVERAGE_ITERATE),
std::nullopt);
EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_AVERAGE_ITERATE),
std::nullopt);
EXPECT_NE(
GetInfeasibilityInformation(stats, POINT_TYPE_ITERATE_DIFFERENCE),
std::nullopt);
EXPECT_NE(GetPointMetadata(stats, POINT_TYPE_ITERATE_DIFFERENCE),
std::nullopt);
}
}
}
TEST(PrimalDualHybridGradientTest,
NoIterationStatsWithRecordIterationStatsOff) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.set_record_iteration_stats(false);
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_major_iteration_frequency(major_iteration_frequency);
// Random projection seeds should have no effect when `record_iteration_stats`
// is false.
params.add_random_projection_seeds(1);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.iteration_stats().size(), 0);
}
TEST(PrimalDualHybridGradientTest, NoRandomProjectionsIfNotRequested) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.set_record_iteration_stats(true);
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_termination_check_frequency(1);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
for (const auto& stats : output.solve_log.iteration_stats()) {
EXPECT_THAT(stats.point_metadata(), Not(IsEmpty()));
for (const auto& metadata : stats.point_metadata()) {
EXPECT_THAT(metadata.random_primal_projections(), IsEmpty());
EXPECT_THAT(metadata.random_dual_projections(), IsEmpty());
}
}
}
TEST(PrimalDualHybridGradientTest, HasRandomProjectionsIfRequested) {
// An arbitrarily chosen major iteration frequency.
const int major_iteration_frequency = 17;
const int iteration_limit = 100;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.set_record_iteration_stats(true);
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_major_iteration_frequency(major_iteration_frequency);
params.set_termination_check_frequency(1);
params.add_random_projection_seeds(1);
params.add_random_projection_seeds(2);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
for (const auto& stats : output.solve_log.iteration_stats()) {
for (const auto& metadata : stats.point_metadata()) {
// There isn't much we can say about the random projection values, so just
// check that the right numbers are present.
EXPECT_THAT(metadata.random_primal_projections(), SizeIs(2));
EXPECT_THAT(metadata.random_dual_projections(), SizeIs(2));
}
}
}
TEST(PrimalDualHybridGradientTest, ProjectInitialPointPrimalBounds) {
const int iteration_limit = 5;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
// The default initial solution (zeros) doesn't satisfy the primal variable
// bounds. The solver should project it to a valid primal solution.
SolverResult output =
PrimalDualHybridGradient(SmallInitializationLp(), params);
EXPECT_EQ(output.solve_log.iteration_stats().size(), iteration_limit + 1);
EXPECT_GT(output.primal_solution[0], 0.0);
}
TEST(PrimalDualHybridGradientTest, ProjectInitialPointDualBounds) {
const int iteration_limit = 5;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
// This initial solution doesn't satisfy the dual variable bounds. The solver
// should project it to a valid dual solution.
const PrimalAndDualSolution initial_solution = {
.primal_solution = Eigen::VectorXd{{1.0, 0.0}},
.dual_solution = -VectorXd::Ones(2),
};
SolverResult output_nonzero_init = PrimalDualHybridGradient(
SmallInitializationLp(), params, initial_solution);
EXPECT_EQ(output_nonzero_init.solve_log.iteration_stats().size(),
iteration_limit + 1);
EXPECT_LE(output_nonzero_init.dual_solution[0], 0.0);
}
TEST(PrimalDualHybridGradientTest, DetectsProblemWithInconsistentBounds) {
SolverResult output = PrimalDualHybridGradient(
SmallInvalidProblemLp(), PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNonconvexQp) {
QuadraticProgram qp = TestDiagonalQp1();
qp.objective_matrix->diagonal()[0] = -1.0;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsProblemWithInconsistentSizes) {
QuadraticProgram qp = TinyLp();
qp.objective_vector.resize(0);
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsUncompressedConstraintMatrix) {
QuadraticProgram qp = TinyLp();
qp.constraint_matrix.uncompress();
SolverResult output =
PrimalDualHybridGradient(std::move(qp), PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsInvalidParameters) {
PrimalDualHybridGradientParams params;
params.set_num_threads(0);
SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PARAMETER);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInConstraintMatrix) {
QuadraticProgram qp = TestLp();
qp.constraint_matrix.coeffRef(0, 0) =
std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintMatrix) {
QuadraticProgram qp = TestLp();
qp.constraint_matrix.coeffRef(0, 0) = 1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallColNormConstraintMatrix) {
QuadraticProgram qp = TestLp();
qp.constraint_matrix.coeffRef(0, 1) = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallRowNormConstraintMatrix) {
QuadraticProgram qp = TestLp();
qp.constraint_matrix.coeffRef(2, 0) = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInConstraintBounds) {
QuadraticProgram qp = TestLp();
qp.constraint_upper_bounds[1] = std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintUpperBound) {
QuadraticProgram qp = TestLp();
qp.constraint_upper_bounds[1] = 1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
ExcessivelySmallConstraintUpperBoundOkWithoutPresolve) {
QuadraticProgram qp = TestLp();
qp.constraint_upper_bounds[1] = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallConstraintUpperBoundWithPresolve) {
QuadraticProgram qp = TestLp();
qp.constraint_upper_bounds[1] = 1.0e-60;
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveConstraintLowerBound) {
QuadraticProgram qp = TestLp();
qp.constraint_lower_bounds[2] = -1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
ExcessivelySmallConstraintLowerBoundOkWithoutPresolve) {
QuadraticProgram qp = TestLp();
qp.constraint_lower_bounds[2] = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallConstraintLowerBoundWithPresolve) {
QuadraticProgram qp = TestLp();
qp.constraint_lower_bounds[2] = 1.0e-60;
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInVariableBound) {
QuadraticProgram qp = TestLp();
qp.variable_lower_bounds[3] = std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveVariableBound) {
QuadraticProgram qp = TestLp();
qp.variable_lower_bounds[3] = -1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
ExcessivelySmallVariableBoundOkWithoutPresolve) {
QuadraticProgram qp = TestLp();
qp.variable_lower_bounds[1] = 0.0;
qp.variable_upper_bounds[1] = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallVariableBoundWithPresolve) {
QuadraticProgram qp = TestLp();
qp.variable_lower_bounds[1] = 0.0;
qp.variable_upper_bounds[1] = 1.0e-60;
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanObjectiveOffset) {
QuadraticProgram qp = TestLp();
qp.objective_offset = std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveObjectiveOffset) {
QuadraticProgram qp = TestLp();
qp.objective_offset = -1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInObjectiveVector) {
QuadraticProgram qp = TestLp();
qp.objective_vector[3] = std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveObjectiveVector) {
QuadraticProgram qp = TestLp();
qp.objective_vector[3] = -1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest,
ExcessivelySmallObjectiveVectorOkWithoutPresolve) {
QuadraticProgram qp = TestLp();
qp.objective_vector[3] = 1.0e-60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessivelySmallObjectiveVectorWithPresolve) {
QuadraticProgram qp = TestLp();
qp.objective_vector[3] = 1.0e-60;
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInObjectiveMatrix) {
QuadraticProgram qp = TestDiagonalQp1();
qp.objective_matrix->diagonal()[0] = std::numeric_limits<double>::quiet_NaN();
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveObjectiveMatrix) {
QuadraticProgram qp = TestDiagonalQp1();
qp.objective_matrix->diagonal()[0] = 1.0e60;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInInitialPrimalSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution =
VectorXd{{1.0, std::numeric_limits<double>::quiet_NaN(), 1.0, 1.0}},
.dual_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest,
DetectsExcessiveValueInInitialPrimalSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution = VectorXd{{1.0, 1.0e100, 1.0, 1.0}},
.dual_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest, DetectsIncorrectSizeInitialPrimalSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution = VectorXd{{1.0, 1.0, 1.0}},
.dual_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest, DetectsNanInInitialDualSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
.dual_solution =
VectorXd{{1.0, std::numeric_limits<double>::quiet_NaN(), 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest, DetectsExcessiveValueInInitialDualSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
.dual_solution = VectorXd{{1.0, 1.0e100, 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest, DetectsIncorrectSizeInitialDualSolution) {
QuadraticProgram qp = TestLp();
const PrimalAndDualSolution initial_solution = {
.primal_solution = VectorXd{{1.0, 1.0, 1.0, 1.0}},
.dual_solution = VectorXd{{1.0, 1.0, 1.0}},
};
SolverResult output = PrimalDualHybridGradient(
qp, PrimalDualHybridGradientParams(), initial_solution);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_INITIAL_SOLUTION);
}
TEST(PrimalDualHybridGradientTest, DetectsZeroObjectiveScalingFactor) {
QuadraticProgram qp = TestLp();
qp.objective_scaling_factor = 0.0;
SolverResult output =
PrimalDualHybridGradient(qp, PrimalDualHybridGradientParams());
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PrimalDualHybridGradientTest, DetectsFeasibilityPolishingForQp) {
QuadraticProgram qp = TestDiagonalQp1();
PrimalDualHybridGradientParams params;
params.set_use_feasibility_polishing(true);
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PARAMETER);
}
TEST(PrimalDualHybridGradientTest, ChecksTerminationAtCorrectFrequency) {
// `termination_check_frequency` is chosen so that it does not divide the
// `major_iteration_frequency`.
const int major_iteration_frequency = 5;
const int termination_check_frequency = 2;
const int iteration_limit = 16;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(termination_check_frequency);
params.set_major_iteration_frequency(major_iteration_frequency);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
std::vector<int> termination_checked_at_iteration;
for (const auto& stats : output.solve_log.iteration_stats()) {
if (stats.convergence_information_size() > 0) {
termination_checked_at_iteration.push_back(stats.iteration_number());
}
}
// Termination is checked on the first iteration, the last iteration, every
// major iteration, and at the `termination_check_frequency` (with a counter
// reset on every major iteration).
EXPECT_THAT(termination_checked_at_iteration,
ElementsAre(0, 2, 4, 5, 7, 9, 10, 12, 14, 15, 16));
}
TEST(PrimalDualHybridGradientTest, CallsCallback) {
// `termination_check_frequency` is chosen so that it does not divide the
// `major_iteration_frequency`.
const int major_iteration_frequency = 5;
const int termination_check_frequency = 5;
const int iteration_limit = 16;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(iteration_limit);
params.set_termination_check_frequency(termination_check_frequency);
params.set_major_iteration_frequency(major_iteration_frequency);
int callback_count = 0;
SolverResult output = PrimalDualHybridGradient(
TestLp(), params, /*interrupt_solve=*/nullptr,
/*message_callback=*/nullptr,
[&callback_count](const IterationCallbackInfo& callback_info) {
++callback_count;
});
ASSERT_EQ(output.solve_log.iteration_count(), iteration_limit);
// The callback should be called at every termination check, that is, at
// iterations 0, 5, 10, 15, and 16, and additionally when returning the final
// solution.
EXPECT_EQ(callback_count, 6);
}
// Returns the unique solution of `TinyLp`.
PrimalAndDualSolution TinyLpSolution() {
return PrimalAndDualSolution{
.primal_solution = Eigen::VectorXd{{1.0, 0.0, 6.0, 2.0}},
.dual_solution = Eigen::VectorXd{{0.5, 4.0, 0.0}},
};
}
TEST(PrimalDualHybridGradientTest, WarmStartedAtOptimum) {
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(1.0e-10);
SolverResult output =
PrimalDualHybridGradient(TinyLp(), params, TinyLpSolution());
// The solver should run a termination check at iteration 0 and determine that
// the initial point is the solution of the problem.
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-10));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-10));
EXPECT_LE(output.solve_log.iteration_count(), 0);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest, NoMovementWhenStartedAtOptimum) {
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
// Disable scaling because round-off causes a small amount of movement on the
// first iteration.
params.set_l2_norm_rescaling(false);
params.set_l_inf_ruiz_iterations(0);
const PrimalAndDualSolution initial_solution = TinyLpSolution();
SolverResult output =
PrimalDualHybridGradient(TinyLp(), params, initial_solution);
EXPECT_THAT(output.primal_solution, ElementsAre(1, 0, 6, 2));
EXPECT_THAT(output.dual_solution, ElementsAre(0.5, 4.0, 0.0));
EXPECT_LE(output.solve_log.iteration_count(), 1);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest, EmptyQp) {
MPModelProto proto;
absl::StatusOr<QuadraticProgram> qp =
QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
ASSERT_TRUE(qp.ok()) << qp.status();
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
SolverResult output = PrimalDualHybridGradient(*qp, params);
EXPECT_THAT(output.primal_solution, ElementsAre());
EXPECT_THAT(output.dual_solution, ElementsAre());
EXPECT_EQ(output.solve_log.iteration_count(), 0);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST(PrimalDualHybridGradientTest, RespectsInterrupt) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(0.0);
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_relative(0.0);
std::atomic<bool> interrupt_solve = true;
const SolverResult output =
PrimalDualHybridGradient(TestLp(), params, &interrupt_solve);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INTERRUPTED_BY_USER);
}
TEST(PrimalDualHybridGradientTest, RespectsInterruptFromCallback) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(0.0);
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_relative(0.0);
std::atomic<bool> interrupt_solve = false;
auto callback = [&](const IterationCallbackInfo& info) {
if (info.iteration_stats.iteration_number() >= 10) {
interrupt_solve.store(true);
}
};
const SolverResult output =
PrimalDualHybridGradient(TestLp(), params, &interrupt_solve,
/*message_callback=*/nullptr, callback);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INTERRUPTED_BY_USER);
EXPECT_GE(output.solve_log.iteration_count(), 10);
}
TEST(PrimalDualHybridGradientTest, IgnoresFalseInterrupt) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_absolute(0.0);
params.mutable_termination_criteria()
->mutable_simple_optimality_criteria()
->set_eps_optimal_relative(0.0);
params.mutable_termination_criteria()->set_kkt_matrix_pass_limit(1);
std::atomic<bool> interrupt_solve = false;
const SolverResult output =
PrimalDualHybridGradient(TestLp(), params, &interrupt_solve);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT);
}
TEST(PrimalDualHybridGradientTest, HugeNumThreads) {
const int iteration_upperbound = 10;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(
iteration_upperbound);
params.set_num_threads(1'000'000'000);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
}
TEST(PrimalDualHybridGradientTest, HugeNumShards) {
const int iteration_upperbound = 10;
PrimalDualHybridGradientParams params = ParamsWithNoLimits();
params.mutable_termination_criteria()->set_iteration_limit(
iteration_upperbound);
params.set_num_shards(1'000'000'000);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
}
TEST(PrimalDualHybridGradientTest, DetailedTerminationCriteria) {
const int iteration_upperbound = 300;
PrimalDualHybridGradientParams params = CreateSolverParams(
iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-5, /*enable_scaling=*/true,
/*num_threads=*/4, /*use_iteration_limit=*/false,
/*use_malitsky_pock_linesearch=*/false,
/*use_diagonal_qp_trust_region_solver=*/false);
params.set_major_iteration_frequency(60);
params.mutable_termination_criteria()->clear_simple_optimality_criteria();
auto* opt_criteria = params.mutable_termination_criteria()
->mutable_detailed_optimality_criteria();
opt_criteria->set_eps_optimal_primal_residual_absolute(1.0e-5);
opt_criteria->set_eps_optimal_primal_residual_relative(0.0);
opt_criteria->set_eps_optimal_dual_residual_absolute(1.0e-5);
opt_criteria->set_eps_optimal_dual_residual_relative(0.0);
opt_criteria->set_eps_optimal_objective_gap_absolute(1.0e-5);
opt_criteria->set_eps_optimal_objective_gap_relative(0.0);
SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
VerifyTerminationReasonAndIterationCount(params, output,
/*use_iteration_limit=*/false);
VerifyObjectiveValues(output, -1.0, 1.0e-4);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
EXPECT_THAT(output.reduced_costs,
EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
}
// Regression test for b/311455838. Note that this test only fails in debug
// mode, when an infeasible primal variable (from the iterate differences)
// violates presolve's assumptions and triggers a DCHECK() failure.
TEST(PrimalDualHybridGradientTest, IterateDifferenceBoundsInPresolve) {
// A trivial (but very badly scaled) LP found by fuzzing.
QuadraticProgram lp(2, 1);
lp.objective_offset = -3.0e+23;
lp.objective_vector =
VectorXd{{2.7369110631344083e-48, -3.0517578125211636e-05}};
lp.constraint_lower_bounds = VectorXd{{-2.7369110631344083e-48}};
lp.constraint_upper_bounds = VectorXd{{0}};
lp.variable_lower_bounds = VectorXd{{1.8446744073709552e+21, -1.0}};
lp.variable_upper_bounds = VectorXd{{kInfinity, 1.8446744073709552e+21}};
lp.constraint_matrix.coeffRef(0, 0) = -2.7369110631344083e-48;
lp.constraint_matrix.coeffRef(0, 1) = 1.0;
lp.constraint_matrix.makeCompressed();
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_iteration_limit(40);
auto& presolve_options = *params.mutable_presolve_options();
presolve_options.set_use_glop(true);
presolve_options.mutable_glop_parameters()->set_solve_dual_problem(
operations_research::glop::GlopParameters::LET_SOLVER_DECIDE);
presolve_options.mutable_glop_parameters()->set_dualizer_threshold(
1.3574141825331e-312);
params.set_infinite_constraint_bound_threshold(1.34785525461908e-312);
SolverResult output = PrimalDualHybridGradient(lp, params);
EXPECT_THAT(
output.solve_log.termination_reason(),
AnyOf(TERMINATION_REASON_ITERATION_LIMIT, TERMINATION_REASON_OPTIMAL));
}
// `FeasibilityPolishingTest` sets `params_` for feasibility polishing, and to
// avoid PDLP features that disrupt a simple analysis of the performance with
// and without feasibility polishing (primal weight adjustments, dynamic step
// sizes, and restarts). It also sets termination criteria with a relaxed
// objective gap tolerance.
class FeasibilityPolishingTest : public ::testing::Test {
protected:
FeasibilityPolishingTest() {
params_.set_linesearch_rule(
PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE);
auto* opt_criteria = params_.mutable_termination_criteria()
->mutable_detailed_optimality_criteria();
opt_criteria->set_eps_optimal_primal_residual_absolute(1.0e-6);
opt_criteria->set_eps_optimal_primal_residual_relative(1.0e-6);
opt_criteria->set_eps_optimal_dual_residual_absolute(1.0e-6);
opt_criteria->set_eps_optimal_dual_residual_relative(1.0e-6);
opt_criteria->set_eps_optimal_objective_gap_absolute(1.0e-2);
opt_criteria->set_eps_optimal_objective_gap_relative(1.0e-2);
params_.mutable_termination_criteria()->set_iteration_limit(500);
params_.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
false);
params_.set_primal_weight_update_smoothing(0.0);
params_.set_use_feasibility_polishing(true);
}
PrimalDualHybridGradientParams params_;
};
// `FeasibilityPolishingPrimalTest` contains a very simple `lp_` that requires
// roughly 2500 iterations without feasibility polishing because the dual is
// slow to converge, but with feasibility polishing solves the first time it
// attempts polishing.
class FeasibilityPolishingPrimalTest : public FeasibilityPolishingTest {
protected:
FeasibilityPolishingPrimalTest() : lp_(2, 1) {
// min x_1 + 1.001 x_2 s.t. x_1 + x_2 = 1
lp_.objective_offset = 0;
lp_.objective_vector = VectorXd{{1.0, 1.001}};
lp_.variable_lower_bounds = VectorXd{{-1, -1}};
lp_.variable_upper_bounds = VectorXd{{kInfinity, kInfinity}};
lp_.constraint_lower_bounds = VectorXd{{1}};
lp_.constraint_upper_bounds = VectorXd{{1}};
lp_.constraint_matrix.coeffRef(0, 0) = 1.0;
lp_.constraint_matrix.coeffRef(0, 1) = 1.0;
lp_.constraint_matrix.makeCompressed();
}
QuadraticProgram lp_;
};
// `FeasibilityPolishingDualTest` contains the dual of the lp from
// `FeasibilityPolishingPrimalTest` with the lower bounds on the variables
// changed to zero. It requires roughly 2500 iterations without feasibility
// polishing because the primal is slow to converge, but with feasibility
// polishing solves the first time it attempts polishing.
class FeasibilityPolishingDualTest : public FeasibilityPolishingTest {
protected:
FeasibilityPolishingDualTest() : lp_(1, 2) {
// min -y s.t. y <= 1, y <= 1.001, y free
lp_.objective_offset = 0;
lp_.objective_vector = VectorXd{{-1.0}};
lp_.variable_lower_bounds = VectorXd{{-kInfinity}};
lp_.variable_upper_bounds = VectorXd{{kInfinity}};
lp_.constraint_lower_bounds = VectorXd{{-kInfinity, -kInfinity}};
lp_.constraint_upper_bounds = VectorXd{{1, 1.001}};
lp_.constraint_matrix.coeffRef(0, 0) = 1.0;
lp_.constraint_matrix.coeffRef(1, 0) = 1.0;
lp_.constraint_matrix.makeCompressed();
}
QuadraticProgram lp_;
};
TEST_F(FeasibilityPolishingPrimalTest, FeasibilityPolishingSolvesFaster) {
// Without feasibility polishing, PDHG requires roughly 2500 iterations.
params_.set_use_feasibility_polishing(false);
SolverResult base_output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(base_output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
// With feasibility polishing, PDHG solves the first time it attempts
// polishing.
params_.set_use_feasibility_polishing(true);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingDualTest, FeasibilityPolishingSolvesFaster) {
// Without feasibility polishing, PDHG requires roughly 2500 iterations.
params_.set_use_feasibility_polishing(false);
SolverResult base_output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(base_output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
// With feasibility polishing, PDHG solves the first time it attempts
// polishing.
params_.set_use_feasibility_polishing(true);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingPrimalTest, FeasibilityPolishingFindsValidSolution) {
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
VerifyObjectiveValues(output, 1.0, 1.0e-2);
ASSERT_EQ(output.primal_solution.size(), 2);
// The optimal solution is {1, 0}. However, the relaxed optimality tolerance
// accepts many solutions, so just check feasibility (in addition to the
// objective value check above).
EXPECT_THAT(output.primal_solution[0] + output.primal_solution[1],
DoubleNear(1.0, 1.0e-6));
EXPECT_GE(output.primal_solution[0], 0.0);
EXPECT_GE(output.primal_solution[1], 0.0);
ASSERT_EQ(output.dual_solution.size(), 1);
// Dual feasibility.
EXPECT_LE(output.dual_solution[0], 1.0);
// Dual optimality.
EXPECT_GE(output.dual_solution[0], 1.0 - 1e-2);
EXPECT_THAT(output.reduced_costs,
EigenArrayNear<double>({1.0 - output.dual_solution[0],
1.001 - output.dual_solution[0]},
1.0e-12));
}
TEST_F(FeasibilityPolishingPrimalTest,
NoPolishingAfterIterationLimitWhenPolishingAfterLimitsDisabled) {
// Feasibility polishing would solve the problem the first time it is
// attempted, which would be at iteration 100.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_after_limits_reached(false);
params_.mutable_termination_criteria()->set_iteration_limit(50);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingAfterIterationLimitWhenPolishingAfterLimitsEnabled) {
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_after_limits_reached(true);
params_.mutable_termination_criteria()->set_iteration_limit(50);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingTerminatesAfterIterationLimitWhenPolishingAfterLimitsDisabled) {
// Feasibility polishing will be triggered at iteration 100. The iteration
// limit prevents primal polishing from completing.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_after_limits_reached(false);
params_.mutable_termination_criteria()->set_iteration_limit(101);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingContinuesAfterIterationLimitWhenPolishingAfterLimitsEnabled) {
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_after_limits_reached(true);
params_.mutable_termination_criteria()->set_iteration_limit(101);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingStopsAfterContinuingAfterIterationLimitWhenNotOptimal) {
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_after_limits_reached(true);
params_.mutable_termination_criteria()->set_iteration_limit(101);
auto* opt_criteria = params_.mutable_termination_criteria()
->mutable_detailed_optimality_criteria();
opt_criteria->set_eps_optimal_primal_residual_absolute(1.0e-16);
opt_criteria->set_eps_optimal_primal_residual_relative(0.0);
opt_criteria->set_eps_optimal_dual_residual_absolute(1.0e-16);
opt_criteria->set_eps_optimal_dual_residual_relative(0.0);
opt_criteria->set_eps_optimal_objective_gap_absolute(1.0e-16);
opt_criteria->set_eps_optimal_objective_gap_relative(0.0);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_ITERATION_LIMIT);
// 100 main iterations + at most 12 primal feasibility polishing iterations
// + at most 12 dual feasibility polishing iterations.
EXPECT_LE(output.solve_log.iteration_count(), 124);
}
TEST_F(FeasibilityPolishingPrimalTest,
NoPolishingAfterInterruptWhenPolishingAfterInterruptDisabled) {
// Feasibility polishing would solve the problem the first time it is
// attempted, which would be at iteration 100.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_if_solver_is_interrupted(false);
std::atomic<bool> interrupt_solve = false;
auto callback = [&](const IterationCallbackInfo& info) {
if (info.iteration_stats.iteration_number() >= 50) {
interrupt_solve.store(true);
}
};
SolverResult output =
PrimalDualHybridGradient(lp_, params_, &interrupt_solve,
/*message_callback=*/nullptr, callback);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INTERRUPTED_BY_USER);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingAfterInterruptWhenPolishingAfterInterruptEnabled) {
// Feasibility polishing would solve the problem the first time it is
// attempted, which would be at iteration 100.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_if_solver_is_interrupted(true);
std::atomic<bool> interrupt_solve = false;
auto callback = [&](const IterationCallbackInfo& info) {
if (info.iteration_stats.iteration_number() >= 50) {
interrupt_solve.store(true);
}
};
SolverResult output =
PrimalDualHybridGradient(lp_, params_, &interrupt_solve,
/*message_callback=*/nullptr, callback);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingTerminatesAfterInterruptWhenPolishingAfterInterruptDisabled) {
// Feasibility polishing would solve the problem the first time it is
// attempted, which would be at iteration 100.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_if_solver_is_interrupted(false);
std::atomic<bool> interrupt_solve = false;
auto callback = [&](const IterationCallbackInfo& info) {
if (info.iteration_type == IterationType::kPrimalFeasibility) {
interrupt_solve.store(true);
}
};
SolverResult output =
PrimalDualHybridGradient(lp_, params_, &interrupt_solve,
/*message_callback=*/nullptr, callback);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INTERRUPTED_BY_USER);
}
TEST_F(FeasibilityPolishingPrimalTest,
PolishingContinuesAfterInterruptWhenPolishingAfterInterruptEnabled) {
// Feasibility polishing would solve the problem the first time it is
// attempted, which would be at iteration 100.
params_.set_use_feasibility_polishing(true);
params_.set_apply_feasibility_polishing_if_solver_is_interrupted(true);
std::atomic<bool> interrupt_solve = false;
auto callback = [&](const IterationCallbackInfo& info) {
if (info.iteration_type == IterationType::kPrimalFeasibility) {
interrupt_solve.store(true);
}
};
SolverResult output =
PrimalDualHybridGradient(lp_, params_, &interrupt_solve,
/*message_callback=*/nullptr, callback);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
}
TEST_F(FeasibilityPolishingPrimalTest, FeasibilityPolishingDetailsInLog) {
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
int primal_optimal_count = 0;
int dual_optimal_count = 0;
for (const auto& phase : output.solve_log.feasibility_polishing_details()) {
switch (phase.polishing_phase_type()) {
case POLISHING_PHASE_TYPE_PRIMAL_FEASIBILITY: {
if (phase.termination_reason() == TERMINATION_REASON_OPTIMAL) {
++primal_optimal_count;
}
break;
}
case POLISHING_PHASE_TYPE_DUAL_FEASIBILITY: {
if (phase.termination_reason() == TERMINATION_REASON_OPTIMAL) {
++dual_optimal_count;
}
break;
}
default: {
ADD_FAILURE() << "Unexpected polishing_phase_type: "
<< phase.polishing_phase_type();
break;
}
}
EXPECT_TRUE(phase.has_solution_stats());
EXPECT_EQ(phase.solution_stats().iteration_number(),
phase.iteration_count());
}
EXPECT_GE(primal_optimal_count, 1);
EXPECT_GE(dual_optimal_count, 1);
}
TEST_F(FeasibilityPolishingPrimalTest,
FeasibilityPolishingUsesInfiniteTolerances) {
SolverResult output = PrimalDualHybridGradient(lp_, params_);
for (const auto& phase : output.solve_log.feasibility_polishing_details()) {
switch (phase.polishing_phase_type()) {
case POLISHING_PHASE_TYPE_PRIMAL_FEASIBILITY: {
EXPECT_TRUE(phase.params()
.termination_criteria()
.has_detailed_optimality_criteria());
const auto& criteria = phase.params()
.termination_criteria()
.detailed_optimality_criteria();
EXPECT_EQ(criteria.eps_optimal_objective_gap_absolute(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_objective_gap_relative(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_dual_residual_absolute(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_dual_residual_relative(), kInfinity);
break;
}
case POLISHING_PHASE_TYPE_DUAL_FEASIBILITY: {
EXPECT_TRUE(phase.params()
.termination_criteria()
.has_detailed_optimality_criteria());
const auto& criteria = phase.params()
.termination_criteria()
.detailed_optimality_criteria();
EXPECT_EQ(criteria.eps_optimal_objective_gap_absolute(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_objective_gap_relative(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_primal_residual_absolute(), kInfinity);
EXPECT_EQ(criteria.eps_optimal_primal_residual_relative(), kInfinity);
break;
}
default: {
ADD_FAILURE() << "Unexpected polishing_phase_type: "
<< phase.polishing_phase_type();
break;
}
}
}
}
TEST_F(FeasibilityPolishingPrimalTest,
FeasibilityPolishingWorkStatsAreCorrect) {
params_.set_record_iteration_stats(true);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
int total_feasibility_iterations = 0;
double total_feasibility_time = 0.0;
for (const auto& phase : output.solve_log.feasibility_polishing_details()) {
EXPECT_TRUE(phase.has_solution_stats());
EXPECT_EQ(phase.solution_stats().iteration_number(),
phase.iteration_count());
// For at least one of these, `phase.iteration_count() > 0`, because
// `total_feasibility_iterations` is verified to be >= 1 below.
EXPECT_GE(phase.iteration_count(), 0);
EXPECT_LE(phase.iteration_count(), phase.main_iteration_count() / 4);
total_feasibility_iterations += phase.iteration_count();
total_feasibility_time += phase.solve_time_sec();
}
ASSERT_THAT(output.solve_log.iteration_stats(), Not(IsEmpty()));
const auto& last_stats = output.solve_log.iteration_stats(
output.solve_log.iteration_stats_size() - 1);
EXPECT_GE(total_feasibility_iterations, 1);
EXPECT_GE(last_stats.iteration_number(), 1);
// This checks that `iteration_count()` includes both the main and feasibility
// iterations, and that `iteration_stats.iteration_number()` does not include
// work from feasibility iterations.
EXPECT_EQ(last_stats.iteration_number() + total_feasibility_iterations,
output.solve_log.iteration_count());
EXPECT_GT(total_feasibility_time, 0.0);
EXPECT_GT(last_stats.cumulative_time_sec(), 0.0);
// This is an approximate check that `solve_time_sec()` includes both the main
// and feasibility iterations, and that
// `iteration_stats.cumulative_time_sec()` does not include time from
// feasibility iterations. We don't expect equality because some clock time
// can pass while switching between phases.
EXPECT_LE(total_feasibility_time + last_stats.cumulative_time_sec(),
output.solve_log.solve_time_sec());
}
TEST_F(FeasibilityPolishingPrimalTest, FeasibilityObjectivesAreZero) {
params_.set_record_iteration_stats(true);
SolverResult output = PrimalDualHybridGradient(lp_, params_);
int primal_convergence_info_count = 0;
int dual_convergence_info_count = 0;
for (const auto& phase : output.solve_log.feasibility_polishing_details()) {
for (const auto& iter_stats : phase.iteration_stats()) {
for (const auto& convergence : iter_stats.convergence_information()) {
switch (phase.polishing_phase_type()) {
case POLISHING_PHASE_TYPE_PRIMAL_FEASIBILITY: {
++primal_convergence_info_count;
EXPECT_EQ(convergence.primal_objective(), 0.0);
break;
}
case POLISHING_PHASE_TYPE_DUAL_FEASIBILITY: {
++dual_convergence_info_count;
EXPECT_EQ(convergence.dual_objective(), 0.0);
break;
}
default: {
ADD_FAILURE() << "Unexpected polishing_phase_type: "
<< phase.polishing_phase_type();
break;
}
}
}
}
}
// These checks both verify that the previous objective tests were actually
// applied, and that iteration stats (and their convergence info) are logged.
EXPECT_GE(primal_convergence_info_count, 1);
EXPECT_GE(dual_convergence_info_count, 1);
}
TEST_F(FeasibilityPolishingPrimalTest, CallsCallbackForAllThreePhases) {
absl::flat_hash_map<IterationType, int> callback_count;
bool polishing_termination_is_last = false;
SolverResult output = PrimalDualHybridGradient(
lp_, params_, /*interrupt_solve=*/nullptr,
/*message_callback=*/nullptr,
[&](const IterationCallbackInfo& callback_info) {
++callback_count[callback_info.iteration_type];
polishing_termination_is_last =
callback_info.iteration_type ==
IterationType::kFeasibilityPolishingTermination;
});
EXPECT_TRUE(polishing_termination_is_last);
EXPECT_THAT(
callback_count,
UnorderedElementsAre(
Pair(IterationType::kPrimalFeasibility, Ge(1)),
Pair(IterationType::kDualFeasibility, Ge(1)),
Pair(IterationType::kNormal, Ge(1)),
Pair(IterationType::kFeasibilityPolishingTermination, Ge(1))));
}
TEST_F(FeasibilityPolishingTest, IterationLimitIncludesFeasibilityPhases) {
params_.mutable_termination_criteria()->set_iteration_limit(300);
params_.set_record_iteration_stats(true);
SolverResult output = PrimalDualHybridGradient(TinyLp(), params_);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
int feasibility_polishing_iterations = 0;
for (const auto& phase : output.solve_log.feasibility_polishing_details()) {
feasibility_polishing_iterations += phase.iteration_count();
}
// `feasibility_polishing_iterations` should at least include 12 iterations
// from the first failed primal feasibility polishing attempt.
EXPECT_GE(feasibility_polishing_iterations, 12);
ASSERT_THAT(output.solve_log.iteration_stats(), Not(IsEmpty()));
const IterationStats& last_stats = output.solve_log.iteration_stats(
output.solve_log.iteration_stats_size() - 1);
EXPECT_EQ(last_stats.iteration_number() + feasibility_polishing_iterations,
output.solve_log.iteration_count());
}
// This test doesn't attempt to check what is logged. Rather, it just verifies
// that the code succeeds at each verbosity level. Other than having the
// verbosity level as a parameter, and fixing the other parameters, it is the
// same as `PrimalDualHybridGradientLPTest.Tiny`.
TEST_P(PrimalDualHybridGradientVerbosityTest, TinyLp) {
const int iteration_upperbound = 300;
const int verbosity_level = GetParam();
PrimalDualHybridGradientParams params =
CreateSolverParams(iteration_upperbound,
/*eps_optimal_absolute=*/1.0e-5,
/*enable_scaling=*/false,
/*num_threads=*/1,
/*use_iteration_limit=*/false,
/*use_malitsky_pock_linesearch=*/false,
/*use_diagonal_qp_trust_region_solver=*/false);
params.set_major_iteration_frequency(60);
params.set_verbosity_level(verbosity_level);
SolverResult output = PrimalDualHybridGradient(TinyLp(), params);
VerifyTerminationReasonAndIterationCount(params, output,
/*use_iteration_limit=*/false);
VerifyObjectiveValues(output, -1.0, 1.0e-4);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({1, 0, 6, 2}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 4.0, 0.0}, 1.0e-4));
EXPECT_THAT(output.reduced_costs,
EigenArrayNear<double>({0.0, 1.5, -3.5, 0.0}, 1.0e-4));
EXPECT_EQ(output.solve_log.original_problem_stats().num_variables(), 4);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_variables(), 4);
EXPECT_EQ(output.solve_log.original_problem_stats().num_constraints(), 3);
EXPECT_LE(output.solve_log.preprocessed_problem_stats().num_constraints(), 3);
}
TEST(PresolveTest, DetectsProblemWithInconsistentBounds) {
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output =
PrimalDualHybridGradient(SmallInvalidProblemLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_INVALID_PROBLEM);
}
TEST(PresolveTest, PresolveSolvesToOptimality) {
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(TestLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
EXPECT_EQ(output.solve_log.iteration_count(), 0);
ASSERT_EQ(output.solve_log.solution_type(), POINT_TYPE_PRESOLVER_SOLUTION);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-10));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-10));
const auto convergence_info = GetConvergenceInformation(
output.solve_log.solution_stats(), POINT_TYPE_PRESOLVER_SOLUTION);
ASSERT_TRUE(convergence_info.has_value());
EXPECT_EQ(convergence_info->candidate_type(), POINT_TYPE_PRESOLVER_SOLUTION);
EXPECT_DOUBLE_EQ(convergence_info->primal_objective(), -34.0);
EXPECT_DOUBLE_EQ(convergence_info->dual_objective(), -34.0);
EXPECT_DOUBLE_EQ(convergence_info->corrected_dual_objective(), -34.0);
EXPECT_DOUBLE_EQ(convergence_info->l_inf_primal_residual(), 0.0);
EXPECT_DOUBLE_EQ(convergence_info->l2_primal_residual(), 0.0);
EXPECT_DOUBLE_EQ(convergence_info->l_inf_dual_residual(), 0.0);
EXPECT_DOUBLE_EQ(convergence_info->l2_dual_residual(), 0.0);
EXPECT_DOUBLE_EQ(convergence_info->l_inf_primal_variable(), 8.0);
EXPECT_DOUBLE_EQ(convergence_info->l2_primal_variable(), 8.5);
EXPECT_DOUBLE_EQ(convergence_info->l_inf_dual_variable(), 2.375);
EXPECT_THAT(convergence_info->l2_dual_variable(),
DoubleNear(3.17569983538, 1.0e-10));
}
TEST(PresolveTest, SolvesWithGlopScalingEnabled) {
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
params.mutable_presolve_options()->mutable_glop_parameters()->set_use_scaling(
true);
params.mutable_presolve_options()
->mutable_glop_parameters()
->set_use_preprocessing(false);
QuadraticProgram test_lp = TestLp();
// Change `objective_scaling_factor`, so that glop's presolve scaling will
// react.
test_lp.objective_scaling_factor = 0.1;
SolverResult output = PrimalDualHybridGradient(test_lp, params);
VerifyTerminationReasonAndIterationCount(params, output,
/*use_iteration_limit=*/false);
VerifyObjectiveValues(output, -3.4, 1.0e-6);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({-1, 8, 1, 2.5}, 1.0e-4));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({-2, 0, 2.375, 2.0 / 3}, 1.0e-4));
}
TEST(PresolveTest, PresolveInfeasible) {
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output =
PrimalDualHybridGradient(SmallPrimalInfeasibleLp(), params);
EXPECT_EQ(output.solve_log.termination_reason(),
TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE);
EXPECT_EQ(output.solve_log.solution_type(), POINT_TYPE_PRESOLVER_SOLUTION);
EXPECT_EQ(output.solve_log.iteration_count(), 0);
}
TEST(PresolveTest, WarnsInitialSolution) {
PrimalAndDualSolution initial_solution;
initial_solution.primal_solution.setZero(6);
initial_solution.dual_solution.setZero(3);
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output = PrimalDualHybridGradient(CorrelationClusteringStarLp(),
params, initial_solution);
}
TEST(PresolveTest, WarnsInitialSolutionViaCallback) {
PrimalAndDualSolution initial_solution;
initial_solution.primal_solution.setZero(6);
initial_solution.dual_solution.setZero(3);
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
std::vector<std::string> message_output;
SolverResult output = PrimalDualHybridGradient(
CorrelationClusteringStarLp(), params, initial_solution,
/*interrupt_solve=*/nullptr,
[&message_output](const std::string& message) {
message_output.push_back(message);
});
EXPECT_THAT(message_output, Contains(AllOf(HasSubstr("initial solution"),
HasSubstr("presolve"))));
}
TEST_P(PresolveDualScalingTest, Dualize) {
auto [dualize, negate_and_scale_objective] = GetParam();
PrimalDualHybridGradientParams params;
params.mutable_presolve_options()->set_use_glop(true);
params.mutable_presolve_options()
->mutable_glop_parameters()
->set_solve_dual_problem(dualize ? glop::GlopParameters::ALWAYS_DO
: glop::GlopParameters::NEVER_DO);
params.mutable_termination_criteria()->set_iteration_limit(1000);
QuadraticProgram qp = CorrelationClusteringStarLp();
if (negate_and_scale_objective) {
qp.objective_scaling_factor = -3;
}
SolverResult output = PrimalDualHybridGradient(qp, params);
EXPECT_EQ(output.solve_log.termination_reason(), TERMINATION_REASON_OPTIMAL);
EXPECT_GT(output.solve_log.iteration_count(), 0);
EXPECT_THAT(output.primal_solution,
EigenArrayNear<double>({0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, 1.0e-10));
EXPECT_THAT(output.dual_solution,
EigenArrayNear<double>({0.5, 0.5, 0.5}, 1.0e-10));
}
TEST(PresolveTest, PresolveParametersAreUsed) {
QuadraticProgram qp = CorrelationClusteringStarLp();
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_iteration_limit(0);
params.mutable_presolve_options()->set_use_glop(true);
SolverResult output_1 = PrimalDualHybridGradient(qp, params);
params.mutable_presolve_options()
->mutable_glop_parameters()
->set_solve_dual_problem(glop::GlopParameters::ALWAYS_DO);
SolverResult output_2 = PrimalDualHybridGradient(qp, params);
EXPECT_NE(output_1.solve_log.preprocessed_problem_stats().num_variables(),
output_2.solve_log.preprocessed_problem_stats().num_variables());
EXPECT_NE(output_1.solve_log.preprocessed_problem_stats().num_constraints(),
output_2.solve_log.preprocessed_problem_stats().num_constraints());
}
TEST(ComputeStatusesTest, AtOptimum) {
QuadraticProgram lp = TestLp();
const PrimalAndDualSolution solution = {
.primal_solution = Eigen::VectorXd{{-1, 8, 1, 2.5}},
.dual_solution = Eigen::VectorXd{{-2, 0, 2.375, 2.0 / 3}},
};
glop::ProblemSolution glop_solution = internal::ComputeStatuses(lp, solution);
EXPECT_THAT(
glop_solution.constraint_statuses,
ElementsAre(ConstraintStatus::FIXED_VALUE, ConstraintStatus::BASIC,
ConstraintStatus::AT_LOWER_BOUND,
ConstraintStatus::AT_LOWER_BOUND));
EXPECT_THAT(
glop_solution.variable_statuses,
ElementsAre(VariableStatus::BASIC, VariableStatus::BASIC,
VariableStatus::BASIC, VariableStatus::AT_LOWER_BOUND));
}
TEST(ComputeStatusesTest, CoverMoreCases) {
QuadraticProgram lp = TestLp();
lp.variable_upper_bounds[3] = 2.5;
lp.variable_upper_bounds[1] = 8;
const PrimalAndDualSolution solution = {
.primal_solution = Eigen::VectorXd{{-1, 8, 1, 2.5}},
.dual_solution = Eigen::VectorXd{{-2, 0, 2.375, -1}},
};
glop::ProblemSolution glop_solution = internal::ComputeStatuses(lp, solution);
EXPECT_THAT(
glop_solution.constraint_statuses,
ElementsAre(ConstraintStatus::FIXED_VALUE, ConstraintStatus::BASIC,
ConstraintStatus::AT_LOWER_BOUND,
ConstraintStatus::AT_UPPER_BOUND));
EXPECT_THAT(glop_solution.variable_statuses,
ElementsAre(VariableStatus::BASIC, VariableStatus::AT_UPPER_BOUND,
VariableStatus::BASIC, VariableStatus::FIXED_VALUE));
}
} // namespace
} // namespace operations_research::pdlp