[bazel] Update bazel files

This commit is contained in:
Guillaume Chatelet
2025-09-29 12:54:27 +00:00
committed by Corentin Le Molgat
parent d8ad3a8f9b
commit 54ae17fa91
23 changed files with 735 additions and 213 deletions

View File

@@ -27,7 +27,7 @@ cc_library(
":solvers_cc_proto",
"//ortools/base:threadpool",
"@abseil-cpp//absl/functional:any_invocable",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/synchronization",
"@eigen",
],
)
@@ -40,7 +40,7 @@ cc_test(
":scheduler",
":solvers_cc_proto",
"//ortools/base:gmock_main",
"@abseil-cpp//absl/functional:any_invocable",
"@abseil-cpp//absl/log",
],
)
@@ -73,9 +73,7 @@ cc_proto_library(
py_proto_library(
name = "solvers_py_pb2",
deps = [
":solvers_proto",
],
deps = [":solvers_proto"],
)
cc_library(
@@ -88,8 +86,8 @@ cc_library(
":sharder",
":solve_log_cc_proto",
":solvers_cc_proto",
"//ortools/base",
"//ortools/base:mathutil",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/random:distributions",
"@eigen",
],
@@ -107,7 +105,7 @@ cc_test(
":solvers_cc_proto",
":test_util",
"//ortools/base:gmock_main",
"//ortools/base:protobuf_util",
"//ortools/base:parse_text_proto",
"@eigen",
],
)
@@ -127,7 +125,6 @@ cc_library(
":solvers_proto_validation",
":termination",
":trust_region",
"//ortools/base",
"//ortools/base:mathutil",
"//ortools/base:timer",
"//ortools/glop:parameters_cc_proto",
@@ -139,6 +136,8 @@ cc_library(
"//ortools/util:logging",
"@abseil-cpp//absl/algorithm:container",
"@abseil-cpp//absl/base:nullability",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",
@@ -164,13 +163,14 @@ cc_test(
":solvers_cc_proto",
":termination",
":test_util",
"//ortools/base",
"//ortools/base:gmock_main",
"//ortools/glop:parameters_cc_proto",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/lp_data",
"//ortools/lp_data:base",
"@abseil-cpp//absl/container:flat_hash_map",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",
"@eigen",
@@ -182,9 +182,9 @@ cc_library(
srcs = ["quadratic_program.cc"],
hdrs = ["quadratic_program.h"],
deps = [
"//ortools/base",
"//ortools/base:status_macros",
"//ortools/linear_solver:linear_solver_cc_proto",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",
@@ -200,8 +200,7 @@ cc_test(
":quadratic_program",
":test_util",
"//ortools/base:gmock_main",
"//ortools/base:protobuf_util",
"//ortools/base:status_macros",
"//ortools/base:parse_text_proto",
"//ortools/linear_solver:linear_solver_cc_proto",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
@@ -215,8 +214,10 @@ cc_library(
hdrs = ["quadratic_program_io.h"],
deps = [
":quadratic_program",
"//ortools/base",
"//ortools/base:file",
"//ortools/base:gzipfile",
"//ortools/base:mathutil",
"//ortools/base:recordio",
"//ortools/base:status_macros",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_exporter",
@@ -224,6 +225,8 @@ cc_library(
"//ortools/util:file_util",
"@abseil-cpp//absl/base",
"@abseil-cpp//absl/container:flat_hash_map",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",
@@ -241,8 +244,9 @@ cc_library(
":sharded_quadratic_program",
":sharder",
":solve_log_cc_proto",
"//ortools/base",
"//ortools/base:mathutil",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/random:distributions",
"@eigen",
],
@@ -274,9 +278,9 @@ cc_library(
":scheduler",
":sharder",
":solvers_cc_proto",
"//ortools/base",
"//ortools/util:logging",
"@abseil-cpp//absl/memory",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/strings",
"@eigen",
],
@@ -302,7 +306,6 @@ cc_library(
hdrs = ["sharder.h"],
deps = [
":scheduler",
"//ortools/base",
"//ortools/base:mathutil",
"//ortools/base:timer",
"@abseil-cpp//absl/log",
@@ -323,9 +326,9 @@ cc_test(
":scheduler",
":sharder",
":solvers_cc_proto",
"//ortools/base",
"//ortools/base:gmock_main",
"//ortools/base:mathutil",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/random:distributions",
"@eigen",
],
@@ -350,7 +353,7 @@ cc_test(
":solvers_cc_proto",
":solvers_proto_validation",
"//ortools/base:gmock_main",
"//ortools/base:protobuf_util",
"//ortools/base:parse_text_proto",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/strings",
],
@@ -363,7 +366,7 @@ cc_library(
deps = [
":solve_log_cc_proto",
":solvers_cc_proto",
"//ortools/base",
"@abseil-cpp//absl/log",
],
)
@@ -376,7 +379,7 @@ cc_test(
":solvers_cc_proto",
":termination",
"//ortools/base:gmock_main",
"//ortools/base:protobuf_util",
"//ortools/base:parse_text_proto",
],
)
@@ -387,8 +390,10 @@ cc_library(
hdrs = ["test_util.h"],
deps = [
":quadratic_program",
"//ortools/base",
"//ortools/base:gmock",
"//ortools/base:path",
"@abseil-cpp//absl/flags:flag",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/types:span",
"@eigen",
],
@@ -399,8 +404,8 @@ cc_test(
srcs = ["test_util_test.cc"],
deps = [
":test_util",
"//ortools/base",
"//ortools/base:gmock_main",
"@abseil-cpp//absl/log:check",
"@abseil-cpp//absl/types:span",
"@eigen",
],
@@ -415,9 +420,10 @@ cc_library(
":sharded_optimization_utils",
":sharded_quadratic_program",
":sharder",
"//ortools/base",
"//ortools/base:mathutil",
"@abseil-cpp//absl/algorithm:container",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",
"@eigen",
],
)

View File

@@ -21,7 +21,7 @@
#include "Eigen/Core"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/base/parse_text_proto.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/solve_log.pb.h"
@@ -31,16 +31,459 @@
namespace operations_research::pdlp {
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::google::protobuf::contrib::parse_proto::ParseTextOrDie;
using ::testing::AllOf;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::EqualsProto;
using ::testing::Ge;
using ::testing::Le;
using ::testing::Ne;
using ::testing::SizeIs;
using ::testing::proto::Approximately;
using ::testing::proto::Partially;
// The following block relies heavily on `EqualsProto`, which isn't open source.
void CheckScaledAndUnscaledConvergenceInformation(
QuadraticProgram qp, const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution,
const double componentwise_primal_residual_offset,
const double componentwise_dual_residual_offset,
const ConvergenceInformation& expected_stats) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(std::move(qp), num_threads, num_shards);
EXPECT_THAT(
ComputeScaledConvergenceInformation(
PrimalDualHybridGradientParams(), sharded_qp, primal_solution,
dual_solution, componentwise_primal_residual_offset,
componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(expected_stats))));
// Rescale the problem so that the primal and dual solutions have elements
// equal to -1.0, 0.0, or 1.0.
Eigen::VectorXd col_scaling_vec = primal_solution.unaryExpr(
[](double x) { return x != 0.0 ? std::abs(x) : 1.0; });
Eigen::VectorXd row_scaling_vec = dual_solution.unaryExpr(
[](double x) { return x != 0.0 ? std::abs(x) : 1.0; });
Eigen::VectorXd scaled_primal_solution =
primal_solution.cwiseQuotient(col_scaling_vec);
Eigen::VectorXd scaled_dual_solution =
dual_solution.cwiseQuotient(row_scaling_vec);
sharded_qp.RescaleQuadraticProgram(col_scaling_vec, row_scaling_vec);
EXPECT_THAT(
ComputeConvergenceInformation(
PrimalDualHybridGradientParams(), sharded_qp, col_scaling_vec,
row_scaling_vec, scaled_primal_solution, scaled_dual_solution,
componentwise_primal_residual_offset,
componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(expected_stats))));
// Also check that the iteration stats for the scaled problem have the correct
// objectives and norms.
ConvergenceInformation expected_scaled_stats;
expected_scaled_stats.set_primal_objective(expected_stats.primal_objective());
expected_scaled_stats.set_dual_objective(expected_stats.dual_objective());
expected_scaled_stats.set_l_inf_primal_variable(1.0);
expected_scaled_stats.set_l_inf_dual_variable(1.0);
EXPECT_THAT(
ComputeScaledConvergenceInformation(
PrimalDualHybridGradientParams(), sharded_qp, scaled_primal_solution,
scaled_dual_solution, componentwise_primal_residual_offset,
componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(expected_scaled_stats))));
}
void CheckScaledAndUnscaledInfeasibilityStats(
QuadraticProgram qp, const Eigen::VectorXd& primal_ray,
const Eigen::VectorXd& dual_ray,
const Eigen::VectorXd& primal_solution_for_residual_tests,
const InfeasibilityInformation& expected_infeasibility_info) {
const int num_threads = 2;
const int num_shards = 2;
ShardedQuadraticProgram sharded_qp(std::move(qp), num_threads, num_shards);
EXPECT_THAT(
ComputeInfeasibilityInformation(
PrimalDualHybridGradientParams(), sharded_qp,
Eigen::VectorXd::Ones(sharded_qp.PrimalSize()),
Eigen::VectorXd::Ones(sharded_qp.DualSize()), primal_ray, dual_ray,
primal_solution_for_residual_tests, POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(expected_infeasibility_info))));
// Rescale the problem so that the primal and dual certificates have elements
// equal to -1.0, 0.0, or 1.0.
Eigen::VectorXd col_scaling_vec = primal_ray.unaryExpr(
[](double x) { return x != 0.0 ? std::abs(x) : 1.0; });
Eigen::VectorXd row_scaling_vec =
dual_ray.unaryExpr([](double x) { return x != 0.0 ? std::abs(x) : 1.0; });
Eigen::VectorXd scaled_primal_solution =
primal_ray.cwiseQuotient(col_scaling_vec);
Eigen::VectorXd scaled_dual_solution =
dual_ray.cwiseQuotient(row_scaling_vec);
Eigen::VectorXd scaled_primal_solution_for_residual_tests =
primal_solution_for_residual_tests.cwiseQuotient(col_scaling_vec);
sharded_qp.RescaleQuadraticProgram(col_scaling_vec, row_scaling_vec);
EXPECT_THAT(
ComputeInfeasibilityInformation(
PrimalDualHybridGradientParams(), sharded_qp, col_scaling_vec,
row_scaling_vec, scaled_primal_solution, scaled_dual_solution,
scaled_primal_solution_for_residual_tests,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(expected_infeasibility_info))));
}
TEST(IterationStatsTest, SimpleLpAtOptimum) {
const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 2.5}};
const Eigen::VectorXd dual_solution{{-2.0, 0.0, 2.375, 2.0 / 3}};
CheckScaledAndUnscaledConvergenceInformation(
TestLp(), primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
ParseTextOrDie<ConvergenceInformation>(R"pb(
primal_objective: -34.0
dual_objective: -34.0
corrected_dual_objective: -34.0
l_inf_primal_residual: 0.0
l2_primal_residual: 0.0
l_inf_componentwise_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_dual_residual: 0.0
l_inf_componentwise_dual_residual: 0.0
l_inf_primal_variable: 8.0
l2_primal_variable: 8.5
l_inf_dual_variable: 2.375
l2_dual_variable: 3.1756998353818715
)pb"));
}
TEST(IterationStatsTest, SimpleLpWithPrimalResidual) {
// This is the optimal solution, except that x_3 (`primal_solution[3]`) has
// been changed from 2.5 to 3.5, increasing the objective by 1, but causing
// the first constraint to be violated by 2 and the last constraint by 1.
const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 3.5}};
const Eigen::VectorXd dual_solution{{-2.0, 0.0, 2.375, 2.0 / 3}};
CheckScaledAndUnscaledConvergenceInformation(
TestLp(), primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
ParseTextOrDie<ConvergenceInformation>(R"pb(
primal_objective: -33.0
dual_objective: -34.0
corrected_dual_objective: -34.0
l_inf_primal_residual: 2.0
l2_primal_residual: 2.2360679774997896
l_inf_componentwise_primal_residual: 0.5
l_inf_dual_residual: 0.0
l2_dual_residual: 0.0
l_inf_componentwise_dual_residual: 0.0
l_inf_primal_variable: 8.0
l2_primal_variable: 8.8459030064770662
l_inf_dual_variable: 2.375
l2_dual_variable: 3.1756998353818715
)pb"));
}
TEST(IterationStatsTest, SimpleLpWithDualResidual) {
// This is the optimal solution, except that y_1 (`dual_solution[1]`) has been
// changed from 0 to -1, causing x_0 and x_2 to have primal gradients (dual
// residuals) of 1.0.
const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 2.5}};
const Eigen::VectorXd dual_solution{{-2.0, -1.0, 2.375, 2.0 / 3}};
CheckScaledAndUnscaledConvergenceInformation(
TestLp(), primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
ParseTextOrDie<ConvergenceInformation>(R"pb(
primal_objective: -34.0
dual_objective: -41.0
corrected_dual_objective: -inf
l_inf_primal_residual: 0.0
l2_primal_residual: 0.0
l_inf_componentwise_primal_residual: 0.0
l_inf_dual_residual: 1.0
l2_dual_residual: 1.4142135623730950
l_inf_componentwise_dual_residual: 0.5
l_inf_primal_variable: 8.0
l2_primal_variable: 8.5
l_inf_dual_variable: 2.375
l2_dual_variable: 3.3294247918288294
)pb"));
}
TEST(IterationStatsTest, SimpleLpWithBothResiduals) {
// This is the optimal solution, except that x_3 (`primal_solution[3]`) has
// been changed from 2.5 to 3.5, increasing the objective by 1, but causing
// the first constraint to be violated by 2 and the last constraint by 1, and
// y_1 (`dual_solution[1]`) has been changed from 0 to -1, causing x_0 and x_2
// to have primal gradients (dual residuals) of 1.0. The primal and dual
// componentwise_residual_offset values are different, to check that the
// correct offset is applied when computing the
// l_inf_componentwise_XXX_residual values.
const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 3.5}};
const Eigen::VectorXd dual_solution{{-2.0, -1.0, 2.375, 2.0 / 3}};
CheckScaledAndUnscaledConvergenceInformation(
TestLp(), primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/3.0,
/*componentwise_dual_residual_offset=*/1.0,
ParseTextOrDie<ConvergenceInformation>(R"pb(
primal_objective: -33.0
dual_objective: -41.0
corrected_dual_objective: -inf
l_inf_primal_residual: 2.0
l2_primal_residual: 2.2360679774997896
l_inf_componentwise_primal_residual: 0.25
l_inf_dual_residual: 1.0
l2_dual_residual: 1.4142135623730950
l_inf_componentwise_dual_residual: 0.5
l_inf_primal_variable: 8.0
l2_primal_variable: 8.8459030064770662
l_inf_dual_variable: 2.375
l2_dual_variable: 3.3294247918288294
)pb"));
}
TEST(IterationStatsTest, SimpleQpAtOptimum) {
const Eigen::VectorXd primal_solution{{1.0, 0.0}};
const Eigen::VectorXd dual_solution{{-1.0}};
CheckScaledAndUnscaledConvergenceInformation(
TestDiagonalQp1(), primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
ParseTextOrDie<ConvergenceInformation>(R"pb(
primal_objective: 6.0
dual_objective: 6.0
corrected_dual_objective: 6.0
l_inf_primal_residual: 0.0
l2_primal_residual: 0.0
l_inf_componentwise_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_dual_residual: 0.0
l_inf_componentwise_dual_residual: 0.0
l_inf_primal_variable: 1.0
l2_primal_variable: 1.0
l_inf_dual_variable: 1.0
l2_dual_variable: 1.0
)pb"));
}
TEST(IterationStatsTest, SimpleLpWithGapResidualsAndZeroPrimalSolution) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
const Eigen::VectorXd primal_solution = Eigen::VectorXd::Zero(4);
const Eigen::VectorXd dual_solution{{1.0, 0.0, 0.0, -1.0}};
PrimalDualHybridGradientParams params_true, params_false;
params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
true);
params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
false);
// c is: [5.5, -2, -1, 1]
// -A^T y is: [-2, -1, 0.5, -3]
// c - A^T y is: [3.5, -3.0, -0.5, -2.0].
// When the primal variable is 0.0 and the bound is not 0.0, the bound
// corresponding to c - A^T y is handled as infinite when
// `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true.
// Thus, for the all zero primal solution: when
// `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true, the
// residuals are [3.5, -3.0, -0.5, -2.0] and all bounds are treated as
// infinite. When `handle_some_primal_gradients_on_finite_bounds_as_residuals`
// is false, the residuals are [3.5, -3.0, 0, 0] and the corresponding bound
// terms are [0.0, -2, 6, 3.5].
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_true, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: -3.0
corrected_dual_objective: -inf
l_inf_dual_residual: 3.5
# 5.0497524691810389 = L_2(3.5, -3.0, -0.5, -2.0)
l2_dual_residual: 5.0497524691810389
)pb"))));
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_false, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: -7.0
corrected_dual_objective: -inf
l_inf_dual_residual: 3.5
# 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0)
l2_dual_residual: 4.6097722286464436
)pb"))));
}
TEST(IterationStatsTest, SimpleLpWithGapResidualsAndNonZeroPrimalSolution) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
const Eigen::VectorXd primal_solution{{0.0, 0.0, 4.0, 3.0}};
const Eigen::VectorXd dual_solution{{1.0, 0.0, 0.0, -1.0}};
PrimalDualHybridGradientParams params_true, params_false;
params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
true);
params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
false);
// c is: [5.5, -2, -1, 1]
// -A^T y is: [-2, -1, 0.5, -3]
// c - A^T y is: [3.5, -3.0, -0.5, -2.0].
// When the primal variable is 0.0 and the bound is not 0.0, the bound
// corresponding to c - A^T y is treated as infinite when
// `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true.
// Thus, for primal_solution [0, 0, 4, 3], whether
// `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true or
// not, the residuals are [3.5, -3.0, 0.0, 0.0] and the corresponding bound
// terms are [0.0, -2, 6, 3.5].
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_true, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: -13.0
corrected_dual_objective: -inf
l_inf_dual_residual: 3.5
# 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0)
l2_dual_residual: 4.6097722286464436
)pb"))));
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_false, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: -7.0
corrected_dual_objective: -inf
l_inf_dual_residual: 3.5
# 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0)
l2_dual_residual: 4.6097722286464436
)pb"))));
}
TEST(IterationStatsTest, SimpleQp) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
num_shards);
const Eigen::VectorXd primal_solution{{1.0, 2.0}};
const Eigen::VectorXd dual_solution{{0.0}};
PrimalDualHybridGradientParams params_true, params_false;
params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
true);
params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals(
false);
// Q*x is: [4.0, 2.0]
// c is: [-1, -1]
// A^T y is zero.
// If `handle_some_primal_gradients_on_finite_bounds_as_residuals` is
// true the second primal gradient term is handled as a residual, not a
// reduced cost.
// Other than the reduced cost terms, the dual objective is 5 (objective
// offset) - 4 (1/2 x^T Q x) = 1
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_true, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: 8
corrected_dual_objective: 2
l_inf_dual_residual: 1.0
l2_dual_residual: 1.0
)pb"))));
EXPECT_THAT(ComputeScaledConvergenceInformation(
params_false, sharded_qp, primal_solution, dual_solution,
/*componentwise_primal_residual_offset=*/1.0,
/*componentwise_dual_residual_offset=*/1.0,
POINT_TYPE_CURRENT_ITERATE),
Partially(Approximately(EqualsProto(R"pb(
dual_objective: 2
corrected_dual_objective: 2
l_inf_dual_residual: 0.0
l2_dual_residual: 0.0
)pb"))));
}
TEST(IterationStatsTest, InfeasibilityInformationWithCertificateLp) {
const Eigen::VectorXd primal_ray{{0.0, 0.0}};
const Eigen::VectorXd dual_ray{{-1.0, -1.0}};
CheckScaledAndUnscaledInfeasibilityStats(
SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray,
ParseTextOrDie<InfeasibilityInformation>(R"pb(
max_primal_ray_infeasibility: 0
primal_ray_linear_objective: 0
primal_ray_quadratic_norm: 0
max_dual_ray_infeasibility: 0
dual_ray_objective: 1
)pb"));
}
TEST(IterationStatsTest, InfeasibilityInformationWithoutCertificateLp) {
const Eigen::VectorXd primal_ray{{2.0, 1.0}};
const Eigen::VectorXd dual_ray{{-1.0, -3.0}};
CheckScaledAndUnscaledInfeasibilityStats(
SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray,
ParseTextOrDie<InfeasibilityInformation>(R"pb(
max_primal_ray_infeasibility: 0.5
primal_ray_linear_objective: 1.5
primal_ray_quadratic_norm: 0
max_dual_ray_infeasibility: 0.66666666666666663
dual_ray_objective: 1.6666666666666667
)pb"));
}
TEST(IterationStatsTest, DetectsDualRayHasInfeasibleComponent) {
const Eigen::VectorXd primal_ray{{0.0, 0.0}};
const Eigen::VectorXd dual_ray{{1.0, 1.0}};
// Components with the wrong sign cause the dual ray objective to be -inf.
CheckScaledAndUnscaledInfeasibilityStats(
SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray,
ParseTextOrDie<InfeasibilityInformation>(R"pb(
max_dual_ray_infeasibility: 0.0
dual_ray_objective: -inf
)pb"));
}
// Regression test for failures of math_opt's
// SimpleLpTest.OptimalAfterInfeasible test.
TEST(IterationStatsTest, HandlesReducedCostsOnDualRayCorrectly) {
// A trivial LP mimicking the one used in math_opt's test:
// 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 Eigen::VectorXd primal_solution{{1.0}};
const Eigen::VectorXd primal_ray{{0.0}};
const Eigen::VectorXd dual_ray{{1.0}};
// `dual_ray_objective` = 2 (objective term) - 1 (reduced cost on x) = 1.
CheckScaledAndUnscaledInfeasibilityStats(
lp, primal_ray, dual_ray, primal_solution,
ParseTextOrDie<InfeasibilityInformation>(R"pb(
max_dual_ray_infeasibility: 0.0
dual_ray_objective: 1.0
)pb"));
}
TEST(CorrectedDualTest, SimpleLpWithSuboptimalDual) {
const int num_threads = 2;

View File

@@ -39,15 +39,13 @@ py_test(
name = "pdlp_test",
size = "small",
srcs = ["pdlp_test.py"],
data = [
":pdlp.so",
],
data = [":pdlp.so"],
deps = [
"//ortools/pdlp:solve_log_py_pb2",
"//ortools/pdlp:solvers_py_pb2",
requirement("absl-py"),
requirement("numpy"),
requirement("scipy"),
"//ortools/linear_solver:linear_solver_py_pb2",
"//ortools/pdlp:solve_log_py_pb2",
"//ortools/pdlp:solvers_py_pb2",
],
)

View File

@@ -26,18 +26,19 @@
#include "absl/status/statusor.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/base/parse_text_proto.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/pdlp/test_util.h"
namespace operations_research::pdlp {
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::google::protobuf::contrib::parse_proto::ParseTextOrDie;
using ::operations_research::pdlp::internal::CombineRepeatedTripletsInPlace;
using ::testing::ElementsAre;
using ::testing::EndsWith;
using ::testing::Eq;
using ::testing::EqualsProto;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Optional;
@@ -45,6 +46,7 @@ using ::testing::PrintToString;
using ::testing::SizeIs;
using ::testing::StartsWith;
using ::testing::StrEq;
using ::testing::status::IsOkAndHolds;
const double kInfinity = std::numeric_limits<double>::infinity();
@@ -231,6 +233,27 @@ TEST_P(ConvertQpMpModelProtoTest, LpFromMpModelProto) {
VerifyTestLp(*lp, maximize);
}
TEST_P(ConvertQpMpModelProtoTest, LpToMpModelProto) {
const bool maximize = GetParam();
QuadraticProgram lp = TestLp();
if (maximize) {
lp.objective_scaling_factor = -1;
lp.objective_vector *= -1;
lp.objective_offset *= -1;
}
EXPECT_THAT(QpToMpModelProto(lp),
IsOkAndHolds(EqualsProto(TestLpProto(maximize))));
}
TEST_P(ConvertQpMpModelProtoTest, LpRoundTrip) {
const bool maximize = GetParam();
ASSERT_OK_AND_ASSIGN(QuadraticProgram qp,
QpFromMpModelProto(TestLpProto(maximize),
/*relax_integer_variables=*/false));
EXPECT_THAT(QpToMpModelProto(qp),
IsOkAndHolds(EqualsProto(TestLpProto(maximize))));
}
// The QP:
// optimize x_0^2 + x_1^2 + 3 x_0 - 4 s.t.
// x_0 + x_1 <= 42
@@ -310,6 +333,29 @@ TEST(CanFitInMpModelProto, SmallQpOk) {
EXPECT_TRUE(CanFitInMpModelProto(*qp).ok());
}
TEST(CanFitInMpModelProto, TooManyVariablesFails) {
QuadraticProgram qp(1024, 5);
EXPECT_THAT(internal::TestableCanFitInMpModelProto(qp, 1023),
testing::status::StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("variable")));
}
TEST(CanFitInMpModelProto, TooManyConstraintsFails) {
QuadraticProgram qp(3, 1024);
EXPECT_THAT(internal::TestableCanFitInMpModelProto(qp, 1023),
testing::status::StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("constraint")));
}
TEST_P(ConvertQpMpModelProtoTest, QpRoundTrip) {
const bool maximize = GetParam();
ASSERT_OK_AND_ASSIGN(QuadraticProgram qp,
QpFromMpModelProto(TestQpProto(maximize),
/*relax_integer_variables=*/false));
EXPECT_THAT(QpToMpModelProto(qp),
IsOkAndHolds(EqualsProto(TestQpProto(maximize))));
}
// The ILP:
// optimize x_0 + 2 * x_1 s.t.
// x_0 + x_1 <= 1

View File

@@ -11,6 +11,33 @@
# See the License for the specific language governing permissions and
# limitations under the License.
load(":code_samples.bzl", "code_sample_cc")
load("@pip_deps//:requirements.bzl", "requirement")
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_python//python:py_binary.bzl", "py_binary")
code_sample_cc(name = "simple_pdlp_program")
cc_binary(
name = "simple_pdlp_program_cc",
srcs = ["simple_pdlp_program.cc"],
deps = [
"//ortools/base",
"//ortools/pdlp:iteration_stats",
"//ortools/pdlp:primal_dual_hybrid_gradient",
"//ortools/pdlp:quadratic_program",
"//ortools/pdlp:solve_log_cc_proto",
"//ortools/pdlp:solvers_cc_proto",
"@eigen",
],
)
py_binary(
name = "simple_pdlp_program_py",
srcs = ["simple_pdlp_program.py"],
main = "simple_pdlp_program.py",
deps = [
"//ortools/pdlp:solve_log_py_pb2",
"//ortools/pdlp:solvers_py_pb2",
"//ortools/pdlp/python:pdlp",
requirement("numpy"),
requirement("scipy"),
],
)

View File

@@ -1,48 +0,0 @@
# 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.
"""Helper macro to compile and test code samples."""
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_cc//cc:cc_test.bzl", "cc_test")
def code_sample_cc(name):
cc_binary(
name = name + "_cc",
srcs = [name + ".cc"],
deps = [
"//ortools/base",
"//ortools/pdlp:iteration_stats",
"//ortools/pdlp:primal_dual_hybrid_gradient",
"//ortools/pdlp:quadratic_program",
"//ortools/pdlp:solve_log_cc_proto",
"//ortools/pdlp:solvers_cc_proto",
Label("@eigen"),
],
)
cc_test(
name = name + "_cc_test",
size = "small",
srcs = [name + ".cc"],
deps = [
":" + name + "_cc",
"//ortools/base",
"//ortools/pdlp:iteration_stats",
"//ortools/pdlp:primal_dual_hybrid_gradient",
"//ortools/pdlp:quadratic_program",
"//ortools/pdlp:solve_log_cc_proto",
"//ortools/pdlp:solvers_cc_proto",
Label("@eigen"),
],
)

View File

@@ -27,7 +27,6 @@
#include <utility>
#include "absl/functional/any_invocable.h"
#include "absl/log/log.h"
#include "absl/synchronization/blocking_counter.h"
#include "ortools/base/threadpool.h"
#include "ortools/pdlp/solvers.pb.h"

View File

@@ -21,13 +21,13 @@
#include "absl/strings/string_view.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/base/parse_text_proto.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::google::protobuf::contrib::parse_proto::ParseTextOrDie;
using ::testing::HasSubstr;
@@ -49,8 +49,7 @@ void TestTerminationCriteriaValidation(
absl::string_view termination_criteria_string,
absl::string_view error_substring) {
TerminationCriteria termination_criteria =
ParseTextOrDie<TerminationCriteria>(
std::string(termination_criteria_string));
ParseTextOrDie<TerminationCriteria>(termination_criteria_string);
const absl::Status status = ValidateTerminationCriteria(termination_criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument)
<< "With termination criteria \"" << termination_criteria_string << "\"";

View File

@@ -20,44 +20,16 @@
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/base/parse_text_proto.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
bool operator==(const TerminationCriteria::DetailedOptimalityCriteria& lhs,
const TerminationCriteria::DetailedOptimalityCriteria& rhs) {
if (lhs.eps_optimal_primal_residual_absolute() !=
rhs.eps_optimal_primal_residual_absolute()) {
return false;
}
if (lhs.eps_optimal_primal_residual_relative() !=
rhs.eps_optimal_primal_residual_relative()) {
return false;
}
if (lhs.eps_optimal_dual_residual_absolute() !=
rhs.eps_optimal_dual_residual_absolute()) {
return false;
}
if (lhs.eps_optimal_dual_residual_relative() !=
rhs.eps_optimal_dual_residual_relative()) {
return false;
}
if (lhs.eps_optimal_objective_gap_absolute() !=
rhs.eps_optimal_objective_gap_absolute()) {
return false;
}
if (lhs.eps_optimal_objective_gap_relative() !=
rhs.eps_optimal_objective_gap_relative()) {
return false;
}
return true;
}
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::testing::Eq;
using ::google::protobuf::contrib::parse_proto::ParseTextOrDie;
using ::testing::EqualsProto;
using ::testing::FieldsAre;
using ::testing::Optional;
@@ -150,17 +122,16 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaOverload) {
const auto criteria =
ParseTextOrDie<TerminationCriteria::SimpleOptimalityCriteria>(
R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb");
EXPECT_THAT(
EffectiveOptimalityCriteria(criteria),
Eq(ParseTextOrDie<TerminationCriteria::DetailedOptimalityCriteria>(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb")));
EXPECT_THAT(EffectiveOptimalityCriteria(criteria),
EqualsProto(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb"));
}
TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) {
@@ -169,17 +140,16 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) {
eps_optimal_absolute: 1.0e-4
eps_optimal_relative: 2.0e-4
})pb");
EXPECT_THAT(
EffectiveOptimalityCriteria(criteria),
Eq(ParseTextOrDie<TerminationCriteria::DetailedOptimalityCriteria>(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb")));
EXPECT_THAT(EffectiveOptimalityCriteria(criteria),
EqualsProto(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb"));
}
TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) {
@@ -193,23 +163,22 @@ TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) {
eps_optimal_objective_gap_relative: 6.0e-4
})pb");
EXPECT_THAT(EffectiveOptimalityCriteria(criteria),
Eq(criteria.detailed_optimality_criteria()));
EqualsProto(criteria.detailed_optimality_criteria()));
}
TEST(EffectiveOptimalityCriteriaTest, DeprecatedInput) {
const auto criteria = ParseTextOrDie<TerminationCriteria>(
R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb");
EXPECT_THAT(
EffectiveOptimalityCriteria(criteria),
Eq(ParseTextOrDie<TerminationCriteria::DetailedOptimalityCriteria>(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb")));
EXPECT_THAT(EffectiveOptimalityCriteria(criteria),
EqualsProto(
R"pb(
eps_optimal_primal_residual_absolute: 1.0e-4
eps_optimal_primal_residual_relative: 2.0e-4
eps_optimal_dual_residual_absolute: 1.0e-4
eps_optimal_dual_residual_relative: 2.0e-4
eps_optimal_objective_gap_absolute: 1.0e-4
eps_optimal_objective_gap_relative: 2.0e-4
)pb"));
}
TEST_P(DetailedRelativeTerminationTest, TerminationWithNearOptimal) {